// Quantified (variable-length) expansion: bounded enumeration walks // relationship-unique trails (one row per path, GQL TRAIL semantics); // zero-length or unbounded quantifiers resolve the distinct reachable set // via a dedup'd BFS so a cyclic walk terminates. Each walk mode threads // the same hopGate seam: a stateless per-hop predicate and a stateful // carry+accept constraint prune during the walk.
package exec import ( ) // hopGate bundles a var-expand op's compiled per-hop constraints: an // optional stateless relationship predicate and an optional stateful // carry. It is the one seam every walk mode shares, so a future // carried-state constraint (sum-bounded, non-decreasing weight) extends // hopCarry instead of threading another field and parameter set. type hopGate struct { pred *hopFilter carry *hopCarry } // hopFilter is a compiled per-hop relationship predicate lifted from // all(r IN rels(e) WHERE pred): the iteration variable reads from slot 0 // of a one-element row per traversed relationship. The row is a reused // scratch on the filter -- a fresh slice per keep would allocate once per // traversed relationship, and the gate is compiled per execution so the // scratch is never shared. type hopFilter struct { eval RowEval scope map[string]int srow [1]value.Value } func ( *hopFilter) ( *eval.Ctx, uint32) bool { .srow[0] = value.Rel() return .eval.Eval(, .srow[:], .scope).IsTruthy() } // carryState is the opaque per-path state a walk threads hop to hop for // its gate's carry; the zero value means "no state yet" (a path's first // hop has nothing to compare against). type carryState struct { val value.Value have bool } // hopCarry is the stateful per-hop constraint: step reads the candidate // hop's carried value and either extends the path's state or rejects the // hop. The monotonic-key constraint is the only instance today -- state // is the previous hop's key, compared with the filter's own three-valued // value.Compare so the pruned set exactly matches the source filter: an // incomparable pair (missing key, NaN, mixed kinds) fails an all()-shape // comparison and prunes, but is not a violation in the violation-count // shape and passes (nullsPass). Carry is meaningless for the reachability // BFS (dedup collapses paths, so there is no per-path state); the planner // never attaches a spec to a min-0/unbounded op. type hopCarry struct { eval RowEval scope map[string]int ascending bool nullsPass bool // srow is the reused one-element eval row (see hopFilter.srow): step // runs once per candidate hop, so a per-call slice would allocate on // every traversed relationship of the walk. srow [1]value.Value } // step consumes one hop: the returned state carries the hop's value, and // ok reports whether the hop may follow the previous state. func ( *hopCarry) ( *eval.Ctx, uint32, carryState) (carryState, bool) { .srow[0] = value.Rel() := .eval.Eval(, .srow[:], .scope) if .have && !.allows(.val, ) { return , false } return carryState{val: , have: true}, true } // allows reports whether a hop whose key is cur may follow a hop whose key // is prev under the spec's order and null semantics. func ( *hopCarry) (, value.Value) bool { , := value.Compare(, ) if ! { return .nullsPass } if .ascending { return < 0 } return > 0 } // buildHopGates compiles each var-length op's per-hop constraints once for // the stage, indexed by op (the per-row form allocated a scope map and a // compiled tree on every row entering the walk). func buildHopGates( *eval.Ctx, []plan.BindOp) []hopGate { := make([]hopGate, len()) for := range { if [].Kind != plan.OpVarExpand { continue } if := [].RelPred; != nil { := map[string]int{.Var: 0} [].pred = &hopFilter{eval: compileEval(, .Pred, ), scope: } } if := [].MonoHop; != nil { := map[string]int{"__r": 0} [].carry = &hopCarry{ eval: compileEval(, &ast.Prop{Var: "__r", Key: .RelKey}, ), scope: , ascending: .Ascending, nullsPass: .NullsPass, } } } return } // varExpandCandidates fills the candidate buffers for a quantified hop: // endpoint nodes, and for a named rel variable the flat rel arena plus // per-candidate (start, len) ranges. The walk state and dedup set live on // the stage's genScratch, reused across the row loop (the walk runs to // completion per call and is never nested, same as reachScratch). func varExpandCandidates( *eval.Ctx, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, hopGate, []value.Value, *uniqEnv, *[]graph.NodeID, *[]uint32, *[][2]int, *[][2]graph.NodeID, *[][2]int, *genScratch) { , := [.From].AsNode() if ! { return } var graph.NodeID := false if .Rebind { , = [.To].AsNode() if ! { return } } // A tracked op with LATER intersecting ops must expose each // candidate's trail pairs so they can be pushed for those ops to check // -- which forces the reach-shaped walk below into per-trail // enumeration. := .Uniq != nil && .Uniq.Contribute // Zero-length / unbounded quantifiers resolve the distinct reachable // set via dedup'd BFS, so an unbounded or cyclic walk can't explode // into per-path enumeration. Excluding the scope's used pairs during // the BFS keeps the endpoint SET exact: any walk in the pair-filtered // graph reduces to a trail avoiding those rels and vice versa. if (.Min == 0 || .Max == nil) && ! { // Chain collapse: a zero-minimum unbounded reach over a // functional rel type whose target label only chain terminals // carry is ONE root-array lookup -- the structural facts are // verified against the data and cached by the engine // (ChainCollapseVia), and the per-op resolution is cached per // execution. Per-hop predicates and rel-uniqueness checks keep // the general walk. if .Min == 0 && .Max == nil && .pred == nil && (.Uniq == nil || !.Uniq.Check) && len(.Labels) > 0 { if , := .chainRootsFor(, ); { := if int() < len() { = [] } if (! || == ) && .G.NodeMatcherAccepts(, ) { * = append(*, ) } return } } // Functional chain walk: a functional type's reachable set is its // ancestor chain, so the general BFS's frontier and dedup maps // reduce to following the single rel -- honoring the same per-hop // predicate and rel-uniqueness checks per step. Falls back to the // BFS past chainWalkMax steps (a cycle in a functional type, or a // pathological chain). if .chainFuncFor(, ) && chainReach(, , , , , , , , , , &.reach) { return } varReach(, , , , , , , , , , &.reach) return } // Contributing reach-shaped walks enumerate trails instead (finite: // a trail never repeats an edge pair). Min 0 also emits the // zero-length walk -- the start node itself, using no relationships. := unboundedHops if .Max != nil { = *.Max } // Reset the persistent walk in place: the buffer fields keep their // backing arrays across rows. := &.walk * = varWalk{ ctx: , op: , m: , rm: , gate: , bound: , haveBound: , collectRels: .RelSlot != plan.NoSlot, collectPairs: , uniq: , max: , cand: , relData: , relRanges: , pairData: , pairRanges: , pathRels: .pathRels[:0], pathPairs: .pathPairs[:0], used: .used[:0], visited: .visited[:0], scratch: .scratch, nbufN: .nbufN[:0], nbufP: .nbufP[:0], } if .Acyclic { .visited = append(.visited, ) } if .Min == 0 && (! || == ) && .G.NodeMatcherAccepts(, ) { * = append(*, ) if .collectRels { * = append(*, [2]int{len(*), 0}) } if { * = append(*, [2]int{len(*), 0}) } } .dfs(, 0, carryState{}) // Without a named rel variable the trail's positions are not bound; // only then does endpoint dedup apply (first-seen order preserved). if !.collectRels { if .DedupEndpoints { if .dedup == nil { .dedup = make(map[graph.NodeID]struct{}, len(*)) } else { clear(.dedup) } := (*)[:0] for , := range * { if , := .dedup[]; ! { .dedup[] = struct{}{} = append(, ) } } * = } * = (*)[:0] * = (*)[:0] } } // varReach is the distinct nodes reachable in min..=max hops (max nil = // unbounded): a dedup'd BFS binding each reachable endpoint once. min 0 // includes the start itself. expanded bounds the walk (each node's // neighbors explored once, so cycles terminate); emitted dedups the result // separately so a node re-reached via a cycle at depth >= min -- e.g. the // start when min >= 1 -- still emits once. The gate's stateless predicate // prunes edges here too; its carry never applies (see hopCarry). // chainWalkMax bounds the functional chain walk; a longer chain (or a // cycle, which a functional type can still form) falls back to the // dedup'd BFS. Real forest chains -- reply threads, org hierarchies -- // sit far below it. const chainWalkMax = 64 // chainReach is varReach for a FUNCTIONAL rel type: the reachable set is // the ancestor chain, so it follows the single rel per node with no // frontier or dedup state, applying the same bound/matcher acceptance, // hop-gate predicate, and rel-uniqueness exclusion per step. Reports // false (leaving out untouched) when the chain exceeds chainWalkMax -- // the caller then runs the general walk. func chainReach( *eval.Ctx, graph.NodeID, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, hopGate, graph.NodeID, bool, *uniqEnv, *[]graph.NodeID, *reachScratch) bool { := uint64(1<<63 - 1) if .Max != nil { = *.Max } := func( graph.NodeID) bool { return (! || == ) && .G.NodeMatcherAccepts(, ) } := func(, graph.NodeID) bool { if .Uniq == nil || !.Uniq.Check { return true } , := uniqPair(.Dir, , ) return !.used(.Uniq.Scope, , ) } := len(*) if .Min == 0 && () { * = append(*, ) } := for := uint64(0); < ; { if >= chainWalkMax { * = (*)[:] return false } .nbuf, .pbuf = .G.AppendRelationshipsMatched(.nbuf[:0], .pbuf[:0], , .Dir, ) if len(.nbuf) == 0 { return true } := .nbuf[0] if .pred != nil && !.pred.keep(, .pbuf[0]) { return true } if !(, ) { return true } ++ // A functional chain revisiting the start has cycled; every // reachable node is already emitted. if == { return true } if >= .Min && () { * = append(*, ) } = } return true } func varReach( *eval.Ctx, graph.NodeID, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, hopGate, graph.NodeID, bool, *uniqEnv, *[]graph.NodeID, *reachScratch) { := uint64(1<<63 - 1) if .Max != nil { = *.Max } := func( graph.NodeID) bool { return (! || == ) && .G.NodeMatcherAccepts(, ) } // A checking op excludes edges whose canonical pair the scope already // used (see varExpandCandidates). := func(, graph.NodeID) bool { if .Uniq == nil || !.Uniq.Check { return true } , := uniqPair(.Dir, , ) return !.used(.Uniq.Scope, , ) } // Reuse the stage's working sets: clear the dedup maps and reset the // frontier/neighbor buffers rather than allocating them per row. if .expanded == nil { .expanded = map[graph.NodeID]struct{}{} .emitted = map[graph.NodeID]struct{}{} } else { clear(.expanded) clear(.emitted) } .expanded[] = struct{}{} if .Min == 0 && () { .emitted[] = struct{}{} * = append(*, ) } := append(.frontier[:0], ) := .next[:0] for := uint64(0); len() > 0 && < ; ++ { := + 1 = [:0] for , := range { if .pred != nil { // The predicate needs rel positions: walk relationships and // keep the passing edges' endpoints. .nbuf, .pbuf = .G.AppendRelationshipsMatched(.nbuf[:0], .pbuf[:0], , .Dir, ) := .nbuf[:0] for , := range .nbuf { if .pred.keep(, .pbuf[]) && (, ) { = append(, ) } } .nbuf = } else { .nbuf = .G.AppendNeighborsMatched(.nbuf[:0], , .Dir, ) if .Uniq != nil && .Uniq.Check { := .nbuf[:0] for , := range .nbuf { if (, ) { = append(, ) } } .nbuf = } } for , := range .nbuf { if >= .Min && () { if , := .emitted[]; ! { .emitted[] = struct{}{} * = append(*, ) } } if , := .expanded[]; ! { .expanded[] = struct{}{} = append(, ) } } } // Swap the two persistent buffers: next becomes the frontier, the // old frontier is reused as next's backing. , = , } .frontier, .next = , } // unboundedHops is the trail walk's cap for an unbounded quantifier under // pair collection (the walk stays finite: a trail never repeats a pair). const unboundedHops = ^uint64(0)