commit 04668a47aedc03ccab5ea37b220324037b490e26
parent 90cebaffa13b0df983269450d187d037427a9584
Author: triesap <tyson@radroots.org>
Date: Tue, 28 Jul 2026 15:39:17 +0000
event_store: close raw repair helper boundaries
- centralize canonical database identity validation
- decode main database availability explicitly
- classify extended SQLite result codes
- preserve primary and rollback failures
Diffstat:
2 files changed, 111 insertions(+), 31 deletions(-)
diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml
@@ -67,7 +67,7 @@ sha256 = "6c90be0b0997ca33e5ab5cfb6f10e9e6ddc6679bcb5c8255dbfc21ed61d769e5"
[[sources]]
path = "crates/event_store/src/store.rs"
-sha256 = "98ec214b98eb71644aeb8cfd571984a074700f18628cd53853ac4a7bf1ef9eca"
+sha256 = "47949ce741c8a13e517a375d82fc6fe004824f76bfa655c3ae3184c152302ab7"
[[sources]]
path = "crates/event_store/src/store/addressable_transition_feed_v1.rs"
diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs
@@ -1294,14 +1294,7 @@ async fn prepare_raw_source_repair_connection_v1(
) -> Result<(), RadrootsEventStoreError> {
let main_filename = main_database_filename(connection).await?;
let actual = canonical_raw_source_repair_main_path_v1(Path::new(&main_filename))?;
- if actual != canonical_path {
- return Err(
- RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch {
- expected: canonical_path.display().to_string(),
- actual: actual.display().to_string(),
- },
- );
- }
+ validate_raw_source_repair_database_identity_v1(canonical_path, &actual)?;
validate_main_database_encoding(connection).await?;
crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(connection).await?;
validate_file_journal_mode_is_wal(connection).await?;
@@ -1336,14 +1329,7 @@ async fn validate_raw_source_repair_canonical_lock_domain_v1(
.await?;
let candidate_filename = main_database_filename(&mut candidate).await?;
let candidate_path = canonical_raw_source_repair_main_path_v1(Path::new(&candidate_filename))?;
- if candidate_path != canonical_path {
- return Err(
- RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch {
- expected: canonical_path.display().to_string(),
- actual: candidate_path.display().to_string(),
- },
- );
- }
+ validate_raw_source_repair_database_identity_v1(canonical_path, &candidate_path)?;
validate_main_database_encoding(&mut candidate).await?;
crate::schema::validate_exact_managed_v4_for_raw_source_rebuild_v1(&mut candidate).await?;
validate_file_journal_mode_is_wal(&mut candidate).await?;
@@ -1373,6 +1359,22 @@ async fn validate_raw_source_repair_canonical_lock_domain_v1(
}
}
+fn validate_raw_source_repair_database_identity_v1(
+ expected: &Path,
+ actual: &Path,
+) -> Result<(), RadrootsEventStoreError> {
+ if actual == expected {
+ Ok(())
+ } else {
+ Err(
+ RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch {
+ expected: expected.display().to_string(),
+ actual: actual.display().to_string(),
+ },
+ )
+ }
+}
+
fn preserve_raw_source_repair_probe_failure<T>(
primary: RadrootsEventStoreError,
rollback: Result<(), sqlx::Error>,
@@ -1451,23 +1453,22 @@ async fn validate_file_journal_mode_is_wal(
}
fn sqlite_error_is_busy(error: &sqlx::Error) -> bool {
- let sqlx::Error::Database(error) = error else {
- return false;
- };
- error
- .code()
- .and_then(|code| code.parse::<i32>().ok())
- .is_some_and(|code| code & 0xff == 5)
+ sqlite_error_primary_result_code(error) == Some(5)
}
fn sqlite_error_is_busy_or_locked(error: &sqlx::Error) -> bool {
+ matches!(sqlite_error_primary_result_code(error), Some(5 | 6))
+}
+
+fn sqlite_error_primary_result_code(error: &sqlx::Error) -> Option<i32> {
let sqlx::Error::Database(error) = error else {
- return false;
+ return None;
};
- error
- .code()
- .and_then(|code| code.parse::<i32>().ok())
- .is_some_and(|code| code & 0xff == 5 || code & 0xff == 6)
+ sqlite_primary_result_code(error.code().as_deref())
+}
+
+fn sqlite_primary_result_code(code: Option<&str>) -> Option<i32> {
+ code?.parse::<i32>().ok().map(|code| code & 0xff)
}
async fn main_database_filename(
@@ -1476,12 +1477,19 @@ async fn main_database_filename(
let rows = sqlx::query("PRAGMA database_list")
.fetch_all(&mut *connection)
.await?;
+ let mut filename = None;
for row in rows {
if row.try_get::<String, _>("name")? == "main" {
- return Ok(row.try_get("file")?);
+ filename = Some(row.try_get("file")?);
}
}
- Err(RadrootsEventStoreError::SqliteMainDatabaseUnavailable)
+ require_main_database_filename(filename)
+}
+
+fn require_main_database_filename(
+ filename: Option<String>,
+) -> Result<String, RadrootsEventStoreError> {
+ filename.ok_or(RadrootsEventStoreError::SqliteMainDatabaseUnavailable)
}
fn preserve_ingest_primary_failure<T>(
@@ -4821,6 +4829,78 @@ mod tests {
));
}
+ #[test]
+ fn raw_source_repair_scalar_helpers_are_closed() {
+ let expected = Path::new("expected.sqlite");
+ validate_raw_source_repair_database_identity_v1(expected, expected)
+ .expect("matching repair identity");
+ assert!(matches!(
+ validate_raw_source_repair_database_identity_v1(
+ expected,
+ Path::new("actual.sqlite"),
+ ),
+ Err(RadrootsEventStoreError::RawSourceRepairDatabaseIdentityMismatch {
+ expected,
+ actual,
+ }) if expected == "expected.sqlite" && actual == "actual.sqlite"
+ ));
+
+ assert_eq!(
+ require_main_database_filename(Some("main.sqlite".to_owned())).expect("main filename"),
+ "main.sqlite"
+ );
+ assert!(matches!(
+ require_main_database_filename(None),
+ Err(RadrootsEventStoreError::SqliteMainDatabaseUnavailable)
+ ));
+
+ for (code, expected) in [
+ (None, None),
+ (Some("invalid"), None),
+ (Some("5"), Some(5)),
+ (Some("261"), Some(5)),
+ (Some("6"), Some(6)),
+ (Some("262"), Some(6)),
+ (Some("19"), Some(19)),
+ ] {
+ assert_eq!(sqlite_primary_result_code(code), expected);
+ }
+
+ let primary = RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch {
+ canonical_path: "main.sqlite".to_owned(),
+ };
+ assert!(matches!(
+ preserve_raw_source_repair_probe_failure::<()>(primary, Ok(())),
+ Err(RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch {
+ canonical_path,
+ }) if canonical_path == "main.sqlite"
+ ));
+
+ let primary = RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch {
+ canonical_path: "main.sqlite".to_owned(),
+ };
+ let error = preserve_raw_source_repair_probe_failure::<()>(
+ primary,
+ Err(sqlx::Error::Protocol("rollback".to_owned())),
+ )
+ .expect_err("rollback failure must preserve both errors");
+ match error {
+ RadrootsEventStoreError::RawSourceRebuildTransactionRollbackFailed {
+ primary,
+ rollback: sqlx::Error::Protocol(rollback),
+ } => {
+ assert!(matches!(
+ *primary,
+ RadrootsEventStoreError::RawSourceRepairCanonicalPathLockDomainMismatch {
+ canonical_path,
+ } if canonical_path == "main.sqlite"
+ ));
+ assert_eq!(rollback, "rollback");
+ }
+ other => panic!("unexpected preserved repair error: {other}"),
+ }
+ }
+
#[tokio::test]
async fn sqlite_busy_classification_is_exact() {
let tempdir = tempfile::tempdir().expect("tempdir");