Repository: vitessio/vitess
Pull request #9390 (2021-12-15 09:32:29)

Multicolumn partial vindex selection
<!--
  Thank you for your contribution to the Vitess project.
  How to contribute: https://vitess.io/docs/contributing/
  Please first make sure there is an open Issue to discuss the feature/fix suggested in this PR.
  If this is a new feature, please mark the Issue as "RFC".
 -->

<!-- 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. -->

This PR adds a new function to MultiColumn Vindex interface to provide information that the multi-column vindex supports partial vindex selection (i.e. fanout to subset of shards if part of the column values are provided).

Eg.
```
cola, colb, colc forms a multicolumn vindex
then providing values for cola or (cola and colb) the vindex should be able to fan out the query to subset of shards.
```

The user does not have to provide any additional information in the VSchema. Based on the table that uses multi-column vindex which allows partial vindex column selection, Vitess will handle it internally and the planner will be able to utilize those columns for vindex selection.

This PR also modifies the existing `region_experimental` vindex to handle partial column values i.e. if only `region_id` is provided to Map function.

## Related Issue(s)
<!-- List related issues and pull requests. If this PR fixes an issue, please add it using Fixes #????  -->

- https://github.com/vitessio/vitess/issues/3481

## Checklist
- [X] Tests were added or are not required
- [X] 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. -->


Patch:
diff --git a/doc/releasenotes/13_0_0_summary.md b/doc/releasenotes/13_0_0_summary.md
index 674baef1ac..1f944b8917 100644
--- a/doc/releasenotes/13_0_0_summary.md
+++ b/doc/releasenotes/13_0_0_summary.md
@@ -9,9 +9,6 @@ The default value used to be false. What this means is that during a failover, w
 In addition, all Vitess-managed databases will be started with `super-read-only` in the cnf file.
 It is expected that this change is safe and backwards-compatible. Anyone who is relying on the current behavior should pass `-use_super_read_only=false` on the vttablet command line, and make sure they are using a custom my.cnf instead of the one provided as the default by Vitess.
 
-### vtgate -buffer_implementation now defaults to keyspace_events
-The default value used to be `healthcheck`. The new `keyspace_events` implementation has been tested in production with good results and shows more consistent buffering behavior during PlannedReparentShard operations. The `keyspace_events` implementation utilizes heuristics to detect additional cluster states where buffering is safe to perform, including cases where the primary may be down. If there is a need to revert back to the previous buffer implementation, ensure buffering is enabled in vtgate and pass the flag `-buffer_implementation=healthcheck`. 
-
 ### ddl_strategy: -postpone-completion flag
 
 `ddl_strategy` (either `@@ddl_strategy` in VtGate or `-ddl_strategy` in `vtctlclient ApplySchema`) supports the flag `-postpone-completion`
diff --git a/go.mod b/go.mod
index e05cee0f69..2d9ea709f4 100644
--- a/go.mod
+++ b/go.mod
@@ -83,7 +83,6 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/tchap/go-patricia v2.2.6+incompatible
 	github.com/tebeka/selenium v0.9.9
-	github.com/tidwall/gjson v1.12.1
 	github.com/tinylib/msgp v1.1.1 // indirect
 	github.com/uber-go/atomic v1.4.0 // indirect
 	github.com/uber/jaeger-client-go v2.16.0+incompatible
@@ -175,8 +174,6 @@ require (
 	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
-	github.com/tidwall/match v1.1.1 // indirect
-	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.0.1 // indirect
 	go.opencensus.io v0.23.0 // indirect
diff --git a/go.sum b/go.sum
index 8148392651..08faa0c6b2 100644
--- a/go.sum
+++ b/go.sum
@@ -740,13 +740,7 @@ github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPx
 github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
 github.com/tebeka/selenium v0.9.9 h1:cNziB+etNgyH/7KlNI7RMC1ua5aH1+5wUlFQyzeMh+w=
 github.com/tebeka/selenium v0.9.9/go.mod h1:5Fr8+pUvU6B1OiPfkdCKdXZyr5znvVkxuPd0NOdZCQc=
-github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo=
-github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
-github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
-github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tinylib/msgp v1.1.1 h1:TnCZ3FIuKeaIy+F45+Cnp+caqdXGy4z74HvwXN+570Y=
 github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
diff --git a/go/vt/vtgate/buffer/flags.go b/go/vt/vtgate/buffer/flags.go
index b60c19fed0..f3863099bc 100644
--- a/go/vt/vtgate/buffer/flags.go
+++ b/go/vt/vtgate/buffer/flags.go
@@ -32,7 +32,7 @@ var (
 	bufferEnabledDryRun = flag.Bool("enable_buffer_dry_run", false, "Detect and log failover events, but do not actually buffer requests.")
 
 	bufferWindow                  = flag.Duration("buffer_window", 10*time.Second, "Duration for how long a request should be buffered at most.")
-	bufferSize                    = flag.Int("buffer_size", 1000, "Maximum number of buffered requests in flight (across all ongoing failovers).")
+	bufferSize                    = flag.Int("buffer_size", 10, "Maximum number of buffered requests in flight (across all ongoing failovers).")
 	bufferMaxFailoverDuration     = flag.Duration("buffer_max_failover_duration", 20*time.Second, "Stop buffering completely if a failover takes longer than this duration.")
 	bufferMinTimeBetweenFailovers = flag.Duration("buffer_min_time_between_failovers", 1*time.Minute, "Minimum time between the end of a failover and the start of the next one (tracked per shard). Faster consecutive failovers will not trigger buffering.")
 
diff --git a/go/vt/vtgate/engine/insert.go b/go/vt/vtgate/engine/insert.go
index 97e7b54398..e01370e173 100644
--- a/go/vt/vtgate/engine/insert.go
+++ b/go/vt/vtgate/engine/insert.go
@@ -61,6 +61,9 @@ type Insert struct {
 	// Insert.Values[i].Values[j].Values[k] represents the value pulled from row k for that column: (k < len(ins.rows))
 	VindexValues [][][]evalengine.Expr
 
+	// ColVindexes are the vindexes that will use the VindexValues
+	ColVindexes []*vindexes.ColumnVindex
+
 	// Table specifies the table for the insert.
 	Table *vindexes.Table
 
@@ -367,9 +370,13 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 	vindexRowsValues := make([][][]sqltypes.Value, len(ins.VindexValues))
 	rowCount := 0
 	env := evalengine.EnvWithBindVars(bindVars)
+	colVindexes := ins.ColVindexes
+	if colVindexes == nil {
+		colVindexes = ins.Table.ColumnVindexes
+	}
 	for vIdx, vColValues := range ins.VindexValues {
-		if len(vColValues) != len(ins.Table.ColumnVindexes[vIdx].Columns) {
-			return nil, nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] supplied vindex column values don't match vschema: %v %v", vColValues, ins.Table.ColumnVindexes[vIdx].Columns)
+		if len(vColValues) != len(colVindexes[vIdx].Columns) {
+			return nil, nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] supplied vindex column values don't match vschema: %v %v", vColValues, colVindexes[vIdx].Columns)
 		}
 		for colIdx, colValues := range vColValues {
 			rowsResolvedValues := make([]sqltypes.Value, 0, len(colValues))
@@ -404,16 +411,16 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 	// keyspace ids. For regular inserts, a failure to find a route
 	// results in an error. For 'ignore' type inserts, the keyspace
 	// id is returned as nil, which is used later to drop the corresponding rows.
-	if len(vindexRowsValues) == 0 || len(ins.Table.ColumnVindexes) == 0 {
+	if len(vindexRowsValues) == 0 || len(colVindexes) == 0 {
 		return nil, nil, vterrors.NewErrorf(vtrpcpb.Code_FAILED_PRECONDITION, vterrors.RequiresPrimaryKey, vterrors.PrimaryVindexNotSet, ins.Table.Name)
 	}
-	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], ins.Table.ColumnVindexes[0])
+	keyspaceIDs, err := ins.processPrimary(vcursor, vindexRowsValues[0], colVindexes[0])
 	if err != nil {
 		return nil, nil, err
 	}
 
-	for vIdx := 1; vIdx < len(ins.Table.ColumnVindexes); vIdx++ {
-		colVindex := ins.Table.ColumnVindexes[vIdx]
+	for vIdx := 1; vIdx < len(colVindexes); vIdx++ {
+		colVindex := colVindexes[vIdx]
 		var err error
 		if colVindex.Owned {
 			err = ins.processOwned(vcursor, vindexRowsValues[vIdx], colVindex, keyspaceIDs)
@@ -427,7 +434,7 @@ func (ins *Insert) getInsertShardedRoute(vcursor VCursor, bindVars map[string]*q
 
 	// Build 3-d bindvars. Skip rows with nil keyspace ids in case
 	// we're executing an insert ignore.
-	for vIdx, colVindex := range ins.Table.ColumnVindexes {
+	for vIdx, colVindex := range colVindexes {
 		for rowNum, rowColumnKeys := range vindexRowsValues[vIdx] {
 			if keyspaceIDs[rowNum] == nil {
 				// InsertShardedIgnore: skip the row.
diff --git a/go/vt/vtgate/gateway.go b/go/vt/vtgate/gateway.go
index 5d69df742a..847ce660b1 100644
--- a/go/vt/vtgate/gateway.go
+++ b/go/vt/vtgate/gateway.go
@@ -37,7 +37,7 @@ import (
 var (
 	// GatewayImplementation allows you to choose which gateway to use for vtgate routing. Defaults to tabletgateway, other option is discoverygateway
 	GatewayImplementation = flag.String("gateway_implementation", "tabletgateway", "Allowed values: discoverygateway (deprecated), tabletgateway (default)")
-	bufferImplementation  = flag.String("buffer_implementation", "keyspace_events", "Allowed values: healthcheck (legacy implementation), keyspace_events (default)")
+	bufferImplementation  = flag.String("buffer_implementation", "healthcheck", "Allowed values: healthcheck (default), keyspace_events (for testing)")
 	initialTabletTimeout  = flag.Duration("gateway_initial_tablet_timeout", 30*time.Second, "At startup, the gateway will wait up to that duration to get one tablet per keyspace/shard/tablettype")
 	// RetryCount is the number of times a query will be retried on error
 	// Make this unexported after DiscoveryGateway is deprecated
diff --git a/go/vt/vtgate/planbuilder/dml.go b/go/vt/vtgate/planbuilder/dml.go
index 5bbefc9e63..38ec08c2f7 100644
--- a/go/vt/vtgate/planbuilder/dml.go
+++ b/go/vt/vtgate/planbuilder/dml.go
@@ -40,7 +40,7 @@ func getDMLRouting(where *sqlparser.Where, table *vindexes.Table) (
 	var ksidVindex vindexes.SingleColumn
 	var ksidCol string
 	for _, index := range table.Ordered {
-		if !index.Vindex.IsUnique() {
+		if !index.IsUnique() {
 			continue
 		}
 		single, ok := index.Vindex.(vindexes.SingleColumn)
diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go
index 95dde5e18c..b9c2488e65 100644
--- a/go/vt/vtgate/planbuilder/insert.go
+++ b/go/vt/vtgate/planbuilder/insert.go
@@ -166,8 +166,15 @@ func buildInsertShardedPlan(ins *sqlparser.Insert, table *vindexes.Table) (engin
 	}
 
 	// Fill out the 3-d Values structure. Please see documentation of Insert.Values for details.
-	routeValues := make([][][]evalengine.Expr, len(eins.Table.ColumnVindexes))
-	for vIdx, colVindex := range eins.Table.ColumnVindexes {
+	var colVindexes []*vindexes.ColumnVindex
+	for _, colVindex := range eins.Table.ColumnVindexes {
+		if colVindex.IgnoreInDML() {
+			continue
+		}
+		colVindexes = append(colVindexes, colVindex)
+	}
+	routeValues := make([][][]evalengine.Expr, len(colVindexes))
+	for vIdx, colVindex := range colVindexes {
 		routeValues[vIdx] = make([][]evalengine.Expr, len(colVindex.Columns))
 		for colIdx, col := range colVindex.Columns {
 			routeValues[vIdx][colIdx] = make([]evalengine.Expr, len(rows))
@@ -181,7 +188,7 @@ func buildInsertShardedPlan(ins *sqlparser.Insert, table *vindexes.Table) (engin
 			}
 		}
 	}
-	for _, colVindex := range eins.Table.ColumnVindexes {
+	for _, colVindex := range colVindexes {
 		for _, col := range colVindex.Columns {
 			colNum := findOrAddColumn(ins, col)
 			for rowNum, row := range rows {
@@ -191,6 +198,7 @@ func buildInsertShardedPlan(ins *sqlparser.Insert, table *vindexes.Table) (engin
 		}
 	}
 	eins.VindexValues = routeValues
+	eins.ColVindexes = colVindexes
 	eins.Query = generateQuery(ins)
 	generateInsertShardedQuery(ins, eins, rows)
 	return eins, nil
diff --git a/go/vt/vtgate/planbuilder/route_planning.go b/go/vt/vtgate/planbuilder/route_planning.go
index 7f14d84d8e..13ac38289b 100644
--- a/go/vt/vtgate/planbuilder/route_planning.go
+++ b/go/vt/vtgate/planbuilder/route_planning.go
@@ -402,7 +402,7 @@ func exprHasUniqueVindex(vschema ContextVSchema, semTable *semantics.SemTable, e
 		return false
 	}
 	for _, vindex := range vschemaTable.ColumnVindexes {
-		if len(vindex.Columns) > 1 || !vindex.Vindex.IsUnique() {
+		if len(vindex.Columns) > 1 || !vindex.IsUnique() {
 			return false
 		}
 		if col.Name.Equal(vindex.Columns[0]) {
@@ -889,13 +889,13 @@ func createRoutePlan(ctx *planningContext, table *abstract.QueryTable, solves se
 	return plan, nil
 }
 
-func findColumnVindex(ctx *planningContext, a *routeTree, exp sqlparser.Expr) vindexes.SingleColumn {
+func findColumnVindex(ctx *planningContext, a *routeTree, exp sqlparser.Expr) *vindexes.ColumnVindex {
 	_, isCol := exp.(*sqlparser.ColName)
 	if !isCol {
 		return nil
 	}
 
-	var singCol vindexes.SingleColumn
+	var colVindex *vindexes.ColumnVindex
 
 	// 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,
@@ -914,21 +914,21 @@ func findColumnVindex(ctx *planningContext, a *routeTree, exp sqlparser.Expr) vi
 			}
 			if leftDep.IsSolvedBy(rb.qtable.TableID) {
 				for _, vindex := range rb.vtable.ColumnVindexes {
-					sC, isSingle := vindex.Vindex.(vindexes.SingleColumn)
+					_, isSingle := vindex.Vindex.(vindexes.SingleColumn)
 					if isSingle && vindex.Columns[0].Equal(col.Name) {
-						singCol = sC
+						colVindex = vindex
 						return false, io.EOF
 					}
 				}
 			}
 			return false, nil
 		})
-		if singCol != nil {
-			return singCol
+		if colVindex != nil {
+			return colVindex
 		}
 	}
 
-	return singCol
+	return colVindex
 }
 
 func canMergeOnFilter(ctx *planningContext, a, b *routeTree, predicate sqlparser.Expr) bool {
@@ -954,7 +954,7 @@ func canMergeOnFilter(ctx *planningContext, a, b *routeTree, predicate sqlparser
 	if rVindex == nil {
 		return false
 	}
-	return rVindex == lVindex
+	return rVindex.Vindex == lVindex.Vindex
 }
 
 func canMergeOnFilters(ctx *planningContext, a, b *routeTree, joinPredicates []sqlparser.Expr) bool {
diff --git a/go/vt/vtgate/planbuilder/routetree.go b/go/vt/vtgate/planbuilder/routetree.go
index fb02a1e133..42a219a533 100644
--- a/go/vt/vtgate/planbuilder/routetree.go
+++ b/go/vt/vtgate/planbuilder/routetree.go
@@ -495,13 +495,16 @@ func (rp *routeTree) haveMatchingVindex(
 				// single column vindex - just add the option
 				routeOpcode := opcode(v.colVindex)
 				vindex := vfunc(v.colVindex)
+				if vindex == nil {
+					continue
+				}
 				v.options = append(v.options, &vindexOption{
 					values:      []evalengine.Expr{value},
 					valueExprs:  []sqlparser.Expr{valueExpr},
 					predicates:  []sqlparser.Expr{node},
 					opcode:      routeOpcode,
 					foundVindex: vindex,
-					cost:        costFor(vindex, routeOpcode),
+					cost:        costFor(v.colVindex, routeOpcode),
 					ready:       true,
 				})
 				newVindexFound = true
@@ -607,7 +610,7 @@ func (option *vindexOption) updateWithNewColumn(
 	routeOpcode := opcode(colVindex)
 	if option.opcode < routeOpcode {
 		option.opcode = routeOpcode
-		option.cost = costFor(option.foundVindex, routeOpcode)
+		option.cost = costFor(colVindex, routeOpcode)
 	}
 	return option.ready
 }
@@ -702,7 +705,7 @@ func justTheVindex(vindex *vindexes.ColumnVindex) vindexes.Vindex {
 }
 
 func equalOrEqualUnique(vindex *vindexes.ColumnVindex) engine.RouteOpcode {
-	if vindex.Vindex.IsUnique() {
+	if vindex.IsUnique() {
 		return engine.SelectEqualUnique
 	}
 
@@ -710,7 +713,7 @@ func equalOrEqualUnique(vindex *vindexes.ColumnVindex) engine.RouteOpcode {
 }
 
 // costFor returns a cost struct to make route choices easier to compare
-func costFor(foundVindex vindexes.Vindex, opcode engine.RouteOpcode) cost {
+func costFor(foundVindex *vindexes.ColumnVindex, opcode engine.RouteOpcode) cost {
 	switch opcode {
 	// For these opcodes, we should not have a vindex, so we just return the opcode as the cost
 	case engine.SelectUnsharded, engine.SelectNext, engine.SelectDBA, engine.SelectReference, engine.SelectNone, engine.SelectScatter:
@@ -719,11 +722,6 @@ func costFor(foundVindex vindexes.Vindex, opcode engine.RouteOpcode) cost {
 		}
 	}
 
-	// if we have a multiplier that is non-zero, we should have a vindex
-	if foundVindex == nil {
-		panic("expected a vindex")
-	}
-
 	return cost{
 		vindexCost: foundVindex.Cost(),
 		isUnique:   foundVindex.IsUnique(),
diff --git a/go/vt/vtgate/vindexes/region_experimental.go b/go/vt/vtgate/vindexes/region_experimental.go
index 7a072dd70e..40f6b83fec 100644
--- a/go/vt/vtgate/vindexes/region_experimental.go
+++ b/go/vt/vtgate/vindexes/region_experimental.go
@@ -91,7 +91,7 @@ func (ge *RegionExperimental) NeedsVCursor() bool {
 func (ge *RegionExperimental) Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
 	destinations := make([]key.Destination, 0, len(rowsColValues))
 	for _, row := range rowsColValues {
-		if len(row) != 2 {
+		if len(row) > 2 {
 			destinations = append(destinations, key.DestinationNone{})
 			continue
 		}
@@ -104,20 +104,25 @@ func (ge *RegionExperimental) Map(vcursor VCursor, rowsColValues [][]sqltypes.Va
 		r := make([]byte, 2, 2+8)
 		binary.BigEndian.PutUint16(r, uint16(rn))
 
-		// Compute hash.
-		hn, err := evalengine.ToUint64(row[1])
-		if err != nil {
-			destinations = append(destinations, key.DestinationNone{})
-			continue
-		}
-		h := vhash(hn)
-
 		// Concatenate and add to destinations.
 		if ge.regionBytes == 1 {
 			r = r[1:]
 		}
-		dest := append(r, h...)
-		destinations = append(destinations, key.DestinationKeyspaceID(dest))
+
+		dest := r
+		if len(row) == 2 {
+			// Compute hash.
+			hn, err := evalengine.ToUint64(row[1])
+			if err != nil {
+				destinations = append(destinations, key.DestinationNone{})
+				continue
+			}
+			h := vhash(hn)
+			dest = append(dest, h...)
+			destinations = append(destinations, key.DestinationKeyspaceID(dest))
+		} else {
+			destinations = append(destinations, NewKeyRangeFromPrefix(dest))
+		}
 	}
 	return destinations, nil
 }
@@ -135,3 +140,7 @@ func (ge *RegionExperimental) Verify(vcursor VCursor, rowsColValues [][]sqltypes
 	}
 	return result, nil
 }
+
+func (ge *RegionExperimental) PartialVindex() bool {
+	return true
+}
diff --git a/go/vt/vtgate/vindexes/region_json.go b/go/vt/vtgate/vindexes/region_json.go
index 664fbfdb08..66fd10c52e 100644
--- a/go/vt/vtgate/vindexes/region_json.go
+++ b/go/vt/vtgate/vindexes/region_json.go
@@ -101,6 +101,11 @@ func (rv *RegionJSON) IsUnique() bool {
 	return true
 }
 
+// NeedsVCursor satisfies the Vindex interface.
+func (rv *RegionJSON) NeedsVCursor() bool {
+	return false
+}
+
 // Map satisfies MultiColumn.
 func (rv *RegionJSON) Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error) {
 	destinations := make([]key.Destination, 0, len(rowsColValues))
@@ -149,7 +154,6 @@ func (rv *RegionJSON) Verify(vcursor VCursor, rowsColValues [][]sqltypes.Value,
 	return result, nil
 }
 
-// NeedsVCursor satisfies the Vindex interface.
-func (rv *RegionJSON) NeedsVCursor() bool {
+func (rv *RegionJSON) PartialVindex() bool {
 	return false
 }
diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go
index 45564f3b2b..cb91bd715b 100644
--- a/go/vt/vtgate/vindexes/vindex.go
+++ b/go/vt/vtgate/vindexes/vindex.go
@@ -89,6 +89,8 @@ type MultiColumn interface {
 	Vindex
 	Map(vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
 	Verify(vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte) ([]bool, error)
+	// PartialVindex returns true if subset of columns can be passed in to the vindex Map and Verify function.
+	PartialVindex() bool
 }
 
 // A Reversible vindex is one that can perform a
diff --git a/go/vt/vtgate/vindexes/vschema.go b/go/vt/vtgate/vindexes/vschema.go
index d9a5fecab6..0198164f31 100644
--- a/go/vt/vtgate/vindexes/vschema.go
+++ b/go/vt/vtgate/vindexes/vschema.go
@@ -103,11 +103,32 @@ type Keyspace struct {
 
 // ColumnVindex contains the index info for each index of a table.
 type ColumnVindex struct {
-	Columns []sqlparser.ColIdent `json:"columns"`
-	Type    string               `json:"type"`
-	Name    string               `json:"name"`
-	Owned   bool                 `json:"owned,omitempty"`
-	Vindex  Vindex               `json:"vindex"`
+	Columns     []sqlparser.ColIdent `json:"columns"`
+	Type        string               `json:"type"`
+	Name        string               `json:"name"`
+	Owned       bool                 `json:"owned,omitempty"`
+	Vindex      Vindex               `json:"vindex"`
+	isUnique    bool
+	cost        int
+	ignoreInDML bool
+}
+
+// IsUnique is used to tell whether the ColumnVindex
+// will return a unique shard value or not when queried with
+// the given column list
+func (c *ColumnVindex) IsUnique() bool {
+	return c.isUnique
+}
+
+// Cost represents the cost associated with using the
+// ColumnVindex
+func (c *ColumnVindex) Cost() int {
+	return c.cost
+}
+
+// IgnoreInDML is used to let planner and engine know that they need to be ignored for dml queries.
+func (c *ColumnVindex) IgnoreInDML() bool {
+	return c.ignoreInDML
 }
 
 // Column describes a column.
@@ -306,11 +327,13 @@ func buildTables(ks *vschemapb.Keyspace, vschema *VSchema, ksvschema *KeyspaceSc
 				}
 			}
 			columnVindex := &ColumnVindex{
-				Columns: columns,
-				Type:    vindexInfo.Type,
-				Name:    ind.Name,
-				Owned:   owned,
-				Vindex:  vindex,
+				Columns:  columns,
+				Type:     vindexInfo.Type,
+				Name:     ind.Name,
+				Owned:    owned,
+				Vindex:   vindex,
+				isUnique: vindex.IsUnique(),
+				cost:     vindex.Cost(),
 			}
 			if i == 0 {
 				// Perform Primary vindex check.
@@ -330,6 +353,34 @@ func buildTables(ks *vschemapb.Keyspace, vschema *VSchema, ksvschema *KeyspaceSc
 				}
 				t.Owned = append(t.Owned, columnVindex)
 			}
+
+			mcv, isMultiColumn := vindex.(MultiColumn)
+			if !isMultiColumn {
+				continue
+			}
+			if i != 0 {
+				return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "multi-column vindex %s should be a primary vindex for table %s", ind.Name, tname)
+			}
+			if !mcv.PartialVindex() {
+				// Partial column selection not allowed.
+				// Do not create subset column vindex.
+				continue
+			}
+			cost := vindex.Cost()
+			for i := len(columns) - 1; i > 0; i-- {
+				columnSubset := columns[:i]
+				cost++
+				columnVindex = &ColumnVindex{
+					Columns:     columnSubset,
+					Type:        vindexInfo.Type,
+					Name:        ind.Name,
+					Owned:       owned,
+					Vindex:      vindex,
+					cost:        cost,
+					ignoreInDML: true,
+				}
+				t.ColumnVindexes = append(t.ColumnVindexes, columnVindex)
+			}
 		}
 		t.Ordered = colVindexSorted(t.ColumnVindexes)
 
@@ -574,7 +625,7 @@ type ByCost []*ColumnVindex
 
 func (bc ByCost) Len() int           { return len(bc) }
 func (bc ByCost) Swap(i, j int)      { bc[i], bc[j] = bc[j], bc[i] }
-func (bc ByCost) Less(i, j int) bool { return bc[i].Vindex.Cost() < bc[j].Vindex.Cost() }
+func (bc ByCost) Less(i, j int) bool { return bc[i].Cost() < bc[j].Cost() }
 
 func colVindexSorted(cvs []*ColumnVindex) (sorted []*ColumnVindex) {
 	sorted = append(sorted, cvs...)
@@ -640,10 +691,10 @@ func FindBestColVindex(table *Table) (*ColumnVindex, error) {
 		if cv.Vindex.NeedsVCursor() {
 			continue
 		}
-		if !cv.Vindex.IsUnique() {
+		if !cv.IsUnique() {
 			continue
 		}
-		if result == nil || result.Vindex.Cost() > cv.Vindex.Cost() {
+		if result == nil || result.Cost() > cv.Cost() {
 			result = cv
 		}
 	}
@@ -668,11 +719,11 @@ func FindVindexForSharding(tableName string, colVindexes []*ColumnVindex) (*Colu
 		if _, ok := colVindex.Vindex.(SingleColumn); !ok {
 			continue
 		}
-		if colVindex.Vindex.Cost() < result.Vindex.Cost() && colVindex.Vindex.IsUnique() {
+		if colVindex.Cost() < result.Cost() && colVindex.IsUnique() {
 			result = colVindex
 		}
 	}
-	if result.Vindex.Cost() > 1 || !result.Vindex.IsUnique() {
+	if result.Cost() > 1 || !result.IsUnique() {
 		return nil, fmt.Errorf("could not find a vindex to use for sharding table %v", tableName)
 	}
 	return result, nil
diff --git a/go/vt/wrangler/materializer.go b/go/vt/wrangler/materializer.go
index 353df8e1ab..6fefdafea4 100644
--- a/go/vt/wrangler/materializer.go
+++ b/go/vt/wrangler/materializer.go
@@ -65,34 +65,6 @@ const (
 	createDDLAsCopyDropConstraint = "copy:drop_constraint"
 )
 
-// addTablesToVSchema adds tables to an (unsharded) vschema. Depending on copyAttributes It will also add any sequence info
-// that is associated with a table by copying it from the vschema of the source keyspace.
-// For a migrate workflow we do not copy attributes since the source keyspace is just a proxy to import data into Vitess
-// Todo: For now we only copy sequence but later we may also want to copy other attributes like authoritative column flag and list of columns
-func (wr *Wrangler) addTablesToVSchema(ctx context.Context, sourceKeyspace string, targetVSchema *vschemapb.Keyspace, tables []string, copyAttributes bool) error {
-	if targetVSchema.Tables == nil {
-		targetVSchema.Tables = make(map[string]*vschemapb.Table)
-	}
-	for _, table := range tables {
-		targetVSchema.Tables[table] = &vschemapb.Table{}
-	}
-
-	if copyAttributes { // if source keyspace is provided, copy over the sequence info.
-		srcVSchema, err := wr.ts.GetVSchema(ctx, sourceKeyspace)
-		if err != nil {
-			return err
-		}
-		for _, table := range tables {
-			srcTable, ok := srcVSchema.Tables[table]
-			if ok {
-				targetVSchema.Tables[table].AutoIncrement = srcTable.AutoIncrement
-			}
-		}
-
-	}
-	return nil
-}
-
 // MoveTables initiates moving table(s) over to another keyspace
 func (wr *Wrangler) MoveTables(ctx context.Context, workflow, sourceKeyspace, targetKeyspace, tableSpecs,
 	cell, tabletTypes string, allTables bool, excludeTables string, autoStart, stopAfterCopy bool,
@@ -102,7 +74,7 @@ func (wr *Wrangler) MoveTables(ctx context.Context, workflow, sourceKeyspace, ta
 	var externalTopo *topo.Server
 	var err error
 
-	if externalCluster != "" { // when the source is an external mysql cluster mounted using the Mount command
+	if externalCluster != "" {
 		externalTopo, err = wr.ts.OpenExternalVitessClusterServer(ctx, externalCluster)
 		if err != nil {
 			return err
@@ -110,7 +82,6 @@ func (wr *Wrangler) MoveTables(ctx context.Context, workflow, sourceKeyspace, ta
 		wr.sourceTs = externalTopo
 		log.Infof("Successfully opened external topo: %+v", externalTopo)
 	}
-
 	var vschema *vschemapb.Keyspace
 	vschema, err = wr.ts.GetVSchema(ctx, targetKeyspace)
 	if err != nil {
@@ -179,8 +150,11 @@ func (wr *Wrangler) MoveTables(ctx context.Context, workflow, sourceKeyspace, ta
 		log.Infof("Found tables to move: %s", strings.Join(tables, ","))
 
 		if !vschema.Sharded {
-			if err := wr.addTablesToVSchema(ctx, sourceKeyspace, vschema, tables, externalTopo == nil); err != nil {
-				return err
+			if vschema.Tables == nil {
+				vschema.Tables = make(map[string]*vschemapb.Table)
+			}
+			for _, table := range tables {
+				vschema.Tables[table] = &vschemapb.Table{}
 			}
 		}
 	}
diff --git a/java/pom.xml b/java/pom.xml
index 9e8943a0ca..83f44b51e8 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -75,7 +75,7 @@
     <protobuf.java.version>3.11.4</protobuf.java.version>
     <protobuf.protoc.version>3.11.4</protobuf.protoc.version>
     <checkstyle.plugin.version>3.0.0</checkstyle.plugin.version>
-    <log4j2.version>2.16.0</log4j2.version>
+    <log4j2.version>2.15.0</log4j2.version>
   </properties>
 
   <!-- Add new dependencies here and then add it below or in your module. -->


Test patch:
diff --git a/go/test/endtoend/vreplication/resharding_workflows_v2_test.go b/go/test/endtoend/vreplication/resharding_workflows_v2_test.go
index 6d7d2e3541..f7e1e69b50 100644
--- a/go/test/endtoend/vreplication/resharding_workflows_v2_test.go
+++ b/go/test/endtoend/vreplication/resharding_workflows_v2_test.go
@@ -22,10 +22,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/tidwall/gjson"
-
-	"github.com/stretchr/testify/assert"
-
 	"vitess.io/vitess/go/vt/log"
 
 	"vitess.io/vitess/go/vt/wrangler"
@@ -248,120 +244,6 @@ func TestBasicV2Workflows(t *testing.T) {
 	log.Flush()
 }
 
-const workflowStartTimeout = 5 * time.Second
-
-func waitForWorkflowToStart(t *testing.T, ksWorkflow string) {
-	done := false
-	ticker := time.NewTicker(100 * time.Millisecond)
-	timer := time.NewTimer(workflowStartTimeout)
-	log.Infof("Waiting for workflow %s to start", ksWorkflow)
-	for {
-		select {
-		case <-ticker.C:
-			if done {
-				log.Infof("Workflow %s has started", ksWorkflow)
-				return
-			}
-			output, err := vc.VtctlClient.ExecuteCommandWithOutput("Workflow", ksWorkflow, "show")
-			require.NoError(t, err)
-			done = true
-			state := ""
-			result := gjson.Get(output, "ShardStatuses")
-			result.ForEach(func(tabletId, tabletStreams gjson.Result) bool { // for each participating tablet
-				tabletStreams.ForEach(func(streamId, streamInfos gjson.Result) bool { // for each stream
-					if streamId.String() == "PrimaryReplicationStatuses" {
-						streamInfos.ForEach(func(attributeKey, attributeValue gjson.Result) bool { // for each attribute in the stream
-							state = attributeValue.Get("State").String()
-							if state != "Running" {
-								done = false // we need to wait for all streams to start
-							}
-							return true
-						})
-					}
-					return true
-				})
-				return true
-			})
-
-		case <-timer.C:
-			require.FailNowf(t, "workflow %s not yet started", ksWorkflow)
-		}
-	}
-}
-
-/*
-testVSchemaForSequenceAfterMoveTables checks that the related sequence tag is migrated correctly in the vschema
-while moving a table with an auto-increment from sharded to unsharded.
-*/
-func testVSchemaForSequenceAfterMoveTables(t *testing.T) {
-	// at this point the unsharded product and sharded customer keyspaces are created by previous tests
-
-	// use MoveTables to move customer2 from product to customer using
-	currentWorkflowType = wrangler.MoveTablesWorkflow
-	err := tstWorkflowExec(t, defaultCellName, "wf2", sourceKs, targetKs,
-		"customer2", workflowActionCreate, "", "", "")
-	require.NoError(t, err)
-
-	waitForWorkflowToStart(t, "customer.wf2")
-
-	err = tstWorkflowExec(t, defaultCellName, "wf2", sourceKs, targetKs,
-		"", workflowActionSwitchTraffic, "", "", "")
-	require.NoError(t, err)
-	err = tstWorkflowExec(t, defaultCellName, "wf2", sourceKs, targetKs,
-		"", workflowActionComplete, "", "", "")
-	require.NoError(t, err)
-
-	// sanity check
-	output, err := vc.VtctlClient.ExecuteCommandWithOutput("GetVSchema", "product")
-	require.NoError(t, err)
-	assert.NotContains(t, output, "customer2\"", "customer2 still found in keyspace product")
-	validateCount(t, vtgateConn, "customer", "customer2", 3)
-
-	// check that customer2 has the sequence tag
-	output, err = vc.VtctlClient.ExecuteCommandWithOutput("GetVSchema", "customer")
-	require.NoError(t, err)
-	assert.Contains(t, output, "\"sequence\": \"customer_seq2\"", "customer2 sequence missing in keyspace customer")
-
-	// ensure sequence is available to vtgate
-	num := 5
-	for i := 0; i < num; i++ {
-		execVtgateQuery(t, vtgateConn, "customer", "insert into customer2(name) values('a')")
-	}
-	validateCount(t, vtgateConn, "customer", "customer2", 3+num)
-	want := fmt.Sprintf("[[INT32(%d)]]", 100+num-1)
-	validateQuery(t, vtgateConn, "customer", "select max(cid) from customer2", want)
-
-	// use MoveTables to move customer2 back to product. Note that now the table has an associated sequence
-	err = tstWorkflowExec(t, defaultCellName, "wf3", targetKs, sourceKs,
-		"customer2", workflowActionCreate, "", "", "")
-	require.NoError(t, err)
-	waitForWorkflowToStart(t, "product.wf3")
-	err = tstWorkflowExec(t, defaultCellName, "wf3", targetKs, sourceKs,
-		"", workflowActionSwitchTraffic, "", "", "")
-	require.NoError(t, err)
-	err = tstWorkflowExec(t, defaultCellName, "wf3", targetKs, sourceKs,
-		"", workflowActionComplete, "", "", "")
-	require.NoError(t, err)
-
-	// sanity check
-	output, err = vc.VtctlClient.ExecuteCommandWithOutput("GetVSchema", "product")
-	require.NoError(t, err)
-	assert.Contains(t, output, "customer2\"", "customer2 not found in keyspace product ")
-
-	// check that customer2 still has the sequence tag
-	output, err = vc.VtctlClient.ExecuteCommandWithOutput("GetVSchema", "product")
-	require.NoError(t, err)
-	assert.Contains(t, output, "\"sequence\": \"customer_seq2\"", "customer2 still found in keyspace product")
-
-	// ensure sequence is available to vtgate
-	for i := 0; i < num; i++ {
-		execVtgateQuery(t, vtgateConn, "product", "insert into customer2(name) values('a')")
-	}
-	validateCount(t, vtgateConn, "product", "customer2", 3+num+num)
-	want = fmt.Sprintf("[[INT32(%d)]]", 100+num+num-1)
-	validateQuery(t, vtgateConn, "product", "select max(cid) from customer2", want)
-}
-
 func testReshardV2Workflow(t *testing.T) {
 	currentWorkflowType = wrangler.ReshardWorkflow
 
@@ -394,9 +276,7 @@ func testMoveTablesV2Workflow(t *testing.T) {
 	output, _ := vc.VtctlClient.ExecuteCommandWithOutput(listAllArgs...)
 	require.Contains(t, output, "No workflows found in keyspace customer")
 
-	testVSchemaForSequenceAfterMoveTables(t)
-
-	createMoveTablesWorkflow(t, "tenant")
+	createMoveTablesWorkflow(t, "customer2")
 	output, _ = vc.VtctlClient.ExecuteCommandWithOutput(listAllArgs...)
 	require.Contains(t, output, "Following workflow(s) found in keyspace customer: wf1")
 
diff --git a/go/test/endtoend/vreplication/vreplication_test.go b/go/test/endtoend/vreplication/vreplication_test.go
index 442c514159..7884fce5c9 100644
--- a/go/test/endtoend/vreplication/vreplication_test.go
+++ b/go/test/endtoend/vreplication/vreplication_test.go
@@ -203,7 +203,6 @@ func insertInitialData(t *testing.T) {
 		execMultipleQueries(t, vtgateConn, "product:0", string(lines))
 		execVtgateQuery(t, vtgateConn, "product:0", "insert into customer_seq(id, next_id, cache) values(0, 100, 100);")
 		execVtgateQuery(t, vtgateConn, "product:0", "insert into order_seq(id, next_id, cache) values(0, 100, 100);")
-		execVtgateQuery(t, vtgateConn, "product:0", "insert into customer_seq2(id, next_id, cache) values(0, 100, 100);")
 		log.Infof("Done inserting initial data")
 
 		validateCount(t, vtgateConn, "product:0", "product", 2)
diff --git a/go/test/endtoend/vtgate/gen4/gen4_test.go b/go/test/endtoend/vtgate/gen4/gen4_test.go
index 93f4215446..54673cac6a 100644
--- a/go/test/endtoend/vtgate/gen4/gen4_test.go
+++ b/go/test/endtoend/vtgate/gen4/gen4_test.go
@@ -19,6 +19,7 @@ package vtgate
 import (
 	"context"
 	"fmt"
+	"strconv"
 	"testing"
 
 	"vitess.io/vitess/go/test/endtoend/vtgate/utils"
@@ -276,3 +277,55 @@ func TestMultiColumnVindex(t *testing.T) {
 		})
 	}
 }
+
+func TestFanoutVindex(t *testing.T) {
+	ctx := context.Background()
+	conn, err := mysql.Connect(ctx, &vtParams)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	tcases := []struct {
+		regionID int
+		exp      string
+	}{{
+		regionID: 24,
+		exp:      `[[INT64(24) INT64(1) VARCHAR("shard--19a0")]]`,
+	}, {
+		regionID: 25,
+		exp:      `[[INT64(25) INT64(2) VARCHAR("shard--19a0")] [INT64(25) INT64(7) VARCHAR("shard-19a0-20")]]`,
+	}, {
+		regionID: 31,
+		exp:      `[[INT64(31) INT64(8) VARCHAR("shard-19a0-20")]]`,
+	}, {
+		regionID: 32,
+		exp:      `[[INT64(32) INT64(14) VARCHAR("shard-20-20c0")] [INT64(32) INT64(19) VARCHAR("shard-20c0-")]]`,
+	}, {
+		regionID: 33,
+		exp:      `[[INT64(33) INT64(20) VARCHAR("shard-20c0-")]]`,
+	}}
+
+	defer utils.ExecAllowError(t, conn, `delete from region_tbl`)
+	uid := 1
+	// insert data in all shards to know where the query fan-out
+	for _, s := range shardedKsShards {
+		utils.Exec(t, conn, fmt.Sprintf("use `%s:%s`", shardedKs, s))
+		for _, tcase := range tcases {
+			utils.Exec(t, conn, fmt.Sprintf("insert into region_tbl(rg,uid,msg) values(%d,%d,'shard-%s')", tcase.regionID, uid, s))
+			uid++
+		}
+	}
+
+	newConn, err := mysql.Connect(ctx, &vtParams)
+	require.NoError(t, err)
+	defer newConn.Close()
+
+	for _, workload := range []string{"olap", "oltp"} {
+		utils.Exec(t, newConn, fmt.Sprintf(`set workload = %s`, workload))
+		for _, tcase := range tcases {
+			t.Run(workload+strconv.Itoa(tcase.regionID), func(t *testing.T) {
+				sql := fmt.Sprintf("select rg, uid, msg from region_tbl where rg = %d order by uid", tcase.regionID)
+				assert.Equal(t, tcase.exp, fmt.Sprintf("%v", utils.Exec(t, newConn, sql).Rows))
+			})
+		}
+	}
+}
diff --git a/go/test/endtoend/vtgate/gen4/main_test.go b/go/test/endtoend/vtgate/gen4/main_test.go
index 92864252af..93ca534f1e 100644
--- a/go/test/endtoend/vtgate/gen4/main_test.go
+++ b/go/test/endtoend/vtgate/gen4/main_test.go
@@ -32,6 +32,7 @@ var (
 	vtParams         mysql.ConnParams
 	shardedKs        = "ks"
 	unshardedKs      = "uks"
+	shardedKsShards  = []string{"-19a0", "19a0-20", "20-20c0", "20c0-"}
 	Cell             = "test"
 	shardedSchemaSQL = `create table t1(
 	id bigint,
@@ -59,6 +60,13 @@ create table user_region(
 	colb bigint,
 	primary key(id)
 ) Engine=InnoDB;
+
+create table region_tbl(
+	rg bigint,
+	uid bigint,
+	msg varchar(50),
+	primary key(uid)
+) Engine=InnoDB;
 `
 	unshardedSchemaSQL = `create table u_a(
 	id bigint,
@@ -131,6 +139,14 @@ create table u_b(
 		  "name": "regional_vdx"
 		}
       ]
+    },
+    "region_tbl": {
+	  "column_vindexes": [
+	    {
+          "columns": ["rg","uid"],
+		  "name": "regional_vdx"
+		}
+      ]
 	}
   }
 }`
@@ -174,7 +190,7 @@ func TestMain(m *testing.M) {
 			SchemaSQL: shardedSchemaSQL,
 			VSchema:   shardedVSchema,
 		}
-		err = clusterInstance.StartKeyspace(*sKs, []string{"-80", "80-"}, 0, false)
+		err = clusterInstance.StartKeyspace(*sKs, shardedKsShards, 0, false)
 		if err != nil {
 			return 1
 		}
diff --git a/go/vt/vtgate/buffer/buffer_test.go b/go/vt/vtgate/buffer/buffer_test.go
index 7f32364d57..6404e7704f 100644
--- a/go/vt/vtgate/buffer/buffer_test.go
+++ b/go/vt/vtgate/buffer/buffer_test.go
@@ -271,15 +271,15 @@ func testPassthrough1(t *testing.T, fail failover) {
 	}
 }
 
-// TestLastReparentTooRecentBufferingSkipped tests that buffering is skipped if
+// TestLastReparentTooRecent_BufferingSkipped tests that buffering is skipped if
 // we see the reparent (end) *before* any request failures due to it.
 // We must not start buffering because we already observed the trigger for
 // stopping buffering (the reparent) and may not see it again.
-func TestLastReparentTooRecentBufferingSkipped(t *testing.T) {
-	testAllImplementations(t, testLastReparentTooRecentBufferingSkipped1)
+func TestLastReparentTooRecent_BufferingSkipped(t *testing.T) {
+	testAllImplementations(t, testLastReparentTooRecent_BufferingSkipped1)
 }
 
-func testLastReparentTooRecentBufferingSkipped1(t *testing.T, fail failover) {
+func testLastReparentTooRecent_BufferingSkipped1(t *testing.T, fail failover) {
 	resetVariables()
 
 	now := time.Now()
@@ -312,14 +312,14 @@ func testLastReparentTooRecentBufferingSkipped1(t *testing.T, fail failover) {
 	}
 }
 
-// TestLastReparentTooRecentBuffering explicitly tests that the "too recent"
+// TestLastReparentTooRecent_Buffering explicitly tests that the "too recent"
 // skipping of the buffering does NOT get triggered because enough time has
 // elapsed since the last seen reparent.
-func TestLastReparentTooRecentBuffering(t *testing.T) {
-	testAllImplementations(t, testLastReparentTooRecentBuffering1)
+func TestLastReparentTooRecent_Buffering(t *testing.T) {
+	testAllImplementations(t, testLastReparentTooRecent_Buffering1)
 }
 
-func testLastReparentTooRecentBuffering1(t *testing.T, fail failover) {
+func testLastReparentTooRecent_Buffering1(t *testing.T, fail failover) {
 	resetVariables()
 
 	now := time.Now()
diff --git a/go/vt/vtgate/buffer/flags_test.go b/go/vt/vtgate/buffer/flags_test.go
index 6794800cac..c8ff49bf52 100644
--- a/go/vt/vtgate/buffer/flags_test.go
+++ b/go/vt/vtgate/buffer/flags_test.go
@@ -27,7 +27,7 @@ func TestVerifyFlags(t *testing.T) {
 		// Set all flags to their default value.
 		flag.Set("enable_buffer", "false")
 		flag.Set("enable_buffer_dry_run", "false")
-		flag.Set("buffer_size", "1000")
+		flag.Set("buffer_size", "10")
 		flag.Set("buffer_window", "10s")
 		flag.Set("buffer_keyspace_shards", "")
 		flag.Set("buffer_max_failover_duration", "20s")
diff --git a/go/vt/vtgate/engine/insert_test.go b/go/vt/vtgate/engine/insert_test.go
index aab3c0cca0..b6a0baadca 100644
--- a/go/vt/vtgate/engine/insert_test.go
+++ b/go/vt/vtgate/engine/insert_test.go
@@ -701,6 +701,12 @@ func TestInsertShardedGeo(t *testing.T) {
 		[]string{" mid1", " mid2"},
 		" suffix",
 	)
+	for _, colVindex := range ks.Tables["t1"].ColumnVindexes {
+		if colVindex.IgnoreInDML() {
+			continue
+		}
+		ins.ColVindexes = append(ins.ColVindexes, colVindex)
+	}
 
 	vc := newDMLTestVCursor("-20", "20-")
 	vc.shardForKsid = []string{"20-", "-20"}
diff --git a/go/vt/vtgate/engine/route_test.go b/go/vt/vtgate/engine/route_test.go
index 60207f7f82..9ec4b00968 100644
--- a/go/vt/vtgate/engine/route_test.go
+++ b/go/vt/vtgate/engine/route_test.go
@@ -1409,6 +1409,43 @@ func TestSelectEqualUniqueMultiColumnVindex(t *testing.T) {
 	expectResult(t, "sel.StreamExecute", result, defaultSelectResult)
 }
 
+func TestSelectEqualMultiColumnVindex(t *testing.T) {
+	vindex, _ := vindexes.NewRegionExperimental("", map[string]string{"region_bytes": "1"})
+	vc := &loggingVCursor{
+		shards:       []string{"-20", "20-"},
+		shardForKsid: []string{"-20", "20-"},
+		results:      []*sqltypes.Result{defaultSelectResult},
+	}
+	sel := NewRoute(
+		SelectEqual,
+		&vindexes.Keyspace{
+			Name:    "ks",
+			Sharded: true,
+		},
+		"dummy_select",
+		"dummy_select_field",
+	)
+	sel.Vindex = vindex
+	sel.Values = []evalengine.Expr{evalengine.NewLiteralInt(32)}
+
+	result, err := sel.TryExecute(vc, map[string]*querypb.BindVariable{}, false)
+	require.NoError(t, err)
+	vc.ExpectLog(t, []string{
+		`ResolveDestinationsMultiCol ks [[INT64(32)]] Destinations:DestinationKeyRange(20-21)`,
+		`ExecuteMultiShard ks.-20: dummy_select {} ks.20-: dummy_select {} false false`,
+	})
+	expectResult(t, "sel.StreamExecute", result, defaultSelectResult)
+
+	vc.Rewind()
+	result, err = wrapStreamExecute(sel, vc, map[string]*querypb.BindVariable{}, false)
+	require.NoError(t, err)
+	vc.ExpectLog(t, []string{
+		`ResolveDestinationsMultiCol ks [[INT64(32)]] Destinations:DestinationKeyRange(20-21)`,
+		`StreamExecuteMulti dummy_select ks.-20: {} ks.20-: {} `,
+	})
+	expectResult(t, "sel.StreamExecute", result, defaultSelectResult)
+}
+
 func TestSelectINMultiColumnVindex(t *testing.T) {
 	vindex, _ := vindexes.NewRegionExperimental("", map[string]string{"region_bytes": "1"})
 	sel := NewRoute(
diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go
index fda61fab3b..a6b9e6c97b 100644
--- a/go/vt/vtgate/executor_framework_test.go
+++ b/go/vt/vtgate/executor_framework_test.go
@@ -460,7 +460,7 @@ func createLegacyExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandb
 
 func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn.SandboxConn) {
 	// Use legacy gateway until we can rewrite these tests to use new tabletgateway
-	*GatewayImplementation = GatewayImplementationDiscovery
+	*GatewayImplementation = tabletGatewayImplementation
 	cell := "aa"
 	hc := discovery.NewFakeHealthCheck(nil)
 	s := createSandbox("TestExecutor")
diff --git a/go/vt/vtgate/executor_select_test.go b/go/vt/vtgate/executor_select_test.go
index e577b6025f..6ed2c68dc4 100644
--- a/go/vt/vtgate/executor_select_test.go
+++ b/go/vt/vtgate/executor_select_test.go
@@ -19,6 +19,7 @@ package vtgate
 import (
 	"fmt"
 	"runtime"
+	"strconv"
 	"strings"
 	"testing"
 	"time"
@@ -2930,3 +2931,60 @@ func TestGen4MultiColMultiEqual(t *testing.T) {
 	require.Nil(t, sbc1.Queries)
 	utils.MustMatch(t, wantQueries, sbc2.Queries)
 }
+
+func TestRegionRange(t *testing.T) {
+	// Special setup: Don't use createLegacyExecutorEnv.
+
+	*GatewayImplementation = tabletGatewayImplementation
+	*plannerVersion = "gen4"
+	defer func() {
+		*plannerVersion = "v3"
+	}()
+	cell := "regioncell"
+	ks := "TestExecutor"
+	hc := discovery.NewFakeHealthCheck(nil)
+	s := createSandbox(ks)
+	s.ShardSpec = "-20-20a0-"
+	s.VSchema = executorVSchema
+	serv := newSandboxForCells([]string{cell})
+	resolver := newTestResolver(hc, serv, cell)
+	shards := []string{"-20", "20-20a0", "20a0-"}
+	var conns []*sandboxconn.SandboxConn
+	for _, shard := range shards {
+		sbc := hc.AddTestTablet(cell, shard, 1, ks, shard, topodatapb.TabletType_PRIMARY, true, 1, nil)
+		conns = append(conns, sbc)
+	}
+	executor := createExecutor(serv, cell, resolver)
+
+	tcases := []struct {
+		regionID          int
+		noOfShardsTouched int
+	}{{
+		regionID:          31,
+		noOfShardsTouched: 1,
+	}, {
+		regionID:          32,
+		noOfShardsTouched: 2,
+	}, {
+		regionID:          33,
+		noOfShardsTouched: 1,
+	}}
+	for _, tcase := range tcases {
+		t.Run(strconv.Itoa(tcase.regionID), func(t *testing.T) {
+			sql := fmt.Sprintf("select * from user_region where cola = %d", tcase.regionID)
+			_, err := executor.Execute(
+				context.Background(),
+				"TestRegionRange",
+				NewAutocommitSession(&vtgatepb.Session{}),
+				sql,
+				nil)
+			require.NoError(t, err)
+			count := 0
+			for _, sbc := range conns {
+				count = count + len(sbc.Queries)
+				sbc.Queries = nil
+			}
+			require.Equal(t, tcase.noOfShardsTouched, count)
+		})
+	}
+}
diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go
index 9e77bb3042..ec70012892 100644
--- a/go/vt/vtgate/planbuilder/plan_test.go
+++ b/go/vt/vtgate/planbuilder/plan_test.go
@@ -170,6 +170,10 @@ func (m *multiColIndex) Verify(vcursor vindexes.VCursor, rowsColValues [][]sqlty
 	return []bool{}, nil
 }
 
+func (m *multiColIndex) PartialVindex() bool {
+	return true
+}
+
 func init() {
 	vindexes.Register("hash_test", newHashIndex)
 	vindexes.Register("lookup_test", newLookupIndex)
diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
index cfc185e339..d6fb3d6692 100644
--- a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
@@ -3622,10 +3622,10 @@ Gen4 plan same as above
 }
 
 # multi column vindex with different order with one IN predicate and one equality
-"select * from multicol_tbl where colb in (3,4) and cola = 1"
+"select * from multicol_tbl where colb = 1 and cola in (3,4)"
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where colb in (3,4) and cola = 1",
+  "Original": "select * from multicol_tbl where colb = 1 and cola in (3,4)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectScatter",
@@ -3634,13 +3634,13 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where colb in (3, 4) and cola = 1",
+    "Query": "select * from multicol_tbl where colb = 1 and cola in (3, 4)",
     "Table": "multicol_tbl"
   }
 }
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where colb in (3,4) and cola = 1",
+  "Original": "select * from multicol_tbl where colb = 1 and cola in (3,4)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectIN",
@@ -3649,11 +3649,11 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where colb in ::__vals1 and cola = 1",
+    "Query": "select * from multicol_tbl where colb = 1 and cola in ::__vals0",
     "Table": "multicol_tbl",
     "Values": [
-      "INT64(1)",
-      "(INT64(3), INT64(4))"
+      "(INT64(3), INT64(4))",
+      "INT64(1)"
     ],
     "Vindex": "multicolIdx"
   }
@@ -3698,10 +3698,10 @@ Gen4 plan same as above
 }
 
 # multi column vindex with one column with equal followed by IN predicate, ordering matters for now
-"select * from multicol_tbl where cola = 4 and cola in (1,10) and colb in (5,6)"
+"select * from multicol_tbl where colb = 4 and colb in (1,10) and cola in (5,6)"
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where cola = 4 and cola in (1,10) and colb in (5,6)",
+  "Original": "select * from multicol_tbl where colb = 4 and colb in (1,10) and cola in (5,6)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectScatter",
@@ -3710,13 +3710,13 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where cola = 4 and cola in (1, 10) and colb in (5, 6)",
+    "Query": "select * from multicol_tbl where colb = 4 and colb in (1, 10) and cola in (5, 6)",
     "Table": "multicol_tbl"
   }
 }
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where cola = 4 and cola in (1,10) and colb in (5,6)",
+  "Original": "select * from multicol_tbl where colb = 4 and colb in (1,10) and cola in (5,6)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectIN",
@@ -3725,21 +3725,21 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where cola = 4 and cola in ::__vals0 and colb in ::__vals1",
+    "Query": "select * from multicol_tbl where colb = 4 and colb in ::__vals1 and cola in ::__vals0",
     "Table": "multicol_tbl",
     "Values": [
-      "(INT64(1), INT64(10))",
-      "(INT64(5), INT64(6))"
+      "(INT64(5), INT64(6))",
+      "(INT64(1), INT64(10))"
     ],
     "Vindex": "multicolIdx"
   }
 }
 
 # multi column vindex with one column with IN followed by equal predicate, ordering matters for now
-"select * from multicol_tbl where cola in (1,10) and cola = 4 and colb in (5,6)"
+"select * from multicol_tbl where colb in (1,10) and colb = 4 and cola in (5,6)"
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where cola in (1,10) and cola = 4 and colb in (5,6)",
+  "Original": "select * from multicol_tbl where colb in (1,10) and colb = 4 and cola in (5,6)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectScatter",
@@ -3748,13 +3748,13 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where cola in (1, 10) and cola = 4 and colb in (5, 6)",
+    "Query": "select * from multicol_tbl where colb in (1, 10) and colb = 4 and cola in (5, 6)",
     "Table": "multicol_tbl"
   }
 }
 {
   "QueryType": "SELECT",
-  "Original": "select * from multicol_tbl where cola in (1,10) and cola = 4 and colb in (5,6)",
+  "Original": "select * from multicol_tbl where colb in (1,10) and colb = 4 and cola in (5,6)",
   "Instructions": {
     "OperatorType": "Route",
     "Variant": "SelectIN",
@@ -3763,11 +3763,11 @@ Gen4 plan same as above
       "Sharded": true
     },
     "FieldQuery": "select * from multicol_tbl where 1 != 1",
-    "Query": "select * from multicol_tbl where cola in (1, 10) and cola = 4 and colb in ::__vals1",
+    "Query": "select * from multicol_tbl where colb in (1, 10) and colb = 4 and cola in ::__vals0",
     "Table": "multicol_tbl",
     "Values": [
-      "INT64(4)",
-      "(INT64(5), INT64(6))"
+      "(INT64(5), INT64(6))",
+      "INT64(4)"
     ],
     "Vindex": "multicolIdx"
   }
@@ -4008,3 +4008,77 @@ Gen4 plan same as above
     ]
   }
 }
+
+# multi column vindex, partial vindex with SelectEqual
+"select * from multicol_tbl where cola = 1"
+{
+  "QueryType": "SELECT",
+  "Original": "select * from multicol_tbl where cola = 1",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "SelectScatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select * from multicol_tbl where 1 != 1",
+    "Query": "select * from multicol_tbl where cola = 1",
+    "Table": "multicol_tbl"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select * from multicol_tbl where cola = 1",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "SelectEqual",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select * from multicol_tbl where 1 != 1",
+    "Query": "select * from multicol_tbl where cola = 1",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
+
+# multi column vindex, partial vindex with SelectEqual over full vindex with SelectIN
+"select * from multicol_tbl where cola = 1 and colb in (2,3)"
+{
+  "QueryType": "SELECT",
+  "Original": "select * from multicol_tbl where cola = 1 and colb in (2,3)",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "SelectScatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select * from multicol_tbl where 1 != 1",
+    "Query": "select * from multicol_tbl where cola = 1 and colb in (2, 3)",
+    "Table": "multicol_tbl"
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select * from multicol_tbl where cola = 1 and colb in (2,3)",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "SelectEqual",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select * from multicol_tbl where 1 != 1",
+    "Query": "select * from multicol_tbl where cola = 1 and colb in (2, 3)",
+    "Table": "multicol_tbl",
+    "Values": [
+      "INT64(1)"
+    ],
+    "Vindex": "multicolIdx"
+  }
+}
diff --git a/go/vt/vtgate/sandbox_test.go b/go/vt/vtgate/sandbox_test.go
index 01165eaee6..4a30748005 100644
--- a/go/vt/vtgate/sandbox_test.go
+++ b/go/vt/vtgate/sandbox_test.go
@@ -284,7 +284,7 @@ func (sct *sandboxTopo) GetSrvKeyspace(ctx context.Context, cell, keyspace strin
 }
 
 func (sct *sandboxTopo) WatchSrvKeyspace(ctx context.Context, cell, keyspace string, callback func(*topodatapb.SrvKeyspace, error) bool) {
-	// panic("not supported: WatchSrvKeyspace")
+	panic("not supported: WatchSrvKeyspace")
 }
 
 // WatchSrvVSchema is part of the srvtopo.Server interface.
diff --git a/go/vt/vtgate/vindexes/region_experimental_test.go b/go/vt/vtgate/vindexes/region_experimental_test.go
index f094038042..efec0f8e25 100644
--- a/go/vt/vtgate/vindexes/region_experimental_test.go
+++ b/go/vt/vtgate/vindexes/region_experimental_test.go
@@ -20,6 +20,8 @@ import (
 	"strconv"
 	"testing"
 
+	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -47,7 +49,7 @@ func TestRegionExperimentalMap(t *testing.T) {
 	}, {
 		sqltypes.NewInt64(256), sqltypes.NewInt64(1),
 	}, {
-		// Invalid length.
+		// only region id provided, partial column for key range mapping.
 		sqltypes.NewInt64(1),
 	}, {
 		// Invalid region.
@@ -62,7 +64,7 @@ func TestRegionExperimentalMap(t *testing.T) {
 		key.DestinationKeyspaceID([]byte("\x01\x16k@\xb4J\xbaK\xd6")),
 		key.DestinationKeyspaceID([]byte("\xff\x16k@\xb4J\xbaK\xd6")),
 		key.DestinationKeyspaceID([]byte("\x00\x16k@\xb4J\xbaK\xd6")),
-		key.DestinationNone{},
+		key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{Start: []byte("\x01"), End: []byte("\x02")}},
 		key.DestinationNone{},
 		key.DestinationNone{},
 	}
diff --git a/go/vt/vtgate/vindexes/vschema_test.go b/go/vt/vtgate/vindexes/vschema_test.go
index 3a1cf31568..257e5bda94 100644
--- a/go/vt/vtgate/vindexes/vschema_test.go
+++ b/go/vt/vtgate/vindexes/vschema_test.go
@@ -23,6 +23,8 @@ import (
 	"strings"
 	"testing"
 
+	"vitess.io/vitess/go/test/utils"
+
 	"google.golang.org/protobuf/proto"
 
 	"github.com/stretchr/testify/assert"
@@ -35,7 +37,6 @@ import (
 
 	querypb "vitess.io/vitess/go/vt/proto/query"
 	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
-	"vitess.io/vitess/go/vt/proto/vschema"
 	vschemapb "vitess.io/vitess/go/vt/proto/vschema"
 )
 
@@ -176,6 +177,26 @@ func NewSTLO(name string, _ map[string]string) (Vindex, error) {
 var _ SingleColumn = (*stLO)(nil)
 var _ Lookup = (*stLO)(nil)
 
+// mcFU is a multi-column Functional, Unique Vindex.
+type mcFU struct {
+	name   string
+	Params map[string]string
+}
+
+func (v *mcFU) String() string                                                      { return v.name }
+func (*mcFU) Cost() int                                                             { return 1 }
+func (*mcFU) IsUnique() bool                                                        { return true }
+func (*mcFU) NeedsVCursor() bool                                                    { return false }
+func (*mcFU) Verify(VCursor, [][]sqltypes.Value, [][]byte) ([]bool, error)          { return []bool{}, nil }
+func (*mcFU) Map(cursor VCursor, ids [][]sqltypes.Value) ([]key.Destination, error) { return nil, nil }
+func (*mcFU) PartialVindex() bool                                                   { return false }
+
+func NewMCFU(name string, params map[string]string) (Vindex, error) {
+	return &mcFU{name: name, Params: params}, nil
+}
+
+var _ MultiColumn = (*mcFU)(nil)
+
 func init() {
 	Register("cheap", NewCheapVindex)
 	Register("stfu", NewSTFU)
@@ -183,13 +204,15 @@ func init() {
 	Register("stln", NewSTLN)
 	Register("stlu", NewSTLU)
 	Register("stlo", NewSTLO)
+	Register("region_experimental_test", NewRegionExperimental)
+	Register("mcfu", NewMCFU)
 }
 
 func TestUnshardedVSchemaValid(t *testing.T) {
 	err := ValidateKeyspace(&vschemapb.Keyspace{
 		Sharded:  false,
-		Vindexes: make(map[string]*vschema.Vindex),
-		Tables:   make(map[string]*vschema.Table),
+		Vindexes: make(map[string]*vschemapb.Vindex),
+		Tables:   make(map[string]*vschemapb.Table),
 	})
 	if err != nil {
 		t.Errorf("TestUnshardedVSchemaValid:\n%v", err)
@@ -504,17 +527,21 @@ func TestShardedVSchemaOwned(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
-				Type:    "stln",
-				Name:    "stln1",
-				Owned:   true,
-				Vindex:  vindex2,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
+				Type:     "stln",
+				Name:     "stln1",
+				Owned:    true,
+				Vindex:   vindex2,
+				isUnique: vindex2.IsUnique(),
+				cost:     vindex2.Cost(),
 			},
 		},
 	}
@@ -552,11 +579,7 @@ func TestShardedVSchemaOwned(t *testing.T) {
 			},
 		},
 	}
-	if !reflect.DeepEqual(got, want) {
-		gotjson, _ := json.Marshal(got)
-		wantjson, _ := json.Marshal(want)
-		t.Errorf("BuildVSchema:\n%s, want\n%s", gotjson, wantjson)
-	}
+	utils.MustMatch(t, want, got)
 }
 
 func TestShardedVSchemaOwnerInfo(t *testing.T) {
@@ -728,10 +751,12 @@ func TestVSchemaRoutingRules(t *testing.T) {
 		Name:     sqlparser.NewTableIdent("t1"),
 		Keyspace: ks1,
 		ColumnVindexes: []*ColumnVindex{{
-			Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-			Type:    "stfu",
-			Name:    "stfu1",
-			Vindex:  vindex1,
+			Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+			Type:     "stfu",
+			Name:     "stfu1",
+			Vindex:   vindex1,
+			isUnique: vindex1.IsUnique(),
+			cost:     vindex1.Cost(),
 		}},
 	}
 	t1.Ordered = []*ColumnVindex{
@@ -1038,17 +1063,21 @@ func TestFindVindexForSharding(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
-				Type:    "stln",
-				Name:    "stln1",
-				Owned:   true,
-				Vindex:  vindex2,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
+				Type:     "stln",
+				Name:     "stln1",
+				Owned:    true,
+				Vindex:   vindex2,
+				isUnique: vindex2.IsUnique(),
+				cost:     vindex2.Cost(),
 			},
 		},
 	}
@@ -1071,17 +1100,21 @@ func TestFindVindexForShardingError(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stlu",
-				Name:    "stlu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stlu",
+				Name:     "stlu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
-				Type:    "stln",
-				Name:    "stln1",
-				Owned:   true,
-				Vindex:  vindex2,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
+				Type:     "stln",
+				Name:     "stln1",
+				Owned:    true,
+				Vindex:   vindex2,
+				isUnique: vindex2.IsUnique(),
+				cost:     vindex2.Cost(),
 			},
 		},
 	}
@@ -1112,17 +1145,21 @@ func TestFindVindexForSharding2(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stlu",
-				Name:    "stlu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stlu",
+				Name:     "stlu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Owned:   true,
-				Vindex:  vindex2,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Owned:    true,
+				Vindex:   vindex2,
+				isUnique: vindex2.IsUnique(),
+				cost:     vindex2.Cost(),
 			},
 		},
 	}
@@ -1178,10 +1215,12 @@ func TestShardedVSchemaMultiColumnVindex(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1"), sqlparser.NewColIdent("c2")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1"), sqlparser.NewColIdent("c2")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 		},
 	}
@@ -1267,18 +1306,22 @@ func TestShardedVSchemaNotOwned(t *testing.T) {
 		Keyspace: ks,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stlu",
-				Name:    "stlu1",
-				Owned:   false,
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stlu",
+				Name:     "stlu1",
+				Owned:    false,
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Owned:   false,
-				Vindex:  vindex2,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c2")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Owned:    false,
+				Vindex:   vindex2,
+				isUnique: vindex2.IsUnique(),
+				cost:     vindex2.Cost(),
 			},
 		},
 	}
@@ -1593,11 +1636,13 @@ func TestBuildVSchemaDupVindex(t *testing.T) {
 		Keyspace: ksa,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stlu",
-				Name:    "stlu1",
-				Owned:   false,
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stlu",
+				Name:     "stlu1",
+				Owned:    false,
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 		},
 	}
@@ -1609,11 +1654,13 @@ func TestBuildVSchemaDupVindex(t *testing.T) {
 		Keyspace: ksb,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stlu",
-				Name:    "stlu1",
-				Owned:   false,
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stlu",
+				Name:     "stlu1",
+				Owned:    false,
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 		},
 	}
@@ -1904,10 +1951,12 @@ func TestSequence(t *testing.T) {
 		Keyspace: kss,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 		},
 		AutoIncrement: &AutoIncrement{
@@ -1923,10 +1972,12 @@ func TestSequence(t *testing.T) {
 		Keyspace: kss,
 		ColumnVindexes: []*ColumnVindex{
 			{
-				Columns: []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
-				Type:    "stfu",
-				Name:    "stfu1",
-				Vindex:  vindex1,
+				Columns:  []sqlparser.ColIdent{sqlparser.NewColIdent("c1")},
+				Type:     "stfu",
+				Name:     "stfu1",
+				Vindex:   vindex1,
+				isUnique: vindex1.IsUnique(),
+				cost:     vindex1.Cost(),
 			},
 		},
 		AutoIncrement: &AutoIncrement{
@@ -2642,3 +2693,70 @@ func TestFindSingleKeyspace(t *testing.T) {
 		t.Errorf("FindTable(\"\"): %v, want %s", err, wantErr)
 	}
 }
+
+func TestMultiColVindexPartialAllowed(t *testing.T) {
+	input := vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"ksa": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"regional_vdx": {
+						Type: "region_experimental_test",
+						Params: map[string]string{
+							"region_bytes": "1",
+						},
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"user_region": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{
+							{
+								Columns: []string{"cola", "colb"},
+								Name:    "regional_vdx",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	vschema := BuildVSchema(&input)
+	table, err := vschema.FindTable("ksa", "user_region")
+	require.NoError(t, err)
+	require.Len(t, table.ColumnVindexes, 2)
+	require.True(t, table.ColumnVindexes[0].IsUnique())
+	require.False(t, table.ColumnVindexes[1].IsUnique())
+	require.EqualValues(t, 1, table.ColumnVindexes[0].Cost())
+	require.EqualValues(t, 2, table.ColumnVindexes[1].Cost())
+}
+
+func TestMultiColVindexPartialNotAllowed(t *testing.T) {
+	input := vschemapb.SrvVSchema{
+		Keyspaces: map[string]*vschemapb.Keyspace{
+			"ksa": {
+				Sharded: true,
+				Vindexes: map[string]*vschemapb.Vindex{
+					"multicol_vdx": {
+						Type: "mcfu",
+					},
+				},
+				Tables: map[string]*vschemapb.Table{
+					"multiColTbl": {
+						ColumnVindexes: []*vschemapb.ColumnVindex{
+							{
+								Columns: []string{"cola", "colb", "colc"},
+								Name:    "multicol_vdx",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	vschema := BuildVSchema(&input)
+	table, err := vschema.FindTable("ksa", "multiColTbl")
+	require.NoError(t, err)
+	require.Len(t, table.ColumnVindexes, 1)
+	require.True(t, table.ColumnVindexes[0].IsUnique())
+	require.EqualValues(t, 1, table.ColumnVindexes[0].Cost())
+}