sqlrite/mvcc/mod.rs
1//! Multi-version concurrency control primitives (Phase 11).
2//!
3//! This module is the foundation for SQLRite's `BEGIN CONCURRENT`
4//! story — see [`docs/concurrent-writes-plan.md`](../../docs/concurrent-writes-plan.md)
5//! for the full sequenced design.
6//!
7//! Surface as of Phase 11.3:
8//!
9//! - [`MvccClock`] — process-wide monotonic `u64` counter that hands
10//! out begin- and commit-timestamps. Persisted to the WAL header
11//! so timestamps don't reuse the same value across reopens.
12//! - [`ActiveTxRegistry`] — tracks the begin-timestamps of in-flight
13//! transactions; [`ActiveTxRegistry::min_active_begin_ts`] is the
14//! GC watermark.
15//! - [`TxId`] / [`TxTimestampOrId`] — types the version chains
16//! carry.
17//! - [`MvStore`] — the in-memory version index. Holds row chains
18//! keyed by [`RowID`]; `read(row, begin_ts)` implements the
19//! snapshot-isolation visibility rule (`begin <= T < end`).
20//! - [`JournalMode`] — per-database setting toggled by
21//! `PRAGMA journal_mode = …`. `Wal` (default) keeps every
22//! pre-Phase-11 read path in place; `Mvcc` is the opt-in that
23//! 11.4 will wire reads through.
24//!
25//! The executor doesn't consult `MvStore` yet — that wiring lives
26//! in 11.4 alongside `BEGIN CONCURRENT` writes. Decoupling the
27//! data structure (this PR) from the read/write integration (next
28//! PR) keeps the diffs reviewable.
29
30pub mod clock;
31pub mod log;
32pub mod registry;
33pub mod store;
34pub mod transaction;
35
36pub use clock::MvccClock;
37pub use log::{MVCC_BODY_MAGIC, MVCC_FRAME_MARKER, MvccCommitBatch, MvccLogRecord};
38pub use registry::{ActiveTxRegistry, TxHandle, TxId, TxTimestampOrId};
39pub use store::{MvStore, MvStoreError, RowID, RowVersion, RowVersionChain, VersionPayload};
40pub use transaction::ConcurrentTx;
41
42/// Selects the durability + concurrency story a database operates
43/// under. Toggled by `PRAGMA journal_mode = …` (see
44/// [`crate::sql::pragma::execute_pragma`]).
45///
46/// - [`JournalMode::Wal`] (default) — every read goes through the
47/// legacy table → pager path; every write fsyncs a per-page
48/// commit frame. This is the only mode pre-Phase-11 builds knew
49/// about, and it's what file-format-v5 + WAL-format-v2 files
50/// produce by default.
51/// - [`JournalMode::Mvcc`] — opts the database into Phase 11's
52/// multi-version concurrency control. Enables snapshot-isolated
53/// reads (consult `MvStore` first, fall back to the pager) and
54/// `BEGIN CONCURRENT` writes (Phase 11.4). On-disk format is
55/// unchanged; the WAL header's `clock_high_water` byte range
56/// carries the persisted clock value either way.
57///
58/// Phase 11.3 ships the parser surface and the per-database
59/// setting; the read path doesn't change behaviour yet. The
60/// `Mvcc` value is observable via the PRAGMA read form so callers
61/// can confirm the toggle landed.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub enum JournalMode {
64 /// Default — legacy WAL-backed pager. Every commit fsyncs a
65 /// page-level frame; every read consults `staged → wal_cache
66 /// → on_disk`.
67 #[default]
68 Wal,
69 /// Phase 11 MVCC + `BEGIN CONCURRENT`. Same on-disk format as
70 /// `Wal`; the in-memory `MvStore` sits in front of the pager
71 /// for reads, and writes go through commit-time validation.
72 Mvcc,
73}
74
75impl JournalMode {
76 /// Parses a PRAGMA value (case-insensitive). Returns `None` for
77 /// unrecognized inputs so the caller can surface a typed
78 /// `unknown journal_mode` error with the bad string.
79 pub fn from_str_lossless(s: &str) -> Option<Self> {
80 match s.to_ascii_lowercase().as_str() {
81 "wal" => Some(Self::Wal),
82 "mvcc" => Some(Self::Mvcc),
83 _ => None,
84 }
85 }
86
87 /// The lowercase string form the PRAGMA read renders.
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 Self::Wal => "wal",
91 Self::Mvcc => "mvcc",
92 }
93 }
94}
95
96impl std::fmt::Display for JournalMode {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.write_str(self.as_str())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn journal_mode_default_is_wal() {
108 assert_eq!(JournalMode::default(), JournalMode::Wal);
109 }
110
111 #[test]
112 fn journal_mode_round_trips_through_str() {
113 assert_eq!(
114 JournalMode::from_str_lossless("wal"),
115 Some(JournalMode::Wal)
116 );
117 assert_eq!(
118 JournalMode::from_str_lossless("WAL"),
119 Some(JournalMode::Wal)
120 );
121 assert_eq!(
122 JournalMode::from_str_lossless("Mvcc"),
123 Some(JournalMode::Mvcc)
124 );
125 assert_eq!(JournalMode::from_str_lossless("delete"), None);
126 assert_eq!(JournalMode::from_str_lossless(""), None);
127 }
128
129 #[test]
130 fn journal_mode_displays_lowercase() {
131 assert_eq!(format!("{}", JournalMode::Wal), "wal");
132 assert_eq!(format!("{}", JournalMode::Mvcc), "mvcc");
133 }
134}