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.
30#[derive(Clone)]
31pub struct MutableRun {
32    pma: Pma<VersionKey, Row>,
33    byte_size: u64,
34}
35
36impl Default for MutableRun {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl MutableRun {
43    pub fn new() -> Self {
44        Self {
45            pma: Pma::new(),
46            byte_size: 0,
47        }
48    }
49
50    /// Fold drained memtable rows (already ascending by `(RowId, Epoch)`) into
51    /// the tier via one bulk merge + re-spread — far cheaper than per-element
52    /// inserts, which would cluster at the tail on sorted input.
53    pub fn insert_many(&mut self, rows: Vec<Row>) {
54        let batch: Vec<(VersionKey, Row)> = rows
55            .into_iter()
56            .map(|r| {
57                self.byte_size = self.byte_size.saturating_add(r.estimated_bytes());
58                ((r.row_id, r.committed_epoch), r)
59            })
60            .collect();
61        self.pma.extend_sorted(batch);
62    }
63
64    /// Number of stored versions.
65    pub fn len(&self) -> usize {
66        self.pma.len()
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.pma.is_empty()
71    }
72
73    /// Approximate bytes held — the spill-threshold signal.
74    pub fn approx_bytes(&self) -> u64 {
75        self.byte_size
76    }
77
78    /// Newest version of `row_id` with `epoch <= snapshot` (including
79    /// tombstones), mirroring `Memtable::get_version`. Returns `None` if no
80    /// version is visible. Seeks straight to `row_id`'s versions via the
81    /// PMA's gappy binary search instead of scanning from the front.
82    pub fn get_version(&self, row_id: RowId, snapshot_epoch: Epoch) -> Option<(Epoch, Row)> {
83        let mut best: Option<(Epoch, Row)> = None;
84        for ((rid, ep), row) in self.pma.iter_from(&(row_id, Epoch::ZERO)) {
85            if *rid != row_id {
86                break;
87            }
88            if *ep <= snapshot_epoch {
89                best = Some((*ep, row.clone()));
90            }
91        }
92        best
93    }
94
95    /// Newest visible version per `RowId` at `snapshot` (including tombstones),
96    /// ascending by `RowId` — mirroring `Memtable::visible_versions`.
97    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
98        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
99        for ((rid, ep), row) in self.pma.iter() {
100            if *ep <= snapshot_epoch {
101                by_row.insert(*rid, row.clone()); // ascending ⇒ newest wins
102            }
103        }
104        by_row.into_values().collect()
105    }
106
107    /// Drain every version in ascending `(RowId, Epoch)` order — the order
108    /// `RunWriter::write` requires when spilling to an immutable run.
109    pub fn drain_sorted(&mut self) -> Vec<Row> {
110        let out: Vec<Row> = self
111            .pma
112            .drain_sorted()
113            .into_iter()
114            .map(|(_, r)| r)
115            .collect();
116        self.byte_size = 0;
117        out
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::memtable::Value;
125
126    fn row(id: u64, epoch: u64, v: i64) -> Row {
127        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(v))
128    }
129
130    fn tomb(id: u64, epoch: u64) -> Row {
131        Row {
132            row_id: RowId(id),
133            committed_epoch: Epoch(epoch),
134            columns: std::collections::HashMap::new(),
135            deleted: true,
136        }
137    }
138
139    fn int_of(r: &Row) -> i64 {
140        match r.columns.get(&1) {
141            Some(Value::Int64(x)) => *x,
142            _ => panic!("expected Int64 column"),
143        }
144    }
145
146    #[test]
147    fn get_version_returns_newest_visible() {
148        let mut mr = MutableRun::new();
149        mr.insert_many(vec![row(1, 1, 10), row(1, 3, 30), row(1, 9, 90)]);
150        // Snapshot before the 9 version sees the 3 version.
151        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(5)).unwrap().1), 30);
152        // Latest snapshot sees the newest.
153        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(9)).unwrap().1), 90);
154        // No version at/before epoch 0.
155        assert!(mr.get_version(RowId(1), Epoch(0)).is_none());
156        // Missing row.
157        assert!(mr.get_version(RowId(2), Epoch(100)).is_none());
158    }
159
160    #[test]
161    fn tombstone_is_returned_as_a_version() {
162        let mut mr = MutableRun::new();
163        mr.insert_many(vec![row(1, 1, 10), tomb(1, 2)]);
164        let v = mr.get_version(RowId(1), Epoch(5)).unwrap().1;
165        assert!(v.deleted);
166        // Before the tombstone the live version is visible.
167        let v0 = mr.get_version(RowId(1), Epoch(1)).unwrap().1;
168        assert!(!v0.deleted);
169    }
170
171    #[test]
172    fn visible_versions_dedups_to_newest_ascending() {
173        let mut mr = MutableRun::new();
174        mr.insert_many(vec![
175            row(3, 1, 30),
176            row(1, 1, 10),
177            row(2, 9, 20), // future relative to snapshot 5
178            row(1, 3, 11), // newer version of row 1
179            row(3, 2, 31),
180        ]);
181        let out = mr.visible_versions(Epoch(5));
182        let got: Vec<(u64, i64)> = out.iter().map(|r| (r.row_id.0, int_of(r))).collect();
183        assert_eq!(got, vec![(1, 11), (3, 31)], "row 2 hidden, newest wins");
184    }
185
186    #[test]
187    fn drain_sorted_is_ascending_version_order_and_empties() {
188        let mut mr = MutableRun::new();
189        mr.insert_many(vec![row(3, 1, 0), row(1, 2, 0), row(1, 1, 0), row(2, 1, 0)]);
190        let out = mr.drain_sorted();
191        let keys: Vec<(u64, u64)> = out
192            .iter()
193            .map(|r| (r.row_id.0, r.committed_epoch.0))
194            .collect();
195        assert_eq!(keys, vec![(1, 1), (1, 2), (2, 1), (3, 1)]);
196        assert!(mr.is_empty());
197        assert_eq!(mr.approx_bytes(), 0);
198    }
199
200    #[test]
201    fn many_inserts_stay_queryable() {
202        let mut mr = MutableRun::new();
203        let mut rows = Vec::new();
204        for i in 0..500u64 {
205            rows.push(row(i, 1, i as i64));
206        }
207        mr.insert_many(rows);
208        assert_eq!(mr.len(), 500);
209        for i in 0..500u64 {
210            assert_eq!(
211                int_of(&mr.get_version(RowId(i), Epoch(1)).unwrap().1),
212                i as i64
213            );
214        }
215        assert!(mr.approx_bytes() > 0);
216    }
217}