Skip to main content

mongreldb_core/
be_tree.rs

1//! Buffered Bε-tree (B-epsilon-tree) over composite `(RowId, Epoch)` keys —
2//! the Phase 1 memtable target.
3//!
4//! Keyed by `(RowId, Epoch)`, so every version of a logical row coexists (an
5//! update inserts a new key; the old version is untouched until compaction). A
6//! Bε-tree buffers many pending mutations per internal node; when a buffer
7//! fills, its messages flush to one child in bulk, giving write amplification
8//! approaching O(1). Reads consult every buffer along the root→leaf path and
9//! return the newest version with `epoch <= snapshot`.
10//!
11//! This is a drop-in MVCC alternative to the skip-list [`crate::Memtable`]; the
12//! engine ships the skip-list today because it is simpler, while this structure
13//! wins on update amplification at scale.
14
15use crate::epoch::Epoch;
16use crate::memtable::Row;
17use crate::rowid::RowId;
18use std::collections::HashMap;
19
20/// Max children per internal node (`B`).
21const FANOUT: usize = 8;
22/// Messages buffered per internal node before flushing down to children.
23const BUFFER_CAP: usize = 16;
24/// Max rows per leaf before it splits.
25const LEAF_CAP: usize = 32;
26
27/// Composite version key: `(row_id, epoch)`.
28type VKey = (RowId, Epoch);
29
30/// A pending mutation pending application to the leaves below a node.
31#[derive(Debug, Clone)]
32enum Message {
33    Upsert(Row),
34    Tombstone { row_id: RowId, epoch: Epoch },
35}
36
37impl Message {
38    fn key(&self) -> VKey {
39        match self {
40            Message::Upsert(r) => (r.row_id, r.committed_epoch),
41            Message::Tombstone { row_id, epoch } => (*row_id, *epoch),
42        }
43    }
44
45    fn to_row(&self) -> (Epoch, Row) {
46        match self {
47            Message::Upsert(r) => (r.committed_epoch, r.clone()),
48            Message::Tombstone { row_id, epoch } => (
49                *epoch,
50                Row {
51                    row_id: *row_id,
52                    committed_epoch: *epoch,
53                    columns: HashMap::new(),
54                    deleted: true,
55                },
56            ),
57        }
58    }
59}
60
61#[derive(Clone)]
62enum Node {
63    Leaf {
64        rows: Vec<Row>,
65    },
66    Internal {
67        keys: Vec<VKey>,
68        children: Vec<Node>,
69        buffer: Vec<Message>,
70    },
71}
72
73impl Node {
74    fn empty_leaf() -> Self {
75        Node::Leaf { rows: Vec::new() }
76    }
77}
78
79struct Split {
80    key: VKey,
81    node: Node,
82}
83
84/// Buffered Bε-tree over `(RowId, Epoch)` → [`Row`].
85#[derive(Clone)]
86pub struct BeTree {
87    root: Node,
88    mutations: usize,
89}
90
91impl Default for BeTree {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl BeTree {
98    pub fn new() -> Self {
99        Self {
100            root: Node::empty_leaf(),
101            mutations: 0,
102        }
103    }
104
105    /// Number of mutations buffered.
106    pub fn mutations(&self) -> usize {
107        self.mutations
108    }
109
110    pub fn is_empty(&self) -> bool {
111        self.mutations == 0
112    }
113
114    /// Insert a row version (keyed by its own `(row_id, committed_epoch)`).
115    pub fn insert_row(&mut self, row: Row) {
116        self.insert(Message::Upsert(row));
117    }
118
119    /// Insert a tombstone at `(row_id, epoch)`.
120    pub fn delete(&mut self, row_id: RowId, epoch: Epoch) {
121        self.insert(Message::Tombstone { row_id, epoch });
122    }
123
124    fn insert(&mut self, msg: Message) {
125        self.mutations += 1;
126        match &mut self.root {
127            Node::Leaf { rows } => Self::leaf_apply(rows, msg),
128            Node::Internal { buffer, .. } => buffer.push(msg),
129        }
130        if let Some(split) = Self::maintain(&mut self.root) {
131            let left = std::mem::replace(&mut self.root, Node::empty_leaf());
132            self.root = Node::Internal {
133                keys: vec![split.key],
134                children: vec![left, split.node],
135                buffer: Vec::new(),
136            };
137        }
138    }
139
140    /// Newest version of `row_id` with `epoch <= snapshot`, including tombstones
141    /// (returned as a `Row` with `deleted=true`). `None` if no such version.
142    pub fn get(&self, row_id: RowId, snapshot: Epoch) -> Option<Row> {
143        self.get_version(row_id, snapshot).map(|(_, r)| r)
144    }
145
146    /// Same as [`Self::get`] but also returns the version's epoch — the shape
147    /// the engine's MVCC merge needs to pick the newest version across the
148    /// memtable, the mutable-run tier, and sorted runs.
149    pub fn get_version(&self, row_id: RowId, snapshot: Epoch) -> Option<(Epoch, Row)> {
150        let mut best: Option<(Epoch, Row)> = None;
151        Self::collect(&self.root, row_id, snapshot, &mut best);
152        best
153    }
154
155    /// Visible (non-deleted) row at `row_id` for `snapshot`.
156    pub fn get_visible(&self, row_id: RowId, snapshot: Epoch) -> Option<Row> {
157        let r = self.get(row_id, snapshot)?;
158        if r.deleted {
159            None
160        } else {
161            Some(r)
162        }
163    }
164
165    /// Every buffered version (non-consuming), in no defined order — leaves
166    /// plus every internal-node buffer. Used by the memtable adapter to dedup
167    /// the newest visible version per `RowId` for a full visible-rows scan.
168    pub fn versions(&self) -> Vec<Row> {
169        let mut out = Vec::with_capacity(self.mutations);
170        Self::collect_all_versions(&self.root, &mut out);
171        out
172    }
173
174    /// Consume the tree, flushing all buffers to leaves, returning every version
175    /// in ascending `(RowId, Epoch)` order.
176    pub fn into_sorted_rows(mut self) -> Vec<Row> {
177        Self::flush_all(&mut self.root);
178        Self::collect_leaves(&self.root)
179    }
180
181    // ---- internals -----------------------------------------------------
182
183    fn maintain(node: &mut Node) -> Option<Split> {
184        match node {
185            Node::Leaf { rows } => {
186                if rows.len() > LEAF_CAP {
187                    Some(Self::split_leaf(rows))
188                } else {
189                    None
190                }
191            }
192            Node::Internal {
193                keys,
194                children,
195                buffer,
196            } => {
197                if buffer.len() > BUFFER_CAP {
198                    let drained = std::mem::take(buffer);
199                    for msg in drained {
200                        let i = Self::child_index(keys, msg.key());
201                        Self::push_into_child(&mut children[i], msg);
202                    }
203                    let mut i = 0;
204                    while i < children.len() {
205                        if let Some(split) = Self::maintain(&mut children[i]) {
206                            keys.insert(i, split.key);
207                            children.insert(i + 1, split.node);
208                            i += 1;
209                        }
210                        i += 1;
211                    }
212                }
213                if children.len() > FANOUT {
214                    Some(Self::split_internal(keys, children))
215                } else {
216                    None
217                }
218            }
219        }
220    }
221
222    fn leaf_apply(rows: &mut Vec<Row>, msg: Message) {
223        // Composite keys are unique ⇒ always an insert (never an overwrite).
224        let key = msg.key();
225        let row = match msg {
226            Message::Upsert(r) => r,
227            Message::Tombstone { row_id, epoch } => Row {
228                row_id,
229                committed_epoch: epoch,
230                columns: HashMap::new(),
231                deleted: true,
232            },
233        };
234        let i = rows.partition_point(|r| (r.row_id, r.committed_epoch) < key);
235        rows.insert(i, row);
236    }
237
238    fn push_into_child(child: &mut Node, msg: Message) {
239        match child {
240            Node::Leaf { rows } => Self::leaf_apply(rows, msg),
241            Node::Internal { buffer, .. } => buffer.push(msg),
242        }
243    }
244
245    fn child_index(keys: &[VKey], key: VKey) -> usize {
246        keys.partition_point(|k| *k <= key)
247    }
248
249    fn split_leaf(rows: &mut Vec<Row>) -> Split {
250        let mid = rows.len() / 2;
251        let right = rows.split_off(mid);
252        let key = (right[0].row_id, right[0].committed_epoch);
253        Split {
254            key,
255            node: Node::Leaf { rows: right },
256        }
257    }
258
259    fn split_internal(keys: &mut Vec<VKey>, children: &mut Vec<Node>) -> Split {
260        let m = keys.len() / 2;
261        let promoted = keys[m];
262        let right_keys = keys.split_off(m + 1);
263        keys.pop();
264        let right_children = children.split_off(m + 1);
265        Split {
266            key: promoted,
267            node: Node::Internal {
268                keys: right_keys,
269                children: right_children,
270                buffer: Vec::new(),
271            },
272        }
273    }
274
275    fn consider(best: &mut Option<(Epoch, Row)>, epoch: Epoch, row: Row) {
276        match best {
277            Some((be, _)) if *be >= epoch => {}
278            _ => *best = Some((epoch, row)),
279        }
280    }
281
282    fn collect(node: &Node, row_id: RowId, snapshot: Epoch, best: &mut Option<(Epoch, Row)>) {
283        match node {
284            Node::Leaf { rows } => {
285                // Versions of `row_id` are contiguous; scan the slice whose
286                // (row_id, epoch) <= (row_id, snapshot) and row_id matches.
287                let upper =
288                    rows.partition_point(|r| (r.row_id, r.committed_epoch) <= (row_id, snapshot));
289                let mut i = upper;
290                while i > 0 {
291                    let i2 = i - 1;
292                    if rows[i2].row_id != row_id {
293                        break;
294                    }
295                    let r = &rows[i2];
296                    if r.committed_epoch <= snapshot {
297                        Self::consider(best, r.committed_epoch, r.clone());
298                    }
299                    i = i2;
300                }
301            }
302            Node::Internal {
303                keys,
304                children,
305                buffer,
306            } => {
307                for msg in buffer.iter() {
308                    let (rid, e) = msg.key();
309                    if rid == row_id && e <= snapshot {
310                        let (epoch, row) = msg.to_row();
311                        Self::consider(best, epoch, row);
312                    }
313                }
314                let i = Self::child_index(keys, (row_id, snapshot));
315                Self::collect(&children[i], row_id, snapshot, best);
316            }
317        }
318    }
319
320    fn flush_all(node: &mut Node) {
321        match node {
322            Node::Leaf { .. } => {}
323            Node::Internal {
324                keys,
325                children,
326                buffer,
327            } => {
328                let drained = std::mem::take(buffer);
329                for msg in drained {
330                    let i = Self::child_index(keys, msg.key());
331                    Self::push_into_child(&mut children[i], msg);
332                }
333                for c in children.iter_mut() {
334                    Self::flush_all(c);
335                }
336            }
337        }
338    }
339
340    fn collect_leaves(node: &Node) -> Vec<Row> {
341        match node {
342            Node::Leaf { rows } => rows.clone(),
343            Node::Internal { children, .. } => {
344                children.iter().flat_map(Self::collect_leaves).collect()
345            }
346        }
347    }
348
349    fn collect_all_versions(node: &Node, out: &mut Vec<Row>) {
350        match node {
351            Node::Leaf { rows } => out.extend(rows.iter().cloned()),
352            Node::Internal {
353                children, buffer, ..
354            } => {
355                for msg in buffer {
356                    out.push(msg.to_row().1);
357                }
358                for c in children {
359                    Self::collect_all_versions(c, out);
360                }
361            }
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use crate::memtable::Value;
370
371    fn val_row(id: u64, epoch: u64, v: i64) -> Row {
372        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(v))
373    }
374
375    #[test]
376    fn point_lookups_round_trip() {
377        let mut t = BeTree::new();
378        for i in 0..50u64 {
379            t.insert_row(val_row(i, i, i as i64 * 10));
380        }
381        for i in 0..50u64 {
382            let r = t.get_visible(RowId(i), Epoch(100)).expect("row present");
383            assert_eq!(r.row_id, RowId(i));
384            assert!(matches!(r.columns.get(&1), Some(Value::Int64(v)) if *v == i as i64 * 10));
385        }
386        assert!(t.get_visible(RowId(500), Epoch(100)).is_none());
387    }
388
389    #[test]
390    fn many_inserts_force_depth_growth() {
391        let mut t = BeTree::new();
392        let n = 5_000u64;
393        for i in 0..n {
394            t.insert_row(val_row(i, i, i as i64));
395        }
396        for i in 0..n {
397            assert!(
398                t.get_visible(RowId(i), Epoch(n + 1)).is_some(),
399                "missing {i}"
400            );
401        }
402        assert_eq!(t.into_sorted_rows().len(), n as usize);
403    }
404
405    #[test]
406    fn multiple_versions_of_same_row_coexist_with_mvcc() {
407        // The whole point of the composite key: an update keeps the old version.
408        let mut t = BeTree::new();
409        t.insert_row(val_row(7, 1, 100));
410        t.insert_row(val_row(7, 5, 200));
411        // Old snapshot sees the old value; new snapshot sees the new value.
412        let old = t.get_visible(RowId(7), Epoch(2)).unwrap();
413        assert!(matches!(old.columns.get(&1), Some(Value::Int64(v)) if *v == 100));
414        let new = t.get_visible(RowId(7), Epoch(10)).unwrap();
415        assert!(matches!(new.columns.get(&1), Some(Value::Int64(v)) if *v == 200));
416    }
417
418    #[test]
419    fn tombstone_hides_row_at_and_after_epoch_but_not_before() {
420        let mut t = BeTree::new();
421        t.insert_row(val_row(3, 1, 42));
422        assert!(t.get_visible(RowId(3), Epoch(1)).is_some());
423        t.delete(RowId(3), Epoch(4));
424        assert!(t.get_visible(RowId(3), Epoch(4)).is_none());
425        assert!(t.get_visible(RowId(3), Epoch(9)).is_none());
426        // Still visible to a snapshot before the tombstone.
427        assert!(t.get_visible(RowId(3), Epoch(3)).is_some());
428    }
429
430    #[test]
431    fn into_sorted_rows_is_keyed_by_row_then_epoch() {
432        let mut t = BeTree::new();
433        t.insert_row(val_row(30, 1, 1));
434        t.insert_row(val_row(10, 1, 1));
435        t.insert_row(val_row(30, 5, 2)); // newer version of row 30
436        t.delete(RowId(10), Epoch(2));
437        let rows = t.into_sorted_rows();
438        let keys: Vec<(u64, u64)> = rows
439            .iter()
440            .map(|r| (r.row_id.0, r.committed_epoch.0))
441            .collect();
442        assert_eq!(keys, vec![(10, 1), (10, 2), (30, 1), (30, 5)]);
443        assert!(
444            rows.iter()
445                .find(|r| r.row_id == RowId(10) && r.committed_epoch == Epoch(2))
446                .unwrap()
447                .deleted
448        );
449    }
450
451    /// Regression for the Phase 11 review's CRITICAL claim: when one row
452    /// accumulates enough versions to span multiple leaf splits (and internal
453    /// buffering), a `(row_id, snapshot)` point lookup must still return the
454    /// newest visible version. This forces many splits and exercises the descent
455    /// across child boundaries for a single high-churn row.
456    #[test]
457    fn many_versions_of_one_row_stay_lookupable_across_splits() {
458        let mut t = BeTree::new();
459        const N: u64 = 600;
460        // Interleave other rows so the composite-key space has many separators;
461        // the high-churn row is 7777, with a version at every epoch 0..N.
462        for e in 0..N {
463            t.insert_row(val_row(7777, e, e as i64 * 2));
464            // A few distinct sibling rows to vary the key space and force
465            // splits at separator keys that are NOT row 7777.
466            t.insert_row(val_row(e, e, 0));
467        }
468        assert_eq!(t.mutations(), 2 * N as usize);
469        // Every snapshot epoch must see exactly epoch `s` as the newest version
470        // of row 7777 (versions are dense 0..N).
471        for s in 0..N {
472            let r = t
473                .get_version(RowId(7777), Epoch(s))
474                .expect("missing version")
475                .0;
476            assert_eq!(r, Epoch(s), "snapshot {s} saw wrong newest version");
477        }
478        // Before the first version: nothing.
479        assert!(t.get_version(RowId(7777), Epoch(0)).is_some()); // epoch 0 exists
480                                                                 // The sibling rows are all visible at their own epoch.
481        for e in 0..N {
482            assert!(t.get_visible(RowId(e), Epoch(e)).is_some(), "sibling {e}");
483        }
484
485        // Tombstones mixed in across splits: delete row 7777 at a late epoch,
486        // then confirm snapshots before/after the tombstone see the right thing.
487        t.delete(RowId(7777), Epoch(N + 5));
488        assert!(
489            t.get_visible(RowId(7777), Epoch(N)).is_some(),
490            "before tombstone"
491        );
492        assert!(
493            t.get_visible(RowId(7777), Epoch(N + 5)).is_none(),
494            "at tombstone"
495        );
496        assert!(
497            t.get_visible(RowId(7777), Epoch(N + 99)).is_none(),
498            "after tombstone"
499        );
500    }
501}