Repository: vitessio/vitess
Pull request #7775 (2021-04-01 09:13:19)

[Gen4] Initial Horizon Planning
## Description
Adds the beginnings of gen4 planning of `SELECT` expressions, including `ORDER BY`, `GROUP BY`, `DISTINCT` and `LIMIT`

## Impacted Areas in Vitess
Components that this PR will affect:

- [x]  Query Serving



Patch:
diff --git a/go/mysql/sql_error.go b/go/mysql/sql_error.go
index fcf02abaf1..491dd3e443 100644
--- a/go/mysql/sql_error.go
+++ b/go/mysql/sql_error.go
@@ -170,6 +170,7 @@ var stateToMysqlCode = map[vterrors.State]struct {
 	vterrors.NotSupportedYet:              {num: ERNotSupportedYet, state: SSClientError},
 	vterrors.ForbidSchemaChange:           {num: ERForbidSchemaChange, state: SSUnknownSQLState},
 	vterrors.NetPacketTooLarge:            {num: ERNetPacketTooLarge, state: SSNetError},
+	vterrors.NonUniqError:                 {num: ERNonUniq, state: SSConstraintViolation},
 	vterrors.NonUniqTable:                 {num: ERNonUniqTable, state: SSClientError},
 	vterrors.QueryInterrupted:             {num: ERQueryInterrupted, state: SSQueryInterrupted},
 	vterrors.SPDoesNotExist:               {num: ERSPDoesNotExist, state: SSClientError},
diff --git a/go/vt/vterrors/state.go b/go/vt/vterrors/state.go
index b3103cd851..4a9a33b122 100644
--- a/go/vt/vterrors/state.go
+++ b/go/vt/vterrors/state.go
@@ -30,6 +30,7 @@ const (
 	EmptyQuery
 	ForbidSchemaChange
 	IncorrectGlobalLocalVar
+	NonUniqError
 	NonUniqTable
 	SyntaxError
 	WrongGroupField
diff --git a/go/vt/vtgate/engine/route.go b/go/vt/vtgate/engine/route.go
index 904afce420..9f73a7f39f 100644
--- a/go/vt/vtgate/engine/route.go
+++ b/go/vt/vtgate/engine/route.go
@@ -739,6 +739,9 @@ func (route *Route) description() PrimitiveDescription {
 	if orderBy != "" {
 		other["OrderBy"] = orderBy
 	}
+	if route.TruncateColumnCount > 0 {
+		other["ResultColumns"] = route.TruncateColumnCount
+	}
 
 	return PrimitiveDescription{
 		OperatorType:      "Route",
diff --git a/go/vt/vtgate/planbuilder/queryprojection.go b/go/vt/vtgate/planbuilder/queryprojection.go
new file mode 100644
index 0000000000..e638d651e1
--- /dev/null
+++ b/go/vt/vtgate/planbuilder/queryprojection.go
@@ -0,0 +1,84 @@
+/*
+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 (
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/semantics"
+)
+
+type queryProjection struct {
+	selectExprs             []*sqlparser.AliasedExpr
+	aggrExprs               []*sqlparser.AliasedExpr
+	groupOrderingCommonExpr map[sqlparser.Expr]*sqlparser.Order
+
+	orderExprs sqlparser.OrderBy
+
+	// orderExprColMap keeps a map between the Order object and the offset into the select expressions list
+	orderExprColMap map[*sqlparser.Order]int
+}
+
+func newQueryProjection() *queryProjection {
+	return &queryProjection{
+		groupOrderingCommonExpr: map[sqlparser.Expr]*sqlparser.Order{},
+		orderExprColMap:         map[*sqlparser.Order]int{},
+	}
+}
+
+func createQPFromSelect(sel *sqlparser.Select) (*queryProjection, error) {
+	qp := newQueryProjection()
+
+	for _, selExp := range sel.SelectExprs {
+		exp, ok := selExp.(*sqlparser.AliasedExpr)
+		if !ok {
+			return nil, semantics.Gen4NotSupportedF("%T in select list", selExp)
+		}
+		fExpr, ok := exp.Expr.(*sqlparser.FuncExpr)
+		if ok && fExpr.IsAggregate() {
+			if len(fExpr.Exprs) != 1 {
+				return nil, vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.SyntaxError, "multiple arguments inside the function '%s'", sqlparser.String(fExpr))
+			}
+			qp.aggrExprs = append(qp.aggrExprs, exp)
+			continue
+		}
+		if nodeHasAggregates(exp.Expr) {
+			return nil, vterrors.New(vtrpcpb.Code_UNIMPLEMENTED, "unsupported: in scatter query: complex aggregate expression")
+		}
+		qp.selectExprs = append(qp.selectExprs, exp)
+	}
+
+	qp.orderExprs = sel.OrderBy
+
+	allExpr := append(qp.selectExprs, qp.aggrExprs...)
+	for _, order := range sel.OrderBy {
+		for offset, expr := range allExpr {
+			if sqlparser.EqualsExpr(order.Expr, expr.Expr) {
+				qp.orderExprColMap[order] = offset
+				break
+			}
+			// TODO: handle alias and column offset
+		}
+	}
+
+	if sel.GroupBy == nil || sel.OrderBy == nil {
+		return qp, nil
+	}
+
+	return qp, nil
+}
diff --git a/go/vt/vtgate/planbuilder/route_planning.go b/go/vt/vtgate/planbuilder/route_planning.go
index dc3bfb2e85..5724584651 100644
--- a/go/vt/vtgate/planbuilder/route_planning.go
+++ b/go/vt/vtgate/planbuilder/route_planning.go
@@ -46,7 +46,7 @@ func gen4Planner(_ string) func(sqlparser.Statement, sqlparser.BindVars, Context
 }
 
 func newBuildSelectPlan(sel *sqlparser.Select, vschema ContextVSchema) (engine.Primitive, error) {
-	semTable, err := semantics.Analyse(sel) // TODO no nil no
+	semTable, err := semantics.Analyse(sel, vschema)
 	if err != nil {
 		return nil, err
 	}
@@ -74,7 +74,8 @@ func newBuildSelectPlan(sel *sqlparser.Select, vschema ContextVSchema) (engine.P
 		return nil, err
 	}
 
-	if err := planProjections(sel, plan, semTable); err != nil {
+	plan, err = planHorizon(sel, plan, semTable)
+	if err != nil {
 		return nil, err
 	}
 
@@ -112,7 +113,7 @@ func planLimit(limit *sqlparser.Limit, plan logicalPlan) (logicalPlan, error) {
 	return lPlan, nil
 }
 
-func planProjections(sel *sqlparser.Select, plan logicalPlan, semTable *semantics.SemTable) error {
+func planHorizon(sel *sqlparser.Select, plan logicalPlan, semTable *semantics.SemTable) (logicalPlan, error) {
 	rb, ok := plan.(*route)
 	if ok && rb.isSingleShard() {
 		ast := rb.Select.(*sqlparser.Select)
@@ -121,30 +122,40 @@ func planProjections(sel *sqlparser.Select, plan logicalPlan, semTable *semantic
 		ast.OrderBy = sel.OrderBy
 		ast.SelectExprs = sel.SelectExprs
 		ast.Comments = sel.Comments
-	} else {
-		// TODO real horizon planning to be done
-		if sel.Distinct {
-			return semantics.Gen4NotSupportedF("DISTINCT")
+		return plan, nil
+	}
+
+	// TODO real horizon planning to be done
+	if sel.Distinct {
+		return nil, semantics.Gen4NotSupportedF("DISTINCT")
+	}
+	if sel.GroupBy != nil {
+		return nil, semantics.Gen4NotSupportedF("GROUP BY")
+	}
+	qp, err := createQPFromSelect(sel)
+	if err != nil {
+		return nil, err
+	}
+	for _, e := range qp.selectExprs {
+		if _, err := pushProjection(e, plan, semTable); err != nil {
+			return nil, err
 		}
-		if sel.GroupBy != nil {
-			return semantics.Gen4NotSupportedF("GROUP BY")
+	}
+
+	if len(qp.aggrExprs) > 0 {
+		plan, err = planAggregations(qp, plan, semTable)
+		if err != nil {
+			return nil, err
 		}
-		for _, expr := range sel.SelectExprs {
-			switch e := expr.(type) {
-			case *sqlparser.AliasedExpr:
-				if nodeHasAggregates(e.Expr) {
-					return semantics.Gen4NotSupportedF("aggregation [%s]", sqlparser.String(e))
-				}
-				if _, err := pushProjection(e, plan, semTable); err != nil {
-					return err
-				}
-			default:
-				return semantics.Gen4NotSupportedF("%T", e)
-			}
+	}
+	if len(sel.OrderBy) > 0 {
+		plan, err = planOrderBy(qp, plan, semTable)
+		if err != nil {
+			return nil, err
 		}
-
 	}
-	return nil
+
+	return plan, nil
 }
 
 type (
diff --git a/go/vt/vtgate/planbuilder/select.go b/go/vt/vtgate/planbuilder/select.go
index c349f70ba2..2f4e906ae5 100644
--- a/go/vt/vtgate/planbuilder/select.go
+++ b/go/vt/vtgate/planbuilder/select.go
@@ -20,6 +20,8 @@ import (
 	"errors"
 	"fmt"
 
+	"vitess.io/vitess/go/sqltypes"
+
 	"vitess.io/vitess/go/vt/orchestrator/external/golib/log"
 
 	"vitess.io/vitess/go/vt/vtgate/semantics"
@@ -100,7 +102,7 @@ func shouldRetryWithCNFRewriting(plan logicalPlan) bool {
 
 }
 
-func pushProjection(expr *sqlparser.AliasedExpr, plan logicalPlan, semTable *semantics.SemTable) (firstOffset int, err error) {
+func pushProjection(expr *sqlparser.AliasedExpr, plan logicalPlan, semTable *semantics.SemTable) (int, error) {
 	switch node := plan.(type) {
 	case *route:
 		sel := node.Select.(*sqlparser.Select)
@@ -133,77 +135,87 @@ func pushProjection(expr *sqlparser.AliasedExpr, plan logicalPlan, semTable *sem
 	}
 }
 
-func pushPredicate(exprs []sqlparser.Expr, plan logicalPlan, semTable *semantics.SemTable) (err error) {
-	if len(exprs) == 0 {
-		return nil
+func planAggregations(qp *queryProjection, plan logicalPlan, semTable *semantics.SemTable) (logicalPlan, error) {
+	eaggr := &engine.OrderedAggregate{}
+	oa := &orderedAggregate{
+		resultsBuilder: newResultsBuilder(plan, eaggr),
+		eaggr:          eaggr,
 	}
-	switch node := plan.(type) {
+	for _, e := range qp.aggrExprs {
+		offset, err := pushProjection(e, plan, semTable)
+		if err != nil {
+			return nil, err
+		}
+		fExpr := e.Expr.(*sqlparser.FuncExpr)
+		opcode := engine.SupportedAggregates[fExpr.Name.Lowered()]
+		oa.eaggr.Aggregates = append(oa.eaggr.Aggregates, engine.AggregateParams{
+			Opcode: opcode,
+			Col:    offset,
+		})
+	}
+	return oa, nil
+}
+
+func planOrderBy(qp *queryProjection, plan logicalPlan, semTable *semantics.SemTable) (logicalPlan, error) {
+	switch plan := plan.(type) {
 	case *route:
-		sel := node.Select.(*sqlparser.Select)
-		finalExpr := reorderExpression(exprs[0], node.tables, semTable)
-		for i, expr := range exprs {
-			if i == 0 {
-				continue
+		for _, order := range qp.orderExprs {
+			offset, exists := qp.orderExprColMap[order]
+			if !exists {
+				return nil, semantics.Gen4NotSupportedF("order by column not exists in select list")
 			}
-			finalExpr = &sqlparser.AndExpr{
-				Left:  finalExpr,
-				Right: reorderExpression(expr, node.tables, semTable),
+			colName, ok := order.Expr.(*sqlparser.ColName)
+			if !ok {
+				return nil, semantics.Gen4NotSupportedF("order by non-column expression")
 			}
-		}
-		if sel.Where != nil {
-			finalExpr = &sqlparser.AndExpr{
-				Left:  sel.Where.Expr,
-				Right: finalExpr,
+
+			table := semTable.Dependencies(colName)
+			tableInfo, err := semTable.TableInfoFor(table)
+			if err != nil {
+				return nil, err
 			}
-		}
-		sel.Where = &sqlparser.Where{
-			Type: sqlparser.WhereClause,
-			Expr: finalExpr,
-		}
-		return nil
-	case *joinV4:
-		var lhs, rhs []sqlparser.Expr
-		lhsSolves := node.Left.ContainsTables()
-		rhsSolves := node.Right.ContainsTables()
-		for _, expr := range exprs {
-			deps := semTable.Dependencies(expr)
-			switch {
-			case deps.IsSolvedBy(lhsSolves):
-				lhs = append(lhs, expr)
-			case deps.IsSolvedBy(rhsSolves):
-				rhs = append(rhs, expr)
-			default:
-				return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unknown dependencies for %s", sqlparser.String(expr))
+			weightStringNeeded := true
+			for _, c := range tableInfo.Table.Columns {
+				if colName.Name.Equal(c.Name) {
+					if sqltypes.IsNumber(c.Type) {
+						weightStringNeeded = false
+					}
+					break
+				}
 			}
-		}
-		err := pushPredicate(lhs, node.Left, semTable)
-		if err != nil {
-			return err
-		}
-		err = pushPredicate(rhs, node.Right, semTable)
-		return err
-	default:
-		return vterrors.Errorf(vtrpcpb.Code_UNIMPLEMENTED, "%T not yet supported", node)
-	}
-}
 
-func reorderExpression(expr sqlparser.Expr, solves semantics.TableSet, semTable *semantics.SemTable) sqlparser.Expr {
-	switch compExpr := expr.(type) {
-	case *sqlparser.ComparisonExpr:
-		if compExpr.Operator == sqlparser.EqualOp {
-			if !dependsOnRoute(solves, compExpr.Left, semTable) && dependsOnRoute(solves, compExpr.Right, semTable) {
-				compExpr.Left, compExpr.Right = compExpr.Right, compExpr.Left
+			weightStringOffset := -1
+			if weightStringNeeded {
+				expr := &sqlparser.AliasedExpr{
+					Expr: &sqlparser.FuncExpr{
+						Name: sqlparser.NewColIdent("weight_string"),
+						Exprs: []sqlparser.SelectExpr{
+							&sqlparser.AliasedExpr{
+								Expr: order.Expr,
+							},
+						},
+					},
+				}
+				weightStringOffset, err = pushProjection(expr, plan, semTable)
+				if err != nil {
+					return nil, err
+				}
+			}
+			if weightStringOffset != -1 {
+				plan.eroute.TruncateColumnCount = len(qp.selectExprs) + len(qp.aggrExprs)
 			}
-		}
-	}
-	return expr
-}
 
-func dependsOnRoute(solves semantics.TableSet, expr sqlparser.Expr, semTable *semantics.SemTable) bool {
-	if node, ok := expr.(*sqlparser.ColName); ok {
-		return semTable.Dependencies(node).IsSolvedBy(solves)
+			plan.eroute.OrderBy = append(plan.eroute.OrderBy, engine.OrderbyParams{
+				Col:             offset,
+				WeightStringCol: weightStringOffset,
+				Desc:            order.Direction == sqlparser.DescOrder,
+			})
+			plan.Select.AddOrder(order)
+		}
+		return plan, nil
+	default:
+		return nil, semantics.Gen4NotSupportedF("ordering on complex query")
 	}
-	return !sqlparser.IsValue(expr)
 }
 
 var errSQLCalcFoundRows = vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.CantUseOptionHere, "Incorrect usage/placement of 'SQL_CALC_FOUND_ROWS'")
diff --git a/go/vt/vtgate/semantics/analyzer.go b/go/vt/vtgate/semantics/analyzer.go
index fe7c65a86d..ef81f01d80 100644
--- a/go/vt/vtgate/semantics/analyzer.go
+++ b/go/vt/vtgate/semantics/analyzer.go
@@ -17,6 +17,8 @@ limitations under the License.
 package semantics
 
 import (
+	"fmt"
+
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
@@ -25,18 +27,21 @@ import (
 type (
 	// analyzer is a struct to work with analyzing the query.
 	analyzer struct {
-		Tables []table
+		si SchemaInformation
+
+		Tables []*TableInfo
+		scopes []*scope
 
-		scopes   []*scope
 		exprDeps map[sqlparser.Expr]TableSet
 		err      error
 	}
 )
 
 // newAnalyzer create the semantic analyzer
-func newAnalyzer() *analyzer {
+func newAnalyzer(si SchemaInformation) *analyzer {
 	return &analyzer{
 		exprDeps: map[sqlparser.Expr]TableSet{},
+		si:       si,
 	}
 }
 
@@ -73,7 +78,7 @@ func (a *analyzer) analyzeDown(cursor *sqlparser.Cursor) bool {
 }
 
 func (a *analyzer) resolveColumn(colName *sqlparser.ColName, current *scope) (TableSet, error) {
-	var t table
+	var t *TableInfo
 	var err error
 	if colName.Qualifier.IsEmpty() {
 		t, err = a.resolveUnQualifiedColumn(current, colName)
@@ -83,7 +88,7 @@ func (a *analyzer) resolveColumn(colName *sqlparser.ColName, current *scope) (Ta
 	if err != nil {
 		return 0, err
 	}
-	return a.tableSetFor(t), nil
+	return a.tableSetFor(t.ASTNode), nil
 }
 
 func (a *analyzer) analyzeTableExprs(tablExprs sqlparser.TableExprs) error {
@@ -119,7 +124,7 @@ func (a *analyzer) analyzeTableExpr(tableExpr sqlparser.TableExpr) error {
 }
 
 // resolveQualifiedColumn handles `tabl.col` expressions
-func (a *analyzer) resolveQualifiedColumn(current *scope, expr *sqlparser.ColName) (table, error) {
+func (a *analyzer) resolveQualifiedColumn(current *scope, expr *sqlparser.ColName) (*TableInfo, error) {
 	qualifier := expr.Qualifier.Name.String()
 
 	for current != nil {
@@ -134,18 +139,33 @@ func (a *analyzer) resolveQualifiedColumn(current *scope, expr *sqlparser.ColNam
 }
 
 // resolveUnQualifiedColumn
-func (a *analyzer) resolveUnQualifiedColumn(current *scope, expr *sqlparser.ColName) (table, error) {
+func (a *analyzer) resolveUnQualifiedColumn(current *scope, expr *sqlparser.ColName) (*TableInfo, error) {
 	if len(current.tables) == 1 {
 		for _, tableExpr := range current.tables {
 			return tableExpr, nil
 		}
 	}
-	return nil, Gen4NotSupportedF("unable to map column to a table: %s", sqlparser.String(expr))
+
+	var tblInfo *TableInfo
+	for _, tbl := range current.tables {
+		if !tbl.Table.ColumnListAuthoritative {
+			return nil, vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.NonUniqError, fmt.Sprintf("Column '%s' in field list is ambiguous", sqlparser.String(expr)))
+		}
+		for _, col := range tbl.Table.Columns {
+			if expr.Name.Equal(col.Name) {
+				if tblInfo != nil {
+					return nil, vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.NonUniqError, fmt.Sprintf("Column '%s' in field list is ambiguous", sqlparser.String(expr)))
+				}
+				tblInfo = tbl
+			}
+		}
+	}
+	return tblInfo, nil
 }
 
-func (a *analyzer) tableSetFor(t table) TableSet {
+func (a *analyzer) tableSetFor(t *sqlparser.AliasedTableExpr) TableSet {
 	for i, t2 := range a.Tables {
-		if t == t2 {
+		if t == t2.ASTNode {
 			return TableSet(1 << i)
 		}
 	}
@@ -161,14 +181,22 @@ func (a *analyzer) bindTable(alias *sqlparser.AliasedTableExpr, expr sqlparser.S
 		}
 		a.popScope()
 		scope := a.currentScope()
-		return scope.addTable(alias.As.String(), alias)
+		return scope.addTable(alias.As.String(), &TableInfo{alias, nil})
 	case sqlparser.TableName:
+		tbl, vdx, _, _, _, err := a.si.FindTableOrVindex(t)
+		if err != nil {
+			return err
+		}
+		if tbl == nil && vdx != nil {
+			return Gen4NotSupportedF("vindex in FROM")
+		}
 		scope := a.currentScope()
-		a.Tables = append(a.Tables, alias)
+		table := &TableInfo{alias, tbl}
+		a.Tables = append(a.Tables, table)
 		if alias.As.IsEmpty() {
-			return scope.addTable(t.Name.String(), alias)
+			return scope.addTable(t.Name.String(), table)
 		}
-		return scope.addTable(alias.As.String(), alias)
+		return scope.addTable(alias.As.String(), table)
 	}
 	return nil
 }
diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go
index 674b6c1b6d..31a187c276 100644
--- a/go/vt/vtgate/semantics/semantic_state.go
+++ b/go/vt/vtgate/semantics/semantic_state.go
@@ -17,14 +17,21 @@ limitations under the License.
 package semantics
 
 import (
+	"vitess.io/vitess/go/vt/key"
+	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
 
 	"vitess.io/vitess/go/vt/sqlparser"
 )
 
 type (
-	table = *sqlparser.AliasedTableExpr
+	// TableInfo contains the alias table expr and vindex table
+	TableInfo struct {
+		ASTNode *sqlparser.AliasedTableExpr
+		Table   *vindexes.Table
+	}
 
 	// TableSet is how a set of tables is expressed.
 	// Tables get unique bits assigned in the order that they are encountered during semantic analysis
@@ -33,13 +40,18 @@ type (
 
 	// SemTable contains semantic analysis information about the query.
 	SemTable struct {
-		Tables           []table
+		Tables           []*TableInfo
 		exprDependencies map[sqlparser.Expr]TableSet
 	}
 
 	scope struct {
 		parent *scope
-		tables map[string]*sqlparser.AliasedTableExpr
+		tables map[string]*TableInfo
+	}
+
+	// SchemaInformation is used tp provide table information from Vschema.
+	SchemaInformation interface {
+		FindTableOrVindex(tablename sqlparser.TableName) (*vindexes.Table, vindexes.Vindex, string, topodatapb.TabletType, key.Destination, error)
 	}
 )
 
@@ -49,15 +61,23 @@ func NewSemTable() *SemTable {
 }
 
 // TableSetFor returns the bitmask for this particular tableshoe
-func (st *SemTable) TableSetFor(t table) TableSet {
+func (st *SemTable) TableSetFor(t *sqlparser.AliasedTableExpr) TableSet {
 	for idx, t2 := range st.Tables {
-		if t == t2 {
+		if t == t2.ASTNode {
 			return 1 << idx
 		}
 	}
 	return 0
 }
 
+// TableInfoFor returns the table info for the table set. It should contains only single table.
+func (st *SemTable) TableInfoFor(id TableSet) (*TableInfo, error) {
+	if id.NumberOfTables() > 1 {
+		return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] should only be used for single tables")
+	}
+	return st.Tables[id.TableOffset()], nil
+}
+
 // Dependencies return the table dependencies of the expression.
 func (st *SemTable) Dependencies(expr sqlparser.Expr) TableSet {
 	deps, found := st.exprDependencies[expr]
@@ -80,10 +100,10 @@ func (st *SemTable) Dependencies(expr sqlparser.Expr) TableSet {
 }
 
 func newScope(parent *scope) *scope {
-	return &scope{tables: map[string]*sqlparser.AliasedTableExpr{}, parent: parent}
+	return &scope{tables: map[string]*TableInfo{}, parent: parent}
 }
 
-func (s *scope) addTable(name string, table *sqlparser.AliasedTableExpr) error {
+func (s *scope) addTable(name string, table *TableInfo) error {
 	_, found := s.tables[name]
 	if found {
 		return vterrors.NewErrorf(vtrpcpb.Code_INVALID_ARGUMENT, vterrors.NonUniqTable, "Not unique table/alias: '%s'", name)
@@ -93,8 +113,8 @@ func (s *scope) addTable(name string, table *sqlparser.AliasedTableExpr) error {
 }
 
 // Analyse analyzes the parsed query.
-func Analyse(statement sqlparser.Statement) (*SemTable, error) {
-	analyzer := newAnalyzer()
+func Analyse(statement sqlparser.Statement, si SchemaInformation) (*SemTable, error) {
+	analyzer := newAnalyzer(si)
 	// Initial scope
 	err := analyzer.analyze(statement)
 	if err != nil {
@@ -120,6 +140,16 @@ func (ts TableSet) NumberOfTables() int {
 	return count
 }
 
+// TableOffset returns the offset in the Tables array from TableSet
+func (ts TableSet) TableOffset() int {
+	offset := 0
+	for ts > 1 {
+		ts = ts >> 1
+		offset++
+	}
+	return offset
+}
+
 // Constituents returns an slice with all the
 // individual tables in their own TableSet identifier
 func (ts TableSet) Constituents() (result []TableSet) {


Test patch:
diff --git a/go/vt/vtgate/planbuilder/querygraph_test.go b/go/vt/vtgate/planbuilder/querygraph_test.go
index 43717a4a46..d353d04aa6 100644
--- a/go/vt/vtgate/planbuilder/querygraph_test.go
+++ b/go/vt/vtgate/planbuilder/querygraph_test.go
@@ -22,6 +22,10 @@ import (
 	"strings"
 	"testing"
 
+	"vitess.io/vitess/go/vt/key"
+	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+
 	"github.com/stretchr/testify/assert"
 
 	"vitess.io/vitess/go/test/utils"
@@ -119,7 +123,7 @@ func TestQueryGraph(t *testing.T) {
 		t.Run(fmt.Sprintf("%d %s", i, sql), func(t *testing.T) {
 			tree, err := sqlparser.Parse(sql)
 			require.NoError(t, err)
-			semTable, err := semantics.Analyse(tree)
+			semTable, err := semantics.Analyse(tree, &fakeSI{})
 			require.NoError(t, err)
 			qgraph, err := createQGFromSelect(tree.(*sqlparser.Select), semTable)
 			require.NoError(t, err)
@@ -132,7 +136,7 @@ func TestQueryGraph(t *testing.T) {
 func TestString(t *testing.T) {
 	tree, err := sqlparser.Parse("select * from a,b join c on b.id = c.id where a.id = b.id and b.col IN (select 42) and func() = 'foo'")
 	require.NoError(t, err)
-	semTable, err := semantics.Analyse(tree)
+	semTable, err := semantics.Analyse(tree, &fakeSI{})
 	require.NoError(t, err)
 	qgraph, err := createQGFromSelect(tree.(*sqlparser.Select), semTable)
 	require.NoError(t, err)
@@ -232,3 +236,13 @@ func (qg *queryGraph) noDepsString() string {
 	}
 	return fmt.Sprintf("\nForAll: %s", sqlparser.String(qg.noDeps))
 }
+
+var _ semantics.SchemaInformation = (*fakeSI)(nil)
+
+type fakeSI struct {
+	tables map[string]*vindexes.Table
+}
+
+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
+}
diff --git a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
index 7c76066eec..ac0345f41a 100644
--- a/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/aggr_cases.txt
@@ -119,6 +119,7 @@ Gen4 plan same as above
         "FieldQuery": "select count(*), a, textcol1, b, weight_string(textcol1), weight_string(a), weight_string(b) from `user` where 1 != 1 group by a, textcol1, b",
         "OrderBy": "1 ASC, 2 ASC, 3 ASC",
         "Query": "select count(*), a, textcol1, b, weight_string(textcol1), weight_string(a), weight_string(b) from `user` group by a, textcol1, b order by a asc, textcol1 asc, b asc",
+        "ResultColumns": 5,
         "Table": "`user`"
       }
     ]
@@ -180,6 +181,7 @@ Gen4 plan same as above
             "FieldQuery": "select count(*) as k, a, textcol1, b, weight_string(textcol1), weight_string(a), weight_string(b) from `user` where 1 != 1 group by a, textcol1, b",
             "OrderBy": "2 ASC, 1 ASC, 3 ASC",
             "Query": "select count(*) as k, a, textcol1, b, weight_string(textcol1), weight_string(a), weight_string(b) from `user` group by a, textcol1, b order by textcol1 asc, a asc, b asc",
+            "ResultColumns": 5,
             "Table": "`user`"
           }
         ]
@@ -213,6 +215,7 @@ Gen4 plan same as above
     ]
   }
 }
+Gen4 plan same as above
 
 # sum aggregate
 "select sum(col) from user"
@@ -239,6 +242,7 @@ Gen4 plan same as above
     ]
   }
 }
+Gen4 plan same as above
 
 # min aggregate
 "select min(col) from user"
@@ -265,6 +269,7 @@ Gen4 plan same as above
     ]
   }
 }
+Gen4 plan same as above
 
 # max aggregate
 "select max(col) from user"
@@ -291,6 +296,7 @@ Gen4 plan same as above
     ]
   }
 }
+Gen4 plan same as above
 
 # distinct and group by together for scatter route
 "select distinct col1, col2 from user group by col1"
@@ -313,6 +319,7 @@ Gen4 plan same as above
         "FieldQuery": "select col1, col2, weight_string(col1), weight_string(col2) from `user` where 1 != 1 group by col1",
         "OrderBy": "0 ASC, 1 ASC, 0 ASC",
         "Query": "select distinct col1, col2, weight_string(col1), weight_string(col2) from `user` group by col1 order by col1 asc, col2 asc, col1 asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -414,6 +421,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, count(*), weight_string(col) from `user` where 1 != 1 group by col",
         "OrderBy": "0 ASC",
         "Query": "select col, count(*), weight_string(col) from `user` group by col order by col asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -446,6 +454,7 @@ Gen4 plan same as above
         "FieldQuery": "select `name`, count(*), weight_string(`name`) from `user` where 1 != 1 group by `name`",
         "OrderBy": "0 ASC",
         "Query": "select `name`, count(*), weight_string(`name`) from `user` group by `name` order by `name` asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -605,6 +614,7 @@ Gen4 plan same as above
     ]
   }
 }
+Gen4 plan same as above
 
 # scatter aggregate using distinct
 "select distinct col from user"
@@ -627,6 +637,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, weight_string(col) from `user` where 1 != 1",
         "OrderBy": "0 ASC",
         "Query": "select distinct col, weight_string(col) from `user` order by col asc",
+        "ResultColumns": 1,
         "Table": "`user`"
       }
     ]
@@ -654,6 +665,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, weight_string(col) from `user` where 1 != 1 group by col",
         "OrderBy": "0 ASC",
         "Query": "select col, weight_string(col) from `user` group by col order by col asc",
+        "ResultColumns": 1,
         "Table": "`user`"
       }
     ]
@@ -700,6 +712,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, count(distinct id), weight_string(col) from `user` where 1 != 1 group by col",
         "OrderBy": "0 ASC",
         "Query": "select col, count(distinct id), weight_string(col) from `user` group by col order by col asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -728,6 +741,7 @@ Gen4 plan same as above
         "FieldQuery": "select col1, col2, weight_string(col1), weight_string(col2) from `user` where 1 != 1 group by col1, col2",
         "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2, weight_string(col1), weight_string(col2) from `user` group by col1, col2 order by col1 asc, col2 asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -755,6 +769,7 @@ Gen4 plan same as above
         "FieldQuery": "select col2, weight_string(col2) from `user` where 1 != 1 group by col2",
         "OrderBy": "0 ASC",
         "Query": "select col2, weight_string(col2) from `user` group by col2 order by col2 asc",
+        "ResultColumns": 1,
         "Table": "`user`"
       }
     ]
@@ -783,6 +798,7 @@ Gen4 plan same as above
         "FieldQuery": "select col1, col2, weight_string(col1), weight_string(col2) from `user` where 1 != 1 group by col1, col2",
         "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2, weight_string(col1), weight_string(col2) from `user` group by col1, col2 order by col1 asc, col2 asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -811,6 +827,7 @@ Gen4 plan same as above
         "FieldQuery": "select col1, col2, weight_string(col1), weight_string(col2) from `user` where 1 != 1 group by col1, col2",
         "OrderBy": "0 ASC, 1 ASC",
         "Query": "select col1, col2, weight_string(col1), weight_string(col2) from `user` group by col1, col2 order by col1 asc, col2 asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -839,6 +856,7 @@ Gen4 plan same as above
         "FieldQuery": "select col1, min(distinct col2), weight_string(col1) from `user` where 1 != 1 group by col1",
         "OrderBy": "0 ASC",
         "Query": "select col1, min(distinct col2), weight_string(col1) from `user` group by col1 order by col1 asc",
+        "ResultColumns": 2,
         "Table": "`user`"
       }
     ]
@@ -872,6 +890,7 @@ Gen4 plan same as above
             "FieldQuery": "select col1, col2, weight_string(col1), weight_string(col2) from `user` where 1 != 1 group by col1, col2",
             "OrderBy": "0 ASC, 1 ASC",
             "Query": "select col1, col2, weight_string(col1), weight_string(col2) from `user` group by col1, col2 order by col1 asc, col2 asc",
+            "ResultColumns": 2,
             "Table": "`user`"
           }
         ]
@@ -906,6 +925,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, count(*), weight_string(b), weight_string(a) from `user` where 1 != 1 group by b, a",
         "OrderBy": "1 ASC, 0 ASC",
         "Query": "select a, b, count(*), weight_string(b), weight_string(a) from `user` group by b, a order by b asc, a asc",
+        "ResultColumns": 3,
         "Table": "`user`"
       }
     ]
@@ -934,6 +954,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, count(*), weight_string(b), weight_string(a) from `user` where 1 != 1 group by 2, 1",
         "OrderBy": "1 ASC, 0 ASC",
         "Query": "select a, b, count(*), weight_string(b), weight_string(a) from `user` group by 2, 1 order by b asc, a asc",
+        "ResultColumns": 3,
         "Table": "`user`"
       }
     ]
@@ -962,6 +983,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, count(*), weight_string(b), weight_string(a) from `user` where 1 != 1 group by b, a",
         "OrderBy": "1 ASC, 0 ASC",
         "Query": "select a, b, count(*), weight_string(b), weight_string(a) from `user` group by b, a order by b asc, a asc",
+        "ResultColumns": 3,
         "Table": "`user`"
       }
     ]
@@ -989,6 +1011,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, weight_string(col) from `user` where 1 != 1 group by 1",
         "OrderBy": "0 ASC",
         "Query": "select col, weight_string(col) from `user` group by 1 order by col asc",
+        "ResultColumns": 1,
         "Table": "`user`"
       }
     ]
@@ -1051,6 +1074,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, c, d, count(*), weight_string(a), weight_string(b), weight_string(c) from `user` where 1 != 1 group by 1, 2, 3",
         "OrderBy": "0 ASC, 1 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*), weight_string(a), weight_string(b), weight_string(c) from `user` group by 1, 2, 3 order by 1 asc, 2 asc, 3 asc",
+        "ResultColumns": 5,
         "Table": "`user`"
       }
     ]
@@ -1079,6 +1103,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, c, d, count(*), weight_string(a), weight_string(b), weight_string(c) from `user` where 1 != 1 group by 1, 2, 3",
         "OrderBy": "0 ASC, 1 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*), weight_string(a), weight_string(b), weight_string(c) from `user` group by 1, 2, 3 order by a asc, b asc, c asc",
+        "ResultColumns": 5,
         "Table": "`user`"
       }
     ]
@@ -1107,6 +1132,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, c, d, count(*), weight_string(d), weight_string(b), weight_string(a), weight_string(c) from `user` where 1 != 1 group by 1, 2, 3, 4",
         "OrderBy": "3 ASC, 1 ASC, 0 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*), weight_string(d), weight_string(b), weight_string(a), weight_string(c) from `user` group by 1, 2, 3, 4 order by d asc, b asc, a asc, c asc",
+        "ResultColumns": 5,
         "Table": "`user`"
       }
     ]
@@ -1135,6 +1161,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, c, d, count(*), weight_string(d), weight_string(b), weight_string(a), weight_string(c) from `user` where 1 != 1 group by 3, 2, 1, 4",
         "OrderBy": "3 ASC, 1 ASC, 0 ASC, 2 ASC",
         "Query": "select a, b, c, d, count(*), weight_string(d), weight_string(b), weight_string(a), weight_string(c) from `user` group by 3, 2, 1, 4 order by d asc, b asc, a asc, c asc",
+        "ResultColumns": 5,
         "Table": "`user`"
       }
     ]
@@ -1163,6 +1190,7 @@ Gen4 plan same as above
         "FieldQuery": "select a, b, c, count(*), weight_string(a), weight_string(c), weight_string(b) from `user` where 1 != 1 group by 3, 2, 1",
         "OrderBy": "0 DESC, 2 DESC, 1 ASC",
         "Query": "select a, b, c, count(*), weight_string(a), weight_string(c), weight_string(b) from `user` group by 3, 2, 1 order by 1 desc, 3 desc, b asc",
+        "ResultColumns": 4,
         "Table": "`user`"
       }
     ]
@@ -1199,6 +1227,7 @@ Gen4 plan same as above
             "FieldQuery": "select col, count(*), weight_string(col) from `user` where 1 != 1 group by col",
             "OrderBy": "0 ASC",
             "Query": "select col, count(*), weight_string(col) from `user` group by col order by col asc limit :__upper_limit",
+            "ResultColumns": 2,
             "Table": "`user`"
           }
         ]
@@ -1292,6 +1321,7 @@ Gen4 plan same as above
             "FieldQuery": "select a, count(*), weight_string(a) from `user` where 1 != 1",
             "OrderBy": "0 ASC",
             "Query": "select a, count(*), weight_string(a) from `user` order by a asc",
+            "ResultColumns": 2,
             "Table": "`user`"
           }
         ]
@@ -1325,6 +1355,7 @@ Gen4 plan same as above
             "FieldQuery": "select a, count(*), weight_string(a) from `user` where 1 != 1 group by a",
             "OrderBy": "0 ASC, 0 ASC",
             "Query": "select a, count(*), weight_string(a) from `user` group by a order by a asc, a asc",
+            "ResultColumns": 2,
             "Table": "`user`"
           }
         ]
diff --git a/go/vt/vtgate/planbuilder/testdata/from_cases.txt b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
index 45aea022d1..50b3be4af1 100644
--- a/go/vt/vtgate/planbuilder/testdata/from_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/from_cases.txt
@@ -2317,10 +2317,12 @@ Gen4 plan same as above
 # non-existent table on left of join
 "select c from t join user"
 "table t not found"
+Gen4 plan same as above
 
 # non-existent table on right of join
 "select c from user join t"
 "table t not found"
+Gen4 plan same as above
 
 # information schema query that uses table_schema
 "select column_name from information_schema.columns where table_schema = (select schema())"
diff --git a/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt b/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
index 53b2220751..87251e6b4c 100644
--- a/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/memory_sort_cases.txt
@@ -26,6 +26,7 @@
             "FieldQuery": "select a, b, count(*), weight_string(b), weight_string(a) from `user` where 1 != 1 group by a",
             "OrderBy": "0 ASC",
             "Query": "select a, b, count(*), weight_string(b), weight_string(a) from `user` group by a order by a asc",
+            "ResultColumns": 4,
             "Table": "`user`"
           }
         ]
@@ -61,6 +62,7 @@
             "FieldQuery": "select a, b, count(*) as k, weight_string(a) from `user` where 1 != 1 group by a",
             "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k, weight_string(a) from `user` group by a order by a asc",
+            "ResultColumns": 3,
             "Table": "`user`"
           }
         ]
@@ -96,6 +98,7 @@
             "FieldQuery": "select a, b, count(*) as k, weight_string(b), weight_string(a) from `user` where 1 != 1 group by a",
             "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k, weight_string(b), weight_string(a) from `user` group by a order by a asc",
+            "ResultColumns": 5,
             "Table": "`user`"
           }
         ]
@@ -135,6 +138,7 @@
                 "FieldQuery": "select a, b, count(*) as k, weight_string(a) from `user` where 1 != 1 group by a",
                 "OrderBy": "0 ASC",
                 "Query": "select a, b, count(*) as k, weight_string(a) from `user` group by a order by a asc",
+                "ResultColumns": 3,
                 "Table": "`user`"
               }
             ]
@@ -172,6 +176,7 @@
             "FieldQuery": "select a, b, count(*) as k, weight_string(a) from `user` where 1 != 1 group by a",
             "OrderBy": "0 ASC",
             "Query": "select a, b, count(*) as k, weight_string(a) from `user` group by a order by 1 asc",
+            "ResultColumns": 4,
             "Table": "`user`"
           }
         ]
@@ -208,6 +213,7 @@
             "FieldQuery": "select textcol1 as t, count(*) as k, weight_string(textcol1) from `user` where 1 != 1 group by textcol1",
             "OrderBy": "0 ASC, 0 ASC",
             "Query": "select textcol1 as t, count(*) as k, weight_string(textcol1) from `user` group by textcol1 order by textcol1 asc, textcol1 asc",
+            "ResultColumns": 3,
             "Table": "`user`"
           }
         ]
@@ -507,6 +513,7 @@
     "FieldQuery": "select a, weight_string(a) from `user` where 1 != 1",
     "OrderBy": "0 DESC",
     "Query": "select a, weight_string(a) from `user` order by binary a desc",
+    "ResultColumns": 1,
     "Table": "`user`"
   }
 }
@@ -532,6 +539,7 @@
         "FieldQuery": "select u.a, weight_string(u.a) from `user` as u where 1 != 1",
         "OrderBy": "0 DESC",
         "Query": "select u.a, weight_string(u.a) from `user` as u order by binary a desc",
+        "ResultColumns": 1,
         "Table": "`user`"
       },
       {
@@ -548,3 +556,23 @@
     ]
   }
 }
+
+# intcol order by
+"select id, intcol from user order by intcol"
+{
+  "QueryType": "SELECT",
+  "Original": "select id, intcol from user order by intcol",
+  "Instructions": {
+    "OperatorType": "Route",
+    "Variant": "SelectScatter",
+    "Keyspace": {
+      "Name": "user",
+      "Sharded": true
+    },
+    "FieldQuery": "select id, intcol from `user` where 1 != 1",
+    "OrderBy": "1 ASC",
+    "Query": "select id, intcol from `user` order by intcol asc",
+    "Table": "`user`"
+  }
+}
+Gen4 plan same as above
diff --git a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
index b1bc45b720..e3eb8cd3d1 100644
--- a/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/postprocess_cases.txt
@@ -194,9 +194,11 @@ Gen4 plan same as above
     "FieldQuery": "select col, weight_string(col) from `user` where 1 != 1",
     "OrderBy": "0 ASC",
     "Query": "select col, weight_string(col) from `user` order by col asc",
+    "ResultColumns": 1,
     "Table": "`user`"
   }
 }
+Gen4 plan same as above
 
 # ORDER BY on select t.*
 "select t.*, t.col from user t order by t.col"
@@ -213,6 +215,7 @@ Gen4 plan same as above
     "FieldQuery": "select t.*, t.col, weight_string(t.col) from `user` as t where 1 != 1",
     "OrderBy": "9 ASC",
     "Query": "select t.*, t.col, weight_string(t.col) from `user` as t order by t.col asc",
+    "ResultColumns": 2,
     "Table": "`user`"
   }
 }
@@ -232,6 +235,7 @@ Gen4 plan same as above
     "FieldQuery": "select *, col, weight_string(col) from `user` where 1 != 1",
     "OrderBy": "9 ASC",
     "Query": "select *, col, weight_string(col) from `user` order by col asc",
+    "ResultColumns": 2,
     "Table": "`user`"
   }
 }
@@ -251,6 +255,7 @@ Gen4 plan same as above
     "FieldQuery": "select t.*, t.`name`, t.*, t.col, weight_string(t.col) from `user` as t where 1 != 1",
     "OrderBy": "19 ASC",
     "Query": "select t.*, t.`name`, t.*, t.col, weight_string(t.col) from `user` as t order by t.col asc",
+    "ResultColumns": 4,
     "Table": "`user`"
   }
 }
@@ -270,6 +275,7 @@ Gen4 plan same as above
     "FieldQuery": "select *, `name`, *, col, weight_string(col) from `user` where 1 != 1",
     "OrderBy": "19 ASC",
     "Query": "select *, `name`, *, col, weight_string(col) from `user` order by col asc",
+    "ResultColumns": 4,
     "Table": "`user`"
   }
 }
@@ -289,6 +295,7 @@ Gen4 plan same as above
     "FieldQuery": "select user_id, col1, col2, weight_string(user_id) from authoritative where 1 != 1",
     "OrderBy": "0 ASC",
     "Query": "select user_id, col1, col2, weight_string(user_id) from authoritative order by user_id asc",
+    "ResultColumns": 3,
     "Table": "authoritative"
   }
 }
@@ -308,6 +315,7 @@ Gen4 plan same as above
     "FieldQuery": "select user_id, col1, col2, weight_string(col1) from authoritative where 1 != 1",
     "OrderBy": "1 ASC",
     "Query": "select user_id, col1, col2, weight_string(col1) from authoritative order by col1 asc",
+    "ResultColumns": 3,
     "Table": "authoritative"
   }
 }
@@ -327,9 +335,11 @@ Gen4 plan same as above
     "FieldQuery": "select a, textcol1, b, weight_string(a), weight_string(textcol1), weight_string(b) from `user` where 1 != 1",
     "OrderBy": "0 ASC, 1 ASC, 2 ASC",
     "Query": "select a, textcol1, b, weight_string(a), weight_string(textcol1), weight_string(b) from `user` order by a asc, textcol1 asc, b asc",
+    "ResultColumns": 3,
     "Table": "`user`"
   }
 }
+Gen4 plan same as above
 
 # ORDER BY on scatter with text column, qualified name
 "select a, user.textcol1, b from user order by a, textcol1, b"
@@ -346,6 +356,7 @@ Gen4 plan same as above
     "FieldQuery": "select a, `user`.textcol1, b, weight_string(a), weight_string(`user`.textcol1), weight_string(b) from `user` where 1 != 1",
     "OrderBy": "0 ASC, 1 ASC, 2 ASC",
     "Query": "select a, `user`.textcol1, b, weight_string(a), weight_string(`user`.textcol1), weight_string(b) from `user` order by a asc, textcol1 asc, b asc",
+    "ResultColumns": 3,
     "Table": "`user`"
   }
 }
@@ -365,9 +376,11 @@ Gen4 plan same as above
     "FieldQuery": "select a, textcol1, b, textcol2, weight_string(a), weight_string(textcol1), weight_string(b), weight_string(textcol2) from `user` where 1 != 1",
     "OrderBy": "0 ASC, 1 ASC, 2 ASC, 3 ASC",
     "Query": "select a, textcol1, b, textcol2, weight_string(a), weight_string(textcol1), weight_string(b), weight_string(textcol2) from `user` order by a asc, textcol1 asc, b asc, textcol2 asc",
+    "ResultColumns": 4,
     "Table": "`user`"
   }
 }
+Gen4 plan same as above
 
 # ORDER BY invalid col number on scatter
 "select col from user order by 2"
@@ -421,6 +434,7 @@ Gen4 plan same as above
         "FieldQuery": "select col, weight_string(col) from `user` where 1 != 1",
         "OrderBy": "0 ASC",
         "Query": "select col, weight_string(col) from `user` where :__sq_has_values1 = 1 and col in ::__sq1 order by col asc",
+        "ResultColumns": 1,
         "Table": "`user`"
       }
     ]
diff --git a/go/vt/vtgate/planbuilder/testdata/select_cases.txt b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
index b2b7906ad3..805066d598 100644
--- a/go/vt/vtgate/planbuilder/testdata/select_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/select_cases.txt
@@ -852,11 +852,13 @@ Gen4 plan same as above
         "FieldQuery": "select user_id, weight_string(user_id) from music where 1 != 1",
         "OrderBy": "0 ASC",
         "Query": "select user_id, weight_string(user_id) from music order by user_id asc limit :__upper_limit",
+        "ResultColumns": 1,
         "Table": "music"
       }
     ]
   }
 }
+Gen4 plan same as above
 
 # Sharding Key Condition in Parenthesis
 "select * from user where name ='abc' AND (id = 4) limit 5"
@@ -1547,6 +1549,7 @@ Gen4 plan same as above
             "FieldQuery": "select user_id, count(id), weight_string(user_id) from music where 1 != 1 group by user_id",
             "OrderBy": "0 ASC",
             "Query": "select user_id, count(id), weight_string(user_id) from music group by user_id having count(user_id) = 1 order by user_id asc limit :__upper_limit",
+            "ResultColumns": 2,
             "Table": "music"
           }
         ]
diff --git a/go/vt/vtgate/planbuilder/testdata/union_cases.txt b/go/vt/vtgate/planbuilder/testdata/union_cases.txt
index c7572346b1..18c159fd04 100644
--- a/go/vt/vtgate/planbuilder/testdata/union_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/union_cases.txt
@@ -122,6 +122,7 @@ Gen4 plan same as above
             "FieldQuery": "select id, weight_string(id) from `user` where 1 != 1",
             "OrderBy": "0 DESC",
             "Query": "select id, weight_string(id) from `user` order by id desc limit :__upper_limit",
+            "ResultColumns": 1,
             "Table": "`user`"
           }
         ]
@@ -140,6 +141,7 @@ Gen4 plan same as above
             "FieldQuery": "select id, weight_string(id) from music where 1 != 1",
             "OrderBy": "0 DESC",
             "Query": "select id, weight_string(id) from music order by id desc limit :__upper_limit",
+            "ResultColumns": 1,
             "Table": "music"
           }
         ]
@@ -208,6 +210,7 @@ Gen4 plan same as above
             "FieldQuery": "select id, weight_string(id) from `user` where 1 != 1",
             "OrderBy": "0 ASC",
             "Query": "select id, weight_string(id) from `user` order by id asc limit :__upper_limit",
+            "ResultColumns": 1,
             "Table": "`user`"
           }
         ]
@@ -226,6 +229,7 @@ Gen4 plan same as above
             "FieldQuery": "select id, weight_string(id) from music where 1 != 1",
             "OrderBy": "0 DESC",
             "Query": "select id, weight_string(id) from music order by id desc limit :__upper_limit",
+            "ResultColumns": 1,
             "Table": "music"
           }
         ]
@@ -647,6 +651,7 @@ Gen4 plan same as above
             "FieldQuery": "select 1, weight_string(1) from `user` where 1 != 1",
             "OrderBy": "0 DESC",
             "Query": "select 1, weight_string(1) from `user` order by 1 desc",
+            "ResultColumns": 1,
             "Table": "`user`"
           },
           {
@@ -659,6 +664,7 @@ Gen4 plan same as above
             "FieldQuery": "select 1, weight_string(1) from `user` where 1 != 1",
             "OrderBy": "0 ASC",
             "Query": "select 1, weight_string(1) from `user` order by 1 asc",
+            "ResultColumns": 1,
             "Table": "`user`"
           }
         ]
diff --git a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
index ef39f94922..eab094f308 100644
--- a/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/unsupported_cases.txt
@@ -92,6 +92,7 @@ Gen4 plan same as above
 # Complex aggregate expression on scatter
 "select 1+count(*) from user"
 "unsupported: in scatter query: complex aggregate expression"
+Gen4 plan same as above
 
 # Multi-value aggregates not supported
 "select count(a,b) from user"
diff --git a/go/vt/vtgate/semantics/analyzer_test.go b/go/vt/vtgate/semantics/analyzer_test.go
index dc30488cac..fe15147f67 100644
--- a/go/vt/vtgate/semantics/analyzer_test.go
+++ b/go/vt/vtgate/semantics/analyzer_test.go
@@ -20,6 +20,11 @@ import (
 	"strings"
 	"testing"
 
+	"vitess.io/vitess/go/vt/key"
+	querypb "vitess.io/vitess/go/vt/proto/query"
+	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+
 	"github.com/stretchr/testify/assert"
 
 	"github.com/stretchr/testify/require"
@@ -163,7 +168,7 @@ func TestNotUniqueTableName(t *testing.T) {
 				t.Skip("table alias not implemented")
 			}
 			parse, _ := sqlparser.Parse(query)
-			_, err := Analyse(parse)
+			_, err := Analyse(parse, &fakeSI{})
 			require.Error(t, err)
 			require.Contains(t, err.Error(), "Not unique table/alias")
 		})
@@ -178,17 +183,69 @@ func TestMissingTable(t *testing.T) {
 	for _, query := range queries {
 		t.Run(query, func(t *testing.T) {
 			parse, _ := sqlparser.Parse(query)
-			_, err := Analyse(parse)
+			_, err := Analyse(parse, &fakeSI{})
 			require.Error(t, err)
 			require.Contains(t, err.Error(), "Unknown table")
 		})
 	}
 }
 
+func TestUnknownColumnMap(t *testing.T) {
+	vt := map[string]*vindexes.Table{
+		"a": {},
+		"b": {},
+	}
+	si := &fakeSI{tables: vt}
+	query := "select col from a, b"
+
+	parse, _ := sqlparser.Parse(query)
+	_, err := Analyse(parse, si)
+	assert.EqualError(t, err, "Column 'col' in field list is ambiguous")
+
+	vt["a"] = &vindexes.Table{
+		Name: sqlparser.NewTableIdent("b"),
+		Columns: []vindexes.Column{{
+			Name: sqlparser.NewColIdent("col2"),
+			Type: querypb.Type_VARCHAR,
+		}},
+		ColumnListAuthoritative: false,
+	}
+	vt["b"] = &vindexes.Table{
+		Name: sqlparser.NewTableIdent("b"),
+		Columns: []vindexes.Column{{
+			Name: sqlparser.NewColIdent("col"),
+			Type: querypb.Type_VARCHAR,
+		}},
+		ColumnListAuthoritative: true,
+	}
+
+	_, err = Analyse(parse, si)
+	assert.EqualError(t, err, "Column 'col' in field list is ambiguous")
+
+	vt["a"].ColumnListAuthoritative = true
+	_, err = Analyse(parse, si)
+	assert.NoError(t, err, "should have found the column")
+
+}
+
 func parseAndAnalyze(t *testing.T, query string) (sqlparser.Statement, *SemTable) {
 	parse, err := sqlparser.Parse(query)
 	require.NoError(t, err)
-	semTable, err := Analyse(parse)
+	semTable, err := Analyse(parse, &fakeSI{
+		tables: map[string]*vindexes.Table{
+			"t": {Name: sqlparser.NewTableIdent("t")},
+		},
+	})
 	require.NoError(t, err)
 	return parse, semTable
 }
+
+var _ SchemaInformation = (*fakeSI)(nil)
+
+type fakeSI struct {
+	tables map[string]*vindexes.Table
+}
+
+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
+}
diff --git a/go/vt/vtgate/semantics/tabletset_test.go b/go/vt/vtgate/semantics/tabletset_test.go
index 8ff41c57ec..4ca005ae99 100644
--- a/go/vt/vtgate/semantics/tabletset_test.go
+++ b/go/vt/vtgate/semantics/tabletset_test.go
@@ -23,8 +23,7 @@ import (
 )
 
 const (
-	_ TableSet = 1 << iota
-	F1
+	F1 TableSet = 1 << iota
 	F2
 	F3
 )
@@ -51,3 +50,9 @@ func TestTableSet_Constituents(t *testing.T) {
 	assert.Equal(t, []TableSet{F2, F3}, (F2 | F3).Constituents())
 	assert.Empty(t, TableSet(0).Constituents())
 }
+
+func TestTableSet_TableOffset(t *testing.T) {
+	assert.Equal(t, 0, F1.TableOffset())
+	assert.Equal(t, 1, F2.TableOffset())
+	assert.Equal(t, 2, F3.TableOffset())
+}