Skip to main content

coven_database/
coven_migration.rs

1//! Coven's bookkeeping-schema migration ladder.
2//!
3//! This ladder is separate from the host schema's `PRAGMA user_version`.
4//! Coven defines and applies its own table changes; a writer's policy decides
5//! whether pending Coven changes may run during open. Readers never authorize
6//! writes and therefore refuse pending changes.
7
8use rusqlite::Connection;
9
10use crate::coven_schema::{
11    expected_coven_schema_manifest, expected_coven_schema_v0_manifest, live_coven_schema_manifest,
12    recreate_current_transition_tables, CovenSchemaManifest,
13};
14use crate::{
15    get_protocol_state_on, set_protocol_state_on, DbError, COVEN_INITIALIZED_STATE_KEY,
16    COVEN_SCHEMA_MANIFEST_STATE_KEY,
17};
18
19pub(crate) const COVEN_SCHEMA_VERSION_STATE_KEY: &str = "coven_schema_version";
20
21type ApplyCovenMigration = fn(&Connection) -> Result<(), CovenMigrationError>;
22
23const COVEN_MIGRATION_COUNT: usize = 1;
24const LATEST_COVEN_SCHEMA_VERSION: u32 = COVEN_MIGRATION_COUNT as u32;
25
26pub(crate) struct CovenMigrationStep<'a> {
27    expected_manifest: &'a CovenSchemaManifest,
28    apply: ApplyCovenMigration,
29}
30
31#[cfg(test)]
32impl<'a> CovenMigrationStep<'a> {
33    pub(crate) fn new_for_test(
34        expected_manifest: &'a CovenSchemaManifest,
35        apply: ApplyCovenMigration,
36    ) -> Self {
37        Self {
38            expected_manifest,
39            apply,
40        }
41    }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum CovenMigrationPolicy {
46    ApplyPending,
47    RefusePending,
48}
49
50#[derive(Debug, thiserror::Error)]
51pub enum CovenMigrationError {
52    #[error("Coven schema migration {current} -> {target} is pending")]
53    Pending { current: u32, target: u32 },
54    #[error("Coven schema version {version} is current, but its version ledger is missing")]
55    PendingLedgerInstallation { version: u32 },
56    #[error("Store database is missing required Coven schema manifest metadata")]
57    MissingManifest,
58    #[error("Store Coven schema manifest is invalid: {0}")]
59    InvalidManifest(#[source] serde_json::Error),
60    #[error("failed to serialize the current Coven schema manifest: {0}")]
61    SerializeManifest(#[source] serde_json::Error),
62    #[error("stored and live Coven schema manifests differ")]
63    StoredLiveManifestMismatch,
64    #[error("unversioned Coven schema does not match a known schema version")]
65    UnknownUnversionedSchema,
66    #[error("uninitialized snapshot contains Coven schema metadata {key:?}")]
67    UnexpectedSnapshotMetadata { key: &'static str },
68    #[error("Coven schema version {value:?} is not an unsigned integer: {source}")]
69    InvalidVersion {
70        value: String,
71        #[source]
72        source: std::num::ParseIntError,
73    },
74    #[error("Coven schema version {found} is unsupported; this binary supports version {latest}")]
75    UnsupportedVersion { found: u32, latest: u32 },
76    #[error("Coven schema version {version} does not match its exact schema manifest")]
77    VersionManifestMismatch { version: u32 },
78    #[error("Coven migration to version {version} did not produce its exact schema manifest")]
79    MigrationResultMismatch { version: u32 },
80    #[error("Coven migration requires empty table {table}")]
81    NonEmptyTable { table: String },
82    #[error(transparent)]
83    Database(#[from] rusqlite::Error),
84    #[error(transparent)]
85    Db(#[from] DbError),
86}
87
88enum CovenSchemaState {
89    Current,
90    Pending { current: u32 },
91    PendingLedgerInstallation { version: u32 },
92}
93
94fn stored_manifest(conn: &Connection) -> Result<CovenSchemaManifest, CovenMigrationError> {
95    let json = get_protocol_state_on(conn, COVEN_SCHEMA_MANIFEST_STATE_KEY)?
96        .ok_or(CovenMigrationError::MissingManifest)?;
97    serde_json::from_str(&json).map_err(CovenMigrationError::InvalidManifest)
98}
99
100fn pre_ledger_version(
101    manifest: &CovenSchemaManifest,
102    version_0_manifest: &CovenSchemaManifest,
103    migrations: &[CovenMigrationStep<'_>],
104) -> Option<u32> {
105    if manifest == version_0_manifest {
106        Some(0)
107    } else if migrations
108        .first()
109        .is_some_and(|migration| manifest == migration.expected_manifest)
110    {
111        Some(1)
112    } else {
113        None
114    }
115}
116
117fn uninitialized_snapshot_version(
118    manifest: &CovenSchemaManifest,
119    version_0_manifest: &CovenSchemaManifest,
120    migrations: &[CovenMigrationStep<'_>],
121) -> Option<u32> {
122    let mut matching_versions = std::iter::once((0, version_0_manifest))
123        .chain(
124            migrations
125                .iter()
126                .enumerate()
127                .map(|(index, migration)| ((index + 1) as u32, migration.expected_manifest)),
128        )
129        .filter_map(|(version, expected)| (manifest == expected).then_some(version));
130    let version = matching_versions.next()?;
131    matching_versions.next().is_none().then_some(version)
132}
133
134fn classify_schema(
135    conn: &Connection,
136    version_0_manifest: &CovenSchemaManifest,
137    migrations: &[CovenMigrationStep<'_>],
138) -> Result<CovenSchemaState, CovenMigrationError> {
139    let stored = stored_manifest(conn)?;
140    let live = live_coven_schema_manifest(conn)?;
141    if stored != live {
142        return Err(CovenMigrationError::StoredLiveManifestMismatch);
143    }
144
145    let version = get_protocol_state_on(conn, COVEN_SCHEMA_VERSION_STATE_KEY)?;
146    match version {
147        None => pre_ledger_version(&stored, version_0_manifest, migrations)
148            .map(|version| {
149                if version == migrations.len() as u32 {
150                    CovenSchemaState::PendingLedgerInstallation { version }
151                } else {
152                    CovenSchemaState::Pending { current: version }
153                }
154            })
155            .ok_or(CovenMigrationError::UnknownUnversionedSchema),
156        Some(value) => {
157            let version = value
158                .parse::<u32>()
159                .map_err(|source| CovenMigrationError::InvalidVersion { value, source })?;
160            let latest = migrations.len() as u32;
161            if version > latest {
162                return Err(CovenMigrationError::UnsupportedVersion {
163                    found: version,
164                    latest,
165                });
166            }
167            let expected = if version == 0 {
168                version_0_manifest
169            } else {
170                migrations[(version - 1) as usize].expected_manifest
171            };
172            if stored != *expected {
173                return Err(CovenMigrationError::VersionManifestMismatch { version });
174            }
175            if version == latest {
176                Ok(CovenSchemaState::Current)
177            } else {
178                Ok(CovenSchemaState::Pending { current: version })
179            }
180        }
181    }
182}
183
184fn write_schema_metadata(
185    conn: &Connection,
186    version: u32,
187    manifest: &CovenSchemaManifest,
188) -> Result<(), CovenMigrationError> {
189    let manifest =
190        serde_json::to_string(manifest).map_err(CovenMigrationError::SerializeManifest)?;
191    set_protocol_state_on(conn, COVEN_SCHEMA_MANIFEST_STATE_KEY, &manifest)?;
192    set_protocol_state_on(conn, COVEN_SCHEMA_VERSION_STATE_KEY, &version.to_string())?;
193    Ok(())
194}
195
196fn require_empty(conn: &Connection, table: &'static str) -> Result<(), CovenMigrationError> {
197    let has_row: bool = conn.query_row(
198        &format!("SELECT EXISTS(SELECT 1 FROM {table})"),
199        [],
200        |row| row.get(0),
201    )?;
202    if has_row {
203        return Err(CovenMigrationError::NonEmptyTable {
204            table: table.to_string(),
205        });
206    }
207    Ok(())
208}
209
210fn add_root_label_to_transition_tables(conn: &Connection) -> Result<(), CovenMigrationError> {
211    require_empty(conn, "cloud_outbox")?;
212    require_empty(conn, "blob_make_remote_intents")?;
213    recreate_current_transition_tables(conn)?;
214    Ok(())
215}
216
217fn apply_migration_steps(
218    conn: &Connection,
219    current_version: u32,
220    migrations: &[CovenMigrationStep<'_>],
221) -> Result<(), CovenMigrationError> {
222    for (index, migration) in migrations.iter().enumerate().skip(current_version as usize) {
223        (migration.apply)(conn)?;
224        let version = (index + 1) as u32;
225        if live_coven_schema_manifest(conn)? != *migration.expected_manifest {
226            return Err(CovenMigrationError::MigrationResultMismatch { version });
227        }
228    }
229    Ok(())
230}
231
232fn apply_pending_migrations(
233    conn: &Connection,
234    current_version: u32,
235    migrations: &[CovenMigrationStep<'_>],
236) -> Result<(), CovenMigrationError> {
237    apply_migration_steps(conn, current_version, migrations)?;
238    let latest = migrations.len() as u32;
239    write_schema_metadata(
240        conn,
241        latest,
242        migrations[(latest - 1) as usize].expected_manifest,
243    )?;
244    Ok(())
245}
246
247fn require_absent_snapshot_metadata(
248    conn: &Connection,
249    key: &'static str,
250) -> Result<(), CovenMigrationError> {
251    if get_protocol_state_on(conn, key)?.is_some() {
252        Err(CovenMigrationError::UnexpectedSnapshotMetadata { key })
253    } else {
254        Ok(())
255    }
256}
257
258fn run_uninitialized_snapshot_migrations_with_ladder(
259    conn: &Connection,
260    policy: CovenMigrationPolicy,
261    version_0_manifest: &CovenSchemaManifest,
262    migrations: &[CovenMigrationStep<'_>],
263) -> Result<(), CovenMigrationError> {
264    require_absent_snapshot_metadata(conn, COVEN_INITIALIZED_STATE_KEY)?;
265    require_absent_snapshot_metadata(conn, COVEN_SCHEMA_MANIFEST_STATE_KEY)?;
266    require_absent_snapshot_metadata(conn, COVEN_SCHEMA_VERSION_STATE_KEY)?;
267
268    let live = live_coven_schema_manifest(conn)?;
269    let current = uninitialized_snapshot_version(&live, version_0_manifest, migrations)
270        .ok_or(CovenMigrationError::UnknownUnversionedSchema)?;
271    let latest = migrations.len() as u32;
272    if current == latest {
273        return Ok(());
274    }
275    match policy {
276        CovenMigrationPolicy::ApplyPending => apply_migration_steps(conn, current, migrations),
277        CovenMigrationPolicy::RefusePending => Err(CovenMigrationError::Pending {
278            current,
279            target: latest,
280        }),
281    }
282}
283
284fn migration_ladder(
285    include_routing: bool,
286) -> Result<[CovenMigrationStep<'static>; COVEN_MIGRATION_COUNT], CovenMigrationError> {
287    Ok([CovenMigrationStep {
288        expected_manifest: expected_coven_schema_manifest(include_routing)?,
289        apply: add_root_label_to_transition_tables,
290    }])
291}
292
293fn run_coven_migrations_with_ladder(
294    conn: &Connection,
295    policy: CovenMigrationPolicy,
296    version_0_manifest: &CovenSchemaManifest,
297    migrations: &[CovenMigrationStep<'_>],
298) -> Result<(), CovenMigrationError> {
299    match (
300        classify_schema(conn, version_0_manifest, migrations)?,
301        policy,
302    ) {
303        (CovenSchemaState::Current, _) => Ok(()),
304        (CovenSchemaState::Pending { current }, CovenMigrationPolicy::ApplyPending) => {
305            apply_pending_migrations(conn, current, migrations)
306        }
307        (CovenSchemaState::Pending { current }, CovenMigrationPolicy::RefusePending) => {
308            Err(CovenMigrationError::Pending {
309                current,
310                target: migrations.len() as u32,
311            })
312        }
313        (
314            CovenSchemaState::PendingLedgerInstallation { version },
315            CovenMigrationPolicy::ApplyPending,
316        ) => write_schema_metadata(
317            conn,
318            version,
319            migrations[(version - 1) as usize].expected_manifest,
320        ),
321        (
322            CovenSchemaState::PendingLedgerInstallation { version },
323            CovenMigrationPolicy::RefusePending,
324        ) => Err(CovenMigrationError::PendingLedgerInstallation { version }),
325    }
326}
327
328#[cfg(test)]
329pub(crate) fn run_coven_migrations_with_ladder_for_test(
330    conn: &Connection,
331    include_routing: bool,
332    policy: CovenMigrationPolicy,
333    migrations: &[CovenMigrationStep<'_>],
334) -> Result<(), CovenMigrationError> {
335    run_coven_migrations_with_ladder(
336        conn,
337        policy,
338        expected_coven_schema_v0_manifest(include_routing)?,
339        migrations,
340    )
341}
342
343#[cfg(test)]
344pub(crate) fn run_uninitialized_snapshot_migrations_with_ladder_for_test(
345    conn: &Connection,
346    include_routing: bool,
347    policy: CovenMigrationPolicy,
348    migrations: &[CovenMigrationStep<'_>],
349) -> Result<(), CovenMigrationError> {
350    run_uninitialized_snapshot_migrations_with_ladder(
351        conn,
352        policy,
353        expected_coven_schema_v0_manifest(include_routing)?,
354        migrations,
355    )
356}
357
358pub(crate) fn initialize_coven_schema_version(conn: &Connection) -> Result<(), DbError> {
359    set_protocol_state_on(
360        conn,
361        COVEN_SCHEMA_VERSION_STATE_KEY,
362        &LATEST_COVEN_SCHEMA_VERSION.to_string(),
363    )
364}
365
366pub(crate) fn run_coven_migrations_in_transaction(
367    conn: &Connection,
368    include_routing: bool,
369    policy: CovenMigrationPolicy,
370) -> Result<(), CovenMigrationError> {
371    let migrations = migration_ladder(include_routing)?;
372    run_coven_migrations_with_ladder(
373        conn,
374        policy,
375        expected_coven_schema_v0_manifest(include_routing)?,
376        &migrations,
377    )
378}
379
380pub(crate) fn run_initialized_coven_schema_migrations_in_transaction(
381    conn: &Connection,
382    include_routing: bool,
383    policy: CovenMigrationPolicy,
384) -> Result<(), CovenMigrationError> {
385    let migrations = migration_ladder(include_routing)?;
386    run_coven_migrations_with_ladder(
387        conn,
388        policy,
389        expected_coven_schema_v0_manifest(include_routing)?,
390        &migrations,
391    )
392}
393
394pub(crate) fn run_uninitialized_snapshot_coven_migrations_in_transaction(
395    conn: &Connection,
396    include_routing: bool,
397    policy: CovenMigrationPolicy,
398) -> Result<(), CovenMigrationError> {
399    let migrations = migration_ladder(include_routing)?;
400    run_uninitialized_snapshot_migrations_with_ladder(
401        conn,
402        policy,
403        expected_coven_schema_v0_manifest(include_routing)?,
404        &migrations,
405    )
406}
407
408#[cfg(any(test, feature = "test-utils"))]
409pub(crate) fn validate_uninitialized_coven_schema_v0_for_test(
410    conn: &Connection,
411    include_routing: bool,
412) -> Result<(), CovenMigrationError> {
413    require_absent_snapshot_metadata(conn, COVEN_INITIALIZED_STATE_KEY)?;
414    require_absent_snapshot_metadata(conn, COVEN_SCHEMA_MANIFEST_STATE_KEY)?;
415    require_absent_snapshot_metadata(conn, COVEN_SCHEMA_VERSION_STATE_KEY)?;
416    let live = live_coven_schema_manifest(conn)?;
417    if live != *expected_coven_schema_v0_manifest(include_routing)? {
418        return Err(CovenMigrationError::UnknownUnversionedSchema);
419    }
420    Ok(())
421}
422
423pub(crate) fn validate_coven_schema_for_reader(
424    conn: &Connection,
425    include_routing: bool,
426) -> Result<(), CovenMigrationError> {
427    let migrations = migration_ladder(include_routing)?;
428    match classify_schema(
429        conn,
430        expected_coven_schema_v0_manifest(include_routing)?,
431        &migrations,
432    )? {
433        CovenSchemaState::Current => Ok(()),
434        CovenSchemaState::Pending { current } => Err(CovenMigrationError::Pending {
435            current,
436            target: migrations.len() as u32,
437        }),
438        CovenSchemaState::PendingLedgerInstallation { version } => {
439            Err(CovenMigrationError::PendingLedgerInstallation { version })
440        }
441    }
442}