// 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 planimport ()// 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) := .Aliasif == "" { = semantics.DerivedName(.Expr) }if [] {returnProjPlan{}, 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 {typestruct {stringint } := make([], 0, len())for , := range { = append(, {, }) }sortSlice(, func(, ) bool { return . < . })iflen() == 0 && len(.Items) == 0 {returnProjPlan{}, bindErrf("RETURN */WITH * requires at least one variable in scope") }for , := range {if [.] {continue } = append(, BoundReturn{Expr: &ast.Var{Name: .}, Name: .}) } } = append(, ...) := falsefor := range { = || [].IsAgg }if .Distinct && {returnProjPlan{}, bindErrf("DISTINCT with aggregates is not supported") }for := range {if := semantics.CheckRefsSkippingAgg([].Expr, ); != nil {returnProjPlan{}, } } := 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 {ifOrderColIndex(.Expr, , ) >= 0 {continue }if := semantics.CheckRefs(.Expr, ); != nil {if {returnProjPlan{}, bindErrf("ORDER BY must reference a projection column when aggregating") }if .Distinct {returnProjPlan{}, bindErrf("ORDER BY under DISTINCT must reference a projection column") }returnProjPlan{}, } } := len()var []intvar []AggColvar []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 []groupColfor := range {if ![].IsAgg { = append(, groupCol{idx: , name: [].Name, expr: [].Expr}) } }for := range { := &[]if !.IsAgg { = append(, )continue }ifisPureAgg(.Expr) { , := extractAgg(.Expr, )if != nil {returnProjPlan{}, } = 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 {returnProjPlan{}, } = substGroupKeys(, ) := make(map[string]int, +len())for := range { [fmt.Sprintf("__agg%d", )] = + }for , := range {if , := [.name]; ! { [.name] = .idx } }if := semantics.CheckRefs(, ); != nil {returnProjPlan{}, } = append(, PostProj{Col: , Expr: }) }returnProjPlan{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 {ifexprEqual([].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) {ifisPureAgg() { := * , := extractAgg(, +)if != nil {returnnil, } * = append(*, ) *++return &ast.Var{Name: fmt.Sprintf("__agg%d", )}, nil }if !semantics.ExprHasAgg() {return , nil }switch n := .(type) {case *ast.Unary: , := (.Expr, , , )if != nil {returnnil, }return &ast.Unary{Op: .Op, Expr: }, nilcase *ast.Binary: , := (.LHS, , , )if != nil {returnnil, } , := (.RHS, , , )if != nil {returnnil, }return &ast.Binary{Op: .Op, LHS: , RHS: }, nilcase *ast.Func:if .Star {return , nil } := make([]ast.Expr, len(.Args))for , := range .Args { , := (, , , )if != nil {returnnil, } [] = }return &ast.Func{Name: .Name, Distinct: .Distinct, Args: }, nilcase *ast.ListExpr: := make([]ast.Expr, len(.Elems))for , := range .Elems { , := (, , , )if != nil {returnnil, } [] = }return &ast.ListExpr{Elems: }, nilcase *ast.In: , := (.Expr, , , )if != nil {returnnil, } , := (.List, , , )if != nil {returnnil, }return &ast.In{Expr: , List: }, nilcase *ast.IsNull: , := (.Expr, , , )if != nil {returnnil, }return &ast.IsNull{Expr: , Negated: .Negated}, nilcase *ast.IsTruth: , := (.Expr, , , )if != nil {returnnil, }return &ast.IsTruth{Expr: , Want: .Want, Negated: .Negated}, nilcase *ast.IsTyped: , := (.Expr, , , )if != nil {returnnil, }return &ast.IsTyped{Expr: , Kind: .Kind, Negated: .Negated}, nilcase *ast.Reduce: , := (.Init, , , )if != nil {returnnil, } , := (.List, , , )if != nil {returnnil, } , := (.Body, , , )if != nil {returnnil, }return &ast.Reduce{Acc: .Acc, Init: , Var: .Var, List: , Body: }, nilcase *ast.Index: , := (.Base, , , )if != nil {returnnil, } , := (.Idx, , , )if != nil {returnnil, }return &ast.Index{Base: , Idx: }, nilcase *ast.Slice: , := (.Base, , , )if != nil {returnnil, } := &ast.Slice{Base: }if .From != nil {if .From, = (.From, , , ); != nil {returnnil, } }if .To != nil {if .To, = (.To, , , ); != nil {returnnil, } }return , nilcase *ast.PropOf: , := (.Base, , , )if != nil {returnnil, }return &ast.PropOf{Base: , Key: .Key}, nilcase *ast.Case: := &ast.Case{}varerrorif .Operand != nil {if .Operand, = (.Operand, , , ); != nil {returnnil, } } .Whens = make([]ast.CaseWhen, len(.Whens))for , := range .Whens { , := (.Cond, , , )if != nil {returnnil, } , := (.Result, , , )if != nil {returnnil, } .Whens[] = ast.CaseWhen{Cond: , Result: } }if .Else != nil {if .Else, = (.Else, , , ); != nil {returnnil, } }return , nil }returnnil, planErrf("an aggregate here is not supported -- it must sit inside scalar arithmetic, a function call, CASE, or a list (Tier 1)")}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.