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 merged as it streams: the projection is
37//!   augmented with the intrinsic key + seqno columns, each segment is a source
38//!   read one row group at a time, and the merge takes the least row across the
39//!   sources by `(key asc, effective seqno desc, source recency asc)`, keeping
40//!   the newest visible row of each key. The key/seqno decode and the gather
41//!   are paid only where segments overlap, and what the merge holds is one
42//!   batch per source, not the group.
43//!
44//! Groups are emitted in ascending key order, so the scan yields projected
45//! [`ColumnBatch`]es in global key order. This mirrors how `InfluxDB` `IOx`
46//! inserts its deduplication operator only over overlapping files and engineers
47//! compaction to keep files non-overlapping: as multi-segment columnar compaction
48//! reduces overlap, more of the scan takes the zero-cost singleton path.
49//!
50//! Deletes reach the scan two ways and both remove the key. A segment's
51//! positional delete-bitmap is applied inside
52//! [`Table::columnar_scan`](crate::Table::columnar_scan); a value-type TOMBSTONE
53//! is consumed here, when the newest visible version of a key is one — the key
54//! then yields no row at all, matching what a point read reports, instead of
55//! surfacing a row a caller who did not project the value-type column could not
56//! tell from a live one. Only a segment that RECORDS deletions pays for it: one
57//! whose metadata counts none keeps its columns untouched (and its zero-copy
58//! verbatim path).
59//!
60//! # Row sources
61//!
62//! The memtables and the row-oriented tables in the range are sources too, so
63//! fresh writes are seen before they reach a columnar table. Each is read at
64//! the snapshot into batches of its keys, versions, value types and whole
65//! values, and always merged, since it holds every version of its keys. A
66//! merge operand row, in a tree with a merge operator, is replaced by what a
67//! point read at the snapshot returns for its key, so a chain reads as the read
68//! path resolves it.
69//!
70//! # Projected fields
71//!
72//! What a batch carries is a [`Projection`](projection::Projection): columns by
73//! id, and declared fields with a type and what their absence reads as. A table
74//! that stores each value whole (rows transposed at a flush or a compaction,
75//! and every row source) yields its declared fields through the projection's
76//! [`ValueProjector`](projection::ValueProjector); a table that stores the
77//! value split into fields yields them as its columns, and a field it lacks is
78//! absent. See [`projection`] for the absence rule every source follows.
79//!
80//! # Blob trees
81//!
82//! The index of a blob tree holds rows written as cells, whose fields sit
83//! inline or in blob files, and whole values, inline or in a blob file. Every
84//! source of it is merged and its rows decided on their keys, seqnos and value
85//! types; only the rows the scan returns have their fields read, and a blob
86//! object only when a projected field needs it. See [`cells`].
87
88use core::ops::{Bound, RangeBounds};
89
90use alloc::{boxed::Box, vec, vec::Vec};
91
92use alloc::borrow::Cow;
93
94use crate::comparator::UserComparator;
95use crate::table::SeqnoVisibility;
96use crate::table::columnar::{
97    COL_SEQNO, COL_USER_KEY, COL_VALUE_TYPE, ColumnBatch, Number, TypeTag, bytes_column_row,
98    fixed_u64_row,
99};
100use crate::table::columnar_predicate::{
101    ColumnRangePredicate, PredicateApply, PredicateSupport, filter_batch, take_column,
102    take_validity,
103};
104use crate::{Error, SeqNo, Table, Tree, UserKey};
105
106mod cells;
107mod merge;
108pub(crate) mod projection;
109mod rows;
110
111use rows::{RowCursor, SourceCursor};
112
113/// What a scan source reads: a columnar segment, or a source of rows.
114enum Source {
115    /// A columnar table, read column by column.
116    Columnar(Table),
117    /// A row-oriented table, read row by row.
118    RowTable(Table),
119    /// A memtable, active or sealed.
120    Memtable(alloc::sync::Arc<crate::memtable::Memtable>),
121}
122
123/// A visible source selected for the scan, with its cached key range,
124/// sequence base, and snapshot-visibility class.
125struct Segment {
126    source: Source,
127    min: UserKey,
128    max: UserKey,
129    /// The segment's `global_seqno` base; a row's effective seqno is
130    /// `local + global`.
131    global: SeqNo,
132    /// Whether every row is visible at the snapshot, or visibility is per-row.
133    visibility: SeqnoVisibility,
134    /// Whether this segment can physically hold several MVCC versions of one
135    /// key (a flush / compaction product with an overwritten key). Proven
136    /// unique only when the writer's distinct-key count equals the row count;
137    /// legacy tables without the count are conservatively assumed to carry
138    /// duplicates. Gates the singleton path's per-key newest-visible dedup.
139    may_dup: bool,
140    /// Source recency: the segment's position in the version's newest-first
141    /// table order (lower = newer). Two segments can hold DIFFERENT values
142    /// for one key at one caller-assigned seqno, and the read path serves
143    /// the newer run's value — the merge path breaks the tie with this rank,
144    /// because `group_by_overlap` re-sorts segments by minimum key and the
145    /// concatenation order alone says nothing about recency.
146    recency_rank: usize,
147    /// Whether the segment stores each value whole, so its declared fields
148    /// are read out of the value through the projector.
149    whole: bool,
150    /// Whether the segment is a blob tree's columnar table, which splits rows
151    /// written as cells into the columns of their fields and keeps every
152    /// other value whole beside them.
153    cells: bool,
154}
155
156impl Segment {
157    /// Whether the source is read row by row.
158    fn is_rows(&self) -> bool {
159        !matches!(self.source, Source::Columnar(_))
160    }
161
162    /// Whether the source can hold a deletion, so the value type is decoded.
163    fn records_deletions(&self) -> bool {
164        match &self.source {
165            Source::Columnar(table) | Source::RowTable(table) => {
166                table.tombstone_count() > 0 || table.weak_tombstone_count() > 0
167            }
168            // A memtable keeps no count; any row of it can be a deletion.
169            Source::Memtable(_) => true,
170        }
171    }
172
173    /// The base the source's range tombstone seqnos are local to. A row
174    /// table's range read already yields effective row seqnos, so its rows
175    /// are merged at base `0`, but its tombstones are stored local.
176    fn rt_base(&self) -> SeqNo {
177        match &self.source {
178            Source::Columnar(_) => self.global,
179            Source::RowTable(table) => table.global_seqno(),
180            Source::Memtable(_) => 0,
181        }
182    }
183
184    /// The source's range tombstones, in its local seqno space.
185    fn range_tombstones(&self) -> Vec<crate::range_tombstone::RangeTombstone> {
186        match &self.source {
187            Source::Columnar(table) | Source::RowTable(table) => table
188                .visible_range_tombstones()
189                .map(alloc::borrow::Cow::into_owned)
190                .collect(),
191            Source::Memtable(memtable) => memtable.range_tombstones_sorted(),
192        }
193    }
194}
195
196/// One key-disjoint group of segments: either a single segment (streamed
197/// verbatim) or several whose key ranges transitively overlap (row-merged).
198struct Group {
199    segments: Vec<Segment>,
200    /// Running maximum key of the group's span, used while grouping.
201    max: UserKey,
202}
203
204impl Tree {
205    /// Runs a projected columnar scan across the whole tree.
206    ///
207    /// Reads every source intersecting `range` at snapshot `seqno`: the
208    /// memtables, the row-oriented tables and the columnar ones. It applies
209    /// deletions and range tombstones from any source to the keys of every
210    /// other, resolves merge chains as a read does, applies the optional
211    /// `predicate` (zone-map block-skip + row filter, after the newest version
212    /// of each key is chosen) and yields projected [`ColumnBatch`]es in
213    /// ascending key order, one row per key: its newest visible version. A
214    /// columnar segment no other source overlaps streams without merge
215    /// overhead.
216    ///
217    /// `range` bounds the result at row granularity: a segment that only
218    /// partially overlaps `range` contributes only the rows whose keys fall
219    /// inside it (the inclusive / exclusive sense of each bound is honored). A
220    /// fully unbounded range keeps the zero-copy fast path for an all-visible
221    /// segment.
222    ///
223    /// `projection` names the columns each batch carries, in its order: column
224    /// ids (the intrinsic [`COL_USER_KEY`] / seqno / value-type columns, or a
225    /// value column typed by what the tables store), converted from a slice of
226    /// ids, or a [`Projection`](projection::Projection) of declared fields,
227    /// which says what a field a row lacks reads as and carries the projector
228    /// the fields are read out of whole values through. Every other column of a
229    /// columnar table is stepped over without decoding.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`Error::Projection`] when the projection names one column id
234    /// twice or the highest id, and, lazily, while iterating, when a row
235    /// returned stores its value whole and no projector is set to read its
236    /// declared fields, or when the data does not satisfy the projection.
237    /// Returns an error on a block read
238    /// or decode failure, or on a failed point read resolving a merge chain.
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use lsm_tree::table::columnar::COL_USER_KEY;
244    /// use lsm_tree::{AbstractTree, Config, SeqNo};
245    ///
246    /// let folder = tempfile::tempdir()?;
247    /// let tree = Config::new(folder, Default::default(), Default::default()).open()?;
248    /// // Rows in the memtable are read like any other source.
249    /// tree.insert("a", "1", 0);
250    /// tree.insert("b", "2", 1);
251    /// let rows: u32 = tree
252    ///     .columnar_scan(&[COL_USER_KEY], None, SeqNo::MAX, ..)?
253    ///     .map(|batch| batch.map(|b| b.row_count))
254    ///     .sum::<lsm_tree::Result<u32>>()?;
255    /// assert_eq!(rows, 2);
256    /// # Ok::<(), lsm_tree::Error>(())
257    /// ```
258    pub fn columnar_scan<R: RangeBounds<UserKey>>(
259        &self,
260        projection: impl Into<projection::Projection>,
261        predicate: Option<&ColumnRangePredicate>,
262        seqno: SeqNo,
263        range: R,
264    ) -> crate::Result<ColumnarScan> {
265        let projection = projection.into();
266        // A batch names its columns by id, so one id projected twice (say a
267        // declared field and the raw value by id) would come out as two
268        // columns no reader can tell apart.
269        let fields = projection.fields();
270        if fields.iter().enumerate().any(|(at, f)| {
271            fields
272                .get(..at)
273                .is_some_and(|before| before.iter().any(|b| b.column_id() == f.column_id()))
274        }) {
275            return Err(Error::Projection(
276                "projection: a column id is projected twice",
277            ));
278        }
279        if fields
280            .iter()
281            .any(|f| f.column_id() == merge::COL_WHOLE_VALUE)
282            || predicate.is_some_and(|p| p.column_id == merge::COL_WHOLE_VALUE)
283        {
284            return Err(Error::Projection(
285                "projection: the highest column id is reserved for the scan",
286            ));
287        }
288        // A merge chain is not a version chain: its older rows are the merge's
289        // INPUTS, not data the newest row shadows, and a read hands back the
290        // merged value. An operand row is therefore replaced by what a read at
291        // the snapshot returns for its key, through the same operator, before
292        // anything is projected from it. Without an operator the read path
293        // returns the newest entry unchanged, the raw operand, which is what
294        // the scan yields, so no resolution runs and no value type is decoded.
295        let comparator = self.config.comparator.clone();
296
297        // Owned bounds keep the returned iterator free of borrows from `range`.
298        let lo = clone_bound(range.start_bound());
299        let hi = clone_bound(range.end_bound());
300        let bounds_ref = (bound_as_ref(&lo), bound_as_ref(&hi));
301
302        // The segment list below is built from the one version a read at
303        // `seqno` resolves to, held for the whole scan: a compaction installing
304        // mid-scan cannot turn a row-major tree columnar (or the reverse)
305        // underneath it, and the per-segment recency ranking is the ranking
306        // THAT version has.
307        //
308        // The active memtable in that version stays writable, and its span is
309        // taken now while its rows are read later: the snapshot is capped at
310        // what the tree holds now, so a write or deletion that lands after the
311        // scan is created is invisible to it, as it is outside the span and
312        // the groups computed here. The version is the one the requested
313        // snapshot is served by: a compaction can raise the retention floor
314        // past every seqno the tree still holds, and a snapshot at or below the
315        // floor is refused because it may need a version the compaction
316        // collected. The cap never needs one: it lies above every seqno the
317        // version holds, so it reads what the latest snapshot reads. It is
318        // taken from that version, not the live tree: a compaction installed
319        // since may have dropped the newest seqno, and a cap below a version
320        // the pinned tables still hold would surface an older one.
321        let super_version = self.get_version_for_snapshot(seqno)?;
322        // A projected scan is a scan of the read mix, as a row-major one is.
323        super_version.version.read_mix().note_scan();
324        let highest = super_version
325            .active_memtable
326            .get_highest_seqno()
327            .max(
328                super_version
329                    .sealed_memtables
330                    .iter()
331                    .filter_map(|memtable| memtable.get_highest_seqno())
332                    .max(),
333            )
334            .max(
335                super_version
336                    .version
337                    .iter_tables()
338                    .map(crate::Table::get_highest_seqno)
339                    .max(),
340            );
341        let seqno = match highest {
342            Some(highest) => highest
343                .checked_add(1)
344                .map_or(seqno, |next: SeqNo| seqno.min(next)),
345            None => 0,
346        };
347        // One blob source for the whole scan: a merge's base and a projected
348        // value are read under the one fill policy the scan began with.
349        let blob_source = self.blob_source();
350        // Operands are resolved in that same version, so a version installed
351        // mid-scan changes no row the scan returns.
352        let resolver = self.config.merge_operator.clone().map(|operator| Resolver {
353            version: super_version.clone(),
354            operator,
355            blob_source: blob_source.clone(),
356        });
357        // The index of a blob tree: its rows' fields are read by the engine,
358        // out of rows written as cells and the objects they reference, and
359        // out of whole values through the projector, a whole value kept in a
360        // blob file read first. Every value column is a field: one projected
361        // by id alone, or a predicate on one no field declares, would read a
362        // stored representation (a cell row, an indirection) rather than a
363        // value, so a field is declared with its type.
364        let cells = blob_source.map(|source| cells::CellSource {
365            version: super_version.clone(),
366            source,
367        });
368        if cells.is_some() {
369            let intrinsic = |id: u16| matches!(id, COL_USER_KEY | COL_SEQNO | COL_VALUE_TYPE);
370            let loose = |id: u16| {
371                !intrinsic(id)
372                    && fields
373                        .iter()
374                        .all(|f| f.column_id() != id || !projection::is_declared(f))
375            };
376            if fields.iter().any(|f| loose(f.column_id()))
377                || predicate.is_some_and(|p| loose(p.column_id))
378            {
379                return Err(Error::Projection(
380                    "projection: a blob tree's fields are projected as declared fields",
381                ));
382            }
383        }
384
385        // A declared field of a segment that stores each value whole lies
386        // inside the value, which only the caller's projector reads, and only
387        // for a row the scan returns.
388        let declared = projection.fields().iter().any(projection::is_declared);
389
390        let mut segments: Vec<Segment> = Vec::new();
391        // Memtables are newer than every table, the active one newest; the
392        // sealed ones are kept oldest first.
393        let memtables = core::iter::once(super_version.active_memtable.clone())
394            .chain(super_version.sealed_memtables.iter().rev().cloned());
395        let mut recency_rank = 0;
396        for memtable in memtables {
397            if let Some((min, max)) = memtable_span(&memtable, &lo, &hi, comparator.as_ref()) {
398                segments.push(Segment {
399                    min,
400                    max,
401                    // A row source yields effective seqnos, and only the rows
402                    // the snapshot sees.
403                    global: 0,
404                    visibility: SeqnoVisibility::All,
405                    may_dup: true,
406                    recency_rank,
407                    whole: true,
408                    cells: false,
409                    source: Source::Memtable(memtable),
410                });
411            }
412            recency_rank += 1;
413        }
414        // `iter_tables` yields newest-first (the same order the sequenced
415        // scan sources rely on), so the enumeration index is the recency
416        // rank after the memtables'.
417        for (rank, table) in super_version.version.iter_tables().enumerate() {
418            let recency_rank = recency_rank + rank;
419            if !table.check_key_range_overlap_cmp(&bounds_ref, comparator.as_ref()) {
420                continue;
421            }
422            // Snapshot visibility (exclusive MVCC). `None` segments postdate the
423            // snapshot and are dropped.
424            let visibility = table.seqno_visibility(seqno);
425            if visibility == SeqnoVisibility::None {
426                continue;
427            }
428            let key_range = &table.metadata.key_range;
429            if !table.metadata.columnar {
430                segments.push(Segment {
431                    min: key_range.min().clone(),
432                    max: key_range.max().clone(),
433                    // Its range read yields effective seqnos, filtered to the
434                    // snapshot by the row cursor.
435                    global: 0,
436                    visibility: SeqnoVisibility::All,
437                    may_dup: true,
438                    recency_rank,
439                    whole: true,
440                    cells: false,
441                    source: Source::RowTable(table.clone()),
442                });
443                continue;
444            }
445            // `key_count == item_count` proves the segment holds one version per
446            // key, so the verbatim path can return its rows untouched. The count
447            // the writer recorded and the duplicate-free claim read here rest on
448            // the SAME identity relation the read path uses to collapse versions
449            // (`comparator::same_user_key`), so a segment this calls unique is
450            // one a normal read would also return whole. `None` (a legacy
451            // segment that recorded no count) proves nothing and dedups.
452            let may_dup = table
453                .metadata
454                .key_count
455                .is_none_or(|k| k != table.metadata.item_count);
456            segments.push(Segment {
457                min: key_range.min().clone(),
458                max: key_range.max().clone(),
459                global: table.global_seqno(),
460                visibility,
461                may_dup,
462                recency_rank,
463                whole: table.metadata.value_layout == crate::table::meta::ValueLayout::Whole,
464                cells: table.metadata.value_layout == crate::table::meta::ValueLayout::Cells,
465                source: Source::Columnar(table.clone()),
466            });
467        }
468
469        let groups = group_by_overlap(segments, comparator.as_ref());
470
471        Ok(ColumnarScan {
472            groups: groups.into_iter().collect(),
473            current: None,
474            projection: projection.column_ids(),
475            fields: projection.fields().to_vec(),
476            projector: declared
477                .then(|| projection.value_projector().cloned())
478                .flatten(),
479            declared,
480            resolver,
481            cells,
482            predicate: predicate.cloned(),
483            support: PredicateSupport::Exact,
484            comparator,
485            seqno,
486            lo,
487            hi,
488            budget: self.config.columnar_scan_budget,
489            peak_payload: core::cell::Cell::new(0),
490            oversized: core::cell::Cell::new(0),
491            #[cfg(feature = "metrics")]
492            metrics: self.0.metrics.clone(),
493        })
494    }
495}
496
497/// The span of keys `memtable` holds in `lo..hi`, rows and range tombstones
498/// alike, or `None` when it holds none there.
499fn memtable_span(
500    memtable: &crate::memtable::Memtable,
501    lo: &Bound<UserKey>,
502    hi: &Bound<UserKey>,
503    cmp: &dyn UserComparator,
504) -> Option<(UserKey, UserKey)> {
505    use core::cmp::Ordering;
506
507    let (ilo, ihi) = crate::range::internal_bounds(lo, hi);
508    let mut rows = memtable.range_internal((ilo, ihi));
509    let first = rows.next().map(|row| row.key.user_key);
510    let last = rows
511        .next_back()
512        .map(|row| row.key.user_key)
513        .or_else(|| first.clone());
514    let mut span = first.zip(last);
515    let bounds = (lo.clone(), hi.clone());
516    for rt in memtable.range_tombstones_sorted() {
517        if !crate::range::range_tombstone_overlaps_bounds(&rt, &bounds, cmp) {
518            continue;
519        }
520        span = Some(match span {
521            None => (rt.start.clone(), rt.end.clone()),
522            Some((min, max)) => (
523                if cmp.compare(&rt.start, &min) == Ordering::Less {
524                    rt.start.clone()
525                } else {
526                    min
527                },
528                if cmp.compare(&rt.end, &max) == Ordering::Greater {
529                    rt.end.clone()
530                } else {
531                    max
532                },
533            ),
534        });
535    }
536    span
537}
538
539/// The tighter of two lower bounds: the greater key, an excluded one when
540/// both name the same key.
541fn tighter_lower(
542    a: Bound<UserKey>,
543    b: &Bound<UserKey>,
544    cmp: &dyn UserComparator,
545) -> Bound<UserKey> {
546    use core::cmp::Ordering;
547
548    match (&a, b) {
549        (_, Bound::Unbounded) => a,
550        (Bound::Unbounded, _) => b.clone(),
551        (Bound::Included(x) | Bound::Excluded(x), Bound::Included(y) | Bound::Excluded(y)) => {
552            match cmp.compare(x, y) {
553                Ordering::Less => b.clone(),
554                Ordering::Equal if matches!(b, Bound::Excluded(_)) => b.clone(),
555                Ordering::Greater | Ordering::Equal => a,
556            }
557        }
558    }
559}
560
561/// The tighter of two upper bounds: the lesser key, an excluded one when
562/// both name the same key.
563fn tighter_upper(
564    a: Bound<UserKey>,
565    b: &Bound<UserKey>,
566    cmp: &dyn UserComparator,
567) -> Bound<UserKey> {
568    use core::cmp::Ordering;
569
570    match (&a, b) {
571        (_, Bound::Unbounded) => a,
572        (Bound::Unbounded, _) => b.clone(),
573        (Bound::Included(x) | Bound::Excluded(x), Bound::Included(y) | Bound::Excluded(y)) => {
574            match cmp.compare(x, y) {
575                Ordering::Greater => b.clone(),
576                Ordering::Equal if matches!(b, Bound::Excluded(_)) => b.clone(),
577                Ordering::Less | Ordering::Equal => a,
578            }
579        }
580    }
581}
582
583/// Partitions `segments` into key-disjoint overlap groups, ordered by ascending
584/// minimum key. Segments are sorted by their minimum key, then greedily extended
585/// into the current group while the next segment's minimum key is `<=` the
586/// group's running maximum (an inclusive-range overlap). The result preserves
587/// global key order across groups: group `i`'s span lies entirely below group
588/// `i + 1`'s.
589fn group_by_overlap(mut segments: Vec<Segment>, cmp: &dyn UserComparator) -> Vec<Group> {
590    use core::cmp::Ordering;
591
592    segments.sort_by(|a, b| cmp.compare(a.min.as_ref(), b.min.as_ref()));
593
594    let mut groups: Vec<Group> = Vec::new();
595    for seg in segments {
596        match groups.last_mut() {
597            Some(g) if cmp.compare(seg.min.as_ref(), g.max.as_ref()) != Ordering::Greater => {
598                if cmp.compare(seg.max.as_ref(), g.max.as_ref()) == Ordering::Greater {
599                    g.max = seg.max.clone();
600                }
601                g.segments.push(seg);
602            }
603            _ => groups.push(Group {
604                max: seg.max.clone(),
605                segments: vec![seg],
606            }),
607        }
608    }
609    groups
610}
611
612/// How a singleton group shapes each batch its table's cursor yields.
613enum SingletonMode {
614    /// Every row visible, the range unbounded, one version per key and no
615    /// deletions: the batch goes out as read, its seqnos globalized.
616    Verbatim,
617    /// Rows masked by seqno visibility and the key range.
618    Masked {
619        /// Whether the snapshot straddles the segment, so rows are masked by
620        /// seqno.
621        partial: bool,
622        /// The snapshot in the segment's local seqno space.
623        threshold: SeqNo,
624        /// Columns decoded only for the mask, dropped from each batch.
625        dropped: Vec<u16>,
626    },
627    /// Newest visible version per key, deletions consumed, the predicate
628    /// applied after the dedup.
629    Dedup(DedupState),
630}
631
632/// The per-scan state of a singleton group's dedup: what each batch needs, and
633/// the key run it last decided, which can span batch boundaries.
634struct DedupState {
635    /// The scan's predicate in the segment's local coordinates.
636    predicate: Option<ColumnRangePredicate>,
637    /// The segment's visible range tombstones.
638    rts: TombstoneSweep,
639    /// Whether the snapshot straddles the segment, so rows are masked by
640    /// seqno.
641    partial: bool,
642    /// Whether the segment records deletions, so the value type is decoded.
643    deletes: bool,
644    /// The snapshot in the segment's local seqno space.
645    threshold: SeqNo,
646    /// Columns decoded only for the dedup and the predicate, dropped from
647    /// each batch.
648    dropped: Vec<u16>,
649    /// The user key of the last key run whose newest visible version was
650    /// already emitted (or deliberately dropped) — owned, because a run can
651    /// span batch boundaries. One REUSED buffer: a fresh `to_vec` per run
652    /// would make unique-key data (the common case) pay an allocation and
653    /// free per row.
654    last_key: Option<Vec<u8>>,
655}
656
657/// A segment's cursor, and whether it carries each row's whole value to the
658/// rows the scan returns (see [`ColumnarScan::read_late`]).
659pub(super) struct SegmentCursor {
660    cursor: SourceCursor,
661    whole: bool,
662    /// The columns the cursor decodes.
663    ids: Vec<u16>,
664}
665
666/// A singleton group streamed from its table's cursor.
667struct SingletonStream {
668    cursor: SourceCursor,
669    /// The segment's `global_seqno` base.
670    global: SeqNo,
671    mode: SingletonMode,
672}
673
674/// The source of the group the scan is in.
675enum GroupStream {
676    /// A singleton group, streamed from its table's cursor: one allocation
677    /// per group, next to the row groups it reads.
678    Singleton(Box<SingletonStream>),
679    /// An overlapping group, merged as it streams.
680    Merge(Box<merge::MergeStream>),
681    /// A group that yields nothing: its segment is ruled out.
682    Empty,
683}
684
685impl GroupStream {
686    /// Page bytes the group's sources hold now.
687    fn held_bytes(&self) -> u64 {
688        match self {
689            Self::Singleton(singleton) => singleton.cursor.held_bytes(),
690            Self::Merge(merge) => merge.held_bytes(),
691            Self::Empty => 0,
692        }
693    }
694
695    /// Reads past a share the group's sources made since this was last
696    /// called.
697    fn take_oversized(&mut self) -> u64 {
698        match self {
699            Self::Singleton(singleton) => singleton.cursor.take_oversized(),
700            Self::Merge(merge) => merge.take_oversized(),
701            Self::Empty => 0,
702        }
703    }
704}
705
706/// The rows of a batch whose operand was resolved to a value, by key.
707#[derive(Debug, Default)]
708pub(super) struct Resolved {
709    /// The keys resolved, sorted: their declared fields are read out of the
710    /// merged value, not out of the cells the operand carried.
711    keys: Vec<crate::Slice>,
712    /// Among them, sorted, the keys of rows a field projected by id alone
713    /// cannot be read for: returned, such a row fails the scan.
714    unreadable: Vec<crate::Slice>,
715}
716
717impl Resolved {
718    /// Whether any operand was resolved to a value.
719    pub(super) fn any(&self) -> bool {
720        !self.keys.is_empty()
721    }
722
723    /// Whether `key`'s operand was resolved to a value.
724    pub(super) fn holds(&self, key: &[u8]) -> bool {
725        self.keys.binary_search_by(|k| (**k).cmp(key)).is_ok()
726    }
727
728    /// Whether `key`'s row cannot be returned: a field projected by id alone
729    /// cannot be read out of its merged value.
730    pub(super) fn unreadable(&self, key: &[u8]) -> bool {
731        self.unreadable.binary_search_by(|k| (**k).cmp(key)).is_ok()
732    }
733}
734
735/// A returned row's operand was resolved to a value read whole, which a field
736/// projected by id alone cannot be read out of.
737pub(super) const UNREADABLE_BY_ID: Error = Error::Projection(
738    "projection: a merged value is read whole, and a field projected by id alone cannot be \
739     read out of it",
740);
741
742/// A batch lacks a column the scan decoded for itself.
743const MISSING_BATCH_COLUMN: Error =
744    Error::InvalidHeader("columnar_scan: a batch is missing a column the scan decoded");
745
746/// Whether a row of `batch` is a merge operand; `false` for a batch without a
747/// value-type column.
748fn holds_operand(batch: &ColumnBatch) -> bool {
749    value_types(batch).is_some_and(|types| {
750        types
751            .iter()
752            .take(batch.row_count as usize)
753            .copied()
754            .any(is_operand)
755    })
756}
757
758/// The value-type cells of `batch`, one byte per row, or `None` for a batch
759/// without a value-type column.
760pub(super) fn value_types(batch: &ColumnBatch) -> Option<&[u8]> {
761    batch
762        .columns
763        .iter()
764        .find(|c| c.column_id == COL_VALUE_TYPE)
765        .map(|c| &*c.data)
766}
767
768/// Whether a value-type cell marks a merge operand.
769pub(super) fn is_operand(byte: u8) -> bool {
770    crate::ValueType::try_from(byte) == Ok(crate::ValueType::MergeOperand)
771}
772
773/// The visible range tombstones of a group, swept in the ascending key order
774/// its rows are decided in: each tombstone is entered once the keys reach its
775/// start and dropped once they pass its end, so a row is checked against the
776/// newest tombstone over it, not against every tombstone of the group.
777pub(super) struct TombstoneSweep {
778    /// `(start, end, effective seqno)`, by start key.
779    rts: Vec<(UserKey, UserKey, SeqNo)>,
780    /// The first tombstone the keys have not reached yet.
781    next: usize,
782    /// The tombstones entered, newest first, each with its place in `rts`;
783    /// one the keys have passed is dropped when it comes to the top.
784    open: alloc::collections::BinaryHeap<(SeqNo, usize)>,
785}
786
787impl TombstoneSweep {
788    /// The sweep over `rts`, ordered by `cmp`.
789    pub(super) fn new(mut rts: Vec<(UserKey, UserKey, SeqNo)>, cmp: &dyn UserComparator) -> Self {
790        rts.sort_by(|a, b| cmp.compare(&a.0, &b.0));
791        Self {
792            rts,
793            next: 0,
794            open: alloc::collections::BinaryHeap::new(),
795        }
796    }
797
798    /// Whether the group has no visible range tombstone.
799    pub(super) fn is_empty(&self) -> bool {
800        self.rts.is_empty()
801    }
802
803    /// Whether a row at `key`, no lower than any key asked before, at the
804    /// tree-global seqno `eff` is deleted: inside a tombstone's half-open
805    /// `[start, end)` span and older than it.
806    pub(super) fn covers(&mut self, key: &[u8], eff: SeqNo, cmp: &dyn UserComparator) -> bool {
807        use core::cmp::Ordering;
808
809        while let Some((start, _, seqno)) = self.rts.get(self.next)
810            && cmp.compare(start, key) != Ordering::Greater
811        {
812            self.open.push((*seqno, self.next));
813            self.next += 1;
814        }
815        // The newest open tombstone the keys have passed can cover no later
816        // key either; one below it is dropped once it comes to the top.
817        while let Some(&(_, at)) = self.open.peek() {
818            let passed = self
819                .rts
820                .get(at)
821                .is_none_or(|(_, end, _)| cmp.compare(key, end) != Ordering::Less);
822            if !passed {
823                break;
824            }
825            self.open.pop();
826        }
827        self.open.peek().is_some_and(|&(seqno, _)| eff < seqno)
828    }
829}
830
831/// When a scan's predicate judges the rows the dedup decided: see
832/// [`ColumnarScan::predicate_timing`].
833#[derive(Clone, Copy, PartialEq, Eq)]
834pub(super) enum PredicateTiming {
835    /// Before the values are read, every row.
836    BeforeValues,
837    /// Before the values are read for the plain rows, after for the operands
838    /// still to be resolved.
839    BeforeValuesExceptOperands,
840    /// After the values are read.
841    AfterValues,
842}
843
844/// Drops from `batch` the columns decoded only for the scan's own use.
845fn drop_columns(batch: &mut ColumnBatch, dropped: &[u16]) {
846    if !dropped.is_empty() {
847        batch.columns.retain(|c| !dropped.contains(&c.column_id));
848    }
849}
850
851/// Resolves a merge operand row as a point read at the scan's snapshot would,
852/// in the version the scan reads.
853struct Resolver {
854    version: crate::version::SuperVersion,
855    operator: alloc::sync::Arc<dyn crate::merge_operator::MergeOperator>,
856    /// Where a merge reads a base kept in the value log.
857    blob_source: Option<crate::blob_tree::BlobSource>,
858}
859
860/// Iterator over a tree-level projected columnar scan.
861///
862/// Yields projected [`ColumnBatch`]es in ascending key order. Created by
863/// [`Tree::columnar_scan`] (and surfaced through
864/// [`AnyTree::columnar_scan`](crate::AnyTree::columnar_scan)). A group of one
865/// segment streams its table as it yields, one row group at a time, so a
866/// caller that stops early reads nothing more; a group of overlapping
867/// segments is merged when the scan reaches it.
868pub struct ColumnarScan {
869    groups: alloc::collections::VecDeque<Group>,
870    current: Option<GroupStream>,
871    /// The projected column ids, in output order.
872    projection: Vec<u16>,
873    /// The projected fields, whose declarations every yielded batch is
874    /// brought to.
875    fields: Vec<projection::ProjectedField>,
876    /// The projector the declared fields of a whole-value segment are read
877    /// through; `None` when no field is declared.
878    projector: Option<alloc::sync::Arc<dyn projection::ValueProjector>>,
879    /// Whether a field is declared, so a whole-value segment is read without
880    /// its value column in the declared ids' place even with no projector,
881    /// which only a memtable holding nothing but deletions allows.
882    declared: bool,
883    /// What resolves a merge operand row, when the tree merges; `None` when
884    /// it has no merge operator.
885    resolver: Option<Resolver>,
886    /// Where the objects of a blob tree's rows are read, when the tree is a
887    /// blob tree's index: every row returned has its fields read late (see
888    /// [`cells`]).
889    cells: Option<cells::CellSource>,
890    predicate: Option<ColumnRangePredicate>,
891    /// The weakest [`PredicateSupport`] over the segments read so far.
892    support: PredicateSupport,
893    comparator: alloc::sync::Arc<dyn UserComparator>,
894    /// The query snapshot, used for per-row seqno visibility masking.
895    seqno: SeqNo,
896    /// The requested key range. Applied as a per-row filter (not just segment
897    /// selection): a segment that only partially overlaps the range must still
898    /// drop the rows that fall outside it.
899    lo: Bound<UserKey>,
900    hi: Bound<UserKey>,
901
902    /// The page bytes the scan may hold at once, shared by the segments of an
903    /// overlapping group.
904    budget: u64,
905    /// The most page bytes the scan held at once so far.
906    peak_payload: core::cell::Cell<u64>,
907    /// Reads past a segment's share, as [`Self::oversized_reads`] counts them.
908    oversized: core::cell::Cell<u64>,
909
910    /// Where this scan's gather cost is recorded. Held rather than reached
911    /// for through the tree because the scan outlives the call that built it.
912    #[cfg(feature = "metrics")]
913    metrics: alloc::sync::Arc<crate::Metrics>,
914}
915
916impl ColumnarScan {
917    /// The most page bytes the scan held at once so far: the batches its
918    /// segments have read and not yet handed to the merge or out. It stays
919    /// within [`Config::columnar_scan_budget`](crate::Config::columnar_scan_budget)
920    /// except by the reads [`Self::oversized_reads`] counts. What a read holds
921    /// only while it runs, such as the pages a filter drops, is what that
922    /// counter sees; this figure is what stays read between batches. A batch
923    /// handed out and the scan's per-segment bookkeeping are not counted: the
924    /// first is the caller's, the second grows with the segments merged, not
925    /// with the rows.
926    #[must_use]
927    pub fn peak_payload_bytes(&self) -> u64 {
928        self.peak_payload.get()
929    }
930
931    /// Reads that went past the share of the budget their segment has: a row
932    /// page larger than the share, read on its own; a run of row pages whose
933    /// rows decoded wider than the rows before them; a row group read whole
934    /// because a pushed-down predicate selects its row pages by their zones.
935    #[must_use]
936    pub fn oversized_reads(&self) -> u64 {
937        self.oversized.get()
938    }
939
940    /// Records that the scan holds `bytes` of page payload now.
941    fn observe_payload(&self, bytes: u64) {
942        if bytes > self.peak_payload.get() {
943            self.peak_payload.set(bytes);
944        }
945    }
946
947    /// Records `count` reads past their segment's share.
948    fn record_oversized(&self, count: u64) {
949        self.oversized.set(self.oversized.get() + count);
950    }
951    /// How far the scan's predicate ran over the segments read so far, or
952    /// `None` when the scan has no predicate: the weakest answer of any
953    /// segment, so it only ever falls as the scan goes, and is the answer for
954    /// the whole scan once it is exhausted.
955    ///
956    /// [`PredicateSupport::Exact`] means every row yielded matches;
957    /// [`PredicateSupport::PruneOnly`] and [`PredicateSupport::Unsupported`]
958    /// mean the rows still need the caller's own check.
959    #[must_use]
960    pub fn predicate_support(&self) -> Option<PredicateSupport> {
961        self.predicate.as_ref().map(|_| self.support)
962    }
963
964    /// Records the bytes a gather moved.
965    ///
966    /// The figure is the SIZE OF THE RESULT — what the operation wrote into a
967    /// new buffer — which is what makes repeated accumulation visible:
968    /// folding `k` batches one at a time records the whole accumulated size
969    /// `k` times, so the counter grows quadratically exactly where the work
970    /// does, while a single pass over the same data records it once.
971    #[inline]
972    #[cfg_attr(
973        not(feature = "metrics"),
974        expect(
975            clippy::unused_self,
976            reason = "the scan's metrics exist only with the feature"
977        )
978    )]
979    fn record_gather(&self, batch: &ColumnBatch) {
980        #[cfg(feature = "metrics")]
981        self.metrics.record_gather(batch.data_size());
982        #[cfg(not(feature = "metrics"))]
983        let _ = batch;
984    }
985
986    /// Records `useful` bytes of chosen rows' cells taken from payload pages
987    /// read for them, and `incidental` bytes those pages held besides.
988    #[inline]
989    #[cfg_attr(
990        not(feature = "metrics"),
991        expect(
992            clippy::unused_self,
993            reason = "the scan's metrics exist only with the feature"
994        )
995    )]
996    fn record_payload(&self, useful: u64, incidental: u64) {
997        #[cfg(feature = "metrics")]
998        {
999            self.metrics.record_payload_useful(useful);
1000            self.metrics.record_payload_incidental(incidental);
1001        }
1002        #[cfg(not(feature = "metrics"))]
1003        let _ = (useful, incidental);
1004    }
1005
1006    /// Records `bytes` a read copied out of the pages it decoded.
1007    #[inline]
1008    #[cfg_attr(
1009        not(feature = "metrics"),
1010        expect(
1011            clippy::unused_self,
1012            reason = "the scan's metrics exist only with the feature"
1013        )
1014    )]
1015    fn record_copied(&self, bytes: usize) {
1016        #[cfg(feature = "metrics")]
1017        self.metrics.record_gather(bytes);
1018        #[cfg(not(feature = "metrics"))]
1019        let _ = bytes;
1020    }
1021}
1022
1023impl ColumnarScan {
1024    /// Opens one overlap group as the stream of its projected, key-ordered
1025    /// output batches. A singleton group streams its segment's table (masking
1026    /// by seqno only when the snapshot straddles the segment); an overlapping
1027    /// group is row-merged with newest-effective-seqno-wins dedup.
1028    ///
1029    /// Lowers `support` to how far the predicate ran over what opening read.
1030    fn open_group(
1031        &self,
1032        group: &Group,
1033        support: &mut PredicateSupport,
1034    ) -> crate::Result<GroupStream> {
1035        let rts = self.visible_group_range_tombstones(&group.segments)?;
1036        // A row source yields every version of its keys, deletions among
1037        // them, and its values whole: the merge decides each key. So does a
1038        // table whose values are read only for the rows it returns.
1039        if let [seg] = group.segments.as_slice()
1040            && !seg.is_rows()
1041            && !self.reads_late(seg)
1042        {
1043            return self.open_singleton(seg, rts, support);
1044        }
1045        Ok(GroupStream::Merge(Box::new(merge::MergeStream::open(
1046            self,
1047            &group.segments,
1048            rts,
1049        )?)))
1050    }
1051
1052    /// Whether `seg` is read through the merge with its values read only for
1053    /// the rows the scan returns: every segment of a tree that merges, whose
1054    /// operands a returned row resolves, and a whole-value table whose
1055    /// declared fields a returned row reads out of its value.
1056    fn reads_late(&self, seg: &Segment) -> bool {
1057        // A blob tree reads every returned row's fields late.
1058        if self.cells.is_some() {
1059            return true;
1060        }
1061        self.resolver.is_some() || (seg.whole && self.declared)
1062    }
1063
1064    /// A cursor over `seg`'s table within the scan's key range, decoding
1065    /// `projection`, pushing `predicate` down and holding at most `share`
1066    /// page bytes at once.
1067    fn segment_cursor(
1068        &self,
1069        seg: &Segment,
1070        projection: &[u16],
1071        predicate: Option<&ColumnRangePredicate>,
1072        share: u64,
1073    ) -> crate::Result<SegmentCursor> {
1074        // A whole value is carried to the rows the scan returns, for its
1075        // declared fields and for the merge operands it may hold; a scan of
1076        // intrinsic columns alone reads no value.
1077        let needs_values = self.declared || self.resolver.is_some();
1078        let whole = seg.whole && needs_values;
1079        // The declared fields of a whole value lie inside it: decode the value
1080        // in their place. A declared field's id may be the value column's own,
1081        // so no predicate is pushed down; the merge filters after the fields
1082        // are read.
1083        let mut ids: Vec<u16> = projection.to_vec();
1084        if seg.cells && needs_values {
1085            // A blob tree's columnar table keeps each field of a split row in
1086            // its own column, the row's references beside them, and every
1087            // other value whole under the id the merge carries whole values
1088            // in: no column moves.
1089            use crate::blob_tree::field_row::CELL_REFS_COLUMN;
1090            for id in [CELL_REFS_COLUMN, merge::COL_WHOLE_VALUE] {
1091                if !ids.contains(&id) {
1092                    ids.push(id);
1093                }
1094            }
1095        } else if whole {
1096            let declared: Vec<u16> = self
1097                .fields
1098                .iter()
1099                .filter(|f| projection::is_declared(f))
1100                .map(projection::ProjectedField::column_id)
1101                .collect();
1102            ids.retain(|id| !declared.contains(id));
1103            if !ids.contains(&crate::table::columnar::COL_VALUE) {
1104                ids.push(crate::table::columnar::COL_VALUE);
1105            }
1106        }
1107        let predicate = predicate.filter(|_| !whole);
1108        // Every segment of the scan is opened here once: its table counts the
1109        // scan as reaching it, as a row scan's tables do.
1110        if let Source::Columnar(table) | Source::RowTable(table) = &seg.source {
1111            table.note_scan();
1112        }
1113        let cursor = match &seg.source {
1114            Source::Columnar(table) => SourceCursor::Columnar(Box::new(table.columnar_cursor(
1115                &ids,
1116                predicate,
1117                self.lo.clone(),
1118                self.hi.clone(),
1119                Some(share),
1120            )?)),
1121            Source::RowTable(table) => SourceCursor::Rows(Box::new(RowCursor::table(
1122                table,
1123                self.lo.clone(),
1124                self.hi.clone(),
1125                self.seqno,
1126                ids.clone(),
1127                share,
1128            ))),
1129            // Within the span its group was formed on: the active memtable
1130            // stays writable, and a row landing outside that span after the
1131            // scan was created would be read out of order, or beside a
1132            // version of its key another group returns. A range tombstone
1133            // widens that span past the scan's range, so the cursor reads
1134            // only where the two meet: every row beyond is dropped by the
1135            // range check anyway.
1136            Source::Memtable(memtable) => {
1137                let cmp = self.comparator.as_ref();
1138                let lo = tighter_lower(Bound::Included(seg.min.clone()), &self.lo, cmp);
1139                let hi = tighter_upper(Bound::Included(seg.max.clone()), &self.hi, cmp);
1140                SourceCursor::Rows(Box::new(RowCursor::memtable(
1141                    memtable.clone(),
1142                    &lo,
1143                    &hi,
1144                    self.seqno,
1145                    ids.clone(),
1146                    share,
1147                )))
1148            }
1149        };
1150        Ok(SegmentCursor { cursor, whole, ids })
1151    }
1152
1153    /// The columns of `seg` a merge reads for its chosen rows only, as the
1154    /// pairs of the id a table stores each under and the id the merge carries
1155    /// it under: the fields the projection declares, but the predicate's,
1156    /// and the columns a whole value or a blob tree's references are carried
1157    /// in. Empty for a source read row by row, and while operands are
1158    /// resolved or the raw value is read as well, whose values are needed
1159    /// whole.
1160    fn late_payload(&self, seg: &Segment) -> Vec<(u16, u16)> {
1161        use crate::blob_tree::field_row::CELL_REFS_COLUMN;
1162        use crate::table::columnar::COL_VALUE;
1163
1164        if seg.is_rows() || self.resolver.is_some() || self.raw_value_read() {
1165            return Vec::new();
1166        }
1167        let predicate = self.predicate.as_ref().map(|p| p.column_id);
1168        let fields = self
1169            .fields
1170            .iter()
1171            .filter(|f| projection::is_declared(f) && Some(f.column_id()) != predicate)
1172            .map(|f| (f.column_id(), f.column_id()));
1173        // Without a declared field no value is read at all, a blob tree's
1174        // included (a returned row's value type reads as a value regardless).
1175        if seg.cells && self.declared {
1176            fields
1177                .chain([
1178                    (CELL_REFS_COLUMN, CELL_REFS_COLUMN),
1179                    (merge::COL_WHOLE_VALUE, merge::COL_WHOLE_VALUE),
1180                ])
1181                .collect()
1182        } else if seg.whole {
1183            // The declared fields lie inside the whole value.
1184            if self.declared {
1185                alloc::vec![(COL_VALUE, merge::COL_WHOLE_VALUE)]
1186            } else {
1187                Vec::new()
1188            }
1189        } else {
1190            fields.collect()
1191        }
1192    }
1193
1194    /// The rows a merge returned, `batch`, with their values read: each merge
1195    /// operand replaced by what a read at the scan's snapshot returns for its
1196    /// key (a key the read finds absent is dropped), then the declared fields
1197    /// of each row carrying a whole value read out of it, and the carried
1198    /// value column dropped. Only returned rows get here, so a shadowed,
1199    /// deleted or invisible version is never resolved or projected. Also
1200    /// returns which keys were resolved to a value: such a row holds no cell
1201    /// of its own for a column no field declares. `judged` says the merge
1202    /// already gave every row its final verdict on the predicate.
1203    pub(super) fn read_late(
1204        &self,
1205        batch: ColumnBatch,
1206        judged: bool,
1207    ) -> crate::Result<(ColumnBatch, Resolved)> {
1208        let (batch, resolved) = self.resolve_operands(batch)?;
1209        if let Some(cells) = &self.cells {
1210            return Ok((cells::materialize(self, cells, batch, judged)?, resolved));
1211        }
1212        let mut batch = if self.declared {
1213            projection::project_decided(
1214                batch,
1215                &self.fields,
1216                self.projector.as_deref(),
1217                merge::COL_WHOLE_VALUE,
1218            )?
1219        } else {
1220            batch
1221        };
1222        drop_columns(&mut batch, &[merge::COL_WHOLE_VALUE]);
1223        Ok((batch, resolved))
1224    }
1225
1226    /// `batch` with each merge operand row replaced by what a read at the
1227    /// scan's snapshot returns for its key, through the tree's own operator
1228    /// exactly as the read path resolves it: that value, carried as the row's
1229    /// whole value (and in the value column when it is projected by id), or
1230    /// the row dropped when the read finds the key absent. Also returns which
1231    /// keys were resolved to a value (see [`Resolved`]).
1232    ///
1233    /// The value an operand resolves to is read whole, like any row value: a
1234    /// value field projected by id alone cannot be read out of it, so such a
1235    /// row is named unreadable, and the scan fails as it does for a whole
1236    /// value if the row is returned, instead of returning the cell the
1237    /// operand itself carried.
1238    fn resolve_operands(&self, batch: ColumnBatch) -> crate::Result<(ColumnBatch, Resolved)> {
1239        use crate::table::columnar::{COL_VALUE, Column, frame_bytes_column};
1240
1241        let Some(resolver) = &self.resolver else {
1242            return Ok((batch, Resolved::default()));
1243        };
1244        if !holds_operand(&batch) {
1245            return Ok((batch, Resolved::default()));
1246        }
1247        let row_count = batch.row_count;
1248        let rows = row_count as usize;
1249        let find = |id: u16| batch.columns.iter().position(|c| c.column_id == id);
1250        let (Some(key_at), Some(type_at), Some(whole_at)) = (
1251            find(COL_USER_KEY),
1252            find(COL_VALUE_TYPE),
1253            find(merge::COL_WHOLE_VALUE),
1254        ) else {
1255            return Err(MISSING_BATCH_COLUMN);
1256        };
1257        // The raw value, read by id or by the predicate, reads the resolved
1258        // value too.
1259        let raw_at = find(COL_VALUE).filter(|&at| {
1260            batch
1261                .columns
1262                .get(at)
1263                .is_some_and(|c| c.type_tag == TypeTag::Bytes)
1264                && self.raw_value_read()
1265        });
1266        let column = |at: usize| batch.columns.get(at).ok_or(MISSING_BATCH_COLUMN);
1267        let (keys, kinds) = (column(key_at)?, column(type_at)?);
1268        // Per row: `None` keeps it as read, `Some(None)` drops it, and
1269        // `Some(Some(value))` makes it that value.
1270        let mut resolved: Vec<Option<Option<crate::Slice>>> = Vec::with_capacity(rows);
1271        for row in 0..row_count {
1272            let byte = *kinds.data.get(row as usize).ok_or(MISSING_BATCH_COLUMN)?;
1273            if crate::ValueType::try_from(byte) != Ok(crate::ValueType::MergeOperand) {
1274                resolved.push(None);
1275                continue;
1276            }
1277            // Each key is returned once, so each is read once.
1278            let key = bytes_column_row(&keys.data, row_count, row)?;
1279            resolved.push(Some(Tree::resolve_or_passthrough(
1280                &resolver.version,
1281                key,
1282                self.seqno,
1283                Some(&resolver.operator),
1284                self.comparator.as_ref(),
1285                || resolver.blob_source.clone(),
1286            )?));
1287        }
1288        // Only the raw value is rewritten below, and only a row that carried
1289        // its value whole holds the raw value under the value column's id: a
1290        // row of a table that splits its values carries no whole value, and
1291        // its cell under that id is a field like any other.
1292        let whole = column(whole_at)?;
1293        let by_id: Vec<u16> = self
1294            .fields
1295            .iter()
1296            .filter(|f| {
1297                f.type_tag().is_none() && batch.columns.iter().any(|c| c.column_id == f.column_id())
1298            })
1299            .map(projection::ProjectedField::column_id)
1300            .collect();
1301        let mut outcome = Resolved::default();
1302        for (row, fix) in (0..row_count).zip(&resolved) {
1303            if !matches!(fix, Some(Some(_))) {
1304                continue;
1305            }
1306            let key = crate::Slice::from(bytes_column_row(&keys.data, row_count, row)?);
1307            let raw_rewritten = raw_at.is_some() && whole.is_valid(row);
1308            if by_id.iter().any(|&id| !(id == COL_VALUE && raw_rewritten)) {
1309                outcome.unreadable.push(key.clone());
1310            }
1311            outcome.keys.push(key);
1312        }
1313        outcome.keys.sort_unstable();
1314        outcome.unreadable.sort_unstable();
1315
1316        // A key the read finds absent is not returned: the rows returned are
1317        // the others, and only they are written below, once each.
1318        let returned = |row: &u32| !matches!(resolved.get(*row as usize), Some(Some(None)));
1319        let kept: Option<Vec<u32>> = resolved
1320            .iter()
1321            .any(|fix| matches!(fix, Some(None)))
1322            .then(|| (0..row_count).filter(returned).collect());
1323        let out_rows = kept.as_ref().map_or(rows, Vec::len);
1324        // The cells of bytes column `at` with the resolved rows' values in
1325        // place: a resolved row's cell is its value, the others as read.
1326        let rewrite = |at: usize| -> crate::Result<Column> {
1327            let old = column(at)?;
1328            let mut cells: Vec<Option<&[u8]>> = Vec::with_capacity(out_rows);
1329            for row in (0..row_count).filter(returned) {
1330                cells.push(match resolved.get(row as usize) {
1331                    Some(Some(Some(value))) => Some(&**value),
1332                    _ if old.is_valid(row) => Some(bytes_column_row(&old.data, row_count, row)?),
1333                    _ => None,
1334                });
1335            }
1336            let validity = cells.iter().any(Option::is_none).then(|| {
1337                let mut bits = alloc::vec![0u8; out_rows.div_ceil(8)];
1338                for (row, cell) in cells.iter().enumerate() {
1339                    if cell.is_some()
1340                        && let Some(byte) = bits.get_mut(row / 8)
1341                    {
1342                        *byte |= 1 << (row % 8);
1343                    }
1344                }
1345                bits
1346            });
1347            Ok(Column {
1348                column_id: old.column_id,
1349                type_tag: TypeTag::Bytes,
1350                validity,
1351                data: frame_bytes_column(out_rows, || cells.iter().map(|c| c.unwrap_or(&[])))?,
1352            })
1353        };
1354        let whole = rewrite(whole_at)?;
1355        let raw = raw_at.map(rewrite).transpose()?;
1356        let mut types: Vec<u8> = Vec::with_capacity(out_rows);
1357        for row in (0..row_count).filter(returned) {
1358            types.push(match resolved.get(row as usize) {
1359                Some(Some(_)) => u8::from(crate::ValueType::Value),
1360                _ => *kinds.data.get(row as usize).ok_or(MISSING_BATCH_COLUMN)?,
1361            });
1362        }
1363
1364        let mut batch = batch;
1365        if let Some(kept) = &kept {
1366            for (at, slot) in batch.columns.iter_mut().enumerate() {
1367                if at == whole_at || raw_at == Some(at) {
1368                    continue;
1369                }
1370                if at == type_at {
1371                    // Its cells are written above; only its nulls are gathered.
1372                    slot.validity = slot
1373                        .validity
1374                        .as_deref()
1375                        .map(|bits| take_validity(bits, kept));
1376                    continue;
1377                }
1378                // Every other column keeps the rows returned, gathered once.
1379                *slot = take_column(slot, rows, kept)?;
1380            }
1381        }
1382        if let Some(slot) = batch.columns.get_mut(whole_at) {
1383            *slot = whole;
1384        }
1385        if let (Some(at), Some(raw)) = (raw_at, raw)
1386            && let Some(slot) = batch.columns.get_mut(at)
1387        {
1388            *slot = raw;
1389        }
1390        if let Some(slot) = batch.columns.get_mut(type_at) {
1391            slot.data = crate::Slice::from(types);
1392        }
1393        // A subset of the batch's rows, whose count is a u32.
1394        batch.row_count = u32::try_from(out_rows).unwrap_or(row_count);
1395        self.record_gather(&batch);
1396        Ok((batch, outcome))
1397    }
1398
1399    /// The next output batch of `stream`, or `None` once it is exhausted.
1400    /// Lowers `support` to how far the predicate ran over what was read.
1401    fn next_from(
1402        &self,
1403        stream: &mut GroupStream,
1404        support: &mut PredicateSupport,
1405    ) -> Option<crate::Result<ColumnBatch>> {
1406        match stream {
1407            GroupStream::Empty => None,
1408            GroupStream::Merge(merge) => merge.next_batch(self, support).transpose(),
1409            GroupStream::Singleton(singleton) => loop {
1410                let batch = match singleton.cursor.next()? {
1411                    Ok(batch) => batch,
1412                    Err(e) => return Some(Err(e)),
1413                };
1414                // Observed before the batch is shaped: a mask or dedup that
1415                // keeps none of its rows drops it here, and what the cursor
1416                // held with it would otherwise never be seen.
1417                self.observe_payload(singleton.cursor.held_bytes() + batch.data_size() as u64);
1418                *support = (*support).min(singleton.cursor.predicate_support());
1419                // A segment written without a projected column, or with null
1420                // cells in one, reads as the field declares before its rows
1421                // are decided, so a predicate after the dedup sees the
1422                // declared defaults, as it does on the merge path.
1423                let judged = self.predicate.as_ref().map(|p| p.column_id);
1424                let (batch, mistyped) =
1425                    match projection::conform_lenient(batch, &self.fields, judged) {
1426                        Ok(conformed) => conformed,
1427                        Err(e) => return Some(Err(e)),
1428                    };
1429                let SingletonStream { global, mode, .. } = &mut **singleton;
1430                // A predicate over a column the batch stores under another
1431                // type cannot judge its rows: it is set aside for this batch,
1432                // so a row the dedup keeps fails the scan below.
1433                let unjudged = match mode {
1434                    SingletonMode::Dedup(state)
1435                        if state
1436                            .predicate
1437                            .as_ref()
1438                            .is_some_and(|p| mistyped.contains(&p.column_id)) =>
1439                    {
1440                        state.predicate.take()
1441                    }
1442                    _ => None,
1443                };
1444                let shaped = self.shape_singleton_batch(batch, *global, mode, support);
1445                if let (Some(predicate), SingletonMode::Dedup(state)) = (unjudged, &mut *mode) {
1446                    state.predicate = Some(predicate);
1447                }
1448                match shaped {
1449                    // A column's type is the batch's, so any row it returns
1450                    // stores the field under the other type.
1451                    Ok(Some(_)) if !mistyped.is_empty() => {
1452                        return Some(Err(projection::MISTYPED));
1453                    }
1454                    // The rows returned are decided: each is held to the
1455                    // declarations.
1456                    Ok(Some(batch)) => return Some(projection::conform(batch, &self.fields)),
1457                    Ok(None) => {}
1458                    Err(e) => return Some(Err(e)),
1459                }
1460            },
1461        }
1462    }
1463
1464    /// Shapes one batch a singleton group's cursor read as `mode` says, or
1465    /// `None` when no row of it survives.
1466    fn shape_singleton_batch(
1467        &self,
1468        batch: ColumnBatch,
1469        global: SeqNo,
1470        mode: &mut SingletonMode,
1471        support: &mut PredicateSupport,
1472    ) -> crate::Result<Option<ColumnBatch>> {
1473        // A cursor yields a batch only for a row page that keeps a row.
1474        debug_assert!(batch.row_count > 0, "a cursor yielded an empty batch");
1475        match mode {
1476            SingletonMode::Verbatim => {
1477                let mut batch = batch;
1478                self.globalize_seqnos(&mut batch, global)?;
1479                Ok(Some(batch))
1480            }
1481            SingletonMode::Masked {
1482                partial,
1483                threshold,
1484                dropped,
1485            } => self.mask_singleton_batch(&batch, global, *partial, *threshold, dropped),
1486            SingletonMode::Dedup(state) => {
1487                self.dedup_singleton_batch(&batch, global, state, support)
1488            }
1489        }
1490    }
1491
1492    /// Whether the raw value is read by id, beside any declared field read
1493    /// out of it: a field projects it by id, or the predicate runs on it and
1494    /// no field declares its id.
1495    pub(super) fn raw_value_read(&self) -> bool {
1496        use crate::table::columnar::COL_VALUE;
1497
1498        let by_id = self
1499            .fields
1500            .iter()
1501            .any(|f| f.column_id() == COL_VALUE && !projection::is_declared(f));
1502        let declared = self
1503            .fields
1504            .iter()
1505            .any(|f| f.column_id() == COL_VALUE && projection::is_declared(f));
1506        by_id
1507            || (!declared
1508                && self
1509                    .predicate
1510                    .as_ref()
1511                    .is_some_and(|p| p.column_id == COL_VALUE))
1512    }
1513
1514    /// When the scan's predicate judges the decided rows: before their values
1515    /// are read where its column is one reading the values does not change.
1516    /// The key and the seqno never change; a column no field declares is the
1517    /// row's own physical cell, except on an operand still to be resolved
1518    /// into a value read whole, which is judged after. A declared field is
1519    /// read out of the value, so a predicate over one runs after.
1520    /// `holds_operand` says whether a decided row is a merge operand, asked
1521    /// only when the answer matters.
1522    pub(super) fn predicate_timing(&self, holds_operand: impl FnOnce() -> bool) -> PredicateTiming {
1523        use crate::table::columnar::COL_SEQNO;
1524
1525        let Some(pred) = &self.predicate else {
1526            return PredicateTiming::AfterValues;
1527        };
1528        // The raw value and the value type are the row's own cells too: a
1529        // whole-value source keeps its raw value in place when the predicate
1530        // reads it, and only an operand still to be resolved rewrites them.
1531        match pred.column_id {
1532            COL_USER_KEY | COL_SEQNO => PredicateTiming::BeforeValues,
1533            // The value type is an opaque fixed-width column, which a range
1534            // predicate does not judge (its support is reported unsupported
1535            // and every row kept), so a blob tree's stored types (an
1536            // indirection, a cell row) never decide a row before it is
1537            // returned as a value.
1538            id if self
1539                .fields
1540                .iter()
1541                .any(|f| f.column_id() == id && projection::is_declared(f)) =>
1542            {
1543                PredicateTiming::AfterValues
1544            }
1545            _ if self.resolver.is_some() && holds_operand() => {
1546                PredicateTiming::BeforeValuesExceptOperands
1547            }
1548            _ => PredicateTiming::BeforeValues,
1549        }
1550    }
1551
1552    /// Applies the scan's predicate to `batch` after the dedup, when it filters,
1553    /// and lowers `support` to how far it ran there.
1554    fn filter_after_dedup(
1555        &self,
1556        batch: ColumnBatch,
1557        pred: Option<&ColumnRangePredicate>,
1558        support: &mut PredicateSupport,
1559    ) -> crate::Result<ColumnBatch> {
1560        let Some(pred) = pred else {
1561            return Ok(batch);
1562        };
1563        *support = (*support).min(pred.support_in(&batch));
1564        if pred.apply != PredicateApply::Filter {
1565            return Ok(batch);
1566        }
1567        let mask = pred.matching_rows(&batch);
1568        // Rows judged on their stored column before the gather all pass
1569        // again: the batch goes on as gathered, not copied once more.
1570        if mask.iter().all(|&keep| keep) {
1571            return Ok(batch);
1572        }
1573        let filtered = filter_batch(&batch, &mask)?;
1574        self.record_gather(&filtered);
1575        Ok(filtered)
1576    }
1577
1578    /// The range tombstones of `segments` visible to the scan snapshot, with
1579    /// tree-global effective seqnos: an UNMATERIALIZED range deletion (a
1580    /// flushed `remove_range` no relocation has folded into a positional
1581    /// delete bitmap yet) lives only in the segments' RT sections, and the
1582    /// scan must suppress the rows it covers exactly as the point and
1583    /// ordinary range reads do. A group is key-disjoint from its neighbours
1584    /// and a tombstone's span is inside its own segment's key range, so
1585    /// per-group collection sees every tombstone that can cover a group row.
1586    fn visible_group_range_tombstones(
1587        &self,
1588        segments: &[Segment],
1589    ) -> crate::Result<Vec<(UserKey, UserKey, SeqNo)>> {
1590        let mut rts = Vec::new();
1591        for seg in segments {
1592            for rt in seg.range_tombstones() {
1593                let eff = rt
1594                    .seqno
1595                    .checked_add(seg.rt_base())
1596                    .ok_or(Error::InvalidHeader(
1597                        "columnar_scan: effective range-tombstone seqno overflows",
1598                    ))?;
1599                // Same exclusive-MVCC visibility as rows: the deletion exists
1600                // for this snapshot only below it.
1601                if eff < self.seqno {
1602                    rts.push((rt.start.clone(), rt.end.clone(), eff));
1603                }
1604            }
1605        }
1606        Ok(rts)
1607    }
1608
1609    /// Whether the requested key range is fully unbounded, so no per-row range
1610    /// filtering is needed (the segment's every row is in range).
1611    fn range_is_full(&self) -> bool {
1612        matches!(self.lo, Bound::Unbounded) && matches!(self.hi, Bound::Unbounded)
1613    }
1614
1615    /// Rewrites a batch's seqno column from its segment's LOCAL space into the
1616    /// tree's global one (`local + global`).
1617    ///
1618    /// A bulk-ingested segment stores every row at local seqno `0` and carries
1619    /// its ordering in a per-segment `global_seqno`, so the stored column is not
1620    /// a commit sequence number any other read surface would recognize. The
1621    /// masking arithmetic elsewhere translates the THRESHOLD into local space
1622    /// instead (cheaper, one subtraction per segment), which is why the column
1623    /// itself still needs this before it reaches a caller. A zero offset leaves
1624    /// the batch untouched. The rewritten column is a gather and is charged.
1625    #[cfg_attr(
1626        not(feature = "metrics"),
1627        expect(
1628            clippy::unused_self,
1629            reason = "the scan's metrics exist only with the feature"
1630        )
1631    )]
1632    fn globalize_seqnos(&self, batch: &mut ColumnBatch, global: SeqNo) -> crate::Result<()> {
1633        if global == 0 {
1634            return Ok(());
1635        }
1636        let Some(col) = batch.columns.iter_mut().find(|c| c.column_id == COL_SEQNO) else {
1637            return Ok(());
1638        };
1639        let len = batch.row_count as usize * 8;
1640        if col.data.len() != len {
1641            return Err(Error::InvalidHeader("columnar_scan: short seqno column"));
1642        }
1643        // Column bytes are an immutable (possibly shared) view, so the
1644        // globalized column is rebuilt into a new buffer, written in place:
1645        // one allocation per batch, and only for bulk-ingested segments
1646        // (`global != 0`).
1647        // SAFETY: the loop writes one 8-byte seqno per row, and `len` is
1648        // exactly `row_count * 8`, which the source column matches, so every
1649        // byte is initialized before the buffer is frozen and read. An early
1650        // return on overflow drops the builder unread.
1651        #[expect(unsafe_code, reason = "see safety")]
1652        let mut out = unsafe { crate::Slice::builder_unzeroed(len) };
1653        for (row, (dst, src)) in out
1654            .as_chunks_mut::<8>()
1655            .0
1656            .iter_mut()
1657            .zip(col.data.as_chunks::<8>().0)
1658            .enumerate()
1659        {
1660            let Some(effective) = u64::from_le_bytes(*src).checked_add(global) else {
1661                // The rows before this one were already rewritten.
1662                #[cfg(feature = "metrics")]
1663                self.metrics.record_gather(row * 8);
1664                #[cfg(not(feature = "metrics"))]
1665                let _ = row;
1666                return Err(Error::InvalidHeader(
1667                    "columnar_scan: effective seqno overflows",
1668                ));
1669            };
1670            *dst = effective.to_le_bytes();
1671        }
1672        col.data = crate::Slice::from(out.freeze());
1673        #[cfg(feature = "metrics")]
1674        self.metrics.record_gather(len);
1675        Ok(())
1676    }
1677
1678    /// The rows of `batch` that `mask` keeps, as the batch a singleton group
1679    /// yields, or `None` when it keeps none: every column but the `dropped`
1680    /// ones the scan decoded for itself, gathered once (shared as decoded when
1681    /// it keeps every row), with the seqno column
1682    /// written in the tree's global space as it is gathered (see
1683    /// [`Self::globalize_seqnos`]). The gather is charged.
1684    fn gather_kept(
1685        &self,
1686        batch: &ColumnBatch,
1687        mask: &[bool],
1688        dropped: &[u16],
1689        global: SeqNo,
1690    ) -> crate::Result<Option<ColumnBatch>> {
1691        let mut kept = Vec::with_capacity(mask.len());
1692        for (row, &keep) in (0..batch.row_count).zip(mask) {
1693            if keep {
1694                kept.push(row);
1695            }
1696        }
1697        if kept.is_empty() {
1698            return Ok(None);
1699        }
1700        let rows = batch.row_count as usize;
1701        // Every row kept: a column goes on as decoded, shared rather than
1702        // gathered, and only the seqno column is written anew.
1703        let all = kept.len() == rows;
1704        let mut copied = 0usize;
1705        let mut columns = Vec::with_capacity(batch.columns.len());
1706        for col in batch
1707            .columns
1708            .iter()
1709            .filter(|c| !dropped.contains(&c.column_id))
1710        {
1711            let written = if col.column_id == COL_SEQNO && global != 0 {
1712                let mut data = Vec::with_capacity(kept.len() * 8);
1713                for &row in &kept {
1714                    let effective = fixed_u64_row(&col.data, row)?.checked_add(global).ok_or(
1715                        Error::InvalidHeader("columnar_scan: effective seqno overflows"),
1716                    )?;
1717                    data.extend_from_slice(&effective.to_le_bytes());
1718                }
1719                crate::table::columnar::Column {
1720                    column_id: col.column_id,
1721                    type_tag: col.type_tag,
1722                    validity: col
1723                        .validity
1724                        .as_deref()
1725                        .map(|bits| take_validity(bits, &kept)),
1726                    data: crate::Slice::from(data),
1727                }
1728            } else if all {
1729                columns.push(col.clone());
1730                continue;
1731            } else {
1732                take_column(col, rows, &kept)?
1733            };
1734            copied += written.data.len() + written.validity.as_ref().map_or(0, Vec::len);
1735            columns.push(written);
1736        }
1737        // A subset of the batch's rows, whose count is a u32.
1738        let row_count = u32::try_from(kept.len()).unwrap_or(batch.row_count);
1739        let out = ColumnBatch { row_count, columns };
1740        #[cfg(feature = "metrics")]
1741        self.metrics.record_gather(copied);
1742        #[cfg(not(feature = "metrics"))]
1743        let _ = copied;
1744        Ok(Some(out))
1745    }
1746
1747    /// Singleton group: no cross-segment merge. When every row is visible and the
1748    /// range is unbounded, the per-SST projected scan streams verbatim (zero-copy
1749    /// column-skip). Otherwise a per-row mask drops rows that are seqno-invisible
1750    /// (when the snapshot straddles the segment) or outside the requested range
1751    /// (when the segment only partially overlaps it).
1752    fn open_singleton(
1753        &self,
1754        seg: &Segment,
1755        rts: Vec<(UserKey, UserKey, SeqNo)>,
1756        support: &mut PredicateSupport,
1757    ) -> crate::Result<GroupStream> {
1758        // Every path below hands the predicate to the segment's table, or
1759        // evaluates it before the seqno column is globalized, so it runs in
1760        // the table's LOCAL seqno coordinates: a seqno bound is translated by
1761        // the segment's base first. A predicate no row of the segment can
1762        // satisfy (a seqno range wholly below its base) rules the segment out.
1763        let predicate = if let Some(pred) = self.predicate.as_ref() {
1764            let Some(local) = localize(pred, seg.global) else {
1765                *support = (*support).min(pred.support(Some(TypeTag::Number(Number::U64_LE))));
1766                return Ok(GroupStream::Empty);
1767            };
1768            Some(local.into_owned())
1769        } else {
1770            None
1771        };
1772        // A segment that RECORDS deletions takes the dedup path even when its
1773        // keys are provably unique: a key whose single row is a tombstone would
1774        // otherwise stream through verbatim and surface a key the point read
1775        // calls absent. Deciding a run is where tombstones are consumed, and
1776        // that lives there. A visible RANGE tombstone routes there for the
1777        // same reason: covered rows must be suppressed, and that needs each
1778        // row's seqno, which the verbatim path never decodes.
1779        // So does a predicate over a declared field: a segment may lack its
1780        // column, which reads as its declaration only once the batch is read,
1781        // and a table's own predicate would find nothing to run on. A segment
1782        // whose values are read late never gets here (see `reads_late`).
1783        let predicate_on_declared = self.predicate.as_ref().is_some_and(|p| {
1784            self.fields
1785                .iter()
1786                .any(|f| f.column_id() == p.column_id && projection::is_declared(f))
1787        });
1788        if seg.may_dup || seg.records_deletions() || predicate_on_declared || !rts.is_empty() {
1789            return self.open_singleton_dedup(seg, rts, predicate);
1790        }
1791        let range_filter = !self.range_is_full();
1792        if seg.visibility == SeqnoVisibility::All && !range_filter {
1793            // Pushed down in local coordinates (translated above); the seqno
1794            // column is globalized only on the way out.
1795            let SegmentCursor { cursor, .. } =
1796                self.segment_cursor(seg, &self.projection, predicate.as_ref(), self.budget)?;
1797            return Ok(GroupStream::Singleton(Box::new(SingletonStream {
1798                cursor,
1799                global: seg.global,
1800                mode: SingletonMode::Verbatim,
1801            })));
1802        }
1803
1804        // Decode the columns the mask needs even when the caller did not project
1805        // them (dropped again at the end): the seqno column for partial-visibility
1806        // masking, the key column for range filtering.
1807        let partial = seg.visibility == SeqnoVisibility::Partial;
1808        let mut needed = Vec::new();
1809        if partial {
1810            needed.push(COL_SEQNO);
1811        }
1812        if range_filter {
1813            needed.push(COL_USER_KEY);
1814        }
1815        let (augmented, dropped) = self.augment(&needed);
1816        // Same local-coordinate pushdown as the verbatim path above.
1817        let SegmentCursor { cursor, .. } =
1818            self.segment_cursor(seg, &augmented, predicate.as_ref(), self.budget)?;
1819        Ok(GroupStream::Singleton(Box::new(SingletonStream {
1820            cursor,
1821            global: seg.global,
1822            mode: SingletonMode::Masked {
1823                partial,
1824                // Visible iff `local < threshold` (the snapshot in this
1825                // segment's local seqno space); `Partial` guarantees the
1826                // subtraction is in range.
1827                threshold: self.seqno.saturating_sub(seg.global),
1828                dropped,
1829            },
1830        })))
1831    }
1832
1833    /// The projection extended by the `needed` columns the caller did not
1834    /// project, in order, and the list of those added columns, which are the
1835    /// scan's own and dropped from what it yields.
1836    fn augment(&self, needed: &[u16]) -> (Vec<u16>, Vec<u16>) {
1837        let mut augmented = self.projection.clone();
1838        let mut dropped = Vec::new();
1839        for &column in needed {
1840            if !augmented.contains(&column) {
1841                augmented.push(column);
1842                dropped.push(column);
1843            }
1844        }
1845        (augmented, dropped)
1846    }
1847
1848    /// One batch of a singleton group masked by seqno visibility and the key
1849    /// range, or `None` when no row of it survives.
1850    fn mask_singleton_batch(
1851        &self,
1852        batch: &ColumnBatch,
1853        global: SeqNo,
1854        partial: bool,
1855        threshold: SeqNo,
1856        dropped: &[u16],
1857    ) -> crate::Result<Option<ColumnBatch>> {
1858        let cmp = self.comparator.as_ref();
1859        let range_filter = !self.range_is_full();
1860        {
1861            let seqno_col = if partial {
1862                Some(
1863                    batch
1864                        .columns
1865                        .iter()
1866                        .find(|c| c.column_id == COL_SEQNO)
1867                        .ok_or(Error::InvalidHeader(
1868                            "columnar_scan: partial-visibility batch missing the seqno column",
1869                        ))?,
1870                )
1871            } else {
1872                None
1873            };
1874            let key_col = if range_filter {
1875                Some(
1876                    batch
1877                        .columns
1878                        .iter()
1879                        .find(|c| c.column_id == COL_USER_KEY)
1880                        .ok_or(Error::InvalidHeader(
1881                            "columnar_scan: range-filtered batch missing the key column",
1882                        ))?,
1883                )
1884            } else {
1885                None
1886            };
1887
1888            let mut mask = Vec::with_capacity(batch.row_count as usize);
1889            for row in 0..batch.row_count {
1890                let seqno_ok = match seqno_col {
1891                    Some(seqno_col) => fixed_u64_row(&seqno_col.data, row)? < threshold,
1892                    None => true,
1893                };
1894                // Evaluate the range bound only when the row survived the seqno
1895                // gate (short-circuit), so a row's key is decoded only if needed.
1896                let keep = if !seqno_ok {
1897                    false
1898                } else if let Some(key_col) = key_col {
1899                    let key = bytes_column_row(&key_col.data, batch.row_count, row)?;
1900                    key_in_bounds(key, &self.lo, &self.hi, cmp)
1901                } else {
1902                    true
1903                };
1904                mask.push(keep);
1905            }
1906            self.gather_kept(batch, &mask, dropped, global)
1907        }
1908    }
1909
1910    /// Singleton whose segment can physically hold several MVCC versions of one
1911    /// key (`Segment::may_dup`): every version is a physical row, so the scan
1912    /// must keep only the newest VISIBLE version per key instead of streaming
1913    /// the segment verbatim. Rows are stored in internal-key order (key
1914    /// ascending, seqno descending within a key), so within each key run the
1915    /// invisible too-new versions come first and the first visible row is the
1916    /// newest visible version; a run can span batch boundaries, so the last
1917    /// kept key carries across batches. The predicate runs AFTER dedup
1918    /// (mirroring the merge of an overlapping group, [`merge::MergeStream`]): a
1919    /// key whose newest version fails the
1920    /// predicate is dropped, never served from an older matching version —
1921    /// which also rules out predicate-driven zone-map block-skip here.
1922    ///
1923    /// `predicate` is the scan's, in this segment's local coordinates: it runs
1924    /// before the seqno column is globalized.
1925    fn open_singleton_dedup(
1926        &self,
1927        seg: &Segment,
1928        rts: Vec<(UserKey, UserKey, SeqNo)>,
1929        predicate: Option<ColumnRangePredicate>,
1930    ) -> crate::Result<GroupStream> {
1931        // Decode the columns the dedup needs even when the caller did not
1932        // project them (dropped again at the end): the key column always, the
1933        // seqno column when the snapshot straddles the segment OR a range
1934        // tombstone needs each row's age, the predicate column for the
1935        // after-dedup filter.
1936        let partial = seg.visibility == SeqnoVisibility::Partial;
1937        let mut needed = vec![COL_USER_KEY];
1938        if partial || !rts.is_empty() {
1939            needed.push(COL_SEQNO);
1940        }
1941        if let Some(pred) = &predicate {
1942            needed.push(pred.column_id);
1943        }
1944        // A deletion is what a key's newest row can BE, so deciding a run needs
1945        // the value type — otherwise a tombstone decides the run and is emitted
1946        // as a row while a point read calls the key absent, and a caller that did
1947        // not project the type column cannot tell that row from a live one with
1948        // an empty value. Decoded only for a segment that RECORDS deletions; one
1949        // without them keeps its columns untouched.
1950        let deletes = seg.records_deletions();
1951        if deletes {
1952            needed.push(COL_VALUE_TYPE);
1953        }
1954        let (augmented, dropped) = self.augment(&needed);
1955        // No predicate pushed down: it runs after the dedup (see above).
1956        let SegmentCursor { cursor, .. } =
1957            self.segment_cursor(seg, &augmented, None, self.budget)?;
1958        Ok(GroupStream::Singleton(Box::new(SingletonStream {
1959            cursor,
1960            global: seg.global,
1961            mode: SingletonMode::Dedup(DedupState {
1962                predicate,
1963                rts: TombstoneSweep::new(rts, self.comparator.as_ref()),
1964                partial,
1965                deletes,
1966                // Visible iff `local < threshold` (the snapshot in this
1967                // segment's local seqno space); `Partial` guarantees the
1968                // subtraction is in range.
1969                threshold: self.seqno.saturating_sub(seg.global),
1970                dropped,
1971                last_key: None,
1972            }),
1973        })))
1974    }
1975
1976    /// One batch of a singleton group deduped to the newest visible version of
1977    /// each key, deletions consumed and the predicate applied after, or `None`
1978    /// when no row of it survives. The key run it last decided is carried in
1979    /// `state` across batches.
1980    fn dedup_singleton_batch(
1981        &self,
1982        batch: &ColumnBatch,
1983        global: SeqNo,
1984        state: &mut DedupState,
1985        support: &mut PredicateSupport,
1986    ) -> crate::Result<Option<ColumnBatch>> {
1987        let (partial, deletes, threshold) = (state.partial, state.deletes, state.threshold);
1988        let seqno_needed = partial || !state.rts.is_empty();
1989        let range_filter = !self.range_is_full();
1990        let cmp = self.comparator.as_ref();
1991        {
1992            let key_col = batch
1993                .columns
1994                .iter()
1995                .find(|c| c.column_id == COL_USER_KEY)
1996                .ok_or(Error::InvalidHeader(
1997                    "columnar_scan: dedup batch missing the key column",
1998                ))?;
1999            let vt_col = if deletes {
2000                Some(
2001                    batch
2002                        .columns
2003                        .iter()
2004                        .find(|c| c.column_id == COL_VALUE_TYPE)
2005                        .ok_or(Error::InvalidHeader(
2006                            "columnar_scan: dedup batch missing the value-type column",
2007                        ))?,
2008                )
2009            } else {
2010                None
2011            };
2012            let seqno_col = if seqno_needed {
2013                Some(
2014                    batch
2015                        .columns
2016                        .iter()
2017                        .find(|c| c.column_id == COL_SEQNO)
2018                        .ok_or(Error::InvalidHeader(
2019                            "columnar_scan: dedup batch missing the seqno column",
2020                        ))?,
2021                )
2022            } else {
2023                None
2024            };
2025
2026            // The predicate judges the deduped survivors only (see doc): a row
2027            // is judged where it survives the dedup, into the same mask, so the
2028            // rows are gathered once.
2029            if let Some(pred) = &state.predicate {
2030                *support = (*support).min(pred.support_in(batch));
2031            }
2032            let matcher = state
2033                .predicate
2034                .as_ref()
2035                .filter(|p| p.apply == PredicateApply::Filter)
2036                .map(|p| p.matcher(batch));
2037            let mut mask = Vec::with_capacity(batch.row_count as usize);
2038            for row in 0..batch.row_count {
2039                let local = match seqno_col {
2040                    Some(seqno_col) => Some(fixed_u64_row(&seqno_col.data, row)?),
2041                    None => None,
2042                };
2043                let visible = !partial || local.is_some_and(|l| l < threshold);
2044                if !visible {
2045                    mask.push(false);
2046                    continue;
2047                }
2048                let key = bytes_column_row(&key_col.data, batch.row_count, row)?;
2049                if state
2050                    .last_key
2051                    .as_deref()
2052                    .is_some_and(|last| cmp.compare(last, key) == core::cmp::Ordering::Equal)
2053                {
2054                    // A later visible version of an already-decided key run —
2055                    // shadowed by the newest visible version above it.
2056                    mask.push(false);
2057                    continue;
2058                }
2059                // First visible row of a new key run = the newest visible
2060                // version. Deciding the run here (even when the range filter or
2061                // a deletion drops the row) also drops its older versions above.
2062                let last = state.last_key.get_or_insert_with(Vec::new);
2063                last.clear();
2064                last.extend_from_slice(key);
2065                // A visible range tombstone deletes the run when it covers the
2066                // NEWEST visible version (older versions are older still); an
2067                // uncovered newest version shadows the covered older ones, so
2068                // deciding on it alone is exact.
2069                if !state.rts.is_empty() {
2070                    let eff =
2071                        local
2072                            .unwrap_or(0)
2073                            .checked_add(global)
2074                            .ok_or(Error::InvalidHeader(
2075                                "columnar_scan: effective seqno overflows",
2076                            ))?;
2077                    if state.rts.covers(key, eff, cmp) {
2078                        mask.push(false);
2079                        continue;
2080                    }
2081                }
2082                if let Some(vt_col) = vt_col {
2083                    let byte = *vt_col.data.get(row as usize).ok_or(Error::InvalidHeader(
2084                        "columnar_scan: value-type column shorter than the row count",
2085                    ))?;
2086                    let value_type = crate::ValueType::try_from(byte)
2087                        .map_err(|()| Error::InvalidTag(("ValueType", byte)))?;
2088                    if value_type.is_tombstone() {
2089                        // The key is GONE as of this row, so the run yields
2090                        // nothing: emitting the tombstone would surface a key the
2091                        // point read reports absent.
2092                        mask.push(false);
2093                        continue;
2094                    }
2095                }
2096                mask.push(
2097                    (!range_filter || key_in_bounds(key, &self.lo, &self.hi, cmp))
2098                        && matcher.as_ref().is_none_or(|m| m.matches(row)),
2099                );
2100            }
2101
2102            // Match the singleton contract: yield exactly the projected columns.
2103            self.gather_kept(batch, &mask, &state.dropped, global)
2104        }
2105    }
2106}
2107
2108/// `pred` in the LOCAL coordinates of a segment based at `global`, or `None`
2109/// when no row of that segment can match it.
2110///
2111/// A segment's table stores each row's seqno locally (a bulk-ingested one at
2112/// `0`, with one `global_seqno` for all of them) and the scan speaks effective
2113/// ones (`local + global`), so a bound on the seqno column is moved down by
2114/// `global` before the table's statistics or its row filter see it. A predicate
2115/// on any other column is passed as it is.
2116fn localize(pred: &ColumnRangePredicate, global: SeqNo) -> Option<Cow<'_, ColumnRangePredicate>> {
2117    if pred.column_id != COL_SEQNO || global == 0 {
2118        return Some(Cow::Borrowed(pred));
2119    }
2120    // An unsigned column's ordinal is its value.
2121    let (lo, hi) = pred.ordinal_span(Number::U64_LE)?;
2122    let global = u128::from(global);
2123    // Every row of the segment is at `global` or above: an upper bound below
2124    // it admits none, and a lower bound below it admits them from the first,
2125    // which is exactly the clamp to local `0`.
2126    let hi = hi.checked_sub(global)?;
2127    let lo = lo.saturating_sub(global);
2128    // Both are below `2^64`: the span is of 8-byte values.
2129    let bound = |v: u128| u64::try_from(v).map(|v| v.to_be_bytes().to_vec());
2130    Some(Cow::Owned(ColumnRangePredicate {
2131        column_id: COL_SEQNO,
2132        lower: Some(bound(lo).ok()?),
2133        upper: Some(bound(hi).ok()?),
2134        apply: pred.apply,
2135    }))
2136}
2137
2138/// Whether `key` lies within the requested `[lo, hi]` key bounds, per the tree
2139/// comparator. An unbounded side never excludes; the inclusive / exclusive sense
2140/// of each bound matches the `RangeBounds` the caller passed.
2141fn key_in_bounds(
2142    key: &[u8],
2143    lo: &Bound<UserKey>,
2144    hi: &Bound<UserKey>,
2145    cmp: &dyn UserComparator,
2146) -> bool {
2147    use core::cmp::Ordering;
2148    let above_lo = match lo {
2149        Bound::Unbounded => true,
2150        Bound::Included(k) => cmp.compare(key, k.as_ref()) != Ordering::Less,
2151        Bound::Excluded(k) => cmp.compare(key, k.as_ref()) == Ordering::Greater,
2152    };
2153    let below_hi = match hi {
2154        Bound::Unbounded => true,
2155        Bound::Included(k) => cmp.compare(key, k.as_ref()) != Ordering::Greater,
2156        Bound::Excluded(k) => cmp.compare(key, k.as_ref()) == Ordering::Less,
2157    };
2158    above_lo && below_hi
2159}
2160
2161impl Iterator for ColumnarScan {
2162    type Item = crate::Result<ColumnBatch>;
2163
2164    fn next(&mut self) -> Option<Self::Item> {
2165        loop {
2166            let mut support = self.support;
2167            if let Some(mut stream) = self.current.take() {
2168                let next = self.next_from(&mut stream, &mut support);
2169                self.support = support;
2170                self.observe_payload(stream.held_bytes());
2171                self.record_oversized(stream.take_oversized());
2172                match next {
2173                    Some(Ok(batch)) => {
2174                        self.current = Some(stream);
2175                        #[cfg(feature = "metrics")]
2176                        self.metrics.record_materialized(batch.data_size());
2177                        return Some(Ok(batch));
2178                    }
2179                    // A failed group yields its error and is dropped; the next
2180                    // call moves on to the next group.
2181                    Some(Err(e)) => return Some(Err(e)),
2182                    None => continue,
2183                }
2184            }
2185            let group = self.groups.pop_front()?;
2186            let opened = self.open_group(&group, &mut support);
2187            self.support = support;
2188            match opened {
2189                Ok(stream) => self.current = Some(stream),
2190                Err(e) => return Some(Err(e)),
2191            }
2192        }
2193    }
2194}
2195
2196/// Clones a borrowed key bound into an owned one.
2197fn clone_bound(bound: Bound<&UserKey>) -> Bound<UserKey> {
2198    match bound {
2199        Bound::Included(k) => Bound::Included(k.clone()),
2200        Bound::Excluded(k) => Bound::Excluded(k.clone()),
2201        Bound::Unbounded => Bound::Unbounded,
2202    }
2203}
2204
2205/// Borrows an owned key bound as a byte-slice bound for key-range overlap checks.
2206fn bound_as_ref(bound: &Bound<UserKey>) -> Bound<&[u8]> {
2207    match bound {
2208        Bound::Included(k) => Bound::Included(k.as_ref()),
2209        Bound::Excluded(k) => Bound::Excluded(k.as_ref()),
2210        Bound::Unbounded => Bound::Unbounded,
2211    }
2212}
2213
2214#[cfg(all(test, feature = "metrics"))]
2215mod tests;