From cbf44c0c72bc5e5d3932ec545604226128da1a7c Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Mon, 11 Sep 2023 17:25:34 +0530 Subject: [PATCH 01/10] moved code to operators Signed-off-by: Harshit Gangal --- .../planbuilder/operators/ast_to_delete_op.go | 210 ------- .../planbuilder/operators/ast_to_update_op.go | 539 ------------------ go/vt/vtgate/planbuilder/operators/delete.go | 187 ++++++ go/vt/vtgate/planbuilder/operators/update.go | 515 +++++++++++++++++ 4 files changed, 702 insertions(+), 749 deletions(-) delete mode 100644 go/vt/vtgate/planbuilder/operators/ast_to_delete_op.go delete mode 100644 go/vt/vtgate/planbuilder/operators/ast_to_update_op.go diff --git a/go/vt/vtgate/planbuilder/operators/ast_to_delete_op.go b/go/vt/vtgate/planbuilder/operators/ast_to_delete_op.go deleted file mode 100644 index 796e71f05bf..00000000000 --- a/go/vt/vtgate/planbuilder/operators/ast_to_delete_op.go +++ /dev/null @@ -1,210 +0,0 @@ -/* -Copyright 2023 The Vitess Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package operators - -import ( - vschemapb "vitess.io/vitess/go/vt/proto/vschema" - "vitess.io/vitess/go/vt/sqlparser" - "vitess.io/vitess/go/vt/vterrors" - "vitess.io/vitess/go/vt/vtgate/engine" - "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" - "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" - "vitess.io/vitess/go/vt/vtgate/vindexes" -) - -func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete) (ops.Operator, error) { - tableInfo, qt, err := createQueryTableForDML(ctx, deleteStmt.TableExprs[0], deleteStmt.Where) - if err != nil { - return nil, err - } - - vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "delete") - if err != nil { - return nil, err - } - - delClone := sqlparser.CloneRefOfDelete(deleteStmt) - // Create the delete operator first. - delOp, err := createDeleteOperator(ctx, deleteStmt, qt, vindexTable, routing) - if err != nil { - return nil, err - } - - // Now we check for the foreign key mode and make changes if required. - ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) - if err != nil { - return nil, err - } - - // Unmanaged foreign-key-mode, we don't need to do anything. - if ksMode != vschemapb.Keyspace_FK_MANAGED { - return delOp, nil - } - - childFks := vindexTable.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.DeleteAction) - // If there are no foreign key constraints, then we don't need to do anything. - if len(childFks) == 0 { - return delOp, nil - } - // If the delete statement has a limit, we don't support it yet. - if deleteStmt.Limit != nil { - return nil, vterrors.VT12001("foreign keys management at vitess with limit") - } - - return createFkCascadeOpForDelete(ctx, delOp, delClone, childFks) -} - -func createDeleteOperator(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete, qt *QueryTable, vindexTable *vindexes.Table, routing Routing) (ops.Operator, error) { - del := &Delete{ - QTable: qt, - VTable: vindexTable, - AST: deleteStmt, - } - route := &Route{ - Source: del, - Routing: routing, - } - - if !vindexTable.Keyspace.Sharded { - return route, nil - } - - primaryVindex, vindexAndPredicates, err := getVindexInformation(qt.ID, qt.Predicates, vindexTable) - if err != nil { - return nil, err - } - - tr, ok := routing.(*ShardedRouting) - if ok { - tr.VindexPreds = vindexAndPredicates - } - - var ovq string - if len(vindexTable.Owned) > 0 { - tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: vindexTable.Name}, As: qt.Alias.As} - ovq = generateOwnedVindexQuery(tblExpr, deleteStmt, vindexTable, primaryVindex.Columns) - } - - del.OwnedVindexQuery = ovq - - for _, predicate := range qt.Predicates { - var err error - route.Routing, err = UpdateRoutingLogic(ctx, predicate, route.Routing) - if err != nil { - return nil, err - } - } - - if routing.OpCode() == engine.Scatter && deleteStmt.Limit != nil { - // TODO systay: we should probably check for other op code types - IN could also hit multiple shards (2022-04-07) - return nil, vterrors.VT12001("multi shard DELETE with LIMIT") - } - - subq, err := createSubqueryFromStatement(ctx, deleteStmt) - if err != nil { - return nil, err - } - if subq == nil { - return route, nil - } - subq.Outer = route - return subq, nil -} - -func createFkCascadeOpForDelete(ctx *plancontext.PlanningContext, parentOp ops.Operator, delStmt *sqlparser.Delete, childFks []vindexes.ChildFKInfo) (ops.Operator, error) { - var fkChildren []*FkChild - var selectExprs []sqlparser.SelectExpr - for _, fk := range childFks { - // Any RESTRICT type foreign keys that arrive here, - // are cross-shard/cross-keyspace RESTRICT cases, which we don't currently support. - if isRestrict(fk.OnDelete) { - return nil, vterrors.VT12002() - } - - // We need to select all the parent columns for the foreign key constraint, to use in the update of the child table. - cols, exprs := selectParentColumns(fk, len(selectExprs)) - selectExprs = append(selectExprs, exprs...) - - fkChild, err := createFkChildForDelete(ctx, fk, cols) - if err != nil { - return nil, err - } - fkChildren = append(fkChildren, fkChild) - } - selectionOp, err := createSelectionOp(ctx, selectExprs, delStmt.TableExprs, delStmt.Where, nil) - if err != nil { - return nil, err - } - - return &FkCascade{ - Selection: selectionOp, - Children: fkChildren, - Parent: parentOp, - }, nil -} - -func createFkChildForDelete(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, cols []int) (*FkChild, error) { - bvName := ctx.ReservedVars.ReserveVariable(foriegnKeyContraintValues) - - var childStmt sqlparser.Statement - switch fk.OnDelete { - case sqlparser.Cascade: - // We now construct the delete query for the child table. - // The query looks something like this - `DELETE FROM WHERE IN ()` - var valTuple sqlparser.ValTuple - for _, column := range fk.ChildColumns { - valTuple = append(valTuple, sqlparser.NewColName(column.String())) - } - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) - childStmt = &sqlparser.Delete{ - TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, - Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: compExpr}, - } - case sqlparser.SetNull: - // We now construct the update query for the child table. - // The query looks something like this - `UPDATE SET = NULL [AND = NULL]... WHERE IN ()` - var valTuple sqlparser.ValTuple - var updExprs sqlparser.UpdateExprs - for _, column := range fk.ChildColumns { - valTuple = append(valTuple, sqlparser.NewColName(column.String())) - updExprs = append(updExprs, &sqlparser.UpdateExpr{ - Name: sqlparser.NewColName(column.String()), - Expr: &sqlparser.NullVal{}, - }) - } - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) - childStmt = &sqlparser.Update{ - Exprs: updExprs, - TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, - Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: compExpr}, - } - case sqlparser.SetDefault: - return nil, vterrors.VT09016() - } - - // For the child statement of a DELETE query, we don't need to verify all the FKs on VTgate or ignore any foreign key explicitly. - childOp, err := createOpFromStmt(ctx, childStmt, false /* verifyAllFKs */, "" /* fkToIgnore */) - if err != nil { - return nil, err - } - - return &FkChild{ - BVName: bvName, - Cols: cols, - Op: childOp, - }, nil -} diff --git a/go/vt/vtgate/planbuilder/operators/ast_to_update_op.go b/go/vt/vtgate/planbuilder/operators/ast_to_update_op.go deleted file mode 100644 index f1efb5d2a0b..00000000000 --- a/go/vt/vtgate/planbuilder/operators/ast_to_update_op.go +++ /dev/null @@ -1,539 +0,0 @@ -/* -Copyright 2023 The Vitess Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package operators - -import ( - vschemapb "vitess.io/vitess/go/vt/proto/vschema" - "vitess.io/vitess/go/vt/sqlparser" - "vitess.io/vitess/go/vt/vterrors" - "vitess.io/vitess/go/vt/vtgate/engine" - "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" - "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" - "vitess.io/vitess/go/vt/vtgate/vindexes" -) - -func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (ops.Operator, error) { - tableInfo, qt, err := createQueryTableForDML(ctx, updStmt.TableExprs[0], updStmt.Where) - if err != nil { - return nil, err - } - - vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "update") - if err != nil { - return nil, err - } - - updClone := sqlparser.CloneRefOfUpdate(updStmt) - updOp, err := createUpdateOperator(ctx, updStmt, vindexTable, qt, routing) - if err != nil { - return nil, err - } - - ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) - if err != nil { - return nil, err - } - // Unmanaged foreign-key-mode, we don't need to do anything. - if ksMode != vschemapb.Keyspace_FK_MANAGED { - return updOp, nil - } - - parentFks, childFks := getFKRequirementsForUpdate(ctx, updStmt.Exprs, vindexTable) - if len(childFks) == 0 && len(parentFks) == 0 { - return updOp, nil - } - - // If the delete statement has a limit, we don't support it yet. - if updStmt.Limit != nil { - return nil, vterrors.VT12001("update with limit with foreign key constraints") - } - - return buildFkOperator(ctx, updOp, updClone, parentFks, childFks, vindexTable) -} - -func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, vindexTable *vindexes.Table, qt *QueryTable, routing Routing) (ops.Operator, error) { - assignments := make(map[string]sqlparser.Expr) - for _, set := range updStmt.Exprs { - assignments[set.Name.Name.String()] = set.Expr - } - - vp, cvv, ovq, err := getUpdateVindexInformation(updStmt, vindexTable, qt.ID, qt.Predicates) - if err != nil { - return nil, err - } - - tr, ok := routing.(*ShardedRouting) - if ok { - tr.VindexPreds = vp - } - - for _, predicate := range qt.Predicates { - routing, err = UpdateRoutingLogic(ctx, predicate, routing) - if err != nil { - return nil, err - } - } - - if routing.OpCode() == engine.Scatter && updStmt.Limit != nil { - // TODO systay: we should probably check for other op code types - IN could also hit multiple shards (2022-04-07) - return nil, vterrors.VT12001("multi shard UPDATE with LIMIT") - } - - r := &Route{ - Source: &Update{ - QTable: qt, - VTable: vindexTable, - Assignments: assignments, - ChangedVindexValues: cvv, - OwnedVindexQuery: ovq, - AST: updStmt, - }, - Routing: routing, - } - - subq, err := createSubqueryFromStatement(ctx, updStmt) - if err != nil { - return nil, err - } - if subq == nil { - return r, nil - } - subq.Outer = r - return subq, nil -} - -// getFKRequirementsForUpdate analyzes update expressions to determine which foreign key constraints needs management at the VTGate. -// It identifies parent and child foreign keys that require verification or cascade operations due to column updates. -func getFKRequirementsForUpdate(ctx *plancontext.PlanningContext, updateExprs sqlparser.UpdateExprs, vindexTable *vindexes.Table) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { - parentFks := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) - childFks := vindexTable.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) - if len(childFks) == 0 && len(parentFks) == 0 { - return nil, nil - } - - pFksRequired := make([]bool, len(parentFks)) - cFksRequired := make([]bool, len(childFks)) - // Go over all the update expressions - for _, updateExpr := range updateExprs { - // Any foreign key to a child table for a column that has been updated - // will require the cascade operations or restrict verification to happen, so we include all such foreign keys. - for idx, childFk := range childFks { - if childFk.ParentColumns.FindColumn(updateExpr.Name.Name) >= 0 { - cFksRequired[idx] = true - } - } - // If we are setting a column to NULL, then we don't need to verify the existance of an - // equivalent row in the parent table, even if this column was part of a foreign key to a parent table. - if sqlparser.IsNull(updateExpr.Expr) { - continue - } - // We add all the possible parent foreign key constraints that need verification that an equivalent row - // exists, given that this column has changed. - for idx, parentFk := range parentFks { - if parentFk.ChildColumns.FindColumn(updateExpr.Name.Name) >= 0 { - pFksRequired[idx] = true - } - } - } - // For the parent foreign keys, if any of the columns part of the fk is set to NULL, - // then, we don't care for the existance of an equivalent row in the parent table. - for idx, parentFk := range parentFks { - for _, updateExpr := range updateExprs { - if !sqlparser.IsNull(updateExpr.Expr) { - continue - } - if parentFk.ChildColumns.FindColumn(updateExpr.Name.Name) >= 0 { - pFksRequired[idx] = false - } - } - } - // Get the filtered lists and return them. - var pFksNeedsHandling []vindexes.ParentFKInfo - var cFksNeedsHandling []vindexes.ChildFKInfo - for idx, parentFk := range parentFks { - if pFksRequired[idx] { - pFksNeedsHandling = append(pFksNeedsHandling, parentFk) - } - } - for idx, childFk := range childFks { - if cFksRequired[idx] { - cFksNeedsHandling = append(cFksNeedsHandling, childFk) - } - } - return pFksNeedsHandling, cFksNeedsHandling -} - -func buildFkOperator(ctx *plancontext.PlanningContext, updOp ops.Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { - // We only support simple expressions in update queries for foreign key handling. - if sqlparser.IsNonLiteral(updClone.Exprs) { - return nil, vterrors.VT12001("update expression with non-literal values with foreign key constraints") - } - - restrictChildFks, cascadeChildFks := splitChildFks(childFks) - - op, err := createFKCascadeOp(ctx, updOp, updClone, cascadeChildFks, updatedTable) - if err != nil { - return nil, err - } - - return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks) -} - -// splitChildFks splits the child foreign keys into restrict and cascade list as restrict is handled through Verify operator and cascade is handled through Cascade operator. -func splitChildFks(fks []vindexes.ChildFKInfo) (restrictChildFks, cascadeChildFks []vindexes.ChildFKInfo) { - for _, fk := range fks { - // Any RESTRICT type foreign keys that arrive here for 2 reasons— - // 1. cross-shard/cross-keyspace RESTRICT cases. - // 2. shard-scoped/unsharded RESTRICT cases arising because we have to validate all the foreign keys on VTGate. - if isRestrict(fk.OnUpdate) { - // For RESTRICT foreign keys, we need to verify that there are no child rows corresponding to the rows being updated. - // This is done using a FkVerify Operator. - restrictChildFks = append(restrictChildFks, fk) - } else { - // For all the other foreign keys like CASCADE, SET NULL, we have to cascade the update to the children, - // This is done by using a FkCascade Operator. - cascadeChildFks = append(cascadeChildFks, fk) - } - } - return -} - -func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp ops.Operator, updStmt *sqlparser.Update, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { - if len(childFks) == 0 { - return parentOp, nil - } - - var fkChildren []*FkChild - var selectExprs []sqlparser.SelectExpr - - for _, fk := range childFks { - // We should have already filtered out update restrict foreign keys. - if isRestrict(fk.OnUpdate) { - return nil, vterrors.VT13001("ON UPDATE RESTRICT foreign keys should already be filtered") - } - - // We need to select all the parent columns for the foreign key constraint, to use in the update of the child table. - cols, exprs := selectParentColumns(fk, len(selectExprs)) - selectExprs = append(selectExprs, exprs...) - - fkChild, err := createFkChildForUpdate(ctx, fk, updStmt, cols, updatedTable) - if err != nil { - return nil, err - } - fkChildren = append(fkChildren, fkChild) - } - - selectionOp, err := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, nil) - if err != nil { - return nil, err - } - - return &FkCascade{ - Selection: selectionOp, - Children: fkChildren, - Parent: parentOp, - }, nil -} - -// createFkChildForUpdate creates the update query operator for the child table based on the foreign key constraints. -func createFkChildForUpdate(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, cols []int, updatedTable *vindexes.Table) (*FkChild, error) { - // Create a ValTuple of child column names - var valTuple sqlparser.ValTuple - for _, column := range fk.ChildColumns { - valTuple = append(valTuple, sqlparser.NewColName(column.String())) - } - - // Reserve a bind variable name - bvName := ctx.ReservedVars.ReserveVariable(foriegnKeyContraintValues) - // Create a comparison expression for WHERE clause - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) - var childWhereExpr sqlparser.Expr = compExpr - - var childOp ops.Operator - var err error - switch fk.OnUpdate { - case sqlparser.Cascade: - childOp, err = buildChildUpdOpForCascade(ctx, fk, updStmt, childWhereExpr, updatedTable) - case sqlparser.SetNull: - childOp, err = buildChildUpdOpForSetNull(ctx, fk, updStmt, childWhereExpr, valTuple) - case sqlparser.SetDefault: - return nil, vterrors.VT09016() - } - if err != nil { - return nil, err - } - - return &FkChild{ - BVName: bvName, - Cols: cols, - Op: childOp, - }, nil -} - -// buildChildUpdOpForCascade builds the child update statement operator for the CASCADE type foreign key constraint. -// The query looks like this - -// -// `UPDATE SET WHERE IN ()` -func buildChildUpdOpForCascade(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, childWhereExpr sqlparser.Expr, updatedTable *vindexes.Table) (ops.Operator, error) { - // The update expressions are the same as the update expressions in the parent update query - // with the column names replaced with the child column names. - var childUpdateExprs sqlparser.UpdateExprs - for _, updateExpr := range updStmt.Exprs { - colIdx := fk.ParentColumns.FindColumn(updateExpr.Name.Name) - if colIdx == -1 { - continue - } - - // The where condition is the same as the comparison expression above - // with the column names replaced with the child column names. - childUpdateExprs = append(childUpdateExprs, &sqlparser.UpdateExpr{ - Name: sqlparser.NewColName(fk.ChildColumns[colIdx].String()), - Expr: updateExpr.Expr, - }) - } - // Because we could be updating the child to a non-null value, - // We have to run with foreign key checks OFF because the parent isn't guaranteed to have - // the data being updated to. - parsedComments := sqlparser.Comments{ - "/*+ SET_VAR(foreign_key_checks=OFF) */", - }.Parsed() - childUpdStmt := &sqlparser.Update{ - Comments: parsedComments, - Exprs: childUpdateExprs, - TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, - Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: childWhereExpr}, - } - // Since we are running the child update with foreign key checks turned off, - // we need to verify the validity of the remaining foreign keys on VTGate, - // while specifically ignoring the parent foreign key in question. - return createOpFromStmt(ctx, childUpdStmt, true, fk.String(updatedTable)) - -} - -// buildChildUpdOpForSetNull builds the child update statement operator for the SET NULL type foreign key constraint. -// The query looks like this - -// -// `UPDATE SET -// WHERE IN () -// [AND NOT IN ()]` -func buildChildUpdOpForSetNull(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, childWhereExpr sqlparser.Expr, valTuple sqlparser.ValTuple) (ops.Operator, error) { - // For the SET NULL type constraint, we need to set all the child columns to NULL. - var childUpdateExprs sqlparser.UpdateExprs - for _, column := range fk.ChildColumns { - childUpdateExprs = append(childUpdateExprs, &sqlparser.UpdateExpr{ - Name: sqlparser.NewColName(column.String()), - Expr: &sqlparser.NullVal{}, - }) - } - - // SET NULL cascade should be avoided for the case where the parent columns remains unchanged on the update. - // We need to add a condition to the where clause to handle this case. - // The additional condition looks like [AND NOT IN ()]. - // If any of the parent columns is being set to NULL, then we don't need this condition. - var updateValues sqlparser.ValTuple - colSetToNull := false - for _, updateExpr := range updStmt.Exprs { - colIdx := fk.ParentColumns.FindColumn(updateExpr.Name.Name) - if colIdx >= 0 { - if sqlparser.IsNull(updateExpr.Expr) { - colSetToNull = true - break - } - updateValues = append(updateValues, updateExpr.Expr) - } - } - if !colSetToNull { - childWhereExpr = &sqlparser.AndExpr{ - Left: childWhereExpr, - Right: sqlparser.NewComparisonExpr(sqlparser.NotInOp, valTuple, updateValues, nil), - } - } - childUpdStmt := &sqlparser.Update{ - Exprs: childUpdateExprs, - TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, - Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: childWhereExpr}, - } - return createOpFromStmt(ctx, childUpdStmt, false, "") -} - -// createFKVerifyOp creates the verify operator for the parent foreign key constraints. -func createFKVerifyOp(ctx *plancontext.PlanningContext, childOp ops.Operator, updStmt *sqlparser.Update, parentFks []vindexes.ParentFKInfo, restrictChildFks []vindexes.ChildFKInfo) (ops.Operator, error) { - if len(parentFks) == 0 && len(restrictChildFks) == 0 { - return childOp, nil - } - - var Verify []*VerifyOp - // This validates that new values exists on the parent table. - for _, fk := range parentFks { - op, err := createFkVerifyOpForParentFKForUpdate(ctx, updStmt, fk) - if err != nil { - return nil, err - } - Verify = append(Verify, &VerifyOp{ - Op: op, - Typ: engine.ParentVerify, - }) - } - // This validates that the old values don't exist on the child table. - for _, fk := range restrictChildFks { - op, err := createFkVerifyOpForChildFKForUpdate(ctx, updStmt, fk) - if err != nil { - return nil, err - } - Verify = append(Verify, &VerifyOp{ - Op: op, - Typ: engine.ChildVerify, - }) - } - - return &FkVerify{ - Verify: Verify, - Input: childOp, - }, nil -} - -// Each parent foreign key constraint is verified by an anti join query of the form: -// select 1 from child_tbl left join parent_tbl on -// where and and limit 1 -// E.g: -// Child (c1, c2) references Parent (p1, p2) -// update Child set c1 = 1 where id = 1 -// verify query: -// select 1 from Child left join Parent on Parent.p1 = 1 and Parent.p2 = Child.c2 -// where Parent.p1 is null and Parent.p2 is null and Child.id = 1 -// and Child.c2 is not null -// limit 1 -func createFkVerifyOpForParentFKForUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, pFK vindexes.ParentFKInfo) (ops.Operator, error) { - childTblExpr := updStmt.TableExprs[0].(*sqlparser.AliasedTableExpr) - childTbl, err := childTblExpr.TableName() - if err != nil { - return nil, err - } - parentTbl := pFK.Table.GetTableName() - var whereCond sqlparser.Expr - var joinCond sqlparser.Expr - for idx, column := range pFK.ChildColumns { - var matchedExpr *sqlparser.UpdateExpr - for _, updateExpr := range updStmt.Exprs { - if column.Equal(updateExpr.Name.Name) { - matchedExpr = updateExpr - break - } - } - parentIsNullExpr := &sqlparser.IsExpr{ - Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), - Right: sqlparser.IsNullOp, - } - var predicate sqlparser.Expr = parentIsNullExpr - var joinExpr sqlparser.Expr - if matchedExpr == nil { - predicate = &sqlparser.AndExpr{ - Left: parentIsNullExpr, - Right: &sqlparser.IsExpr{ - Left: sqlparser.NewColNameWithQualifier(pFK.ChildColumns[idx].String(), childTbl), - Right: sqlparser.IsNotNullOp, - }, - } - joinExpr = &sqlparser.ComparisonExpr{ - Operator: sqlparser.EqualOp, - Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), - Right: sqlparser.NewColNameWithQualifier(pFK.ChildColumns[idx].String(), childTbl), - } - } else { - joinExpr = &sqlparser.ComparisonExpr{ - Operator: sqlparser.EqualOp, - Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), - Right: prefixColNames(childTbl, matchedExpr.Expr), - } - } - - if idx == 0 { - joinCond, whereCond = joinExpr, predicate - continue - } - joinCond = &sqlparser.AndExpr{Left: joinCond, Right: joinExpr} - whereCond = &sqlparser.AndExpr{Left: whereCond, Right: predicate} - } - // add existing where condition on the update statement - if updStmt.Where != nil { - whereCond = &sqlparser.AndExpr{Left: whereCond, Right: prefixColNames(childTbl, updStmt.Where.Expr)} - } - return createSelectionOp(ctx, - sqlparser.SelectExprs{sqlparser.NewAliasedExpr(sqlparser.NewIntLiteral("1"), "")}, - []sqlparser.TableExpr{ - sqlparser.NewJoinTableExpr( - childTblExpr, - sqlparser.LeftJoinType, - sqlparser.NewAliasedTableExpr(parentTbl, ""), - sqlparser.NewJoinCondition(joinCond, nil)), - }, - sqlparser.NewWhere(sqlparser.WhereClause, whereCond), - sqlparser.NewLimitWithoutOffset(1)) -} - -// Each child foreign key constraint is verified by a join query of the form: -// select 1 from child_tbl join parent_tbl on where limit 1 -// E.g: -// Child (c1, c2) references Parent (p1, p2) -// update Parent set p1 = 1 where id = 1 -// verify query: -// select 1 from Child join Parent on Parent.p1 = Child.c1 and Parent.p2 = Child.c2 -// where Parent.id = 1 limit 1 -func createFkVerifyOpForChildFKForUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, cFk vindexes.ChildFKInfo) (ops.Operator, error) { - // ON UPDATE RESTRICT foreign keys that require validation, should only be allowed in the case where we - // are verifying all the FKs on vtgate level. - if !ctx.VerifyAllFKs { - return nil, vterrors.VT12002() - } - parentTblExpr := updStmt.TableExprs[0].(*sqlparser.AliasedTableExpr) - parentTbl, err := parentTblExpr.TableName() - if err != nil { - return nil, err - } - childTbl := cFk.Table.GetTableName() - var joinCond sqlparser.Expr - for idx := range cFk.ParentColumns { - joinExpr := &sqlparser.ComparisonExpr{ - Operator: sqlparser.EqualOp, - Left: sqlparser.NewColNameWithQualifier(cFk.ParentColumns[idx].String(), parentTbl), - Right: sqlparser.NewColNameWithQualifier(cFk.ChildColumns[idx].String(), childTbl), - } - - if idx == 0 { - joinCond = joinExpr - continue - } - joinCond = &sqlparser.AndExpr{Left: joinCond, Right: joinExpr} - } - - var whereCond sqlparser.Expr - // add existing where condition on the update statement - if updStmt.Where != nil { - whereCond = prefixColNames(parentTbl, updStmt.Where.Expr) - } - return createSelectionOp(ctx, - sqlparser.SelectExprs{sqlparser.NewAliasedExpr(sqlparser.NewIntLiteral("1"), "")}, - []sqlparser.TableExpr{ - sqlparser.NewJoinTableExpr( - parentTblExpr, - sqlparser.NormalJoinType, - sqlparser.NewAliasedTableExpr(childTbl, ""), - sqlparser.NewJoinCondition(joinCond, nil)), - }, - sqlparser.NewWhere(sqlparser.WhereClause, whereCond), - sqlparser.NewLimitWithoutOffset(1)) -} diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index 01b3ab11520..323d1abb845 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -19,8 +19,12 @@ package operators import ( "fmt" + vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" + "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" + "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" "vitess.io/vitess/go/vt/vtgate/semantics" "vitess.io/vitess/go/vt/vtgate/vindexes" ) @@ -69,3 +73,186 @@ func (d *Delete) ShortDescription() string { func (d *Delete) Statement() sqlparser.Statement { return d.AST } + +func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete) (ops.Operator, error) { + tableInfo, qt, err := createQueryTableForDML(ctx, deleteStmt.TableExprs[0], deleteStmt.Where) + if err != nil { + return nil, err + } + + vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "delete") + if err != nil { + return nil, err + } + + delClone := sqlparser.CloneRefOfDelete(deleteStmt) + // Create the delete operator first. + delOp, err := createDeleteOperator(ctx, deleteStmt, qt, vindexTable, routing) + if err != nil { + return nil, err + } + + // Now we check for the foreign key mode and make changes if required. + ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) + if err != nil { + return nil, err + } + + // Unmanaged foreign-key-mode, we don't need to do anything. + if ksMode != vschemapb.Keyspace_FK_MANAGED { + return delOp, nil + } + + childFks := vindexTable.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.DeleteAction) + // If there are no foreign key constraints, then we don't need to do anything. + if len(childFks) == 0 { + return delOp, nil + } + // If the delete statement has a limit, we don't support it yet. + if deleteStmt.Limit != nil { + return nil, vterrors.VT12001("foreign keys management at vitess with limit") + } + + return createFkCascadeOpForDelete(ctx, delOp, delClone, childFks) +} + +func createDeleteOperator(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete, qt *QueryTable, vindexTable *vindexes.Table, routing Routing) (ops.Operator, error) { + del := &Delete{ + QTable: qt, + VTable: vindexTable, + AST: deleteStmt, + } + route := &Route{ + Source: del, + Routing: routing, + } + + if !vindexTable.Keyspace.Sharded { + return route, nil + } + + primaryVindex, vindexAndPredicates, err := getVindexInformation(qt.ID, qt.Predicates, vindexTable) + if err != nil { + return nil, err + } + + tr, ok := routing.(*ShardedRouting) + if ok { + tr.VindexPreds = vindexAndPredicates + } + + var ovq string + if len(vindexTable.Owned) > 0 { + tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: vindexTable.Name}, As: qt.Alias.As} + ovq = generateOwnedVindexQuery(tblExpr, deleteStmt, vindexTable, primaryVindex.Columns) + } + + del.OwnedVindexQuery = ovq + + for _, predicate := range qt.Predicates { + var err error + route.Routing, err = UpdateRoutingLogic(ctx, predicate, route.Routing) + if err != nil { + return nil, err + } + } + + if routing.OpCode() == engine.Scatter && deleteStmt.Limit != nil { + // TODO systay: we should probably check for other op code types - IN could also hit multiple shards (2022-04-07) + return nil, vterrors.VT12001("multi shard DELETE with LIMIT") + } + + subq, err := createSubqueryFromStatement(ctx, deleteStmt) + if err != nil { + return nil, err + } + if subq == nil { + return route, nil + } + subq.Outer = route + return subq, nil +} + +func createFkCascadeOpForDelete(ctx *plancontext.PlanningContext, parentOp ops.Operator, delStmt *sqlparser.Delete, childFks []vindexes.ChildFKInfo) (ops.Operator, error) { + var fkChildren []*FkChild + var selectExprs []sqlparser.SelectExpr + for _, fk := range childFks { + // Any RESTRICT type foreign keys that arrive here, + // are cross-shard/cross-keyspace RESTRICT cases, which we don't currently support. + if isRestrict(fk.OnDelete) { + return nil, vterrors.VT12002() + } + + // We need to select all the parent columns for the foreign key constraint, to use in the update of the child table. + cols, exprs := selectParentColumns(fk, len(selectExprs)) + selectExprs = append(selectExprs, exprs...) + + fkChild, err := createFkChildForDelete(ctx, fk, cols) + if err != nil { + return nil, err + } + fkChildren = append(fkChildren, fkChild) + } + selectionOp, err := createSelectionOp(ctx, selectExprs, delStmt.TableExprs, delStmt.Where, nil) + if err != nil { + return nil, err + } + + return &FkCascade{ + Selection: selectionOp, + Children: fkChildren, + Parent: parentOp, + }, nil +} + +func createFkChildForDelete(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, cols []int) (*FkChild, error) { + bvName := ctx.ReservedVars.ReserveVariable(foriegnKeyContraintValues) + + var childStmt sqlparser.Statement + switch fk.OnDelete { + case sqlparser.Cascade: + // We now construct the delete query for the child table. + // The query looks something like this - `DELETE FROM WHERE IN ()` + var valTuple sqlparser.ValTuple + for _, column := range fk.ChildColumns { + valTuple = append(valTuple, sqlparser.NewColName(column.String())) + } + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) + childStmt = &sqlparser.Delete{ + TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, + Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: compExpr}, + } + case sqlparser.SetNull: + // We now construct the update query for the child table. + // The query looks something like this - `UPDATE SET = NULL [AND = NULL]... WHERE IN ()` + var valTuple sqlparser.ValTuple + var updExprs sqlparser.UpdateExprs + for _, column := range fk.ChildColumns { + valTuple = append(valTuple, sqlparser.NewColName(column.String())) + updExprs = append(updExprs, &sqlparser.UpdateExpr{ + Name: sqlparser.NewColName(column.String()), + Expr: &sqlparser.NullVal{}, + }) + } + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) + childStmt = &sqlparser.Update{ + Exprs: updExprs, + TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, + Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: compExpr}, + } + case sqlparser.SetDefault: + return nil, vterrors.VT09016() + } + + // For the child statement of a DELETE query, we don't need to verify all the FKs on VTgate or ignore any foreign key explicitly. + childOp, err := createOpFromStmt(ctx, childStmt, false /* verifyAllFKs */, "" /* fkToIgnore */) + if err != nil { + return nil, err + } + + return &FkChild{ + BVName: bvName, + Cols: cols, + Op: childOp, + }, nil +} diff --git a/go/vt/vtgate/planbuilder/operators/update.go b/go/vt/vtgate/planbuilder/operators/update.go index f523643a84e..8acb0e20a71 100644 --- a/go/vt/vtgate/planbuilder/operators/update.go +++ b/go/vt/vtgate/planbuilder/operators/update.go @@ -17,9 +17,12 @@ limitations under the License. package operators import ( + vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" + "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" "vitess.io/vitess/go/vt/vtgate/semantics" "vitess.io/vitess/go/vt/vtgate/vindexes" ) @@ -72,3 +75,515 @@ func (u *Update) ShortDescription() string { func (u *Update) Statement() sqlparser.Statement { return u.AST } + +func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (ops.Operator, error) { + tableInfo, qt, err := createQueryTableForDML(ctx, updStmt.TableExprs[0], updStmt.Where) + if err != nil { + return nil, err + } + + vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "update") + if err != nil { + return nil, err + } + + updClone := sqlparser.CloneRefOfUpdate(updStmt) + updOp, err := createUpdateOperator(ctx, updStmt, vindexTable, qt, routing) + if err != nil { + return nil, err + } + + ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) + if err != nil { + return nil, err + } + // Unmanaged foreign-key-mode, we don't need to do anything. + if ksMode != vschemapb.Keyspace_FK_MANAGED { + return updOp, nil + } + + parentFks, childFks := getFKRequirementsForUpdate(ctx, updStmt.Exprs, vindexTable) + if len(childFks) == 0 && len(parentFks) == 0 { + return updOp, nil + } + + // If the delete statement has a limit, we don't support it yet. + if updStmt.Limit != nil { + return nil, vterrors.VT12001("update with limit with foreign key constraints") + } + + return buildFkOperator(ctx, updOp, updClone, parentFks, childFks, vindexTable) +} + +func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, vindexTable *vindexes.Table, qt *QueryTable, routing Routing) (ops.Operator, error) { + assignments := make(map[string]sqlparser.Expr) + for _, set := range updStmt.Exprs { + assignments[set.Name.Name.String()] = set.Expr + } + + vp, cvv, ovq, err := getUpdateVindexInformation(updStmt, vindexTable, qt.ID, qt.Predicates) + if err != nil { + return nil, err + } + + tr, ok := routing.(*ShardedRouting) + if ok { + tr.VindexPreds = vp + } + + for _, predicate := range qt.Predicates { + routing, err = UpdateRoutingLogic(ctx, predicate, routing) + if err != nil { + return nil, err + } + } + + if routing.OpCode() == engine.Scatter && updStmt.Limit != nil { + // TODO systay: we should probably check for other op code types - IN could also hit multiple shards (2022-04-07) + return nil, vterrors.VT12001("multi shard UPDATE with LIMIT") + } + + r := &Route{ + Source: &Update{ + QTable: qt, + VTable: vindexTable, + Assignments: assignments, + ChangedVindexValues: cvv, + OwnedVindexQuery: ovq, + AST: updStmt, + }, + Routing: routing, + } + + subq, err := createSubqueryFromStatement(ctx, updStmt) + if err != nil { + return nil, err + } + if subq == nil { + return r, nil + } + subq.Outer = r + return subq, nil +} + +// getFKRequirementsForUpdate analyzes update expressions to determine which foreign key constraints needs management at the VTGate. +// It identifies parent and child foreign keys that require verification or cascade operations due to column updates. +func getFKRequirementsForUpdate(ctx *plancontext.PlanningContext, updateExprs sqlparser.UpdateExprs, vindexTable *vindexes.Table) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { + parentFks := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) + childFks := vindexTable.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) + if len(childFks) == 0 && len(parentFks) == 0 { + return nil, nil + } + + pFksRequired := make([]bool, len(parentFks)) + cFksRequired := make([]bool, len(childFks)) + // Go over all the update expressions + for _, updateExpr := range updateExprs { + // Any foreign key to a child table for a column that has been updated + // will require the cascade operations or restrict verification to happen, so we include all such foreign keys. + for idx, childFk := range childFks { + if childFk.ParentColumns.FindColumn(updateExpr.Name.Name) >= 0 { + cFksRequired[idx] = true + } + } + // If we are setting a column to NULL, then we don't need to verify the existance of an + // equivalent row in the parent table, even if this column was part of a foreign key to a parent table. + if sqlparser.IsNull(updateExpr.Expr) { + continue + } + // We add all the possible parent foreign key constraints that need verification that an equivalent row + // exists, given that this column has changed. + for idx, parentFk := range parentFks { + if parentFk.ChildColumns.FindColumn(updateExpr.Name.Name) >= 0 { + pFksRequired[idx] = true + } + } + } + // For the parent foreign keys, if any of the columns part of the fk is set to NULL, + // then, we don't care for the existance of an equivalent row in the parent table. + for idx, parentFk := range parentFks { + for _, updateExpr := range updateExprs { + if !sqlparser.IsNull(updateExpr.Expr) { + continue + } + if parentFk.ChildColumns.FindColumn(updateExpr.Name.Name) >= 0 { + pFksRequired[idx] = false + } + } + } + // Get the filtered lists and return them. + var pFksNeedsHandling []vindexes.ParentFKInfo + var cFksNeedsHandling []vindexes.ChildFKInfo + for idx, parentFk := range parentFks { + if pFksRequired[idx] { + pFksNeedsHandling = append(pFksNeedsHandling, parentFk) + } + } + for idx, childFk := range childFks { + if cFksRequired[idx] { + cFksNeedsHandling = append(cFksNeedsHandling, childFk) + } + } + return pFksNeedsHandling, cFksNeedsHandling +} + +func buildFkOperator(ctx *plancontext.PlanningContext, updOp ops.Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { + // We only support simple expressions in update queries for foreign key handling. + if sqlparser.IsNonLiteral(updClone.Exprs) { + return nil, vterrors.VT12001("update expression with non-literal values with foreign key constraints") + } + + restrictChildFks, cascadeChildFks := splitChildFks(childFks) + + op, err := createFKCascadeOp(ctx, updOp, updClone, cascadeChildFks, updatedTable) + if err != nil { + return nil, err + } + + return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks) +} + +// splitChildFks splits the child foreign keys into restrict and cascade list as restrict is handled through Verify operator and cascade is handled through Cascade operator. +func splitChildFks(fks []vindexes.ChildFKInfo) (restrictChildFks, cascadeChildFks []vindexes.ChildFKInfo) { + for _, fk := range fks { + // Any RESTRICT type foreign keys that arrive here for 2 reasons— + // 1. cross-shard/cross-keyspace RESTRICT cases. + // 2. shard-scoped/unsharded RESTRICT cases arising because we have to validate all the foreign keys on VTGate. + if isRestrict(fk.OnUpdate) { + // For RESTRICT foreign keys, we need to verify that there are no child rows corresponding to the rows being updated. + // This is done using a FkVerify Operator. + restrictChildFks = append(restrictChildFks, fk) + } else { + // For all the other foreign keys like CASCADE, SET NULL, we have to cascade the update to the children, + // This is done by using a FkCascade Operator. + cascadeChildFks = append(cascadeChildFks, fk) + } + } + return +} + +func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp ops.Operator, updStmt *sqlparser.Update, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { + if len(childFks) == 0 { + return parentOp, nil + } + + var fkChildren []*FkChild + var selectExprs []sqlparser.SelectExpr + + for _, fk := range childFks { + // We should have already filtered out update restrict foreign keys. + if isRestrict(fk.OnUpdate) { + return nil, vterrors.VT13001("ON UPDATE RESTRICT foreign keys should already be filtered") + } + + // We need to select all the parent columns for the foreign key constraint, to use in the update of the child table. + cols, exprs := selectParentColumns(fk, len(selectExprs)) + selectExprs = append(selectExprs, exprs...) + + fkChild, err := createFkChildForUpdate(ctx, fk, updStmt, cols, updatedTable) + if err != nil { + return nil, err + } + fkChildren = append(fkChildren, fkChild) + } + + selectionOp, err := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, nil) + if err != nil { + return nil, err + } + + return &FkCascade{ + Selection: selectionOp, + Children: fkChildren, + Parent: parentOp, + }, nil +} + +// createFkChildForUpdate creates the update query operator for the child table based on the foreign key constraints. +func createFkChildForUpdate(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, cols []int, updatedTable *vindexes.Table) (*FkChild, error) { + // Create a ValTuple of child column names + var valTuple sqlparser.ValTuple + for _, column := range fk.ChildColumns { + valTuple = append(valTuple, sqlparser.NewColName(column.String())) + } + + // Reserve a bind variable name + bvName := ctx.ReservedVars.ReserveVariable(foriegnKeyContraintValues) + // Create a comparison expression for WHERE clause + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, valTuple, sqlparser.NewListArg(bvName), nil) + var childWhereExpr sqlparser.Expr = compExpr + + var childOp ops.Operator + var err error + switch fk.OnUpdate { + case sqlparser.Cascade: + childOp, err = buildChildUpdOpForCascade(ctx, fk, updStmt, childWhereExpr, updatedTable) + case sqlparser.SetNull: + childOp, err = buildChildUpdOpForSetNull(ctx, fk, updStmt, childWhereExpr, valTuple) + case sqlparser.SetDefault: + return nil, vterrors.VT09016() + } + if err != nil { + return nil, err + } + + return &FkChild{ + BVName: bvName, + Cols: cols, + Op: childOp, + }, nil +} + +// buildChildUpdOpForCascade builds the child update statement operator for the CASCADE type foreign key constraint. +// The query looks like this - +// +// `UPDATE SET WHERE IN ()` +func buildChildUpdOpForCascade(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, childWhereExpr sqlparser.Expr, updatedTable *vindexes.Table) (ops.Operator, error) { + // The update expressions are the same as the update expressions in the parent update query + // with the column names replaced with the child column names. + var childUpdateExprs sqlparser.UpdateExprs + for _, updateExpr := range updStmt.Exprs { + colIdx := fk.ParentColumns.FindColumn(updateExpr.Name.Name) + if colIdx == -1 { + continue + } + + // The where condition is the same as the comparison expression above + // with the column names replaced with the child column names. + childUpdateExprs = append(childUpdateExprs, &sqlparser.UpdateExpr{ + Name: sqlparser.NewColName(fk.ChildColumns[colIdx].String()), + Expr: updateExpr.Expr, + }) + } + // Because we could be updating the child to a non-null value, + // We have to run with foreign key checks OFF because the parent isn't guaranteed to have + // the data being updated to. + parsedComments := sqlparser.Comments{ + "/*+ SET_VAR(foreign_key_checks=OFF) */", + }.Parsed() + childUpdStmt := &sqlparser.Update{ + Comments: parsedComments, + Exprs: childUpdateExprs, + TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, + Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: childWhereExpr}, + } + // Since we are running the child update with foreign key checks turned off, + // we need to verify the validity of the remaining foreign keys on VTGate, + // while specifically ignoring the parent foreign key in question. + return createOpFromStmt(ctx, childUpdStmt, true, fk.String(updatedTable)) + +} + +// buildChildUpdOpForSetNull builds the child update statement operator for the SET NULL type foreign key constraint. +// The query looks like this - +// +// `UPDATE SET +// WHERE IN () +// [AND NOT IN ()]` +func buildChildUpdOpForSetNull(ctx *plancontext.PlanningContext, fk vindexes.ChildFKInfo, updStmt *sqlparser.Update, childWhereExpr sqlparser.Expr, valTuple sqlparser.ValTuple) (ops.Operator, error) { + // For the SET NULL type constraint, we need to set all the child columns to NULL. + var childUpdateExprs sqlparser.UpdateExprs + for _, column := range fk.ChildColumns { + childUpdateExprs = append(childUpdateExprs, &sqlparser.UpdateExpr{ + Name: sqlparser.NewColName(column.String()), + Expr: &sqlparser.NullVal{}, + }) + } + + // SET NULL cascade should be avoided for the case where the parent columns remains unchanged on the update. + // We need to add a condition to the where clause to handle this case. + // The additional condition looks like [AND NOT IN ()]. + // If any of the parent columns is being set to NULL, then we don't need this condition. + var updateValues sqlparser.ValTuple + colSetToNull := false + for _, updateExpr := range updStmt.Exprs { + colIdx := fk.ParentColumns.FindColumn(updateExpr.Name.Name) + if colIdx >= 0 { + if sqlparser.IsNull(updateExpr.Expr) { + colSetToNull = true + break + } + updateValues = append(updateValues, updateExpr.Expr) + } + } + if !colSetToNull { + childWhereExpr = &sqlparser.AndExpr{ + Left: childWhereExpr, + Right: sqlparser.NewComparisonExpr(sqlparser.NotInOp, valTuple, updateValues, nil), + } + } + childUpdStmt := &sqlparser.Update{ + Exprs: childUpdateExprs, + TableExprs: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(fk.Table.GetTableName(), "")}, + Where: &sqlparser.Where{Type: sqlparser.WhereClause, Expr: childWhereExpr}, + } + return createOpFromStmt(ctx, childUpdStmt, false, "") +} + +// createFKVerifyOp creates the verify operator for the parent foreign key constraints. +func createFKVerifyOp(ctx *plancontext.PlanningContext, childOp ops.Operator, updStmt *sqlparser.Update, parentFks []vindexes.ParentFKInfo, restrictChildFks []vindexes.ChildFKInfo) (ops.Operator, error) { + if len(parentFks) == 0 && len(restrictChildFks) == 0 { + return childOp, nil + } + + var Verify []*VerifyOp + // This validates that new values exists on the parent table. + for _, fk := range parentFks { + op, err := createFkVerifyOpForParentFKForUpdate(ctx, updStmt, fk) + if err != nil { + return nil, err + } + Verify = append(Verify, &VerifyOp{ + Op: op, + Typ: engine.ParentVerify, + }) + } + // This validates that the old values don't exist on the child table. + for _, fk := range restrictChildFks { + op, err := createFkVerifyOpForChildFKForUpdate(ctx, updStmt, fk) + if err != nil { + return nil, err + } + Verify = append(Verify, &VerifyOp{ + Op: op, + Typ: engine.ChildVerify, + }) + } + + return &FkVerify{ + Verify: Verify, + Input: childOp, + }, nil +} + +// Each parent foreign key constraint is verified by an anti join query of the form: +// select 1 from child_tbl left join parent_tbl on +// where and and limit 1 +// E.g: +// Child (c1, c2) references Parent (p1, p2) +// update Child set c1 = 1 where id = 1 +// verify query: +// select 1 from Child left join Parent on Parent.p1 = 1 and Parent.p2 = Child.c2 +// where Parent.p1 is null and Parent.p2 is null and Child.id = 1 +// and Child.c2 is not null +// limit 1 +func createFkVerifyOpForParentFKForUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, pFK vindexes.ParentFKInfo) (ops.Operator, error) { + childTblExpr := updStmt.TableExprs[0].(*sqlparser.AliasedTableExpr) + childTbl, err := childTblExpr.TableName() + if err != nil { + return nil, err + } + parentTbl := pFK.Table.GetTableName() + var whereCond sqlparser.Expr + var joinCond sqlparser.Expr + for idx, column := range pFK.ChildColumns { + var matchedExpr *sqlparser.UpdateExpr + for _, updateExpr := range updStmt.Exprs { + if column.Equal(updateExpr.Name.Name) { + matchedExpr = updateExpr + break + } + } + parentIsNullExpr := &sqlparser.IsExpr{ + Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), + Right: sqlparser.IsNullOp, + } + var predicate sqlparser.Expr = parentIsNullExpr + var joinExpr sqlparser.Expr + if matchedExpr == nil { + predicate = &sqlparser.AndExpr{ + Left: parentIsNullExpr, + Right: &sqlparser.IsExpr{ + Left: sqlparser.NewColNameWithQualifier(pFK.ChildColumns[idx].String(), childTbl), + Right: sqlparser.IsNotNullOp, + }, + } + joinExpr = &sqlparser.ComparisonExpr{ + Operator: sqlparser.EqualOp, + Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), + Right: sqlparser.NewColNameWithQualifier(pFK.ChildColumns[idx].String(), childTbl), + } + } else { + joinExpr = &sqlparser.ComparisonExpr{ + Operator: sqlparser.EqualOp, + Left: sqlparser.NewColNameWithQualifier(pFK.ParentColumns[idx].String(), parentTbl), + Right: prefixColNames(childTbl, matchedExpr.Expr), + } + } + + if idx == 0 { + joinCond, whereCond = joinExpr, predicate + continue + } + joinCond = &sqlparser.AndExpr{Left: joinCond, Right: joinExpr} + whereCond = &sqlparser.AndExpr{Left: whereCond, Right: predicate} + } + // add existing where condition on the update statement + if updStmt.Where != nil { + whereCond = &sqlparser.AndExpr{Left: whereCond, Right: prefixColNames(childTbl, updStmt.Where.Expr)} + } + return createSelectionOp(ctx, + sqlparser.SelectExprs{sqlparser.NewAliasedExpr(sqlparser.NewIntLiteral("1"), "")}, + []sqlparser.TableExpr{ + sqlparser.NewJoinTableExpr( + childTblExpr, + sqlparser.LeftJoinType, + sqlparser.NewAliasedTableExpr(parentTbl, ""), + sqlparser.NewJoinCondition(joinCond, nil)), + }, + sqlparser.NewWhere(sqlparser.WhereClause, whereCond), + sqlparser.NewLimitWithoutOffset(1)) +} + +// Each child foreign key constraint is verified by a join query of the form: +// select 1 from child_tbl join parent_tbl on where limit 1 +// E.g: +// Child (c1, c2) references Parent (p1, p2) +// update Parent set p1 = 1 where id = 1 +// verify query: +// select 1 from Child join Parent on Parent.p1 = Child.c1 and Parent.p2 = Child.c2 +// where Parent.id = 1 limit 1 +func createFkVerifyOpForChildFKForUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, cFk vindexes.ChildFKInfo) (ops.Operator, error) { + // ON UPDATE RESTRICT foreign keys that require validation, should only be allowed in the case where we + // are verifying all the FKs on vtgate level. + if !ctx.VerifyAllFKs { + return nil, vterrors.VT12002() + } + parentTblExpr := updStmt.TableExprs[0].(*sqlparser.AliasedTableExpr) + parentTbl, err := parentTblExpr.TableName() + if err != nil { + return nil, err + } + childTbl := cFk.Table.GetTableName() + var joinCond sqlparser.Expr + for idx := range cFk.ParentColumns { + joinExpr := &sqlparser.ComparisonExpr{ + Operator: sqlparser.EqualOp, + Left: sqlparser.NewColNameWithQualifier(cFk.ParentColumns[idx].String(), parentTbl), + Right: sqlparser.NewColNameWithQualifier(cFk.ChildColumns[idx].String(), childTbl), + } + + if idx == 0 { + joinCond = joinExpr + continue + } + joinCond = &sqlparser.AndExpr{Left: joinCond, Right: joinExpr} + } + + var whereCond sqlparser.Expr + // add existing where condition on the update statement + if updStmt.Where != nil { + whereCond = prefixColNames(parentTbl, updStmt.Where.Expr) + } + return createSelectionOp(ctx, + sqlparser.SelectExprs{sqlparser.NewAliasedExpr(sqlparser.NewIntLiteral("1"), "")}, + []sqlparser.TableExpr{ + sqlparser.NewJoinTableExpr( + parentTblExpr, + sqlparser.NormalJoinType, + sqlparser.NewAliasedTableExpr(childTbl, ""), + sqlparser.NewJoinCondition(joinCond, nil)), + }, + sqlparser.NewWhere(sqlparser.WhereClause, whereCond), + sqlparser.NewLimitWithoutOffset(1)) +} From 44c41a8254d8cc9b84aace6c983d03a810cbee07 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Mon, 11 Sep 2023 19:42:10 +0530 Subject: [PATCH 02/10] moved around insert operator code Signed-off-by: Harshit Gangal --- go/vt/sqlparser/ast_funcs.go | 12 +- go/vt/vtgate/planbuilder/operators/ast2op.go | 317 ------------------- go/vt/vtgate/planbuilder/operators/delete.go | 2 +- go/vt/vtgate/planbuilder/operators/insert.go | 310 ++++++++++++++++++ go/vt/vtgate/planbuilder/operators/update.go | 6 +- 5 files changed, 325 insertions(+), 322 deletions(-) diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go index 21eb2bed982..e3a96890267 100644 --- a/go/vt/sqlparser/ast_funcs.go +++ b/go/vt/sqlparser/ast_funcs.go @@ -2533,7 +2533,7 @@ func (v *visitor) visitAllSelects(in SelectStatement, f func(p *Select, idx int) panic("switch should be exhaustive") } -func IsNonLiteral(updExprs UpdateExprs) bool { +func (updExprs UpdateExprs) IsNonLiteral() bool { for _, updateExpr := range updExprs { switch updateExpr.Expr.(type) { case *Argument, *NullVal, BoolVal, *Literal: @@ -2543,3 +2543,13 @@ func IsNonLiteral(updExprs UpdateExprs) bool { } return false } + +// IsRestrict returns true if the reference action is of restrict type. +func (ra ReferenceAction) IsRestrict() bool { + switch ra { + case Restrict, NoAction, DefaultAction: + return true + default: + return false + } +} diff --git a/go/vt/vtgate/planbuilder/operators/ast2op.go b/go/vt/vtgate/planbuilder/operators/ast2op.go index 9103b37fcb1..9c7d34d1e05 100644 --- a/go/vt/vtgate/planbuilder/operators/ast2op.go +++ b/go/vt/vtgate/planbuilder/operators/ast2op.go @@ -18,13 +18,9 @@ package operators import ( "fmt" - "strconv" - vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vterrors" - "vitess.io/vitess/go/vt/vtgate/engine" - "vitess.io/vitess/go/vt/vtgate/evalengine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" "vitess.io/vitess/go/vt/vtgate/semantics" @@ -126,310 +122,6 @@ func createOpFromStmt(ctx *plancontext.PlanningContext, stmt sqlparser.Statement return PlanQuery(newCtx, stmt) } -func createOperatorFromInsert(ctx *plancontext.PlanningContext, ins *sqlparser.Insert) (ops.Operator, error) { - tableInfo, qt, err := createQueryTableForDML(ctx, ins.Table, nil) - if err != nil { - return nil, err - } - - vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "insert") - if err != nil { - return nil, err - } - - if _, target := routing.(*TargetedRouting); target { - return nil, vterrors.VT12001("INSERT with a target destination") - } - - insOp := &Insert{ - VTable: vindexTable, - AST: ins, - } - route := &Route{ - Source: insOp, - Routing: routing, - } - - // Find the foreign key mode and store the ParentFKs that we need to verify. - ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) - if err != nil { - return nil, err - } - if ksMode == vschemapb.Keyspace_FK_MANAGED { - parentFKs := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) - if len(parentFKs) > 0 { - return nil, vterrors.VT12002() - } - } - - // Table column list is nil then add all the columns - // If the column list is empty then add only the auto-inc column and - // this happens on calling modifyForAutoinc - if ins.Columns == nil && valuesProvided(ins.Rows) { - if vindexTable.ColumnListAuthoritative { - ins = populateInsertColumnlist(ins, vindexTable) - } else { - return nil, vterrors.VT09004() - } - } - - // modify column list or values for autoincrement column. - autoIncGen, err := modifyForAutoinc(ins, vindexTable) - if err != nil { - return nil, err - } - insOp.AutoIncrement = autoIncGen - - // set insert ignore. - insOp.Ignore = bool(ins.Ignore) || ins.OnDup != nil - - insOp.ColVindexes = getColVindexes(insOp) - switch rows := ins.Rows.(type) { - case sqlparser.Values: - route.Source, err = insertRowsPlan(insOp, ins, rows) - if err != nil { - return nil, err - } - case sqlparser.SelectStatement: - route.Source, err = insertSelectPlan(ctx, insOp, ins, rows) - if err != nil { - return nil, err - } - } - return route, nil -} - -func insertSelectPlan(ctx *plancontext.PlanningContext, insOp *Insert, ins *sqlparser.Insert, sel sqlparser.SelectStatement) (*Insert, error) { - if columnMismatch(insOp.AutoIncrement, ins, sel) { - return nil, vterrors.VT03006() - } - - selOp, err := PlanQuery(ctx, sel) - if err != nil { - return nil, err - } - - // select plan will be taken as input to insert rows into the table. - insOp.Input = selOp - - // When the table you are steaming data from and table you are inserting from are same. - // Then due to locking of the index range on the table we might not be able to insert into the table. - // Therefore, instead of streaming, this flag will ensure the records are first read and then inserted. - insertTbl := insOp.TablesUsed()[0] - selTables := TablesUsed(selOp) - for _, tbl := range selTables { - if insertTbl == tbl { - insOp.ForceNonStreaming = true - break - } - } - - if len(insOp.ColVindexes) == 0 { - return insOp, nil - } - - colVindexes := insOp.ColVindexes - vv := make([][]int, len(colVindexes)) - for idx, colVindex := range colVindexes { - for _, col := range colVindex.Columns { - err := checkAndErrIfVindexChanging(sqlparser.UpdateExprs(ins.OnDup), col) - if err != nil { - return nil, err - } - - colNum := findColumn(ins, col) - // sharding column values should be provided in the insert. - if colNum == -1 && idx == 0 { - return nil, vterrors.VT09003(col) - } - vv[idx] = append(vv[idx], colNum) - } - } - insOp.VindexValueOffset = vv - return insOp, nil -} - -func columnMismatch(gen *Generate, ins *sqlparser.Insert, sel sqlparser.SelectStatement) bool { - origColCount := len(ins.Columns) - if gen != nil && gen.added { - // One column got added to the insert query ast for auto increment column. - // adjusting it here for comparison. - origColCount-- - } - if origColCount < sel.GetColumnCount() { - return true - } - if origColCount > sel.GetColumnCount() { - sel := sqlparser.GetFirstSelect(sel) - var hasStarExpr bool - for _, sExpr := range sel.SelectExprs { - if _, hasStarExpr = sExpr.(*sqlparser.StarExpr); hasStarExpr { - break - } - } - if !hasStarExpr { - return true - } - } - return false -} - -func insertRowsPlan(insOp *Insert, ins *sqlparser.Insert, rows sqlparser.Values) (*Insert, error) { - for _, row := range rows { - if len(ins.Columns) != len(row) { - return nil, vterrors.VT03006() - } - } - - if len(insOp.ColVindexes) == 0 { - return insOp, nil - } - - colVindexes := insOp.ColVindexes - routeValues := make([][][]evalengine.Expr, len(colVindexes)) - for vIdx, colVindex := range colVindexes { - routeValues[vIdx] = make([][]evalengine.Expr, len(colVindex.Columns)) - for colIdx, col := range colVindex.Columns { - err := checkAndErrIfVindexChanging(sqlparser.UpdateExprs(ins.OnDup), col) - if err != nil { - return nil, err - } - routeValues[vIdx][colIdx] = make([]evalengine.Expr, len(rows)) - colNum, _ := findOrAddColumn(ins, col) - for rowNum, row := range rows { - innerpv, err := evalengine.Translate(row[colNum], nil) - if err != nil { - return nil, err - } - routeValues[vIdx][colIdx][rowNum] = innerpv - } - } - } - // here we are replacing the row value with the argument. - for _, colVindex := range colVindexes { - for _, col := range colVindex.Columns { - colNum, _ := findOrAddColumn(ins, col) - for rowNum, row := range rows { - name := engine.InsertVarName(col, rowNum) - row[colNum] = sqlparser.NewArgument(name) - } - } - } - insOp.VindexValues = routeValues - return insOp, nil -} - -func valuesProvided(rows sqlparser.InsertRows) bool { - switch values := rows.(type) { - case sqlparser.Values: - return len(values) >= 0 && len(values[0]) > 0 - case sqlparser.SelectStatement: - return true - } - return false -} - -func getColVindexes(insOp *Insert) (colVindexes []*vindexes.ColumnVindex) { - // For unsharded table the Column Vindex does not mean anything. - // And therefore should be ignored. - if !insOp.VTable.Keyspace.Sharded { - return - } - for _, colVindex := range insOp.VTable.ColumnVindexes { - if colVindex.IsPartialVindex() { - continue - } - colVindexes = append(colVindexes, colVindex) - } - return -} - -func checkAndErrIfVindexChanging(setClauses sqlparser.UpdateExprs, col sqlparser.IdentifierCI) error { - for _, assignment := range setClauses { - if col.Equal(assignment.Name.Name) { - valueExpr, isValuesFuncExpr := assignment.Expr.(*sqlparser.ValuesFuncExpr) - // update on duplicate key is changing the vindex column, not supported. - if !isValuesFuncExpr || !valueExpr.Name.Name.Equal(assignment.Name.Name) { - return vterrors.VT12001("DML cannot update vindex column") - } - return nil - } - } - return nil -} - -// findOrAddColumn finds the position of a column in the insert. If it's -// absent it appends it to the with NULL values. -// It returns the position of the column and also boolean representing whether it was added or already present. -func findOrAddColumn(ins *sqlparser.Insert, col sqlparser.IdentifierCI) (int, bool) { - colNum := findColumn(ins, col) - if colNum >= 0 { - return colNum, false - } - colOffset := len(ins.Columns) - ins.Columns = append(ins.Columns, col) - if rows, ok := ins.Rows.(sqlparser.Values); ok { - for i := range rows { - rows[i] = append(rows[i], &sqlparser.NullVal{}) - } - } - return colOffset, true -} - -// findColumn returns the column index where it is placed on the insert column list. -// Otherwise, return -1 when not found. -func findColumn(ins *sqlparser.Insert, col sqlparser.IdentifierCI) int { - for i, column := range ins.Columns { - if col.Equal(column) { - return i - } - } - return -1 -} - -func populateInsertColumnlist(ins *sqlparser.Insert, table *vindexes.Table) *sqlparser.Insert { - cols := make(sqlparser.Columns, 0, len(table.Columns)) - for _, c := range table.Columns { - cols = append(cols, c.Name) - } - ins.Columns = cols - return ins -} - -// modifyForAutoinc modifies the AST and the plan to generate necessary autoinc values. -// For row values cases, bind variable names are generated using baseName. -func modifyForAutoinc(ins *sqlparser.Insert, vTable *vindexes.Table) (*Generate, error) { - if vTable.AutoIncrement == nil { - return nil, nil - } - gen := &Generate{ - Keyspace: vTable.AutoIncrement.Sequence.Keyspace, - TableName: sqlparser.TableName{Name: vTable.AutoIncrement.Sequence.Name}, - } - colNum, newColAdded := findOrAddColumn(ins, vTable.AutoIncrement.Column) - switch rows := ins.Rows.(type) { - case sqlparser.SelectStatement: - gen.Offset = colNum - gen.added = newColAdded - case sqlparser.Values: - autoIncValues := make([]evalengine.Expr, 0, len(rows)) - for rowNum, row := range rows { - // Support the DEFAULT keyword by treating it as null - if _, ok := row[colNum].(*sqlparser.Default); ok { - row[colNum] = &sqlparser.NullVal{} - } - expr, err := evalengine.Translate(row[colNum], nil) - if err != nil { - return nil, err - } - autoIncValues = append(autoIncValues, expr) - row[colNum] = sqlparser.NewArgument(engine.SeqVarName + strconv.Itoa(rowNum)) - } - gen.Values = evalengine.NewTupleExpr(autoIncValues...) - } - return gen, nil -} - func getOperatorFromTableExpr(ctx *plancontext.PlanningContext, tableExpr sqlparser.TableExpr, onlyTable bool) (ops.Operator, error) { switch tableExpr := tableExpr.(type) { case *sqlparser.AliasedTableExpr: @@ -585,15 +277,6 @@ func addColumnEquality(ctx *plancontext.PlanningContext, expr sqlparser.Expr) { } } -func isRestrict(onDelete sqlparser.ReferenceAction) bool { - switch onDelete { - case sqlparser.Restrict, sqlparser.NoAction, sqlparser.DefaultAction: - return true - default: - return false - } -} - // createSelectionOp creates the selection operator to select the parent columns for the foreign key constraints. // The Select statement looks something like this - `SELECT FROM WHERE ` // TODO (@Harshit, @GuptaManan100): Compress the columns in the SELECT statement, if there are multiple foreign key constraints using the same columns. diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index 323d1abb845..d75167bb538 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -179,7 +179,7 @@ func createFkCascadeOpForDelete(ctx *plancontext.PlanningContext, parentOp ops.O for _, fk := range childFks { // Any RESTRICT type foreign keys that arrive here, // are cross-shard/cross-keyspace RESTRICT cases, which we don't currently support. - if isRestrict(fk.OnDelete) { + if fk.OnDelete.IsRestrict() { return nil, vterrors.VT12002() } diff --git a/go/vt/vtgate/planbuilder/operators/insert.go b/go/vt/vtgate/planbuilder/operators/insert.go index 78ae6cc133e..c9d85ea7823 100644 --- a/go/vt/vtgate/planbuilder/operators/insert.go +++ b/go/vt/vtgate/planbuilder/operators/insert.go @@ -17,9 +17,15 @@ limitations under the License. package operators import ( + "strconv" + + vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" + "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/evalengine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" + "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" "vitess.io/vitess/go/vt/vtgate/vindexes" ) @@ -121,3 +127,307 @@ func (i *Insert) TablesUsed() []string { func (i *Insert) Statement() sqlparser.Statement { return i.AST } + +func createOperatorFromInsert(ctx *plancontext.PlanningContext, ins *sqlparser.Insert) (ops.Operator, error) { + tableInfo, qt, err := createQueryTableForDML(ctx, ins.Table, nil) + if err != nil { + return nil, err + } + + vindexTable, routing, err := buildVindexTableForDML(ctx, tableInfo, qt, "insert") + if err != nil { + return nil, err + } + + if _, target := routing.(*TargetedRouting); target { + return nil, vterrors.VT12001("INSERT with a target destination") + } + + insOp := &Insert{ + VTable: vindexTable, + AST: ins, + } + route := &Route{ + Source: insOp, + Routing: routing, + } + + // Find the foreign key mode and store the ParentFKs that we need to verify. + ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) + if err != nil { + return nil, err + } + if ksMode == vschemapb.Keyspace_FK_MANAGED { + parentFKs := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) + if len(parentFKs) > 0 { + return nil, vterrors.VT12002() + } + } + + // Table column list is nil then add all the columns + // If the column list is empty then add only the auto-inc column and + // this happens on calling modifyForAutoinc + if ins.Columns == nil && valuesProvided(ins.Rows) { + if vindexTable.ColumnListAuthoritative { + ins = populateInsertColumnlist(ins, vindexTable) + } else { + return nil, vterrors.VT09004() + } + } + + // modify column list or values for autoincrement column. + autoIncGen, err := modifyForAutoinc(ins, vindexTable) + if err != nil { + return nil, err + } + insOp.AutoIncrement = autoIncGen + + // set insert ignore. + insOp.Ignore = bool(ins.Ignore) || ins.OnDup != nil + + insOp.ColVindexes = getColVindexes(insOp) + switch rows := ins.Rows.(type) { + case sqlparser.Values: + route.Source, err = insertRowsPlan(insOp, ins, rows) + if err != nil { + return nil, err + } + case sqlparser.SelectStatement: + route.Source, err = insertSelectPlan(ctx, insOp, ins, rows) + if err != nil { + return nil, err + } + } + return route, nil +} + +func insertSelectPlan(ctx *plancontext.PlanningContext, insOp *Insert, ins *sqlparser.Insert, sel sqlparser.SelectStatement) (*Insert, error) { + if columnMismatch(insOp.AutoIncrement, ins, sel) { + return nil, vterrors.VT03006() + } + + selOp, err := PlanQuery(ctx, sel) + if err != nil { + return nil, err + } + + // select plan will be taken as input to insert rows into the table. + insOp.Input = selOp + + // When the table you are steaming data from and table you are inserting from are same. + // Then due to locking of the index range on the table we might not be able to insert into the table. + // Therefore, instead of streaming, this flag will ensure the records are first read and then inserted. + insertTbl := insOp.TablesUsed()[0] + selTables := TablesUsed(selOp) + for _, tbl := range selTables { + if insertTbl == tbl { + insOp.ForceNonStreaming = true + break + } + } + + if len(insOp.ColVindexes) == 0 { + return insOp, nil + } + + colVindexes := insOp.ColVindexes + vv := make([][]int, len(colVindexes)) + for idx, colVindex := range colVindexes { + for _, col := range colVindex.Columns { + err := checkAndErrIfVindexChanging(sqlparser.UpdateExprs(ins.OnDup), col) + if err != nil { + return nil, err + } + + colNum := findColumn(ins, col) + // sharding column values should be provided in the insert. + if colNum == -1 && idx == 0 { + return nil, vterrors.VT09003(col) + } + vv[idx] = append(vv[idx], colNum) + } + } + insOp.VindexValueOffset = vv + return insOp, nil +} + +func columnMismatch(gen *Generate, ins *sqlparser.Insert, sel sqlparser.SelectStatement) bool { + origColCount := len(ins.Columns) + if gen != nil && gen.added { + // One column got added to the insert query ast for auto increment column. + // adjusting it here for comparison. + origColCount-- + } + if origColCount < sel.GetColumnCount() { + return true + } + if origColCount > sel.GetColumnCount() { + sel := sqlparser.GetFirstSelect(sel) + var hasStarExpr bool + for _, sExpr := range sel.SelectExprs { + if _, hasStarExpr = sExpr.(*sqlparser.StarExpr); hasStarExpr { + break + } + } + if !hasStarExpr { + return true + } + } + return false +} + +func insertRowsPlan(insOp *Insert, ins *sqlparser.Insert, rows sqlparser.Values) (*Insert, error) { + for _, row := range rows { + if len(ins.Columns) != len(row) { + return nil, vterrors.VT03006() + } + } + + if len(insOp.ColVindexes) == 0 { + return insOp, nil + } + + colVindexes := insOp.ColVindexes + routeValues := make([][][]evalengine.Expr, len(colVindexes)) + for vIdx, colVindex := range colVindexes { + routeValues[vIdx] = make([][]evalengine.Expr, len(colVindex.Columns)) + for colIdx, col := range colVindex.Columns { + err := checkAndErrIfVindexChanging(sqlparser.UpdateExprs(ins.OnDup), col) + if err != nil { + return nil, err + } + routeValues[vIdx][colIdx] = make([]evalengine.Expr, len(rows)) + colNum, _ := findOrAddColumn(ins, col) + for rowNum, row := range rows { + innerpv, err := evalengine.Translate(row[colNum], nil) + if err != nil { + return nil, err + } + routeValues[vIdx][colIdx][rowNum] = innerpv + } + } + } + // here we are replacing the row value with the argument. + for _, colVindex := range colVindexes { + for _, col := range colVindex.Columns { + colNum, _ := findOrAddColumn(ins, col) + for rowNum, row := range rows { + name := engine.InsertVarName(col, rowNum) + row[colNum] = sqlparser.NewArgument(name) + } + } + } + insOp.VindexValues = routeValues + return insOp, nil +} + +func valuesProvided(rows sqlparser.InsertRows) bool { + switch values := rows.(type) { + case sqlparser.Values: + return len(values) >= 0 && len(values[0]) > 0 + case sqlparser.SelectStatement: + return true + } + return false +} + +func getColVindexes(insOp *Insert) (colVindexes []*vindexes.ColumnVindex) { + // For unsharded table the Column Vindex does not mean anything. + // And therefore should be ignored. + if !insOp.VTable.Keyspace.Sharded { + return + } + for _, colVindex := range insOp.VTable.ColumnVindexes { + if colVindex.IsPartialVindex() { + continue + } + colVindexes = append(colVindexes, colVindex) + } + return +} + +func checkAndErrIfVindexChanging(setClauses sqlparser.UpdateExprs, col sqlparser.IdentifierCI) error { + for _, assignment := range setClauses { + if col.Equal(assignment.Name.Name) { + valueExpr, isValuesFuncExpr := assignment.Expr.(*sqlparser.ValuesFuncExpr) + // update on duplicate key is changing the vindex column, not supported. + if !isValuesFuncExpr || !valueExpr.Name.Name.Equal(assignment.Name.Name) { + return vterrors.VT12001("DML cannot update vindex column") + } + return nil + } + } + return nil +} + +// findOrAddColumn finds the position of a column in the insert. If it's +// absent it appends it to the with NULL values. +// It returns the position of the column and also boolean representing whether it was added or already present. +func findOrAddColumn(ins *sqlparser.Insert, col sqlparser.IdentifierCI) (int, bool) { + colNum := findColumn(ins, col) + if colNum >= 0 { + return colNum, false + } + colOffset := len(ins.Columns) + ins.Columns = append(ins.Columns, col) + if rows, ok := ins.Rows.(sqlparser.Values); ok { + for i := range rows { + rows[i] = append(rows[i], &sqlparser.NullVal{}) + } + } + return colOffset, true +} + +// findColumn returns the column index where it is placed on the insert column list. +// Otherwise, return -1 when not found. +func findColumn(ins *sqlparser.Insert, col sqlparser.IdentifierCI) int { + for i, column := range ins.Columns { + if col.Equal(column) { + return i + } + } + return -1 +} + +func populateInsertColumnlist(ins *sqlparser.Insert, table *vindexes.Table) *sqlparser.Insert { + cols := make(sqlparser.Columns, 0, len(table.Columns)) + for _, c := range table.Columns { + cols = append(cols, c.Name) + } + ins.Columns = cols + return ins +} + +// modifyForAutoinc modifies the AST and the plan to generate necessary autoinc values. +// For row values cases, bind variable names are generated using baseName. +func modifyForAutoinc(ins *sqlparser.Insert, vTable *vindexes.Table) (*Generate, error) { + if vTable.AutoIncrement == nil { + return nil, nil + } + gen := &Generate{ + Keyspace: vTable.AutoIncrement.Sequence.Keyspace, + TableName: sqlparser.TableName{Name: vTable.AutoIncrement.Sequence.Name}, + } + colNum, newColAdded := findOrAddColumn(ins, vTable.AutoIncrement.Column) + switch rows := ins.Rows.(type) { + case sqlparser.SelectStatement: + gen.Offset = colNum + gen.added = newColAdded + case sqlparser.Values: + autoIncValues := make([]evalengine.Expr, 0, len(rows)) + for rowNum, row := range rows { + // Support the DEFAULT keyword by treating it as null + if _, ok := row[colNum].(*sqlparser.Default); ok { + row[colNum] = &sqlparser.NullVal{} + } + expr, err := evalengine.Translate(row[colNum], nil) + if err != nil { + return nil, err + } + autoIncValues = append(autoIncValues, expr) + row[colNum] = sqlparser.NewArgument(engine.SeqVarName + strconv.Itoa(rowNum)) + } + gen.Values = evalengine.NewTupleExpr(autoIncValues...) + } + return gen, nil +} diff --git a/go/vt/vtgate/planbuilder/operators/update.go b/go/vt/vtgate/planbuilder/operators/update.go index 8acb0e20a71..36dfb38ba4b 100644 --- a/go/vt/vtgate/planbuilder/operators/update.go +++ b/go/vt/vtgate/planbuilder/operators/update.go @@ -229,7 +229,7 @@ func getFKRequirementsForUpdate(ctx *plancontext.PlanningContext, updateExprs sq func buildFkOperator(ctx *plancontext.PlanningContext, updOp ops.Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { // We only support simple expressions in update queries for foreign key handling. - if sqlparser.IsNonLiteral(updClone.Exprs) { + if updClone.Exprs.IsNonLiteral() { return nil, vterrors.VT12001("update expression with non-literal values with foreign key constraints") } @@ -249,7 +249,7 @@ func splitChildFks(fks []vindexes.ChildFKInfo) (restrictChildFks, cascadeChildFk // Any RESTRICT type foreign keys that arrive here for 2 reasons— // 1. cross-shard/cross-keyspace RESTRICT cases. // 2. shard-scoped/unsharded RESTRICT cases arising because we have to validate all the foreign keys on VTGate. - if isRestrict(fk.OnUpdate) { + if fk.OnUpdate.IsRestrict() { // For RESTRICT foreign keys, we need to verify that there are no child rows corresponding to the rows being updated. // This is done using a FkVerify Operator. restrictChildFks = append(restrictChildFks, fk) @@ -272,7 +272,7 @@ func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp ops.Operator, for _, fk := range childFks { // We should have already filtered out update restrict foreign keys. - if isRestrict(fk.OnUpdate) { + if fk.OnUpdate.IsRestrict() { return nil, vterrors.VT13001("ON UPDATE RESTRICT foreign keys should already be filtered") } From a442c3784adedfd29b7bdb2751780f851acf0285 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Mon, 11 Sep 2023 23:51:38 +0530 Subject: [PATCH 03/10] disallow insert on duplicate key update with foreign keys Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/insert.go | 29 ++++++++- go/vt/vtgate/planbuilder/operators/insert.go | 62 ++++++++++++------- .../testdata/foreignkey_cases.json | 27 ++++++++ go/vt/vtgate/planbuilder/update.go | 6 +- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 52066e9d1f7..961a35696f3 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -18,6 +18,7 @@ package planbuilder import ( querypb "vitess.io/vitess/go/vt/proto/query" + vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators" @@ -44,9 +45,11 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm // We cannot shortcut here as sequence column needs additional planning. ks, tables := ctx.SemTable.SingleUnshardedKeyspace() if ks != nil && tables[0].AutoIncrement == nil { - plan := insertUnshardedShortcut(insStmt, ks, tables) - plan = pushCommentDirectivesOnPlan(plan, insStmt) - return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil + if fkManagementNotRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup)) { + plan := insertUnshardedShortcut(insStmt, ks, tables) + plan = pushCommentDirectivesOnPlan(plan, insStmt) + return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil + } } tblInfo, err := ctx.SemTable.TableInfoFor(ctx.SemTable.TableSetFor(insStmt.Table)) @@ -83,6 +86,26 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm return newPlanResult(plan.Primitive(), operators.TablesUsed(op)...), nil } +// TODO: Handle all this in semantic analysis. +func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, updateExprs sqlparser.UpdateExprs) bool { + ksMode, err := ctx.VSchema.ForeignKeyMode(vTbl.Keyspace.Name) + if err != nil || ksMode != vschemapb.Keyspace_FK_MANAGED { + return true + } + + if len(vTbl.ParentFKsNeedsHandling(ctx.VerifyAllFKs, "")) > 0 { + return true + } + + childFks := vTbl.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) + getFKInfo := func(expr *sqlparser.UpdateExpr) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { + return nil, childFks + } + + // Check if any column in the parent table is being updated which has a child foreign key. + return !columnModified(updateExprs, getFKInfo) +} + func insertUnshardedShortcut(stmt *sqlparser.Insert, ks *vindexes.Keyspace, tables []*vindexes.Table) logicalPlan { eIns := &engine.Insert{} eIns.Keyspace = ks diff --git a/go/vt/vtgate/planbuilder/operators/insert.go b/go/vt/vtgate/planbuilder/operators/insert.go index c9d85ea7823..51ed6d570b3 100644 --- a/go/vt/vtgate/planbuilder/operators/insert.go +++ b/go/vt/vtgate/planbuilder/operators/insert.go @@ -139,61 +139,79 @@ func createOperatorFromInsert(ctx *plancontext.PlanningContext, ins *sqlparser.I return nil, err } + insOp, err := createInsertOperator(ctx, ins, vindexTable, routing) + if err != nil { + return nil, err + } + + // Find the foreign key mode and for unmanaged foreign-key-mode, we don't need to do anything. + ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) + if err != nil { + return nil, err + } + if ksMode != vschemapb.Keyspace_FK_MANAGED { + return insOp, nil + } + + parentFKsForInsert := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) + if len(parentFKsForInsert) > 0 { + return nil, vterrors.VT12002() + } + if len(ins.OnDup) == 0 { + return insOp, nil + } + + parentFksForUpdate, childFksForUpdate := getFKRequirementsForUpdate(ctx, sqlparser.UpdateExprs(ins.OnDup), vindexTable) + if len(parentFksForUpdate) == 0 && len(childFksForUpdate) == 0 { + return insOp, nil + } + return nil, vterrors.VT12001("ON DUPLICATE KEY UPDATE with foreign keys") +} + +func createInsertOperator(ctx *plancontext.PlanningContext, insStmt *sqlparser.Insert, vTbl *vindexes.Table, routing Routing) (ops.Operator, error) { if _, target := routing.(*TargetedRouting); target { return nil, vterrors.VT12001("INSERT with a target destination") } insOp := &Insert{ - VTable: vindexTable, - AST: ins, + VTable: vTbl, + AST: insStmt, } route := &Route{ Source: insOp, Routing: routing, } - // Find the foreign key mode and store the ParentFKs that we need to verify. - ksMode, err := ctx.VSchema.ForeignKeyMode(vindexTable.Keyspace.Name) - if err != nil { - return nil, err - } - if ksMode == vschemapb.Keyspace_FK_MANAGED { - parentFKs := vindexTable.ParentFKsNeedsHandling(ctx.VerifyAllFKs, ctx.ParentFKToIgnore) - if len(parentFKs) > 0 { - return nil, vterrors.VT12002() - } - } - // Table column list is nil then add all the columns // If the column list is empty then add only the auto-inc column and // this happens on calling modifyForAutoinc - if ins.Columns == nil && valuesProvided(ins.Rows) { - if vindexTable.ColumnListAuthoritative { - ins = populateInsertColumnlist(ins, vindexTable) + if insStmt.Columns == nil && valuesProvided(insStmt.Rows) { + if vTbl.ColumnListAuthoritative { + insStmt = populateInsertColumnlist(insStmt, vTbl) } else { return nil, vterrors.VT09004() } } // modify column list or values for autoincrement column. - autoIncGen, err := modifyForAutoinc(ins, vindexTable) + autoIncGen, err := modifyForAutoinc(insStmt, vTbl) if err != nil { return nil, err } insOp.AutoIncrement = autoIncGen // set insert ignore. - insOp.Ignore = bool(ins.Ignore) || ins.OnDup != nil + insOp.Ignore = bool(insStmt.Ignore) || insStmt.OnDup != nil insOp.ColVindexes = getColVindexes(insOp) - switch rows := ins.Rows.(type) { + switch rows := insStmt.Rows.(type) { case sqlparser.Values: - route.Source, err = insertRowsPlan(insOp, ins, rows) + route.Source, err = insertRowsPlan(insOp, insStmt, rows) if err != nil { return nil, err } case sqlparser.SelectStatement: - route.Source, err = insertSelectPlan(ctx, insOp, ins, rows) + route.Source, err = insertSelectPlan(ctx, insOp, insStmt, rows) if err != nil { return nil, err } diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index c7666a07bea..4686f6a4aac 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -1057,5 +1057,32 @@ "unsharded_fk_allow.u_tbl9" ] } + }, + { + "comment": "Insert with on duplicate key update - foreign keys disallowed", + "query": "insert into u_tbl1 (id, col1) values (1, 3) on duplicate key update col1 = 5", + "plan": "VT12001: unsupported: ON DUPLICATE KEY UPDATE with foreign keys" + }, + { + "comment": "Insert with on duplicate key update - foreign keys not on update column - allowed", + "query": "insert into u_tbl1 (id, col1, foo) values (1, 3, 'bar') on duplicate key update foo = 'baz'", + "plan": { + "QueryType": "INSERT", + "Original": "insert into u_tbl1 (id, col1, foo) values (1, 3, 'bar') on duplicate key update foo = 'baz'", + "Instructions": { + "OperatorType": "Insert", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "insert into u_tbl1(id, col1, foo) values (1, 3, 'bar') on duplicate key update foo = 'baz'", + "TableName": "u_tbl1" + }, + "TablesUsed": [ + "unsharded_fk_allow.u_tbl1" + ] + } } ] diff --git a/go/vt/vtgate/planbuilder/update.go b/go/vt/vtgate/planbuilder/update.go index 496f8ddbf22..b99631f6b55 100644 --- a/go/vt/vtgate/planbuilder/update.go +++ b/go/vt/vtgate/planbuilder/update.go @@ -48,7 +48,7 @@ func gen4UpdateStmtPlanner( } if ks, tables := ctx.SemTable.SingleUnshardedKeyspace(); ks != nil { - if fkManagementNotRequiredForUpdate(ctx, vschema, tables, updStmt.Exprs) { + if fkManagementNotRequiredForUpdate(ctx, tables, updStmt.Exprs) { plan := updateUnshardedShortcut(updStmt, ks, tables) plan = pushCommentDirectivesOnPlan(plan, updStmt) return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil @@ -86,12 +86,12 @@ func gen4UpdateStmtPlanner( } // TODO: Handle all this in semantic analysis. -func fkManagementNotRequiredForUpdate(ctx *plancontext.PlanningContext, vschema plancontext.VSchema, vTables []*vindexes.Table, updateExprs sqlparser.UpdateExprs) bool { +func fkManagementNotRequiredForUpdate(ctx *plancontext.PlanningContext, vTables []*vindexes.Table, updateExprs sqlparser.UpdateExprs) bool { childFkMap := make(map[string][]vindexes.ChildFKInfo) // Find the foreign key mode and check for any managed child foreign keys. for _, vTable := range vTables { - ksMode, err := vschema.ForeignKeyMode(vTable.Keyspace.Name) + ksMode, err := ctx.VSchema.ForeignKeyMode(vTable.Keyspace.Name) if err != nil { return false } From 343fb899025c6d8f10573aa2a2b94fe4133f78b1 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 12:35:30 +0530 Subject: [PATCH 04/10] reject update query only when fk columns have non-literal value Signed-off-by: Harshit Gangal --- go/vt/sqlparser/ast_funcs.go | 21 +- go/vt/vtgate/planbuilder/operators/update.go | 21 +- .../testdata/foreignkey_cases.json | 201 ++++++++++++++++++ 3 files changed, 231 insertions(+), 12 deletions(-) diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go index e3a96890267..e92bd1b4bce 100644 --- a/go/vt/sqlparser/ast_funcs.go +++ b/go/vt/sqlparser/ast_funcs.go @@ -2533,17 +2533,6 @@ func (v *visitor) visitAllSelects(in SelectStatement, f func(p *Select, idx int) panic("switch should be exhaustive") } -func (updExprs UpdateExprs) IsNonLiteral() bool { - for _, updateExpr := range updExprs { - switch updateExpr.Expr.(type) { - case *Argument, *NullVal, BoolVal, *Literal: - default: - return true - } - } - return false -} - // IsRestrict returns true if the reference action is of restrict type. func (ra ReferenceAction) IsRestrict() bool { switch ra { @@ -2553,3 +2542,13 @@ func (ra ReferenceAction) IsRestrict() bool { return false } } + +// IsLiteral returns true if the expression is of a literal type. +func IsLiteral(expr Expr) bool { + switch expr.(type) { + case *Argument, *NullVal, BoolVal, *Literal: + return true + default: + return false + } +} diff --git a/go/vt/vtgate/planbuilder/operators/update.go b/go/vt/vtgate/planbuilder/operators/update.go index 36dfb38ba4b..4da451b9557 100644 --- a/go/vt/vtgate/planbuilder/operators/update.go +++ b/go/vt/vtgate/planbuilder/operators/update.go @@ -229,7 +229,7 @@ func getFKRequirementsForUpdate(ctx *plancontext.PlanningContext, updateExprs sq func buildFkOperator(ctx *plancontext.PlanningContext, updOp ops.Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) (ops.Operator, error) { // We only support simple expressions in update queries for foreign key handling. - if updClone.Exprs.IsNonLiteral() { + if isNonLiteral(updClone.Exprs, parentFks, childFks) { return nil, vterrors.VT12001("update expression with non-literal values with foreign key constraints") } @@ -243,6 +243,25 @@ func buildFkOperator(ctx *plancontext.PlanningContext, updOp ops.Operator, updCl return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks) } +func isNonLiteral(updExprs sqlparser.UpdateExprs, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo) bool { + for _, updateExpr := range updExprs { + if sqlparser.IsLiteral(updateExpr.Expr) { + continue + } + for _, parentFk := range parentFks { + if parentFk.ChildColumns.FindColumn(updateExpr.Name.Name) >= 0 { + return true + } + } + for _, childFk := range childFks { + if childFk.ParentColumns.FindColumn(updateExpr.Name.Name) >= 0 { + return true + } + } + } + return false +} + // splitChildFks splits the child foreign keys into restrict and cascade list as restrict is handled through Verify operator and cascade is handled through Cascade operator. func splitChildFks(fks []vindexes.ChildFKInfo) (restrictChildFks, cascadeChildFks []vindexes.ChildFKInfo) { for _, fk := range fks { diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index 4686f6a4aac..34891895738 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -798,6 +798,207 @@ "query": "update u_tbl1 set m = 2, col1 = x + 'bar' where id = 1", "plan": "VT12001: unsupported: update expression with non-literal values with foreign key constraints" }, + { + "comment": "update in a table with set null, non-literal value on non-foreign key column - allowed", + "query": "update u_tbl2 set m = col1 + 'bar', col2 = 2 where id = 1", + "plan": { + "QueryType": "UPDATE", + "Original": "update u_tbl2 set m = col1 + 'bar', col2 = 2 where id = 1", + "Instructions": { + "OperatorType": "FkCascade", + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select col2 from u_tbl2 where 1 != 1", + "Query": "select col2 from u_tbl2 where id = 1 lock in share mode", + "Table": "u_tbl2" + }, + { + "InputName": "CascadeChild-1", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "BvName": "fkc_vals", + "Cols": [ + 0 + ], + "Query": "update u_tbl3 set col3 = null where (col3) in ::fkc_vals and (col3) not in (2)", + "Table": "u_tbl3" + }, + { + "InputName": "Parent", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "update u_tbl2 set m = col1 + 'bar', col2 = 2 where id = 1", + "Table": "u_tbl2" + } + ] + }, + "TablesUsed": [ + "unsharded_fk_allow.u_tbl2", + "unsharded_fk_allow.u_tbl3" + ] + } + }, + { + "comment": "update in a table with cascade, non-literal value on non-foreign key column - allowed", + "query": "update u_tbl1 set m = x + 'bar', col1 = 2 where id = 1", + "plan": { + "QueryType": "UPDATE", + "Original": "update u_tbl1 set m = x + 'bar', col1 = 2 where id = 1", + "Instructions": { + "OperatorType": "FkCascade", + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select col1, col1 from u_tbl1 where 1 != 1", + "Query": "select col1, col1 from u_tbl1 where id = 1 lock in share mode", + "Table": "u_tbl1" + }, + { + "InputName": "CascadeChild-1", + "OperatorType": "FkCascade", + "BvName": "fkc_vals", + "Cols": [ + 0 + ], + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select col2 from u_tbl2 where 1 != 1", + "Query": "select col2 from u_tbl2 where (col2) in ::fkc_vals", + "Table": "u_tbl2" + }, + { + "InputName": "CascadeChild-1", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "BvName": "fkc_vals1", + "Cols": [ + 0 + ], + "Query": "update u_tbl3 set col3 = null where (col3) in ::fkc_vals1 and (col3) not in (2)", + "Table": "u_tbl3" + }, + { + "InputName": "Parent", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "update /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl2 set col2 = 2 where (col2) in ::fkc_vals", + "Table": "u_tbl2" + } + ] + }, + { + "InputName": "CascadeChild-2", + "OperatorType": "FkCascade", + "BvName": "fkc_vals2", + "Cols": [ + 1 + ], + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select col9 from u_tbl9 where 1 != 1", + "Query": "select col9 from u_tbl9 where (col9) in ::fkc_vals2 and (col9) not in (2)", + "Table": "u_tbl9" + }, + { + "InputName": "CascadeChild-1", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "BvName": "fkc_vals3", + "Cols": [ + 0 + ], + "Query": "update u_tbl8 set col8 = null where (col8) in ::fkc_vals3", + "Table": "u_tbl8" + }, + { + "InputName": "Parent", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "update u_tbl9 set col9 = null where (col9) in ::fkc_vals2 and (col9) not in (2)", + "Table": "u_tbl9" + } + ] + }, + { + "InputName": "Parent", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "update u_tbl1 set m = x + 'bar', col1 = 2 where id = 1", + "Table": "u_tbl1" + } + ] + }, + "TablesUsed": [ + "unsharded_fk_allow.u_tbl1", + "unsharded_fk_allow.u_tbl2", + "unsharded_fk_allow.u_tbl3", + "unsharded_fk_allow.u_tbl8", + "unsharded_fk_allow.u_tbl9" + ] + } + }, { "comment": "update in a table with a child table having SET DEFAULT constraint - disallowed", "query": "update tbl20 set col2 = 'bar'", From b1e2910f0ad4503fc738ffdf38904c03ff3a30c4 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 14:49:57 +0530 Subject: [PATCH 05/10] fk required when unsharded table references sharded table Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/insert.go | 2 +- go/vt/vtgate/planbuilder/plan_test.go | 4 +++- .../vtgate/planbuilder/testdata/foreignkey_cases.json | 5 +++++ .../vtgate/planbuilder/testdata/vschemas/schema.json | 11 ++++++++++- go/vt/vtgate/vindexes/foreign_keys.go | 8 ++++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 961a35696f3..25ecf3659cc 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -94,7 +94,7 @@ func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vi } if len(vTbl.ParentFKsNeedsHandling(ctx.VerifyAllFKs, "")) > 0 { - return true + return false } childFks := vTbl.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 9bd1778ab6c..41977532048 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -166,6 +166,7 @@ func setFks(t *testing.T, vschema *vindexes.VSchema) { _ = vschema.AddForeignKey("unsharded_fk_allow", "u_tbl8", createFkDefinition([]string{"col8"}, "u_tbl6", []string{"col6"}, sqlparser.Cascade, sqlparser.CASCADE)) _ = vschema.AddForeignKey("unsharded_fk_allow", "u_tbl4", createFkDefinition([]string{"col4"}, "u_tbl7", []string{"col7"}, sqlparser.Cascade, sqlparser.Cascade)) _ = vschema.AddForeignKey("unsharded_fk_allow", "u_tbl9", createFkDefinition([]string{"col9"}, "u_tbl4", []string{"col4"}, sqlparser.Restrict, sqlparser.Restrict)) + _ = vschema.AddForeignKey("unsharded_fk_allow", "u_tbl", createFkDefinition([]string{"col"}, "sharded_fk_allow.s_tbl", []string{"col"}, sqlparser.Restrict, sqlparser.Restrict)) } } @@ -502,10 +503,11 @@ func loadSchema(t testing.TB, filename string, setCollation bool) *vindexes.VSch // createFkDefinition is a helper function to create a Foreign key definition struct from the columns used in it provided as list of strings. func createFkDefinition(childCols []string, parentTableName string, parentCols []string, onUpdate, onDelete sqlparser.ReferenceAction) *sqlparser.ForeignKeyDefinition { + pKs, pTbl, _ := sqlparser.ParseTable(parentTableName) return &sqlparser.ForeignKeyDefinition{ Source: sqlparser.MakeColumns(childCols...), ReferenceDefinition: &sqlparser.ReferenceDefinition{ - ReferencedTable: sqlparser.NewTableName(parentTableName), + ReferencedTable: sqlparser.NewTableNameWithQualifier(pTbl, pKs), ReferencedColumns: sqlparser.MakeColumns(parentCols...), OnUpdate: onUpdate, OnDelete: onDelete, diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index 34891895738..5032f6d2ece 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -1285,5 +1285,10 @@ "unsharded_fk_allow.u_tbl1" ] } + }, + { + "comment": "Insert with unsharded table having fk reference in sharded table", + "query": "insert into u_tbl (id, col) values (1, 2)", + "plan": "VT12002: unsupported: cross-shard foreign keys" } ] diff --git a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json index e7f38d88404..7a489a475a7 100644 --- a/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json +++ b/go/vt/vtgate/planbuilder/testdata/vschemas/schema.json @@ -734,6 +734,14 @@ "name": "hash_vin" } ] + }, + "s_tbl": { + "column_vindexes": [ + { + "column": "col", + "name": "hash_vin" + } + ] } } }, @@ -748,7 +756,8 @@ "u_tbl6": {}, "u_tbl7": {}, "u_tbl8": {}, - "u_tbl9": {} + "u_tbl9": {}, + "u_tbl": {} } } } diff --git a/go/vt/vtgate/vindexes/foreign_keys.go b/go/vt/vtgate/vindexes/foreign_keys.go index 9ac5d93e7a4..3fcbc719624 100644 --- a/go/vt/vtgate/vindexes/foreign_keys.go +++ b/go/vt/vtgate/vindexes/foreign_keys.go @@ -233,6 +233,14 @@ func (vschema *VSchema) AddForeignKey(ksname, childTableName string, fkConstrain if !ok { return fmt.Errorf("child table %s not found in keyspace %s", childTableName, ksname) } + pKsName := fkConstraint.ReferenceDefinition.ReferencedTable.Qualifier.String() + if pKsName != "" { + ks, ok = vschema.Keyspaces[pKsName] + if !ok { + return fmt.Errorf("keyspace %s not found in vschema", pKsName) + } + ksname = pKsName + } parentTableName := fkConstraint.ReferenceDefinition.ReferencedTable.Name.String() pTbl, ok := ks.Tables[parentTableName] if !ok { From 7093339ce5f547ec4d4058601fa9070167237382 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 18:02:08 +0530 Subject: [PATCH 06/10] reject replace into queries with fk Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/insert.go | 31 ++++++++++++++----- .../testdata/foreignkey_cases.json | 5 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 25ecf3659cc..589da9f6c0c 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -20,6 +20,7 @@ import ( querypb "vitess.io/vitess/go/vt/proto/query" vschemapb "vitess.io/vitess/go/vt/proto/vschema" "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators" "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" @@ -45,7 +46,7 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm // We cannot shortcut here as sequence column needs additional planning. ks, tables := ctx.SemTable.SingleUnshardedKeyspace() if ks != nil && tables[0].AutoIncrement == nil { - if fkManagementNotRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup)) { + if fkManagementNotRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup), insStmt.Action == sqlparser.ReplaceAct) { plan := insertUnshardedShortcut(insStmt, ks, tables) plan = pushCommentDirectivesOnPlan(plan, insStmt) return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil @@ -56,8 +57,9 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm if err != nil { return nil, err } - if tblInfo.GetVindexTable().Keyspace.Sharded && ctx.SemTable.NotUnshardedErr != nil { - return nil, ctx.SemTable.NotUnshardedErr + + if err = errOutIfPlanCannotBeConstructed(ctx, tblInfo.GetVindexTable(), insStmt); err != nil { + return nil, err } err = queryRewrite(ctx.SemTable, reservedVars, insStmt) @@ -86,8 +88,21 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm return newPlanResult(plan.Primitive(), operators.TablesUsed(op)...), nil } +func errOutIfPlanCannotBeConstructed(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, insStmt *sqlparser.Insert) error { + if insStmt.Action == sqlparser.ReplaceAct { + if vTbl.AutoIncrement != nil { + return vterrors.VT12001("REPLACE INTO with auto-increment column") + } + return vterrors.VT12001("REPLACE INTO with foreign keys") + } + if vTbl.Keyspace.Sharded { + return ctx.SemTable.NotUnshardedErr + } + return nil +} + // TODO: Handle all this in semantic analysis. -func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, updateExprs sqlparser.UpdateExprs) bool { +func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, updateExprs sqlparser.UpdateExprs, replace bool) bool { ksMode, err := ctx.VSchema.ForeignKeyMode(vTbl.Keyspace.Name) if err != nil || ksMode != vschemapb.Keyspace_FK_MANAGED { return true @@ -98,12 +113,14 @@ func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vi } childFks := vTbl.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) - getFKInfo := func(expr *sqlparser.UpdateExpr) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { - return nil, childFks + if len(childFks) > 0 && replace { + return false } // Check if any column in the parent table is being updated which has a child foreign key. - return !columnModified(updateExprs, getFKInfo) + return !columnModified(updateExprs, func(expr *sqlparser.UpdateExpr) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { + return nil, childFks + }) } func insertUnshardedShortcut(stmt *sqlparser.Insert, ks *vindexes.Keyspace, tables []*vindexes.Table) logicalPlan { diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index 5032f6d2ece..44d96169eb5 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -1290,5 +1290,10 @@ "comment": "Insert with unsharded table having fk reference in sharded table", "query": "insert into u_tbl (id, col) values (1, 2)", "plan": "VT12002: unsupported: cross-shard foreign keys" + }, + { + "comment": "replace with fk reference unsupported", + "query": "replace into u_tbl1 (id, col1) values (1, 2)", + "plan": "VT12001: unsupported: REPLACE INTO with foreign keys" } ] From f36cad73726c81d3a33a69f38731170b028a6088 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 18:05:49 +0530 Subject: [PATCH 07/10] addressed review comment Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/operators/delete.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index d75167bb538..5cbbde7e778 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -116,7 +116,12 @@ func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlp return createFkCascadeOpForDelete(ctx, delOp, delClone, childFks) } -func createDeleteOperator(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete, qt *QueryTable, vindexTable *vindexes.Table, routing Routing) (ops.Operator, error) { +func createDeleteOperator( + ctx *plancontext.PlanningContext, + deleteStmt *sqlparser.Delete, + qt *QueryTable, + vindexTable *vindexes.Table, + routing Routing) (ops.Operator, error) { del := &Delete{ QTable: qt, VTable: vindexTable, From ea92db1e3a0396e0cc866e8ebf13850af8ea29f0 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 21:38:32 +0530 Subject: [PATCH 08/10] fix cases for error messages Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/insert.go | 35 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 589da9f6c0c..7151d5233ca 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -45,8 +45,11 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm // Check single unsharded. Even if the table is for single unsharded but sequence table is used. // We cannot shortcut here as sequence column needs additional planning. ks, tables := ctx.SemTable.SingleUnshardedKeyspace() - if ks != nil && tables[0].AutoIncrement == nil { - if fkManagementNotRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup), insStmt.Action == sqlparser.ReplaceAct) { + FkPlanNeeded := false + if ks != nil { + noAutoInc := tables[0].AutoIncrement == nil + FkPlanNeeded = fkManagementRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup), insStmt.Action == sqlparser.ReplaceAct) + if noAutoInc && !FkPlanNeeded { plan := insertUnshardedShortcut(insStmt, ks, tables) plan = pushCommentDirectivesOnPlan(plan, insStmt) return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil @@ -58,7 +61,7 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm return nil, err } - if err = errOutIfPlanCannotBeConstructed(ctx, tblInfo.GetVindexTable(), insStmt); err != nil { + if err = errOutIfPlanCannotBeConstructed(ctx, tblInfo.GetVindexTable(), insStmt, FkPlanNeeded); err != nil { return nil, err } @@ -88,37 +91,37 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm return newPlanResult(plan.Primitive(), operators.TablesUsed(op)...), nil } -func errOutIfPlanCannotBeConstructed(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, insStmt *sqlparser.Insert) error { - if insStmt.Action == sqlparser.ReplaceAct { - if vTbl.AutoIncrement != nil { - return vterrors.VT12001("REPLACE INTO with auto-increment column") - } - return vterrors.VT12001("REPLACE INTO with foreign keys") - } - if vTbl.Keyspace.Sharded { +func errOutIfPlanCannotBeConstructed(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, insStmt *sqlparser.Insert, fkPlanNeeded bool) error { + if vTbl.Keyspace.Sharded && ctx.SemTable.NotUnshardedErr != nil { return ctx.SemTable.NotUnshardedErr } + if insStmt.Action != sqlparser.ReplaceAct { + return nil + } + if fkPlanNeeded { + return vterrors.VT12001("REPLACE INTO with foreign keys") + } return nil } // TODO: Handle all this in semantic analysis. -func fkManagementNotRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, updateExprs sqlparser.UpdateExprs, replace bool) bool { +func fkManagementRequiredForInsert(ctx *plancontext.PlanningContext, vTbl *vindexes.Table, updateExprs sqlparser.UpdateExprs, replace bool) bool { ksMode, err := ctx.VSchema.ForeignKeyMode(vTbl.Keyspace.Name) if err != nil || ksMode != vschemapb.Keyspace_FK_MANAGED { - return true + return false } if len(vTbl.ParentFKsNeedsHandling(ctx.VerifyAllFKs, "")) > 0 { - return false + return true } childFks := vTbl.ChildFKsNeedsHandling(ctx.VerifyAllFKs, vindexes.UpdateAction) if len(childFks) > 0 && replace { - return false + return true } // Check if any column in the parent table is being updated which has a child foreign key. - return !columnModified(updateExprs, func(expr *sqlparser.UpdateExpr) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { + return columnModified(updateExprs, func(expr *sqlparser.UpdateExpr) ([]vindexes.ParentFKInfo, []vindexes.ChildFKInfo) { return nil, childFks }) } From 5cdebadbeba6e2407684b74bb108011c8395d591 Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Tue, 12 Sep 2023 23:03:21 +0530 Subject: [PATCH 09/10] set lock and comments on route, set lock in share mode for fk verify, set for update lock for fk cascade selection Signed-off-by: Harshit Gangal --- go/vt/sqlparser/ast.go | 1 + go/vt/sqlparser/ast_funcs.go | 10 +++++ go/vt/sqlparser/comments.go | 7 ++++ .../planbuilder/operator_transformers.go | 6 +++ go/vt/vtgate/planbuilder/operators/ast2op.go | 3 +- go/vt/vtgate/planbuilder/operators/delete.go | 2 +- .../vtgate/planbuilder/operators/operator.go | 15 ++++++++ go/vt/vtgate/planbuilder/operators/route.go | 3 ++ go/vt/vtgate/planbuilder/operators/update.go | 8 ++-- go/vt/vtgate/planbuilder/select.go | 38 ------------------- .../testdata/foreignkey_cases.json | 14 +++---- go/vt/vtgate/semantics/check_invalid.go | 3 ++ 12 files changed, 60 insertions(+), 50 deletions(-) diff --git a/go/vt/sqlparser/ast.go b/go/vt/sqlparser/ast.go index 18cf98d4388..00db3b93436 100644 --- a/go/vt/sqlparser/ast.go +++ b/go/vt/sqlparser/ast.go @@ -63,6 +63,7 @@ type ( GetOrderBy() OrderBy GetLimit() *Limit SetLimit(*Limit) + GetLock() Lock SetLock(lock Lock) SetInto(into *SelectInto) SetWith(with *With) diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go index e92bd1b4bce..f3fcaba8560 100644 --- a/go/vt/sqlparser/ast_funcs.go +++ b/go/vt/sqlparser/ast_funcs.go @@ -1050,6 +1050,11 @@ func (node *Select) GetLimit() *Limit { return node.Limit } +// GetLock returns the lock clause +func (node *Select) GetLock() Lock { + return node.Lock +} + // SetLock sets the lock clause func (node *Select) SetLock(lock Lock) { node.Lock = lock @@ -1196,6 +1201,11 @@ func (node *Union) GetColumns() SelectExprs { return node.Left.GetColumns() } +// GetLock returns the lock clause +func (node *Union) GetLock() Lock { + return node.Lock +} + // SetLock sets the lock clause func (node *Union) SetLock(lock Lock) { node.Lock = lock diff --git a/go/vt/sqlparser/comments.go b/go/vt/sqlparser/comments.go index 911498d8242..4ecf7b1b293 100644 --- a/go/vt/sqlparser/comments.go +++ b/go/vt/sqlparser/comments.go @@ -273,6 +273,13 @@ func (c *ParsedComments) Length() int { return len(c.comments) } +func (c *ParsedComments) GetComments() Comments { + if c != nil { + return c.comments + } + return nil +} + func (c *ParsedComments) Prepend(comment string) Comments { if c == nil { return Comments{comment} diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go index 602a61ccc81..0de1a48a279 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -439,6 +439,12 @@ func transformRoutePlan(ctx *plancontext.PlanningContext, op *operators.Route) ( switch stmt := stmt.(type) { case sqlparser.SelectStatement: + if op.Lock != sqlparser.NoLock { + stmt.SetLock(op.Lock) + } + if op.Comments != nil { + stmt.SetComments(op.Comments.GetComments()) + } return buildRouteLogicalPlan(ctx, op, stmt) case *sqlparser.Update: return buildUpdateLogicalPlan(ctx, op, dmlOp, stmt) diff --git a/go/vt/vtgate/planbuilder/operators/ast2op.go b/go/vt/vtgate/planbuilder/operators/ast2op.go index 9c7d34d1e05..8e8d0ddde33 100644 --- a/go/vt/vtgate/planbuilder/operators/ast2op.go +++ b/go/vt/vtgate/planbuilder/operators/ast2op.go @@ -280,12 +280,13 @@ func addColumnEquality(ctx *plancontext.PlanningContext, expr sqlparser.Expr) { // createSelectionOp creates the selection operator to select the parent columns for the foreign key constraints. // The Select statement looks something like this - `SELECT FROM WHERE ` // TODO (@Harshit, @GuptaManan100): Compress the columns in the SELECT statement, if there are multiple foreign key constraints using the same columns. -func createSelectionOp(ctx *plancontext.PlanningContext, selectExprs []sqlparser.SelectExpr, tableExprs sqlparser.TableExprs, where *sqlparser.Where, limit *sqlparser.Limit) (ops.Operator, error) { +func createSelectionOp(ctx *plancontext.PlanningContext, selectExprs []sqlparser.SelectExpr, tableExprs sqlparser.TableExprs, where *sqlparser.Where, limit *sqlparser.Limit, lock sqlparser.Lock) (ops.Operator, error) { selectionStmt := &sqlparser.Select{ SelectExprs: selectExprs, From: tableExprs, Where: where, Limit: limit, + Lock: lock, } // There are no foreign keys to check for a select query, so we can pass anything for verifyAllFKs and fkToIgnore. return createOpFromStmt(ctx, selectionStmt, false /* verifyAllFKs */, "" /* fkToIgnore */) diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index 5cbbde7e778..bdac9e7c99a 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -198,7 +198,7 @@ func createFkCascadeOpForDelete(ctx *plancontext.PlanningContext, parentOp ops.O } fkChildren = append(fkChildren, fkChild) } - selectionOp, err := createSelectionOp(ctx, selectExprs, delStmt.TableExprs, delStmt.Where, nil) + selectionOp, err := createSelectionOp(ctx, selectExprs, delStmt.TableExprs, delStmt.Where, nil, sqlparser.ForUpdateLock) if err != nil { return nil, err } diff --git a/go/vt/vtgate/planbuilder/operators/operator.go b/go/vt/vtgate/planbuilder/operators/operator.go index 23f5bd99b70..da2be2c3242 100644 --- a/go/vt/vtgate/planbuilder/operators/operator.go +++ b/go/vt/vtgate/planbuilder/operators/operator.go @@ -89,6 +89,21 @@ func PlanQuery(ctx *plancontext.PlanningContext, stmt sqlparser.Statement) (ops. return nil, ctx.SemTable.NotSingleRouteErr } + // set lock and comments on the route to be set on the sql query on conversion. + _ = rewrite.Visit(op, func(op ops.Operator) error { + route, ok := op.(*Route) + if !ok { + return nil + } + if stmtWithComments, ok := stmt.(sqlparser.Commented); ok { + route.Comments = stmtWithComments.GetParsedComments() + } + if stmtWithLock, ok := stmt.(sqlparser.SelectStatement); ok { + route.Lock = stmtWithLock.GetLock() + } + return nil + }) + return op, err } diff --git a/go/vt/vtgate/planbuilder/operators/route.go b/go/vt/vtgate/planbuilder/operators/route.go index b41575794d7..eef886249af 100644 --- a/go/vt/vtgate/planbuilder/operators/route.go +++ b/go/vt/vtgate/planbuilder/operators/route.go @@ -42,6 +42,9 @@ type ( Ordering []RouteOrdering + Comments *sqlparser.ParsedComments + Lock sqlparser.Lock + ResultColumns int } diff --git a/go/vt/vtgate/planbuilder/operators/update.go b/go/vt/vtgate/planbuilder/operators/update.go index 4da451b9557..7e9598a18d6 100644 --- a/go/vt/vtgate/planbuilder/operators/update.go +++ b/go/vt/vtgate/planbuilder/operators/update.go @@ -306,7 +306,7 @@ func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp ops.Operator, fkChildren = append(fkChildren, fkChild) } - selectionOp, err := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, nil) + selectionOp, err := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, nil, sqlparser.ForUpdateLock) if err != nil { return nil, err } @@ -551,7 +551,8 @@ func createFkVerifyOpForParentFKForUpdate(ctx *plancontext.PlanningContext, updS sqlparser.NewJoinCondition(joinCond, nil)), }, sqlparser.NewWhere(sqlparser.WhereClause, whereCond), - sqlparser.NewLimitWithoutOffset(1)) + sqlparser.NewLimitWithoutOffset(1), + sqlparser.ShareModeLock) } // Each child foreign key constraint is verified by a join query of the form: @@ -604,5 +605,6 @@ func createFkVerifyOpForChildFKForUpdate(ctx *plancontext.PlanningContext, updSt sqlparser.NewJoinCondition(joinCond, nil)), }, sqlparser.NewWhere(sqlparser.WhereClause, whereCond), - sqlparser.NewLimitWithoutOffset(1)) + sqlparser.NewLimitWithoutOffset(1), + sqlparser.ShareModeLock) } diff --git a/go/vt/vtgate/planbuilder/select.go b/go/vt/vtgate/planbuilder/select.go index 032a3e623e6..52f0384981a 100644 --- a/go/vt/vtgate/planbuilder/select.go +++ b/go/vt/vtgate/planbuilder/select.go @@ -234,12 +234,6 @@ func newBuildSelectPlan( optimizePlan(plan) - if sel, isSel := selStmt.(*sqlparser.Select); isSel { - if err = setMiscFunc(plan, sel); err != nil { - return nil, nil, err - } - } - if err = plan.Wireup(ctx); err != nil { return nil, nil, err } @@ -396,38 +390,6 @@ func shouldRetryAfterPredicateRewriting(plan logicalPlan) bool { len(sysTableTableSchema) == 0 } -func setMiscFunc(in logicalPlan, sel *sqlparser.Select) error { - _, err := visit(in, func(plan logicalPlan) (bool, logicalPlan, error) { - switch node := plan.(type) { - case *route: - err := copyCommentsAndLocks(node.Select, sel, node.eroute.Opcode) - if err != nil { - return false, nil, err - } - return true, node, nil - } - return true, plan, nil - }) - - if err != nil { - return err - } - return nil -} - -func copyCommentsAndLocks(statement sqlparser.SelectStatement, sel *sqlparser.Select, opcode engine.Opcode) error { - query := sqlparser.GetFirstSelect(statement) - query.Comments = sel.Comments - query.Lock = sel.Lock - if sel.Into != nil { - if opcode != engine.Unsharded { - return vterrors.VT12001("INTO on sharded keyspace") - } - query.Into = sel.Into - } - return nil -} - func handleDualSelects(sel *sqlparser.Select, vschema plancontext.VSchema) (engine.Primitive, error) { if !isOnlyDual(sel) { return nil, nil diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index 44d96169eb5..d029bc31134 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -677,7 +677,7 @@ "Sharded": false }, "FieldQuery": "select col2 from u_tbl2 where 1 != 1", - "Query": "select col2 from u_tbl2 where (col2) in ::fkc_vals", + "Query": "select col2 from u_tbl2 where (col2) in ::fkc_vals for update", "Table": "u_tbl2" }, { @@ -727,7 +727,7 @@ "Sharded": false }, "FieldQuery": "select col9 from u_tbl9 where 1 != 1", - "Query": "select col9 from u_tbl9 where (col9) in ::fkc_vals2 and (col9) not in ('foo')", + "Query": "select col9 from u_tbl9 where (col9) in ::fkc_vals2 and (col9) not in ('foo') for update", "Table": "u_tbl9" }, { @@ -893,7 +893,7 @@ "Sharded": false }, "FieldQuery": "select col2 from u_tbl2 where 1 != 1", - "Query": "select col2 from u_tbl2 where (col2) in ::fkc_vals", + "Query": "select col2 from u_tbl2 where (col2) in ::fkc_vals for update", "Table": "u_tbl2" }, { @@ -943,7 +943,7 @@ "Sharded": false }, "FieldQuery": "select col9 from u_tbl9 where 1 != 1", - "Query": "select col9 from u_tbl9 where (col9) in ::fkc_vals2 and (col9) not in (2)", + "Query": "select col9 from u_tbl9 where (col9) in ::fkc_vals2 and (col9) not in (2) for update", "Table": "u_tbl9" }, { @@ -1132,7 +1132,7 @@ "Sharded": false }, "FieldQuery": "select 1 from u_tbl8 left join u_tbl9 on u_tbl9.col9 = 'foo' where 1 != 1", - "Query": "select 1 from u_tbl8 left join u_tbl9 on u_tbl9.col9 = 'foo' where (u_tbl8.col8) in ::fkc_vals and u_tbl9.col9 is null limit 1", + "Query": "select 1 from u_tbl8 left join u_tbl9 on u_tbl9.col9 = 'foo' where (u_tbl8.col8) in ::fkc_vals and u_tbl9.col9 is null limit 1 lock in share mode", "Table": "u_tbl8, u_tbl9" }, { @@ -1208,7 +1208,7 @@ "Sharded": false }, "FieldQuery": "select 1 from u_tbl4 left join u_tbl3 on u_tbl3.col3 = 'foo' where 1 != 1", - "Query": "select 1 from u_tbl4 left join u_tbl3 on u_tbl3.col3 = 'foo' where (u_tbl4.col4) in ::fkc_vals and u_tbl3.col3 is null limit 1", + "Query": "select 1 from u_tbl4 left join u_tbl3 on u_tbl3.col3 = 'foo' where (u_tbl4.col4) in ::fkc_vals and u_tbl3.col3 is null limit 1 lock in share mode", "Table": "u_tbl3, u_tbl4" }, { @@ -1220,7 +1220,7 @@ "Sharded": false }, "FieldQuery": "select 1 from u_tbl4, u_tbl9 where 1 != 1", - "Query": "select 1 from u_tbl4, u_tbl9 where (u_tbl4.col4) in ::fkc_vals and u_tbl4.col4 = u_tbl9.col9 limit 1", + "Query": "select 1 from u_tbl4, u_tbl9 where (u_tbl4.col4) in ::fkc_vals and u_tbl4.col4 = u_tbl9.col9 limit 1 lock in share mode", "Table": "u_tbl4, u_tbl9" }, { diff --git a/go/vt/vtgate/semantics/check_invalid.go b/go/vt/vtgate/semantics/check_invalid.go index 84b5a8a0bbc..c5f5c016398 100644 --- a/go/vt/vtgate/semantics/check_invalid.go +++ b/go/vt/vtgate/semantics/check_invalid.go @@ -132,6 +132,9 @@ func (a *analyzer) checkSelect(cursor *sqlparser.Cursor, node *sqlparser.Select) if a.scoper.currentScope().parent != nil { return &CantUseOptionHereError{Msg: errMsg} } + if node.Into != nil { + return ShardedError{Inner: &UnsupportedConstruct{errString: "INTO on sharded keyspace"}} + } return nil } From 9fa55302e092f8485c9cc4400671be8c03c47a9f Mon Sep 17 00:00:00 2001 From: Harshit Gangal Date: Wed, 13 Sep 2023 12:33:57 +0530 Subject: [PATCH 10/10] addressed review comments Signed-off-by: Harshit Gangal --- go/vt/vtgate/planbuilder/insert.go | 8 ++--- .../planbuilder/operator_transformers.go | 7 +++-- go/vt/vtgate/planbuilder/operators/insert.go | 2 +- .../vtgate/planbuilder/operators/operator.go | 30 +++++++++++-------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 7151d5233ca..55ef9148b1a 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -45,11 +45,11 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm // Check single unsharded. Even if the table is for single unsharded but sequence table is used. // We cannot shortcut here as sequence column needs additional planning. ks, tables := ctx.SemTable.SingleUnshardedKeyspace() - FkPlanNeeded := false + fkPlanNeeded := false if ks != nil { noAutoInc := tables[0].AutoIncrement == nil - FkPlanNeeded = fkManagementRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup), insStmt.Action == sqlparser.ReplaceAct) - if noAutoInc && !FkPlanNeeded { + fkPlanNeeded = fkManagementRequiredForInsert(ctx, tables[0], sqlparser.UpdateExprs(insStmt.OnDup), insStmt.Action == sqlparser.ReplaceAct) + if noAutoInc && !fkPlanNeeded { plan := insertUnshardedShortcut(insStmt, ks, tables) plan = pushCommentDirectivesOnPlan(plan, insStmt) return newPlanResult(plan.Primitive(), operators.QualifiedTables(ks, tables)...), nil @@ -61,7 +61,7 @@ func gen4InsertStmtPlanner(version querypb.ExecuteOptions_PlannerVersion, insStm return nil, err } - if err = errOutIfPlanCannotBeConstructed(ctx, tblInfo.GetVindexTable(), insStmt, FkPlanNeeded); err != nil { + if err = errOutIfPlanCannotBeConstructed(ctx, tblInfo.GetVindexTable(), insStmt, fkPlanNeeded); err != nil { return nil, err } diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go index 0de1a48a279..04ce68564c0 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -437,14 +437,15 @@ func transformRoutePlan(ctx *plancontext.PlanningContext, op *operators.Route) ( replaceSubQuery(ctx, stmt) + if stmtWithComments, ok := stmt.(sqlparser.Commented); ok && op.Comments != nil { + stmtWithComments.SetComments(op.Comments.GetComments()) + } + switch stmt := stmt.(type) { case sqlparser.SelectStatement: if op.Lock != sqlparser.NoLock { stmt.SetLock(op.Lock) } - if op.Comments != nil { - stmt.SetComments(op.Comments.GetComments()) - } return buildRouteLogicalPlan(ctx, op, stmt) case *sqlparser.Update: return buildUpdateLogicalPlan(ctx, op, dmlOp, stmt) diff --git a/go/vt/vtgate/planbuilder/operators/insert.go b/go/vt/vtgate/planbuilder/operators/insert.go index 51ed6d570b3..9853e714c47 100644 --- a/go/vt/vtgate/planbuilder/operators/insert.go +++ b/go/vt/vtgate/planbuilder/operators/insert.go @@ -97,7 +97,7 @@ func (i *Insert) ShortDescription() string { } func (i *Insert) GetOrdering() ([]ops.OrderBy, error) { - panic("does not expect insert operator to receive get ordering call") + return nil, nil } var _ ops.Operator = (*Insert)(nil) diff --git a/go/vt/vtgate/planbuilder/operators/operator.go b/go/vt/vtgate/planbuilder/operators/operator.go index da2be2c3242..7ee780472bc 100644 --- a/go/vt/vtgate/planbuilder/operators/operator.go +++ b/go/vt/vtgate/planbuilder/operators/operator.go @@ -90,19 +90,7 @@ func PlanQuery(ctx *plancontext.PlanningContext, stmt sqlparser.Statement) (ops. } // set lock and comments on the route to be set on the sql query on conversion. - _ = rewrite.Visit(op, func(op ops.Operator) error { - route, ok := op.(*Route) - if !ok { - return nil - } - if stmtWithComments, ok := stmt.(sqlparser.Commented); ok { - route.Comments = stmtWithComments.GetParsedComments() - } - if stmtWithLock, ok := stmt.(sqlparser.SelectStatement); ok { - route.Lock = stmtWithLock.GetLock() - } - return nil - }) + setCommentsAndLockOnRoute(op, stmt) return op, err } @@ -181,3 +169,19 @@ func transformColumnsToSelectExprs(ctx *plancontext.PlanningContext, op ops.Oper }) return selExprs, nil } + +func setCommentsAndLockOnRoute(op ops.Operator, stmt sqlparser.Statement) { + _ = rewrite.Visit(op, func(op ops.Operator) error { + route, ok := op.(*Route) + if !ok { + return nil + } + if stmtWithComments, ok := stmt.(sqlparser.Commented); ok { + route.Comments = stmtWithComments.GetParsedComments() + } + if stmtWithLock, ok := stmt.(sqlparser.SelectStatement); ok { + route.Lock = stmtWithLock.GetLock() + } + return nil + }) +}