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