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, LeafVersions};
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/// Min-heap key used to merge memtable leaf streams in ascending
189/// `(RowId, Epoch)` order. We carry the version's epoch too so that the
190/// dedup pass at emit time has it without re-matching on `Cow`.
191struct MemHead<'a> {
192    rid: RowId,
193    epoch: Epoch,
194    index: u32,
195    row: Cow<'a, Row>,
196}
197
198impl PartialEq for MemHead<'_> {
199    fn eq(&self, other: &Self) -> bool {
200        (self.rid, self.epoch, self.index) == (other.rid, other.epoch, other.index)
201    }
202}
203impl Eq for MemHead<'_> {}
204impl PartialOrd for MemHead<'_> {
205    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
206        Some(self.cmp(other))
207    }
208}
209impl Ord for MemHead<'_> {
210    fn cmp(&self, other: &Self) -> Ordering {
211        (other.rid, other.epoch, other.index).cmp(&(self.rid, self.epoch, self.index))
212    }
213}
214
215pub struct MemtableVisibleVersionCursor<'a> {
216    leaves: Vec<LeafVersions<'a>>,
217    heap: BinaryHeap<MemHead<'a>>,
218    snapshot: Snapshot,
219    finished: bool,
220    /// Number of source versions examined during the last `next()` call —
221    /// the dedup pass that picks the newest visible version per RowId.
222    pub(crate) last_examined: usize,
223    /// Peak `last_examined` across all calls so far.
224    pub(crate) peak_examined: usize,
225}
226
227impl<'a> Iterator for MemtableVisibleVersionCursor<'a> {
228    type Item = (RowId, Epoch, Cow<'a, Row>);
229
230    fn next(&mut self) -> Option<Self::Item> {
231        if self.finished {
232            return None;
233        }
234        self.last_examined = 0;
235        loop {
236            let MemHead {
237                rid,
238                epoch,
239                index,
240                row,
241            } = self.heap.pop()?;
242            self.last_examined += 1;
243            // Advance the source the popped head came from.
244            if let Some(next) = self.leaves[index as usize].next() {
245                let (nepoch, _nrow_id) = (next.committed_epoch, next.row_id);
246                self.heap.push(MemHead {
247                    rid: next.row_id,
248                    epoch: nepoch,
249                    index,
250                    row: next,
251                });
252            }
253            if !self.snapshot.observes_version(epoch, row.commit_ts) {
254                continue;
255            }
256            // Gather every remaining head that shares this rid; pick the
257            // newest visible version among them.
258            let mut best = Some(row);
259            while self.heap.peek().is_some_and(|h| h.rid == rid) {
260                let head = self.heap.pop().unwrap();
261                self.last_examined += 1;
262                if let Some(next) = self.leaves[head.index as usize].next() {
263                    let nepoch = next.committed_epoch;
264                    self.heap.push(MemHead {
265                        rid: next.row_id,
266                        epoch: nepoch,
267                        index: head.index,
268                        row: next,
269                    });
270                }
271                if self
272                    .snapshot
273                    .observes_version(head.epoch, head.row.commit_ts)
274                    && best.as_ref().is_none_or(|current| {
275                        Snapshot::version_is_newer(
276                            head.epoch,
277                            head.row.commit_ts,
278                            current.committed_epoch,
279                            current.commit_ts,
280                        )
281                    })
282                {
283                    best = Some(head.row);
284                }
285            }
286            let best = best?;
287            let row_id = best.row_id;
288            let epoch = best.committed_epoch;
289            self.peak_examined = self.peak_examined.max(self.last_examined);
290            return Some((row_id, epoch, best));
291        }
292    }
293}
294
295impl<'a> MemtableVisibleVersionCursor<'a> {
296    /// Cursor is exhausted when the heap is empty.
297    pub fn is_exhausted(&self) -> bool {
298        self.finished
299    }
300}
301
302/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
303/// for the prototype skip list: the same MVCC semantics with lower write
304/// amplification (buffered messages flush to children in bulk).
305#[derive(Clone)]
306struct MemtableSegment {
307    tree: BeTree,
308    byte_size: u64,
309}
310
311/// Structurally shared committed overlays plus one small mutable write delta.
312#[derive(Clone)]
313pub struct Memtable {
314    frozen: Arc<Vec<Arc<MemtableSegment>>>,
315    active: MemtableSegment,
316    byte_size: u64,
317}
318
319impl Default for Memtable {
320    fn default() -> Self {
321        Self::new()
322    }
323}
324
325impl Memtable {
326    pub fn new() -> Self {
327        Self {
328            frozen: Arc::new(Vec::new()),
329            active: MemtableSegment {
330                tree: BeTree::new(),
331                byte_size: 0,
332            },
333            byte_size: 0,
334        }
335    }
336
337    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
338    /// never overwritten; the newest visible one wins at read time.
339    pub fn upsert(&mut self, row: Row) {
340        let bytes = row.estimated_bytes();
341        self.byte_size = self.byte_size.saturating_add(bytes);
342        self.active.byte_size = self.active.byte_size.saturating_add(bytes);
343        self.active.tree.insert_row(row);
344    }
345
346    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
347    /// the columns from the newest live version so that engine-level HOT cleanup
348    /// can recover the primary-key value during WAL replay.
349    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
350        let mut columns = HashMap::new();
351        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
352            columns = live.columns;
353        }
354        let row = Row {
355            row_id,
356            committed_epoch: epoch,
357            columns,
358            deleted: true,
359            commit_ts: None,
360        };
361        self.upsert(row);
362    }
363
364    /// Read the row at `row_id` visible to `snapshot`: the newest version with
365    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
366    /// such version exists).
367    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
368        self.get_version(row_id, snapshot_epoch)
369            .and_then(|(_, row)| (!row.deleted).then_some(row))
370    }
371
372    /// Newest version of `row_id` with `epoch <= snapshot`, **including
373    /// tombstones** (as a `Row` with `deleted=true`). Legacy epoch-only entry
374    /// point; prefer [`Self::get_version_at`] when the caller holds a full
375    /// [`crate::epoch::Snapshot`].
376    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
377        self.get_version_at(row_id, crate::epoch::Snapshot::at(snapshot_epoch))
378    }
379
380    /// Newest version of `row_id` visible under `snapshot` (including
381    /// tombstones). Uses HLC authority when stamps are present (P0.5-T3).
382    ///
383    /// Seeks each segment's composite-key range for `row_id` so dual-model
384    /// mixes (stamped + unstamped) and HLC/epoch order inversions stay correct
385    /// without materializing every version in the memtable.
386    pub fn get_version_at(
387        &self,
388        row_id: RowId,
389        snapshot: crate::epoch::Snapshot,
390    ) -> Option<(Epoch, Row)> {
391        if !snapshot.uses_hlc_authority() {
392            let mut best = self.active.tree.get_version(row_id, snapshot.epoch);
393            for segment in self.frozen.iter().rev() {
394                let Some(candidate) = segment.tree.get_version(row_id, snapshot.epoch) else {
395                    continue;
396                };
397                if best.as_ref().is_none_or(|(epoch, _)| candidate.0 > *epoch) {
398                    best = Some(candidate);
399                }
400            }
401            return best;
402        }
403
404        let mut best: Option<Row> = None;
405        for segment in self
406            .frozen
407            .iter()
408            .map(|segment| &segment.tree)
409            .chain(std::iter::once(&self.active.tree))
410        {
411            segment.visit_versions(row_id, |row| {
412                if !snapshot.observes_row(row.committed_epoch, row.commit_ts) {
413                    return;
414                }
415                if best.as_ref().is_none_or(|current| {
416                    crate::epoch::Snapshot::version_is_newer(
417                        row.committed_epoch,
418                        row.commit_ts,
419                        current.committed_epoch,
420                        current.commit_ts,
421                    )
422                }) {
423                    best = Some(row);
424                }
425            });
426        }
427        best.map(|row| (row.committed_epoch, row))
428    }
429
430    /// Number of stored versions.
431    pub fn len(&self) -> usize {
432        self.active.tree.mutations()
433            + self
434                .frozen
435                .iter()
436                .map(|segment| segment.tree.mutations())
437                .sum::<usize>()
438    }
439
440    pub fn is_empty(&self) -> bool {
441        self.active.tree.is_empty() && self.frozen.is_empty()
442    }
443
444    pub fn approx_bytes(&self) -> u64 {
445        self.byte_size
446    }
447
448    /// Visible rows at `snapshot`, deduplicated to the newest version per
449    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
450    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
451        self.visible_versions(snapshot_epoch)
452            .into_iter()
453            .filter(|r| !r.deleted)
454            .collect()
455    }
456
457    /// Newest visible version per `RowId` at `snapshot`, **including
458    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
459    /// versions across the memtable and sorted runs.
460    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
461        self.visible_versions_at(crate::epoch::Snapshot::at(snapshot_epoch))
462    }
463
464    pub fn visible_versions_at(&self, snapshot: crate::epoch::Snapshot) -> Vec<Row> {
465        self.newest_visible_map(snapshot).into_values().collect()
466    }
467
468    /// Newest visible version per `RowId` as an ordered map (ascending RowId).
469    /// Callers that need to stream into a controlled merge without a second
470    /// full `Vec` should drain this map in batches rather than collecting.
471    pub(crate) fn newest_visible_map(
472        &self,
473        snapshot: crate::epoch::Snapshot,
474    ) -> BTreeMap<RowId, Row> {
475        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
476        for segment in std::iter::once(&self.active.tree)
477            .chain(self.frozen.iter().rev().map(|segment| &segment.tree))
478        {
479            for row in segment.versions() {
480                if !snapshot.observes_version(row.committed_epoch, row.commit_ts) {
481                    continue;
482                }
483                by_row
484                    .entry(row.row_id)
485                    .and_modify(|existing| {
486                        if crate::epoch::Snapshot::version_is_newer(
487                            row.committed_epoch,
488                            row.commit_ts,
489                            existing.committed_epoch,
490                            existing.commit_ts,
491                        ) {
492                            *existing = row.clone();
493                        }
494                    })
495                    .or_insert(row);
496            }
497        }
498        by_row
499    }
500
501    pub fn newest_visible_iter<'a>(
502        &'a self,
503        snapshot: &Snapshot,
504    ) -> MemtableVisibleVersionCursor<'a> {
505        let mut leaves: Vec<LeafVersions<'a>> = self
506            .frozen
507            .iter()
508            .map(|segment| segment.tree.leaf_versions_iter())
509            .chain(std::iter::once(self.active.tree.leaf_versions_iter()))
510            .collect();
511        let mut heap = BinaryHeap::new();
512        for (index, leaf) in leaves.iter_mut().enumerate() {
513            if let Some(row) = leaf.next() {
514                heap.push(MemHead {
515                    rid: row.row_id,
516                    epoch: row.committed_epoch,
517                    index: index as u32,
518                    row,
519                });
520            }
521        }
522        MemtableVisibleVersionCursor {
523            leaves,
524            heap,
525            snapshot: *snapshot,
526            finished: false,
527            last_examined: 0,
528            peak_examined: 0,
529        }
530    }
531
532    /// Freeze the current write delta so future clones share it by `Arc`.
533    pub(crate) fn seal(&mut self) {
534        if self.active.tree.is_empty() {
535            return;
536        }
537        let active = std::mem::replace(
538            &mut self.active,
539            MemtableSegment {
540                tree: BeTree::new(),
541                byte_size: 0,
542            },
543        );
544        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
545        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
546            self.consolidate();
547        }
548    }
549
550    fn consolidate(&mut self) {
551        let mut tree = BeTree::new();
552        for row in self
553            .frozen
554            .iter()
555            .flat_map(|segment| segment.tree.versions())
556        {
557            tree.insert_row(row);
558        }
559        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
560            tree,
561            byte_size: self.byte_size,
562        })]);
563    }
564
565    #[cfg(test)]
566    pub(crate) fn frozen_layer_count(&self) -> usize {
567        self.frozen.len()
568    }
569
570    /// Drain all versions (for a memtable-to-run flush). Returns them in
571    /// ascending `(RowId, Epoch)` order.
572    pub fn drain_sorted(&mut self) -> Vec<Row> {
573        let mut out = self
574            .frozen
575            .iter()
576            .flat_map(|segment| segment.tree.versions())
577            .chain(self.active.tree.versions())
578            .collect::<Vec<_>>();
579        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
580        self.frozen = Arc::new(Vec::new());
581        self.active = MemtableSegment {
582            tree: BeTree::new(),
583            byte_size: 0,
584        };
585        self.byte_size = 0;
586        out
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    fn row(id: u64, epoch: u64) -> Row {
595        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
596    }
597
598    #[test]
599    fn upsert_get_and_visibility() {
600        let mut m = Memtable::new();
601        m.upsert(row(1, 5));
602        assert_eq!(m.len(), 1);
603        assert!(m.get(RowId(1), Epoch(5)).is_some());
604        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
605        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
606    }
607
608    #[test]
609    fn tombstone_supersedes_at_its_epoch() {
610        let mut m = Memtable::new();
611        m.upsert(row(1, 1));
612        // Before the tombstone: the live version is visible.
613        assert!(m.get(RowId(1), Epoch(1)).is_some());
614        m.tombstone(RowId(1), Epoch(2));
615        // At/after the tombstone: hidden.
616        assert!(m.get(RowId(1), Epoch(2)).is_none());
617        assert!(m.get(RowId(1), Epoch(9)).is_none());
618        // A snapshot before the tombstone still sees the live version.
619        assert!(m.get(RowId(1), Epoch(1)).is_some());
620    }
621
622    #[test]
623    fn sealed_generations_share_rows_and_consolidate() {
624        let mut writer = Memtable::new();
625        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
626            writer.upsert(row(id, id + 1));
627            writer.seal();
628        }
629        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
630        let generation = writer.clone();
631        writer.upsert(row(99, 99));
632        assert!(generation.get(RowId(99), Epoch(99)).is_none());
633        assert!(writer.get(RowId(99), Epoch(99)).is_some());
634    }
635
636    #[test]
637    fn hlc_visibility_is_authoritative_when_stamped() {
638        use mongreldb_types::hlc::HlcTimestamp;
639        let mut m = Memtable::new();
640        let early = HlcTimestamp {
641            physical_micros: 100,
642            logical: 0,
643            node_tiebreaker: 1,
644        };
645        let late = HlcTimestamp {
646            physical_micros: 200,
647            logical: 0,
648            node_tiebreaker: 1,
649        };
650        let mut r1 = Row::new_with_hlc(RowId(1), Epoch(1), early);
651        r1.columns.insert(1, Value::Int64(1));
652        let mut r2 = Row::new_with_hlc(RowId(1), Epoch(2), late);
653        r2.columns.insert(1, Value::Int64(2));
654        m.upsert(r1);
655        m.upsert(r2);
656        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
657        let versions = m.visible_versions_at(snap);
658        assert_eq!(versions.len(), 1);
659        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
660        let snap2 = crate::epoch::Snapshot::at_hlc(Epoch(1), late);
661        assert_eq!(
662            m.visible_versions_at(snap2)[0].columns.get(&1),
663            Some(&Value::Int64(2))
664        );
665    }
666
667    #[test]
668    fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
669        use mongreldb_types::hlc::HlcTimestamp;
670        let mut m = Memtable::new();
671        let early = HlcTimestamp {
672            physical_micros: 100,
673            logical: 0,
674            node_tiebreaker: 1,
675        };
676        let late = HlcTimestamp {
677            physical_micros: 200,
678            logical: 0,
679            node_tiebreaker: 1,
680        };
681        // Epoch(1) with late HLC would win under epoch-only rules when snap
682        // epoch is 99 — HLC authority must hide it under an early pin.
683        let mut late_row = Row::new_with_hlc(RowId(1), Epoch(1), late);
684        late_row.columns.insert(1, Value::Int64(99));
685        let mut early_row = Row::new_with_hlc(RowId(1), Epoch(50), early);
686        early_row.columns.insert(1, Value::Int64(1));
687        m.upsert(late_row);
688        m.upsert(early_row);
689        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
690        let versions = m.visible_versions_at(snap);
691        assert_eq!(versions.len(), 1);
692        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
693        assert_eq!(versions[0].commit_ts, Some(early));
694    }
695
696    #[test]
697    fn epoch_only_snapshot_sees_hlc_stamped_rows_by_epoch() {
698        use mongreldb_types::hlc::HlcTimestamp;
699        let mut m = Memtable::new();
700        let ts = HlcTimestamp {
701            physical_micros: 50,
702            logical: 0,
703            node_tiebreaker: 1,
704        };
705        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), ts).with_column(1, Value::Int64(1)));
706        m.upsert(Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)));
707        let legacy = crate::epoch::Snapshot::at(Epoch(99));
708        let versions = m.visible_versions_at(legacy);
709        assert_eq!(
710            versions.len(),
711            2,
712            "dual-model: epoch pin sees HLC rows by epoch"
713        );
714        assert!(m.get_version_at(RowId(1), legacy).is_some());
715        assert!(m.get_version_at(RowId(2), legacy).is_some());
716        let future = crate::epoch::Snapshot::at(Epoch(0));
717        assert!(m.get_version_at(RowId(1), future).is_none());
718    }
719
720    #[test]
721    fn get_version_at_prefers_hlc_over_epoch_order() {
722        use mongreldb_types::hlc::HlcTimestamp;
723        let mut m = Memtable::new();
724        let early = HlcTimestamp {
725            physical_micros: 100,
726            logical: 0,
727            node_tiebreaker: 1,
728        };
729        let late = HlcTimestamp {
730            physical_micros: 200,
731            logical: 0,
732            node_tiebreaker: 1,
733        };
734        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
735        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
736        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
737        let (_, row) = m.get_version_at(RowId(1), snap).expect("visible");
738        assert_eq!(row.columns.get(&1), Some(&Value::Int64(1)));
739        assert_eq!(row.commit_ts, Some(early));
740    }
741
742    /// WAL `Put` payloads must keep the 0.63.1 bincode layout
743    /// `(row_id, committed_epoch, columns, deleted)`. `commit_ts` is
744    /// in-memory only (`#[serde(skip)]`) so a 0.63.1-shaped blob still opens.
745    #[test]
746    fn wal_put_row_bincode_matches_0_63_1_layout() {
747        use mongreldb_types::hlc::HlcTimestamp;
748        use serde::{Deserialize, Serialize};
749
750        #[derive(Serialize, Deserialize)]
751        struct LegacyRow {
752            row_id: RowId,
753            committed_epoch: Epoch,
754            columns: HashMap<u16, Value>,
755            deleted: bool,
756        }
757
758        let legacy = LegacyRow {
759            row_id: RowId(7),
760            committed_epoch: Epoch(3),
761            columns: [(1, Value::Int64(42))].into_iter().collect(),
762            deleted: false,
763        };
764        let bytes = bincode::serialize(&legacy).expect("legacy encode");
765
766        let decoded: Row = bincode::deserialize(&bytes).expect("0.63.1 payload must decode");
767        assert_eq!(decoded.row_id, RowId(7));
768        assert_eq!(decoded.committed_epoch, Epoch(3));
769        assert_eq!(decoded.columns.get(&1), Some(&Value::Int64(42)));
770        assert!(!decoded.deleted);
771        assert!(decoded.commit_ts.is_none());
772
773        // Round-trip through Row: commit_ts is not on the wire.
774        let stamped = HlcTimestamp {
775            physical_micros: 1_700_000_000_000,
776            logical: 2,
777            node_tiebreaker: 9,
778        };
779        let mut live = Row::new_with_hlc(RowId(7), Epoch(3), stamped);
780        live.columns.insert(1, Value::Int64(42));
781        let wire = bincode::serialize(&live).expect("row encode");
782        assert_eq!(
783            wire, bytes,
784            "WAL Put encoding must match the 0.63.1 four-field layout"
785        );
786        let again: Row = bincode::deserialize(&wire).expect("row decode");
787        assert!(
788            again.commit_ts.is_none(),
789            "commit_ts is restored from Op::CommitTimestamp, not the Put blob"
790        );
791    }
792
793    #[test]
794    fn drain_sorted_is_ascending_and_empties() {
795        let mut m = Memtable::new();
796        m.upsert(row(3, 1));
797        m.upsert(row(1, 1));
798        m.upsert(row(2, 1));
799        let out = m.drain_sorted();
800        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
801        assert_eq!(ids, vec![1, 2, 3]);
802        assert!(m.is_empty());
803        assert_eq!(m.approx_bytes(), 0);
804    }
805
806    #[test]
807    fn visible_rows_dedups_to_newest_version() {
808        let mut m = Memtable::new();
809        m.upsert(row(1, 1));
810        m.upsert(row(2, 9)); // future relative to snapshot 5
811        m.upsert(row(3, 1));
812        m.upsert(row(1, 3)); // newer version of row 1
813        let ids: Vec<u64> = m
814            .visible_rows(Epoch(5))
815            .iter()
816            .map(|r| r.row_id.0)
817            .collect();
818        assert_eq!(ids, vec![1, 3]);
819    }
820
821    #[test]
822    fn newest_visible_map_prefers_active_on_equal_version() {
823        let mut m = Memtable::new();
824        let mut deleted = row(1, 2);
825        deleted.deleted = true;
826        m.upsert(deleted);
827        m.seal();
828        m.upsert(row(1, 2));
829
830        let versions = m.visible_versions_at(Snapshot::at(Epoch(2)));
831        assert_eq!(versions.len(), 1);
832        assert!(!versions[0].deleted);
833    }
834
835    #[test]
836    fn newest_visible_iter_empty_memtable_yields_nothing() {
837        let m = Memtable::new();
838        assert!(m
839            .newest_visible_iter(&Snapshot::at(Epoch(9)))
840            .next()
841            .is_none());
842    }
843
844    #[test]
845    fn newest_visible_iter_single_insert_yields_one() {
846        let mut m = Memtable::new();
847        m.upsert(row(1, 3));
848        let values: Vec<_> = m
849            .newest_visible_iter(&Snapshot::at(Epoch(3)))
850            .map(|(id, epoch, _)| (id, epoch))
851            .collect();
852        assert_eq!(values, vec![(RowId(1), Epoch(3))]);
853    }
854
855    #[test]
856    fn newest_visible_iter_newer_epoch_wins() {
857        let mut m = Memtable::new();
858        m.upsert(row(1, 1));
859        m.upsert(row(1, 2));
860        let values: Vec<_> = m
861            .newest_visible_iter(&Snapshot::at(Epoch(2)))
862            .map(|(_, epoch, _)| epoch)
863            .collect();
864        assert_eq!(values, vec![Epoch(2)]);
865    }
866
867    #[test]
868    fn newest_visible_iter_tombstone_suppresses_older_live_version() {
869        // Mirrors MutableRunVisibleVersionCursor: the cursor yields the
870        // tombstone Row itself (so the caller can classify it as a Tombstone
871        // fallback or a StaleRowId), but the pre-tombstone live version is
872        // suppressed when the tombstone is in scope of the calling snapshot.
873        let mut m = Memtable::new();
874        m.upsert(row(1, 1));
875        m.tombstone(RowId(1), Epoch(2));
876        let snap = Snapshot::at(Epoch(2));
877        let got: Vec<(u64, u64, bool)> = m
878            .newest_visible_iter(&snap)
879            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
880            .collect();
881        assert_eq!(got, vec![(1, 2, true)], "tombstone is the newest");
882        // Pre-tombstone snapshot still sees the live version.
883        let snap_early = Snapshot::at(Epoch(1));
884        let got_early: Vec<(u64, u64, bool)> = m
885            .newest_visible_iter(&snap_early)
886            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
887            .collect();
888        assert_eq!(got_early, vec![(1, 1, false)]);
889    }
890
891    /// Regression for the BeTree root-buffer-not-iterated bug (iss10).
892    ///
893    /// Before the fix, [`crate::be_tree::LeafVersions`] walked only the
894    /// consolidated leaves of the Bε-tree — silently skipping messages that
895    /// were still sitting in an internal-node buffer pending flush. A scan
896    /// over a live memtable that has triggered at least one split therefore
897    /// returned a subset of the inserted rows (typically the leaf-resident
898    /// ones, missing every row still buffered at the root).
899    ///
900    /// This test inserts 1,000 rows without flushing. The first
901    /// `LEAF_CAP = 32` rows go directly into a leaf; subsequent splits and
902    /// buffer flushes leave a meaningful fraction of the rows sitting in
903    /// internal-node buffers. The streaming cursor must yield all 1,000
904    /// distinct `(RowId, Epoch)` pairs.
905    #[test]
906    fn newest_visible_iter_includes_root_buffer_rows() {
907        const N: u64 = 1_000;
908        let mut m = Memtable::new();
909        // Each row gets a fresh RowId and its own (epoch-bumped) version; the
910        // memtable has no flush path in this scope so the rows have to be
911        // reachable via the active BeTree.
912        for i in 0..N {
913            let mut r = Row::new(RowId(i), Epoch(i + 1));
914            r.columns.insert(1, Value::Int64(i as i64 * 10));
915            m.upsert(r);
916        }
917        assert_eq!(m.len(), N as usize);
918
919        // Snapshot high enough that every version is visible.
920        let snap = Snapshot::at(Epoch(N + 10));
921        let got: Vec<(u64, u64, i64)> = m
922            .newest_visible_iter(&snap)
923            .map(|(rid, epoch, row)| (rid.0, epoch.0, int_of_value(&row)))
924            .collect();
925
926        // Count: every distinct RowId must be visible exactly once.
927        assert_eq!(
928            got.len(),
929            N as usize,
930            "buffered rows must be visible to a streaming scan (got {})",
931            got.len()
932        );
933        let mut seen_row_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
934        for (rid, _epoch, _v) in &got {
935            assert!(
936                seen_row_ids.insert(*rid),
937                "duplicate RowId {rid} in streaming scan output"
938            );
939        }
940        // Set equality: every input RowId was emitted, no extras.
941        let expected_ids: std::collections::HashSet<u64> = (0..N).collect();
942        let got_ids: std::collections::HashSet<u64> = got.iter().map(|(rid, _, _)| *rid).collect();
943        assert_eq!(got_ids, expected_ids, "must yield every input RowId");
944
945        // Spot-check: epoch and column bytes for a buffered row (high RowId
946        // is almost certainly still buffered, not yet flushed to a leaf).
947        let (_, epoch_raw, v) = got
948            .iter()
949            .find(|(rid, _, _)| *rid == N - 1)
950            .copied()
951            .expect("highest RowId present");
952        assert_eq!(epoch_raw, N);
953        assert_eq!(v, (N as i64 - 1) * 10);
954    }
955
956    /// Same shape as `newest_visible_iter_includes_root_buffer_rows`, but
957    /// drives the cursor against one row with many versions — exercising the
958    /// case where the same `RowId` coexists in both a leaf and the root
959    /// buffer (dedup picks the newest visible, which must come from the
960    /// buffer when the buffered version is the latest).
961    #[test]
962    fn newest_visible_iter_buffered_versions_dedup_against_leaf_resident() {
963        // Seed the leaf via a few inserts.
964        let mut m = Memtable::new();
965        for i in 0..16u64 {
966            let mut r = Row::new(RowId(7), Epoch(i + 1));
967            r.columns.insert(1, Value::Int64(i as i64));
968            m.upsert(r);
969        }
970        // Then drive the tree through several splits by inserting many more
971        // rows so the buffered message for the same RowId has to coexist with
972        // the leaf-resident version (different epochs for the same RowId in
973        // two locations).
974        for i in 0u64..4_000 {
975            let mut r = Row::new(RowId(1000 + i), Epoch(i + 100));
976            r.columns.insert(1, Value::Int64(i as i64));
977            m.upsert(r);
978        }
979        // One more version of row 7, distinctly newer than the leaf-resident
980        // ones — guaranteed to land in some internal-node buffer.
981        let mut latest_seven = Row::new(RowId(7), Epoch(20_000));
982        latest_seven.columns.insert(1, Value::Int64(999));
983        m.upsert(latest_seven);
984
985        let snap = Snapshot::at(Epoch(20_001));
986        let seven = m
987            .newest_visible_iter(&snap)
988            .find(|(rid, _epoch, _row)| *rid == RowId(7))
989            .expect("row 7 visible");
990        assert_eq!(seven.1, Epoch(20_000), "buffered newest wins");
991        // The full cursor must yield every distinct RowId (latest version):
992        // 4,000 background rows + 1 distinct version of row 7.
993        let total: u64 = m
994            .newest_visible_iter(&snap)
995            .map(|(rid, _epoch, _row)| rid.0)
996            .fold(0u64, |acc, _| acc + 1);
997        assert_eq!(total, 4_001, "buffer + leaf coverage");
998    }
999
1000    fn int_of_value(row: &Row) -> i64 {
1001        match row.columns.get(&1) {
1002            Some(Value::Int64(x)) => *x,
1003            other => panic!("expected Int64 column, got {other:?}"),
1004        }
1005    }
1006}