// Projection binding (split from lower.go for the file-size norm): // compile a projection body against the segment scope, resolve ORDER BY // keys, and hoist nested aggregates into hidden accumulator slots.
package plan import ( ) // bindProjection compiles a projection body against the segment scope. func bindProjection( ast.Projection, map[string]int) (ProjPlan, error) { var []BoundReturn // The explicit items carry the output-column names: an alias, else the // derived name. Two explicit items may not share a name -- the result // table cannot have two columns of the same name, and a later ORDER BY / // column reference to it would be ambiguous (openCypher and ISO GQL both // reject it). An explicit item also shadows a same-named `*` variable, so // the expanded projection stays a set of distinct names (this is how a // re-binding LET, desugared to `WITH *, expr AS x`, keeps a single x). := make([]BoundReturn, 0, len(.Items)) := make(map[string]bool, len(.Items)) for , := range .Items { := semantics.ExprHasAgg(.Expr) := .Alias if == "" { = semantics.DerivedName(.Expr) } if [] { return ProjPlan{}, bindErrf("duplicate output column name %q: a projection cannot return two columns with the same name", ) } [] = true = append(, BoundReturn{Expr: .Expr, Name: , IsAgg: }) } // `*` expands to every in-scope variable ahead of the explicit items, in // introduction (slot) order so column order is stable -- minus any // variable an explicit item re-projects under the same name (shadowed). if .Star { type struct { string int } := make([], 0, len()) for , := range { = append(, {, }) } sortSlice(, func(, ) bool { return . < . }) if len() == 0 && len(.Items) == 0 { return ProjPlan{}, bindErrf("RETURN */WITH * requires at least one variable in scope") } for , := range { if [.] { continue } = append(, BoundReturn{Expr: &ast.Var{Name: .}, Name: .}) } } = append(, ...) := false for := range { = || [].IsAgg } if .Distinct && { return ProjPlan{}, bindErrf("DISTINCT with aggregates is not supported") } for := range { if := semantics.CheckRefsSkippingAgg([].Expr, ); != nil { return ProjPlan{}, } } := make([]string, len()) for := range { [] = [].Name } // ORDER BY resolves against the output columns -- by alias or by a key // expression equal to a projected one. A plain projection additionally // keeps the incoming variables in scope; after aggregation or DISTINCT // only the output columns survive -- a key over a discarded variable // would be ambiguous per surviving row (both ISO GQL and openCypher // reject it), and the executor would silently evaluate it against the // first-encountered duplicate's bindings. := map[string]int{} if ! && !.Distinct { for , := range { [] = } } for , := range { if , := []; ! { [] = 0 } } for , := range .OrderBy { if OrderColIndex(.Expr, , ) >= 0 { continue } if := semantics.CheckRefs(.Expr, ); != nil { if { return ProjPlan{}, bindErrf("ORDER BY must reference a projection column when aggregating") } if .Distinct { return ProjPlan{}, bindErrf("ORDER BY under DISTINCT must reference a projection column") } return ProjPlan{}, } } := len() var []int var []AggCol var []PostProj := 0 // Grouping items (the non-aggregate projections, which may be listed // after a wrapper that uses them) are legal references inside a // nested-aggregate wrapper -- standard grouping semantics, e.g. the // carried alias in `RETURN xs, xs + collect(q) AS ys` (the Rust // engine's tasks/150). var []groupCol for := range { if ![].IsAgg { = append(, groupCol{idx: , name: [].Name, expr: [].Expr}) } } for := range { := &[] if !.IsAgg { = append(, ) continue } if isPureAgg(.Expr) { , := extractAgg(.Expr, ) if != nil { return ProjPlan{}, } = append(, ) continue } // A nested aggregate: hoist each aggregate into a hidden slot and // project the rewritten wrapper after grouping. The wrapper must // reduce to a scalar over those hidden slots and the grouping keys // -- any other variable reference is rejected. , := extractNestedAggs(.Expr, , &, &) if != nil { return ProjPlan{}, } = substGroupKeys(, ) := make(map[string]int, +len()) for := range { [fmt.Sprintf("__agg%d", )] = + } for , := range { if , := [.name]; ! { [.name] = .idx } } if := semantics.CheckRefs(, ); != nil { return ProjPlan{}, } = append(, PostProj{Col: , Expr: }) } return ProjPlan{ Returns: , Distinct: .Distinct, Aggregated: , GroupIdx: , Aggs: , Post: , NHidden: , OrderBy: .OrderBy, Skip: .Skip, Limit: .Limit, Columns: , }, nil } // OrderColIndex resolves an ORDER BY key to an output column as a whole: a // bare variable naming a projected column, or a key expression structurally // equal to a projected one. -1 when the key is a composite the executor // evaluates against the projected row instead. func ( ast.Expr, []string, []BoundReturn) int { if , := .(*ast.Var); { for , := range { if == .Name { return } } } for := range { if exprEqual([].Expr, ) { return } } return -1 } // isPureAgg reports a top-level pure aggregate projection (the aggregate // is the whole expression, so it owns its column). func isPureAgg( ast.Expr) bool { , := .(*ast.Func) return && semantics.IsAggName(.Name) } // extractNestedAggs rewrites a nested-aggregate projection: each aggregate // sub-expression is pulled into a hidden slot (recorded in aggs) and // replaced by a __agg{k} reference, yielding an aggregate-free wrapper // evaluated once per finalized group. Recursion descends only through // scalar operators/functions/CASE/list constructs. func extractNestedAggs( ast.Expr, int, *int, *[]AggCol) (ast.Expr, error) { if isPureAgg() { := * , := extractAgg(, +) if != nil { return nil, } * = append(*, ) *++ return &ast.Var{Name: fmt.Sprintf("__agg%d", )}, nil } if !semantics.ExprHasAgg() { return , nil } switch n := .(type) { case *ast.Unary: , := (.Expr, , , ) if != nil { return nil, } return &ast.Unary{Op: .Op, Expr: }, nil case *ast.Binary: , := (.LHS, , , ) if != nil { return nil, } , := (.RHS, , , ) if != nil { return nil, } return &ast.Binary{Op: .Op, LHS: , RHS: }, nil case *ast.Func: if .Star { return , nil } := make([]ast.Expr, len(.Args)) for , := range .Args { , := (, , , ) if != nil { return nil, } [] = } return &ast.Func{Name: .Name, Distinct: .Distinct, Args: }, nil case *ast.ListExpr: := make([]ast.Expr, len(.Elems)) for , := range .Elems { , := (, , , ) if != nil { return nil, } [] = } return &ast.ListExpr{Elems: }, nil case *ast.In: , := (.Expr, , , ) if != nil { return nil, } , := (.List, , , ) if != nil { return nil, } return &ast.In{Expr: , List: }, nil case *ast.IsNull: , := (.Expr, , , ) if != nil { return nil, } return &ast.IsNull{Expr: , Negated: .Negated}, nil case *ast.IsTruth: , := (.Expr, , , ) if != nil { return nil, } return &ast.IsTruth{Expr: , Want: .Want, Negated: .Negated}, nil case *ast.IsTyped: , := (.Expr, , , ) if != nil { return nil, } return &ast.IsTyped{Expr: , Kind: .Kind, Negated: .Negated}, nil case *ast.Reduce: , := (.Init, , , ) if != nil { return nil, } , := (.List, , , ) if != nil { return nil, } , := (.Body, , , ) if != nil { return nil, } return &ast.Reduce{Acc: .Acc, Init: , Var: .Var, List: , Body: }, nil case *ast.Index: , := (.Base, , , ) if != nil { return nil, } , := (.Idx, , , ) if != nil { return nil, } return &ast.Index{Base: , Idx: }, nil case *ast.Slice: , := (.Base, , , ) if != nil { return nil, } := &ast.Slice{Base: } if .From != nil { if .From, = (.From, , , ); != nil { return nil, } } if .To != nil { if .To, = (.To, , , ); != nil { return nil, } } return , nil case *ast.PropOf: , := (.Base, , , ) if != nil { return nil, } return &ast.PropOf{Base: , Key: .Key}, nil case *ast.Case: := &ast.Case{} var error if .Operand != nil { if .Operand, = (.Operand, , , ); != nil { return nil, } } .Whens = make([]ast.CaseWhen, len(.Whens)) for , := range .Whens { , := (.Cond, , , ) if != nil { return nil, } , := (.Result, , , ) if != nil { return nil, } .Whens[] = ast.CaseWhen{Cond: , Result: } } if .Else != nil { if .Else, = (.Else, , , ); != nil { return nil, } } return , nil } return nil, planErrf("an aggregate here is not supported -- it must sit inside scalar arithmetic, a function call, CASE, or a list (Tier 1)") }