// Core lowering primitives (port of the Rust plan/lower.rs): slot // assignment, scan-source selection, hop building, var-length // hop-predicate extraction, projection binding, and the shared AND-chain // helpers. The anchor-seek recognizers live in lower_seeks.go.
package plan import ( ) // assignSlot resolves a node pattern's slot: an existing variable keeps its // slot (wasBound reports whether it is already bound); a new or anonymous // node takes the next slot. func assignSlot( *ast.NodePat, map[string]int, map[int]bool, *int) ( int, bool) { if .Var != "" { if , := [.Var]; { return , [] } := * *++ [.Var] = return , false } := * *++ return , false } // scanSource picks a fresh node's default scan source: the most selective // indexed property seek over its inline props and WHERE equalities (via // bestPropSeek), else a label scan, else all nodes. func scanSource( *ast.NodePat, ast.Expr, graph.Graph) ScanSource { if , := bestPropSeek(, , ); { if .inVals != nil { return ScanSource{Kind: ScanPropertyIn, Label: .Labels[0], Key: .key, Values: .inVals} } return ScanSource{Kind: ScanProperty, Label: .Labels[0], Key: .key, Value: .val} } if len(.Labels) > 0 { return ScanSource{Kind: ScanLabel, Label: .Labels[0]} } return ScanSource{Kind: ScanAll} } // buildHop lowers one relationship hop to an Expand or VarExpand op. func buildHop( *ast.RelPat, , , int, bool, *ast.NodePat) (BindOp, error) { := BindOp{ From: , To: , Rebind: , Dir: mapDir(.Dir), Types: .Types, RelSlot: , Labels: .Labels, Props: .Props, } if .Length == nil { .Kind = OpExpand return , nil } // GQL quantifier defaulting: an absent lower bound is 0 ({,3}, *), // per GRAMMAR.md -- unlike Cypher's *..3, whose min defaults to 1. := .Length var uint64 if .Min != nil { = *.Min } if .Max != nil && > *.Max { return BindOp{}, planErrf("quantifier bound {%d,%d} is empty", , *.Max) } // Zero-length or unbounded resolves the distinct reachable set (a // dedup'd BFS), which has no per-path rel list, so a named rel // variable is not supported there. := == 0 || .Max == nil if && .Var != "" { return BindOp{}, planErrf("a relationship variable is not supported on a zero-length or unbounded quantified pattern -- it resolves a reachable set, not paths") } .Kind = OpVarExpand .Min = .Max = .Max .RelVar = .Var return , nil } // extractVarlenHopPreds lifts all(r IN rels(e) WHERE pred) conjuncts from // a stage WHERE onto the matching var-expand as a per-hop filter; consumed // conjuncts drop from the WHERE. (Monotonic-order conjuncts lift later, in // the segment-wide pushMonoPreds pass.) func extractVarlenHopPreds( *ast.Expr, []BindOp) error { if * == nil { return nil } var []ast.Expr SplitAnd(*, &) var []ast.Expr for , := range { , := tryPushHopPred(, ) if != nil { return } if { continue } = append(, ) } * = rebuildAnd() return nil } // tryPushHopPred lifts a single all(r IN rels(e) WHERE pred-over-r) // conjunct onto the var-expand binding e. Any other shape returns false // and is handled generally (never rejected here). func tryPushHopPred( ast.Expr, []BindOp) (bool, error) { , := .(*ast.ListPred) if ! { return false, nil } := relsArg(.List) if == "" { return false, nil } var *BindOp for := range { if [].Kind == OpVarExpand && [].RelVar == { = &[] break } } if == nil { return false, nil } if .Quant != ast.QuantAll || predRefsOnly(.Pred, .Var) != nil { return false, nil } if .RelPred != nil { return false, nil } .RelPred = &RelHopPred{Var: .Var, Pred: .Pred} return true, nil } // relsArg matches rels(<var>) returning the variable name; "" otherwise. func relsArg( ast.Expr) string { , := .(*ast.Func) if ! || .Star || len(.Args) != 1 || !eqFold(.Name, "rels") { return "" } , := .Args[0].(*ast.Var) if ! { return "" } return .Name } // predRefsOnly validates that a per-hop predicate's free variables are // only the iteration variable v (so it can be evaluated against one // relationship). func predRefsOnly( ast.Expr, string) error { if := freeVarsOutside(, []string{}); len() > 0 { return planErrf("a per-hop predicate may only reference the relationship variable `%s` (found `%s`)", , [0]) } return nil } // SplitAnd walks a WHERE's top-level AND chain, appending each conjunct // (exported: the executor's conjunct bucketing splits the same chains). func ( ast.Expr, *[]ast.Expr) { if , := .(*ast.Binary); && .Op == ast.OpAnd { (.LHS, ) (.RHS, ) return } * = append(*, ) } // rebuildAnd re-joins conjuncts left-associatively; nil for an empty list. func rebuildAnd( []ast.Expr) ast.Expr { if len() == 0 { return nil } := [0] for , := range [1:] { = &ast.Binary{Op: ast.OpAnd, LHS: , RHS: } } return } // mapDir converts pattern syntax direction to a traversal direction. func mapDir( ast.Dir) graph.Direction { switch { case ast.DirOut: return graph.Outgoing case ast.DirIn: return graph.Incoming } return graph.Both }