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. Seeks straight to `row_id`'s versions via the
80    /// PMA's gappy binary search instead of scanning from the front.
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_from(&(row_id, Epoch::ZERO)) {
84            if *rid != row_id {
85                break;
86            }
87            if *ep <= snapshot_epoch {
88                best = Some((*ep, row.clone()));
89            }
90        }
91        best
92    }
93
94    /// Newest visible version per `RowId` at `snapshot` (including tombstones),
95    /// ascending by `RowId` — mirroring `Memtable::visible_versions`.
96    pub fn visible_versions(&self, snapshot_epoch: Epoch) -> Vec<Row> {
97        let mut by_row: BTreeMap<RowId, Row> = BTreeMap::new();
98        for ((rid, ep), row) in self.pma.iter() {
99            if *ep <= snapshot_epoch {
100                by_row.insert(*rid, row.clone()); // ascending ⇒ newest wins
101            }
102        }
103        by_row.into_values().collect()
104    }
105
106    /// Drain every version in ascending `(RowId, Epoch)` order — the order
107    /// `RunWriter::write` requires when spilling to an immutable run.
108    pub fn drain_sorted(&mut self) -> Vec<Row> {
109        let out: Vec<Row> = self
110            .pma
111            .drain_sorted()
112            .into_iter()
113            .map(|(_, r)| r)
114            .collect();
115        self.byte_size = 0;
116        out
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::memtable::Value;
124
125    fn row(id: u64, epoch: u64, v: i64) -> Row {
126        Row::new(RowId(id), Epoch(epoch)).with_column(1, Value::Int64(v))
127    }
128
129    fn tomb(id: u64, epoch: u64) -> Row {
130        Row {
131            row_id: RowId(id),
132            committed_epoch: Epoch(epoch),
133            columns: std::collections::HashMap::new(),
134            deleted: true,
135        }
136    }
137
138    fn int_of(r: &Row) -> i64 {
139        match r.columns.get(&1) {
140            Some(Value::Int64(x)) => *x,
141            _ => panic!("expected Int64 column"),
142        }
143    }
144
145    #[test]
146    fn get_version_returns_newest_visible() {
147        let mut mr = MutableRun::new();
148        mr.insert_many(vec![row(1, 1, 10), row(1, 3, 30), row(1, 9, 90)]);
149        // Snapshot before the 9 version sees the 3 version.
150        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(5)).unwrap().1), 30);
151        // Latest snapshot sees the newest.
152        assert_eq!(int_of(&mr.get_version(RowId(1), Epoch(9)).unwrap().1), 90);
153        // No version at/before epoch 0.
154        assert!(mr.get_version(RowId(1), Epoch(0)).is_none());
155        // Missing row.
156        assert!(mr.get_version(RowId(2), Epoch(100)).is_none());
157    }
158
159    #[test]
160    fn tombstone_is_returned_as_a_version() {
161        let mut mr = MutableRun::new();
162        mr.insert_many(vec![row(1, 1, 10), tomb(1, 2)]);
163        let v = mr.get_version(RowId(1), Epoch(5)).unwrap().1;
164        assert!(v.deleted);
165        // Before the tombstone the live version is visible.
166        let v0 = mr.get_version(RowId(1), Epoch(1)).unwrap().1;
167        assert!(!v0.deleted);
168    }
169
170    #[test]
171    fn visible_versions_dedups_to_newest_ascending() {
172        let mut mr = MutableRun::new();
173        mr.insert_many(vec![
174            row(3, 1, 30),
175            row(1, 1, 10),
176            row(2, 9, 20), // future relative to snapshot 5
177            row(1, 3, 11), // newer version of row 1
178            row(3, 2, 31),
179        ]);
180        let out = mr.visible_versions(Epoch(5));
181        let got: Vec<(u64, i64)> = out.iter().map(|r| (r.row_id.0, int_of(r))).collect();
182        assert_eq!(got, vec![(1, 11), (3, 31)], "row 2 hidden, newest wins");
183    }
184
185    #[test]
186    fn drain_sorted_is_ascending_version_order_and_empties() {
187        let mut mr = MutableRun::new();
188        mr.insert_many(vec![row(3, 1, 0), row(1, 2, 0), row(1, 1, 0), row(2, 1, 0)]);
189        let out = mr.drain_sorted();
190        let keys: Vec<(u64, u64)> = out
191            .iter()
192            .map(|r| (r.row_id.0, r.committed_epoch.0))
193            .collect();
194        assert_eq!(keys, vec![(1, 1), (1, 2), (2, 1), (3, 1)]);
195        assert!(mr.is_empty());
196        assert_eq!(mr.approx_bytes(), 0);
197    }
198
199    #[test]
200    fn many_inserts_stay_queryable() {
201        let mut mr = MutableRun::new();
202        let mut rows = Vec::new();
203        for i in 0..500u64 {
204            rows.push(row(i, 1, i as i64));
205        }
206        mr.insert_many(rows);
207        assert_eq!(mr.len(), 500);
208        for i in 0..500u64 {
209            assert_eq!(
210                int_of(&mr.get_version(RowId(i), Epoch(1)).unwrap().1),
211                i as i64
212            );
213        }
214        assert!(mr.approx_bytes() > 0);
215    }
216}