// Non-aggregated projection: the terminal sink evaluates the output // columns per pushed row, applies DISTINCT on arrival (before ORDER // BY/LIMIT, as the standard requires -- first occurrence kept), then // finalize sorts and paginates. Projected rows live in a chunked arena; // the matched row is retained (arena-copied) only when an ORDER BY key is // not a projected column. The Rust bounded top-k heap remains a possible // follow-up with byte-identical results.
package exec import ( ) // projSink is the non-aggregated terminal sink. type projSink struct { ctx *eval.Ctx proj *plan.ProjPlan slots map[string]int returns []RowEval outs [][]value.Value oArena rowArena // needM: an ORDER BY key evaluates over the matched row, so matched // rows must be retained alongside their projections. needM bool ms [][]value.Value mArena rowArena // DISTINCT state: a single output column dedups through distinctSet // (the u32 entity-id fast path for node/rel values, AppendKey bytes // otherwise); a multi-column row keys on the concatenated AppendKey // encoding -- both dedups thus share one canonical value encoding. seenOne *distinctSet seen *flatset.ByteSet key []byte // topk is the streaming bounded accumulator under ORDER BY + LIMIT: // at most skip+limit rows are retained, ordered by the sort's exact // total order (keys, then arrival), so a rejected row costs one // comparison and leaves nothing in the arenas. limitCap is the // LIMIT-without-ORDER-BY retention cap (arrival order IS the output // order, so rows past skip+limit can never surface). topk *topKRows limitCap int // topk key evaluation state (mirrors sortRowsByOrder's scope). kColIdx []int kScope map[string]int kBase int kRowbuf []value.Value kBuf []value.Value // kGate marks the payload-gated top-k path: every ORDER BY key either // is an output column or evaluates against the matched row alone // (kRowEval, the RETURN m ... ORDER BY m.x identity family), and no // DISTINCT dedups on the full row -- so the keys can evaluate first // and the heap can refuse a candidate BEFORE its remaining columns are // built (most candidates die on one comparison once the heap fills). kGate bool kRowEval []RowEval } // topkPayloadBuilds counts full projected payload constructions on the // gated top-k path -- the load-independent oracle: ORDER BY v ASC LIMIT k // over ascending input builds exactly k payloads, not one per row. // disableTopkGate pins differential tests to the unguarded // build-then-offer flow (the reference the gate must match exactly). var ( topkPayloadBuilds int disableTopkGate bool ) func newProjSink( *eval.Ctx, *plan.ProjPlan, map[string]int, int) *projSink { := &projSink{ ctx: , proj: , slots: , returns: make([]RowEval, len(.Returns)), oArena: rowArena{width: len(.Returns)}, limitCap: -1, } for , := range .Returns { .returns[] = compileEval(, .Expr, ) } for := range .OrderBy { if plan.OrderColIndex(.OrderBy[].Expr, .Columns, .Returns) < 0 { .needM = true .mArena = rowArena{width: } break } } if .Distinct { if len(.Returns) == 1 { .seenOne = &distinctSet{} } else { .seen = &flatset.ByteSet{} } } if := orderBound(); >= 0 { // Retention is bounded, so size arena chunks to it (plus eviction // turnover) instead of the default sweep-sized chunk -- a // LIMIT-bounded sink otherwise pays a near-empty 16K-value chunk // per arena (the aggregate finalize precedent). .oArena.chunkValues = min(arenaChunkValues, max(64, 2*)*len(.Returns)) if .needM { .mArena.chunkValues = min(arenaChunkValues, max(64, 2*)*) } if := len(.OrderBy); > 0 { .topk = newTopKRows(, , .OrderBy) .kColIdx = make([]int, ) for := range .OrderBy { .kColIdx[] = plan.OrderColIndex(.OrderBy[].Expr, .Columns, .Returns) } .kScope = make(map[string]int, len()+len(.Columns)) maps.Copy(.kScope, ) .kBase = 0 if .needM { .kBase = } for , := range .Columns { .kScope[] = .kBase + } .kBuf = make([]value.Value, ) .kGate = !.Distinct && !disableTopkGate for := range .OrderBy { if .kColIdx[] < 0 { // Not an output column, but still gateable when the // key's references all resolve in the matched row's // scope (RETURN m ... ORDER BY m.x and friends): it // then evaluates pre-build, exactly like a column key. if semantics.CheckRefs(.OrderBy[].Expr, ) == nil { if .kRowEval == nil { .kRowEval = make([]RowEval, ) } .kRowEval[] = compileEval(, .OrderBy[].Expr, ) continue } .kGate = false // key needs the built row break } } } else { // No ORDER BY: arrival order is output order, so retention // past skip+limit can never surface. .limitCap = } } return } // pushKeys evaluates the ORDER BY key vector for a just-projected row // into p.kBuf (out is the projected row, row the matched row). func ( *projSink) (, []value.Value) []value.Value { := false for := range .proj.OrderBy { if := .kColIdx[]; >= 0 { .kBuf[] = [] continue } if ! { .kRowbuf = append(.kRowbuf[:0], ...) .kRowbuf = append(.kRowbuf, ...) = true } .kBuf[] = eval.Eval(.ctx, .proj.OrderBy[].Expr, .kRowbuf, .kScope) } return .kBuf } func ( *projSink) ( []value.Value) bool { if .limitCap >= 0 && len(.outs) >= .limitCap { // Arrival order is output order (no ORDER BY), so nothing a // producer could still emit can surface: stop the walk. return false } // Gated top-k: evaluate only the key columns and ask the heap first; // a refused candidate never builds its remaining columns (nor touches // the arena). Skipping is byte-identical to offer-then-pop (see // wouldAccept), so the surviving rows match the unguarded path // exactly. if .kGate { for := range .proj.OrderBy { if := .kColIdx[]; >= 0 { .kBuf[] = .returns[].Eval(.ctx, , .slots) } else { .kBuf[] = .kRowEval[].Eval(.ctx, , .slots) } } if !.topk.wouldAccept(.kBuf) { .topk.seq++ // rejected offers still order future arrivals return true } topkPayloadBuilds++ := .oArena.alloc() for := range .proj.OrderBy { if := .kColIdx[]; >= 0 { [] = .kBuf[] } } for , := range .returns { if !.kGateCol() { [] = .Eval(.ctx, , .slots) } } if !.topk.offer(.pushKeys(, ), ) { .oArena.rollback() } return true } := .oArena.alloc() for , := range .returns { [] = .Eval(.ctx, , .slots) } if .seenOne != nil { if !.seenOne.add([0], &.key) { .oArena.rollback() return true } } else if .seen != nil { .key = .key[:0] for , := range { .key = value.AppendKey(.key, ) } if !.seen.Add(.key) { .oArena.rollback() return true } } if .topk != nil { // Keys evaluate here, against the live matched row -- so the // matched row never needs retaining at all on this path. if !.topk.offer(.pushKeys(, ), ) { .oArena.rollback() } return true } .outs = append(.outs, ) if .needM { .ms = append(.ms, .mArena.copyRow()) } // Reaching the unordered cap exactly now also stops the walk (a // deferred false would cost one more full candidate). return .limitCap < 0 || len(.outs) < .limitCap } // kGateCol reports whether output column i is one of the gated path's // already-evaluated key columns. func ( *projSink) ( int) bool { for := range .kColIdx { if .kColIdx[] == { return true } } return false } func ( *projSink) () {} // topKRows is the sink's bounded ORDER BY + LIMIT accumulator: at most // bound rows survive, under the same total order finalize's sort applies // (ORDER BY keys, then arrival sequence), so streaming selection equals // materialize-sort-truncate exactly. A max-heap over parallel arrays; // the worst survivor sits at the root and each rejected offer costs one // comparison. type topKRows struct { bound int nk int desc []bool keys []value.Value // n*nk, admitted key vectors outs [][]value.Value seqs []int n int seq int } func newTopKRows(, int, []ast.SortItem) *topKRows { := &topKRows{bound: , nk: , desc: make([]bool, )} for := range { .desc[] = [].Desc } // The heap fills to bound and stays there; presizing (capped, so a // huge LIMIT over few offers does not overcommit) removes the append // regrowth of the three parallel arrays. if := min(, 1024); > 0 { .keys = make([]value.Value, 0, *) .outs = make([][]value.Value, 0, ) .seqs = make([]int, 0, ) } return } // cmpTo compares candidate (keys, seq) against admitted entry i. func ( *topKRows) ( []value.Value, , int) int { for := 0; < .nk; ++ { := value.OrderCmp([], .keys[*.nk+]) if .desc[] { = - } if != 0 { return } } return - .seqs[] } // cmpEntries compares admitted entries a and b. func ( *topKRows) (, int) int { return .cmpTo(.keys[*.nk:(+1)*.nk], .seqs[], ) } // offer admits the row when it belongs in the current top bound, copying // its keys (the caller's buffer is reused). Reports whether the row was // retained. // wouldAccept reports whether an offer carrying keys could be admitted, // consulted BEFORE the payload is built: true under capacity; once full, // only when keys sort strictly before the worst survivor. A key TIE // loses -- a new candidate's arrival sequence is larger than every // admitted row's, so a tied offer would be popped immediately -- which // makes skipping a rejected candidate byte-identical to offering it, and // preserves stability across a tie straddling the LIMIT boundary (the // kept rows' relative order never changes). func ( *topKRows) ( []value.Value) bool { if .bound == 0 { return false } if .n < .bound { return true } for := 0; < .nk; ++ { := value.OrderCmp([], .keys[]) if .desc[] { = - } if != 0 { return < 0 } } return false } func ( *topKRows) ( []value.Value, []value.Value) bool { := .seq .seq++ if .bound == 0 { return false } if .n == .bound && .cmpTo(, , 0) >= 0 { return false } if .n < .bound { .keys = append(.keys, ...) .outs = append(.outs, ) .seqs = append(.seqs, ) .n++ .siftUp(.n - 1) return true } copy(.keys[:.nk], ) .outs[0], .seqs[0] = , .siftDown(0) return true } func ( *topKRows) (, int) { for := 0; < .nk; ++ { .keys[*.nk+], .keys[*.nk+] = .keys[*.nk+], .keys[*.nk+] } .outs[], .outs[] = .outs[], .outs[] .seqs[], .seqs[] = .seqs[], .seqs[] } func ( *topKRows) ( int) { for > 0 { := ( - 1) / 2 if .cmpEntries(, ) <= 0 { return } .swap(, ) = } } func ( *topKRows) ( int) { for { := 2* + 1 if >= .n { return } := if := + 1; < .n && .cmpEntries(, ) > 0 { = } if .cmpEntries(, ) <= 0 { return } .swap(, ) = } } // sorted returns the survivors in final order (ascending under the total // order). func ( *topKRows) () [][]value.Value { := make([]int, .n) for := range { [] = } slices.SortFunc(, .cmpEntries) := make([][]value.Value, .n) for , := range { [] = .outs[] } return } func ( *projSink) () [][]value.Value { if .topk != nil { return paginate(.topk.sorted(), .proj.Skip, .proj.Limit) } := .outs if len(.proj.OrderBy) > 0 { := func(int) []value.Value { return nil } := 0 if .needM { = func( int) []value.Value { return .ms[] } if len(.ms) > 0 { = len(.ms[0]) } } = sortRowsByOrder(.ctx, .proj, .slots, , , ) } return paginate(, .proj.Skip, .proj.Limit) }