// 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 semanticsimport ()// 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 , := rangeaggNames {ifstrings.EqualFold(, ) {returntrue } }returnfalse}// IsKnownScalarFunc reports whether name is a scalar function the engine// can evaluate (case-insensitive).func ( string) bool {returneval.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 {returnIsAggName() || 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 .Namecase *ast.Prop:return .Var + "." + .Keycase *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:ifIsAggName(.Name) {returntrue }for , := range .Args {if () {returntrue } }returnfalsecase *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 () {returntrue } }returnfalsecase *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) {returntrue }for , := range .Whens {if (.Cond) || (.Result) {returntrue } }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) {returntrue } }returnfalsecase *ast.MapProj:for , := range .Entries {if .Kind == ast.MapProjField && (.Expr) {returntrue } }returnfalsedefault:returnfalse }}// 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:returnreq(.Name, )case *ast.Prop:returnreq(.Var, )case *ast.Unary:return (.Expr, )case *ast.Binary:if := (.LHS, ); != nil {return }return (.RHS, )case *ast.Func:if !IsKnownFunction(.Name) {returnbindErrf("unknown function `%s`", .Name) }for , := range .Args {if := (, ); != nil {return } }returnnilcase *ast.ListExpr:for , := range .Elems {if := (, ); != nil {return } }returnnilcase *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, ) }returnnilcase *ast.Exists:returncheckSubqueryRefs(.Pattern, .Where, )case *ast.CountSub:returncheckSubqueryRefs(.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 }returnreq(.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, ) }returnnilcase *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, ) }returnnilcase *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 } } }returnnilcase *ast.MapLit:for , := range .Fields {if := (.Val, ); != nil {return } }returnnilcase *ast.HasLabelExpr:returnreq(.Var, ) }// Literals bind no references.returnnil}// 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 } }returnnil }returnCheckRefs(, )}// 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 {returnnil }returnCheckRefs(, 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 , := []; {returnnil }returnbindErrf("unknown variable `%s`", )}
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.