// MATCH stage runner: compile the stage's matchers and WHERE-conjunct // pushdown buckets once, then walk each input row through the bind chain.
package exec import ( ) // stageComp is a MATCH stage's per-op pre-resolved state: node matchers, // rel matchers, and the WHERE conjuncts bucketed by pushdown level -- // each level's bucket split into specialized per-candidate predicates // (levelPreds) and general row evaluations (levelFilters). type stageComp struct { // ops are the stage's EXEC ops -- stage.Ops unless a constant-anchored // chain was absorbed (semired.go); profMap maps exec indices back to // original op indices for PROFILE alignment (nil when identical). ops []plan.BindOp profMap []int // gates test carried-in slots against absorbed-chain membership sets // once per input row (semired.go). gates []memberGate matchers []*graph.NodeMatcher relMatchers []*graph.RelMatcher levelFilters [][]RowEval levelPreds [][]compile.CandPred levelBatch [][]compile.CandBatch hopGates []hopGate semijoins []semiCache // seedRel/seedNode are per-op per-chain per-hop matchers for // ScanExistsSeed walks, resolved once with the rest of the stage. seedRel [][][]*graph.RelMatcher seedNode [][][]*graph.NodeMatcher } // compileStage pre-resolves the stage's constant names once (labels, // property keys, rel types, params) so the per-candidate work is bitmap // contains + column reads. constIn reports segment-wide hoisting-constant // slots; sample is a seeded input row carrying their values. func compileStage( *eval.Ctx, *plan.MatchStage, map[string]int, , func(int) bool, []value.Value, bool) *stageComp { := .Ops var []int var []memberGate if := absorbConstChains(, , , , , ); != nil { , , = .ops, .profMap, .gates } , , := buildLevelFilters(, , , , , ) := &stageComp{ ops: , profMap: , gates: , matchers: make([]*graph.NodeMatcher, len()), relMatchers: make([]*graph.RelMatcher, len()), levelFilters: , levelPreds: , levelBatch: , hopGates: buildHopGates(, ), semijoins: buildSemijoins(), } .seedRel = make([][][]*graph.RelMatcher, len()) .seedNode = make([][][]*graph.NodeMatcher, len()) for := range { := &[] := make([]graph.PropSpec, len(.Props)) for , := range .Props { [] = graph.PropSpec{Key: .Key, Val: eval.LitValue(, .Val)} } .matchers[] = .G.CompileNodeMatcher(.Labels, ) .relMatchers[] = .G.CompileRelMatcher(.Types) if .Kind == plan.OpScan && .Source.Kind == plan.ScanExistsSeed { .seedRel[] = make([][]*graph.RelMatcher, len(.Source.Seeds)) .seedNode[] = make([][]*graph.NodeMatcher, len(.Source.Seeds)) for := range .Source.Seeds { := .Source.Seeds[].Hops .seedRel[][] = make([]*graph.RelMatcher, len()) .seedNode[][] = make([]*graph.NodeMatcher, len()) for := range { .seedRel[][][] = .G.CompileRelMatcher([].Types) .seedNode[][][] = .G.CompileNodeMatcher([].Labels, nil) } } } } return } // buildLevelFilters splits the stage WHERE into top-level AND-conjuncts // and buckets each at the earliest op level where every slot it reads is // bound; graph-touching conjuncts keep last-level timing. A conjunct then // prunes a candidate the moment it can fail instead of after the whole // pattern binds. A conjunct whose only row dependency is its level's own // node slot and whose shape specializes lands in the batch buckets // (compile.CandidateBatch: one columnar pass over the whole candidate // buffer) or the preds buckets (compile.CandidatePred: per-candidate // closure) instead of the general row evaluation. func buildLevelFilters( *eval.Ctx, *plan.MatchStage, []plan.BindOp, map[string]int, func(int) bool, []value.Value) ([][]RowEval, [][]compile.CandPred, [][]compile.CandBatch) { := max(len(), 1) := make([][]RowEval, ) := make([][]compile.CandPred, ) := make([][]compile.CandBatch, ) if .Where == nil { return , , } // A named-path bind assembles p only AFTER the walk, so its WHERE runs // as a post-path filter instead (a level filter over the still-null // path slot would silently drop every row). if .PathBind != nil { return , , } := make(map[int]int, len()) for := range { := &[] if , := [slotOf()]; ! { [slotOf()] = } if := relSlotOf(); != plan.NoSlot { if , := []; ! { [] = } } } // A slot is batch-constant when the segment never binds it and the // seeded inputs agree on its value (constIn); a carried slot is // loop-invariant per match-call even when it varies across input rows. := func( int) bool { , := []; return } := func( int) bool { return !() && () } := func( int) bool { return !() } var []ast.Expr plan.SplitAnd(.Where, &) = rewriteDateEqRanges(, , ) for , := range { := hoistEval(, compileEval(, , ), , , , ) var []int , := false, false evalPushdown(, , , &, &, &) := - 1 if ! { = 0 for , := range { if , := []; && > { = } } // A graph-walking conjunct (a memoized subquery: its memo bounds // only repeat cost -- every distinct correlation tuple still // pays a walk) placed at its binding level would run once per // row the NEXT ops are about to discard. Ops that can only // SHRINK the row set -- rebind expands, existence checks // emitting at most one row per input -- cannot change what the // conjunct is asked about, and a conjunction is // order-independent, so slide the walk forward over them and // stop at the first op that can grow the rows. The early-prune // placement is preserved everywhere it is a win (any op after // the walk that fans out). if { for +1 < { := &[+1] if .Kind == plan.OpExpand && .Rebind { ++ continue } break } } } := min(, -1) if , := .(*compile.Compiled); && < len() { // A uniqueness-tracked level never fill-sweeps, so its // conjuncts stay per-candidate (CandidatePred below). if [].Uniq == nil { if , := compile.CandidateBatch(, slotOf(&[])); { [] = append([], ) continue } } if , := compile.CandidatePred(, slotOf(&[]), ); { [] = append([], ) continue } } [] = append([], ) } return , , } // slotAgrees reports whether slot holds an identical value on every row of // the batch (vacuously true for an empty or single-row batch) -- the one // "is this slot batch-constant" test, with an explicit out-of-range // policy. padNull treats a position beyond a row's width as Null (the // seeded-input convention, where narrow inputs pad with nulls); without it // any out-of-range read across differing rows disqualifies, guarding // callers that will index rows at the slot directly. func slotAgrees( int, [][]value.Value, bool) bool { if len() == 0 { return true } var value.Value if < len([0]) { = [0][] } else if ! && len() > 1 { return false } for , := range [1:] { var value.Value if < len() { = [] } else if ! { return false } if !value.Identical(, ) { return false } } return true }