Skip to main content

mongreldb_core/
cursor.rs

1//! Lazy, page-aware native-column cursor for streaming scans (Phase 6.2).
2//!
3//! [`NativePageCursor`] is the streaming source backing the SQL scan's
4//! single-run fast path. It is built up front under the DB lock, where MVCC
5//! visibility and predicate survivor resolution happen once; the cursor then
6//! owns the run reader and lazily decodes **only the projected columns of pages
7//! that contain survivors**, one page (batch) per [`NativePageCursor::next_batch`].
8//!
9//! Pages with no surviving rows are never decoded (page skipping), and projected
10//! columns are decoded only as the consumer pulls (late materialization — a
11//! `LIMIT` satisfied early stops paying the decode cost of later pages). The
12//! cursor never uses page `min/max` for MVCC visibility; visibility comes from
13//! the system columns (`RowId`/`Epoch`/deleted) resolved at build time.
14
15use crate::columnar::{decode_page_native, NativeColumn};
16use crate::error::Result;
17use crate::row_id_set::RowIdSet;
18use crate::schema::TypeId;
19use crate::sorted_run::{RunReader, SYS_ROW_ID};
20
21/// A forward streaming scan cursor over typed native columns. Implemented by
22/// the single-run [`NativePageCursor`] (page-plan fast path) and the multi-run
23/// [`MultiRunCursor`] (k-way merge by `RowId` across N runs — Phase 16.1). The
24/// SQL scan holds a `Box<dyn Cursor>` so both layouts stream lazily instead of
25/// materializing every row up front.
26pub trait Cursor: Send {
27    /// Decode the next batch of survivor rows as projected native columns, in
28    /// ascending `RowId` order. `None` when the stream is exhausted.
29    fn next_batch(&mut self) -> Result<Option<Vec<NativeColumn>>>;
30    /// Exact count of surviving rows still to be yielded (without decoding).
31    fn remaining_rows(&self) -> usize;
32    /// The projected column types, in output order.
33    fn projection_types(&self) -> Vec<TypeId>;
34}
35
36/// Drain every batch from `cursor` and concatenate per-column into a single
37/// `Vec<(column_id, NativeColumn)>` in `projection` order. Each batch's columns
38/// are appended via [`NativeColumn::concat`], so the result is one typed buffer
39/// per projected column spanning all surviving rows.
40///
41/// This is the columnar alternative to `rows_for_rids` for layouts where the
42/// single-run fast gather does not apply (multi-run, non-empty overlay): it
43/// lets [`crate::engine::Table::query_columns_native`] stay columnar end-to-end
44/// instead of materializing `Row { HashMap }` objects and pivoting back.
45pub fn drain_cursor_to_columns(
46    cursor: &mut dyn Cursor,
47    projection: &[(u16, TypeId)],
48) -> Result<Vec<(u16, NativeColumn)>> {
49    drain_cursor_to_columns_inner(cursor, projection, None)
50}
51
52pub fn drain_cursor_to_columns_with_control(
53    cursor: &mut dyn Cursor,
54    projection: &[(u16, TypeId)],
55    control: &crate::ExecutionControl,
56) -> Result<Vec<(u16, NativeColumn)>> {
57    drain_cursor_to_columns_inner(cursor, projection, Some(control))
58}
59
60fn drain_cursor_to_columns_inner(
61    cursor: &mut dyn Cursor,
62    projection: &[(u16, TypeId)],
63    control: Option<&crate::ExecutionControl>,
64) -> Result<Vec<(u16, NativeColumn)>> {
65    let ncols = projection.len();
66    let mut acc: Vec<Vec<NativeColumn>> = (0..ncols).map(|_| Vec::new()).collect();
67    while let Some(batch) = cursor.next_batch()? {
68        control
69            .map(crate::ExecutionControl::checkpoint)
70            .transpose()?;
71        for (j, col) in batch.into_iter().enumerate() {
72            if j < ncols {
73                acc[j].push(col);
74            }
75        }
76    }
77    let mut columns = Vec::with_capacity(ncols);
78    for (j, pieces) in acc.into_iter().enumerate() {
79        control
80            .map(crate::ExecutionControl::checkpoint)
81            .transpose()?;
82        columns.push({
83            let col = if pieces.is_empty() {
84                crate::columnar::null_native(projection[j].1.clone(), 0)
85            } else {
86                NativeColumn::concat(&pieces)
87            };
88            (projection[j].0, col)
89        });
90    }
91    Ok(columns)
92}
93
94/// One page's worth of within-page survivor positions to decode.
95#[derive(Clone)]
96pub(crate) struct PagePlan {
97    /// Page sequence number (0-based) across the run's PAX pages.
98    pub(crate) seq: usize,
99    /// Within-page row positions that survive MVCC + the predicate.
100    pub(crate) positions: Vec<usize>,
101}
102
103/// A forward cursor over a single sorted run that yields the projected columns
104/// of surviving rows, page by page. Built by [`crate::engine::Table`].
105///
106/// All MVCC visibility and predicate resolution is settled at construction
107/// (the `PagePlan`s); [`Self::next_batch`] is pure lazy column decode + gather.
108pub struct NativePageCursor {
109    reader: RunReader,
110    projection: Vec<(u16, TypeId)>,
111    plans: Vec<PagePlan>,
112    next: usize,
113    /// Phase 13.1: pre-materialized columns from the memtable / mutable-run
114    /// overlay, yielded as a single final batch after all page plans are
115    /// drained. `None` when there is no overlay (clean single-run layout).
116    overlay: Option<Vec<NativeColumn>>,
117    /// Row count of the overlay batch (tracked separately so `remaining_rows`
118    /// works even when `projection` is empty — the COUNT(*) path).
119    overlay_rows: usize,
120}
121
122impl NativePageCursor {
123    /// Build a cursor over `reader` with an optional overlay batch (Phase 13.1).
124    /// The overlay — pre-materialized columns from the memtable / mutable-run
125    /// tier — is yielded as a single final batch after all page plans. `None`
126    /// for a clean single-run layout (no overlay).
127    pub(crate) fn new_with_overlay(
128        reader: RunReader,
129        projection: Vec<(u16, TypeId)>,
130        plans: Vec<PagePlan>,
131        overlay: Option<Vec<NativeColumn>>,
132    ) -> Self {
133        let overlay_rows = overlay
134            .as_ref()
135            .map(|cols| cols.first().map(|c| c.len()).unwrap_or(0))
136            .unwrap_or(0);
137        Self {
138            reader,
139            projection,
140            plans,
141            next: 0,
142            overlay,
143            overlay_rows,
144        }
145    }
146
147    /// The projected column types, in output order.
148    pub fn projection_types(&self) -> Vec<TypeId> {
149        self.projection.iter().map(|(_, t)| t.clone()).collect()
150    }
151
152    /// Total surviving rows still to be yielded across all remaining plans plus
153    /// the overlay (the scan's exact output row count, without decoding pages).
154    pub fn remaining_rows(&self) -> usize {
155        let pages: usize = self.plans[self.next..]
156            .iter()
157            .map(|p| p.positions.len())
158            .sum();
159        pages + self.overlay_rows
160    }
161
162    /// Decode the next surviving page's projected columns, gathered to that
163    /// page's survivor positions. Returns `None` when no pages remain. The
164    /// overlay batch (if any) is yielded as the final batch.
165    pub fn next_batch(&mut self) -> Result<Option<Vec<NativeColumn>>> {
166        while self.next < self.plans.len() {
167            let plan = self.plans[self.next].clone();
168            self.next += 1;
169            if plan.positions.is_empty() {
170                continue;
171            }
172            let nrows = self
173                .reader
174                .page_row_counts(SYS_ROW_ID)?
175                .get(plan.seq)
176                .copied()
177                .unwrap_or(0);
178            let mut cols = Vec::with_capacity(self.projection.len());
179            for (cid, ty) in &self.projection {
180                // Schema evolution: a column added via `add_column` after this
181                // run was written is absent here, so decode all-null at the
182                // survivor positions (mirroring RunReader::column_native).
183                let col = if self.reader.has_column(*cid) {
184                    let page = self.reader.read_page(*cid, plan.seq)?;
185                    let decoded = decode_page_native(ty.clone(), &page, nrows)?;
186                    decoded.gather(&plan.positions)
187                } else {
188                    crate::columnar::null_native(ty.clone(), plan.positions.len())
189                };
190                cols.push(col);
191            }
192            return Ok(Some(cols));
193        }
194        // Phase 13.1: yield the pre-materialized overlay batch (memtable /
195        // mutable-run tier) as the final batch, then clear it. When the
196        // projection is empty (COUNT(*) path) but the overlay has rows, emit
197        // an empty-column batch carrying just the row count.
198        if self.overlay_rows > 0 {
199            self.overlay_rows = 0;
200            if let Some(cols) = self.overlay.take() {
201                return Ok(Some(cols));
202            }
203            // Empty projection: fabricate a zero-column batch with the right
204            // row count so the caller's `RecordBatch` infers the count.
205            return Ok(Some(Vec::new()));
206        }
207        Ok(None)
208    }
209}
210
211impl Cursor for NativePageCursor {
212    fn next_batch(&mut self) -> Result<Option<Vec<NativeColumn>>> {
213        NativePageCursor::next_batch(self)
214    }
215    fn remaining_rows(&self) -> usize {
216        NativePageCursor::remaining_rows(self)
217    }
218    fn projection_types(&self) -> Vec<TypeId> {
219        NativePageCursor::projection_types(self)
220    }
221}
222
223/// Number of survivor rows materialized per `next_batch` on the multi-run path.
224/// Matches the encoded 65 536-row page size so a batch typically spans at most
225/// a handful of pages across runs.
226const MERGE_BATCH_ROWS: usize = 65_536;
227
228/// One run's contribution to a [`MultiRunCursor`]: the run's owned survivors —
229/// rows whose newest MVCC-visible version lives in *this* run (not shadowed by
230/// the overlay or a newer run) that also satisfy the predicate — plus a lazily
231/// decoded cache of the current page's projected columns.
232pub(crate) struct RunStream {
233    reader: RunReader,
234    /// Owned survivors as `(row_id, page_seq, within_page_pos)`, ascending by
235    /// `row_id` (runs are sorted by `RowId`, so this is also position order).
236    survivors: Vec<(u64, usize, usize)>,
237    head: usize,
238    page_row_counts: Vec<usize>,
239    /// Page seq currently decoded into `cur_cols` (`None` before the first decode).
240    cur_page: Option<usize>,
241    cur_cols: Vec<NativeColumn>,
242}
243
244impl RunStream {
245    pub(crate) fn new(
246        reader: RunReader,
247        survivors: Vec<(u64, usize, usize)>,
248        page_row_counts: Vec<usize>,
249    ) -> Self {
250        Self {
251            reader,
252            survivors,
253            head: 0,
254            page_row_counts,
255            cur_page: None,
256            cur_cols: Vec::new(),
257        }
258    }
259}
260
261/// A forward cursor over **multiple** sorted runs that yields the projected
262/// columns of surviving rows via a k-way merge by `RowId` (Phase 16.1).
263///
264/// Cross-run MVCC resolution (newest visible version per `RowId`) and predicate
265/// survivor resolution are settled at construction using only the cheap system
266/// columns; `next_batch` then lazily decodes the projected data columns of just
267/// the pages that own survivors, each page at most once. This generalizes the
268/// single-run [`NativePageCursor`] to arbitrary run counts so multi-run tables
269/// stream instead of fully materializing.
270pub struct MultiRunCursor {
271    streams: Vec<RunStream>,
272    projection: Vec<(u16, TypeId)>,
273    /// Min-merge heap of `(row_id, stream_index)` over each stream's next survivor.
274    heap: std::collections::BinaryHeap<std::cmp::Reverse<(u64, usize)>>,
275    remaining: usize,
276    overlay: Option<Vec<NativeColumn>>,
277    overlay_rows: usize,
278    overlay_done: bool,
279}
280
281impl MultiRunCursor {
282    pub(crate) fn new(
283        streams: Vec<RunStream>,
284        projection: Vec<(u16, TypeId)>,
285        heap: std::collections::BinaryHeap<std::cmp::Reverse<(u64, usize)>>,
286        remaining: usize,
287        overlay: Option<Vec<NativeColumn>>,
288    ) -> Self {
289        let overlay_rows = overlay
290            .as_ref()
291            .map(|cols| cols.first().map(|c| c.len()).unwrap_or(0))
292            .unwrap_or(0);
293        Self {
294            streams,
295            projection,
296            heap,
297            remaining,
298            overlay,
299            overlay_rows,
300            overlay_done: false,
301        }
302    }
303
304    fn decode_page(&mut self, sidx: usize, page_seq: usize) -> Result<()> {
305        let ncols = self.projection.len();
306        let stream = &mut self.streams[sidx];
307        let nrows = stream.page_row_counts.get(page_seq).copied().unwrap_or(0);
308        let mut cols = Vec::with_capacity(ncols);
309        for (cid, ty) in &self.projection {
310            let col = if stream.reader.has_column(*cid) {
311                let page = stream.reader.read_page(*cid, page_seq)?;
312                decode_page_native(ty.clone(), &page, nrows)?
313            } else {
314                crate::columnar::null_native(ty.clone(), nrows)
315            };
316            cols.push(col);
317        }
318        stream.cur_page = Some(page_seq);
319        stream.cur_cols = cols;
320        Ok(())
321    }
322}
323
324impl Cursor for MultiRunCursor {
325    fn projection_types(&self) -> Vec<TypeId> {
326        self.projection.iter().map(|(_, t)| t.clone()).collect()
327    }
328
329    fn remaining_rows(&self) -> usize {
330        self.remaining + self.overlay_rows
331    }
332
333    fn next_batch(&mut self) -> Result<Option<Vec<NativeColumn>>> {
334        // Phase 1 — k-way merge: pop survivors in ascending RowId order into
335        // per-(stream, page) segments. No data-column decode here; only the
336        // precomputed (page_seq, pos) is used.
337        if !self.heap.is_empty() {
338            let mut segments: Vec<(usize, usize, Vec<usize>)> = Vec::new();
339            let mut count = 0usize;
340            while count < MERGE_BATCH_ROWS {
341                let Some(std::cmp::Reverse((_, sidx))) = self.heap.pop() else {
342                    break;
343                };
344                let stream = &mut self.streams[sidx];
345                if stream.head >= stream.survivors.len() {
346                    continue;
347                }
348                let (_rid, page_seq, pos) = stream.survivors[stream.head];
349                stream.head += 1;
350                if let Some(last) = segments.last_mut() {
351                    if last.0 == sidx && last.1 == page_seq {
352                        last.2.push(pos);
353                    } else {
354                        segments.push((sidx, page_seq, vec![pos]));
355                    }
356                } else {
357                    segments.push((sidx, page_seq, vec![pos]));
358                }
359                count += 1;
360                self.remaining -= 1;
361                if stream.head < stream.survivors.len() {
362                    let next_rid = stream.survivors[stream.head].0;
363                    self.heap.push(std::cmp::Reverse((next_rid, sidx)));
364                }
365            }
366
367            // Phase 2 — gather: decode each segment's page (lazily, cached per
368            // stream; segments are in ascending page order within a stream, so
369            // each page decodes at most once) and gather its positions.
370            let ncols = self.projection.len();
371            if ncols == 0 {
372                // COUNT(*) carries only a row count via a zero-column batch.
373                return Ok(Some(Vec::new()));
374            }
375            let mut pieces: Vec<Vec<NativeColumn>> = vec![Vec::new(); ncols];
376            for (sidx, page_seq, positions) in &segments {
377                if self.streams[*sidx].cur_page != Some(*page_seq) {
378                    self.decode_page(*sidx, *page_seq)?;
379                }
380                let cur_cols = &self.streams[*sidx].cur_cols;
381                for j in 0..ncols {
382                    pieces[j].push(cur_cols[j].gather(positions));
383                }
384            }
385            let out: Vec<NativeColumn> = (0..ncols)
386                .map(|j| NativeColumn::concat(&pieces[j]))
387                .collect();
388            return Ok(Some(out));
389        }
390
391        // Overlay (memtable / mutable-run tier) as the final batch.
392        if !self.overlay_done && self.overlay_rows > 0 {
393            self.overlay_done = true;
394            self.overlay_rows = 0;
395            if let Some(cols) = self.overlay.take() {
396                return Ok(Some(cols));
397            }
398            return Ok(Some(Vec::new()));
399        }
400        Ok(None)
401    }
402}
403
404/// Map each visible, survivor row position to its page and within-page offset,
405/// dropping pages that end up with no survivors.
406///
407/// * `visible_positions` / `rids` — MVCC-visible rows and their `RowId`s
408///   (from [`RunReader::visible_positions_with_rids`]).
409/// * `page_row_counts` — PAX page row counts (from [`RunReader::page_row_counts`]).
410/// * `survivors` — `None` for an unfiltered full scan, or the predicate-resolved
411///   `RowId` set to intersect with the visible rows.
412///
413/// Pure indexing/arithmetic — no page bytes are read — so it is cheap to call
414/// up front. Plans come out in ascending page order with within-page positions
415/// ascending.
416pub(crate) fn build_page_plans(
417    visible_positions: &[usize],
418    rids: &[i64],
419    page_row_counts: &[usize],
420    survivors: Option<&RowIdSet>,
421) -> Vec<PagePlan> {
422    debug_assert_eq!(visible_positions.len(), rids.len());
423    // Cumulative page start offsets.
424    let mut starts = Vec::with_capacity(page_row_counts.len());
425    let mut acc = 0usize;
426    for &r in page_row_counts {
427        starts.push(acc);
428        acc += r;
429    }
430    let mut by_page: std::collections::BTreeMap<usize, Vec<usize>> =
431        std::collections::BTreeMap::new();
432
433    let n = visible_positions.len();
434    // `rids` is sorted ascending (runs are written `(RowId, Epoch)`-ordered and
435    // visible positions are emitted in rid-ascending order). For a *selective*
436    // predicate (few survivors of many visible rows) it is ~k·log n cheaper to
437    // iterate the small survivor set and binary-search `rids` than to walk all
438    // visible positions doing O(1) HashSet contains — this is the inverse of the
439    // pre-16.3c loop and matches `query_columns_native`'s pattern. The factor 32
440    // bounds log2(n) for run sizes up to ~4 B rows, so `k·32 < n` ⟺ `k·log n < n`.
441    let selective = match survivors {
442        Some(set) if n > 0 => (set.len() as u64).saturating_mul(32) < n as u64,
443        _ => false,
444    };
445    if selective {
446        let set = survivors.unwrap();
447        for s in set.to_sorted_vec() {
448            let Ok(i) = rids.binary_search(&(s as i64)) else {
449                continue; // survivor lives in the overlay, not this run
450            };
451            let global = visible_positions[i];
452            let page_seq = match starts.partition_point(|&st| st <= global) {
453                0 => continue,
454                p => p - 1,
455            };
456            by_page
457                .entry(page_seq)
458                .or_default()
459                .push(global - starts[page_seq]);
460        }
461    } else {
462        for (i, &global) in visible_positions.iter().enumerate() {
463            if let Some(set) = survivors {
464                if !set.contains(rids[i] as u64) {
465                    continue;
466                }
467            }
468            // Pages are contiguous; find the last page whose start <= global.
469            let page_seq = match starts.partition_point(|&s| s <= global) {
470                0 => continue,
471                p => p - 1,
472            };
473            let within = global - starts[page_seq];
474            by_page.entry(page_seq).or_default().push(within);
475        }
476    }
477    by_page
478        .into_iter()
479        .map(|(seq, mut positions)| {
480            positions.sort_unstable();
481            PagePlan { seq, positions }
482        })
483        .collect()
484}