diff --git a/ui/src/app/components/workspace/workspace.component.ts b/ui/src/app/components/workspace/workspace.component.ts index fbe29bbbd..533d45cc8 100644 --- a/ui/src/app/components/workspace/workspace.component.ts +++ b/ui/src/app/components/workspace/workspace.component.ts @@ -425,7 +425,16 @@ export class WorkspaceComponent implements OnInit, OnDestroy { maxWidth: '500px', }) } else { - this.router.navigate(['/prepare-migration']) + this.fetch.verifyExpression().subscribe((res:any)=>{ + if(!res){ + this.router.navigate(['/prepare-migration']) + } + else{ + this.data.getSummary() + window.location.reload() + } + + }) } } }) diff --git a/ui/src/app/services/fetch/fetch.service.ts b/ui/src/app/services/fetch/fetch.service.ts index 16b917190..2a5375821 100644 --- a/ui/src/app/services/fetch/fetch.service.ts +++ b/ui/src/app/services/fetch/fetch.service.ts @@ -210,8 +210,8 @@ export class FetchService { return this.http.post(`${this.url}/restore/tables`, payload) } - validateCheckConstraint() { - return this.http.get(`${this.url}/validateCheckConstraint`) + verifyExpression() { + return this.http.get(`${this.url}/verifyExpression`) } updateCheckConstraint(tableId: string, payload: ICheckConstraints[]): any { diff --git a/webv2/api/schema.go b/webv2/api/schema.go index f8b8ca3d3..b1d5d5b8b 100644 --- a/webv2/api/schema.go +++ b/webv2/api/schema.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -13,6 +14,7 @@ import ( "github.com/GoogleCloudPlatform/spanner-migration-tool/common/constants" "github.com/GoogleCloudPlatform/spanner-migration-tool/common/utils" "github.com/GoogleCloudPlatform/spanner-migration-tool/conversion" + "github.com/GoogleCloudPlatform/spanner-migration-tool/expressions_api" "github.com/GoogleCloudPlatform/spanner-migration-tool/internal" "github.com/GoogleCloudPlatform/spanner-migration-tool/internal/reports" "github.com/GoogleCloudPlatform/spanner-migration-tool/profiles" @@ -488,6 +490,117 @@ func RestoreSecondaryIndex(w http.ResponseWriter, r *http.Request) { } +// findColId based on constraint condition it will return colId. +func findColId(colDefs map[string]ddl.ColumnDef, condition string) string { + for _, colDef := range colDefs { + if strings.Contains(condition, colDef.Name) { + return colDef.Id + } + } + return "" +} + +// removeCheckConstraint this method will remove the constraint which has error +func removeCheckConstraint(checkConstraints []ddl.CheckConstraint, expId string) []ddl.CheckConstraint { + var filteredConstraints []ddl.CheckConstraint + + for _, checkConstraint := range checkConstraints { + if checkConstraint.ExprId != expId { + filteredConstraints = append(filteredConstraints, checkConstraint) + } + } + return filteredConstraints +} + +// VerifyExpression this function will use expression_api to validate check constraint expressions and add the relevant error +// to suggestion tab and remove the check constraint which has error +func VerifyExpression(w http.ResponseWriter, r *http.Request) { + 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 + } + sessionState.Conv.ConvLock.Lock() + defer sessionState.Conv.ConvLock.Unlock() + + spschema := sessionState.Conv.SpSchema + + hasErrorOccurred := false + + expressionDetailList := []internal.ExpressionDetail{} + ctx := context.Background() + + for _, sp := range spschema { + for _, cc := range sp.CheckConstraints { + + colId := findColId(sp.ColDefs, cc.Expr) + expressionDetail := internal.ExpressionDetail{ + Expression: cc.Expr, + Type: "CHECK", + ReferenceElement: internal.ReferenceElement{Name: sp.Name}, + ExpressionId: cc.ExprId, + Metadata: map[string]string{"tableId": sp.Id, "colId": colId, "checkConstraintName": cc.Name}, + } + expressionDetailList = append(expressionDetailList, expressionDetail) + } + } + + accessor, err := expressions_api.NewExpressionVerificationAccessorImpl(ctx, session.GetSessionState().SpannerProjectId, session.GetSessionState().SpannerInstanceID) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create ExpressionVerificationAccessorImpl: %v", err), http.StatusNotFound) + return + } + verifyExpressionsInput := internal.VerifyExpressionsInput{ + Conv: sessionState.Conv, + Source: "mysql", + ExpressionDetailList: expressionDetailList, + } + + result := accessor.VerifyExpressions(ctx, verifyExpressionsInput) + if result.ExpressionVerificationOutputList != nil { + for _, ev := range result.ExpressionVerificationOutputList { + if !ev.Result { + hasErrorOccurred = true + tableId := ev.ExpressionDetail.Metadata["tableId"] + colId := ev.ExpressionDetail.Metadata["colId"] + + spschema := sessionState.Conv.SpSchema[tableId] + spschema.CheckConstraints = removeCheckConstraint(spschema.CheckConstraints, ev.ExpressionDetail.ExpressionId) + sessionState.Conv.SpSchema[tableId] = spschema + + err := ev.Err.Error() + var issueType internal.SchemaIssue + + switch { + case strings.Contains(err, "No matching signature for operator"): + issueType = internal.TypeMismatch + case strings.Contains(err, "Syntax error"): + issueType = internal.InvalidCondition + case strings.Contains(err, "Unrecognized name"): + issueType = internal.ColumnNotFound + default: + fmt.Println("Unhandled error:", err) + return + } + + colIssue := sessionState.Conv.SchemaIssues[tableId].ColumnLevelIssues[colId] + + if !utilities.IsSchemaIssuePresent(colIssue, issueType) { + colIssue = append(colIssue, issueType) + } + sessionState.Conv.SchemaIssues[tableId].ColumnLevelIssues[colId] = colIssue + + } + + } + } + + session.UpdateSessionFile() + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(hasErrorOccurred) +} + // 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. diff --git a/webv2/routes.go b/webv2/routes.go index d1861541a..8cd35239c 100644 --- a/webv2/routes.go +++ b/webv2/routes.go @@ -44,7 +44,7 @@ func getRoutes() *mux.Router { } ctx := context.Background() - spanneraccessor, _:= spanneraccessor.NewSpannerAccessorClientImpl(ctx) + spanneraccessor, _ := spanneraccessor.NewSpannerAccessorClientImpl(ctx) dsClient, _ := ds.NewDatastreamClientImpl(ctx) storageclient, _ := storageclient.NewStorageClientImpl(ctx) validateResourceImpl := conversion.NewValidateResourcesImpl(spanneraccessor, &datastream_accessor.DatastreamAccessorImpl{}, @@ -79,6 +79,8 @@ func getRoutes() *mux.Router { router.HandleFunc("/setparent", api.SetParentTable).Methods("GET") router.HandleFunc("/removeParent", api.RemoveParentTable).Methods("POST") + router.HandleFunc("/verifyExpression", api.VerifyExpression).Methods("GET") + // TODO:(searce) take constraint names themselves which are guaranteed to be unique for Spanner. router.HandleFunc("/drop/secondaryindex", api.DropSecondaryIndex).Methods("POST") router.HandleFunc("/restore/secondaryIndex", api.RestoreSecondaryIndex).Methods("POST")