Skip to content

Commit

Permalink
Foreign Key: Add support for Multi Table and Multi Target Update Stat…
Browse files Browse the repository at this point in the history
…ement (vitessio#15523)

Signed-off-by: Harshit Gangal <[email protected]>
  • Loading branch information
harshit-gangal authored Mar 21, 2024
1 parent 2dfc0dc commit 943b07c
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 30 deletions.
27 changes: 26 additions & 1 deletion go/test/endtoend/vtgate/foreignkey/fk_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,17 @@ func (fz *fuzzer) generateInsertDMLQuery(insertType string) string {
}
}

// generateUpdateDMLQuery generates an UPDATE query from the parameters for the fuzzer.
// generateUpdateDMLQuery generates a UPDATE query from the parameters for the fuzzer.
func (fz *fuzzer) generateUpdateDMLQuery() string {
multiTableUpdate := rand.IntN(2) + 1
if multiTableUpdate == 1 {
return fz.generateSingleUpdateDMLQuery()
}
return fz.generateMultiUpdateDMLQuery()
}

// generateSingleUpdateDMLQuery generates an UPDATE query from the parameters for the fuzzer.
func (fz *fuzzer) generateSingleUpdateDMLQuery() string {
tableId := rand.IntN(len(fkTables))
idValue := 1 + rand.IntN(fz.maxValForId)
tableName := fkTables[tableId]
Expand Down Expand Up @@ -195,6 +204,22 @@ func (fz *fuzzer) generateUpdateDMLQuery() string {
}
}

// generateMultiUpdateDMLQuery generates a UPDATE query using 2 tables from the parameters for the fuzzer.
func (fz *fuzzer) generateMultiUpdateDMLQuery() string {
tableId := rand.IntN(len(fkTables))
tableId2 := rand.IntN(len(fkTables))
idValue := 1 + rand.IntN(fz.maxValForId)
colValue := convertIntValueToString(rand.IntN(1 + fz.maxValForCol))
col2Value := convertIntValueToString(rand.IntN(1 + fz.maxValForCol))
setVarFkChecksVal := fz.getSetVarFkChecksVal()
setExprs := fmt.Sprintf("%v.col = %v", fkTables[tableId], colValue)
if rand.IntN(2)%2 == 0 {
setExprs += ", " + fmt.Sprintf("%v.col = %v", fkTables[tableId2], col2Value)
}
query := fmt.Sprintf("update %v%v join %v using (id) set %s where %v.id = %v", setVarFkChecksVal, fkTables[tableId], fkTables[tableId2], setExprs, fkTables[tableId], idValue)
return query
}

// generateDeleteDMLQuery generates a DELETE query using 1 table from the parameters for the fuzzer.
func (fz *fuzzer) generateSingleDeleteDMLQuery() string {
tableId := rand.IntN(len(fkTables))
Expand Down
30 changes: 30 additions & 0 deletions go/test/endtoend/vtgate/foreignkey/fk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,36 @@ func TestFkScenarios(t *testing.T) {
"select * from fk_multicol_t17 order by id",
"select * from fk_multicol_t19 order by id",
},
}, {
name: "Multi Table Update with non-literal update",
dataQueries: []string{
"insert into fk_multicol_t15(id, cola, colb) values (1, 7, 1), (2, 9, 1), (3, 12, 1)",
"insert into fk_multicol_t16(id, cola, colb) values (1, 7, 1), (2, 9, 1), (3, 12, 1)",
"insert into fk_multicol_t17(id, cola, colb) values (1, 7, 1)",
"insert into fk_multicol_t19(id, cola, colb) values (1, 7, 1)",
},
dmlQuery: "update fk_multicol_t15 m1 join fk_multicol_t17 on m1.id = fk_multicol_t17.id set m1.cola = m1.id + 8 where m1.id < 3",
assertionQueries: []string{
"select * from fk_multicol_t15 order by id",
"select * from fk_multicol_t16 order by id",
"select * from fk_multicol_t17 order by id",
"select * from fk_multicol_t19 order by id",
},
}, {
name: "Multi Target Update with non-literal update",
dataQueries: []string{
"insert into fk_multicol_t15(id, cola, colb) values (1, 7, 1), (2, 9, 1), (3, 12, 1)",
"insert into fk_multicol_t16(id, cola, colb) values (1, 7, 1), (2, 9, 1), (3, 12, 1)",
"insert into fk_multicol_t17(id, cola, colb) values (1, 7, 1), (2, 9, 1)",
"insert into fk_multicol_t19(id, cola, colb) values (1, 7, 1)",
},
dmlQuery: "update fk_multicol_t15 m1 join fk_multicol_t17 on m1.id = fk_multicol_t17.id set m1.cola = m1.id + 8, fk_multicol_t17.colb = 32 where m1.id < 3",
assertionQueries: []string{
"select * from fk_multicol_t15 order by id",
"select * from fk_multicol_t16 order by id",
"select * from fk_multicol_t17 order by id",
"select * from fk_multicol_t19 order by id",
},
},
}

Expand Down
50 changes: 21 additions & 29 deletions go/vt/vtgate/planbuilder/operators/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,28 +103,28 @@ func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlpars
childFks := ctx.SemTable.GetChildForeignKeysForTargets()

// We check if dml with input plan is required. DML with input planning is generally
// slower, because it does a selection and then creates a update statement wherein we have to
// slower, because it does a selection and then creates an update statement wherein we have to
// list all the primary key values.
if updateWithInputPlanningRequired(ctx, childFks, parentFks, updStmt) {
return createUpdateWithInputOp(ctx, updStmt)
}

var updClone *sqlparser.Update
var vTbl *vindexes.Table

op, vTbl, updClone = createUpdateOperator(ctx, updStmt)
var targetTbl TargetTable
op, targetTbl, updClone = createUpdateOperator(ctx, updStmt)

op = &LockAndComment{
Source: op,
Comments: updStmt.Comments,
Lock: sqlparser.ShareModeLock,
}

parentFks = ctx.SemTable.GetParentForeignKeysForTableSet(targetTbl.ID)
childFks = ctx.SemTable.GetChildForeignKeysForTableSet(targetTbl.ID)
if len(childFks) == 0 && len(parentFks) == 0 {
return op
}

return buildFkOperator(ctx, op, updClone, parentFks, childFks, vTbl)
return buildFkOperator(ctx, op, updClone, parentFks, childFks, targetTbl)
}

func updateWithInputPlanningRequired(
Expand All @@ -133,7 +133,7 @@ func updateWithInputPlanningRequired(
parentFks []vindexes.ParentFKInfo,
updateStmt *sqlparser.Update,
) bool {
if isMultiTargetUpdate(ctx, childFks, parentFks, updateStmt) {
if isMultiTargetUpdate(ctx, updateStmt) {
return true
}
// If there are no foreign keys, we don't need to use delete with input.
Expand All @@ -147,20 +147,12 @@ func updateWithInputPlanningRequired(
return false
}

func isMultiTargetUpdate(ctx *plancontext.PlanningContext, childFks []vindexes.ChildFKInfo, parentFks []vindexes.ParentFKInfo, updateStmt *sqlparser.Update) bool {
func isMultiTargetUpdate(ctx *plancontext.PlanningContext, updateStmt *sqlparser.Update) bool {
var targetTS semantics.TableSet
for _, ue := range updateStmt.Exprs {
targetTS = targetTS.Merge(ctx.SemTable.DirectDeps(ue.Name))
}
if targetTS.NumberOfTables() == 1 {
return false
}

if len(childFks) > 0 || len(parentFks) > 0 {
panic(vterrors.VT12001("multi table update with foreign keys"))
}

return true
return targetTS.NumberOfTables() > 1
}

func createUpdateWithInputOp(ctx *plancontext.PlanningContext, upd *sqlparser.Update) (op Operator) {
Expand Down Expand Up @@ -282,7 +274,7 @@ func errIfUpdateNotSupported(ctx *plancontext.PlanningContext, stmt *sqlparser.U
}
}

func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (Operator, *vindexes.Table, *sqlparser.Update) {
func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (Operator, TargetTable, *sqlparser.Update) {
op := crossJoin(ctx, updStmt.TableExprs)

sqc := &SubQueryBuilder{}
Expand Down Expand Up @@ -364,7 +356,7 @@ func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.U
}
}

return sqc.getRootOperator(updOp, nil), vTbl, updClone
return sqc.getRootOperator(updOp, nil), targetTbl, updClone
}

func getUpdateVindexInformation(
Expand All @@ -382,14 +374,14 @@ func getUpdateVindexInformation(
return changedVindexValues, ownedVindexQuery, subQueriesArgOnChangedVindex
}

func buildFkOperator(ctx *plancontext.PlanningContext, updOp Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) Operator {
func buildFkOperator(ctx *plancontext.PlanningContext, updOp Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, targetTbl TargetTable) Operator {
// If there is a subquery container above update operator, we want to do the foreign key planning inside it,
// because we want the Inner of the subquery to execute first and its result be used for the entire foreign key update planning.
foundSubqc := false
TopDown(updOp, TableID, func(in Operator, _ semantics.TableSet, _ bool) (Operator, *ApplyResult) {
if op, isSubqc := in.(*SubQueryContainer); isSubqc {
foundSubqc = true
op.Outer = buildFkOperator(ctx, op.Outer, updClone, parentFks, childFks, updatedTable)
op.Outer = buildFkOperator(ctx, op.Outer, updClone, parentFks, childFks, targetTbl)
}
return in, NoRewrite
}, stopAtUpdateOp)
Expand All @@ -399,9 +391,9 @@ func buildFkOperator(ctx *plancontext.PlanningContext, updOp Operator, updClone

restrictChildFks, cascadeChildFks := splitChildFks(childFks)

op := createFKCascadeOp(ctx, updOp, updClone, cascadeChildFks, updatedTable)
op := createFKCascadeOp(ctx, updOp, updClone, cascadeChildFks, targetTbl)

return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks, updatedTable)
return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks, targetTbl.VTable)
}

func stopAtUpdateOp(operator Operator) VisitRule {
Expand All @@ -428,7 +420,7 @@ func splitChildFks(fks []vindexes.ChildFKInfo) (restrictChildFks, cascadeChildFk
return
}

func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp Operator, updStmt *sqlparser.Update, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) Operator {
func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp Operator, updStmt *sqlparser.Update, childFks []vindexes.ChildFKInfo, targetTbl TargetTable) Operator {
if len(childFks) == 0 {
return parentOp
}
Expand All @@ -444,29 +436,29 @@ func createFKCascadeOp(ctx *plancontext.PlanningContext, parentOp Operator, updS

// We need to select all the parent columns for the foreign key constraint, to use in the update of the child table.
var selectOffsets []int
selectOffsets, selectExprs = addColumns(ctx, fk.ParentColumns, selectExprs, updatedTable.GetTableName())
selectOffsets, selectExprs = addColumns(ctx, fk.ParentColumns, selectExprs, targetTbl.Name)

// If we are updating a foreign key column to a non-literal value then, need information about
// 1. whether the new value is different from the old value
// 2. the new value itself.
// 3. the bind variable to assign to this value.
var nonLiteralUpdateInfo []engine.NonLiteralUpdateInfo
ue := ctx.SemTable.GetUpdateExpressionsForFk(fk.String(updatedTable))
ue := ctx.SemTable.GetUpdateExpressionsForFk(fk.String(targetTbl.VTable))
// We only need to store these offsets and add these expressions to SELECT when there are non-literal updates present.
if hasNonLiteralUpdate(ue) {
for _, updExpr := range ue {
// We add the expression and a comparison expression to the SELECT exprssion while storing their offsets.
var info engine.NonLiteralUpdateInfo
info, selectExprs = addNonLiteralUpdExprToSelect(ctx, updatedTable, updExpr, selectExprs)
info, selectExprs = addNonLiteralUpdExprToSelect(ctx, targetTbl.VTable, updExpr, selectExprs)
nonLiteralUpdateInfo = append(nonLiteralUpdateInfo, info)
}
}

fkChild := createFkChildForUpdate(ctx, fk, selectOffsets, nonLiteralUpdateInfo, updatedTable)
fkChild := createFkChildForUpdate(ctx, fk, selectOffsets, nonLiteralUpdateInfo, targetTbl.VTable)
fkChildren = append(fkChildren, fkChild)
}

selectionOp := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, updStmt.OrderBy, nil, getUpdateLock(updatedTable))
selectionOp := createSelectionOp(ctx, selectExprs, updStmt.TableExprs, updStmt.Where, updStmt.OrderBy, nil, getUpdateLock(targetTbl.VTable))

return &FkCascade{
Selection: selectionOp,
Expand Down
Loading

0 comments on commit 943b07c

Please sign in to comment.