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