Skip to main content

mongreldb_core/
memtable.rs

1//! In-memory write buffer (the "memtable").
2//!
3//! Phase 11.2 wires the buffered [`crate::be_tree::BeTree`] (a Bε-tree over the
4//! composite `(RowId, Epoch)` version key) in as the live memtable, replacing
5//! the prototype skip list. A Bε-tree buffers many pending mutations per
6//! internal node and flushes them to one child in bulk, so write amplification
7//! approaches O(1) — the update-amplification win the design calls for. The
8//! composite key keeps multiple versions of a logical row coexisting. Product
9//! visibility prefers HLC via [`crate::epoch::Snapshot::observes_row`] /
10//! [`crate::epoch::Snapshot::version_is_newer`] when versions carry `commit_ts`
11//! (P0.5-T3); epoch-only APIs remain for dual-model legacy call sites.
12
13use crate::be_tree::{BeTree, BeTreeVersionCursor, BeTreeVersionCursorStats};
14use crate::epoch::{Epoch, Snapshot};
15use crate::rowid::RowId;
16use serde::{Deserialize, Serialize};
17use std::borrow::Cow;
18use std::cmp::Ordering;
19use std::collections::{BTreeMap, BinaryHeap, HashMap};
20use std::sync::Arc;
21
22/// A cell value in the in-memory path. The flush path re-encodes these into
23/// columnar pages; it is intentionally simple for the prototype.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub enum Value {
26    Null,
27    Bool(bool),
28    Int64(i64),
29    Float64(f64),
30    Bytes(Vec<u8>),
31    Embedding(Vec<f32>),
32    /// Unscaled decimal value (i128). The column's `TypeId::Decimal128`
33    /// carries the precision/scale for formatting.
34    Decimal(i128),
35    /// SQL INTERVAL value: months, days, nanoseconds.
36    Interval {
37        months: i64,
38        days: i32,
39        nanos: i64,
40    },
41    /// RFC 4122 UUID (16 bytes, big-endian for sort order).
42    Uuid([u8; 16]),
43    /// JSON value stored as a UTF-8 byte sequence.
44    Json(Vec<u8>),
45    /// Generated embedding with durable source and model provenance.
46    ///
47    /// Kept last so existing bincode enum discriminants remain stable.
48    GeneratedEmbedding(Box<crate::embedding::GeneratedEmbeddingValue>),
49}
50
51impl Value {
52    pub fn as_embedding(&self) -> Option<&[f32]> {
53        match self {
54            Self::Embedding(values) => Some(values),
55            Self::GeneratedEmbedding(value) => Some(&value.vector),
56            _ => None,
57        }
58    }
59
60    pub fn generated_embedding_metadata(
61        &self,
62    ) -> Option<&crate::embedding::GeneratedEmbeddingMetadata> {
63        match self {
64            Self::GeneratedEmbedding(value) => Some(&value.metadata),
65            _ => None,
66        }
67    }
68
69    /// Lexicographically-comparable byte encoding for index keys (PK HOT,
70    /// bitmaps). Big-endian for integers so byte order matches value order.
71    pub fn encode_key(&self) -> Vec<u8> {
72        match self {
73            Value::Null => Vec::new(),
74            Value::Bool(b) => vec![*b as u8],
75            Value::Int64(n) => n.to_be_bytes().to_vec(),
76            Value::Float64(f) => f.to_bits().to_be_bytes().to_vec(),
77            Value::Bytes(b) => b.clone(),
78            Value::Embedding(v) => {
79                let mut out = Vec::with_capacity(v.len() * 4);
80                for x in v {
81                    out.extend_from_slice(&x.to_bits().to_be_bytes());
82                }
83                out
84            }
85            Value::GeneratedEmbedding(value) => {
86                let mut out = Vec::with_capacity(value.vector.len() * 4);
87                for x in &value.vector {
88                    out.extend_from_slice(&x.to_bits().to_be_bytes());
89                }
90                out
91            }
92            Value::Decimal(d) => d.to_be_bytes().to_vec(),
93            Value::Interval {
94                months,
95                days,
96                nanos,
97            } => {
98                let mut out = Vec::with_capacity(20);
99                out.extend_from_slice(&months.to_be_bytes());
100                out.extend_from_slice(&days.to_be_bytes());
101                out.extend_from_slice(&nanos.to_be_bytes());
102                out
103            }
104            Value::Uuid(b) => b.to_vec(),
105            Value::Json(b) => b.clone(),
106        }
107    }
108
109    pub(crate) fn estimated_bytes(&self) -> u64 {
110        match self {
111            Value::Null => 1,
112            Value::Bool(_) => 1,
113            Value::Int64(_) | Value::Float64(_) => 8,
114            Value::Bytes(bytes) | Value::Json(bytes) => 16 + bytes.len() as u64,
115            Value::Embedding(values) => 16 + (values.len() as u64) * 4,
116            Value::GeneratedEmbedding(value) => {
117                16 + (value.vector.len() as u64) * 4
118                    + value.metadata.provider_id.len() as u64
119                    + value.metadata.model_id.len() as u64
120                    + value.metadata.model_version.len() as u64
121                    + value.metadata.preprocessing_version.len() as u64
122                    + 48
123            }
124            Value::Decimal(_) | Value::Uuid(_) => 16,
125            Value::Interval { .. } => 20,
126        }
127    }
128}
129
130/// One logical row held in the memtable. A `deleted` row is a tombstone.
131///
132/// Field order of the **bincode WAL `Put` payload** is fixed as
133/// `(row_id, committed_epoch, columns, deleted)` — the 0.63.1 layout.
134/// [`Self::commit_ts`] is in-memory only (`#[serde(skip)]`); durable HLC for
135/// WAL recovery is `Op::CommitTimestamp`, and sorted runs use the
136/// `SYS_COMMIT_TS` system column (with its own legacy-compatible path).
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct Row {
139    pub row_id: RowId,
140    pub committed_epoch: Epoch,
141    pub columns: HashMap<u16, Value>,
142    pub deleted: bool,
143    /// Optional HLC stamp (P0.5 dual-model). Not encoded in WAL `Put` bincode
144    /// payloads — see struct-level docs. Kept last so call sites and future
145    /// wire evolution treat the 0.63.1 fields as the stable prefix.
146    #[serde(skip)]
147    pub commit_ts: Option<mongreldb_types::hlc::HlcTimestamp>,
148}
149
150impl Row {
151    pub fn new(row_id: RowId, committed_epoch: Epoch) -> Self {
152        Self {
153            row_id,
154            committed_epoch,
155            columns: HashMap::new(),
156            deleted: false,
157            commit_ts: None,
158        }
159    }
160
161    pub fn new_with_hlc(
162        row_id: RowId,
163        committed_epoch: Epoch,
164        commit_ts: mongreldb_types::hlc::HlcTimestamp,
165    ) -> Self {
166        Self {
167            row_id,
168            committed_epoch,
169            columns: HashMap::new(),
170            deleted: false,
171            commit_ts: Some(commit_ts),
172        }
173    }
174
175    pub fn with_column(mut self, column_id: u16, value: Value) -> Self {
176        self.columns.insert(column_id, value);
177        self
178    }
179
180    /// Rough byte estimate for flush-threshold decisions.
181    pub fn estimated_bytes(&self) -> u64 {
182        self.columns
183            .values()
184            .fold(32, |bytes, value| bytes + value.estimated_bytes())
185    }
186}
187
188/// Same-`RowId` group members gathered between cooperative cancellation
189/// checkpoints inside the memtable merge cursor (REM-C §7.11).
190const CURSOR_GROUP_CHECKPOINT_INTERVAL: usize = 256;
191
192/// Min-heap key used to merge memtable leaf streams in ascending
193/// `(RowId, Epoch)` order. We carry the version's epoch too so that the
194/// dedup pass at emit time has it without re-matching on `Cow`.
195struct MemHead<'a> {
196    rid: RowId,
197    epoch: Epoch,
198    index: u32,
199    row: Cow<'a, Row>,
200}
201
202impl PartialEq for MemHead<'_> {
203    fn eq(&self, other: &Self) -> bool {
204        (self.rid, self.epoch, self.index) == (other.rid, other.epoch, other.index)
205    }
206}
207impl Eq for MemHead<'_> {}
208impl PartialOrd for MemHead<'_> {
209    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
210        Some(self.cmp(other))
211    }
212}
213impl Ord for MemHead<'_> {
214    fn cmp(&self, other: &Self) -> Ordering {
215        (other.rid, other.epoch, other.index).cmp(&(self.rid, self.epoch, self.index))
216    }
217}
218
219/// K-way merge over the per-segment lazy [`BeTreeVersionCursor`] streams
220/// (REM-C §7.12). Holds one heap head per segment, pops the lowest `RowId`,
221/// gathers every head sharing that `RowId`, and emits the newest visible
222/// version exactly once per `RowId`. Candidates arrive oldest-first
223/// (ascending epoch; exact-key ties in physical write order — older segment
224/// index first, within a segment the tree's own write order) and the fold
225/// resolves ties with `epoch::version_supersedes`, so the later physical
226/// write wins an exact-stamp tie. No segment ever materializes more than
227/// its current head plus the current same-row group.
228pub struct MemtableVisibleVersionCursor<'a> {
229    segments: Vec<BeTreeVersionCursor<'a>>,
230    heap: BinaryHeap<MemHead<'a>>,
231    snapshot: Snapshot,
232    /// Heap heads are pulled from each segment lazily on the first advance
233    /// so that construction is O(segments) and a cancelled control is
234    /// observed before any version streams.
235    primed: bool,
236    finished: bool,
237    /// Number of source versions examined during the last advance — the
238    /// dedup pass that picks the newest visible version per RowId.
239    pub(crate) last_examined: usize,
240    /// Peak `last_examined` across all calls so far.
241    pub(crate) peak_examined: usize,
242}
243
244impl<'a> MemtableVisibleVersionCursor<'a> {
245    /// Controlled advance: checkpoints the supplied control while gathering
246    /// large same-`RowId` groups (§7.11) and threads it into the per-segment
247    /// Bε-tree cursors.
248    #[doc(hidden)] // streaming-cursor plumbing; used by the engine and tests
249    pub fn next_controlled(
250        &mut self,
251        control: &crate::ExecutionControl,
252    ) -> crate::Result<Option<(RowId, Epoch, Cow<'a, Row>)>> {
253        self.advance_impl(Some(control))
254    }
255
256    /// Aggregate Bε-tree cursor statistics across every segment source
257    /// (REM-C §7.6 test instrumentation).
258    #[doc(hidden)] // test instrumentation; not a stable public API
259    pub fn be_tree_cursor_stats(&self) -> BeTreeVersionCursorStats {
260        let mut total = BeTreeVersionCursorStats::default();
261        for segment in &self.segments {
262            let stats = segment.stats();
263            total.active_frames += stats.active_frames;
264            total.peak_active_frames = total.peak_active_frames.max(stats.peak_active_frames);
265            total.buffered_messages_owned += stats.buffered_messages_owned;
266            total.peak_buffered_messages_owned = total
267                .peak_buffered_messages_owned
268                .max(stats.peak_buffered_messages_owned);
269            total.total_versions_precollected += stats.total_versions_precollected;
270            total.versions_examined += stats.versions_examined;
271            total.checkpoints += stats.checkpoints;
272        }
273        total
274    }
275
276    fn segment_next(
277        &mut self,
278        index: usize,
279        control: Option<&crate::ExecutionControl>,
280    ) -> crate::Result<Option<Cow<'a, Row>>> {
281        match control {
282            Some(control) => self.segments[index].next_controlled(control),
283            None => Ok(self.segments[index].next()),
284        }
285    }
286
287    fn push_head(&mut self, index: usize, row: Cow<'a, Row>) {
288        self.heap.push(MemHead {
289            rid: row.row_id,
290            epoch: row.committed_epoch,
291            index: index as u32,
292            row,
293        });
294    }
295
296    fn prime(&mut self, control: Option<&crate::ExecutionControl>) -> crate::Result<()> {
297        for index in 0..self.segments.len() {
298            if let Some(row) = self.segment_next(index, control)? {
299                self.push_head(index, row);
300            }
301        }
302        Ok(())
303    }
304
305    fn advance_impl(
306        &mut self,
307        control: Option<&crate::ExecutionControl>,
308    ) -> crate::Result<Option<(RowId, Epoch, Cow<'a, Row>)>> {
309        if self.finished {
310            return Ok(None);
311        }
312        if !self.primed {
313            self.prime(control)?;
314            self.primed = true;
315        }
316        self.last_examined = 0;
317        loop {
318            let Some(MemHead {
319                rid,
320                epoch,
321                index,
322                row,
323            }) = self.heap.pop()
324            else {
325                self.finished = true;
326                return Ok(None);
327            };
328            self.last_examined += 1;
329            // Advance the source the popped head came from.
330            if let Some(next) = self.segment_next(index as usize, control)? {
331                self.push_head(index as usize, next);
332            }
333            if !self.snapshot.observes_version(epoch, row.commit_ts) {
334                continue;
335            }
336            // Gather every remaining head that shares this rid; pick the
337            // newest visible version among them. Cancellation is observed
338            // inside large same-row groups (§7.11).
339            let mut best = Some(row);
340            let mut gathered = 0usize;
341            while self.heap.peek().is_some_and(|h| h.rid == rid) {
342                gathered += 1;
343                if gathered.is_multiple_of(CURSOR_GROUP_CHECKPOINT_INTERVAL) {
344                    if let Some(control) = control {
345                        control.checkpoint()?;
346                    }
347                }
348                let head = self.heap.pop().unwrap();
349                self.last_examined += 1;
350                if let Some(next) = self.segment_next(head.index as usize, control)? {
351                    self.push_head(head.index as usize, next);
352                }
353                if self
354                    .snapshot
355                    .observes_version(head.epoch, head.row.commit_ts)
356                    && best.as_ref().is_none_or(|current| {
357                        // Candidates arrive oldest-first (ascending epoch,
358                        // ties broken by physical write order); an exact
359                        // stamp tie goes to the later write.
360                        crate::epoch::version_supersedes(
361                            head.epoch,
362                            head.row.commit_ts,
363                            current.committed_epoch,
364                            current.commit_ts,
365                        )
366                    })
367                {
368                    best = Some(head.row);
369                }
370            }
371            let Some(best) = best else {
372                continue;
373            };
374            let row_id = best.row_id;
375            let epoch = best.committed_epoch;
376            self.peak_examined = self.peak_examined.max(self.last_examined);
377            return Ok(Some((row_id, epoch, best)));
378        }
379    }
380}
381
382impl<'a> Iterator for MemtableVisibleVersionCursor<'a> {
383    type Item = (RowId, Epoch, Cow<'a, Row>);
384
385    fn next(&mut self) -> Option<Self::Item> {
386        match self.advance_impl(None) {
387            Ok(item) => item,
388            Err(_) => unreachable!("the no-cancellation path cannot fail"),
389        }
390    }
391}
392
393impl<'a> MemtableVisibleVersionCursor<'a> {
394    /// Cursor is exhausted when the heap is empty.
395    pub fn is_exhausted(&self) -> bool {
396        self.finished
397    }
398}
399
400/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
401/// for the prototype skip list: the same MVCC semantics with lower write
402/// amplification (buffered messages flush to children in bulk).
403#[derive(Clone)]
404struct MemtableSegment {
405    tree: BeTree,
406    byte_size: u64,
407}
408
409/// Structurally shared committed overlays plus one small mutable write delta.
410#[derive(Clone)]
411pub struct Memtable {
412    frozen: Arc<Vec<Arc<MemtableSegment>>>,
413    active: MemtableSegment,
414    byte_size: u64,
415}
416
417impl Default for Memtable {
418    fn default() -> Self {
419        Self::new()
420    }
421}
422
423impl Memtable {
424    pub fn new() -> Self {
425        Self {
426            frozen: Arc::new(Vec::new()),
427            active: MemtableSegment {
428                tree: BeTree::new(),
429                byte_size: 0,
430            },
431            byte_size: 0,
432        }
433    }
434
435    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
436    /// never overwritten; the newest visible one wins at read time.
437    pub fn upsert(&mut self, row: Row) {
438        let bytes = row.estimated_bytes();
439        self.byte_size = self.byte_size.saturating_add(bytes);
440        self.active.byte_size = self.active.byte_size.saturating_add(bytes);
441        self.active.tree.insert_row(row);
442    }
443
444    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
445    /// the columns from the newest live version so that engine-level HOT cleanup
446    /// can recover the primary-key value during WAL replay.
447    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
448        let mut columns = HashMap::new();
449        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
450            columns = live.columns;
451        }
452        let row = Row {
453            row_id,
454            committed_epoch: epoch,
455            columns,
456            deleted: true,
457            commit_ts: None,
458        };
459        self.upsert(row);
460    }
461
462    /// Read the row at `row_id` visible to `snapshot`: the newest version with
463    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
464    /// such version exists).
465    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
466        self.get_version(row_id, snapshot_epoch)
467            .and_then(|(_, row)| (!row.deleted).then_some(row))
468    }
469
470    /// Newest version of `row_id` with `epoch <= snapshot`, **including
471    /// tombstones** (as a `Row` with `deleted=true`). Legacy epoch-only entry
472    /// point; prefer [`Self::get_version_at`] when the caller holds a full
473    /// [`crate::epoch::Snapshot`].
474    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
475        self.get_version_at(row_id, crate::epoch::Snapshot::at(snapshot_epoch))
476    }
477
478    /// Newest version of `row_id` visible under `snapshot` (including
479    /// tombstones). Uses HLC authority when stamps are present (P0.5-T3).
480    ///
481    /// Seeks each segment's composite-key range for `row_id` so dual-model
482    /// mixes (stamped + unstamped) and HLC/epoch order inversions stay correct
483    /// without materializing every version in the memtable.
484    pub fn get_version_at(
485        &self,
486        row_id: RowId,
487        snapshot: crate::epoch::Snapshot,
488    ) -> Option<(Epoch, Row)> {
489        if !snapshot.uses_hlc_authority() {
490            // Newest segment first (active, then frozen in reverse): on an
491            // exact stamp tie the physically newer segment wins — within one
492            // segment ties are already resolved by the tree itself.
493            let mut best = self.active.tree.get_version(row_id, snapshot.epoch);
494            for segment in self.frozen.iter().rev() {
495                let Some(candidate) = segment.tree.get_version(row_id, snapshot.epoch) else {
496                    continue;
497                };
498                if best.as_ref().is_none_or(|(epoch, _)| candidate.0 > *epoch) {
499                    best = Some(candidate);
500                }
501            }
502            return best;
503        }
504
505        let mut best: Option<Row> = None;
506        for segment in self
507            .frozen
508            .iter()
509            .map(|segment| &segment.tree)
510            .chain(std::iter::once(&self.active.tree))
511        {
512            segment.visit_versions(row_id, |row| {
513                if !snapshot.observes_row(row.committed_epoch, row.commit_ts) {
514                    return;
515                }
516                // Candidates arrive in physical write order (oldest first);
517                // the later write wins an exact stamp tie.
518                if best.as_ref().is_none_or(|current| {
519                    crate::epoch::version_supersedes(
520                        row.committed_epoch,
521                        row.commit_ts,
522                        current.committed_epoch,
523                        current.commit_ts,
524                    )
525                }) {
526                    best = Some(row);
527                }
528            });
529        }
530        best.map(|row| (row.committed_epoch, row))
531    }
532
533    /// Number of stored versions.
534    pub fn len(&self) -> usize {
535        self.active.tree.mutations()
536            + self
537                .frozen
538                .iter()
539                .map(|segment| segment.tree.mutations())
540                .sum::<usize>()
541    }
542
543    pub fn is_empty(&self) -> bool {
544        self.active.tree.is_empty() && self.frozen.is_empty()
545    }
546
547    pub fn approx_bytes(&self) -> u64 {
548        self.byte_size
549    }
550
551    /// Visible rows at `snapshot`, deduplicated to the newest version per
552    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
553    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
554        self.visible_versions(snapshot_epoch)
555            .into_iter()
556            .filter(|r| !r.deleted)
557            .collect()
558    }
559
560    /// Newest visible version per `RowId` at `snapshot`, **including
561    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
562    /// versions across the memtable and sorted runs.
563    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
564        self.visible_versions_at(crate::epoch::Snapshot::at(snapshot_epoch))
565    }
566
567    pub fn visible_versions_at(&self, snapshot: crate::epoch::Snapshot) -> Vec<Row> {
568        self.newest_visible_map(snapshot).into_values().collect()
569    }
570
571    /// Newest visible version per `RowId` as an ordered map (ascending RowId).
572    /// Callers that need to stream into a controlled merge without a second
573    /// full `Vec` should drain this map in batches rather than collecting.
574    pub(crate) fn newest_visible_map(
575        &self,
576        snapshot: crate::epoch::Snapshot,
577    ) -> BTreeMap<RowId, Row> {
578        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
579        // Oldest segment first, and within a segment oldest write first, so
580        // `version_supersedes` can hand an exact stamp tie to the later
581        // physical write (newer segment / newer entry wins).
582        for segment in self
583            .frozen
584            .iter()
585            .map(|segment| &segment.tree)
586            .chain(std::iter::once(&self.active.tree))
587        {
588            for row in segment.versions() {
589                if !snapshot.observes_version(row.committed_epoch, row.commit_ts) {
590                    continue;
591                }
592                by_row
593                    .entry(row.row_id)
594                    .and_modify(|existing| {
595                        if crate::epoch::version_supersedes(
596                            row.committed_epoch,
597                            row.commit_ts,
598                            existing.committed_epoch,
599                            existing.commit_ts,
600                        ) {
601                            *existing = row.clone();
602                        }
603                    })
604                    .or_insert(row);
605            }
606        }
607        by_row
608    }
609
610    pub fn newest_visible_iter<'a>(
611        &'a self,
612        snapshot: &Snapshot,
613    ) -> MemtableVisibleVersionCursor<'a> {
614        let segments: Vec<BeTreeVersionCursor<'a>> = self
615            .frozen
616            .iter()
617            .map(|segment| segment.tree.leaf_versions_iter())
618            .chain(std::iter::once(self.active.tree.leaf_versions_iter()))
619            .collect();
620        MemtableVisibleVersionCursor {
621            segments,
622            heap: BinaryHeap::new(),
623            snapshot: *snapshot,
624            primed: false,
625            finished: false,
626            last_examined: 0,
627            peak_examined: 0,
628        }
629    }
630
631    /// Freeze the current write delta so future clones share it by `Arc`.
632    pub(crate) fn seal(&mut self) {
633        if self.active.tree.is_empty() {
634            return;
635        }
636        let active = std::mem::replace(
637            &mut self.active,
638            MemtableSegment {
639                tree: BeTree::new(),
640                byte_size: 0,
641            },
642        );
643        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
644        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
645            self.consolidate();
646        }
647    }
648
649    fn consolidate(&mut self) {
650        let mut tree = BeTree::new();
651        for row in self
652            .frozen
653            .iter()
654            .flat_map(|segment| segment.tree.versions())
655        {
656            tree.insert_row(row);
657        }
658        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
659            tree,
660            byte_size: self.byte_size,
661        })]);
662    }
663
664    #[cfg(test)]
665    pub(crate) fn frozen_layer_count(&self) -> usize {
666        self.frozen.len()
667    }
668
669    /// Drain all versions (for a memtable-to-run flush). Returns them in
670    /// ascending `(RowId, Epoch)` order.
671    pub fn drain_sorted(&mut self) -> Vec<Row> {
672        let mut out = self
673            .frozen
674            .iter()
675            .flat_map(|segment| segment.tree.versions())
676            .chain(self.active.tree.versions())
677            .collect::<Vec<_>>();
678        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
679        self.frozen = Arc::new(Vec::new());
680        self.active = MemtableSegment {
681            tree: BeTree::new(),
682            byte_size: 0,
683        };
684        self.byte_size = 0;
685        out
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692
693    fn row(id: u64, epoch: u64) -> Row {
694        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
695    }
696
697    #[test]
698    fn upsert_get_and_visibility() {
699        let mut m = Memtable::new();
700        m.upsert(row(1, 5));
701        assert_eq!(m.len(), 1);
702        assert!(m.get(RowId(1), Epoch(5)).is_some());
703        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
704        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
705    }
706
707    #[test]
708    fn tombstone_supersedes_at_its_epoch() {
709        let mut m = Memtable::new();
710        m.upsert(row(1, 1));
711        // Before the tombstone: the live version is visible.
712        assert!(m.get(RowId(1), Epoch(1)).is_some());
713        m.tombstone(RowId(1), Epoch(2));
714        // At/after the tombstone: hidden.
715        assert!(m.get(RowId(1), Epoch(2)).is_none());
716        assert!(m.get(RowId(1), Epoch(9)).is_none());
717        // A snapshot before the tombstone still sees the live version.
718        assert!(m.get(RowId(1), Epoch(1)).is_some());
719    }
720
721    #[test]
722    fn sealed_generations_share_rows_and_consolidate() {
723        let mut writer = Memtable::new();
724        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
725            writer.upsert(row(id, id + 1));
726            writer.seal();
727        }
728        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
729        let generation = writer.clone();
730        writer.upsert(row(99, 99));
731        assert!(generation.get(RowId(99), Epoch(99)).is_none());
732        assert!(writer.get(RowId(99), Epoch(99)).is_some());
733    }
734
735    #[test]
736    fn hlc_visibility_is_authoritative_when_stamped() {
737        use mongreldb_types::hlc::HlcTimestamp;
738        let mut m = Memtable::new();
739        let early = HlcTimestamp {
740            physical_micros: 100,
741            logical: 0,
742            node_tiebreaker: 1,
743        };
744        let late = HlcTimestamp {
745            physical_micros: 200,
746            logical: 0,
747            node_tiebreaker: 1,
748        };
749        let mut r1 = Row::new_with_hlc(RowId(1), Epoch(1), early);
750        r1.columns.insert(1, Value::Int64(1));
751        let mut r2 = Row::new_with_hlc(RowId(1), Epoch(2), late);
752        r2.columns.insert(1, Value::Int64(2));
753        m.upsert(r1);
754        m.upsert(r2);
755        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
756        let versions = m.visible_versions_at(snap);
757        assert_eq!(versions.len(), 1);
758        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
759        let snap2 = crate::epoch::Snapshot::at_hlc(Epoch(1), late);
760        assert_eq!(
761            m.visible_versions_at(snap2)[0].columns.get(&1),
762            Some(&Value::Int64(2))
763        );
764    }
765
766    #[test]
767    fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
768        use mongreldb_types::hlc::HlcTimestamp;
769        let mut m = Memtable::new();
770        let early = HlcTimestamp {
771            physical_micros: 100,
772            logical: 0,
773            node_tiebreaker: 1,
774        };
775        let late = HlcTimestamp {
776            physical_micros: 200,
777            logical: 0,
778            node_tiebreaker: 1,
779        };
780        // Epoch(1) with late HLC would win under epoch-only rules when snap
781        // epoch is 99 — HLC authority must hide it under an early pin.
782        let mut late_row = Row::new_with_hlc(RowId(1), Epoch(1), late);
783        late_row.columns.insert(1, Value::Int64(99));
784        let mut early_row = Row::new_with_hlc(RowId(1), Epoch(50), early);
785        early_row.columns.insert(1, Value::Int64(1));
786        m.upsert(late_row);
787        m.upsert(early_row);
788        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
789        let versions = m.visible_versions_at(snap);
790        assert_eq!(versions.len(), 1);
791        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
792        assert_eq!(versions[0].commit_ts, Some(early));
793    }
794
795    #[test]
796    fn epoch_only_snapshot_sees_hlc_stamped_rows_by_epoch() {
797        use mongreldb_types::hlc::HlcTimestamp;
798        let mut m = Memtable::new();
799        let ts = HlcTimestamp {
800            physical_micros: 50,
801            logical: 0,
802            node_tiebreaker: 1,
803        };
804        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), ts).with_column(1, Value::Int64(1)));
805        m.upsert(Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)));
806        let legacy = crate::epoch::Snapshot::at(Epoch(99));
807        let versions = m.visible_versions_at(legacy);
808        assert_eq!(
809            versions.len(),
810            2,
811            "dual-model: epoch pin sees HLC rows by epoch"
812        );
813        assert!(m.get_version_at(RowId(1), legacy).is_some());
814        assert!(m.get_version_at(RowId(2), legacy).is_some());
815        let future = crate::epoch::Snapshot::at(Epoch(0));
816        assert!(m.get_version_at(RowId(1), future).is_none());
817    }
818
819    #[test]
820    fn get_version_at_prefers_hlc_over_epoch_order() {
821        use mongreldb_types::hlc::HlcTimestamp;
822        let mut m = Memtable::new();
823        let early = HlcTimestamp {
824            physical_micros: 100,
825            logical: 0,
826            node_tiebreaker: 1,
827        };
828        let late = HlcTimestamp {
829            physical_micros: 200,
830            logical: 0,
831            node_tiebreaker: 1,
832        };
833        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
834        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
835        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
836        let (_, row) = m.get_version_at(RowId(1), snap).expect("visible");
837        assert_eq!(row.columns.get(&1), Some(&Value::Int64(1)));
838        assert_eq!(row.commit_ts, Some(early));
839    }
840
841    /// WAL `Put` payloads must keep the 0.63.1 bincode layout
842    /// `(row_id, committed_epoch, columns, deleted)`. `commit_ts` is
843    /// in-memory only (`#[serde(skip)]`) so a 0.63.1-shaped blob still opens.
844    #[test]
845    fn wal_put_row_bincode_matches_0_63_1_layout() {
846        use mongreldb_types::hlc::HlcTimestamp;
847        use serde::{Deserialize, Serialize};
848
849        #[derive(Serialize, Deserialize)]
850        struct LegacyRow {
851            row_id: RowId,
852            committed_epoch: Epoch,
853            columns: HashMap<u16, Value>,
854            deleted: bool,
855        }
856
857        let legacy = LegacyRow {
858            row_id: RowId(7),
859            committed_epoch: Epoch(3),
860            columns: [(1, Value::Int64(42))].into_iter().collect(),
861            deleted: false,
862        };
863        let bytes = bincode::serialize(&legacy).expect("legacy encode");
864
865        let decoded: Row = bincode::deserialize(&bytes).expect("0.63.1 payload must decode");
866        assert_eq!(decoded.row_id, RowId(7));
867        assert_eq!(decoded.committed_epoch, Epoch(3));
868        assert_eq!(decoded.columns.get(&1), Some(&Value::Int64(42)));
869        assert!(!decoded.deleted);
870        assert!(decoded.commit_ts.is_none());
871
872        // Round-trip through Row: commit_ts is not on the wire.
873        let stamped = HlcTimestamp {
874            physical_micros: 1_700_000_000_000,
875            logical: 2,
876            node_tiebreaker: 9,
877        };
878        let mut live = Row::new_with_hlc(RowId(7), Epoch(3), stamped);
879        live.columns.insert(1, Value::Int64(42));
880        let wire = bincode::serialize(&live).expect("row encode");
881        assert_eq!(
882            wire, bytes,
883            "WAL Put encoding must match the 0.63.1 four-field layout"
884        );
885        let again: Row = bincode::deserialize(&wire).expect("row decode");
886        assert!(
887            again.commit_ts.is_none(),
888            "commit_ts is restored from Op::CommitTimestamp, not the Put blob"
889        );
890    }
891
892    #[test]
893    fn drain_sorted_is_ascending_and_empties() {
894        let mut m = Memtable::new();
895        m.upsert(row(3, 1));
896        m.upsert(row(1, 1));
897        m.upsert(row(2, 1));
898        let out = m.drain_sorted();
899        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
900        assert_eq!(ids, vec![1, 2, 3]);
901        assert!(m.is_empty());
902        assert_eq!(m.approx_bytes(), 0);
903    }
904
905    #[test]
906    fn visible_rows_dedups_to_newest_version() {
907        let mut m = Memtable::new();
908        m.upsert(row(1, 1));
909        m.upsert(row(2, 9)); // future relative to snapshot 5
910        m.upsert(row(3, 1));
911        m.upsert(row(1, 3)); // newer version of row 1
912        let ids: Vec<u64> = m
913            .visible_rows(Epoch(5))
914            .iter()
915            .map(|r| r.row_id.0)
916            .collect();
917        assert_eq!(ids, vec![1, 3]);
918    }
919
920    #[test]
921    fn newest_visible_map_prefers_active_on_equal_version() {
922        let mut m = Memtable::new();
923        let mut deleted = row(1, 2);
924        deleted.deleted = true;
925        m.upsert(deleted);
926        m.seal();
927        m.upsert(row(1, 2));
928
929        let versions = m.visible_versions_at(Snapshot::at(Epoch(2)));
930        assert_eq!(versions.len(), 1);
931        assert!(!versions[0].deleted);
932    }
933
934    #[test]
935    fn newest_visible_iter_empty_memtable_yields_nothing() {
936        let m = Memtable::new();
937        assert!(m
938            .newest_visible_iter(&Snapshot::at(Epoch(9)))
939            .next()
940            .is_none());
941    }
942
943    #[test]
944    fn newest_visible_iter_single_insert_yields_one() {
945        let mut m = Memtable::new();
946        m.upsert(row(1, 3));
947        let values: Vec<_> = m
948            .newest_visible_iter(&Snapshot::at(Epoch(3)))
949            .map(|(id, epoch, _)| (id, epoch))
950            .collect();
951        assert_eq!(values, vec![(RowId(1), Epoch(3))]);
952    }
953
954    #[test]
955    fn newest_visible_iter_newer_epoch_wins() {
956        let mut m = Memtable::new();
957        m.upsert(row(1, 1));
958        m.upsert(row(1, 2));
959        let values: Vec<_> = m
960            .newest_visible_iter(&Snapshot::at(Epoch(2)))
961            .map(|(_, epoch, _)| epoch)
962            .collect();
963        assert_eq!(values, vec![Epoch(2)]);
964    }
965
966    #[test]
967    fn newest_visible_iter_tombstone_suppresses_older_live_version() {
968        // Mirrors MutableRunVisibleVersionCursor: the cursor yields the
969        // tombstone Row itself (so the caller can classify it as a Tombstone
970        // fallback or a StaleRowId), but the pre-tombstone live version is
971        // suppressed when the tombstone is in scope of the calling snapshot.
972        let mut m = Memtable::new();
973        m.upsert(row(1, 1));
974        m.tombstone(RowId(1), Epoch(2));
975        let snap = Snapshot::at(Epoch(2));
976        let got: Vec<(u64, u64, bool)> = m
977            .newest_visible_iter(&snap)
978            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
979            .collect();
980        assert_eq!(got, vec![(1, 2, true)], "tombstone is the newest");
981        // Pre-tombstone snapshot still sees the live version.
982        let snap_early = Snapshot::at(Epoch(1));
983        let got_early: Vec<(u64, u64, bool)> = m
984            .newest_visible_iter(&snap_early)
985            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
986            .collect();
987        assert_eq!(got_early, vec![(1, 1, false)]);
988    }
989
990    /// Regression for the BeTree root-buffer-not-iterated bug (iss10).
991    ///
992    /// Before the fix, the Bε-tree version stream (now
993    /// [`crate::be_tree::BeTreeVersionCursor`]) walked only the
994    /// consolidated leaves of the Bε-tree — silently skipping messages that
995    /// were still sitting in an internal-node buffer pending flush. A scan
996    /// over a live memtable that has triggered at least one split therefore
997    /// returned a subset of the inserted rows (typically the leaf-resident
998    /// ones, missing every row still buffered at the root).
999    ///
1000    /// This test inserts 1,000 rows without flushing. The first
1001    /// `LEAF_CAP = 32` rows go directly into a leaf; subsequent splits and
1002    /// buffer flushes leave a meaningful fraction of the rows sitting in
1003    /// internal-node buffers. The streaming cursor must yield all 1,000
1004    /// distinct `(RowId, Epoch)` pairs.
1005    #[test]
1006    fn newest_visible_iter_includes_root_buffer_rows() {
1007        const N: u64 = 1_000;
1008        let mut m = Memtable::new();
1009        // Each row gets a fresh RowId and its own (epoch-bumped) version; the
1010        // memtable has no flush path in this scope so the rows have to be
1011        // reachable via the active BeTree.
1012        for i in 0..N {
1013            let mut r = Row::new(RowId(i), Epoch(i + 1));
1014            r.columns.insert(1, Value::Int64(i as i64 * 10));
1015            m.upsert(r);
1016        }
1017        assert_eq!(m.len(), N as usize);
1018
1019        // Snapshot high enough that every version is visible.
1020        let snap = Snapshot::at(Epoch(N + 10));
1021        let got: Vec<(u64, u64, i64)> = m
1022            .newest_visible_iter(&snap)
1023            .map(|(rid, epoch, row)| (rid.0, epoch.0, int_of_value(&row)))
1024            .collect();
1025
1026        // Count: every distinct RowId must be visible exactly once.
1027        assert_eq!(
1028            got.len(),
1029            N as usize,
1030            "buffered rows must be visible to a streaming scan (got {})",
1031            got.len()
1032        );
1033        let mut seen_row_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
1034        for (rid, _epoch, _v) in &got {
1035            assert!(
1036                seen_row_ids.insert(*rid),
1037                "duplicate RowId {rid} in streaming scan output"
1038            );
1039        }
1040        // Set equality: every input RowId was emitted, no extras.
1041        let expected_ids: std::collections::HashSet<u64> = (0..N).collect();
1042        let got_ids: std::collections::HashSet<u64> = got.iter().map(|(rid, _, _)| *rid).collect();
1043        assert_eq!(got_ids, expected_ids, "must yield every input RowId");
1044
1045        // Spot-check: epoch and column bytes for a buffered row (high RowId
1046        // is almost certainly still buffered, not yet flushed to a leaf).
1047        let (_, epoch_raw, v) = got
1048            .iter()
1049            .find(|(rid, _, _)| *rid == N - 1)
1050            .copied()
1051            .expect("highest RowId present");
1052        assert_eq!(epoch_raw, N);
1053        assert_eq!(v, (N as i64 - 1) * 10);
1054    }
1055
1056    /// Same shape as `newest_visible_iter_includes_root_buffer_rows`, but
1057    /// drives the cursor against one row with many versions — exercising the
1058    /// case where the same `RowId` coexists in both a leaf and the root
1059    /// buffer (dedup picks the newest visible, which must come from the
1060    /// buffer when the buffered version is the latest).
1061    #[test]
1062    fn newest_visible_iter_buffered_versions_dedup_against_leaf_resident() {
1063        // Seed the leaf via a few inserts.
1064        let mut m = Memtable::new();
1065        for i in 0..16u64 {
1066            let mut r = Row::new(RowId(7), Epoch(i + 1));
1067            r.columns.insert(1, Value::Int64(i as i64));
1068            m.upsert(r);
1069        }
1070        // Then drive the tree through several splits by inserting many more
1071        // rows so the buffered message for the same RowId has to coexist with
1072        // the leaf-resident version (different epochs for the same RowId in
1073        // two locations).
1074        for i in 0u64..4_000 {
1075            let mut r = Row::new(RowId(1000 + i), Epoch(i + 100));
1076            r.columns.insert(1, Value::Int64(i as i64));
1077            m.upsert(r);
1078        }
1079        // One more version of row 7, distinctly newer than the leaf-resident
1080        // ones — guaranteed to land in some internal-node buffer.
1081        let mut latest_seven = Row::new(RowId(7), Epoch(20_000));
1082        latest_seven.columns.insert(1, Value::Int64(999));
1083        m.upsert(latest_seven);
1084
1085        let snap = Snapshot::at(Epoch(20_001));
1086        let seven = m
1087            .newest_visible_iter(&snap)
1088            .find(|(rid, _epoch, _row)| *rid == RowId(7))
1089            .expect("row 7 visible");
1090        assert_eq!(seven.1, Epoch(20_000), "buffered newest wins");
1091        // The full cursor must yield every distinct RowId (latest version):
1092        // 4,000 background rows + 1 distinct version of row 7.
1093        let total: u64 = m
1094            .newest_visible_iter(&snap)
1095            .map(|(rid, _epoch, _row)| rid.0)
1096            .fold(0u64, |acc, _| acc + 1);
1097        assert_eq!(total, 4_001, "buffer + leaf coverage");
1098    }
1099
1100    fn int_of_value(row: &Row) -> i64 {
1101        match row.columns.get(&1) {
1102            Some(Value::Int64(x)) => *x,
1103            other => panic!("expected Int64 column, got {other:?}"),
1104        }
1105    }
1106
1107    /// Build a memtable whose active tree has row 7 flushed into a leaf and
1108    /// a root buffer that still has free capacity: 33 inserts split the root
1109    /// leaf into an internal node, and 10 more inserts sit in its buffer
1110    /// (BUFFER_CAP is 16), so one further mutation is guaranteed to stay
1111    /// buffered at the root rather than flush to a leaf.
1112    fn memtable_with_leaf_resident_seven() -> Memtable {
1113        let mut m = Memtable::new();
1114        for i in 0..33u64 {
1115            m.upsert(row(i, 1));
1116        }
1117        for i in 100..110u64 {
1118            m.upsert(row(i, 1));
1119        }
1120        m
1121    }
1122
1123    /// REM-C §7.13: one `RowId` resident in both a leaf and an internal-node
1124    /// buffer emits exactly once, with the newest (buffered) version.
1125    #[test]
1126    fn same_rowid_in_buffer_and_leaf_dedups_once() {
1127        let mut m = memtable_with_leaf_resident_seven();
1128        m.upsert(row(7, 2)); // buffered at the root; newer than the leaf copy
1129        let got: Vec<(u64, u64, i64)> = m
1130            .newest_visible_iter(&Snapshot::at(Epoch(3)))
1131            .filter(|(rid, _, _)| *rid == RowId(7))
1132            .map(|(rid, epoch, row)| (rid.0, epoch.0, int_of_value(&row)))
1133            .collect();
1134        assert_eq!(got, vec![(7, 2, 70)], "row 7 must emit exactly once");
1135    }
1136
1137    /// REM-C §7.13: one `RowId` spanning several frozen segments plus the
1138    /// active segment emits exactly once, with the newest version.
1139    #[test]
1140    fn same_rowid_across_frozen_segments_dedups_once() {
1141        let mut m = Memtable::new();
1142        m.upsert(row(7, 1));
1143        m.seal();
1144        m.upsert(row(7, 2));
1145        m.seal();
1146        m.upsert(row(7, 3));
1147        let got: Vec<(u64, u64)> = m
1148            .newest_visible_iter(&Snapshot::at(Epoch(10)))
1149            .map(|(rid, epoch, _)| (rid.0, epoch.0))
1150            .collect();
1151        assert_eq!(got, vec![(7, 3)]);
1152    }
1153
1154    /// REM-C §7.13: a tombstone still sitting in an internal-node buffer
1155    /// suppresses the older leaf-resident live row.
1156    #[test]
1157    fn buffered_tombstone_suppresses_leaf_live_row() {
1158        let mut m = memtable_with_leaf_resident_seven();
1159        m.tombstone(RowId(7), Epoch(2)); // buffered at the root
1160        let at_tombstone: Vec<(u64, bool)> = m
1161            .newest_visible_iter(&Snapshot::at(Epoch(3)))
1162            .filter(|(rid, _, _)| *rid == RowId(7))
1163            .map(|(_, epoch, row)| (epoch.0, row.deleted))
1164            .collect();
1165        assert_eq!(
1166            at_tombstone,
1167            vec![(2, true)],
1168            "buffered tombstone is the newest visible version"
1169        );
1170        let before: Vec<(u64, bool)> = m
1171            .newest_visible_iter(&Snapshot::at(Epoch(1)))
1172            .filter(|(rid, _, _)| *rid == RowId(7))
1173            .map(|(_, epoch, row)| (epoch.0, row.deleted))
1174            .collect();
1175        assert_eq!(before, vec![(1, false)]);
1176    }
1177
1178    /// REM-C §7.13: HLC/epoch order inversion inside a single segment — the
1179    /// cursor must pick the higher HLC even at a lower epoch.
1180    #[test]
1181    fn hlc_inversion_inside_one_segment() {
1182        use mongreldb_types::hlc::HlcTimestamp;
1183        let early = HlcTimestamp {
1184            physical_micros: 100,
1185            logical: 0,
1186            node_tiebreaker: 1,
1187        };
1188        let late = HlcTimestamp {
1189            physical_micros: 200,
1190            logical: 0,
1191            node_tiebreaker: 1,
1192        };
1193        let mut m = Memtable::new();
1194        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
1195        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
1196        let snap = Snapshot::at_hlc(Epoch(99), late);
1197        let got: Vec<(u64, i64)> = m
1198            .newest_visible_iter(&snap)
1199            .map(|(_, epoch, row)| (epoch.0, int_of_value(&row)))
1200            .collect();
1201        assert_eq!(got, vec![(1, 99)], "higher HLC wins over higher epoch");
1202    }
1203
1204    /// REM-C §7.13: the same inversion across a frozen segment and the
1205    /// active segment.
1206    #[test]
1207    fn hlc_inversion_across_frozen_segments() {
1208        use mongreldb_types::hlc::HlcTimestamp;
1209        let early = HlcTimestamp {
1210            physical_micros: 100,
1211            logical: 0,
1212            node_tiebreaker: 1,
1213        };
1214        let late = HlcTimestamp {
1215            physical_micros: 200,
1216            logical: 0,
1217            node_tiebreaker: 1,
1218        };
1219        let mut m = Memtable::new();
1220        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
1221        m.seal();
1222        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
1223        let snap = Snapshot::at_hlc(Epoch(99), late);
1224        let got: Vec<(u64, i64)> = m
1225            .newest_visible_iter(&snap)
1226            .map(|(_, epoch, row)| (epoch.0, int_of_value(&row)))
1227            .collect();
1228        assert_eq!(got, vec![(1, 99)], "higher HLC wins across segments");
1229    }
1230
1231    /// REM-C §7.13: cancellation is observed while gathering a large
1232    /// same-`RowId` version group, not only between groups.
1233    #[test]
1234    fn cancellation_during_large_same_row_group() {
1235        let mut m = Memtable::new();
1236        m.upsert(row(1, 1));
1237        for v in 0..10_000u64 {
1238            m.upsert(row(7, v + 1));
1239        }
1240        let snap = Snapshot::at(Epoch(20_000));
1241        let mut cursor = m.newest_visible_iter(&snap);
1242        let control = crate::ExecutionControl::new(None);
1243        let (rid, ..) = cursor
1244            .next_controlled(&control)
1245            .expect("first advance")
1246            .expect("row 1");
1247        assert_eq!(rid, RowId(1));
1248        // Row 7 has a 10,000-version group; the cancelled control must stop
1249        // the gather at its 256-version checkpoint.
1250        control.cancel(crate::CancellationReason::ClientRequest);
1251        let err = cursor
1252            .next_controlled(&control)
1253            .expect_err("cancelled control must stop the gather");
1254        assert!(matches!(err, crate::MongrelError::Cancelled));
1255    }
1256}