commit aba7ccd97cffb3017cf81b96203134df160da0d7
parent 47842f1339562a92b8fdf2a97ad05e27a008cf37
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 17:03:14 +0000
storage-sqlite: establish one runtime schema authority
- add one forward-only runtime.sqlite schema for every final durable storage authority
- keep embedded SQL private behind checksummed non-SQL migration descriptors
- preserve append-only source generations and immutable canonical event payloads
- verify fresh schema objects, integrity, foreign keys, and the governed plan snapshot
Diffstat:
6 files changed, 498 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -5139,7 +5139,12 @@ dependencies = [
"radroots_event_codec",
"radroots_secrets",
"radroots_storage",
+ "serde",
+ "sha2",
+ "sqlx",
"tempfile",
+ "tokio",
+ "toml",
]
[[package]]
diff --git a/contracts/storage/runtime_schema_v1.toml b/contracts/storage/runtime_schema_v1.toml
@@ -0,0 +1,61 @@
+schema_version = 1
+database = "runtime.sqlite"
+minimum_version = 1
+current_version = 1
+migration_name = "runtime_authority"
+migration_sha256 = "3b869122dd5bd58f4a15e7a71fd1377879640b36cd496d7b3f15278ef1e128c9"
+forward_only = true
+raw_sql_public = false
+
+authorities = [
+ "canonical_events",
+ "event_provenance",
+ "operation_journal",
+ "outbox",
+ "delivery_evidence",
+ "projection_coordination",
+ "event_index_metadata",
+ "atomic_commit_receipts",
+]
+
+source_invariants = [
+ "source_generation_identity_immutable",
+ "source_generation_history_append_only",
+ "canonical_event_identity_immutable",
+ "canonical_event_payload_immutable",
+ "canonical_event_history_append_only",
+]
+
+[[migrations]]
+version = 1
+name = "runtime_authority"
+sha256 = "3b869122dd5bd58f4a15e7a71fd1377879640b36cd496d7b3f15278ef1e128c9"
+
+owned_objects = [
+ "radroots_runtime_atomic_commits",
+ "radroots_runtime_delivery_evidence",
+ "radroots_runtime_delivery_evidence_item_idx",
+ "radroots_runtime_event_index_checkpoints",
+ "radroots_runtime_event_index_manifests",
+ "radroots_runtime_event_index_shards",
+ "radroots_runtime_event_provenance",
+ "radroots_runtime_event_provenance_observed_idx",
+ "radroots_runtime_events",
+ "radroots_runtime_events_admission_idx",
+ "radroots_runtime_events_delete_guard",
+ "radroots_runtime_events_event_id_idx",
+ "radroots_runtime_events_raw_update_guard",
+ "radroots_runtime_journal_idempotency_idx",
+ "radroots_runtime_journal_operations",
+ "radroots_runtime_journal_recovery_idx",
+ "radroots_runtime_outbox_items",
+ "radroots_runtime_outbox_ready_idx",
+ "radroots_runtime_outbox_targets",
+ "radroots_runtime_projection_checkpoints",
+ "radroots_runtime_projection_invalidations",
+ "radroots_runtime_projection_rebuilds",
+ "radroots_runtime_projection_rebuilds_stage_idx",
+ "radroots_runtime_source_generations",
+ "radroots_runtime_source_generations_delete_guard",
+ "radroots_runtime_source_generations_identity_guard",
+]
diff --git a/crates/storage_sqlite/Cargo.toml b/crates/storage_sqlite/Cargo.toml
@@ -20,7 +20,12 @@ radroots_secrets = { workspace = true, default-features = false }
radroots_storage = { workspace = true, default-features = false }
[dev-dependencies]
+serde = { workspace = true, features = ["derive", "std"] }
+sha2 = { workspace = true, features = ["std"] }
+sqlx = { workspace = true, features = ["runtime-tokio", "sqlite-bundled"] }
tempfile = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt"] }
+toml = { workspace = true }
[lints]
workspace = true
diff --git a/crates/storage_sqlite/src/migration.rs b/crates/storage_sqlite/src/migration.rs
@@ -1 +1,4 @@
//! Governed SQLite schema migration boundary.
+
+/// Versioned schema authority for `runtime.sqlite`.
+pub mod runtime;
diff --git a/crates/storage_sqlite/src/migration/runtime/0001_runtime.up.sql b/crates/storage_sqlite/src/migration/runtime/0001_runtime.up.sql
@@ -0,0 +1,232 @@
+CREATE TABLE radroots_runtime_source_generations (
+ generation BLOB PRIMARY KEY NOT NULL CHECK (length(generation) = 32),
+ sequence_head INTEGER NOT NULL DEFAULT 0 CHECK (sequence_head >= 0),
+ state TEXT NOT NULL CHECK (state IN ('active', 'retired')),
+ created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0),
+ retired_at_unix_ms INTEGER,
+ CHECK (
+ (state = 'active' AND retired_at_unix_ms IS NULL)
+ OR (state = 'retired' AND retired_at_unix_ms >= created_at_unix_ms)
+ )
+) STRICT, WITHOUT ROWID;
+
+CREATE TRIGGER radroots_runtime_source_generations_delete_guard
+BEFORE DELETE ON radroots_runtime_source_generations
+BEGIN
+ SELECT RAISE(ABORT, 'runtime source generations are append-only');
+END;
+
+CREATE TRIGGER radroots_runtime_source_generations_identity_guard
+BEFORE UPDATE OF generation, created_at_unix_ms ON radroots_runtime_source_generations
+BEGIN
+ SELECT RAISE(ABORT, 'runtime source generation identity is immutable');
+END;
+
+CREATE TABLE radroots_runtime_events (
+ source_generation BLOB NOT NULL
+ REFERENCES radroots_runtime_source_generations(generation),
+ source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
+ event_id BLOB NOT NULL CHECK (length(event_id) = 32),
+ admission_stage TEXT NOT NULL CHECK (admission_stage IN ('raw', 'verified', 'visible')),
+ signed_event BLOB NOT NULL CHECK (length(signed_event) > 0),
+ admitted_at_unix_ms INTEGER NOT NULL CHECK (admitted_at_unix_ms > 0),
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms >= admitted_at_unix_ms),
+ PRIMARY KEY (source_generation, source_sequence),
+ UNIQUE (event_id)
+) STRICT, WITHOUT ROWID;
+
+CREATE UNIQUE INDEX radroots_runtime_events_event_id_idx
+ON radroots_runtime_events(event_id);
+
+CREATE INDEX radroots_runtime_events_admission_idx
+ON radroots_runtime_events(admission_stage, source_generation, source_sequence);
+
+CREATE TRIGGER radroots_runtime_events_delete_guard
+BEFORE DELETE ON radroots_runtime_events
+BEGIN
+ SELECT RAISE(ABORT, 'canonical runtime events are append-only');
+END;
+
+CREATE TRIGGER radroots_runtime_events_raw_update_guard
+BEFORE UPDATE OF source_generation, source_sequence, event_id, signed_event, admitted_at_unix_ms
+ON radroots_runtime_events
+BEGIN
+ SELECT RAISE(ABORT, 'canonical runtime event authority is immutable');
+END;
+
+CREATE TABLE radroots_runtime_event_provenance (
+ event_id BLOB NOT NULL REFERENCES radroots_runtime_events(event_id),
+ transport_kind TEXT NOT NULL CHECK (length(transport_kind) > 0),
+ endpoint_fingerprint BLOB NOT NULL CHECK (length(endpoint_fingerprint) > 0),
+ observation_kind TEXT NOT NULL CHECK (length(observation_kind) > 0),
+ first_observed_at_unix_ms INTEGER NOT NULL CHECK (first_observed_at_unix_ms > 0),
+ last_observed_at_unix_ms INTEGER NOT NULL
+ CHECK (last_observed_at_unix_ms >= first_observed_at_unix_ms),
+ observation_count INTEGER NOT NULL CHECK (observation_count > 0),
+ PRIMARY KEY (event_id, transport_kind, endpoint_fingerprint, observation_kind)
+) STRICT, WITHOUT ROWID;
+
+CREATE INDEX radroots_runtime_event_provenance_observed_idx
+ON radroots_runtime_event_provenance(last_observed_at_unix_ms, event_id);
+
+CREATE TABLE radroots_runtime_journal_operations (
+ instance_id BLOB PRIMARY KEY NOT NULL CHECK (length(instance_id) = 16),
+ operation_id BLOB NOT NULL CHECK (length(operation_id) > 0),
+ idempotency_key TEXT NOT NULL CHECK (length(idempotency_key) BETWEEN 1 AND 256),
+ input_digest BLOB NOT NULL CHECK (length(input_digest) = 32),
+ prepared_at_unix_ms INTEGER NOT NULL CHECK (prepared_at_unix_ms > 0),
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ stage TEXT NOT NULL CHECK (stage IN ('prepared', 'signed', 'recoverable', 'committed')),
+ event_id BLOB CHECK (event_id IS NULL OR length(event_id) = 32),
+ recovery_record BLOB,
+ cancellation_state TEXT NOT NULL
+ CHECK (cancellation_state IN ('not_requested', 'cancelled_before_commit', 'observed_after_commit')),
+ committed_at_unix_ms INTEGER,
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms >= prepared_at_unix_ms),
+ CHECK ((stage IN ('signed', 'committed') AND event_id IS NOT NULL) OR stage IN ('prepared', 'recoverable')),
+ CHECK ((stage = 'recoverable' AND recovery_record IS NOT NULL) OR (stage <> 'recoverable' AND recovery_record IS NULL)),
+ CHECK ((stage = 'committed' AND committed_at_unix_ms >= prepared_at_unix_ms) OR (stage <> 'committed' AND committed_at_unix_ms IS NULL))
+) STRICT, WITHOUT ROWID;
+
+CREATE UNIQUE INDEX radroots_runtime_journal_idempotency_idx
+ON radroots_runtime_journal_operations(operation_id, idempotency_key);
+
+CREATE INDEX radroots_runtime_journal_recovery_idx
+ON radroots_runtime_journal_operations(stage, updated_at_unix_ms, instance_id)
+WHERE stage = 'recoverable';
+
+CREATE TABLE radroots_runtime_outbox_items (
+ item_id BLOB PRIMARY KEY NOT NULL CHECK (length(item_id) = 16),
+ operation_instance_id BLOB NOT NULL
+ REFERENCES radroots_runtime_journal_operations(instance_id),
+ plan_digest BLOB NOT NULL CHECK (length(plan_digest) = 32),
+ delivery_request BLOB NOT NULL CHECK (length(delivery_request) > 0),
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ stage TEXT NOT NULL CHECK (stage IN ('pending', 'leased', 'retryable', 'satisfied', 'exhausted')),
+ lease_id BLOB CHECK (lease_id IS NULL OR length(lease_id) = 16),
+ lease_owner TEXT CHECK (lease_owner IS NULL OR length(lease_owner) BETWEEN 1 AND 128),
+ lease_acquired_at_unix_ms INTEGER,
+ lease_expires_at_unix_ms INTEGER,
+ last_attempt INTEGER CHECK (last_attempt IS NULL OR last_attempt > 0),
+ satisfaction TEXT NOT NULL CHECK (satisfaction IN ('pending', 'satisfied', 'exhausted')),
+ retry_not_before_unix_ms INTEGER,
+ created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0),
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms >= created_at_unix_ms),
+ UNIQUE (operation_instance_id, plan_digest),
+ CHECK (
+ (stage = 'leased' AND lease_id IS NOT NULL AND lease_owner IS NOT NULL
+ AND lease_acquired_at_unix_ms > 0 AND lease_expires_at_unix_ms > lease_acquired_at_unix_ms)
+ OR (stage <> 'leased' AND lease_id IS NULL AND lease_owner IS NULL
+ AND lease_acquired_at_unix_ms IS NULL AND lease_expires_at_unix_ms IS NULL)
+ )
+) STRICT, WITHOUT ROWID;
+
+CREATE INDEX radroots_runtime_outbox_ready_idx
+ON radroots_runtime_outbox_items(stage, retry_not_before_unix_ms, created_at_unix_ms, item_id);
+
+CREATE TABLE radroots_runtime_outbox_targets (
+ item_id BLOB NOT NULL REFERENCES radroots_runtime_outbox_items(item_id) ON DELETE CASCADE,
+ target_fingerprint BLOB NOT NULL CHECK (length(target_fingerprint) > 0),
+ target_request BLOB NOT NULL CHECK (length(target_request) > 0),
+ ordinal INTEGER NOT NULL CHECK (ordinal >= 0),
+ PRIMARY KEY (item_id, target_fingerprint),
+ UNIQUE (item_id, ordinal)
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_delivery_evidence (
+ item_id BLOB NOT NULL,
+ target_fingerprint BLOB NOT NULL,
+ attempt INTEGER NOT NULL CHECK (attempt > 0),
+ attempted INTEGER NOT NULL CHECK (attempted IN (0, 1)),
+ outcome BLOB NOT NULL CHECK (length(outcome) > 0),
+ retryability TEXT NOT NULL CHECK (retryability IN ('retryable', 'terminal', 'not_applicable')),
+ recorded_at_unix_ms INTEGER NOT NULL CHECK (recorded_at_unix_ms > 0),
+ PRIMARY KEY (item_id, target_fingerprint, attempt),
+ FOREIGN KEY (item_id, target_fingerprint)
+ REFERENCES radroots_runtime_outbox_targets(item_id, target_fingerprint) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID;
+
+CREATE INDEX radroots_runtime_delivery_evidence_item_idx
+ON radroots_runtime_delivery_evidence(item_id, attempt, target_fingerprint);
+
+CREATE TABLE radroots_runtime_projection_checkpoints (
+ projection_id TEXT NOT NULL CHECK (length(projection_id) BETWEEN 1 AND 128),
+ projection_generation BLOB NOT NULL CHECK (length(projection_generation) = 32),
+ source_generation BLOB,
+ source_sequence INTEGER,
+ projected_rows INTEGER NOT NULL CHECK (projected_rows >= 0),
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms > 0),
+ PRIMARY KEY (projection_id, projection_generation),
+ FOREIGN KEY (source_generation) REFERENCES radroots_runtime_source_generations(generation),
+ CHECK ((source_generation IS NULL AND source_sequence IS NULL) OR (length(source_generation) = 32 AND source_sequence > 0))
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_projection_invalidations (
+ projection_id TEXT NOT NULL CHECK (length(projection_id) BETWEEN 1 AND 128),
+ invalid_generation BLOB NOT NULL CHECK (length(invalid_generation) = 32),
+ replacement_generation BLOB NOT NULL CHECK (length(replacement_generation) = 32),
+ reason TEXT NOT NULL CHECK (reason IN ('source_generation_changed', 'projection_generation_changed', 'event_index_manifest_changed', 'integrity_failure', 'operator_requested')),
+ invalidated_at_unix_ms INTEGER NOT NULL CHECK (invalidated_at_unix_ms > 0),
+ PRIMARY KEY (projection_id, invalid_generation),
+ CHECK (invalid_generation <> replacement_generation)
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_projection_rebuilds (
+ ticket_id BLOB PRIMARY KEY NOT NULL CHECK (length(ticket_id) = 16),
+ projection_id TEXT NOT NULL,
+ invalid_generation BLOB NOT NULL,
+ replacement_generation BLOB NOT NULL,
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ stage TEXT NOT NULL CHECK (stage IN ('requested', 'running', 'completed', 'failed')),
+ requested_at_unix_ms INTEGER NOT NULL CHECK (requested_at_unix_ms > 0),
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms >= requested_at_unix_ms),
+ FOREIGN KEY (projection_id, invalid_generation)
+ REFERENCES radroots_runtime_projection_invalidations(projection_id, invalid_generation)
+) STRICT, WITHOUT ROWID;
+
+CREATE INDEX radroots_runtime_projection_rebuilds_stage_idx
+ON radroots_runtime_projection_rebuilds(stage, updated_at_unix_ms, ticket_id);
+
+CREATE TABLE radroots_runtime_event_index_manifests (
+ projection_id TEXT NOT NULL CHECK (length(projection_id) BETWEEN 1 AND 128),
+ projection_generation BLOB NOT NULL CHECK (length(projection_generation) = 32),
+ manifest_digest BLOB NOT NULL CHECK (length(manifest_digest) = 32),
+ source_generation BLOB NOT NULL
+ REFERENCES radroots_runtime_source_generations(generation),
+ created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0),
+ PRIMARY KEY (projection_id, projection_generation),
+ UNIQUE (manifest_digest)
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_event_index_shards (
+ manifest_digest BLOB NOT NULL
+ REFERENCES radroots_runtime_event_index_manifests(manifest_digest) ON DELETE CASCADE,
+ shard_id TEXT NOT NULL CHECK (length(shard_id) BETWEEN 1 AND 128),
+ ordinal INTEGER NOT NULL CHECK (ordinal >= 0),
+ artifact_path TEXT NOT NULL CHECK (length(artifact_path) BETWEEN 1 AND 512),
+ artifact_digest BLOB NOT NULL CHECK (length(artifact_digest) = 32),
+ cursor BLOB NOT NULL CHECK (length(cursor) BETWEEN 1 AND 2048),
+ PRIMARY KEY (manifest_digest, shard_id),
+ UNIQUE (manifest_digest, ordinal),
+ UNIQUE (manifest_digest, artifact_path)
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_event_index_checkpoints (
+ manifest_digest BLOB NOT NULL,
+ shard_id TEXT NOT NULL,
+ indexed_through_event_id BLOB CHECK (indexed_through_event_id IS NULL OR length(indexed_through_event_id) = 32),
+ indexed_events INTEGER NOT NULL CHECK (indexed_events >= 0),
+ updated_at_unix_ms INTEGER NOT NULL CHECK (updated_at_unix_ms > 0),
+ PRIMARY KEY (manifest_digest, shard_id),
+ FOREIGN KEY (manifest_digest, shard_id)
+ REFERENCES radroots_runtime_event_index_shards(manifest_digest, shard_id) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID;
+
+CREATE TABLE radroots_runtime_atomic_commits (
+ commit_id BLOB PRIMARY KEY NOT NULL CHECK (length(commit_id) = 16),
+ commit_digest BLOB NOT NULL CHECK (length(commit_digest) = 32),
+ workflow_kind TEXT NOT NULL CHECK (workflow_kind IN ('prepared', 'signed', 'enqueued', 'delivered', 'ingested')),
+ requested_at_unix_ms INTEGER NOT NULL CHECK (requested_at_unix_ms > 0),
+ committed_at_unix_ms INTEGER NOT NULL CHECK (committed_at_unix_ms >= requested_at_unix_ms),
+ receipt BLOB NOT NULL CHECK (length(receipt) > 0)
+) STRICT, WITHOUT ROWID;
diff --git a/crates/storage_sqlite/src/migration/runtime/mod.rs b/crates/storage_sqlite/src/migration/runtime/mod.rs
@@ -0,0 +1,192 @@
+//! Versioned schema authority for `runtime.sqlite`.
+//!
+//! The public descriptor surface exposes version and integrity metadata only.
+//! Embedded SQL remains an implementation detail of this backend.
+
+/// Lowest runtime schema version this package can recognize.
+pub const MINIMUM_VERSION: u32 = 1;
+/// Current runtime schema version created by this package.
+pub const CURRENT_VERSION: u32 = 1;
+
+#[allow(dead_code)] // Consumed by the migration executor introduced in its ordered RCL step.
+const RUNTIME_V1_SQL: &str = include_str!("0001_runtime.up.sql");
+
+/// Stable, non-SQL description of one forward runtime migration.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct MigrationDescriptor {
+ version: u32,
+ name: &'static str,
+ up_sha256: &'static str,
+ owned_objects: &'static [&'static str],
+}
+
+impl MigrationDescriptor {
+ /// Returns the positive, monotonically increasing migration version.
+ pub const fn version(self) -> u32 {
+ self.version
+ }
+
+ /// Returns the stable migration name.
+ pub const fn name(self) -> &'static str {
+ self.name
+ }
+
+ /// Returns the lowercase SHA-256 of the exact embedded migration bytes.
+ pub const fn up_sha256(self) -> &'static str {
+ self.up_sha256
+ }
+
+ /// Returns the complete, sorted SQLite catalog owned after this migration.
+ pub const fn owned_objects(self) -> &'static [&'static str] {
+ self.owned_objects
+ }
+}
+
+const RUNTIME_V1_OBJECTS: &[&str] = &[
+ "radroots_runtime_atomic_commits",
+ "radroots_runtime_delivery_evidence",
+ "radroots_runtime_delivery_evidence_item_idx",
+ "radroots_runtime_event_index_checkpoints",
+ "radroots_runtime_event_index_manifests",
+ "radroots_runtime_event_index_shards",
+ "radroots_runtime_event_provenance",
+ "radroots_runtime_event_provenance_observed_idx",
+ "radroots_runtime_events",
+ "radroots_runtime_events_admission_idx",
+ "radroots_runtime_events_delete_guard",
+ "radroots_runtime_events_event_id_idx",
+ "radroots_runtime_events_raw_update_guard",
+ "radroots_runtime_journal_idempotency_idx",
+ "radroots_runtime_journal_operations",
+ "radroots_runtime_journal_recovery_idx",
+ "radroots_runtime_outbox_items",
+ "radroots_runtime_outbox_ready_idx",
+ "radroots_runtime_outbox_targets",
+ "radroots_runtime_projection_checkpoints",
+ "radroots_runtime_projection_invalidations",
+ "radroots_runtime_projection_rebuilds",
+ "radroots_runtime_projection_rebuilds_stage_idx",
+ "radroots_runtime_source_generations",
+ "radroots_runtime_source_generations_delete_guard",
+ "radroots_runtime_source_generations_identity_guard",
+];
+
+/// Ordered, immutable runtime migration plan.
+pub const MIGRATIONS: &[MigrationDescriptor] = &[MigrationDescriptor {
+ version: 1,
+ name: "runtime_authority",
+ up_sha256: "3b869122dd5bd58f4a15e7a71fd1377879640b36cd496d7b3f15278ef1e128c9",
+ owned_objects: RUNTIME_V1_OBJECTS,
+}];
+
+#[allow(dead_code)] // Keeps raw SQL crate-private until the migration executor is installed.
+pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> {
+ match version {
+ 1 => Some(RUNTIME_V1_SQL),
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{CURRENT_VERSION, MIGRATIONS, MINIMUM_VERSION, migration_sql};
+ use serde::Deserialize;
+ use sha2::{Digest, Sha256};
+ use sqlx::{Connection, Row, SqliteConnection};
+
+ const PLAN_SNAPSHOT: &str =
+ include_str!("../../../../../contracts/storage/runtime_schema_v1.toml");
+
+ #[derive(Debug, Deserialize)]
+ struct PlanSnapshot {
+ schema_version: u32,
+ database: String,
+ minimum_version: u32,
+ current_version: u32,
+ migration_name: String,
+ migration_sha256: String,
+ forward_only: bool,
+ raw_sql_public: bool,
+ authorities: Vec<String>,
+ source_invariants: Vec<String>,
+ migrations: Vec<MigrationSnapshot>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ struct MigrationSnapshot {
+ version: u32,
+ name: String,
+ sha256: String,
+ owned_objects: Vec<String>,
+ }
+
+ #[test]
+ fn migration_plan_matches_governed_snapshot() {
+ let snapshot = toml::from_str::<PlanSnapshot>(PLAN_SNAPSHOT).expect("valid snapshot");
+ assert_eq!(MINIMUM_VERSION, 1);
+ assert_eq!(CURRENT_VERSION, 1);
+ assert_eq!(MIGRATIONS.len(), 1);
+ let migration = MIGRATIONS[0];
+ assert_eq!(snapshot.schema_version, 1);
+ assert_eq!(snapshot.database, "runtime.sqlite");
+ assert_eq!(snapshot.minimum_version, MINIMUM_VERSION);
+ assert_eq!(snapshot.current_version, CURRENT_VERSION);
+ assert_eq!(snapshot.migration_name, migration.name());
+ assert_eq!(snapshot.migration_sha256, migration.up_sha256());
+ assert!(snapshot.forward_only);
+ assert!(!snapshot.raw_sql_public);
+ assert_eq!(snapshot.authorities.len(), 8);
+ assert_eq!(snapshot.source_invariants.len(), 5);
+ assert_eq!(snapshot.migrations.len(), 1);
+ assert_eq!(snapshot.migrations[0].version, migration.version());
+ assert_eq!(snapshot.migrations[0].name, migration.name());
+ assert_eq!(snapshot.migrations[0].sha256, migration.up_sha256());
+ assert_eq!(
+ snapshot.migrations[0].owned_objects,
+ migration.owned_objects()
+ );
+ }
+
+ #[test]
+ fn embedded_migration_checksum_is_pinned() {
+ let actual = format!("{:x}", Sha256::digest(migration_sql(1).expect("v1 SQL")));
+ assert_eq!(actual, MIGRATIONS[0].up_sha256());
+ assert_eq!(migration_sql(2), None);
+ }
+
+ #[tokio::test]
+ async fn fresh_database_has_exact_owned_schema() {
+ let mut connection = SqliteConnection::connect("sqlite::memory:")
+ .await
+ .expect("open memory SQLite");
+ sqlx::raw_sql(migration_sql(1).expect("v1 SQL"))
+ .execute(&mut connection)
+ .await
+ .expect("apply runtime schema");
+
+ let rows = sqlx::query(
+ "SELECT name FROM sqlite_schema \
+ WHERE name LIKE 'radroots_runtime_%' \
+ ORDER BY name",
+ )
+ .fetch_all(&mut connection)
+ .await
+ .expect("inspect runtime schema");
+ let actual = rows
+ .iter()
+ .map(|row| row.get::<String, _>("name"))
+ .collect::<Vec<_>>();
+ assert_eq!(actual, MIGRATIONS[0].owned_objects());
+
+ let foreign_key_violations = sqlx::query("PRAGMA foreign_key_check")
+ .fetch_all(&mut connection)
+ .await
+ .expect("inspect foreign keys");
+ assert!(foreign_key_violations.is_empty());
+ let integrity = sqlx::query_scalar::<_, String>("PRAGMA integrity_check")
+ .fetch_one(&mut connection)
+ .await
+ .expect("inspect integrity");
+ assert_eq!(integrity, "ok");
+ }
+}