Skip to main content

spacedb_store/
meta.rs

1//! The `_meta` schema gate — **refuse or migrate, never silently open**.
2//!
3//! redb gives a stable on-disk *page* format; this gate guards the layer above
4//! it: SpaceDB's own store format (how `_dek_wrappings`, sealed rows, etc. are
5//! laid out). The reserved `_meta` table records a single `store_format_version`,
6//! and [`open_meta`] enforces the rule that keeps an appliance upgrade from
7//! bricking a user's data:
8//!
9//! - **Fresh store** → stamp the current version. (`Initialized`)
10//! - **Same version** → proceed. (`Current`)
11//! - **Older version** → run the registered [`Migration`] steps up to current,
12//!   then stamp it. (`Migrated`)
13//! - **Newer version** → **refuse** with [`StoreError::SchemaTooNew`]. Reading a
14//!   format written by newer software risks silent misinterpretation, so we stop.
15//!
16//! This is the same discipline as MATA's `dek_wrappings` format gate,
17//! generalized to the whole store.
18//!
19//! Migrations must be **idempotent**: the version is stamped only after all steps
20//! succeed, so a crash mid-migration re-runs the steps from the old version on the
21//! next open.
22
23use crate::engine::{Durability, KvEngine, WriteTx};
24use crate::error::{StoreError, StoreResult};
25use crate::table::Table;
26
27/// The reserved metadata table.
28pub const META_TABLE: &str = "_meta";
29
30/// The key under which the store format version is recorded in [`META_TABLE`].
31pub const STORE_VERSION_KEY: &str = "store_format_version";
32
33/// The store format version this build writes and understands. Bump it (and add a
34/// [`Migration`]) whenever the on-disk layout this crate owns changes.
35pub const STORE_FORMAT_VERSION: u32 = 1;
36
37fn meta_table() -> Table<String, u32> {
38    Table::new(META_TABLE)
39}
40
41/// What [`open_meta`] did.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum MetaStatus {
44    /// A fresh store; the current version was stamped.
45    Initialized,
46    /// The store was already at the current version.
47    Current,
48    /// The store was at `from` and was migrated up to the current version.
49    Migrated { from: u32 },
50}
51
52/// One step in the migration ladder: it upgrades a store at version
53/// [`from_version`](Migration::from_version) to `from_version + 1`.
54///
55/// Object-safe so a ladder is just `&[&dyn Migration<E>]`. Implementations open
56/// their own transactions against the engine and **must be idempotent** (see the
57/// module note on crash safety).
58pub trait Migration<E: KvEngine>: Send + Sync {
59    /// The version this step upgrades *from* (producing `from_version + 1`).
60    fn from_version(&self) -> u32;
61
62    /// Apply the migration against the engine.
63    fn apply(&self, engine: &E) -> StoreResult<()>;
64}
65
66/// Read the recorded store format version, or `None` for a never-initialized
67/// store.
68pub fn read_store_version<E: KvEngine>(engine: &E) -> StoreResult<Option<u32>> {
69    let r = engine.begin_read()?;
70    meta_table().get(&r, &STORE_VERSION_KEY.to_string())
71}
72
73/// Stamp the store format version. **Advanced/admin** — bypasses the gate; normal
74/// callers use [`open_meta`]. Useful for tests and recovery tooling.
75pub fn write_store_version<E: KvEngine>(engine: &E, version: u32) -> StoreResult<()> {
76    let mut w = engine.begin_write(Durability::Immediate)?;
77    meta_table().put(&mut w, &STORE_VERSION_KEY.to_string(), &version)?;
78    w.commit()
79}
80
81/// Open the store's `_meta` gate at [`STORE_FORMAT_VERSION`] with no migrations.
82pub fn open_meta<E: KvEngine>(engine: &E) -> StoreResult<MetaStatus> {
83    open_meta_with(engine, STORE_FORMAT_VERSION, &[])
84}
85
86/// Open the store's `_meta` gate at `current_version`, running `migrations` to
87/// bring an older store up to it. See the module docs for the four cases.
88pub fn open_meta_with<E: KvEngine>(
89    engine: &E,
90    current_version: u32,
91    migrations: &[&dyn Migration<E>],
92) -> StoreResult<MetaStatus> {
93    match read_store_version(engine)? {
94        None => {
95            write_store_version(engine, current_version)?;
96            Ok(MetaStatus::Initialized)
97        }
98        Some(v) if v == current_version => Ok(MetaStatus::Current),
99        Some(v) if v > current_version => Err(StoreError::SchemaTooNew {
100            found: v,
101            supported: current_version,
102        }),
103        Some(v) => {
104            // v < current_version: walk the ladder one step at a time.
105            for from in v..current_version {
106                let step = migrations
107                    .iter()
108                    .find(|m| m.from_version() == from)
109                    .ok_or_else(|| {
110                        StoreError::Schema(format!(
111                            "no migration registered from store format version {from}"
112                        ))
113                    })?;
114                step.apply(engine)?;
115            }
116            // Stamp current only after every step succeeded (idempotency contract).
117            write_store_version(engine, current_version)?;
118            Ok(MetaStatus::Migrated { from: v })
119        }
120    }
121}