Skip to main content

mongreldb_core/
mutable_run.rs

1//! Mutable run tier — the LSM layer between the skip-list memtable and the
2//! immutable `.sr` sorted runs (Phase 11.1).
3//!
4//! A flush drains the live memtable into this in-memory tier instead of
5//! immediately writing a new sorted run. The tier is a [`crate::pma::Pma`] keyed
6//! by the composite `(RowId, Epoch)` version key, so it stays sorted (the
7//! natural order `RunWriter` consumes) and absorbs further flushes in place with
8//! amortized `O(log² n)` inserts — exactly the "cache-oblivious mutable sorted
9//! run" described in §2. Only once the tier crosses a byte watermark does it
10//! spill to an immutable sorted run on disk, coalescing many small flushes into
11//! one larger run (fewer runs ⇒ fewer reader merges ⇒ faster scans).
12//!
13//! MVCC semantics mirror [`crate::memtable::Memtable`]: every version is kept,
14//! keyed by `(RowId, Epoch)`; a snapshot read returns the newest version with
15//! `epoch <= snapshot`. The tier is purely in-memory and rebuilds from WAL
16//! replay on reopen, so it carries no on-disk state of its own.
17
18use crate::epoch::Epoch;
19use crate::memtable::Row;
20use crate::pma::Pma;
21use crate::rowid::RowId;
22use std::collections::BTreeMap;
23
24/// Composite version key — identical to the memtable's, so all versions of one
25/// `RowId` sort contiguously in ascending-epoch order.
26type VersionKey = (RowId, Epoch);
27
28/// The PMA-backed mutable run tier. Holds flushed-but-not-yet-spilled rows in
29/// sorted `(RowId, Epoch)` order.
30pub struct MutableRun {
31    pma: Pma<VersionKey, Row>,
32    byte_size: u64,
33}
34
35impl Default for MutableRun {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl MutableRun {
42    pub fn new() -> Self {
43        Self {
44            pma: Pma::new(),
45            byte_size: 0,
46        }
47    }
48
49    /// Fold drained memtable rows (already ascending by `(RowId, Epoch)`) into
50    /// the tier via one bulk merge + re-spread — far cheaper than per-element
51    /// inserts, which would cluster at the tail on sorted input.
52    pub fn insert_many(&mut self, rows: Vec<Row>) {
53        let batch: Vec<(VersionKey, Row)> = rows
54            .into_iter()
55            .map(|r| {
56                self.byte_size = self.byte_size.saturating_add(r.estimated_bytes());
57                ((r.row_id, r.committed_epoch), r)
58            })
59            .collect();
60        self.pma.extend_sorted(batch);
61    }
62
63    /// Number of stored versions.
64    pub fn len(&self) -> usize {
65        self.pma.len()
66    }
67
68    pub fn is_empty(&self) -> bool {
69        self.pma.is_empty()
70    }
71
72    /// Approximate bytes held — the spill-threshold signal.
73    pub fn approx_bytes(&self) -> u64 {
74        self.byte_size
75    }
76
77    /// Newest version of `row_id` with `epoch <= snapshot` (including
78    /// tombstones), mirroring `Memtable::get_version`. Returns `None` if no
79    /// version is visible. The PMA iterates ascending, so the last matching
80    /// entry for the row wins.
81    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
82        let mut best: Option<(Epoch, Row)> = None;
83        for ((rid, ep), row) in self.pma.iter() {
84            match rid.cmp(&row_id) {
85                std::cmp::Ordering::Less => continue,
86                std::cmp::Ordering::Greater => break,
87                std::cmp::Ordering::Equal => {
88                    if *ep <= snapshot_epoch {
89                        best = Some((*ep, row.clone()));
90                    }
91                }
92            }
93        }
94        best
95    }
96
97    /// Newest visible version per `RowId` at `snapshot` (including tombstones),
98    /// ascending by `RowId` — mirroring `Memtable::visible_versions`.
99    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
100        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
101        for ((rid, ep), row) in self.pma.iter() {
102            if *ep <= snapshot_epoch {
103                by_row.insert(*rid, row.clone()); // ascending ⇒ newest wins
104            }
105        }
106        by_row.into_values().collect()
107    }
108
109    /// Drain every version in ascending `(RowId, Epoch)` order — the order
110    /// `RunWriter::write` requires when spilling to an immutable run.
111    pub fn drain_sorted(&mut self) -> Vec<Row> {
112        let out: Vec<Row> = self
113            .pma
114            .drain_sorted()
115            .into_iter()
116            .map(|(_, r)| r)
117            .collect();
118        self.byte_size = 0;
119        out
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::memtable::Value;
127
128    fn row(id: u64, epoch: u64, v: i64) -> Row {
129        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(v))
130    }
131
132    fn tomb(id: u64, epoch: u64) -> Row {
133        Row {
134            row_id: RowId(id),
135            committed_epoch: Epoch(epoch),
136            columns: std::collections::HashMap::new(),
137            deleted: true,
138        }
139    }
140
141    fn int_of(r: &Row) -> i64 {
142        match r.columns.get(&1) {
143            Some(Value::Int64(x)) => *x,
144            _ => panic!("expected Int64 column"),
145        }
146    }
147
148    #[test]
149    fn get_version_returns_newest_visible() {
150        let mut mr = MutableRun::new();
151        mr.insert_many(vec![row(1, 1, 10), row(1, 3, 30), row(1, 9, 90)]);
152        // Snapshot before the 9 version sees the 3 version.
153        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(5)).unwrap().1), 30);
154        // Latest snapshot sees the newest.
155        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(9)).unwrap().1), 90);
156        // No version at/before epoch 0.
157        assert!(mr.get_version(RowId(1), Epoch(0)).is_none());
158        // Missing row.
159        assert!(mr.get_version(RowId(2), Epoch(100)).is_none());
160    }
161
162    #[test]
163    fn tombstone_is_returned_as_a_version() {
164        let mut mr = MutableRun::new();
165        mr.insert_many(vec![row(1, 1, 10), tomb(1, 2)]);
166        let v = mr.get_version(RowId(1), Epoch(5)).unwrap().1;
167        assert!(v.deleted);
168        // Before the tombstone the live version is visible.
169        let v0 = mr.get_version(RowId(1), Epoch(1)).unwrap().1;
170        assert!(!v0.deleted);
171    }
172
173    #[test]
174    fn visible_versions_dedups_to_newest_ascending() {
175        let mut mr = MutableRun::new();
176        mr.insert_many(vec![
177            row(3, 1, 30),
178            row(1, 1, 10),
179            row(2, 9, 20), // future relative to snapshot 5
180            row(1, 3, 11), // newer version of row 1
181            row(3, 2, 31),
182        ]);
183        let out = mr.visible_versions(Epoch(5));
184        let got: Vec<(u64, i64)> = out.iter().map(|r| (r.row_id.0, int_of(r))).collect();
185        assert_eq!(got, vec![(1, 11), (3, 31)], "row 2 hidden, newest wins");
186    }
187
188    #[test]
189    fn drain_sorted_is_ascending_version_order_and_empties() {
190        let mut mr = MutableRun::new();
191        mr.insert_many(vec![row(3, 1, 0), row(1, 2, 0), row(1, 1, 0), row(2, 1, 0)]);
192        let out = mr.drain_sorted();
193        let keys: Vec<(u64, u64)> = out
194            .iter()
195            .map(|r| (r.row_id.0, r.committed_epoch.0))
196            .collect();
197        assert_eq!(keys, vec![(1, 1), (1, 2), (2, 1), (3, 1)]);
198        assert!(mr.is_empty());
199        assert_eq!(mr.approx_bytes(), 0);
200    }
201
202    #[test]
203    fn many_inserts_stay_queryable() {
204        let mut mr = MutableRun::new();
205        let mut rows = Vec::new();
206        for i in 0..500u64 {
207            rows.push(row(i, 1, i as i64));
208        }
209        mr.insert_many(rows);
210        assert_eq!(mr.len(), 500);
211        for i in 0..500u64 {
212            assert_eq!(
213                int_of(&mr.get_version(RowId(i), Epoch(1)).unwrap().1),
214                i as i64
215            );
216        }
217        assert!(mr.approx_bytes() > 0);
218    }
219}