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    /// Seeks each segment's composite-key range for `row_id` so dual-model
268    /// mixes (stamped + unstamped) and HLC/epoch order inversions stay correct
269    /// without materializing every version in the memtable.
270    pub fn get_version_at(
271        &self,
272        row_id: RowId,
273        snapshot: crate::epoch::Snapshot,
274    ) -> Option<(Epoch, Row)> {
275        if !snapshot.uses_hlc_authority() {
276            let mut best = self.active.tree.get_version(row_id, snapshot.epoch);
277            for segment in self.frozen.iter().rev() {
278                let Some(candidate) = segment.tree.get_version(row_id, snapshot.epoch) else {
279                    continue;
280                };
281                if best.as_ref().is_none_or(|(epoch, _)| candidate.0 > *epoch) {
282                    best = Some(candidate);
283                }
284            }
285            return best;
286        }
287
288        let mut best: Option<Row> = None;
289        for segment in self
290            .frozen
291            .iter()
292            .map(|segment| &segment.tree)
293            .chain(std::iter::once(&self.active.tree))
294        {
295            segment.visit_versions(row_id, |row| {
296                if !snapshot.observes_row(row.committed_epoch, row.commit_ts) {
297                    return;
298                }
299                if best.as_ref().is_none_or(|current| {
300                    crate::epoch::Snapshot::version_is_newer(
301                        row.committed_epoch,
302                        row.commit_ts,
303                        current.committed_epoch,
304                        current.commit_ts,
305                    )
306                }) {
307                    best = Some(row);
308                }
309            });
310        }
311        best.map(|row| (row.committed_epoch, row))
312    }
313
314    /// Number of stored versions.
315    pub fn len(&self) -> usize {
316        self.active.tree.mutations()
317            + self
318                .frozen
319                .iter()
320                .map(|segment| segment.tree.mutations())
321                .sum::<usize>()
322    }
323
324    pub fn is_empty(&self) -> bool {
325        self.active.tree.is_empty() && self.frozen.is_empty()
326    }
327
328    pub fn approx_bytes(&self) -> u64 {
329        self.byte_size
330    }
331
332    /// Visible rows at `snapshot`, deduplicated to the newest version per
333    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
334    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
335        self.visible_versions(snapshot_epoch)
336            .into_iter()
337            .filter(|r| !r.deleted)
338            .collect()
339    }
340
341    /// Newest visible version per `RowId` at `snapshot`, **including
342    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
343    /// versions across the memtable and sorted runs.
344    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
345        self.visible_versions_at(crate::epoch::Snapshot::at(snapshot_epoch))
346    }
347
348    pub fn visible_versions_at(&self, snapshot: crate::epoch::Snapshot) -> Vec<Row> {
349        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
350        for segment in self
351            .frozen
352            .iter()
353            .map(|segment| &segment.tree)
354            .chain(std::iter::once(&self.active.tree))
355        {
356            for row in segment.versions() {
357                if !snapshot.observes_version(row.committed_epoch, row.commit_ts) {
358                    continue;
359                }
360                by_row
361                    .entry(row.row_id)
362                    .and_modify(|existing| {
363                        if crate::epoch::Snapshot::version_is_newer(
364                            row.committed_epoch,
365                            row.commit_ts,
366                            existing.committed_epoch,
367                            existing.commit_ts,
368                        ) {
369                            *existing = row.clone();
370                        }
371                    })
372                    .or_insert(row);
373            }
374        }
375        by_row.into_values().collect()
376    }
377
378    /// Freeze the current write delta so future clones share it by `Arc`.
379    pub(crate) fn seal(&mut self) {
380        if self.active.tree.is_empty() {
381            return;
382        }
383        let active = std::mem::replace(
384            &mut self.active,
385            MemtableSegment {
386                tree: BeTree::new(),
387                byte_size: 0,
388            },
389        );
390        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
391        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
392            self.consolidate();
393        }
394    }
395
396    fn consolidate(&mut self) {
397        let mut tree = BeTree::new();
398        for row in self
399            .frozen
400            .iter()
401            .flat_map(|segment| segment.tree.versions())
402        {
403            tree.insert_row(row);
404        }
405        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
406            tree,
407            byte_size: self.byte_size,
408        })]);
409    }
410
411    #[cfg(test)]
412    pub(crate) fn frozen_layer_count(&self) -> usize {
413        self.frozen.len()
414    }
415
416    /// Drain all versions (for a memtable-to-run flush). Returns them in
417    /// ascending `(RowId, Epoch)` order.
418    pub fn drain_sorted(&mut self) -> Vec<Row> {
419        let mut out = self
420            .frozen
421            .iter()
422            .flat_map(|segment| segment.tree.versions())
423            .chain(self.active.tree.versions())
424            .collect::<Vec<_>>();
425        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
426        self.frozen = Arc::new(Vec::new());
427        self.active = MemtableSegment {
428            tree: BeTree::new(),
429            byte_size: 0,
430        };
431        self.byte_size = 0;
432        out
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    fn row(id: u64, epoch: u64) -> Row {
441        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
442    }
443
444    #[test]
445    fn upsert_get_and_visibility() {
446        let mut m = Memtable::new();
447        m.upsert(row(1, 5));
448        assert_eq!(m.len(), 1);
449        assert!(m.get(RowId(1), Epoch(5)).is_some());
450        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
451        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
452    }
453
454    #[test]
455    fn tombstone_supersedes_at_its_epoch() {
456        let mut m = Memtable::new();
457        m.upsert(row(1, 1));
458        // Before the tombstone: the live version is visible.
459        assert!(m.get(RowId(1), Epoch(1)).is_some());
460        m.tombstone(RowId(1), Epoch(2));
461        // At/after the tombstone: hidden.
462        assert!(m.get(RowId(1), Epoch(2)).is_none());
463        assert!(m.get(RowId(1), Epoch(9)).is_none());
464        // A snapshot before the tombstone still sees the live version.
465        assert!(m.get(RowId(1), Epoch(1)).is_some());
466    }
467
468    #[test]
469    fn sealed_generations_share_rows_and_consolidate() {
470        let mut writer = Memtable::new();
471        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
472            writer.upsert(row(id, id + 1));
473            writer.seal();
474        }
475        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
476        let generation = writer.clone();
477        writer.upsert(row(99, 99));
478        assert!(generation.get(RowId(99), Epoch(99)).is_none());
479        assert!(writer.get(RowId(99), Epoch(99)).is_some());
480    }
481
482    #[test]
483    fn hlc_visibility_is_authoritative_when_stamped() {
484        use mongreldb_types::hlc::HlcTimestamp;
485        let mut m = Memtable::new();
486        let early = HlcTimestamp {
487            physical_micros: 100,
488            logical: 0,
489            node_tiebreaker: 1,
490        };
491        let late = HlcTimestamp {
492            physical_micros: 200,
493            logical: 0,
494            node_tiebreaker: 1,
495        };
496        let mut r1 = Row::new_with_hlc(RowId(1), Epoch(1), early);
497        r1.columns.insert(1, Value::Int64(1));
498        let mut r2 = Row::new_with_hlc(RowId(1), Epoch(2), late);
499        r2.columns.insert(1, Value::Int64(2));
500        m.upsert(r1);
501        m.upsert(r2);
502        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
503        let versions = m.visible_versions_at(snap);
504        assert_eq!(versions.len(), 1);
505        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
506        let snap2 = crate::epoch::Snapshot::at_hlc(Epoch(1), late);
507        assert_eq!(
508            m.visible_versions_at(snap2)[0].columns.get(&1),
509            Some(&Value::Int64(2))
510        );
511    }
512
513    #[test]
514    fn snapshot_hlc_hides_later_commit_ts_even_if_epoch_higher() {
515        use mongreldb_types::hlc::HlcTimestamp;
516        let mut m = Memtable::new();
517        let early = HlcTimestamp {
518            physical_micros: 100,
519            logical: 0,
520            node_tiebreaker: 1,
521        };
522        let late = HlcTimestamp {
523            physical_micros: 200,
524            logical: 0,
525            node_tiebreaker: 1,
526        };
527        // Epoch(1) with late HLC would win under epoch-only rules when snap
528        // epoch is 99 — HLC authority must hide it under an early pin.
529        let mut late_row = Row::new_with_hlc(RowId(1), Epoch(1), late);
530        late_row.columns.insert(1, Value::Int64(99));
531        let mut early_row = Row::new_with_hlc(RowId(1), Epoch(50), early);
532        early_row.columns.insert(1, Value::Int64(1));
533        m.upsert(late_row);
534        m.upsert(early_row);
535        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
536        let versions = m.visible_versions_at(snap);
537        assert_eq!(versions.len(), 1);
538        assert_eq!(versions[0].columns.get(&1), Some(&Value::Int64(1)));
539        assert_eq!(versions[0].commit_ts, Some(early));
540    }
541
542    #[test]
543    fn epoch_only_snapshot_sees_hlc_stamped_rows_by_epoch() {
544        use mongreldb_types::hlc::HlcTimestamp;
545        let mut m = Memtable::new();
546        let ts = HlcTimestamp {
547            physical_micros: 50,
548            logical: 0,
549            node_tiebreaker: 1,
550        };
551        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), ts).with_column(1, Value::Int64(1)));
552        m.upsert(Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)));
553        let legacy = crate::epoch::Snapshot::at(Epoch(99));
554        let versions = m.visible_versions_at(legacy);
555        assert_eq!(
556            versions.len(),
557            2,
558            "dual-model: epoch pin sees HLC rows by epoch"
559        );
560        assert!(m.get_version_at(RowId(1), legacy).is_some());
561        assert!(m.get_version_at(RowId(2), legacy).is_some());
562        let future = crate::epoch::Snapshot::at(Epoch(0));
563        assert!(m.get_version_at(RowId(1), future).is_none());
564    }
565
566    #[test]
567    fn get_version_at_prefers_hlc_over_epoch_order() {
568        use mongreldb_types::hlc::HlcTimestamp;
569        let mut m = Memtable::new();
570        let early = HlcTimestamp {
571            physical_micros: 100,
572            logical: 0,
573            node_tiebreaker: 1,
574        };
575        let late = HlcTimestamp {
576            physical_micros: 200,
577            logical: 0,
578            node_tiebreaker: 1,
579        };
580        m.upsert(Row::new_with_hlc(RowId(1), Epoch(1), late).with_column(1, Value::Int64(99)));
581        m.upsert(Row::new_with_hlc(RowId(1), Epoch(50), early).with_column(1, Value::Int64(1)));
582        let snap = crate::epoch::Snapshot::at_hlc(Epoch(99), early);
583        let (_, row) = m.get_version_at(RowId(1), snap).expect("visible");
584        assert_eq!(row.columns.get(&1), Some(&Value::Int64(1)));
585        assert_eq!(row.commit_ts, Some(early));
586    }
587
588    /// WAL `Put` payloads must keep the 0.63.1 bincode layout
589    /// `(row_id, committed_epoch, columns, deleted)`. `commit_ts` is
590    /// in-memory only (`#[serde(skip)]`) so a 0.63.1-shaped blob still opens.
591    #[test]
592    fn wal_put_row_bincode_matches_0_63_1_layout() {
593        use mongreldb_types::hlc::HlcTimestamp;
594        use serde::{Deserialize, Serialize};
595
596        #[derive(Serialize, Deserialize)]
597        struct LegacyRow {
598            row_id: RowId,
599            committed_epoch: Epoch,
600            columns: HashMap<u16, Value>,
601            deleted: bool,
602        }
603
604        let legacy = LegacyRow {
605            row_id: RowId(7),
606            committed_epoch: Epoch(3),
607            columns: [(1, Value::Int64(42))].into_iter().collect(),
608            deleted: false,
609        };
610        let bytes = bincode::serialize(&legacy).expect("legacy encode");
611
612        let decoded: Row = bincode::deserialize(&bytes).expect("0.63.1 payload must decode");
613        assert_eq!(decoded.row_id, RowId(7));
614        assert_eq!(decoded.committed_epoch, Epoch(3));
615        assert_eq!(decoded.columns.get(&1), Some(&Value::Int64(42)));
616        assert!(!decoded.deleted);
617        assert!(decoded.commit_ts.is_none());
618
619        // Round-trip through Row: commit_ts is not on the wire.
620        let stamped = HlcTimestamp {
621            physical_micros: 1_700_000_000_000,
622            logical: 2,
623            node_tiebreaker: 9,
624        };
625        let mut live = Row::new_with_hlc(RowId(7), Epoch(3), stamped);
626        live.columns.insert(1, Value::Int64(42));
627        let wire = bincode::serialize(&live).expect("row encode");
628        assert_eq!(
629            wire, bytes,
630            "WAL Put encoding must match the 0.63.1 four-field layout"
631        );
632        let again: Row = bincode::deserialize(&wire).expect("row decode");
633        assert!(
634            again.commit_ts.is_none(),
635            "commit_ts is restored from Op::CommitTimestamp, not the Put blob"
636        );
637    }
638
639    #[test]
640    fn drain_sorted_is_ascending_and_empties() {
641        let mut m = Memtable::new();
642        m.upsert(row(3, 1));
643        m.upsert(row(1, 1));
644        m.upsert(row(2, 1));
645        let out = m.drain_sorted();
646        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
647        assert_eq!(ids, vec![1, 2, 3]);
648        assert!(m.is_empty());
649        assert_eq!(m.approx_bytes(), 0);
650    }
651
652    #[test]
653    fn visible_rows_dedups_to_newest_version() {
654        let mut m = Memtable::new();
655        m.upsert(row(1, 1));
656        m.upsert(row(2, 9)); // future relative to snapshot 5
657        m.upsert(row(3, 1));
658        m.upsert(row(1, 3)); // newer version of row 1
659        let ids: Vec<u64> = m
660            .visible_rows(Epoch(5))
661            .iter()
662            .map(|r| r.row_id.0)
663            .collect();
664        assert_eq!(ids, vec![1, 3]);
665    }
666}