Repository: vitessio/vitess
Pull request #10490 (2022-06-10 10:13:04)

Extract vindex lookup queries into their own primitive
## Description
This PR changes how we plan vindex lookup queries. Before this change, the vindex lookup query was done by the vindex at runtime. This means that at plan time, we still haven't planned the lookup query and can't show it in plan descriptions.

With this change, vindex lookups are performed by a new engine primitive, the `VindexLookup`. It looks like a join - it has two inputs, and fetches data from the left-hand side query to figure out where to send the right-hand side query.

This makes the lookup query explicit in the plan, helping see the full scope of a query. This can be important to optimize queries as well as possible.

As an example, here is the plan for `select id from user where name = 'bb'`:
```json
{
  "OperatorType": "VindexLookup",
  "Variant": "Equal",
  "Keyspace": {
    "Name": "user",
    "Sharded": true
  },
  "Values": [
    "VARCHAR(\"bb\")"
  ],
  "Vindex": "name_user_map",
  "Inputs": [
    {
      "OperatorType": "Route",
      "Variant": "EqualUnique",
      "Keyspace": {
        "Name": "user",
        "Sharded": true
      },
      "FieldQuery": "select name, id from user_name_lookup where 1 != 1",
      "Query": "select name, id from user_name_lookup where name in ::col1",
      "Table": "`user`"
    },
    {
      "OperatorType": "Route",
      "Variant": "ByDestination",
      "Keyspace": {
        "Name": "user",
        "Sharded": true
      },
      "FieldQuery": "select id from `user` where 1 != 1",
      "Query": "select id from `user` where `name` = 'bb'",
      "Table": "`user`"
    }
  ]
}
```

In the plan above, you can see the first route, which is the query used to fetch data from the lookup vindex,

 ## Left todo:
- [x] Streaming implementation
- [x] Work on plan_test to make more sense. Lookup queries should not scatter
- [x] Add e2e with dirty transaction state - inserts, deletes and updates
- [x] Find more corner cases and failure cases
- [x] Implement PlanableVindex for more vindexes

## Checklist
-   [x] "Backport me!" label has been added if this change should be backported
-   [x] Tests were added or are not required
-   [x] Documentation was added or is not required

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

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

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

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

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

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

systay: LGTM. I'll let someone else ACK it though
frouioui: Approving this PR as @systay has already ack this

Patch:
diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go
index 1e8cc0364d..796c1dcb55 100644
--- a/go/vt/vtgate/engine/primitive.go
+++ b/go/vt/vtgate/engine/primitive.go
@@ -170,8 +170,16 @@ type (
 		// RemoveAdvisoryLock removes advisory lock from the session
 		RemoveAdvisoryLock(name string)
 
+		// VtExplainLogging enables logging of all interactions to the tablets so
+		// EXPLAIN `format=vtexplain` can report what's being done
 		VtExplainLogging()
+
+		// GetVTExplainLogs retrieves the vttablet interaction logs
 		GetVTExplainLogs() []ExecuteEntry
+
+		// SetCommitOrder sets the commit order for the shard session in respect of the type of vindex lookup.
+		// This is used to select the right shard session to perform the vindex lookup query.
+		SetCommitOrder(co vtgatepb.CommitOrder)
 	}
 
 	// Match is used to check if a Primitive matches
diff --git a/go/vt/vtgate/engine/route.go b/go/vt/vtgate/engine/route.go
index faf190dcfa..d233b7f640 100644
--- a/go/vt/vtgate/engine/route.go
+++ b/go/vt/vtgate/engine/route.go
@@ -191,12 +191,28 @@ const (
 	IgnoreReserveTxn cxtKey = iota
 )
 
-func (route *Route) executeInternal(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error) {
+func (route *Route) executeInternal(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+) (*sqltypes.Result, error) {
 	rss, bvs, err := route.findRoute(ctx, vcursor, bindVars)
 	if err != nil {
 		return nil, err
 	}
 
+	return route.executeShards(ctx, vcursor, bindVars, wantfields, rss, bvs)
+}
+
+func (route *Route) executeShards(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	rss []*srvtopo.ResolvedShard,
+	bvs []map[string]*querypb.BindVariable,
+) (*sqltypes.Result, error) {
 	// Select Next - sequence query does not need to be executed in a dedicated connection (reserved or transaction)
 	if route.Opcode == Next {
 		ctx = context.WithValue(ctx, IgnoreReserveTxn, true)
@@ -218,6 +234,7 @@ func (route *Route) executeInternal(ctx context.Context, vcursor VCursor, bindVa
 		// and the ones that don't. So, we are sending the query to any shard! This is safe because
 		// the query contains a predicate that make it not match any rows on that shard. (If they did,
 		// we should have gotten that shard back already from findRoute)
+		var err error
 		rss, bvs, err = route.anyShard(ctx, vcursor, bindVars)
 		if err != nil {
 			return nil, err
@@ -259,7 +276,13 @@ func filterOutNilErrors(errs []error) []error {
 }
 
 // TryStreamExecute performs a streaming exec.
-func (route *Route) TryStreamExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
+func (route *Route) TryStreamExecute(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	callback func(*sqltypes.Result) error,
+) error {
 	if route.QueryTimeout != 0 {
 		var cancel context.CancelFunc
 		ctx, cancel = context.WithTimeout(ctx, time.Duration(route.QueryTimeout)*time.Millisecond)
@@ -270,6 +293,18 @@ func (route *Route) TryStreamExecute(ctx context.Context, vcursor VCursor, bindV
 		return err
 	}
 
+	return route.streamExecuteShards(ctx, vcursor, bindVars, wantfields, callback, rss, bvs)
+}
+
+func (route *Route) streamExecuteShards(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	callback func(*sqltypes.Result) error,
+	rss []*srvtopo.ResolvedShard,
+	bvs []map[string]*querypb.BindVariable,
+) error {
 	// No route.
 	if len(rss) == 0 {
 		if !route.NoRoutesSpecialHandling {
@@ -290,6 +325,7 @@ func (route *Route) TryStreamExecute(ctx context.Context, vcursor VCursor, bindV
 		// and the ones that don't. So, we are sending the query to any shard! This is safe because
 		// the query contains a predicate that make it not match any rows on that shard. (If they did,
 		// we should have gotten that shard back already from findRoute)
+		var err error
 		rss, bvs, err = route.anyShard(ctx, vcursor, bindVars)
 		if err != nil {
 			return err
@@ -317,7 +353,15 @@ func (route *Route) TryStreamExecute(ctx context.Context, vcursor VCursor, bindV
 	return route.mergeSort(ctx, vcursor, bindVars, wantfields, callback, rss, bvs)
 }
 
-func (route *Route) mergeSort(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error, rss []*srvtopo.ResolvedShard, bvs []map[string]*querypb.BindVariable) error {
+func (route *Route) mergeSort(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	callback func(*sqltypes.Result) error,
+	rss []*srvtopo.ResolvedShard,
+	bvs []map[string]*querypb.BindVariable,
+) error {
 	prims := make([]StreamExecutor, 0, len(rss))
 	for i, rs := range rss {
 		prims = append(prims, &shardRoute{
@@ -450,7 +494,61 @@ func (route *Route) description() PrimitiveDescription {
 	}
 }
 
-func execShard(ctx context.Context, vcursor VCursor, query string, bindVars map[string]*querypb.BindVariable, rs *srvtopo.ResolvedShard, rollbackOnError, canAutocommit bool) (*sqltypes.Result, error) {
+func (route *Route) executeAfterLookup(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	ids []sqltypes.Value,
+	dest []key.Destination,
+) (*sqltypes.Result, error) {
+	protoIds := make([]*querypb.Value, 0, len(ids))
+	for _, id := range ids {
+		protoIds = append(protoIds, sqltypes.ValueToProto(id))
+	}
+	rss, _, err := vcursor.ResolveDestinations(ctx, route.Keyspace.Name, protoIds, dest)
+	if err != nil {
+		return nil, err
+	}
+	bvs := make([]map[string]*querypb.BindVariable, len(rss))
+	for i := range bvs {
+		bvs[i] = bindVars
+	}
+	return route.executeShards(ctx, vcursor, bindVars, wantfields, rss, bvs)
+}
+
+func (route *Route) streamExecuteAfterLookup(
+	ctx context.Context,
+	vcursor VCursor,
+	bindVars map[string]*querypb.BindVariable,
+	wantfields bool,
+	callback func(*sqltypes.Result) error,
+	ids []sqltypes.Value,
+	dest []key.Destination,
+) error {
+	protoIds := make([]*querypb.Value, 0, len(ids))
+	for _, id := range ids {
+		protoIds = append(protoIds, sqltypes.ValueToProto(id))
+	}
+	rss, _, err := vcursor.ResolveDestinations(ctx, route.Keyspace.Name, protoIds, dest)
+	if err != nil {
+		return err
+	}
+	bvs := make([]map[string]*querypb.BindVariable, len(rss))
+	for i := range bvs {
+		bvs[i] = bindVars
+	}
+	return route.streamExecuteShards(ctx, vcursor, bindVars, wantfields, callback, rss, bvs)
+}
+
+func execShard(
+	ctx context.Context,
+	vcursor VCursor,
+	query string,
+	bindVars map[string]*querypb.BindVariable,
+	rs *srvtopo.ResolvedShard,
+	rollbackOnError, canAutocommit bool,
+) (*sqltypes.Result, error) {
 	autocommit := canAutocommit && vcursor.AutocommitApproval()
 	result, errs := vcursor.ExecuteMultiShard(ctx, []*srvtopo.ResolvedShard{rs}, []*querypb.BoundQuery{
 		{
diff --git a/go/vt/vtgate/engine/vindex_lookup.go b/go/vt/vtgate/engine/vindex_lookup.go
new file mode 100644
index 0000000000..8883f138dc
--- /dev/null
+++ b/go/vt/vtgate/engine/vindex_lookup.go
@@ -0,0 +1,245 @@
+/*
+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 engine
+
+import (
+	"context"
+
+	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
+
+	"vitess.io/vitess/go/vt/key"
+
+	"vitess.io/vitess/go/sqltypes"
+	querypb "vitess.io/vitess/go/vt/proto/query"
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/evalengine"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+)
+
+var _ Primitive = (*VindexLookup)(nil)
+
+type VindexLookup struct {
+	Opcode Opcode
+
+	// The vindex to use to do the Map
+	Vindex vindexes.LookupPlanable
+
+	// Keyspace specifies the keyspace to send the query to.
+	Keyspace *vindexes.Keyspace
+
+	Arguments []string
+
+	// Values specifies the vindex values to use for routing.
+	Values []evalengine.Expr
+
+	// We fetch data in order to do the map from this primitive
+	Lookup Primitive
+
+	// This is the side that needs to be routed
+	SendTo *Route
+}
+
+// RouteType implements the Primitive interface
+func (vr *VindexLookup) RouteType() string {
+	return "VindexLookup"
+}
+
+// GetKeyspaceName implements the Primitive interface
+func (vr *VindexLookup) GetKeyspaceName() string {
+	return vr.SendTo.GetKeyspaceName()
+}
+
+// GetTableName implements the Primitive interface
+func (vr *VindexLookup) GetTableName() string {
+	return vr.SendTo.GetTableName()
+}
+
+// GetFields implements the Primitive interface
+func (vr *VindexLookup) GetFields(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
+	return vr.SendTo.GetFields(ctx, vcursor, bindVars)
+}
+
+// NeedsTransaction implements the Primitive interface
+func (vr *VindexLookup) NeedsTransaction() bool {
+	return vr.SendTo.NeedsTransaction()
+}
+
+// TryExecute implements the Primitive interface
+func (vr *VindexLookup) TryExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool) (*sqltypes.Result, error) {
+	ids, err := vr.generateIds(vcursor, bindVars)
+	if err != nil {
+		return nil, err
+	}
+	results, err := vr.lookup(ctx, vcursor, ids)
+	if err != nil {
+		return nil, err
+	}
+
+	dest, err := vr.mapVindexToDestination(ids, results, bindVars)
+	if err != nil {
+		return nil, err
+	}
+
+	return vr.SendTo.executeAfterLookup(ctx, vcursor, bindVars, wantfields, ids, dest)
+}
+
+func (vr *VindexLookup) mapVindexToDestination(ids []sqltypes.Value, results []*sqltypes.Result, bindVars map[string]*querypb.BindVariable) ([]key.Destination, error) {
+	dest, err := vr.Vindex.MapResult(ids, results)
+	if err != nil {
+		return nil, err
+	}
+
+	if vr.Opcode == IN {
+		valsBV := &querypb.BindVariable{Type: querypb.Type_TUPLE}
+		valsBV.Values = make([]*querypb.Value, 0, len(ids))
+		for _, value := range ids {
+			valsBV.Values = append(valsBV.Values, sqltypes.ValueToProto(value))
+		}
+		bindVars[ListVarName] = valsBV
+	}
+	return dest, nil
+}
+
+// TryStreamExecute implements the Primitive interface
+func (vr *VindexLookup) TryStreamExecute(ctx context.Context, vcursor VCursor, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
+	ids, err := vr.generateIds(vcursor, bindVars)
+	if err != nil {
+		return err
+	}
+
+	results, err := vr.lookup(ctx, vcursor, ids)
+	if err != nil {
+		return err
+	}
+
+	dest, err := vr.mapVindexToDestination(ids, results, bindVars)
+	if err != nil {
+		return err
+	}
+
+	return vr.SendTo.streamExecuteAfterLookup(ctx, vcursor, bindVars, wantfields, callback, ids, dest)
+}
+
+// Inputs implements the Primitive interface
+func (vr *VindexLookup) Inputs() []Primitive {
+	if vr.Lookup != nil {
+		return []Primitive{vr.Lookup, vr.SendTo}
+	}
+
+	return []Primitive{vr.SendTo}
+}
+
+// description implements the Primitive interface
+func (vr *VindexLookup) description() PrimitiveDescription {
+	other := map[string]any{}
+	if vr.Values != nil {
+		formattedValues := make([]string, 0, len(vr.Values))
+		for _, value := range vr.Values {
+			formattedValues = append(formattedValues, evalengine.FormatExpr(value))
+		}
+		other["Values"] = formattedValues
+	}
+	other["Vindex"] = vr.Vindex.String()
+
+	return PrimitiveDescription{
+		OperatorType: "VindexLookup",
+		Variant:      vr.Opcode.String(),
+		Keyspace:     vr.Keyspace,
+		Other:        other,
+	}
+}
+
+func (vr *VindexLookup) lookup(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]*sqltypes.Result, error) {
+	co := vr.Vindex.GetCommitOrder()
+	if co != vtgatepb.CommitOrder_NORMAL {
+		vcursor.Session().SetCommitOrder(co)
+		defer vcursor.Session().SetCommitOrder(vtgatepb.CommitOrder_NORMAL)
+	}
+	if ids[0].IsIntegral() || vr.Vindex.AllowBatch() {
+		return vr.executeBatch(ctx, vcursor, ids)
+	}
+	return vr.executeNonBatch(ctx, vcursor, ids)
+}
+
+func (vr *VindexLookup) executeNonBatch(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]*sqltypes.Result, error) {
+	results := make([]*sqltypes.Result, 0, len(ids))
+	// for non integral and binary type, fallback to send query per id
+	for _, id := range ids {
+		vars, err := sqltypes.BuildBindVariable([]any{id})
+		if err != nil {
+			return nil, err
+		}
+		bindVars := map[string]*querypb.BindVariable{
+			vr.Arguments[0]: vars,
+		}
+		result, err := vcursor.ExecutePrimitive(ctx, vr.Lookup, bindVars, false)
+		if err != nil {
+			return nil, err
+		}
+		rows := make([][]sqltypes.Value, 0, len(result.Rows))
+		for _, row := range result.Rows {
+			rows = append(rows, []sqltypes.Value{row[1]})
+		}
+		results = append(results, &sqltypes.Result{
+			Rows: rows,
+		})
+	}
+	return results, nil
+}
+
+func (vr *VindexLookup) executeBatch(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]*sqltypes.Result, error) {
+	results := make([]*sqltypes.Result, 0, len(ids))
+	// for integral types, batch query all ids and then map them back to the input order
+	vars, err := sqltypes.BuildBindVariable(ids)
+	if err != nil {
+		return nil, err
+	}
+	bindVars := map[string]*querypb.BindVariable{
+		vr.Arguments[0]: vars,
+	}
+	result, err := vcursor.ExecutePrimitive(ctx, vr.Lookup, bindVars, false)
+	if err != nil {
+		return nil, vterrors.Wrapf(err, "failed while running the lookup query")
+	}
+	resultMap := make(map[string][][]sqltypes.Value)
+	for _, row := range result.Rows {
+		resultMap[row[0].ToString()] = append(resultMap[row[0].ToString()], []sqltypes.Value{row[1]})
+	}
+
+	for _, id := range ids {
+		results = append(results, &sqltypes.Result{
+			Rows: resultMap[id.ToString()],
+		})
+	}
+	return results, nil
+}
+
+func (vr *VindexLookup) generateIds(vcursor VCursor, bindVars map[string]*querypb.BindVariable) ([]sqltypes.Value, error) {
+	env := evalengine.EnvWithBindVars(bindVars, vcursor.ConnCollation())
+	value, err := env.Evaluate(vr.Values[0])
+	if err != nil {
+		return nil, err
+	}
+	switch vr.Opcode {
+	case Equal, EqualUnique:
+		return []sqltypes.Value{value.Value()}, nil
+	case IN:
+		return value.TupleValues(), nil
+	}
+	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "opcode %s not supported for VindexLookup", vr.Opcode.String())
+}
diff --git a/go/vt/vtgate/planbuilder/concatenateGen4.go b/go/vt/vtgate/planbuilder/concatenateGen4.go
index 8c14b1ba0f..152e415096 100644
--- a/go/vt/vtgate/planbuilder/concatenateGen4.go
+++ b/go/vt/vtgate/planbuilder/concatenateGen4.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -55,9 +56,9 @@ func (c *concatenateGen4) Wireup(plan logicalPlan, jt *jointab) error {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (c *concatenateGen4) WireupGen4(semTable *semantics.SemTable) error {
+func (c *concatenateGen4) WireupGen4(ctx *plancontext.PlanningContext) error {
 	for _, source := range c.sources {
-		err := source.WireupGen4(semTable)
+		err := source.WireupGen4(ctx)
 		if err != nil {
 			return err
 		}
diff --git a/go/vt/vtgate/planbuilder/gen4_planner.go b/go/vt/vtgate/planbuilder/gen4_planner.go
index 4f6ff20f41..deba9b87e7 100644
--- a/go/vt/vtgate/planbuilder/gen4_planner.go
+++ b/go/vt/vtgate/planbuilder/gen4_planner.go
@@ -105,16 +105,22 @@ func gen4SelectStmtPlanner(
 	}
 
 	primitive := plan.Primitive()
-	if rb, ok := primitive.(*engine.Route); ok && isSel {
-		// this is done because engine.Route doesn't handle the empty result well
-		// if it doesn't find a shard to send the query to.
-		// All other engine primitives can handle this, so we only need it when
-		// Route is the last (and only) instruction before the user sees a result
-		if isOnlyDual(sel) || (len(sel.GroupBy) == 0 && sel.SelectExprs.AllAggregation()) {
-			rb.NoRoutesSpecialHandling = true
+	if !isSel {
+		return newPlanResult(primitive, tablesFromSemantics(st)...), nil
+	}
+
+	// this is done because engine.Route doesn't handle the empty result well
+	// if it doesn't find a shard to send the query to.
+	// All other engine primitives can handle this, so we only need it when
+	// Route is the last (and only) instruction before the user sees a result
+	if isOnlyDual(sel) || (len(sel.GroupBy) == 0 && sel.SelectExprs.AllAggregation()) {
+		switch prim := primitive.(type) {
+		case *engine.Route:
+			prim.NoRoutesSpecialHandling = true
+		case *engine.VindexLookup:
+			prim.SendTo.NoRoutesSpecialHandling = true
 		}
 	}
-
 	return newPlanResult(primitive, tablesFromSemantics(st)...), nil
 }
 
@@ -134,10 +140,6 @@ func gen4planSQLCalcFoundRows(vschema plancontext.VSchema, sel *sqlparser.Select
 	if err != nil {
 		return nil, err
 	}
-	err = plan.WireupGen4(semTable)
-	if err != nil {
-		return nil, err
-	}
 	return newPlanResult(plan.Primitive(), tablesFromSemantics(semTable)...), nil
 }
 
@@ -180,8 +182,10 @@ func newBuildSelectPlan(
 	// record any warning as planner warning.
 	vschema.PlannerWarning(semTable.Warning)
 
+	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema, version)
+
 	if ks, _ := semTable.SingleUnshardedKeyspace(); ks != nil {
-		plan, err := unshardedShortcut(selStmt, ks, semTable)
+		plan, err := unshardedShortcut(ctx, selStmt, ks)
 		return plan, semTable, err
 	}
 
@@ -195,7 +199,6 @@ func newBuildSelectPlan(
 		return nil, nil, err
 	}
 
-	ctx := plancontext.NewPlanningContext(reservedVars, semTable, vschema, version)
 	logical, err := abstract.CreateLogicalOperatorFromAST(selStmt, semTable)
 	if err != nil {
 		return nil, nil, err
@@ -227,7 +230,7 @@ func newBuildSelectPlan(
 		}
 	}
 
-	if err := plan.WireupGen4(semTable); err != nil {
+	if err := plan.WireupGen4(ctx); err != nil {
 		return nil, nil, err
 	}
 
@@ -312,7 +315,7 @@ func gen4UpdateStmtPlanner(
 
 	setLockOnAllSelect(plan)
 
-	if err := plan.WireupGen4(semTable); err != nil {
+	if err := plan.WireupGen4(ctx); err != nil {
 		return nil, err
 	}
 
diff --git a/go/vt/vtgate/planbuilder/hash_join.go b/go/vt/vtgate/planbuilder/hash_join.go
index a0bf8d607b..26322d5e48 100644
--- a/go/vt/vtgate/planbuilder/hash_join.go
+++ b/go/vt/vtgate/planbuilder/hash_join.go
@@ -23,6 +23,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -49,12 +50,12 @@ type hashJoin struct {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (hj *hashJoin) WireupGen4(semTable *semantics.SemTable) error {
-	err := hj.Left.WireupGen4(semTable)
+func (hj *hashJoin) WireupGen4(ctx *plancontext.PlanningContext) error {
+	err := hj.Left.WireupGen4(ctx)
 	if err != nil {
 		return err
 	}
-	return hj.Right.WireupGen4(semTable)
+	return hj.Right.WireupGen4(ctx)
 }
 
 // Primitive implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/joinGen4.go b/go/vt/vtgate/planbuilder/joinGen4.go
index efcfacd3dd..a038f40109 100644
--- a/go/vt/vtgate/planbuilder/joinGen4.go
+++ b/go/vt/vtgate/planbuilder/joinGen4.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -51,12 +52,12 @@ type joinGen4 struct {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (j *joinGen4) WireupGen4(semTable *semantics.SemTable) error {
-	err := j.Left.WireupGen4(semTable)
+func (j *joinGen4) WireupGen4(ctx *plancontext.PlanningContext) error {
+	err := j.Left.WireupGen4(ctx)
 	if err != nil {
 		return err
 	}
-	return j.Right.WireupGen4(semTable)
+	return j.Right.WireupGen4(ctx)
 }
 
 // Primitive implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/logical_plan.go b/go/vt/vtgate/planbuilder/logical_plan.go
index 6ccc34701a..b74ff5b35d 100644
--- a/go/vt/vtgate/planbuilder/logical_plan.go
+++ b/go/vt/vtgate/planbuilder/logical_plan.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -50,7 +51,7 @@ type logicalPlan interface {
 	Wireup(lp logicalPlan, jt *jointab) error
 
 	// WireupGen4 does the wire up work for the Gen4 planner
-	WireupGen4(semTable *semantics.SemTable) error
+	WireupGen4(*plancontext.PlanningContext) error
 
 	// SupplyVar finds the common root between from and to. If it's
 	// the common root, it supplies the requested var to the rhs tree.
@@ -131,7 +132,7 @@ func (*gen4Plan) SupplyWeightString(int, bool) (weightcolNumber int, err error)
 // v3Plan implements methods that are only used by gen4
 type v3Plan struct{}
 
-func (*v3Plan) WireupGen4(*semantics.SemTable) error {
+func (*v3Plan) WireupGen4(*plancontext.PlanningContext) error {
 	panic("[BUG]: should not be called. This is a V3 primitive")
 }
 
@@ -218,8 +219,8 @@ func (bc *logicalPlanCommon) Wireup(plan logicalPlan, jt *jointab) error {
 	return bc.input.Wireup(plan, jt)
 }
 
-func (bc *logicalPlanCommon) WireupGen4(semTable *semantics.SemTable) error {
-	return bc.input.WireupGen4(semTable)
+func (bc *logicalPlanCommon) WireupGen4(ctx *plancontext.PlanningContext) error {
+	return bc.input.WireupGen4(ctx)
 }
 
 func (bc *logicalPlanCommon) SupplyVar(from, to int, col *sqlparser.ColName, varname string) {
diff --git a/go/vt/vtgate/planbuilder/memory_sort.go b/go/vt/vtgate/planbuilder/memory_sort.go
index 89be7ee4b1..9d1b04bba1 100644
--- a/go/vt/vtgate/planbuilder/memory_sort.go
+++ b/go/vt/vtgate/planbuilder/memory_sort.go
@@ -20,9 +20,9 @@ import (
 	"errors"
 	"fmt"
 
-	"vitess.io/vitess/go/mysql/collations"
-	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 
+	"vitess.io/vitess/go/mysql/collations"
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
@@ -138,6 +138,6 @@ func (ms *memorySort) Wireup(plan logicalPlan, jt *jointab) error {
 	return ms.input.Wireup(plan, jt)
 }
 
-func (ms *memorySort) WireupGen4(semTable *semantics.SemTable) error {
-	return ms.input.WireupGen4(semTable)
+func (ms *memorySort) WireupGen4(ctx *plancontext.PlanningContext) error {
+	return ms.input.WireupGen4(ctx)
 }
diff --git a/go/vt/vtgate/planbuilder/merge_sort.go b/go/vt/vtgate/planbuilder/merge_sort.go
index ee94cdd422..4e72d06224 100644
--- a/go/vt/vtgate/planbuilder/merge_sort.go
+++ b/go/vt/vtgate/planbuilder/merge_sort.go
@@ -20,7 +20,7 @@ import (
 	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
-	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 )
 
 var _ logicalPlan = (*mergeSort)(nil)
@@ -85,8 +85,8 @@ func (ms *mergeSort) Wireup(plan logicalPlan, jt *jointab) error {
 	return ms.input.Wireup(plan, jt)
 }
 
-func (ms *mergeSort) WireupGen4(semTable *semantics.SemTable) error {
-	return ms.input.WireupGen4(semTable)
+func (ms *mergeSort) WireupGen4(ctx *plancontext.PlanningContext) error {
+	return ms.input.WireupGen4(ctx)
 }
 
 // OutputColumns implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/ordered_aggregate.go b/go/vt/vtgate/planbuilder/ordered_aggregate.go
index ad952881dc..763aa31362 100644
--- a/go/vt/vtgate/planbuilder/ordered_aggregate.go
+++ b/go/vt/vtgate/planbuilder/ordered_aggregate.go
@@ -21,6 +21,8 @@ import (
 	"strconv"
 	"strings"
 
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+
 	"vitess.io/vitess/go/mysql/collations"
 
 	"vitess.io/vitess/go/sqltypes"
@@ -28,8 +30,6 @@ import (
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/vterrors"
 
-	"vitess.io/vitess/go/vt/vtgate/semantics"
-
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
@@ -394,8 +394,8 @@ func (oa *orderedAggregate) Wireup(plan logicalPlan, jt *jointab) error {
 	return oa.input.Wireup(plan, jt)
 }
 
-func (oa *orderedAggregate) WireupGen4(semTable *semantics.SemTable) error {
-	return oa.input.WireupGen4(semTable)
+func (oa *orderedAggregate) WireupGen4(ctx *plancontext.PlanningContext) error {
+	return oa.input.WireupGen4(ctx)
 }
 
 // OutputColumns implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/primitive_wrapper.go b/go/vt/vtgate/planbuilder/primitive_wrapper.go
index 0fac2f98f1..46c54b140a 100644
--- a/go/vt/vtgate/planbuilder/primitive_wrapper.go
+++ b/go/vt/vtgate/planbuilder/primitive_wrapper.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -30,7 +31,7 @@ type primitiveWrapper struct {
 	gen4Plan
 }
 
-func (p *primitiveWrapper) WireupGen4(*semantics.SemTable) error {
+func (p *primitiveWrapper) WireupGen4(*plancontext.PlanningContext) error {
 	return nil
 }
 
diff --git a/go/vt/vtgate/planbuilder/projection.go b/go/vt/vtgate/planbuilder/projection.go
index 88311a4946..ad051b8f53 100644
--- a/go/vt/vtgate/planbuilder/projection.go
+++ b/go/vt/vtgate/planbuilder/projection.go
@@ -22,6 +22,7 @@ import (
 	"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/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -39,10 +40,10 @@ type projection struct {
 var _ logicalPlan = (*projection)(nil)
 
 // WireupGen4 implements the logicalPlan interface
-func (p *projection) WireupGen4(semTable *semantics.SemTable) error {
+func (p *projection) WireupGen4(ctx *plancontext.PlanningContext) error {
 	columns := make([]evalengine.Expr, 0, len(p.columns))
 	for _, expr := range p.columns {
-		convert, err := evalengine.Translate(expr, semTable)
+		convert, err := evalengine.Translate(expr, ctx.SemTable)
 		if err != nil {
 			return err
 		}
@@ -53,7 +54,7 @@ func (p *projection) WireupGen4(semTable *semantics.SemTable) error {
 		Exprs: columns,
 	}
 
-	return p.source.WireupGen4(semTable)
+	return p.source.WireupGen4(ctx)
 }
 
 // Inputs implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/pullout_subquery.go b/go/vt/vtgate/planbuilder/pullout_subquery.go
index 7cf0a5985c..dd33bb4a30 100644
--- a/go/vt/vtgate/planbuilder/pullout_subquery.go
+++ b/go/vt/vtgate/planbuilder/pullout_subquery.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -88,11 +89,11 @@ func (ps *pulloutSubquery) Wireup(plan logicalPlan, jt *jointab) error {
 }
 
 // Wireup2 implements the logicalPlan interface
-func (ps *pulloutSubquery) WireupGen4(semTable *semantics.SemTable) error {
-	if err := ps.underlying.WireupGen4(semTable); err != nil {
+func (ps *pulloutSubquery) WireupGen4(ctx *plancontext.PlanningContext) error {
+	if err := ps.underlying.WireupGen4(ctx); err != nil {
 		return err
 	}
-	return ps.subquery.WireupGen4(semTable)
+	return ps.subquery.WireupGen4(ctx)
 }
 
 // SupplyVar implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/routeGen4.go b/go/vt/vtgate/planbuilder/routeGen4.go
index 659c523c13..c4d60944b3 100644
--- a/go/vt/vtgate/planbuilder/routeGen4.go
+++ b/go/vt/vtgate/planbuilder/routeGen4.go
@@ -17,8 +17,10 @@ limitations under the License.
 package planbuilder
 
 import (
+	querypb "vitess.io/vitess/go/vt/proto/query"
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 
 	"vitess.io/vitess/go/vt/sqlparser"
@@ -47,13 +49,16 @@ type routeGen4 struct {
 	// eroute is the primitive being built.
 	eroute *engine.Route
 
+	// is the engine primitive we will return from the Primitive() method. Note that it could be different than eroute
+	enginePrimitive engine.Primitive
+
 	// tables keeps track of which tables this route is covering
 	tables semantics.TableSet
 }
 
 // Primitive implements the logicalPlan interface
 func (rb *routeGen4) Primitive() engine.Primitive {
-	return rb.eroute
+	return rb.enginePrimitive
 }
 
 // SetLimit adds a LIMIT clause to the route.
@@ -62,15 +67,49 @@ func (rb *routeGen4) SetLimit(limit *sqlparser.Limit) {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (rb *routeGen4) WireupGen4(_ *semantics.SemTable) error {
+func (rb *routeGen4) WireupGen4(ctx *plancontext.PlanningContext) error {
 	rb.prepareTheAST()
 
+	// prepare the queries we will pass down
 	rb.eroute.Query = sqlparser.String(rb.Select)
-
 	buffer := sqlparser.NewTrackedBuffer(sqlparser.FormatImpossibleQuery)
 	node := buffer.WriteNode(rb.Select)
-	query := node.ParsedQuery()
-	rb.eroute.FieldQuery = query.Query
+	parsedQuery := node.ParsedQuery()
+	rb.eroute.FieldQuery = parsedQuery.Query
+
+	// if we have a planable vindex lookup, let's extract it into its own primitive
+	planableVindex, ok := rb.eroute.RoutingParameters.Vindex.(vindexes.LookupPlanable)
+	if !ok {
+		rb.enginePrimitive = rb.eroute
+		return nil
+	}
+
+	query, args := planableVindex.Query()
+	stmt, reserved, err := sqlparser.Parse2(query)
+	if err != nil {
+		return err
+	}
+	reservedVars := sqlparser.NewReservedVars("vtg", reserved)
+
+	lookupPrimitive, err := gen4SelectStmtPlanner(query, querypb.ExecuteOptions_Gen4, stmt.(sqlparser.SelectStatement), reservedVars, ctx.VSchema)
+	if err != nil {
+		return vterrors.Wrapf(err, "failed to plan the lookup query: [%s]", query)
+	}
+
+	rb.enginePrimitive = &engine.VindexLookup{
+		Opcode:    rb.eroute.Opcode,
+		Vindex:    planableVindex,
+		Keyspace:  rb.eroute.Keyspace,
+		Values:    rb.eroute.Values,
+		SendTo:    rb.eroute,
+		Arguments: args,
+		Lookup:    lookupPrimitive.primitive,
+	}
+
+	rb.eroute.RoutingParameters.Opcode = engine.ByDestination
+	rb.eroute.RoutingParameters.Values = nil
+	rb.eroute.RoutingParameters.Vindex = nil
+
 	return nil
 }
 
diff --git a/go/vt/vtgate/planbuilder/semi_join.go b/go/vt/vtgate/planbuilder/semi_join.go
index 1b2d6c1807..0204a0f991 100644
--- a/go/vt/vtgate/planbuilder/semi_join.go
+++ b/go/vt/vtgate/planbuilder/semi_join.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -63,11 +64,11 @@ func (ps *semiJoin) Primitive() engine.Primitive {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (ps *semiJoin) WireupGen4(semTable *semantics.SemTable) error {
-	if err := ps.lhs.WireupGen4(semTable); err != nil {
+func (ps *semiJoin) WireupGen4(ctx *plancontext.PlanningContext) error {
+	if err := ps.lhs.WireupGen4(ctx); err != nil {
 		return err
 	}
-	return ps.rhs.WireupGen4(semTable)
+	return ps.rhs.WireupGen4(ctx)
 }
 
 // Rewrite implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/single_sharded_shortcut.go b/go/vt/vtgate/planbuilder/single_sharded_shortcut.go
index 7abc337065..868f96cee8 100644
--- a/go/vt/vtgate/planbuilder/single_sharded_shortcut.go
+++ b/go/vt/vtgate/planbuilder/single_sharded_shortcut.go
@@ -20,13 +20,15 @@ import (
 	"sort"
 	"strings"
 
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vtgate/engine"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 	"vitess.io/vitess/go/vt/vtgate/vindexes"
 )
 
-func unshardedShortcut(stmt sqlparser.SelectStatement, ks *vindexes.Keyspace, semTable *semantics.SemTable) (logicalPlan, error) {
+func unshardedShortcut(ctx *plancontext.PlanningContext, stmt sqlparser.SelectStatement, ks *vindexes.Keyspace) (logicalPlan, error) {
 	// this method is used when the query we are handling has all tables in the same unsharded keyspace
 	sqlparser.Rewrite(stmt, func(cursor *sqlparser.Cursor) bool {
 		switch node := cursor.Node().(type) {
@@ -40,7 +42,7 @@ func unshardedShortcut(stmt sqlparser.SelectStatement, ks *vindexes.Keyspace, se
 		return true
 	}, nil)
 
-	tableNames, err := getTableNames(semTable)
+	tableNames, err := getTableNames(ctx.SemTable)
 	if err != nil {
 		return nil, err
 	}
@@ -54,7 +56,8 @@ func unshardedShortcut(stmt sqlparser.SelectStatement, ks *vindexes.Keyspace, se
 		},
 		Select: stmt,
 	}
-	if err := plan.WireupGen4(semTable); err != nil {
+
+	if err := plan.WireupGen4(ctx); err != nil {
 		return nil, err
 	}
 	return plan, nil
diff --git a/go/vt/vtgate/planbuilder/sql_calc_found_rows.go b/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
index 7d7cdc87d0..41dcd1df45 100644
--- a/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
+++ b/go/vt/vtgate/planbuilder/sql_calc_found_rows.go
@@ -21,6 +21,7 @@ import (
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 )
 
@@ -43,12 +44,12 @@ func (s *sqlCalcFoundRows) Wireup(logicalPlan, *jointab) error {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (s *sqlCalcFoundRows) WireupGen4(semTable *semantics.SemTable) error {
-	err := s.LimitQuery.WireupGen4(semTable)
+func (s *sqlCalcFoundRows) WireupGen4(ctx *plancontext.PlanningContext) error {
+	err := s.LimitQuery.WireupGen4(ctx)
 	if err != nil {
 		return err
 	}
-	return s.CountQuery.WireupGen4(semTable)
+	return s.CountQuery.WireupGen4(ctx)
 }
 
 // ContainsTables implements the logicalPlan interface
diff --git a/go/vt/vtgate/planbuilder/vindex_func.go b/go/vt/vtgate/planbuilder/vindex_func.go
index 07d3db95b4..8f9a423b94 100644
--- a/go/vt/vtgate/planbuilder/vindex_func.go
+++ b/go/vt/vtgate/planbuilder/vindex_func.go
@@ -19,6 +19,8 @@ package planbuilder
 import (
 	"fmt"
 
+	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
+
 	"vitess.io/vitess/go/vt/vtgate/semantics"
 
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
@@ -107,7 +109,7 @@ func (vf *vindexFunc) Wireup(logicalPlan, *jointab) error {
 }
 
 // WireupGen4 implements the logicalPlan interface
-func (vf *vindexFunc) WireupGen4(*semantics.SemTable) error {
+func (vf *vindexFunc) WireupGen4(*plancontext.PlanningContext) error {
 	return nil
 }
 
diff --git a/go/vt/vtgate/vcursor_impl.go b/go/vt/vtgate/vcursor_impl.go
index 62c4486986..6b0769b3de 100644
--- a/go/vt/vtgate/vcursor_impl.go
+++ b/go/vt/vtgate/vcursor_impl.go
@@ -801,6 +801,10 @@ func (vc *vcursorImpl) RemoveAdvisoryLock(name string) {
 	vc.safeSession.RemoveAdvisoryLock(name)
 }
 
+func (vc *vcursorImpl) SetCommitOrder(co vtgatepb.CommitOrder) {
+	vc.safeSession.SetCommitOrder(co)
+}
+
 // GetDBDDLPluginName implements the VCursor interface
 func (vc *vcursorImpl) GetDBDDLPluginName() string {
 	return *dbDDLPlugin
diff --git a/go/vt/vtgate/vindexes/consistent_lookup.go b/go/vt/vtgate/vindexes/consistent_lookup.go
index f2eb7167f4..1cd372906f 100644
--- a/go/vt/vtgate/vindexes/consistent_lookup.go
+++ b/go/vt/vtgate/vindexes/consistent_lookup.go
@@ -36,12 +36,14 @@ import (
 )
 
 var (
-	_ SingleColumn  = (*ConsistentLookupUnique)(nil)
-	_ Lookup        = (*ConsistentLookupUnique)(nil)
-	_ WantOwnerInfo = (*ConsistentLookupUnique)(nil)
-	_ SingleColumn  = (*ConsistentLookup)(nil)
-	_ Lookup        = (*ConsistentLookup)(nil)
-	_ WantOwnerInfo = (*ConsistentLookup)(nil)
+	_ SingleColumn   = (*ConsistentLookupUnique)(nil)
+	_ Lookup         = (*ConsistentLookupUnique)(nil)
+	_ WantOwnerInfo  = (*ConsistentLookupUnique)(nil)
+	_ LookupPlanable = (*ConsistentLookupUnique)(nil)
+	_ SingleColumn   = (*ConsistentLookup)(nil)
+	_ Lookup         = (*ConsistentLookup)(nil)
+	_ WantOwnerInfo  = (*ConsistentLookup)(nil)
+	_ LookupPlanable = (*ConsistentLookup)(nil)
 )
 
 func init() {
@@ -106,6 +108,18 @@ func (lu *ConsistentLookup) Map(ctx context.Context, vcursor VCursor, ids []sqlt
 	if err != nil {
 		return nil, err
 	}
+	return lu.MapResult(ids, results)
+}
+
+// MapResult implements the LookupPlanable interface
+func (lu *ConsistentLookup) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lu.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
 	for _, result := range results {
 		if len(result.Rows) == 0 {
 			out = append(out, key.DestinationNone{})
@@ -124,6 +138,16 @@ func (lu *ConsistentLookup) Map(ctx context.Context, vcursor VCursor, ids []sqlt
 	return out, nil
 }
 
+// Query implements the LookupPlanable interface
+func (lu *ConsistentLookup) Query() (selQuery string, arguments []string) {
+	return lu.lkp.query()
+}
+
+// AllowBatch implements the LookupPlanable interface
+func (lu *ConsistentLookup) AllowBatch() bool {
+	return lu.lkp.BatchLookup
+}
+
 //====================================================================
 
 // ConsistentLookupUnique defines a vindex that uses a lookup table.
@@ -176,6 +200,19 @@ func (lu *ConsistentLookupUnique) Map(ctx context.Context, vcursor VCursor, ids
 	if err != nil {
 		return nil, err
 	}
+	return lu.MapResult(ids, results)
+}
+
+// MapResult implements the LookupPlanable interface
+func (lu *ConsistentLookupUnique) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lu.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
+
 	for i, result := range results {
 		switch len(result.Rows) {
 		case 0:
@@ -193,6 +230,16 @@ func (lu *ConsistentLookupUnique) Map(ctx context.Context, vcursor VCursor, ids
 	return out, nil
 }
 
+// Query implements the LookupPlanable interface
+func (lu *ConsistentLookupUnique) Query() (selQuery string, arguments []string) {
+	return lu.lkp.query()
+}
+
+// AllowBatch implements the LookupPlanable interface
+func (lu *ConsistentLookupUnique) AllowBatch() bool {
+	return lu.lkp.BatchLookup
+}
+
 //====================================================================
 
 // clCommon defines a vindex that uses a lookup table.
@@ -406,6 +453,11 @@ func (lu *clCommon) addWhere(buf *bytes.Buffer, cols []string) {
 	}
 }
 
+// GetCommitOrder implements the LookupPlanable interface
+func (lu *clCommon) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_PRE
+}
+
 // IsBackfilling implements the LookupBackfill interface
 func (lu *ConsistentLookupUnique) IsBackfilling() bool {
 	return lu.writeOnly
diff --git a/go/vt/vtgate/vindexes/lookup.go b/go/vt/vtgate/vindexes/lookup.go
index fb713fb751..09c838ede4 100644
--- a/go/vt/vtgate/vindexes/lookup.go
+++ b/go/vt/vtgate/vindexes/lookup.go
@@ -28,10 +28,12 @@ import (
 )
 
 var (
-	_ SingleColumn = (*LookupUnique)(nil)
-	_ Lookup       = (*LookupUnique)(nil)
-	_ SingleColumn = (*LookupNonUnique)(nil)
-	_ Lookup       = (*LookupNonUnique)(nil)
+	_ SingleColumn   = (*LookupUnique)(nil)
+	_ Lookup         = (*LookupUnique)(nil)
+	_ LookupPlanable = (*LookupUnique)(nil)
+	_ SingleColumn   = (*LookupNonUnique)(nil)
+	_ Lookup         = (*LookupNonUnique)(nil)
+	_ LookupPlanable = (*LookupNonUnique)(nil)
 )
 
 func init() {
@@ -47,6 +49,14 @@ type LookupNonUnique struct {
 	lkp       lookupInternal
 }
 
+func (ln *LookupNonUnique) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
+
+func (ln *LookupNonUnique) AllowBatch() bool {
+	return ln.lkp.BatchLookup
+}
+
 // String returns the name of the vindex.
 func (ln *LookupNonUnique) String() string {
 	return ln.name
@@ -89,6 +99,19 @@ func (ln *LookupNonUnique) Map(ctx context.Context, vcursor VCursor, ids []sqlty
 	if err != nil {
 		return nil, err
 	}
+
+	return ln.MapResult(ids, results)
+}
+
+// MapResult implements the LookupPlanable interface
+func (ln *LookupNonUnique) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if ln.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
 	for _, result := range results {
 		if len(result.Rows) == 0 {
 			out = append(out, key.DestinationNone{})
@@ -139,6 +162,11 @@ func (ln *LookupNonUnique) MarshalJSON() ([]byte, error) {
 	return json.Marshal(ln.lkp)
 }
 
+// Query implements the LookupPlanable interface
+func (ln *LookupNonUnique) Query() (selQuery string, arguments []string) {
+	return ln.lkp.query()
+}
+
 // NewLookup creates a LookupNonUnique vindex.
 // The supplied map has the following required fields:
 //
@@ -189,6 +217,14 @@ type LookupUnique struct {
 	lkp       lookupInternal
 }
 
+func (lu *LookupUnique) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
+
+func (lu *LookupUnique) AllowBatch() bool {
+	return lu.lkp.BatchLookup
+}
+
 // NewLookupUnique creates a LookupUnique vindex.
 // The supplied map has the following required fields:
 //
@@ -241,8 +277,8 @@ func (lu *LookupUnique) NeedsVCursor() bool {
 
 // Map can map ids to key.Destination objects.
 func (lu *LookupUnique) Map(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
-	out := make([]key.Destination, 0, len(ids))
 	if lu.writeOnly {
+		out := make([]key.Destination, 0, len(ids))
 		for range ids {
 			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
 		}
@@ -252,6 +288,11 @@ func (lu *LookupUnique) Map(ctx context.Context, vcursor VCursor, ids []sqltypes
 	if err != nil {
 		return nil, err
 	}
+	return lu.MapResult(ids, results)
+}
+
+func (lu *LookupUnique) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
 	for i, result := range results {
 		switch len(result.Rows) {
 		case 0:
@@ -305,3 +346,11 @@ func (lu *LookupUnique) MarshalJSON() ([]byte, error) {
 func (lu *LookupUnique) IsBackfilling() bool {
 	return lu.writeOnly
 }
+
+func (lu *LookupUnique) LookupQuery() (string, error) {
+	return lu.lkp.sel, nil
+}
+
+func (lu *LookupUnique) Query() (string, []string) {
+	return lu.lkp.query()
+}
diff --git a/go/vt/vtgate/vindexes/lookup_hash.go b/go/vt/vtgate/vindexes/lookup_hash.go
index 4670e24c5f..455f3be09b 100644
--- a/go/vt/vtgate/vindexes/lookup_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_hash.go
@@ -30,10 +30,12 @@ import (
 )
 
 var (
-	_ SingleColumn = (*LookupHash)(nil)
-	_ Lookup       = (*LookupHash)(nil)
-	_ SingleColumn = (*LookupHashUnique)(nil)
-	_ Lookup       = (*LookupHashUnique)(nil)
+	_ SingleColumn   = (*LookupHash)(nil)
+	_ Lookup         = (*LookupHash)(nil)
+	_ LookupPlanable = (*LookupHash)(nil)
+	_ SingleColumn   = (*LookupHashUnique)(nil)
+	_ Lookup         = (*LookupHashUnique)(nil)
+	_ LookupPlanable = (*LookupHashUnique)(nil)
 )
 
 func init() {
@@ -126,6 +128,19 @@ func (lh *LookupHash) Map(ctx context.Context, vcursor VCursor, ids []sqltypes.V
 	if err != nil {
 		return nil, err
 	}
+	return lh.MapResult(ids, results)
+}
+
+// MapResult implements the LookupPlanable interface
+func (lh *LookupHash) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lh.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
+
 	for _, result := range results {
 		if len(result.Rows) == 0 {
 			out = append(out, key.DestinationNone{})
@@ -146,6 +161,21 @@ func (lh *LookupHash) Map(ctx context.Context, vcursor VCursor, ids []sqltypes.V
 	return out, nil
 }
 
+// Query implements the LookupPlanable interface
+func (lh *LookupHash) Query() (selQuery string, arguments []string) {
+	return lh.lkp.query()
+}
+
+// AllowBatch implements the LookupPlanable interface
+func (lh *LookupHash) AllowBatch() bool {
+	return lh.lkp.BatchLookup
+}
+
+// GetCommitOrder implements the LookupPlanable interface
+func (lh *LookupHash) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
+
 // Verify returns true if ids maps to ksids.
 func (lh *LookupHash) Verify(ctx context.Context, vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	if lh.writeOnly {
@@ -220,6 +250,8 @@ type LookupHashUnique struct {
 	lkp       lookupInternal
 }
 
+var _ LookupPlanable = (*LookupHashUnique)(nil)
+
 // NewLookupHashUnique creates a LookupHashUnique vindex.
 // The supplied map has the following required fields:
 //
@@ -272,8 +304,8 @@ func (lhu *LookupHashUnique) NeedsVCursor() bool {
 
 // Map can map ids to key.Destination objects.
 func (lhu *LookupHashUnique) Map(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error) {
-	out := make([]key.Destination, 0, len(ids))
 	if lhu.writeOnly {
+		out := make([]key.Destination, 0, len(ids))
 		for range ids {
 			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
 		}
@@ -284,6 +316,17 @@ func (lhu *LookupHashUnique) Map(ctx context.Context, vcursor VCursor, ids []sql
 	if err != nil {
 		return nil, err
 	}
+	return lhu.MapResult(ids, results)
+}
+
+func (lhu *LookupHashUnique) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lhu.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
 	for i, result := range results {
 		switch len(result.Rows) {
 		case 0:
@@ -355,3 +398,16 @@ func (lhu *LookupHashUnique) MarshalJSON() ([]byte, error) {
 func (lhu *LookupHashUnique) IsBackfilling() bool {
 	return lhu.writeOnly
 }
+
+func (lhu *LookupHashUnique) AllowBatch() bool {
+	return lhu.lkp.BatchLookup
+}
+
+func (lhu *LookupHashUnique) Query() (selQuery string, arguments []string) {
+	return lhu.lkp.query()
+}
+
+// GetCommitOrder implements the LookupPlanable interface
+func (lhu *LookupHashUnique) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
diff --git a/go/vt/vtgate/vindexes/lookup_internal.go b/go/vt/vtgate/vindexes/lookup_internal.go
index 832ae846c7..e0bd06196c 100644
--- a/go/vt/vtgate/vindexes/lookup_internal.go
+++ b/go/vt/vtgate/vindexes/lookup_internal.go
@@ -43,7 +43,7 @@ type lookupInternal struct {
 	Upsert               bool     `json:"upsert,omitempty"`
 	IgnoreNulls          bool     `json:"ignore_nulls,omitempty"`
 	BatchLookup          bool     `json:"batch_lookup,omitempty"`
-	sel, ver, del        string
+	sel, ver, del        string   // sel: map query, ver: verify query, del: delete query
 }
 
 func (lkp *lookupInternal) Init(lookupQueryParams map[string]string, autocommit, upsert, multiShardAutocommit bool) error {
@@ -356,6 +356,10 @@ func (lkp *lookupInternal) initDelStmt() string {
 	return delBuffer.String()
 }
 
+func (lkp *lookupInternal) query() (selQuery string, arguments []string) {
+	return lkp.sel, lkp.FromColumns
+}
+
 type commonConfig struct {
 	autocommit           bool
 	multiShardAutocommit bool
diff --git a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
index b922ae1bdd..884fd7c99b 100644
--- a/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
+++ b/go/vt/vtgate/vindexes/lookup_unicodeloosemd5_hash.go
@@ -31,10 +31,12 @@ import (
 )
 
 var (
-	_ SingleColumn = (*LookupUnicodeLooseMD5Hash)(nil)
-	_ Lookup       = (*LookupUnicodeLooseMD5Hash)(nil)
-	_ SingleColumn = (*LookupUnicodeLooseMD5HashUnique)(nil)
-	_ Lookup       = (*LookupUnicodeLooseMD5HashUnique)(nil)
+	_ SingleColumn   = (*LookupUnicodeLooseMD5Hash)(nil)
+	_ Lookup         = (*LookupUnicodeLooseMD5Hash)(nil)
+	_ LookupPlanable = (*LookupUnicodeLooseMD5Hash)(nil)
+	_ SingleColumn   = (*LookupUnicodeLooseMD5HashUnique)(nil)
+	_ Lookup         = (*LookupUnicodeLooseMD5HashUnique)(nil)
+	_ LookupPlanable = (*LookupUnicodeLooseMD5HashUnique)(nil)
 )
 
 func init() {
@@ -150,6 +152,51 @@ func (lh *LookupUnicodeLooseMD5Hash) Map(ctx context.Context, vcursor VCursor, i
 	return out, nil
 }
 
+// MapResult implements the LookupPlanable interface
+func (lh *LookupUnicodeLooseMD5Hash) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lh.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
+
+	for _, result := range results {
+		if len(result.Rows) == 0 {
+			out = append(out, key.DestinationNone{})
+			continue
+		}
+		ksids := make([][]byte, 0, len(result.Rows))
+		for _, row := range result.Rows {
+			num, err := evalengine.ToUint64(row[0])
+			if err != nil {
+				// A failure to convert is equivalent to not being
+				// able to map.
+				continue
+			}
+			ksids = append(ksids, vhash(num))
+		}
+		out = append(out, key.DestinationKeyspaceIDs(ksids))
+	}
+	return out, nil
+}
+
+// Query implements the LookupPlanable interface
+func (lh *LookupUnicodeLooseMD5Hash) Query() (selQuery string, arguments []string) {
+	return lh.lkp.query()
+}
+
+// AllowBatch implements the LookupPlanable interface
+func (lh *LookupUnicodeLooseMD5Hash) AllowBatch() bool {
+	return lh.lkp.BatchLookup
+}
+
+// GetCommitOrder implements the LookupPlanable interface
+func (lh *LookupUnicodeLooseMD5Hash) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
+
 // Verify returns true if ids maps to ksids.
 func (lh *LookupUnicodeLooseMD5Hash) Verify(ctx context.Context, vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	if lh.writeOnly {
@@ -317,6 +364,49 @@ func (lhu *LookupUnicodeLooseMD5HashUnique) Map(ctx context.Context, vcursor VCu
 	return out, nil
 }
 
+// MapResult implements the LookupPlanable interface
+func (lhu *LookupUnicodeLooseMD5HashUnique) MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error) {
+	out := make([]key.Destination, 0, len(ids))
+	if lhu.writeOnly {
+		for range ids {
+			out = append(out, key.DestinationKeyRange{KeyRange: &topodatapb.KeyRange{}})
+		}
+		return out, nil
+	}
+
+	for i, result := range results {
+		switch len(result.Rows) {
+		case 0:
+			out = append(out, key.DestinationNone{})
+		case 1:
+			num, err := evalengine.ToUint64(result.Rows[0][0])
+			if err != nil {
+				out = append(out, key.DestinationNone{})
+				continue
+			}
+			out = append(out, key.DestinationKeyspaceID(vhash(num)))
+		default:
+			return nil, fmt.Errorf("LookupUnicodeLooseMD5HashUnique.Map: unexpected multiple results from vindex %s: %v", lhu.lkp.Table, ids[i])
+		}
+	}
+	return out, nil
+}
+
+// Query implements the LookupPlanable interface
+func (lhu *LookupUnicodeLooseMD5HashUnique) Query() (selQuery string, arguments []string) {
+	return lhu.lkp.query()
+}
+
+// AllowBatch implements the LookupPlanable interface
+func (lhu *LookupUnicodeLooseMD5HashUnique) AllowBatch() bool {
+	return lhu.lkp.BatchLookup
+}
+
+// GetCommitOrder implements the LookupPlanable interface
+func (lhu *LookupUnicodeLooseMD5HashUnique) GetCommitOrder() vtgatepb.CommitOrder {
+	return vtgatepb.CommitOrder_NORMAL
+}
+
 // Verify returns true if ids maps to ksids.
 func (lhu *LookupUnicodeLooseMD5HashUnique) Verify(ctx context.Context, vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error) {
 	if lhu.writeOnly {
diff --git a/go/vt/vtgate/vindexes/vindex.go b/go/vt/vtgate/vindexes/vindex.go
index 4667e3013f..dcb96e73a2 100644
--- a/go/vt/vtgate/vindexes/vindex.go
+++ b/go/vt/vtgate/vindexes/vindex.go
@@ -32,126 +32,136 @@ import (
 
 // This file defines interfaces and registration for vindexes.
 
-// A VCursor is an interface that allows you to execute queries
-// in the current context and session of a VTGate request. Vindexes
-// can use this interface to execute lookup queries.
-type VCursor interface {
-	Execute(ctx context.Context, method string, query string, bindvars map[string]*querypb.BindVariable, rollbackOnError bool, co vtgatepb.CommitOrder) (*sqltypes.Result, error)
-	ExecuteKeyspaceID(ctx context.Context, keyspace string, ksid []byte, query string, bindVars map[string]*querypb.BindVariable, rollbackOnError, autocommit bool) (*sqltypes.Result, error)
-	InTransactionAndIsDML() bool
-	LookupRowLockShardSession() vtgatepb.CommitOrder
-}
+type (
+	// A VCursor is an interface that allows you to execute queries
+	// in the current context and session of a VTGate request. Vindexes
+	// can use this interface to execute lookup queries.
+	VCursor interface {
+		Execute(ctx context.Context, method string, query string, bindvars map[string]*querypb.BindVariable, rollbackOnError bool, co vtgatepb.CommitOrder) (*sqltypes.Result, error)
+		ExecuteKeyspaceID(ctx context.Context, keyspace string, ksid []byte, query string, bindVars map[string]*querypb.BindVariable, rollbackOnError, autocommit bool) (*sqltypes.Result, error)
+		InTransactionAndIsDML() bool
+		LookupRowLockShardSession() vtgatepb.CommitOrder
+	}
 
-// Vindex defines the interface required to register a vindex.
-type Vindex interface {
-	// String returns the name of the Vindex instance.
-	// It's used for testing and diagnostics. Use pointer
-	// comparison to see if two objects refer to the same
-	// Vindex.
-	String() string
-
-	// Cost is used by planbuilder to prioritize vindexes.
-	// The cost can be 0 if the id is basically a keyspace id.
-	// The cost can be 1 if the id can be hashed to a keyspace id.
-	// The cost can be 2 or above if the id needs to be looked up
-	// from an external data source. These guidelines are subject
-	// to change in the future.
-	Cost() int
-
-	// IsUnique returns true if the Vindex is unique.
-	// A Unique Vindex is allowed to return non-unique values like
-	// a keyrange. This is in situations where the vindex does not
-	// have enough information to map to a keyspace id. If so, such
-	// a vindex cannot be primary.
-	IsUnique() bool
-
-	// NeedsVCursor returns true if the Vindex makes calls into the
-	// VCursor. Such vindexes cannot be used by vreplication.
-	NeedsVCursor() bool
-}
+	// Vindex defines the interface required to register a vindex.
+	Vindex interface {
+		// String returns the name of the Vindex instance.
+		// It's used for testing and diagnostics. Use pointer
+		// comparison to see if two objects refer to the same
+		// Vindex.
+		String() string
+
+		// Cost is used by planbuilder to prioritize vindexes.
+		// The cost can be 0 if the id is basically a keyspace id.
+		// The cost can be 1 if the id can be hashed to a keyspace id.
+		// The cost can be 2 or above if the id needs to be looked up
+		// from an external data source. These guidelines are subject
+		// to change in the future.
+		Cost() int
+
+		// IsUnique returns true if the Vindex is unique.
+		// A Unique Vindex is allowed to return non-unique values like
+		// a keyrange. This is in situations where the vindex does not
+		// have enough information to map to a keyspace id. If so, such
+		// a vindex cannot be primary.
+		IsUnique() bool
+
+		// NeedsVCursor returns true if the Vindex makes calls into the
+		// VCursor. Such vindexes cannot be used by vreplication.
+		NeedsVCursor() bool
+	}
 
-// SingleColumn defines the interface for a single column vindex.
-type SingleColumn interface {
-	Vindex
-	// Map can map ids to key.Destination objects.
-	// If the Vindex is unique, each id would map to either
-	// a KeyRange, or a single KeyspaceID.
-	// If the Vindex is non-unique, each id would map to either
-	// a KeyRange, or a list of KeyspaceID.
-	Map(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error)
-
-	// Verify returns true for every id that successfully maps to the
-	// specified keyspace id.
-	Verify(ctx context.Context, vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error)
-}
+	// SingleColumn defines the interface for a single column vindex.
+	SingleColumn interface {
+		Vindex
+		// Map can map ids to key.Destination objects.
+		// If the Vindex is unique, each id would map to either
+		// a KeyRange, or a single KeyspaceID.
+		// If the Vindex is non-unique, each id would map to either
+		// a KeyRange, or a list of KeyspaceID.
+		Map(ctx context.Context, vcursor VCursor, ids []sqltypes.Value) ([]key.Destination, error)
+
+		// Verify returns true for every id that successfully maps to the
+		// specified keyspace id.
+		Verify(ctx context.Context, vcursor VCursor, ids []sqltypes.Value, ksids [][]byte) ([]bool, error)
+	}
 
-// MultiColumn defines the interface for a multi-column vindex.
-type MultiColumn interface {
-	Vindex
-	Map(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
-	Verify(ctx context.Context, 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
-}
+	// MultiColumn defines the interface for a multi-column vindex.
+	MultiColumn interface {
+		Vindex
+		Map(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value) ([]key.Destination, error)
+		Verify(ctx context.Context, 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
+	}
 
-// Hashing defined the interface for the vindexes that export the Hash function to be used by multi-column vindex.
-type Hashing interface {
-	Hash(id sqltypes.Value) ([]byte, error)
-}
+	// Hashing defined the interface for the vindexes that export the Hash function to be used by multi-column vindex.
+	Hashing interface {
+		Hash(id sqltypes.Value) ([]byte, error)
+	}
+	// A Reversible vindex is one that can perform a
+	// reverse lookup from a keyspace id to an id. This
+	// is optional. If present, VTGate can use it to
+	// fill column values based on the target keyspace id.
+	// Reversible is supported only for SingleColumn vindexes.
+	Reversible interface {
+		SingleColumn
+		ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error)
+	}
 
-// A Reversible vindex is one that can perform a
-// reverse lookup from a keyspace id to an id. This
-// is optional. If present, VTGate can use it to
-// fill column values based on the target keyspace id.
-// Reversible is supported only for SingleColumn vindexes.
-type Reversible interface {
-	SingleColumn
-	ReverseMap(vcursor VCursor, ks [][]byte) ([]sqltypes.Value, error)
-}
+	// A Prefixable vindex is one that maps the prefix of a id to a keyspace range
+	// instead of a single keyspace id. It's being used to reduced the fan out for
+	// 'LIKE' expressions.
+	Prefixable interface {
+		SingleColumn
+		PrefixVindex() SingleColumn
+	}
 
-// A Prefixable vindex is one that maps the prefix of a id to a keyspace range
-// instead of a single keyspace id. It's being used to reduced the fan out for
-// 'LIKE' expressions.
-type Prefixable interface {
-	SingleColumn
-	PrefixVindex() SingleColumn
-}
+	// A Lookup vindex is one that needs to lookup
+	// a previously stored map to compute the keyspace
+	// id from an id. This means that the creation of
+	// a lookup vindex entry requires a keyspace id as
+	// input.
+	// A Lookup vindex need not be unique because the
+	// keyspace_id, which must be supplied, can be used
+	// to determine the target shard for an insert operation.
+	Lookup interface {
+		// Create creates an association between ids and ksids. If ignoreMode
+		// is true, then the Create should ignore dup key errors.
+		Create(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte, ignoreMode bool) error
+
+		Delete(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksid []byte) error
+
+		// Update replaces the mapping of old values with new values for a keyspace id.
+		Update(ctx context.Context, vcursor VCursor, oldValues []sqltypes.Value, ksid []byte, newValues []sqltypes.Value) error
+	}
 
-// A Lookup vindex is one that needs to lookup
-// a previously stored map to compute the keyspace
-// id from an id. This means that the creation of
-// a lookup vindex entry requires a keyspace id as
-// input.
-// A Lookup vindex need not be unique because the
-// keyspace_id, which must be supplied, can be used
-// to determine the target shard for an insert operation.
-type Lookup interface {
-	// Create creates an association between ids and ksids. If ignoreMode
-	// is true, then the Create should ignore dup key errors.
-	Create(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksids [][]byte, ignoreMode bool) error
-
-	Delete(ctx context.Context, vcursor VCursor, rowsColValues [][]sqltypes.Value, ksid []byte) error
-
-	// Update replaces the mapping of old values with new values for a keyspace id.
-	Update(ctx context.Context, vcursor VCursor, oldValues []sqltypes.Value, ksid []byte, newValues []sqltypes.Value) error
-}
+	// LookupPlanable are for lookup vindexes where we can extract the lookup query at plan time
+	LookupPlanable interface {
+		String() string
+		Query() (selQuery string, arguments []string)
+		MapResult(ids []sqltypes.Value, results []*sqltypes.Result) ([]key.Destination, error)
+		AllowBatch() bool
+		GetCommitOrder() vtgatepb.CommitOrder
+	}
 
-// LookupBackfill interfaces all lookup vindexes that can backfill rows, such as LookupUnique.
-type LookupBackfill interface {
-	IsBackfilling() bool
-}
+	// LookupBackfill interfaces all lookup vindexes that can backfill rows, such as LookupUnique.
+	LookupBackfill interface {
+		IsBackfilling() bool
+	}
 
-// WantOwnerInfo defines the interface that a vindex must
-// satisfy to request info about the owner table. This information can
-// be used to query the owner's table for the owning row's presence.
-type WantOwnerInfo interface {
-	SetOwnerInfo(keyspace, table string, cols []sqlparser.IdentifierCI) error
-}
+	// WantOwnerInfo defines the interface that a vindex must
+	// satisfy to request info about the owner table. This information can
+	// be used to query the owner's table for the owning row's presence.
+	WantOwnerInfo interface {
+		SetOwnerInfo(keyspace, table string, cols []sqlparser.IdentifierCI) error
+	}
 
-// A NewVindexFunc is a function that creates a Vindex based on the
-// properties specified in the input map. Every vindex must
-// register a NewVindexFunc under a unique vindexType.
-type NewVindexFunc func(string, map[string]string) (Vindex, error)
+	// A NewVindexFunc is a function that creates a Vindex based on the
+	// properties specified in the input map. Every vindex must
+	// register a NewVindexFunc under a unique vindexType.
+	NewVindexFunc func(string, map[string]string) (Vindex, error)
+)
 
 var registry = make(map[string]NewVindexFunc)
 


Test patch:
diff --git a/go/test/endtoend/vtgate/queries/lookup_queries/main_test.go b/go/test/endtoend/vtgate/queries/lookup_queries/main_test.go
new file mode 100644
index 0000000000..9660c25e0c
--- /dev/null
+++ b/go/test/endtoend/vtgate/queries/lookup_queries/main_test.go
@@ -0,0 +1,152 @@
+/*
+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 vtgate
+
+import (
+	_ "embed"
+	"flag"
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"vitess.io/vitess/go/test/endtoend/utils"
+
+	"vitess.io/vitess/go/vt/vtgate/planbuilder"
+
+	"vitess.io/vitess/go/mysql"
+	"vitess.io/vitess/go/test/endtoend/cluster"
+)
+
+var (
+	clusterInstance *cluster.LocalProcessCluster
+	vtParams        mysql.ConnParams
+	mysqlParams     mysql.ConnParams
+	shardedKs       = "ks"
+
+	shardedKsShards = []string{"-19a0", "19a0-20", "20-20c0", "20c0-"}
+	Cell            = "test"
+	//go:embed schema.sql
+	shardedSchemaSQL string
+
+	//go:embed vschema.json
+	shardedVSchema string
+)
+
+func TestMain(m *testing.M) {
+	defer cluster.PanicHandler(nil)
+	flag.Parse()
+
+	exitCode := func() int {
+		clusterInstance = cluster.NewCluster(Cell, "localhost")
+		defer clusterInstance.Teardown()
+
+		// Start topo server
+		err := clusterInstance.StartTopo()
+		if err != nil {
+			return 1
+		}
+
+		// Start keyspace
+		sKs := &cluster.Keyspace{
+			Name:      shardedKs,
+			SchemaSQL: shardedSchemaSQL,
+			VSchema:   shardedVSchema,
+		}
+
+		clusterInstance.VtTabletExtraArgs = []string{"--queryserver-config-schema-change-signal-interval", "0.1"}
+		err = clusterInstance.StartKeyspace(*sKs, shardedKsShards, 0, false)
+		if err != nil {
+			return 1
+		}
+
+		err = clusterInstance.VtctlclientProcess.ExecuteCommand("RebuildVSchemaGraph")
+		if err != nil {
+			return 1
+		}
+
+		// Start vtgate
+		clusterInstance.VtGatePlannerVersion = planbuilder.Gen4 // enable Gen4 planner.
+		err = clusterInstance.StartVtgate()
+		if err != nil {
+			return 1
+		}
+		vtParams = mysql.ConnParams{
+			Host: clusterInstance.Hostname,
+			Port: clusterInstance.VtgateMySQLPort,
+		}
+
+		conn, closer, err := utils.NewMySQL(clusterInstance, shardedKs, shardedSchemaSQL)
+		if err != nil {
+			fmt.Println(err)
+			return 1
+		}
+		defer closer()
+		mysqlParams = conn
+		return m.Run()
+	}()
+	os.Exit(exitCode)
+}
+
+func start(t *testing.T) (utils.MySQLCompare, func()) {
+	mcmp, err := utils.NewMySQLCompare(t, vtParams, mysqlParams)
+	require.NoError(t, err)
+	deleteAll := func() {
+		_, _ = utils.ExecAllowError(t, mcmp.VtConn, "set workload = oltp")
+
+		tables := []string{"user", "lookup"}
+		for _, table := range tables {
+			_, _ = mcmp.ExecAndIgnore("delete from " + table)
+		}
+	}
+
+	deleteAll()
+
+	return mcmp, func() {
+		deleteAll()
+		mcmp.Close()
+		cluster.PanicHandler(t)
+	}
+}
+
+func TestLookupQueries(t *testing.T) {
+	mcmp, closer := start(t)
+	defer closer()
+
+	mcmp.Exec(`insert into user 
+    (id, lookup,   lookup_unique) values 
+	(1, 'apa',    'apa'), 
+	(2, 'apa',    'bandar'), 
+	(3, 'monkey', 'monkey')`)
+
+	for _, workload := range []string{"olap", "oltp"} {
+		t.Run(workload, func(t *testing.T) {
+			utils.Exec(t, mcmp.VtConn, "set workload = "+workload)
+
+			mcmp.AssertMatches("select id from user where lookup = 'apa'", "[[INT64(1)] [INT64(2)]]")
+			mcmp.AssertMatches("select id from user where lookup = 'not there'", "[]")
+			mcmp.AssertMatchesNoOrder("select id from user where lookup in ('apa', 'monkey')", "[[INT64(1)] [INT64(2)] [INT64(3)]]")
+			mcmp.AssertMatches("select count(*) from user where lookup in ('apa', 'monkey')", "[[INT64(3)]]")
+
+			mcmp.AssertMatches("select id from user where lookup_unique = 'apa'", "[[INT64(1)]]")
+			mcmp.AssertMatches("select id from user where lookup_unique = 'not there'", "[]")
+			mcmp.AssertMatchesNoOrder("select id from user where lookup_unique in ('apa', 'bandar')", "[[INT64(1)] [INT64(2)]]")
+			mcmp.AssertMatches("select count(*) from user where lookup_unique in ('apa', 'monkey', 'bandar')", "[[INT64(3)]]")
+		})
+	}
+}
diff --git a/go/test/endtoend/vtgate/queries/lookup_queries/schema.sql b/go/test/endtoend/vtgate/queries/lookup_queries/schema.sql
new file mode 100644
index 0000000000..5232fee3ac
--- /dev/null
+++ b/go/test/endtoend/vtgate/queries/lookup_queries/schema.sql
@@ -0,0 +1,22 @@
+create table user
+(
+    id            bigint,
+    lookup        varchar(128),
+    lookup_unique varchar(128),
+    primary key (id)
+) Engine = InnoDB;
+
+create table lookup
+(
+    lookup      varchar(128),
+    id          bigint,
+    keyspace_id varbinary(100),
+    primary key (id)
+) Engine = InnoDB;
+
+create table lookup_unique
+(
+    lookup_unique varchar(128),
+    keyspace_id   varbinary(100),
+    primary key (lookup_unique)
+) Engine = InnoDB;
\ No newline at end of file
diff --git a/go/test/endtoend/vtgate/queries/lookup_queries/vschema.json b/go/test/endtoend/vtgate/queries/lookup_queries/vschema.json
new file mode 100644
index 0000000000..cdd5abe0d2
--- /dev/null
+++ b/go/test/endtoend/vtgate/queries/lookup_queries/vschema.json
@@ -0,0 +1,69 @@
+{
+  "sharded": true,
+  "vindexes": {
+    "hash_index": {
+      "type": "hash"
+    },
+    "md5_index": {
+      "type": "unicode_loose_md5"
+    },
+    "lookup_vdx": {
+      "type": "lookup",
+      "params": {
+        "table": "lookup",
+        "from": "lookup,id",
+        "to": "keyspace_id",
+        "autocommit": "true"
+      },
+      "owner": "user"
+    },
+    "lookup_unique_vdx": {
+      "type": "lookup_unique",
+      "params": {
+        "table": "lookup_unique",
+        "from": "lookup_unique",
+        "to": "keyspace_id",
+        "autocommit": "true"
+      },
+      "owner": "user"
+    }
+  },
+  "tables": {
+    "user": {
+      "column_vindexes": [
+        {
+          "column": "id",
+          "name": "hash_index"
+        },
+        {
+          "columns": [
+            "lookup",
+            "id"
+          ],
+          "name": "lookup_vdx"
+        },
+        {
+          "column": "lookup_unique",
+          "name": "lookup_unique_vdx"
+        }
+      ]
+    },
+    "lookup": {
+      "column_vindexes": [
+        {
+          "column": "lookup",
+          "name": "md5_index"
+        }
+      ]
+    },
+    "lookup_unique": {
+      "column_vindexes": [
+        {
+          "column": "lookup_unique",
+          "name": "md5_index"
+        }
+      ]
+    }
+  }
+}
+	
\ No newline at end of file
diff --git a/go/vt/vtexplain/vtexplain_test.go b/go/vt/vtexplain/vtexplain_test.go
index 4fa02cbc72..ab1b61567d 100644
--- a/go/vt/vtexplain/vtexplain_test.go
+++ b/go/vt/vtexplain/vtexplain_test.go
@@ -34,8 +34,6 @@ import (
 	"vitess.io/vitess/go/vt/topo"
 )
 
-var testOutputTempDir string
-
 func defaultTestOpts() *Options {
 	return &Options{
 		ReplicationMode: "ROW",
@@ -122,9 +120,8 @@ func runTestCase(testcase, mode string, opts *Options, topts *testopts, t *testi
 			// temp file to be able to diff the results.
 			t.Errorf("Text output did not match (-want +got):\n%s", diff)
 
-			if testOutputTempDir == "" {
-				testOutputTempDir = t.TempDir()
-			}
+			testOutputTempDir, err := os.MkdirTemp("testdata", "plan_test")
+			require.NoError(t, err)
 			gotFile := fmt.Sprintf("%s/%s-output.txt", testOutputTempDir, testcase)
 			os.WriteFile(gotFile, []byte(explainText), 0644)
 
diff --git a/go/vt/vtgate/engine/fake_vcursor_test.go b/go/vt/vtgate/engine/fake_vcursor_test.go
index f5ebc5d4ca..e5f4c4dd74 100644
--- a/go/vt/vtgate/engine/fake_vcursor_test.go
+++ b/go/vt/vtgate/engine/fake_vcursor_test.go
@@ -50,6 +50,11 @@ var _ SessionActions = (*noopVCursor)(nil)
 type noopVCursor struct {
 }
 
+func (t *noopVCursor) SetCommitOrder(co vtgatepb.CommitOrder) {
+	//TODO implement me
+	panic("implement me")
+}
+
 func (t *noopVCursor) StreamExecutePrimitiveStandalone(ctx context.Context, primitive Primitive, bindVars map[string]*querypb.BindVariable, wantfields bool, callback func(result *sqltypes.Result) error) error {
 	return primitive.TryStreamExecute(ctx, t, bindVars, wantfields, callback)
 }
diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go
index faeca866f1..dc3b783b62 100644
--- a/go/vt/vtgate/planbuilder/plan_test.go
+++ b/go/vt/vtgate/planbuilder/plan_test.go
@@ -29,6 +29,8 @@ import (
 	"strings"
 	"testing"
 
+	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
+
 	"vitess.io/vitess/go/test/utils"
 	vschemapb "vitess.io/vitess/go/vt/proto/vschema"
 	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
@@ -101,10 +103,12 @@ var _ vindexes.Lookup = (*lookupIndex)(nil)
 // nameLkpIndex satisfies Lookup, NonUnique.
 type nameLkpIndex struct{ name string }
 
-func (v *nameLkpIndex) String() string   { return v.name }
-func (*nameLkpIndex) Cost() int          { return 3 }
-func (*nameLkpIndex) IsUnique() bool     { return false }
-func (*nameLkpIndex) NeedsVCursor() bool { return false }
+func (v *nameLkpIndex) String() string                     { return v.name }
+func (*nameLkpIndex) Cost() int                            { return 3 }
+func (*nameLkpIndex) IsUnique() bool                       { return false }
+func (*nameLkpIndex) NeedsVCursor() bool                   { return false }
+func (*nameLkpIndex) AllowBatch() bool                     { return true }
+func (*nameLkpIndex) GetCommitOrder() vtgatepb.CommitOrder { return vtgatepb.CommitOrder_NORMAL }
 func (*nameLkpIndex) Verify(context.Context, vindexes.VCursor, []sqltypes.Value, [][]byte) ([]bool, error) {
 	return []bool{}, nil
 }
@@ -120,6 +124,12 @@ func (*nameLkpIndex) Delete(context.Context, vindexes.VCursor, [][]sqltypes.Valu
 func (*nameLkpIndex) Update(context.Context, vindexes.VCursor, []sqltypes.Value, []byte, []sqltypes.Value) error {
 	return nil
 }
+func (v *nameLkpIndex) Query() (string, []string) {
+	return "select name, keyspace_id from name_user_vdx where name in ::name", []string{"name"}
+}
+func (*nameLkpIndex) MapResult([]sqltypes.Value, []*sqltypes.Result) ([]key.Destination, error) {
+	return nil, nil
+}
 
 func newNameLkpIndex(name string, _ map[string]string) (vindexes.Vindex, error) {
 	return &nameLkpIndex{name: name}, nil
@@ -127,6 +137,7 @@ func newNameLkpIndex(name string, _ map[string]string) (vindexes.Vindex, error)
 
 var _ vindexes.Vindex = (*nameLkpIndex)(nil)
 var _ vindexes.Lookup = (*nameLkpIndex)(nil)
+var _ vindexes.LookupPlanable = (*nameLkpIndex)(nil)
 
 // costlyIndex satisfies Lookup, NonUnique.
 type costlyIndex struct{ name string }
@@ -306,6 +317,15 @@ func TestOneWithUserAsDefault(t *testing.T) {
 	testFile(t, "onecase.txt", "", vschema, false)
 }
 
+func TestOneWithTPCHVSchema(t *testing.T) {
+	vschema := &vschemaWrapper{
+		v:             loadSchema(t, "tpch_schema_test.json", true),
+		sysVarEnabled: true,
+	}
+
+	testFile(t, "onecase.txt", "", vschema, false)
+}
+
 func TestRubyOnRailsQueries(t *testing.T) {
 	vschemaWrapper := &vschemaWrapper{
 		v:             loadSchema(t, "rails_schema_test.json", true),
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
index 8714ade770..5e2dd96ca9 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
@@ -3838,19 +3838,44 @@ Gen4 plan same as above
             "Aggregates": "random(0) AS 1, sum_count(1) AS count(id)",
             "Inputs": [
               {
-                "OperatorType": "Route",
+                "OperatorType": "VindexLookup",
                 "Variant": "Equal",
                 "Keyspace": {
                   "Name": "user",
                   "Sharded": true
                 },
-                "FieldQuery": "select 1, count(id) from `user` where 1 != 1",
-                "Query": "select 1, count(id) from `user` where `name` = 'a'",
-                "Table": "`user`",
                 "Values": [
                   "VARCHAR(\"a\")"
                 ],
-                "Vindex": "name_user_map"
+                "Vindex": "name_user_map",
+                "Inputs": [
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "IN",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+                    "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+                    "Table": "name_user_vdx",
+                    "Values": [
+                      ":name"
+                    ],
+                    "Vindex": "user_index"
+                  },
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "ByDestination",
+                    "Keyspace": {
+                      "Name": "user",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select 1, count(id) from `user` where 1 != 1",
+                    "Query": "select 1, count(id) from `user` where `name` = 'a'",
+                    "Table": "`user`"
+                  }
+                ]
               }
             ]
           }
diff --git a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
index 6bc69c608b..84b53c4464 100644
--- a/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/filter_cases.txt
@@ -259,19 +259,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where costly = 'aa' and name = 'bb'",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where costly = 'aa' and `name` = 'bb'",
-    "Table": "`user`",
     "Values": [
       "VARCHAR(\"bb\")"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where costly = 'aa' and `name` = 'bb'",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -303,19 +328,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where costly in ('aa', 'bb') and name in ('aa', 'bb')",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "IN",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where costly in ('aa', 'bb') and `name` in ::__vals",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"aa\"), VARCHAR(\"bb\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where costly in ('aa', 'bb') and `name` in ::__vals",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -347,19 +397,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (name, col) in (('aa', 'bb'), ('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (`name`, col) in (('aa', 'bb'), ('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"aa\"), VARCHAR(\"cc\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (`name`, col) in (('aa', 'bb'), ('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -391,19 +466,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (col, name) in (('aa', 'bb'), ('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (col, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"bb\"), VARCHAR(\"dd\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (col, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -435,19 +535,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (costly, name) in (('aa', 'bb'), ('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (costly, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"bb\"), VARCHAR(\"dd\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (costly, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -479,19 +604,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (name, costly) in (('aa', 'bb'), ('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (`name`, costly) in (('aa', 'bb'), ('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"aa\"), VARCHAR(\"cc\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (`name`, costly) in (('aa', 'bb'), ('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -523,19 +673,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (col, costly) in (('aa', 'bb')) and (col, name) in (('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (col, costly) in (('aa', 'bb')) and (col, `name`) in (('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"dd\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (col, costly) in (('aa', 'bb')) and (col, `name`) in (('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -611,19 +786,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (costly, name) in (('aa', 'bb'), ('cc', 'dd'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (costly, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"bb\"), VARCHAR(\"dd\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (costly, `name`) in (('aa', 'bb'), ('cc', 'dd'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -655,19 +855,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where ((col1, name), col2) in ((('aa', 'bb'), 'cc'), (('dd', 'ee'), 'ff'))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where ((col1, `name`), col2) in ((('aa', 'bb'), 'cc'), (('dd', 'ee'), 'ff'))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"bb\"), VARCHAR(\"ee\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where ((col1, `name`), col2) in ((('aa', 'bb'), 'cc'), (('dd', 'ee'), 'ff'))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -699,19 +924,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (name, (col1, col2)) in (('aa', ('bb', 'cc')), ('dd', ('ee', 'ff')))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (`name`, (col1, col2)) in (('aa', ('bb', 'cc')), ('dd', ('ee', 'ff')))",
-    "Table": "`user`",
     "Values": [
       "(VARCHAR(\"aa\"), VARCHAR(\"dd\"))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (`name`, (col1, col2)) in (('aa', ('bb', 'cc')), ('dd', ('ee', 'ff')))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -812,19 +1062,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where (col1, name) in (('aa', 1+1))",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "MultiEqual",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where (col1, `name`) in (('aa', 1 + 1))",
-    "Table": "`user`",
     "Values": [
       "(INT64(2))"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where (col1, `name`) in (('aa', 1 + 1))",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -928,19 +1203,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where name = :a",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where `name` = :a",
-    "Table": "`user`",
     "Values": [
       ":a"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where `name` = :a",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -972,19 +1272,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where name = 18446744073709551615",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where `name` = 18446744073709551615",
-    "Table": "`user`",
     "Values": [
       "UINT64(18446744073709551615)"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where `name` = 18446744073709551615",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -1016,19 +1341,44 @@
   "QueryType": "SELECT",
   "Original": "select id from user where name in ::list",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "IN",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where `name` in ::__vals",
-    "Table": "`user`",
     "Values": [
       ":list"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where `name` in ::__vals",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -1622,19 +1972,44 @@ Gen4 plan same as above
   "QueryType": "SELECT",
   "Original": "select (id or col) as val from user where user.col = 5 and user.id in (1, 2) and user.name = 'aa'",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id or col as val from `user` where 1 != 1",
-    "Query": "select id or col as val from `user` where `user`.col = 5 and `user`.id in (1, 2) and `user`.`name` = 'aa'",
-    "Table": "`user`",
     "Values": [
       "VARCHAR(\"aa\")"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id or col as val from `user` where 1 != 1",
+        "Query": "select id or col as val from `user` where `user`.col = 5 and `user`.id in (1, 2) and `user`.`name` = 'aa'",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -1666,19 +2041,44 @@ Gen4 plan same as above
   "QueryType": "SELECT",
   "Original": "select id from user where user.col = false and user.id in (1, 2) and user.name = 'aa'",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select id from `user` where 1 != 1",
-    "Query": "select id from `user` where `user`.col = false and `user`.id in (1, 2) and `user`.`name` = 'aa'",
-    "Table": "`user`",
     "Values": [
       "VARCHAR(\"aa\")"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select id from `user` where 1 != 1",
+        "Query": "select id from `user` where `user`.col = false and `user`.id in (1, 2) and `user`.`name` = 'aa'",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
diff --git a/go/vt/vtgate/planbuilder/testdata/from_cases.txt b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
index a714276125..e6112bc7f0 100644
--- a/go/vt/vtgate/planbuilder/testdata/from_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
@@ -5000,19 +5000,44 @@ Gen4 plan same as above
   "QueryType": "SELECT",
   "Original": "select u.a from (select id as b, name from user) u(a, n) where u.n = 1",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
-    "Query": "select u.a from (select id as b, `name` from `user` where `name` = 1) as u(a, n)",
-    "Table": "`user`",
     "Values": [
       "INT64(1)"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
+        "Query": "select u.a from (select id as b, `name` from `user` where `name` = 1) as u(a, n)",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
@@ -5026,19 +5051,44 @@ Gen4 plan same as above
   "QueryType": "SELECT",
   "Original": "select u.a from (select id as b, name from user where b = 1) u(a, n) where u.n = 1",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
-    "Query": "select u.a from (select id as b, `name` from `user` where b = 1 and `name` = 1) as u(a, n)",
-    "Table": "`user`",
     "Values": [
       "INT64(1)"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
+        "Query": "select u.a from (select id as b, `name` from `user` where b = 1 and `name` = 1) as u(a, n)",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
diff --git a/go/vt/vtgate/planbuilder/testdata/schema_test.json b/go/vt/vtgate/planbuilder/testdata/schema_test.json
index 26992b77fb..ebd0b431b6 100644
--- a/go/vt/vtgate/planbuilder/testdata/schema_test.json
+++ b/go/vt/vtgate/planbuilder/testdata/schema_test.json
@@ -56,7 +56,12 @@
         },
         "name_user_map": {
           "type": "name_lkp_test",
-          "owner": "user"
+          "owner": "user",
+          "params": {
+            "table": "name_user_vdx",
+            "from": "name",
+            "to": "keyspace_id"
+          }
         },
         "email_user_map": {
           "type": "lookup_test",
@@ -318,6 +323,14 @@
               "name": "name_muticoltbl_map"
             }
           ]
+        },
+        "name_user_vdx": {
+          "column_vindexes": [
+            {
+              "column": "name",
+              "name": "user_index"
+            }
+          ]
         }
       }
     },
diff --git a/go/vt/vtgate/planbuilder/testdata/select_cases.txt b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
index 9321d8aac0..8ccba18005 100644
--- a/go/vt/vtgate/planbuilder/testdata/select_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
@@ -5341,3 +5341,167 @@ Gen4 plan same as above
     "user.user"
   ]
 }
+
+# SQL_CALC_FOUND_ROWS with vindex lookup
+"select SQL_CALC_FOUND_ROWS id, name from user where name = 'aa' order by id limit 2"
+{
+  "QueryType": "SELECT",
+  "Original": "select SQL_CALC_FOUND_ROWS id, name from user where name = 'aa' order by id limit 2",
+  "Instructions": {
+    "OperatorType": "SQL_CALC_FOUND_ROWS",
+    "Inputs": [
+      {
+        "OperatorType": "Limit",
+        "Count": "INT64(2)",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "Equal",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select id, `name`, weight_string(id) from `user` where 1 != 1",
+            "OrderBy": "(0|2) ASC",
+            "Query": "select id, `name`, weight_string(id) from `user` where `name` = 'aa' order by id asc limit :__upper_limit",
+            "ResultColumns": 2,
+            "Table": "`user`",
+            "Values": [
+              "VARCHAR(\"aa\")"
+            ],
+            "Vindex": "name_user_map"
+          }
+        ]
+      },
+      {
+        "OperatorType": "Aggregate",
+        "Variant": "Scalar",
+        "Aggregates": "sum_count(0) AS count",
+        "Inputs": [
+          {
+            "OperatorType": "Route",
+            "Variant": "Equal",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "FieldQuery": "select count(*) from `user` where 1 != 1",
+            "Query": "select count(*) from `user` where `name` = 'aa'",
+            "Table": "`user`",
+            "Values": [
+              "VARCHAR(\"aa\")"
+            ],
+            "Vindex": "name_user_map"
+          }
+        ]
+      }
+    ]
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select SQL_CALC_FOUND_ROWS id, name from user where name = 'aa' order by id limit 2",
+  "Instructions": {
+    "OperatorType": "SQL_CALC_FOUND_ROWS",
+    "Inputs": [
+      {
+        "OperatorType": "Limit",
+        "Count": "INT64(2)",
+        "Inputs": [
+          {
+            "OperatorType": "VindexLookup",
+            "Variant": "Equal",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "Values": [
+              "VARCHAR(\"aa\")"
+            ],
+            "Vindex": "name_user_map",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "IN",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+                "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+                "Table": "name_user_vdx",
+                "Values": [
+                  ":name"
+                ],
+                "Vindex": "user_index"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "ByDestination",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select id, `name`, weight_string(id) from `user` where 1 != 1",
+                "OrderBy": "(0|2) ASC",
+                "Query": "select id, `name`, weight_string(id) from `user` where `name` = 'aa' order by id asc limit :__upper_limit",
+                "ResultColumns": 2,
+                "Table": "`user`"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "OperatorType": "Aggregate",
+        "Variant": "Scalar",
+        "Aggregates": "sum_count_star(0) AS count(*)",
+        "Inputs": [
+          {
+            "OperatorType": "VindexLookup",
+            "Variant": "Equal",
+            "Keyspace": {
+              "Name": "user",
+              "Sharded": true
+            },
+            "Values": [
+              "VARCHAR(\"aa\")"
+            ],
+            "Vindex": "name_user_map",
+            "Inputs": [
+              {
+                "OperatorType": "Route",
+                "Variant": "IN",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+                "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+                "Table": "name_user_vdx",
+                "Values": [
+                  ":name"
+                ],
+                "Vindex": "user_index"
+              },
+              {
+                "OperatorType": "Route",
+                "Variant": "ByDestination",
+                "Keyspace": {
+                  "Name": "user",
+                  "Sharded": true
+                },
+                "FieldQuery": "select count(*) from `user` where 1 != 1",
+                "Query": "select count(*) from `user` where `name` = 'aa'",
+                "Table": "`user`"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  "TablesUsed": [
+    "user.user"
+  ]
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/tpch_cases.txt b/go/vt/vtgate/planbuilder/testdata/tpch_cases.txt
index 85d69244e4..e0de875dbd 100644
--- a/go/vt/vtgate/planbuilder/testdata/tpch_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/tpch_cases.txt
@@ -168,19 +168,44 @@ Gen4 error: unsupported: cross-shard correlated subquery
                 "Table": "orders"
               },
               {
-                "OperatorType": "Route",
+                "OperatorType": "VindexLookup",
                 "Variant": "EqualUnique",
                 "Keyspace": {
                   "Name": "main",
                   "Sharded": true
                 },
-                "FieldQuery": "select 1 from lineitem where 1 != 1",
-                "Query": "select 1 from lineitem where l_commitdate \u003c l_receiptdate and l_orderkey = :o_orderkey",
-                "Table": "lineitem",
                 "Values": [
                   ":o_orderkey"
                 ],
-                "Vindex": "lineitem_map"
+                "Vindex": "lineitem_map",
+                "Inputs": [
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "IN",
+                    "Keyspace": {
+                      "Name": "main",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select l_orderkey, l_linenumber from lineitem_map where 1 != 1",
+                    "Query": "select l_orderkey, l_linenumber from lineitem_map where l_orderkey in ::__vals",
+                    "Table": "lineitem_map",
+                    "Values": [
+                      ":l_orderkey"
+                    ],
+                    "Vindex": "md5"
+                  },
+                  {
+                    "OperatorType": "Route",
+                    "Variant": "ByDestination",
+                    "Keyspace": {
+                      "Name": "main",
+                      "Sharded": true
+                    },
+                    "FieldQuery": "select 1 from lineitem where 1 != 1",
+                    "Query": "select 1 from lineitem where l_commitdate \u003c l_receiptdate and l_orderkey = :o_orderkey",
+                    "Table": "lineitem"
+                  }
+                ]
               }
             ]
           }
@@ -291,19 +316,44 @@ Gen4 error: unsupported: cross-shard correlated subquery
                             "TableName": "lineitem_supplier",
                             "Inputs": [
                               {
-                                "OperatorType": "Route",
+                                "OperatorType": "VindexLookup",
                                 "Variant": "EqualUnique",
                                 "Keyspace": {
                                   "Name": "main",
                                   "Sharded": true
                                 },
-                                "FieldQuery": "select l_suppkey, sum(l_extendedprice * (1 - l_discount)) as revenue, weight_string(l_suppkey) from lineitem where 1 != 1 group by l_suppkey, weight_string(l_suppkey)",
-                                "Query": "select l_suppkey, sum(l_extendedprice * (1 - l_discount)) as revenue, weight_string(l_suppkey) from lineitem where l_orderkey = :o_orderkey group by l_suppkey, weight_string(l_suppkey)",
-                                "Table": "lineitem",
                                 "Values": [
                                   ":o_orderkey"
                                 ],
-                                "Vindex": "lineitem_map"
+                                "Vindex": "lineitem_map",
+                                "Inputs": [
+                                  {
+                                    "OperatorType": "Route",
+                                    "Variant": "IN",
+                                    "Keyspace": {
+                                      "Name": "main",
+                                      "Sharded": true
+                                    },
+                                    "FieldQuery": "select l_orderkey, l_linenumber from lineitem_map where 1 != 1",
+                                    "Query": "select l_orderkey, l_linenumber from lineitem_map where l_orderkey in ::__vals",
+                                    "Table": "lineitem_map",
+                                    "Values": [
+                                      ":l_orderkey"
+                                    ],
+                                    "Vindex": "md5"
+                                  },
+                                  {
+                                    "OperatorType": "Route",
+                                    "Variant": "ByDestination",
+                                    "Keyspace": {
+                                      "Name": "main",
+                                      "Sharded": true
+                                    },
+                                    "FieldQuery": "select l_suppkey, sum(l_extendedprice * (1 - l_discount)) as revenue, weight_string(l_suppkey) from lineitem where 1 != 1 group by l_suppkey, weight_string(l_suppkey)",
+                                    "Query": "select l_suppkey, sum(l_extendedprice * (1 - l_discount)) as revenue, weight_string(l_suppkey) from lineitem where l_orderkey = :o_orderkey group by l_suppkey, weight_string(l_suppkey)",
+                                    "Table": "lineitem"
+                                  }
+                                ]
                               },
                               {
                                 "OperatorType": "Route",
@@ -709,19 +759,44 @@ Gen4 error: aggregation on columns from different sources not supported yet
                                 "Table": "orders"
                               },
                               {
-                                "OperatorType": "Route",
+                                "OperatorType": "VindexLookup",
                                 "Variant": "EqualUnique",
                                 "Keyspace": {
                                   "Name": "main",
                                   "Sharded": true
                                 },
-                                "FieldQuery": "select 1, sum(l_extendedprice * (1 - l_discount)) as revenue from lineitem where 1 != 1 group by 1",
-                                "Query": "select 1, sum(l_extendedprice * (1 - l_discount)) as revenue from lineitem where l_returnflag = 'R' and l_orderkey = :o_orderkey group by 1",
-                                "Table": "lineitem",
                                 "Values": [
                                   ":o_orderkey"
                                 ],
-                                "Vindex": "lineitem_map"
+                                "Vindex": "lineitem_map",
+                                "Inputs": [
+                                  {
+                                    "OperatorType": "Route",
+                                    "Variant": "IN",
+                                    "Keyspace": {
+                                      "Name": "main",
+                                      "Sharded": true
+                                    },
+                                    "FieldQuery": "select l_orderkey, l_linenumber from lineitem_map where 1 != 1",
+                                    "Query": "select l_orderkey, l_linenumber from lineitem_map where l_orderkey in ::__vals",
+                                    "Table": "lineitem_map",
+                                    "Values": [
+                                      ":l_orderkey"
+                                    ],
+                                    "Vindex": "md5"
+                                  },
+                                  {
+                                    "OperatorType": "Route",
+                                    "Variant": "ByDestination",
+                                    "Keyspace": {
+                                      "Name": "main",
+                                      "Sharded": true
+                                    },
+                                    "FieldQuery": "select 1, sum(l_extendedprice * (1 - l_discount)) as revenue from lineitem where 1 != 1 group by 1",
+                                    "Query": "select 1, sum(l_extendedprice * (1 - l_discount)) as revenue from lineitem where l_returnflag = 'R' and l_orderkey = :o_orderkey group by 1",
+                                    "Table": "lineitem"
+                                  }
+                                ]
                               }
                             ]
                           },
@@ -839,19 +914,44 @@ Gen4 error: unsupported: in scatter query: complex aggregate expression
                     "Table": "orders"
                   },
                   {
-                    "OperatorType": "Route",
+                    "OperatorType": "VindexLookup",
                     "Variant": "EqualUnique",
                     "Keyspace": {
                       "Name": "main",
                       "Sharded": true
                     },
-                    "FieldQuery": "select count(*), l_shipmode, weight_string(l_shipmode) from lineitem where 1 != 1 group by l_shipmode, weight_string(l_shipmode)",
-                    "Query": "select count(*), l_shipmode, weight_string(l_shipmode) from lineitem where l_shipmode in ('MAIL', 'SHIP') and l_commitdate \u003c l_receiptdate and l_shipdate \u003c l_commitdate and l_receiptdate \u003e= date('1994-01-01') and l_receiptdate \u003c date('1994-01-01') + interval '1' year and l_orderkey = :o_orderkey group by l_shipmode, weight_string(l_shipmode)",
-                    "Table": "lineitem",
                     "Values": [
                       ":o_orderkey"
                     ],
-                    "Vindex": "lineitem_map"
+                    "Vindex": "lineitem_map",
+                    "Inputs": [
+                      {
+                        "OperatorType": "Route",
+                        "Variant": "IN",
+                        "Keyspace": {
+                          "Name": "main",
+                          "Sharded": true
+                        },
+                        "FieldQuery": "select l_orderkey, l_linenumber from lineitem_map where 1 != 1",
+                        "Query": "select l_orderkey, l_linenumber from lineitem_map where l_orderkey in ::__vals",
+                        "Table": "lineitem_map",
+                        "Values": [
+                          ":l_orderkey"
+                        ],
+                        "Vindex": "md5"
+                      },
+                      {
+                        "OperatorType": "Route",
+                        "Variant": "ByDestination",
+                        "Keyspace": {
+                          "Name": "main",
+                          "Sharded": true
+                        },
+                        "FieldQuery": "select count(*), l_shipmode, weight_string(l_shipmode) from lineitem where 1 != 1 group by l_shipmode, weight_string(l_shipmode)",
+                        "Query": "select count(*), l_shipmode, weight_string(l_shipmode) from lineitem where l_shipmode in ('MAIL', 'SHIP') and l_commitdate \u003c l_receiptdate and l_shipdate \u003c l_commitdate and l_receiptdate \u003e= date('1994-01-01') and l_receiptdate \u003c date('1994-01-01') + interval '1' year and l_orderkey = :o_orderkey group by l_shipmode, weight_string(l_shipmode)",
+                        "Table": "lineitem"
+                      }
+                    ]
                   }
                 ]
               }
diff --git a/go/vt/vtgate/planbuilder/testdata/tpch_schema_test.json b/go/vt/vtgate/planbuilder/testdata/tpch_schema_test.json
index 6934f03d25..523949d0be 100644
--- a/go/vt/vtgate/planbuilder/testdata/tpch_schema_test.json
+++ b/go/vt/vtgate/planbuilder/testdata/tpch_schema_test.json
@@ -314,8 +314,7 @@
               "column": "l_orderkey",
               "name": "md5"
             }
-          ],
-          "column_list_authoritative": true
+          ]
         },
         "revenue0": {
           "column_vindexes": [
diff --git a/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt b/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
index 1b64599dd6..becba8162f 100644
--- a/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/wireup_cases.txt
@@ -1590,19 +1590,44 @@
   "QueryType": "SELECT",
   "Original": "select /*vt+ PLANNER=gen4 */ u.a from (select id as b, name from user) u(a, n) where u.n = 1",
   "Instructions": {
-    "OperatorType": "Route",
+    "OperatorType": "VindexLookup",
     "Variant": "Equal",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
-    "Query": "select /*vt+ PLANNER=gen4 */ u.a from (select id as b, `name` from `user` where `name` = 1) as u(a, n)",
-    "Table": "`user`",
     "Values": [
       "INT64(1)"
     ],
-    "Vindex": "name_user_map"
+    "Vindex": "name_user_map",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "IN",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select `name`, keyspace_id from name_user_vdx where 1 != 1",
+        "Query": "select `name`, keyspace_id from name_user_vdx where `name` in ::__vals",
+        "Table": "name_user_vdx",
+        "Values": [
+          ":name"
+        ],
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "ByDestination",
+        "Keyspace": {
+          "Name": "user",
+          "Sharded": true
+        },
+        "FieldQuery": "select u.a from (select id as b, `name` from `user` where 1 != 1) as u(a, n) where 1 != 1",
+        "Query": "select /*vt+ PLANNER=gen4 */ u.a from (select id as b, `name` from `user` where `name` = 1) as u(a, n)",
+        "Table": "`user`"
+      }
+    ]
   },
   "TablesUsed": [
     "user.user"
diff --git a/go/vt/vtgate/vindexes/lookup_test.go b/go/vt/vtgate/vindexes/lookup_test.go
index 2151f8ce3f..538a009c9c 100644
--- a/go/vt/vtgate/vindexes/lookup_test.go
+++ b/go/vt/vtgate/vindexes/lookup_test.go
@@ -19,7 +19,6 @@ package vindexes
 import (
 	"context"
 	"errors"
-	"reflect"
 	"testing"
 
 	"vitess.io/vitess/go/test/utils"
@@ -69,14 +68,14 @@ func (vc *vcursor) Execute(ctx context.Context, method string, query string, bin
 	case vtgatepb.CommitOrder_AUTOCOMMIT:
 		vc.autocommits++
 	}
-	return vc.execute(method, query, bindvars, rollbackOnError)
+	return vc.execute(query, bindvars)
 }
 
 func (vc *vcursor) ExecuteKeyspaceID(ctx context.Context, keyspace string, ksid []byte, query string, bindVars map[string]*querypb.BindVariable, rollbackOnError, autocommit bool) (*sqltypes.Result, error) {
-	return vc.execute("ExecuteKeyspaceID", query, bindVars, rollbackOnError)
+	return vc.execute(query, bindVars)
 }
 
-func (vc *vcursor) execute(method string, query string, bindvars map[string]*querypb.BindVariable, rollbackOnError bool) (*sqltypes.Result, error) {
+func (vc *vcursor) execute(query string, bindvars map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
 	vc.queries = append(vc.queries, &querypb.BoundQuery{
 		Sql:           query,
 		BindVariables: bindvars,
@@ -115,14 +114,10 @@ func (vc *vcursor) execute(method string, query string, bindvars map[string]*que
 
 func TestLookupNonUniqueNew(t *testing.T) {
 	l := createLookup(t, "lookup", false /* writeOnly */)
-	if want, got := l.(*LookupNonUnique).writeOnly, false; got != want {
-		t.Errorf("Create(lookup, false): %v, want %v", got, want)
-	}
+	assert.False(t, l.(*LookupNonUnique).writeOnly, "Create(lookup, false)")
 
 	l = createLookup(t, "lookup", true)
-	if want, got := l.(*LookupNonUnique).writeOnly, true; got != want {
-		t.Errorf("Create(lookup, false): %v, want %v", got, want)
-	}
+	assert.True(t, l.(*LookupNonUnique).writeOnly, "Create(lookup, false)")
 
 	_, err := CreateVindex("lookup", "lookup", map[string]string{
 		"table":      "t",
@@ -130,10 +125,7 @@ func TestLookupNonUniqueNew(t *testing.T) {
 		"to":         "toc",
 		"write_only": "invalid",
 	})
-	want := "write_only value must be 'true' or 'false': 'invalid'"
-	if err == nil || err.Error() != want {
-		t.Errorf("Create(bad_scatter): %v, want %s", err, want)
-	}
+	require.EqualError(t, err, "write_only value must be 'true' or 'false': 'invalid'")
 }
 
 func TestLookupNonUniqueInfo(t *testing.T) {
@@ -146,12 +138,8 @@ func TestLookupNonUniqueInfo(t *testing.T) {
 
 func TestLookupNilVCursor(t *testing.T) {
 	lookupNonUnique := createLookup(t, "lookup", false /* writeOnly */)
-
 	_, err := lookupNonUnique.Map(context.Background(), nil, []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)})
-	want := "cannot perform lookup: no vcursor provided"
-	if err == nil || err.Error() != want {
-		t.Errorf("Map(nil vcursor) err: %v, want %v", err, want)
-	}
+	require.EqualError(t, err, "cannot perform lookup: no vcursor provided")
 }
 
 func TestLookupNonUniqueMap(t *testing.T) {
@@ -170,9 +158,7 @@ func TestLookupNonUniqueMap(t *testing.T) {
 			[]byte("2"),
 		}),
 	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("Map(): %#v, want %+v", got, want)
-	}
+	utils.MustMatch(t, want, got)
 
 	vars, err := sqltypes.BuildBindVariable([]any{sqltypes.NewInt64(1), sqltypes.NewInt64(2)})
 	require.NoError(t, err)
@@ -187,11 +173,7 @@ func TestLookupNonUniqueMap(t *testing.T) {
 	// Test query fail.
 	vc.mustFail = true
 	_, err = lookupNonUnique.Map(context.Background(), vc, []sqltypes.Value{sqltypes.NewInt64(1)})
-	wantErr := "lookup.Map: execute failed"
-	if err == nil || err.Error() != wantErr {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, wantErr)
-	}
-	vc.mustFail = false
+	require.EqualError(t, err, "lookup.Map: execute failed")
 }
 
 func TestLookupNonUniqueMapAutocommit(t *testing.T) {
@@ -201,9 +183,7 @@ func TestLookupNonUniqueMapAutocommit(t *testing.T) {
 		"to":         "toc",
 		"autocommit": "true",
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 2}
 
@@ -219,9 +199,7 @@ func TestLookupNonUniqueMapAutocommit(t *testing.T) {
 			[]byte("2"),
 		}),
 	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("Map(): %#v, want %+v", got, want)
-	}
+	utils.MustMatch(t, want, got)
 
 	vars, err := sqltypes.BuildBindVariable([]any{sqltypes.NewInt64(1), sqltypes.NewInt64(2)})
 	require.NoError(t, err)
@@ -231,13 +209,8 @@ func TestLookupNonUniqueMapAutocommit(t *testing.T) {
 			"fromc": vars,
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Map queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
-
-	if got, want := vc.autocommits, 1; got != want {
-		t.Errorf("Create(autocommit) count: %d, want %d", got, want)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
+	assert.Equal(t, 1, vc.autocommits, "autocommits")
 }
 
 func TestLookupNonUniqueMapWriteOnly(t *testing.T) {
@@ -254,9 +227,7 @@ func TestLookupNonUniqueMapWriteOnly(t *testing.T) {
 			KeyRange: &topodatapb.KeyRange{},
 		},
 	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("Map(): %#v, want %+v", got, want)
-	}
+	utils.MustMatch(t, want, got)
 }
 
 func TestLookupNonUniqueMapAbsent(t *testing.T) {
@@ -269,9 +240,7 @@ func TestLookupNonUniqueMapAbsent(t *testing.T) {
 		key.DestinationNone{},
 		key.DestinationNone{},
 	}
-	if !reflect.DeepEqual(got, want) {
-		t.Errorf("Map(): %#v, want %+v", got, want)
-	}
+	utils.MustMatch(t, want, got)
 }
 
 func TestLookupNonUniqueVerify(t *testing.T) {
@@ -294,17 +263,12 @@ func TestLookupNonUniqueVerify(t *testing.T) {
 			"toc":   sqltypes.BytesBindVariable([]byte("test2")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Verify queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
 
 	// Test query fail.
 	vc.mustFail = true
 	_, err = lookupNonUnique.Verify(context.Background(), vc, []sqltypes.Value{sqltypes.NewInt64(1)}, [][]byte{[]byte("\x16k@\xb4J\xbaK\xd6")})
-	want := "lookup.Verify: execute failed"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	require.EqualError(t, err, "lookup.Verify: execute failed")
 	vc.mustFail = false
 
 	// writeOnly true should always yield true.
@@ -313,13 +277,8 @@ func TestLookupNonUniqueVerify(t *testing.T) {
 
 	got, err := lookupNonUnique.Verify(context.Background(), vc, []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}, [][]byte{[]byte(""), []byte("")})
 	require.NoError(t, err)
-	if vc.queries != nil {
-		t.Errorf("lookup.Verify(writeOnly), queries: %v, want nil", vc.queries)
-	}
-	wantBools := []bool{true, true}
-	if !reflect.DeepEqual(got, wantBools) {
-		t.Errorf("lookup.Verify(writeOnly): %v, want %v", got, wantBools)
-	}
+	assert.Empty(t, vc.queries, "lookup verify queries")
+	utils.MustMatch(t, []bool{true, true}, got)
 }
 
 func TestLookupNonUniqueVerifyAutocommit(t *testing.T) {
@@ -329,9 +288,7 @@ func TestLookupNonUniqueVerifyAutocommit(t *testing.T) {
 		"to":         "toc",
 		"autocommit": "true",
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	lookupNonUnique := vindex.(SingleColumn)
 	vc := &vcursor{numRows: 1}
 
@@ -351,13 +308,9 @@ func TestLookupNonUniqueVerifyAutocommit(t *testing.T) {
 			"toc":   sqltypes.BytesBindVariable([]byte("test2")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Verify queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
 
-	if got, want := vc.autocommits, 2; got != want {
-		t.Errorf("Create(autocommit) count: %d, want %d", got, want)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
+	assert.Equal(t, 2, vc.autocommits, "autocommits")
 }
 
 func TestLookupNonUniqueCreate(t *testing.T) {
@@ -376,25 +329,18 @@ func TestLookupNonUniqueCreate(t *testing.T) {
 			"toc_1":   sqltypes.BytesBindVariable([]byte("test2")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Create queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
 
 	// With ignore.
 	vc.queries = nil
 	err = lookupNonUnique.(Lookup).Create(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(2)}, {sqltypes.NewInt64(1)}}, [][]byte{[]byte("test2"), []byte("test1")}, true /* ignoreMode */)
 	require.NoError(t, err)
 	wantqueries[0].Sql = "insert ignore into t(fromc, toc) values(:fromc_0, :toc_0), (:fromc_1, :toc_1)"
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Create queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
 
 	// With ignore_nulls off
 	err = lookupNonUnique.(Lookup).Create(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(2)}, {sqltypes.NULL}}, [][]byte{[]byte("test2"), []byte("test1")}, true /* ignoreMode */)
-	want := "lookup.Create: input has null values: row: 1, col: 0"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	assert.EqualError(t, err, "lookup.Create: input has null values: row: 1, col: 0")
 
 	// With ignore_nulls on
 	vc.queries = nil
@@ -408,25 +354,17 @@ func TestLookupNonUniqueCreate(t *testing.T) {
 			"toc_0":   sqltypes.BytesBindVariable([]byte("test2")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Create queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
 
 	// Test query fail.
 	vc.mustFail = true
 	err = lookupNonUnique.(Lookup).Create(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(1)}}, [][]byte{[]byte("\x16k@\xb4J\xbaK\xd6")}, false /* ignoreMode */)
-	want = "lookup.Create: execute failed"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	assert.EqualError(t, err, "lookup.Create: execute failed")
 	vc.mustFail = false
 
 	// Test column mismatch.
 	err = lookupNonUnique.(Lookup).Create(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}}, [][]byte{[]byte("\x16k@\xb4J\xbaK\xd6")}, false /* ignoreMode */)
-	want = "lookup.Create: column vindex count does not match the columns in the lookup: 2 vs [fromc]"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	assert.EqualError(t, err, "lookup.Create: column vindex count does not match the columns in the lookup: 2 vs [fromc]")
 }
 
 func TestLookupNonUniqueCreateAutocommit(t *testing.T) {
@@ -436,9 +374,7 @@ func TestLookupNonUniqueCreateAutocommit(t *testing.T) {
 		"to":         "toc",
 		"autocommit": "true",
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	vc := &vcursor{}
 
 	err = lookupNonUnique.(Lookup).Create(context.Background(), vc, [][]sqltypes.Value{{
@@ -459,13 +395,8 @@ func TestLookupNonUniqueCreateAutocommit(t *testing.T) {
 			"toc_1":   sqltypes.BytesBindVariable([]byte("test2")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Create queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
-
-	if got, want := vc.autocommits, 1; got != want {
-		t.Errorf("Create(autocommit) count: %d, want %d", got, want)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
+	assert.Equal(t, 1, vc.autocommits, "autocommits")
 }
 
 func TestLookupNonUniqueDelete(t *testing.T) {
@@ -488,25 +419,17 @@ func TestLookupNonUniqueDelete(t *testing.T) {
 			"toc":   sqltypes.BytesBindVariable([]byte("test")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Delete queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, wantqueries, vc.queries)
 
 	// Test query fail.
 	vc.mustFail = true
 	err = lookupNonUnique.(Lookup).Delete(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(1)}}, []byte("\x16k@\xb4J\xbaK\xd6"))
-	want := "lookup.Delete: execute failed"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	assert.EqualError(t, err, "lookup.Delete: execute failed")
 	vc.mustFail = false
 
 	// Test column count fail.
 	err = lookupNonUnique.(Lookup).Delete(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}}, []byte("\x16k@\xb4J\xbaK\xd6"))
-	want = "lookup.Delete: column vindex count does not match the columns in the lookup: 2 vs [fromc]"
-	if err == nil || err.Error() != want {
-		t.Errorf("lookupNonUnique(query fail) err: %v, want %s", err, want)
-	}
+	assert.EqualError(t, err, "lookup.Delete: column vindex count does not match the columns in the lookup: 2 vs [fromc]")
 }
 
 func TestLookupNonUniqueDeleteAutocommit(t *testing.T) {
@@ -521,10 +444,7 @@ func TestLookupNonUniqueDeleteAutocommit(t *testing.T) {
 	err := lookupNonUnique.(Lookup).Delete(context.Background(), vc, [][]sqltypes.Value{{sqltypes.NewInt64(1)}, {sqltypes.NewInt64(2)}}, []byte("test"))
 	require.NoError(t, err)
 
-	wantqueries := []*querypb.BoundQuery(nil)
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Delete queries:\n%v, want\n%v", vc.queries, wantqueries)
-	}
+	utils.MustMatch(t, []*querypb.BoundQuery(nil), vc.queries)
 }
 
 func TestLookupNonUniqueUpdate(t *testing.T) {
@@ -547,9 +467,57 @@ func TestLookupNonUniqueUpdate(t *testing.T) {
 			"toc_0":   sqltypes.BytesBindVariable([]byte("test")),
 		},
 	}}
-	if !reflect.DeepEqual(vc.queries, wantqueries) {
-		t.Errorf("lookup.Update queries:\n%v, want\n%v", vc.queries, wantqueries)
+	utils.MustMatch(t, wantqueries, vc.queries)
+}
+
+func TestLookupMapResult(t *testing.T) {
+	lookup := createLookup(t, "lookup", false)
+
+	ids := []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}
+	results := []*sqltypes.Result{{
+		Fields:       sqltypes.MakeTestFields("key|col", "int64|int32"),
+		RowsAffected: 2,
+		Rows: []sqltypes.Row{
+			{sqltypes.NewInt64(1), sqltypes.NewInt64(3)},
+			{sqltypes.NewInt64(3), sqltypes.NewInt64(4)},
+			{sqltypes.NewInt64(5), sqltypes.NewInt64(6)},
+		},
+	}}
+
+	got, err := lookup.(LookupPlanable).MapResult(ids, results)
+	require.NoError(t, err)
+	want := []key.Destination{
+		key.DestinationKeyspaceIDs([][]byte{
+			[]byte("1"),
+			[]byte("3"),
+			[]byte("5"),
+		}),
+	}
+	utils.MustMatch(t, want, got)
+}
+
+func TestLookupUniqueMapResult(t *testing.T) {
+	lookup := createLookup(t, "lookup_unique", false)
+
+	ids := []sqltypes.Value{sqltypes.NewInt64(1), sqltypes.NewInt64(2)}
+	results := []*sqltypes.Result{{
+		Fields:       sqltypes.MakeTestFields("key|col", "int64|int32"),
+		RowsAffected: 2,
+		Rows: []sqltypes.Row{
+			{sqltypes.NewInt64(1), sqltypes.NewInt64(3)},
+		},
+	}}
+
+	got, err := lookup.(LookupPlanable).MapResult(ids, results)
+	require.NoError(t, err)
+	want := []key.Destination{
+		key.DestinationKeyspaceID("1"),
 	}
+	utils.MustMatch(t, want, got)
+
+	results[0].Rows = append(results[0].Rows, results[0].Rows...)
+	_, err = lookup.(LookupPlanable).MapResult(ids, results)
+	require.Error(t, err)
 }
 
 func TestLookupNonUniqueCreateMultiShardAutocommit(t *testing.T) {
@@ -596,8 +564,6 @@ func createLookup(t *testing.T, name string, writeOnly bool) SingleColumn {
 		"to":         "toc",
 		"write_only": write,
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 	return l.(SingleColumn)
 }