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}