1use 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}