Skip to main content

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}