lib

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

commit c8af311bc58580e67b8202263dceb2e481df373d
parent 8c510f91446c28ddcdf82e44563b9beb685f3160
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 05:57:17 +0000

storage-sqlite: seal validated legacy imports

- add governed runtime v9 and private v3 commit markers
- finalize private-first with atomic runtime completion
- recover private-only commits and lost success responses
- retain immutable staging without source deletion or dual writes

Diffstat:
Acontracts/storage/legacy_import_finalize_policy_v1.toml | 15+++++++++++++++
Mcontracts/storage/private_schema_v1.toml | 29++++++++++++++++++++++++++---
Mcontracts/storage/runtime_schema_v1.toml | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/storage_sqlite/README.md | 7+++++++
Mcrates/storage_sqlite/src/legacy.rs | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/storage_sqlite/src/migration.rs | 9+++++----
Acrates/storage_sqlite/src/migration/private/0003_legacy_import_commits.up.sql | 18++++++++++++++++++
Mcrates/storage_sqlite/src/migration/private/mod.rs | 36+++++++++++++++++++++++++++++++-----
Acrates/storage_sqlite/src/migration/runtime/0009_legacy_import_commits.up.sql | 19+++++++++++++++++++
Mcrates/storage_sqlite/src/migration/runtime/mod.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
10 files changed, 549 insertions(+), 21 deletions(-)

diff --git a/contracts/storage/legacy_import_finalize_policy_v1.toml b/contracts/storage/legacy_import_finalize_policy_v1.toml @@ -0,0 +1,15 @@ +schema_version = 1 +input = "exact_legacy_import_validation" +commit_order = ["private_commit_marker", "runtime_atomic_completion"] +private_replay = "insert_or_ignore_then_exact_verify" +runtime_replay = "completed_receipt_exact_verify" +crash_before_private_commit = "journal_ready_no_private_marker" +crash_after_private_commit = "journal_ready_exact_private_marker_replay" +crash_during_runtime_completion = "runtime_transaction_rolls_back" +lost_success_response = "exact_completed_receipt_reconstructed" +retained_representation = "immutable_owned_legacy_staging" +live_product_dual_write = false +source_deletion = false +studio_row_import = false +host_timestamp = "positive_monotonic_completion_time" +hidden_clock_or_entropy = false diff --git a/contracts/storage/private_schema_v1.toml b/contracts/storage/private_schema_v1.toml @@ -2,9 +2,9 @@ schema_version = 1 database = "private.sqlite" application_id = 1380208722 # ASCII "RDPR" minimum_version = 1 -current_version = 2 -migration_name = "legacy_private_staging" -migration_sha256 = "299ec0c476b2f5ab995f245d36603969af345827c9ecdf9490cfe0b0dbe4b9f9" +current_version = 3 +migration_name = "legacy_import_commits" +migration_sha256 = "9377f0af8f070d977a5237e2a1294e6977f5b704e7a8434d97a3dc5f4ae75e86" forward_only = true raw_sql_public = false encrypted_envelopes = true @@ -15,6 +15,7 @@ authorities = [ "private_trade_artifacts", "permitted_nip46_sessions", "legacy_private_import_staging", + "legacy_import_commits", ] forbidden_tables = [ @@ -54,3 +55,25 @@ owned_objects = [ "radroots_private_legacy_import_staging_parent_idx", "radroots_private_legacy_import_staging_update_guard", ] + +[[migrations]] +version = 3 +name = "legacy_import_commits" +sha256 = "9377f0af8f070d977a5237e2a1294e6977f5b704e7a8434d97a3dc5f4ae75e86" +owned_objects = [ + "radroots_private_artifacts", + "radroots_private_artifacts_delete_guard", + "radroots_private_artifacts_envelope_guard", + "radroots_private_artifacts_expiry_idx", + "radroots_private_artifacts_identity_guard", + "radroots_private_artifacts_key_version_idx", + "radroots_private_artifacts_kind_idx", + "radroots_private_legacy_import_commit_delete_guard", + "radroots_private_legacy_import_commit_update_guard", + "radroots_private_legacy_import_commits", + "radroots_private_legacy_import_staging", + "radroots_private_legacy_import_staging_delete_guard", + "radroots_private_legacy_import_staging_insert_guard", + "radroots_private_legacy_import_staging_parent_idx", + "radroots_private_legacy_import_staging_update_guard", +] diff --git a/contracts/storage/runtime_schema_v1.toml b/contracts/storage/runtime_schema_v1.toml @@ -2,9 +2,9 @@ schema_version = 1 database = "runtime.sqlite" application_id = 1380209236 # ASCII "RDRT" minimum_version = 1 -current_version = 8 -migration_name = "legacy_outbox_staging" -migration_sha256 = "b2ad0ee5bf7ac9e56584623c641e5208f5446dd0ff5ced9f235cd1756781be34" +current_version = 9 +migration_name = "legacy_import_commits" +migration_sha256 = "f0807eecd652a26844c3502d81386a9d54480cb178abe1b71035e0601916afb7" forward_only = true raw_sql_public = false @@ -20,6 +20,7 @@ authorities = [ "legacy_import_recovery", "legacy_event_staging", "legacy_outbox_staging", + "legacy_import_commits", ] source_invariants = [ @@ -360,3 +361,61 @@ owned_objects = [ "radroots_runtime_source_generations_identity_guard", "radroots_runtime_source_generations_sequence_guard", ] + +[[migrations]] +version = 9 +name = "legacy_import_commits" +sha256 = "f0807eecd652a26844c3502d81386a9d54480cb178abe1b71035e0601916afb7" + +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_legacy_event_staging", + "radroots_runtime_legacy_event_staging_delete_guard", + "radroots_runtime_legacy_event_staging_insert_guard", + "radroots_runtime_legacy_event_staging_update_guard", + "radroots_runtime_legacy_import_commit_delete_guard", + "radroots_runtime_legacy_import_commit_update_guard", + "radroots_runtime_legacy_import_commits", + "radroots_runtime_legacy_import_delete_guard", + "radroots_runtime_legacy_import_identity_guard", + "radroots_runtime_legacy_import_member_delete_guard", + "radroots_runtime_legacy_import_member_identity_guard", + "radroots_runtime_legacy_import_member_state_guard", + "radroots_runtime_legacy_import_members", + "radroots_runtime_legacy_import_state_guard", + "radroots_runtime_legacy_import_state_idx", + "radroots_runtime_legacy_imports", + "radroots_runtime_legacy_outbox_staging", + "radroots_runtime_legacy_outbox_staging_delete_guard", + "radroots_runtime_legacy_outbox_staging_insert_guard", + "radroots_runtime_legacy_outbox_staging_parent_idx", + "radroots_runtime_legacy_outbox_staging_update_guard", + "radroots_runtime_outbox_items", + "radroots_runtime_outbox_operation_idx", + "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_active_idx", + "radroots_runtime_source_generations_delete_guard", + "radroots_runtime_source_generations_identity_guard", + "radroots_runtime_source_generations_sequence_guard", +] diff --git a/crates/storage_sqlite/README.md b/crates/storage_sqlite/README.md @@ -92,6 +92,13 @@ count, and hashes framed member cursors plus every runtime/private staging row under write-stable snapshots. The returned digest is the deterministic commit identity; validation itself mutates nothing. +Finalization seals that identity private-first, then completes the runtime +journal and all members in one transaction. A crash after the private marker +replays and verifies it while the runtime journal remains ready; a lost success +response reconstructs the exact receipt from both commit markers. Immutable +legacy staging remains retained as owned migration evidence, while live product +tables are not dual-written and predecessor evidence is not deleted. + ```rust,no_run use radroots_storage::event::SourceGeneration; use radroots_storage_sqlite::{OpenMode, OpenOptions, Paths, SqliteStorage}; diff --git a/crates/storage_sqlite/src/legacy.rs b/crates/storage_sqlite/src/legacy.rs @@ -525,6 +525,31 @@ pub struct LegacyImportValidation { validation_sha256: MemberDigest, } +/// Durable receipt for one fully sealed, forward-only legacy import. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LegacyImportCommitReceipt { + validation_sha256: MemberDigest, + imported_row_count: u64, + completed_at_unix_ms: u64, +} + +impl LegacyImportCommitReceipt { + /// Returns the exact validation identity sealed by both databases. + pub const fn validation_sha256(&self) -> MemberDigest { + self.validation_sha256 + } + + /// Returns the exact retained SDK-owned predecessor row count. + pub const fn imported_row_count(&self) -> u64 { + self.imported_row_count + } + + /// Returns the positive host-supplied completion timestamp. + pub const fn completed_at_unix_ms(&self) -> u64 { + self.completed_at_unix_ms + } +} + impl LegacyImportValidation { /// Returns the exact number of predecessor rows staged for SDK-owned storage. pub const fn imported_row_count(&self) -> u64 { @@ -2140,6 +2165,151 @@ impl SqliteStorage { }) } + /// Seals validated legacy staging through a private-first recovery protocol. + pub async fn finalize_legacy_import( + &self, + classified: &ClassifiedLegacyImport, + expected: LegacyImportValidation, + completed_at_unix_ms: u64, + ) -> Result<LegacyImportCommitReceipt, Error> { + self.require_legacy_import_writer(classified.target_generation())?; + if completed_at_unix_ms == 0 { + return Err(Error::InvalidLegacyImportStageRequest); + } + let classification_sha256 = classification_digest(classified); + let journal = self + .legacy_import_journal(classified.import_id()) + .await? + .ok_or(Error::InvalidLegacyImportJournal)?; + if !journal_matches_classified(&journal, classified, classification_sha256) { + return Err(Error::LegacyImportConflict); + } + if journal.state() == LegacyImportState::Complete { + return self + .completed_legacy_import_receipt(classified.import_id(), expected) + .await; + } + if journal.state() != LegacyImportState::Ready + || completed_at_unix_ms < journal.updated_at_unix_ms() + { + return Err(Error::LegacyImportConflict); + } + let actual = self.validate_legacy_import(classified).await?; + if actual != expected { + return Err(Error::LegacyImportConflict); + } + let completed_at = i64::try_from(completed_at_unix_ms) + .map_err(|_| Error::InvalidLegacyImportStageRequest)?; + let imported_row_count = i64::try_from(expected.imported_row_count()) + .map_err(|_| Error::LegacyImportStagingFailed)?; + + let mut private_tx = self + .private_pool + .begin_with("BEGIN IMMEDIATE") + .await + .map_err(|_| Error::LegacyImportStagingFailed)?; + sqlx::query("INSERT OR IGNORE INTO radroots_private_legacy_import_commits(import_id, validation_sha256, imported_row_count, committed_at_ms) VALUES (?, ?, ?, ?)") + .bind(classified.import_id().as_bytes().as_slice()).bind(expected.validation_sha256().as_bytes().as_slice()).bind(imported_row_count).bind(completed_at).execute(&mut *private_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + let private_record = sqlx::query("SELECT validation_sha256, imported_row_count, committed_at_ms FROM radroots_private_legacy_import_commits WHERE import_id = ?") + .bind(classified.import_id().as_bytes().as_slice()).fetch_one(&mut *private_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + let private_committed_at = private_record + .try_get::<i64, _>("committed_at_ms") + .map_err(|_| Error::LegacyImportStagingFailed)?; + if decode_digest( + private_record + .try_get("validation_sha256") + .map_err(|_| Error::LegacyImportStagingFailed)?, + )? != expected.validation_sha256() + || private_record + .try_get::<i64, _>("imported_row_count") + .map_err(|_| Error::LegacyImportStagingFailed)? + != imported_row_count + || private_committed_at + < i64::try_from(journal.updated_at_unix_ms()) + .map_err(|_| Error::LegacyImportStagingFailed)? + { + return Err(Error::LegacyImportConflict); + } + private_tx + .commit() + .await + .map_err(|_| Error::LegacyImportStagingFailed)?; + let completed_at = private_committed_at; + let completed_at_unix_ms = + u64::try_from(completed_at).map_err(|_| Error::LegacyImportStagingFailed)?; + + let mut runtime_tx = self + .pool + .begin_with("BEGIN IMMEDIATE") + .await + .map_err(|_| Error::LegacyImportStagingFailed)?; + sqlx::query("INSERT INTO radroots_runtime_legacy_import_commits(import_id, validation_sha256, imported_row_count, completed_at_ms) VALUES (?, ?, ?, ?)") + .bind(classified.import_id().as_bytes().as_slice()).bind(expected.validation_sha256().as_bytes().as_slice()).bind(imported_row_count).bind(completed_at).execute(&mut *runtime_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + let changed = sqlx::query("UPDATE radroots_runtime_legacy_imports SET state = 'committing', updated_at_ms = ? WHERE import_id = ? AND state = 'ready'") + .bind(completed_at).bind(classified.import_id().as_bytes().as_slice()).execute(&mut *runtime_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + if changed.rows_affected() != 1 { + return Err(Error::LegacyImportConflict); + } + let changed = sqlx::query("UPDATE radroots_runtime_legacy_import_members SET state = 'complete', updated_at_ms = ? WHERE import_id = ? AND state = 'ready'") + .bind(completed_at).bind(classified.import_id().as_bytes().as_slice()).execute(&mut *runtime_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + if usize::try_from(changed.rows_affected()).map_err(|_| Error::LegacyImportStagingFailed)? + != classified.sources().len() + { + return Err(Error::LegacyImportConflict); + } + let changed = sqlx::query("UPDATE radroots_runtime_legacy_imports SET state = 'complete', updated_at_ms = ?, completed_at_ms = ? WHERE import_id = ? AND state = 'committing'") + .bind(completed_at).bind(completed_at).bind(classified.import_id().as_bytes().as_slice()).execute(&mut *runtime_tx).await.map_err(|_| Error::LegacyImportStagingFailed)?; + if changed.rows_affected() != 1 { + return Err(Error::LegacyImportConflict); + } + runtime_tx + .commit() + .await + .map_err(|_| Error::LegacyImportStagingFailed)?; + Ok(LegacyImportCommitReceipt { + validation_sha256: expected.validation_sha256(), + imported_row_count: expected.imported_row_count(), + completed_at_unix_ms, + }) + } + + async fn completed_legacy_import_receipt( + &self, + import_id: LegacyImportId, + expected: LegacyImportValidation, + ) -> Result<LegacyImportCommitReceipt, Error> { + let row = sqlx::query("SELECT validation_sha256, imported_row_count, completed_at_ms FROM radroots_runtime_legacy_import_commits WHERE import_id = ?") + .bind(import_id.as_bytes().as_slice()).fetch_one(&self.pool).await.map_err(|_| Error::LegacyImportStagingFailed)?; + let validation_sha256 = decode_digest( + row.try_get("validation_sha256") + .map_err(|_| Error::LegacyImportStagingFailed)?, + )?; + let imported_row_count = u64::try_from( + row.try_get::<i64, _>("imported_row_count") + .map_err(|_| Error::LegacyImportStagingFailed)?, + ) + .map_err(|_| Error::LegacyImportStagingFailed)?; + let completed_at_unix_ms = decode_positive_time( + row.try_get("completed_at_ms") + .map_err(|_| Error::LegacyImportStagingFailed)?, + )?; + if validation_sha256 != expected.validation_sha256() + || imported_row_count != expected.imported_row_count() + { + return Err(Error::LegacyImportConflict); + } + let private_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM radroots_private_legacy_import_commits WHERE import_id = ? AND validation_sha256 = ? AND imported_row_count = ? AND committed_at_ms = ?") + .bind(import_id.as_bytes().as_slice()).bind(validation_sha256.as_bytes().as_slice()).bind(i64::try_from(imported_row_count).map_err(|_| Error::LegacyImportStagingFailed)?).bind(i64::try_from(completed_at_unix_ms).map_err(|_| Error::LegacyImportStagingFailed)?).fetch_one(&self.private_pool).await.map_err(|_| Error::LegacyImportStagingFailed)?; + if private_count != 1 { + return Err(Error::LegacyImportConflict); + } + Ok(LegacyImportCommitReceipt { + validation_sha256, + imported_row_count, + completed_at_unix_ms, + }) + } + fn require_legacy_import_writer( &self, target_generation: SourceGeneration, @@ -3349,6 +3519,8 @@ mod tests { include_str!("../../../contracts/storage/legacy_studio_handoff_policy_v1.toml"); const IMPORT_VALIDATION_POLICY: &str = include_str!("../../../contracts/storage/legacy_import_validation_policy_v1.toml"); + const IMPORT_FINALIZE_POLICY: &str = + include_str!("../../../contracts/storage/legacy_import_finalize_policy_v1.toml"); const SDK_PRIVATE_STORE_SOURCE: &str = include_str!("../../../../sdk/crates/sdk/src/private_store.rs"); @@ -3534,6 +3706,25 @@ mod tests { hidden_clock_or_entropy: bool, } + #[derive(Deserialize)] + struct ImportFinalizePolicy { + schema_version: u32, + input: String, + commit_order: Vec<String>, + private_replay: String, + runtime_replay: String, + crash_before_private_commit: String, + crash_after_private_commit: String, + crash_during_runtime_completion: String, + lost_success_response: String, + retained_representation: String, + live_product_dual_write: bool, + source_deletion: bool, + studio_row_import: bool, + host_timestamp: String, + hidden_clock_or_entropy: bool, + } + fn generation(byte: u8) -> SourceGeneration { SourceGeneration::new([byte; 32]).expect("source generation") } @@ -4206,6 +4397,45 @@ mod tests { assert!(!policy.hidden_clock_or_entropy); } + #[test] + fn implementation_matches_the_governed_import_finalize_policy() { + let policy = toml::from_str::<ImportFinalizePolicy>(IMPORT_FINALIZE_POLICY) + .expect("import finalize policy"); + assert_eq!(policy.schema_version, 1); + assert_eq!(policy.input, "exact_legacy_import_validation"); + assert_eq!( + policy.commit_order, + ["private_commit_marker", "runtime_atomic_completion"] + ); + assert_eq!(policy.private_replay, "insert_or_ignore_then_exact_verify"); + assert_eq!(policy.runtime_replay, "completed_receipt_exact_verify"); + assert_eq!( + policy.crash_before_private_commit, + "journal_ready_no_private_marker" + ); + assert_eq!( + policy.crash_after_private_commit, + "journal_ready_exact_private_marker_replay" + ); + assert_eq!( + policy.crash_during_runtime_completion, + "runtime_transaction_rolls_back" + ); + assert_eq!( + policy.lost_success_response, + "exact_completed_receipt_reconstructed" + ); + assert_eq!( + policy.retained_representation, + "immutable_owned_legacy_staging" + ); + assert!(!policy.live_product_dual_write); + assert!(!policy.source_deletion); + assert!(!policy.studio_row_import); + assert_eq!(policy.host_timestamp, "positive_monotonic_completion_time"); + assert!(!policy.hidden_clock_or_entropy); + } + fn assert_fixed_schema_policy( policy: &FixedSchemaPolicy, user_version: i64, @@ -4972,6 +5202,75 @@ mod tests { .expect("repeat private validation"), validation ); + sqlx::query("INSERT INTO radroots_private_legacy_import_commits(import_id, validation_sha256, imported_row_count, committed_at_ms) VALUES (?, ?, 8, 13110)") + .bind(plan.import_id().as_bytes().as_slice()).bind(validation.validation_sha256().as_bytes().as_slice()).execute(reopened.private_pool()).await.expect("simulate private commit before runtime completion"); + let receipt = reopened + .finalize_legacy_import(&classified, validation, 13_999) + .await + .expect("recover and finalize private import"); + assert_eq!(receipt.validation_sha256(), validation.validation_sha256()); + assert_eq!(receipt.imported_row_count(), 8); + assert_eq!(receipt.completed_at_unix_ms(), 13_110); + let completed = reopened + .legacy_import_journal(plan.import_id()) + .await + .expect("completed journal") + .expect("durable completed journal"); + assert_eq!(completed.state(), LegacyImportState::Complete); + assert_eq!(completed.completed_at_unix_ms(), Some(13_110)); + assert_eq!( + completed.members()[0].state(), + LegacyImportMemberState::Complete + ); + assert_eq!( + reopened + .finalize_legacy_import(&classified, validation, 13_999) + .await + .expect("lost success response retry"), + receipt + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_runtime_legacy_import_commits" + ) + .fetch_one(reopened.pool()) + .await + .expect("runtime commit count"), + 1 + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM radroots_private_legacy_import_commits" + ) + .fetch_one(reopened.private_pool()) + .await + .expect("private commit count"), + 1 + ); + for statement in [ + "UPDATE radroots_runtime_legacy_import_commits SET imported_row_count = 9", + "DELETE FROM radroots_runtime_legacy_import_commits", + ] { + assert!( + sqlx::query(statement) + .execute(reopened.pool()) + .await + .is_err(), + "runtime commit guard accepted `{statement}`" + ); + } + for statement in [ + "UPDATE radroots_private_legacy_import_commits SET imported_row_count = 9", + "DELETE FROM radroots_private_legacy_import_commits", + ] { + assert!( + sqlx::query(statement) + .execute(reopened.private_pool()) + .await + .is_err(), + "private commit guard accepted `{statement}`" + ); + } private_connection .close() .await diff --git a/crates/storage_sqlite/src/migration.rs b/crates/storage_sqlite/src/migration.rs @@ -280,7 +280,7 @@ async fn metadata( fn validate_plan(plan: &MigrationPlan) -> Result<(), Error> { let valid = plan.minimum_version > 0 && plan.minimum_version <= plan.current_version - && plan.current_version <= 8 + && plan.current_version <= 9 && plan.steps.len() == usize::try_from(plan.current_version).unwrap_or(usize::MAX) && plan .steps @@ -391,6 +391,7 @@ const fn set_user_version_sql(version: u32) -> Option<&'static str> { 6 => Some("PRAGMA user_version = 6"), 7 => Some("PRAGMA user_version = 7"), 8 => Some("PRAGMA user_version = 8"), + 9 => Some("PRAGMA user_version = 9"), _ => None, } } @@ -583,7 +584,7 @@ mod tests { .execute(&mut newer) .await .expect("application id"); - sqlx::raw_sql("PRAGMA user_version = 9") + sqlx::raw_sql("PRAGMA user_version = 10") .execute(&mut newer) .await .expect("newer version"); @@ -592,10 +593,10 @@ mod tests { Err(Error::SchemaTooNew { database: RUNTIME_DATABASE, supported: runtime::CURRENT_VERSION, - actual: 9, + actual: 10, }) )); - assert_eq!(pragma(&mut newer, "user_version").await, 9); + assert_eq!(pragma(&mut newer, "user_version").await, 10); let mut wrong_identity = connection().await; establish_runtime_version(&mut wrong_identity, 1).await; diff --git a/crates/storage_sqlite/src/migration/private/0003_legacy_import_commits.up.sql b/crates/storage_sqlite/src/migration/private/0003_legacy_import_commits.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE radroots_private_legacy_import_commits ( + import_id BLOB PRIMARY KEY NOT NULL CHECK(length(import_id) = 16), + validation_sha256 BLOB NOT NULL CHECK(length(validation_sha256) = 32), + imported_row_count INTEGER NOT NULL CHECK(imported_row_count >= 0), + committed_at_ms INTEGER NOT NULL CHECK(committed_at_ms > 0) +) STRICT, WITHOUT ROWID; + +CREATE TRIGGER radroots_private_legacy_import_commit_update_guard +BEFORE UPDATE ON radroots_private_legacy_import_commits +BEGIN + SELECT RAISE(ABORT, 'legacy private import commit identity is immutable'); +END; + +CREATE TRIGGER radroots_private_legacy_import_commit_delete_guard +BEFORE DELETE ON radroots_private_legacy_import_commits +BEGIN + SELECT RAISE(ABORT, 'legacy private import commit evidence is retained'); +END; diff --git a/crates/storage_sqlite/src/migration/private/mod.rs b/crates/storage_sqlite/src/migration/private/mod.rs @@ -6,10 +6,11 @@ /// Lowest private schema version this package can recognize. pub const MINIMUM_VERSION: u32 = 1; /// Current private schema version created by this package. -pub const CURRENT_VERSION: u32 = 2; +pub const CURRENT_VERSION: u32 = 3; const PRIVATE_V1_SQL: &str = include_str!("0001_private.up.sql"); const LEGACY_PRIVATE_STAGING_V2_SQL: &str = include_str!("0002_legacy_private_staging.up.sql"); +const LEGACY_IMPORT_COMMITS_V3_SQL: &str = include_str!("0003_legacy_import_commits.up.sql"); /// Stable, non-SQL description of one forward private migration. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -63,6 +64,24 @@ const PRIVATE_V2_OBJECTS: &[&str] = &[ "radroots_private_legacy_import_staging_update_guard", ]; +const PRIVATE_V3_OBJECTS: &[&str] = &[ + "radroots_private_artifacts", + "radroots_private_artifacts_delete_guard", + "radroots_private_artifacts_envelope_guard", + "radroots_private_artifacts_expiry_idx", + "radroots_private_artifacts_identity_guard", + "radroots_private_artifacts_key_version_idx", + "radroots_private_artifacts_kind_idx", + "radroots_private_legacy_import_commit_delete_guard", + "radroots_private_legacy_import_commit_update_guard", + "radroots_private_legacy_import_commits", + "radroots_private_legacy_import_staging", + "radroots_private_legacy_import_staging_delete_guard", + "radroots_private_legacy_import_staging_insert_guard", + "radroots_private_legacy_import_staging_parent_idx", + "radroots_private_legacy_import_staging_update_guard", +]; + /// Ordered, immutable private migration plan. pub const MIGRATIONS: &[MigrationDescriptor] = &[ MigrationDescriptor { @@ -77,12 +96,19 @@ pub const MIGRATIONS: &[MigrationDescriptor] = &[ up_sha256: "299ec0c476b2f5ab995f245d36603969af345827c9ecdf9490cfe0b0dbe4b9f9", owned_objects: PRIVATE_V2_OBJECTS, }, + MigrationDescriptor { + version: 3, + name: "legacy_import_commits", + up_sha256: "9377f0af8f070d977a5237e2a1294e6977f5b704e7a8434d97a3dc5f4ae75e86", + owned_objects: PRIVATE_V3_OBJECTS, + }, ]; pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> { match version { 1 => Some(PRIVATE_V1_SQL), 2 => Some(LEGACY_PRIVATE_STAGING_V2_SQL), + 3 => Some(LEGACY_IMPORT_COMMITS_V3_SQL), _ => None, } } @@ -125,7 +151,7 @@ mod tests { #[test] fn migration_plan_matches_governed_snapshot() { let snapshot = toml::from_str::<PlanSnapshot>(PLAN_SNAPSHOT).expect("valid snapshot"); - let migration = MIGRATIONS[1]; + let migration = MIGRATIONS[2]; assert_eq!(snapshot.schema_version, 1); assert_eq!(snapshot.database, "private.sqlite"); assert_eq!(snapshot.application_id, 1_380_208_722); @@ -136,7 +162,7 @@ mod tests { assert!(snapshot.forward_only); assert!(!snapshot.raw_sql_public); assert!(snapshot.encrypted_envelopes); - assert_eq!(snapshot.authorities.len(), 5); + assert_eq!(snapshot.authorities.len(), 6); assert_eq!(snapshot.forbidden_tables, ["studio", "ui_state"]); assert_eq!(snapshot.migrations.len(), MIGRATIONS.len()); for (expected, actual) in snapshot.migrations.iter().zip(MIGRATIONS) { @@ -153,7 +179,7 @@ mod tests { let sql = migration_sql(migration.version()).expect("registered SQL"); assert_eq!(format!("{:x}", Sha256::digest(sql)), migration.up_sha256()); } - assert_eq!(migration_sql(3), None); + assert_eq!(migration_sql(4), None); } #[tokio::test] @@ -179,7 +205,7 @@ mod tests { .iter() .map(|row| row.get::<String, _>("name")) .collect::<Vec<_>>(); - assert_eq!(actual, MIGRATIONS[1].owned_objects()); + assert_eq!(actual, MIGRATIONS[2].owned_objects()); let forbidden = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM sqlite_schema WHERE lower(name) LIKE '%studio%' OR lower(name) LIKE '%ui_state%'", diff --git a/crates/storage_sqlite/src/migration/runtime/0009_legacy_import_commits.up.sql b/crates/storage_sqlite/src/migration/runtime/0009_legacy_import_commits.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE radroots_runtime_legacy_import_commits ( + import_id BLOB PRIMARY KEY NOT NULL CHECK(length(import_id) = 16) + REFERENCES radroots_runtime_legacy_imports(import_id) ON DELETE RESTRICT, + validation_sha256 BLOB NOT NULL CHECK(length(validation_sha256) = 32), + imported_row_count INTEGER NOT NULL CHECK(imported_row_count >= 0), + completed_at_ms INTEGER NOT NULL CHECK(completed_at_ms > 0) +) STRICT, WITHOUT ROWID; + +CREATE TRIGGER radroots_runtime_legacy_import_commit_update_guard +BEFORE UPDATE ON radroots_runtime_legacy_import_commits +BEGIN + SELECT RAISE(ABORT, 'legacy import commit identity is immutable'); +END; + +CREATE TRIGGER radroots_runtime_legacy_import_commit_delete_guard +BEFORE DELETE ON radroots_runtime_legacy_import_commits +BEGIN + SELECT RAISE(ABORT, 'legacy import commit evidence is retained'); +END; diff --git a/crates/storage_sqlite/src/migration/runtime/mod.rs b/crates/storage_sqlite/src/migration/runtime/mod.rs @@ -6,7 +6,7 @@ /// 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 = 8; +pub const CURRENT_VERSION: u32 = 9; const RUNTIME_V1_SQL: &str = include_str!("0001_runtime.up.sql"); const CANONICAL_EVENT_STORAGE_V2_SQL: &str = include_str!("0002_canonical_event_storage.up.sql"); @@ -16,6 +16,7 @@ const PROJECTION_METADATA_V5_SQL: &str = include_str!("0005_projection_metadata. const LEGACY_IMPORT_JOURNAL_V6_SQL: &str = include_str!("0006_legacy_import_journal.up.sql"); const LEGACY_EVENT_STAGING_V7_SQL: &str = include_str!("0007_legacy_event_staging.up.sql"); const LEGACY_OUTBOX_STAGING_V8_SQL: &str = include_str!("0008_legacy_outbox_staging.up.sql"); +const LEGACY_IMPORT_COMMITS_V9_SQL: &str = include_str!("0009_legacy_import_commits.up.sql"); /// Stable, non-SQL description of one forward runtime migration. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -276,6 +277,59 @@ const RUNTIME_V8_OBJECTS: &[&str] = &[ "radroots_runtime_source_generations_sequence_guard", ]; +const RUNTIME_V9_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_legacy_event_staging", + "radroots_runtime_legacy_event_staging_delete_guard", + "radroots_runtime_legacy_event_staging_insert_guard", + "radroots_runtime_legacy_event_staging_update_guard", + "radroots_runtime_legacy_import_commit_delete_guard", + "radroots_runtime_legacy_import_commit_update_guard", + "radroots_runtime_legacy_import_commits", + "radroots_runtime_legacy_import_delete_guard", + "radroots_runtime_legacy_import_identity_guard", + "radroots_runtime_legacy_import_member_delete_guard", + "radroots_runtime_legacy_import_member_identity_guard", + "radroots_runtime_legacy_import_member_state_guard", + "radroots_runtime_legacy_import_members", + "radroots_runtime_legacy_import_state_guard", + "radroots_runtime_legacy_import_state_idx", + "radroots_runtime_legacy_imports", + "radroots_runtime_legacy_outbox_staging", + "radroots_runtime_legacy_outbox_staging_delete_guard", + "radroots_runtime_legacy_outbox_staging_insert_guard", + "radroots_runtime_legacy_outbox_staging_parent_idx", + "radroots_runtime_legacy_outbox_staging_update_guard", + "radroots_runtime_outbox_items", + "radroots_runtime_outbox_operation_idx", + "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_active_idx", + "radroots_runtime_source_generations_delete_guard", + "radroots_runtime_source_generations_identity_guard", + "radroots_runtime_source_generations_sequence_guard", +]; + /// Ordered, immutable runtime migration plan. pub const MIGRATIONS: &[MigrationDescriptor] = &[ MigrationDescriptor { @@ -326,6 +380,12 @@ pub const MIGRATIONS: &[MigrationDescriptor] = &[ up_sha256: "b2ad0ee5bf7ac9e56584623c641e5208f5446dd0ff5ced9f235cd1756781be34", owned_objects: RUNTIME_V8_OBJECTS, }, + MigrationDescriptor { + version: 9, + name: "legacy_import_commits", + up_sha256: "f0807eecd652a26844c3502d81386a9d54480cb178abe1b71035e0601916afb7", + owned_objects: RUNTIME_V9_OBJECTS, + }, ]; pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> { @@ -338,6 +398,7 @@ pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> { 6 => Some(LEGACY_IMPORT_JOURNAL_V6_SQL), 7 => Some(LEGACY_EVENT_STAGING_V7_SQL), 8 => Some(LEGACY_OUTBOX_STAGING_V8_SQL), + 9 => Some(LEGACY_IMPORT_COMMITS_V9_SQL), _ => None, } } @@ -380,9 +441,9 @@ mod tests { 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, 8); - assert_eq!(MIGRATIONS.len(), 8); - let migration = MIGRATIONS[7]; + assert_eq!(CURRENT_VERSION, 9); + assert_eq!(MIGRATIONS.len(), 9); + let migration = MIGRATIONS[8]; assert_eq!(snapshot.schema_version, 1); assert_eq!(snapshot.database, "runtime.sqlite"); assert_eq!(snapshot.application_id, 1_380_209_236); @@ -392,7 +453,7 @@ mod tests { assert_eq!(snapshot.migration_sha256, migration.up_sha256()); assert!(snapshot.forward_only); assert!(!snapshot.raw_sql_public); - assert_eq!(snapshot.authorities.len(), 11); + assert_eq!(snapshot.authorities.len(), 12); assert_eq!(snapshot.source_invariants.len(), 5); assert_eq!(snapshot.migrations.len(), MIGRATIONS.len()); for (expected, actual) in snapshot.migrations.iter().zip(MIGRATIONS) { @@ -409,7 +470,7 @@ mod tests { let sql = migration_sql(migration.version()).expect("registered SQL"); assert_eq!(format!("{:x}", Sha256::digest(sql)), migration.up_sha256()); } - assert_eq!(migration_sql(9), None); + assert_eq!(migration_sql(10), None); } #[tokio::test]