Skip to main content

spg_storage/
snapshot.rs

1//! v7.37.15 (Phase A) — per-statement / per-transaction snapshot.
2//!
3//! A `Snapshot` captures **which other transactions had committed**
4//! at the moment the reader took the snapshot. Combined with the
5//! row's [`crate::row_header::RowHeader`] it answers the central
6//! MVCC question: "should THIS reader see THIS row?"
7//!
8//! ## Compared to PG
9//!
10//! PG `SnapshotData` carries `xmin / xmax / xip[] / xcnt /
11//! suboverflowed / takenDuringRecovery / curcid / speculativeToken /
12//! whenTaken / lsn`. SPG strips that to the four fields the
13//! visibility rule actually consumes:
14//!
15//! - `version` — the upper bound: any row whose `xmin` exceeds
16//!   this didn't exist at snapshot time.
17//! - `in_progress` — the bitset of transactions that were ALREADY
18//!   ALLOCATED (i.e. `xmin <= version`) but had NOT YET committed
19//!   at snapshot time. Their writes are invisible to this reader.
20//! - `oldest_active` — the floor used by vacuum to safely reclaim
21//!   tombstones: any row whose `xmax < oldest_active` is dead to
22//!   every live snapshot.
23//! - `tx_id` — the reader's OWN transaction id, so the snapshot
24//!   can implement the "see your own writes" rule (READ COMMITTED
25//!   sees its own UPDATE result).
26//!
27//! That is enough for READ COMMITTED + REPEATABLE READ +
28//! SERIALIZABLE (SSI conflict tracking lives in a sidecar — see
29//! Phase E).
30
31extern crate alloc;
32use alloc::vec::Vec;
33
34use crate::row_header::{HEAP_XMIN_FROZEN, RowHeader, XMAX_ALIVE};
35
36/// v7.37.15 (Phase C.2) — terminal state of a transaction / row
37/// version, as seen by the visibility oracle.
38///
39/// The current [`Snapshot::visible`] rule assumes "not in the
40/// snapshot's `in_progress` set ⟹ committed". That holds while every
41/// write is committed-and-alive or frozen. Once Phase C.3's in-place
42/// write path leaves ABORTED xmin/xmax stamps physically present
43/// (rolled-back or crash-orphaned versions vacuum hasn't reclaimed
44/// yet), a version can be `<= snapshot.version` and `∉ in_progress`
45/// yet aborted — and the two-state rule would wrongly show its rows.
46/// [`Snapshot::visible_with_status`] adds the third state.
47///
48/// The state is **monotonic and immutable once terminal**: a version
49/// only ever moves InProgress → {Committed, Aborted} and never back,
50/// so a live oracle lookup for a version outside the snapshot's frozen
51/// `in_progress` set returns the same answer forever — which is why
52/// the snapshot stays a value type and only the oracle is shared.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum XactStatus {
55    /// Allocated but not yet committed or aborted.
56    InProgress,
57    /// Committed — its inserts are real, its deletes took effect.
58    Committed,
59    /// Rolled back / crash-orphaned — its inserts never happened, its
60    /// deletes never took effect.
61    Aborted,
62}
63
64/// v7.37.15 (Phase C.2) — the visibility oracle: maps a version id to
65/// its terminal [`XactStatus`].
66///
67/// `spg-storage` (`no_std`) defines the contract; the real registry
68/// (a sharded `DashMap<u64, XactStatus>`) lives in `spg-engine` and
69/// implements this. Kept as a trait so the visibility rule stays in
70/// storage next to [`Snapshot::visible`] without storage depending on
71/// the engine's concurrency primitives.
72pub trait XactStatusOracle {
73    /// Terminal status of `version`. Implementations return
74    /// [`XactStatus::Committed`] for any version they no longer track
75    /// (pruned below `oldest_active`, or frozen) — those are, by
76    /// definition, committed-and-old.
77    fn status(&self, version: u64) -> XactStatus;
78}
79
80/// An oracle that reports every version as committed. Equivalent to
81/// the pre-Phase-C.2 two-state world; lets a caller that does not yet
82/// track aborts reuse [`Snapshot::visible_with_status`] and get
83/// behaviour identical to [`Snapshot::visible`].
84#[derive(Debug, Clone, Copy, Default)]
85pub struct AllCommitted;
86
87impl XactStatusOracle for AllCommitted {
88    #[inline]
89    fn status(&self, _version: u64) -> XactStatus {
90        XactStatus::Committed
91    }
92}
93
94/// Compact in-progress set. Stored as a sorted `Vec<u64>` so the
95/// `contains` check is a binary search — O(log n) and zero
96/// allocation per lookup. We expect `n` to be tens at most (the
97/// active transaction count); for that range bsearch beats a
98/// hashset by a wide margin on both wall-clock and cache.
99///
100/// When `n` blows past ~1k (which would mean a runaway leak — every
101/// real OLTP workload caps at a few dozen concurrent writers) we
102/// would consider a roaring-bitmap-style sparse representation;
103/// not needed at v7.37.15.0 scale.
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105pub struct InProgressSet {
106    sorted: Vec<u64>,
107}
108
109impl InProgressSet {
110    /// Construct from a pre-sorted slice. The caller must verify
111    /// monotonic order; debug builds assert.
112    #[must_use]
113    pub fn from_sorted(sorted: Vec<u64>) -> Self {
114        debug_assert!(
115            sorted.windows(2).all(|w| w[0] < w[1]),
116            "InProgressSet::from_sorted requires strictly monotonic input"
117        );
118        Self { sorted }
119    }
120
121    /// Empty set — no transactions in flight. The default for
122    /// a snapshot taken in a quiescent moment.
123    #[must_use]
124    pub const fn empty() -> Self {
125        Self { sorted: Vec::new() }
126    }
127
128    /// v7.39 (round 518) — the ids, in order. `txid_current_snapshot()`
129    /// renders them as PG's `xip_list`.
130    #[must_use]
131    pub fn ids(&self) -> &[u64] {
132        &self.sorted
133    }
134
135    /// True iff `xid` is one of the in-flight transactions.
136    /// Binary search; O(log n).
137    #[must_use]
138    pub fn contains(&self, xid: u64) -> bool {
139        self.sorted.binary_search(&xid).is_ok()
140    }
141
142    /// Number of in-flight transactions captured.
143    #[must_use]
144    pub fn len(&self) -> usize {
145        self.sorted.len()
146    }
147
148    /// True iff no transactions were in flight at capture time.
149    #[must_use]
150    pub fn is_empty(&self) -> bool {
151        self.sorted.is_empty()
152    }
153}
154
155/// Per-statement / per-transaction snapshot.
156///
157/// Cheap to clone (Vec inside an InProgressSet is the only
158/// non-Copy field; bounded at active-tx count which is small).
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct Snapshot {
161    /// The upper bound. A row whose `xmin` exceeds this is
162    /// in the snapshot's future — invisible.
163    pub version: u64,
164    /// In-flight transactions at snapshot time.
165    pub in_progress: InProgressSet,
166    /// Floor used by vacuum. Any row whose `xmax < oldest_active`
167    /// is dead to every live snapshot.
168    pub oldest_active: u64,
169    /// The reading transaction's OWN id. Used to implement "see
170    /// your own writes" — a row your transaction inserted is
171    /// visible to you even before commit. `0` for non-
172    /// transactional reads (autocommit SELECT).
173    pub tx_id: u64,
174    /// v7.39 (round 297, E3 Phase 1b) — rows a `SKIP LOCKED` pre-pass
175    /// found held by another transaction, as `(relation, row indices)`.
176    ///
177    /// It rides the SNAPSHOT because that is the only channel every row
178    /// source already threads. Adding the filter at individual scan
179    /// sites missed the real path three times running — `index_access`
180    /// alone performs the visibility test in ten places. A row that
181    /// someone else holds is, for this statement, exactly as
182    /// unavailable as a row the snapshot cannot see.
183    pub locked_out: Option<(
184        crate::row_header::RelId,
185        alloc::collections::BTreeSet<usize>,
186    )>,
187}
188
189impl Snapshot {
190    /// A "see everything visible" snapshot — version at the
191    /// current upper-bound u64, in-progress empty. Equivalent to
192    /// the pre-v7.37.15 "Arc-snapshot reads the entire catalog"
193    /// behaviour; useful for phase-A migration where the engine
194    /// doesn't yet track per-tx state.
195    #[must_use]
196    pub const fn unbounded() -> Self {
197        Self {
198            locked_out: None,
199            version: u64::MAX,
200            in_progress: InProgressSet::empty(),
201            oldest_active: u64::MAX,
202            tx_id: 0,
203        }
204    }
205
206    /// Construct from explicit fields. The version cursor and
207    /// in-progress set come from the engine's per-process
208    /// version counter at snapshot time; oldest_active is the
209    /// MIN of every live snapshot's version (vacuum reads it).
210    #[must_use]
211    pub fn new(version: u64, in_progress: InProgressSet, oldest_active: u64, tx_id: u64) -> Self {
212        Self {
213            locked_out: None,
214            version,
215            in_progress,
216            oldest_active,
217            tx_id,
218        }
219    }
220
221    /// Should the row be visible to a reader holding this
222    /// snapshot? The five-step rule mirrors PG's HeapTupleSatisfiesMVCC.
223    ///
224    /// 1. Self-write: if the row's writer is THIS reader's own tx,
225    ///    the row is visible (READ COMMITTED sees its own writes).
226    /// 2. xmin in the future: invisible.
227    /// 3. xmin still in-progress: invisible.
228    /// 4. Alive (xmax == ALIVE): visible.
229    /// 5. xmax in the future or in-progress: visible (the delete
230    ///    hasn't committed yet from this reader's point of view).
231    /// 6. xmax in the past + committed: invisible (deleted before
232    ///    this reader's snapshot).
233    #[must_use]
234    pub fn visible(&self, h: &RowHeader) -> bool {
235        // Step 1: your own writes. A row THIS transaction deleted is
236        // invisible to it (whatever inserted it) — PG's "you don't see
237        // what you deleted"; a row this transaction inserted and has
238        // not deleted is visible. (If `xmin == tx_id` and it isn't the
239        // deleter, `xmax` can only be ALIVE — no other transaction can
240        // delete a row this uncommitted tx inserted.)
241        if self.tx_id != 0 {
242            if h.xmax == self.tx_id {
243                return false;
244            }
245            if h.xmin == self.tx_id {
246                return true;
247            }
248        }
249        // Step 2: future.
250        if h.xmin > self.version {
251            return false;
252        }
253        // Step 3: in-flight at snapshot time.
254        if self.in_progress.contains(h.xmin) {
255            return false;
256        }
257        // Step 4: still alive.
258        if h.xmax == XMAX_ALIVE {
259            return true;
260        }
261        // Step 5: deletion is future or in-flight → still visible.
262        if h.xmax > self.version || self.in_progress.contains(h.xmax) {
263            return true;
264        }
265        // Step 6: deletion committed before our snapshot.
266        false
267    }
268
269    /// v7.37.15 (Phase C.2) — abort-aware visibility. Same as
270    /// [`Self::visible`] but consults a [`XactStatusOracle`] to
271    /// distinguish a *committed* version from an *aborted* one when
272    /// neither is in the snapshot's `in_progress` set. Phase C.3's
273    /// in-place write path needs this: a rolled-back or crash-orphaned
274    /// version leaves its xmin/xmax stamp physically present until
275    /// vacuum reclaims it, and the two-state [`Self::visible`] would
276    /// wrongly treat it as committed.
277    ///
278    /// Two extra branches vs `visible` (marked NEW):
279    /// - xmin aborted → the insert never happened → invisible.
280    /// - xmax aborted → the delete never happened → still visible.
281    ///
282    /// The frozen-and-alive fast path returns before any oracle call,
283    /// so steady-state scans over old data pay no oracle cost —
284    /// preserving the Phase B "≤5% overhead" result. Passing
285    /// [`AllCommitted`] makes this behave exactly like `visible`.
286    #[must_use]
287    pub fn visible_with_status<O: XactStatusOracle + ?Sized>(
288        &self,
289        h: &RowHeader,
290        xact: &O,
291    ) -> bool {
292        // Step 1: your own writes (see `visible` for the rationale). A
293        // row this tx deleted is invisible; a row it inserted and has
294        // not deleted is visible.
295        if self.tx_id != 0 {
296            if h.xmax == self.tx_id {
297                return false;
298            }
299            if h.xmin == self.tx_id {
300                return true;
301            }
302        }
303        // Frozen fast path: frozen + alive is visible to everyone and
304        // never consults the oracle (a frozen xmin is committed-and-old
305        // by definition).
306        if h.flags & HEAP_XMIN_FROZEN != 0 && h.xmax == XMAX_ALIVE {
307            return true;
308        }
309        // Step 2: future.
310        if h.xmin > self.version {
311            return false;
312        }
313        // Step 3: in-flight at snapshot time.
314        if self.in_progress.contains(h.xmin) {
315            return false;
316        }
317        // Step 4 (NEW): xmin aborted → the insert never committed.
318        if xact.status(h.xmin) == XactStatus::Aborted {
319            return false;
320        }
321        // Step 5: still alive.
322        if h.xmax == XMAX_ALIVE {
323            return true;
324        }
325        // Step 6: deletion is future or in-flight → still visible.
326        if h.xmax > self.version || self.in_progress.contains(h.xmax) {
327            return true;
328        }
329        // Step 7 (NEW): xmax aborted → the delete never took effect.
330        if xact.status(h.xmax) == XactStatus::Aborted {
331            return true;
332        }
333        // Step 8: deletion committed before our snapshot.
334        false
335    }
336}
337
338impl Default for Snapshot {
339    fn default() -> Self {
340        Self::unbounded()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::row_header::RowHeader;
348
349    fn ips(xs: &[u64]) -> InProgressSet {
350        InProgressSet::from_sorted(xs.to_vec())
351    }
352
353    #[test]
354    fn unbounded_snapshot_sees_everything() {
355        let s = Snapshot::unbounded();
356        let frozen = RowHeader::frozen();
357        let alive = RowHeader::alive(7);
358        assert!(s.visible(&frozen));
359        assert!(s.visible(&alive));
360    }
361
362    #[test]
363    fn snapshot_hides_future_writes() {
364        let s = Snapshot::new(100, ips(&[]), 100, 0);
365        let row = RowHeader::alive(150); // written after snapshot
366        assert!(!s.visible(&row));
367    }
368
369    #[test]
370    fn snapshot_hides_in_progress_writes() {
371        let s = Snapshot::new(200, ips(&[50, 60, 70]), 50, 0);
372        let row = RowHeader::alive(60); // tx 60 still in flight
373        assert!(!s.visible(&row));
374        let row2 = RowHeader::alive(55); // tx 55 not in in_progress => committed
375        assert!(s.visible(&row2));
376    }
377
378    #[test]
379    fn snapshot_hides_committed_deletions() {
380        let s = Snapshot::new(200, ips(&[]), 100, 0);
381        let row = RowHeader {
382            xmin: 50,
383            xmax: 100, // deleted before our snapshot
384            flags: 0,
385        };
386        assert!(!s.visible(&row));
387    }
388
389    #[test]
390    fn snapshot_keeps_pending_deletions_visible() {
391        let s = Snapshot::new(200, ips(&[150]), 100, 0);
392        let row = RowHeader {
393            xmin: 50,
394            xmax: 150, // delete by an in-flight tx
395            flags: 0,
396        };
397        assert!(s.visible(&row));
398    }
399
400    #[test]
401    fn reader_sees_its_own_insert_but_not_its_own_delete() {
402        let s = Snapshot::new(100, ips(&[]), 100, 42);
403        // A row I inserted and have not deleted: visible.
404        let own_insert = RowHeader::alive(42);
405        assert!(s.visible(&own_insert));
406        // A row I inserted AND deleted (BEGIN; INSERT; DELETE; SELECT):
407        // invisible — you don't see what you deleted (PG semantics).
408        let own_insert_then_delete = RowHeader {
409            xmin: 42,
410            xmax: 42,
411            flags: 0,
412        };
413        assert!(!s.visible(&own_insert_then_delete));
414        // A committed row I deleted (xmin other, xmax me): invisible.
415        let other_insert_i_deleted = RowHeader {
416            xmin: 7,
417            xmax: 42,
418            flags: 0,
419        };
420        assert!(!s.visible(&other_insert_i_deleted));
421    }
422
423    #[test]
424    fn snapshot_hides_future_deletion_done_by_in_flight_tx() {
425        // Edge: tx 150 in-flight AND xmin = 30 (not in in_progress)
426        // → row is alive to us even though xmax is set.
427        let s = Snapshot::new(200, ips(&[150]), 30, 0);
428        let row = RowHeader {
429            xmin: 30,
430            xmax: 150,
431            flags: 0,
432        };
433        assert!(s.visible(&row));
434    }
435
436    /// Test oracle: every version in the set is Aborted, all others
437    /// Committed. Mirrors what the engine's real registry reports for
438    /// a version whose tx rolled back.
439    struct AbortedSet(alloc::vec::Vec<u64>);
440    impl XactStatusOracle for AbortedSet {
441        fn status(&self, v: u64) -> XactStatus {
442            if self.0.contains(&v) {
443                XactStatus::Aborted
444            } else {
445                XactStatus::Committed
446            }
447        }
448    }
449
450    #[test]
451    fn all_committed_oracle_matches_plain_visible() {
452        // With every version committed, visible_with_status must
453        // agree with visible on a spread of header shapes.
454        let s = Snapshot::new(200, ips(&[150]), 50, 42);
455        let headers = [
456            RowHeader::frozen(),
457            RowHeader::alive(60),
458            RowHeader::alive(250),
459            RowHeader {
460                xmin: 50,
461                xmax: 100,
462                flags: 0,
463            },
464            RowHeader {
465                xmin: 50,
466                xmax: 150,
467                flags: 0,
468            },
469            RowHeader {
470                xmin: 42,
471                xmax: XMAX_ALIVE,
472                flags: 0,
473            },
474        ];
475        for h in &headers {
476            assert_eq!(
477                s.visible(h),
478                s.visible_with_status(h, &AllCommitted),
479                "mismatch on {h:?}"
480            );
481        }
482    }
483
484    #[test]
485    fn aborted_xmin_hides_the_row() {
486        // Row inserted by version 60, which then aborted. It is NOT in
487        // in_progress (it reached a terminal state), so plain visible
488        // would wrongly show it; the oracle hides it.
489        let s = Snapshot::new(200, ips(&[]), 50, 0);
490        let row = RowHeader::alive(60);
491        assert!(s.visible(&row), "two-state rule shows the orphan");
492        assert!(
493            !s.visible_with_status(&row, &AbortedSet(alloc::vec![60])),
494            "abort oracle hides the never-committed insert"
495        );
496    }
497
498    #[test]
499    fn aborted_xmax_revives_the_row() {
500        // Row inserted by (committed) 50, deleted by 90 which aborted.
501        // The delete never took effect → the row is still visible.
502        let s = Snapshot::new(200, ips(&[]), 50, 0);
503        let row = RowHeader {
504            xmin: 50,
505            xmax: 90,
506            flags: 0,
507        };
508        assert!(
509            !s.visible(&row),
510            "two-state rule treats delete as committed"
511        );
512        assert!(
513            s.visible_with_status(&row, &AbortedSet(alloc::vec![90])),
514            "abort oracle keeps the row whose delete was rolled back"
515        );
516    }
517
518    #[test]
519    fn frozen_row_skips_the_oracle() {
520        // A frozen+alive row is visible without ever consulting the
521        // oracle — even a (nonsensical) oracle that would abort xmin=1.
522        struct Panicking;
523        impl XactStatusOracle for Panicking {
524            fn status(&self, _v: u64) -> XactStatus {
525                panic!("oracle must not be consulted for a frozen+alive row");
526            }
527        }
528        let s = Snapshot::new(200, ips(&[]), 50, 0);
529        assert!(s.visible_with_status(&RowHeader::frozen(), &Panicking));
530    }
531
532    #[test]
533    fn in_progress_set_binary_search_correctness() {
534        let s = ips(&[10, 20, 30, 40, 50]);
535        assert!(s.contains(10));
536        assert!(s.contains(30));
537        assert!(s.contains(50));
538        assert!(!s.contains(0));
539        assert!(!s.contains(25));
540        assert!(!s.contains(60));
541    }
542}