// Aggregating-projection helpers: substituting grouping-key subexpressions // with references to their output columns, and compiling a top-level // aggregate call into an AggCol. Split from proj_bind.go, which holds the // projection binding and nested-aggregate extraction.
package plan import ( ) // groupCol is one grouping item of an aggregating projection: its output // column index, name, and source expression. type groupCol struct { idx int name string expr ast.Expr } // alphaLocal renames the binder local *v to a fresh name when it collides // with a grouping column's output name, rewriting the bodies in place -- // substituting a column reference into the body can then never be captured // by the local. avoid carries sibling locals of the same binder so the // fresh name cannot collide with them either; nil bodies are skipped. func alphaLocal( *string, []groupCol, []string, ...*ast.Expr) { := false for , := range { if .name == * { = true break } } if ! { return } := map[string]bool{} for , := range { [] = true } for , := range { if * != nil { collectAllVars(*, ) } } for , := range { [.name] = true collectAllVars(.expr, ) } var string for := 1; ; ++ { = fmt.Sprintf("%s__%d", *, ) if ![] { break } } for , := range { if * != nil { * = renameFree(*, *, ) } } * = } // shadowFiltered returns groups minus the ones a binder's locals make // unusable in its sub-scope: a group whose key expression mentions a local // (the body reference means the local, not the key) or whose output name // IS a local (defense in depth -- alphaLocal renames such locals away // before this filter runs on the wired paths). The common case -- no // collision -- returns groups unchanged without allocating. func shadowFiltered( []groupCol, ...string) []groupCol { := func( groupCol) bool { for , := range { if == "" { continue } if .name == || MentionsVar(.expr, ) { return true } } return false } for , := range { if () { := make([]groupCol, 0, len()-1) = append(, [:]...) for , := range [+1:] { if !() { = append(, ) } } return } } return } // substGroupKeys re-points wrapper subexpressions at the grouping columns: // a subexpression structurally equal to a grouping item's expression // becomes a reference to its output column. Aggregates were already pulled // out by extractNestedAggs, so no aggregate call remains in the wrapper. // Comprehension/quantifier/reduce bodies bind their own variables but // still evaluate post-projection, so the walk descends into them: a local // that collides with a group's output name is first alpha-renamed to a // fresh name (substituting the column reference could otherwise be // captured), and a group is dropped for a sub-scope when the binder // shadows a variable its key expression mentions -- that body reference // means the local, not the key. Correlated subquery interiors // (EXISTS/COUNT/pattern comprehensions) remain untouched: their filters // evaluate against the subquery's own pattern scope, and a composite key // appearing only there stays a bind error. func substGroupKeys( ast.Expr, []groupCol) ast.Expr { for , := range { if exprEqual(.expr, ) { return &ast.Var{Name: .name} } } switch n := .(type) { // `var.key` where `var` is a grouping key projected under a different // name: re-point the property access at the output column // (`RETURN n AS m, n.x + count(*)`). case *ast.Prop: for , := range { if , := .expr.(*ast.Var); && .Name == .Var && .name != .Var { return &ast.PropOf{Base: &ast.Var{Name: .name}, Key: .Key} } } // `var:Label` where `var` is a grouping key projected under a different // name: the label test references its variable by name, so it needs the // same re-pointing as a property access (`RETURN c AS comp, count(*) + // (CASE WHEN c:Company THEN 1 ELSE 0 END) AS x`). case *ast.HasLabelExpr: for , := range { if , := .expr.(*ast.Var); && .Name == .Var && .name != .Var { return &ast.HasLabelExpr{Var: .name, Expr: .Expr} } } case *ast.Unary: .Expr = (.Expr, ) case *ast.IsNull: .Expr = (.Expr, ) case *ast.IsTruth: .Expr = (.Expr, ) case *ast.IsTyped: .Expr = (.Expr, ) case *ast.PropOf: .Base = (.Base, ) case *ast.Binary: .LHS = (.LHS, ) .RHS = (.RHS, ) case *ast.In: .Expr = (.Expr, ) .List = (.List, ) case *ast.Index: .Base = (.Base, ) .Idx = (.Idx, ) case *ast.Func: for := range .Args { .Args[] = (.Args[], ) } case *ast.ListExpr: for := range .Elems { .Elems[] = (.Elems[], ) } case *ast.Case: if .Operand != nil { .Operand = (.Operand, ) } for := range .Whens { .Whens[].Cond = (.Whens[].Cond, ) .Whens[].Result = (.Whens[].Result, ) } if .Else != nil { .Else = (.Else, ) } case *ast.Slice: .Base = (.Base, ) if .From != nil { .From = (.From, ) } if .To != nil { .To = (.To, ) } // Sources evaluate in the outer scope; the bodies descend after any // local colliding with an output name is alpha-renamed away, with the // remaining shadowed keys filtered out of the group set (see above). case *ast.ListPred: .List = (.List, ) alphaLocal(&.Var, , nil, &.Pred) .Pred = (.Pred, shadowFiltered(, .Var)) case *ast.ListComp: .List = (.List, ) alphaLocal(&.Var, , nil, &.Filter, &.Map) := shadowFiltered(, .Var) if .Filter != nil { .Filter = (.Filter, ) } if .Map != nil { .Map = (.Map, ) } case *ast.Reduce: .Init = (.Init, ) .List = (.List, ) alphaLocal(&.Acc, , []string{.Var}, &.Body) alphaLocal(&.Var, , []string{.Acc}, &.Body) .Body = (.Body, shadowFiltered(, .Acc, .Var)) case *ast.MapLit: for := range .Fields { .Fields[].Val = (.Fields[].Val, ) } case *ast.MapProj: // The projected variable is held by name, so it re-points like a // property access or label test when its key is renamed. for , := range { if , := .expr.(*ast.Var); && .Name == .Var && .name != .Var { .Var = .name break } } for := range .Entries { if .Entries[].Kind == ast.MapProjField { .Entries[].Expr = (.Entries[].Expr, ) } } } return } // extractAgg compiles a top-level aggregate projection expression. func extractAgg( ast.Expr, int) (AggCol, error) { , := .(*ast.Func) if ! { return AggCol{}, planErrf("an aggregate must be a top-level projection expression") } var AggKind switch lower(.Name) { case "count": = AggCount case "sum": = AggSum case "avg": = AggAvg case "min": = AggMin case "max": = AggMax case "collect", "collect_list": = AggCollect case "stddev_samp": = AggStddevSamp case "stddev_pop": = AggStddevPop case "percentile_cont", "percentilecont": = AggPercentileCont case "percentile_disc", "percentiledisc": = AggPercentileDisc default: return AggCol{}, planErrf("aggregate function `%s` is not supported (Tier 1)", .Name) } if == AggPercentileCont || == AggPercentileDisc { if .Star || len(.Args) != 2 { return AggCol{}, planErrf("`%s(...)` takes exactly two arguments (a value and a percentile)", .Name) } // The percentile is per-query, not per-row: a constant literal // (number or parameter) keeps the accumulator single-valued. if , := .Args[1].(*ast.Lit); ! { return AggCol{}, planErrf("`%s(...)` requires a constant percentile", .Name) } return AggCol{Kind: , Arg: .Args[0], Arg2: .Args[1], Distinct: .Distinct, OutIdx: }, nil } var ast.Expr switch { case .Star: if != AggCount { return AggCol{}, planErrf("`%s(*)` is not valid", .Name) } case len(.Args) == 1: = .Args[0] default: return AggCol{}, planErrf("`%s(...)` takes exactly one argument (Tier 1)", .Name) } return AggCol{Kind: , Arg: , Distinct: .Distinct, OutIdx: }, nil }