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