diff --git a/go/slices2/slices.go b/go/slices2/slices.go index c69acb13cd5..5868c635a15 100644 --- a/go/slices2/slices.go +++ b/go/slices2/slices.go @@ -37,3 +37,11 @@ func Any[T any](s []T, fn func(T) bool) bool { } return false } + +func Map[From, To any](in []From, f func(From) To) []To { + result := make([]To, len(in)) + for i, col := range in { + result[i] = f(col) + } + return result +} diff --git a/go/vt/vtgate/planbuilder/operator_transformers.go b/go/vt/vtgate/planbuilder/operator_transformers.go index 7ce349b21b5..4ce7d22c857 100644 --- a/go/vt/vtgate/planbuilder/operator_transformers.go +++ b/go/vt/vtgate/planbuilder/operator_transformers.go @@ -59,32 +59,58 @@ func transformToLogicalPlan(ctx *plancontext.PlanningContext, op ops.Operator, i return transformCorrelatedSubQueryPlan(ctx, op) case *operators.Derived: return transformDerivedPlan(ctx, op) + case *operators.SimpleProjection: + return transformSimpleProjection(ctx, op) case *operators.Filter: - plan, err := transformToLogicalPlan(ctx, op.Source, false) - if err != nil { - return nil, err - } - ast := ctx.SemTable.AndExpressions(op.Predicates...) - predicate, err := evalengine.Translate(ast, &evalengine.Config{ + return transformFilter(ctx, op) + case *operators.Horizon: + return transformHorizon(ctx, op, isRoot) + } + + return nil, vterrors.VT13001(fmt.Sprintf("unknown type encountered: %T (transformToLogicalPlan)", op)) +} + +func transformFilter(ctx *plancontext.PlanningContext, op *operators.Filter) (logicalPlan, error) { + plan, err := transformToLogicalPlan(ctx, op.Source, false) + if err != nil { + return nil, err + } + + predicate := op.FinalPredicate + ast := ctx.SemTable.AndExpressions(op.Predicates...) + + // this might already have been done on the operators + if predicate == nil { + predicate, err = evalengine.Translate(ast, &evalengine.Config{ ResolveColumn: resolveFromPlan(ctx, plan, true), Collation: ctx.SemTable.Collation, }) if err != nil { return nil, err } + } - return &filter{ - logicalPlanCommon: newBuilderCommon(plan), - efilter: &engine.Filter{ - Predicate: predicate, - ASTPredicate: ast, - }, - }, nil - case *operators.Horizon: - return transformHorizon(ctx, op, isRoot) + return &filter{ + logicalPlanCommon: newBuilderCommon(plan), + efilter: &engine.Filter{ + Predicate: predicate, + ASTPredicate: ast, + }, + }, nil +} + +func transformSimpleProjection(ctx *plancontext.PlanningContext, op *operators.SimpleProjection) (logicalPlan, error) { + src, err := transformToLogicalPlan(ctx, op.Source, false) + if err != nil { + return nil, err } - return nil, vterrors.VT13001(fmt.Sprintf("unknown type encountered: %T (transformToLogicalPlan)", op)) + return &simpleProjection{ + logicalPlanCommon: newBuilderCommon(src), + eSimpleProj: &engine.SimpleProjection{ + Cols: op.Columns, + }, + }, nil } func transformHorizon(ctx *plancontext.PlanningContext, op *operators.Horizon, isRoot bool) (logicalPlan, error) { diff --git a/go/vt/vtgate/planbuilder/operators/apply_join.go b/go/vt/vtgate/planbuilder/operators/apply_join.go index 2968d463b1c..12666b73568 100644 --- a/go/vt/vtgate/planbuilder/operators/apply_join.go +++ b/go/vt/vtgate/planbuilder/operators/apply_join.go @@ -122,7 +122,7 @@ func (a *ApplyJoin) AddJoinPredicate(ctx *plancontext.PlanningContext, expr sqlp return err } for i, col := range cols { - offset, err := a.LHS.AddColumn(ctx, col) + offset, err := a.pushColLeft(ctx, aeWrap(col)) if err != nil { return err } @@ -140,54 +140,76 @@ func (a *ApplyJoin) AddJoinPredicate(ctx *plancontext.PlanningContext, expr sqlp return nil } -func (a *ApplyJoin) AddColumn(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (int, error) { - // first check if we already are passing through this expression - for i, existing := range a.ColumnsAST { - if ctx.SemTable.EqualsExpr(existing, expr) { - return i, nil - } +func (a *ApplyJoin) pushColLeft(ctx *plancontext.PlanningContext, e *sqlparser.AliasedExpr) (int, error) { + newLHS, offset, err := a.LHS.AddColumn(ctx, e, true) + if err != nil { + return 0, err + } + a.LHS = newLHS + return offset, nil +} + +func (a *ApplyJoin) pushColRight(ctx *plancontext.PlanningContext, e *sqlparser.AliasedExpr) (int, error) { + newRHS, offset, err := a.RHS.AddColumn(ctx, e, true) + if err != nil { + return 0, err + } + a.RHS = newRHS + return offset, nil +} + +func (a *ApplyJoin) GetColumns() ([]sqlparser.Expr, error) { + return a.ColumnsAST, nil +} + +func (a *ApplyJoin) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + if offset, found := canReuseColumn(ctx, reuseCol, a.ColumnsAST, expr.Expr); found { + return a, offset, nil } lhs := TableID(a.LHS) rhs := TableID(a.RHS) both := lhs.Merge(rhs) - deps := ctx.SemTable.RecursiveDeps(expr) + deps := ctx.SemTable.RecursiveDeps(expr.Expr) // if we get here, it's a new expression we are dealing with. // We need to decide if we can push it all on either side, // or if we have to break the expression into left and right parts switch { case deps.IsSolvedBy(lhs): - offset, err := a.LHS.AddColumn(ctx, expr) + offset, err := a.pushColLeft(ctx, expr) if err != nil { - return 0, err + return nil, 0, err } a.Columns = append(a.Columns, -offset-1) + case deps.IsSolvedBy(rhs): + offset, err := a.pushColRight(ctx, expr) + if err != nil { + return nil, 0, err + } + a.Columns = append(a.Columns, offset+1) case deps.IsSolvedBy(both): - bvNames, lhsExprs, rhsExpr, err := BreakExpressionInLHSandRHS(ctx, expr, lhs) + bvNames, lhsExprs, rhsExpr, err := BreakExpressionInLHSandRHS(ctx, expr.Expr, lhs) if err != nil { - return 0, err + return nil, 0, err } for i, lhsExpr := range lhsExprs { - offset, err := a.LHS.AddColumn(ctx, lhsExpr) + offset, err := a.pushColLeft(ctx, aeWrap(lhsExpr)) if err != nil { - return 0, err + return nil, 0, err } a.Vars[bvNames[i]] = offset } - expr = rhsExpr - fallthrough // now we just pass the rest to the RHS of the join - case deps.IsSolvedBy(rhs): - offset, err := a.RHS.AddColumn(ctx, expr) + offset, err := a.pushColRight(ctx, aeWrap(rhsExpr)) if err != nil { - return 0, err + return nil, 0, err } a.Columns = append(a.Columns, offset+1) default: - return 0, vterrors.VT13002(sqlparser.String(expr)) + return nil, 0, vterrors.VT13002(sqlparser.String(expr)) } // the expression wasn't already there - let's add it - a.ColumnsAST = append(a.ColumnsAST, expr) - return len(a.Columns) - 1, nil + a.ColumnsAST = append(a.ColumnsAST, expr.Expr) + return a, len(a.Columns) - 1, nil } diff --git a/go/vt/vtgate/planbuilder/operators/derived.go b/go/vt/vtgate/planbuilder/operators/derived.go index 06fa8a3f7af..1d0e95ac329 100644 --- a/go/vt/vtgate/planbuilder/operators/derived.go +++ b/go/vt/vtgate/planbuilder/operators/derived.go @@ -19,6 +19,8 @@ package operators import ( "golang.org/x/exp/slices" + "vitess.io/vitess/go/slices2" + "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" "vitess.io/vitess/go/vt/sqlparser" @@ -132,15 +134,19 @@ func (d *Derived) AddPredicate(ctx *plancontext.PlanningContext, expr sqlparser. return d, nil } -func (d *Derived) AddColumn(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (int, error) { - col, ok := expr.(*sqlparser.ColName) +func (d *Derived) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + col, ok := expr.Expr.(*sqlparser.ColName) if !ok { - return 0, vterrors.VT13001("cannot push non-colname expression to a derived table") + return nil, 0, vterrors.VT13001("cannot push non-colname expression to a derived table") + } + + if offset, found := canReuseColumn(ctx, reuseCol, d.Columns, col); found { + return d, offset, nil } i, err := d.findOutputColumn(col) if err != nil { - return 0, err + return nil, 0, err } var pos int d.ColumnsOffset, pos = addToIntSlice(d.ColumnsOffset, i) @@ -148,12 +154,38 @@ func (d *Derived) AddColumn(ctx *plancontext.PlanningContext, expr sqlparser.Exp d.Columns = append(d.Columns, col) // add it to the source if we were not already passing it through if i <= -1 { - _, err := d.Source.AddColumn(ctx, sqlparser.NewColName(col.Name.String())) + newSrc, _, err := d.Source.AddColumn(ctx, aeWrap(sqlparser.NewColName(col.Name.String())), true) if err != nil { - return 0, err + return nil, 0, err + } + d.Source = newSrc + } + return d, pos, nil +} + +// canReuseColumn is generic, so it can be used with slices of different types. +// We don't care about the actual type, as long as we know it's a sqlparser.Expr +func canReuseColumn[Expr sqlparser.Expr]( + ctx *plancontext.PlanningContext, + reuseCol bool, + columns []Expr, + col sqlparser.Expr, +) (offset int, found bool) { + if !reuseCol { + return + } + + for offset, column := range columns { + if ctx.SemTable.EqualsExpr(col, column) { + return offset, true } } - return pos, nil + + return +} + +func (d *Derived) GetColumns() ([]sqlparser.Expr, error) { + return slices2.Map(d.Columns, colNameToExpr), nil } func addToIntSlice(columnOffset []int, valToAdd int) ([]int, int) { diff --git a/go/vt/vtgate/planbuilder/operators/filter.go b/go/vt/vtgate/planbuilder/operators/filter.go index d28511dbe86..397ad71b74b 100644 --- a/go/vt/vtgate/planbuilder/operators/filter.go +++ b/go/vt/vtgate/planbuilder/operators/filter.go @@ -18,6 +18,7 @@ package operators import ( "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vtgate/evalengine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/rewrite" "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" @@ -27,6 +28,10 @@ import ( type Filter struct { Source ops.Operator Predicates []sqlparser.Expr + + // FinalPredicate is the evalengine expression that will finally be used. + // It contains the ANDed predicates in Predicates, with ColName:s replaced by Offset:s + FinalPredicate evalengine.Expr } var _ ops.PhysicalOperator = (*Filter)(nil) @@ -77,8 +82,17 @@ func (f *Filter) AddPredicate(ctx *plancontext.PlanningContext, expr sqlparser.E return f, nil } -func (f *Filter) AddColumn(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (int, error) { - return f.Source.AddColumn(ctx, expr) +func (f *Filter) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + newSrc, offset, err := f.Source.AddColumn(ctx, expr, reuseCol) + if err != nil { + return nil, 0, err + } + f.Source = newSrc + return f, offset, nil +} + +func (f *Filter) GetColumns() ([]sqlparser.Expr, error) { + return f.Source.GetColumns() } func (f *Filter) Compact(*plancontext.PlanningContext) (ops.Operator, rewrite.TreeIdentity, error) { diff --git a/go/vt/vtgate/planbuilder/operators/helpers.go b/go/vt/vtgate/planbuilder/operators/helpers.go index 3ac028851b4..8c43ed1c372 100644 --- a/go/vt/vtgate/planbuilder/operators/helpers.go +++ b/go/vt/vtgate/planbuilder/operators/helpers.go @@ -35,7 +35,7 @@ func Compact(ctx *plancontext.PlanningContext, op ops.Operator) (ops.Operator, e Compact(ctx *plancontext.PlanningContext) (ops.Operator, rewrite.TreeIdentity, error) } - newOp, err := rewrite.BottomUp(op, semantics.EmptyTableSet(), TableID, func(_ semantics.TableSet, op ops.Operator) (ops.Operator, rewrite.TreeIdentity, error) { + newOp, err := rewrite.BottomUpAll(op, TableID, func(op ops.Operator, _ semantics.TableSet) (ops.Operator, rewrite.TreeIdentity, error) { newOp, ok := op.(compactable) if !ok { return op, rewrite.SameTree, nil diff --git a/go/vt/vtgate/planbuilder/operators/horizon.go b/go/vt/vtgate/planbuilder/operators/horizon.go index 7bbe3eb9e98..8be1fbb8776 100644 --- a/go/vt/vtgate/planbuilder/operators/horizon.go +++ b/go/vt/vtgate/planbuilder/operators/horizon.go @@ -22,8 +22,13 @@ import ( "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" ) -// Horizon is an operator we use until we decide how to handle the source to the horizon. +// Horizon is an operator that allows us to postpone planning things like SELECT/GROUP BY/ORDER BY/LIMIT until later. // It contains information about the planning we have to do after deciding how we will send the query to the tablets. +// If we are able to push down the Horizon under a route, we don't have to plan these things separately and can +// just copy over the AST constructs to the query being sent to a tablet. +// If we are not able to push it down, this operator needs to be split up into smaller +// Project/Aggregate/Sort/Limit operations, some which can be pushed down, +// and some that have to be evaluated at the vtgate level. type Horizon struct { Source ops.Operator Select sqlparser.SelectStatement diff --git a/go/vt/vtgate/planbuilder/operators/horizon_planning.go b/go/vt/vtgate/planbuilder/operators/horizon_planning.go index 61d6b56f4f7..5a853cbf916 100644 --- a/go/vt/vtgate/planbuilder/operators/horizon_planning.go +++ b/go/vt/vtgate/planbuilder/operators/horizon_planning.go @@ -19,35 +19,68 @@ package operators import ( "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vterrors" + "vitess.io/vitess/go/vt/vtgate/evalengine" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/rewrite" "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" + "vitess.io/vitess/go/vt/vtgate/semantics" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" ) var errNotHorizonPlanned = vterrors.VT12001("query cannot be fully operator planned") -func planHorizons(ctx *plancontext.PlanningContext, in ops.Operator) (ops.Operator, error) { - return rewrite.TopDown(in, func(in ops.Operator) (ops.Operator, rewrite.TreeIdentity, rewrite.VisitRule, error) { +// planColumns is the process of figuring out all necessary columns. +// They can be needed because the user wants to return the value of a column, +// or because we need a column for filtering, grouping or ordering +func planColumns(ctx *plancontext.PlanningContext, root ops.Operator) (ops.Operator, error) { + // We only need to do column planning to the point we hit a Route. + // Everything underneath the route is handled by the mysql planner + stopAtRoute := func(operator ops.Operator) rewrite.VisitRule { + _, isRoute := operator.(*Route) + return rewrite.VisitRule(!isRoute) + } + visitor := func(in ops.Operator, _ semantics.TableSet) (ops.Operator, rewrite.TreeIdentity, error) { switch in := in.(type) { case *Horizon: - op, visit, err := planHorizon(ctx, in) + op, err := planHorizon(ctx, in, in == root) + if err != nil { + return nil, false, err + } + return op, rewrite.NewTree, nil + case *Derived: + op, err := planDerived(ctx, in, in == root) if err != nil { - return nil, rewrite.SameTree, rewrite.SkipChildren, err + return nil, false, err } - return op, rewrite.NewTree, visit, nil - case *Route: - return in, rewrite.SameTree, rewrite.SkipChildren, nil + return op, rewrite.NewTree, err + case *Filter: + err := planFilter(ctx, in) + if err != nil { + return nil, false, err + } + return in, rewrite.SameTree, nil default: - return in, rewrite.SameTree, rewrite.VisitChildren, nil + return in, rewrite.SameTree, nil } - }) + } + + newOp, err := rewrite.BottomUp(root, TableID, visitor, stopAtRoute) + if err != nil { + if vterr, ok := err.(*vterrors.VitessError); ok && vterr.ID == "VT13001" { + // we encountered a bug. let's try to back out + return nil, errNotHorizonPlanned + } + return nil, err + } + + return newOp, nil } -func planHorizon(ctx *plancontext.PlanningContext, in *Horizon) (ops.Operator, rewrite.VisitRule, error) { +func planHorizon(ctx *plancontext.PlanningContext, in *Horizon, isRoot bool) (ops.Operator, error) { rb, isRoute := in.Source.(*Route) - if !isRoute { - return in, rewrite.VisitChildren, nil + if isRoot && !isRoute && ctx.SemTable.NotSingleRouteErr != nil { + // If we got here, we don't have a single shard plan + return nil, ctx.SemTable.NotSingleRouteErr } if isRoute && rb.IsSingleShard() && in.Select.GetLimit() == nil { return planSingleRoute(rb, in) @@ -55,24 +88,127 @@ func planHorizon(ctx *plancontext.PlanningContext, in *Horizon) (ops.Operator, r sel, isSel := in.Select.(*sqlparser.Select) if !isSel { - return nil, rewrite.VisitChildren, errNotHorizonPlanned + return nil, errNotHorizonPlanned } qp, err := CreateQPFromSelect(ctx, sel) if err != nil { - return nil, rewrite.VisitChildren, err + return nil, err } needsOrdering := len(qp.OrderExprs) > 0 canShortcut := isRoute && sel.Having == nil && !needsOrdering + _, isDerived := in.Source.(*Derived) - if !qp.NeedsAggregation() && sel.Having == nil && canShortcut && !needsOrdering && !qp.NeedsDistinct() && in.Select.GetLimit() == nil { + switch { + case qp.NeedsAggregation() || sel.Having != nil || sel.Limit != nil || isDerived || needsOrdering || qp.NeedsDistinct(): + return nil, errNotHorizonPlanned + case canShortcut: return planSingleRoute(rb, in) + default: + return pushProjections(ctx, qp, in.Source, isRoot) + } +} + +func planDerived(ctx *plancontext.PlanningContext, in *Derived, isRoot bool) (ops.Operator, error) { + rb, isRoute := in.Source.(*Route) + + sel, isSel := in.Query.(*sqlparser.Select) + if !isSel { + return nil, errNotHorizonPlanned + } + + qp, err := CreateQPFromSelect(ctx, sel) + if err != nil { + return nil, err + } + + needsOrdering := len(qp.OrderExprs) > 0 + canShortcut := isRoute && sel.Having == nil && !needsOrdering + _, isDerived := in.Source.(*Derived) + + switch { + case qp.NeedsAggregation() || sel.Having != nil || sel.Limit != nil || isDerived || needsOrdering || qp.NeedsDistinct(): + return nil, errNotHorizonPlanned + case canShortcut: + // shortcut here means we don't need to plan the derived table, we can just push it under the route + rb.Source, in.Source = in.Source, in + return rb, nil + default: + return pushProjections(ctx, qp, in.Source, isRoot) } - return nil, rewrite.VisitChildren, errNotHorizonPlanned } -func planSingleRoute(rb *Route, horizon *Horizon) (ops.Operator, rewrite.VisitRule, error) { +func pushProjections(ctx *plancontext.PlanningContext, qp *QueryProjection, src ops.Operator, isRoot bool) (ops.Operator, error) { + // if we are at the root, we have to return the columns the user asked for. in all other levels, we reuse as much as possible + canReuseCols := !isRoot + proj := newSimpleProjection(src) + needProj := false + for idx, e := range qp.SelectExprs { + expr, err := e.GetAliasedExpr() + if err != nil { + return nil, err + } + if !expr.As.IsEmpty() { + // we are not handling column names correct yet, so let's fail here for now + return nil, errNotHorizonPlanned + } + var offset int + src, offset, err = src.AddColumn(ctx, expr, canReuseCols) + if err != nil { + return nil, err + } + + if offset != idx { + needProj = true + } + proj.ASTColumns = append(proj.ASTColumns, expr) + proj.Columns = append(proj.Columns, offset) + } + + if needProj { + return proj, nil + } + + columns, err := src.GetColumns() + if err != nil { + return nil, err + } + if len(columns) == qp.GetColumnCount() { + return src, nil + } + return proj, nil +} + +func planSingleRoute(rb *Route, horizon *Horizon) (ops.Operator, error) { rb.Source, horizon.Source = horizon, rb.Source - return rb, rewrite.SkipChildren, nil + return rb, nil +} + +func aeWrap(e sqlparser.Expr) *sqlparser.AliasedExpr { + return &sqlparser.AliasedExpr{Expr: e} +} + +func planFilter(ctx *plancontext.PlanningContext, in *Filter) error { + resolveColumn := func(col *sqlparser.ColName) (int, error) { + newSrc, offset, err := in.Source.AddColumn(ctx, aeWrap(col), true) + if err != nil { + return 0, err + } + in.Source = newSrc + return offset, nil + } + cfg := &evalengine.Config{ + ResolveType: ctx.SemTable.TypeForExpr, + Collation: ctx.SemTable.Collation, + ResolveColumn: resolveColumn, + } + + eexpr, err := evalengine.Translate(sqlparser.AndExpressions(in.Predicates...), cfg) + if err != nil { + return err + } + + in.FinalPredicate = eexpr + return nil } diff --git a/go/vt/vtgate/planbuilder/operators/operator.go b/go/vt/vtgate/planbuilder/operators/operator.go index 35a3d2af91d..738673480ec 100644 --- a/go/vt/vtgate/planbuilder/operators/operator.go +++ b/go/vt/vtgate/planbuilder/operators/operator.go @@ -68,7 +68,7 @@ func PlanQuery(ctx *plancontext.PlanningContext, selStmt sqlparser.Statement) (o backup := Clone(op) - op, err = planHorizons(ctx, op) + op, err = planColumns(ctx, op) if err == errNotHorizonPlanned { op = backup } else if err != nil { @@ -88,8 +88,12 @@ func (noInputs) Inputs() []ops.Operator { } // AddColumn implements the Operator interface -func (noColumns) AddColumn(*plancontext.PlanningContext, sqlparser.Expr) (int, error) { - return 0, vterrors.VT13001("the noColumns operator cannot accept columns") +func (noColumns) AddColumn(*plancontext.PlanningContext, *sqlparser.AliasedExpr, bool) (ops.Operator, int, error) { + return nil, 0, vterrors.VT13001("the noColumns operator cannot accept columns") +} + +func (noColumns) GetColumns() ([]sqlparser.Expr, error) { + return nil, vterrors.VT13001("the noColumns operator cannot accept columns") } // AddPredicate implements the Operator interface diff --git a/go/vt/vtgate/planbuilder/operators/ops/op.go b/go/vt/vtgate/planbuilder/operators/ops/op.go index 4deeb5cee1e..a22df663368 100644 --- a/go/vt/vtgate/planbuilder/operators/ops/op.go +++ b/go/vt/vtgate/planbuilder/operators/ops/op.go @@ -39,7 +39,9 @@ type ( // AddColumn tells an operator to also output an additional column specified. // The offset to the column is returned. - AddColumn(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (int, error) + AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (Operator, int, error) + + GetColumns() ([]sqlparser.Expr, error) } // PhysicalOperator means that this operator is ready to be turned into a logical plan diff --git a/go/vt/vtgate/planbuilder/operators/rewrite/rewriters.go b/go/vt/vtgate/planbuilder/operators/rewrite/rewriters.go index 30d6891c193..1eb3b879bf8 100644 --- a/go/vt/vtgate/planbuilder/operators/rewrite/rewriters.go +++ b/go/vt/vtgate/planbuilder/operators/rewrite/rewriters.go @@ -22,8 +22,14 @@ import ( ) type ( - Func func(semantics.TableSet, ops.Operator) (ops.Operator, TreeIdentity, error) - BreakableFunc func(ops.Operator) (ops.Operator, TreeIdentity, VisitRule, error) + // VisitF is the visitor that walks an operator tree + VisitF func( + op ops.Operator, // op is the operator being visited + lhsTables semantics.TableSet, // lhsTables contains the TableSet for all table on the LHS of our parent + ) (ops.Operator, TreeIdentity, error) + + // ShouldVisit is used when we want to control which nodes and ancestors to visit and which to skip + ShouldVisit func(ops.Operator) VisitRule // TreeIdentity tracks modifications to node and expression trees. // Only return SameTree when it is acceptable to return the original @@ -44,7 +50,7 @@ const ( // Visit allows for the walking of the operator tree. If any error is returned, the walk is aborted func Visit(root ops.Operator, visitor func(ops.Operator) error) error { - _, err := TopDown(root, func(op ops.Operator) (ops.Operator, TreeIdentity, VisitRule, error) { + _, _, err := breakableTopDown(root, func(op ops.Operator) (ops.Operator, TreeIdentity, VisitRule, error) { err := visitor(op) if err != nil { return nil, SameTree, SkipChildren, err @@ -57,24 +63,43 @@ func Visit(root ops.Operator, visitor func(ops.Operator) error) error { // BottomUp rewrites an operator tree from the bottom up. BottomUp applies a transformation function to // the given operator tree from the bottom up. Each callback [f] returns a TreeIdentity that is aggregated // into a final output indicating whether the operator tree was changed. -func BottomUp(root ops.Operator, rootID semantics.TableSet, resolveID func(ops.Operator) semantics.TableSet, f Func) (ops.Operator, error) { - op, _, err := bottomUp(root, rootID, resolveID, f) +func BottomUp( + root ops.Operator, + resolveID func(ops.Operator) semantics.TableSet, + visit VisitF, + shouldVisit ShouldVisit, +) (ops.Operator, error) { + op, _, err := bottomUp(root, semantics.EmptyTableSet(), resolveID, visit, shouldVisit) if err != nil { return nil, err } return op, nil } -// TopDown applies a transformation function to the given operator tree from the bottom up. = -// Each callback [f] returns a TreeIdentity that is aggregated into a final output indicating whether the -// operator tree was changed. -// The callback also returns a VisitRule that signals whether the children of this operator should be visited or not -func TopDown(in ops.Operator, rewriter BreakableFunc) (ops.Operator, error) { - op, _, err := breakableTopDown(in, rewriter) - return op, err +// BottomUp rewrites an operator tree from the bottom up. BottomUp applies a transformation function to +// the given operator tree from the bottom up. Each callback [f] returns a TreeIdentity that is aggregated +// into a final output indicating whether the operator tree was changed. +func BottomUpAll( + root ops.Operator, + resolveID func(ops.Operator) semantics.TableSet, + visit VisitF, +) (ops.Operator, error) { + return BottomUp(root, resolveID, visit, func(ops.Operator) VisitRule { + return VisitChildren + }) } -func bottomUp(root ops.Operator, rootID semantics.TableSet, resolveID func(ops.Operator) semantics.TableSet, rewriter Func) (ops.Operator, TreeIdentity, error) { +func bottomUp( + root ops.Operator, + rootID semantics.TableSet, + resolveID func(ops.Operator) semantics.TableSet, + rewriter VisitF, + shouldVisit ShouldVisit, +) (ops.Operator, TreeIdentity, error) { + if !shouldVisit(root) { + return root, SameTree, nil + } + oldInputs := root.Inputs() anythingChanged := false newInputs := make([]ops.Operator, len(oldInputs)) @@ -88,12 +113,12 @@ func bottomUp(root ops.Operator, rootID semantics.TableSet, resolveID func(ops.O for i, operator := range oldInputs { // We merge the table set of all the LHS above the current root so that we can // send it down to the current RHS. - // We don't want to send the LHS table set to the RHS if the root is an UNION. + // We don't want to send the LHS table set to the RHS if the root is a UNION. // Some operators, like SubQuery, can have multiple child operators on the RHS if _, isUnion := root.(noLHSTableSet); !isUnion && i > 0 { childID = childID.Merge(resolveID(oldInputs[0])) } - in, changed, err := bottomUp(operator, childID, resolveID, rewriter) + in, changed, err := bottomUp(operator, childID, resolveID, rewriter, shouldVisit) if err != nil { return nil, SameTree, err } @@ -107,7 +132,7 @@ func bottomUp(root ops.Operator, rootID semantics.TableSet, resolveID func(ops.O root = root.Clone(newInputs) } - newOp, treeIdentity, err := rewriter(rootID, root) + newOp, treeIdentity, err := rewriter(root, rootID) if err != nil { return nil, SameTree, err } @@ -117,7 +142,10 @@ func bottomUp(root ops.Operator, rootID semantics.TableSet, resolveID func(ops.O return newOp, treeIdentity, nil } -func breakableTopDown(in ops.Operator, rewriter BreakableFunc) (ops.Operator, TreeIdentity, error) { +func breakableTopDown( + in ops.Operator, + rewriter func(ops.Operator) (ops.Operator, TreeIdentity, VisitRule, error), +) (ops.Operator, TreeIdentity, error) { newOp, identity, visit, err := rewriter(in) if err != nil || visit == SkipChildren { return newOp, identity, err diff --git a/go/vt/vtgate/planbuilder/operators/route.go b/go/vt/vtgate/planbuilder/operators/route.go index 276de2a23c1..bc967c82722 100644 --- a/go/vt/vtgate/planbuilder/operators/route.go +++ b/go/vt/vtgate/planbuilder/operators/route.go @@ -517,8 +517,17 @@ func (r *Route) AddPredicate(ctx *plancontext.PlanningContext, expr sqlparser.Ex return r, err } -func (r *Route) AddColumn(ctx *plancontext.PlanningContext, e sqlparser.Expr) (int, error) { - return r.Source.AddColumn(ctx, e) +func (r *Route) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + newSrc, offset, err := r.Source.AddColumn(ctx, expr, reuseCol) + if err != nil { + return nil, 0, err + } + r.Source = newSrc + return r, offset, nil +} + +func (r *Route) GetColumns() ([]sqlparser.Expr, error) { + return r.Source.GetColumns() } // TablesUsed returns tables used by MergedWith routes, which are not included diff --git a/go/vt/vtgate/planbuilder/operators/route_planning.go b/go/vt/vtgate/planbuilder/operators/route_planning.go index 8386b2e0f5b..3b6bdc8734a 100644 --- a/go/vt/vtgate/planbuilder/operators/route_planning.go +++ b/go/vt/vtgate/planbuilder/operators/route_planning.go @@ -50,7 +50,7 @@ type ( // Here we try to merge query parts into the same route primitives. At the end of this process, // all the operators in the tree are guaranteed to be PhysicalOperators func transformToPhysical(ctx *plancontext.PlanningContext, in ops.Operator) (ops.Operator, error) { - op, err := rewrite.BottomUp(in, semantics.EmptyTableSet(), TableID, func(ts semantics.TableSet, operator ops.Operator) (ops.Operator, rewrite.TreeIdentity, error) { + op, err := rewrite.BottomUpAll(in, TableID, func(operator ops.Operator, ts semantics.TableSet) (ops.Operator, rewrite.TreeIdentity, error) { switch op := operator.(type) { case *QueryGraph: return optimizeQueryGraph(ctx, op) @@ -115,7 +115,7 @@ func optimizeDerived(ctx *plancontext.PlanningContext, op *Derived) (ops.Operato func optimizeJoin(ctx *plancontext.PlanningContext, op *Join) (ops.Operator, rewrite.TreeIdentity, error) { join, err := mergeOrJoin(ctx, op.LHS, op.RHS, sqlparser.SplitAndExpression(nil, op.Predicate), !op.LeftJoin) if err != nil { - return nil, rewrite.SameTree, err + return nil, false, err } return join, rewrite.NewTree, nil } diff --git a/go/vt/vtgate/planbuilder/operators/simple_projection.go b/go/vt/vtgate/planbuilder/operators/simple_projection.go new file mode 100644 index 00000000000..f533f7875bb --- /dev/null +++ b/go/vt/vtgate/planbuilder/operators/simple_projection.go @@ -0,0 +1,73 @@ +/* +Copyright 2023 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 operators + +import ( + "golang.org/x/exp/slices" + + "vitess.io/vitess/go/slices2" + "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" + "vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext" +) + +// SimpleProjection is used to be selective about which columns to pass through +// All it does is to map through columns from the input +// It's used to limit the number of columns to hide result from the user that was not requested +type SimpleProjection struct { + Source ops.Operator + Columns []int + ASTColumns []*sqlparser.AliasedExpr +} + +var _ ops.PhysicalOperator = (*SimpleProjection)(nil) + +func newSimpleProjection(src ops.Operator) *SimpleProjection { + return &SimpleProjection{ + Source: src, + } +} + +func (s *SimpleProjection) IPhysical() {} + +func (s *SimpleProjection) Clone(inputs []ops.Operator) ops.Operator { + return &SimpleProjection{ + Source: inputs[0], + Columns: slices.Clone(s.Columns), + ASTColumns: slices.Clone(s.ASTColumns), + } +} + +func (s *SimpleProjection) Inputs() []ops.Operator { + return []ops.Operator{s.Source} +} + +func (s *SimpleProjection) AddPredicate(ctx *plancontext.PlanningContext, expr sqlparser.Expr) (ops.Operator, error) { + // TODO implement me + return nil, errNotHorizonPlanned +} + +func (s *SimpleProjection) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + // TODO implement me + return nil, 0, errNotHorizonPlanned +} + +func exprFromAliasedExpr(from *sqlparser.AliasedExpr) sqlparser.Expr { return from.Expr } + +func (s *SimpleProjection) GetColumns() ([]sqlparser.Expr, error) { + return slices2.Map(s.ASTColumns, exprFromAliasedExpr), nil +} diff --git a/go/vt/vtgate/planbuilder/operators/subquery_planning.go b/go/vt/vtgate/planbuilder/operators/subquery_planning.go index 890dae5005f..320387dee59 100644 --- a/go/vt/vtgate/planbuilder/operators/subquery_planning.go +++ b/go/vt/vtgate/planbuilder/operators/subquery_planning.go @@ -44,7 +44,7 @@ func optimizeSubQuery(ctx *plancontext.PlanningContext, op *SubQuery, ts semanti } merged, err := tryMergeSubQueryOp(ctx, outer, innerOp, newInner, preds, newSubQueryMerge(ctx, newInner), ts) if err != nil { - return nil, rewrite.SameTree, err + return nil, false, err } if merged != nil { @@ -65,13 +65,13 @@ func optimizeSubQuery(ctx *plancontext.PlanningContext, op *SubQuery, ts semanti if inner.ExtractedSubquery.OpCode == int(popcode.PulloutExists) { correlatedTree, err := createCorrelatedSubqueryOp(ctx, innerOp, outer, preds, inner.ExtractedSubquery) if err != nil { - return nil, rewrite.SameTree, err + return nil, false, err } outer = correlatedTree continue } - return nil, rewrite.SameTree, vterrors.VT12001("cross-shard correlated subquery") + return nil, false, vterrors.VT12001("cross-shard correlated subquery") } for _, tree := range unmerged { @@ -361,11 +361,12 @@ func rewriteColumnsInSubqueryOpForJoin( return true } // if it does not exist, then push this as an output column there and add it to the joinVars - offset, err := resultInnerOp.AddColumn(ctx, node) + newInnerOp, offset, err := resultInnerOp.AddColumn(ctx, aeWrap(node), true) if err != nil { rewriteError = err return false } + resultInnerOp = newInnerOp outerTree.Vars[bindVar] = offset return true }) @@ -426,11 +427,12 @@ func createCorrelatedSubqueryOp( bindVars[node] = bindVar // if it does not exist, then push this as an output column in the outerOp and add it to the joinVars - offset, err := resultOuterOp.AddColumn(ctx, node) + newOuterOp, offset, err := resultOuterOp.AddColumn(ctx, aeWrap(node), true) if err != nil { rewriteError = err return true } + resultOuterOp = newOuterOp lhsCols = append(lhsCols, node) vars[bindVar] = offset return true diff --git a/go/vt/vtgate/planbuilder/operators/table.go b/go/vt/vtgate/planbuilder/operators/table.go index 593dfe3ec7a..c9aad4a25dd 100644 --- a/go/vt/vtgate/planbuilder/operators/table.go +++ b/go/vt/vtgate/planbuilder/operators/table.go @@ -17,6 +17,7 @@ limitations under the License. package operators import ( + "vitess.io/vitess/go/slices2" "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/vt/vtgate/planbuilder/operators/ops" @@ -34,7 +35,7 @@ type ( noInputs } ColNameColumns interface { - GetColumns() []*sqlparser.ColName + GetColNames() []*sqlparser.ColName AddCol(*sqlparser.ColName) } ) @@ -67,11 +68,20 @@ func (to *Table) AddPredicate(_ *plancontext.PlanningContext, expr sqlparser.Exp return newFilter(to, expr), nil } -func (to *Table) AddColumn(_ *plancontext.PlanningContext, e sqlparser.Expr) (int, error) { - return addColumn(to, e) +func (to *Table) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + offset, err := addColumn(ctx, to, expr.Expr, reuseCol) + if err != nil { + return nil, 0, err + } + + return to, offset, nil +} + +func (to *Table) GetColumns() ([]sqlparser.Expr, error) { + return slices2.Map(to.Columns, colNameToExpr), nil } -func (to *Table) GetColumns() []*sqlparser.ColName { +func (to *Table) GetColNames() []*sqlparser.ColName { return to.Columns } func (to *Table) AddCol(col *sqlparser.ColName) { @@ -85,16 +95,15 @@ func (to *Table) TablesUsed() []string { return SingleQualifiedIdentifier(to.VTable.Keyspace, to.VTable.Name) } -func addColumn(op ColNameColumns, e sqlparser.Expr) (int, error) { +func addColumn(ctx *plancontext.PlanningContext, op ColNameColumns, e sqlparser.Expr, reuseCol bool) (int, error) { col, ok := e.(*sqlparser.ColName) if !ok { return 0, vterrors.VT13001("cannot push this expression to a table/vindex") } - cols := op.GetColumns() - for idx, column := range cols { - if col.Name.Equal(column.Name) { - return idx, nil - } + sqlparser.RemoveKeyspaceFromColName(col) + cols := op.GetColNames() + if offset, found := canReuseColumn(ctx, reuseCol, cols, e); found { + return offset, nil } offset := len(cols) op.AddCol(col) diff --git a/go/vt/vtgate/planbuilder/operators/vindex.go b/go/vt/vtgate/planbuilder/operators/vindex.go index 0c0d6976fb5..44045a4ed4a 100644 --- a/go/vt/vtgate/planbuilder/operators/vindex.go +++ b/go/vt/vtgate/planbuilder/operators/vindex.go @@ -17,6 +17,7 @@ limitations under the License. package operators import ( + "vitess.io/vitess/go/slices2" "vitess.io/vitess/go/vt/sqlparser" "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/vt/vtgate/engine" @@ -66,18 +67,28 @@ func (v *Vindex) Clone([]ops.Operator) ops.Operator { var _ ops.PhysicalOperator = (*Vindex)(nil) -func (v *Vindex) AddColumn(_ *plancontext.PlanningContext, expr sqlparser.Expr) (int, error) { - return addColumn(v, expr) +func (v *Vindex) AddColumn(ctx *plancontext.PlanningContext, expr *sqlparser.AliasedExpr, reuseCol bool) (ops.Operator, int, error) { + offset, err := addColumn(ctx, v, expr.Expr, reuseCol) + if err != nil { + return nil, 0, err + } + + return v, offset, nil +} + +func colNameToExpr(c *sqlparser.ColName) sqlparser.Expr { return c } + +func (v *Vindex) GetColumns() ([]sqlparser.Expr, error) { + return slices2.Map(v.Columns, colNameToExpr), nil } -func (v *Vindex) GetColumns() []*sqlparser.ColName { +func (v *Vindex) GetColNames() []*sqlparser.ColName { return v.Columns } func (v *Vindex) AddCol(col *sqlparser.ColName) { v.Columns = append(v.Columns, col) } -// checkValid implements the Operator interface func (v *Vindex) CheckValid() error { if len(v.Table.Predicates) == 0 { return vterrors.VT12001(VindexUnsupported + " (where clause missing)") diff --git a/go/vt/vtgate/planbuilder/simple_projection.go b/go/vt/vtgate/planbuilder/simple_projection.go index fb9894a89e9..c413630f386 100644 --- a/go/vt/vtgate/planbuilder/simple_projection.go +++ b/go/vt/vtgate/planbuilder/simple_projection.go @@ -35,7 +35,6 @@ var _ logicalPlan = (*simpleProjection)(nil) // a new route that keeps the subquery in the FROM // clause, because a route is more versatile than // a simpleProjection. -// this should not be used by the gen4 planner type simpleProjection struct { logicalPlanCommon resultColumns []*resultColumn diff --git a/go/vt/vtgate/planbuilder/testdata/from_cases.json b/go/vt/vtgate/planbuilder/testdata/from_cases.json index 3e0aab39179..641a34bda46 100644 --- a/go/vt/vtgate/planbuilder/testdata/from_cases.json +++ b/go/vt/vtgate/planbuilder/testdata/from_cases.json @@ -3158,7 +3158,7 @@ "Instructions": { "OperatorType": "Join", "Variant": "Join", - "JoinColumnIndexes": "L:0", + "JoinColumnIndexes": "L:1", "JoinVars": { "t_col1": 0, "t_id": 1 @@ -3166,41 +3166,32 @@ "TableName": "`user`_user_extra_unsharded", "Inputs": [ { - "OperatorType": "SimpleProjection", - "Columns": [ - 1, - 0 - ], + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0,L:1", + "TableName": "`user`_user_extra", "Inputs": [ { - "OperatorType": "Join", - "Variant": "Join", - "JoinColumnIndexes": "L:0,L:1", - "TableName": "`user`_user_extra", - "Inputs": [ - { - "OperatorType": "Route", - "Variant": "Scatter", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", - "Query": "select `user`.id, `user`.col1 from `user`", - "Table": "`user`" - }, - { - "OperatorType": "Route", - "Variant": "Scatter", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select 1 from user_extra where 1 != 1", - "Query": "select 1 from user_extra", - "Table": "user_extra" - } - ] + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", + "Query": "select `user`.id, `user`.col1 from `user`", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from user_extra where 1 != 1", + "Query": "select 1 from user_extra", + "Table": "user_extra" } ] }, @@ -3392,7 +3383,7 @@ "Instructions": { "OperatorType": "Join", "Variant": "Join", - "JoinColumnIndexes": "R:0", + "JoinColumnIndexes": "R:1", "TableName": "unsharded_a_`user`_user_extra", "Inputs": [ { @@ -3407,40 +3398,32 @@ "Table": "unsharded_a" }, { - "OperatorType": "SimpleProjection", - "Columns": [ - 1 - ], + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0,L:1", + "TableName": "`user`_user_extra", "Inputs": [ { - "OperatorType": "Join", - "Variant": "Join", - "JoinColumnIndexes": "L:0,L:1", - "TableName": "`user`_user_extra", - "Inputs": [ - { - "OperatorType": "Route", - "Variant": "Scatter", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", - "Query": "select `user`.id, `user`.col1 from `user`", - "Table": "`user`" - }, - { - "OperatorType": "Route", - "Variant": "Scatter", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select 1 from user_extra where 1 != 1", - "Query": "select 1 from user_extra", - "Table": "user_extra" - } - ] + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", + "Query": "select `user`.id, `user`.col1 from `user`", + "Table": "`user`" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from user_extra where 1 != 1", + "Query": "select 1 from user_extra", + "Table": "user_extra" } ] } @@ -3463,7 +3446,7 @@ "Instructions": { "OperatorType": "Join", "Variant": "Join", - "JoinColumnIndexes": "R:0", + "JoinColumnIndexes": "R:1", "JoinVars": { "ua_id": 0 }, @@ -3481,44 +3464,36 @@ "Table": "unsharded_a" }, { - "OperatorType": "SimpleProjection", - "Columns": [ - 1 - ], + "OperatorType": "Join", + "Variant": "Join", + "JoinColumnIndexes": "L:0,L:1", + "TableName": "`user`_user_extra", "Inputs": [ { - "OperatorType": "Join", - "Variant": "Join", - "JoinColumnIndexes": "L:0,L:1", - "TableName": "`user`_user_extra", - "Inputs": [ - { - "OperatorType": "Route", - "Variant": "EqualUnique", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", - "Query": "select `user`.id, `user`.col1 from `user` where `user`.id = :ua_id", - "Table": "`user`", - "Values": [ - ":ua_id" - ], - "Vindex": "user_index" - }, - { - "OperatorType": "Route", - "Variant": "Scatter", - "Keyspace": { - "Name": "user", - "Sharded": true - }, - "FieldQuery": "select 1 from user_extra where 1 != 1", - "Query": "select 1 from user_extra", - "Table": "user_extra" - } - ] + "OperatorType": "Route", + "Variant": "EqualUnique", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select `user`.id, `user`.col1 from `user` where 1 != 1", + "Query": "select `user`.id, `user`.col1 from `user` where `user`.id = :ua_id", + "Table": "`user`", + "Values": [ + ":ua_id" + ], + "Vindex": "user_index" + }, + { + "OperatorType": "Route", + "Variant": "Scatter", + "Keyspace": { + "Name": "user", + "Sharded": true + }, + "FieldQuery": "select 1 from user_extra where 1 != 1", + "Query": "select 1 from user_extra", + "Table": "user_extra" } ] } diff --git a/go/vt/vtgate/semantics/semantic_state.go b/go/vt/vtgate/semantics/semantic_state.go index 377f60b6455..b2843159305 100644 --- a/go/vt/vtgate/semantics/semantic_state.go +++ b/go/vt/vtgate/semantics/semantic_state.go @@ -129,6 +129,16 @@ func (st *SemTable) CopyDependencies(from, to sqlparser.Expr) { st.Direct[to] = st.DirectDeps(from) } +// CopyDependencies copies the dependencies from one expression into the other +func (st *SemTable) Cloned(from, to sqlparser.SQLNode) { + f, fromOK := from.(sqlparser.Expr) + t, toOK := to.(sqlparser.Expr) + if !(fromOK && toOK) { + return + } + st.CopyDependencies(f, t) +} + // EmptySemTable creates a new empty SemTable func EmptySemTable() *SemTable { return &SemTable{