diff --git a/changelog/19.0/19.0.0/summary.md b/changelog/19.0/19.0.0/summary.md index 54071fded14..cde1608562a 100644 --- a/changelog/19.0/19.0.0/summary.md +++ b/changelog/19.0/19.0.0/summary.md @@ -11,17 +11,17 @@ - **[New Stats](#new-stats)** - [Stream Consolidations](#stream-consolidations) - [Build Version in `/debug/vars`](#build-version-in-debug-vars) - - **[VTGate](#vtgate)** + - **[Planned Reparent Shard](#planned-reparent-shard)** + - [`--tolerable-replication-lag` Sub-flag](#tolerable-repl-lag) + - **[Query Compatibility](#query-compatibility)** + - [Multi Table Delete Support](#multi-table-delete) + - [`SHOW VSCHEMA KEYSPACES` Query](#show-vschema-keyspaces) - [`FOREIGN_KEY_CHECKS` is now a Vitess Aware Variable](#fk-checks-vitess-aware) - **[Vttestserver](#vttestserver)** - [`--vtcombo-bind-host` flag](#vtcombo-bind-host) - - **[Query Compatibility](#query-compatibility)** - - [`SHOW VSCHEMA KEYSPACES` Query](#show-vschema-keyspaces) - - **[Planned Reparent Shard](#planned-reparent-shard)** - - [`--tolerable-replication-lag` Sub-flag](#tolerable-repl-lag) - **[Minor Changes](#minor-changes)** - - **[Apply VSchema](#apply-vschema)** - - [`--strict` sub-flag and `strict` gRPC field](#strict-flag-and-field) + - **[Apply VSchema](#apply-vschema)** + - [`--strict` sub-flag and `strict` gRPC field](#strict-flag-and-field) ## Major Changes @@ -32,6 +32,7 @@ upgrading to v19. Vitess will however, continue to support importing from MySQL 5.7 into Vitess even in v19. + ### Deprecations and Deletions - The `MYSQL_FLAVOR` environment variable is now removed from all Docker Images. @@ -42,6 +43,7 @@ Vitess will however, continue to support importing from MySQL 5.7 into Vitess ev `--vreplication_healthcheck_topology_refresh`, `--vreplication_healthcheck_retry_delay`, and `--vreplication_healthcheck_timeout`. - The `--vreplication_tablet_type` flag is now deprecated and ignored. + ### Docker #### New MySQL Image @@ -51,7 +53,8 @@ This lightweight image is a replacement of `vitess/lite` to only run `mysqld`. Several tags are available to let you choose what version of MySQL you want to use: `vitess/mysql:8.0.30`, `vitess/mysql:8.0.34`. -### new stats + +### New Stats #### Stream Consolidations @@ -61,27 +64,34 @@ Prior to 19.0 VTTablet reported how much time non-streaming executions spend wai The build version (e.g., `19.0.0-SNAPSHOT`) has been added to `/debug/vars`, allowing users to programmatically inspect Vitess components' build version at runtime. -### VTGate -#### `FOREIGN_KEY_CHECKS` is now a Vitess Aware Variable +### Planned Reparent Shard -When VTGate receives a query to change the `FOREIGN_KEY_CHECKS` value for a session, instead of sending the value down to MySQL, VTGate now keeps track of the value and changes the queries by adding `SET_VAR(FOREIGN_KEY_CHECKS=On/Off)` style query optimizer hints wherever required. +#### `--tolerable-replication-lag` Sub-flag -### Vttestserver +A new sub-flag `--tolerable-replication-lag` has been added to the command `PlannedReparentShard` that allows users to specify the amount of replication lag that is considered acceptable for a tablet to be eligible for promotion when Vitess makes the choice of a new primary. +This feature is opt-in and not specifying this sub-flag makes Vitess ignore the replication lag entirely. -#### `--vtcombo-bind-host` flag +A new flag in VTOrc with the same name has been added to control the behaviour of the PlannedReparentShard calls that VTOrc issues. -A new flag `--vtcombo-bind-host` has been added to vttestserver that allows the users to configure the bind host that vtcombo uses. This is especially useful when running vttestserver as a docker image and you want to run vtctld commands and look at the vtcombo `/debug/status` dashboard. ### Query Compatibility +#### Multi Table Delete Support + +Support is added for sharded multi-table delete with target on single table using multiple table join. + +Example: `Delete t1 from t1 join t2 on t1.id = t2.id join t3 on t1.col = t3.col where t3.foo = 5 and t2.bar = 7` + +More details about how it works is available in [MySQL Docs](https://dev.mysql.com/doc/refman/8.0/en/delete.html) + #### `SHOW VSCHEMA KEYSPACES` Query A SQL query, `SHOW VSCHEMA KEYSPACES` is now supported in Vitess. This query prints the vschema information for all the keyspaces. It is useful for seeing the foreign key mode, whether the keyspace is sharded, and if there is an error in the VSchema for the keyspace. -An example output of the query looks like - +An example output of the query looks like - ```sql mysql> show vschema keyspaces; +----------+---------+-------------+---------+ @@ -93,14 +103,16 @@ mysql> show vschema keyspaces; 2 rows in set (0.01 sec) ``` -### Planned Reparent Shard +#### `FOREIGN_KEY_CHECKS` is now a Vitess Aware Variable -#### `--tolerable-replication-lag` Sub-flag +When VTGate receives a query to change the `FOREIGN_KEY_CHECKS` value for a session, instead of sending the value down to MySQL, VTGate now keeps track of the value and changes the queries by adding `SET_VAR(FOREIGN_KEY_CHECKS=On/Off)` style query optimizer hints wherever required. -A new sub-flag `--tolerable-replication-lag` has been added to the command `PlannedReparentShard` that allows users to specify the amount of replication lag that is considered acceptable for a tablet to be eligible for promotion when Vitess makes the choice of a new primary. -This feature is opt-in and not specifying this sub-flag makes Vitess ignore the replication lag entirely. +### Vttestserver + +#### `--vtcombo-bind-host` flag + +A new flag `--vtcombo-bind-host` has been added to vttestserver that allows the users to configure the bind host that vtcombo uses. This is especially useful when running vttestserver as a docker image and you want to run vtctld commands and look at the vtcombo `/debug/status` dashboard. -A new flag in VTOrc with the same name has been added to control the behaviour of the PlannedReparentShard calls that VTOrc issues. ## Minor Changes diff --git a/go/test/endtoend/vtgate/queries/dml/dml_test.go b/go/test/endtoend/vtgate/queries/dml/dml_test.go index 52a64acaa56..0210bbb0cba 100644 --- a/go/test/endtoend/vtgate/queries/dml/dml_test.go +++ b/go/test/endtoend/vtgate/queries/dml/dml_test.go @@ -20,6 +20,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "vitess.io/vitess/go/test/endtoend/utils" ) func TestMultiEqual(t *testing.T) { @@ -39,3 +41,40 @@ func TestMultiEqual(t *testing.T) { qr = mcmp.Exec("delete from user_tbl where (id, region_id) in ((1,1), (2,4))") assert.EqualValues(t, 1, qr.RowsAffected) } + +// TestMultiTableDelete executed multi-table delete queries +func TestMultiTableDelete(t *testing.T) { + utils.SkipIfBinaryIsBelowVersion(t, 19, "vtgate") + + mcmp, closer := start(t) + defer closer() + + // initial rows + mcmp.Exec("insert into order_tbl(region_id, oid, cust_no) values (1,1,4), (1,2,2), (2,3,5), (2,4,55)") + mcmp.Exec("insert into oevent_tbl(oid, ename) values (1,'a'), (2,'b'), (3,'a'), (4,'c')") + + // check rows + 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)]]`) + 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")]]`) + + // multi table delete + qr := mcmp.Exec(`delete o from order_tbl o join oevent_tbl ev where o.oid = ev.oid and ev.ename = 'a'`) + assert.EqualValues(t, 2, qr.RowsAffected) + + // check rows + mcmp.AssertMatches(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[INT64(1) INT64(2) INT64(2)] [INT64(2) INT64(4) INT64(55)]]`) + 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")]]`) + + qr = mcmp.Exec(`delete o from order_tbl o join oevent_tbl ev where o.cust_no = ev.oid`) + assert.EqualValues(t, 1, qr.RowsAffected) + + // check rows + mcmp.AssertMatches(`select region_id, oid, cust_no from order_tbl order by oid`, + `[[INT64(2) INT64(4) INT64(55)]]`) + 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")]]`) +} diff --git a/go/vt/sqlparser/ast_funcs.go b/go/vt/sqlparser/ast_funcs.go index 1de529c973b..54195ed435c 100644 --- a/go/vt/sqlparser/ast_funcs.go +++ b/go/vt/sqlparser/ast_funcs.go @@ -2551,3 +2551,11 @@ func (node *Delete) isSingleAliasExpr() bool { _, isAliasExpr := node.TableExprs[0].(*AliasedTableExpr) return isAliasExpr } + +func (node TableExprs) MultiTable() bool { + if len(node) > 1 { + return true + } + _, singleTbl := node[0].(*AliasedTableExpr) + return !singleTbl +} diff --git a/go/vt/vtgate/engine/cached_size.go b/go/vt/vtgate/engine/cached_size.go index 93c5e97bd89..19a329047c1 100644 --- a/go/vt/vtgate/engine/cached_size.go +++ b/go/vt/vtgate/engine/cached_size.go @@ -189,6 +189,24 @@ func (cached *Delete) CachedSize(alloc bool) int64 { size += cached.DML.CachedSize(true) return size } +func (cached *DeleteMulti) CachedSize(alloc bool) int64 { + if cached == nil { + return int64(0) + } + size := int64(0) + if alloc { + size += int64(32) + } + // field Delete vitess.io/vitess/go/vt/vtgate/engine.Primitive + if cc, ok := cached.Delete.(cachedObject); ok { + size += cc.CachedSize(true) + } + // field Input vitess.io/vitess/go/vt/vtgate/engine.Primitive + if cc, ok := cached.Input.(cachedObject); ok { + size += cc.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_multi.go b/go/vt/vtgate/engine/delete_multi.go new file mode 100644 index 00000000000..d38eeed56af --- /dev/null +++ b/go/vt/vtgate/engine/delete_multi.go @@ -0,0 +1,94 @@ +/* +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 new file mode 100644 index 00000000000..d91c23f2d33 --- /dev/null +++ b/go/vt/vtgate/engine/delete_multi_test.go @@ -0,0 +1,70 @@ +/* +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/executor_dml_test.go b/go/vt/vtgate/executor_dml_test.go index 4ef598d2e61..3d48e96dabc 100644 --- a/go/vt/vtgate/executor_dml_test.go +++ b/go/vt/vtgate/executor_dml_test.go @@ -3017,3 +3017,64 @@ func TestInsertReference(t *testing.T) { _, err = executorExec(ctx, executor, session, "insert into TestExecutor.zip_detail(id, status) values (1, 'CLOSED')", nil) 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) { + executor, sbc1, sbc2, sbclookup, ctx := createExecutorEnv(t) + executor.vschema.Keyspaces["TestExecutor"].Tables["user"].PrimaryKey = sqlparser.Columns{sqlparser.NewIdentifierCI("id")} + + logChan := executor.queryLogger.Subscribe("TestDeleteMulti") + defer executor.queryLogger.Unsubscribe(logChan) + + session := &vtgatepb.Session{TargetString: "@primary"} + _, err := executorExec(ctx, executor, session, "delete user from user join music on user.col = music.col where music.user_id = 1", nil) + require.NoError(t, err) + + var dmlVals []*querypb.Value + for i := 0; i < 8; i++ { + dmlVals = append(dmlVals, sqltypes.TupleToProto([]sqltypes.Value{sqltypes.TestValue(sqltypes.Int32, "1")})) + } + + bq := &querypb.BoundQuery{ + Sql: "select 1 from music where music.user_id = 1 and music.col = :user_col", + BindVariables: map[string]*querypb.BindVariable{"user_col": sqltypes.StringBindVariable("foo")}, + } + 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}}}} + 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}}}, + } + assertQueries(t, sbc2, wantQueries) + + bq = &querypb.BoundQuery{ + Sql: "delete from name_user_map where `name` = :name and user_id = :user_id", + BindVariables: map[string]*querypb.BindVariable{ + "name": sqltypes.StringBindVariable("foo"), + "user_id": sqltypes.Uint64BindVariable(1), + }} + wantQueries = []*querypb.BoundQuery{ + bq, bq, bq, bq, bq, bq, bq, bq, + } + assertQueries(t, sbclookup, wantQueries) + + testQueryLog(t, executor, logChan, "MarkSavepoint", "SAVEPOINT", "savepoint s1", 8) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + testQueryLog(t, executor, logChan, "VindexDelete", "DELETE", "delete from name_user_map where `name` = :name and user_id = :user_id", 1) + 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 - 8 shards + // delete from `user` where (`user`.id) in ::dm_vals - 8 shards + testQueryLog(t, executor, logChan, "TestExecute", "DELETE", "delete `user` from `user` join music on `user`.col = music.col where music.user_id = 1", 32) +} diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go index 831e133770a..01a5aebc6d5 100644 --- a/go/vt/vtgate/executor_framework_test.go +++ b/go/vt/vtgate/executor_framework_test.go @@ -368,8 +368,8 @@ func assertQueries(t *testing.T, sbc *sandboxconn.SandboxConn, wantQueries []*qu } got := query.Sql expected := wantQueries[idx].Sql - utils.MustMatch(t, expected, got) - utils.MustMatch(t, wantQueries[idx].BindVariables, query.BindVariables) + utils.MustMatch(t, expected, got, fmt.Sprintf("query did not match on index: %d", idx)) + utils.MustMatch(t, wantQueries[idx].BindVariables, query.BindVariables, fmt.Sprintf("bind variables did not match on index: %d", idx)) idx++ } } diff --git a/go/vt/vtgate/planbuilder/delete_multi.go b/go/vt/vtgate/planbuilder/delete_multi.go new file mode 100644 index 00000000000..9365835d089 --- /dev/null +++ b/go/vt/vtgate/planbuilder/delete_multi.go @@ -0,0 +1,38 @@ +/* +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 planbuilder + +import ( + "vitess.io/vitess/go/vt/vtgate/engine" +) + +type deleteMulti struct { + input logicalPlan + delete logicalPlan +} + +var _ logicalPlan = (*deleteMulti)(nil) + +// Primitive implements the logicalPlan interface +func (d *deleteMulti) Primitive() engine.Primitive { + inp := d.input.Primitive() + del := d.delete.Primitive() + return &engine.DeleteMulti{ + Delete: del, + Input: inp, + } +} diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go index 65012e68e02..6891b885a81 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -73,11 +73,29 @@ 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) } return nil, vterrors.VT13001(fmt.Sprintf("unknown type encountered: %T (transformToLogicalPlan)", op)) } +func transformDeleteMulti(ctx *plancontext.PlanningContext, op *operators.DeleteMulti) (logicalPlan, error) { + input, err := transformToLogicalPlan(ctx, op.Source) + if err != nil { + return nil, err + } + + del, err := transformToLogicalPlan(ctx, op.Delete) + if err != nil { + return nil, err + } + return &deleteMulti{ + input: input, + delete: del, + }, nil +} + func transformUpsert(ctx *plancontext.PlanningContext, op *operators.Upsert) (logicalPlan, error) { u := &upsert{} for _, source := range op.Sources { @@ -698,11 +716,15 @@ func buildDeleteLogicalPlan(ctx *plancontext.PlanningContext, rb *operators.Rout Query: generateQuery(stmt), TableNames: []string{vtable.Name.String()}, Vindexes: vtable.Owned, - OwnedVindexQuery: del.OwnedVindexQuery, RoutingParameters: rp, } - transformDMLPlan(vtable, edml, rb.Routing, del.OwnedVindexQuery != "") + hasLookupVindex := del.OwnedVindexQuery != nil + if hasLookupVindex { + edml.OwnedVindexQuery = sqlparser.String(del.OwnedVindexQuery) + } + + transformDMLPlan(vtable, edml, rb.Routing, hasLookupVindex) e := &engine.Delete{ DML: edml, diff --git a/go/vt/vtgate/planbuilder/operators/delete.go b/go/vt/vtgate/planbuilder/operators/delete.go index f9934306f38..8761b55a82d 100644 --- a/go/vt/vtgate/planbuilder/operators/delete.go +++ b/go/vt/vtgate/planbuilder/operators/delete.go @@ -28,7 +28,7 @@ import ( type Delete struct { Target TargetTable - OwnedVindexQuery string + OwnedVindexQuery *sqlparser.Select OrderBy sqlparser.OrderBy Limit *sqlparser.Limit Ignore bool @@ -62,7 +62,7 @@ func (d *Delete) Inputs() []Operator { func (d *Delete) SetInputs(inputs []Operator) { if len(inputs) != 1 { - panic(vterrors.VT13001("unexpected number of inputs to Delete operator")) + panic(vterrors.VT13001("unexpected number of inputs for Delete operator")) } d.Source = inputs[0] } @@ -104,7 +104,7 @@ func createOperatorFromDelete(ctx *plancontext.PlanningContext, deleteStmt *sqlp childFks := ctx.SemTable.GetChildForeignKeysList() // If there are no foreign key constraints, then we don't need to do anything. if len(childFks) == 0 { - return + return op } // If the delete statement has a limit, we don't support it yet. if delClone.Limit != nil { @@ -137,26 +137,28 @@ func createDeleteOperator(ctx *plancontext.PlanningContext, del *sqlparser.Delet vTbl = updateQueryGraphWithSource(ctx, op, tblID, vTbl) } - var ovq string + name, err := tblInfo.Name() + if err != nil { + panic(err) + } + + targetTbl := TargetTable{ + ID: tblID, + VTable: vTbl, + Name: name, + } + + var ovq *sqlparser.Select if vTbl.Keyspace.Sharded && vTbl.Type == vindexes.TypeTable { primaryVindex, _ := getVindexInformation(tblID, vTbl) ate := tblInfo.GetAliasedTableExpr() if len(vTbl.Owned) > 0 { - ovq = generateOwnedVindexQuery(ate, del, vTbl, primaryVindex.Columns) + ovq = generateOwnedVindexQuery(ate, del, targetTbl, primaryVindex.Columns) } } - name, err := tblInfo.Name() - if err != nil { - panic(err) - } - return &Delete{ - Target: TargetTable{ - ID: tblID, - VTable: vTbl, - Name: name, - }, + Target: targetTbl, Source: op, Ignore: bool(del.Ignore), Limit: del.Limit, diff --git a/go/vt/vtgate/planbuilder/operators/delete_multi.go b/go/vt/vtgate/planbuilder/operators/delete_multi.go new file mode 100644 index 00000000000..00006418962 --- /dev/null +++ b/go/vt/vtgate/planbuilder/operators/delete_multi.go @@ -0,0 +1,55 @@ +/* +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/query_planning.go b/go/vt/vtgate/planbuilder/operators/query_planning.go index b2d51c2935e..8137ec502a4 100644 --- a/go/vt/vtgate/planbuilder/operators/query_planning.go +++ b/go/vt/vtgate/planbuilder/operators/query_planning.go @@ -22,6 +22,7 @@ import ( "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" ) @@ -98,7 +99,7 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { case *LockAndComment: return pushLockAndComment(in) case *Delete: - return tryPushDelete(in) + return tryPushDelete(ctx, in) default: return in, NoRewrite } @@ -107,30 +108,87 @@ func runRewriters(ctx *plancontext.PlanningContext, root Operator) Operator { return FixedPointBottomUp(root, TableID, visitor, stopAtRoute) } -func tryPushDelete(in *Delete) (Operator, *ApplyResult) { +func tryPushDelete(ctx *plancontext.PlanningContext, in *Delete) (Operator, *ApplyResult) { switch src := in.Source.(type) { case *Route: - if in.Limit != nil && !src.IsSingleShardOrByDestination() { - panic(vterrors.VT12001("multi shard DELETE with LIMIT")) + return pushDeleteUnderRoute(in, src) + case *ApplyJoin: + return pushDeleteUnderJoin(ctx, in, src) + } + return in, nil +} + +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 + src.Routing = &AnyShardRouting{ + keyspace: r.keyspace, } + case *AnyShardRouting: + // References would have an unsharded source + // Alternates are not required. + r.Alternates = nil + } + return Swap(in, src, "pushed delete under route") +} - switch r := src.Routing.(type) { - case *SequenceRouting: - // Sequences are just unsharded routes - src.Routing = &AnyShardRouting{ - keyspace: r.keyspace, - } - case *AnyShardRouting: - // References would have an unsharded source - // Alternates are not required. - r.Alternates = nil +func pushDeleteUnderJoin(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 + var leftComp sqlparser.ValTuple + for _, col := range in.Target.VTable.PrimaryKey { + colName := sqlparser.NewColNameWithQualifier(col.String(), in.Target.Name) + selExprs = append(selExprs, sqlparser.NewAliasedExpr(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) + + 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 Swap(in, src, "pushed delete under route") - case *ApplyJoin: - panic(vterrors.VT12001("multi shard DELETE with join table references")) + return nil + }) + if targetTable == nil { + panic(vterrors.VT13001("target DELETE table not found")) + } + compExpr := sqlparser.NewComparisonExpr(sqlparser.InOp, leftComp, sqlparser.ListArg(engine.DM_VALS), nil) + targetQT := targetTable.QTable + qt := &QueryTable{ + ID: targetQT.ID, + Alias: sqlparser.CloneRefOfAliasedTableExpr(targetQT.Alias), + Table: sqlparser.CloneTableName(targetQT.Table), + Predicates: []sqlparser.Expr{compExpr}, } - return in, nil + 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("Delete Multi on top of Delete and ApplyJoin") } func pushLockAndComment(l *LockAndComment) (Operator, *ApplyResult) { diff --git a/go/vt/vtgate/planbuilder/operators/route_planning.go b/go/vt/vtgate/planbuilder/operators/route_planning.go index 07dbab3bc90..bae8ed33625 100644 --- a/go/vt/vtgate/planbuilder/operators/route_planning.go +++ b/go/vt/vtgate/planbuilder/operators/route_planning.go @@ -124,23 +124,34 @@ func buildVindexTableForDML( return vindexTable, routing } -func generateOwnedVindexQuery(tblExpr sqlparser.TableExpr, del *sqlparser.Delete, table *vindexes.Table, ksidCols []sqlparser.IdentifierCI) string { - buf := sqlparser.NewTrackedBuffer(nil) - for idx, col := range ksidCols { - if idx == 0 { - buf.Myprintf("select %v", col) - } else { - buf.Myprintf(", %v", col) - } - } - for _, cv := range table.Owned { - for _, column := range cv.Columns { - buf.Myprintf(", %v", column) +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, del.TableExprs.MultiTable()) + selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) + } + for _, cv := range table.VTable.Owned { + for _, col := range cv.Columns { + colName := makeColName(col, table, del.TableExprs.MultiTable()) + selExprs = append(selExprs, sqlparser.NewAliasedExpr(colName, "")) } } sqlparser.RemoveKeyspaceInTables(tblExpr) - buf.Myprintf(" from %v%v%v%v for update", tblExpr, del.Where, del.OrderBy, del.Limit) - return buf.String() + 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( diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 6ebd71dcf1b..7bf16178b75 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -62,6 +62,7 @@ func TestPlan(t *testing.T) { TestBuilder: TestBuilder, } testOutputTempDir := makeTestOutput(t) + addPKs(t, vschemaWrapper.V, "user", []string{"user", "music"}) // You will notice that some tests expect user.Id instead of user.id. // This is because we now pre-create vindex columns in the symbol @@ -266,6 +267,7 @@ func TestOne(t *testing.T) { lv := loadSchema(t, "vschemas/schema.json", true) setFks(t, lv) + addPKs(t, lv, "user", []string{"user", "music"}) vschema := &vschemawrapper.VSchemaWrapper{ V: lv, TestBuilder: TestBuilder, diff --git a/go/vt/vtgate/planbuilder/testdata/dml_cases.json b/go/vt/vtgate/planbuilder/testdata/dml_cases.json index 5ca6b034d24..36bd0943bce 100644 --- a/go/vt/vtgate/planbuilder/testdata/dml_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/dml_cases.json @@ -5012,7 +5012,7 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select Id, `Name`, Costly from `user` as u for update", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u join ref_with_source as r on u.col = r.col for update", "Query": "delete u from `user` as u, ref_with_source as r where u.col = r.col", "Table": "user" }, @@ -5038,7 +5038,7 @@ "TargetTabletType": "PRIMARY", "KsidLength": 1, "KsidVindex": "user_index", - "OwnedVindexQuery": "select Id, `Name`, Costly from `user` as u for update", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u join music as m on u.id = m.user_id for update", "Query": "delete u from `user` as u, music as m where u.id = m.user_id", "Table": "user" }, @@ -5047,5 +5047,362 @@ "user.user" ] } + }, + { + "comment": "multi delete multi table", + "query": "delete user from user join user_extra on user.id = user_extra.id where user.name = 'foo'", + "plan": { + "QueryType": "DELETE", + "Original": "delete user from user join user_extra on user.id = user_extra.id where user.name = 'foo'", + "Instructions": { + "OperatorType": "DeleteMulti", + "TargetTabletType": "PRIMARY", + "Inputs": [ + { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "R:0", + "JoinVars": { + "user_extra_id": 0 + }, + "TableName": "user_extra_`user`", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select user_extra.id from user_extra where 1 != 1", + "Query": "select user_extra.id from user_extra", + "Table": "user_extra" + }, + { + "OperatorType": "Route", + "Variant": "EqualUnique", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id from `user` where 1 != 1", + "Query": "select `user`.id from `user` where `user`.`name` = 'foo' and `user`.id = :user_extra_id", + "Table": "`user`", + "Values": [ + ":user_extra_id" + ], + "Vindex": "user_index" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user" + } + ] + }, + "TablesUsed": [ + "user.user", + "user.user_extra" + ] + } + }, + { + "comment": "multi delete multi table with alias", + "query": "delete u from user u join music m on u.col = m.col", + "plan": { + "QueryType": "DELETE", + "Original": "delete u from user u join music m on u.col = m.col", + "Instructions": { + "OperatorType": "DeleteMulti", + "TargetTabletType": "PRIMARY", + "Inputs": [ + { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0", + "JoinVars": { + "u_col": 1 + }, + "TableName": "`user`_music", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select u.id, u.col from `user` as u where 1 != 1", + "Query": "select u.id, u.col from `user` as u", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from music as m where 1 != 1", + "Query": "select 1 from music as m where m.col = :u_col", + "Table": "music" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user" + } + ] + }, + "TablesUsed": [ + "user.music", + "user.user" + ] + } + }, + { + "comment": "reverse the join order for delete", + "query": "delete u from music m join user u where u.col = m.col and m.foo = 42", + "plan": { + "QueryType": "DELETE", + "Original": "delete u from music m join user u where u.col = m.col and m.foo = 42", + "Instructions": { + "OperatorType": "DeleteMulti", + "TargetTabletType": "PRIMARY", + "Inputs": [ + { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "R:0", + "JoinVars": { + "m_col": 0 + }, + "TableName": "music_`user`", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select m.col from music as m where 1 != 1", + "Query": "select m.col from music as m where m.foo = 42", + "Table": "music" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select u.id from `user` as u where 1 != 1", + "Query": "select u.id from `user` as u where u.col = :m_col", + "Table": "`user`" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user" + } + ] + }, + "TablesUsed": [ + "user.music", + "user.user" + ] + } + }, + { + "comment": "multi table delete with join on vindex column", + "query": "delete u from user u join music m where u.id = m.user_id and m.foo = 42", + "plan": { + "QueryType": "DELETE", + "Original": "delete u from user u join music m where u.id = m.user_id and m.foo = 42", + "Instructions": { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "TargetTabletType": "PRIMARY", + "KsidLength": 1, + "KsidVindex": "user_index", + "OwnedVindexQuery": "select u.Id, u.`Name`, u.Costly from `user` as u join music as m where u.id = m.user_id and m.foo = 42 for update", + "Query": "delete u from `user` as u, music as m where m.foo = 42 and u.id = m.user_id", + "Table": "user" + }, + "TablesUsed": [ + "user.music", + "user.user" + ] + } + }, + { + "comment": "delete 3 way join with sharding key and primary key same", + "query": "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", + "plan": { + "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", + "TargetTabletType": "PRIMARY", + "Inputs": [ + { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0", + "JoinVars": { + "u_col": 1 + }, + "TableName": "`user`_music, user_extra", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select u.id, u.col from `user` as u where 1 != 1", + "Query": "select u.id, u.col from `user` as u where u.col = 30", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from music as m, user_extra as ue where 1 != 1", + "Query": "select 1 from music as m, user_extra as ue where m.bar = 40 and m.col = :u_col and ue.foo = 20 and m.user_id = ue.user_id", + "Table": "music, user_extra" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "user" + } + ] + }, + "TablesUsed": [ + "user.music", + "user.user", + "user.user_extra" + ] + } + }, + { + "comment": "delete 3 way join with sharding key and primary key different", + "query": "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", + "plan": { + "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", + "TargetTabletType": "PRIMARY", + "Inputs": [ + { + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "R:0", + "JoinVars": { + "u_col": 0 + }, + "TableName": "`user`_music, user_extra", + "Inputs": [ + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select u.col from `user` as u where 1 != 1", + "Query": "select u.col from `user` as u where u.col = 30", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select m.id from music as m, user_extra as ue where 1 != 1", + "Query": "select m.id from music as m, user_extra as ue where m.bar = 40 and m.col = :u_col and ue.foo = 20 and m.user_id = ue.user_id", + "Table": "music, user_extra" + } + ] + }, + { + "OperatorType": "Delete", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "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", + "Table": "music" + } + ] + }, + "TablesUsed": [ + "user.music", + "user.user", + "user.user_extra" + ] + } } ] diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json index 3765ae9b278..f78e2749899 100644 --- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.json @@ -394,16 +394,6 @@ "query": "delete u, r from user u join ref_with_source r on u.col = r.col", "plan": "VT12001: unsupported: multi-table DELETE statement in a sharded keyspace" }, - { - "comment": "multi delete multi table", - "query": "delete user from user join user_extra on user.id = user_extra.id where user.name = 'foo'", - "plan": "VT12001: unsupported: multi shard DELETE with join table references" - }, - { - "comment": "multi delete multi table with alias", - "query": "delete u from user u join music m on u.col = m.col", - "plan": "VT12001: unsupported: multi shard DELETE with join table references" - }, { "comment": "reference table delete with join", "query": "delete r from user u join ref_with_source r on u.col = r.col",