lsm_tree/tree/columnar_scan.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5//! Tree-level projected columnar scan.
6//!
7//! Lifts the per-SST [`Table::columnar_scan`](crate::Table::columnar_scan) to the
8//! whole tree: a consumer holding a [`Tree`] (or an
9//! [`AnyTree`](crate::AnyTree)) can run a projected, predicate-pushed columnar
10//! scan across every columnar segment intersecting a key range and visible at an
11//! MVCC snapshot, without reimplementing segment selection, snapshot visibility,
12//! delete-masking, or cross-segment ordering.
13//!
14//! # Strategy (overlap-aware merge)
15//!
16//! A row's effective sequence number is `local_seqno + global_seqno`. Bulk
17//! ingested segments carry a *uniform per-segment* seqno (every local seqno is
18//! `0`, one `global_seqno` per table), so their visibility is segment-granular;
19//! flush-produced segments carry per-row seqnos, so a snapshot can straddle them.
20//! A projected seqno column is emitted in that EFFECTIVE (tree-global) space,
21//! which is what every other read surface speaks: the stored local value would
22//! read as `0` for an ingested row and name a commit the tree never had. The
23//! masking arithmetic still runs in local space (one subtraction per segment
24//! instead of one addition per row), so only the emitted column is translated.
25//! The visible columnar segments overlapping the range are grouped by key-range
26//! overlap:
27//!
28//! - A **singleton** group (a segment whose key range overlaps no other) whose
29//! rows are all visible AND provably one-version-per-key (the writer's
30//! distinct-key count equals its row count) streams its
31//! [`Table::columnar_scan`](crate::Table::columnar_scan) batches verbatim —
32//! zero-copy column-skip, no key decode, no row gather. A singleton the
33//! snapshot straddles gets a per-row seqno mask first, and one that can hold
34//! several versions of a key (an overwritten key in a flush / compaction
35//! product) additionally gets per-key newest-visible dedup.
36//! - An **overlapping** group is row-merged: the projection is augmented with the
37//! intrinsic key + seqno columns, each segment's rows are visibility-masked and
38//! tagged with their effective seqno, the union is sorted by `(key asc,
39//! effective seqno desc)`, and the first (newest) row of each key is kept. The
40//! expensive key/seqno decode + gather is paid only where segments overlap.
41//!
42//! Groups are emitted in ascending key order, so the scan yields projected
43//! [`ColumnBatch`]es in global key order. This mirrors how `InfluxDB` `IOx`
44//! inserts its deduplication operator only over overlapping files and engineers
45//! compaction to keep files non-overlapping: as multi-segment columnar compaction
46//! reduces overlap, more of the scan takes the zero-cost singleton path.
47//!
48//! Deletes reach the scan two ways and both remove the key. A segment's
49//! positional delete-bitmap is applied inside
50//! [`Table::columnar_scan`](crate::Table::columnar_scan); a value-type TOMBSTONE
51//! is consumed here, when the newest visible version of a key is one — the key
52//! then yields no row at all, matching what a point read reports, instead of
53//! surfacing a row a caller who did not project the value-type column could not
54//! tell from a live one. Only a segment that RECORDS deletions pays for it: one
55//! whose metadata counts none keeps its columns untouched (and its zero-copy
56//! verbatim path). Memtable rows are not consulted —
57//! columnar data lives only in segments — and a visible non-columnar segment
58//! overlapping the range is rejected (a mixed-mode tree is unsupported here).
59
60use core::ops::{Bound, RangeBounds};
61
62use alloc::{vec, vec::Vec};
63
64use crate::comparator::UserComparator;
65use crate::table::SeqnoVisibility;
66use crate::table::columnar::{
67 COL_SEQNO, COL_USER_KEY, COL_VALUE_TYPE, ColumnBatch, TypeTag, bytes_column_row, fixed_u64_row,
68};
69use crate::table::columnar_predicate::{ColumnRangePredicate, filter_batch, take_rows};
70use crate::{Error, SeqNo, Table, Tree, UserKey};
71
72/// A visible columnar segment selected for the scan, with its cached key range,
73/// sequence base, and snapshot-visibility class.
74struct Segment {
75 table: Table,
76 min: UserKey,
77 max: UserKey,
78 /// The segment's `global_seqno` base; a row's effective seqno is
79 /// `local + global`.
80 global: SeqNo,
81 /// Whether every row is visible at the snapshot, or visibility is per-row.
82 visibility: SeqnoVisibility,
83 /// Whether this segment can physically hold several MVCC versions of one
84 /// key (a flush / compaction product with an overwritten key). Proven
85 /// unique only when the writer's distinct-key count equals the row count;
86 /// legacy tables without the count are conservatively assumed to carry
87 /// duplicates. Gates the singleton path's per-key newest-visible dedup.
88 may_dup: bool,
89 /// Source recency: the segment's position in the version's newest-first
90 /// table order (lower = newer). Two segments can hold DIFFERENT values
91 /// for one key at one caller-assigned seqno, and the read path serves
92 /// the newer run's value — the merge path breaks the tie with this rank,
93 /// because `group_by_overlap` re-sorts segments by minimum key and the
94 /// concatenation order alone says nothing about recency.
95 recency_rank: usize,
96}
97
98/// One key-disjoint group of segments: either a single segment (streamed
99/// verbatim) or several whose key ranges transitively overlap (row-merged).
100struct Group {
101 segments: Vec<Segment>,
102 /// Running maximum key of the group's span, used while grouping.
103 max: UserKey,
104}
105
106impl Tree {
107 /// Runs a projected columnar scan across the whole tree.
108 ///
109 /// Iterates the columnar segments intersecting `range` and visible at
110 /// `seqno`, applies each segment's positional delete-bitmap and the optional
111 /// `predicate` (zone-map block-skip + row filter), and yields projected
112 /// [`ColumnBatch`]es in ascending key order. Overlapping segments are merged
113 /// with newest-`seqno`-wins semantics so an overwritten key is returned once
114 /// (its newest version); disjoint segments stream without merge overhead.
115 ///
116 /// `range` bounds the result at row granularity: a segment that only
117 /// partially overlaps `range` contributes only the rows whose keys fall
118 /// inside it (the inclusive / exclusive sense of each bound is honored). A
119 /// fully unbounded range keeps the zero-copy fast path for an all-visible
120 /// segment.
121 ///
122 /// `projection` lists the column ids to decode (value sub-column ids, plus
123 /// optionally the intrinsic [`COL_USER_KEY`] / seqno / value-type columns);
124 /// every other column is stepped over without decoding. Each yielded batch
125 /// carries exactly the projected columns.
126 ///
127 /// This reads only segments; memtable rows are not consulted (columnar data
128 /// is written directly to segments via
129 /// [`write_columnar_batch`](crate::AnyIngestion::write_columnar_batch)).
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if a visible non-columnar segment overlaps `range` (a
134 /// mixed-mode tree is unsupported here), if the tree carries a merge
135 /// operator (see below), or — lazily, while iterating — on a block read /
136 /// decode failure or a layout mismatch between segments of an overlapping
137 /// group.
138 pub fn columnar_scan<R: RangeBounds<UserKey>>(
139 &self,
140 projection: &[u16],
141 predicate: Option<&ColumnRangePredicate>,
142 seqno: SeqNo,
143 range: R,
144 ) -> crate::Result<ColumnarScan> {
145 // A merge chain is not a version chain: its older rows are the merge's
146 // INPUTS, not data the newest row shadows. The newest-version-wins dedup
147 // below would hand back the raw operand where a read hands back the
148 // merged value, and it drops the base row, so the consumer cannot
149 // resolve the chain itself either. Refuse instead of disagreeing with
150 // the read path.
151 //
152 // Gated on the OPERATOR rather than on the rows: without one the read
153 // path returns the newest entry unchanged — the raw operand — which is
154 // exactly what this scan yields, so nothing diverges. With one, no
155 // metadata says whether a segment holds operands, and finding out means
156 // decoding the value-type column of every batch, which would cost the
157 // zero-copy fast path on every scan of every tree that merges.
158 if self.config.merge_operator.is_some() {
159 return Err(Error::FeatureUnsupported(
160 "columnar scan of a tree with a merge operator: merge chains \
161 would be returned unresolved",
162 ));
163 }
164
165 let comparator = self.config.comparator.clone();
166
167 // Owned bounds keep the returned iterator free of borrows from `range`.
168 let lo = clone_bound(range.start_bound());
169 let hi = clone_bound(range.end_bound());
170 let bounds_ref = (bound_as_ref(&lo), bound_as_ref(&hi));
171
172 let super_version = self
173 .version_history
174 .read()
175 .get_version_for_snapshot(seqno)?;
176
177 let mut segments: Vec<Segment> = Vec::new();
178 // `iter_tables` yields newest-first (the same order the sequenced
179 // scan sources rely on), so the enumeration index is the recency
180 // rank.
181 for (recency_rank, table) in super_version.version.iter_tables().enumerate() {
182 if !table.check_key_range_overlap_cmp(&bounds_ref, comparator.as_ref()) {
183 continue;
184 }
185 // Snapshot visibility (exclusive MVCC). `None` segments postdate the
186 // snapshot and are dropped before the columnar check, so an invisible
187 // non-columnar segment never trips the mixed-mode error.
188 let visibility = table.seqno_visibility(seqno);
189 if visibility == SeqnoVisibility::None {
190 continue;
191 }
192 if !table.metadata.columnar {
193 return Err(Error::FeatureUnsupported(
194 "columnar_scan: a non-columnar segment overlaps the range (mixed-mode tree)",
195 ));
196 }
197 let key_range = &table.metadata.key_range;
198 // `key_count == item_count` proves the segment holds one version per
199 // key, so the verbatim path can return its rows untouched. The count
200 // the writer recorded and the duplicate-free claim read here rest on
201 // the SAME identity relation the read path uses to collapse versions
202 // (`comparator::same_user_key`), so a segment this calls unique is
203 // one a normal read would also return whole. `None` (a legacy
204 // segment that recorded no count) proves nothing and dedups.
205 let may_dup = table
206 .metadata
207 .key_count
208 .is_none_or(|k| k != table.metadata.item_count);
209 segments.push(Segment {
210 min: key_range.min().clone(),
211 max: key_range.max().clone(),
212 global: table.global_seqno(),
213 visibility,
214 may_dup,
215 recency_rank,
216 table: table.clone(),
217 });
218 }
219
220 let groups = group_by_overlap(segments, comparator.as_ref());
221
222 Ok(ColumnarScan {
223 groups: groups.into_iter().collect(),
224 buffered: Vec::new().into(),
225 projection: projection.to_vec(),
226 predicate: predicate.cloned(),
227 comparator,
228 seqno,
229 lo,
230 hi,
231 })
232 }
233}
234
235/// Partitions `segments` into key-disjoint overlap groups, ordered by ascending
236/// minimum key. Segments are sorted by their minimum key, then greedily extended
237/// into the current group while the next segment's minimum key is `<=` the
238/// group's running maximum (an inclusive-range overlap). The result preserves
239/// global key order across groups: group `i`'s span lies entirely below group
240/// `i + 1`'s.
241fn group_by_overlap(mut segments: Vec<Segment>, cmp: &dyn UserComparator) -> Vec<Group> {
242 use core::cmp::Ordering;
243
244 segments.sort_by(|a, b| cmp.compare(a.min.as_ref(), b.min.as_ref()));
245
246 let mut groups: Vec<Group> = Vec::new();
247 for seg in segments {
248 match groups.last_mut() {
249 Some(g) if cmp.compare(seg.min.as_ref(), g.max.as_ref()) != Ordering::Greater => {
250 if cmp.compare(seg.max.as_ref(), g.max.as_ref()) == Ordering::Greater {
251 g.max = seg.max.clone();
252 }
253 g.segments.push(seg);
254 }
255 _ => groups.push(Group {
256 max: seg.max.clone(),
257 segments: vec![seg],
258 }),
259 }
260 }
261 groups
262}
263
264/// Iterator over a tree-level projected columnar scan.
265///
266/// Yields projected [`ColumnBatch`]es in ascending key order. Created by
267/// [`Tree::columnar_scan`] (and surfaced through
268/// [`AnyTree::columnar_scan`](crate::AnyTree::columnar_scan)). Each overlap group
269/// is processed lazily on demand, so at most one group's output is buffered at a
270/// time.
271pub struct ColumnarScan {
272 groups: alloc::collections::VecDeque<Group>,
273 buffered: alloc::collections::VecDeque<ColumnBatch>,
274 projection: Vec<u16>,
275 predicate: Option<ColumnRangePredicate>,
276 comparator: alloc::sync::Arc<dyn UserComparator>,
277 /// The query snapshot, used for per-row seqno visibility masking.
278 seqno: SeqNo,
279 /// The requested key range. Applied as a per-row filter (not just segment
280 /// selection): a segment that only partially overlaps the range must still
281 /// drop the rows that fall outside it.
282 lo: Bound<UserKey>,
283 hi: Bound<UserKey>,
284}
285
286impl ColumnarScan {
287 /// Processes one overlap group into its projected, key-ordered output
288 /// batches. A singleton group streams its segment's batches (masking by seqno
289 /// only when the snapshot straddles the segment); an overlapping group is
290 /// row-merged with newest-effective-seqno-wins dedup.
291 fn process_group(&self, group: &Group) -> crate::Result<Vec<ColumnBatch>> {
292 let rts = self.visible_group_range_tombstones(&group.segments)?;
293 if let [seg] = group.segments.as_slice() {
294 return self.process_singleton(seg, &rts);
295 }
296 self.merge_group(group, &rts)
297 }
298
299 /// The range tombstones of `segments` visible to the scan snapshot, with
300 /// tree-global effective seqnos: an UNMATERIALIZED range deletion (a
301 /// flushed `remove_range` no relocation has folded into a positional
302 /// delete bitmap yet) lives only in the segments' RT sections, and the
303 /// scan must suppress the rows it covers exactly as the point and
304 /// ordinary range reads do. A group is key-disjoint from its neighbours
305 /// and a tombstone's span is inside its own segment's key range, so
306 /// per-group collection sees every tombstone that can cover a group row.
307 fn visible_group_range_tombstones(
308 &self,
309 segments: &[Segment],
310 ) -> crate::Result<Vec<(UserKey, UserKey, SeqNo)>> {
311 let mut rts = Vec::new();
312 for seg in segments {
313 for rt in seg.table.visible_range_tombstones() {
314 let eff = rt
315 .seqno
316 .checked_add(seg.global)
317 .ok_or(Error::InvalidHeader(
318 "columnar_scan: effective range-tombstone seqno overflows",
319 ))?;
320 // Same exclusive-MVCC visibility as rows: the deletion exists
321 // for this snapshot only below it.
322 if eff < self.seqno {
323 rts.push((rt.start.clone(), rt.end.clone(), eff));
324 }
325 }
326 }
327 Ok(rts)
328 }
329
330 /// Whether a row (`key` at tree-global `eff` seqno) is deleted by one of
331 /// the group's visible range tombstones: inside the half-open
332 /// `[start, end)` span and older than the deletion.
333 fn rt_covered(&self, rts: &[(UserKey, UserKey, SeqNo)], key: &[u8], eff: SeqNo) -> bool {
334 let cmp = self.comparator.as_ref();
335 rts.iter().any(|(start, end, rt_eff)| {
336 eff < *rt_eff
337 && cmp.compare(key, start.as_ref()) != core::cmp::Ordering::Less
338 && cmp.compare(key, end.as_ref()) == core::cmp::Ordering::Less
339 })
340 }
341
342 /// Whether the requested key range is fully unbounded, so no per-row range
343 /// filtering is needed (the segment's every row is in range).
344 fn range_is_full(&self) -> bool {
345 matches!(self.lo, Bound::Unbounded) && matches!(self.hi, Bound::Unbounded)
346 }
347
348 /// Rewrites a batch's seqno column from its segment's LOCAL space into the
349 /// tree's global one (`local + global`).
350 ///
351 /// A bulk-ingested segment stores every row at local seqno `0` and carries
352 /// its ordering in a per-segment `global_seqno`, so the stored column is not
353 /// a commit sequence number any other read surface would recognize. The
354 /// masking arithmetic elsewhere translates the THRESHOLD into local space
355 /// instead (cheaper, one subtraction per segment), which is why the column
356 /// itself still needs this before it reaches a caller. A zero offset leaves
357 /// the batch untouched.
358 fn globalize_seqnos(batch: &mut ColumnBatch, global: SeqNo) -> crate::Result<()> {
359 if global == 0 {
360 return Ok(());
361 }
362 let Some(col) = batch.columns.iter_mut().find(|c| c.column_id == COL_SEQNO) else {
363 return Ok(());
364 };
365 // Column bytes are an immutable (possibly shared) view, so the
366 // globalized column is rebuilt into an owned buffer — one allocation
367 // per batch, and only for bulk-ingested segments (`global != 0`).
368 let mut out = alloc::vec::Vec::with_capacity(batch.row_count as usize * 8);
369 for row in 0..batch.row_count as usize {
370 let at = row * 8;
371 let bytes = col
372 .data
373 .get(at..at + 8)
374 .ok_or(Error::InvalidHeader("columnar_scan: short seqno column"))?;
375 let local = u64::from_le_bytes(
376 bytes
377 .try_into()
378 .map_err(|_| Error::InvalidHeader("columnar_scan: short seqno column"))?,
379 );
380 let effective = local.checked_add(global).ok_or(Error::InvalidHeader(
381 "columnar_scan: effective seqno overflows",
382 ))?;
383 out.extend_from_slice(&effective.to_le_bytes());
384 }
385 col.data = crate::Slice::from(out);
386 Ok(())
387 }
388
389 /// Singleton group: no cross-segment merge. When every row is visible and the
390 /// range is unbounded, the per-SST projected scan streams verbatim (zero-copy
391 /// column-skip). Otherwise a per-row mask drops rows that are seqno-invisible
392 /// (when the snapshot straddles the segment) or outside the requested range
393 /// (when the segment only partially overlaps it).
394 fn process_singleton(
395 &self,
396 seg: &Segment,
397 rts: &[(UserKey, UserKey, SeqNo)],
398 ) -> crate::Result<Vec<ColumnBatch>> {
399 // A segment that RECORDS deletions takes the dedup path even when its
400 // keys are provably unique: a key whose single row is a tombstone would
401 // otherwise stream through verbatim and surface a key the point read
402 // calls absent. Deciding a run is where tombstones are consumed, and
403 // that lives there. A visible RANGE tombstone routes there for the
404 // same reason: covered rows must be suppressed, and that needs each
405 // row's seqno, which the verbatim path never decodes.
406 if seg.may_dup
407 || seg.table.tombstone_count() > 0
408 || seg.table.weak_tombstone_count() > 0
409 || !rts.is_empty()
410 {
411 return self.process_singleton_dedup(seg, rts);
412 }
413 let range_filter = !self.range_is_full();
414 if seg.visibility == SeqnoVisibility::All && !range_filter {
415 // The predicate is pushed down in the SST's LOCAL coordinates while
416 // the seqno column is globalized only afterwards. That is sound
417 // today because a `COL_SEQNO` predicate is inert at the SST level:
418 // the zone map omits fixed-width columns (no block-skip entry) and
419 // `matching_rows` treats non-`Bytes` columns as all-matching —
420 // both pinned by tests. Any future comparable encoding for fixed
421 // columns MUST translate seqno bounds by `seg.global` before this
422 // pushdown, or a bulk-ingested segment (rows stored at local
423 // seqnos, returned at global ones) would skip matching blocks.
424 let mut out = seg
425 .table
426 .columnar_scan(&self.projection, self.predicate.as_ref())?;
427 out.retain(|b| b.row_count > 0);
428 for batch in &mut out {
429 Self::globalize_seqnos(batch, seg.global)?;
430 }
431 return Ok(out);
432 }
433
434 // Decode the columns the mask needs even when the caller did not project
435 // them (dropped again at the end): the seqno column for partial-visibility
436 // masking, the key column for range filtering.
437 let partial = seg.visibility == SeqnoVisibility::Partial;
438 let seqno_projected = self.projection.contains(&COL_SEQNO);
439 let key_projected = self.projection.contains(&COL_USER_KEY);
440 let mut augmented = self.projection.clone();
441 if partial && !seqno_projected {
442 augmented.push(COL_SEQNO);
443 }
444 if range_filter && !key_projected {
445 augmented.push(COL_USER_KEY);
446 }
447 // Visible iff `local < threshold` (the snapshot in this segment's local
448 // seqno space); `Partial` guarantees the subtraction is in range.
449 let threshold = self.seqno.saturating_sub(seg.global);
450 let cmp = self.comparator.as_ref();
451
452 let mut out = Vec::new();
453 // Same local-coordinate pushdown note as the verbatim path above.
454 for batch in seg
455 .table
456 .columnar_scan(&augmented, self.predicate.as_ref())?
457 {
458 if batch.row_count == 0 {
459 continue;
460 }
461 let seqno_col = if partial {
462 Some(
463 batch
464 .columns
465 .iter()
466 .find(|c| c.column_id == COL_SEQNO)
467 .ok_or(Error::InvalidHeader(
468 "columnar_scan: partial-visibility batch missing the seqno column",
469 ))?,
470 )
471 } else {
472 None
473 };
474 let key_col = if range_filter {
475 Some(
476 batch
477 .columns
478 .iter()
479 .find(|c| c.column_id == COL_USER_KEY)
480 .ok_or(Error::InvalidHeader(
481 "columnar_scan: range-filtered batch missing the key column",
482 ))?,
483 )
484 } else {
485 None
486 };
487
488 let mut mask = Vec::with_capacity(batch.row_count as usize);
489 for row in 0..batch.row_count {
490 let seqno_ok = match seqno_col {
491 Some(seqno_col) => fixed_u64_row(&seqno_col.data, row)? < threshold,
492 None => true,
493 };
494 // Evaluate the range bound only when the row survived the seqno
495 // gate (short-circuit), so a row's key is decoded only if needed.
496 let keep = if !seqno_ok {
497 false
498 } else if let Some(key_col) = key_col {
499 let key = bytes_column_row(&key_col.data, batch.row_count, row)?;
500 key_in_bounds(key, &self.lo, &self.hi, cmp)
501 } else {
502 true
503 };
504 mask.push(keep);
505 }
506 let mut visible = filter_batch(&batch, &mask);
507 if partial && !seqno_projected {
508 visible.columns.retain(|c| c.column_id != COL_SEQNO);
509 }
510 if range_filter && !key_projected {
511 visible.columns.retain(|c| c.column_id != COL_USER_KEY);
512 }
513 if visible.row_count > 0 {
514 Self::globalize_seqnos(&mut visible, seg.global)?;
515 out.push(visible);
516 }
517 }
518 Ok(out)
519 }
520
521 /// Singleton whose segment can physically hold several MVCC versions of one
522 /// key (`Segment::may_dup`): every version is a physical row, so the scan
523 /// must keep only the newest VISIBLE version per key instead of streaming
524 /// the segment verbatim. Rows are stored in internal-key order (key
525 /// ascending, seqno descending within a key), so within each key run the
526 /// invisible too-new versions come first and the first visible row is the
527 /// newest visible version; a run can span batch boundaries, so the last
528 /// kept key carries across batches. The predicate runs AFTER dedup
529 /// (mirroring [`Self::merge_group`]): a key whose newest version fails the
530 /// predicate is dropped, never served from an older matching version —
531 /// which also rules out predicate-driven zone-map block-skip here.
532 fn process_singleton_dedup(
533 &self,
534 seg: &Segment,
535 rts: &[(UserKey, UserKey, SeqNo)],
536 ) -> crate::Result<Vec<ColumnBatch>> {
537 // Decode the columns the dedup needs even when the caller did not
538 // project them (dropped again at the end): the key column always, the
539 // seqno column when the snapshot straddles the segment OR a range
540 // tombstone needs each row's age, the predicate column for the
541 // after-dedup filter.
542 let key_projected = self.projection.contains(&COL_USER_KEY);
543 let seqno_projected = self.projection.contains(&COL_SEQNO);
544 let partial = seg.visibility == SeqnoVisibility::Partial;
545 let seqno_needed = partial || !rts.is_empty();
546 let mut augmented = self.projection.clone();
547 if !key_projected {
548 augmented.push(COL_USER_KEY);
549 }
550 if seqno_needed && !seqno_projected {
551 augmented.push(COL_SEQNO);
552 }
553 let predicate_col = self.predicate.as_ref().map(|p| p.column_id);
554 let predicate_col_projected = predicate_col.is_some_and(|c| self.projection.contains(&c));
555 if let Some(pc) = predicate_col
556 && !augmented.contains(&pc)
557 {
558 augmented.push(pc);
559 }
560 // A deletion is what a key's newest row can BE, so deciding a run needs
561 // the value type — otherwise a tombstone decides the run and is emitted
562 // as a row while a point read calls the key absent, and a caller that did
563 // not project the type column cannot tell that row from a live one with
564 // an empty value. Decoded only for a segment that RECORDS deletions; one
565 // without them keeps its columns untouched.
566 let deletes = seg.table.tombstone_count() > 0 || seg.table.weak_tombstone_count() > 0;
567 let vt_projected = self.projection.contains(&COL_VALUE_TYPE);
568 if deletes && !vt_projected {
569 augmented.push(COL_VALUE_TYPE);
570 }
571
572 // Visible iff `local < threshold` (the snapshot in this segment's local
573 // seqno space); `Partial` guarantees the subtraction is in range.
574 let threshold = self.seqno.saturating_sub(seg.global);
575 let range_filter = !self.range_is_full();
576 let cmp = self.comparator.as_ref();
577
578 let mut out = Vec::new();
579 // The user key of the last key run whose newest visible version was
580 // already emitted (or deliberately dropped by the range filter) —
581 // owned, because a run can span batch boundaries. One REUSED buffer:
582 // a fresh `to_vec` per run would make unique-key data (the common
583 // case) pay an allocation and free per row.
584 let mut last_key: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
585 let mut have_last = false;
586 for batch in seg.table.columnar_scan(&augmented, None)? {
587 if batch.row_count == 0 {
588 continue;
589 }
590 let key_col = batch
591 .columns
592 .iter()
593 .find(|c| c.column_id == COL_USER_KEY)
594 .ok_or(Error::InvalidHeader(
595 "columnar_scan: dedup batch missing the key column",
596 ))?;
597 let vt_col = if deletes {
598 Some(
599 batch
600 .columns
601 .iter()
602 .find(|c| c.column_id == COL_VALUE_TYPE)
603 .ok_or(Error::InvalidHeader(
604 "columnar_scan: dedup batch missing the value-type column",
605 ))?,
606 )
607 } else {
608 None
609 };
610 let seqno_col = if seqno_needed {
611 Some(
612 batch
613 .columns
614 .iter()
615 .find(|c| c.column_id == COL_SEQNO)
616 .ok_or(Error::InvalidHeader(
617 "columnar_scan: dedup batch missing the seqno column",
618 ))?,
619 )
620 } else {
621 None
622 };
623
624 let mut mask = Vec::with_capacity(batch.row_count as usize);
625 for row in 0..batch.row_count {
626 let local = match seqno_col {
627 Some(seqno_col) => Some(fixed_u64_row(&seqno_col.data, row)?),
628 None => None,
629 };
630 let visible = !partial || local.is_some_and(|l| l < threshold);
631 if !visible {
632 mask.push(false);
633 continue;
634 }
635 let key = bytes_column_row(&key_col.data, batch.row_count, row)?;
636 if have_last && cmp.compare(&last_key, key) == core::cmp::Ordering::Equal {
637 // A later visible version of an already-decided key run —
638 // shadowed by the newest visible version above it.
639 mask.push(false);
640 continue;
641 }
642 // First visible row of a new key run = the newest visible
643 // version. Deciding the run here (even when the range filter or
644 // a deletion drops the row) also drops its older versions above.
645 last_key.clear();
646 last_key.extend_from_slice(key);
647 have_last = true;
648 // A visible range tombstone deletes the run when it covers the
649 // NEWEST visible version (older versions are older still); an
650 // uncovered newest version shadows the covered older ones, so
651 // deciding on it alone is exact.
652 if !rts.is_empty() {
653 let eff =
654 local
655 .unwrap_or(0)
656 .checked_add(seg.global)
657 .ok_or(Error::InvalidHeader(
658 "columnar_scan: effective seqno overflows",
659 ))?;
660 if self.rt_covered(rts, key, eff) {
661 mask.push(false);
662 continue;
663 }
664 }
665 if let Some(vt_col) = vt_col {
666 let byte = *vt_col.data.get(row as usize).ok_or(Error::InvalidHeader(
667 "columnar_scan: value-type column shorter than the row count",
668 ))?;
669 let value_type = crate::ValueType::try_from(byte)
670 .map_err(|()| Error::InvalidTag(("ValueType", byte)))?;
671 if value_type.is_tombstone() {
672 // The key is GONE as of this row, so the run yields
673 // nothing: emitting the tombstone would surface a key the
674 // point read reports absent.
675 mask.push(false);
676 continue;
677 }
678 }
679 mask.push(!range_filter || key_in_bounds(key, &self.lo, &self.hi, cmp));
680 }
681
682 let mut visible = filter_batch(&batch, &mask);
683 // The predicate runs on the deduped survivors only (see doc).
684 if let Some(pred) = self.predicate.as_ref() {
685 let pred_mask = pred.matching_rows(&visible);
686 visible = filter_batch(&visible, &pred_mask);
687 }
688 // Match the singleton contract: yield exactly the projected columns.
689 if !key_projected {
690 visible.columns.retain(|c| c.column_id != COL_USER_KEY);
691 }
692 if !seqno_projected {
693 visible.columns.retain(|c| c.column_id != COL_SEQNO);
694 }
695 if deletes && !vt_projected {
696 visible.columns.retain(|c| c.column_id != COL_VALUE_TYPE);
697 }
698 if let Some(pc) = predicate_col
699 && !predicate_col_projected
700 {
701 visible.columns.retain(|c| c.column_id != pc);
702 }
703 if visible.row_count > 0 {
704 Self::globalize_seqnos(&mut visible, seg.global)?;
705 out.push(visible);
706 }
707 }
708 Ok(out)
709 }
710
711 /// Row-merges an overlapping segment group: over the union of the segments'
712 /// visible projected rows, keep the newest version of each key (highest
713 /// effective seqno), gathered in key order.
714 fn merge_group(
715 &self,
716 group: &Group,
717 rts: &[(UserKey, UserKey, SeqNo)],
718 ) -> crate::Result<Vec<ColumnBatch>> {
719 // The merge needs each row's key and effective seqno, so decode the
720 // intrinsic key + seqno columns even when the caller did not project them
721 // (dropped again at the end).
722 let key_projected = self.projection.contains(&COL_USER_KEY);
723 let seqno_projected = self.projection.contains(&COL_SEQNO);
724 let mut augmented = self.projection.clone();
725 if !key_projected {
726 augmented.push(COL_USER_KEY);
727 }
728 if !seqno_projected {
729 augmented.push(COL_SEQNO);
730 }
731 // The predicate is applied AFTER newest-version dedup (below), so its
732 // column must be decoded here even when the caller did not project it.
733 let predicate_col = self.predicate.as_ref().map(|p| p.column_id);
734 let predicate_col_projected = predicate_col.is_some_and(|c| self.projection.contains(&c));
735 if let Some(pc) = predicate_col
736 && !augmented.contains(&pc)
737 {
738 augmented.push(pc);
739 }
740 // Same rule as the singleton path: the newest version of a key can BE a
741 // deletion, and then the key yields nothing. Decoded only when a segment
742 // of this group records deletions.
743 let deletes = group
744 .segments
745 .iter()
746 .any(|s| s.table.tombstone_count() > 0 || s.table.weak_tombstone_count() > 0);
747 let vt_projected = self.projection.contains(&COL_VALUE_TYPE);
748 if deletes && !vt_projected {
749 augmented.push(COL_VALUE_TYPE);
750 }
751
752 // Concatenate every segment's visible rows into one batch, tracking each
753 // surviving row's effective seqno (`local + global`) — and its source
754 // recency rank — in lockstep so the dedup can compare versions across
755 // segments with different bases and break equal-seqno ties the way the
756 // read path does (newer source wins).
757 let mut combined: Option<ColumnBatch> = None;
758 let mut effective: Vec<SeqNo> = Vec::new();
759 let mut source_rank: Vec<usize> = Vec::new();
760 for seg in &group.segments {
761 let threshold = self.seqno.saturating_sub(seg.global);
762 // No predicate here: in an overlap group the predicate must run after
763 // newest-version dedup, so every version (including a newest one that
764 // fails the predicate but shadows an older matching version) has to be
765 // collected first. Predicate-driven zone-map block-skip is likewise
766 // unsafe here for the same reason, so it is also dropped.
767 for batch in seg.table.columnar_scan(&augmented, None)? {
768 if batch.row_count == 0 {
769 continue;
770 }
771 let seqno_col = batch
772 .columns
773 .iter()
774 .find(|c| c.column_id == COL_SEQNO)
775 .ok_or(Error::InvalidHeader(
776 "columnar_scan: merged group missing the seqno column",
777 ))?;
778 let mut mask = Vec::with_capacity(batch.row_count as usize);
779 for row in 0..batch.row_count {
780 let local = fixed_u64_row(&seqno_col.data, row)?;
781 let visible = seg.visibility == SeqnoVisibility::All || local < threshold;
782 mask.push(visible);
783 if visible {
784 // Translate to the global coordinate for cross-segment
785 // comparison; a visible row cannot overflow (its effective
786 // seqno is `< snapshot <= SeqNo::MAX`).
787 let eff = local.checked_add(seg.global).ok_or(Error::InvalidHeader(
788 "columnar_scan: effective seqno overflow",
789 ))?;
790 effective.push(eff);
791 source_rank.push(seg.recency_rank);
792 }
793 }
794 let visible = filter_batch(&batch, &mask);
795 if visible.row_count == 0 {
796 continue;
797 }
798 match &mut combined {
799 Some(acc) => acc.append(&visible)?,
800 None => combined = Some(visible),
801 }
802 }
803 }
804 let Some(combined) = combined else {
805 return Ok(Vec::new());
806 };
807
808 // Extract every row's key once (fallible framing read), then sort indices
809 // by (key asc, effective seqno desc) and keep the first per key.
810 let key_col = combined
811 .columns
812 .iter()
813 .find(|c| c.column_id == COL_USER_KEY)
814 .ok_or(Error::InvalidHeader(
815 "columnar_scan: merged group missing the key column",
816 ))?;
817 if key_col.type_tag != TypeTag::Bytes {
818 return Err(Error::InvalidHeader(
819 "columnar_scan: key column is not a bytes column",
820 ));
821 }
822 let rows = combined.row_count;
823 debug_assert_eq!(rows as usize, effective.len(), "seqno tracked per row");
824 debug_assert_eq!(rows as usize, source_rank.len(), "rank tracked per row");
825 let mut keys: Vec<&[u8]> = Vec::with_capacity(rows as usize);
826 for i in 0..rows {
827 keys.push(bytes_column_row(&key_col.data, rows, i)?);
828 }
829
830 // Indices are always in range (`0..rows`, and `keys` / `effective` both
831 // have `rows` entries), so the `get` defaults below are never taken; they
832 // only satisfy the no-panic-indexing lint.
833 let key_at = |i: u32| keys.get(i as usize).copied().unwrap_or(&[]);
834 let eff_at = |i: u32| effective.get(i as usize).copied().unwrap_or(0);
835 let rank_at = |i: u32| source_rank.get(i as usize).copied().unwrap_or(usize::MAX);
836 let cmp = self.comparator.as_ref();
837 let mut order: Vec<u32> = (0..rows).collect();
838 // (key asc, effective seqno desc, source recency asc): a caller can
839 // reuse one seqno across separately flushed overlapping segments with
840 // DIFFERENT values, and the read path serves the newer run's value —
841 // the rank tie-break makes the dedup below pick the same winner
842 // (combined order alone reflects `group_by_overlap`'s min-key sort,
843 // not recency).
844 order.sort_by(|&a, &b| {
845 cmp.compare(key_at(a), key_at(b))
846 .then_with(|| eff_at(b).cmp(&eff_at(a)))
847 .then_with(|| rank_at(a).cmp(&rank_at(b)))
848 });
849
850 // Keep the first index of each distinct key (highest effective seqno);
851 // drop the shadowed older duplicates and any key outside the requested
852 // range (a segment may only partially overlap it).
853 let range_filter = !self.range_is_full();
854 let vt_col = if deletes {
855 Some(
856 combined
857 .columns
858 .iter()
859 .find(|c| c.column_id == COL_VALUE_TYPE)
860 .ok_or(Error::InvalidHeader(
861 "columnar_scan: merged group missing the value-type column",
862 ))?,
863 )
864 } else {
865 None
866 };
867 let mut kept: Vec<u32> = Vec::with_capacity(order.len());
868 let mut prev: Option<&[u8]> = None;
869 for &i in &order {
870 let key = key_at(i);
871 if let Some(p) = prev
872 && cmp.compare(p, key) == core::cmp::Ordering::Equal
873 {
874 continue;
875 }
876 prev = Some(key);
877 if range_filter && !key_in_bounds(key, &self.lo, &self.hi, cmp) {
878 continue;
879 }
880 if let Some(vt_col) = vt_col {
881 let byte = *vt_col.data.get(i as usize).ok_or(Error::InvalidHeader(
882 "columnar_scan: value-type column shorter than the row count",
883 ))?;
884 let value_type = crate::ValueType::try_from(byte)
885 .map_err(|()| Error::InvalidTag(("ValueType", byte)))?;
886 // The newest version deletes the key, so the key yields nothing —
887 // the run is already decided, so the older versions stay dropped.
888 if value_type.is_tombstone() {
889 continue;
890 }
891 }
892 // A visible range tombstone covering the newest visible version
893 // deletes the key (older versions are older still); an uncovered
894 // newest version shadows the covered older ones.
895 if self.rt_covered(rts, key, eff_at(i)) {
896 continue;
897 }
898 kept.push(i);
899 }
900
901 let mut merged = take_rows(&combined, &kept)?;
902
903 // The union spans segments with DIFFERENT offsets, so no single one
904 // applies: write each surviving row's effective seqno — already computed
905 // for the dedup above — into the column, in the tree's global
906 // coordinates. Done before the predicate filter, while row `i` of
907 // `merged` still corresponds to `kept[i]`.
908 if let Some(col) = merged.columns.iter_mut().find(|c| c.column_id == COL_SEQNO) {
909 // Column bytes are an immutable (possibly shared) view — rebuild
910 // the globalized column into an owned buffer (one per merged batch
911 // on this multi-segment path).
912 let mut out = alloc::vec::Vec::with_capacity(kept.len() * 8);
913 for &i in &kept {
914 out.extend_from_slice(&eff_at(i).to_le_bytes());
915 }
916 if out.len() != col.data.len() {
917 return Err(Error::InvalidHeader("columnar_scan: short seqno column"));
918 }
919 col.data = crate::Slice::from(out);
920 }
921
922 // Apply the row predicate AFTER newest-version dedup: each surviving row is
923 // now the newest visible version of its key, so a key whose newest version
924 // fails the predicate is correctly dropped instead of falling back to an
925 // older matching version.
926 if let Some(pred) = self.predicate.as_ref() {
927 let mask = pred.matching_rows(&merged);
928 merged = filter_batch(&merged, &mask);
929 }
930
931 // Match the singleton contract: yield exactly the projected columns.
932 if !key_projected {
933 merged.columns.retain(|c| c.column_id != COL_USER_KEY);
934 }
935 if !seqno_projected {
936 merged.columns.retain(|c| c.column_id != COL_SEQNO);
937 }
938 if deletes && !vt_projected {
939 merged.columns.retain(|c| c.column_id != COL_VALUE_TYPE);
940 }
941 if let Some(pc) = predicate_col
942 && !predicate_col_projected
943 {
944 merged.columns.retain(|c| c.column_id != pc);
945 }
946 if merged.row_count == 0 {
947 return Ok(Vec::new());
948 }
949 Ok(vec![merged])
950 }
951}
952
953/// Whether `key` lies within the requested `[lo, hi]` key bounds, per the tree
954/// comparator. An unbounded side never excludes; the inclusive / exclusive sense
955/// of each bound matches the `RangeBounds` the caller passed.
956fn key_in_bounds(
957 key: &[u8],
958 lo: &Bound<UserKey>,
959 hi: &Bound<UserKey>,
960 cmp: &dyn UserComparator,
961) -> bool {
962 use core::cmp::Ordering;
963 let above_lo = match lo {
964 Bound::Unbounded => true,
965 Bound::Included(k) => cmp.compare(key, k.as_ref()) != Ordering::Less,
966 Bound::Excluded(k) => cmp.compare(key, k.as_ref()) == Ordering::Greater,
967 };
968 let below_hi = match hi {
969 Bound::Unbounded => true,
970 Bound::Included(k) => cmp.compare(key, k.as_ref()) != Ordering::Greater,
971 Bound::Excluded(k) => cmp.compare(key, k.as_ref()) == Ordering::Less,
972 };
973 above_lo && below_hi
974}
975
976impl Iterator for ColumnarScan {
977 type Item = crate::Result<ColumnBatch>;
978
979 fn next(&mut self) -> Option<Self::Item> {
980 loop {
981 if let Some(batch) = self.buffered.pop_front() {
982 return Some(Ok(batch));
983 }
984 let group = self.groups.pop_front()?;
985 match self.process_group(&group) {
986 Ok(batches) => self.buffered.extend(batches),
987 Err(e) => return Some(Err(e)),
988 }
989 }
990 }
991}
992
993/// Clones a borrowed key bound into an owned one.
994fn clone_bound(bound: Bound<&UserKey>) -> Bound<UserKey> {
995 match bound {
996 Bound::Included(k) => Bound::Included(k.clone()),
997 Bound::Excluded(k) => Bound::Excluded(k.clone()),
998 Bound::Unbounded => Bound::Unbounded,
999 }
1000}
1001
1002/// Borrows an owned key bound as a byte-slice bound for key-range overlap checks.
1003fn bound_as_ref(bound: &Bound<UserKey>) -> Bound<&[u8]> {
1004 match bound {
1005 Bound::Included(k) => Bound::Included(k.as_ref()),
1006 Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
1007 Bound::Unbounded => Bound::Unbounded,
1008 }
1009}