// Correlated subquery matching: EXISTS { MATCH ... }, COUNT { MATCH ... }, // and pattern comprehensions, all sharing one anchored DFS over the // pattern. The DFS's evaluation-invariant setup (anchor-reversal, level // slots, the extended scope, compiled node and rel matchers) is shaped // once per pattern and cached on the Ctx with its evaluation scratch, so // the per-row cost is a row refresh instead of map/slice/matcher rebuilds.
package eval import ( ) // engineDir maps a pattern direction to the engine's traversal direction. func engineDir( ast.Dir) graph.Direction { switch { case ast.DirOut: return graph.Outgoing case ast.DirIn: return graph.Incoming } return graph.Both } // evalExists is EXISTS { MATCH pattern [WHERE ...] } -- true if the // correlated subquery matches at least once. func evalExists( *Ctx, *ast.Pattern, ast.Expr, []value.Value, map[string]int) bool { return SubqueryCount(, , , , , true) > 0 } // evalCountSub is COUNT { MATCH pattern [WHERE ...] } -- how many times // the correlated subquery matches the current outer row. func evalCountSub( *Ctx, *ast.Pattern, ast.Expr, []value.Value, map[string]int) int64 { return int64(SubqueryCount(, , , , , false)) } // subqueryShape is the shared setup of the subquery DFS -- the pattern // (reversed when only its far end is anchored to an outer variable), the // per-level node patterns, their row slots, and which are outer-anchored // -- plus the reusable evaluation scratch (row buffer, per-level // candidates, BFS sets). Shapes cache on the Ctx keyed by pattern; validFor // re-checks the outer scope on every hit, so a pattern evaluated under a // different scope rebuilds instead of misbinding. type subqueryShape struct { pattern *ast.Pattern nodes []*ast.NodePat nodeSlots []int anchored []bool outer map[string]int outerRowW int slots map[string]int row []value.Value cand [][]graph.NodeID // scan0 memoizes the unanchored level-0 scan (evaluation-invariant: // labels and literal props only); scan0Done marks it filled. scan0 []graph.NodeID scan0Done bool // whereFn is the compiled per-row WHERE evaluator (Ctx.CompileWhere), // cached per shape; whereFor guards against a shape being reused with // a different WHERE expression. whereFn func(*Ctx, []value.Value) value.Value whereFor ast.Expr // matchers lazily holds each hop's pre-resolved rel matcher, so the // per-candidate seam calls skip name resolution. matchers []*graph.RelMatcher // nodeMatchers lazily holds each level's compiled node matcher (labels // resolved to membership tests, inline-prop params resolved to values), // so the per-candidate accept is a bitmap probe plus a column read // instead of a string-keyed label lookup -- the same hoist the main // expand path gets from CompileNodeMatcher, mirrored for the subquery // walk. Params are fixed per Ctx, so the resolved values stay valid for // the shape's lifetime (the invariant scan0 already relies on). nodeMatchers []*graph.NodeMatcher // BFS scratch for quantified hops. expanded map[graph.NodeID]struct{} emitted map[graph.NodeID]struct{} frontier []graph.NodeID next []graph.NodeID reach []graph.NodeID // rev is the reversed-pattern shape, built when BOTH endpoints anchor // to outer variables: the probe then starts per row from whichever // bound endpoint has the smaller first-hop fan-out (a hot hub on one // side -- e.g. a popular tag -- otherwise costs its whole neighbor // list on every row). rev *subqueryShape } // subqueryShapeFor returns the pattern's cached shape with its row buffer // refreshed from the outer row, building (and caching) it on first use. // With both endpoints anchored, the forward or reversed shape is chosen // per row by the bound endpoints' actual first-hop degree (O(1) reads). func subqueryShapeFor( *Ctx, map[string]int, []value.Value, *ast.Pattern, bool) *subqueryShape { if .subqShapes == nil { .subqShapes = map[*ast.Pattern]*subqueryShape{} } := .subqShapes[] if == nil || !.validFor(, ) { = buildSubqueryShape(, , , ) .subqShapes[] = } if .rev != nil { if , := [.nodeSlots[0]].AsNode(); { if , := [.rev.nodeSlots[0]].AsNode(); { := .G.Degree(, engineDir(.pattern.Hops[0].Rel.Dir)) := .G.Degree(, engineDir(.rev.pattern.Hops[0].Rel.Dir)) if < { = .rev } } } } copy(.row, ) for := .outerRowW; < len(.row); ++ { .row[] = value.Null() } return } // validFor reports whether the shape was built against an identical outer // scope and row width. func ( *subqueryShape) ( map[string]int, []value.Value) bool { return len() == .outerRowW && maps.Equal(, .outer) } // buildSubqueryShape anchors the DFS on a bound endpoint: if the start // node isn't an outer variable but the end node is, the pattern reverses // so expansion starts from the (few) bound node's neighbors rather than a // label scan. When BOTH endpoints are anchored, a reversed twin shape is // attached and subqueryShapeFor picks a side per row by actual degree. // When NEITHER endpoint is anchored, the walk seeds from the statically // stronger endpoint (property literal beats bare label beats unlabeled) // when the caller allows reversal -- an unlabeled written start would // otherwise seed the level-0 scan with the whole id space to reach a // seek-sized end. Reversal enumerates the same match set, so counting // callers are safe; a COLLECTING caller (pattern comprehension) must pass // allowSeedReverse=false, because reversing its walk reverses the list's // element order -- a visible output change. Outer variables keep their // slots; inner-only pattern variables extend the row. func buildSubqueryShape( map[string]int, []value.Value, *ast.Pattern, bool) *subqueryShape { := func( *ast.NodePat) bool { if .Var == "" { return false } , := [.Var] return } := (&.Start) := (.EndNode()) switch { case ! && : := .Reversed() = & case && ! && ! && len(.Hops) > 0 && seedStrength(.EndNode()) > seedStrength(&.Start): := .Reversed() = & } := buildOneSubqueryShape(, , ) if && && len(.Hops) > 0 { := .Reversed() .rev = buildOneSubqueryShape(, , &) } return } // seedStrength ranks a node pattern's static selectivity as a level-0 // seed: an inline property ({name: 'x'} or a param) beats a bare label // beats an unlabeled node. func seedStrength( *ast.NodePat) int { if len(.Props) > 0 || len(.PropExprs) > 0 { return 2 } if len(.Labels) > 0 || .LabelExpr != nil { return 1 } return 0 } // buildOneSubqueryShape builds a single direction's shape. func buildOneSubqueryShape( map[string]int, []value.Value, *ast.Pattern) *subqueryShape { := &subqueryShape{ pattern: , outer: , outerRowW: len(), slots: make(map[string]int, len()+len(.Hops)+1), } maps.Copy(.slots, ) .nodes = append(.nodes, &.Start) for := range .Hops { .nodes = append(.nodes, &.Hops[].Node) } := len() for , := range .nodes { switch { case .Var != "" && hasKey(.slots, .Var): .nodeSlots = append(.nodeSlots, .slots[.Var]) .anchored = append(.anchored, true) case .Var != "": .slots[.Var] = .nodeSlots = append(.nodeSlots, ) .anchored = append(.anchored, false) ++ default: .nodeSlots = append(.nodeSlots, ) .anchored = append(.anchored, false) ++ } } .row = make([]value.Value, ) .cand = make([][]graph.NodeID, len(.nodes)) return } func hasKey( map[string]int, string) bool { , := [] return } // SubqueryCount counts matches of a correlated subquery against the // current outer row, anchoring pattern nodes that name an outer variable // to their bound value. stopAtFirst short-circuits for EXISTS. A single // fixed-length pattern (quantifiers are treated as one hop). func ( *Ctx, *ast.Pattern, ast.Expr, []value.Value, map[string]int, bool) int { := subqueryShapeFor(, , , , true) := .whereEval(, ) := 0 .dfs(, 0, func() bool { if == nil || (, .row).IsTruthy() { ++ } return && > 0 }) return } // whereEval resolves the walk's WHERE evaluator: compiled once per shape // through Ctx.CompileWhere when the executor installed it, else the // per-row interpreter. subqWhereCompiles is the engagement oracle. func ( *subqueryShape) ( *Ctx, ast.Expr) func(*Ctx, []value.Value) value.Value { if == nil { return nil } if .CompileWhere == nil || .ForceInterp { return func( *Ctx, []value.Value) value.Value { return Eval(, , , .slots) } } if .whereFn == nil || .whereFor != { .whereFn = .CompileWhere(, .slots) .whereFor = subqWhereCompiles++ } return .whereFn } // subqWhereCompiles counts compiled subquery WHERE builds -- the // engagement oracle: a walk over N candidates must compile its WHERE // exactly once per shape, not per row; 0 across a filtered subquery // means every candidate paid the interpreter. var subqWhereCompiles int // SubqueryGroupCount evaluates a correlated subquery ONCE with the group // variable left free, returning the match count bucketed by the node the // group variable binds to. It is the decorrelated form of SubqueryCount: // where SubqueryCount re-walks the pattern once per outer entity bound to // the group variable, a single SubqueryGroupCount call -- anchored on the // SHARED endpoint (anchorVar, still bound from the outer row) -- yields every // entity's count in one pass over the anchored set, so the per-entity cost // collapses from a walk to an O(1) map read. // // anchorVar must name an outer variable bound in outerRow; groupVar names the // pattern endpoint to bucket by, left UNBOUND here even when it is itself an // outer variable (its outer binding is ignored -- enumeration rebinds it per // match). The returned map is keyed only by node id, so it is independent of // what the outer query calls the group variable: two subqueries with the same // pattern, WHERE, and anchor share one table (see task 084 / rustychickpeas // 091). The result for a given entity equals SubqueryCount with that entity // bound -- the invariant the decor parity test asserts. func ( *Ctx, *ast.Pattern, ast.Expr, []value.Value, map[string]int, , string) map[graph.NodeID]int { // Anchor only anchorVar: a reduced outer scope that drops groupVar so the // group endpoint enumerates through the DFS instead of binding. := make(map[string]int, len()) for , := range { if != { [] = } } := buildSubqueryShape(, , , true) copy(.row, ) for := len(); < len(.row); ++ { .row[] = value.Null() } := map[graph.NodeID]int{} , := .slots[] if ! { return } := .whereEval(, ) .dfs(, 0, func() bool { if == nil || (, .row).IsTruthy() { if , := .row[].AsNode(); { []++ } } return false }) return } // evalPatternComp collects a projection over each correlated match of a // pattern: [ (pattern) [WHERE filter] | proj ]. func evalPatternComp( *Ctx, *ast.PatternComp, []value.Value, map[string]int) value.Value { // Collecting walk: list element order is enumeration order, so the // unanchored seed reversal must not fire here. := subqueryShapeFor(, , , .Pattern, false) := .whereEval(, .Where) := []value.Value{} .dfs(, 0, func() bool { if == nil || (, .row).IsTruthy() { = append(, Eval(, .Proj, .row, .slots)) } return false }) return value.List() }