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