diff --git a/go/test/endtoend/vtgate/queries/dml/dml_test.go b/go/test/endtoend/vtgate/queries/dml/dml_test.go index 0210bbb0cba..9d060e99881 100644 --- a/go/test/endtoend/vtgate/queries/dml/dml_test.go +++ b/go/test/endtoend/vtgate/queries/dml/dml_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "vitess.io/vitess/go/test/endtoend/utils" ) @@ -78,3 +79,64 @@ func TestMultiTableDelete(t *testing.T) { mcmp.AssertMatches(`select oid, ename from oevent_tbl order by oid`, `[[INT64(1) VARCHAR("a")] [INT64(2) VARCHAR("b")] [INT64(3) VARCHAR("a")] [INT64(4) VARCHAR("c")]]`) } + +// TestDeleteWithLimit executed delete queries with limit +func TestDeleteWithLimit(t *testing.T) { + utils.SkipIfBinaryIsBelowVersion(t, 19, "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)]]`) + + // delete with limit + qr := mcmp.Exec(`delete from s_tbl order by num, id limit 3`) + require.EqualValues(t, 3, qr.RowsAffected) + + qr = mcmp.Exec(`delete from order_tbl 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(3) INT64(10)] [INT64(4) INT64(20)] [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 remain in table. + mcmp.AssertMatchesAnyNoCompare(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[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(2) INT64(3) INT64(5)] [INT64(2) INT64(4) INT64(55)]]`) + + // delete with limit + qr = mcmp.Exec(`delete from s_tbl where num < 20 limit 2`) + require.EqualValues(t, 2, qr.RowsAffected) + + qr = mcmp.Exec(`delete from order_tbl limit 5`) + require.EqualValues(t, 3, qr.RowsAffected) + + // check rows + // 3 rows matches `num < 20` but limit is 2 so any one of them can remain in the table. + mcmp.AssertMatchesAnyNoCompare(`select id, num from s_tbl order by id`, + `[[INT64(4) INT64(20)] [INT64(7) INT64(17)] [INT64(8) INT64(80)]]`, + `[[INT64(3) INT64(10)] [INT64(4) INT64(20)] [INT64(8) INT64(80)]]`, + `[[INT64(4) INT64(20)] [INT64(6) INT64(15)] [INT64(8) INT64(80)]]`) + mcmp.AssertMatches(`select region_id, oid, cust_no from order_tbl order by oid`, + `[]`) + + // remove all rows + mcmp.Exec(`delete from s_tbl`) + mcmp.Exec(`delete from order_tbl limit 5`) + + // try with limit again on empty table. + qr = mcmp.Exec(`delete from s_tbl where num < 20 limit 2`) + require.EqualValues(t, 0, qr.RowsAffected) + + qr = mcmp.Exec(`delete from order_tbl limit 5`) + require.EqualValues(t, 0, qr.RowsAffected) + +} diff --git a/go/vt/vtgate/engine/cached_size.go b/go/vt/vtgate/engine/cached_size.go index 19a329047c1..5f0e79f5608 100644 --- a/go/vt/vtgate/engine/cached_size.go +++ b/go/vt/vtgate/engine/cached_size.go @@ -189,13 +189,13 @@ func (cached *Delete) CachedSize(alloc bool) int64 { size += cached.DML.CachedSize(true) return size } -func (cached *DeleteMulti) CachedSize(alloc bool) int64 { +func (cached *DeleteWithInput) CachedSize(alloc bool) int64 { if cached == nil { return int64(0) } size := int64(0) if alloc { - size += int64(32) + size += int64(64) } // field Delete vitess.io/vitess/go/vt/vtgate/engine.Primitive if cc, ok := cached.Delete.(cachedObject); ok { @@ -205,6 +205,10 @@ func (cached *DeleteMulti) CachedSize(alloc bool) int64 { if cc, ok := cached.Input.(cachedObject); ok { size += cc.CachedSize(true) } + // field OutputCols []int + { + size += hack.RuntimeAllocSize(int64(cap(cached.OutputCols)) * int64(8)) + } return size } func (cached *Distinct) CachedSize(alloc bool) int64 { diff --git a/go/vt/vtgate/engine/delete_multi.go b/go/vt/vtgate/engine/delete_multi.go deleted file mode 100644 index d38eeed56af..00000000000 --- a/go/vt/vtgate/engine/delete_multi.go +++ /dev/null @@ -1,94 +0,0 @@ -/* -Copyright 2023 The Vitess Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package engine - -import ( - "context" - - topodatapb "vitess.io/vitess/go/vt/proto/topodata" - "vitess.io/vitess/go/vt/vterrors" - - "vitess.io/vitess/go/sqltypes" - querypb "vitess.io/vitess/go/vt/proto/query" -) - -var _ Primitive = (*DeleteMulti)(nil) - -const DM_VALS = "dm_vals" - -// DeleteMulti represents the instructions to perform a delete. -type DeleteMulti struct { - Delete Primitive - Input Primitive - - txNeeded -} - -func (del *DeleteMulti) RouteType() string { - return "DELETEMULTI" -} - -func (del *DeleteMulti) GetKeyspaceName() string { - return del.Input.GetKeyspaceName() -} - -func (del *DeleteMulti) GetTableName() string { - return del.Input.GetTableName() -} - -func (del *DeleteMulti) Inputs() ([]Primitive, []map[string]any) { - return []Primitive{del.Input, del.Delete}, nil -} - -// TryExecute performs a non-streaming exec. -func (del *DeleteMulti) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, _ bool) (*sqltypes.Result, error) { - inputRes, err := vcursor.ExecutePrimitive(ctx, del.Input, bindVars, false) - if err != nil { - return nil, err - } - - bv := &querypb.BindVariable{ - Type: querypb.Type_TUPLE, - } - for _, row := range inputRes.Rows { - bv.Values = append(bv.Values, sqltypes.TupleToProto(row)) - } - return vcursor.ExecutePrimitive(ctx, del.Delete, map[string]*querypb.BindVariable{ - DM_VALS: bv, - }, false) -} - -// TryStreamExecute performs a streaming exec. -func (del *DeleteMulti) 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) - if err != nil { - return err - } - return callback(res) -} - -// GetFields fetches the field info. -func (del *DeleteMulti) GetFields(context.Context, VCursor, map[string]*querypb.BindVariable) (*sqltypes.Result, error) { - return nil, vterrors.VT13001("unreachable code for MULTI DELETE") -} - -func (del *DeleteMulti) description() PrimitiveDescription { - return PrimitiveDescription{ - OperatorType: "DeleteMulti", - TargetTabletType: topodatapb.TabletType_PRIMARY, - } -} diff --git a/go/vt/vtgate/engine/delete_multi_test.go b/go/vt/vtgate/engine/delete_multi_test.go deleted file mode 100644 index d91c23f2d33..00000000000 --- a/go/vt/vtgate/engine/delete_multi_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2023 The Vitess Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package engine - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "vitess.io/vitess/go/sqltypes" - "vitess.io/vitess/go/vt/vtgate/vindexes" -) - -func TestDeleteMulti(t *testing.T) { - input := &fakePrimitive{results: []*sqltypes.Result{ - sqltypes.MakeTestResult(sqltypes.MakeTestFields("id", "int64"), "1", "2", "3"), - }} - - del := &DeleteMulti{ - Input: input, - Delete: &Delete{ - DML: &DML{ - RoutingParameters: &RoutingParameters{ - Opcode: Scatter, - Keyspace: &vindexes.Keyspace{ - Name: "ks", - Sharded: true, - }, - }, - Query: "dummy_delete", - }, - }, - } - - vc := newDMLTestVCursor("-20", "20-") - _, err := del.TryExecute(context.Background(), vc, nil, false) - require.NoError(t, err) - vc.ExpectLog(t, []string{ - `ResolveDestinations ks [] Destinations:DestinationAllShards()`, - `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x89\x02\x011"} values:{type:TUPLE value:"\x89\x02\x012"} values:{type:TUPLE value:"\x89\x02\x013"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x89\x02\x011"} values:{type:TUPLE value:"\x89\x02\x012"} values:{type:TUPLE value:"\x89\x02\x013"}} true false`, - }) - - vc.Rewind() - input.rewind() - err = del.TryStreamExecute(context.Background(), vc, nil, false, func(result *sqltypes.Result) error { return nil }) - require.NoError(t, err) - vc.ExpectLog(t, []string{ - `ResolveDestinations ks [] Destinations:DestinationAllShards()`, - `ExecuteMultiShard ` + - `ks.-20: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x89\x02\x011"} values:{type:TUPLE value:"\x89\x02\x012"} values:{type:TUPLE value:"\x89\x02\x013"}} ` + - `ks.20-: dummy_delete {dm_vals: type:TUPLE values:{type:TUPLE value:"\x89\x02\x011"} values:{type:TUPLE value:"\x89\x02\x012"} values:{type:TUPLE value:"\x89\x02\x013"}} true false`, - }) -} diff --git a/go/vt/vtgate/engine/delete_with_input.go b/go/vt/vtgate/engine/delete_with_input.go new file mode 100644 index 00000000000..9a0168b946b --- /dev/null +++ b/go/vt/vtgate/engine/delete_with_input.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "context" + + topodatapb "vitess.io/vitess/go/vt/proto/topodata" + "vitess.io/vitess/go/vt/vterrors" + + "vitess.io/vitess/go/sqltypes" + querypb "vitess.io/vitess/go/vt/proto/query" +) + +var _ Primitive = (*DeleteWithInput)(nil) + +const DmVals = "dm_vals" + +// DeleteWithInput represents the instructions to perform a delete operation based on the input result. +type DeleteWithInput struct { + Delete Primitive + Input Primitive + + OutputCols []int + + txNeeded +} + +func (del *DeleteWithInput) RouteType() string { + return "DeleteWithInput" +} + +func (del *DeleteWithInput) GetKeyspaceName() string { + return del.Input.GetKeyspaceName() +} + +func (del *DeleteWithInput) GetTableName() string { + return del.Input.GetTableName() +} + +func (del *DeleteWithInput) Inputs() ([]Primitive, []map[string]any) { + return []Primitive{del.Input, del.Delete}, 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) + if err != nil { + return nil, err + } + + var bv *querypb.BindVariable + if len(del.OutputCols) == 1 { + bv = getBVSingle(inputRes, del.OutputCols[0]) + } else { + bv = getBVMulti(inputRes, del.OutputCols) + } + + bindVars[DmVals] = bv + return vcursor.ExecutePrimitive(ctx, del.Delete, bindVars, false) +} + +func getBVSingle(res *sqltypes.Result, offset int) *querypb.BindVariable { + bv := &querypb.BindVariable{Type: querypb.Type_TUPLE} + for _, row := range res.Rows { + bv.Values = append(bv.Values, sqltypes.ValueToProto(row[offset])) + } + return bv +} + +func getBVMulti(res *sqltypes.Result, offsets []int) *querypb.BindVariable { + bv := &querypb.BindVariable{Type: querypb.Type_TUPLE} + outputVals := make([]sqltypes.Value, 0, len(offsets)) + for _, row := range res.Rows { + for _, offset := range offsets { + outputVals = append(outputVals, row[offset]) + } + bv.Values = append(bv.Values, sqltypes.TupleToProto(outputVals)) + outputVals = outputVals[:0] + } + return bv +} + +// 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) + if err != nil { + return err + } + return callback(res) +} + +// 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 (del *DeleteWithInput) description() PrimitiveDescription { + other := map[string]any{ + "Offset": del.OutputCols, + } + return PrimitiveDescription{ + OperatorType: "DeleteWithInput", + TargetTabletType: topodatapb.TabletType_PRIMARY, + Other: other, + } +} diff --git a/go/vt/vtgate/engine/delete_with_input_test.go b/go/vt/vtgate/engine/delete_with_input_test.go new file mode 100644 index 00000000000..b87e2bbc74a --- /dev/null +++ b/go/vt/vtgate/engine/delete_with_input_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2023 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "vitess.io/vitess/go/sqltypes" + querypb "vitess.io/vitess/go/vt/proto/query" + "vitess.io/vitess/go/vt/vtgate/vindexes" +) + +func TestDeleteWithInputSingleOffset(t *testing.T) { + input := &fakePrimitive{results: []*sqltypes.Result{ + sqltypes.MakeTestResult(sqltypes.MakeTestFields("id", "int64"), "1", "2", "3"), + }} + + del := &DeleteWithInput{ + Input: input, + Delete: &Delete{ + DML: &DML{ + RoutingParameters: &RoutingParameters{ + Opcode: Scatter, + Keyspace: &vindexes.Keyspace{ + Name: "ks", + Sharded: true, + }, + }, + Query: "dummy_delete", + }, + }, + OutputCols: []int{0}, + } + + vc := newDMLTestVCursor("-20", "20-") + _, err := del.TryExecute(context.Background(), vc, map[string]*querypb.BindVariable{}, false) + require.NoError(t, err) + 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`, + }) + + vc.Rewind() + input.rewind() + err = del.TryStreamExecute(context.Background(), vc, map[string]*querypb.BindVariable{}, false, func(result *sqltypes.Result) error { return nil }) + require.NoError(t, err) + 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`, + }) +} + +func TestDeleteWithInputMultiOffset(t *testing.T) { + input := &fakePrimitive{results: []*sqltypes.Result{ + sqltypes.MakeTestResult(sqltypes.MakeTestFields("id|col", "int64|varchar"), "1|a", "2|b", "3|c"), + }} + + del := &DeleteWithInput{ + Input: input, + Delete: &Delete{ + DML: &DML{ + RoutingParameters: &RoutingParameters{ + Opcode: Scatter, + Keyspace: &vindexes.Keyspace{ + Name: "ks", + Sharded: true, + }, + }, + Query: "dummy_delete", + }, + }, + OutputCols: []int{1, 0}, + } + + vc := newDMLTestVCursor("-20", "20-") + _, err := del.TryExecute(context.Background(), vc, map[string]*querypb.BindVariable{}, false) + require.NoError(t, err) + 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`, + }) + + vc.Rewind() + input.rewind() + err = del.TryStreamExecute(context.Background(), vc, map[string]*querypb.BindVariable{}, false, func(result *sqltypes.Result) error { return nil }) + require.NoError(t, err) + 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`, + }) +} diff --git a/go/vt/vtgate/executor_dml_test.go b/go/vt/vtgate/executor_dml_test.go index b427e198e5e..91089c61e67 100644 --- a/go/vt/vtgate/executor_dml_test.go +++ b/go/vt/vtgate/executor_dml_test.go @@ -3018,11 +3018,11 @@ func TestInsertReference(t *testing.T) { require.NoError(t, err) // Gen4 planner can redirect the query to correct source for update when reference table is involved. } -func TestDeleteMulti(t *testing.T) { +func TestDeleteMultiTable(t *testing.T) { executor, sbc1, sbc2, sbclookup, ctx := createExecutorEnv(t) executor.vschema.Keyspaces["TestExecutor"].Tables["user"].PrimaryKey = sqlparser.Columns{sqlparser.NewIdentifierCI("id")} - logChan := executor.queryLogger.Subscribe("TestDeleteMulti") + logChan := executor.queryLogger.Subscribe("TestDeleteMultiTable") defer executor.queryLogger.Unsubscribe(logChan) session := &vtgatepb.Session{TargetString: "@primary"} @@ -3031,7 +3031,7 @@ func TestDeleteMulti(t *testing.T) { var dmlVals []*querypb.Value for i := 0; i < 8; i++ { - dmlVals = append(dmlVals, sqltypes.TupleToProto([]sqltypes.Value{sqltypes.TestValue(sqltypes.Int32, "1")})) + dmlVals = append(dmlVals, sqltypes.ValueToProto(sqltypes.NewInt32(1))) } bq := &querypb.BoundQuery{ @@ -3041,14 +3041,14 @@ func TestDeleteMulti(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 ::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}}}} 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 ::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}}}, } assertQueries(t, sbc2, wantQueries) diff --git a/go/vt/vtgate/planbuilder/delete_multi.go b/go/vt/vtgate/planbuilder/delete_with_input.go similarity index 76% rename from go/vt/vtgate/planbuilder/delete_multi.go rename to go/vt/vtgate/planbuilder/delete_with_input.go index 9365835d089..9b6ebb08d3b 100644 --- a/go/vt/vtgate/planbuilder/delete_multi.go +++ b/go/vt/vtgate/planbuilder/delete_with_input.go @@ -20,19 +20,22 @@ import ( "vitess.io/vitess/go/vt/vtgate/engine" ) -type deleteMulti struct { +type deleteWithInput struct { input logicalPlan delete logicalPlan + + outputCols []int } -var _ logicalPlan = (*deleteMulti)(nil) +var _ logicalPlan = (*deleteWithInput)(nil) // Primitive implements the logicalPlan interface -func (d *deleteMulti) Primitive() engine.Primitive { +func (d *deleteWithInput) Primitive() engine.Primitive { inp := d.input.Primitive() del := d.delete.Primitive() - return &engine.DeleteMulti{ - Delete: del, - Input: inp, + return &engine.DeleteWithInput{ + Delete: 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 8cfab78bcfa..f4cdce82a53 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -74,14 +74,14 @@ func transformToLogicalPlan(ctx *plancontext.PlanningContext, op operators.Opera return transformHashJoin(ctx, op) case *operators.Sequential: return transformSequential(ctx, op) - case *operators.DeleteMulti: - return transformDeleteMulti(ctx, op) + case *operators.DeleteWithInput: + return transformDeleteWithInput(ctx, op) } return nil, vterrors.VT13001(fmt.Sprintf("unknown type encountered: %T (transformToLogicalPlan)", op)) } -func transformDeleteMulti(ctx *plancontext.PlanningContext, op *operators.DeleteMulti) (logicalPlan, error) { +func transformDeleteWithInput(ctx *plancontext.PlanningContext, op *operators.DeleteWithInput) (logicalPlan, error) { input, err := transformToLogicalPlan(ctx, op.Source) if err != nil { return nil, err @@ -91,9 +91,10 @@ func transformDeleteMulti(ctx *plancontext.PlanningContext, op *operators.Delete if err != nil { return nil, err } - return &deleteMulti{ - input: input, - delete: del, + return &deleteWithInput{ + input: input, + delete: del, + outputCols: op.Offsets, }, nil } diff --git a/go/vt/vtgate/planbuilder/operators/SQL_builder.go b/go/vt/vtgate/planbuilder/operators/SQL_builder.go index e5c7d6dcc7f..d042ff3032e 100644 --- a/go/vt/vtgate/planbuilder/operators/SQL_builder.go +++ b/go/vt/vtgate/planbuilder/operators/SQL_builder.go @@ -401,8 +401,8 @@ func buildDelete(op *Delete, qb *queryBuilder) { Targets: sqlparser.TableNames{op.Target.Name}, TableExprs: sel.From, Where: sel.Where, - OrderBy: op.OrderBy, - Limit: op.Limit, + Limit: sel.Limit, + OrderBy: sel.OrderBy, } } diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index 8761b55a82d..e074e2685ef 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -29,8 +29,6 @@ import ( type Delete struct { Target TargetTable OwnedVindexQuery *sqlparser.Select - OrderBy sqlparser.OrderBy - Limit *sqlparser.Limit Ignore bool Source Operator @@ -76,16 +74,7 @@ func (d *Delete) GetOrdering(*plancontext.PlanningContext) []OrderBy { } func (d *Delete) ShortDescription() string { - limit := "" - orderBy := "" - if d.Limit != nil { - limit = " " + sqlparser.String(d.Limit) - } - if len(d.OrderBy) > 0 { - orderBy = " " + sqlparser.String(d.OrderBy) - } - - return fmt.Sprintf("%s.%s%s%s", d.Target.VTable.Keyspace.Name, d.Target.VTable.Name.String(), orderBy, limit) + return fmt.Sprintf("%s.%s", d.Target.VTable.Keyspace.Name, d.Target.VTable.Name.String()) } func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlparser.Delete) (op Operator) { @@ -157,14 +146,48 @@ func createDeleteOperator(ctx *plancontext.PlanningContext, del *sqlparser.Delet } } - return &Delete{ + delOp := &Delete{ Target: targetTbl, Source: op, Ignore: bool(del.Ignore), - Limit: del.Limit, - OrderBy: del.OrderBy, OwnedVindexQuery: ovq, } + + if del.Limit == nil { + return delOp + } + + addOrdering(ctx, del, delOp) + + delOp.Source = &Limit{ + Source: delOp.Source, + AST: del.Limit, + } + + return delOp +} + +func addOrdering(ctx *plancontext.PlanningContext, del *sqlparser.Delete, delOp *Delete) { + es := &expressionSet{} + ordering := &Ordering{ + Source: delOp.Source, + } + for _, order := range del.OrderBy { + if sqlparser.IsNull(order.Expr) { + // ORDER BY null can safely be ignored + continue + } + if !es.add(ctx, order.Expr) { + continue + } + ordering.Order = append(ordering.Order, OrderBy{ + Inner: sqlparser.CloneRefOfOrder(order), + SimplifiedExpr: order.Expr, + }) + } + if len(ordering.Order) > 0 { + delOp.Source = ordering + } } func updateQueryGraphWithSource(ctx *plancontext.PlanningContext, input Operator, tblID semantics.TableSet, vTbl *vindexes.Table) *vindexes.Table { diff --git a/go/vt/vtgate/planbuilder/operators/delete_multi.go b/go/vt/vtgate/planbuilder/operators/delete_multi.go deleted file mode 100644 index 00006418962..00000000000 --- a/go/vt/vtgate/planbuilder/operators/delete_multi.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2023 The Vitess Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package operators - -import "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" - -type DeleteMulti struct { - Source Operator - Delete Operator - - noColumns - noPredicates -} - -func (d *DeleteMulti) Clone(inputs []Operator) Operator { - newD := *d - newD.SetInputs(inputs) - return &newD -} - -func (d *DeleteMulti) Inputs() []Operator { - return []Operator{d.Source, d.Delete} -} - -func (d *DeleteMulti) SetInputs(inputs []Operator) { - if len(inputs) != 2 { - panic("unexpected number of inputs for DeleteMulti operator") - } - d.Source = inputs[0] - d.Delete = inputs[1] -} - -func (d *DeleteMulti) ShortDescription() string { - return "" -} - -func (d *DeleteMulti) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy { - return nil -} - -var _ Operator = (*DeleteMulti)(nil) diff --git a/go/vt/vtgate/planbuilder/operators/delete_with_input.go b/go/vt/vtgate/planbuilder/operators/delete_with_input.go new file mode 100644 index 00000000000..56b468e7ead --- /dev/null +++ b/go/vt/vtgate/planbuilder/operators/delete_with_input.go @@ -0,0 +1,83 @@ +/* +Copyright 2023 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operators + +import ( + "fmt" + + "vitess.io/vitess/go/slice" + "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" +) + +type DeleteWithInput struct { + Source Operator + Delete Operator + + cols []*sqlparser.ColName + Offsets []int + + noColumns + noPredicates +} + +func (d *DeleteWithInput) Clone(inputs []Operator) Operator { + newD := *d + newD.SetInputs(inputs) + return &newD +} + +func (d *DeleteWithInput) Inputs() []Operator { + return []Operator{d.Source, d.Delete} +} + +func (d *DeleteWithInput) SetInputs(inputs []Operator) { + if len(inputs) != 2 { + panic("unexpected number of inputs for DeleteWithInput operator") + } + d.Source = inputs[0] + d.Delete = inputs[1] +} + +func (d *DeleteWithInput) ShortDescription() string { + colStrings := slice.Map(d.cols, func(from *sqlparser.ColName) string { + return sqlparser.String(from) + }) + out := "" + for idx, colString := range colStrings { + out += colString + if len(d.Offsets) > idx { + out += fmt.Sprintf(":%d", d.Offsets[idx]) + } + out += " " + } + return out +} + +func (d *DeleteWithInput) GetOrdering(ctx *plancontext.PlanningContext) []OrderBy { + return nil +} + +func (d *DeleteWithInput) 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) + } + return d +} + +var _ Operator = (*DeleteWithInput)(nil) diff --git a/go/vt/vtgate/planbuilder/operators/phases.go b/go/vt/vtgate/planbuilder/operators/phases.go index e9c35568400..011e366f5e0 100644 --- a/go/vt/vtgate/planbuilder/operators/phases.go +++ b/go/vt/vtgate/planbuilder/operators/phases.go @@ -35,6 +35,7 @@ const ( delegateAggregation addAggrOrdering cleanOutPerfDistinct + deleteWithInput subquerySettling DONE ) @@ -55,6 +56,8 @@ func (p Phase) String() string { return "optimize Distinct operations" case subquerySettling: return "settle subqueries" + case deleteWithInput: + return "expand delete to delete with input" default: panic(vterrors.VT13001("unhandled default case")) } @@ -72,6 +75,8 @@ func (p Phase) shouldRun(s semantics.QuerySignature) bool { return s.Distinct case subquerySettling: return s.SubQueries + case deleteWithInput: + return s.Delete default: return true } @@ -89,6 +94,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) default: return op } @@ -113,6 +120,19 @@ func (p *phaser) next(ctx *plancontext.PlanningContext) Phase { } } +func findDeletesAboveRoute(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 + } + + return createDeleteWithInput(ctx, delOp, delOp.Source) + } + + return BottomUp(root, TableID, visitor, stopAtRoute) +} + 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 c0cdce4c4d6..135126a15bd 100644 --- a/go/vt/vtgate/planbuilder/operators/query_planning.go +++ b/go/vt/vtgate/planbuilder/operators/query_planning.go @@ -99,7 +99,7 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { case *LockAndComment: return pushLockAndComment(in) case *Delete: - return tryPushDelete(ctx, in) + return tryPushDelete(in) default: return in, NoRewrite } @@ -108,21 +108,14 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { return FixedPointBottomUp(root, TableID, visitor, stopAtRoute) } -func tryPushDelete(ctx *plancontext.PlanningContext, in *Delete) (Operator, *ApplyResult) { - switch src := in.Source.(type) { - case *Route: +func tryPushDelete(in *Delete) (Operator, *ApplyResult) { + if src, ok := in.Source.(*Route); ok { return pushDeleteUnderRoute(in, src) - case *ApplyJoin: - return pushDeleteUnderJoin(ctx, in, src) } - return in, nil + return in, NoRewrite } func pushDeleteUnderRoute(in *Delete, src *Route) (Operator, *ApplyResult) { - if in.Limit != nil && !src.IsSingleShardOrByDestination() { - panic(vterrors.VT12001("multi shard DELETE with LIMIT")) - } - switch r := src.Routing.(type) { case *SequenceRouting: // Sequences are just unsharded routes @@ -137,27 +130,22 @@ func pushDeleteUnderRoute(in *Delete, src *Route) (Operator, *ApplyResult) { return Swap(in, src, "pushed delete under route") } -func pushDeleteUnderJoin(ctx *plancontext.PlanningContext, in *Delete, src Operator) (Operator, *ApplyResult) { +func createDeleteWithInput(ctx *plancontext.PlanningContext, in *Delete, src Operator) (Operator, *ApplyResult) { if len(in.Target.VTable.PrimaryKey) == 0 { panic(vterrors.VT09015()) } - dm := &DeleteMulti{} - var selExprs sqlparser.SelectExprs + dm := &DeleteWithInput{} var leftComp sqlparser.ValTuple + proj := newAliasedProjection(src) for _, col := range in.Target.VTable.PrimaryKey { colName := sqlparser.NewColNameWithQualifier(col.String(), in.Target.Name) - selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) + proj.AddColumn(ctx, true, false, aeWrap(colName)) + dm.cols = append(dm.cols, colName) leftComp = append(leftComp, colName) ctx.SemTable.Recursive[colName] = in.Target.ID } - sel := &sqlparser.Select{ - SelectExprs: selExprs, - OrderBy: in.OrderBy, - Limit: in.Limit, - Lock: sqlparser.ForUpdateLock, - } - dm.Source = newHorizon(src, sel) + dm.Source = proj var targetTable *Table _ = Visit(src, func(operator Operator) error { @@ -170,7 +158,13 @@ func pushDeleteUnderJoin(ctx *plancontext.PlanningContext, in *Delete, src Opera if targetTable == nil { panic(vterrors.VT13001("target DELETE table not found")) } - compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, leftComp, sqlparser.ListArg(engine.DM_VALS), nil) + + // 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, @@ -188,7 +182,7 @@ func pushDeleteUnderJoin(ctx *plancontext.PlanningContext, in *Delete, src Opera } dm.Delete = in - return dm, Rewrote("Delete Multi on top of Delete and ApplyJoin") + return dm, Rewrote("changed Delete to DeleteWithInput") } func pushLockAndComment(l *LockAndComment) (Operator, *ApplyResult) { @@ -543,7 +537,7 @@ func tryPushLimit(in *Limit) (Operator, *ApplyResult) { } func tryPushingDownLimitInRoute(in *Limit, src *Route) (Operator, *ApplyResult) { - if src.IsSingleShard() { + if src.IsSingleShardOrByDestination() { return Swap(in, src, "push limit under route") } diff --git a/go/vt/vtgate/planbuilder/testdata/dml_cases.json b/go/vt/vtgate/planbuilder/testdata/dml_cases.json index 2e8053c2e78..99306aac737 100644 --- a/go/vt/vtgate/planbuilder/testdata/dml_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/dml_cases.json @@ -5055,8 +5055,11 @@ "QueryType": "DELETE", "Original": "delete user from user join user_extra on user.id = user_extra.id where user.name = 'foo'", "Instructions": { - "OperatorType": "DeleteMulti", + "OperatorType": "DeleteWithInput", "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], "Inputs": [ { "OperatorType": "Join", @@ -5097,7 +5100,7 @@ }, { "OperatorType": "Delete", - "Variant": "MultiEqual", + "Variant": "IN", "Keyspace": { "Name": "user", "Sharded": true @@ -5105,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 ::dm_vals for update", + "Query": "delete from `user` where `user`.id in ::dm_vals", "Table": "user", "Values": [ - "dm_vals:0" + "::dm_vals" ], "Vindex": "user_index" } @@ -5128,8 +5131,11 @@ "QueryType": "DELETE", "Original": "delete u from user u join music m on u.col = m.col", "Instructions": { - "OperatorType": "DeleteMulti", + "OperatorType": "DeleteWithInput", "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], "Inputs": [ { "OperatorType": "Join", @@ -5166,7 +5172,7 @@ }, { "OperatorType": "Delete", - "Variant": "MultiEqual", + "Variant": "IN", "Keyspace": { "Name": "user", "Sharded": true @@ -5174,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 ::dm_vals for update", + "Query": "delete from `user` as u where u.id in ::dm_vals", "Table": "user", "Values": [ - "dm_vals:0" + "::dm_vals" ], "Vindex": "user_index" } @@ -5197,8 +5203,11 @@ "QueryType": "DELETE", "Original": "delete u from music m join user u where u.col = m.col and m.foo = 42", "Instructions": { - "OperatorType": "DeleteMulti", + "OperatorType": "DeleteWithInput", "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], "Inputs": [ { "OperatorType": "Join", @@ -5235,7 +5244,7 @@ }, { "OperatorType": "Delete", - "Variant": "MultiEqual", + "Variant": "IN", "Keyspace": { "Name": "user", "Sharded": true @@ -5243,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 ::dm_vals for update", + "Query": "delete from `user` as u where u.id in ::dm_vals", "Table": "user", "Values": [ - "dm_vals:0" + "::dm_vals" ], "Vindex": "user_index" } @@ -5292,8 +5301,11 @@ "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": "DeleteMulti", + "OperatorType": "DeleteWithInput", "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], "Inputs": [ { "OperatorType": "Join", @@ -5330,7 +5342,7 @@ }, { "OperatorType": "Delete", - "Variant": "MultiEqual", + "Variant": "IN", "Keyspace": { "Name": "user", "Sharded": true @@ -5338,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 ::dm_vals for update", + "Query": "delete from `user` as u where u.id in ::dm_vals", "Table": "user", "Values": [ - "dm_vals:0" + "::dm_vals" ], "Vindex": "user_index" } @@ -5362,8 +5374,11 @@ "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": "DeleteMulti", + "OperatorType": "DeleteWithInput", "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], "Inputs": [ { "OperatorType": "Join", @@ -5400,7 +5415,7 @@ }, { "OperatorType": "Delete", - "Variant": "MultiEqual", + "Variant": "IN", "Keyspace": { "Name": "user", "Sharded": true @@ -5408,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 ::dm_vals for update", + "Query": "delete from music as m where m.id in ::dm_vals", "Table": "music", "Values": [ - "dm_vals:0" + "::dm_vals" ], "Vindex": "music_user_map" } @@ -5424,5 +5439,116 @@ "user.user_extra" ] } + }, + { + "comment": "sharded delete with limit clause", + "query": "delete from user limit 10", + "plan": { + "QueryType": "DELETE", + "Original": "delete from user limit 10", + "Instructions": { + "OperatorType": "DeleteWithInput", + "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], + "Inputs": [ + { + "OperatorType": "Limit", + "Count": "10", + "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` limit :__upper_limit", + "Table": "`user`" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "IN", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user", + "Values": [ + "::dm_vals" + ], + "Vindex": "user_index" + } + ] + }, + "TablesUsed": [ + "user.user" + ] + } + }, + { + "comment": "sharded delete with order by and limit clause", + "query": "delete from user order by name, col limit 5", + "plan": { + "QueryType": "DELETE", + "Original": "delete from user order by name, col limit 5", + "Instructions": { + "OperatorType": "DeleteWithInput", + "TargetTabletType": "PRIMARY", + "Offset": [ + 0 + ], + "Inputs": [ + { + "OperatorType": "Limit", + "Count": "5", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id, `name`, weight_string(`name`), col from `user` where 1 != 1", + "OrderBy": "(1|2) ASC, 3 ASC", + "Query": "select `user`.id, `name`, weight_string(`name`), col from `user` order by `name` asc, col asc limit :__upper_limit", + "Table": "`user`" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "IN", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user", + "Values": [ + "::dm_vals" + ], + "Vindex": "user_index" + } + ] + }, + "TablesUsed": [ + "user.user" + ] + } } ] diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json index f78e2749899..4e6fb251be8 100644 --- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json @@ -29,11 +29,6 @@ "query": "delete from unsharded where col = (select id from user)", "plan": "VT12001: unsupported: subqueries in DML" }, - { - "comment": "sharded delete with limit clasue", - "query": "delete from user_extra limit 10", - "plan": "VT12001: unsupported: multi shard DELETE with LIMIT" - }, { "comment": "sharded subquery in unsharded subquery in unsharded delete", "query": "delete from unsharded where col = (select id from unsharded where id = (select id from user))", diff --git a/go/vt/vtgate/semantics/analyzer.go b/go/vt/vtgate/semantics/analyzer.go index eba50d47286..e6252a60ca3 100644 --- a/go/vt/vtgate/semantics/analyzer.go +++ b/go/vt/vtgate/semantics/analyzer.go @@ -316,6 +316,8 @@ func (a *analyzer) noteQuerySignature(node sqlparser.SQLNode) { } case sqlparser.AggrFunc: a.sig.Aggregation = true + case *sqlparser.Delete: + a.sig.Delete = true } } diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go index d52875de9b8..08b89027b83 100644 --- a/go/vt/vtgate/semantics/semantic_state.go +++ b/go/vt/vtgate/semantics/semantic_state.go @@ -77,11 +77,12 @@ type ( // QuerySignature is used to identify shortcuts in the planning process QuerySignature struct { - Union, - Aggregation, - Distinct, - SubQueries, - HashJoin bool + Aggregation bool + Delete bool + Distinct bool + HashJoin bool + SubQueries bool + Union bool } // SemTable contains semantic analysis information about the query.