Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreign Key: Add support for Multi Table and Multi Target Update Statement #15523

Merged
merged 3 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an was just fine? Later in the PR you actually turn a to an 😅

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
Loading