diff --git a/go/test/endtoend/vtgate/queries/dml/dml_test.go b/go/test/endtoend/vtgate/queries/dml/dml_test.go index 9d060e99881..f23320a3fe9 100644 --- a/go/test/endtoend/vtgate/queries/dml/dml_test.go +++ b/go/test/endtoend/vtgate/queries/dml/dml_test.go @@ -140,3 +140,67 @@ func TestDeleteWithLimit(t *testing.T) { require.EqualValues(t, 0, qr.RowsAffected) } + +// TestUpdateWithLimit executed update queries with limit +func TestUpdateWithLimit(t *testing.T) { + utils.SkipIfBinaryIsBelowVersion(t, 20, "vtgate") + + mcmp, closer := start(t) + defer closer() + + // initial rows + mcmp.Exec("insert into s_tbl(id, num) values (1,10), (2,10), (3,10), (4,20), (5,5), (6,15), (7,17), (8,80)") + mcmp.Exec("insert into order_tbl(region_id, oid, cust_no) values (1,1,4), (1,2,2), (2,3,5), (2,4,55)") + + // check rows + mcmp.AssertMatches(`select id, num from s_tbl order by id`, + `[[INT64(1) INT64(10)] [INT64(2) INT64(10)] [INT64(3) INT64(10)] [INT64(4) INT64(20)] [INT64(5) INT64(5)] [INT64(6) INT64(15)] [INT64(7) INT64(17)] [INT64(8) INT64(80)]]`) + mcmp.AssertMatches(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[INT64(1) INT64(1) INT64(4)] [INT64(1) INT64(2) INT64(2)] [INT64(2) INT64(3) INT64(5)] [INT64(2) INT64(4) INT64(55)]]`) + + // update with limit + qr := mcmp.Exec(`update s_tbl set num = 12 order by num, id limit 3`) + require.EqualValues(t, 3, qr.RowsAffected) + + qr = mcmp.Exec(`update order_tbl set cust_no = 12 where region_id = 1 limit 1`) + require.EqualValues(t, 1, qr.RowsAffected) + + // check rows + mcmp.AssertMatches(`select id, num from s_tbl order by id`, + `[[INT64(1) INT64(12)] [INT64(2) INT64(12)] [INT64(3) INT64(10)] [INT64(4) INT64(20)] [INT64(5) INT64(12)] [INT64(6) INT64(15)] [INT64(7) INT64(17)] [INT64(8) INT64(80)]]`) + // 2 rows matches but limit is 1, so any one of the row can be modified in the table. + mcmp.AssertMatchesAnyNoCompare(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[INT64(1) INT64(1) INT64(12)] [INT64(1) INT64(2) INT64(2)] [INT64(2) INT64(3) INT64(5)] [INT64(2) INT64(4) INT64(55)]]`, + `[[INT64(1) INT64(1) INT64(4)] [INT64(1) INT64(2) INT64(12)] [INT64(2) INT64(3) INT64(5)] [INT64(2) INT64(4) INT64(55)]]`) + + // update with limit + qr = mcmp.Exec(`update s_tbl set num = 32 where num > 17 limit 1`) + require.EqualValues(t, 1, qr.RowsAffected) + + qr = mcmp.Exec(`update order_tbl set cust_no = cust_no + 10 limit 5`) + require.EqualValues(t, 4, qr.RowsAffected) + + // check rows + // 2 rows matches `num > 17` but limit is 1 so any one of them will be updated. + mcmp.AssertMatchesAnyNoCompare(`select id, num from s_tbl order by id`, + `[[INT64(1) INT64(12)] [INT64(2) INT64(12)] [INT64(3) INT64(10)] [INT64(4) INT64(32)] [INT64(5) INT64(12)] [INT64(6) INT64(15)] [INT64(7) INT64(17)] [INT64(8) INT64(80)]]`, + `[[INT64(1) INT64(12)] [INT64(2) INT64(12)] [INT64(3) INT64(10)] [INT64(4) INT64(20)] [INT64(5) INT64(12)] [INT64(6) INT64(15)] [INT64(7) INT64(17)] [INT64(8) INT64(32)]]`) + mcmp.AssertMatchesAnyNoCompare(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[INT64(1) INT64(1) INT64(22)] [INT64(1) INT64(2) INT64(12)] [INT64(2) INT64(3) INT64(15)] [INT64(2) INT64(4) INT64(65)]]`, + `[[INT64(1) INT64(1) INT64(14)] [INT64(1) INT64(2) INT64(22)] [INT64(2) INT64(3) INT64(15)] [INT64(2) INT64(4) INT64(65)]]`) + + // trying with zero limit. + qr = mcmp.Exec(`update s_tbl set num = 44 limit 0`) + require.EqualValues(t, 0, qr.RowsAffected) + + qr = mcmp.Exec(`update order_tbl set oid = 44 limit 0`) + require.EqualValues(t, 0, qr.RowsAffected) + + // trying with limit with no-matching row. + qr = mcmp.Exec(`update s_tbl set num = 44 where id > 100 limit 2`) + require.EqualValues(t, 0, qr.RowsAffected) + + qr = mcmp.Exec(`update order_tbl set oid = 44 where region_id > 100 limit 2`) + require.EqualValues(t, 0, qr.RowsAffected) + +} diff --git a/go/vt/vterrors/code.go b/go/vt/vterrors/code.go index 48529abe515..eb2b1c4be6d 100644 --- a/go/vt/vterrors/code.go +++ b/go/vt/vterrors/code.go @@ -93,7 +93,6 @@ var ( VT09021 = errorWithState("VT09021", vtrpcpb.Code_FAILED_PRECONDITION, KeyDoesNotExist, "Vindex '%s' does not exist in table '%s'", "Vindex hints have to reference an existing vindex, and no such vindex could be found for the given table.") VT10001 = errorWithoutState("VT10001", vtrpcpb.Code_ABORTED, "foreign key constraints are not allowed", "Foreign key constraints are not allowed, see https://vitess.io/blog/2021-06-15-online-ddl-why-no-fk/.") - VT10002 = errorWithoutState("VT10002", vtrpcpb.Code_ABORTED, "'replace into' with foreign key constraints are not allowed", "Foreign key constraints sometimes are not written in binary logs and will cause issue with vreplication workflows like online-ddl.") VT12001 = errorWithoutState("VT12001", vtrpcpb.Code_UNIMPLEMENTED, "unsupported: %s", "This statement is unsupported by Vitess. Please rewrite your query to use supported syntax.") VT12002 = errorWithoutState("VT12002", vtrpcpb.Code_UNIMPLEMENTED, "unsupported: cross-shard foreign keys", "Vitess does not support cross shard foreign keys.") @@ -171,7 +170,6 @@ var ( VT09018, VT09019, VT10001, - VT10002, VT12001, VT12002, VT13001, diff --git a/go/vt/vtgate/engine/cached_size.go b/go/vt/vtgate/engine/cached_size.go index 2221830c8b4..781c5904044 100644 --- a/go/vt/vtgate/engine/cached_size.go +++ b/go/vt/vtgate/engine/cached_size.go @@ -177,19 +177,7 @@ func (cached *DML) CachedSize(alloc bool) int64 { size += cached.RoutingParameters.CachedSize(true) return size } -func (cached *Delete) CachedSize(alloc bool) int64 { - if cached == nil { - return int64(0) - } - size := int64(0) - if alloc { - size += int64(16) - } - // field DML *vitess.io/vitess/go/vt/vtgate/engine.DML - size += cached.DML.CachedSize(true) - return size -} -func (cached *DeleteWithInput) CachedSize(alloc bool) int64 { +func (cached *DMLWithInput) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) } @@ -197,8 +185,8 @@ func (cached *DeleteWithInput) CachedSize(alloc bool) int64 { if alloc { size += int64(64) } - // field Delete vitess.io/vitess/go/vt/vtgate/engine.Primitive - if cc, ok := cached.Delete.(cachedObject); ok { + // field DML vitess.io/vitess/go/vt/vtgate/engine.Primitive + if cc, ok := cached.DML.(cachedObject); ok { size += cc.CachedSize(true) } // field Input vitess.io/vitess/go/vt/vtgate/engine.Primitive @@ -211,6 +199,18 @@ func (cached *DeleteWithInput) CachedSize(alloc bool) int64 { } return size } +func (cached *Delete) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(16) + } + // field DML *vitess.io/vitess/go/vt/vtgate/engine.DML + size += cached.DML.CachedSize(true) + return size +} func (cached *Distinct) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) diff --git a/go/vt/vtgate/engine/delete_with_input.go b/go/vt/vtgate/engine/dml_with_input.go similarity index 55% rename from go/vt/vtgate/engine/delete_with_input.go rename to go/vt/vtgate/engine/dml_with_input.go index 5e636869cfc..87d7c1d9826 100644 --- a/go/vt/vtgate/engine/delete_with_input.go +++ b/go/vt/vtgate/engine/dml_with_input.go @@ -26,39 +26,39 @@ import ( querypb "vitess.io/vitess/go/vt/proto/query" ) -var _ Primitive = (*DeleteWithInput)(nil) +var _ Primitive = (*DMLWithInput)(nil) -const DmVals = "dm_vals" +const DmlVals = "dml_vals" -// DeleteWithInput represents the instructions to perform a delete operation based on the input result. -type DeleteWithInput struct { +// DMLWithInput represents the instructions to perform a DML operation based on the input result. +type DMLWithInput struct { txNeeded - Delete Primitive - Input Primitive + DML Primitive + Input Primitive OutputCols []int } -func (del *DeleteWithInput) RouteType() string { - return "DeleteWithInput" +func (dml *DMLWithInput) RouteType() string { + return "DMLWithInput" } -func (del *DeleteWithInput) GetKeyspaceName() string { - return del.Input.GetKeyspaceName() +func (dml *DMLWithInput) GetKeyspaceName() string { + return dml.Input.GetKeyspaceName() } -func (del *DeleteWithInput) GetTableName() string { - return del.Input.GetTableName() +func (dml *DMLWithInput) GetTableName() string { + return dml.Input.GetTableName() } -func (del *DeleteWithInput) Inputs() ([]Primitive, []map[string]any) { - return []Primitive{del.Input, del.Delete}, nil +func (dml *DMLWithInput) Inputs() ([]Primitive, []map[string]any) { + return []Primitive{dml.Input, dml.DML}, nil } // TryExecute performs a non-streaming exec. -func (del *DeleteWithInput) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, _ bool) (*sqltypes.Result, error) { - inputRes, err := vcursor.ExecutePrimitive(ctx, del.Input, bindVars, false) +func (dml *DMLWithInput) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, _ bool) (*sqltypes.Result, error) { + inputRes, err := vcursor.ExecutePrimitive(ctx, dml.Input, bindVars, false) if err != nil { return nil, err } @@ -67,14 +67,14 @@ func (del *DeleteWithInput) TryExecute(ctx context.Context, vcursor VCursor, bin } var bv *querypb.BindVariable - if len(del.OutputCols) == 1 { - bv = getBVSingle(inputRes, del.OutputCols[0]) + if len(dml.OutputCols) == 1 { + bv = getBVSingle(inputRes, dml.OutputCols[0]) } else { - bv = getBVMulti(inputRes, del.OutputCols) + bv = getBVMulti(inputRes, dml.OutputCols) } - bindVars[DmVals] = bv - return vcursor.ExecutePrimitive(ctx, del.Delete, bindVars, false) + bindVars[DmlVals] = bv + return vcursor.ExecutePrimitive(ctx, dml.DML, bindVars, false) } func getBVSingle(res *sqltypes.Result, offset int) *querypb.BindVariable { @@ -99,8 +99,8 @@ func getBVMulti(res *sqltypes.Result, offsets []int) *querypb.BindVariable { } // TryStreamExecute performs a streaming exec. -func (del *DeleteWithInput) TryStreamExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error { - res, err := del.TryExecute(ctx, vcursor, bindVars, wantfields) +func (dml *DMLWithInput) TryStreamExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error { + res, err := dml.TryExecute(ctx, vcursor, bindVars, wantfields) if err != nil { return err } @@ -108,16 +108,16 @@ func (del *DeleteWithInput) TryStreamExecute(ctx context.Context, vcursor VCurso } // GetFields fetches the field info. -func (del *DeleteWithInput) GetFields(context.Context, VCursor, map[string]*querypb.BindVariable) (*sqltypes.Result, error) { - return nil, vterrors.VT13001("unreachable code for MULTI DELETE") +func (dml *DMLWithInput) GetFields(context.Context, VCursor, map[string]*querypb.BindVariable) (*sqltypes.Result, error) { + return nil, vterrors.VT13001("unreachable code for DMLs") } -func (del *DeleteWithInput) description() PrimitiveDescription { +func (dml *DMLWithInput) description() PrimitiveDescription { other := map[string]any{ - "Offset": del.OutputCols, + "Offset": dml.OutputCols, } return PrimitiveDescription{ - OperatorType: "DeleteWithInput", + OperatorType: "DMLWithInput", TargetTabletType: topodatapb.TabletType_PRIMARY, Other: other, } diff --git a/go/vt/vtgate/engine/delete_with_input_test.go b/go/vt/vtgate/engine/dml_with_input_test.go similarity index 65% rename from go/vt/vtgate/engine/delete_with_input_test.go rename to go/vt/vtgate/engine/dml_with_input_test.go index b87e2bbc74a..fb75ae70f1d 100644 --- a/go/vt/vtgate/engine/delete_with_input_test.go +++ b/go/vt/vtgate/engine/dml_with_input_test.go @@ -32,9 +32,9 @@ func TestDeleteWithInputSingleOffset(t *testing.T) { sqltypes.MakeTestResult(sqltypes.MakeTestFields("id", "int64"), "1", "2", "3"), }} - del := &DeleteWithInput{ + del := &DMLWithInput{ Input: input, - Delete: &Delete{ + DML: &Delete{ DML: &DML{ RoutingParameters: &RoutingParameters{ Opcode: Scatter, @@ -55,8 +55,8 @@ func TestDeleteWithInputSingleOffset(t *testing.T) { vc.ExpectLog(t, []string{ `ResolveDestinations ks [] Destinations:DestinationAllShards()`, `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} true false`, + `ks.-20: dummy_delete {dml_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} ` + + `ks.20-: dummy_delete {dml_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} true false`, }) vc.Rewind() @@ -66,8 +66,8 @@ func TestDeleteWithInputSingleOffset(t *testing.T) { vc.ExpectLog(t, []string{ `ResolveDestinations ks [] Destinations:DestinationAllShards()`, `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} true false`, + `ks.-20: dummy_delete {dml_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} ` + + `ks.20-: dummy_delete {dml_vals: type:TUPLE values:{type:INT64 value:"1"} values:{type:INT64 value:"2"} values:{type:INT64 value:"3"}} true false`, }) } @@ -76,9 +76,9 @@ func TestDeleteWithInputMultiOffset(t *testing.T) { sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col", "int64|varchar"), "1|a", "2|b", "3|c"), }} - del := &DeleteWithInput{ + del := &DMLWithInput{ Input: input, - Delete: &Delete{ + DML: &Delete{ DML: &DML{ RoutingParameters: &RoutingParameters{ Opcode: Scatter, @@ -99,8 +99,8 @@ func TestDeleteWithInputMultiOffset(t *testing.T) { vc.ExpectLog(t, []string{ `ResolveDestinations ks [] Destinations:DestinationAllShards()`, `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} true false`, + `ks.-20: dummy_delete {dml_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} ` + + `ks.20-: dummy_delete {dml_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} true false`, }) vc.Rewind() @@ -110,7 +110,7 @@ func TestDeleteWithInputMultiOffset(t *testing.T) { vc.ExpectLog(t, []string{ `ResolveDestinations ks [] Destinations:DestinationAllShards()`, `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} true false`, + `ks.-20: dummy_delete {dml_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} ` + + `ks.20-: dummy_delete {dml_vals: type:TUPLE values:{type:TUPLE value:"\x950\x01a\x89\x02\x011"} values:{type:TUPLE value:"\x950\x01b\x89\x02\x012"} values:{type:TUPLE value:"\x950\x01c\x89\x02\x013"}} true false`, }) } diff --git a/go/vt/vtgate/executor_dml_test.go b/go/vt/vtgate/executor_dml_test.go index 91089c61e67..d0efcfe765a 100644 --- a/go/vt/vtgate/executor_dml_test.go +++ b/go/vt/vtgate/executor_dml_test.go @@ -3041,14 +3041,14 @@ func TestDeleteMultiTable(t *testing.T) { wantQueries := []*querypb.BoundQuery{ {Sql: "select `user`.id, `user`.col from `user`", BindVariables: map[string]*querypb.BindVariable{}}, bq, bq, bq, bq, bq, bq, bq, bq, - {Sql: "select `user`.Id, `user`.`name` from `user` where `user`.id in ::dm_vals for update", BindVariables: map[string]*querypb.BindVariable{"dm_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, - {Sql: "delete from `user` where `user`.id in ::dm_vals", BindVariables: map[string]*querypb.BindVariable{"dm_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}} + {Sql: "select `user`.Id, `user`.`name` from `user` where `user`.id in ::dml_vals for update", BindVariables: map[string]*querypb.BindVariable{"dml_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, + {Sql: "delete from `user` where `user`.id in ::dml_vals", BindVariables: map[string]*querypb.BindVariable{"dml_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}} assertQueries(t, sbc1, wantQueries) wantQueries = []*querypb.BoundQuery{ {Sql: "select `user`.id, `user`.col from `user`", BindVariables: map[string]*querypb.BindVariable{}}, - {Sql: "select `user`.Id, `user`.`name` from `user` where `user`.id in ::dm_vals for update", BindVariables: map[string]*querypb.BindVariable{"dm_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, - {Sql: "delete from `user` where `user`.id in ::dm_vals", BindVariables: map[string]*querypb.BindVariable{"dm_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, + {Sql: "select `user`.Id, `user`.`name` from `user` where `user`.id in ::dml_vals for update", BindVariables: map[string]*querypb.BindVariable{"dml_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, + {Sql: "delete from `user` where `user`.id in ::dml_vals", BindVariables: map[string]*querypb.BindVariable{"dml_vals": {Type: querypb.Type_TUPLE, Values: dmlVals}}}, } assertQueries(t, sbc2, wantQueries) @@ -3067,7 +3067,7 @@ func TestDeleteMultiTable(t *testing.T) { testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) // select `user`.id, `user`.col from `user` - 8 shard // select 1 from music where music.user_id = 1 and music.col = :user_col - 8 shards - // select Id, `name` from `user` where (`user`.id) in ::dm_vals for update - 1 shard - // delete from `user` where (`user`.id) in ::dm_vals - 1 shard + // select Id, `name` from `user` where (`user`.id) in ::dml_vals for update - 1 shard + // delete from `user` where (`user`.id) in ::dml_vals - 1 shard testQueryLog(t, executor, logChan, "TestExecute", "DELETE", "delete `user` from `user` join music on `user`.col = music.col where music.user_id = 1", 18) } diff --git a/go/vt/vtgate/planbuilder/delete_with_input.go b/go/vt/vtgate/planbuilder/dml_with_input.go similarity index 75% rename from go/vt/vtgate/planbuilder/delete_with_input.go rename to go/vt/vtgate/planbuilder/dml_with_input.go index 9b6ebb08d3b..1b6d1c0835a 100644 --- a/go/vt/vtgate/planbuilder/delete_with_input.go +++ b/go/vt/vtgate/planbuilder/dml_with_input.go @@ -20,21 +20,21 @@ import ( "vitess.io/vitess/go/vt/vtgate/engine" ) -type deleteWithInput struct { - input logicalPlan - delete logicalPlan +type dmlWithInput struct { + input logicalPlan + dml logicalPlan outputCols []int } -var _ logicalPlan = (*deleteWithInput)(nil) +var _ logicalPlan = (*dmlWithInput)(nil) // Primitive implements the logicalPlan interface -func (d *deleteWithInput) Primitive() engine.Primitive { +func (d *dmlWithInput) Primitive() engine.Primitive { inp := d.input.Primitive() - del := d.delete.Primitive() - return &engine.DeleteWithInput{ - Delete: del, + del := d.dml.Primitive() + return &engine.DMLWithInput{ + DML: del, Input: inp, OutputCols: d.outputCols, } diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go index cadfba91772..674849ffc61 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -74,26 +74,26 @@ func transformToLogicalPlan(ctx *plancontext.PlanningContext, op operators.Opera return transformHashJoin(ctx, op) case *operators.Sequential: return transformSequential(ctx, op) - case *operators.DeleteWithInput: - return transformDeleteWithInput(ctx, op) + case *operators.DMLWithInput: + return transformDMLWithInput(ctx, op) } return nil, vterrors.VT13001(fmt.Sprintf("unknown type encountered: %T (transformToLogicalPlan)", op)) } -func transformDeleteWithInput(ctx *plancontext.PlanningContext, op *operators.DeleteWithInput) (logicalPlan, error) { +func transformDMLWithInput(ctx *plancontext.PlanningContext, op *operators.DMLWithInput) (logicalPlan, error) { input, err := transformToLogicalPlan(ctx, op.Source) if err != nil { return nil, err } - del, err := transformToLogicalPlan(ctx, op.Delete) + del, err := transformToLogicalPlan(ctx, op.DML) if err != nil { return nil, err } - return &deleteWithInput{ + return &dmlWithInput{ input: input, - delete: del, + dml: del, outputCols: op.Offsets, }, nil } @@ -685,66 +685,61 @@ func buildUpdateLogicalPlan( hints *queryHints, ) (logicalPlan, error) { upd := dmlOp.(*operators.Update) - rp := newRoutingParams(ctx, rb.Routing.OpCode()) - rb.Routing.UpdateRoutingParams(ctx, rp) - edml := &engine.DML{ - Query: generateQuery(stmt), - TableNames: []string{upd.VTable.Name.String()}, - Vindexes: upd.VTable.ColumnVindexes, - OwnedVindexQuery: upd.OwnedVindexQuery, - RoutingParameters: rp, + var vindexes []*vindexes.ColumnVindex + vQuery := "" + if len(upd.ChangedVindexValues) > 0 { + vQuery = sqlparser.String(upd.OwnedVindexQuery) + vindexes = upd.Target.VTable.ColumnVindexes + if upd.OwnedVindexQuery.Limit != nil && len(upd.OwnedVindexQuery.OrderBy) == 0 { + return nil, vterrors.VT12001("Vindex update should have ORDER BY clause when using LIMIT") + } } - transformDMLPlan(upd.VTable, edml, rb.Routing, len(upd.ChangedVindexValues) > 0) + edml := createDMLPrimitive(ctx, rb, hints, upd.Target.VTable, generateQuery(stmt), vindexes, vQuery) - e := &engine.Update{ - ChangedVindexValues: upd.ChangedVindexValues, + return &primitiveWrapper{prim: &engine.Update{ DML: edml, - } - if hints != nil { - e.MultiShardAutocommit = hints.multiShardAutocommit - e.QueryTimeout = hints.queryTimeout - } - - return &primitiveWrapper{prim: e}, nil + ChangedVindexValues: upd.ChangedVindexValues, + }}, nil } func buildDeleteLogicalPlan(ctx *plancontext.PlanningContext, rb *operators.Route, dmlOp operators.Operator, stmt *sqlparser.Delete, hints *queryHints) (logicalPlan, error) { del := dmlOp.(*operators.Delete) + + var vindexes []*vindexes.ColumnVindex + vQuery := "" + if del.OwnedVindexQuery != nil { + vQuery = sqlparser.String(del.OwnedVindexQuery) + vindexes = del.Target.VTable.Owned + } + + edml := createDMLPrimitive(ctx, rb, hints, del.Target.VTable, generateQuery(stmt), vindexes, vQuery) + + return &primitiveWrapper{prim: &engine.Delete{DML: edml}}, nil +} + +func createDMLPrimitive(ctx *plancontext.PlanningContext, rb *operators.Route, hints *queryHints, vTbl *vindexes.Table, query string, colVindexes []*vindexes.ColumnVindex, vindexQuery string) *engine.DML { rp := newRoutingParams(ctx, rb.Routing.OpCode()) rb.Routing.UpdateRoutingParams(ctx, rp) - vtable := del.Target.VTable edml := &engine.DML{ - Query: generateQuery(stmt), - TableNames: []string{vtable.Name.String()}, - Vindexes: vtable.Owned, + Query: query, + TableNames: []string{vTbl.Name.String()}, + Vindexes: colVindexes, + OwnedVindexQuery: vindexQuery, RoutingParameters: rp, } - hasLookupVindex := del.OwnedVindexQuery != nil - if hasLookupVindex { - edml.OwnedVindexQuery = sqlparser.String(del.OwnedVindexQuery) + if rb.Routing.OpCode() != engine.Unsharded && vindexQuery != "" { + primary := vTbl.ColumnVindexes[0] + edml.KsidVindex = primary.Vindex + edml.KsidLength = len(primary.Columns) } - transformDMLPlan(vtable, edml, rb.Routing, hasLookupVindex) - - e := &engine.Delete{ - DML: edml, - } if hints != nil { - e.MultiShardAutocommit = hints.multiShardAutocommit - e.QueryTimeout = hints.queryTimeout - } - - return &primitiveWrapper{prim: e}, nil -} - -func transformDMLPlan(vtable *vindexes.Table, edml *engine.DML, routing operators.Routing, setVindex bool) { - if routing.OpCode() != engine.Unsharded && setVindex { - primary := vtable.ColumnVindexes[0] - edml.KsidVindex = primary.Vindex - edml.KsidLength = len(primary.Columns) + edml.MultiShardAutocommit = hints.multiShardAutocommit + edml.QueryTimeout = hints.queryTimeout } + return edml } func updateSelectedVindexPredicate(op *operators.Route) sqlparser.Expr { diff --git a/go/vt/vtgate/planbuilder/operators/SQL_builder.go b/go/vt/vtgate/planbuilder/operators/SQL_builder.go index 998f849ba3c..aeb60cc8d01 100644 --- a/go/vt/vtgate/planbuilder/operators/SQL_builder.go +++ b/go/vt/vtgate/planbuilder/operators/SQL_builder.go @@ -398,7 +398,7 @@ func buildDelete(op *Delete, qb *queryBuilder) { qb.dmlOperator = op qb.stmt = &sqlparser.Delete{ - Ignore: sqlparser.Ignore(op.Ignore), + Ignore: op.Ignore, Targets: sqlparser.TableNames{op.Target.Name}, TableExprs: sel.From, Where: sel.Where, @@ -408,11 +408,6 @@ func buildDelete(op *Delete, qb *queryBuilder) { } func buildUpdate(op *Update, qb *queryBuilder) { - tblName := sqlparser.NewTableName(op.QTable.Table.Name.String()) - aTblExpr := &sqlparser.AliasedTableExpr{ - Expr: tblName, - As: op.QTable.Alias.As, - } updExprs := make(sqlparser.UpdateExprs, 0, len(op.Assignments)) for _, se := range op.Assignments { updExprs = append(updExprs, &sqlparser.UpdateExpr{ @@ -421,19 +416,25 @@ func buildUpdate(op *Update, qb *queryBuilder) { }) } + buildQuery(op.Source, qb) + // currently the qb builds a select query underneath. + // Will take the `From` and `Where` from this select + // and create a update statement. + // TODO: change it to directly produce `update` statement. + sel, ok := qb.stmt.(*sqlparser.Select) + if !ok { + panic(vterrors.VT13001("expected a select here")) + } + + qb.dmlOperator = op qb.stmt = &sqlparser.Update{ Ignore: op.Ignore, - TableExprs: sqlparser.TableExprs{aTblExpr}, + TableExprs: sel.From, Exprs: updExprs, - OrderBy: op.OrderBy, - Limit: op.Limit, - } - - for _, pred := range op.QTable.Predicates { - qb.addPredicate(pred) + Where: sel.Where, + Limit: sel.Limit, + OrderBy: sel.OrderBy, } - - qb.dmlOperator = op } type OpWithAST interface { diff --git a/go/vt/vtgate/planbuilder/operators/ast_to_op.go b/go/vt/vtgate/planbuilder/operators/ast_to_op.go index 8a46109e959..14d4de1bf51 100644 --- a/go/vt/vtgate/planbuilder/operators/ast_to_op.go +++ b/go/vt/vtgate/planbuilder/operators/ast_to_op.go @@ -69,6 +69,11 @@ func createOperatorFromSelect(ctx *plancontext.PlanningContext, sel *sqlparser.S func addWherePredicates(ctx *plancontext.PlanningContext, expr sqlparser.Expr, op Operator) Operator { sqc := &SubQueryBuilder{} + op = addWherePredsToSubQueryBuilder(ctx, expr, op, sqc) + return sqc.getRootOperator(op, nil) +} + +func addWherePredsToSubQueryBuilder(ctx *plancontext.PlanningContext, expr sqlparser.Expr, op Operator, sqc *SubQueryBuilder) Operator { outerID := TableID(op) exprs := sqlparser.SplitAndExpression(nil, expr) for _, expr := range exprs { @@ -80,7 +85,7 @@ func addWherePredicates(ctx *plancontext.PlanningContext, expr sqlparser.Expr, o op = op.AddPredicate(ctx, expr) addColumnEquality(ctx, expr) } - return sqc.getRootOperator(op, nil) + return op } // cloneASTAndSemState clones the AST and the semantic state of the input node. diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index 82ab6fe09ca..22b8f1e4281 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -28,10 +28,7 @@ import ( ) type Delete struct { - Target TargetTable - OwnedVindexQuery *sqlparser.Select - Ignore bool - Source Operator + *DMLCommon noColumns noPredicates @@ -75,13 +72,17 @@ func (d *Delete) GetOrdering(*plancontext.PlanningContext) []OrderBy { } func (d *Delete) ShortDescription() string { - return fmt.Sprintf("%s.%s", d.Target.VTable.Keyspace.Name, d.Target.VTable.Name.String()) + ovq := "" + if d.OwnedVindexQuery != nil { + ovq = " vindexQuery:%s" + sqlparser.String(d.OwnedVindexQuery) + } + return fmt.Sprintf("%s.%s%s", d.Target.VTable.Keyspace.Name, d.Target.VTable.Name.String(), ovq) } func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete) (op Operator) { childFks := ctx.SemTable.GetChildForeignKeysForTable(deleteStmt.Targets[0]) - // If the delete statement has a limit and has foreign keys, we will use a DeleteWithInput + // If the delete statement has a limit and has foreign keys, we will use a DMLWithInput // operator wherein we do a selection first and use that output for the subsequent deletes. if len(childFks) > 0 && deleteStmt.Limit != nil { return deletePlanningForLimitFk(ctx, deleteStmt) @@ -139,11 +140,11 @@ func deletePlanningForLimitFk(ctx *plancontext.PlanningContext, del *sqlparser.D if len(leftComp) == 1 { lhs = leftComp[0] } - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, lhs, sqlparser.ListArg(engine.DmVals), nil) + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, lhs, sqlparser.ListArg(engine.DmlVals), nil) del.Where = sqlparser.NewWhere(sqlparser.WhereClause, compExpr) - return &DeleteWithInput{ - Delete: createOperatorFromDelete(ctx, del), + return &DMLWithInput{ + DML: createOperatorFromDelete(ctx, del), Source: createOperatorFromSelect(ctx, selectStmt), cols: cols, } @@ -193,17 +194,19 @@ func createDeleteOperator(ctx *plancontext.PlanningContext, del *sqlparser.Delet } delOp := &Delete{ - Target: targetTbl, - Source: op, - Ignore: bool(del.Ignore), - OwnedVindexQuery: ovq, + DMLCommon: &DMLCommon{ + Ignore: del.Ignore, + Target: targetTbl, + OwnedVindexQuery: ovq, + Source: op, + }, } if del.Limit == nil { return delOp } - addOrdering(ctx, del, delOp) + addOrdering(ctx, del.OrderBy, delOp) delOp.Source = &Limit{ Source: delOp.Source, @@ -213,12 +216,41 @@ func createDeleteOperator(ctx *plancontext.PlanningContext, del *sqlparser.Delet return delOp } -func addOrdering(ctx *plancontext.PlanningContext, del *sqlparser.Delete, delOp *Delete) { - es := &expressionSet{} - ordering := &Ordering{ - Source: delOp.Source, +func generateOwnedVindexQuery(tblExpr sqlparser.TableExpr, del *sqlparser.Delete, table TargetTable, ksidCols []sqlparser.IdentifierCI) *sqlparser.Select { + var selExprs sqlparser.SelectExprs + for _, col := range ksidCols { + colName := makeColName(col, table, sqlparser.MultiTable(del.TableExprs)) + selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) + } + for _, cv := range table.VTable.Owned { + for _, col := range cv.Columns { + colName := makeColName(col, table, sqlparser.MultiTable(del.TableExprs)) + selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) + } + } + sqlparser.RemoveKeyspaceInTables(tblExpr) + return &sqlparser.Select{ + SelectExprs: selExprs, + From: del.TableExprs, + Where: del.Where, + OrderBy: del.OrderBy, + Limit: del.Limit, + Lock: sqlparser.ForUpdateLock, } - for _, order := range del.OrderBy { +} + +func makeColName(col sqlparser.IdentifierCI, table TargetTable, isMultiTbl bool) *sqlparser.ColName { + if isMultiTbl { + return sqlparser.NewColNameWithQualifier(col.String(), table.Name) + } + return sqlparser.NewColName(col.String()) +} + +func addOrdering(ctx *plancontext.PlanningContext, orderBy sqlparser.OrderBy, op Operator) { + es := &expressionSet{} + ordering := &Ordering{} + ordering.SetInputs(op.Inputs()) + for _, order := range orderBy { if sqlparser.IsNull(order.Expr) { // ORDER BY null can safely be ignored continue @@ -232,7 +264,7 @@ func addOrdering(ctx *plancontext.PlanningContext, del *sqlparser.Delete, delOp }) } if len(ordering.Order) > 0 { - delOp.Source = ordering + op.SetInputs([]Operator{ordering}) } } diff --git a/go/vt/vtgate/planbuilder/operators/dml_planning.go b/go/vt/vtgate/planbuilder/operators/dml_planning.go index 530f18391e2..6dbe7a74ff3 100644 --- a/go/vt/vtgate/planbuilder/operators/dml_planning.go +++ b/go/vt/vtgate/planbuilder/operators/dml_planning.go @@ -28,6 +28,13 @@ import ( "vitess.io/vitess/go/vt/vtgate/vindexes" ) +type DMLCommon struct { + Ignore sqlparser.Ignore + Target TargetTable + OwnedVindexQuery *sqlparser.Select + Source Operator +} + // getVindexInformation returns the vindex and VindexPlusPredicates for the DML, // If it cannot find a unique vindex match, it returns an error. func getVindexInformation(id semantics.TableSet, table *vindexes.Table) ( @@ -56,70 +63,77 @@ func getVindexInformation(id semantics.TableSet, table *vindexes.Table) ( return primaryVindex, vindexesAndPredicates } +func createAssignmentExpressions( + ctx *plancontext.PlanningContext, + assignments []SetExpr, + vcol sqlparser.IdentifierCI, + subQueriesArgOnChangedVindex []string, + vindexValueMap map[string]evalengine.Expr, + compExprs []sqlparser.Expr, +) ([]string, []sqlparser.Expr) { + // Searching in order of columns in colvindex. + found := false + for _, assignment := range assignments { + if !vcol.Equal(assignment.Name.Name) { + continue + } + if found { + panic(vterrors.VT03015(assignment.Name.Name)) + } + found = true + pv, err := evalengine.Translate(assignment.Expr.EvalExpr, &evalengine.Config{ + ResolveType: ctx.SemTable.TypeForExpr, + Collation: ctx.SemTable.Collation, + Environment: ctx.VSchema.Environment(), + }) + if err != nil { + panic(invalidUpdateExpr(assignment.Name.Name.String(), assignment.Expr.EvalExpr)) + } + + if assignment.Expr.Info != nil { + sqe, ok := assignment.Expr.Info.(SubQueryExpression) + if ok { + for _, sq := range sqe { + subQueriesArgOnChangedVindex = append(subQueriesArgOnChangedVindex, sq.ArgName) + } + } + } + + vindexValueMap[vcol.String()] = pv + compExprs = append(compExprs, sqlparser.NewComparisonExpr(sqlparser.EqualOp, assignment.Name, assignment.Expr.EvalExpr, nil)) + } + return subQueriesArgOnChangedVindex, compExprs +} + func buildChangedVindexesValues( ctx *plancontext.PlanningContext, update *sqlparser.Update, table *vindexes.Table, ksidCols []sqlparser.IdentifierCI, assignments []SetExpr, -) (vv map[string]*engine.VindexValues, ownedVindexQuery string, subQueriesArgOnChangedVindex []string) { +) (vv map[string]*engine.VindexValues, ownedVindexQuery *sqlparser.Select, subQueriesArgOnChangedVindex []string) { changedVindexes := make(map[string]*engine.VindexValues) - buf, offset := initialQuery(ksidCols, table) + selExprs, offset := initialQuery(ksidCols, table) for i, vindex := range table.ColumnVindexes { vindexValueMap := make(map[string]evalengine.Expr) - first := true + var compExprs []sqlparser.Expr for _, vcol := range vindex.Columns { - // Searching in order of columns in colvindex. - found := false - for _, assignment := range assignments { - if !vcol.Equal(assignment.Name.Name) { - continue - } - if found { - panic(vterrors.VT03015(assignment.Name.Name)) - } - found = true - pv, err := evalengine.Translate(assignment.Expr.EvalExpr, &evalengine.Config{ - ResolveType: ctx.SemTable.TypeForExpr, - Collation: ctx.SemTable.Collation, - Environment: ctx.VSchema.Environment(), - }) - if err != nil { - panic(invalidUpdateExpr(assignment.Name.Name.String(), assignment.Expr.EvalExpr)) - } - - if assignment.Expr.Info != nil { - sqe, ok := assignment.Expr.Info.(SubQueryExpression) - if ok { - for _, sq := range sqe { - subQueriesArgOnChangedVindex = append(subQueriesArgOnChangedVindex, sq.ArgName) - } - } - } - - vindexValueMap[vcol.String()] = pv - if first { - buf.Myprintf(", %s", assignment.String()) - first = false - } else { - buf.Myprintf(" and %s", assignment.String()) - } - } + subQueriesArgOnChangedVindex, compExprs = + createAssignmentExpressions(ctx, assignments, vcol, subQueriesArgOnChangedVindex, vindexValueMap, compExprs) } if len(vindexValueMap) == 0 { // Vindex not changing, continue continue } - - if update.Limit != nil && len(update.OrderBy) == 0 { - panic(vterrors.VT12001(fmt.Sprintf("you need to provide the ORDER BY clause when using LIMIT; invalid update on vindex: %v", vindex.Name))) - } if i == 0 { panic(vterrors.VT12001(fmt.Sprintf("you cannot UPDATE primary vindex columns; invalid update on vindex: %v", vindex.Name))) } if _, ok := vindex.Vindex.(vindexes.Lookup); !ok { panic(vterrors.VT12001(fmt.Sprintf("you can only UPDATE lookup vindexes; invalid update on vindex: %v", vindex.Name))) } + + // Checks done, let's actually add the expressions and the vindex map + selExprs = append(selExprs, aeWrap(sqlparser.AndExpressions(compExprs...))) changedVindexes[vindex.Name] = &engine.VindexValues{ EvalExprMap: vindexValueMap, Offset: offset, @@ -127,7 +141,7 @@ func buildChangedVindexesValues( offset++ } if len(changedVindexes) == 0 { - return nil, "", nil + return nil, nil, nil } // generate rest of the owned vindex query. aTblExpr, ok := update.TableExprs[0].(*sqlparser.AliasedTableExpr) @@ -135,28 +149,31 @@ func buildChangedVindexesValues( panic(vterrors.VT12001("UPDATE on complex table expression")) } tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: table.Name}, As: aTblExpr.As} - buf.Myprintf(" from %v%v%v%v for update", tblExpr, update.Where, update.OrderBy, update.Limit) - return changedVindexes, buf.String(), subQueriesArgOnChangedVindex + ovq := &sqlparser.Select{ + From: []sqlparser.TableExpr{tblExpr}, + SelectExprs: selExprs, + Where: update.Where, + OrderBy: update.OrderBy, + Limit: update.Limit, + Lock: sqlparser.ForUpdateLock, + } + return changedVindexes, ovq, subQueriesArgOnChangedVindex } -func initialQuery(ksidCols []sqlparser.IdentifierCI, table *vindexes.Table) (*sqlparser.TrackedBuffer, int) { - buf := sqlparser.NewTrackedBuffer(nil) +func initialQuery(ksidCols []sqlparser.IdentifierCI, table *vindexes.Table) (sqlparser.SelectExprs, int) { + var selExprs sqlparser.SelectExprs offset := 0 for _, col := range ksidCols { - if offset == 0 { - buf.Myprintf("select %v", col) - } else { - buf.Myprintf(", %v", col) - } + selExprs = append(selExprs, aeWrap(sqlparser.NewColName(col.String()))) offset++ } for _, cv := range table.Owned { for _, column := range cv.Columns { - buf.Myprintf(", %v", column) + selExprs = append(selExprs, aeWrap(sqlparser.NewColName(column.String()))) offset++ } } - return buf, offset + return selExprs, offset } func invalidUpdateExpr(upd string, expr sqlparser.Expr) error { diff --git a/go/vt/vtgate/planbuilder/operators/delete_with_input.go b/go/vt/vtgate/planbuilder/operators/dml_with_input.go similarity index 67% rename from go/vt/vtgate/planbuilder/operators/delete_with_input.go rename to go/vt/vtgate/planbuilder/operators/dml_with_input.go index 56b468e7ead..e15a1042a47 100644 --- a/go/vt/vtgate/planbuilder/operators/delete_with_input.go +++ b/go/vt/vtgate/planbuilder/operators/dml_with_input.go @@ -24,9 +24,10 @@ import ( "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" ) -type DeleteWithInput struct { +// DMLWithInput is used to represent a DML Operator taking input from a Source Operator +type DMLWithInput struct { Source Operator - Delete Operator + DML Operator cols []*sqlparser.ColName Offsets []int @@ -35,25 +36,25 @@ type DeleteWithInput struct { noPredicates } -func (d *DeleteWithInput) Clone(inputs []Operator) Operator { +func (d *DMLWithInput) Clone(inputs []Operator) Operator { newD := *d newD.SetInputs(inputs) return &newD } -func (d *DeleteWithInput) Inputs() []Operator { - return []Operator{d.Source, d.Delete} +func (d *DMLWithInput) Inputs() []Operator { + return []Operator{d.Source, d.DML} } -func (d *DeleteWithInput) SetInputs(inputs []Operator) { +func (d *DMLWithInput) SetInputs(inputs []Operator) { if len(inputs) != 2 { - panic("unexpected number of inputs for DeleteWithInput operator") + panic("unexpected number of inputs for DMLWithInput operator") } d.Source = inputs[0] - d.Delete = inputs[1] + d.DML = inputs[1] } -func (d *DeleteWithInput) ShortDescription() string { +func (d *DMLWithInput) ShortDescription() string { colStrings := slice.Map(d.cols, func(from *sqlparser.ColName) string { return sqlparser.String(from) }) @@ -68,11 +69,11 @@ func (d *DeleteWithInput) ShortDescription() string { return out } -func (d *DeleteWithInput) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy { +func (d *DMLWithInput) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy { return nil } -func (d *DeleteWithInput) planOffsets(ctx *plancontext.PlanningContext) Operator { +func (d *DMLWithInput) planOffsets(ctx *plancontext.PlanningContext) Operator { for _, col := range d.cols { offset := d.Source.AddColumn(ctx, true, false, aeWrap(col)) d.Offsets = append(d.Offsets, offset) @@ -80,4 +81,4 @@ func (d *DeleteWithInput) planOffsets(ctx *plancontext.PlanningContext) Operator return d } -var _ Operator = (*DeleteWithInput)(nil) +var _ Operator = (*DMLWithInput)(nil) diff --git a/go/vt/vtgate/planbuilder/operators/limit.go b/go/vt/vtgate/planbuilder/operators/limit.go index 1ba6b61149d..152872ff8ff 100644 --- a/go/vt/vtgate/planbuilder/operators/limit.go +++ b/go/vt/vtgate/planbuilder/operators/limit.go @@ -17,6 +17,8 @@ limitations under the License. package operators import ( + "strconv" + "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" ) @@ -35,6 +37,7 @@ func (l *Limit) Clone(inputs []Operator) Operator { return &Limit{ Source: inputs[0], AST: sqlparser.CloneRefOfLimit(l.AST), + Pushed: l.Pushed, } } @@ -72,5 +75,5 @@ func (l *Limit) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy { } func (l *Limit) ShortDescription() string { - return sqlparser.String(l.AST) + return sqlparser.String(l.AST) + " Pushed:" + strconv.FormatBool(l.Pushed) } diff --git a/go/vt/vtgate/planbuilder/operators/phases.go b/go/vt/vtgate/planbuilder/operators/phases.go index 011e366f5e0..b45fbd5c9ad 100644 --- a/go/vt/vtgate/planbuilder/operators/phases.go +++ b/go/vt/vtgate/planbuilder/operators/phases.go @@ -17,9 +17,12 @@ limitations under the License. package operators import ( + "io" + "vitess.io/vitess/go/slice" "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/plancontext" "vitess.io/vitess/go/vt/vtgate/semantics" ) @@ -35,7 +38,7 @@ const ( delegateAggregation addAggrOrdering cleanOutPerfDistinct - deleteWithInput + dmlWithInput subquerySettling DONE ) @@ -56,8 +59,8 @@ func (p Phase) String() string { return "optimize Distinct operations" case subquerySettling: return "settle subqueries" - case deleteWithInput: - return "expand delete to delete with input" + case dmlWithInput: + return "expand update/delete to dml with input" default: panic(vterrors.VT13001("unhandled default case")) } @@ -75,8 +78,8 @@ func (p Phase) shouldRun(s semantics.QuerySignature) bool { return s.Distinct case subquerySettling: return s.SubQueries - case deleteWithInput: - return s.Delete + case dmlWithInput: + return s.Dml default: return true } @@ -94,8 +97,8 @@ func (p Phase) act(ctx *plancontext.PlanningContext, op Operator) Operator { return removePerformanceDistinctAboveRoute(ctx, op) case subquerySettling: return settleSubqueries(ctx, op) - case deleteWithInput: - return findDeletesAboveRoute(ctx, op) + case dmlWithInput: + return findDMLAboveRoute(ctx, op) default: return op } @@ -120,19 +123,77 @@ func (p *phaser) next(ctx *plancontext.PlanningContext) Phase { } } -func findDeletesAboveRoute(ctx *plancontext.PlanningContext, root Operator) Operator { +func findDMLAboveRoute(ctx *plancontext.PlanningContext, root Operator) Operator { visitor := func(in Operator, _ semantics.TableSet, isRoot bool) (Operator, *ApplyResult) { - delOp, ok := in.(*Delete) - if !ok { - return in, NoRewrite + switch op := in.(type) { + case *Delete: + return createDMLWithInput(ctx, op, op.Source, op.DMLCommon) + case *Update: + return createDMLWithInput(ctx, op, op.Source, op.DMLCommon) } - - return createDeleteWithInput(ctx, delOp, delOp.Source) + return in, NoRewrite } return BottomUp(root, TableID, visitor, stopAtRoute) } +func createDMLWithInput(ctx *plancontext.PlanningContext, op, src Operator, in *DMLCommon) (Operator, *ApplyResult) { + if len(in.Target.VTable.PrimaryKey) == 0 { + panic(vterrors.VT09015()) + } + dm := &DMLWithInput{} + var leftComp sqlparser.ValTuple + proj := newAliasedProjection(src) + for _, col := range in.Target.VTable.PrimaryKey { + colName := sqlparser.NewColNameWithQualifier(col.String(), in.Target.Name) + proj.AddColumn(ctx, true, false, aeWrap(colName)) + dm.cols = append(dm.cols, colName) + leftComp = append(leftComp, colName) + ctx.SemTable.Recursive[colName] = in.Target.ID + } + + dm.Source = proj + + var targetTable *Table + _ = Visit(src, func(operator Operator) error { + if tbl, ok := operator.(*Table); ok && tbl.QTable.ID == in.Target.ID { + targetTable = tbl + return io.EOF + } + return nil + }) + if targetTable == nil { + panic(vterrors.VT13001("target DELETE table not found")) + } + + // optimize for case when there is only single column on left hand side. + var lhs sqlparser.Expr = leftComp + if len(leftComp) == 1 { + lhs = leftComp[0] + } + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, lhs, sqlparser.ListArg(engine.DmlVals), nil) + targetQT := targetTable.QTable + qt := &QueryTable{ + ID: targetQT.ID, + Alias: sqlparser.CloneRefOfAliasedTableExpr(targetQT.Alias), + Table: sqlparser.CloneTableName(targetQT.Table), + Predicates: []sqlparser.Expr{compExpr}, + } + + qg := &QueryGraph{Tables: []*QueryTable{qt}} + in.Source = qg + + if in.OwnedVindexQuery != nil { + in.OwnedVindexQuery.From = sqlparser.TableExprs{targetQT.Alias} + in.OwnedVindexQuery.Where = sqlparser.NewWhere(sqlparser.WhereClause, compExpr) + in.OwnedVindexQuery.OrderBy = nil + in.OwnedVindexQuery.Limit = nil + } + dm.DML = op + + return dm, Rewrote("changed Delete to DMLWithInput") +} + func removePerformanceDistinctAboveRoute(_ *plancontext.PlanningContext, op Operator) Operator { return BottomUp(op, TableID, func(innerOp Operator, _ semantics.TableSet, _ bool) (Operator, *ApplyResult) { d, ok := innerOp.(*Distinct) diff --git a/go/vt/vtgate/planbuilder/operators/query_planning.go b/go/vt/vtgate/planbuilder/operators/query_planning.go index f412e783f42..e31d06122da 100644 --- a/go/vt/vtgate/planbuilder/operators/query_planning.go +++ b/go/vt/vtgate/planbuilder/operators/query_planning.go @@ -21,8 +21,6 @@ import ( "io" "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/plancontext" "vitess.io/vitess/go/vt/vtgate/semantics" ) @@ -92,6 +90,8 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { return pushLockAndComment(in) case *Delete: return tryPushDelete(in) + case *Update: + return tryPushUpdate(in) default: return in, NoRewrite } @@ -102,12 +102,19 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { func tryPushDelete(in *Delete) (Operator, *ApplyResult) { if src, ok := in.Source.(*Route); ok { - return pushDeleteUnderRoute(in, src) + return pushDMLUnderRoute(in, src, "pushed delete under route") } return in, NoRewrite } -func pushDeleteUnderRoute(in *Delete, src *Route) (Operator, *ApplyResult) { +func tryPushUpdate(in *Update) (Operator, *ApplyResult) { + if src, ok := in.Source.(*Route); ok { + return pushDMLUnderRoute(in, src, "pushed update under route") + } + return in, NoRewrite +} + +func pushDMLUnderRoute(in Operator, src *Route, msg string) (Operator, *ApplyResult) { switch r := src.Routing.(type) { case *SequenceRouting: // Sequences are just unsharded routes @@ -119,62 +126,7 @@ func pushDeleteUnderRoute(in *Delete, src *Route) (Operator, *ApplyResult) { // Alternates are not required. r.Alternates = nil } - return Swap(in, src, "pushed delete under route") -} - -func createDeleteWithInput(ctx *plancontext.PlanningContext, in *Delete, src Operator) (Operator, *ApplyResult) { - if len(in.Target.VTable.PrimaryKey) == 0 { - panic(vterrors.VT09015()) - } - dm := &DeleteWithInput{} - var leftComp sqlparser.ValTuple - proj := newAliasedProjection(src) - for _, col := range in.Target.VTable.PrimaryKey { - colName := sqlparser.NewColNameWithQualifier(col.String(), in.Target.Name) - proj.AddColumn(ctx, true, false, aeWrap(colName)) - dm.cols = append(dm.cols, colName) - leftComp = append(leftComp, colName) - ctx.SemTable.Recursive[colName] = in.Target.ID - } - - dm.Source = proj - - var targetTable *Table - _ = Visit(src, func(operator Operator) error { - if tbl, ok := operator.(*Table); ok && tbl.QTable.ID == in.Target.ID { - targetTable = tbl - return io.EOF - } - return nil - }) - if targetTable == nil { - panic(vterrors.VT13001("target DELETE table not found")) - } - - // optimize for case when there is only single column on left hand side. - var lhs sqlparser.Expr = leftComp - if len(leftComp) == 1 { - lhs = leftComp[0] - } - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, lhs, sqlparser.ListArg(engine.DmVals), nil) - targetQT := targetTable.QTable - qt := &QueryTable{ - ID: targetQT.ID, - Alias: sqlparser.CloneRefOfAliasedTableExpr(targetQT.Alias), - Table: sqlparser.CloneTableName(targetQT.Table), - Predicates: []sqlparser.Expr{compExpr}, - } - - qg := &QueryGraph{Tables: []*QueryTable{qt}} - in.Source = qg - - if in.OwnedVindexQuery != nil { - in.OwnedVindexQuery.From = sqlparser.TableExprs{targetQT.Alias} - in.OwnedVindexQuery.Where = sqlparser.NewWhere(sqlparser.WhereClause, compExpr) - } - dm.Delete = in - - return dm, Rewrote("changed Delete to DeleteWithInput") + return Swap(in, src, msg) } func pushLockAndComment(l *LockAndComment) (Operator, *ApplyResult) { @@ -187,6 +139,20 @@ func pushLockAndComment(l *LockAndComment) (Operator, *ApplyResult) { src.Comments = l.Comments src.Lock = l.Lock return src, Rewrote("put lock and comment into route") + case *SubQueryContainer: + src.Outer = &LockAndComment{ + Source: src.Outer, + Comments: l.Comments, + Lock: l.Lock, + } + for _, sq := range src.Inner { + sq.Subquery = &LockAndComment{ + Source: sq.Subquery, + Comments: l.Comments, + Lock: l.Lock, + } + } + return src, Rewrote("push lock and comment into subquery container") default: inputs := src.Inputs() for i, op := range inputs { @@ -283,7 +249,7 @@ func setUpperLimit(in *Limit) (Operator, *ApplyResult) { Pushed: false, } op.Source = newSrc - result = result.Merge(Rewrote("push limit under route")) + result = result.Merge(Rewrote("push upper limit under route")) return SkipChildren default: return VisitChildren diff --git a/go/vt/vtgate/planbuilder/operators/route_planning.go b/go/vt/vtgate/planbuilder/operators/route_planning.go index c6940f7d356..ddb2f0d1210 100644 --- a/go/vt/vtgate/planbuilder/operators/route_planning.go +++ b/go/vt/vtgate/planbuilder/operators/route_planning.go @@ -124,52 +124,6 @@ func buildVindexTableForDML( return vindexTable, routing } -func generateOwnedVindexQuery(tblExpr sqlparser.TableExpr, del *sqlparser.Delete, table TargetTable, ksidCols []sqlparser.IdentifierCI) *sqlparser.Select { - var selExprs sqlparser.SelectExprs - for _, col := range ksidCols { - colName := makeColName(col, table, sqlparser.MultiTable(del.TableExprs)) - selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) - } - for _, cv := range table.VTable.Owned { - for _, col := range cv.Columns { - colName := makeColName(col, table, sqlparser.MultiTable(del.TableExprs)) - selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) - } - } - sqlparser.RemoveKeyspaceInTables(tblExpr) - return &sqlparser.Select{ - SelectExprs: selExprs, - From: del.TableExprs, - Where: del.Where, - OrderBy: del.OrderBy, - Limit: del.Limit, - Lock: sqlparser.ForUpdateLock, - } -} - -func makeColName(col sqlparser.IdentifierCI, table TargetTable, isMultiTbl bool) *sqlparser.ColName { - if isMultiTbl { - return sqlparser.NewColNameWithQualifier(col.String(), table.Name) - } - return sqlparser.NewColName(col.String()) -} - -func getUpdateVindexInformation( - ctx *plancontext.PlanningContext, - updStmt *sqlparser.Update, - vindexTable *vindexes.Table, - tableID semantics.TableSet, - assignments []SetExpr, -) ([]*VindexPlusPredicates, map[string]*engine.VindexValues, string, []string) { - if !vindexTable.Keyspace.Sharded { - return nil, nil, "", nil - } - - primaryVindex, vindexAndPredicates := getVindexInformation(tableID, vindexTable) - changedVindexValues, ownedVindexQuery, subQueriesArgOnChangedVindex := buildChangedVindexesValues(ctx, updStmt, vindexTable, primaryVindex.Columns, assignments) - return vindexAndPredicates, changedVindexValues, ownedVindexQuery, subQueriesArgOnChangedVindex -} - /* The greedy planner will plan a query by finding first finding the best route plan for every table. Then, iteratively, it finds the cheapest join that can be produced between the remaining plans, diff --git a/go/vt/vtgate/planbuilder/operators/update.go b/go/vt/vtgate/planbuilder/operators/update.go index 44b7c6efa72..a4aafa222fa 100644 --- a/go/vt/vtgate/planbuilder/operators/update.go +++ b/go/vt/vtgate/planbuilder/operators/update.go @@ -20,7 +20,6 @@ import ( "fmt" "maps" "slices" - "strings" "vitess.io/vitess/go/sqltypes" querypb "vitess.io/vitess/go/vt/proto/query" @@ -35,21 +34,16 @@ import ( type ( Update struct { - QTable *QueryTable - VTable *vindexes.Table + *DMLCommon + Assignments []SetExpr ChangedVindexValues map[string]*engine.VindexValues - OwnedVindexQuery string - Ignore sqlparser.Ignore - OrderBy sqlparser.OrderBy - Limit *sqlparser.Limit // these subqueries cannot be merged as they are part of the changed vindex values // these values are needed to be sent over to lookup vindex for update. // On merging this information will be lost, so subquery merge is blocked. SubQueriesArgOnChangedVindex []string - noInputs noColumns noPredicates } @@ -60,20 +54,35 @@ type ( } ) +func (u *Update) Inputs() []Operator { + if u.Source == nil { + return nil + } + return []Operator{u.Source} +} + +func (u *Update) SetInputs(inputs []Operator) { + if len(inputs) != 1 { + panic(vterrors.VT13001("unexpected number of inputs for Update operator")) + } + u.Source = inputs[0] +} + func (se SetExpr) String() string { return fmt.Sprintf("%s = %s", sqlparser.String(se.Name), sqlparser.String(se.Expr.EvalExpr)) } // Introduces implements the PhysicalOperator interface func (u *Update) introducesTableID() semantics.TableSet { - return u.QTable.ID + return u.Target.ID } // Clone implements the Operator interface -func (u *Update) Clone([]Operator) Operator { +func (u *Update) Clone(inputs []Operator) Operator { upd := *u upd.Assignments = slices.Clone(u.Assignments) upd.ChangedVindexValues = maps.Clone(u.ChangedVindexValues) + upd.SetInputs(inputs) return &upd } @@ -82,34 +91,33 @@ func (u *Update) GetOrdering(*plancontext.PlanningContext) []OrderBy { } func (u *Update) TablesUsed() []string { - if u.VTable != nil { - return SingleQualifiedIdentifier(u.VTable.Keyspace, u.VTable.Name) - } - return nil + return SingleQualifiedIdentifier(u.Target.VTable.Keyspace, u.Target.VTable.Name) } func (u *Update) ShortDescription() string { - s := []string{u.VTable.String()} - if u.Limit != nil { - s = append(s, sqlparser.String(u.Limit)) + ovq := "" + if u.OwnedVindexQuery != nil { + ovq = " vindexQuery:%s" + sqlparser.String(u.OwnedVindexQuery) } - if len(u.OrderBy) > 0 { - s = append(s, sqlparser.String(u.OrderBy)) - } - return strings.Join(s, " ") + return fmt.Sprintf("%s.%s%s", u.Target.VTable.Keyspace.Name, u.Target.VTable.Name.String(), ovq) } -func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) Operator { - tableInfo, qt := createQueryTableForDML(ctx, updStmt.TableExprs[0], updStmt.Where) +func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (op Operator) { + var updClone *sqlparser.Update + var vTbl *vindexes.Table - vindexTable, routing := buildVindexTableForDML(ctx, tableInfo, qt, "update") + op, vTbl, updClone = createUpdateOperator(ctx, updStmt) - updOp, updClone := createUpdateOperator(ctx, updStmt, vindexTable, qt, routing) + op = &LockAndComment{ + Source: op, + Comments: updStmt.Comments, + Lock: sqlparser.ShareModeLock, + } parentFks := ctx.SemTable.GetParentForeignKeysList() childFks := ctx.SemTable.GetChildForeignKeysList() if len(childFks) == 0 && len(parentFks) == 0 { - return updOp + return op } // If the delete statement has a limit, we don't support it yet. @@ -123,22 +131,30 @@ func createOperatorFromUpdate(ctx *plancontext.PlanningContext, updStmt *sqlpars panic(err) } - return buildFkOperator(ctx, updOp, updClone, parentFks, childFks, vindexTable) + return buildFkOperator(ctx, op, updClone, parentFks, childFks, vTbl) } -func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update, vindexTable *vindexes.Table, qt *QueryTable, routing Routing) (Operator, *sqlparser.Update) { +func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.Update) (Operator, *vindexes.Table, *sqlparser.Update) { + op := crossJoin(ctx, updStmt.TableExprs) + sqc := &SubQueryBuilder{} + if updStmt.Where != nil { + op = addWherePredsToSubQueryBuilder(ctx, updStmt.Where.Expr, op, sqc) + } + + outerID := TableID(op) assignments := make([]SetExpr, len(updStmt.Exprs)) // updClone is used in foreign key planning to create the selection statements to be used for verification and selection. // If we encounter subqueries, we want to fix the updClone to use the replaced expression, so that the pulled out subquery's // result is used everywhere instead of running the subquery multiple times, which is wasteful. updClone := sqlparser.CloneRefOfUpdate(updStmt) for idx, updExpr := range updStmt.Exprs { - expr, subqs := sqc.pullOutValueSubqueries(ctx, updExpr.Expr, qt.ID, true) + expr, subqs := sqc.pullOutValueSubqueries(ctx, updExpr.Expr, outerID, true) if len(subqs) == 0 { expr = updExpr.Expr } else { updClone.Exprs[idx].Expr = sqlparser.CloneExpr(expr) + ctx.SemTable.UpdateChildFKExpr(updExpr, expr) } proj := newProjExpr(aeWrap(expr)) if len(subqs) != 0 { @@ -150,59 +166,92 @@ func createUpdateOperator(ctx *plancontext.PlanningContext, updStmt *sqlparser.U } } - vp, cvv, ovq, subQueriesArgOnChangedVindex := getUpdateVindexInformation(ctx, updStmt, vindexTable, qt.ID, assignments) + target := updStmt.TableExprs[0] + atbl, ok := target.(*sqlparser.AliasedTableExpr) + if !ok { + panic(vterrors.VT12001("multi table update")) + } + tblID := ctx.SemTable.TableSetFor(atbl) + tblInfo, err := ctx.SemTable.TableInfoFor(tblID) + if err != nil { + panic(err) + } - tr, ok := routing.(*ShardedRouting) - if ok { - tr.VindexPreds = vp + vTbl := tblInfo.GetVindexTable() + // Reference table should update the source table. + if vTbl.Type == vindexes.TypeReference && vTbl.Source != nil { + vTbl = updateQueryGraphWithSource(ctx, op, tblID, vTbl) } - for _, predicate := range qt.Predicates { - if subq := sqc.handleSubquery(ctx, predicate, qt.ID); subq != nil { - continue - } - routing = UpdateRoutingLogic(ctx, predicate, routing) + name, err := tblInfo.Name() + if err != nil { + panic(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) - panic(vterrors.VT12001("multi shard UPDATE with LIMIT")) + targetTbl := TargetTable{ + ID: tblID, + VTable: vTbl, + Name: name, } - route := &Route{ - Source: &Update{ - QTable: qt, - VTable: vindexTable, - Assignments: assignments, - ChangedVindexValues: cvv, - OwnedVindexQuery: ovq, - Ignore: updStmt.Ignore, - Limit: updStmt.Limit, - OrderBy: updStmt.OrderBy, - SubQueriesArgOnChangedVindex: subQueriesArgOnChangedVindex, + _, cvv, ovq, subQueriesArgOnChangedVindex := getUpdateVindexInformation(ctx, updStmt, targetTbl, assignments) + + updOp := &Update{ + DMLCommon: &DMLCommon{ + Ignore: updStmt.Ignore, + Target: targetTbl, + OwnedVindexQuery: ovq, + Source: op, }, - Routing: routing, - Comments: updStmt.Comments, + Assignments: assignments, + ChangedVindexValues: cvv, + SubQueriesArgOnChangedVindex: subQueriesArgOnChangedVindex, + } + + if len(updStmt.OrderBy) > 0 { + addOrdering(ctx, updStmt.OrderBy, updOp) } - decorator := func(op Operator) Operator { - return &LockAndComment{ - Source: op, - Lock: sqlparser.ShareModeLock, + if updStmt.Limit != nil { + updOp.Source = &Limit{ + Source: updOp.Source, + AST: updStmt.Limit, } } - return sqc.getRootOperator(route, decorator), updClone + return sqc.getRootOperator(updOp, nil), vTbl, updClone +} + +func getUpdateVindexInformation( + ctx *plancontext.PlanningContext, + updStmt *sqlparser.Update, + table TargetTable, + assignments []SetExpr, +) ([]*VindexPlusPredicates, map[string]*engine.VindexValues, *sqlparser.Select, []string) { + if !table.VTable.Keyspace.Sharded { + return nil, nil, nil, nil + } + + primaryVindex, vindexAndPredicates := getVindexInformation(table.ID, table.VTable) + changedVindexValues, ownedVindexQuery, subQueriesArgOnChangedVindex := buildChangedVindexesValues(ctx, updStmt, table.VTable, primaryVindex.Columns, assignments) + return vindexAndPredicates, changedVindexValues, ownedVindexQuery, subQueriesArgOnChangedVindex } func buildFkOperator(ctx *plancontext.PlanningContext, updOp Operator, updClone *sqlparser.Update, parentFks []vindexes.ParentFKInfo, childFks []vindexes.ChildFKInfo, updatedTable *vindexes.Table) Operator { - // If the outermost operator is a subquery container, we want to do the foreign key planning inside it, - // because we want to Inner of the subquery to execute first and its result be used for the entire update planning. - subqc, isSubqc := updOp.(*SubQueryContainer) - if isSubqc { - subqc.Outer = buildFkOperator(ctx, subqc.Outer, updClone, parentFks, childFks, updatedTable) - return subqc + // 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) + } + return in, NoRewrite + }, stopAtUpdateOp) + if foundSubqc { + return updOp } + restrictChildFks, cascadeChildFks := splitChildFks(childFks) op := createFKCascadeOp(ctx, updOp, updClone, cascadeChildFks, updatedTable) @@ -210,6 +259,11 @@ func buildFkOperator(ctx *plancontext.PlanningContext, updOp Operator, updClone return createFKVerifyOp(ctx, op, updClone, parentFks, restrictChildFks, updatedTable) } +func stopAtUpdateOp(operator Operator) VisitRule { + _, isUpdate := operator.(*Update) + return VisitRule(!isUpdate) +} + // 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/dml_cases.json b/go/vt/vtgate/planbuilder/testdata/dml_cases.json index d7a6b32adf3..5e4987be8c3 100644 --- a/go/vt/vtgate/planbuilder/testdata/dml_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/dml_cases.json @@ -5055,7 +5055,7 @@ "QueryType": "DELETE", "Original": "delete user from user join user_extra on user.id = user_extra.id where user.name = 'foo'", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5108,11 +5108,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select `user`.Id, `user`.`Name`, `user`.Costly from `user` where `user`.id in ::dm_vals for update", - "Query": "delete from `user` where `user`.id in ::dm_vals", + "OwnedVindexQuery": "select `user`.Id, `user`.`Name`, `user`.Costly from `user` where `user`.id in ::dml_vals for update", + "Query": "delete from `user` where `user`.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "user_index" } @@ -5131,7 +5131,7 @@ "QueryType": "DELETE", "Original": "delete u from user u join music m on u.col = m.col", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5180,11 +5180,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dm_vals for update", - "Query": "delete from `user` as u where u.id in ::dm_vals", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dml_vals for update", + "Query": "delete from `user` as u where u.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "user_index" } @@ -5203,7 +5203,7 @@ "QueryType": "DELETE", "Original": "delete u from music m join user u where u.col = m.col and m.foo = 42", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5252,11 +5252,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dm_vals for update", - "Query": "delete from `user` as u where u.id in ::dm_vals", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dml_vals for update", + "Query": "delete from `user` as u where u.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "user_index" } @@ -5301,7 +5301,7 @@ "QueryType": "DELETE", "Original": "delete u from user u join music m on u.col = m.col join user_extra ue on m.user_id = ue.user_id where ue.foo = 20 and u.col = 30 and m.bar = 40", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5350,11 +5350,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dm_vals for update", - "Query": "delete from `user` as u where u.id in ::dm_vals", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u where u.id in ::dml_vals for update", + "Query": "delete from `user` as u where u.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "user_index" } @@ -5374,7 +5374,7 @@ "QueryType": "DELETE", "Original": "delete m from user u join music m on u.col = m.col join user_extra ue on m.user_id = ue.user_id where ue.foo = 20 and u.col = 30 and m.bar = 40", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5423,11 +5423,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select m.user_id, m.id from music as m where m.id in ::dm_vals for update", - "Query": "delete from music as m where m.id in ::dm_vals", + "OwnedVindexQuery": "select m.user_id, m.id from music as m where m.id in ::dml_vals for update", + "Query": "delete from music as m where m.id in ::dml_vals", "Table": "music", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "music_user_map" } @@ -5447,7 +5447,7 @@ "QueryType": "DELETE", "Original": "delete from user limit 10", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5480,11 +5480,11 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select Id, `Name`, Costly from `user` where `user`.id in ::dm_vals limit 10 for update", - "Query": "delete from `user` where `user`.id in ::dm_vals", + "OwnedVindexQuery": "select Id, `Name`, Costly from `user` where `user`.id in ::dml_vals for update", + "Query": "delete from `user` where `user`.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" ], "Vindex": "user_index" } @@ -5502,7 +5502,7 @@ "QueryType": "DELETE", "Original": "delete from user order by name, col limit 5", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -5536,11 +5536,121 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select Id, `Name`, Costly from `user` where `user`.id in ::dm_vals order by `name` asc, col asc limit 5 for update", - "Query": "delete from `user` where `user`.id in ::dm_vals", + "OwnedVindexQuery": "select Id, `Name`, Costly from `user` where `user`.id in ::dml_vals for update", + "Query": "delete from `user` where `user`.id in ::dml_vals", "Table": "user", "Values": [ - "::dm_vals" + "::dml_vals" + ], + "Vindex": "user_index" + } + ] + }, + "TablesUsed": [ + "user.user" + ] + } + }, + { + "comment": "update with limit clause", + "query": "update user set val = 1 where (name = 'foo' or id = 1) limit 1", + "plan": { + "QueryType": "UPDATE", + "Original": "update user set val = 1 where (name = 'foo' or id = 1) limit 1", + "Instructions": { + "OperatorType": "DMLWithInput", + "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], + "Inputs": [ + { + "OperatorType": "Limit", + "Count": "1", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id from `user` where 1 != 1", + "Query": "select `user`.id from `user` where `name` = 'foo' or id = 1 limit :__upper_limit lock in share mode", + "Table": "`user`" + } + ] + }, + { + "OperatorType": "Update", + "Variant": "IN", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "TargetTabletType": "PRIMARY", + "Query": "update `user` set val = 1 where `user`.id in ::dml_vals", + "Table": "user", + "Values": [ + "::dml_vals" + ], + "Vindex": "user_index" + } + ] + }, + "TablesUsed": [ + "user.user" + ] + } + }, + { + "comment": "update a vindex column with limit", + "query": "update user set name = 'abc' where id > 10 limit 1", + "plan": { + "QueryType": "UPDATE", + "Original": "update user set name = 'abc' where id > 10 limit 1", + "Instructions": { + "OperatorType": "DMLWithInput", + "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], + "Inputs": [ + { + "OperatorType": "Limit", + "Count": "1", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id from `user` where 1 != 1", + "Query": "select `user`.id from `user` where id > 10 limit :__upper_limit lock in share mode", + "Table": "`user`" + } + ] + }, + { + "OperatorType": "Update", + "Variant": "IN", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "TargetTabletType": "PRIMARY", + "ChangedVindexValues": [ + "name_user_map:3" + ], + "KsidLength": 1, + "KsidVindex": "user_index", + "OwnedVindexQuery": "select Id, `Name`, Costly, `name` = 'abc' from `user` where `user`.id in ::dml_vals for update", + "Query": "update `user` set `name` = 'abc' where `user`.id in ::dml_vals", + "Table": "user", + "Values": [ + "::dml_vals" ], "Vindex": "user_index" } diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json index b5fba1dd68e..b304f9323e4 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_cases.json @@ -1252,7 +1252,7 @@ "QueryType": "DELETE", "Original": "delete from u_tbl2 limit 2", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -1281,7 +1281,7 @@ "Sharded": false }, "FieldQuery": "select u_tbl2.col2 from u_tbl2 where 1 != 1", - "Query": "select u_tbl2.col2 from u_tbl2 where id in ::dm_vals for update", + "Query": "select u_tbl2.col2 from u_tbl2 where id in ::dml_vals for update", "Table": "u_tbl2" }, { @@ -1309,7 +1309,7 @@ "Sharded": false }, "TargetTabletType": "PRIMARY", - "Query": "delete from u_tbl2 where id in ::dm_vals", + "Query": "delete from u_tbl2 where id in ::dml_vals", "Table": "u_tbl2" } ] @@ -3336,7 +3336,7 @@ "QueryType": "DELETE", "Original": "delete from u_tbl1 order by id limit 1", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -3365,7 +3365,7 @@ "Sharded": false }, "FieldQuery": "select u_tbl1.col1 from u_tbl1 where 1 != 1", - "Query": "select u_tbl1.col1 from u_tbl1 where id in ::dm_vals for update", + "Query": "select u_tbl1.col1 from u_tbl1 where id in ::dml_vals for update", "Table": "u_tbl1" }, { @@ -3427,7 +3427,7 @@ "Sharded": false }, "TargetTabletType": "PRIMARY", - "Query": "delete from u_tbl1 where id in ::dm_vals", + "Query": "delete from u_tbl1 where id in ::dml_vals", "Table": "u_tbl1" } ] @@ -3463,7 +3463,7 @@ "Sharded": false }, "FieldQuery": "select col14 from u_tbl1 where 1 != 1", - "Query": "select col14 from u_tbl1 where x = 2 and y = 4 lock in share mode", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ col14 from u_tbl1 where x = 2 and y = 4 lock in share mode", "Table": "u_tbl1" }, { @@ -3479,7 +3479,7 @@ "Sharded": false }, "FieldQuery": "select 1 from u_tbl4 left join u_tbl1 on u_tbl1.col14 = cast(:__sq1 as SIGNED) where 1 != 1", - "Query": "select 1 from u_tbl4 left join u_tbl1 on u_tbl1.col14 = cast(:__sq1 as SIGNED) where not (u_tbl4.col41) <=> (cast(:__sq1 as SIGNED)) and u_tbl4.col4 = 3 and cast(:__sq1 as SIGNED) is not null and u_tbl1.col14 is null limit 1 for share", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ 1 from u_tbl4 left join u_tbl1 on u_tbl1.col14 = cast(:__sq1 as SIGNED) where not (u_tbl4.col41) <=> (cast(:__sq1 as SIGNED)) and u_tbl4.col4 = 3 and cast(:__sq1 as SIGNED) is not null and u_tbl1.col14 is null limit 1 lock in share mode", "Table": "u_tbl1, u_tbl4" }, { @@ -3503,5 +3503,171 @@ "unsharded_fk_allow.u_tbl4" ] } + }, + { + "comment": "update with a subquery", + "query": "update u_tbl1 set col1 = (select foo from u_tbl1 where id = 1) order by id desc", + "plan": { + "QueryType": "UPDATE", + "Original": "update u_tbl1 set col1 = (select foo from u_tbl1 where id = 1) order by id desc", + "Instructions": { + "OperatorType": "UncorrelatedSubquery", + "Variant": "PulloutValue", + "PulloutVars": [ + "__sq1" + ], + "Inputs": [ + { + "InputName": "SubQuery", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select foo from u_tbl1 where 1 != 1", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ foo from u_tbl1 where id = 1 lock in share mode", + "Table": "u_tbl1" + }, + { + "InputName": "Outer", + "OperatorType": "FkCascade", + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select u_tbl1.col1 from u_tbl1 where 1 != 1", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl1.col1 from u_tbl1 order by id desc 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 u_tbl2.col2 from u_tbl2 where 1 != 1", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl2.col2 from u_tbl2 where (col2) in ::fkc_vals 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_vals1", + "Cols": [ + 0 + ], + "Query": "update /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl3 set col3 = null where (col3) in ::fkc_vals1 and (cast(:__sq1 as CHAR) is null or (col3) not in ((cast(:__sq1 as CHAR))))", + "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 = :__sq1 where (col2) in ::fkc_vals", + "Table": "u_tbl2" + } + ] + }, + { + "InputName": "CascadeChild-2", + "OperatorType": "FkCascade", + "BvName": "fkc_vals2", + "Cols": [ + 0 + ], + "Inputs": [ + { + "InputName": "Selection", + "OperatorType": "Route", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "FieldQuery": "select u_tbl9.col9 from u_tbl9 where 1 != 1", + "Query": "select /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl9.col9 from u_tbl9 where (col9) in ::fkc_vals2 and (cast(:__sq1 as CHAR) is null or (col9) not in ((cast(:__sq1 as CHAR)))) lock in share mode", + "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 /*+ SET_VAR(foreign_key_checks=OFF) */ 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 /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl9 set col9 = null where (col9) in ::fkc_vals2 and (cast(:__sq1 as CHAR) is null or (col9) not in ((cast(:__sq1 as CHAR))))", + "Table": "u_tbl9" + } + ] + }, + { + "InputName": "Parent", + "OperatorType": "Update", + "Variant": "Unsharded", + "Keyspace": { + "Name": "unsharded_fk_allow", + "Sharded": false + }, + "TargetTabletType": "PRIMARY", + "Query": "update /*+ SET_VAR(foreign_key_checks=OFF) */ u_tbl1 set col1 = :__sq1 order by id desc", + "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" + ] + } } ] diff --git a/go/vt/vtgate/planbuilder/testdata/foreignkey_checks_on_cases.json b/go/vt/vtgate/planbuilder/testdata/foreignkey_checks_on_cases.json index 1f961a8c6e6..413f3246e38 100644 --- a/go/vt/vtgate/planbuilder/testdata/foreignkey_checks_on_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/foreignkey_checks_on_cases.json @@ -1252,7 +1252,7 @@ "QueryType": "DELETE", "Original": "delete from u_tbl2 limit 2", "Instructions": { - "OperatorType": "DeleteWithInput", + "OperatorType": "DMLWithInput", "TargetTabletType": "PRIMARY", "Offset": [ 0 @@ -1281,7 +1281,7 @@ "Sharded": false }, "FieldQuery": "select u_tbl2.col2 from u_tbl2 where 1 != 1", - "Query": "select u_tbl2.col2 from u_tbl2 where id in ::dm_vals for update", + "Query": "select u_tbl2.col2 from u_tbl2 where id in ::dml_vals for update", "Table": "u_tbl2" }, { @@ -1309,7 +1309,7 @@ "Sharded": false }, "TargetTabletType": "PRIMARY", - "Query": "delete /*+ SET_VAR(foreign_key_checks=On) */ from u_tbl2 where id in ::dm_vals", + "Query": "delete /*+ SET_VAR(foreign_key_checks=On) */ from u_tbl2 where id in ::dml_vals", "Table": "u_tbl2" } ] diff --git a/go/vt/vtgate/planbuilder/testdata/select_cases.json b/go/vt/vtgate/planbuilder/testdata/select_cases.json index 6c88aa1e6ec..1466135dd6c 100644 --- a/go/vt/vtgate/planbuilder/testdata/select_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/select_cases.json @@ -4044,7 +4044,7 @@ "Sharded": true }, "FieldQuery": "select id from (select id from (select music.id from music where 1 != 1) as subquery_for_limit where 1 != 1) as subquery_for_limit where 1 != 1", - "Query": "select id from (select id from (select music.id from music where music.user_id in ::__vals) as subquery_for_limit limit :__upper_limit) as subquery_for_limit limit :__upper_limit", + "Query": "select id from (select id from (select music.id from music where music.user_id in ::__vals) as subquery_for_limit limit :__upper_limit) as subquery_for_limit", "Table": "music", "Values": [ "(5, 6)" @@ -4103,7 +4103,7 @@ "Sharded": true }, "FieldQuery": "select id from (select id from (select music.id from music where 1 != 1) as subquery_for_limit where 1 != 1) as subquery_for_limit where 1 != 1", - "Query": "select id from (select id from (select music.id from music) as subquery_for_limit limit :__upper_limit) as subquery_for_limit limit :__upper_limit", + "Query": "select id from (select id from (select music.id from music) as subquery_for_limit limit :__upper_limit) as subquery_for_limit", "Table": "music" } ] diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json index 979b50c3d3d..59071c7e810 100644 --- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json @@ -39,11 +39,6 @@ "query": "delete from unsharded where col = (select id from unsharded join user on unsharded.id = user.id)", "plan": "VT12001: unsupported: subqueries in DML" }, - { - "comment": "scatter update with limit clause", - "query": "update user_extra set val = 1 where (name = 'foo' or id = 1) limit 1", - "plan": "VT12001: unsupported: multi shard UPDATE with LIMIT" - }, { "comment": "update changes primary vindex column", "query": "update user set id = 1 where id = 1", @@ -67,7 +62,7 @@ { "comment": "update by primary keyspace id, changing one vindex column, limit without order clause", "query": "update user_metadata set email = 'juan@vitess.io' where user_id = 1 limit 10", - "plan": "VT12001: unsupported: you need to provide the ORDER BY clause when using LIMIT; invalid update on vindex: email_user_map" + "plan": "VT12001: unsupported: Vindex update should have ORDER BY clause when using LIMIT" }, { "comment": "update with derived table", diff --git a/go/vt/vtgate/semantics/analyzer.go b/go/vt/vtgate/semantics/analyzer.go index 3c7470026d2..95d645d7d9d 100644 --- a/go/vt/vtgate/semantics/analyzer.go +++ b/go/vt/vtgate/semantics/analyzer.go @@ -328,8 +328,8 @@ func (a *analyzer) noteQuerySignature(node sqlparser.SQLNode) { } case sqlparser.AggrFunc: a.sig.Aggregation = true - case *sqlparser.Delete: - a.sig.Delete = true + case *sqlparser.Delete, *sqlparser.Update: + a.sig.Dml = true } } diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go index b5267b98cb6..949a1b72017 100644 --- a/go/vt/vtgate/semantics/semantic_state.go +++ b/go/vt/vtgate/semantics/semantic_state.go @@ -78,7 +78,7 @@ type ( // QuerySignature is used to identify shortcuts in the planning process QuerySignature struct { Aggregation bool - Delete bool + Dml bool Distinct bool HashJoin bool SubQueries bool @@ -923,3 +923,13 @@ func (st *SemTable) Clone(n sqlparser.SQLNode) sqlparser.SQLNode { cursor.Replace(sqlparser.CloneExpr(expr)) }, st.CopySemanticInfo) } + +func (st *SemTable) UpdateChildFKExpr(origUpdExpr *sqlparser.UpdateExpr, newExpr sqlparser.Expr) { + for _, exprs := range st.childFkToUpdExprs { + for idx, updateExpr := range exprs { + if updateExpr == origUpdExpr { + exprs[idx].Expr = newExpr + } + } + } +}