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