lib

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

commit 2a107702873ebe07d3286c3ced45220c10aaf484
parent 164d62c1493cd28c0af48bd5dc872936742a52e0
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 18:54:19 +0000

event_store: close reconciliation authority branches

- type rebuild cardinality and row effects
- centralize persisted authority comparisons
- preserve every failure diagnostic
- execute raw-tag and integrity edge paths

Diffstat:
Mcontracts/event_store_production_sources.toml | 2+-
Mcrates/event_store/src/nip09/reconciliation_v1.rs | 768+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 658 insertions(+), 112 deletions(-)

diff --git a/contracts/event_store_production_sources.toml b/contracts/event_store_production_sources.toml @@ -47,7 +47,7 @@ sha256 = "fbd8a3b36d7f36e7b0d301aee0847d42c3908659f066cafcae3e247d67a75845" [[sources]] path = "crates/event_store/src/nip09/reconciliation_v1.rs" -sha256 = "6e2afd00f9c3d7c9addf650b22903055c121e56044320ae8fca2c5ed8b0f0952" +sha256 = "4b7f9fb91eb6ac0bffa97d0efa4c2c4eb7842dccb0a6209d752979d3b274c88c" [[sources]] path = "crates/event_store/src/nip09/reconciliation_v1/raw_source_rebuild.rs" diff --git a/crates/event_store/src/nip09/reconciliation_v1.rs b/crates/event_store/src/nip09/reconciliation_v1.rs @@ -333,6 +333,140 @@ struct SourceRebuildPlan { prior: Option<SourceState>, } +#[derive(Clone, Copy)] +enum SourceStateCardinality { + Absent, + Present, +} + +#[derive(Clone, Copy)] +struct SourceRebuildBaseline { + transition_floor_seq: i64, + raw_event_count: i64, + raw_tag_count: i64, + raw_high_water_seq: i64, +} + +impl SourceRebuildBaseline { + fn agrees_with(self, state: &SourceState) -> bool { + self.transition_floor_seq == state.last_transition_seq + && self.raw_event_count == state.raw_event_count + && self.raw_tag_count == state.raw_tag_count + && self.raw_high_water_seq == state.raw_high_water_seq + } +} + +#[derive(Clone, Copy)] +enum ReconciliationRowEffect { + StateRebuildTransition, + RebuildMarkerClose, + AuthorityUpdate, +} + +impl ReconciliationRowEffect { + fn failure_reason(self, rows_affected: u64) -> String { + match self { + Self::StateRebuildTransition => { + format!("source state rebuild transition affected {rows_affected} rows") + } + Self::RebuildMarkerClose => { + format!("source rebuild marker close affected {rows_affected} rows") + } + Self::AuthorityUpdate => { + format!("source authority update affected {rows_affected} rows") + } + } + } +} + +#[derive(Clone, Copy)] +enum ReconciliationAuthorityComparison { + Equal { + observed: i64, + expected: i64, + }, + OptionalEqual { + observed: Option<i64>, + expected: Option<i64>, + }, + LessOrEqual { + value: i64, + limit: i64, + }, + GreaterThan { + value: i64, + floor: i64, + }, + NonNegative(i64), + Zero(i64), +} + +impl ReconciliationAuthorityComparison { + fn holds(self) -> bool { + match self { + Self::Equal { observed, expected } => observed == expected, + Self::OptionalEqual { observed, expected } => observed == expected, + Self::LessOrEqual { value, limit } => value <= limit, + Self::GreaterThan { value, floor } => value > floor, + Self::NonNegative(value) => value >= 0, + Self::Zero(value) => value == 0, + } + } +} + +fn reconciliation_authority_matches<const N: usize>( + comparisons: [ReconciliationAuthorityComparison; N], +) -> bool { + comparisons + .into_iter() + .all(ReconciliationAuthorityComparison::holds) +} + +#[derive(Clone, Copy)] +enum ReconciliationCardinality { + ActiveSourceState(usize), + RebuildMarkerOutsideReconciliation(i64), + BoundActiveRebuildMarker(i64), +} + +impl ReconciliationCardinality { + fn validate(self) -> Result<(), RadrootsEventStoreError> { + match self { + Self::ActiveSourceState(1) + | Self::RebuildMarkerOutsideReconciliation(0) + | Self::BoundActiveRebuildMarker(1) => Ok(()), + Self::ActiveSourceState(count) => { + hook_drift(format!("expected one active source state, found {count}")) + } + Self::RebuildMarkerOutsideReconciliation(count) => hook_drift(format!( + "source rebuild marker residue is present outside reconciliation: {count} row(s)" + )), + Self::BoundActiveRebuildMarker(_) => hook_drift( + "open source rebuild marker does not bind completed active authority".to_owned(), + ), + } + } +} + +fn validate_rebuild_generation( + actual: RadrootsEventStoreSourceGeneration, + expected: RadrootsEventStoreSourceGeneration, +) -> Result<(), RadrootsEventStoreError> { + if actual == expected { + Ok(()) + } else { + hook_drift("open rebuild marker target does not match active source generation".to_owned()) + } +} + +fn validate_append_sequence_space(inserted_seq: i64) -> Result<(), RadrootsEventStoreError> { + if inserted_seq < i64::MAX { + Ok(()) + } else { + hook_drift("raw source sequence space is exhausted at SQLite INTEGER maximum".to_owned()) + } +} + struct SourceRebuildMarkerTokenV1 { generation: RadrootsEventStoreSourceGeneration, } @@ -575,6 +709,40 @@ fn sqlite_reconciliation_count( } } +fn source_state_cardinality(count: i64) -> Result<SourceStateCardinality, RadrootsEventStoreError> { + match count { + 0 => Ok(SourceStateCardinality::Absent), + 1 => Ok(SourceStateCardinality::Present), + count => hook_drift(format!( + "expected zero or one source state before rebuild, found {count}" + )), + } +} + +fn validate_prior_rebuild_baseline( + prior: Option<&SourceState>, + baseline: SourceRebuildBaseline, +) -> Result<(), RadrootsEventStoreError> { + match prior { + None => Ok(()), + Some(prior) if baseline.agrees_with(prior) => Ok(()), + Some(_) => hook_drift( + "prior source authority does not bind the immutable rebuild baseline".to_owned(), + ), + } +} + +fn require_reconciliation_row_effect( + rows_affected: u64, + effect: ReconciliationRowEffect, +) -> Result<(), RadrootsEventStoreError> { + if rows_affected == 1 { + Ok(()) + } else { + hook_drift(effect.failure_reason(rows_affected)) + } +} + pub(crate) async fn apply_reconciliation_hook( connection: &mut SqliteConnection, generation_provider: &dyn SourceGenerationProvider, @@ -598,17 +766,12 @@ pub(crate) async fn apply_reconciliation_hook( sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_source_state") .fetch_one(&mut *connection) .await?; - let prior = match source_state_count { - 0 => None, - 1 => { + let prior = match source_state_cardinality(source_state_count)? { + SourceStateCardinality::Absent => None, + SourceStateCardinality::Present => { validate_applied_hook_state_with_events(connection, &events).await?; Some(read_source_state(connection).await?) } - count => { - return hook_drift(format!( - "expected zero or one source state before rebuild, found {count}" - )); - } }; let mut generation_bytes = [0_u8; 32]; @@ -638,16 +801,13 @@ pub(crate) async fn apply_reconciliation_hook( .await?; let generation_ordinal = checked_authority_add(prior_generation_ordinal, 1, "source generation ordinal")?; - if let Some(prior) = prior.as_ref() - && (prior.last_transition_seq != transition_floor_seq - || prior.raw_event_count != raw_event_count - || prior.raw_tag_count != raw_tag_count - || prior.raw_high_water_seq != raw_high_water_seq) - { - return hook_drift( - "prior source authority does not bind the immutable rebuild baseline".to_owned(), - ); - } + let baseline = SourceRebuildBaseline { + transition_floor_seq, + raw_event_count, + raw_tag_count, + raw_high_water_seq, + }; + validate_prior_rebuild_baseline(prior.as_ref(), baseline)?; let plan = SourceRebuildPlan { generation, generation_ordinal, @@ -787,13 +947,10 @@ async fn rotate_source_state( .execute(&mut *connection) .await? }; - if changed.rows_affected() != 1 { - return hook_drift(format!( - "source state rebuild transition affected {} rows", - changed.rows_affected() - )); - } - Ok(()) + require_reconciliation_row_effect( + changed.rows_affected(), + ReconciliationRowEffect::StateRebuildTransition, + ) } async fn close_source_rebuild_marker( @@ -806,13 +963,10 @@ async fn close_source_rebuild_marker( .bind(marker.generation.as_bytes().as_slice()) .execute(&mut *connection) .await?; - if deleted.rows_affected() != 1 { - return hook_drift(format!( - "source rebuild marker close affected {} rows", - deleted.rows_affected() - )); - } - Ok(()) + require_reconciliation_row_effect( + deleted.rows_affected(), + ReconciliationRowEffect::RebuildMarkerClose, + ) } async fn validate_sqlite_integrity_after_rebuild( @@ -835,10 +989,20 @@ async fn validate_sqlite_integrity_after_rebuild( let integrity_rows = sqlx::query("PRAGMA integrity_check") .fetch_all(&mut *connection) .await?; - if integrity_rows.len() != 1 || integrity_rows[0].try_get::<String, _>(0)?.as_str() != "ok" { + validate_sqlite_integrity_rows(&integrity_rows) +} + +fn validate_sqlite_integrity_rows( + rows: &[sqlx::sqlite::SqliteRow], +) -> Result<(), RadrootsEventStoreError> { + let [row] = rows else { return hook_drift("SQLite integrity validation failed after source rebuild".to_owned()); + }; + if row.try_get::<String, _>(0)?.as_str() == "ok" { + Ok(()) + } else { + hook_drift("SQLite integrity validation failed after source rebuild".to_owned()) } - Ok(()) } #[cfg(test)] @@ -866,11 +1030,7 @@ async fn validate_rebuild_hook_state_with_events( ) -> Result<(), RadrootsEventStoreError> { validate_active_rebuild_marker(connection, generation).await?; let state = validate_structural_source_state(connection).await?; - if state.generation != generation { - return hook_drift( - "open rebuild marker target does not match active source generation".to_owned(), - ); - } + validate_rebuild_generation(state.generation, generation)?; validate_hook_state_with_events(connection, &state, events).await } @@ -881,11 +1041,7 @@ async fn validate_raw_source_rebuild_core_with_events_v1( ) -> Result<(), RadrootsEventStoreError> { validate_active_rebuild_marker(connection, generation).await?; let state = read_source_state(connection).await?; - if state.generation != generation { - return hook_drift( - "open rebuild marker target does not match active source generation".to_owned(), - ); - } + validate_rebuild_generation(state.generation, generation)?; validate_source_raw_authority_with_state(connection, &state).await?; validate_transition_interval_full(connection, &state).await?; validate_derived_event_storage(connection, events).await?; @@ -947,12 +1103,7 @@ async fn validate_structural_source_state_fast( ) .fetch_all(&mut *connection) .await?; - if rows.len() != 1 { - return hook_drift(format!( - "expected one active source state, found {}", - rows.len() - )); - } + ReconciliationCardinality::ActiveSourceState(rows.len()).validate()?; let row = &rows[0]; let generation = generation_from_blob(row.try_get("active_generation")?)?; let profile = reconciliation_profile( @@ -979,11 +1130,24 @@ async fn validate_structural_source_state_fast( if generation_ordinal != max_generation_ordinal { return hook_drift("active generation contract metadata is inconsistent".to_owned()); } - if state.baseline_raw_event_count > state.raw_event_count - || state.baseline_raw_tag_count > state.raw_tag_count - || state.baseline_raw_high_water_seq > state.raw_high_water_seq - || state.transition_floor_seq > state.last_transition_seq - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::LessOrEqual { + value: state.baseline_raw_event_count, + limit: state.raw_event_count, + }, + ReconciliationAuthorityComparison::LessOrEqual { + value: state.baseline_raw_tag_count, + limit: state.raw_tag_count, + }, + ReconciliationAuthorityComparison::LessOrEqual { + value: state.baseline_raw_high_water_seq, + limit: state.raw_high_water_seq, + }, + ReconciliationAuthorityComparison::LessOrEqual { + value: state.transition_floor_seq, + limit: state.last_transition_seq, + }, + ]) { return hook_drift("active generation baseline exceeds current authority".to_owned()); } let actual_high_water: i64 = @@ -1016,10 +1180,17 @@ async fn validate_structural_source_state_fast( None }; let expected_last = (expected_count > 0).then_some(state.last_transition_seq); - if expected_count < 0 - || first_transition_seq != expected_first - || transition_high_water != expected_last - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::NonNegative(expected_count), + ReconciliationAuthorityComparison::OptionalEqual { + observed: first_transition_seq, + expected: expected_first, + }, + ReconciliationAuthorityComparison::OptionalEqual { + observed: transition_high_water, + expected: expected_last, + }, + ]) { return hook_drift(format!( "active transition bounds are inconsistent: floor={}, last={}, first={first_transition_seq:?}, high-water={transition_high_water:?}", state.transition_floor_seq, state.last_transition_seq @@ -1036,12 +1207,7 @@ async fn validate_rebuild_marker_absent( sqlx::query_scalar("SELECT COUNT(*) FROM radroots_event_store_source_rebuild_marker") .fetch_one(&mut *connection) .await?; - if marker_count != 0 { - return hook_drift(format!( - "source rebuild marker residue is present outside reconciliation: {marker_count} row(s)" - )); - } - Ok(()) + ReconciliationCardinality::RebuildMarkerOutsideReconciliation(marker_count).validate() } async fn validate_active_rebuild_marker( @@ -1054,12 +1220,7 @@ async fn validate_active_rebuild_marker( .bind(generation.as_bytes().as_slice()) .fetch_one(&mut *connection) .await?; - if valid_marker_count != 1 { - return hook_drift( - "open source rebuild marker does not bind completed active authority".to_owned(), - ); - } - Ok(()) + ReconciliationCardinality::BoundActiveRebuildMarker(valid_marker_count).validate() } async fn validate_projection_cursor_authority( @@ -1191,11 +1352,15 @@ async fn validate_transition_interval_full( .bind(state.transition_floor_seq) .fetch_one(&mut *connection) .await?; - if expected_count < 0 - || transition_count != expected_count - || foreign_transition_count != 0 - || pre_floor_active_count != 0 - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::NonNegative(expected_count), + ReconciliationAuthorityComparison::Equal { + observed: transition_count, + expected: expected_count, + }, + ReconciliationAuthorityComparison::Zero(foreign_transition_count), + ReconciliationAuthorityComparison::Zero(pre_floor_active_count), + ]) { return hook_drift(format!( "active transition interval is not contiguous: floor={}, last={}, count={}, foreign={foreign_transition_count}, pre-floor={pre_floor_active_count}", state.transition_floor_seq, state.last_transition_seq, transition_count @@ -1230,11 +1395,7 @@ pub(crate) async fn synchronize_after_insert( raw_head_decision: &RadrootsRawHeadDecision, ) -> Result<(), RadrootsEventStoreError> { validate_rebuild_marker_absent(connection).await?; - if inserted_seq == i64::MAX { - return hook_drift( - "raw source sequence space is exhausted at SQLite INTEGER maximum".to_owned(), - ); - } + validate_append_sequence_space(inserted_seq)?; let prior = read_source_state(connection).await?; let actual_high_water: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(seq), 0) FROM event_envelopes") @@ -1249,10 +1410,20 @@ pub(crate) async fn synchronize_after_insert( inserted_tag_count as u128, "inserted tag count exceeds SQLite integer range", )?; - if actual_inserted_tag_count != inserted_tag_count - || inserted_seq <= prior.raw_high_water_seq - || actual_high_water != inserted_seq - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::Equal { + observed: actual_inserted_tag_count, + expected: inserted_tag_count, + }, + ReconciliationAuthorityComparison::GreaterThan { + value: inserted_seq, + floor: prior.raw_high_water_seq, + }, + ReconciliationAuthorityComparison::Equal { + observed: actual_high_water, + expected: inserted_seq, + }, + ]) { let expected = SourceState { generation: prior.generation, profile: prior.profile, @@ -1672,17 +1843,32 @@ fn compare_raw_tags( let expected_name = tag.first().map(String::as_str).unwrap_or(""); let expected_value = tag.get(1).map(String::as_str); let expected_json = serde_json::to_string(tag)?; - if row.tag_index != expected_index - || row.tag_name != expected_name - || row.tag_value.as_deref() != expected_value - || row.tag_json != expected_json - { + if !stored_raw_tag_matches( + &row, + expected_index, + expected_name, + expected_value, + expected_json.as_str(), + ) { return Err(raw_mismatch(event_id, "tag_rows")); } } Ok(()) } +fn stored_raw_tag_matches( + row: &StoredRawTag, + expected_index: i64, + expected_name: &str, + expected_value: Option<&str>, + expected_json: &str, +) -> bool { + row.tag_index == expected_index + && row.tag_name == expected_name + && row.tag_value.as_deref() == expected_value + && row.tag_json == expected_json +} + #[derive(Clone, Copy)] enum ReconciliationPaginationAxis<'a> { RawTagSnapshot, @@ -2941,10 +3127,20 @@ async fn validate_baseline_authority( .bind(source.baseline_raw_high_water_seq) .fetch_one(&mut *connection) .await?; - if baseline_event_count != source.baseline_raw_event_count - || baseline_tag_count != source.baseline_raw_tag_count - || baseline_high_water != source.baseline_raw_high_water_seq - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::Equal { + observed: baseline_event_count, + expected: source.baseline_raw_event_count, + }, + ReconciliationAuthorityComparison::Equal { + observed: baseline_tag_count, + expected: source.baseline_raw_tag_count, + }, + ReconciliationAuthorityComparison::Equal { + observed: baseline_high_water, + expected: source.baseline_raw_high_water_seq, + }, + ]) { return hook_drift( "active generation baseline raw authority disagrees with canonical replay".to_owned(), ); @@ -3444,12 +3640,7 @@ async fn read_source_state( ) .fetch_all(&mut *connection) .await?; - if rows.len() != 1 { - return hook_drift(format!( - "expected one active source state, found {}", - rows.len() - )); - } + ReconciliationCardinality::ActiveSourceState(rows.len()).validate()?; let row = &rows[0]; let generation_ordinal: i64 = row.try_get("generation_ordinal")?; let max_generation_ordinal: i64 = row.try_get("max_generation_ordinal")?; @@ -3521,10 +3712,20 @@ async fn validate_source_raw_authority_with_state( let actual_tag_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM event_envelope_tags") .fetch_one(&mut *connection) .await?; - if actual_count != state.raw_event_count - || actual_high_water != state.raw_high_water_seq - || actual_tag_count != state.raw_tag_count - { + if !reconciliation_authority_matches([ + ReconciliationAuthorityComparison::Equal { + observed: actual_count, + expected: state.raw_event_count, + }, + ReconciliationAuthorityComparison::Equal { + observed: actual_high_water, + expected: state.raw_high_water_seq, + }, + ReconciliationAuthorityComparison::Equal { + observed: actual_tag_count, + expected: state.raw_tag_count, + }, + ]) { return Err(RadrootsEventStoreError::RawEventSourceDrift { expected_count: state.raw_event_count, expected_tag_count: state.raw_tag_count, @@ -3557,13 +3758,10 @@ async fn update_source_authority( .bind(last_transition_seq) .execute(&mut *connection) .await?; - if updated.rows_affected() != 1 { - return hook_drift(format!( - "source authority update affected {} rows", - updated.rows_affected() - )); - } - Ok(()) + require_reconciliation_row_effect( + updated.rows_affected(), + ReconciliationRowEffect::AuthorityUpdate, + ) } pub(crate) fn generation_from_blob( @@ -3876,6 +4074,276 @@ mod tests { reason, }) if reason == "fixture count range" )); + assert!(matches!( + source_state_cardinality(0), + Ok(SourceStateCardinality::Absent) + )); + assert!(matches!( + source_state_cardinality(1), + Ok(SourceStateCardinality::Present) + )); + assert!(matches!( + source_state_cardinality(2), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == "expected zero or one source state before rebuild, found 2" + )); + + let baseline = SourceRebuildBaseline { + transition_floor_seq: 1, + raw_event_count: 2, + raw_tag_count: 3, + raw_high_water_seq: 4, + }; + let prior = SourceState { + generation: RadrootsEventStoreSourceGeneration::from_bytes([1; 32]), + profile: ReconciliationProfile::Nip09V1RegistryV7, + raw_event_count: 2, + raw_tag_count: 3, + raw_high_water_seq: 4, + last_transition_seq: 1, + transition_floor_seq: 0, + baseline_raw_event_count: 0, + baseline_raw_tag_count: 0, + baseline_raw_high_water_seq: 0, + }; + validate_prior_rebuild_baseline(None, baseline).expect("no prior baseline"); + validate_prior_rebuild_baseline(Some(&prior), baseline).expect("matching prior baseline"); + for (mut mismatched, mutate) in [ + (prior.clone(), 0_u8), + (prior.clone(), 1), + (prior.clone(), 2), + (prior.clone(), 3), + ] { + match mutate { + 0 => mismatched.last_transition_seq += 1, + 1 => mismatched.raw_event_count += 1, + 2 => mismatched.raw_tag_count += 1, + 3 => mismatched.raw_high_water_seq += 1, + _ => unreachable!(), + } + assert!(matches!( + validate_prior_rebuild_baseline(Some(&mismatched), baseline), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == "prior source authority does not bind the immutable rebuild baseline" + )); + } + for (effect, expected_reason) in [ + ( + ReconciliationRowEffect::StateRebuildTransition, + "source state rebuild transition affected 0 rows", + ), + ( + ReconciliationRowEffect::RebuildMarkerClose, + "source rebuild marker close affected 0 rows", + ), + ( + ReconciliationRowEffect::AuthorityUpdate, + "source authority update affected 0 rows", + ), + ] { + require_reconciliation_row_effect(1, effect).expect("one row effect"); + assert!(matches!( + require_reconciliation_row_effect(0, effect), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == expected_reason + )); + } + for (matching, mismatching) in [ + ( + ReconciliationAuthorityComparison::Equal { + observed: 1, + expected: 1, + }, + ReconciliationAuthorityComparison::Equal { + observed: 1, + expected: 2, + }, + ), + ( + ReconciliationAuthorityComparison::OptionalEqual { + observed: Some(1), + expected: Some(1), + }, + ReconciliationAuthorityComparison::OptionalEqual { + observed: Some(1), + expected: None, + }, + ), + ( + ReconciliationAuthorityComparison::LessOrEqual { value: 1, limit: 1 }, + ReconciliationAuthorityComparison::LessOrEqual { value: 2, limit: 1 }, + ), + ( + ReconciliationAuthorityComparison::GreaterThan { value: 2, floor: 1 }, + ReconciliationAuthorityComparison::GreaterThan { value: 1, floor: 1 }, + ), + ( + ReconciliationAuthorityComparison::NonNegative(0), + ReconciliationAuthorityComparison::NonNegative(-1), + ), + ( + ReconciliationAuthorityComparison::Zero(0), + ReconciliationAuthorityComparison::Zero(1), + ), + ] { + assert!(matching.holds()); + assert!(!mismatching.holds()); + } + assert!(reconciliation_authority_matches([ + ReconciliationAuthorityComparison::Equal { + observed: 1, + expected: 1, + }, + ReconciliationAuthorityComparison::Zero(0), + ])); + assert!(!reconciliation_authority_matches([ + ReconciliationAuthorityComparison::Equal { + observed: 1, + expected: 1, + }, + ReconciliationAuthorityComparison::Zero(1), + ])); + for (valid, invalid, expected_reason) in [ + ( + ReconciliationCardinality::ActiveSourceState(1), + ReconciliationCardinality::ActiveSourceState(2), + "expected one active source state, found 2", + ), + ( + ReconciliationCardinality::RebuildMarkerOutsideReconciliation(0), + ReconciliationCardinality::RebuildMarkerOutsideReconciliation(1), + "source rebuild marker residue is present outside reconciliation: 1 row(s)", + ), + ( + ReconciliationCardinality::BoundActiveRebuildMarker(1), + ReconciliationCardinality::BoundActiveRebuildMarker(0), + "open source rebuild marker does not bind completed active authority", + ), + ] { + valid.validate().expect("valid reconciliation cardinality"); + assert!(matches!( + invalid.validate(), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == expected_reason + )); + } + let generation = RadrootsEventStoreSourceGeneration::from_bytes([2; 32]); + validate_rebuild_generation(generation, generation).expect("matching rebuild generation"); + assert!(matches!( + validate_rebuild_generation( + generation, + RadrootsEventStoreSourceGeneration::from_bytes([3; 32]), + ), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == "open rebuild marker target does not match active source generation" + )); + validate_append_sequence_space(i64::MAX - 1).expect("remaining sequence space"); + assert!(matches!( + validate_append_sequence_space(i64::MAX), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == "raw source sequence space is exhausted at SQLite INTEGER maximum" + )); + for (row, expected) in [ + ( + StoredRawTag { + tag_index: 1, + tag_name: "name".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: "json".to_owned(), + }, + true, + ), + ( + StoredRawTag { + tag_index: 2, + tag_name: "name".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: "json".to_owned(), + }, + false, + ), + ( + StoredRawTag { + tag_index: 1, + tag_name: "other".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: "json".to_owned(), + }, + false, + ), + ( + StoredRawTag { + tag_index: 1, + tag_name: "name".to_owned(), + tag_value: None, + tag_json: "json".to_owned(), + }, + false, + ), + ( + StoredRawTag { + tag_index: 1, + tag_name: "name".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: "other".to_owned(), + }, + false, + ), + ] { + assert_eq!( + stored_raw_tag_matches(&row, 1, "name", Some("value"), "json"), + expected, + ); + } + compare_raw_tags( + "fixture-event", + &[Vec::new()], + vec![StoredRawTag { + tag_index: 0, + tag_name: String::new(), + tag_value: None, + tag_json: "[]".to_owned(), + }], + ) + .expect("matching empty raw tag"); + let empty_tag_rows = vec![StoredRawTag { + tag_index: 0, + tag_name: String::new(), + tag_value: None, + tag_json: "[]".to_owned(), + }]; + assert!(matches!( + compare_raw_tags("fixture-event", &[], empty_tag_rows), + Err(RadrootsEventStoreError::RawEventReconciliationMismatch { + event_id, + field: "tag_rows", + }) if event_id == "fixture-event" + )); + assert!( + compare_raw_tags( + "fixture-event", + &[Vec::new()], + vec![StoredRawTag { + tag_index: 0, + tag_name: "forged".to_owned(), + tag_value: None, + tag_json: "[]".to_owned(), + }], + ) + .is_err() + ); assert_eq!( next_reconciliation_page_value(0, ReconciliationPaginationAxis::RawTagSnapshot) .expect("bounded page successor"), @@ -3951,6 +4419,33 @@ mod tests { let pool = open_v1_test_pool().await; let mut connection = pool.acquire().await.expect("connection"); + let integrity_ok = sqlx::query("SELECT 'ok'") + .fetch_all(&mut *connection) + .await + .expect("valid integrity observation"); + validate_sqlite_integrity_rows(&integrity_ok).expect("valid integrity result"); + for integrity_invalid in [ + sqlx::query("SELECT 'not ok'") + .fetch_all(&mut *connection) + .await + .expect("invalid integrity observation"), + sqlx::query("SELECT 'ok' WHERE 0") + .fetch_all(&mut *connection) + .await + .expect("empty integrity observation"), + sqlx::query("SELECT 'ok' UNION ALL SELECT 'ok'") + .fetch_all(&mut *connection) + .await + .expect("multiple integrity observations"), + ] { + assert!(matches!( + validate_sqlite_integrity_rows(&integrity_invalid), + Err(RadrootsEventStoreError::MigrationHookStateDrift { + hook_id: NIP09_HOOK_ID, + reason, + }) if reason == "SQLite integrity validation failed after source rebuild" + )); + } let stored_decision = stored_suppression_decision( &mut connection, RadrootsEventStoreSourceGeneration::from_bytes([1; 32]), @@ -4033,6 +4528,57 @@ mod tests { ); assert!(raw_coordinate_parts("invalid").is_err()); assert!(raw_coordinate_parts("30402:invalid").is_err()); + let expected_tag = vec!["e".to_owned(), "value".to_owned()]; + let expected_tag_json = serde_json::to_string(&expected_tag).expect("tag JSON"); + compare_raw_tags( + "fixture-event", + std::slice::from_ref(&expected_tag), + vec![StoredRawTag { + tag_index: 0, + tag_name: "e".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: expected_tag_json.clone(), + }], + ) + .expect("matching raw tag"); + for mismatched in [ + StoredRawTag { + tag_index: 1, + tag_name: "e".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: expected_tag_json.clone(), + }, + StoredRawTag { + tag_index: 0, + tag_name: "a".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: expected_tag_json.clone(), + }, + StoredRawTag { + tag_index: 0, + tag_name: "e".to_owned(), + tag_value: None, + tag_json: expected_tag_json.clone(), + }, + StoredRawTag { + tag_index: 0, + tag_name: "e".to_owned(), + tag_value: Some("value".to_owned()), + tag_json: "[]".to_owned(), + }, + ] { + assert!(matches!( + compare_raw_tags( + "fixture-event", + std::slice::from_ref(&expected_tag), + vec![mismatched], + ), + Err(RadrootsEventStoreError::RawEventReconciliationMismatch { + ref event_id, + field: "tag_rows", + }) if event_id == "fixture-event" + )); + } require_expected_insert(1, "fixture").expect("one insert"); assert!(require_expected_insert(0, "fixture").is_err()); assert!(checked_authority_add(i64::MAX, 1, "fixture").is_err());