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