lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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:
MCargo.lock | 5+++++
Acontracts/storage/runtime_schema_v1.toml | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/storage_sqlite/Cargo.toml | 5+++++
Mcrates/storage_sqlite/src/migration.rs | 3+++
Acrates/storage_sqlite/src/migration/runtime/0001_runtime.up.sql | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/storage_sqlite/src/migration/runtime/mod.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"); + } +}