Repository: vitessio/vitess
Pull request #10720 (2022-07-17 23:24:33)

Reduce Flakiness of ERS/PRS e2e Tests Using Retries With a Timeout
## Description

The `ers_prs_heavy` workflow has been very flaky in this PR: https://github.com/vitessio/vitess/pull/10639. Often having to run 4+ times to succeed. I suspect that all of [the new DDL](https://github.com/vitessio/vitess/pull/10639/files#diff-0842ac0cabd40352e88bfcb98b9a9ae213d441b11eabc4bc51afbd47e211db56) executed at tablet init when the VDiff engine opens is causing replication to be slightly slower and thus we're getting this basic error from various individual tests (the ID and specific test differing on the failed runs):
```
2022-07-17T21:32:58.8852569Z     utils.go:348: 
2022-07-17T21:32:58.8852912Z         	Error Trace:	utils.go:354
2022-07-17T21:32:58.8853472Z         	            				utils.go:348
2022-07-17T21:32:58.8853942Z         	            				utils.go:526
2022-07-17T21:32:58.8854402Z         	            				utils.go:460
2022-07-17T21:32:58.8854875Z         	            				ers_test.go:453
2022-07-17T21:32:58.8855872Z         	Error:      	Expected nil, but got: &mysql.SQLError{Num:1146, State:"42S02", Message:"Table 'vt_ks.vt_insert_test' doesn't exist", Query:"select msg from vt_insert_test where id=20"}
```

This patch was [tested and verified here](https://github.com/vitessio/vitess/pull/10639/commits/bcef7958eb76fbc812600265fc837ca135f4299e) and it [passed on the first try](https://github.com/vitessio/vitess/runs/7380971321?check_suite_focus=true). 🥳 It was then re-tested and re-verified after [this minor follow-up](https://github.com/vitessio/vitess/pull/10639/commits/1225cb7e90e3bba0c79d285e14e13a09493f60c7) and it [passed on the first try again](https://github.com/vitessio/vitess/runs/7381224980?check_suite_focus=true). 🥳 🥳 

## Related Issue(s)
  - https://github.com/vitessio/vitess/pull/10639

## Checklist

-   [x] "Backport me!" label has been added if this change should be backported
-   [x] Tests were added or are not required
-   [x] Documentation was added or is not required

vitess-bot[bot]: ### Review Checklist
            
Hello reviewers! :wave: Please follow this checklist when reviewing this Pull Request.

#### General
- [ ] Ensure that the Pull Request has a descriptive title.
- [ ] If this is a change that users need to know about, please apply the `release notes (needs details)` label so that merging is blocked unless the summary release notes document is included.
- [ ] If a new flag is being introduced, review whether it is really needed. The flag names should be clear and intuitive (as far as possible), and the flag's help should be descriptive. Additionally, flag names should use dashes (`-`) as word separators rather than underscores (`_`).
- [ ] If a workflow is added or modified, each items in `Jobs` should be named in order to mark it as `required`. If the workflow should be required, the GitHub Admin should be notified.

#### Bug fixes
- [ ] There should be at least one unit or end-to-end test.
- [ ] The Pull Request description should either include a link to an issue that describes the bug OR an actual description of the bug and how to reproduce, along with a description of the fix.

#### Non-trivial changes
- [ ] There should be some code comments as to why things are implemented the way they are.

#### New/Existing features
- [ ] Should be documented, either by modifying the existing documentation or creating new documentation.
- [ ] New features should have a link to a feature request issue or an RFC that documents the use cases, corner cases and test cases.

#### Backward compatibility            
- [ ] Protobuf changes should be wire-compatible.
- [ ] Changes to `_vt` tables and RPCs need to be backward compatible.
- [ ] `vtctl` command output order should be stable and `awk`-able.


Patch:
diff --git a/go/vt/schemamanager/tablet_executor.go b/go/vt/schemamanager/tablet_executor.go
index 7ab3f5d3eb..7cf3945286 100644
--- a/go/vt/schemamanager/tablet_executor.go
+++ b/go/vt/schemamanager/tablet_executor.go
@@ -276,10 +276,10 @@ func (exec *TabletExecutor) preflightSchemaChanges(ctx context.Context, sqls []s
 
 // executeSQL executes a single SQL statement either as online DDL or synchronously on all tablets.
 // In online DDL case, the query may be exploded into multiple queries during
-func (exec *TabletExecutor) executeSQL(ctx context.Context, sql string, providedUUID string, execResult *ExecuteResult) error {
+func (exec *TabletExecutor) executeSQL(ctx context.Context, sql string, providedUUID string, execResult *ExecuteResult) (executedAsynchronously bool, err error) {
 	stmt, err := sqlparser.Parse(sql)
 	if err != nil {
-		return err
+		return false, err
 	}
 	switch stmt := stmt.(type) {
 	case sqlparser.DDLStatement:
@@ -287,7 +287,7 @@ func (exec *TabletExecutor) executeSQL(ctx context.Context, sql string, provided
 			onlineDDLs, err := schema.NewOnlineDDLs(exec.keyspace, sql, stmt, exec.ddlStrategySetting, exec.migrationContext, providedUUID)
 			if err != nil {
 				execResult.ExecutorErr = err.Error()
-				return err
+				return false, err
 			}
 			for _, onlineDDL := range onlineDDLs {
 				exec.executeOnAllTablets(ctx, execResult, onlineDDL.SQL, true)
@@ -296,25 +296,25 @@ func (exec *TabletExecutor) executeSQL(ctx context.Context, sql string, provided
 					exec.logger.Printf("%s\n", onlineDDL.UUID)
 				}
 			}
-			return nil
+			return true, nil
 		}
 	case *sqlparser.RevertMigration:
 		strategySetting := schema.NewDDLStrategySetting(schema.DDLStrategyOnline, exec.ddlStrategySetting.Options)
 		onlineDDL, err := schema.NewOnlineDDL(exec.keyspace, "", sqlparser.String(stmt), strategySetting, exec.migrationContext, providedUUID)
 		if err != nil {
 			execResult.ExecutorErr = err.Error()
-			return err
+			return false, err
 		}
 		exec.executeOnAllTablets(ctx, execResult, onlineDDL.SQL, true)
 		execResult.UUIDs = append(execResult.UUIDs, onlineDDL.UUID)
 		exec.logger.Printf("%s\n", onlineDDL.UUID)
-		return nil
+		return true, nil
 	case *sqlparser.AlterMigration:
 		exec.executeOnAllTablets(ctx, execResult, sql, true)
-		return nil
+		return true, nil
 	}
 	exec.executeOnAllTablets(ctx, execResult, sql, false)
-	return nil
+	return false, nil
 }
 
 // Execute applies schema changes
@@ -357,19 +357,61 @@ func (exec *TabletExecutor) Execute(ctx context.Context, sqls []string) *Execute
 	}
 	providedUUID := ""
 
+	syncOperationExecuted := false
 	for index, sql := range sqls {
 		execResult.CurSQLIndex = index
 		if exec.hasProvidedUUIDs() {
 			providedUUID = exec.uuids[index]
 		}
-		if err := exec.executeSQL(ctx, sql, providedUUID, &execResult); err != nil {
+		executedAsynchronously, err := exec.executeSQL(ctx, sql, providedUUID, &execResult)
+		if err != nil {
 			execResult.ExecutorErr = err.Error()
 			return &execResult
 		}
+		if !executedAsynchronously {
+			syncOperationExecuted = true
+		}
 		if len(execResult.FailedShards) > 0 {
 			break
 		}
 	}
+
+	if syncOperationExecuted {
+		// same shards will appear multiple times in execResult.SuccessShards when there are
+		// multiple SQLs
+		uniqueShards := map[string]*ShardResult{}
+		for _, result := range execResult.SuccessShards {
+			uniqueShards[result.Shard] = &result
+		}
+		var wg sync.WaitGroup
+		// If all shards succeeded, wait (up to waitReplicasTimeout) for replicas to
+		// execute the schema change via replication. This is best-effort, meaning
+		// we still return overall success if the timeout expires.
+		concurrency := sync2.NewSemaphore(10, 0)
+		reloadCtx, cancel := context.WithTimeout(ctx, exec.waitReplicasTimeout)
+		defer cancel()
+		for _, result := range uniqueShards {
+			wg.Add(1)
+			go func(result *ShardResult) {
+				defer wg.Done()
+				exec.logger.Infof("ReloadSchema on shard: %s", result.Shard)
+				schematools.ReloadShard(
+					reloadCtx,
+					exec.ts,
+					exec.tmc,
+					exec.logger,
+					exec.keyspace,
+					result.Shard,
+					result.Position,
+					concurrency,
+					false, /* includePrimary */
+				)
+			}(result)
+		}
+		wg.Wait()
+	} else {
+		exec.logger.Infof("Skipped ReloadSchema since all SQLs executed asynchronously")
+	}
 	return &execResult
 }
 
@@ -401,31 +443,6 @@ func (exec *TabletExecutor) executeOnAllTablets(ctx context.Context, execResult
 	if len(execResult.FailedShards) > 0 {
 		return
 	}
-
-	// If all shards succeeded, wait (up to waitReplicasTimeout) for replicas to
-	// execute the schema change via replication. This is best-effort, meaning
-	// we still return overall success if the timeout expires.
-	concurrency := sync2.NewSemaphore(10, 0)
-	reloadCtx, cancel := context.WithTimeout(ctx, exec.waitReplicasTimeout)
-	defer cancel()
-	for _, result := range execResult.SuccessShards {
-		wg.Add(1)
-		go func(result ShardResult) {
-			defer wg.Done()
-			schematools.ReloadShard(
-				reloadCtx,
-				exec.ts,
-				exec.tmc,
-				exec.logger,
-				exec.keyspace,
-				result.Shard,
-				result.Position,
-				concurrency,
-				false, /* includePrimary */
-			)
-		}(result)
-	}
-	wg.Wait()
 }
 
 func (exec *TabletExecutor) executeOneTablet(
diff --git a/go/vt/vtgate/engine/cached_size.go b/go/vt/vtgate/engine/cached_size.go
index 323e48bb4d..d5e7973864 100644
--- a/go/vt/vtgate/engine/cached_size.go
+++ b/go/vt/vtgate/engine/cached_size.go
@@ -154,7 +154,9 @@ func (cached *DML) CachedSize(alloc bool) int64 {
 		size += cc.CachedSize(true)
 	}
 	// field Table *vitess.io/vitess/go/vt/vtgate/vindexes.Table
-	size += cached.Table.CachedSize(true)
+	for _, table := range cached.Table {
+		size += table.CachedSize(true)
+	}
 	// field OwnedVindexQuery string
 	size += hack.RuntimeAllocSize(int64(len(cached.OwnedVindexQuery)))
 	// field RoutingParameters *vitess.io/vitess/go/vt/vtgate/engine.RoutingParameters
diff --git a/go/vt/vtgate/engine/delete.go b/go/vt/vtgate/engine/delete.go
index a49099fa53..4a8257741c 100644
--- a/go/vt/vtgate/engine/delete.go
+++ b/go/vt/vtgate/engine/delete.go
@@ -42,24 +42,6 @@ type Delete struct {
 	noInputs
 }
 
-// RouteType returns a description of the query routing type used by the primitive
-func (del *Delete) RouteType() string {
-	return del.Opcode.String()
-}
-
-// GetKeyspaceName specifies the Keyspace that this primitive routes to.
-func (del *Delete) GetKeyspaceName() string {
-	return del.Keyspace.Name
-}
-
-// GetTableName specifies the table that this primitive routes to.
-func (del *Delete) GetTableName() string {
-	if del.Table != nil {
-		return del.Table.Name.String()
-	}
-	return ""
-}
-
 // TryExecute performs a non-streaming exec.
 func (del *Delete) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, _ bool) (*sqltypes.Result, error) {
 	if del.QueryTimeout != 0 {
@@ -130,7 +112,11 @@ func (del *Delete) deleteVindexEntries(ctx context.Context, vcursor VCursor, bin
 			return err
 		}
 		colnum := del.KsidLength
-		for _, colVindex := range del.Table.Owned {
+		vindexTable, err := del.GetSingleTable()
+		if err != nil {
+			return err
+		}
+		for _, colVindex := range vindexTable.Owned {
 			// Fetch the column values. colnum must keep incrementing.
 			fromIds := make([]sqltypes.Value, 0, len(colVindex.Columns))
 			for range colVindex.Columns {
diff --git a/go/vt/vtgate/engine/dml.go b/go/vt/vtgate/engine/dml.go
index 5862f26016..990b5a0a4e 100644
--- a/go/vt/vtgate/engine/dml.go
+++ b/go/vt/vtgate/engine/dml.go
@@ -19,6 +19,8 @@ package engine
 import (
 	"context"
 	"fmt"
+	"sort"
+	"strings"
 
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/key"
@@ -44,7 +46,7 @@ type DML struct {
 	KsidLength int
 
 	// Table specifies the table for the update.
-	Table *vindexes.Table
+	Table []*vindexes.Table
 
 	// OwnedVindexQuery is used for updating changes in lookup vindexes.
 	OwnedVindexQuery string
@@ -89,6 +91,43 @@ func (dml *DML) execMultiDestination(ctx context.Context, vcursor VCursor, bindV
 	return execMultiShard(ctx, vcursor, rss, queries, dml.MultiShardAutocommit)
 }
 
+// RouteType returns a description of the query routing type used by the primitive
+func (dml *DML) RouteType() string {
+	return dml.Opcode.String()
+}
+
+// GetKeyspaceName specifies the Keyspace that this primitive routes to.
+func (dml *DML) GetKeyspaceName() string {
+	return dml.Keyspace.Name
+}
+
+// GetTableName specifies the table that this primitive routes to.
+func (dml *DML) GetTableName() string {
+	if dml.Table != nil {
+		tableNameMap := map[string]any{}
+		for _, table := range dml.Table {
+			tableNameMap[table.Name.String()] = nil
+		}
+
+		var tableNames []string
+		for name := range tableNameMap {
+			tableNames = append(tableNames, name)
+		}
+		sort.Strings(tableNames)
+
+		return strings.Join(tableNames, ", ")
+	}
+	return ""
+}
+
+// GetSingleTable returns single table used in dml.
+func (dml *DML) GetSingleTable() (*vindexes.Table, error) {
+	if len(dml.Table) > 1 {
+		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported dml on complex table expression")
+	}
+	return dml.Table[0], nil
+}
+
 func allowOnlyPrimary(rss ...*srvtopo.ResolvedShard) error {
 	for _, rs := range rss {
 		if rs != nil && rs.Target.TabletType != topodatapb.TabletType_PRIMARY {
diff --git a/go/vt/vtgate/engine/update.go b/go/vt/vtgate/engine/update.go
index db88405542..af9123253e 100644
--- a/go/vt/vtgate/engine/update.go
+++ b/go/vt/vtgate/engine/update.go
@@ -52,24 +52,6 @@ type Update struct {
 	noInputs
 }
 
-// RouteType returns a description of the query routing type used by the primitive
-func (upd *Update) RouteType() string {
-	return upd.Opcode.String()
-}
-
-// GetKeyspaceName specifies the Keyspace that this primitive routes to.
-func (upd *Update) GetKeyspaceName() string {
-	return upd.Keyspace.Name
-}
-
-// GetTableName specifies the table that this primitive routes to.
-func (upd *Update) GetTableName() string {
-	if upd.Table != nil {
-		return upd.Table.Name.String()
-	}
-	return ""
-}
-
 // TryExecute performs a non-streaming exec.
 func (upd *Update) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error) {
 	if upd.QueryTimeout != 0 {
@@ -150,7 +132,11 @@ func (upd *Update) updateVindexEntries(ctx context.Context, vcursor VCursor, bin
 			return err
 		}
 
-		for _, colVindex := range upd.Table.ColumnVindexes {
+		vindexTable, err := upd.GetSingleTable()
+		if err != nil {
+			return err
+		}
+		for _, colVindex := range vindexTable.ColumnVindexes {
 			// Skip this vindex if no rows are being changed
 			updColValues, ok := upd.ChangedVindexValues[colVindex.Name]
 			if !ok {
diff --git a/go/vt/vtgate/planbuilder/delete.go b/go/vt/vtgate/planbuilder/delete.go
index 9475c17c45..62148520db 100644
--- a/go/vt/vtgate/planbuilder/delete.go
+++ b/go/vt/vtgate/planbuilder/delete.go
@@ -51,17 +51,21 @@ func buildDeletePlan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedV
 		return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "multi-table delete statement in not supported in sharded database")
 	}
 
-	if len(del.Targets) == 1 && del.Targets[0].Name != edel.Table.Name {
+	edelTable, err := edel.GetSingleTable()
+	if err != nil {
+		return nil, err
+	}
+	if len(del.Targets) == 1 && del.Targets[0].Name != edelTable.Name {
 		return nil, vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.UnknownTable, "Unknown table '%s' in MULTI DELETE", del.Targets[0].Name.String())
 	}
 
-	if len(edel.Table.Owned) > 0 {
+	if len(edelTable.Owned) > 0 {
 		aTblExpr, ok := del.TableExprs[0].(*sqlparser.AliasedTableExpr)
 		if !ok {
 			return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: delete on complex table expression")
 		}
-		tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: edel.Table.Name}, As: aTblExpr.As}
-		edel.OwnedVindexQuery = generateDMLSubquery(tblExpr, del.Where, del.OrderBy, del.Limit, edel.Table, ksidVindex.Columns)
+		tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: edelTable.Name}, As: aTblExpr.As}
+		edel.OwnedVindexQuery = generateDMLSubquery(tblExpr, del.Where, del.OrderBy, del.Limit, edelTable, ksidVindex.Columns)
 		edel.KsidVindex = ksidVindex.Vindex
 		edel.KsidLength = len(ksidVindex.Columns)
 	}
diff --git a/go/vt/vtgate/planbuilder/dml_planner.go b/go/vt/vtgate/planbuilder/dml_planner.go
index 818be02d67..b1d05ee642 100644
--- a/go/vt/vtgate/planbuilder/dml_planner.go
+++ b/go/vt/vtgate/planbuilder/dml_planner.go
@@ -285,12 +285,17 @@ func buildDMLPlan(vschema plancontext.VSchema, dmlType string, stmt sqlparser.St
 		return nil, nil, err
 	}
 	edml.Keyspace = rb.eroute.Keyspace
+	edml.Table, err = pb.st.AllVschemaTableNames()
+	if err != nil {
+		return nil, nil, err
+	}
 	if !edml.Keyspace.Sharded {
 		// We only validate non-table subexpressions because the previous analysis has already validated them.
 		var subqueryArgs []sqlparser.SQLNode
 		subqueryArgs = append(subqueryArgs, nodes...)
 		subqueryArgs = append(subqueryArgs, where, orderBy, limit)
-		if pb.finalizeUnshardedDMLSubqueries(reservedVars, subqueryArgs...) {
+		subqueryIsUnsharded, subqueryTables := pb.finalizeUnshardedDMLSubqueries(reservedVars, subqueryArgs...)
+		if subqueryIsUnsharded {
 			vschema.WarnUnshardedOnly("subqueries can't be sharded in DML")
 		} else {
 			return nil, nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: sharded subqueries in DML")
@@ -299,6 +304,7 @@ func buildDMLPlan(vschema plancontext.VSchema, dmlType string, stmt sqlparser.St
 		// Generate query after all the analysis. Otherwise table name substitutions for
 		// routed tables won't happen.
 		edml.Query = generateQuery(stmt)
+		edml.Table = append(edml.Table, subqueryTables...)
 		return edml, nil, nil
 	}
 
@@ -320,12 +326,11 @@ func buildDMLPlan(vschema plancontext.VSchema, dmlType string, stmt sqlparser.St
 	if len(pb.st.tables) != 1 {
 		return nil, nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "multi-table %s statement is not supported in sharded database", dmlType)
 	}
-	for _, tval := range pb.st.tables {
-		// There is only one table.
-		edml.Table = tval.vschemaTable
+	edmlTable, err := edml.GetSingleTable()
+	if err != nil {
+		return nil, nil, err
 	}
-
-	routingType, ksidVindex, vindex, values, err := getDMLRouting(where, edml.Table)
+	routingType, ksidVindex, vindex, values, err := getDMLRouting(where, edmlTable)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/go/vt/vtgate/planbuilder/expr.go b/go/vt/vtgate/planbuilder/expr.go
index 7ba2588cfe..72e45b660b 100644
--- a/go/vt/vtgate/planbuilder/expr.go
+++ b/go/vt/vtgate/planbuilder/expr.go
@@ -24,6 +24,7 @@ import (
 
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
 )
 
 type subqueryInfo struct {
@@ -209,13 +210,14 @@ func hasSubquery(node sqlparser.SQLNode) bool {
 	return has
 }
 
-func (pb *primitiveBuilder) finalizeUnshardedDMLSubqueries(reservedVars *sqlparser.ReservedVars, nodes ...sqlparser.SQLNode) bool {
+func (pb *primitiveBuilder) finalizeUnshardedDMLSubqueries(reservedVars *sqlparser.ReservedVars, nodes ...sqlparser.SQLNode) (bool, []*vindexes.Table) {
 	var keyspace string
+	var tables []*vindexes.Table
 	if rb, ok := pb.plan.(*route); ok {
 		keyspace = rb.eroute.Keyspace.Name
 	} else {
 		// This code is unreachable because the caller checks.
-		return false
+		return false, nil
 	}
 
 	for _, node := range nodes {
@@ -247,6 +249,11 @@ func (pb *primitiveBuilder) finalizeUnshardedDMLSubqueries(reservedVars *sqlpars
 				for _, sub := range innerRoute.substitutions {
 					*sub.oldExpr = *sub.newExpr
 				}
+				spbTables, err := spb.st.AllVschemaTableNames()
+				if err != nil {
+					return false, err
+				}
+				tables = append(tables, spbTables...)
 			case *sqlparser.Union:
 				if !inSubQuery {
 					return true, nil
@@ -270,10 +277,10 @@ func (pb *primitiveBuilder) finalizeUnshardedDMLSubqueries(reservedVars *sqlpars
 			return true, nil
 		}, node)
 		if !samePlan {
-			return false
+			return false, nil
 		}
 	}
-	return true
+	return true, tables
 }
 
 func valEqual(a, b sqlparser.Expr) bool {
diff --git a/go/vt/vtgate/planbuilder/from.go b/go/vt/vtgate/planbuilder/from.go
index d6976a6cd2..18d653778a 100644
--- a/go/vt/vtgate/planbuilder/from.go
+++ b/go/vt/vtgate/planbuilder/from.go
@@ -19,6 +19,7 @@ package planbuilder
 import (
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
 
 	"vitess.io/vitess/go/mysql/collations"
@@ -153,8 +154,18 @@ func (pb *primitiveBuilder) processAliasedTable(tableExpr *sqlparser.AliasedTabl
 		// a new set of column references will be generated against the new tables,
 		// and those vindex maps will be returned. They have to replace the old vindex
 		// maps of the inherited route options.
+		var tableNames []string
+		spbTables, err := spb.st.AllVschemaTableNames()
+		if err != nil {
+			return err
+		}
+		for _, table := range spbTables {
+			tableNames = append(tableNames, table.Name.String())
+		}
+		sort.Strings(tableNames)
 		vschemaTable := &vindexes.Table{
 			Keyspace: subroute.eroute.Keyspace,
+			Name:     sqlparser.NewIdentifierCS(strings.Join(tableNames, ", ")),
 		}
 		for _, rc := range subroute.ResultColumns() {
 			if rc.column.vindex == nil {
diff --git a/go/vt/vtgate/planbuilder/gen4_planner.go b/go/vt/vtgate/planbuilder/gen4_planner.go
index 3559e34069..a062e4f42c 100644
--- a/go/vt/vtgate/planbuilder/gen4_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_planner.go
@@ -170,7 +170,7 @@ func newBuildSelectPlan(
 	// record any warning as planner warning.
 	vschema.PlannerWarning(semTable.Warning)
 
-	if ks := semTable.SingleUnshardedKeyspace(); ks != nil {
+	if ks, _ := semTable.SingleUnshardedKeyspace(); ks != nil {
 		return unshardedShortcut(selStmt, ks, semTable)
 	}
 
@@ -254,9 +254,10 @@ func gen4UpdateStmtPlanner(
 		return nil, err
 	}
 
-	if ks := semTable.SingleUnshardedKeyspace(); ks != nil {
+	if ks, tables := semTable.SingleUnshardedKeyspace(); ks != nil {
 		edml := engine.NewDML()
 		edml.Keyspace = ks
+		edml.Table = tables
 		edml.Opcode = engine.Unsharded
 		edml.Query = generateQuery(updStmt)
 		upd := &engine.Update{DML: edml}
diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go
index a66ec558d6..9bc0fd986b 100644
--- a/go/vt/vtgate/planbuilder/operator_transformers.go
+++ b/go/vt/vtgate/planbuilder/operator_transformers.go
@@ -180,8 +180,10 @@ func transformUpdatePlan(ctx *plancontext.PlanningContext, op *physical.Route, u
 	ast := upd.AST
 	replaceSubQuery(ctx, ast)
 	edml := &engine.DML{
-		Query:            generateQuery(ast),
-		Table:            upd.VTable,
+		Query: generateQuery(ast),
+		Table: []*vindexes.Table{
+			upd.VTable,
+		},
 		OwnedVindexQuery: upd.OwnedVindexQuery,
 		RoutingParameters: &engine.RoutingParameters{
 			Opcode:            op.RouteOpCode,
diff --git a/go/vt/vtgate/planbuilder/symtab.go b/go/vt/vtgate/planbuilder/symtab.go
index 395b45c5f9..f3f3bf6997 100644
--- a/go/vt/vtgate/planbuilder/symtab.go
+++ b/go/vt/vtgate/planbuilder/symtab.go
@@ -205,6 +205,24 @@ func (st *symtab) AllTables() []*table {
 	return tables
 }
 
+// AllVschemaTableNames returns an ordered list of all current vschema tables.
+func (st *symtab) AllVschemaTableNames() ([]*vindexes.Table, error) {
+	if len(st.tableNames) == 0 {
+		return nil, nil
+	}
+	tables := make([]*vindexes.Table, 0, len(st.tableNames))
+	for _, tname := range st.tableNames {
+		t, ok := st.tables[tname]
+		if !ok {
+			return nil, fmt.Errorf("table %v not found", sqlparser.String(tname))
+		}
+		if t.vschemaTable != nil {
+			tables = append(tables, t.vschemaTable)
+		}
+	}
+	return tables, nil
+}
+
 // FindTable finds a table in symtab. This function is specifically used
 // for expanding 'select a.*' constructs. If you're in a subquery,
 // you're most likely referring to a table in the local 'from' clause.
diff --git a/go/vt/vtgate/planbuilder/update_planner.go b/go/vt/vtgate/planbuilder/update_planner.go
index c295cdc49a..21a5be60b6 100644
--- a/go/vt/vtgate/planbuilder/update_planner.go
+++ b/go/vt/vtgate/planbuilder/update_planner.go
@@ -45,8 +45,11 @@ func buildUpdatePlan(query string) stmtPlanner {
 		if dml.Opcode == engine.Unsharded {
 			return eupd, nil
 		}
-
-		cvv, ovq, err := buildChangedVindexesValues(upd, eupd.Table, ksidVindex.Columns)
+		eupdTable, err := eupd.GetSingleTable()
+		if err != nil {
+			return nil, err
+		}
+		cvv, ovq, err := buildChangedVindexesValues(upd, eupdTable, ksidVindex.Columns)
 		if err != nil {
 			return nil, err
 		}
diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go
index 4fe0ce7537..9592556cd4 100644
--- a/go/vt/vtgate/semantics/semantic_state.go
+++ b/go/vt/vtgate/semantics/semantic_state.go
@@ -366,10 +366,12 @@ func (st *SemTable) ColumnLookup(col *sqlparser.ColName) (int, error) {
 }
 
 // SingleUnshardedKeyspace returns the single keyspace if all tables in the query are in the same, unsharded keyspace
-func (st *SemTable) SingleUnshardedKeyspace() *vindexes.Keyspace {
+func (st *SemTable) SingleUnshardedKeyspace() (*vindexes.Keyspace, []*vindexes.Table) {
 	var ks *vindexes.Keyspace
+	var tables []*vindexes.Table
 	for _, table := range st.Tables {
 		vindexTable := table.GetVindexTable()
+
 		if vindexTable == nil || vindexTable.Type != "" {
 			_, isDT := table.getExpr().Expr.(*sqlparser.DerivedTable)
 			if isDT {
@@ -377,27 +379,28 @@ func (st *SemTable) SingleUnshardedKeyspace() *vindexes.Keyspace {
 				// we check the real tables inside the derived table as well for same unsharded keyspace.
 				continue
 			}
-			return nil
+			return nil, nil
 		}
 		name, ok := table.getExpr().Expr.(sqlparser.TableName)
 		if !ok {
-			return nil
+			return nil, nil
 		}
 		if name.Name.String() != vindexTable.Name.String() {
 			// this points to a table alias. safer to not shortcut
-			return nil
+			return nil, nil
 		}
 		this := vindexTable.Keyspace
 		if this == nil || this.Sharded {
-			return nil
+			return nil, nil
 		}
 		if ks == nil {
 			ks = this
 		} else {
 			if ks != this {
-				return nil
+				return nil, nil
 			}
 		}
+		tables = append(tables, vindexTable)
 	}
-	return ks
+	return ks, tables
 }


Test patch:
diff --git a/go/test/endtoend/reparent/emergencyreparent/ers_test.go b/go/test/endtoend/reparent/emergencyreparent/ers_test.go
index e0f3e1a073..9bff176a9e 100644
--- a/go/test/endtoend/reparent/emergencyreparent/ers_test.go
+++ b/go/test/endtoend/reparent/emergencyreparent/ers_test.go
@@ -22,7 +22,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"vitess.io/vitess/go/test/endtoend/cluster"
@@ -438,10 +437,7 @@ func TestERSForInitialization(t *testing.T) {
 	utils.RunSQL(context.Background(), t, "create table vt_insert_test (id bigint, msg varchar(64), primary key (id)) Engine=InnoDB", tablets[0])
 	utils.CheckPrimaryTablet(t, clusterInstance, tablets[0])
 	utils.ValidateTopology(t, clusterInstance, false)
-	time.Sleep(100 * time.Millisecond) // wait for replication to catchup
-	strArray := utils.GetShardReplicationPositions(t, clusterInstance, utils.KeyspaceName, utils.ShardName, true)
-	assert.Equal(t, len(tablets), len(strArray))
-	assert.Contains(t, strArray[0], "primary") // primary first
+	utils.WaitForReplicationToStart(t, clusterInstance, utils.KeyspaceName, utils.ShardName, len(tablets), true)
 	utils.ConfirmReplication(t, tablets[0], tablets[1:])
 }
 
diff --git a/go/test/endtoend/reparent/plannedreparent/reparent_test.go b/go/test/endtoend/reparent/plannedreparent/reparent_test.go
index 71330e0e1c..2e6b67c68d 100644
--- a/go/test/endtoend/reparent/plannedreparent/reparent_test.go
+++ b/go/test/endtoend/reparent/plannedreparent/reparent_test.go
@@ -65,9 +65,7 @@ func TestReparentGraceful(t *testing.T) {
 	tablets := clusterInstance.Keyspaces[0].Shards[0].Vttablets
 
 	// Run this to make sure it succeeds.
-	strArray := utils.GetShardReplicationPositions(t, clusterInstance, utils.KeyspaceName, utils.ShardName, false)
-	assert.Equal(t, 4, len(strArray))          // one primary, three replicas
-	assert.Contains(t, strArray[0], "primary") // primary first
+	utils.WaitForReplicationToStart(t, clusterInstance, utils.KeyspaceName, utils.ShardName, len(tablets), true)
 
 	// Perform a graceful reparent operation
 	utils.Prs(t, clusterInstance, tablets[1])
diff --git a/go/test/endtoend/reparent/utils/utils.go b/go/test/endtoend/reparent/utils/utils.go
index 255a663b74..bffcad957d 100644
--- a/go/test/endtoend/reparent/utils/utils.go
+++ b/go/test/endtoend/reparent/utils/utils.go
@@ -58,10 +58,11 @@ var (
 	primary key (id)
 	) Engine=InnoDB	
 `
-	cell1         = "zone1"
-	cell2         = "zone2"
-	ShardName     = "0"
-	KeyspaceShard = KeyspaceName + "/" + ShardName
+	cell1                  = "zone1"
+	cell2                  = "zone2"
+	ShardName              = "0"
+	KeyspaceShard          = KeyspaceName + "/" + ShardName
+	replicationWaitTimeout = time.Duration(15 * time.Second)
 )
 
 //region cluster setup/teardown
@@ -198,10 +199,7 @@ func setupShard(ctx context.Context, t *testing.T, clusterInstance *cluster.Loca
 	CheckPrimaryTablet(t, clusterInstance, tablets[0])
 
 	ValidateTopology(t, clusterInstance, false)
-	time.Sleep(100 * time.Millisecond) // wait for replication to catchup
-	strArray := GetShardReplicationPositions(t, clusterInstance, KeyspaceName, shardName, true)
-	assert.Equal(t, len(tablets), len(strArray))
-	assert.Contains(t, strArray[0], "primary") // primary first
+	WaitForReplicationToStart(t, clusterInstance, KeyspaceName, shardName, len(tablets), true)
 }
 
 func setupClusterLegacy(ctx context.Context, t *testing.T, shardName string, cells []string, numTablets []int, enableSemiSync bool) *cluster.LocalProcessCluster {
@@ -321,10 +319,7 @@ func setupShardLegacy(ctx context.Context, t *testing.T, clusterInstance *cluste
 	CheckPrimaryTablet(t, clusterInstance, tablets[0])
 
 	ValidateTopology(t, clusterInstance, false)
-	time.Sleep(100 * time.Millisecond) // wait for replication to catchup
-	strArray := GetShardReplicationPositions(t, clusterInstance, KeyspaceName, shardName, true)
-	assert.Equal(t, len(tablets), len(strArray))
-	assert.Contains(t, strArray[0], "primary") // primary first
+	WaitForReplicationToStart(t, clusterInstance, KeyspaceName, shardName, len(tablets), true)
 }
 
 //endregion
@@ -455,7 +450,6 @@ func ConfirmReplication(t *testing.T, primary *cluster.Vttablet, replicas []*clu
 	// insert data into the new primary, check the connected replica work
 	insertSQL := fmt.Sprintf(insertSQL, n, n)
 	RunSQL(ctx, t, insertSQL, primary)
-	time.Sleep(100 * time.Millisecond)
 	for _, tab := range replicas {
 		err := CheckInsertedValues(ctx, t, tab, n)
 		require.NoError(t, err)
@@ -518,20 +512,28 @@ func isHealthyPrimaryTablet(t *testing.T, clusterInstance *cluster.LocalProcessC
 
 // CheckInsertedValues checks that the given value is present in the given tablet
 func CheckInsertedValues(ctx context.Context, t *testing.T, tablet *cluster.Vttablet, index int) error {
+	query := fmt.Sprintf("select msg from vt_insert_test where id=%d", index)
+	tabletParams := getMysqlConnParam(tablet)
+	conn, err := mysql.Connect(ctx, &tabletParams)
+	require.Nil(t, err)
+	defer conn.Close()
+
 	// wait until it gets the data
-	timeout := time.Now().Add(15 * time.Second)
+	timeout := time.Now().Add(replicationWaitTimeout)
 	i := 0
 	for time.Now().Before(timeout) {
-		selectSQL := fmt.Sprintf("select msg from vt_insert_test where id=%d", index)
-		qr := RunSQL(ctx, t, selectSQL, tablet)
-		if len(qr.Rows) == 1 {
+		// We'll get a mysql.ERNoSuchTable (1146) error if the CREATE TABLE has not replicated yet and
+		// it's possible that we get other ephemeral errors too, so we make the tests more robust by
+		// retrying with the timeout.
+		qr, err := conn.ExecuteFetch(query, 1, true)
+		if err == nil && len(qr.Rows) == 1 {
 			return nil
 		}
 		t := time.Duration(300 * i)
 		time.Sleep(t * time.Millisecond)
 		i++
 	}
-	return fmt.Errorf("data is not yet replicated on tablet %s", tablet.Alias)
+	return fmt.Errorf("data did not get replicated on tablet %s within the timeout of %v", tablet.Alias, replicationWaitTimeout)
 }
 
 func CheckSemiSyncSetupCorrectly(t *testing.T, tablet *cluster.Vttablet, semiSyncVal string) {
@@ -618,6 +620,7 @@ func GetNewPrimary(t *testing.T, clusterInstance *cluster.LocalProcessCluster) *
 }
 
 // GetShardReplicationPositions gets the shards replication positions.
+// This should not generally be called directly, instead use the WaitForReplicationToCatchup method.
 func GetShardReplicationPositions(t *testing.T, clusterInstance *cluster.LocalProcessCluster, keyspaceName, shardName string, doPrint bool) []string {
 	output, err := clusterInstance.VtctlclientProcess.ExecuteCommandWithOutput(
 		"ShardReplicationPositions", fmt.Sprintf("%s/%s", keyspaceName, shardName))
@@ -635,6 +638,23 @@ func GetShardReplicationPositions(t *testing.T, clusterInstance *cluster.LocalPr
 	return strArray
 }
 
+func WaitForReplicationToStart(t *testing.T, clusterInstance *cluster.LocalProcessCluster, keyspaceName, shardName string, tabletCnt int, doPrint bool) {
+	tck := time.NewTicker(500 * time.Millisecond)
+	for {
+		select {
+		case <-tck.C:
+			strArray := GetShardReplicationPositions(t, clusterInstance, KeyspaceName, shardName, true)
+			if len(strArray) == tabletCnt && strings.Contains(strArray[0], "primary") { // primary first
+				return
+			}
+		case <-time.After(replicationWaitTimeout):
+			require.FailNow(t, fmt.Sprintf("replication did not start everywhere in %s/%s within the timeout of %v",
+				keyspaceName, shardName, replicationWaitTimeout))
+			return
+		}
+	}
+}
+
 // endregion
 
 // CheckReplicaStatus checks the replication status and asserts that the replication is stopped
diff --git a/go/vt/vtgate/engine/delete_test.go b/go/vt/vtgate/engine/delete_test.go
index 3b98feba53..25c30307e3 100644
--- a/go/vt/vtgate/engine/delete_test.go
+++ b/go/vt/vtgate/engine/delete_test.go
@@ -197,8 +197,10 @@ func TestDeleteOwnedVindex(t *testing.T) {
 				Vindex:   ks.Vindexes["hash"],
 				Values:   []evalengine.Expr{evalengine.NewLiteralInt(1)},
 			},
-			Query:            "dummy_delete",
-			Table:            ks.Tables["t1"],
+			Query: "dummy_delete",
+			Table: []*vindexes.Table{
+				ks.Tables["t1"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["hash"],
 			KsidLength:       1,
@@ -283,8 +285,10 @@ func TestDeleteOwnedVindexMultiCol(t *testing.T) {
 				Vindex:   ks.Vindexes["rg_vdx"],
 				Values:   []evalengine.Expr{evalengine.NewLiteralInt(1), evalengine.NewLiteralInt(2)},
 			},
-			Query:            "dummy_delete",
-			Table:            ks.Tables["rg_tbl"],
+			Query: "dummy_delete",
+			Table: []*vindexes.Table{
+				ks.Tables["rg_tbl"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["rg_vdx"],
 			KsidLength:       2,
@@ -365,7 +369,9 @@ func TestDeleteSharded(t *testing.T) {
 				Keyspace: ks.Keyspace,
 			},
 			Query: "dummy_delete",
-			Table: ks.Tables["t2"],
+			Table: []*vindexes.Table{
+				ks.Tables["t2"],
+			},
 		},
 	}
 
@@ -392,7 +398,9 @@ func TestDeleteShardedStreaming(t *testing.T) {
 				Keyspace: ks.Keyspace,
 			},
 			Query: "dummy_delete",
-			Table: ks.Tables["t2"],
+			Table: []*vindexes.Table{
+				ks.Tables["t2"],
+			},
 		},
 	}
 
@@ -415,8 +423,10 @@ func TestDeleteScatterOwnedVindex(t *testing.T) {
 				Opcode:   Scatter,
 				Keyspace: ks.Keyspace,
 			},
-			Query:            "dummy_delete",
-			Table:            ks.Tables["t1"],
+			Query: "dummy_delete",
+			Table: []*vindexes.Table{
+				ks.Tables["t1"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["hash"],
 			KsidLength:       1,
@@ -505,8 +515,10 @@ func TestDeleteInChangedVindexMultiCol(t *testing.T) {
 					evalengine.NewLiteralInt(3),
 				},
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["rg_tbl"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["rg_tbl"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["rg_vdx"],
 			KsidLength:       2,
diff --git a/go/vt/vtgate/engine/update_test.go b/go/vt/vtgate/engine/update_test.go
index 7531c70076..46bbf6a751 100644
--- a/go/vt/vtgate/engine/update_test.go
+++ b/go/vt/vtgate/engine/update_test.go
@@ -247,8 +247,10 @@ func TestUpdateEqualChangedVindex(t *testing.T) {
 				Vindex:   ks.Vindexes["hash"],
 				Values:   []evalengine.Expr{evalengine.NewLiteralInt(1)},
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["t1"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["t1"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["hash"],
 			KsidLength:       1,
@@ -390,8 +392,10 @@ func TestUpdateEqualMultiColChangedVindex(t *testing.T) {
 				Vindex:   ks.Vindexes["rg_vdx"],
 				Values:   []evalengine.Expr{evalengine.NewLiteralInt(1), evalengine.NewLiteralInt(2)},
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["rg_tbl"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["rg_tbl"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["rg_vdx"],
 			KsidLength:       2,
@@ -509,8 +513,10 @@ func TestUpdateScatterChangedVindex(t *testing.T) {
 				Opcode:   Scatter,
 				Keyspace: ks.Keyspace,
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["t1"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["t1"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["hash"],
 			KsidLength:       1,
@@ -703,8 +709,10 @@ func TestUpdateInChangedVindex(t *testing.T) {
 					evalengine.NewLiteralInt(2),
 				}},
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["t1"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["t1"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["hash"],
 			KsidLength:       1,
@@ -832,8 +840,10 @@ func TestUpdateInChangedVindexMultiCol(t *testing.T) {
 					evalengine.NewLiteralInt(3),
 				},
 			},
-			Query:            "dummy_update",
-			Table:            ks.Tables["rg_tbl"],
+			Query: "dummy_update",
+			Table: []*vindexes.Table{
+				ks.Tables["rg_tbl"],
+			},
 			OwnedVindexQuery: "dummy_subquery",
 			KsidVindex:       ks.Vindexes["rg_vdx"],
 			KsidLength:       2,
diff --git a/go/vt/vtgate/planbuilder/testdata/dml_cases.txt b/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
index d01635938c..8357e9a678 100644
--- a/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
@@ -22,7 +22,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update m1 set val = 1"
+    "Query": "update m1 set val = 1",
+    "Table": "m1"
   }
 }
 Gen4 plan same as above
@@ -41,7 +42,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded set val = 1"
+    "Query": "update unsharded set val = 1",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -60,7 +62,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded set col = (select col from unsharded limit 1)"
+    "Query": "update unsharded set col = (select col from unsharded limit 1)",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -79,7 +82,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded set col = (select id from unsharded union select id from unsharded)"
+    "Query": "update unsharded set col = (select id from unsharded union select id from unsharded)",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -98,7 +102,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded set col = (select id from unsharded as a join unsharded as b on a.id = b.id)"
+    "Query": "update unsharded set col = (select id from unsharded as a join unsharded as b on a.id = b.id)",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -117,7 +122,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded as foo left join (select id from unsharded where col is not null order by col desc limit 10) as keepers on foo.id = keepers.id set col1 = 'asdf' where keepers.id is null and foo.col is not null and foo.col \u003c 1000"
+    "Query": "update unsharded as foo left join (select id from unsharded where col is not null order by col desc limit 10) as keepers on foo.id = keepers.id set col1 = 'asdf' where keepers.id is null and foo.col is not null and foo.col \u003c 1000",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -179,7 +185,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded_a set a = (select a from unsharded as route2)"
+    "Query": "update unsharded_a set a = (select a from unsharded as route2)",
+    "Table": "unsharded, unsharded_a"
   }
 }
 Gen4 plan same as above
@@ -198,7 +205,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete from unsharded"
+    "Query": "delete from unsharded",
+    "Table": "unsharded"
   }
 }
 Gen4 plan same as above
@@ -729,7 +737,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete a from unsharded_a as a, unsharded_b as b where a.id = b.id and b.val = 1"
+    "Query": "delete a from unsharded_a as a, unsharded_b as b where a.id = b.id and b.val = 1",
+    "Table": "unsharded_a, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -748,7 +757,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete a from unsharded_a as a join unsharded_b as b on a.id = b.id where b.val = 1"
+    "Query": "delete a from unsharded_a as a join unsharded_b as b on a.id = b.id where b.val = 1",
+    "Table": "unsharded_a, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -767,7 +777,28 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete foo from unsharded as foo left join (select id from unsharded where col is not null order by col desc limit 10) as keepers on foo.id = keepers.id where keepers.id is null and foo.col is not null and foo.col \u003c 1000"
+    "Query": "delete foo from unsharded as foo left join (select id from unsharded where col is not null order by col desc limit 10) as keepers on foo.id = keepers.id where keepers.id is null and foo.col is not null and foo.col \u003c 1000",
+    "Table": "unsharded"
+  }
+}
+Gen4 plan same as above
+
+#delete with join from multi table join subquery
+"delete foo from unsharded as foo left join (select id from unsharded a join unsharded_b b on a.user_id = b.user_id) as keepers on foo.id = keepers.id where keepers.id is null and foo.col is not null and foo.col < 1000"
+{
+  "QueryType": "DELETE",
+  "Original": "delete foo from unsharded as foo left join (select id from unsharded a join unsharded_b b on a.user_id = b.user_id) as keepers on foo.id = keepers.id where keepers.id is null and foo.col is not null and foo.col \u003c 1000",
+  "Instructions": {
+    "OperatorType": "Delete",
+    "Variant": "Unsharded",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": false
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "delete foo from unsharded as foo left join (select id from unsharded as a join unsharded_b as b on a.user_id = b.user_id) as keepers on foo.id = keepers.id where keepers.id is null and foo.col is not null and foo.col \u003c 1000",
+    "Table": "unsharded, unsharded, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -813,7 +844,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete from unsharded_a where a = (select a from unsharded as route2)"
+    "Query": "delete from unsharded_a where a = (select a from unsharded as route2)",
+    "Table": "unsharded, unsharded_a"
   }
 }
 Gen4 plan same as above
@@ -875,7 +907,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded_a as a join unsharded_b as b on a.id = b.id set a.val = 'foo' where b.val = 1"
+    "Query": "update unsharded_a as a join unsharded_b as b on a.id = b.id set a.val = 'foo' where b.val = 1",
+    "Table": "unsharded_a, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -894,7 +927,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded_a as a, unsharded_b as b set a.val = 'foo' where a.id = b.id and b.val = 1"
+    "Query": "update unsharded_a as a, unsharded_b as b set a.val = 'foo' where a.id = b.id and b.val = 1",
+    "Table": "unsharded_a, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -2431,7 +2465,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "update unsharded set col = (select id from unsharded_a where id = unsharded.col) where col = (select id from unsharded_b)"
+    "Query": "update unsharded set col = (select id from unsharded_a where id = unsharded.col) where col = (select id from unsharded_b)",
+    "Table": "unsharded, unsharded_a, unsharded_b"
   }
 }
 Gen4 plan same as above
@@ -2450,7 +2485,8 @@ Gen4 plan same as above
     },
     "TargetTabletType": "PRIMARY",
     "MultiShardAutocommit": false,
-    "Query": "delete from unsharded where col = (select id from unsharded_a where id = unsharded.col)"
+    "Query": "delete from unsharded where col = (select id from unsharded_a where id = unsharded.col)",
+    "Table": "unsharded, unsharded_a"
   }
 }
 Gen4 plan same as above
diff --git a/go/vt/vtgate/semantics/analyzer_test.go b/go/vt/vtgate/semantics/analyzer_test.go
index 8f9d4f24d4..cfb7d0d229 100644
--- a/go/vt/vtgate/semantics/analyzer_test.go
+++ b/go/vt/vtgate/semantics/analyzer_test.go
@@ -1361,26 +1361,36 @@ func TestSingleUnshardedKeyspace(t *testing.T) {
 	tests := []struct {
 		query     string
 		unsharded *vindexes.Keyspace
+		tables    []*vindexes.Table
 	}{
 		{
 			query:     "select 1 from t, t1",
 			unsharded: nil, // both tables are unsharded, but from different keyspaces
+			tables:    nil,
 		}, {
 			query:     "select 1 from t2",
 			unsharded: nil,
+			tables:    nil,
 		}, {
 			query:     "select 1 from t, t2",
 			unsharded: nil,
+			tables:    nil,
 		}, {
 			query:     "select 1 from t as A, t as B",
 			unsharded: ks1,
+			tables: []*vindexes.Table{
+				{Keyspace: ks1, Name: sqlparser.NewIdentifierCS("t")},
+				{Keyspace: ks1, Name: sqlparser.NewIdentifierCS("t")},
+			},
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.query, func(t *testing.T) {
 			_, semTable := parseAndAnalyze(t, test.query, "d")
-			assert.Equal(t, test.unsharded, semTable.SingleUnshardedKeyspace())
+			queryIsUnsharded, tables := semTable.SingleUnshardedKeyspace()
+			assert.Equal(t, test.unsharded, queryIsUnsharded)
+			assert.Equal(t, test.tables, tables)
 		})
 	}
 }