// The push pipeline: a segment's stages run as a chain of row sinks, each // holding one reused full-width row buffer, so matched rows flow through // the chain without per-row allocation. Only the terminal sink retains // rows (in chunked arenas); the shortest-path stage keeps its batch // semantics behind a buffering barrier. Depth-first push order equals the // former materialize-then-iterate order, so group encounter order, // DISTINCT first-occurrence, and stable-sort ties are unchanged.
package exec import ( chickpeas ) // rowSink consumes full-width rows pushed through a segment's stage chain. // A pushed row is valid only for the duration of the call; a sink that // retains it must copy. push reports whether the consumer wants more rows: // false is the stop protocol -- an unordered LIMIT that has its k rows // tells every producer above it to abandon the walk (aggregation and // ORDER BY must consume everything and always report true). A producer // receiving false stops emitting and propagates false after its own // cleanup. close flushes any buffered state downstream. type rowSink interface { push(row []value.Value) bool close() } // arenaChunkValues caps the bump-arena chunk size in values (not rows); // arenaChunkStart seeds the geometric ladder. const ( arenaChunkValues = 16384 arenaChunkStart = 512 ) // rowArena bump-allocates fixed-width rows out of large chunks, so // retaining n rows costs n/chunk allocations instead of n. A sink that // knows its retention is bounded sets chunkValues so a small bound does // not pay a full-size chunk. type rowArena struct { width int chunkValues int // fixed chunk size in values; 0 means geometric growth // nextChunk is the geometric ladder state: chunks start small and // double up to arenaChunkValues, so a sink retaining a handful of // rows pays kilobytes rather than a near-empty full-size chunk, // while a large retention converges on the same big chunks after a // few (amortized-free) steps. nextChunk int chunk []value.Value off int } // alloc returns the next zeroed width-wide row. func ( *rowArena) () []value.Value { if .off+.width > len(.chunk) { := .chunkValues if == 0 { if .nextChunk == 0 { .nextChunk = arenaChunkStart } = .nextChunk if .nextChunk < arenaChunkValues { .nextChunk *= 2 } } := max(, .width) .chunk = make([]value.Value, ) .off = 0 } := .chunk[.off : .off+.width : .off+.width] .off += .width return } // rollback releases the arena's most recent alloc (a DISTINCT duplicate). func ( *rowArena) () { .off -= .width } // copyRow retains a transient row in the arena. func ( *rowArena) ( []value.Value) []value.Value { := .alloc() copy(, ) return } // matchSink runs one MATCH stage per pushed row: genMatches walks the bind // chain over the stage-local buffer, forwarding each bound match. A named // path is assembled and post-path-filtered per match; an OPTIONAL stage // that produced nothing re-emits the input row from the orig buffer. type matchSink struct { ctx *eval.Ctx stage *plan.MatchStage comp *stageComp slots map[string]int buf, orig []value.Value scratch genScratch next rowSink opRows []uint64 profScratch []uint64 fired bool pathFilters []RowEval // uniq is the segment chain's shared used-relationship env: chained // MATCH stages from one clause (comma patterns, planner splits) see // one stack per in-flight row. uniq *uniqEnv // emitFn is the emit method bound once, so genMatches gets the same // closure every push instead of a fresh method value. emitFn func([]value.Value) bool } func ( *matchSink) ( []value.Value) bool { // Absorbed-chain gates on carried slots: a failing row cannot match // the stage (the absorber refuses OPTIONAL stages, so dropping is // exact). for := range .comp.gates { if !.comp.gates[].pass() { return true } } copy(.buf, ) if .stage.Optional { copy(.orig, ) .fired = false := genMatches(.ctx, .comp.ops, .buf, .comp, .slots, .uniq, .emitFn, &.scratch, .profRows()) .scatterProf() if && !.fired { // The re-emitted row takes the path assembly and post-path // WHERE too, exactly like the former batch bindPaths pass. return .forward(.orig) } return } := genMatches(.ctx, .comp.ops, .buf, .comp, .slots, .uniq, .emitFn, &.scratch, .profRows()) .scatterProf() return } // profRows is the opRows slice genMatches writes: the stage's own when // exec ops match the plan's, else a scratch sized to the exec ops that // scatterProf redistributes -- PROFILE lines index ORIGINAL operators, // and an absorbed chain's ops must report 0, not another op's count. func ( *matchSink) () []uint64 { if .opRows == nil || .comp.profMap == nil { return .opRows } if .profScratch == nil { .profScratch = make([]uint64, len(.comp.ops)+1) } clear(.profScratch) return .profScratch } // scatterProf folds a scratch profile back onto the original op lines. func ( *matchSink) () { if .opRows == nil || .comp.profMap == nil { return } for , := range .comp.profMap { .opRows[] += .profScratch[] } // The trailing stage-WHERE tally. .opRows[len(.opRows)-1] += .profScratch[len(.profScratch)-1] } // emit forwards one bound match. The OPTIONAL no-match probe counts every // match (like the former pre-bindPaths batch check), so it flips before // the post-path filter can prune. func ( *matchSink) ( []value.Value) bool { .fired = true return .forward() } // forward assembles the row's named path and applies the post-path WHERE // conjuncts, then hands the row downstream. func ( *matchSink) ( []value.Value) bool { if := .stage.PathBind; != nil { := pathRelPositionsOf([.RelsSlot]) var []graph.NodeID if , := [.FromSlot].AsNode(); { = reconstructPathNodes(.ctx, , ) } [.PathSlot] = value.Path(, ) for , := range .pathFilters { if !.Eval(.ctx, , .slots).IsTruthy() { return true } } } return .next.push() } func ( *matchSink) () { .next.close() } // unwindSink is FOR x IN list per pushed row: a list emits one row per // element, null emits none, any other scalar emits a single row. type unwindSink struct { ctx *eval.Ctx list RowEval slots map[string]int out int buf []value.Value next rowSink count *uint64 } func ( *unwindSink) ( []value.Value) bool { := .list.Eval(.ctx, , .slots) if , := .AsList(); { for , := range { copy(.buf, ) .buf[.out] = .bump() if !.next.push(.buf) { return false } } return true } if .IsNull() { return true } copy(.buf, ) .buf[.out] = .bump() return .next.push(.buf) } func ( *unwindSink) () { if .count != nil { *.count++ } } func ( *unwindSink) () { .next.close() } // callSink crosses each pushed row with a procedure's results -- computed // once at build for a constant-argument call (per-node scalar vector, // index search hit-set, or propagation rows), or per pushed row for a // correlated call (argEvals non-nil), where a row whose evaluated // arguments fail validation yields no rows. A backend without the native // capability passes rows through unchanged. type callSink struct { ctx *eval.Ctx cs *plan.CallStage values []value.Value // hits is the search hit-set's ascending-id iteration (nil = no hits); // unnamed so the nodeset iterator assigns directly. hits func(yield func(graph.NodeID) bool) // prop is the resolved algo.propagate result set. prop []chickpeas.PropagateResult argEvals []RowEval slots map[string]int g *chickpeas.Snapshot native bool buf []value.Value next rowSink count *uint64 } func ( *callSink) ( []value.Value) bool { if !.native { .bump() return .next.push() } , , := .values, .hits, .prop if .argEvals != nil { := make([]value.Value, len(.argEvals)) for , := range .argEvals { [] = .Eval(.ctx, , .slots) } , := plan.ResolveCallProc(.cs.ProcName, ) if != nil { return true } , , = callResults(&, .g) } if .cs.Proc.Kind == plan.ProcPropagate { for , := range { copy(.buf, ) if .cs.NodeSlot != plan.NoSlot { .buf[.cs.NodeSlot] = value.Node(.Node) } if .cs.ValueSlot != plan.NoSlot { .buf[.cs.ValueSlot] = value.Float(.Value) } if .cs.DepthSlot != plan.NoSlot { .buf[.cs.DepthSlot] = value.Int(int64(.Depth)) } .bump() if !.next.push(.buf) { return false } } return true } if != nil { // The per-node vector is id-space-sized, so on a sparse space its // indexes include gap ids; the existence oracle keeps phantom // rows out of the result (a real node structurally cannot drop -- // the vector covers the whole space). for , := range { if !.g.NodeExists(chickpeas.NodeID()) { continue } copy(.buf, ) if .cs.NodeSlot != plan.NoSlot { .buf[.cs.NodeSlot] = value.Node(graph.NodeID()) } if .cs.ValueSlot != plan.NoSlot { .buf[.cs.ValueSlot] = } .bump() if !.next.push(.buf) { return false } } return true } if == nil { return true } := true (func( graph.NodeID) bool { copy(.buf, ) if .cs.NodeSlot != plan.NoSlot { .buf[.cs.NodeSlot] = value.Node() } .bump() = .next.push(.buf) return }) return } func ( *callSink) () { if .count != nil { *.count++ } } func ( *callSink) () { .next.close() } // subquerySink is CALL { } per pushed row: a correlated subquery runs the // sub-plan seeded from the outer row's imports; an uncorrelated one runs // once on first use and cross-joins. type subquerySink struct { ctx *eval.Ctx cs *plan.CallSubqueryStage buf []value.Value seed []value.Value subRows [][]value.Value subRun bool next rowSink count *uint64 } func ( *subquerySink) ( []value.Value) bool { var [][]value.Value if len(.cs.ImportSlots) == 0 { if !.subRun { .subRows = runSubplan(.ctx, .cs.Sub, nil) .subRun = true } = .subRows } else { for , := range .cs.ImportSlots { .seed[] = [] } = runSubplan(.ctx, .cs.Sub, .seed) } for , := range { copy(.buf, ) for , := range .cs.OutSlots { .buf[] = [] } if .count != nil { *.count++ } if !.next.push(.buf) { return false } } return true } func ( *subquerySink) () { .next.close() } // spSink is the shortest-path stage's batch barrier: input rows buffer in // an arena until close, because the batch runner memoizes a parent tree // for sources shared by multiple rows. type spSink struct { ctx *eval.Ctx sp *plan.SpStage rows [][]value.Value arena rowArena next rowSink count *uint64 } func ( *spSink) ( []value.Value) bool { .rows = append(.rows, .arena.copyRow()) return true } func ( *spSink) () { := runSPStage(.ctx, .sp, .rows) if .count != nil { *.count = uint64(len()) } for , := range { if !.next.push() { break } } .next.close() } // gateSink is an injected early row gate (plan.GateStage): like spSink it // buffers its input so the batch path runner keeps its shared-source tree // memo, then binds the path into its hidden slot, evaluates the hoisted // LET chain, and forwards only rows passing the hoisted filter. Rows it // drops are exactly rows the downstream chain would drop (the search is // deterministic per endpoint pair), so results are identical -- just // cheaper, because survivors alone reach the stages behind it. type gateSink struct { ctx *eval.Ctx gs *plan.GateStage slots map[string]int derived []RowEval where RowEval rows [][]value.Value arena rowArena next rowSink count *uint64 } func ( *gateSink) ( []value.Value) bool { .rows = append(.rows, .arena.copyRow()) return true } func ( *gateSink) () { := runSPStage(.ctx, &.gs.Sp, .rows) for , := range { for , := range .gs.Derived { [.Slot] = .derived[].Eval(.ctx, , .slots) } if !.where.Eval(.ctx, , .slots).IsTruthy() { continue } if .count != nil { *.count++ } if !.next.push() { break } } .next.close() }