Repository: vitessio/vitess
Pull request #8742 (2021-08-30 17:00:09)

Gen4: support for Vindex queries
## Description

This pull request adds support for Vindex queries in Gen4.

## Related Issue(s)
- #7280 

## Checklist
- [x] Tests were added or are not required
- [x] Documentation was added or is not required



Patch:
diff --git a/go/vt/vtgate/planbuilder/abstract/operator.go b/go/vt/vtgate/planbuilder/abstract/operator.go
index eb75ebca9f..265d8002db 100644
--- a/go/vt/vtgate/planbuilder/abstract/operator.go
+++ b/go/vt/vtgate/planbuilder/abstract/operator.go
@@ -32,6 +32,7 @@ type (
 	//	*  LeftJoin - A left join. These can't be evaluated in any order, so we keep them separate
 	//	*  Join - A join represents inner join.
 	//  *  SubQuery - Represents a query that encapsulates one or more sub-queries (SubQueryInner).
+	//  *  Vindex - Represents a query that selects from vindex tables.
 	//  *  Concatenate - Represents concatenation of the outputs of all the input sources
 	//  *  Distinct - Represents elimination of duplicates from the output of the input source
 	Operator interface {
@@ -52,12 +53,21 @@ func getOperatorFromTableExpr(tableExpr sqlparser.TableExpr, semTable *semantics
 	case *sqlparser.AliasedTableExpr:
 		switch tbl := tableExpr.Expr.(type) {
 		case sqlparser.TableName:
-			qg := newQueryGraph()
 			tableID := semTable.TableSetFor(tableExpr)
 			tableInfo, err := semTable.TableInfoFor(tableID)
 			if err != nil {
 				return nil, err
 			}
+
+			if vt, isVindex := tableInfo.(*semantics.VindexTable); isVindex {
+				return &Vindex{Table: VindexTable{
+					TableID: tableID,
+					Alias:   tableExpr,
+					Table:   tbl,
+					VTable:  vt.Table.GetVindexTable(),
+				}, Vindex: vt.Vindex}, nil
+			}
+			qg := newQueryGraph()
 			isInfSchema := tableInfo.IsInfSchema()
 			qt := &QueryTable{Alias: tableExpr, Table: tbl, TableID: tableID, IsInfSchema: isInfSchema}
 			qg.Tables = append(qg.Tables, qt)
@@ -83,9 +93,11 @@ func getOperatorFromTableExpr(tableExpr sqlparser.TableExpr, semTable *semantics
 				return nil, err
 			}
 			op := createJoin(lhs, rhs)
-			err = op.PushPredicate(tableExpr.Condition.On, semTable)
-			if err != nil {
-				return nil, err
+			if tableExpr.Condition.On != nil {
+				err = op.PushPredicate(tableExpr.Condition.On, semTable)
+				if err != nil {
+					return nil, err
+				}
 			}
 			return op, nil
 		case sqlparser.LeftJoinType, sqlparser.RightJoinType:
diff --git a/go/vt/vtgate/planbuilder/abstract/vindex.go b/go/vt/vtgate/planbuilder/abstract/vindex.go
new file mode 100644
index 0000000000..892fb802ee
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/abstract/vindex.go
@@ -0,0 +1,103 @@
+/*
+Copyright 2021 The Vitess Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package abstract
+
+import (
+	"vitess.io/vitess/go/sqltypes"
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/engine"
+	"vitess.io/vitess/go/vt/vtgate/semantics"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+)
+
+type (
+	// Vindex stores the information about the vindex query
+	Vindex struct {
+		OpCode engine.VindexOpcode
+		Table  VindexTable
+		Vindex vindexes.Vindex
+		Value  sqltypes.PlanValue
+	}
+
+	// VindexTable contains information about the vindex table we want to query
+	VindexTable struct {
+		TableID    semantics.TableSet
+		Alias      *sqlparser.AliasedTableExpr
+		Table      sqlparser.TableName
+		Predicates []sqlparser.Expr
+		VTable     *vindexes.Table
+	}
+)
+
+var _ Operator = (*Vindex)(nil)
+
+// TableID implements the Operator interface
+func (v Vindex) TableID() semantics.TableSet {
+	return v.Table.TableID
+}
+
+const vindexUnsupported = "unsupported: where clause for vindex function must be of the form id = <val>"
+
+// PushPredicate implements the Operator interface
+func (v *Vindex) PushPredicate(expr sqlparser.Expr, semTable *semantics.SemTable) error {
+	for _, e := range sqlparser.SplitAndExpression(nil, expr) {
+		deps := semTable.BaseTableDependencies(e)
+		if deps.NumberOfTables() > 1 {
+			return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, vindexUnsupported+" (multiple tables involved)")
+		}
+		// check if we already have a predicate
+		if v.OpCode != engine.VindexNone {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (multiple filters)")
+		}
+
+		// check LHS
+		comparison, ok := e.(*sqlparser.ComparisonExpr)
+		if !ok {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (not a comparison)")
+		}
+		if comparison.Operator != sqlparser.EqualOp {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (not equality)")
+		}
+		colname, ok := comparison.Left.(*sqlparser.ColName)
+		if !ok {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (lhs is not a column)")
+		}
+		if !colname.Name.EqualString("id") {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (lhs is not id)")
+		}
+
+		// check RHS
+		if !sqlparser.IsValue(comparison.Right) {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+" (rhs is not a value)")
+		}
+		var err error
+		v.Value, err = sqlparser.NewPlanValue(comparison.Right)
+		if err != nil {
+			return vterrors.Errorf(vtrpcpb.Code_INTERNAL, vindexUnsupported+": %v", err)
+		}
+		v.OpCode = engine.VindexMap
+		v.Table.Predicates = append(v.Table.Predicates, e)
+	}
+	return nil
+}
+
+// UnsolvedPredicates implements the Operator interface
+func (v Vindex) UnsolvedPredicates(semTable *semantics.SemTable) []sqlparser.Expr {
+	panic("implement me")
+}
diff --git a/go/vt/vtgate/planbuilder/horizon_planning.go b/go/vt/vtgate/planbuilder/horizon_planning.go
index 73eb03d672..bcde69a38e 100644
--- a/go/vt/vtgate/planbuilder/horizon_planning.go
+++ b/go/vt/vtgate/planbuilder/horizon_planning.go
@@ -213,6 +213,12 @@ func pushProjection(expr *sqlparser.AliasedExpr, plan logicalPlan, semTable *sem
 			}
 		}
 		return 0, false, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "cannot push projections in ordered aggregates")
+	case *vindexFunc:
+		i, err := node.SupplyProjection(expr, reuseCol)
+		if err != nil {
+			return 0, false, err
+		}
+		return i, true, nil
 	case *limit:
 		return pushProjection(expr, node.input, semTable, inner, reuseCol)
 	default:
diff --git a/go/vt/vtgate/planbuilder/querytree.go b/go/vt/vtgate/planbuilder/querytree.go
index d877eb73d1..bb6dfe5167 100644
--- a/go/vt/vtgate/planbuilder/querytree.go
+++ b/go/vt/vtgate/planbuilder/querytree.go
@@ -49,6 +49,10 @@ type (
 		tableNames() []string
 	}
 
+	vindexTable struct {
+		table abstract.VindexTable
+	}
+
 	joinTables struct {
 		lhs, rhs relation
 		pred     sqlparser.Expr
@@ -88,6 +92,11 @@ var _ relation = (*routeTable)(nil)
 var _ relation = (*joinTables)(nil)
 var _ relation = (parenTables)(nil)
 var _ relation = (*derivedTable)(nil)
+var _ relation = (*vindexTable)(nil)
+
+func (v vindexTable) tableID() semantics.TableSet { return v.table.TableID }
+
+func (v vindexTable) tableNames() []string { return []string{v.table.Table.Name.String()} }
 
 func (d *derivedTable) tableID() semantics.TableSet { return d.tables.tableID() }
 
diff --git a/go/vt/vtgate/planbuilder/querytree_transformers.go b/go/vt/vtgate/planbuilder/querytree_transformers.go
index 4e7f31e943..e973958448 100644
--- a/go/vt/vtgate/planbuilder/querytree_transformers.go
+++ b/go/vt/vtgate/planbuilder/querytree_transformers.go
@@ -44,6 +44,8 @@ func transformToLogicalPlan(ctx planningContext, tree queryTree) (logicalPlan, e
 		return transformConcatenatePlan(ctx, n)
 	case *distinctTree:
 		return transformDistinctPlan(ctx, n)
+	case *vindexTree:
+		return transformVindexTree(n)
 	}
 
 	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] unknown query tree encountered: %T", tree)
@@ -64,6 +66,34 @@ func pushDistinct(plan logicalPlan) error {
 	return nil
 }
 
+func transformVindexTree(n *vindexTree) (logicalPlan, error) {
+	single, ok := n.vindex.(vindexes.SingleColumn)
+	if !ok {
+		return nil, vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "multi-column vindexes not supported")
+	}
+	plan := &vindexFunc{
+		order:         1,
+		tableID:       n.solved,
+		resultColumns: nil,
+		eVindexFunc: &engine.VindexFunc{
+			Opcode: n.opCode,
+			Vindex: single,
+			Value:  n.value,
+		},
+	}
+
+	for _, col := range n.columns {
+		_, err := plan.SupplyProjection(&sqlparser.AliasedExpr{
+			Expr: col,
+			As:   sqlparser.ColIdent{},
+		}, false)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return plan, nil
+}
+
 func transformSubqueryTree(ctx planningContext, n *subqueryTree) (logicalPlan, error) {
 	innerPlan, err := transformToLogicalPlan(ctx, n.inner)
 	if err != nil {
diff --git a/go/vt/vtgate/planbuilder/route_planning.go b/go/vt/vtgate/planbuilder/route_planning.go
index bf7581b0b8..970bd13abe 100644
--- a/go/vt/vtgate/planbuilder/route_planning.go
+++ b/go/vt/vtgate/planbuilder/route_planning.go
@@ -74,7 +74,7 @@ func optimizeQuery(ctx planningContext, opTree abstract.Operator) (queryTree, er
 		if err != nil {
 			return nil, err
 		}
-		return mergeOrJoin(ctx, treeInner, treeOuter, []sqlparser.Expr{op.Exp}, true)
+		return mergeOrJoin(ctx, treeInner, treeOuter, sqlparser.SplitAndExpression(nil, op.Exp), true)
 	case *abstract.Derived:
 		treeInner, err := optimizeQuery(ctx, op.Inner)
 		if err != nil {
@@ -87,6 +87,8 @@ func optimizeQuery(ctx planningContext, opTree abstract.Operator) (queryTree, er
 		}, nil
 	case *abstract.SubQuery:
 		return optimizeSubQuery(ctx, op)
+	case *abstract.Vindex:
+		return createVindexTree(ctx, op)
 	case *abstract.Concatenate:
 		return optimizeUnion(ctx, op)
 	case *abstract.Distinct:
@@ -119,6 +121,18 @@ func optimizeUnion(ctx planningContext, op *abstract.Concatenate) (queryTree, er
 	}, nil
 }
 
+func createVindexTree(ctx planningContext, op *abstract.Vindex) (*vindexTree, error) {
+	solves := ctx.semTable.TableSetFor(op.Table.Alias)
+	plan := &vindexTree{
+		opCode: op.OpCode,
+		solved: solves,
+		table:  vindexTable{table: op.Table},
+		vindex: op.Vindex,
+		value:  op.Value,
+	}
+	return plan, nil
+}
+
 func optimizeSubQuery(ctx planningContext, op *abstract.SubQuery) (queryTree, error) {
 	outerTree, err := optimizeQuery(ctx, op.Outer)
 	if err != nil {
@@ -384,6 +398,9 @@ func pushJoinPredicate(ctx planningContext, exprs []sqlparser.Expr, tree queryTr
 
 		plan.inner = newInner
 		return plan, nil
+	case *vindexTree:
+		// vindexFunc cannot accept predicates from the other side of a join
+		return node, nil
 	default:
 		panic(fmt.Sprintf("BUG: unknown type %T", node))
 	}
diff --git a/go/vt/vtgate/planbuilder/vindex_func.go b/go/vt/vtgate/planbuilder/vindex_func.go
index 217c0fc8c4..bf7ed7e912 100644
--- a/go/vt/vtgate/planbuilder/vindex_func.go
+++ b/go/vt/vtgate/planbuilder/vindex_func.go
@@ -37,6 +37,9 @@ var _ logicalPlan = (*vindexFunc)(nil)
 type vindexFunc struct {
 	order int
 
+	// the tableID field is only used by the gen4 planner
+	tableID semantics.TableSet
+
 	// resultColumns represent the columns returned by this route.
 	resultColumns []*resultColumn
 
@@ -98,7 +101,7 @@ func (vf *vindexFunc) Wireup(logicalPlan, *jointab) error {
 	return nil
 }
 
-// Wireup2 implements the logicalPlan interface
+// WireupGen4 implements the logicalPlan interface
 func (vf *vindexFunc) WireupGen4(*semantics.SemTable) error {
 	return nil
 }
@@ -131,6 +134,43 @@ func (vf *vindexFunc) SupplyCol(col *sqlparser.ColName) (rc *resultColumn, colNu
 	return rc, len(vf.resultColumns) - 1
 }
 
+// SupplyProjection pushes the given aliased expression into the fields and cols slices of the
+// vindexFunc engine primitive. The method returns the offset of the new expression in the columns
+// list.
+func (vf *vindexFunc) SupplyProjection(expr *sqlparser.AliasedExpr, reuse bool) (int, error) {
+	colName, isColName := expr.Expr.(*sqlparser.ColName)
+	if !isColName {
+		return 0, vterrors.New(vtrpcpb.Code_INTERNAL, "unsupported: expression on results of a vindex function")
+	}
+
+	enum := vindexColumnToIndex(colName)
+	if enum == -1 {
+		return 0, vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "unknown vindex column: %s", colName.Name.String())
+	}
+
+	if reuse {
+		for i, col := range vf.eVindexFunc.Cols {
+			if col == enum {
+				return i, nil
+			}
+		}
+	}
+
+	var name string
+	if expr.As.IsEmpty() {
+		name = sqlparser.String(colName)
+	} else {
+		name = expr.As.String()
+	}
+
+	vf.eVindexFunc.Fields = append(vf.eVindexFunc.Fields, &querypb.Field{
+		Name: name,
+		Type: querypb.Type_VARBINARY,
+	})
+	vf.eVindexFunc.Cols = append(vf.eVindexFunc.Cols, enum)
+	return len(vf.eVindexFunc.Cols) - 1, nil
+}
+
 // UnsupportedSupplyWeightString represents the error where the supplying a weight string is not supported
 type UnsupportedSupplyWeightString struct {
 	Type string
@@ -156,10 +196,29 @@ func (vf *vindexFunc) Rewrite(inputs ...logicalPlan) error {
 
 // ContainsTables implements the logicalPlan interface
 func (vf *vindexFunc) ContainsTables() semantics.TableSet {
-	return 0
+	return vf.tableID
 }
 
 // Inputs implements the logicalPlan interface
 func (vf *vindexFunc) Inputs() []logicalPlan {
 	return []logicalPlan{}
 }
+
+func vindexColumnToIndex(column *sqlparser.ColName) int {
+	switch column.Name.String() {
+	case "id":
+		return 0
+	case "keyspace_id":
+		return 1
+	case "range_start":
+		return 2
+	case "range_end":
+		return 3
+	case "hex_keyspace_id":
+		return 4
+	case "shard":
+		return 5
+	default:
+		return -1
+	}
+}
diff --git a/go/vt/vtgate/planbuilder/vindextree.go b/go/vt/vtgate/planbuilder/vindextree.go
new file mode 100644
index 0000000000..5d152a6285
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/vindextree.go
@@ -0,0 +1,65 @@
+/*
+Copyright 2021 The Vitess Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package planbuilder
+
+import (
+	"vitess.io/vitess/go/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/vindexes"
+)
+
+type vindexTree struct {
+	opCode  engine.VindexOpcode
+	table   vindexTable
+	vindex  vindexes.Vindex
+	solved  semantics.TableSet
+	columns []*sqlparser.ColName
+	value   sqltypes.PlanValue
+}
+
+var _ queryTree = (*vindexTree)(nil)
+
+func (v *vindexTree) tableID() semantics.TableSet {
+	return v.solved
+}
+
+func (v *vindexTree) clone() queryTree {
+	clone := *v
+	return &clone
+}
+
+func (v *vindexTree) cost() int {
+	return int(engine.SelectEqualUnique)
+}
+
+func (v *vindexTree) pushOutputColumns(col []*sqlparser.ColName, _ *semantics.SemTable) ([]int, error) {
+	idxs := make([]int, len(col))
+outer:
+	for i, newCol := range col {
+		for j, existingCol := range v.columns {
+			if sqlparser.EqualsExpr(newCol, existingCol) {
+				idxs[i] = j
+				continue outer
+			}
+		}
+		idxs[i] = len(v.columns)
+		v.columns = append(v.columns, newCol)
+	}
+	return idxs, nil
+}
diff --git a/go/vt/vtgate/semantics/FakeSI.go b/go/vt/vtgate/semantics/FakeSI.go
index 41f54b4ba2..7ecf958a58 100644
--- a/go/vt/vtgate/semantics/FakeSI.go
+++ b/go/vt/vtgate/semantics/FakeSI.go
@@ -27,10 +27,15 @@ var _ SchemaInformation = (*FakeSI)(nil)
 
 // FakeSI is a fake SchemaInformation for testing
 type FakeSI struct {
-	Tables map[string]*vindexes.Table
+	Tables       map[string]*vindexes.Table
+	VindexTables map[string]vindexes.Vindex
 }
 
 // FindTableOrVindex implements the SchemaInformation interface
 func (s *FakeSI) FindTableOrVindex(tablename sqlparser.TableName) (*vindexes.Table, vindexes.Vindex, string, topodatapb.TabletType, key.Destination, error) {
-	return s.Tables[sqlparser.String(tablename)], nil, "", 0, nil, nil
+	table, ok := s.Tables[sqlparser.String(tablename)]
+	if ok {
+		return table, nil, "", 0, nil, nil
+	}
+	return nil, s.VindexTables[sqlparser.String(tablename)], "", 0, nil, nil
 }
diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go
index a48ff6d5ee..99f32d509d 100644
--- a/go/vt/vtgate/semantics/semantic_state.go
+++ b/go/vt/vtgate/semantics/semantic_state.go
@@ -81,6 +81,14 @@ type (
 		cols        []sqlparser.Expr
 	}
 
+	// VindexTable contains a vindexes.Vindex and a TableInfo. The former represents the vindex
+	// we are keeping information about, and the latter represents the additional table information
+	// (usually a RealTable or an AliasedTable) of our vindex.
+	VindexTable struct {
+		Table  TableInfo
+		Vindex vindexes.Vindex
+	}
+
 	// TableSet is how a set of tables is expressed.
 	// Tables get unique bits assigned in the order that they are encountered during semantic analysis
 	TableSet uint64 // we can only join 64 tables with this underlying data type
@@ -139,6 +147,11 @@ type (
 	}
 )
 
+// GetExprFor implements the TableInfo interface
+func (v *VindexTable) GetExprFor(s string) (sqlparser.Expr, error) {
+	panic("implement me")
+}
+
 // CopyDependencies copies the dependencies from one expression into the other
 func (st *SemTable) CopyDependencies(from, to sqlparser.Expr) {
 	st.ExprBaseTableDeps[to] = st.BaseTableDependencies(from)
@@ -303,6 +316,7 @@ func (r *RealTable) IsActualTable() bool {
 var _ TableInfo = (*RealTable)(nil)
 var _ TableInfo = (*AliasedTable)(nil)
 var _ TableInfo = (*vTableInfo)(nil)
+var _ TableInfo = (*VindexTable)(nil)
 
 func (v *vTableInfo) Matches(name sqlparser.TableName) bool {
 	return v.tableName == name.Name.String() && name.Qualifier.IsEmpty()
@@ -325,6 +339,11 @@ func (v *vTableInfo) GetVindexTable() *vindexes.Table {
 	return nil
 }
 
+// GetVindexTable implements the TableInfo interface
+func (v *VindexTable) GetVindexTable() *vindexes.Table {
+	return v.Table.GetVindexTable()
+}
+
 func (v *vTableInfo) GetColumns() []ColumnInfo {
 	cols := make([]ColumnInfo, 0, len(v.columnNames))
 	for _, col := range v.columnNames {
@@ -432,6 +451,51 @@ func (r *RealTable) Matches(name sqlparser.TableName) bool {
 	return r.tableName == name.Name.String()
 }
 
+// Matches implements the TableInfo interface
+func (v *VindexTable) Matches(name sqlparser.TableName) bool {
+	return v.Table.Matches(name)
+}
+
+// Authoritative implements the TableInfo interface
+func (v *VindexTable) Authoritative() bool {
+	return true
+}
+
+// Name implements the TableInfo interface
+func (v *VindexTable) Name() (sqlparser.TableName, error) {
+	return v.Table.Name()
+}
+
+// GetExpr implements the TableInfo interface
+func (v *VindexTable) GetExpr() *sqlparser.AliasedTableExpr {
+	return v.Table.GetExpr()
+}
+
+// GetColumns implements the TableInfo interface
+func (v *VindexTable) GetColumns() []ColumnInfo {
+	return v.Table.GetColumns()
+}
+
+// IsActualTable implements the TableInfo interface
+func (v *VindexTable) IsActualTable() bool {
+	return true
+}
+
+// RecursiveDepsFor implements the TableInfo interface
+func (v *VindexTable) RecursiveDepsFor(col *sqlparser.ColName, org originable, single bool) (*TableSet, *querypb.Type, error) {
+	return v.Table.RecursiveDepsFor(col, org, single)
+}
+
+// DepsFor implements the TableInfo interface
+func (v *VindexTable) DepsFor(col *sqlparser.ColName, org originable, single bool) (*TableSet, error) {
+	return v.Table.DepsFor(col, org, single)
+}
+
+// IsInfSchema implements the TableInfo interface
+func (v *VindexTable) IsInfSchema() bool {
+	return v.Table.IsInfSchema()
+}
+
 // NewSemTable creates a new empty SemTable
 func NewSemTable() *SemTable {
 	return &SemTable{ExprBaseTableDeps: map[sqlparser.Expr]TableSet{}, ColumnEqualities: map[columnName][]sqlparser.Expr{}}
diff --git a/go/vt/vtgate/semantics/table_collector.go b/go/vt/vtgate/semantics/table_collector.go
index b46adf5221..0201b834c4 100644
--- a/go/vt/vtgate/semantics/table_collector.go
+++ b/go/vt/vtgate/semantics/table_collector.go
@@ -78,21 +78,22 @@ func (tc *tableCollector) up(cursor *sqlparser.Cursor) error {
 
 	case sqlparser.TableName:
 		var tbl *vindexes.Table
+		var vindex vindexes.Vindex
 		var isInfSchema bool
 		if sqlparser.SystemSchema(t.Qualifier.String()) {
 			isInfSchema = true
 		} else {
-			table, vdx, _, _, _, err := tc.si.FindTableOrVindex(t)
+			var err error
+			tbl, vindex, _, _, _, err = tc.si.FindTableOrVindex(t)
 			if err != nil {
 				return err
 			}
-			tbl = table
-			if tbl == nil && vdx != nil {
-				return Gen4NotSupportedF("vindex in FROM")
+			if tbl == nil && vindex != nil {
+				tbl = newVindexTable(t.Name)
 			}
 		}
 		scope := tc.scoper.currentScope()
-		tableInfo := tc.createTable(t, node, tbl, isInfSchema)
+		tableInfo := tc.createTable(t, node, tbl, isInfSchema, vindex)
 
 		tc.Tables = append(tc.Tables, tableInfo)
 		return scope.addTable(tableInfo)
@@ -100,6 +101,23 @@ func (tc *tableCollector) up(cursor *sqlparser.Cursor) error {
 	return nil
 }
 
+func newVindexTable(t sqlparser.TableIdent) *vindexes.Table {
+	vindexCols := []vindexes.Column{
+		{Name: sqlparser.NewColIdent("id")},
+		{Name: sqlparser.NewColIdent("keyspace_id")},
+		{Name: sqlparser.NewColIdent("range_start")},
+		{Name: sqlparser.NewColIdent("range_end")},
+		{Name: sqlparser.NewColIdent("hex_keyspace_id")},
+		{Name: sqlparser.NewColIdent("shard")},
+	}
+
+	return &vindexes.Table{
+		Name:                    t,
+		Columns:                 vindexCols,
+		ColumnListAuthoritative: true,
+	}
+}
+
 // tabletSetFor implements the originable interface, and that is why it lives on the analyser struct.
 // The code lives in this file since it is only touching tableCollector data
 func (tc *tableCollector) tableSetFor(t *sqlparser.AliasedTableExpr) TableSet {
@@ -111,24 +129,40 @@ func (tc *tableCollector) tableSetFor(t *sqlparser.AliasedTableExpr) TableSet {
 	panic("unknown table")
 }
 
-func (tc *tableCollector) createTable(t sqlparser.TableName, alias *sqlparser.AliasedTableExpr, tbl *vindexes.Table, isInfSchema bool) TableInfo {
+func (tc *tableCollector) createTable(
+	t sqlparser.TableName,
+	alias *sqlparser.AliasedTableExpr,
+	tbl *vindexes.Table,
+	isInfSchema bool,
+	vindex vindexes.Vindex,
+) TableInfo {
 	dbName := t.Qualifier.String()
 	if dbName == "" {
 		dbName = tc.currentDb
 	}
+	var table TableInfo
 	if alias.As.IsEmpty() {
-		return &RealTable{
+		table = &RealTable{
 			dbName:      dbName,
 			tableName:   t.Name.String(),
 			ASTNode:     alias,
 			Table:       tbl,
 			isInfSchema: isInfSchema,
 		}
+	} else {
+		table = &AliasedTable{
+			tableName:   alias.As.String(),
+			ASTNode:     alias,
+			Table:       tbl,
+			isInfSchema: isInfSchema,
+		}
 	}
-	return &AliasedTable{
-		tableName:   alias.As.String(),
-		ASTNode:     alias,
-		Table:       tbl,
-		isInfSchema: isInfSchema,
+
+	if vindex != nil {
+		return &VindexTable{
+			Table:  table,
+			Vindex: vindex,
+		}
 	}
+	return table
 }


Test patch:
diff --git a/go/vt/vtgate/planbuilder/abstract/operator_test.go b/go/vt/vtgate/planbuilder/abstract/operator_test.go
index 8afed7a311..579cbbdae8 100644
--- a/go/vt/vtgate/planbuilder/abstract/operator_test.go
+++ b/go/vt/vtgate/planbuilder/abstract/operator_test.go
@@ -22,6 +22,8 @@ import (
 	"strings"
 	"testing"
 
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -237,6 +239,34 @@ JoinPredicates:
 	JoinPredicates:
 		1:2 - u.id = (select id from user_extra where id = u.id)
 	}
+}`,
+	}, {
+		input: "select id from user_index where id = :id",
+		output: `Vindex: {
+	Name: user_index
+	Value: id
+}`,
+	}, {
+		input: "select ui.id from user_index as ui join user as u where ui.id = 1 and ui.id = u.id",
+		output: `Join: {
+	LHS: 	Vindex: {
+		Name: user_index
+		Value: 1
+	}
+	RHS: 	QueryGraph: {
+	Tables:
+		2:` + "`user`" + ` AS u
+	}
+	Predicate: ui.id = u.id
+}`,
+	}, {
+		input: "select u.id from (select id from user_index where id = 2) as u",
+		output: `Derived u: {
+	Query: select id from user_index where id = 2
+	Inner:	Vindex: {
+		Name: user_index
+		Value: 2
+	}
 }`,
 	}, {
 		input: "select 1 from a union select 2 from b",
@@ -296,13 +326,15 @@ JoinPredicates:
 }`,
 	}}
 
+	hash, _ := vindexes.NewHash("user_index", map[string]string{})
+	si := &semantics.FakeSI{VindexTables: map[string]vindexes.Vindex{"user_index": hash}}
 	for i, tc := range tcases {
 		sql := tc.input
 		t.Run(fmt.Sprintf("%d %s", i, sql), func(t *testing.T) {
 			tree, err := sqlparser.Parse(sql)
 			require.NoError(t, err)
 			stmt := tree.(sqlparser.SelectStatement)
-			semTable, err := semantics.Analyze(stmt, "", &semantics.FakeSI{}, semantics.NoRewrite)
+			semTable, err := semantics.Analyze(stmt, "", si, semantics.NoRewrite)
 			require.NoError(t, err)
 			optree, err := CreateOperatorFromAST(stmt, semTable)
 			require.NoError(t, err)
@@ -338,6 +370,12 @@ func testString(op Operator) string {
 		}
 		outer := indent(testString(op.Outer))
 		return fmt.Sprintf("SubQuery: {\n\tSubQueries: %s\n\tOuter: %s\n}", inners, outer)
+	case *Vindex:
+		value := op.Value.Value.ToString()
+		if value == "" {
+			value = op.Value.Key
+		}
+		return fmt.Sprintf("Vindex: {\n\tName: %s\n\tValue: %s\n}", op.Vindex.String(), value)
 	case *Distinct:
 		inner := indent(testString(op.Source))
 		return fmt.Sprintf("Distinct {\n%s\n}", inner)
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index 0e2ee7b306..4e78335b18 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -330,27 +330,34 @@ Gen4 plan same as above
 
 "select keyspace_id from user_index where id = 1 and id = 2"
 "unsupported: where clause for vindex function must be of the form id = <val> (multiple filters)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index where func(id)"
 "unsupported: where clause for vindex function must be of the form id = <val> (not a comparison)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index where id > 1"
 "unsupported: where clause for vindex function must be of the form id = <val> (not equality)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index where 1 = id"
 "unsupported: where clause for vindex function must be of the form id = <val> (lhs is not a column)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index where keyspace_id = 1"
 "unsupported: where clause for vindex function must be of the form id = <val> (lhs is not id)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index where id = id+1"
 "unsupported: where clause for vindex function must be of the form id = <val> (rhs is not a value)"
+Gen4 plan same as above
 
 "select keyspace_id from user_index"
 "unsupported: where clause for vindex function must be of the form id = <val> (where clause missing)"
 
 "select func(keyspace_id) from user_index where id = :id"
 "unsupported: expression on results of a vindex function"
+Gen4 plan same as above
 
 # delete with multi-table targets
 "delete music,user from music inner join user where music.id = user.id"
diff --git a/go/vt/vtgate/planbuilder/testdata/vindex_func_cases.txt b/go/vt/vtgate/planbuilder/testdata/vindex_func_cases.txt
index f420659bea..aff957920f 100644
--- a/go/vt/vtgate/planbuilder/testdata/vindex_func_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/vindex_func_cases.txt
@@ -26,6 +26,7 @@
     "Vindex": "user_index"
   }
 }
+Gen4 plan same as above
 
 # vindex func select *
 "select * from user_index where id = :id"
@@ -55,6 +56,7 @@
     "Vindex": "user_index"
   }
 }
+Gen4 plan same as above
 
 # vindex func read with id repeated
 "select id, keyspace_id, id from user_index where id = :id"
@@ -77,6 +79,7 @@
     "Vindex": "user_index"
   }
 }
+Gen4 plan same as above
 
 # ambiguous vindex reference
 "select id, keyspace_id, id from hash_dup where id = :id"
@@ -104,6 +107,7 @@ Gen4 plan same as above
     "Vindex": "hash_dup"
   }
 }
+Gen4 plan same as above
 
 # You can even join with a vindexFunc primitive
 "select user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id"
@@ -142,6 +146,41 @@ Gen4 plan same as above
     ]
   }
 }
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-1,1",
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          1
+        ],
+        "Fields": {
+          "user_index.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
 
 # Join vindexFunc on RHS
 "select user_index.keyspace_id, unsharded.id from unsharded join user_index where user_index.id = :id"
@@ -180,6 +219,41 @@ Gen4 plan same as above
     ]
   }
 }
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.keyspace_id, unsharded.id from unsharded join user_index where user_index.id = :id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "1,-1",
+    "TableName": "unsharded_",
+    "Inputs": [
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded",
+        "Table": "unsharded"
+      },
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          1
+        ],
+        "Fields": {
+          "user_index.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      }
+    ]
+  }
+}
 
 # Join with vindexFunc on a column of it, already present in select list
 "select user_index.id, user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id"
@@ -223,6 +297,129 @@ Gen4 plan same as above
     ]
   }
 }
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.id, user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-1,-2,1",
+    "JoinVars": {
+      "user_index_id": 0
+    },
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          0,
+          1
+        ],
+        "Fields": {
+          "user_index.id": "VARBINARY",
+          "user_index.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded where unsharded.id = :user_index_id",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
+
+# Join with vindexFunc on a column of it, already present at the end of the select list
+"select user_index.keyspace_id, user_index.id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id"
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.keyspace_id, user_index.id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-1,-2,1",
+    "JoinVars": {
+      "user_index_id": 1
+    },
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          1,
+          0
+        ],
+        "Fields": {
+          "id": "VARBINARY",
+          "keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded where unsharded.id = :user_index_id",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.keyspace_id, user_index.id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-2,-1,1",
+    "JoinVars": {
+      "user_index_id": 0
+    },
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          0,
+          1
+        ],
+        "Fields": {
+          "user_index.id": "VARBINARY",
+          "user_index.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded where unsharded.id = :user_index_id",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
 
 # Join with vindexFunc on a column of it, not present in select list
 "select user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id"
@@ -266,6 +463,46 @@ Gen4 plan same as above
     ]
   }
 }
+{
+  "QueryType": "SELECT",
+  "Original": "select user_index.keyspace_id, unsharded.id from user_index join unsharded where user_index.id = :id and unsharded.id = user_index.id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-2,1",
+    "JoinVars": {
+      "user_index_id": 0
+    },
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          0,
+          1
+        ],
+        "Fields": {
+          "user_index.id": "VARBINARY",
+          "user_index.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded where unsharded.id = :user_index_id",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
 
 # Join with aliased table name
 "select ui.keyspace_id, unsharded.id from user_index ui join unsharded where ui.id = :id and unsharded.id = ui.id"
@@ -309,6 +546,47 @@ Gen4 plan same as above
     ]
   }
 }
+{
+  "QueryType": "SELECT",
+  "Original": "select ui.keyspace_id, unsharded.id from user_index ui join unsharded where ui.id = :id and unsharded.id = ui.id",
+  "Instructions": {
+    "OperatorType": "Join",
+    "Variant": "Join",
+    "JoinColumnIndexes": "-2,1",
+    "JoinVars": {
+      "ui_id": 0
+    },
+    "TableName": "_unsharded",
+    "Inputs": [
+      {
+        "OperatorType": "VindexFunc",
+        "Variant": "VindexMap",
+        "Columns": [
+          0,
+          1
+        ],
+        "Fields": {
+          "ui.id": "VARBINARY",
+          "ui.keyspace_id": "VARBINARY"
+        },
+        "Value": ":id",
+        "Vindex": "user_index"
+      },
+      {
+        "OperatorType": "Route",
+        "Variant": "SelectUnsharded",
+        "Keyspace": {
+          "Name": "main",
+          "Sharded": false
+        },
+        "FieldQuery": "select unsharded.id from unsharded where 1 != 1",
+        "Query": "select unsharded.id from unsharded where unsharded.id = :ui_id",
+        "Table": "unsharded"
+      }
+    ]
+  }
+}
 
 "select none from user_index where id = :id"
 "symbol `none` not found in table or subquery"
+"Unknown column 'none' in 'field list'"
diff --git a/go/vt/vtgate/semantics/analyzer_test.go b/go/vt/vtgate/semantics/analyzer_test.go
index 1a33793f94..e7251f181b 100644
--- a/go/vt/vtgate/semantics/analyzer_test.go
+++ b/go/vt/vtgate/semantics/analyzer_test.go
@@ -731,6 +731,48 @@ func TestScopingWDerivedTables(t *testing.T) {
 	}
 }
 
+func TestScopingWVindexTables(t *testing.T) {
+	queries := []struct {
+		query                string
+		errorMessage         string
+		recursiveExpectation TableSet
+		expectation          TableSet
+	}{
+		{
+			query:                "select id from user_index where id = 1",
+			recursiveExpectation: T1,
+			expectation:          T1,
+		}, {
+			query:                "select u.id + t.id from t as t join user_index as u where u.id = 1 and u.id = t.id",
+			recursiveExpectation: T1 | T2,
+			expectation:          T1 | T2,
+		},
+	}
+	for _, query := range queries {
+		t.Run(query.query, func(t *testing.T) {
+			parse, err := sqlparser.Parse(query.query)
+			require.NoError(t, err)
+			hash, _ := vindexes.NewHash("user_index", nil)
+			st, err := Analyze(parse.(sqlparser.SelectStatement), "user", &FakeSI{
+				Tables: map[string]*vindexes.Table{
+					"t": {Name: sqlparser.NewTableIdent("t")},
+				},
+				VindexTables: map[string]vindexes.Vindex{
+					"user_index": hash,
+				},
+			}, NoRewrite)
+			if query.errorMessage != "" {
+				require.EqualError(t, err, query.errorMessage)
+			} else {
+				require.NoError(t, err)
+				sel := parse.(*sqlparser.Select)
+				assert.Equal(t, query.recursiveExpectation, st.BaseTableDependencies(extract(sel, 0)))
+				assert.Equal(t, query.expectation, st.Dependencies(extract(sel, 0)))
+			}
+		})
+	}
+}
+
 func parseAndAnalyze(t *testing.T, query, dbName string) (sqlparser.Statement, *SemTable) {
 	t.Helper()
 	parse, err := sqlparser.Parse(query)