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;
14use crate::epoch::Epoch;
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
186/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
187/// for the prototype skip list: the same MVCC semantics with lower write
188/// amplification (buffered messages flush to children in bulk).
189#[derive(Clone)]
190struct MemtableSegment {
191    tree: BeTree,
192    byte_size: u64,
193}
194
195/// Structurally shared committed overlays plus one small mutable write delta.
196#[derive(Clone)]
197pub struct Memtable {
198    frozen: Arc<Vec<Arc<MemtableSegment>>>,
199    active: MemtableSegment,
200    byte_size: u64,
201}
202
203impl Default for Memtable {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl Memtable {
210    pub fn new() -> Self {
211        Self {
212            frozen: Arc::new(Vec::new()),
213            active: MemtableSegment {
214                tree: BeTree::new(),
215                byte_size: 0,
216            },
217            byte_size: 0,
218        }
219    }
220
221    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
222    /// never overwritten; the newest visible one wins at read time.
223    pub fn upsert(&mut self, row: Row) {
224        let bytes = row.estimated_bytes();
225        self.byte_size = self.byte_size.saturating_add(bytes);
226        self.active.byte_size = self.active.byte_size.saturating_add(bytes);
227        self.active.tree.insert_row(row);
228    }
229
230    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
231    /// the columns from the newest live version so that engine-level HOT cleanup
232    /// can recover the primary-key value during WAL replay.
233    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
234        let mut columns = HashMap::new();
235        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
236            columns = live.columns;
237        }
238        let row = Row {
239            row_id,
240            committed_epoch: epoch,
241            columns,
242            deleted: true,
243            commit_ts: None,
244        };
245        self.upsert(row);
246    }
247
248    /// Read the row at `row_id` visible to `snapshot`: the newest version with
249    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
250    /// such version exists).
251    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
252        self.get_version(row_id, snapshot_epoch)
253            .and_then(|(_, row)| (!row.deleted).then_some(row))
254    }
255
256    /// Newest version of `row_id` with `epoch <= snapshot`, **including
257    /// tombstones** (as a `Row` with `deleted=true`). Legacy epoch-only entry
258    /// point; prefer [`Self::get_version_at`] when the caller holds a full
259    /// [`crate::epoch::Snapshot`].
260    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
261        self.get_version_at(row_id, crate::epoch::Snapshot::at(snapshot_epoch))
262    }
263
264    /// Newest version of `row_id` visible under `snapshot` (including
265    /// tombstones). Uses HLC authority when stamps are present (P0.5-T3).
266    ///
267    /// Scans each segment's versions of `row_id` so dual-model mixes
268    /// (stamped + unstamped) and HLC/epoch order inversions stay correct.
269    pub fn get_version_at(
270        &self,
271        row_id: RowId,
272        snapshot: crate::epoch::Snapshot,
273    ) -> Option<(Epoch, Row)> {
274        let mut best: Option<Row> = None;
275        for segment in self
276            .frozen
277            .iter()
278            .map(|segment| &segment.tree)
279            .chain(std::iter::once(&self.active.tree))
280        {
281            for row in segment.versions() {
282                if row.row_id != row_id {
283                    continue;
284                }
285                if !snapshot.observes_row(row.committed_epoch, row.commit_ts) {
286                    continue;
287                }
288                if best.as_ref().is_none_or(|current| {
289                    crate::epoch::Snapshot::version_is_newer(
290                        row.committed_epoch,
291                        row.commit_ts,
292                        current.committed_epoch,
293                        current.commit_ts,
294                    )
295                }) {
296                    best = Some(row);
297                }
298            }
299        }
300        best.map(|row| (row.committed_epoch, row))
301    }
302
303    /// Number of stored versions.
304    pub fn len(&self) -> usize {
305        self.active.tree.mutations()
306            + self
307                .frozen
308                .iter()
309                .map(|segment| segment.tree.mutations())
310                .sum::<usize>()
311    }
312
313    pub fn is_empty(&self) -> bool {
314        self.active.tree.is_empty() && self.frozen.is_empty()
315    }
316
317    pub fn approx_bytes(&self) -> u64 {
318        self.byte_size
319    }
320
321    /// Visible rows at `snapshot`, deduplicated to the newest version per
322    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
323    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
324        self.visible_versions(snapshot_epoch)
325            .into_iter()
326            .filter(|r| !r.deleted)
327            .collect()
328    }
329
330    /// Newest visible version per `RowId` at `snapshot`, **including
331    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
332    /// versions across the memtable and sorted runs.
333    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
334        self.visible_versions_at(crate::epoch::Snapshot::at(snapshot_epoch))
335    }
336
337    pub fn visible_versions_at(&self, snapshot: crate::epoch::Snapshot) -> Vec<Row> {
338        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
339        for segment in self
340            .frozen
341            .iter()
342            .map(|segment| &segment.tree)
343            .chain(std::iter::once(&self.active.tree))
344        {
345            for row in segment.versions() {
346                if !snapshot.observes_version(row.committed_epoch, row.commit_ts) {
347                    continue;
348                }
349                by_row
350                    .entry(row.row_id)
351                    .and_modify(|existing| {
352                        if crate::epoch::Snapshot::version_is_newer(
353                            row.committed_epoch,
354                            row.commit_ts,
355                            existing.committed_epoch,
356                            existing.commit_ts,
357                        ) {
358                            *existing = row.clone();
359                        }
360                    })
361                    .or_insert(row);
362            }
363        }
364        by_row.into_values().collect()
365    }
366
367    /// Freeze the current write delta so future clones share it by `Arc`.
368    pub(crate) fn seal(&mut self) {
369        if self.active.tree.is_empty() {
370            return;
371        }
372        let active = std::mem::replace(
373            &mut self.active,
374            MemtableSegment {
375                tree: BeTree::new(),
376                byte_size: 0,
377            },
378        );
379        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
380        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
381            self.consolidate();
382        }
383    }
384
385    fn consolidate(&mut self) {
386        let mut tree = BeTree::new();
387        for row in self
388            .frozen
389            .iter()
390            .flat_map(|segment| segment.tree.versions())
391        {
392            tree.insert_row(row);
393        }
394        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
395            tree,
396            byte_size: self.byte_size,
397        })]);
398    }
399
400    #[cfg(test)]
401    pub(crate) fn frozen_layer_count(&self) -> usize {
402        self.frozen.len()
403    }
404
405    /// Drain all versions (for a memtable-to-run flush). Returns them in
406    /// ascending `(RowId, Epoch)` order.
407    pub fn drain_sorted(&mut self) -> Vec<Row> {
408        let mut out = self
409            .frozen
410            .iter()
411            .flat_map(|segment| segment.tree.versions())
412            .chain(self.active.tree.versions())
413            .collect::<Vec<_>>();
414        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
415        self.frozen = Arc::new(Vec::new());
416        self.active = MemtableSegment {
417            tree: BeTree::new(),
418            byte_size: 0,
419        };
420        self.byte_size = 0;
421        out
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    fn row(id: u64, epoch: u64) -> Row {
430        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
431    }
432
433    #[test]
434    fn upsert_get_and_visibility() {
435        let mut m = Memtable::new();
436        m.upsert(row(1, 5));
437        assert_eq!(m.len(), 1);
438        assert!(m.get(RowId(1), Epoch(5)).is_some());
439        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
440        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
441    }
442
443    #[test]
444    fn tombstone_supersedes_at_its_epoch() {
445        let mut m = Memtable::new();
446        m.upsert(row(1, 1));
447        // Before the tombstone: the live version is visible.
448        assert!(m.get(RowId(1), Epoch(1)).is_some());
449        m.tombstone(RowId(1), Epoch(2));
450        // At/after the tombstone: hidden.
451        assert!(m.get(RowId(1), Epoch(2)).is_none());
452        assert!(m.get(RowId(1), Epoch(9)).is_none());
453        // A snapshot before the tombstone still sees the live version.
454        assert!(m.get(RowId(1), Epoch(1)).is_some());
455    }
456
457    #[test]
458    fn sealed_generations_share_rows_and_consolidate() {
459        let mut writer = Memtable::new();
460        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
461            writer.upsert(row(id, id + 1));
462            writer.seal();
463        }
464        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
465        let generation = writer.clone();
466        writer.upsert(row(99, 99));
467        assert!(generation.get(RowId(99), Epoch(99)).is_none());
468        assert!(writer.get(RowId(99), Epoch(99)).is_some());
469    }
470
471    #[test]
472    fn hlc_visibility_is_authoritative_when_stamped() {
473        use mongreldb_types::hlc::HlcTimestamp;
474        let mut m = Memtable::new();
475        let early = HlcTimestamp {
476            physical_micros: 100,
477            logical: 0,
478            node_tiebreaker: 1,
479        };
480        let late = HlcTimestamp {
481            physical_micros: 200,
482            logical: 0,
483            node_tiebreaker: 1,
484        };
485        let mut r1 = Row::new_with_hlc(RowId(1), Epoch(1), early);
486        r1.columns.insert(1, Value::Int64(1));
487        let mut r2 = Row::new_with_hlc(RowId(1), Epoch(2), late);
488        r2.columns.insert(1, Value::Int64(2));
489        m.upsert(r1);
490        m.upsert(r2);
491        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
492        let versions = m.visible_versions_at(snap);
493        assert_eq!(versions.len(), 1);
494        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
495        let snap2 = crate::epoch::Snapshot::at_hlc(Epoch(1), late);
496        assert_eq!(
497            m.visible_versions_at(snap2)[0].columns.get(&1),
498            Some(&Value::Int64(2))
499        );
500    }
501
502    #[test]
503    fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
504        use mongreldb_types::hlc::HlcTimestamp;
505        let mut m = Memtable::new();
506        let early = HlcTimestamp {
507            physical_micros: 100,
508            logical: 0,
509            node_tiebreaker: 1,
510        };
511        let late = HlcTimestamp {
512            physical_micros: 200,
513            logical: 0,
514            node_tiebreaker: 1,
515        };
516        // Epoch(1) with late HLC would win under epoch-only rules when snap
517        // epoch is 99 — HLC authority must hide it under an early pin.
518        let mut late_row = Row::new_with_hlc(RowId(1), Epoch(1), late);
519        late_row.columns.insert(1, Value::Int64(99));
520        let mut early_row = Row::new_with_hlc(RowId(1), Epoch(50), early);
521        early_row.columns.insert(1, Value::Int64(1));
522        m.upsert(late_row);
523        m.upsert(early_row);
524        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
525        let versions = m.visible_versions_at(snap);
526        assert_eq!(versions.len(), 1);
527        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
528        assert_eq!(versions[0].commit_ts, Some(early));
529    }
530
531    #[test]
532    fn epoch_only_snapshot_sees_hlc_stamped_rows_by_epoch() {
533        use mongreldb_types::hlc::HlcTimestamp;
534        let mut m = Memtable::new();
535        let ts = HlcTimestamp {
536            physical_micros: 50,
537            logical: 0,
538            node_tiebreaker: 1,
539        };
540        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), ts).with_column(1, Value::Int64(1)));
541        m.upsert(Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)));
542        let legacy = crate::epoch::Snapshot::at(Epoch(99));
543        let versions = m.visible_versions_at(legacy);
544        assert_eq!(
545            versions.len(),
546            2,
547            "dual-model: epoch pin sees HLC rows by epoch"
548        );
549        assert!(m.get_version_at(RowId(1), legacy).is_some());
550        assert!(m.get_version_at(RowId(2), legacy).is_some());
551        let future = crate::epoch::Snapshot::at(Epoch(0));
552        assert!(m.get_version_at(RowId(1), future).is_none());
553    }
554
555    #[test]
556    fn get_version_at_prefers_hlc_over_epoch_order() {
557        use mongreldb_types::hlc::HlcTimestamp;
558        let mut m = Memtable::new();
559        let early = HlcTimestamp {
560            physical_micros: 100,
561            logical: 0,
562            node_tiebreaker: 1,
563        };
564        let late = HlcTimestamp {
565            physical_micros: 200,
566            logical: 0,
567            node_tiebreaker: 1,
568        };
569        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
570        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
571        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
572        let (_, row) = m.get_version_at(RowId(1), snap).expect("visible");
573        assert_eq!(row.columns.get(&1), Some(&Value::Int64(1)));
574        assert_eq!(row.commit_ts, Some(early));
575    }
576
577    /// WAL `Put` payloads must keep the 0.63.1 bincode layout
578    /// `(row_id, committed_epoch, columns, deleted)`. `commit_ts` is
579    /// in-memory only (`#[serde(skip)]`) so a 0.63.1-shaped blob still opens.
580    #[test]
581    fn wal_put_row_bincode_matches_0_63_1_layout() {
582        use mongreldb_types::hlc::HlcTimestamp;
583        use serde::{Deserialize, Serialize};
584
585        #[derive(Serialize, Deserialize)]
586        struct LegacyRow {
587            row_id: RowId,
588            committed_epoch: Epoch,
589            columns: HashMap<u16, Value>,
590            deleted: bool,
591        }
592
593        let legacy = LegacyRow {
594            row_id: RowId(7),
595            committed_epoch: Epoch(3),
596            columns: [(1, Value::Int64(42))].into_iter().collect(),
597            deleted: false,
598        };
599        let bytes = bincode::serialize(&legacy).expect("legacy encode");
600
601        let decoded: Row = bincode::deserialize(&bytes).expect("0.63.1 payload must decode");
602        assert_eq!(decoded.row_id, RowId(7));
603        assert_eq!(decoded.committed_epoch, Epoch(3));
604        assert_eq!(decoded.columns.get(&1), Some(&Value::Int64(42)));
605        assert!(!decoded.deleted);
606        assert!(decoded.commit_ts.is_none());
607
608        // Round-trip through Row: commit_ts is not on the wire.
609        let stamped = HlcTimestamp {
610            physical_micros: 1_700_000_000_000,
611            logical: 2,
612            node_tiebreaker: 9,
613        };
614        let mut live = Row::new_with_hlc(RowId(7), Epoch(3), stamped);
615        live.columns.insert(1, Value::Int64(42));
616        let wire = bincode::serialize(&live).expect("row encode");
617        assert_eq!(
618            wire, bytes,
619            "WAL Put encoding must match the 0.63.1 four-field layout"
620        );
621        let again: Row = bincode::deserialize(&wire).expect("row decode");
622        assert!(
623            again.commit_ts.is_none(),
624            "commit_ts is restored from Op::CommitTimestamp, not the Put blob"
625        );
626    }
627
628    #[test]
629    fn drain_sorted_is_ascending_and_empties() {
630        let mut m = Memtable::new();
631        m.upsert(row(3, 1));
632        m.upsert(row(1, 1));
633        m.upsert(row(2, 1));
634        let out = m.drain_sorted();
635        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
636        assert_eq!(ids, vec![1, 2, 3]);
637        assert!(m.is_empty());
638        assert_eq!(m.approx_bytes(), 0);
639    }
640
641    #[test]
642    fn visible_rows_dedups_to_newest_version() {
643        let mut m = Memtable::new();
644        m.upsert(row(1, 1));
645        m.upsert(row(2, 9)); // future relative to snapshot 5
646        m.upsert(row(3, 1));
647        m.upsert(row(1, 3)); // newer version of row 1
648        let ids: Vec<u64> = m
649            .visible_rows(Epoch(5))
650            .iter()
651            .map(|r| r.row_id.0)
652            .collect();
653        assert_eq!(ids, vec![1, 3]);
654    }
655}