// Fixed-hop expansion: one relationship step from a bound node, plus the// bound-target rebind semijoin (probe a memoized reverse-neighbor set// instead of re-expanding per row).
package execimport ()// expandCandidates appends the endpoints of one relationship hop from the// row's bound from-node, filtered by the op's matcher and (for a rebind)// the already-bound target; a named relationship also records each rel's// CSR position parallel to the node.func expandCandidates( *eval.Ctx, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, []value.Value, *[]graph.NodeID, *[]uint32) { , := [.From].AsNode()if ! {return }vargraph.NodeID := falseif .Rebind { , = [.To].AsNode()if ! {return } } := func( graph.NodeID) bool {return (! || == ) && .G.NodeMatcherAccepts(, ) }// A native graph filters the appended tail through one batch call (the // hot single-dense-label matcher runs as a hoisted bitmap loop with no // per-candidate dispatch); the bound-target compaction stays ahead of // it, and non-native graphs keep the per-candidate closure. , := .G.(*graph.SnapshotGraph)// Batch-append the hop into the pooled buffers, then filter the // appended tail in place (the iter.Seq forms would heap-allocate their // closures on every row -- see Graph.AppendNeighborsMatched). A named // relationship variable captures each rel's position alongside its // neighbor. := len(*)if .RelSlot != plan.NoSlot {// Bound-both-endpoints named expand: the target is fixed, so match it // once and seek the (from, bound) relationship positions directly -- // scanning the lower-degree endpoint -- instead of enumerating from's // whole degree and filtering to bound. Each parallel relationship is // one appended position (the named side of the enumeration/existence // boundary keeps per-relationship bindings observable).if && {if !.G.NodeMatcherAccepts(, ) {return }boundPairSeeks++ := len(*) * = .AppendRelsBetween(*, , , .Dir, )forrange (*)[:] { * = append(*, ) }return } := len(*) *, * = .G.AppendRelationshipsMatched(*, *, , .Dir, )if { *, * = .FilterMatchedTail(, *, , *, )return } := for , := range (*)[:] {if () { (*)[] = (*)[+(-)] = (*)[+] ++ } } * = (*)[:] * = (*)[:+(-)]return } * = .G.AppendNeighborsMatched(*, , .Dir, )if {if { := for , := range (*)[:] {if == { (*)[] = ++ } } * = (*)[:] } *, _ = .FilterMatchedTail(, *, , nil, 0)return } := for , := range (*)[:] {if () { (*)[] = ++ } } * = (*)[:]}// semiCache memoizes per-target reverse-neighbor sets for one semijoin op,// so a constant target builds its set once for the whole stage. Each set is// a sorted id slice probed by binary search -- one allocation per target,// versus a roaring bitmap's per-range containers for the same membership.type semiCache map[graph.NodeID][]uint32// semijoinSetBuilds counts reverse-neighbor-set materializations -- the// build-once oracle for the semijoin memo: a stage of N rows probing one// constant target must build exactly ONE set (the invariant neighborhood// materialized once, membership per row); N builds means the memo is dead.var semijoinSetBuilds int// boundPairSeeks counts bound-both-endpoints named-expand position seeks --// the dispatch oracle for the seek path: a named rebind expand over a bound// target must reach the seek (this counter climbs) rather than fall back to// enumerating the from-node's whole degree. A stage of N such rows registers// N seeks; 0 means the seek is not firing.var boundPairSeeks int// buildSemijoins recognizes each bound-target rebind expand with no named// relationship as an existence semijoin: probe from's membership in// neighbors(to, flip(dir), types) O(1) per row. Multiplicity-identical to// the expand on a simple graph; a named relationship keeps the expand so// per-relationship bindings stay observable.func buildSemijoins( []plan.BindOp) []semiCache { := make([]semiCache, len())for := range {if [].Kind == plan.OpExpand && [].Rebind && [].RelSlot == plan.NoSlot { [] = semiCache{} } }return}// semijoinCandidates yields the already-bound target once PER qualifying// relationship from the row's from-node (parallel rels are adjacent// duplicates in the sorted set, so multiplicity costs one forward walk on// a hit and nothing on simple graphs), else nothing. Collapsing parallels// here while the enumerated rebind expand multiplies them made the// semijoin rewrite result-visible on multigraphs. An empty set// also stands in for a target failing its own constraints.func semijoinCandidates( *eval.Ctx, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, semiCache, []value.Value, *[]graph.NodeID, *[]graph.NodeID) { , := [.From].AsNode() , := [.To].AsNode()if ! || ! {return } := semiSetFor(, , , , , , )if , := slices.BinarySearch(, uint32()); {for := ; < len() && [] == uint32(); ++ { * = append(*, ) } }}// semiSetFor resolves (building and memoizing on first sight) the// target's reverse-neighbor membership set for one semijoin op.func semiSetFor( *eval.Ctx, *plan.BindOp, *graph.NodeMatcher, *graph.RelMatcher, semiCache, graph.NodeID, *[]graph.NodeID) []uint32 { , := []if ! {if .G.NodeMatcherAccepts(, ) {semijoinSetBuilds++ = reverseNeighborSet(, , .Dir, , ) } [] = }return}// semijoinLookaheadPrunes counts fill-time lookahead sweeps that removed// at least one candidate -- the engagement oracle: a level followed by a// rebind semijoin over a bound target must prune doomed candidates// BEFORE they bind, not enumerate rows the next level then discards.var semijoinLookaheadPrunes int// disableSemijoinLookahead pins result identity in tests: the un-pruned// enumeration must produce exactly the rows the pruned walk does.var disableSemijoinLookahead bool// pruneSemijoinLookahead sweeps a freshly filled node-candidate level// against the NEXT op's semijoin membership: a candidate with no// qualifying relationship to the (already bound) target cannot survive// the next level, so it never binds -- the pruned subtree produced no// rows, and its relationship-uniqueness pushes were scoped to that// subtree, so removal is invisible to every other candidate. Var-length// levels keep their fill (their parallel per-trail buffers don't// compact); the semijoin level itself still runs, serving multiplicity// and profile exactly as before over the survivors.func pruneSemijoinLookahead( *eval.Ctx, []plan.BindOp, *stageComp, int, []value.Value, *genScratch) { := + 1ifdisableSemijoinLookahead || >= len() || .semijoins[] == nil {return } := &[]if .Kind == plan.OpVarExpand {return } := &[]if .From != slotOf() {return } , := [.To].AsNode()if ! {return } := semiSetFor(, , .matchers[], .relMatchers[], .semijoins[], , &.semiBuf) := .cand[] := .candRel[] := len() == len() && len() > 0 := 0for , := range {if , := slices.BinarySearch(, uint32()); { [] = if { [] = [] } ++ } }if < len() {semijoinLookaheadPrunes++ } .cand[] = [:]if { .candRel[] = [:] }}// reverseNeighborSet is the set of nodes with a matching relationship to c// over dir -- c's neighbors looking back along the flipped direction// (matchers are direction-independent), collected into a reused buffer and// returned as a sorted id slice for O(log n) membership probes.func reverseNeighborSet( *eval.Ctx, graph.NodeID, graph.Direction, *graph.RelMatcher, *[]graph.NodeID) []uint32 { * = .G.AppendNeighborsMatched((*)[:0], , flipDir(), ) := make([]uint32, len(*))copy(, *)slices.Sort()return}// flipDir is the direction seen when traversing a relationship the other// way.func flipDir( graph.Direction) graph.Direction {switch {casegraph.Outgoing:returngraph.Incomingcasegraph.Incoming:returngraph.Outgoing }returngraph.Both}
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.