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 so a
9//! snapshot read returns the newest version with `committed_epoch <= snapshot`,
10//! which is what makes MVCC correct within the live memtable.
11
12use crate::be_tree::BeTree;
13use crate::epoch::Epoch;
14use crate::rowid::RowId;
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, HashMap};
17
18/// A cell value in the in-memory path. The flush path re-encodes these into
19/// columnar pages; it is intentionally simple for the prototype.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum Value {
22    Null,
23    Bool(bool),
24    Int64(i64),
25    Float64(f64),
26    Bytes(Vec<u8>),
27    Embedding(Vec<f32>),
28}
29
30impl Value {
31    /// Lexicographically-comparable byte encoding for index keys (PK HOT,
32    /// bitmaps). Big-endian for integers so byte order matches value order.
33    pub fn encode_key(&self) -> Vec<u8> {
34        match self {
35            Value::Null => Vec::new(),
36            Value::Bool(b) => vec![*b as u8],
37            Value::Int64(n) => n.to_be_bytes().to_vec(),
38            Value::Float64(f) => f.to_bits().to_be_bytes().to_vec(),
39            Value::Bytes(b) => b.clone(),
40            Value::Embedding(v) => {
41                let mut out = Vec::with_capacity(v.len() * 4);
42                for x in v {
43                    out.extend_from_slice(&x.to_bits().to_be_bytes());
44                }
45                out
46            }
47        }
48    }
49}
50
51/// One logical row held in the memtable. A `deleted` row is a tombstone.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Row {
54    pub row_id: RowId,
55    pub committed_epoch: Epoch,
56    pub columns: HashMap<u16, Value>,
57    pub deleted: bool,
58}
59
60impl Row {
61    pub fn new(row_id: RowId, committed_epoch: Epoch) -> Self {
62        Self {
63            row_id,
64            committed_epoch,
65            columns: HashMap::new(),
66            deleted: false,
67        }
68    }
69
70    pub fn with_column(mut self, column_id: u16, value: Value) -> Self {
71        self.columns.insert(column_id, value);
72        self
73    }
74
75    /// Rough byte estimate for flush-threshold decisions.
76    pub fn estimated_bytes(&self) -> u64 {
77        let mut n = 32; // header overhead
78        for v in self.columns.values() {
79            n += match v {
80                Value::Null => 1,
81                Value::Bool(_) => 1,
82                Value::Int64(_) => 8,
83                Value::Float64(_) => 8,
84                Value::Bytes(b) => 16 + b.len() as u64,
85                Value::Embedding(v) => 16 + (v.len() as u64) * 4,
86            };
87        }
88        n
89    }
90}
91
92/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
93/// for the prototype skip list: the same MVCC semantics with lower write
94/// amplification (buffered messages flush to children in bulk).
95pub struct Memtable {
96    tree: BeTree,
97    byte_size: u64,
98}
99
100impl Default for Memtable {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl Memtable {
107    pub fn new() -> Self {
108        Self {
109            tree: BeTree::new(),
110            byte_size: 0,
111        }
112    }
113
114    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
115    /// never overwritten; the newest visible one wins at read time.
116    pub fn upsert(&mut self, row: Row) {
117        self.byte_size += row.estimated_bytes();
118        self.tree.insert_row(row);
119    }
120
121    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
122    /// the columns from the newest live version so that engine-level HOT cleanup
123    /// can recover the primary-key value during WAL replay.
124    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
125        let mut columns = HashMap::new();
126        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
127            columns = live.columns;
128        }
129        let row = Row {
130            row_id,
131            committed_epoch: epoch,
132            columns,
133            deleted: true,
134        };
135        self.upsert(row);
136    }
137
138    /// Read the row at `row_id` visible to `snapshot`: the newest version with
139    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
140    /// such version exists).
141    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
142        self.tree.get_visible(row_id, snapshot_epoch)
143    }
144
145    /// Newest version of `row_id` with `epoch <= snapshot`, **including
146    /// tombstones** (as a `Row` with `deleted=true`). Used by the engine to
147    /// merge versions across the memtable and sorted runs.
148    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
149        self.tree.get_version(row_id, snapshot_epoch)
150    }
151
152    /// Number of stored versions.
153    pub fn len(&self) -> usize {
154        self.tree.mutations()
155    }
156
157    pub fn is_empty(&self) -> bool {
158        self.tree.is_empty()
159    }
160
161    pub fn approx_bytes(&self) -> u64 {
162        self.byte_size
163    }
164
165    /// Visible rows at `snapshot`, deduplicated to the newest version per
166    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
167    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
168        self.visible_versions(snapshot_epoch)
169            .into_iter()
170            .filter(|r| !r.deleted)
171            .collect()
172    }
173
174    /// Newest visible version per `RowId` at `snapshot`, **including
175    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
176    /// versions across the memtable and sorted runs.
177    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
178        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
179        for row in self.tree.versions() {
180            if row.committed_epoch <= snapshot_epoch {
181                by_row
182                    .entry(row.row_id)
183                    .and_modify(|e| {
184                        if row.committed_epoch > e.committed_epoch {
185                            *e = row.clone();
186                        }
187                    })
188                    .or_insert(row);
189            }
190        }
191        by_row.into_values().collect()
192    }
193
194    /// Drain all versions (for a memtable-to-run flush). Returns them in
195    /// ascending `(RowId, Epoch)` order.
196    pub fn drain_sorted(&mut self) -> Vec<Row> {
197        let out = std::mem::take(&mut self.tree).into_sorted_rows();
198        self.byte_size = 0;
199        out
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn row(id: u64, epoch: u64) -> Row {
208        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
209    }
210
211    #[test]
212    fn upsert_get_and_visibility() {
213        let mut m = Memtable::new();
214        m.upsert(row(1, 5));
215        assert_eq!(m.len(), 1);
216        assert!(m.get(RowId(1), Epoch(5)).is_some());
217        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
218        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
219    }
220
221    #[test]
222    fn tombstone_supersedes_at_its_epoch() {
223        let mut m = Memtable::new();
224        m.upsert(row(1, 1));
225        // Before the tombstone: the live version is visible.
226        assert!(m.get(RowId(1), Epoch(1)).is_some());
227        m.tombstone(RowId(1), Epoch(2));
228        // At/after the tombstone: hidden.
229        assert!(m.get(RowId(1), Epoch(2)).is_none());
230        assert!(m.get(RowId(1), Epoch(9)).is_none());
231        // A snapshot before the tombstone still sees the live version.
232        assert!(m.get(RowId(1), Epoch(1)).is_some());
233    }
234
235    #[test]
236    fn drain_sorted_is_ascending_and_empties() {
237        let mut m = Memtable::new();
238        m.upsert(row(3, 1));
239        m.upsert(row(1, 1));
240        m.upsert(row(2, 1));
241        let out = m.drain_sorted();
242        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
243        assert_eq!(ids, vec![1, 2, 3]);
244        assert!(m.is_empty());
245        assert_eq!(m.approx_bytes(), 0);
246    }
247
248    #[test]
249    fn visible_rows_dedups_to_newest_version() {
250        let mut m = Memtable::new();
251        m.upsert(row(1, 1));
252        m.upsert(row(2, 9)); // future relative to snapshot 5
253        m.upsert(row(3, 1));
254        m.upsert(row(1, 3)); // newer version of row 1
255        let ids: Vec<u64> = m
256            .visible_rows(Epoch(5))
257            .iter()
258            .map(|r| r.row_id.0)
259            .collect();
260        assert_eq!(ids, vec![1, 3]);
261    }
262}