Repository: cloudspannerecosystem/harbourbridge
Pull request #393 (2022-11-02 08:41:43)

Feature - restore index

- [ ] Restore index implementation


Patch:
diff --git a/sources/common/toddl.go b/sources/common/toddl.go
index 2dffae0..deba9b8 100644
--- a/sources/common/toddl.go
+++ b/sources/common/toddl.go
@@ -189,41 +189,8 @@ func cvtForeignKeysHelper(conv *internal.Conv, spTableName string, srcTable stri
 func cvtIndexes(conv *internal.Conv, spTableName string, srcTable string, srcIndexes []schema.Index) []ddl.CreateIndex {
 	var spIndexes []ddl.CreateIndex
 	for _, srcIndex := range srcIndexes {
-		var spKeys []ddl.IndexKey
-		var spStoredColumns []string
-
-		for _, k := range srcIndex.Keys {
-			spCol, err := internal.GetSpannerCol(conv, srcTable, k.Column, true)
-			if err != nil {
-				conv.Unexpected(fmt.Sprintf("Can't map index key column name for table %s column %s", srcTable, k.Column))
-				continue
-			}
-			spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})
-		}
-		for _, k := range srcIndex.StoredColumns {
-			spCol, err := internal.GetSpannerCol(conv, srcTable, k, true)
-			if err != nil {
-				conv.Unexpected(fmt.Sprintf("Can't map index column name for table %s column %s", srcTable, k))
-				continue
-			}
-			spStoredColumns = append(spStoredColumns, spCol)
-		}
-		if srcIndex.Name == "" {
-			// Generate a name if index name is empty in MySQL.
-			// Collision of index name will be handled by ToSpannerIndexName.
-			srcIndex.Name = fmt.Sprintf("Index_%s", srcTable)
-		}
-		spIndexName := internal.ToSpannerIndexName(conv, srcIndex.Name)
-		spIndex := ddl.CreateIndex{
-			Name:          spIndexName,
-			Table:         spTableName,
-			Unique:        srcIndex.Unique,
-			Keys:          spKeys,
-			StoredColumns: spStoredColumns,
-		}
+		spIndex := CvtIndexHelper(conv, spTableName, srcTable, srcIndex)
 		spIndexes = append(spIndexes, spIndex)
-		conv.Audit.ToSpannerFkIdx[srcTable].Index[srcIndex.Name] = spIndexName
-		conv.Audit.ToSourceFkIdx[spTableName].Index[spIndexName] = srcIndex.Name
 	}
 	return spIndexes
 }
@@ -263,3 +230,41 @@ func cvtForeignKeysForAReferenceTable(conv *internal.Conv, spTableName string, s
 	}
 	return spKeys
 }
+
+func CvtIndexHelper(conv *internal.Conv, spTableName string, srcTable string, srcIndex schema.Index) ddl.CreateIndex {
+	var spKeys []ddl.IndexKey
+	var spStoredColumns []string
+
+	for _, k := range srcIndex.Keys {
+		spCol, err := internal.GetSpannerCol(conv, srcTable, k.Column, true)
+		if err != nil {
+			conv.Unexpected(fmt.Sprintf("Can't map index key column name for table %s column %s", srcTable, k.Column))
+			continue
+		}
+		spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})
+	}
+	for _, k := range srcIndex.StoredColumns {
+		spCol, err := internal.GetSpannerCol(conv, srcTable, k, true)
+		if err != nil {
+			conv.Unexpected(fmt.Sprintf("Can't map index column name for table %s column %s", srcTable, k))
+			continue
+		}
+		spStoredColumns = append(spStoredColumns, spCol)
+	}
+	if srcIndex.Name == "" {
+		// Generate a name if index name is empty in MySQL.
+		// Collision of index name will be handled by ToSpannerIndexName.
+		srcIndex.Name = fmt.Sprintf("Index_%s", srcTable)
+	}
+	spIndexName := internal.ToSpannerIndexName(conv, srcIndex.Name)
+	spIndex := ddl.CreateIndex{
+		Name:          spIndexName,
+		Table:         spTableName,
+		Unique:        srcIndex.Unique,
+		Keys:          spKeys,
+		StoredColumns: spStoredColumns,
+	}
+	conv.Audit.ToSpannerFkIdx[srcTable].Index[srcIndex.Name] = spIndexName
+	conv.Audit.ToSourceFkIdx[spTableName].Index[spIndexName] = srcIndex.Name
+	return spIndex
+}
diff --git a/ui/src/app/components/object-detail/object-detail.component.html b/ui/src/app/components/object-detail/object-detail.component.html
index 43c65bd..e236853 100644
--- a/ui/src/app/components/object-detail/object-detail.component.html
+++ b/ui/src/app/components/object-detail/object-detail.component.html
@@ -67,11 +67,29 @@
         color="primary"
         class="icon drop"
         (click)="dropIndex()"
-        *ngIf="currentObject!.isSpannerNode && currentObject!.type === 'indexName'"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          !currentObject!.isDeleted &&
+          currentObject!.type === 'indexName'
+        "
       >
         <mat-icon>delete</mat-icon>
         <span>DROP INDEX</span>
       </button>
+      <button
+        mat-button
+        color="primary"
+        class="icon drop"
+        (click)="restoreIndex()"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          currentObject!.isDeleted &&
+          currentObject!.type == ObjectExplorerNodeType.Index
+        "
+      >
+        <mat-icon>undo</mat-icon>
+        <span> RESTORE INDEX</span>
+      </button>
       <button
         mat-button
         color="primary"
@@ -91,7 +109,11 @@
         color="primary"
         class="icon drop"
         (click)="restoreSpannerTable()"
-        *ngIf="currentObject!.isSpannerNode && currentObject!.isDeleted"
+        *ngIf="
+          currentObject!.isSpannerNode &&
+          currentObject!.isDeleted &&
+          currentObject!.type == ObjectExplorerNodeType.Table
+        "
       >
         <mat-icon>undo</mat-icon>
         <span> RESTORE TABLE</span>
@@ -756,7 +778,9 @@
           <div class="spanner_edit-button">
             <span>Spanner</span>
 
-            <div *ngIf="!isIndexEditMode && currentObject!.isSpannerNode">
+            <div
+              *ngIf="!isIndexEditMode && !currentObject!.isDeleted && currentObject!.isSpannerNode"
+            >
               <button mat-stroked-button color="primary" (click)="toggleIndexEdit()">
                 <mat-icon class="edit-icon">edit</mat-icon>
                 EDIT
diff --git a/ui/src/app/components/object-detail/object-detail.component.ts b/ui/src/app/components/object-detail/object-detail.component.ts
index 5f08d3b..f100e82 100644
--- a/ui/src/app/components/object-detail/object-detail.component.ts
+++ b/ui/src/app/components/object-detail/object-detail.component.ts
@@ -967,6 +967,21 @@ export class ObjectDetailComponent implements OnInit {
       }
     })
   }
+
+  restoreIndex() {
+    let tableId = this.currentObject!.parentId
+    let indexId = this.currentObject!.id
+    this.data
+      .restoreIndex(tableId, indexId)
+      .pipe(take(1))
+      .subscribe((res: string) => {
+        if (res === '') {
+          this.isObjectSelected = false
+        }
+      })
+    this.currentObject = null
+  }
+
   dropIndexKey(index: number) {
     if (this.localIndexData[index].srcColName) {
       this.localIndexData[index].spColName = ''
diff --git a/ui/src/app/components/object-explorer/object-explorer.component.html b/ui/src/app/components/object-explorer/object-explorer.component.html
index af8535a..0fbb46d 100644
--- a/ui/src/app/components/object-explorer/object-explorer.component.html
+++ b/ui/src/app/components/object-explorer/object-explorer.component.html
@@ -220,7 +220,7 @@
                 error
               </mat-icon>
               <mat-icon
-                *ngIf="data.status === 'DARK'"
+                *ngIf="data.status === 'DARK' || data.isDeleted==true"
                 class="icon dark"
                 matTooltip="Deleted"
                 matTooltipPosition="above"
diff --git a/ui/src/app/services/conversion/conversion.service.ts b/ui/src/app/services/conversion/conversion.service.ts
index 1cf8208..32fc4e3 100644
--- a/ui/src/app/services/conversion/conversion.service.ts
+++ b/ui/src/app/services/conversion/conversion.service.ts
@@ -35,6 +35,8 @@ export class ConversionService {
       }
       return true
     })
+    let deletedIndexes = this.getDeletedIndexes(conv)
+
     let parentNode: ISchemaObjectNode = {
       name: `Tables (${srcTableNames.length})`,
       type: ObjectExplorerNodeType.Tables,
@@ -102,6 +104,25 @@ export class ConversionService {
         parentId: '',
       })
     })
+
+    // add deleted indexes
+    parentNode.children?.forEach((tableNode: ISchemaObjectNode, i: number) => {
+      if (deletedIndexes[tableNode.id]) {
+        deletedIndexes[tableNode.id].forEach((index: IIndex) => {
+          parentNode.children![i].children![0].children?.push({
+            name: index.Name.replace(/[^A-Za-z0-9_]/g, '_'),
+            type: ObjectExplorerNodeType.Index,
+            parent: conv.SpSchema[tableNode.name]?.Name,
+            pos: i,
+            isSpannerNode: true,
+            isDeleted: true,
+            id: index.Id,
+            parentId: tableNode.id,
+          })
+        })
+      }
+    })
+
     return [
       {
         name: conv.DatabaseName,
@@ -372,6 +393,29 @@ export class ConversionService {
     return res
   }
 
+  getDeletedIndexes(conv: IConv): Record<string, IIndex[]> {
+    let deletedIndexes: Record<string, IIndex[]> = {}
+    Object.keys(conv.SpSchema).map((spTableName: string) => {
+      let spTable = conv.SpSchema[spTableName]
+      let srcTableName = this.getSourceTableNameFromId(spTable.Id, conv)
+      let srcTable = conv.SrcSchema[srcTableName]
+      let spIndexIds = spTable.Indexes?.map((index: ICreateIndex) => {
+        return index.Id
+      })
+      let tableDeletedIndexes = srcTable.Indexes?.filter((index: IIndex) => {
+        if (!spIndexIds.includes(index.Id)) {
+          return true
+        }
+        return false
+      })
+      if (tableDeletedIndexes && tableDeletedIndexes.length > 0) {
+        deletedIndexes[spTable.Id] = tableDeletedIndexes
+      }
+    })
+
+    return deletedIndexes
+  }
+
   getSpannerColDefFromId(tableName: string, id: string, data: IConv): IColumnDef | null {
     let res: IColumnDef | null = null
     Object.keys(data.SpSchema[tableName].ColDefs).forEach((colName) => {
diff --git a/ui/src/app/services/data/data.service.ts b/ui/src/app/services/data/data.service.ts
index c9bcc6b..d852531 100644
--- a/ui/src/app/services/data/data.service.ts
+++ b/ui/src/app/services/data/data.service.ts
@@ -387,6 +387,25 @@ export class DataService {
     )
   }
 
+  restoreIndex(tableId: string, indexId: string): Observable<string> {
+    return this.fetch.restoreIndex(tableId, indexId).pipe(
+      catchError((e: any) => {
+        return of({ error: e.error })
+      }),
+      tap(console.log),
+      map((data) => {
+        if (data.error) {
+          this.snackbar.openSnackBar(data.error, 'Close')
+          return data.error
+        } else {
+          this.convSubject.next(data)
+          this.snackbar.openSnackBar('Index restored successfully', 'Close', 5)
+          return ''
+        }
+      })
+    )
+  }
+
   getInterleaveConversionForATable(tableName: string) {
     this.fetch.getInterleaveStatus(tableName).subscribe((res: IInterleaveStatus) => {
       this.tableInterleaveStatusSub.next(res)
diff --git a/ui/src/app/services/fetch/fetch.service.ts b/ui/src/app/services/fetch/fetch.service.ts
index e0e71f4..a1ae5e7 100644
--- a/ui/src/app/services/fetch/fetch.service.ts
+++ b/ui/src/app/services/fetch/fetch.service.ts
@@ -185,6 +185,13 @@ export class FetchService {
     })
   }
 
+  restoreIndex(tableId: string, indexId: string) {
+    return this.http.post<HttpResponse<IConv>>(
+      `${this.url}/restore/secondaryIndex?tableId=${tableId}&indexId=${indexId}`,
+      {}
+    )
+  }
+
   getInterleaveStatus(tableName: string) {
     return this.http.get<IInterleaveStatus>(`${this.url}/setparent?table=${tableName}`)
   }
diff --git a/ui/src/styles.scss b/ui/src/styles.scss
index 409693a..33c421f 100644
--- a/ui/src/styles.scss
+++ b/ui/src/styles.scss
@@ -53,13 +53,18 @@ body {
 }
 
 .warning {
-  color: #f3b300;
+  color: #ff9800;
 }
 
 .success {
   color: green;
 }
 
+.suggestion {
+  color: #f6be00;
+  transform: rotate(180deg);
+}
+
 .orange {
   color: rgb(248, 116, 68);
 }
@@ -250,12 +255,11 @@ body {
   }
 }
 
-.sidenav-header-border{
+.sidenav-header-border {
   border-bottom-width: 1px;
   border-bottom-style: solid;
 }
 
-
 .sidenav-percentage-bar {
   height: 0.8rem;
   display: flex;
@@ -280,21 +284,23 @@ body {
   }
 }
 
-.sidenav-conversionByTable{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-conversionByTable {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-.sidenav-conversionByPercentage{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-conversionByPercentage {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-.sidenav-databaseDefinitions{
-  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%);
+.sidenav-databaseDefinitions {
+  box-shadow: 0px 2px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%),
+    0px 1px 3px 0px rgb(0 0 0 / 12%);
 }
 
-
-.sidenav-title{
-  margin-top :10px;
+.sidenav-title {
+  margin-top: 10px;
   margin-bottom: 0px;
 }
 .radio-button-container mat-radio-button:hover {
@@ -328,4 +334,4 @@ body {
 .spanner-tab-link{
   color:  #1967d2;
   font-weight: 400;
-}
\ No newline at end of file
+}
diff --git a/webv2/routes.go b/webv2/routes.go
index 4f26165..ebbe58b 100644
--- a/webv2/routes.go
+++ b/webv2/routes.go
@@ -53,6 +53,7 @@ func getRoutes() *mux.Router {
 
 	// TODO:(searce) take constraint names themselves which are guaranteed to be unique for Spanner.
 	router.HandleFunc("/drop/secondaryindex", dropSecondaryIndex).Methods("POST")
+	router.HandleFunc("/restore/secondaryIndex", restoreSecondaryIndex).Methods("POST")
 
 	router.HandleFunc("/restore/table", restoreTable).Methods("POST")
 	router.HandleFunc("/drop/table", dropTable).Methods("POST")
diff --git a/webv2/web.go b/webv2/web.go
index 1e4bfb9..e180069 100644
--- a/webv2/web.go
+++ b/webv2/web.go
@@ -43,6 +43,7 @@ import (
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
 	"github.com/cloudspannerecosystem/harbourbridge/profiles"
 	"github.com/cloudspannerecosystem/harbourbridge/proto/migration"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/common"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/mysql"
 	"github.com/cloudspannerecosystem/harbourbridge/sources/oracle"
@@ -1082,6 +1083,84 @@ func dropForeignKey(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(convm)
 }
 
+func restoreSecondaryIndex(w http.ResponseWriter, r *http.Request) {
+	tableId := r.FormValue("tableId")
+	indexId := r.FormValue("indexId")
+	sessionState := session.GetSessionState()
+	if sessionState.Conv == nil || sessionState.Driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if tableId == "" {
+		http.Error(w, fmt.Sprintf("Table Id is empty"), http.StatusBadRequest)
+		return
+	}
+	if indexId == "" {
+		http.Error(w, fmt.Sprintf("Index Id is empty"), http.StatusBadRequest)
+		return
+	}
+
+	srcTableName := ""
+	for _, value := range sessionState.Conv.SrcSchema {
+		if value.Id == tableId {
+			srcTableName = value.Name
+			break
+		}
+	}
+	if srcTableName == "" {
+		http.Error(w, fmt.Sprintf("Source Table not found"), http.StatusBadRequest)
+		return
+	}
+
+	spTableName := ""
+	for _, value := range sessionState.Conv.SpSchema {
+		if value.Id == tableId {
+			spTableName = value.Name
+			break
+		}
+	}
+	if spTableName == "" {
+		http.Error(w, fmt.Sprintf("Spanner Table not found"), http.StatusBadRequest)
+		return
+	}
+
+	var srcIndex schema.Index
+	srcIndexFound := false
+	for _, index := range sessionState.Conv.SrcSchema[srcTableName].Indexes {
+		if index.Id == indexId {
+			srcIndex = index
+			srcIndexFound = true
+			break
+		}
+	}
+	if !srcIndexFound {
+		http.Error(w, fmt.Sprintf("Source index not found"), http.StatusBadRequest)
+		return
+	}
+
+	conv := sessionState.Conv
+
+	spIndex := common.CvtIndexHelper(conv, spTableName, srcTableName, srcIndex)
+	spIndexes := conv.SpSchema[spTableName].Indexes
+	spIndexes = append(spIndexes, spIndex)
+	spTable := conv.SpSchema[spTableName]
+	spTable.Indexes = spIndexes
+	conv.SpSchema[spTableName] = spTable
+
+	uniqueid.CopyUniqueIdToSpannerTable(conv, spTable.Name)
+	sessionState.Conv = conv
+	index.AssignInitialOrders()
+	index.IndexSuggestion()
+
+	convm := session.ConvWithMetadata{
+		SessionMetadata: sessionState.SessionMetadata,
+		Conv:            *sessionState.Conv,
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(convm)
+
+}
+
 // renameForeignKeys checks the new names for spanner name validity, ensures the new names are already not used by existing tables
 // secondary indexes or foreign key constraints. If above checks passed then foreignKey renaming reflected in the schema else appropriate
 // error thrown.
@@ -1627,6 +1706,8 @@ func dropSecondaryIndex(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	usedNames := sessionState.Conv.UsedNames
+	delete(usedNames, sp.Indexes[position].Name)
 	index.RemoveIndexIssues(table, sp.Indexes[position])
 
 	sp.Indexes = utilities.RemoveSecondaryIndex(sp.Indexes, position)


Test patch:
diff --git a/webv2/web_test.go b/webv2/web_test.go
index 1b29db2..4cf8371 100644
--- a/webv2/web_test.go
+++ b/webv2/web_test.go
@@ -1920,6 +1920,138 @@ func TestDropSecondaryIndex(t *testing.T) {
 	}
 }
 
+func TestRestoreSecondaryIndex(t *testing.T) {
+	tc := []struct {
+		name         string
+		tableId      string
+		indexId      string
+		statusCode   int64
+		conv         *internal.Conv
+		expectedConv *internal.Conv
+	}{
+		{
+			name:       "Test restore valid secondary index success",
+			tableId:    "t1",
+			indexId:    "i1",
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SrcSchema: map[string]schema.Table{
+					"t1": {
+						Name: "t1",
+						Indexes: []schema.Index{
+							{Name: "idx1", Unique: false, Keys: []schema.Key{{Column: "b", Desc: false, Order: 1}}, Id: "i1"},
+							{Name: "idx2", Unique: false, Keys: []schema.Key{{Column: "c", Desc: false, Order: 1}}, Id: "i2"},
+						},
+						Id: "t1",
+					},
+				},
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Name: "t1",
+						Indexes: []ddl.CreateIndex{
+							{Name: "idx2", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "c", Desc: false, Order: 1}}, Id: "i2"},
+						},
+						Id: "t1",
+					},
+				},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+					ToSourceFkIdx: map[string]internal.FkeyAndIdxs{
+						"t1": {
+							Name:       "t1",
+							ForeignKey: map[string]string{},
+							Index:      map[string]string{"idx2": "idx2"},
+						},
+					},
+					ToSpannerFkIdx: map[string]internal.FkeyAndIdxs{
+						"t1": {
+							Name:       "t1",
+							ForeignKey: map[string]string{},
+							Index:      map[string]string{"idx2": "idx2"},
+						},
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": {Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": {Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				UsedNames: map[string]bool{"t1": true, "idx2": true},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Name: "t1",
+						Indexes: []ddl.CreateIndex{
+							{Name: "idx2", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "c", Desc: false, Order: 1}}, Id: "i2"},
+							{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"},
+						},
+						Id: "t1",
+					},
+				},
+			},
+		},
+
+		{
+			name:       "Test restore secondary index invalid index id",
+			tableId:    "t1",
+			indexId:    "A",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Indexes: []ddl.CreateIndex{{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"}},
+					}},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+				},
+			},
+			expectedConv: &internal.Conv{},
+		},
+		{
+			name:       "Test drop secondary index invalid table id",
+			tableId:    "X",
+			indexId:    "i1",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": {
+						Indexes: []ddl.CreateIndex{{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{{Col: "b", Desc: false, Order: 1}}, Id: "i1"}},
+					}},
+				Audit: internal.Audit{
+					MigrationType: migration.MigrationData_SCHEMA_ONLY.Enum(),
+				},
+			},
+			expectedConv: &internal.Conv{},
+		},
+	}
+	for _, tc := range tc {
+		sessionState := session.GetSessionState()
+
+		sessionState.Driver = constants.MYSQL
+		sessionState.Conv = tc.conv
+		payload := `{}`
+		req, err := http.NewRequest("POST", "/restore/secondaryIndex?tableId="+tc.tableId+"&indexId="+tc.indexId, strings.NewReader(payload))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(restoreSecondaryIndex)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedConv.SpSchema, res.SpSchema)
+		}
+	}
+}
+
 func TestDropTable(t *testing.T) {
 	sessionState := session.GetSessionState()
 	sessionState.Driver = constants.MYSQL