lib

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

commit 16624cbe72864e63b2d4efd64c008209cb541042
parent aa6325d7ec56ba14544d934d77d8171573213bc6
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 12:43:19 +0000

event_store: cover schema validation edges

- test hookless pending migration classification
- require typed FTS integrity corruption failures
- cover invalid ledger versions and delta directions
- normalize reachable schema validation branches

Diffstat:
Mcontracts/event_store_production_sources.toml | 2+-
Mcrates/event_store/src/schema.rs | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
2 files changed, 76 insertions(+), 34 deletions(-)

diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml @@ -59,7 +59,7 @@ sha256 = "9a9f3e12bd0eebf0e7ce570daa812cca0c3ffe1d0bd84a4a6f81b0d1452a4679" [[sources]] path = "crates/event_store/src/schema.rs" -sha256 = "0527f9cc7d8d0bf1a4481327f8bd9549eba1bd6bbcce87d57f8bc5fbdb990bb7" +sha256 = "2095b5d6bcdbfc2a1c6eaf7a5e43ff8cd7b6375b29f46fad174f82fcc7ede8e4" [[sources]] path = "crates/event_store/src/source_maintenance_v1.rs" diff --git a/crates/event_store/src/schema.rs b/crates/event_store/src/schema.rs @@ -969,11 +969,17 @@ fn validate_ledger_catalog(catalog: &[CatalogRow]) -> Result<bool, RadrootsEvent }); } let row = rows[0]; - if row.object_type != "table" - || row.name != EVENT_STORE_LEDGER_NAME - || row.table_name != EVENT_STORE_LEDGER_NAME - || row.sql.as_deref() != Some(EVENT_STORE_LEDGER_DDL) - { + if ( + row.object_type.as_str(), + row.name.as_str(), + row.table_name.as_str(), + row.sql.as_deref(), + ) != ( + "table", + EVENT_STORE_LEDGER_NAME, + EVENT_STORE_LEDGER_NAME, + Some(EVENT_STORE_LEDGER_DDL), + ) { return Err(RadrootsEventStoreError::MigrationLedgerDrift { reason: "ledger table definition does not match the canonical catalog SQL".to_owned(), }); @@ -1050,12 +1056,7 @@ async fn read_repair_history_bounded_v1( connection: &mut SqliteConnection, supported_current: u32, ) -> Result<Vec<AppliedMigration>, RadrootsEventStoreError> { - let row_limit = i64::from(supported_current).checked_add(1).ok_or_else(|| { - RadrootsEventStoreError::RawSourceRebuildStateDrift { - kind: RadrootsEventStoreRawSourceRebuildDriftV1::ManagedSchemaAuthority, - detail: "managed migration-history authority cannot reserve a drift row".to_owned(), - } - })?; + let row_limit = i64::from(supported_current) + 1; let rows = sqlx::query( "SELECT version, name, up_sha256, down_sha256, schema_sha256 FROM main.radroots_event_store_schema_migrations @@ -1212,6 +1213,30 @@ async fn validate_database_integrity( } let catalog = read_catalog(connection).await?; + validate_fts5_integrity(connection, registry, &catalog).await?; + + let foreign_key_rows = sqlx::query("PRAGMA foreign_key_check") + .fetch_all(&mut *connection) + .await?; + for row in foreign_key_rows { + let table: String = row.try_get("table")?; + if is_event_store_owned_table_name(registry, &table) { + return Err(RadrootsEventStoreError::ForeignKeyViolation { + table, + rowid: row.try_get("rowid")?, + parent: row.try_get("parent")?, + foreign_key_index: row.try_get("fkid")?, + }); + } + } + Ok(()) +} + +async fn validate_fts5_integrity( + connection: &mut SqliteConnection, + registry: &[EventStoreMigration], + catalog: &[CatalogRow], +) -> Result<(), RadrootsEventStoreError> { for table in registry .iter() .flat_map(|migration| migration.fts5_table_names.iter().copied()) @@ -1230,21 +1255,6 @@ async fn validate_database_integrity( source, })?; } - - let foreign_key_rows = sqlx::query("PRAGMA foreign_key_check") - .fetch_all(&mut *connection) - .await?; - for row in foreign_key_rows { - let table: String = row.try_get("table")?; - if is_event_store_owned_table_name(registry, &table) { - return Err(RadrootsEventStoreError::ForeignKeyViolation { - table, - rowid: row.try_get("rowid")?, - parent: row.try_get("parent")?, - foreign_key_index: row.try_get("fkid")?, - }); - } - } Ok(()) } @@ -1290,6 +1300,18 @@ mod migration_framework { &current, EVENT_STORE_MIGRATIONS, )); + + let mut future_without_hook = EVENT_STORE_MIGRATIONS[1]; + future_without_hook.hook = EventStoreMigrationHook::None; + let no_hook_registry = [EVENT_STORE_MIGRATIONS[0], future_without_hook]; + assert!(!has_pending_source_capacity_hook( + &baseline, + &no_hook_registry, + )); + assert!(!has_pending_source_maintenance_hook( + &baseline, + &no_hook_registry, + )); } const SYNTHETIC_V2_UP: &str = "CREATE TABLE radroots_event_store_v2_parent ( @@ -2421,18 +2443,17 @@ DROP TABLE event_envelopes;"; .execute(&mut *connection) .await .expect("corrupt FTS index"); - let error = validate_database_integrity(&mut connection, EVENT_STORE_MIGRATIONS) + let catalog = read_catalog(&mut connection).await.expect("catalog"); + let error = validate_fts5_integrity(&mut connection, EVENT_STORE_MIGRATIONS, &catalog) .await .expect_err("corrupt FTS index"); - match error { + assert!(matches!( + error, RadrootsEventStoreError::Fts5IntegrityCheckFailed { table: "listing_search_fts", .. - } => {} - RadrootsEventStoreError::IntegrityCheckFailed { detail } - if detail.to_ascii_lowercase().contains("fts5") => {} - other => panic!("unexpected FTS integrity error: {other:?}"), - } + } + )); } #[tokio::test] @@ -2667,6 +2688,19 @@ DROP TABLE event_envelopes;"; ), Err(RadrootsEventStoreError::UnknownMigration { version: 5 }) )); + + let negative = AppliedMigration { + version: -1, + name: "invalid".to_owned(), + up_sha256: "0".repeat(64), + down_sha256: "1".repeat(64), + schema_sha256: "2".repeat(64), + }; + assert!(matches!( + validate_history_against_registry(&[negative], EVENT_STORE_MIGRATIONS, 4), + Err(RadrootsEventStoreError::MigrationLedgerDrift { reason }) + if reason.contains("outside the supported positive range") + )); } #[test] @@ -3177,6 +3211,14 @@ INSERT INTO caller_child(id, parent_id) VALUES (1, 999);", migration.replaced_object_names = REPLACED_OBJECTS; validate_catalog_delta(&before, &changed, &migration, "up") .expect("exact up replacement delta"); + assert!(matches!( + validate_catalog_delta(&before, &changed, &migration, "sideways"), + Err(RadrootsEventStoreError::MigrationCatalogDeltaMismatch { + direction: "sideways", + reason, + .. + }) if reason.contains("expected removed objects") + )); let mut undeclared = migration; undeclared.replaced_object_names = &[];