// Binder helpers shared by the planner: aggregate detection, column // naming, and reference validation against a slot map (port of binder.rs). // The per-segment compilation that uses these lives in the plan package.
package semantics import ( ) // aggNames are the aggregate functions the engine recognises. Execution // support is a subset (see plan/exec); collect binds but may be rejected // at planning. var aggNames = []string{"count", "sum", "avg", "min", "max", "collect", "collect_list", "stddev_samp", "stddev_pop", "percentile_cont", "percentile_disc", "percentilecont", "percentiledisc"} // IsAggName reports whether name is an aggregate function // (case-insensitive). func ( string) bool { for , := range aggNames { if strings.EqualFold(, ) { return true } } return false } // IsKnownScalarFunc reports whether name is a scalar function the engine // can evaluate (case-insensitive). func ( string) bool { return eval.IsKnownScalarFunc() } // IsKnownFunction reports whether name is any function the engine // recognizes -- an aggregate or a scalar. Unknown names are rejected at // plan time by CheckRefs; without this they would evaluate to null per // row, silently nulling columns and filtering rows. func ( string) bool { return IsAggName() || IsKnownScalarFunc() } // DerivedName derives a column name for an unaliased projection expression // (the standard uses the source text; this is a close-enough // reconstruction, observable in result column names). func ( ast.Expr) string { switch n := .(type) { case *ast.Var: return .Name case *ast.Prop: return .Var + "." + .Key case *ast.Func: if .Star { return .Name + "(*)" } := make([]string, len(.Args)) for , := range .Args { [] = () } return .Name + "(" + strings.Join(, ", ") + ")" default: return "expr" } } // ExprHasAgg reports whether e contains an aggregate call. Deliberately // does NOT descend into EXISTS/COUNT subqueries (their expressions are a // different scope), mirroring the Rust binder exactly -- so the shared // pre-order walker is not used here. func ( ast.Expr) bool { switch n := .(type) { case *ast.Func: if IsAggName(.Name) { return true } for , := range .Args { if () { return true } } return false case *ast.Unary: return (.Expr) case *ast.Binary: return (.LHS) || (.RHS) case *ast.In: return (.Expr) || (.List) case *ast.IsNull: return (.Expr) case *ast.IsTruth: return (.Expr) case *ast.IsTyped: return (.Expr) case *ast.ListExpr: for , := range .Elems { if () { return true } } return false case *ast.ListPred: return (.List) || (.Pred) case *ast.Reduce: return (.Init) || (.List) || (.Body) case *ast.ListComp: return (.List) || (.Filter != nil && (.Filter)) || (.Map != nil && (.Map)) case *ast.Index: return (.Base) || (.Idx) case *ast.Slice: return (.Base) || (.From != nil && (.From)) || (.To != nil && (.To)) case *ast.PropOf: return (.Base) case *ast.Case: if .Operand != nil && (.Operand) { return true } for , := range .Whens { if (.Cond) || (.Result) { return true } } return .Else != nil && (.Else) // A map literal / projection can hold an aggregate in a computed // field, e.g. RETURN person{.name, cnt: count(m)}. Recurse so it is // planned as an aggregate instead of silently reaching the scalar // evaluator. case *ast.MapLit: for , := range .Fields { if (.Val) { return true } } return false case *ast.MapProj: for , := range .Entries { if .Kind == ast.MapProjField && (.Expr) { return true } } return false default: return false } } // CheckRefs validates that every variable referenced in e is present in // scope (the planner's name -> slot map) and every called function is // known. func ( ast.Expr, map[string]int) error { switch n := .(type) { case *ast.Var: return req(.Name, ) case *ast.Prop: return req(.Var, ) case *ast.Unary: return (.Expr, ) case *ast.Binary: if := (.LHS, ); != nil { return } return (.RHS, ) case *ast.Func: if !IsKnownFunction(.Name) { return bindErrf("unknown function `%s`", .Name) } for , := range .Args { if := (, ); != nil { return } } return nil case *ast.ListExpr: for , := range .Elems { if := (, ); != nil { return } } return nil case *ast.In: if := (.Expr, ); != nil { return } return (.List, ) case *ast.IsNull: return (.Expr, ) case *ast.IsTruth: return (.Expr, ) case *ast.IsTyped: return (.Expr, ) case *ast.Case: if .Operand != nil { if := (.Operand, ); != nil { return } } for , := range .Whens { if := (.Cond, ); != nil { return } if := (.Result, ); != nil { return } } if .Else != nil { return (.Else, ) } return nil case *ast.Exists: return checkSubqueryRefs(.Pattern, .Where, ) case *ast.CountSub: return checkSubqueryRefs(.Pattern, .Where, ) case *ast.PatternComp: // The pattern's node/rel variables are bound for both the filter // and projection; outer variables remain in scope. := scopeWithPatternVars(, .Pattern) if .Where != nil { if := (.Where, ); != nil { return } } return (.Proj, ) case *ast.Cost: if := req(.From, ); != nil { return } return req(.To, ) case *ast.ListPred: // The iteration variable is in scope only for the inner predicate. if := (.List, ); != nil { return } := cloneScope() addVar(, .Var) return (.Pred, ) case *ast.Reduce: if := (.Init, ); != nil { return } if := (.List, ); != nil { return } := cloneScope() addVar(, .Acc) addVar(, .Var) return (.Body, ) case *ast.ListComp: // The iteration variable is in scope only for the filter and map; // the source list is validated in the outer scope. if := (.List, ); != nil { return } := cloneScope() addVar(, .Var) if .Filter != nil { if := (.Filter, ); != nil { return } } if .Map != nil { return (.Map, ) } return nil case *ast.Index: if := (.Base, ); != nil { return } return (.Idx, ) case *ast.Slice: if := (.Base, ); != nil { return } if .From != nil { if := (.From, ); != nil { return } } if .To != nil { return (.To, ) } return nil case *ast.PropOf: return (.Base, ) case *ast.MapProj: // Reads the base var and each computed field; .key entries only // read the base var's properties. if := req(.Var, ); != nil { return } for , := range .Entries { if .Kind == ast.MapProjField { if := (.Expr, ); != nil { return } } } return nil case *ast.MapLit: for , := range .Fields { if := (.Val, ); != nil { return } } return nil case *ast.HasLabelExpr: return req(.Var, ) } // Literals bind no references. return nil } // CheckRefsSkippingAgg is CheckRefs, except a top-level aggregate call's // arguments are validated directly (an aggregate's own name needs no // scalar-function check, and count(*) binds nothing). func ( ast.Expr, map[string]int) error { if , := .(*ast.Func); && IsAggName(.Name) { for , := range .Args { if := CheckRefs(, ); != nil { return } } return nil } return CheckRefs(, ) } // checkSubqueryRefs validates an EXISTS/COUNT subquery's WHERE: it may // reference outer variables plus the pattern's own. func checkSubqueryRefs( *ast.Pattern, ast.Expr, map[string]int) error { if == nil { return nil } return CheckRefs(, scopeWithPatternVars(, )) } // scopeWithPatternVars clones scope and adds the pattern's node and rel // variables (slot 0; subquery variables are matched, not slotted). func scopeWithPatternVars( map[string]int, *ast.Pattern) map[string]int { := cloneScope() addVar(, .Start.Var) for := range .Hops { addVar(, .Hops[].Rel.Var) addVar(, .Hops[].Node.Var) } return } func cloneScope( map[string]int) map[string]int { := make(map[string]int, len()+4) maps.Copy(, ) return } // addVar adds a pattern-introduced variable to a scope, keeping an // existing (outer) slot. func addVar( map[string]int, string) { if == "" { return } if , := []; ! { [] = 0 } } func req( string, map[string]int) error { if , := []; { return nil } return bindErrf("unknown variable `%s`", ) }