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::collections::{BTreeMap, HashMap};
19use std::sync::Arc;
20
21/// A cell value in the in-memory path. The flush path re-encodes these into
22/// columnar pages; it is intentionally simple for the prototype.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum Value {
25    Null,
26    Bool(bool),
27    Int64(i64),
28    Float64(f64),
29    Bytes(Vec<u8>),
30    Embedding(Vec<f32>),
31    /// Unscaled decimal value (i128). The column's `TypeId::Decimal128`
32    /// carries the precision/scale for formatting.
33    Decimal(i128),
34    /// SQL INTERVAL value: months, days, nanoseconds.
35    Interval {
36        months: i64,
37        days: i32,
38        nanos: i64,
39    },
40    /// RFC 4122 UUID (16 bytes, big-endian for sort order).
41    Uuid([u8; 16]),
42    /// JSON value stored as a UTF-8 byte sequence.
43    Json(Vec<u8>),
44    /// Generated embedding with durable source and model provenance.
45    ///
46    /// Kept last so existing bincode enum discriminants remain stable.
47    GeneratedEmbedding(Box<crate::embedding::GeneratedEmbeddingValue>),
48}
49
50impl Value {
51    pub fn as_embedding(&self) -> Option<&[f32]> {
52        match self {
53            Self::Embedding(values) => Some(values),
54            Self::GeneratedEmbedding(value) => Some(&value.vector),
55            _ => None,
56        }
57    }
58
59    pub fn generated_embedding_metadata(
60        &self,
61    ) -> Option<&crate::embedding::GeneratedEmbeddingMetadata> {
62        match self {
63            Self::GeneratedEmbedding(value) => Some(&value.metadata),
64            _ => None,
65        }
66    }
67
68    /// Lexicographically-comparable byte encoding for index keys (PK HOT,
69    /// bitmaps). Big-endian for integers so byte order matches value order.
70    pub fn encode_key(&self) -> Vec<u8> {
71        match self {
72            Value::Null => Vec::new(),
73            Value::Bool(b) => vec![*b as u8],
74            Value::Int64(n) => n.to_be_bytes().to_vec(),
75            Value::Float64(f) => f.to_bits().to_be_bytes().to_vec(),
76            Value::Bytes(b) => b.clone(),
77            Value::Embedding(v) => {
78                let mut out = Vec::with_capacity(v.len() * 4);
79                for x in v {
80                    out.extend_from_slice(&x.to_bits().to_be_bytes());
81                }
82                out
83            }
84            Value::GeneratedEmbedding(value) => {
85                let mut out = Vec::with_capacity(value.vector.len() * 4);
86                for x in &value.vector {
87                    out.extend_from_slice(&x.to_bits().to_be_bytes());
88                }
89                out
90            }
91            Value::Decimal(d) => d.to_be_bytes().to_vec(),
92            Value::Interval {
93                months,
94                days,
95                nanos,
96            } => {
97                let mut out = Vec::with_capacity(20);
98                out.extend_from_slice(&months.to_be_bytes());
99                out.extend_from_slice(&days.to_be_bytes());
100                out.extend_from_slice(&nanos.to_be_bytes());
101                out
102            }
103            Value::Uuid(b) => b.to_vec(),
104            Value::Json(b) => b.clone(),
105        }
106    }
107
108    pub(crate) fn estimated_bytes(&self) -> u64 {
109        match self {
110            Value::Null => 1,
111            Value::Bool(_) => 1,
112            Value::Int64(_) | Value::Float64(_) => 8,
113            Value::Bytes(bytes) | Value::Json(bytes) => 16 + bytes.len() as u64,
114            Value::Embedding(values) => 16 + (values.len() as u64) * 4,
115            Value::GeneratedEmbedding(value) => {
116                16 + (value.vector.len() as u64) * 4
117                    + value.metadata.provider_id.len() as u64
118                    + value.metadata.model_id.len() as u64
119                    + value.metadata.model_version.len() as u64
120                    + value.metadata.preprocessing_version.len() as u64
121                    + 48
122            }
123            Value::Decimal(_) | Value::Uuid(_) => 16,
124            Value::Interval { .. } => 20,
125        }
126    }
127}
128
129/// One logical row held in the memtable. A `deleted` row is a tombstone.
130///
131/// Field order of the **bincode WAL `Put` payload** is fixed as
132/// `(row_id, committed_epoch, columns, deleted)` — the 0.63.1 layout.
133/// [`Self::commit_ts`] is in-memory only (`#[serde(skip)]`); durable HLC for
134/// WAL recovery is `Op::CommitTimestamp`, and sorted runs use the
135/// `SYS_COMMIT_TS` system column (with its own legacy-compatible path).
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct Row {
138    pub row_id: RowId,
139    pub committed_epoch: Epoch,
140    pub columns: HashMap<u16, Value>,
141    pub deleted: bool,
142    /// Optional HLC stamp (P0.5 dual-model). Not encoded in WAL `Put` bincode
143    /// payloads — see struct-level docs. Kept last so call sites and future
144    /// wire evolution treat the 0.63.1 fields as the stable prefix.
145    #[serde(skip)]
146    pub commit_ts: Option<mongreldb_types::hlc::HlcTimestamp>,
147}
148
149impl Row {
150    pub fn new(row_id: RowId, committed_epoch: Epoch) -> Self {
151        Self {
152            row_id,
153            committed_epoch,
154            columns: HashMap::new(),
155            deleted: false,
156            commit_ts: None,
157        }
158    }
159
160    pub fn new_with_hlc(
161        row_id: RowId,
162        committed_epoch: Epoch,
163        commit_ts: mongreldb_types::hlc::HlcTimestamp,
164    ) -> Self {
165        Self {
166            row_id,
167            committed_epoch,
168            columns: HashMap::new(),
169            deleted: false,
170            commit_ts: Some(commit_ts),
171        }
172    }
173
174    pub fn with_column(mut self, column_id: u16, value: Value) -> Self {
175        self.columns.insert(column_id, value);
176        self
177    }
178
179    /// Rough byte estimate for flush-threshold decisions.
180    pub fn estimated_bytes(&self) -> u64 {
181        self.columns
182            .values()
183            .fold(32, |bytes, value| bytes + value.estimated_bytes())
184    }
185}
186
187pub struct MemtableVisibleVersionCursor<'a> {
188    leaves: Vec<LeafVersions<'a>>,
189    leaf_index: usize,
190    snapshot: Snapshot,
191    current_row_id: Option<RowId>,
192    best: Option<Cow<'a, Row>>,
193    finished: bool,
194}
195
196impl<'a> Iterator for MemtableVisibleVersionCursor<'a> {
197    type Item = (RowId, Epoch, Cow<'a, Row>);
198
199    fn next(&mut self) -> Option<Self::Item> {
200        if self.finished {
201            return None;
202        }
203        loop {
204            let next = self
205                .leaves
206                .get_mut(self.leaf_index)
207                .and_then(Iterator::next);
208            let Some(version) = next else {
209                self.leaf_index += 1;
210                if self.leaf_index < self.leaves.len() {
211                    continue;
212                }
213                self.finished = true;
214                return self.take_best();
215            };
216            // Skip versions outside the snapshot's visibility window.
217            let (epoch, commit_ts, row_id) = cow_parts(&version);
218            if !self.snapshot.observes_version(epoch, commit_ts) {
219                continue;
220            }
221            if self.current_row_id != Some(row_id) {
222                let result = self.take_best();
223                self.current_row_id = Some(row_id);
224                self.best = Some(version);
225                if result.is_some() {
226                    return result;
227                }
228            } else if self.best.as_ref().is_none_or(|best| {
229                let (best_epoch, best_ts, _best_id) = cow_parts(best);
230                Snapshot::version_is_newer(epoch, commit_ts, best_epoch, best_ts)
231            }) {
232                self.best = Some(version);
233            }
234        }
235    }
236}
237
238#[allow(clippy::ptr_arg)] // `Cow<'_, Row>::Deref` doesn't auto-coerce, so keep the type explicit here
239fn cow_parts(cow: &Cow<'_, Row>) -> (Epoch, Option<mongreldb_types::hlc::HlcTimestamp>, RowId) {
240    match cow {
241        Cow::Borrowed(r) => (r.committed_epoch, r.commit_ts, r.row_id),
242        Cow::Owned(r) => (r.committed_epoch, r.commit_ts, r.row_id),
243    }
244}
245
246impl<'a> MemtableVisibleVersionCursor<'a> {
247    fn take_best(&mut self) -> Option<(RowId, Epoch, Cow<'a, Row>)> {
248        let best = self.best.take()?;
249        let epoch = match &best {
250            Cow::Borrowed(r) => r.committed_epoch,
251            Cow::Owned(r) => r.committed_epoch,
252        };
253        let row_id = match &best {
254            Cow::Borrowed(r) => r.row_id,
255            Cow::Owned(r) => r.row_id,
256        };
257        Some((row_id, epoch, best))
258    }
259}
260
261/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
262/// for the prototype skip list: the same MVCC semantics with lower write
263/// amplification (buffered messages flush to children in bulk).
264#[derive(Clone)]
265struct MemtableSegment {
266    tree: BeTree,
267    byte_size: u64,
268}
269
270/// Structurally shared committed overlays plus one small mutable write delta.
271#[derive(Clone)]
272pub struct Memtable {
273    frozen: Arc<Vec<Arc<MemtableSegment>>>,
274    active: MemtableSegment,
275    byte_size: u64,
276}
277
278impl Default for Memtable {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284impl Memtable {
285    pub fn new() -> Self {
286        Self {
287            frozen: Arc::new(Vec::new()),
288            active: MemtableSegment {
289                tree: BeTree::new(),
290                byte_size: 0,
291            },
292            byte_size: 0,
293        }
294    }
295
296    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
297    /// never overwritten; the newest visible one wins at read time.
298    pub fn upsert(&mut self, row: Row) {
299        let bytes = row.estimated_bytes();
300        self.byte_size = self.byte_size.saturating_add(bytes);
301        self.active.byte_size = self.active.byte_size.saturating_add(bytes);
302        self.active.tree.insert_row(row);
303    }
304
305    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
306    /// the columns from the newest live version so that engine-level HOT cleanup
307    /// can recover the primary-key value during WAL replay.
308    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
309        let mut columns = HashMap::new();
310        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
311            columns = live.columns;
312        }
313        let row = Row {
314            row_id,
315            committed_epoch: epoch,
316            columns,
317            deleted: true,
318            commit_ts: None,
319        };
320        self.upsert(row);
321    }
322
323    /// Read the row at `row_id` visible to `snapshot`: the newest version with
324    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
325    /// such version exists).
326    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
327        self.get_version(row_id, snapshot_epoch)
328            .and_then(|(_, row)| (!row.deleted).then_some(row))
329    }
330
331    /// Newest version of `row_id` with `epoch <= snapshot`, **including
332    /// tombstones** (as a `Row` with `deleted=true`). Legacy epoch-only entry
333    /// point; prefer [`Self::get_version_at`] when the caller holds a full
334    /// [`crate::epoch::Snapshot`].
335    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
336        self.get_version_at(row_id, crate::epoch::Snapshot::at(snapshot_epoch))
337    }
338
339    /// Newest version of `row_id` visible under `snapshot` (including
340    /// tombstones). Uses HLC authority when stamps are present (P0.5-T3).
341    ///
342    /// Seeks each segment's composite-key range for `row_id` so dual-model
343    /// mixes (stamped + unstamped) and HLC/epoch order inversions stay correct
344    /// without materializing every version in the memtable.
345    pub fn get_version_at(
346        &self,
347        row_id: RowId,
348        snapshot: crate::epoch::Snapshot,
349    ) -> Option<(Epoch, Row)> {
350        if !snapshot.uses_hlc_authority() {
351            let mut best = self.active.tree.get_version(row_id, snapshot.epoch);
352            for segment in self.frozen.iter().rev() {
353                let Some(candidate) = segment.tree.get_version(row_id, snapshot.epoch) else {
354                    continue;
355                };
356                if best.as_ref().is_none_or(|(epoch, _)| candidate.0 > *epoch) {
357                    best = Some(candidate);
358                }
359            }
360            return best;
361        }
362
363        let mut best: Option<Row> = None;
364        for segment in self
365            .frozen
366            .iter()
367            .map(|segment| &segment.tree)
368            .chain(std::iter::once(&self.active.tree))
369        {
370            segment.visit_versions(row_id, |row| {
371                if !snapshot.observes_row(row.committed_epoch, row.commit_ts) {
372                    return;
373                }
374                if best.as_ref().is_none_or(|current| {
375                    crate::epoch::Snapshot::version_is_newer(
376                        row.committed_epoch,
377                        row.commit_ts,
378                        current.committed_epoch,
379                        current.commit_ts,
380                    )
381                }) {
382                    best = Some(row);
383                }
384            });
385        }
386        best.map(|row| (row.committed_epoch, row))
387    }
388
389    /// Number of stored versions.
390    pub fn len(&self) -> usize {
391        self.active.tree.mutations()
392            + self
393                .frozen
394                .iter()
395                .map(|segment| segment.tree.mutations())
396                .sum::<usize>()
397    }
398
399    pub fn is_empty(&self) -> bool {
400        self.active.tree.is_empty() && self.frozen.is_empty()
401    }
402
403    pub fn approx_bytes(&self) -> u64 {
404        self.byte_size
405    }
406
407    /// Visible rows at `snapshot`, deduplicated to the newest version per
408    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
409    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
410        self.visible_versions(snapshot_epoch)
411            .into_iter()
412            .filter(|r| !r.deleted)
413            .collect()
414    }
415
416    /// Newest visible version per `RowId` at `snapshot`, **including
417    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
418    /// versions across the memtable and sorted runs.
419    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
420        self.visible_versions_at(crate::epoch::Snapshot::at(snapshot_epoch))
421    }
422
423    pub fn visible_versions_at(&self, snapshot: crate::epoch::Snapshot) -> Vec<Row> {
424        self.newest_visible_map(snapshot).into_values().collect()
425    }
426
427    /// Newest visible version per `RowId` as an ordered map (ascending RowId).
428    /// Callers that need to stream into a controlled merge without a second
429    /// full `Vec` should drain this map in batches rather than collecting.
430    pub(crate) fn newest_visible_map(
431        &self,
432        snapshot: crate::epoch::Snapshot,
433    ) -> BTreeMap<RowId, Row> {
434        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
435        for segment in std::iter::once(&self.active.tree)
436            .chain(self.frozen.iter().rev().map(|segment| &segment.tree))
437        {
438            for row in segment.versions() {
439                if !snapshot.observes_version(row.committed_epoch, row.commit_ts) {
440                    continue;
441                }
442                by_row
443                    .entry(row.row_id)
444                    .and_modify(|existing| {
445                        if crate::epoch::Snapshot::version_is_newer(
446                            row.committed_epoch,
447                            row.commit_ts,
448                            existing.committed_epoch,
449                            existing.commit_ts,
450                        ) {
451                            *existing = row.clone();
452                        }
453                    })
454                    .or_insert(row);
455            }
456        }
457        by_row
458    }
459
460    pub fn newest_visible_iter<'a>(
461        &'a self,
462        snapshot: &Snapshot,
463    ) -> MemtableVisibleVersionCursor<'a> {
464        let leaves = self
465            .frozen
466            .iter()
467            .map(|segment| segment.tree.leaf_versions_iter())
468            .chain(std::iter::once(self.active.tree.leaf_versions_iter()))
469            .collect();
470        MemtableVisibleVersionCursor {
471            leaves,
472            leaf_index: 0,
473            snapshot: *snapshot,
474            current_row_id: None,
475            best: None,
476            finished: false,
477        }
478    }
479
480    /// Freeze the current write delta so future clones share it by `Arc`.
481    pub(crate) fn seal(&mut self) {
482        if self.active.tree.is_empty() {
483            return;
484        }
485        let active = std::mem::replace(
486            &mut self.active,
487            MemtableSegment {
488                tree: BeTree::new(),
489                byte_size: 0,
490            },
491        );
492        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
493        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
494            self.consolidate();
495        }
496    }
497
498    fn consolidate(&mut self) {
499        let mut tree = BeTree::new();
500        for row in self
501            .frozen
502            .iter()
503            .flat_map(|segment| segment.tree.versions())
504        {
505            tree.insert_row(row);
506        }
507        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
508            tree,
509            byte_size: self.byte_size,
510        })]);
511    }
512
513    #[cfg(test)]
514    pub(crate) fn frozen_layer_count(&self) -> usize {
515        self.frozen.len()
516    }
517
518    /// Drain all versions (for a memtable-to-run flush). Returns them in
519    /// ascending `(RowId, Epoch)` order.
520    pub fn drain_sorted(&mut self) -> Vec<Row> {
521        let mut out = self
522            .frozen
523            .iter()
524            .flat_map(|segment| segment.tree.versions())
525            .chain(self.active.tree.versions())
526            .collect::<Vec<_>>();
527        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
528        self.frozen = Arc::new(Vec::new());
529        self.active = MemtableSegment {
530            tree: BeTree::new(),
531            byte_size: 0,
532        };
533        self.byte_size = 0;
534        out
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    fn row(id: u64, epoch: u64) -> Row {
543        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
544    }
545
546    #[test]
547    fn upsert_get_and_visibility() {
548        let mut m = Memtable::new();
549        m.upsert(row(1, 5));
550        assert_eq!(m.len(), 1);
551        assert!(m.get(RowId(1), Epoch(5)).is_some());
552        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
553        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
554    }
555
556    #[test]
557    fn tombstone_supersedes_at_its_epoch() {
558        let mut m = Memtable::new();
559        m.upsert(row(1, 1));
560        // Before the tombstone: the live version is visible.
561        assert!(m.get(RowId(1), Epoch(1)).is_some());
562        m.tombstone(RowId(1), Epoch(2));
563        // At/after the tombstone: hidden.
564        assert!(m.get(RowId(1), Epoch(2)).is_none());
565        assert!(m.get(RowId(1), Epoch(9)).is_none());
566        // A snapshot before the tombstone still sees the live version.
567        assert!(m.get(RowId(1), Epoch(1)).is_some());
568    }
569
570    #[test]
571    fn sealed_generations_share_rows_and_consolidate() {
572        let mut writer = Memtable::new();
573        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
574            writer.upsert(row(id, id + 1));
575            writer.seal();
576        }
577        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
578        let generation = writer.clone();
579        writer.upsert(row(99, 99));
580        assert!(generation.get(RowId(99), Epoch(99)).is_none());
581        assert!(writer.get(RowId(99), Epoch(99)).is_some());
582    }
583
584    #[test]
585    fn hlc_visibility_is_authoritative_when_stamped() {
586        use mongreldb_types::hlc::HlcTimestamp;
587        let mut m = Memtable::new();
588        let early = HlcTimestamp {
589            physical_micros: 100,
590            logical: 0,
591            node_tiebreaker: 1,
592        };
593        let late = HlcTimestamp {
594            physical_micros: 200,
595            logical: 0,
596            node_tiebreaker: 1,
597        };
598        let mut r1 = Row::new_with_hlc(RowId(1), Epoch(1), early);
599        r1.columns.insert(1, Value::Int64(1));
600        let mut r2 = Row::new_with_hlc(RowId(1), Epoch(2), late);
601        r2.columns.insert(1, Value::Int64(2));
602        m.upsert(r1);
603        m.upsert(r2);
604        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
605        let versions = m.visible_versions_at(snap);
606        assert_eq!(versions.len(), 1);
607        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
608        let snap2 = crate::epoch::Snapshot::at_hlc(Epoch(1), late);
609        assert_eq!(
610            m.visible_versions_at(snap2)[0].columns.get(&1),
611            Some(&Value::Int64(2))
612        );
613    }
614
615    #[test]
616    fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
617        use mongreldb_types::hlc::HlcTimestamp;
618        let mut m = Memtable::new();
619        let early = HlcTimestamp {
620            physical_micros: 100,
621            logical: 0,
622            node_tiebreaker: 1,
623        };
624        let late = HlcTimestamp {
625            physical_micros: 200,
626            logical: 0,
627            node_tiebreaker: 1,
628        };
629        // Epoch(1) with late HLC would win under epoch-only rules when snap
630        // epoch is 99 — HLC authority must hide it under an early pin.
631        let mut late_row = Row::new_with_hlc(RowId(1), Epoch(1), late);
632        late_row.columns.insert(1, Value::Int64(99));
633        let mut early_row = Row::new_with_hlc(RowId(1), Epoch(50), early);
634        early_row.columns.insert(1, Value::Int64(1));
635        m.upsert(late_row);
636        m.upsert(early_row);
637        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
638        let versions = m.visible_versions_at(snap);
639        assert_eq!(versions.len(), 1);
640        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
641        assert_eq!(versions[0].commit_ts, Some(early));
642    }
643
644    #[test]
645    fn epoch_only_snapshot_sees_hlc_stamped_rows_by_epoch() {
646        use mongreldb_types::hlc::HlcTimestamp;
647        let mut m = Memtable::new();
648        let ts = HlcTimestamp {
649            physical_micros: 50,
650            logical: 0,
651            node_tiebreaker: 1,
652        };
653        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), ts).with_column(1, Value::Int64(1)));
654        m.upsert(Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)));
655        let legacy = crate::epoch::Snapshot::at(Epoch(99));
656        let versions = m.visible_versions_at(legacy);
657        assert_eq!(
658            versions.len(),
659            2,
660            "dual-model: epoch pin sees HLC rows by epoch"
661        );
662        assert!(m.get_version_at(RowId(1), legacy).is_some());
663        assert!(m.get_version_at(RowId(2), legacy).is_some());
664        let future = crate::epoch::Snapshot::at(Epoch(0));
665        assert!(m.get_version_at(RowId(1), future).is_none());
666    }
667
668    #[test]
669    fn get_version_at_prefers_hlc_over_epoch_order() {
670        use mongreldb_types::hlc::HlcTimestamp;
671        let mut m = Memtable::new();
672        let early = HlcTimestamp {
673            physical_micros: 100,
674            logical: 0,
675            node_tiebreaker: 1,
676        };
677        let late = HlcTimestamp {
678            physical_micros: 200,
679            logical: 0,
680            node_tiebreaker: 1,
681        };
682        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
683        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
684        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
685        let (_, row) = m.get_version_at(RowId(1), snap).expect("visible");
686        assert_eq!(row.columns.get(&1), Some(&Value::Int64(1)));
687        assert_eq!(row.commit_ts, Some(early));
688    }
689
690    /// WAL `Put` payloads must keep the 0.63.1 bincode layout
691    /// `(row_id, committed_epoch, columns, deleted)`. `commit_ts` is
692    /// in-memory only (`#[serde(skip)]`) so a 0.63.1-shaped blob still opens.
693    #[test]
694    fn wal_put_row_bincode_matches_0_63_1_layout() {
695        use mongreldb_types::hlc::HlcTimestamp;
696        use serde::{Deserialize, Serialize};
697
698        #[derive(Serialize, Deserialize)]
699        struct LegacyRow {
700            row_id: RowId,
701            committed_epoch: Epoch,
702            columns: HashMap<u16, Value>,
703            deleted: bool,
704        }
705
706        let legacy = LegacyRow {
707            row_id: RowId(7),
708            committed_epoch: Epoch(3),
709            columns: [(1, Value::Int64(42))].into_iter().collect(),
710            deleted: false,
711        };
712        let bytes = bincode::serialize(&legacy).expect("legacy encode");
713
714        let decoded: Row = bincode::deserialize(&bytes).expect("0.63.1 payload must decode");
715        assert_eq!(decoded.row_id, RowId(7));
716        assert_eq!(decoded.committed_epoch, Epoch(3));
717        assert_eq!(decoded.columns.get(&1), Some(&Value::Int64(42)));
718        assert!(!decoded.deleted);
719        assert!(decoded.commit_ts.is_none());
720
721        // Round-trip through Row: commit_ts is not on the wire.
722        let stamped = HlcTimestamp {
723            physical_micros: 1_700_000_000_000,
724            logical: 2,
725            node_tiebreaker: 9,
726        };
727        let mut live = Row::new_with_hlc(RowId(7), Epoch(3), stamped);
728        live.columns.insert(1, Value::Int64(42));
729        let wire = bincode::serialize(&live).expect("row encode");
730        assert_eq!(
731            wire, bytes,
732            "WAL Put encoding must match the 0.63.1 four-field layout"
733        );
734        let again: Row = bincode::deserialize(&wire).expect("row decode");
735        assert!(
736            again.commit_ts.is_none(),
737            "commit_ts is restored from Op::CommitTimestamp, not the Put blob"
738        );
739    }
740
741    #[test]
742    fn drain_sorted_is_ascending_and_empties() {
743        let mut m = Memtable::new();
744        m.upsert(row(3, 1));
745        m.upsert(row(1, 1));
746        m.upsert(row(2, 1));
747        let out = m.drain_sorted();
748        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
749        assert_eq!(ids, vec![1, 2, 3]);
750        assert!(m.is_empty());
751        assert_eq!(m.approx_bytes(), 0);
752    }
753
754    #[test]
755    fn visible_rows_dedups_to_newest_version() {
756        let mut m = Memtable::new();
757        m.upsert(row(1, 1));
758        m.upsert(row(2, 9)); // future relative to snapshot 5
759        m.upsert(row(3, 1));
760        m.upsert(row(1, 3)); // newer version of row 1
761        let ids: Vec<u64> = m
762            .visible_rows(Epoch(5))
763            .iter()
764            .map(|r| r.row_id.0)
765            .collect();
766        assert_eq!(ids, vec![1, 3]);
767    }
768
769    #[test]
770    fn newest_visible_map_prefers_active_on_equal_version() {
771        let mut m = Memtable::new();
772        let mut deleted = row(1, 2);
773        deleted.deleted = true;
774        m.upsert(deleted);
775        m.seal();
776        m.upsert(row(1, 2));
777
778        let versions = m.visible_versions_at(Snapshot::at(Epoch(2)));
779        assert_eq!(versions.len(), 1);
780        assert!(!versions[0].deleted);
781    }
782
783    #[test]
784    fn newest_visible_iter_empty_memtable_yields_nothing() {
785        let m = Memtable::new();
786        assert!(m
787            .newest_visible_iter(&Snapshot::at(Epoch(9)))
788            .next()
789            .is_none());
790    }
791
792    #[test]
793    fn newest_visible_iter_single_insert_yields_one() {
794        let mut m = Memtable::new();
795        m.upsert(row(1, 3));
796        let values: Vec<_> = m
797            .newest_visible_iter(&Snapshot::at(Epoch(3)))
798            .map(|(id, epoch, _)| (id, epoch))
799            .collect();
800        assert_eq!(values, vec![(RowId(1), Epoch(3))]);
801    }
802
803    #[test]
804    fn newest_visible_iter_newer_epoch_wins() {
805        let mut m = Memtable::new();
806        m.upsert(row(1, 1));
807        m.upsert(row(1, 2));
808        let values: Vec<_> = m
809            .newest_visible_iter(&Snapshot::at(Epoch(2)))
810            .map(|(_, epoch, _)| epoch)
811            .collect();
812        assert_eq!(values, vec![Epoch(2)]);
813    }
814
815    #[test]
816    fn newest_visible_iter_tombstone_suppresses_older_live_version() {
817        // Mirrors MutableRunVisibleVersionCursor: the cursor yields the
818        // tombstone Row itself (so the caller can classify it as a Tombstone
819        // fallback or a StaleRowId), but the pre-tombstone live version is
820        // suppressed when the tombstone is in scope of the calling snapshot.
821        let mut m = Memtable::new();
822        m.upsert(row(1, 1));
823        m.tombstone(RowId(1), Epoch(2));
824        let snap = Snapshot::at(Epoch(2));
825        let got: Vec<(u64, u64, bool)> = m
826            .newest_visible_iter(&snap)
827            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
828            .collect();
829        assert_eq!(got, vec![(1, 2, true)], "tombstone is the newest");
830        // Pre-tombstone snapshot still sees the live version.
831        let snap_early = Snapshot::at(Epoch(1));
832        let got_early: Vec<(u64, u64, bool)> = m
833            .newest_visible_iter(&snap_early)
834            .map(|(rid, epoch, row)| (rid.0, epoch.0, row.deleted))
835            .collect();
836        assert_eq!(got_early, vec![(1, 1, false)]);
837    }
838
839    /// Regression for the BeTree root-buffer-not-iterated bug (iss10).
840    ///
841    /// Before the fix, [`crate::be_tree::LeafVersions`] walked only the
842    /// consolidated leaves of the Bε-tree — silently skipping messages that
843    /// were still sitting in an internal-node buffer pending flush. A scan
844    /// over a live memtable that has triggered at least one split therefore
845    /// returned a subset of the inserted rows (typically the leaf-resident
846    /// ones, missing every row still buffered at the root).
847    ///
848    /// This test inserts 1,000 rows without flushing. The first
849    /// `LEAF_CAP = 32` rows go directly into a leaf; subsequent splits and
850    /// buffer flushes leave a meaningful fraction of the rows sitting in
851    /// internal-node buffers. The streaming cursor must yield all 1,000
852    /// distinct `(RowId, Epoch)` pairs.
853    #[test]
854    fn newest_visible_iter_includes_root_buffer_rows() {
855        const N: u64 = 1_000;
856        let mut m = Memtable::new();
857        // Each row gets a fresh RowId and its own (epoch-bumped) version; the
858        // memtable has no flush path in this scope so the rows have to be
859        // reachable via the active BeTree.
860        for i in 0..N {
861            let mut r = Row::new(RowId(i), Epoch(i + 1));
862            r.columns.insert(1, Value::Int64(i as i64 * 10));
863            m.upsert(r);
864        }
865        assert_eq!(m.len(), N as usize);
866
867        // Snapshot high enough that every version is visible.
868        let snap = Snapshot::at(Epoch(N + 10));
869        let got: Vec<(u64, u64, i64)> = m
870            .newest_visible_iter(&snap)
871            .map(|(rid, epoch, row)| (rid.0, epoch.0, int_of_value(&row)))
872            .collect();
873
874        // Count: every distinct RowId must be visible exactly once.
875        assert_eq!(
876            got.len(),
877            N as usize,
878            "buffered rows must be visible to a streaming scan (got {})",
879            got.len()
880        );
881        let mut seen_row_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
882        for (rid, _epoch, _v) in &got {
883            assert!(
884                seen_row_ids.insert(*rid),
885                "duplicate RowId {rid} in streaming scan output"
886            );
887        }
888        // Set equality: every input RowId was emitted, no extras.
889        let expected_ids: std::collections::HashSet<u64> = (0..N).collect();
890        let got_ids: std::collections::HashSet<u64> = got.iter().map(|(rid, _, _)| *rid).collect();
891        assert_eq!(got_ids, expected_ids, "must yield every input RowId");
892
893        // Spot-check: epoch and column bytes for a buffered row (high RowId
894        // is almost certainly still buffered, not yet flushed to a leaf).
895        let (_, epoch_raw, v) = got
896            .iter()
897            .find(|(rid, _, _)| *rid == N - 1)
898            .copied()
899            .expect("highest RowId present");
900        assert_eq!(epoch_raw, N);
901        assert_eq!(v, (N as i64 - 1) * 10);
902    }
903
904    /// Same shape as `newest_visible_iter_includes_root_buffer_rows`, but
905    /// drives the cursor against one row with many versions — exercising the
906    /// case where the same `RowId` coexists in both a leaf and the root
907    /// buffer (dedup picks the newest visible, which must come from the
908    /// buffer when the buffered version is the latest).
909    #[test]
910    fn newest_visible_iter_buffered_versions_dedup_against_leaf_resident() {
911        // Seed the leaf via a few inserts.
912        let mut m = Memtable::new();
913        for i in 0..16u64 {
914            let mut r = Row::new(RowId(7), Epoch(i + 1));
915            r.columns.insert(1, Value::Int64(i as i64));
916            m.upsert(r);
917        }
918        // Then drive the tree through several splits by inserting many more
919        // rows so the buffered message for the same RowId has to coexist with
920        // the leaf-resident version (different epochs for the same RowId in
921        // two locations).
922        for i in 0u64..4_000 {
923            let mut r = Row::new(RowId(1000 + i), Epoch(i + 100));
924            r.columns.insert(1, Value::Int64(i as i64));
925            m.upsert(r);
926        }
927        // One more version of row 7, distinctly newer than the leaf-resident
928        // ones — guaranteed to land in some internal-node buffer.
929        let mut latest_seven = Row::new(RowId(7), Epoch(20_000));
930        latest_seven.columns.insert(1, Value::Int64(999));
931        m.upsert(latest_seven);
932
933        let snap = Snapshot::at(Epoch(20_001));
934        let seven = m
935            .newest_visible_iter(&snap)
936            .find(|(rid, _epoch, _row)| *rid == RowId(7))
937            .expect("row 7 visible");
938        assert_eq!(seven.1, Epoch(20_000), "buffered newest wins");
939        // The full cursor must yield every distinct RowId (latest version):
940        // 4,000 background rows + 1 distinct version of row 7.
941        let total: u64 = m
942            .newest_visible_iter(&snap)
943            .map(|(rid, _epoch, _row)| rid.0)
944            .fold(0u64, |acc, _| acc + 1);
945        assert_eq!(total, 4_001, "buffer + leaf coverage");
946    }
947
948    fn int_of_value(row: &Row) -> i64 {
949        match row.columns.get(&1) {
950            Some(Value::Int64(x)) => *x,
951            other => panic!("expected Int64 column, got {other:?}"),
952        }
953    }
954}