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};
17use std::sync::Arc;
18
19/// A cell value in the in-memory path. The flush path re-encodes these into
20/// columnar pages; it is intentionally simple for the prototype.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum Value {
23    Null,
24    Bool(bool),
25    Int64(i64),
26    Float64(f64),
27    Bytes(Vec<u8>),
28    Embedding(Vec<f32>),
29    /// Unscaled decimal value (i128). The column's `TypeId::Decimal128`
30    /// carries the precision/scale for formatting.
31    Decimal(i128),
32    /// SQL INTERVAL value: months, days, nanoseconds.
33    Interval {
34        months: i64,
35        days: i32,
36        nanos: i64,
37    },
38    /// RFC 4122 UUID (16 bytes, big-endian for sort order).
39    Uuid([u8; 16]),
40    /// JSON value stored as a UTF-8 byte sequence.
41    Json(Vec<u8>),
42}
43
44impl Value {
45    /// Lexicographically-comparable byte encoding for index keys (PK HOT,
46    /// bitmaps). Big-endian for integers so byte order matches value order.
47    pub fn encode_key(&self) -> Vec<u8> {
48        match self {
49            Value::Null => Vec::new(),
50            Value::Bool(b) => vec![*b as u8],
51            Value::Int64(n) => n.to_be_bytes().to_vec(),
52            Value::Float64(f) => f.to_bits().to_be_bytes().to_vec(),
53            Value::Bytes(b) => b.clone(),
54            Value::Embedding(v) => {
55                let mut out = Vec::with_capacity(v.len() * 4);
56                for x in v {
57                    out.extend_from_slice(&x.to_bits().to_be_bytes());
58                }
59                out
60            }
61            Value::Decimal(d) => d.to_be_bytes().to_vec(),
62            Value::Interval {
63                months,
64                days,
65                nanos,
66            } => {
67                let mut out = Vec::with_capacity(20);
68                out.extend_from_slice(&months.to_be_bytes());
69                out.extend_from_slice(&days.to_be_bytes());
70                out.extend_from_slice(&nanos.to_be_bytes());
71                out
72            }
73            Value::Uuid(b) => b.to_vec(),
74            Value::Json(b) => b.clone(),
75        }
76    }
77}
78
79/// One logical row held in the memtable. A `deleted` row is a tombstone.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Row {
82    pub row_id: RowId,
83    pub committed_epoch: Epoch,
84    pub columns: HashMap<u16, Value>,
85    pub deleted: bool,
86}
87
88impl Row {
89    pub fn new(row_id: RowId, committed_epoch: Epoch) -> Self {
90        Self {
91            row_id,
92            committed_epoch,
93            columns: HashMap::new(),
94            deleted: false,
95        }
96    }
97
98    pub fn with_column(mut self, column_id: u16, value: Value) -> Self {
99        self.columns.insert(column_id, value);
100        self
101    }
102
103    /// Rough byte estimate for flush-threshold decisions.
104    pub fn estimated_bytes(&self) -> u64 {
105        let mut n = 32; // header overhead
106        for v in self.columns.values() {
107            n += match v {
108                Value::Null => 1,
109                Value::Bool(_) => 1,
110                Value::Int64(_) => 8,
111                Value::Float64(_) => 8,
112                Value::Bytes(b) => 16 + b.len() as u64,
113                Value::Embedding(v) => 16 + (v.len() as u64) * 4,
114                Value::Decimal(_) => 16,
115                Value::Interval { .. } => 20,
116                Value::Uuid(_) => 16,
117                Value::Json(b) => 16 + b.len() as u64,
118            };
119        }
120        n
121    }
122}
123
124/// Bε-tree-backed memtable, ordered by `(RowId, Epoch)`. A drop-in replacement
125/// for the prototype skip list: the same MVCC semantics with lower write
126/// amplification (buffered messages flush to children in bulk).
127#[derive(Clone)]
128struct MemtableSegment {
129    tree: BeTree,
130    byte_size: u64,
131}
132
133/// Structurally shared committed overlays plus one small mutable write delta.
134#[derive(Clone)]
135pub struct Memtable {
136    frozen: Arc<Vec<Arc<MemtableSegment>>>,
137    active: MemtableSegment,
138    byte_size: u64,
139}
140
141impl Default for Memtable {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl Memtable {
148    pub fn new() -> Self {
149        Self {
150            frozen: Arc::new(Vec::new()),
151            active: MemtableSegment {
152                tree: BeTree::new(),
153                byte_size: 0,
154            },
155            byte_size: 0,
156        }
157    }
158
159    /// Append a row version (keyed by `(row_id, committed_epoch)`). Versions are
160    /// never overwritten; the newest visible one wins at read time.
161    pub fn upsert(&mut self, row: Row) {
162        let bytes = row.estimated_bytes();
163        self.byte_size = self.byte_size.saturating_add(bytes);
164        self.active.byte_size = self.active.byte_size.saturating_add(bytes);
165        self.active.tree.insert_row(row);
166    }
167
168    /// Append a tombstone version for `row_id` at `epoch`. The tombstone copies
169    /// the columns from the newest live version so that engine-level HOT cleanup
170    /// can recover the primary-key value during WAL replay.
171    pub fn tombstone(&mut self, row_id: RowId, epoch: Epoch) {
172        let mut columns = HashMap::new();
173        if let Some(live) = self.get(row_id, Epoch(epoch.0.saturating_sub(1))) {
174            columns = live.columns;
175        }
176        let row = Row {
177            row_id,
178            committed_epoch: epoch,
179            columns,
180            deleted: true,
181        };
182        self.upsert(row);
183    }
184
185    /// Read the row at `row_id` visible to `snapshot`: the newest version with
186    /// `epoch <= snapshot`. Returns `None` if that version is a tombstone (or no
187    /// such version exists).
188    pub fn get(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<Row> {
189        self.get_version(row_id, snapshot_epoch)
190            .and_then(|(_, row)| (!row.deleted).then_some(row))
191    }
192
193    /// Newest version of `row_id` with `epoch <= snapshot`, **including
194    /// tombstones** (as a `Row` with `deleted=true`). Used by the engine to
195    /// merge versions across the memtable and sorted runs.
196    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
197        let mut best = self.active.tree.get_version(row_id, snapshot_epoch);
198        for segment in self.frozen.iter().rev() {
199            let Some(candidate) = segment.tree.get_version(row_id, snapshot_epoch) else {
200                continue;
201            };
202            if best
203                .as_ref()
204                .map_or(true, |(epoch, _)| candidate.0 > *epoch)
205            {
206                best = Some(candidate);
207            }
208        }
209        best
210    }
211
212    /// Number of stored versions.
213    pub fn len(&self) -> usize {
214        self.active.tree.mutations()
215            + self
216                .frozen
217                .iter()
218                .map(|segment| segment.tree.mutations())
219                .sum::<usize>()
220    }
221
222    pub fn is_empty(&self) -> bool {
223        self.active.tree.is_empty() && self.frozen.is_empty()
224    }
225
226    pub fn approx_bytes(&self) -> u64 {
227        self.byte_size
228    }
229
230    /// Visible rows at `snapshot`, deduplicated to the newest version per
231    /// `RowId` (tombstones drop their row). Returned in ascending `RowId` order.
232    pub fn visible_rows(&self, snapshot_epoch: Epoch) -> Vec<Row> {
233        self.visible_versions(snapshot_epoch)
234            .into_iter()
235            .filter(|r| !r.deleted)
236            .collect()
237    }
238
239    /// Newest visible version per `RowId` at `snapshot`, **including
240    /// tombstones** (as `Row`s with `deleted=true`). Used by the engine to merge
241    /// versions across the memtable and sorted runs.
242    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
243        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
244        for segment in self
245            .frozen
246            .iter()
247            .map(|segment| &segment.tree)
248            .chain(std::iter::once(&self.active.tree))
249        {
250            for row in segment.versions() {
251                if row.committed_epoch <= snapshot_epoch {
252                    by_row
253                        .entry(row.row_id)
254                        .and_modify(|existing| {
255                            if row.committed_epoch > existing.committed_epoch {
256                                *existing = row.clone();
257                            }
258                        })
259                        .or_insert(row);
260                }
261            }
262        }
263        by_row.into_values().collect()
264    }
265
266    /// Freeze the current write delta so future clones share it by `Arc`.
267    pub(crate) fn seal(&mut self) {
268        if self.active.tree.is_empty() {
269            return;
270        }
271        let active = std::mem::replace(
272            &mut self.active,
273            MemtableSegment {
274                tree: BeTree::new(),
275                byte_size: 0,
276            },
277        );
278        Arc::make_mut(&mut self.frozen).push(Arc::new(active));
279        if self.frozen.len() >= crate::MAX_READ_GENERATION_LAYERS {
280            self.consolidate();
281        }
282    }
283
284    fn consolidate(&mut self) {
285        let mut tree = BeTree::new();
286        for row in self
287            .frozen
288            .iter()
289            .flat_map(|segment| segment.tree.versions())
290        {
291            tree.insert_row(row);
292        }
293        self.frozen = Arc::new(vec![Arc::new(MemtableSegment {
294            tree,
295            byte_size: self.byte_size,
296        })]);
297    }
298
299    #[cfg(test)]
300    pub(crate) fn frozen_layer_count(&self) -> usize {
301        self.frozen.len()
302    }
303
304    /// Drain all versions (for a memtable-to-run flush). Returns them in
305    /// ascending `(RowId, Epoch)` order.
306    pub fn drain_sorted(&mut self) -> Vec<Row> {
307        let mut out = self
308            .frozen
309            .iter()
310            .flat_map(|segment| segment.tree.versions())
311            .chain(self.active.tree.versions())
312            .collect::<Vec<_>>();
313        out.sort_by_key(|row| (row.row_id, row.committed_epoch));
314        self.frozen = Arc::new(Vec::new());
315        self.active = MemtableSegment {
316            tree: BeTree::new(),
317            byte_size: 0,
318        };
319        self.byte_size = 0;
320        out
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    fn row(id: u64, epoch: u64) -> Row {
329        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(id as i64 * 10))
330    }
331
332    #[test]
333    fn upsert_get_and_visibility() {
334        let mut m = Memtable::new();
335        m.upsert(row(1, 5));
336        assert_eq!(m.len(), 1);
337        assert!(m.get(RowId(1), Epoch(5)).is_some());
338        assert!(m.get(RowId(1), Epoch(4)).is_none()); // not yet visible
339        assert!(m.get(RowId(2), Epoch(9)).is_none()); // missing
340    }
341
342    #[test]
343    fn tombstone_supersedes_at_its_epoch() {
344        let mut m = Memtable::new();
345        m.upsert(row(1, 1));
346        // Before the tombstone: the live version is visible.
347        assert!(m.get(RowId(1), Epoch(1)).is_some());
348        m.tombstone(RowId(1), Epoch(2));
349        // At/after the tombstone: hidden.
350        assert!(m.get(RowId(1), Epoch(2)).is_none());
351        assert!(m.get(RowId(1), Epoch(9)).is_none());
352        // A snapshot before the tombstone still sees the live version.
353        assert!(m.get(RowId(1), Epoch(1)).is_some());
354    }
355
356    #[test]
357    fn sealed_generations_share_rows_and_consolidate() {
358        let mut writer = Memtable::new();
359        for id in 0..crate::MAX_READ_GENERATION_LAYERS as u64 + 2 {
360            writer.upsert(row(id, id + 1));
361            writer.seal();
362        }
363        assert!(writer.frozen_layer_count() < crate::MAX_READ_GENERATION_LAYERS);
364        let generation = writer.clone();
365        writer.upsert(row(99, 99));
366        assert!(generation.get(RowId(99), Epoch(99)).is_none());
367        assert!(writer.get(RowId(99), Epoch(99)).is_some());
368    }
369
370    #[test]
371    fn drain_sorted_is_ascending_and_empties() {
372        let mut m = Memtable::new();
373        m.upsert(row(3, 1));
374        m.upsert(row(1, 1));
375        m.upsert(row(2, 1));
376        let out = m.drain_sorted();
377        let ids: Vec<u64> = out.iter().map(|r| r.row_id.0).collect();
378        assert_eq!(ids, vec![1, 2, 3]);
379        assert!(m.is_empty());
380        assert_eq!(m.approx_bytes(), 0);
381    }
382
383    #[test]
384    fn visible_rows_dedups_to_newest_version() {
385        let mut m = Memtable::new();
386        m.upsert(row(1, 1));
387        m.upsert(row(2, 9)); // future relative to snapshot 5
388        m.upsert(row(3, 1));
389        m.upsert(row(1, 3)); // newer version of row 1
390        let ids: Vec<u64> = m
391            .visible_rows(Epoch(5))
392            .iter()
393            .map(|r| r.row_id.0)
394            .collect();
395        assert_eq!(ids, vec![1, 3]);
396    }
397}