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:
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]