Repository: vitessio/vitess
Pull request #10090 (2022-04-13 10:16:30)

Gen4 feature: optimize OR queries to IN
## Description
When using a vindex is difficult because the necessary predicates are hidden behind `OR`, we can try and see if the `OR`s can be merged into an `IN` instead, which is easier to plan efficiently. 

## Checklist
- [x] Should this PR be backported? `nope`
- [x] Tests were added or are not required
- [x] Documentation was added or is not required



Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 6f4d154a2c..8acc1f57f7 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -8,17 +8,20 @@
 <!-- if this PR is Work in Progress please create it as a Draft Pull Request -->
 
 ## Description
+
 <!-- A few sentences describing the overall goals of the pull request's commits. -->
 <!-- If this is a bug fix and you think the fix should be backported, please write so. -->
 
 ## Related Issue(s)
-<!-- List related issues and pull requests. If this PR fixes an issue, please add it using Fixes #????  -->
 
+<!-- List related issues and pull requests. If this PR fixes an issue, please add it using Fixes #????  -->
 
 ## Checklist
-- [ ] Should this PR be backported?
-- [ ] Tests were added or are not required
-- [ ] Documentation was added or is not required
+
+-   [ ] "Backport me!" label has been added if this change should be backported
+-   [ ] Tests were added or are not required
+-   [ ] Documentation was added or is not required
 
 ## Deployment Notes
-<!-- Notes regarding deployment of the contained body of work. These should note any db migrations, etc. -->
\ No newline at end of file
+
+<!-- Notes regarding deployment of the contained body of work. These should note any db migrations, etc. -->
diff --git a/.github/workflows/check_formatting.yml b/.github/workflows/check_formatting.yml
index ec97374151..b5249f0124 100644
--- a/.github/workflows/check_formatting.yml
+++ b/.github/workflows/check_formatting.yml
@@ -6,7 +6,6 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out code
-      if: needs.check_relevant_files.outputs.go_files == 'true'
       uses: actions/checkout@v2
 
     - name: Check for changes in Go files
diff --git a/.github/workflows/check_make_sizegen.yml b/.github/workflows/check_make_sizegen.yml
index ba77be0ca8..323824cbc0 100644
--- a/.github/workflows/check_make_sizegen.yml
+++ b/.github/workflows/check_make_sizegen.yml
@@ -6,6 +6,9 @@ jobs:
     name: Check Make Sizegen
     runs-on: ubuntu-latest
     steps:
+    - name: Check out code
+      uses: actions/checkout@v2
+
     - name: Check for changes in relevant files
       uses: dorny/paths-filter@v2
       id: changes
@@ -31,10 +34,6 @@ jobs:
       run: |
         echo '1024 65535' | sudo tee -a /proc/sys/net/ipv4/ip_local_port_range
 
-    - name: Check out code
-      if: steps.changes.outputs.sizegen == 'true'
-      uses: actions/checkout@v2
-
     - name: Get dependencies
       if: steps.changes.outputs.sizegen == 'true'
       run: |
diff --git a/.github/workflows/legacy_local_example.yml b/.github/workflows/legacy_local_example.yml
index b9f51293a0..46814bb126 100644
--- a/.github/workflows/legacy_local_example.yml
+++ b/.github/workflows/legacy_local_example.yml
@@ -19,7 +19,7 @@ jobs:
       id: changes
       with:
         filters: |
-          example:
+          examples:
             - 'go/**/*.go'
             - 'test.go'
             - 'Makefile'
diff --git a/.github/workflows/local_example.yml b/.github/workflows/local_example.yml
index c356d0402d..9f01bfcf49 100644
--- a/.github/workflows/local_example.yml
+++ b/.github/workflows/local_example.yml
@@ -19,7 +19,7 @@ jobs:
       id: changes
       with:
         filters: |
-          example:
+          examples:
             - 'go/**/*.go'
             - 'test.go'
             - 'Makefile'
diff --git a/.github/workflows/region_example.yml b/.github/workflows/region_example.yml
index b81e06794e..5a3ea26912 100644
--- a/.github/workflows/region_example.yml
+++ b/.github/workflows/region_example.yml
@@ -19,7 +19,7 @@ jobs:
       id: changes
       with:
         filters: |
-          example:
+          examples:
             - 'go/**/*.go'
             - 'test.go'
             - 'Makefile'
@@ -32,18 +32,18 @@ jobs:
             - 'examples/**'
 
     - name: Set up Go
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       uses: actions/setup-go@v2
       with:
         go-version: 1.18
 
     - name: Tune the OS
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       run: |
         echo '1024 65535' | sudo tee -a /proc/sys/net/ipv4/ip_local_port_range
 
     - name: Get dependencies
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       run: |
         if [ ${{matrix.os}} = "ubuntu-latest" ]; then
           # Setup MySQL 8.0
@@ -65,17 +65,17 @@ jobs:
         go mod download
 
     - name: Run make minimaltools
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       run: |
         make minimaltools
 
     - name: Build
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       run: |
         make build
 
     - name: region_example
-      if: steps.changes.outputs.example == 'true'
+      if: steps.changes.outputs.examples == 'true'
       timeout-minutes: 30
       run: |
         export TOPO=${{matrix.topo}}
diff --git a/go/cmd/vtexplain/vtexplain.go b/go/cmd/vtexplain/vtexplain.go
index 472868c18e..f838b61d01 100644
--- a/go/cmd/vtexplain/vtexplain.go
+++ b/go/cmd/vtexplain/vtexplain.go
@@ -21,6 +21,8 @@ import (
 	"fmt"
 	"os"
 
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+
 	"vitess.io/vitess/go/exit"
 	"vitess.io/vitess/go/vt/log"
 	"vitess.io/vitess/go/vt/logutil"
@@ -145,7 +147,7 @@ func parseAndRun() error {
 		return err
 	}
 
-	plannerVersion := querypb.ExecuteOptions_PlannerVersion(querypb.ExecuteOptions_PlannerVersion_value[*plannerVersionStr])
+	plannerVersion, _ := plancontext.PlannerNameToVersion(*plannerVersionStr)
 	if plannerVersion != querypb.ExecuteOptions_V3 && plannerVersion != querypb.ExecuteOptions_Gen4 {
 		return fmt.Errorf("invalid value specified for planner-version of '%s' -- valid values are V3 and Gen4", *plannerVersionStr)
 	}
diff --git a/go/vt/sqlparser/comments.go b/go/vt/sqlparser/comments.go
index 66fb88f414..1378426e8c 100644
--- a/go/vt/sqlparser/comments.go
+++ b/go/vt/sqlparser/comments.go
@@ -255,6 +255,9 @@ func (c *ParsedComments) Prepend(comment string) Comments {
 // IsSet checks the directive map for the named directive and returns
 // true if the directive is set and has a true/false or 0/1 value
 func (d CommentDirectives) IsSet(key string) bool {
+	if d == nil {
+		return false
+	}
 	val, ok := d[key]
 	if !ok {
 		return false
diff --git a/go/vt/vtgate/engine/routing.go b/go/vt/vtgate/engine/routing.go
index 73e671b29b..28d038f94c 100644
--- a/go/vt/vtgate/engine/routing.go
+++ b/go/vt/vtgate/engine/routing.go
@@ -110,7 +110,7 @@ type RoutingParameters struct {
 
 	// TargetDestination specifies an explicit target destination to send the query to.
 	// This will bypass the routing logic.
-	TargetDestination key.Destination
+	TargetDestination key.Destination // update `user[-]@replica`.user set ....
 
 	// Vindex specifies the vindex to be used.
 	Vindex vindexes.Vindex
diff --git a/go/vt/vtgate/engine/update.go b/go/vt/vtgate/engine/update.go
index c1fcd39ec8..7f6e6d31e4 100644
--- a/go/vt/vtgate/engine/update.go
+++ b/go/vt/vtgate/engine/update.go
@@ -88,7 +88,7 @@ func (upd *Update) TryExecute(vcursor VCursor, bindVars map[string]*querypb.Bind
 	switch upd.Opcode {
 	case Unsharded:
 		return upd.execUnsharded(vcursor, bindVars, rss)
-	case Equal, IN, Scatter, ByDestination:
+	case Equal, EqualUnique, IN, Scatter, ByDestination:
 		return upd.execMultiDestination(vcursor, bindVars, rss, upd.updateVindexEntries)
 	default:
 		// Unreachable.
diff --git a/go/vt/vtgate/planbuilder/abstract/operator.go b/go/vt/vtgate/planbuilder/abstract/operator.go
index 940a37832e..ffaa779e77 100644
--- a/go/vt/vtgate/planbuilder/abstract/operator.go
+++ b/go/vt/vtgate/planbuilder/abstract/operator.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
 )
 
 type (
@@ -56,6 +57,12 @@ type (
 		// Clone creates a copy of the operator that can be updated without changing the original
 		Clone() PhysicalOperator
 	}
+
+	// IntroducesTable is used to make it possible to gather information about the table an operator introduces
+	IntroducesTable interface {
+		GetQTable() *QueryTable
+		GetVTable() *vindexes.Table
+	}
 )
 
 func getOperatorFromTableExpr(tableExpr sqlparser.TableExpr, semTable *semantics.SemTable) (LogicalOperator, error) {
@@ -83,7 +90,7 @@ func getOperatorFromTableExpr(tableExpr sqlparser.TableExpr, semTable *semantics
 			qg.Tables = append(qg.Tables, qt)
 			return qg, nil
 		case *sqlparser.DerivedTable:
-			inner, err := CreateOperatorFromAST(tbl.Select, semTable)
+			inner, err := CreateLogicalOperatorFromAST(tbl.Select, semTable)
 			if err != nil {
 				return nil, err
 			}
@@ -158,13 +165,15 @@ func getSelect(s sqlparser.SelectStatement) *sqlparser.Select {
 	}
 }
 
-// CreateOperatorFromAST creates an operator tree that represents the input SELECT or UNION query
-func CreateOperatorFromAST(selStmt sqlparser.SelectStatement, semTable *semantics.SemTable) (op LogicalOperator, err error) {
+// CreateLogicalOperatorFromAST creates an operator tree that represents the input SELECT or UNION query
+func CreateLogicalOperatorFromAST(selStmt sqlparser.Statement, semTable *semantics.SemTable) (op LogicalOperator, err error) {
 	switch node := selStmt.(type) {
 	case *sqlparser.Select:
 		op, err = createOperatorFromSelect(node, semTable)
 	case *sqlparser.Union:
 		op, err = createOperatorFromUnion(node, semTable)
+	case *sqlparser.Update:
+		op, err = createOperatorFromUpdate(node, semTable)
 	default:
 		err = vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "%T: operator not yet supported", selStmt)
 	}
@@ -175,7 +184,7 @@ func CreateOperatorFromAST(selStmt sqlparser.SelectStatement, semTable *semantic
 }
 
 func createOperatorFromUnion(node *sqlparser.Union, semTable *semantics.SemTable) (LogicalOperator, error) {
-	opLHS, err := CreateOperatorFromAST(node.Left, semTable)
+	opLHS, err := CreateLogicalOperatorFromAST(node.Left, semTable)
 	if err != nil {
 		return nil, err
 	}
@@ -184,7 +193,7 @@ func createOperatorFromUnion(node *sqlparser.Union, semTable *semantics.SemTable
 	if isRHSUnion {
 		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "nesting of unions at the right-hand side is not yet supported")
 	}
-	opRHS, err := CreateOperatorFromAST(node.Right, semTable)
+	opRHS, err := CreateLogicalOperatorFromAST(node.Right, semTable)
 	if err != nil {
 		return nil, err
 	}
@@ -199,19 +208,9 @@ func createOperatorFromUnion(node *sqlparser.Union, semTable *semantics.SemTable
 
 // createOperatorFromSelect creates an operator tree that represents the input SELECT query
 func createOperatorFromSelect(sel *sqlparser.Select, semTable *semantics.SemTable) (LogicalOperator, error) {
-	var resultantOp *SubQuery
-	if len(semTable.SubqueryMap[sel]) > 0 {
-		resultantOp = &SubQuery{}
-		for _, sq := range semTable.SubqueryMap[sel] {
-			opInner, err := CreateOperatorFromAST(sq.Subquery.Select, semTable)
-			if err != nil {
-				return nil, err
-			}
-			resultantOp.Inner = append(resultantOp.Inner, &SubQueryInner{
-				ExtractedSubquery: sq,
-				Inner:             opInner,
-			})
-		}
+	subq, err := createSubqueryFromStatement(sel, semTable)
+	if err != nil {
+		return nil, err
 	}
 	op, err := crossJoin(sel.From, semTable)
 	if err != nil {
@@ -227,11 +226,83 @@ func createOperatorFromSelect(sel *sqlparser.Select, semTable *semantics.SemTabl
 			addColumnEquality(semTable, expr)
 		}
 	}
-	if resultantOp == nil {
+	if subq == nil {
 		return op, nil
 	}
-	resultantOp.Outer = op
-	return resultantOp, nil
+	subq.Outer = op
+	return subq, nil
+}
+
+func createOperatorFromUpdate(updStmt *sqlparser.Update, semTable *semantics.SemTable) (LogicalOperator, error) {
+	subq, err := createSubqueryFromStatement(updStmt, semTable)
+	if err != nil {
+		return nil, err
+	}
+	alTbl, ok := updStmt.TableExprs[0].(*sqlparser.AliasedTableExpr)
+	if !ok {
+		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "expected AliasedTableExpr")
+	}
+	tblName, ok := alTbl.Expr.(sqlparser.TableName)
+	if !ok {
+		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "expected TableName")
+	}
+
+	tableID := semTable.TableSetFor(alTbl)
+	tableInfo, err := semTable.TableInfoFor(tableID)
+	if err != nil {
+		return nil, err
+	}
+
+	if tableInfo.IsInfSchema() {
+		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "can't update information schema tables")
+	}
+
+	var predicates []sqlparser.Expr
+	if updStmt.Where != nil {
+		predicates = sqlparser.SplitAndExpression(nil, updStmt.Where.Expr)
+	}
+	qt := &QueryTable{
+		ID:          tableID,
+		Alias:       alTbl,
+		Table:       tblName,
+		Predicates:  predicates,
+		IsInfSchema: false,
+	}
+
+	assignments := make(map[string]sqlparser.Expr)
+	for _, set := range updStmt.Exprs {
+		assignments[set.Name.Name.String()] = set.Expr
+	}
+
+	u := &Update{
+		Table:       qt,
+		Assignments: assignments,
+		AST:         updStmt,
+		TableInfo:   tableInfo,
+	}
+	if subq == nil {
+		return u, nil
+	}
+	subq.Outer = u
+	return subq, nil
+}
+
+func createSubqueryFromStatement(stmt sqlparser.Statement, semTable *semantics.SemTable) (*SubQuery, error) {
+	if len(semTable.SubqueryMap[stmt]) == 0 {
+		return nil, nil
+	}
+	subq := &SubQuery{}
+	for _, sq := range semTable.SubqueryMap[stmt] {
+		opInner, err := CreateLogicalOperatorFromAST(sq.Subquery.Select, semTable)
+		if err != nil {
+			return nil, err
+		}
+		subq.Inner = append(subq.Inner, &SubQueryInner{
+			ExtractedSubquery: sq,
+			Inner:             opInner,
+		})
+	}
+	return subq, nil
 }
 
 func addColumnEquality(semTable *semantics.SemTable, expr sqlparser.Expr) {
diff --git a/go/vt/vtgate/planbuilder/abstract/update.go b/go/vt/vtgate/planbuilder/abstract/update.go
new file mode 100644
index 0000000000..018dc1c3b3
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/abstract/update.go
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 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 abstract
+
+import (
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/semantics"
+)
+
+type Update struct {
+	Table       *QueryTable
+	TableInfo   semantics.TableInfo
+	Assignments map[string]sqlparser.Expr
+	AST         *sqlparser.Update
+}
+
+var _ LogicalOperator = (*Update)(nil)
+
+// TableID implements the LogicalOperator interface
+func (u *Update) TableID() semantics.TableSet {
+	return u.Table.ID
+}
+
+// UnsolvedPredicates implements the LogicalOperator interface
+func (u *Update) UnsolvedPredicates(semTable *semantics.SemTable) []sqlparser.Expr {
+	return nil
+}
+
+// CheckValid implements the LogicalOperator interface
+func (u *Update) CheckValid() error {
+	return nil
+}
+
+// iLogical implements the LogicalOperator interface
+func (u *Update) iLogical() {}
+
+// PushPredicate implements the LogicalOperator interface
+func (u *Update) PushPredicate(expr sqlparser.Expr, semTable *semantics.SemTable) (LogicalOperator, error) {
+	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "can't accept predicates")
+}
+
+// Compact implements the LogicalOperator interface
+func (u *Update) Compact(semTable *semantics.SemTable) (LogicalOperator, error) {
+	return u, nil
+}
diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go
index 83c9a215eb..300bd137e6 100644
--- a/go/vt/vtgate/planbuilder/builder.go
+++ b/go/vt/vtgate/planbuilder/builder.go
@@ -88,7 +88,7 @@ func BuildFromStmt(query string, stmt sqlparser.Statement, reservedVars *sqlpars
 	return plan, nil
 }
 
-func getConfiguredPlanner(vschema plancontext.VSchema, v3planner func(string) selectPlanner, stmt sqlparser.SelectStatement, query string) (selectPlanner, error) {
+func getConfiguredPlanner(vschema plancontext.VSchema, v3planner func(string) stmtPlanner, stmt sqlparser.Statement, query string) (stmtPlanner, error) {
 	planner, ok := getPlannerFromQuery(stmt)
 	if !ok {
 		// if the query doesn't specify the planner, we check what the configuration is
@@ -117,7 +117,7 @@ func getConfiguredPlanner(vschema plancontext.VSchema, v3planner func(string) se
 // The default planner can be overridden using /*vt+ PLANNER=gen4 */
 // We will also fall back on the gen4 planner if we encounter outer join,
 // since there are known problems with the v3 planner and outer joins
-func getPlannerFromQuery(stmt sqlparser.SelectStatement) (version plancontext.PlannerVersion, found bool) {
+func getPlannerFromQuery(stmt sqlparser.Statement) (version plancontext.PlannerVersion, found bool) {
 	version, found = getPlannerFromQueryHint(stmt)
 	if found {
 		return
@@ -138,13 +138,13 @@ func getPlannerFromQuery(stmt sqlparser.SelectStatement) (version plancontext.Pl
 	return
 }
 
-func getPlannerFromQueryHint(stmt sqlparser.SelectStatement) (plancontext.PlannerVersion, bool) {
-	var d sqlparser.CommentDirectives
-
-	firstSelect := sqlparser.GetFirstSelect(stmt)
-	if firstSelect != nil {
-		d = firstSelect.Comments.Directives()
+func getPlannerFromQueryHint(stmt sqlparser.Statement) (plancontext.PlannerVersion, bool) {
+	cm, isCom := stmt.(sqlparser.Commented)
+	if !isCom {
+		return plancontext.PlannerVersion(0), false
 	}
+
+	d := cm.GetParsedComments().Directives()
 	val, ok := d[sqlparser.DirectiveQueryPlanner]
 	if !ok {
 		return plancontext.PlannerVersion(0), false
@@ -159,7 +159,7 @@ func buildRoutePlan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVa
 	return f(stmt, reservedVars, vschema)
 }
 
-type selectPlanner func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error)
+type stmtPlanner func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error)
 
 func createInstructionFor(query string, stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema, enableOnlineDDL, enableDirectDDL bool) (engine.Primitive, error) {
 	switch stmt := stmt.(type) {
@@ -172,7 +172,11 @@ func createInstructionFor(query string, stmt sqlparser.Statement, reservedVars *
 	case *sqlparser.Insert:
 		return buildRoutePlan(stmt, reservedVars, vschema, buildInsertPlan)
 	case *sqlparser.Update:
-		return buildRoutePlan(stmt, reservedVars, vschema, buildUpdatePlan)
+		configuredPlanner, err := getConfiguredPlanner(vschema, buildUpdatePlan, stmt, query)
+		if err != nil {
+			return nil, err
+		}
+		return buildRoutePlan(stmt, reservedVars, vschema, configuredPlanner)
 	case *sqlparser.Delete:
 		return buildRoutePlan(stmt, reservedVars, vschema, buildDeletePlan)
 	case *sqlparser.Union:
diff --git a/go/vt/vtgate/planbuilder/dml.go b/go/vt/vtgate/planbuilder/dml_planner.go
similarity index 100%
rename from go/vt/vtgate/planbuilder/dml.go
rename to go/vt/vtgate/planbuilder/dml_planner.go
diff --git a/go/vt/vtgate/planbuilder/fallback_planner.go b/go/vt/vtgate/planbuilder/fallback_planner.go
index d931322789..31fac1a920 100644
--- a/go/vt/vtgate/planbuilder/fallback_planner.go
+++ b/go/vt/vtgate/planbuilder/fallback_planner.go
@@ -26,10 +26,10 @@ import (
 )
 
 type fallbackPlanner struct {
-	primary, fallback selectPlanner
+	primary, fallback stmtPlanner
 }
 
-var _ selectPlanner = (*fallbackPlanner)(nil).plan
+var _ stmtPlanner = (*fallbackPlanner)(nil).plan
 
 func (fp *fallbackPlanner) safePrimary(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (res engine.Primitive, err error) {
 	defer func() {
diff --git a/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go b/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
index 364b0603f0..10135699f9 100644
--- a/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_compare_v3_planner.go
@@ -25,6 +25,13 @@ import (
 
 func gen4CompareV3Planner(query string) func(sqlparser.Statement, *sqlparser.ReservedVars, plancontext.VSchema) (engine.Primitive, error) {
 	return func(statement sqlparser.Statement, vars *sqlparser.ReservedVars, ctxVSchema plancontext.VSchema) (engine.Primitive, error) {
+		switch statement.(type) {
+		case *sqlparser.Select, *sqlparser.Union:
+		// These we can compare. Everything else we'll just use the Gen4 planner
+		default:
+			return planWithPlannerVersion(statement, vars, ctxVSchema, query, Gen4)
+		}
+
 		// we will be switching the planner version to Gen4 and V3 in order to
 		// create instructions using them, thus we make sure to switch back to
 		// the Gen4CompareV3 planner before exiting this method.
diff --git a/go/vt/vtgate/planbuilder/gen4_planner.go b/go/vt/vtgate/planbuilder/gen4_planner.go
index 57a6f0ba89..43fdaf4a12 100644
--- a/go/vt/vtgate/planbuilder/gen4_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_planner.go
@@ -26,60 +26,74 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/physical"
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
 )
 
-var _ selectPlanner = gen4Planner("apa", 0)
+var _ stmtPlanner = gen4Planner("apa", 0)
 
-func gen4Planner(query string, plannerVersion querypb.ExecuteOptions_PlannerVersion) selectPlanner {
+func gen4Planner(query string, plannerVersion querypb.ExecuteOptions_PlannerVersion) stmtPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
-		selStatement, ok := stmt.(sqlparser.SelectStatement)
-		if !ok {
+		switch stmt := stmt.(type) {
+		case sqlparser.SelectStatement:
+			return gen4SelectStmtPlanner(query, plannerVersion, stmt, reservedVars, vschema)
+		case *sqlparser.Update:
+			return gen4UpdateStmtPlanner(plannerVersion, stmt, reservedVars, vschema)
+		default:
 			return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "%T not yet supported", stmt)
 		}
-		switch node := selStatement.(type) {
-		case *sqlparser.Select:
-			if node.With != nil {
-				return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in select statement")
-			}
-		case *sqlparser.Union:
-			if node.With != nil {
-				return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in union statement")
-			}
-		}
-
-		sel, isSel := selStatement.(*sqlparser.Select)
-		if isSel {
-			// handle dual table for processing at vtgate.
-			p, err := handleDualSelects(sel, vschema)
-			if err != nil || p != nil {
-				return p, err
-			}
+	}
+}
 
-			if sel.SQLCalcFoundRows && sel.Limit != nil {
-				return gen4planSQLCalcFoundRows(vschema, sel, query, reservedVars)
-			}
-			// if there was no limit, we can safely ignore the SQLCalcFoundRows directive
-			sel.SQLCalcFoundRows = false
+func gen4SelectStmtPlanner(
+	query string,
+	plannerVersion querypb.ExecuteOptions_PlannerVersion,
+	stmt sqlparser.SelectStatement,
+	reservedVars *sqlparser.ReservedVars,
+	vschema plancontext.VSchema,
+) (engine.Primitive, error) {
+	switch node := stmt.(type) {
+	case *sqlparser.Select:
+		if node.With != nil {
+			return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in select statement")
+		}
+	case *sqlparser.Union:
+		if node.With != nil {
+			return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in union statement")
 		}
+	}
 
-		getPlan := func(selStatement sqlparser.SelectStatement) (logicalPlan, error) {
-			return newBuildSelectPlan(selStatement, reservedVars, vschema, plannerVersion)
+	sel, isSel := stmt.(*sqlparser.Select)
+	if isSel {
+		// handle dual table for processing at vtgate.
+		p, err := handleDualSelects(sel, vschema)
+		if err != nil || p != nil {
+			return p, err
 		}
 
-		plan, err := getPlan(selStatement)
-		if err != nil {
-			return nil, err
+		if sel.SQLCalcFoundRows && sel.Limit != nil {
+			return gen4planSQLCalcFoundRows(vschema, sel, query, reservedVars)
 		}
+		// if there was no limit, we can safely ignore the SQLCalcFoundRows directive
+		sel.SQLCalcFoundRows = false
+	}
 
-		if shouldRetryWithCNFRewriting(plan) {
-			// by transforming the predicates to CNF, the planner will sometimes find better plans
-			primitive := gen4CNFRewrite(stmt, getPlan)
-			if primitive != nil {
-				return primitive, nil
-			}
+	getPlan := func(selStatement sqlparser.SelectStatement) (logicalPlan, error) {
+		return newBuildSelectPlan(selStatement, reservedVars, vschema, plannerVersion)
+	}
+
+	plan, err := getPlan(stmt)
+	if err != nil {
+		return nil, err
+	}
+
+	if shouldRetryWithCNFRewriting(plan) {
+		// by transforming the predicates to CNF, the planner will sometimes find better plans
+		primitive := gen4CNFRewrite(stmt, getPlan)
+		if primitive != nil {
+			return primitive, nil
 		}
-		return plan.Primitive(), nil
 	}
+	return plan.Primitive(), nil
 }
 
 func gen4planSQLCalcFoundRows(vschema plancontext.VSchema, sel *sqlparser.Select, query string, reservedVars *sqlparser.ReservedVars) (engine.Primitive, error) {
@@ -159,7 +173,7 @@ func newBuildSelectPlan(
 	}
 
 	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema, version)
-	logical, err := abstract.CreateOperatorFromAST(selStmt, semTable)
+	logical, err := abstract.CreateLogicalOperatorFromAST(selStmt, semTable)
 	if err != nil {
 		return nil, err
 	}
@@ -202,6 +216,128 @@ func newBuildSelectPlan(
 	return plan, nil
 }
 
+func gen4UpdateStmtPlanner(
+	version querypb.ExecuteOptions_PlannerVersion,
+	updStmt *sqlparser.Update,
+	reservedVars *sqlparser.ReservedVars,
+	vschema plancontext.VSchema,
+) (engine.Primitive, error) {
+	if updStmt.With != nil {
+		return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in update statement")
+	}
+
+	ksName := ""
+	if ks, _ := vschema.DefaultKeyspace(); ks != nil {
+		ksName = ks.Name
+	}
+	semTable, err := semantics.Analyze(updStmt, ksName, vschema)
+	if err != nil {
+		return nil, err
+	}
+	// record any warning as planner warning.
+	vschema.PlannerWarning(semTable.Warning)
+
+	err = rewriteRoutedTables(updStmt, vschema)
+	if err != nil {
+		return nil, err
+	}
+
+	if ks := semTable.SingleUnshardedKeyspace(); ks != nil {
+		edml := engine.NewDML()
+		edml.Keyspace = ks
+		edml.Opcode = engine.Unsharded
+		edml.Query = generateQuery(updStmt)
+		upd := &engine.Update{DML: edml}
+		return upd, nil
+	}
+
+	if semTable.NotUnshardedErr != nil {
+		return nil, semTable.NotUnshardedErr
+	}
+
+	err = queryRewrite(semTable, reservedVars, updStmt)
+	if err != nil {
+		return nil, err
+	}
+
+	logical, err := abstract.CreateLogicalOperatorFromAST(updStmt, semTable)
+	if err != nil {
+		return nil, err
+	}
+	err = logical.CheckValid()
+	if err != nil {
+		return nil, err
+	}
+
+	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema, version)
+
+	physOp, err := physical.CreatePhysicalOperator(ctx, logical)
+	if err != nil {
+		return nil, err
+	}
+
+	plan, err := transformToLogicalPlan(ctx, physOp, true)
+	if err != nil {
+		return nil, err
+	}
+
+	plan, err = pushCommentDirectivesOnPlan(plan, updStmt)
+	if err != nil {
+		return nil, err
+	}
+
+	setLockOnAllSelect(err, plan)
+
+	if err := plan.WireupGen4(semTable); err != nil {
+		return nil, err
+	}
+
+	return plan.Primitive(), nil
+}
+
+func rewriteRoutedTables(updStmt *sqlparser.Update, vschema plancontext.VSchema) (err error) {
+	// Rewrite routed tables
+	_ = sqlparser.Rewrite(updStmt, func(cursor *sqlparser.Cursor) bool {
+		aliasTbl, isAlias := cursor.Node().(*sqlparser.AliasedTableExpr)
+		if !isAlias {
+			return err == nil
+		}
+		tableName, ok := aliasTbl.Expr.(sqlparser.TableName)
+		if !ok {
+			return err == nil
+		}
+		var vschemaTable *vindexes.Table
+		vschemaTable, _, _, _, _, err = vschema.FindTableOrVindex(tableName)
+		if err != nil {
+			return false
+		}
+
+		if vschemaTable.Name.String() != tableName.Name.String() {
+			name := tableName.Name
+			if aliasTbl.As.IsEmpty() {
+				// if the user hasn't specified an alias, we'll insert one here so the old table name still works
+				aliasTbl.As = sqlparser.NewTableIdent(name.String())
+			}
+			tableName.Name = sqlparser.NewTableIdent(vschemaTable.Name.String())
+			aliasTbl.Expr = tableName
+		}
+
+		return err == nil
+	}, nil)
+	return
+}
+
+func setLockOnAllSelect(err error, plan logicalPlan) {
+	_, _ = visit(plan, func(plan logicalPlan) (bool, logicalPlan, error) {
+		switch node := plan.(type) {
+		case *routeGen4:
+			node.Select.SetLock(sqlparser.ShareModeLock)
+			return true, node, nil
+		}
+		return true, plan, nil
+	})
+}
+
 func planLimit(limit *sqlparser.Limit, plan logicalPlan) (logicalPlan, error) {
 	if limit == nil {
 		return plan, nil
@@ -283,8 +419,15 @@ func planOrderByOnUnion(ctx *plancontext.PlanningContext, plan logicalPlan, unio
 	return plan, nil
 }
 
-func pushCommentDirectivesOnPlan(plan logicalPlan, stmt sqlparser.SelectStatement) (logicalPlan, error) {
-	directives := sqlparser.GetFirstSelect(stmt).Comments.Directives()
+func pushCommentDirectivesOnPlan(plan logicalPlan, stmt sqlparser.Statement) (logicalPlan, error) {
+	var directives sqlparser.CommentDirectives
+	cmt, ok := stmt.(sqlparser.Commented)
+	if ok {
+		directives = cmt.GetParsedComments().Directives()
+	} else {
+		directives = make(sqlparser.CommentDirectives)
+	}
+
 	scatterAsWarns := directives.IsSet(sqlparser.DirectiveScatterErrorsAsWarnings)
 	queryTimeout := queryTimeout(directives)
 
diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go
index b6bce9dd58..d6a6573f7c 100644
--- a/go/vt/vtgate/planbuilder/insert.go
+++ b/go/vt/vtgate/planbuilder/insert.go
@@ -247,7 +247,7 @@ func subquerySelectPlan(ins *sqlparser.Insert, vschema plancontext.VSchema, rese
 func getStatementAndPlanner(
 	ins *sqlparser.Insert,
 	vschema plancontext.VSchema,
-) (selectStmt sqlparser.SelectStatement, configuredPlanner selectPlanner, err error) {
+) (selectStmt sqlparser.SelectStatement, configuredPlanner stmtPlanner, err error) {
 	switch stmt := ins.Rows.(type) {
 	case *sqlparser.Select:
 		configuredPlanner, err = getConfiguredPlanner(vschema, buildSelectPlan, stmt, "")
diff --git a/go/vt/vtgate/planbuilder/logical_plan.go b/go/vt/vtgate/planbuilder/logical_plan.go
index 6dda9e6ddf..6ccc34701a 100644
--- a/go/vt/vtgate/planbuilder/logical_plan.go
+++ b/go/vt/vtgate/planbuilder/logical_plan.go
@@ -157,16 +157,22 @@ func visit(node logicalPlan, visitor planVisitor) (logicalPlan, error) {
 		node = newNode
 	}
 	inputs := node.Inputs()
+	rewrite := false
 	for i, input := range inputs {
 		newInput, err := visit(input, visitor)
 		if err != nil {
 			return nil, err
 		}
+		if newInput != input {
+			rewrite = true
+		}
 		inputs[i] = newInput
 	}
-	err := node.Rewrite(inputs...)
-	if err != nil {
-		return nil, err
+	if rewrite {
+		err := node.Rewrite(inputs...)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	return node, nil
@@ -182,7 +188,7 @@ func first(input logicalPlan) logicalPlan {
 	return first(inputs[0])
 }
 
-//-------------------------------------------------------------------------
+// -------------------------------------------------------------------------
 
 // logicalPlanCommon implements some common functionality of builders.
 // Make sure to override in case behavior needs to be changed.
@@ -252,7 +258,7 @@ func (bc *logicalPlanCommon) OutputColumns() []sqlparser.SelectExpr {
 	return bc.input.OutputColumns()
 }
 
-//-------------------------------------------------------------------------
+// -------------------------------------------------------------------------
 
 // resultsBuilder is a superset of logicalPlanCommon. It also handles
 // resultsColumn functionality.
@@ -321,4 +327,4 @@ func (rsb *resultsBuilder) SupplyWeightString(colNumber int, alsoAddToGroupBy bo
 	return weightcolNumber, nil
 }
 
-//-------------------------------------------------------------------------
+// -------------------------------------------------------------------------
diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go
index ef7553bcb9..96e0a6bdde 100644
--- a/go/vt/vtgate/planbuilder/operator_transformers.go
+++ b/go/vt/vtgate/planbuilder/operator_transformers.go
@@ -133,7 +133,11 @@ func transformApplyJoinPlan(ctx *plancontext.PlanningContext, n *physical.ApplyJ
 	}, nil
 }
 
-func transformRoutePlan(ctx *plancontext.PlanningContext, op *physical.Route) (*routeGen4, error) {
+func transformRoutePlan(ctx *plancontext.PlanningContext, op *physical.Route) (logicalPlan, error) {
+	upd, isUpdate := op.Source.(*physical.Update)
+	if isUpdate {
+		return transformUpdatePlan(ctx, op, upd)
+	}
 	tableNames, err := getAllTableNames(op)
 	if err != nil {
 		return nil, err
@@ -166,7 +170,49 @@ func transformRoutePlan(ctx *plancontext.PlanningContext, op *physical.Route) (*
 
 }
 
-func replaceSubQuery(ctx *plancontext.PlanningContext, sel sqlparser.SelectStatement) {
+func transformUpdatePlan(ctx *plancontext.PlanningContext, op *physical.Route, upd *physical.Update) (logicalPlan, error) {
+	var vindex vindexes.Vindex
+	var values []evalengine.Expr
+	if op.Selected != nil {
+		vindex = op.Selected.FoundVindex
+		values = op.Selected.Values
+	}
+	ast := upd.AST
+	replaceSubQuery(ctx, ast)
+	edml := &engine.DML{
+		Query:            generateQuery(ast),
+		Table:            upd.VTable,
+		OwnedVindexQuery: upd.OwnedVindexQuery,
+		RoutingParameters: &engine.RoutingParameters{
+			Opcode:            op.RouteOpCode,
+			Keyspace:          op.Keyspace,
+			Vindex:            vindex,
+			Values:            values,
+			TargetDestination: op.TargetDestination,
+		},
+	}
+
+	directives := upd.AST.GetParsedComments().Directives()
+	if directives.IsSet(sqlparser.DirectiveMultiShardAutocommit) {
+		edml.MultiShardAutocommit = true
+	}
+	edml.QueryTimeout = queryTimeout(directives)
+
+	e := &engine.Update{
+		ChangedVindexValues: upd.ChangedVindexValues,
+	}
+	e.DML = edml
+
+	if op.RouteOpCode != engine.Unsharded && len(upd.ChangedVindexValues) > 0 {
+		primary := upd.VTable.ColumnVindexes[0]
+		e.DML.KsidVindex = primary.Vindex
+		e.DML.KsidLength = len(primary.Columns)
+	}
+
+	return &primitiveWrapper{prim: e}, nil
+}
+
+func replaceSubQuery(ctx *plancontext.PlanningContext, sel sqlparser.Statement) {
 	extractedSubqueries := ctx.SemTable.GetSubqueryNeedingRewrite()
 	if len(extractedSubqueries) == 0 {
 		return
diff --git a/go/vt/vtgate/planbuilder/physical/dml_planning.go b/go/vt/vtgate/planbuilder/physical/dml_planning.go
new file mode 100644
index 0000000000..5370f97b1a
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/physical/dml_planning.go
@@ -0,0 +1,173 @@
+/*
+Copyright 2022 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 physical
+
+import (
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"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/evalengine"
+	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+)
+
+// getVindexInformation returns the vindex and VindexPlusPredicates for the DML,
+// If it cannot find a unique vindex match, it returns an error.
+func getVindexInformation(
+	id semantics.TableSet,
+	predicates []sqlparser.Expr,
+	table *vindexes.Table,
+) (*vindexes.ColumnVindex, []*VindexPlusPredicates, error) {
+	// Check that we have a primary vindex which is valid
+	if len(table.ColumnVindexes) == 0 || !table.ColumnVindexes[0].IsUnique() {
+		return nil, nil, vterrors.NewErrorf(vtrpcpb.Code_FAILED_PRECONDITION, vterrors.RequiresPrimaryKey, vterrors.PrimaryVindexNotSet, table.Name)
+	}
+	primaryVindex := table.ColumnVindexes[0]
+	if len(predicates) == 0 {
+		return primaryVindex, nil, nil
+	}
+
+	var vindexesAndPredicates []*VindexPlusPredicates
+	for _, colVindex := range table.Ordered {
+		if lu, isLu := colVindex.Vindex.(vindexes.LookupBackfill); isLu && lu.IsBackfilling() {
+			// Checking if the Vindex is currently backfilling or not, if it isn't we can read from the vindex table
+			// and we will be able to do a delete equal. Otherwise, we continue to look for next best vindex.
+			continue
+		}
+
+		vindexesAndPredicates = append(vindexesAndPredicates, &VindexPlusPredicates{
+			ColVindex: colVindex,
+			TableID:   id,
+		})
+	}
+	return primaryVindex, vindexesAndPredicates, nil
+}
+
+// buildChangedVindexesValues adds to the plan all the lookup vindexes that are changing.
+// Updates can only be performed to secondary lookup vindexes with no complex expressions
+// in the set clause.
+func buildChangedVindexesValues(update *sqlparser.Update, table *vindexes.Table, ksidCols []sqlparser.ColIdent) (map[string]*engine.VindexValues, string, error) {
+	changedVindexes := make(map[string]*engine.VindexValues)
+	buf, offset := initialQuery(ksidCols, table)
+	for i, vindex := range table.ColumnVindexes {
+		vindexValueMap := make(map[string]evalengine.Expr)
+		first := true
+		for _, vcol := range vindex.Columns {
+			// Searching in order of columns in colvindex.
+			found := false
+			for _, assignment := range update.Exprs {
+				if !vcol.Equal(assignment.Name.Name) {
+					continue
+				}
+				if found {
+					return nil, "", vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "column has duplicate set values: '%v'", assignment.Name.Name)
+				}
+				found = true
+				pv, err := extractValueFromUpdate(assignment)
+				if err != nil {
+					return nil, "", err
+				}
+				vindexValueMap[vcol.String()] = pv
+				if first {
+					buf.Myprintf(", %v", assignment)
+					first = false
+				} else {
+					buf.Myprintf(" and %v", assignment)
+				}
+			}
+		}
+		if len(vindexValueMap) == 0 {
+			// Vindex not changing, continue
+			continue
+		}
+
+		if update.Limit != nil && len(update.OrderBy) == 0 {
+			return nil, "", vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: Need to provide order by clause when using limit. Invalid update on vindex: %v", vindex.Name)
+		}
+		if i == 0 {
+			return nil, "", vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: You can't update primary vindex columns. Invalid update on vindex: %v", vindex.Name)
+		}
+		if _, ok := vindex.Vindex.(vindexes.Lookup); !ok {
+			return nil, "", vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: You can only update lookup vindexes. Invalid update on vindex: %v", vindex.Name)
+		}
+		if !vindex.Owned {
+			return nil, "", vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: You can only update owned vindexes. Invalid update on vindex: %v", vindex.Name)
+		}
+		changedVindexes[vindex.Name] = &engine.VindexValues{
+			PvMap:  vindexValueMap,
+			Offset: offset,
+		}
+		offset++
+	}
+	if len(changedVindexes) == 0 {
+		return nil, "", nil
+	}
+	// generate rest of the owned vindex query.
+	aTblExpr, ok := update.TableExprs[0].(*sqlparser.AliasedTableExpr)
+	if !ok {
+		return nil, "", vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: update on complex table expression")
+	}
+	tblExpr := &sqlparser.AliasedTableExpr{Expr: sqlparser.TableName{Name: table.Name}, As: aTblExpr.As}
+	buf.Myprintf(" from %v%v%v%v for update", tblExpr, update.Where, update.OrderBy, update.Limit)
+	return changedVindexes, buf.String(), nil
+}
+
+func initialQuery(ksidCols []sqlparser.ColIdent, table *vindexes.Table) (*sqlparser.TrackedBuffer, int) {
+	buf := sqlparser.NewTrackedBuffer(nil)
+	offset := 0
+	for _, col := range ksidCols {
+		if offset == 0 {
+			buf.Myprintf("select %v", col)
+		} else {
+			buf.Myprintf(", %v", col)
+		}
+		offset++
+	}
+	for _, cv := range table.Owned {
+		for _, column := range cv.Columns {
+			buf.Myprintf(", %v", column)
+			offset++
+		}
+	}
+	return buf, offset
+}
+
+// extractValueFromUpdate given an UpdateExpr, builds an evalengine.Expr
+func extractValueFromUpdate(upd *sqlparser.UpdateExpr) (evalengine.Expr, error) {
+	expr := upd.Expr
+	if sq, ok := expr.(*sqlparser.ExtractedSubquery); ok {
+		// if we are planning an update that needs one or more values from the outside, we can trust that they have
+		// been correctly extracted from this query before we reach this far
+		// if NeedsRewrite is true, it means that this subquery was happily merged with the outer.
+		// But in that case we should not be here, so we fail
+		if sq.NeedsRewrite {
+			return nil, invalidUpdateExpr(upd, expr)
+		}
+		expr = sqlparser.NewArgument(sq.GetArgName())
+	}
+
+	pv, err := evalengine.Translate(expr, semantics.EmptySemTable())
+	if err != nil || sqlparser.IsSimpleTuple(expr) {
+		return nil, invalidUpdateExpr(upd, expr)
+	}
+	return pv, nil
+}
+
+func invalidUpdateExpr(upd *sqlparser.UpdateExpr, expr sqlparser.Expr) error {
+	return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: Only values are supported. Invalid update on column: `%s` with expr: [%s]", upd.Name.Name.String(), sqlparser.String(expr))
+}
diff --git a/go/vt/vtgate/planbuilder/physical/route.go b/go/vt/vtgate/planbuilder/physical/route.go
index 92e706cba7..3cccfd60d6 100644
--- a/go/vt/vtgate/planbuilder/physical/route.go
+++ b/go/vt/vtgate/planbuilder/physical/route.go
@@ -17,6 +17,7 @@ limitations under the License.
 package physical
 
 import (
+	"vitess.io/vitess/go/vt/key"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
 	"vitess.io/vitess/go/vt/vtgate/evalengine"
@@ -48,6 +49,9 @@ type (
 		// SeenPredicates contains all the predicates that have had a chance to influence routing.
 		// If we need to replan routing, we'll use this list
 		SeenPredicates []sqlparser.Expr
+
+		// TargetDestination specifies an explicit target destination tablet type
+		TargetDestination key.Destination
 	}
 
 	// VindexPlusPredicates is a struct used to store all the predicates that the vindex can be used to query
@@ -130,6 +134,10 @@ func (r *Route) Clone() abstract.PhysicalOperator {
 
 func (r *Route) UpdateRoutingLogic(ctx *plancontext.PlanningContext, expr sqlparser.Expr) error {
 	r.SeenPredicates = append(r.SeenPredicates, expr)
+	return r.tryImprovingVindex(ctx, expr)
+}
+
+func (r *Route) tryImprovingVindex(ctx *plancontext.PlanningContext, expr sqlparser.Expr) error {
 	if r.canImprove() {
 		newVindexFound, err := r.searchForNewVindexes(ctx, expr)
 		if err != nil {
@@ -606,7 +614,7 @@ func (r *Route) resetRoutingSelections(ctx *plancontext.PlanningContext) error {
 	}
 
 	for _, predicate := range r.SeenPredicates {
-		err := r.UpdateRoutingLogic(ctx, predicate)
+		err := r.tryImprovingVindex(ctx, predicate)
 		if err != nil {
 			return err
 		}
diff --git a/go/vt/vtgate/planbuilder/physical/route_planning.go b/go/vt/vtgate/planbuilder/physical/route_planning.go
index e7926c5ef6..4e86272ee2 100644
--- a/go/vt/vtgate/planbuilder/physical/route_planning.go
+++ b/go/vt/vtgate/planbuilder/physical/route_planning.go
@@ -21,6 +21,9 @@ import (
 	"fmt"
 	"io"
 
+	"vitess.io/vitess/go/vt/key"
+	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
+
 	querypb "vitess.io/vitess/go/vt/proto/query"
 
 	"vitess.io/vitess/go/mysql/collations"
@@ -100,11 +103,86 @@ func CreatePhysicalOperator(ctx *plancontext.PlanningContext, opTree abstract.Lo
 		filter.Source = src
 
 		return filter, nil
+	case *abstract.Update:
+		var typ topodatapb.TabletType
+		var dest key.Destination
+
+		vindexTable := op.TableInfo.GetVindexTable()
+		opCode := engine.Unsharded
+		if vindexTable.Keyspace.Sharded {
+			opCode = engine.Scatter
+		}
+
+		tblName, ok := op.Table.Alias.Expr.(sqlparser.TableName)
+		if ok {
+			var err error
+			_, _, _, typ, dest, err = ctx.VSchema.FindTableOrVindex(tblName)
+			if err != nil {
+				return nil, err
+			}
+			if dest != nil {
+				if typ != topodatapb.TabletType_PRIMARY {
+					return nil, vterrors.NewErrorf(vtrpcpb.Code_FAILED_PRECONDITION, vterrors.InnodbReadOnly, "unsupported: update statement with a replica target")
+				}
+				// we are dealing with an explicitly targeted UPDATE
+				opCode = engine.ByDestination
+			}
+		}
+
+		vp, cvv, ovq, err := getUpdateVindexInformation(op, vindexTable)
+		if err != nil {
+			return nil, err
+		}
+
+		r := &Route{
+			Source: &Update{
+				QTable:              op.Table,
+				VTable:              vindexTable,
+				Assignments:         op.Assignments,
+				ChangedVindexValues: cvv,
+				OwnedVindexQuery:    ovq,
+				AST:                 op.AST,
+			},
+			RouteOpCode:       opCode,
+			Keyspace:          vindexTable.Keyspace,
+			VindexPreds:       vp,
+			TargetDestination: dest,
+		}
+
+		for _, predicate := range op.Table.Predicates {
+			err := r.UpdateRoutingLogic(ctx, predicate)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if r.RouteOpCode == engine.Scatter && op.AST.Limit != nil {
+			// TODO systay: we should probably check for other op code types - IN could also hit multiple shards (2022-04-07)
+			return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "multi shard update with limit is not supported")
+		}
+
+		return r, nil
 	default:
 		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "invalid operator tree: %T", op)
 	}
 }
 
+func getUpdateVindexInformation(op *abstract.Update, vindexTable *vindexes.Table) ([]*VindexPlusPredicates, map[string]*engine.VindexValues, string, error) {
+	if !vindexTable.Keyspace.Sharded {
+		return nil, nil, "", nil
+	}
+	primaryVindex, vindexAndPredicates, err := getVindexInformation(op.TableID(), op.Table.Predicates, vindexTable)
+	if err != nil {
+		return nil, nil, "", err
+	}
+
+	changedVindexValues, ownedVindexQuery, err := buildChangedVindexesValues(op.AST, vindexTable, primaryVindex.Columns)
+	if err != nil {
+		return nil, nil, "", err
+	}
+	return vindexAndPredicates, changedVindexValues, ownedVindexQuery, nil
+}
+
 /*
 	The greedy planner will plan a query by finding first finding the best route plan for every table.
     Then, iteratively, it finds the cheapest join that can be produced between the remaining plans,
@@ -173,7 +251,10 @@ func createRoute(ctx *plancontext.PlanningContext, table *abstract.QueryTable, s
 	if table.IsInfSchema {
 		return createInfSchemaRoute(ctx, table)
 	}
-	vschemaTable, _, _, _, _, err := ctx.VSchema.FindTableOrVindex(table.Table)
+	vschemaTable, _, _, _, target, err := ctx.VSchema.FindTableOrVindex(table.Table)
+	if target != nil {
+		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: SELECT with a target destination")
+	}
 	if err != nil {
 		return nil, err
 	}
@@ -238,9 +319,87 @@ func createRoute(ctx *plancontext.PlanningContext, table *abstract.QueryTable, s
 		}
 	}
 
+	if plan.RouteOpCode == engine.Scatter && len(table.Predicates) > 0 {
+		// If we have a scatter query, it's worth spending a little extra time seeing if we can't improve it
+		for _, pred := range table.Predicates {
+			rewritten := tryRewriteOrToIn(pred)
+			if rewritten != nil {
+				err = plan.UpdateRoutingLogic(ctx, rewritten)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+	}
+
 	return plan, nil
 }
 
+func tryRewriteOrToIn(expr sqlparser.Expr) sqlparser.Expr {
+	rewrote := false
+	newPred := sqlparser.Rewrite(sqlparser.CloneExpr(expr), func(cursor *sqlparser.Cursor) bool {
+		_, ok := cursor.Node().(*sqlparser.OrExpr)
+		return ok
+	}, func(cursor *sqlparser.Cursor) bool {
+		// we are looking for the pattern WHERE c = 1 or c = 2
+		switch or := cursor.Node().(type) {
+		case *sqlparser.OrExpr:
+			lftCmp, ok := or.Left.(*sqlparser.ComparisonExpr)
+			if !ok {
+				return true
+			}
+			rgtCmp, ok := or.Right.(*sqlparser.ComparisonExpr)
+			if !ok {
+				return true
+			}
+
+			col, ok := lftCmp.Left.(*sqlparser.ColName)
+			if !ok || !sqlparser.EqualsExpr(lftCmp.Left, rgtCmp.Left) {
+				return true
+			}
+
+			var tuple sqlparser.ValTuple
+			switch lftCmp.Operator {
+			case sqlparser.EqualOp:
+				tuple = sqlparser.ValTuple{lftCmp.Right}
+			case sqlparser.InOp:
+				lft, ok := lftCmp.Right.(sqlparser.ValTuple)
+				if !ok {
+					return true
+				}
+				tuple = lft
+			default:
+				return true
+			}
+
+			switch rgtCmp.Operator {
+			case sqlparser.EqualOp:
+				tuple = append(tuple, rgtCmp.Right)
+			case sqlparser.InOp:
+				lft, ok := rgtCmp.Right.(sqlparser.ValTuple)
+				if !ok {
+					return true
+				}
+				tuple = append(tuple, lft...)
+			default:
+				return true
+			}
+
+			rewrote = true
+			cursor.Replace(&sqlparser.ComparisonExpr{
+				Operator: sqlparser.InOp,
+				Left:     col,
+				Right:    tuple,
+			})
+		}
+		return true
+	})
+	if rewrote {
+		return newPred.(sqlparser.Expr)
+	}
+	return nil
+}
+
 func createInfSchemaRoute(ctx *plancontext.PlanningContext, table *abstract.QueryTable) (*Route, error) {
 	ks, err := ctx.VSchema.AnyKeyspace()
 	if err != nil {
@@ -369,7 +528,7 @@ func getJoinFor(ctx *plancontext.PlanningContext, cm opCacheMap, lhs, rhs abstra
 func mergeOrJoin(ctx *plancontext.PlanningContext, lhs, rhs abstract.PhysicalOperator, joinPredicates []sqlparser.Expr, inner bool) (abstract.PhysicalOperator, error) {
 
 	merger := func(a, b *Route) (*Route, error) {
-		return createRouteOperatorForJoin(ctx, a, b, joinPredicates, inner)
+		return createRouteOperatorForJoin(a, b, joinPredicates, inner)
 	}
 
 	newPlan, _ := tryMerge(ctx, lhs, rhs, joinPredicates, merger)
@@ -387,7 +546,7 @@ func mergeOrJoin(ctx *plancontext.PlanningContext, lhs, rhs abstract.PhysicalOpe
 	return pushJoinPredicates(ctx, joinPredicates, join)
 }
 
-func createRouteOperatorForJoin(ctx *plancontext.PlanningContext, aRoute, bRoute *Route, joinPredicates []sqlparser.Expr, inner bool) (*Route, error) {
+func createRouteOperatorForJoin(aRoute, bRoute *Route, joinPredicates []sqlparser.Expr, inner bool) (*Route, error) {
 	// append system table names from both the routes.
 	sysTableName := aRoute.SysTableTableName
 	if sysTableName == nil {
@@ -634,7 +793,7 @@ func findColumnVindex(ctx *plancontext.PlanningContext, a *Route, exp sqlparser.
 	var singCol vindexes.SingleColumn
 
 	// for each equality expression that exp has with other column name, we check if it
-	// can be solved by any table in our routeTree a. If an equality expression can be solved,
+	// can be solved by any table in our routeTree. If an equality expression can be solved,
 	// we check if the equality expression and our table share the same vindex, if they do:
 	// the method will return the associated vindexes.SingleColumn.
 	for _, expr := range ctx.SemTable.GetExprAndEqualities(exp) {
@@ -645,12 +804,12 @@ func findColumnVindex(ctx *plancontext.PlanningContext, a *Route, exp sqlparser.
 		leftDep := ctx.SemTable.RecursiveDeps(expr)
 
 		_ = VisitOperators(a, func(rel abstract.PhysicalOperator) (bool, error) {
-			to, isTableOp := rel.(*Table)
+			to, isTableOp := rel.(abstract.IntroducesTable)
 			if !isTableOp {
 				return true, nil
 			}
-			if leftDep.IsSolvedBy(to.QTable.ID) {
-				for _, vindex := range to.VTable.ColumnVindexes {
+			if leftDep.IsSolvedBy(to.GetQTable().ID) {
+				for _, vindex := range to.GetVTable().ColumnVindexes {
 					sC, isSingle := vindex.Vindex.(vindexes.SingleColumn)
 					if isSingle && vindex.Columns[0].Equal(col.Name) {
 						singCol = sC
@@ -690,7 +849,7 @@ func VisitOperators(op abstract.PhysicalOperator, f func(tbl abstract.PhysicalOp
 	}
 
 	switch op := op.(type) {
-	case *Table, *Vindex:
+	case *Table, *Vindex, *Update:
 		// leaf - no children to visit
 	case *Route:
 		err := VisitOperators(op.Source, f)
diff --git a/go/vt/vtgate/planbuilder/physical/table.go b/go/vt/vtgate/planbuilder/physical/table.go
index fe95a05013..24e021ac20 100644
--- a/go/vt/vtgate/planbuilder/physical/table.go
+++ b/go/vt/vtgate/planbuilder/physical/table.go
@@ -32,6 +32,7 @@ type Table struct {
 }
 
 var _ abstract.PhysicalOperator = (*Table)(nil)
+var _ abstract.IntroducesTable = (*Table)(nil)
 
 // IPhysical implements the PhysicalOperator interface
 func (to *Table) IPhysical() {}
@@ -78,3 +79,13 @@ func (to *Table) CheckValid() error {
 func (to *Table) Compact(semTable *semantics.SemTable) (abstract.Operator, error) {
 	return to, nil
 }
+
+// GetQTable implements the IntroducesTable interface
+func (to *Table) GetQTable() *abstract.QueryTable {
+	return to.QTable
+}
+
+// GetVTable implements the IntroducesTable interface
+func (to *Table) GetVTable() *vindexes.Table {
+	return to.VTable
+}
diff --git a/go/vt/vtgate/planbuilder/physical/update.go b/go/vt/vtgate/planbuilder/physical/update.go
new file mode 100644
index 0000000000..37e4010c11
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/physical/update.go
@@ -0,0 +1,82 @@
+/*
+Copyright 2022 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 physical
+
+import (
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/abstract"
+	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+)
+
+type Update struct {
+	QTable              *abstract.QueryTable
+	VTable              *vindexes.Table
+	Assignments         map[string]sqlparser.Expr
+	ChangedVindexValues map[string]*engine.VindexValues
+	OwnedVindexQuery    string
+	AST                 *sqlparser.Update
+}
+
+var _ abstract.PhysicalOperator = (*Update)(nil)
+var _ abstract.IntroducesTable = (*Update)(nil)
+
+// TableID implements the PhysicalOperator interface
+func (u *Update) TableID() semantics.TableSet {
+	return u.QTable.ID
+}
+
+// UnsolvedPredicates implements the PhysicalOperator interface
+func (u *Update) UnsolvedPredicates(semTable *semantics.SemTable) []sqlparser.Expr {
+	return nil
+}
+
+// CheckValid implements the PhysicalOperator interface
+func (u *Update) CheckValid() error {
+	return nil
+}
+
+// IPhysical implements the PhysicalOperator interface
+func (u *Update) IPhysical() {}
+
+// Cost implements the PhysicalOperator interface
+func (u *Update) Cost() int {
+	return 1
+}
+
+// Clone implements the PhysicalOperator interface
+func (u *Update) Clone() abstract.PhysicalOperator {
+	return &Update{
+		QTable:              u.QTable,
+		VTable:              u.VTable,
+		Assignments:         u.Assignments,
+		ChangedVindexValues: u.ChangedVindexValues,
+		OwnedVindexQuery:    u.OwnedVindexQuery,
+		AST:                 u.AST,
+	}
+}
+
+// GetQTable implements the IntroducesTable interface
+func (u *Update) GetQTable() *abstract.QueryTable {
+	return u.QTable
+}
+
+// GetVTable implements the IntroducesTable interface
+func (u *Update) GetVTable() *vindexes.Table {
+	return u.VTable
+}
diff --git a/go/vt/vtgate/planbuilder/primitive_builder.go b/go/vt/vtgate/planbuilder/primitive_builder.go
index 88a2dfa0ae..29655e81e4 100644
--- a/go/vt/vtgate/planbuilder/primitive_builder.go
+++ b/go/vt/vtgate/planbuilder/primitive_builder.go
@@ -16,7 +16,9 @@ limitations under the License.
 
 package planbuilder
 
-import "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+import (
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+)
 
 // primitiveBuilder is the top level type for building plans.
 // It contains the current logicalPlan tree, the symtab and
diff --git a/go/vt/vtgate/planbuilder/primitive_wrapper.go b/go/vt/vtgate/planbuilder/primitive_wrapper.go
new file mode 100644
index 0000000000..0fac2f98f1
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/primitive_wrapper.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2022 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 (
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"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/semantics"
+)
+
+// primitiveWrapper is used when only need a logical plan that supports plan.Primitive() and nothing else
+type primitiveWrapper struct {
+	prim engine.Primitive
+	gen4Plan
+}
+
+func (p *primitiveWrapper) WireupGen4(*semantics.SemTable) error {
+	return nil
+}
+
+func (p *primitiveWrapper) Primitive() engine.Primitive {
+	return p.prim
+}
+
+func (p *primitiveWrapper) Inputs() []logicalPlan {
+	return nil
+}
+
+func (p *primitiveWrapper) Rewrite(...logicalPlan) error {
+	return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "can't rewrite")
+}
+
+func (p *primitiveWrapper) ContainsTables() semantics.TableSet {
+	return semantics.EmptyTableSet()
+}
+
+func (p *primitiveWrapper) OutputColumns() []sqlparser.SelectExpr {
+	return nil
+}
+
+var _ logicalPlan = (*primitiveWrapper)(nil)
diff --git a/go/vt/vtgate/planbuilder/rewrite.go b/go/vt/vtgate/planbuilder/rewrite.go
index a866176791..c8712134ea 100644
--- a/go/vt/vtgate/planbuilder/rewrite.go
+++ b/go/vt/vtgate/planbuilder/rewrite.go
@@ -31,7 +31,7 @@ type rewriter struct {
 	err          error
 }
 
-func queryRewrite(semTable *semantics.SemTable, reservedVars *sqlparser.ReservedVars, statement sqlparser.SelectStatement) error {
+func queryRewrite(semTable *semantics.SemTable, reservedVars *sqlparser.ReservedVars, statement sqlparser.Statement) error {
 	r := rewriter{
 		semTable:     semTable,
 		reservedVars: reservedVars,
diff --git a/go/vt/vtgate/planbuilder/select.go b/go/vt/vtgate/planbuilder/select.go
index 38bb39f23d..087646607f 100644
--- a/go/vt/vtgate/planbuilder/select.go
+++ b/go/vt/vtgate/planbuilder/select.go
@@ -35,7 +35,7 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-func buildSelectPlan(query string) selectPlanner {
+func buildSelectPlan(query string) stmtPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
 		sel := stmt.(*sqlparser.Select)
 		if sel.With != nil {
diff --git a/go/vt/vtgate/planbuilder/union.go b/go/vt/vtgate/planbuilder/union.go
index 32dbf5ad21..3a355de6eb 100644
--- a/go/vt/vtgate/planbuilder/union.go
+++ b/go/vt/vtgate/planbuilder/union.go
@@ -31,7 +31,7 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-func buildUnionPlan(string) selectPlanner {
+func buildUnionPlan(string) stmtPlanner {
 	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
 		union := stmt.(*sqlparser.Union)
 		if union.With != nil {
diff --git a/go/vt/vtgate/planbuilder/update.go b/go/vt/vtgate/planbuilder/update_planner.go
similarity index 82%
rename from go/vt/vtgate/planbuilder/update.go
rename to go/vt/vtgate/planbuilder/update_planner.go
index b787e78fbf..406ef8e23c 100644
--- a/go/vt/vtgate/planbuilder/update.go
+++ b/go/vt/vtgate/planbuilder/update_planner.go
@@ -29,33 +29,35 @@ import (
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 )
 
-// buildUpdatePlan builds the instructions for an UPDATE statement.
-func buildUpdatePlan(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
-	upd := stmt.(*sqlparser.Update)
-	if upd.With != nil {
-		return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in update statement")
-	}
-	dml, ksidVindex, err := buildDMLPlan(vschema, "update", stmt, reservedVars, upd.TableExprs, upd.Where, upd.OrderBy, upd.Limit, upd.Comments, upd.Exprs)
-	if err != nil {
-		return nil, err
-	}
-	eupd := &engine.Update{DML: dml}
+// buildUpdatePlan returns a stmtPlanner that builds the instructions for an UPDATE statement.
+func buildUpdatePlan(query string) stmtPlanner {
+	return func(stmt sqlparser.Statement, reservedVars *sqlparser.ReservedVars, vschema plancontext.VSchema) (engine.Primitive, error) {
+		upd := stmt.(*sqlparser.Update)
+		if upd.With != nil {
+			return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: with expression in update statement")
+		}
+		dml, ksidVindex, err := buildDMLPlan(vschema, "update", stmt, reservedVars, upd.TableExprs, upd.Where, upd.OrderBy, upd.Limit, upd.Comments, upd.Exprs)
+		if err != nil {
+			return nil, err
+		}
+		eupd := &engine.Update{DML: dml}
 
-	if dml.Opcode == engine.Unsharded {
-		return eupd, nil
-	}
+		if dml.Opcode == engine.Unsharded {
+			return eupd, nil
+		}
 
-	cvv, ovq, err := buildChangedVindexesValues(upd, eupd.Table, ksidVindex.Columns)
-	if err != nil {
-		return nil, err
-	}
-	eupd.ChangedVindexValues = cvv
-	eupd.OwnedVindexQuery = ovq
-	if len(eupd.ChangedVindexValues) != 0 {
-		eupd.KsidVindex = ksidVindex.Vindex
-		eupd.KsidLength = len(ksidVindex.Columns)
+		cvv, ovq, err := buildChangedVindexesValues(upd, eupd.Table, ksidVindex.Columns)
+		if err != nil {
+			return nil, err
+		}
+		eupd.ChangedVindexValues = cvv
+		eupd.OwnedVindexQuery = ovq
+		if len(eupd.ChangedVindexValues) != 0 {
+			eupd.KsidVindex = ksidVindex.Vindex
+			eupd.KsidLength = len(ksidVindex.Columns)
+		}
+		return eupd, nil
 	}
-	return eupd, nil
 }
 
 // buildChangedVindexesValues adds to the plan all the lookup vindexes that are changing.
diff --git a/go/vt/vtgate/semantics/analyzer.go b/go/vt/vtgate/semantics/analyzer.go
index f191fdc4fe..0ad2d3cb1d 100644
--- a/go/vt/vtgate/semantics/analyzer.go
+++ b/go/vt/vtgate/semantics/analyzer.go
@@ -60,7 +60,7 @@ func newAnalyzer(dbName string, si SchemaInformation) *analyzer {
 }
 
 // Analyze analyzes the parsed query.
-func Analyze(statement sqlparser.SelectStatement, currentDb string, si SchemaInformation) (*SemTable, error) {
+func Analyze(statement sqlparser.Statement, currentDb string, si SchemaInformation) (*SemTable, error) {
 	analyzer := newAnalyzer(currentDb, si)
 
 	// Analysis for initial scope
@@ -75,7 +75,13 @@ func Analyze(statement sqlparser.SelectStatement, currentDb string, si SchemaInf
 	return semTable, nil
 }
 
-func (a analyzer) newSemTable(statement sqlparser.SelectStatement, coll collations.ID) *SemTable {
+func (a analyzer) newSemTable(statement sqlparser.Statement, coll collations.ID) *SemTable {
+	var comments *sqlparser.ParsedComments
+	commentedStmt, isCommented := statement.(sqlparser.Commented)
+	if isCommented {
+		comments = commentedStmt.GetParsedComments()
+	}
+
 	return &SemTable{
 		Recursive:         a.binder.recursive,
 		Direct:            a.binder.direct,
@@ -85,7 +91,7 @@ func (a analyzer) newSemTable(statement sqlparser.SelectStatement, coll collatio
 		NotSingleRouteErr: a.projErr,
 		NotUnshardedErr:   a.unshardedErr,
 		Warning:           a.warning,
-		Comments:          statement.GetParsedComments(),
+		Comments:          comments,
 		SubqueryMap:       a.binder.subqueryMap,
 		SubqueryRef:       a.binder.subqueryRef,
 		ColumnEqualities:  map[columnName][]sqlparser.Expr{},
@@ -246,6 +252,18 @@ func (a *analyzer) analyze(statement sqlparser.Statement) error {
 
 func (a *analyzer) checkForInvalidConstructs(cursor *sqlparser.Cursor) error {
 	switch node := cursor.Node().(type) {
+	case *sqlparser.Update:
+		if len(node.TableExprs) != 1 {
+			return UnshardedError{Inner: vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: multiple tables in update")}
+		}
+		alias, isAlias := node.TableExprs[0].(*sqlparser.AliasedTableExpr)
+		if !isAlias {
+			return UnshardedError{Inner: vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: multiple tables in update")}
+		}
+		_, isDerived := alias.Expr.(*sqlparser.DerivedTable)
+		if isDerived {
+			return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.NonUpdateableTable, "The target table %s of the UPDATE is not updatable", alias.As.String())
+		}
 	case *sqlparser.Select:
 		parent := cursor.Parent()
 		if _, isUnion := parent.(*sqlparser.Union); isUnion && node.SQLCalcFoundRows {
diff --git a/go/vt/vtgate/semantics/binder.go b/go/vt/vtgate/semantics/binder.go
index aeddddad02..13c06709fd 100644
--- a/go/vt/vtgate/semantics/binder.go
+++ b/go/vt/vtgate/semantics/binder.go
@@ -35,7 +35,7 @@ type binder struct {
 	tc          *tableCollector
 	org         originable
 	typer       *typer
-	subqueryMap map[*sqlparser.Select][]*sqlparser.ExtractedSubquery
+	subqueryMap map[sqlparser.Statement][]*sqlparser.ExtractedSubquery
 	subqueryRef map[*sqlparser.Subquery]*sqlparser.ExtractedSubquery
 }
 
@@ -47,7 +47,7 @@ func newBinder(scoper *scoper, org originable, tc *tableCollector, typer *typer)
 		org:         org,
 		tc:          tc,
 		typer:       typer,
-		subqueryMap: map[*sqlparser.Select][]*sqlparser.ExtractedSubquery{},
+		subqueryMap: map[sqlparser.Statement][]*sqlparser.ExtractedSubquery{},
 		subqueryRef: map[*sqlparser.Subquery]*sqlparser.ExtractedSubquery{},
 	}
 }
@@ -61,7 +61,7 @@ func (b *binder) up(cursor *sqlparser.Cursor) error {
 			return err
 		}
 
-		b.subqueryMap[currScope.selectStmt] = append(b.subqueryMap[currScope.selectStmt], sq)
+		b.subqueryMap[currScope.stmt] = append(b.subqueryMap[currScope.stmt], sq)
 		b.subqueryRef[node] = sq
 
 		b.setSubQueryDependencies(node, currScope)
@@ -122,7 +122,7 @@ func (b *binder) setSubQueryDependencies(subq *sqlparser.Subquery, currScope *sc
 }
 
 func (b *binder) createExtractedSubquery(cursor *sqlparser.Cursor, currScope *scope, subq *sqlparser.Subquery) (*sqlparser.ExtractedSubquery, error) {
-	if currScope.selectStmt == nil {
+	if currScope.stmt == nil {
 		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] unable to bind subquery to select statement")
 	}
 
diff --git a/go/vt/vtgate/semantics/early_rewriter.go b/go/vt/vtgate/semantics/early_rewriter.go
index 6b41f07f85..f8ef08953b 100644
--- a/go/vt/vtgate/semantics/early_rewriter.go
+++ b/go/vt/vtgate/semantics/early_rewriter.go
@@ -102,21 +102,25 @@ func (r *earlyRewriter) rewriteOrderByExpr(node *sqlparser.Literal) (sqlparser.E
 	num, err := strconv.Atoi(node.Val)
 	if err != nil {
 		return nil, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "error parsing column number: %s", node.Val)
-
 	}
-	if num < 1 || num > len(currScope.selectStmt.SelectExprs) {
+	stmt, isSel := currScope.stmt.(*sqlparser.Select)
+	if !isSel {
+		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "error invalid statement type, expect Select, got: %T", currScope.stmt)
+	}
+
+	if num < 1 || num > len(stmt.SelectExprs) {
 		return nil, vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.BadFieldError, "Unknown column '%d' in '%s'", num, r.clause)
 	}
 
 	for i := 0; i < num; i++ {
-		expr := currScope.selectStmt.SelectExprs[i]
+		expr := stmt.SelectExprs[i]
 		_, ok := expr.(*sqlparser.AliasedExpr)
 		if !ok {
 			return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "cannot use column offsets in %s when using `%s`", r.clause, sqlparser.String(expr))
 		}
 	}
 
-	aliasedExpr, ok := currScope.selectStmt.SelectExprs[num-1].(*sqlparser.AliasedExpr)
+	aliasedExpr, ok := stmt.SelectExprs[num-1].(*sqlparser.AliasedExpr)
 	if !ok {
 		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "don't know how to handle %s", sqlparser.String(node))
 	}
diff --git a/go/vt/vtgate/semantics/scoper.go b/go/vt/vtgate/semantics/scoper.go
index fcc4455f54..1d70829bed 100644
--- a/go/vt/vtgate/semantics/scoper.go
+++ b/go/vt/vtgate/semantics/scoper.go
@@ -40,10 +40,10 @@ type (
 	}
 
 	scope struct {
-		parent     *scope
-		selectStmt *sqlparser.Select
-		tables     []TableInfo
-		isUnion    bool
+		parent  *scope
+		stmt    sqlparser.Statement
+		tables  []TableInfo
+		isUnion bool
 	}
 )
 
@@ -58,12 +58,17 @@ func newScoper() *scoper {
 func (s *scoper) down(cursor *sqlparser.Cursor) error {
 	node := cursor.Node()
 	switch node := node.(type) {
+	case *sqlparser.Update:
+		currScope := newScope(s.currentScope())
+		s.push(currScope)
+
+		currScope.stmt = node
 	case *sqlparser.Select:
 		currScope := newScope(s.currentScope())
 		s.push(currScope)
 
 		// Needed for order by with Literal to find the Expression.
-		currScope.selectStmt = node
+		currScope.stmt = node
 
 		s.rScope[node] = currScope
 		s.wScope[node] = newScope(nil)
@@ -74,7 +79,7 @@ func (s *scoper) down(cursor *sqlparser.Cursor) error {
 			// To create this special context, we create a special scope here that is then merged with
 			// the surrounding scope when we come back out from the JOIN
 			nScope := newScope(nil)
-			nScope.selectStmt = cursor.Parent().(*sqlparser.Select)
+			nScope.stmt = cursor.Parent().(*sqlparser.Select)
 			s.push(nScope)
 		}
 	case sqlparser.SelectExprs:
@@ -149,7 +154,7 @@ func (s *scoper) up(cursor *sqlparser.Cursor) error {
 		if isParentSelectStatement(cursor) {
 			s.popScope()
 		}
-	case *sqlparser.Select, sqlparser.GroupBy:
+	case *sqlparser.Select, sqlparser.GroupBy, *sqlparser.Update:
 		s.popScope()
 	case *sqlparser.Where:
 		if node.Type != sqlparser.HavingClause {
@@ -186,7 +191,7 @@ func (s *scoper) createSpecialScopePostProjection(parent sqlparser.SQLNode) erro
 		incomingScope := s.currentScope()
 		nScope := newScope(incomingScope)
 		nScope.tables = s.wScope[parent].tables
-		nScope.selectStmt = incomingScope.selectStmt
+		nScope.stmt = incomingScope.stmt
 		s.push(nScope)
 
 		if s.rScope[parent] != incomingScope {
@@ -199,7 +204,7 @@ func (s *scoper) createSpecialScopePostProjection(parent sqlparser.SQLNode) erro
 
 		for i, sel := range sqlparser.GetAllSelects(parent) {
 			if i == 0 {
-				nScope.selectStmt = sel
+				nScope.stmt = sel
 				tableInfo = createVTableInfoForExpressions(sel.SelectExprs, nil /*needed for star expressions*/, s.org)
 				nScope.tables = append(nScope.tables, tableInfo)
 			}
diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go
index ae67faf9c7..4fe0ce7537 100644
--- a/go/vt/vtgate/semantics/semantic_state.go
+++ b/go/vt/vtgate/semantics/semantic_state.go
@@ -89,7 +89,7 @@ type (
 		ExprTypes   map[sqlparser.Expr]Type
 		selectScope map[*sqlparser.Select]*scope
 		Comments    *sqlparser.ParsedComments
-		SubqueryMap map[*sqlparser.Select][]*sqlparser.ExtractedSubquery
+		SubqueryMap map[sqlparser.Statement][]*sqlparser.ExtractedSubquery
 		SubqueryRef map[*sqlparser.Subquery]*sqlparser.ExtractedSubquery
 
 		// ColumnEqualities is used to enable transitive closures
diff --git a/go/vt/vtgate/semantics/table_collector.go b/go/vt/vtgate/semantics/table_collector.go
index bc8c648838..9af71cad99 100644
--- a/go/vt/vtgate/semantics/table_collector.go
+++ b/go/vt/vtgate/semantics/table_collector.go
@@ -17,7 +17,6 @@ limitations under the License.
 package semantics
 
 import (
-	"vitess.io/vitess/go/vt/key"
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
@@ -90,14 +89,10 @@ func (tc *tableCollector) up(cursor *sqlparser.Cursor) error {
 			isInfSchema = true
 		} else {
 			var err error
-			var target key.Destination
-			tbl, vindex, _, _, target, err = tc.si.FindTableOrVindex(t)
+			tbl, vindex, _, _, _, err = tc.si.FindTableOrVindex(t)
 			if err != nil {
 				return err
 			}
-			if target != nil {
-				return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: SELECT with a target destination")
-			}
 			if tbl == nil && vindex != nil {
 				tbl = newVindexTable(t.Name)
 			}


Test patch:
diff --git a/go/test/endtoend/cluster/vtctlclient_process.go b/go/test/endtoend/cluster/vtctlclient_process.go
index 6de7e10ac5..7004fbd97d 100644
--- a/go/test/endtoend/cluster/vtctlclient_process.go
+++ b/go/test/endtoend/cluster/vtctlclient_process.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"os/exec"
 	"strings"
+	"time"
 
 	"vitess.io/vitess/go/vt/vterrors"
 
@@ -189,19 +190,31 @@ func (vtctlclient *VtctlClientProcess) ExecuteCommand(args ...string) (err error
 }
 
 // ExecuteCommandWithOutput executes any vtctlclient command and returns output
-func (vtctlclient *VtctlClientProcess) ExecuteCommandWithOutput(args ...string) (result string, err error) {
+func (vtctlclient *VtctlClientProcess) ExecuteCommandWithOutput(args ...string) (string, error) {
+	var resultByte []byte
+	var resultStr string
+	var err error
+	retries := 10
+	retryDelay := 1 * time.Second
 	pArgs := []string{"--server", vtctlclient.Server}
 	if *isCoverage {
 		pArgs = append(pArgs, "--test.coverprofile="+getCoveragePath("vtctlclient-"+args[0]+".out"), "--test.v")
 	}
 	pArgs = append(pArgs, args...)
-	tmpProcess := exec.Command(
-		vtctlclient.Binary,
-		filterDoubleDashArgs(pArgs, vtctlclient.VtctlClientMajorVersion)...,
-	)
-	log.Infof("Executing vtctlclient with command: %v", strings.Join(tmpProcess.Args, " "))
-	resultByte, err := tmpProcess.CombinedOutput()
-	return filterResultWhenRunsForCoverage(string(resultByte)), err
+	for i := 1; i <= retries; i++ {
+		tmpProcess := exec.Command(
+			vtctlclient.Binary,
+			filterDoubleDashArgs(pArgs, vtctlclient.VtctlClientMajorVersion)...,
+		)
+		log.Infof("Executing vtctlclient with command: %v (attempt %d of %d)", strings.Join(tmpProcess.Args, " "), i, retries)
+		resultByte, err = tmpProcess.CombinedOutput()
+		resultStr = string(resultByte)
+		if err == nil || !shouldRetry(resultStr) {
+			break
+		}
+		time.Sleep(retryDelay)
+	}
+	return filterResultWhenRunsForCoverage(resultStr), err
 }
 
 // VtctlClientProcessInstance returns a VtctlProcess handle for vtctlclient process
@@ -241,3 +254,12 @@ func (vtctlclient *VtctlClientProcess) InitTablet(tablet *Vttablet, cell string,
 	args = append(args, fmt.Sprintf("%s-%010d", cell, tablet.TabletUID), tabletType)
 	return vtctlclient.ExecuteCommand(args...)
 }
+
+// shouldRetry tells us if the command should be retried based on the results/output -- meaning that it
+// is likely an ephemeral or recoverable issue that is likely to succeed when retried.
+func shouldRetry(cmdResults string) bool {
+	if strings.Contains(cmdResults, "Deadlock found when trying to get lock; try restarting transaction") {
+		return true
+	}
+	return false
+}
diff --git a/go/test/endtoend/vtgate/queries/subquery/subquery_test.go b/go/test/endtoend/vtgate/queries/subquery/subquery_test.go
index deed499f22..d76b4ec521 100644
--- a/go/test/endtoend/vtgate/queries/subquery/subquery_test.go
+++ b/go/test/endtoend/vtgate/queries/subquery/subquery_test.go
@@ -91,7 +91,20 @@ func TestSubqueryInINClause(t *testing.T) {
 	mcmp, closer := start(t)
 	defer closer()
 
-	defer mcmp.Exec(`delete from t1`)
 	mcmp.Exec("insert into t1(id1, id2) values(0,0),(1,1)")
 	mcmp.AssertMatches(`SELECT id2 FROM t1 WHERE id1 IN (SELECT 1 FROM dual)`, `[[INT64(1)]]`)
 }
+
+func TestSubqueryInUpdate(t *testing.T) {
+	mcmp, closer := start(t)
+	defer closer()
+
+	conn := mcmp.VtConn
+
+	utils.Exec(t, conn, `insert into t1(id1, id2) values (1, 10), (2, 20), (3, 30), (4, 40), (5, 50)`)
+	utils.Exec(t, conn, `insert into t2(id3, id4) values (1, 3), (2, 4)`)
+	utils.AssertMatches(t, conn, `SELECT id2, keyspace_id FROM t1_id2_idx WHERE id2 IN (2,10)`, `[[INT64(10) VARBINARY("\x16k@\xb4J\xbaK\xd6")]]`)
+	utils.Exec(t, conn, `update /*vt+ PLANNER=gen4 */ t1 set id2 = (select count(*) from t2) where id1 = 1`)
+	utils.AssertMatches(t, conn, `SELECT id2 FROM t1 WHERE id1 = 1`, `[[INT64(2)]]`)
+	utils.AssertMatches(t, conn, `SELECT id2, keyspace_id FROM t1_id2_idx WHERE id2 IN (2,10)`, `[[INT64(2) VARBINARY("\x16k@\xb4J\xbaK\xd6")]]`)
+}
diff --git a/go/vt/vtexplain/testdata/multi-output/updatesharded-output.txt b/go/vt/vtexplain/testdata/multi-output/updatesharded-output.txt
index 4cb74a9400..9787bffa2c 100644
--- a/go/vt/vtexplain/testdata/multi-output/updatesharded-output.txt
+++ b/go/vt/vtexplain/testdata/multi-output/updatesharded-output.txt
@@ -52,19 +52,19 @@ update user set name='alicia' where name='alice'
 8 ks_sharded/c0-: commit
 
 ----------------------------------------------------------------------
-update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname=1 where nickname != ''
+update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info='apa' where name != 'hog'
 
 1 ks_sharded/-40: begin
-1 ks_sharded/-40: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname = 1 where nickname != '' limit 10001
+1 ks_sharded/-40: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info = 'apa' where `name` != 'hog' limit 10001
 1 ks_sharded/-40: commit
 1 ks_sharded/40-80: begin
-1 ks_sharded/40-80: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname = 1 where nickname != '' limit 10001
+1 ks_sharded/40-80: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info = 'apa' where `name` != 'hog' limit 10001
 1 ks_sharded/40-80: commit
 1 ks_sharded/80-c0: begin
-1 ks_sharded/80-c0: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname = 1 where nickname != '' limit 10001
+1 ks_sharded/80-c0: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info = 'apa' where `name` != 'hog' limit 10001
 1 ks_sharded/80-c0: commit
 1 ks_sharded/c0-: begin
-1 ks_sharded/c0-: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname = 1 where nickname != '' limit 10001
+1 ks_sharded/c0-: update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info = 'apa' where `name` != 'hog' limit 10001
 1 ks_sharded/c0-: commit
 
 ----------------------------------------------------------------------
diff --git a/go/vt/vtexplain/testdata/updatesharded-queries.sql b/go/vt/vtexplain/testdata/updatesharded-queries.sql
index f2e1ecdab3..f947b14c45 100644
--- a/go/vt/vtexplain/testdata/updatesharded-queries.sql
+++ b/go/vt/vtexplain/testdata/updatesharded-queries.sql
@@ -10,7 +10,7 @@ update user set name='alicia' where name='alice';
 /* update name_info set has_nickname=1 where nickname != ''; */
 
 /* scatter update autocommit */
-update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set has_nickname=1 where nickname != '';
+update /*vt+ MULTI_SHARD_AUTOCOMMIT=1 */ name_info set info='apa' where name != 'hog';
 
 /* multi-shard update by secondary vindex */
 update user set pet='rover' where name='alice';
diff --git a/go/vt/vtgate/executor_dml_test.go b/go/vt/vtgate/executor_dml_test.go
index a3e29e53de..b3a411ba84 100644
--- a/go/vt/vtgate/executor_dml_test.go
+++ b/go/vt/vtgate/executor_dml_test.go
@@ -132,6 +132,40 @@ func TestUpdateEqual(t *testing.T) {
 	assertQueries(t, sbclookup, wantQueries)
 }
 
+func TestUpdateFromSubQuery(t *testing.T) {
+	executor, sbc1, sbc2, _ := createExecutorEnv()
+
+	logChan := QueryLogger.Subscribe("Test")
+	defer QueryLogger.Unsubscribe(logChan)
+
+	fields := []*querypb.Field{
+		{Name: "count(*)", Type: sqltypes.Int64},
+	}
+	sbc2.SetResults([]*sqltypes.Result{{
+		Fields: fields,
+		Rows: [][]sqltypes.Value{{
+			sqltypes.NewInt64(4),
+		}},
+	}})
+
+	// Update by primary vindex, but first execute subquery
+	_, err := executorExec(executor, "update user set a=(select count(*) from user where id = 3) where id = 1", nil)
+	require.NoError(t, err)
+	wantQueriesSbc1 := []*querypb.BoundQuery{{
+		Sql: "update `user` set a = :__sq1 where id = 1",
+		BindVariables: map[string]*querypb.BindVariable{
+			"__sq1": sqltypes.Int64BindVariable(4),
+		},
+	}}
+	wantQueriesSbc2 := []*querypb.BoundQuery{{
+		Sql:           "select count(*) from `user` where id = 3 lock in share mode",
+		BindVariables: map[string]*querypb.BindVariable{},
+	}}
+	assertQueries(t, sbc1, wantQueriesSbc1)
+	assertQueries(t, sbc2, wantQueriesSbc2)
+	testQueryLog(t, logChan, "TestExecute", "UPDATE", "update user set a=(select count(*) from user where id = 3) where id = 1", 2)
+}
+
 func TestUpdateEqualWithWriteOnlyLookupUniqueVindex(t *testing.T) {
 	res := []*sqltypes.Result{sqltypes.MakeTestResult(
 		sqltypes.MakeTestFields("id|wo_lu_col|lu_col|t2_lu_vdx", "int64|int64|int64|int64"),
diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go
index 1c07300456..a80d6fcf74 100644
--- a/go/vt/vtgate/executor_framework_test.go
+++ b/go/vt/vtgate/executor_framework_test.go
@@ -441,7 +441,6 @@ func init() {
 }
 
 func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn.SandboxConn) {
-	// Use legacy gateway until we can rewrite these tests to use new tabletgateway
 	cell := "aa"
 	hc := discovery.NewFakeHealthCheck(nil)
 	s := createSandbox("TestExecutor")
diff --git a/go/vt/vtgate/planbuilder/abstract/operator_test.go b/go/vt/vtgate/planbuilder/abstract/operator_test.go
index 3cf4c43efb..b7b3fc07a6 100644
--- a/go/vt/vtgate/planbuilder/abstract/operator_test.go
+++ b/go/vt/vtgate/planbuilder/abstract/operator_test.go
@@ -98,10 +98,10 @@ func TestOperator(t *testing.T) {
 			require.NoError(t, err)
 			tree, err := sqlparser.Parse(tc.query)
 			require.NoError(t, err)
-			stmt := tree.(sqlparser.SelectStatement)
+			stmt := tree.(sqlparser.Statement)
 			semTable, err := semantics.Analyze(stmt, "", si)
 			require.NoError(t, err)
-			optree, err := CreateOperatorFromAST(stmt, semTable)
+			optree, err := CreateLogicalOperatorFromAST(stmt, semTable)
 			require.NoError(t, err)
 			output := testString(optree)
 			if tc.expected != output {
@@ -164,8 +164,15 @@ func testString(op Operator) string {
 			dist = "(distinct)"
 		}
 		return fmt.Sprintf("Concatenate%s {\n%s\n}", dist, strings.Join(inners, ",\n"))
+	case *Update:
+		tbl := "table: " + op.Table.testString()
+		var assignments []string
+		for name, expr := range op.Assignments {
+			assignments = append(assignments, fmt.Sprintf("\t%s = %s", name, sqlparser.String(expr)))
+		}
+		return fmt.Sprintf("Update {\n\t%s\nassignments:\n%s\n}", tbl, strings.Join(assignments, "\n"))
 	}
-	return fmt.Sprintf("implement me: %T", op)
+	panic(fmt.Sprintf("%T", op))
 }
 
 func indent(s string) string {
diff --git a/go/vt/vtgate/planbuilder/abstract/operator_test_data.txt b/go/vt/vtgate/planbuilder/abstract/operator_test_data.txt
index 23d23666eb..8e162a84ca 100644
--- a/go/vt/vtgate/planbuilder/abstract/operator_test_data.txt
+++ b/go/vt/vtgate/planbuilder/abstract/operator_test_data.txt
@@ -444,3 +444,35 @@ OuterJoin: {
 	}
 	Predicate: t.id = y.t_id
 }
+
+update tbl set col1 = apa
+Update {
+	table: 	TableSet{0}:tbl
+assignments:
+	col1 = apa
+}
+
+update tbl set col1 = 'apa', col2 = 1337 where id = 12 and name = 'gangal'
+Update {
+	table: 	TableSet{0}:tbl where id = 12 and `name` = 'gangal'
+assignments:
+	col1 = 'apa'
+	col2 = 1337
+}
+
+update user set u = 1 where id = (select id from user_extra where id = 42)
+SubQuery: {
+	SubQueries: [
+	{
+		Type: PulloutValue
+		Query: 	QueryGraph: {
+		Tables:
+			TableSet{1}:user_extra where id = 42
+		}
+	}]
+	Outer: 	Update {
+		table: 	TableSet{0}:`user` where id = (select id from user_extra where id = 42)
+	assignments:
+		u = 1
+	}
+}
diff --git a/go/vt/vtgate/planbuilder/fallback_planner_test.go b/go/vt/vtgate/planbuilder/fallback_planner_test.go
index e02601566f..b277511611 100644
--- a/go/vt/vtgate/planbuilder/fallback_planner_test.go
+++ b/go/vt/vtgate/planbuilder/fallback_planner_test.go
@@ -37,7 +37,7 @@ type testPlanner struct {
 	called      bool
 }
 
-var _ selectPlanner = (*testPlanner)(nil).plan
+var _ stmtPlanner = (*testPlanner)(nil).plan
 
 func (tp *testPlanner) plan(statement sqlparser.Statement, vars *sqlparser.ReservedVars, schema plancontext.VSchema) (engine.Primitive, error) {
 	tp.called = true
diff --git a/go/vt/vtgate/planbuilder/testdata/dml_cases.txt b/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
index 891eb9e740..665d9f24c6 100644
--- a/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/dml_cases.txt
@@ -144,7 +144,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update route1 set a=1 where id=1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` as route1 set a = 1 where id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update: routing rules for subquery.
 "update unsharded_a set a=(select a from route2)"
@@ -206,7 +225,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set val = 1 where id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set val = 1 where id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id with alias
 "update user as user_alias set val = 1 where user_alias.id = 1"
@@ -230,7 +268,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user as user_alias set val = 1 where user_alias.id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` as user_alias set val = 1 where user_alias.id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id with parenthesized expression
 "update user set val = 1 where (id = 1)"
@@ -254,7 +311,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set val = 1 where (id = 1)",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set val = 1 where id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id with multi-part where clause with parens
 "update user set val = 1 where (name = 'foo' and id = 1)"
@@ -278,7 +354,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set val = 1 where (name = 'foo' and id = 1)",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set val = 1 where `name` = 'foo' and id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id, changing one vindex column
 "update user_metadata set email = 'juan@vitess.io' where user_id = 1"
@@ -308,7 +403,32 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user_metadata set email = 'juan@vitess.io' where user_id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "email_user_map:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "user_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select user_id, email, address, email = 'juan@vitess.io' from user_metadata where user_id = 1 for update",
+    "Query": "update user_metadata set email = 'juan@vitess.io' where user_id = 1",
+    "Table": "user_metadata",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id, changing same vindex twice
 "update user_metadata set email = 'a', email = 'b' where user_id = 1"
@@ -344,7 +464,33 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user_metadata set email = 'juan@vitess.io', address = '155 5th street' where user_id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "address_user_map:4",
+      "email_user_map:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "user_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select user_id, email, address, email = 'juan@vitess.io', address = '155 5th street' from user_metadata where user_id = 1 for update",
+    "Query": "update user_metadata set email = 'juan@vitess.io', address = '155 5th street' where user_id = 1",
+    "Table": "user_metadata",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id, changing one vindex column, using order by and limit
 "update user_metadata set email = 'juan@vitess.io' where user_id = 1 order by user_id asc limit 10"
@@ -374,7 +520,32 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user_metadata set email = 'juan@vitess.io' where user_id = 1 order by user_id asc limit 10",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "email_user_map:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "user_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select user_id, email, address, email = 'juan@vitess.io' from user_metadata where user_id = 1 order by user_id asc limit 10 for update",
+    "Query": "update user_metadata set email = 'juan@vitess.io' where user_id = 1 order by user_id asc limit 10",
+    "Table": "user_metadata",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id, stray where clause
 "update user set val = 1 where id = id2 and id = 1"
@@ -398,7 +569,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set val = 1 where id = id2 and id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set val = 1 where id = id2 and id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # update by primary keyspace id, stray where clause with conversion error
 "update user set val = 1 where id = 18446744073709551616 and id = 1"
@@ -422,7 +612,26 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set val = 1 where id = 18446744073709551616 and id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set val = 1 where id = 18446744073709551616 and id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # delete from by primary keyspace id
 "delete from user where id = 1"
@@ -576,7 +785,26 @@ Gen4 plan same as above
     "Vindex": "music_user_map"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update music set val = 1 where id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update music set val = 1 where id = 1",
+    "Table": "music",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "music_user_map"
+  }
+}
 
 # update multi-table ansi join
 "update unsharded_a a join unsharded_b b on a.id = b.id set a.val = 'foo' where b.val = 1"
@@ -1720,30 +1948,25 @@ Gen4 plan same as above
     "Vindex": "kid_index"
   }
 }
-Gen4 plan same as above
-
-# update multiple vindexes, with multi column vindex
-"update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1"
 {
   "QueryType": "UPDATE",
-  "Original": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+  "Original": "update multicolvin set column_b = 1, column_c = 2 where kid = 1",
   "Instructions": {
     "OperatorType": "Update",
-    "Variant": "Equal",
+    "Variant": "EqualUnique",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
     "TargetTabletType": "PRIMARY",
     "ChangedVindexValues": [
-      "cola_map:4",
-      "colb_colc_map:5"
+      "colb_colc_map:4"
     ],
     "KsidLength": 1,
     "KsidVindex": "kid_index",
     "MultiShardAutocommit": false,
-    "OwnedVindexQuery": "select kid, column_a, column_b, column_c, column_a = 0, column_b = 1 and column_c = 2 from multicolvin where kid = 1 for update",
-    "Query": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+    "OwnedVindexQuery": "select kid, column_a, column_b, column_c, column_b = 1 and column_c = 2 from multicolvin where kid = 1 for update",
+    "Query": "update multicolvin set column_b = 1, column_c = 2 where kid = 1",
     "Table": "multicolvin",
     "Values": [
       "INT64(1)"
@@ -1751,10 +1974,66 @@ Gen4 plan same as above
     "Vindex": "kid_index"
   }
 }
-Gen4 plan same as above
 
-# update with no primary vindex on where clause (scatter update)
-"update user_extra set val = 1"
+# update multiple vindexes, with multi column vindex
+"update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1"
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "Equal",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "cola_map:4",
+      "colb_colc_map:5"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "kid_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select kid, column_a, column_b, column_c, column_a = 0, column_b = 1 and column_c = 2 from multicolvin where kid = 1 for update",
+    "Query": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+    "Table": "multicolvin",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "kid_index"
+  }
+}
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "cola_map:4",
+      "colb_colc_map:5"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "kid_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select kid, column_a, column_b, column_c, column_a = 0, column_b = 1 and column_c = 2 from multicolvin where kid = 1 for update",
+    "Query": "update multicolvin set column_a = 0, column_b = 1, column_c = 2 where kid = 1",
+    "Table": "multicolvin",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "kid_index"
+  }
+}
+
+# update with no primary vindex on where clause (scatter update)
+"update user_extra set val = 1"
 {
   "QueryType": "UPDATE",
   "Original": "update user_extra set val = 1",
@@ -2149,7 +2428,32 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set name = null where id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "name_user_map:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "user_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select Id, `Name`, Costly, `name` = null from `user` where id = 1 for update",
+    "Query": "update `user` set `name` = null where id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # insert using last_insert_id
 "insert into unsharded values(last_insert_id(), 2)"
@@ -2424,7 +2728,32 @@ Gen4 plan same as above
     "Vindex": "kid_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicolvin set column_c = 2 where kid = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "colb_colc_map:4"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "kid_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select kid, column_a, column_b, column_c, column_c = 2 from multicolvin where kid = 1 for update",
+    "Query": "update multicolvin set column_c = 2 where kid = 1",
+    "Table": "multicolvin",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "kid_index"
+  }
+}
 
 # update with binary value
 "update user set name = _binary 'abc' where id = 1"
@@ -2454,7 +2783,32 @@ Gen4 plan same as above
     "Vindex": "user_index"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set name = _binary 'abc' where id = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "name_user_map:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "user_index",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select Id, `Name`, Costly, `name` = _binary 'abc' from `user` where id = 1 for update",
+    "Query": "update `user` set `name` = _binary 'abc' where id = 1",
+    "Table": "user",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "user_index"
+  }
+}
 
 # delete with binary value
 "delete from user where name = _binary 'abc'"
@@ -2709,7 +3063,32 @@ Gen4 plan same as above
     "Vindex": "lookup_t1_2"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update zlookup_unique.t1 set c2 = 1 where c2 = 10 and c3 = 20",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "zlookup_unique",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "lookup_t1:3"
+    ],
+    "KsidLength": 1,
+    "KsidVindex": "xxhash",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select c1, c2, c3, c2 = 1 from t1 where c2 = 10 and c3 = 20 for update",
+    "Query": "update t1 set c2 = 1 where c2 = 10 and c3 = 20",
+    "Table": "t1",
+    "Values": [
+      "INT64(20)"
+    ],
+    "Vindex": "lookup_t1_2"
+  }
+}
 
 # Delete EQUAL and IN on backfilling and non-backfilling unique lookup vindexes should be a delete IN
 "delete from zlookup_unique.t1 where c2 = 10 and c3 in (20, 21)"
@@ -2840,7 +3219,27 @@ Gen4 plan same as above
     "Vindex": "multicolIdx"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicol_tbl set x = 1 where cola = 1 and colb = 2",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update multicol_tbl set x = 1 where cola = 1 and colb = 2",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)",
+      "INT64(2)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
 
 # update with a multicol vindex - reverse order
 "update multicol_tbl set x = 1 where colb = 2 and cola = 1"
@@ -2865,7 +3264,27 @@ Gen4 plan same as above
     "Vindex": "multicolIdx"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicol_tbl set x = 1 where colb = 2 and cola = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update multicol_tbl set x = 1 where colb = 2 and cola = 1",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)",
+      "INT64(2)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
 
 # update with a multicol vindex using an IN clause
 "update multicol_tbl set x = 1 where colb IN (1,2) and cola = 1"
@@ -2890,7 +3309,26 @@ Gen4 plan same as above
     "Vindex": "multicolIdx"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicol_tbl set x = 1 where colb IN (1,2) and cola = 1",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "Equal",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update multicol_tbl set x = 1 where colb in (1, 2) and cola = 1",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
 
 # update with a multicol vindex using an IN clause
 "update multicol_tbl set x = 1 where colb IN (1,2) and cola IN (3,4)"
@@ -3058,7 +3496,33 @@ Gen4 plan same as above
     "Vindex": "multicolIdx"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "update multicol_tbl set colc = 1 where cola = 1 and colb = 2",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "ChangedVindexValues": [
+      "colc_map:4"
+    ],
+    "KsidLength": 2,
+    "KsidVindex": "multicolIdx",
+    "MultiShardAutocommit": false,
+    "OwnedVindexQuery": "select cola, colb, colc, `name`, colc = 1 from multicol_tbl where cola = 1 and colb = 2 for update",
+    "Query": "update multicol_tbl set colc = 1 where cola = 1 and colb = 2",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)",
+      "INT64(2)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
 
 # update with routing using non-unique lookup vindex
 "update multicol_tbl set x = 42 where name = 'foo'"
@@ -3695,3 +4159,217 @@ Gen4 plan same as above
   }
 }
 Gen4 plan same as above
+
+# unsharded subquery in sharded update, not the same keyspace between outer and inner
+"update user set col = (select id from unsharded)"
+"unsupported: subqueries in sharded DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set col = (select id from unsharded)",
+  "Instructions": {
+    "OperatorType": "Subquery",
+    "Variant": "PulloutValue",
+    "PulloutVars": [
+      "__sq1"
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "Unsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select id from unsharded where 1 != 1",
+        "Query": "select id from unsharded lock in share mode",
+        "Table": "unsharded"
+      },
+      {
+        "OperatorType": "Update",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "TargetTabletType": "PRIMARY",
+        "MultiShardAutocommit": false,
+        "Query": "update `user` set col = :__sq1",
+        "Table": "user"
+      }
+    ]
+  }
+}
+
+# sharded subquery in unsharded update, not the same keyspace
+"update unsharded set col = (select id from user)"
+"unsupported: sharded subqueries in DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update unsharded set col = (select id from user)",
+  "Instructions": {
+    "OperatorType": "Subquery",
+    "Variant": "PulloutValue",
+    "PulloutVars": [
+      "__sq1"
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "Scatter",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` lock in share mode",
+        "Table": "`user`"
+      },
+      {
+        "OperatorType": "Update",
+        "Variant": "Unsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "TargetTabletType": "PRIMARY",
+        "MultiShardAutocommit": false,
+        "Query": "update unsharded set col = :__sq1",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
+
+# sharded join unsharded subqueries in unsharded update
+"update unsharded set col = (select id from unsharded join user on unsharded.id = user.id)"
+"unsupported: sharded subqueries in DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update unsharded set col = (select id from unsharded join user on unsharded.id = user.id)",
+  "Instructions": {
+    "OperatorType": "Subquery",
+    "Variant": "PulloutValue",
+    "PulloutVars": [
+      "__sq1"
+    ],
+    "Inputs": [
+      {
+        "OperatorType": "Join",
+        "Variant": "Join",
+        "JoinColumnIndexes": "R:0",
+        "JoinVars": {
+          "unsharded_id": 0
+        },
+        "TableName": "unsharded_`user`",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "Unsharded",
+            "Keyspace": {
+              "Name": "main",
+              "Sharded": false
+            },
+            "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+            "Query": "select unsharded.id from unsharded lock in share mode",
+            "Table": "unsharded"
+          },
+          {
+            "OperatorType": "Route",
+            "Variant": "EqualUnique",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select id from `user` where 1 != 1",
+            "Query": "select id from `user` where `user`.id = :unsharded_id lock in share mode",
+            "Table": "`user`",
+            "Values": [
+              ":unsharded_id"
+            ],
+            "Vindex": "user_index"
+          }
+        ]
+      },
+      {
+        "OperatorType": "Update",
+        "Variant": "Unsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "TargetTabletType": "PRIMARY",
+        "MultiShardAutocommit": false,
+        "Query": "update unsharded set col = :__sq1",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
+
+# sharded update with sub query where the sources can be merged into a single query
+"update user set col = (select count(*) from user_extra where user_extra.user_id = 5) where id = 5"
+"unsupported: subqueries in sharded DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set col = (select count(*) from user_extra where user_extra.user_id = 5) where id = 5",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set col = (select count(*) from user_extra where user_extra.user_id = 5) where id = 5",
+    "Table": "user",
+    "Values": [
+      "INT64(5)"
+    ],
+    "Vindex": "user_index"
+  }
+}
+
+# merge through correlated subquery
+"update user set col = (select count(*) from user_extra where user_extra.user_id = user.id) where id = 5"
+"unsupported: subqueries in sharded DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set col = (select count(*) from user_extra where user_extra.user_id = user.id) where id = 5",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set col = (select count(*) from user_extra where user_extra.user_id = `user`.id) where id = 5",
+    "Table": "user",
+    "Values": [
+      "INT64(5)"
+    ],
+    "Vindex": "user_index"
+  }
+}
+
+# merge through correlated subquery #2
+"update user set col = (select count(*) from user_extra where user_extra.user_id = user.id) where id > 5"
+"unsupported: subqueries in sharded DML"
+{
+  "QueryType": "UPDATE",
+  "Original": "update user set col = (select count(*) from user_extra where user_extra.user_id = user.id) where id \u003e 5",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update `user` set col = (select count(*) from user_extra where user_extra.user_id = `user`.id) where id \u003e 5",
+    "Table": "user"
+  }
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
index 48fec667fc..754c8a0e1c 100644
--- a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
@@ -3927,3 +3927,114 @@ Gen4 plan same as above
   }
 }
 Gen4 plan same as above
+
+# optimize ORs to IN route op codes #1
+"select col from user where id = 1 or id = 2"
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where id = 1 or id = 2",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2",
+    "Table": "`user`"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where id = 1 or id = 2",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "IN",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2",
+    "Table": "`user`",
+    "Values": [
+      "(INT64(1), INT64(2))"
+    ],
+    "Vindex": "user_index"
+  }
+}
+
+# optimize ORs to IN route op codes #2
+"select col from user where id = 1 or id = 2 or id = 3"
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where id = 1 or id = 2 or id = 3",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2 or id = 3",
+    "Table": "`user`"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where id = 1 or id = 2 or id = 3",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "IN",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2 or id = 3",
+    "Table": "`user`",
+    "Values": [
+      "(INT64(1), INT64(2), INT64(3))"
+    ],
+    "Vindex": "user_index"
+  }
+}
+
+# optimize ORs to IN route op codes #3
+"select col from user where (id = 1 or id = 2) or (id = 3 or id = 4)"
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where (id = 1 or id = 2) or (id = 3 or id = 4)",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "Scatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2 or (id = 3 or id = 4)",
+    "Table": "`user`"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select col from user where (id = 1 or id = 2) or (id = 3 or id = 4)",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "IN",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select col from `user` where 1 != 1",
+    "Query": "select col from `user` where id = 1 or id = 2 or (id = 3 or id = 4)",
+    "Table": "`user`",
+    "Values": [
+      "(INT64(1), INT64(2), INT64(3), INT64(4))"
+    ],
+    "Vindex": "user_index"
+  }
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/oltp_cases.txt b/go/vt/vtgate/planbuilder/testdata/oltp_cases.txt
index 1a3c51b41a..9daac80684 100644
--- a/go/vt/vtgate/planbuilder/testdata/oltp_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/oltp_cases.txt
@@ -197,7 +197,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE sbtest6 SET k=k+1 WHERE id=5",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update sbtest6 set k = k + 1 where id = 5",
+    "Table": "sbtest6",
+    "Values": [
+      "INT64(5)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # OLTP non index update
 "UPDATE sbtest9 SET c=7 WHERE id=8"
@@ -221,7 +240,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE sbtest9 SET c=7 WHERE id=8",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update sbtest9 set c = 7 where id = 8",
+    "Table": "sbtest9",
+    "Values": [
+      "INT64(8)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # OLTP delete
 "DELETE FROM sbtest15 WHERE id=7525"
diff --git a/go/vt/vtgate/planbuilder/testdata/tpcc_cases.txt b/go/vt/vtgate/planbuilder/testdata/tpcc_cases.txt
index c05eb495fb..618984769f 100644
--- a/go/vt/vtgate/planbuilder/testdata/tpcc_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/tpcc_cases.txt
@@ -84,7 +84,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE district1 SET d_next_o_id = 56 WHERE d_id = 9842 AND d_w_id= 8546",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update district1 set d_next_o_id = 56 where d_id = 9842 and d_w_id = 8546",
+    "Table": "district1",
+    "Values": [
+      "INT64(8546)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C insert into orders1
 "INSERT INTO orders1 (o_id, o_d_id, o_w_id, o_c_id,  o_entry_d, o_ol_cnt, o_all_local) VALUES (334983,59896,99,156,NOW(),781038,'hello')"
@@ -200,7 +219,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE stock1 SET s_quantity = 894 WHERE s_i_id = 156 AND s_w_id= 6",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update stock1 set s_quantity = 894 where s_i_id = 156 and s_w_id = 6",
+    "Table": "stock1",
+    "Values": [
+      "INT64(6)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C insert into order_line1
 "INSERT INTO order_line1 (ol_o_id, ol_d_id, ol_w_id, ol_number, ol_i_id, ol_supply_w_id, ol_quantity, ol_amount, ol_dist_info) VALUES (648,36812,3201,4946378,3,7,89,1,'info')"
@@ -247,7 +285,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE warehouse1 SET w_ytd = w_ytd + 946879 WHERE w_id = 3",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update warehouse1 set w_ytd = w_ytd + 946879 where w_id = 3",
+    "Table": "warehouse1",
+    "Values": [
+      "INT64(3)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C select warehouse1 unique
 "SELECT w_street_1, w_street_2, w_city, w_state, w_zip, w_name FROM warehouse1 WHERE w_id = 998"
@@ -294,7 +351,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE district1 SET d_ytd = d_ytd + 2 WHERE d_w_id = 89 AND d_id= 9",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update district1 set d_ytd = d_ytd + 2 where d_w_id = 89 and d_id = 9",
+    "Table": "district1",
+    "Values": [
+      "INT64(89)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C select district1 unique
 "SELECT d_street_1, d_street_2, d_city, d_state, d_zip, d_name FROM district1 WHERE d_w_id = 896 AND d_id = 9"
@@ -433,7 +509,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE customer1 SET c_balance=508.98, c_ytd_payment=48941.980301, c_data='i am data' WHERE c_w_id = 20  AND c_d_id=387 AND c_id=98",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update customer1 set c_balance = 508.98, c_ytd_payment = 48941.980301, c_data = 'i am data' where c_w_id = 20 and c_d_id = 387 and c_id = 98",
+    "Table": "customer1",
+    "Values": [
+      "INT64(20)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C update customer1 unique and float value
 "UPDATE customer1 SET c_balance=508.98, c_ytd_payment=48941.980301 WHERE c_w_id = 20  AND c_d_id=387 AND c_id=98"
@@ -457,7 +552,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE customer1 SET c_balance=508.98, c_ytd_payment=48941.980301 WHERE c_w_id = 20  AND c_d_id=387 AND c_id=98",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update customer1 set c_balance = 508.98, c_ytd_payment = 48941.980301 where c_w_id = 20 and c_d_id = 387 and c_id = 98",
+    "Table": "customer1",
+    "Values": [
+      "INT64(20)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C insert into history1
 "INSERT INTO history1 (h_c_d_id, h_c_w_id, h_c_id, h_d_id,  h_w_id, h_date, h_amount, h_data) VALUES (6809887,38748,8746,210,8,NOW(),8907,'data')"
@@ -689,7 +803,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE orders1 SET o_carrier_id = 9 WHERE o_id = 56 AND o_d_id = 98 AND o_w_id = 897",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update orders1 set o_carrier_id = 9 where o_id = 56 and o_d_id = 98 and o_w_id = 897",
+    "Table": "orders1",
+    "Values": [
+      "INT64(897)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C update order_line1
 "UPDATE order_line1 SET ol_delivery_d = NOW() WHERE ol_o_id = 235 AND ol_d_id = 315 AND ol_w_id = 8"
@@ -713,7 +846,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE order_line1 SET ol_delivery_d = NOW() WHERE ol_o_id = 235 AND ol_d_id = 315 AND ol_w_id = 8",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update order_line1 set ol_delivery_d = NOW() where ol_o_id = 235 and ol_d_id = 315 and ol_w_id = 8",
+    "Table": "order_line1",
+    "Values": [
+      "INT64(8)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C select sum order_line1
 "SELECT SUM(ol_amount) sm FROM order_line1 WHERE ol_o_id = 680 AND ol_d_id = 201 AND ol_w_id = 87"
@@ -760,7 +912,26 @@ Gen4 plan same as above
     "Vindex": "hash"
   }
 }
-Gen4 plan same as above
+{
+  "QueryType": "UPDATE",
+  "Original": "UPDATE customer1 SET c_balance = c_balance + 988.01, c_delivery_cnt = c_delivery_cnt + 1 WHERE c_id = 6 AND c_d_id = 5 AND c_w_id = 160",
+  "Instructions": {
+    "OperatorType": "Update",
+    "Variant": "EqualUnique",
+    "Keyspace": {
+      "Name": "main",
+      "Sharded": true
+    },
+    "TargetTabletType": "PRIMARY",
+    "MultiShardAutocommit": false,
+    "Query": "update customer1 set c_balance = c_balance + 988.01, c_delivery_cnt = c_delivery_cnt + 1 where c_id = 6 and c_d_id = 5 and c_w_id = 160",
+    "Table": "customer1",
+    "Values": [
+      "INT64(160)"
+    ],
+    "Vindex": "hash"
+  }
+}
 
 # TPC-C select unique district1
 "SELECT d_next_o_id FROM district1 WHERE d_id = 6 AND d_w_id= 21"
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index 8d233fe943..e71fb4d6df 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -143,21 +143,6 @@ Gen4 plan same as above
 "unsupported: in scatter query: complex order by expression: 1 collate utf8_general_ci"
 Gen4 error: unsupported: in scatter query: complex order by expression: a collate utf8_general_ci
 
-# subqueries in update
-"update user set col = (select id from unsharded)"
-"unsupported: subqueries in sharded DML"
-Gen4 plan same as above
-
-# sharded subqueries in unsharded update
-"update unsharded set col = (select id from user)"
-"unsupported: sharded subqueries in DML"
-Gen4 plan same as above
-
-# sharded join unsharded subqueries in unsharded update
-"update unsharded set col = (select id from unsharded join user on unsharded.id = user.id)"
-"unsupported: sharded subqueries in DML"
-Gen4 plan same as above
-
 # subqueries in delete
 "delete from user where col = (select id from unsharded)"
 "unsupported: subqueries in sharded DML"
@@ -223,20 +208,20 @@ Gen4 plan same as above
 "unsupported: Need to provide order by clause when using limit. Invalid update on vindex: email_user_map"
 Gen4 plan same as above
 
-# cross-shard update tables
+# update with derived table
 "update (select id from user) as u set id = 4"
 "unsupported: subqueries in sharded DML"
-Gen4 plan same as above
+Gen4 error: The target table u of the UPDATE is not updatable
 
 # join in update tables
 "update user join user_extra on user.id = user_extra.id set user.name = 'foo'"
 "unsupported: multi-shard or vindex write statement"
-Gen4 plan same as above
+Gen4 error: unsupported: multiple tables in update
 
 # multiple tables in update
 "update user as u, user_extra as ue set u.name = 'foo' where u.id = ue.id"
 "unsupported: multi-shard or vindex write statement"
-Gen4 plan same as above
+Gen4 error: unsupported: multiple tables in update
 
 # unsharded insert, unqualified names and auto-inc combined
 "insert into unsharded_auto select col from unsharded"
diff --git a/go/vt/vtgate/semantics/analyzer_test.go b/go/vt/vtgate/semantics/analyzer_test.go
index 7b0d68a814..36f063aeca 100644
--- a/go/vt/vtgate/semantics/analyzer_test.go
+++ b/go/vt/vtgate/semantics/analyzer_test.go
@@ -1352,7 +1352,7 @@ func parseAndAnalyze(t *testing.T, query, dbName string) (sqlparser.Statement, *
 	parse, err := sqlparser.Parse(query)
 	require.NoError(t, err)
 
-	semTable, err := Analyze(parse.(sqlparser.SelectStatement), dbName, fakeSchemaInfo())
+	semTable, err := Analyze(parse, dbName, fakeSchemaInfo())
 	require.NoError(t, err)
 	return parse, semTable
 }
diff --git a/go/vt/vtgate/semantics/analyzer_update_test.go b/go/vt/vtgate/semantics/analyzer_update_test.go
new file mode 100644
index 0000000000..001d18683b
--- /dev/null
+++ b/go/vt/vtgate/semantics/analyzer_update_test.go
@@ -0,0 +1,177 @@
+/*
+Copyright 2022 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 semantics
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"vitess.io/vitess/go/vt/sqlparser"
+)
+
+func TestUpdBindingColName(t *testing.T) {
+	queries := []string{
+		"update tabl set col = 1",
+		"update t2 set uid = 5",
+		"update tabl set tabl.col = 1 ",
+		"update tabl set d.tabl.col = 5",
+		"update d.tabl set col = 1",
+		"update d.tabl set tabl.col = 5",
+		"update d.tabl set d.tabl.col = 1",
+	}
+	for _, query := range queries {
+		t.Run(query, func(t *testing.T) {
+			stmt, semTable := parseAndAnalyze(t, query, "d")
+			upd, _ := stmt.(*sqlparser.Update)
+			t1 := upd.TableExprs[0].(*sqlparser.AliasedTableExpr)
+			ts := semTable.TableSetFor(t1)
+			assert.Equal(t, SingleTableSet(0), ts)
+
+			updExpr := extractFromUpdateSet(upd, 0)
+			recursiveDeps := semTable.RecursiveDeps(updExpr.Name)
+			assert.Equal(t, T1, recursiveDeps, query)
+			assert.Equal(t, T1, semTable.DirectDeps(updExpr.Name), query)
+			assert.Equal(t, 1, recursiveDeps.NumberOfTables(), "number of tables is wrong")
+
+			recursiveDeps = semTable.RecursiveDeps(updExpr.Expr)
+			assert.Equal(t, None, recursiveDeps, query)
+			assert.Equal(t, None, semTable.DirectDeps(updExpr.Expr), query)
+		})
+	}
+}
+
+func TestUpdBindingExpr(t *testing.T) {
+	queries := []string{
+		"update tabl set col = col",
+		"update tabl set d.tabl.col = tabl.col",
+		"update d.tabl set col = d.tabl.col",
+		"update d.tabl set tabl.col = col",
+	}
+	for _, query := range queries {
+		t.Run(query, func(t *testing.T) {
+			stmt, semTable := parseAndAnalyze(t, query, "d")
+			upd, _ := stmt.(*sqlparser.Update)
+			t1 := upd.TableExprs[0].(*sqlparser.AliasedTableExpr)
+			ts := semTable.TableSetFor(t1)
+			assert.Equal(t, SingleTableSet(0), ts)
+
+			updExpr := extractFromUpdateSet(upd, 0)
+			recursiveDeps := semTable.RecursiveDeps(updExpr.Name)
+			assert.Equal(t, T1, recursiveDeps, query)
+			assert.Equal(t, T1, semTable.DirectDeps(updExpr.Name), query)
+			assert.Equal(t, 1, recursiveDeps.NumberOfTables(), "number of tables is wrong")
+
+			recursiveDeps = semTable.RecursiveDeps(updExpr.Expr)
+			assert.Equal(t, T1, recursiveDeps, query)
+			assert.Equal(t, T1, semTable.DirectDeps(updExpr.Expr), query)
+			assert.Equal(t, 1, recursiveDeps.NumberOfTables(), "number of tables is wrong")
+		})
+	}
+}
+
+func TestUpdSetSubquery(t *testing.T) {
+	queries := []string{
+		"update tabl set col = (select id from a)",
+		"update tabl set col = (select id from a)+1",
+		"update tabl set col = 1 IN (select id from a)",
+		"update tabl set col = (select id from a), t = (select x from a)",
+	}
+	for _, query := range queries {
+		t.Run(query, func(t *testing.T) {
+			stmt, semTable := parseAndAnalyze(t, query, "d")
+			upd, _ := stmt.(*sqlparser.Update)
+			t1 := upd.TableExprs[0].(*sqlparser.AliasedTableExpr)
+			ts := semTable.TableSetFor(t1)
+			assert.Equal(t, SingleTableSet(0), ts)
+
+			updExpr := extractFromUpdateSet(upd, 0)
+			recursiveDeps := semTable.RecursiveDeps(updExpr.Name)
+			assert.Equal(t, T1, recursiveDeps, query)
+			assert.Equal(t, T1, semTable.DirectDeps(updExpr.Name), query)
+			assert.Equal(t, 1, recursiveDeps.NumberOfTables(), "number of tables is wrong")
+
+			extractedSubqs := semTable.SubqueryMap[upd]
+			require.Len(t, extractedSubqs, len(upd.Exprs))
+
+			for _, esubq := range extractedSubqs {
+				subq := esubq.Subquery
+				extractedSubq := semTable.SubqueryRef[subq]
+				assert.True(t, sqlparser.EqualsExpr(extractedSubq.Subquery, subq))
+			}
+		})
+	}
+}
+
+func TestUpdWhereSubquery(t *testing.T) {
+	queries := []string{
+		"update tabl set col = 1 where id = (select id from a)",
+		"update tabl set col = 1 where id IN (select id from a)",
+		"update tabl set col = 1 where exists (select id from a)",
+		"update tabl set col = 1 where 1 = (select id from a)",
+		"update tabl set col = 1 where exists (select id from a) and id > (select name from city) and col < (select i from a)",
+	}
+	for _, query := range queries {
+		t.Run(query, func(t *testing.T) {
+			stmt, semTable := parseAndAnalyze(t, query, "d")
+			upd, _ := stmt.(*sqlparser.Update)
+			t1 := upd.TableExprs[0].(*sqlparser.AliasedTableExpr)
+			ts := semTable.TableSetFor(t1)
+			assert.Equal(t, SingleTableSet(0), ts)
+
+			extractedSubqs := semTable.SubqueryMap[upd]
+			require.Len(t, extractedSubqs, len(sqlparser.SplitAndExpression(nil, upd.Where.Expr)))
+
+			for _, esubq := range extractedSubqs {
+				subq := esubq.Subquery
+				extractedSubq := semTable.SubqueryRef[subq]
+				assert.True(t, sqlparser.EqualsExpr(extractedSubq.Subquery, subq))
+			}
+		})
+	}
+}
+
+func TestUpdSetAndWhereSubquery(t *testing.T) {
+	queries := []string{
+		"update tabl set col = (select b from alpha) where id = (select id from a)",
+		"update tabl set col = (select b from alpha) where exists (select id from a)",
+		"update tabl set col = 1+(select b from alpha) where 1 > (select id from a)",
+	}
+	for _, query := range queries {
+		t.Run(query, func(t *testing.T) {
+			stmt, semTable := parseAndAnalyze(t, query, "d")
+			upd, _ := stmt.(*sqlparser.Update)
+			t1 := upd.TableExprs[0].(*sqlparser.AliasedTableExpr)
+			ts := semTable.TableSetFor(t1)
+			assert.Equal(t, SingleTableSet(0), ts)
+
+			extractedSubqs := semTable.SubqueryMap[upd]
+			require.Len(t, extractedSubqs, len(sqlparser.SplitAndExpression(nil, upd.Where.Expr))+len(upd.Exprs))
+
+			for _, esubq := range extractedSubqs {
+				subq := esubq.Subquery
+				extractedSubq := semTable.SubqueryRef[subq]
+				assert.True(t, sqlparser.EqualsExpr(extractedSubq.Subquery, subq))
+			}
+		})
+	}
+}
+
+func extractFromUpdateSet(in *sqlparser.Update, idx int) *sqlparser.UpdateExpr {
+	return in.Exprs[idx]
+}