Skip to main content

arc_es_sqlite/
lib.rs

1//! # Arc ES SQLite
2//!
3//! SQLite implementation of the [`EventStore`] trait from `arc-core`.
4//!
5//! Persists [`AuditMetadata`] inline alongside each event (see HIPAA-1 in
6//! `docs/ark/refactor-plan.md`). `append` calls
7//! [`validate_audit_batch`](arc_core::event_store::validate_audit_batch)
8//! before any write — defense-in-depth against an upstream that forgot to
9//! stamp.
10
11use arc_core::audit::AuditMetadata;
12use arc_core::event::Event;
13#[cfg(test)]
14use arc_core::event::NewEvent;
15use arc_core::event_store::{
16    validate_audit_batch, EventStore, EventStoreError, EventStoreResult, VersionCheck,
17};
18use arc_core::integrity::{EventSignature, HmacSha256Chain, IntegrityChain, IntegrityError};
19use arc_core::snapshot::Snapshot;
20use async_trait::async_trait;
21use diesel::prelude::*;
22use diesel::r2d2::{self, ConnectionManager};
23use diesel::sqlite::SqliteConnection;
24use std::sync::Arc;
25use uuid::Uuid;
26
27// Re-export for convenience
28pub use arc_core::{Deserialize, Serialize};
29
30pub mod session;
31pub use session::SqliteSessionStore;
32
33pub mod read_model_store;
34pub use read_model_store::SqliteReadModelStore;
35
36#[cfg(test)]
37mod test_support;
38
39/// Database row used for inserting events.
40#[derive(Debug, Insertable, Clone)]
41#[diesel(table_name = events)]
42struct NewEventRecord {
43    pub event_id: String,
44    pub aggregate_type: String,
45    pub aggregate_id: String,
46    pub sequence: i64,
47    pub event_type: String,
48    pub payload: String,
49    pub timestamp: i64,
50    pub actor_id: String,
51    pub actor_session_id: Option<String>,
52    pub source_ip: Option<String>,
53    pub user_agent: Option<String>,
54    pub timestamp_utc_us: i64,
55    pub causation_id: Option<String>,
56    pub correlation_id: String,
57    pub integrity_signature: Option<String>,
58    pub integrity_key_id: Option<String>,
59}
60
61#[derive(Debug, Queryable, Clone)]
62struct EventRecord {
63    #[allow(dead_code)]
64    pub id: Option<i32>,
65    pub event_id: String,
66    pub aggregate_type: String,
67    pub aggregate_id: String,
68    pub sequence: i64,
69    pub event_type: String,
70    pub payload: String,
71    pub timestamp: i64,
72    pub actor_id: String,
73    pub actor_session_id: Option<String>,
74    pub source_ip: Option<String>,
75    pub user_agent: Option<String>,
76    pub timestamp_utc_us: i64,
77    pub causation_id: Option<String>,
78    pub correlation_id: String,
79    pub integrity_signature: Option<String>,
80    pub integrity_key_id: Option<String>,
81}
82
83impl NewEventRecord {
84    fn from_event(
85        event: &Event,
86        integrity_signature: Option<String>,
87        integrity_key_id: Option<String>,
88    ) -> Result<Self, EventStoreError> {
89        // sequence and timestamp are i64 end-to-end now — no truncation.
90        let timestamp_seconds: i64 = (event.timestamp / 1000) as i64;
91        Ok(NewEventRecord {
92            event_id: event.event_id.to_string(),
93            aggregate_type: event.aggregate_type.clone(),
94            aggregate_id: event.aggregate_id.clone(),
95            sequence: event.sequence,
96            event_type: event.event_type.clone(),
97            payload: serde_json::to_string(&event.payload)
98                .map_err(|e| EventStoreError::serialization(e.to_string()))?,
99            timestamp: timestamp_seconds,
100            actor_id: event.audit.actor_id.clone(),
101            actor_session_id: event.audit.actor_session_id.clone(),
102            source_ip: event.audit.source_ip.clone(),
103            user_agent: event.audit.user_agent.clone(),
104            timestamp_utc_us: event.audit.timestamp_utc_us,
105            causation_id: event.audit.causation_id.map(|u| u.to_string()),
106            correlation_id: event.audit.correlation_id.to_string(),
107            integrity_signature,
108            integrity_key_id,
109        })
110    }
111}
112
113impl EventRecord {
114    fn to_event(&self) -> Result<Event, EventStoreError> {
115        let event_id = Uuid::parse_str(&self.event_id)
116            .map_err(|e| EventStoreError::serialization(format!("Invalid UUID: {}", e)))?;
117
118        let payload: serde_json::Value = serde_json::from_str(&self.payload)
119            .map_err(|e| EventStoreError::serialization(e.to_string()))?;
120
121        let causation_id = match self.causation_id.as_deref() {
122            Some(s) => Some(Uuid::parse_str(s).map_err(|e| {
123                EventStoreError::serialization(format!("Invalid causation UUID: {}", e))
124            })?),
125            None => None,
126        };
127
128        let correlation_id = Uuid::parse_str(&self.correlation_id).map_err(|e| {
129            EventStoreError::serialization(format!("Invalid correlation UUID: {}", e))
130        })?;
131
132        let audit = AuditMetadata {
133            actor_id: self.actor_id.clone(),
134            actor_session_id: self.actor_session_id.clone(),
135            source_ip: self.source_ip.clone(),
136            user_agent: self.user_agent.clone(),
137            timestamp_utc_us: self.timestamp_utc_us,
138            causation_id,
139            correlation_id,
140        };
141
142        Ok(Event {
143            event_id,
144            aggregate_type: self.aggregate_type.clone(),
145            aggregate_id: self.aggregate_id.clone(),
146            sequence: self.sequence,
147            event_type: self.event_type.clone(),
148            payload,
149            audit,
150            timestamp: (self.timestamp as u64) * 1000,
151        })
152    }
153}
154
155/// Database row used for upserting snapshots. `state` holds the aggregate's
156/// serialized JSON; `created_at` is milliseconds since epoch (same unit as the
157/// core `Snapshot`), stored as i64 to match the events table's `timestamp`.
158#[derive(Debug, Insertable, Clone)]
159#[diesel(table_name = snapshots)]
160struct NewSnapshotRecord {
161    pub aggregate_id: String,
162    pub aggregate_type: String,
163    pub version: i64,
164    pub state: String,
165    pub created_at: i64,
166}
167
168#[derive(Debug, Queryable, Clone)]
169struct SnapshotRecord {
170    pub aggregate_id: String,
171    pub aggregate_type: String,
172    pub version: i64,
173    pub state: String,
174    pub created_at: i64,
175}
176
177impl NewSnapshotRecord {
178    fn from_snapshot(snapshot: &Snapshot) -> Result<Self, EventStoreError> {
179        Ok(NewSnapshotRecord {
180            aggregate_id: snapshot.aggregate_id.clone(),
181            aggregate_type: snapshot.aggregate_type.clone(),
182            version: snapshot.version,
183            state: serde_json::to_string(&snapshot.state)
184                .map_err(|e| EventStoreError::serialization(e.to_string()))?,
185            created_at: snapshot.created_at as i64,
186        })
187    }
188}
189
190impl SnapshotRecord {
191    fn to_snapshot(&self) -> Result<Snapshot, EventStoreError> {
192        let state: serde_json::Value = serde_json::from_str(&self.state)
193            .map_err(|e| EventStoreError::serialization(e.to_string()))?;
194        Ok(Snapshot {
195            aggregate_id: self.aggregate_id.clone(),
196            aggregate_type: self.aggregate_type.clone(),
197            version: self.version,
198            state,
199            created_at: self.created_at as u64,
200        })
201    }
202}
203
204mod schema {
205    diesel::table! {
206        events (id) {
207            id -> Nullable<Integer>,
208            event_id -> Text,
209            aggregate_type -> Text,
210            aggregate_id -> Text,
211            sequence -> BigInt,
212            event_type -> Text,
213            payload -> Text,
214            timestamp -> BigInt,
215            actor_id -> Text,
216            actor_session_id -> Nullable<Text>,
217            source_ip -> Nullable<Text>,
218            user_agent -> Nullable<Text>,
219            timestamp_utc_us -> BigInt,
220            causation_id -> Nullable<Text>,
221            correlation_id -> Text,
222            integrity_signature -> Nullable<Text>,
223            integrity_key_id -> Nullable<Text>,
224        }
225    }
226
227    diesel::table! {
228        snapshots (aggregate_type, aggregate_id) {
229            aggregate_id -> Text,
230            aggregate_type -> Text,
231            version -> BigInt,
232            state -> Text,
233            created_at -> BigInt,
234        }
235    }
236}
237
238use schema::{events, snapshots};
239
240type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
241
242/// SQLite implementation of EventStore.
243#[derive(Clone)]
244pub struct SqliteEventStore {
245    pool: Arc<Pool>,
246    integrity: Option<Arc<IntegrityConfig>>,
247}
248
249struct IntegrityConfig {
250    chain: Arc<dyn IntegrityChain>,
251    key_id: String,
252}
253
254impl SqliteEventStore {
255    pub async fn new(database_url: &str) -> EventStoreResult<Self> {
256        let manager = ConnectionManager::<SqliteConnection>::new(database_url);
257        let pool = Pool::builder()
258            .max_size(10)
259            .build(manager)
260            .map_err(|e| EventStoreError::database(format!("Failed to create pool: {}", e)))?;
261
262        Ok(SqliteEventStore {
263            pool: Arc::new(pool),
264            integrity: None,
265        })
266    }
267
268    pub async fn new_with_integrity_key(
269        database_url: &str,
270        key: impl Into<Vec<u8>>,
271        key_id: impl Into<String>,
272    ) -> EventStoreResult<Self> {
273        let mut store = Self::new(database_url).await?;
274        store.integrity = Some(Arc::new(IntegrityConfig {
275            chain: Arc::new(HmacSha256Chain::new(key).map_err(EventStoreError::from)?),
276            key_id: key_id.into(),
277        }));
278        Ok(store)
279    }
280
281    pub fn with_pool(pool: Pool) -> Self {
282        SqliteEventStore {
283            pool: Arc::new(pool),
284            integrity: None,
285        }
286    }
287
288    pub fn with_pool_and_integrity_key(
289        pool: Pool,
290        key: impl Into<Vec<u8>>,
291        key_id: impl Into<String>,
292    ) -> EventStoreResult<Self> {
293        Ok(SqliteEventStore {
294            pool: Arc::new(pool),
295            integrity: Some(Arc::new(IntegrityConfig {
296                chain: Arc::new(HmacSha256Chain::new(key).map_err(EventStoreError::from)?),
297                key_id: key_id.into(),
298            })),
299        })
300    }
301}
302
303fn required_signature(
304    record: &EventRecord,
305    aggregate_id: &str,
306    sequence: i64,
307) -> EventStoreResult<EventSignature> {
308    let _key_id = record.integrity_key_id.as_ref().ok_or_else(|| {
309        EventStoreError::from(IntegrityError::BrokenAt {
310            aggregate_id: aggregate_id.to_string(),
311            sequence,
312        })
313    })?;
314
315    record
316        .integrity_signature
317        .as_ref()
318        .map(|s| EventSignature(s.clone()))
319        .ok_or_else(|| {
320            EventStoreError::from(IntegrityError::BrokenAt {
321                aggregate_id: aggregate_id.to_string(),
322                sequence,
323            })
324        })
325}
326
327fn verify_integrity_records(
328    integrity: &IntegrityConfig,
329    records: &[EventRecord],
330    previous_signature: EventSignature,
331) -> EventStoreResult<Vec<Event>> {
332    let mut previous = previous_signature;
333    let mut events = Vec::with_capacity(records.len());
334
335    for record in records {
336        let event = record.to_event()?;
337        let expected = integrity.chain.sign_event(&previous, &event)?;
338        let claimed = required_signature(record, &event.aggregate_id, event.sequence)?;
339
340        if expected != claimed {
341            return Err(EventStoreError::from(IntegrityError::BrokenAt {
342                aggregate_id: event.aggregate_id,
343                sequence: event.sequence,
344            }));
345        }
346
347        previous = claimed;
348        events.push(event);
349    }
350
351    Ok(events)
352}
353
354fn previous_signature_for_aggregate(
355    conn: &mut SqliteConnection,
356    aggregate_type: Option<&str>,
357    aggregate_id: &str,
358    before_sequence: i64,
359) -> EventStoreResult<EventSignature> {
360    if before_sequence <= 1 {
361        return Ok(EventSignature::genesis());
362    }
363
364    let mut query = events::table
365        .filter(events::aggregate_id.eq(aggregate_id))
366        .filter(events::sequence.lt(before_sequence))
367        .into_boxed();
368    if let Some(aggregate_type) = aggregate_type {
369        query = query.filter(events::aggregate_type.eq(aggregate_type));
370    }
371    let record = query
372        .order(events::sequence.desc())
373        .first::<EventRecord>(conn)
374        .optional()
375        .map_err(|e| EventStoreError::database(e.to_string()))?;
376
377    match record {
378        Some(record) => required_signature(&record, aggregate_id, record.sequence),
379        None => Ok(EventSignature::genesis()),
380    }
381}
382
383fn verify_stream_integrity_records(
384    conn: &mut SqliteConnection,
385    integrity: &IntegrityConfig,
386    records: &[EventRecord],
387) -> EventStoreResult<Vec<Event>> {
388    use std::collections::HashMap;
389
390    let mut previous_by_aggregate: HashMap<(String, String), EventSignature> = HashMap::new();
391    let mut events = Vec::with_capacity(records.len());
392
393    for record in records {
394        let event = record.to_event()?;
395        let stream = (event.aggregate_type.clone(), event.aggregate_id.clone());
396        let previous = match previous_by_aggregate.get(&stream) {
397            Some(sig) => sig.clone(),
398            None => previous_signature_for_aggregate(
399                conn,
400                Some(&event.aggregate_type),
401                &event.aggregate_id,
402                event.sequence,
403            )?,
404        };
405
406        let expected = integrity.chain.sign_event(&previous, &event)?;
407        let claimed = required_signature(record, &event.aggregate_id, event.sequence)?;
408
409        if expected != claimed {
410            return Err(EventStoreError::from(IntegrityError::BrokenAt {
411                aggregate_id: event.aggregate_id,
412                sequence: event.sequence,
413            }));
414        }
415
416        previous_by_aggregate.insert(stream, claimed);
417        events.push(event);
418    }
419
420    Ok(events)
421}
422
423#[async_trait]
424impl EventStore for SqliteEventStore {
425    async fn append(
426        &self,
427        aggregate_id: &str,
428        version_check: VersionCheck,
429        new_events: Vec<Event>,
430    ) -> EventStoreResult<()> {
431        let aggregate_type = new_events
432            .first()
433            .map(|event| event.aggregate_type.clone())
434            .unwrap_or_default();
435        self.append_to(&aggregate_type, aggregate_id, version_check, new_events)
436            .await
437    }
438
439    async fn append_to(
440        &self,
441        aggregate_type: &str,
442        aggregate_id: &str,
443        version_check: VersionCheck,
444        new_events: Vec<Event>,
445    ) -> EventStoreResult<()> {
446        if new_events.is_empty() {
447            return Ok(());
448        }
449
450        // Defense-in-depth: reject any event with invalid audit before touching the DB.
451        validate_audit_batch(aggregate_id, &new_events)?;
452
453        let aggregate_type = aggregate_type.to_string();
454        let aggregate_id = aggregate_id.to_string();
455        let pool = self.pool.clone();
456        let integrity = self.integrity.clone();
457
458        tokio::task::spawn_blocking(move || -> EventStoreResult<()> {
459            use diesel::connection::AnsiTransactionManager;
460            use diesel::connection::TransactionManager;
461
462            let mut conn = pool.get().map_err(|e| {
463                EventStoreError::database(format!("Failed to get connection: {}", e))
464            })?;
465
466            AnsiTransactionManager::begin_transaction(&mut *conn)
467                .map_err(|e| EventStoreError::database(e.to_string()))?;
468
469            let result = (|| -> EventStoreResult<()> {
470                let current_version = events::table
471                    .filter(events::aggregate_type.eq(&aggregate_type))
472                    .filter(events::aggregate_id.eq(&aggregate_id))
473                    .select(diesel::dsl::max(events::sequence))
474                    .first::<Option<i64>>(&mut *conn)
475                    .map_err(|e| EventStoreError::database(e.to_string()))?
476                    .unwrap_or(0);
477
478                if let Some(expected) = version_check.version() {
479                    if current_version != expected {
480                        return Err(EventStoreError::ConcurrencyConflict {
481                            aggregate_id: aggregate_id.clone(),
482                            expected,
483                            actual: current_version,
484                        });
485                    }
486                }
487
488                for (expected_sequence, event) in (current_version + 1..).zip(new_events.iter()) {
489                    if event.sequence != expected_sequence {
490                        return Err(EventStoreError::InvalidSequence {
491                            aggregate_id: aggregate_id.clone(),
492                            expected: expected_sequence,
493                            actual: event.sequence,
494                        });
495                    }
496                }
497
498                let mut previous_signature = if integrity.is_some() {
499                    previous_signature_for_aggregate(
500                        &mut conn,
501                        Some(&aggregate_type),
502                        &aggregate_id,
503                        current_version + 1,
504                    )?
505                } else {
506                    EventSignature::genesis()
507                };
508
509                for event in &new_events {
510                    let mut record = NewEventRecord::from_event(event, None, None)?;
511
512                    if let Some(integrity) = integrity.as_ref() {
513                        let mut persisted_event = event.clone();
514                        persisted_event.timestamp = (record.timestamp as u64) * 1000;
515                        let signature = integrity
516                            .chain
517                            .sign_event(&previous_signature, &persisted_event)
518                            .map_err(EventStoreError::from)?;
519                        previous_signature = signature.clone();
520                        record.integrity_signature = Some(signature.0);
521                        record.integrity_key_id = Some(integrity.key_id.clone());
522                    }
523
524                    diesel::insert_into(events::table)
525                        .values(&record)
526                        .execute(&mut *conn)
527                        .map_err(|e| EventStoreError::database(e.to_string()))?;
528                }
529
530                Ok(())
531            })();
532
533            match result {
534                Ok(_) => {
535                    AnsiTransactionManager::commit_transaction(&mut *conn)
536                        .map_err(|e| EventStoreError::database(e.to_string()))?;
537                    Ok(())
538                }
539                Err(e) => {
540                    let _ = AnsiTransactionManager::rollback_transaction(&mut *conn);
541                    Err(e)
542                }
543            }
544        })
545        .await
546        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
547    }
548
549    async fn load(&self, aggregate_id: &str) -> EventStoreResult<Vec<Event>> {
550        self.load_from(aggregate_id, 1).await
551    }
552
553    async fn load_stream(
554        &self,
555        aggregate_type: &str,
556        aggregate_id: &str,
557    ) -> EventStoreResult<Vec<Event>> {
558        self.load_stream_from(aggregate_type, aggregate_id, 1).await
559    }
560
561    async fn load_from(
562        &self,
563        aggregate_id: &str,
564        from_sequence: i64,
565    ) -> EventStoreResult<Vec<Event>> {
566        let aggregate_id = aggregate_id.to_string();
567        let pool = self.pool.clone();
568        let integrity = self.integrity.clone();
569
570        tokio::task::spawn_blocking(move || {
571            let mut conn = pool.get().map_err(|e| {
572                EventStoreError::database(format!("Failed to get connection: {}", e))
573            })?;
574
575            let records: Vec<EventRecord> = events::table
576                .filter(events::aggregate_id.eq(&aggregate_id))
577                .filter(events::sequence.ge(from_sequence))
578                .order(events::sequence.asc())
579                .load(&mut conn)
580                .map_err(|e| EventStoreError::database(e.to_string()))?;
581
582            match integrity.as_ref() {
583                Some(integrity) => {
584                    let previous = previous_signature_for_aggregate(
585                        &mut conn,
586                        None,
587                        &aggregate_id,
588                        from_sequence,
589                    )?;
590                    verify_integrity_records(integrity, &records, previous)
591                }
592                None => records.iter().map(|r| r.to_event()).collect(),
593            }
594        })
595        .await
596        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
597    }
598
599    async fn load_stream_from(
600        &self,
601        aggregate_type: &str,
602        aggregate_id: &str,
603        from_sequence: i64,
604    ) -> EventStoreResult<Vec<Event>> {
605        let aggregate_type = aggregate_type.to_string();
606        let aggregate_id = aggregate_id.to_string();
607        let pool = self.pool.clone();
608        let integrity = self.integrity.clone();
609
610        tokio::task::spawn_blocking(move || {
611            let mut conn = pool.get().map_err(|e| {
612                EventStoreError::database(format!("Failed to get connection: {}", e))
613            })?;
614
615            let records: Vec<EventRecord> = events::table
616                .filter(events::aggregate_type.eq(&aggregate_type))
617                .filter(events::aggregate_id.eq(&aggregate_id))
618                .filter(events::sequence.ge(from_sequence))
619                .order(events::sequence.asc())
620                .load(&mut conn)
621                .map_err(|e| EventStoreError::database(e.to_string()))?;
622
623            match integrity.as_ref() {
624                Some(integrity) => {
625                    let previous = previous_signature_for_aggregate(
626                        &mut conn,
627                        Some(&aggregate_type),
628                        &aggregate_id,
629                        from_sequence,
630                    )?;
631                    verify_integrity_records(integrity, &records, previous)
632                }
633                None => records.iter().map(|record| record.to_event()).collect(),
634            }
635        })
636        .await
637        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
638    }
639
640    async fn stream_all(&self, from_position: i64) -> EventStoreResult<Vec<Event>> {
641        let pool = self.pool.clone();
642        let integrity = self.integrity.clone();
643
644        tokio::task::spawn_blocking(move || {
645            let mut conn = pool.get().map_err(|e| {
646                EventStoreError::database(format!("Failed to get connection: {}", e))
647            })?;
648
649            let records: Vec<EventRecord> = events::table
650                .filter(events::id.ge(from_position as i32))
651                .order(events::id.asc())
652                .load(&mut conn)
653                .map_err(|e| EventStoreError::database(e.to_string()))?;
654
655            match integrity.as_ref() {
656                Some(integrity) => verify_stream_integrity_records(&mut conn, integrity, &records),
657                None => records.iter().map(|r| r.to_event()).collect(),
658            }
659        })
660        .await
661        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
662    }
663
664    async fn get_version(&self, aggregate_id: &str) -> EventStoreResult<i64> {
665        let aggregate_id = aggregate_id.to_string();
666        let pool = self.pool.clone();
667
668        tokio::task::spawn_blocking(move || {
669            let mut conn = pool.get().map_err(|e| {
670                EventStoreError::database(format!("Failed to get connection: {}", e))
671            })?;
672
673            let version = events::table
674                .filter(events::aggregate_id.eq(&aggregate_id))
675                .select(diesel::dsl::max(events::sequence))
676                .first::<Option<i64>>(&mut conn)
677                .map_err(|e| EventStoreError::database(e.to_string()))?
678                .unwrap_or(0);
679
680            Ok(version)
681        })
682        .await
683        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
684    }
685
686    async fn get_stream_version(
687        &self,
688        aggregate_type: &str,
689        aggregate_id: &str,
690    ) -> EventStoreResult<i64> {
691        let aggregate_type = aggregate_type.to_string();
692        let aggregate_id = aggregate_id.to_string();
693        let pool = self.pool.clone();
694
695        tokio::task::spawn_blocking(move || {
696            let mut conn = pool.get().map_err(|e| {
697                EventStoreError::database(format!("Failed to get connection: {}", e))
698            })?;
699            events::table
700                .filter(events::aggregate_type.eq(&aggregate_type))
701                .filter(events::aggregate_id.eq(&aggregate_id))
702                .select(diesel::dsl::max(events::sequence))
703                .first::<Option<i64>>(&mut conn)
704                .map(|version| version.unwrap_or(0))
705                .map_err(|e| EventStoreError::database(e.to_string()))
706        })
707        .await
708        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
709    }
710
711    async fn save_snapshot(&self, snapshot: &Snapshot) -> EventStoreResult<()> {
712        let record = NewSnapshotRecord::from_snapshot(snapshot)?;
713        let pool = self.pool.clone();
714
715        tokio::task::spawn_blocking(move || -> EventStoreResult<()> {
716            let mut conn = pool.get().map_err(|e| {
717                EventStoreError::database(format!("Failed to get connection: {}", e))
718            })?;
719
720            // One snapshot per aggregate: replace the stored row in place rather
721            // than accumulating stale versions.
722            diesel::insert_into(snapshots::table)
723                .values(&record)
724                .on_conflict((snapshots::aggregate_type, snapshots::aggregate_id))
725                .do_update()
726                .set((
727                    snapshots::version.eq(record.version),
728                    snapshots::state.eq(&record.state),
729                    snapshots::created_at.eq(record.created_at),
730                ))
731                .execute(&mut *conn)
732                .map_err(|e| EventStoreError::database(e.to_string()))?;
733
734            Ok(())
735        })
736        .await
737        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
738    }
739
740    async fn load_snapshot(&self, aggregate_id: &str) -> EventStoreResult<Option<Snapshot>> {
741        let aggregate_id = aggregate_id.to_string();
742        let pool = self.pool.clone();
743
744        tokio::task::spawn_blocking(move || {
745            let mut conn = pool.get().map_err(|e| {
746                EventStoreError::database(format!("Failed to get connection: {}", e))
747            })?;
748
749            let record: Option<SnapshotRecord> = snapshots::table
750                .filter(snapshots::aggregate_id.eq(&aggregate_id))
751                .first::<SnapshotRecord>(&mut conn)
752                .optional()
753                .map_err(|e| EventStoreError::database(e.to_string()))?;
754
755            record.map(|r| r.to_snapshot()).transpose()
756        })
757        .await
758        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
759    }
760
761    async fn load_snapshot_for(
762        &self,
763        aggregate_type: &str,
764        aggregate_id: &str,
765    ) -> EventStoreResult<Option<Snapshot>> {
766        let aggregate_type = aggregate_type.to_string();
767        let aggregate_id = aggregate_id.to_string();
768        let pool = self.pool.clone();
769
770        tokio::task::spawn_blocking(move || {
771            let mut conn = pool.get().map_err(|e| {
772                EventStoreError::database(format!("Failed to get connection: {}", e))
773            })?;
774
775            let record: Option<SnapshotRecord> = snapshots::table
776                .filter(snapshots::aggregate_type.eq(&aggregate_type))
777                .filter(snapshots::aggregate_id.eq(&aggregate_id))
778                .first::<SnapshotRecord>(&mut conn)
779                .optional()
780                .map_err(|e| EventStoreError::database(e.to_string()))?;
781
782            record.map(|record| record.to_snapshot()).transpose()
783        })
784        .await
785        .map_err(|e| EventStoreError::other(format!("Task join error: {}", e)))?
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792    use arc_core::audit::AuditMetadata;
793    use serde_json::json;
794
795    async fn setup_test_store() -> SqliteEventStore {
796        let manager = ConnectionManager::<SqliteConnection>::new(":memory:");
797        let pool = Pool::builder()
798            .max_size(1)
799            .build(manager)
800            .expect("Failed to create pool");
801
802        let mut conn = pool.get().expect("Failed to get connection");
803        crate::test_support::migrate(&mut conn);
804        drop(conn);
805
806        SqliteEventStore::with_pool(pool)
807    }
808
809    async fn setup_integrity_test_store() -> SqliteEventStore {
810        let manager = ConnectionManager::<SqliteConnection>::new(":memory:");
811        let pool = Pool::builder()
812            .max_size(1)
813            .build(manager)
814            .expect("Failed to create pool");
815
816        let mut conn = pool.get().expect("Failed to get connection");
817        crate::test_support::migrate(&mut conn);
818        drop(conn);
819
820        SqliteEventStore::with_pool_and_integrity_key(pool, integrity_key(), "test-key")
821            .expect("integrity store")
822    }
823
824    fn integrity_key() -> Vec<u8> {
825        b"012345678901234567890123456789AB".to_vec()
826    }
827
828    /// Helper: build an event with stamped audit.
829    fn stamped_event(
830        agg_type: &str,
831        agg_id: &str,
832        sequence: i64,
833        event_type: &str,
834        payload: serde_json::Value,
835    ) -> Event {
836        Event::new(NewEvent {
837            aggregate_type: agg_type,
838            aggregate_id: agg_id,
839            sequence,
840            event_type,
841            payload,
842        })
843        .with_audit(AuditMetadata::test_default())
844    }
845
846    #[derive(QueryableByName, Debug)]
847    struct SignatureRow {
848        #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
849        integrity_signature: Option<String>,
850        #[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
851        integrity_key_id: Option<String>,
852    }
853
854    #[tokio::test]
855    async fn test_append_and_load_single_event() {
856        let store = setup_test_store().await;
857        let event = stamped_event(
858            "User",
859            "user-123",
860            1,
861            "UserCreated",
862            json!({ "name": "Alice" }),
863        );
864
865        store
866            .append("user-123", VersionCheck::New, vec![event.clone()])
867            .await
868            .unwrap();
869        let loaded = store.load("user-123").await.unwrap();
870
871        assert_eq!(loaded.len(), 1);
872        assert_eq!(loaded[0].aggregate_id, "user-123");
873        assert_eq!(loaded[0].event_type, "UserCreated");
874        assert_eq!(loaded[0].sequence, 1);
875        assert_eq!(loaded[0].audit.actor_id, "test");
876    }
877
878    #[tokio::test]
879    async fn same_instance_id_is_isolated_by_aggregate_type() {
880        let store = setup_test_store().await;
881        store
882            .append_to(
883                "Product",
884                "shared-id",
885                VersionCheck::New,
886                vec![stamped_event(
887                    "Product",
888                    "shared-id",
889                    1,
890                    "ProductCreated",
891                    json!({}),
892                )],
893            )
894            .await
895            .unwrap();
896        store
897            .append_to(
898                "Order",
899                "shared-id",
900                VersionCheck::New,
901                vec![stamped_event(
902                    "Order",
903                    "shared-id",
904                    1,
905                    "OrderPlaced",
906                    json!({}),
907                )],
908            )
909            .await
910            .unwrap();
911
912        assert_eq!(
913            store.load_stream("Product", "shared-id").await.unwrap()[0].event_type,
914            "ProductCreated"
915        );
916        assert_eq!(
917            store.load_stream("Order", "shared-id").await.unwrap()[0].event_type,
918            "OrderPlaced"
919        );
920    }
921
922    #[tokio::test]
923    async fn test_append_multiple_events() {
924        let store = setup_test_store().await;
925        let events = vec![
926            stamped_event("User", "user-456", 1, "UserCreated", json!({})),
927            stamped_event("User", "user-456", 2, "ProfileUpdated", json!({})),
928            stamped_event("User", "user-456", 3, "EmailChanged", json!({})),
929        ];
930
931        store
932            .append("user-456", VersionCheck::New, events)
933            .await
934            .unwrap();
935        let loaded = store.load("user-456").await.unwrap();
936
937        assert_eq!(loaded.len(), 3);
938        assert_eq!(loaded[0].sequence, 1);
939        assert_eq!(loaded[2].sequence, 3);
940    }
941
942    #[tokio::test]
943    async fn test_integrity_append_persists_signatures() {
944        let store = setup_integrity_test_store().await;
945        store
946            .append(
947                "signed-1",
948                VersionCheck::New,
949                vec![
950                    stamped_event("User", "signed-1", 1, "UserCreated", json!({})),
951                    stamped_event("User", "signed-1", 2, "ProfileUpdated", json!({})),
952                ],
953            )
954            .await
955            .unwrap();
956
957        let pool = store.pool.clone();
958        let rows = tokio::task::spawn_blocking(move || -> EventStoreResult<Vec<SignatureRow>> {
959            let mut conn = pool
960                .get()
961                .map_err(|e| EventStoreError::database(e.to_string()))?;
962            diesel::sql_query(
963                "SELECT integrity_signature, integrity_key_id
964                 FROM events WHERE aggregate_id = 'signed-1' ORDER BY sequence",
965            )
966            .load(&mut *conn)
967            .map_err(|e| EventStoreError::database(e.to_string()))
968        })
969        .await
970        .unwrap()
971        .unwrap();
972
973        assert_eq!(rows.len(), 2);
974        for row in rows {
975            assert_eq!(row.integrity_signature.as_deref().map(str::len), Some(64));
976            assert_eq!(row.integrity_key_id.as_deref(), Some("test-key"));
977        }
978    }
979
980    #[tokio::test]
981    async fn test_integrity_load_rejects_tampered_payload() {
982        let store = setup_integrity_test_store().await;
983        store
984            .append(
985                "tamper-1",
986                VersionCheck::New,
987                vec![stamped_event(
988                    "User",
989                    "tamper-1",
990                    1,
991                    "UserCreated",
992                    json!({"ok": true}),
993                )],
994            )
995            .await
996            .unwrap();
997
998        let pool = store.pool.clone();
999        tokio::task::spawn_blocking(move || -> EventStoreResult<()> {
1000            let mut conn = pool
1001                .get()
1002                .map_err(|e| EventStoreError::database(e.to_string()))?;
1003            diesel::sql_query(
1004                "UPDATE events SET payload = '{\"ok\": false}' WHERE aggregate_id = 'tamper-1'",
1005            )
1006            .execute(&mut *conn)
1007            .map_err(|e| EventStoreError::database(e.to_string()))?;
1008            Ok(())
1009        })
1010        .await
1011        .unwrap()
1012        .unwrap();
1013
1014        let err = store.load("tamper-1").await.unwrap_err();
1015        assert!(
1016            matches!(err, EventStoreError::Integrity { .. }),
1017            "expected integrity error, got {err:?}"
1018        );
1019    }
1020
1021    #[tokio::test]
1022    async fn test_integrity_load_rejects_missing_signature() {
1023        let store = setup_integrity_test_store().await;
1024        store
1025            .append(
1026                "missing-sig",
1027                VersionCheck::New,
1028                vec![stamped_event(
1029                    "User",
1030                    "missing-sig",
1031                    1,
1032                    "UserCreated",
1033                    json!({}),
1034                )],
1035            )
1036            .await
1037            .unwrap();
1038
1039        let pool = store.pool.clone();
1040        tokio::task::spawn_blocking(move || -> EventStoreResult<()> {
1041            let mut conn = pool
1042                .get()
1043                .map_err(|e| EventStoreError::database(e.to_string()))?;
1044            diesel::sql_query(
1045                "UPDATE events SET integrity_signature = NULL WHERE aggregate_id = 'missing-sig'",
1046            )
1047            .execute(&mut *conn)
1048            .map_err(|e| EventStoreError::database(e.to_string()))?;
1049            Ok(())
1050        })
1051        .await
1052        .unwrap()
1053        .unwrap();
1054
1055        let err = store.load("missing-sig").await.unwrap_err();
1056        assert!(
1057            matches!(err, EventStoreError::Integrity { .. }),
1058            "expected integrity error, got {err:?}"
1059        );
1060    }
1061
1062    #[tokio::test]
1063    async fn test_integrity_load_from_uses_previous_signature() {
1064        let store = setup_integrity_test_store().await;
1065        store
1066            .append(
1067                "load-from-signed",
1068                VersionCheck::New,
1069                vec![
1070                    stamped_event("User", "load-from-signed", 1, "UserCreated", json!({})),
1071                    stamped_event("User", "load-from-signed", 2, "ProfileUpdated", json!({})),
1072                ],
1073            )
1074            .await
1075            .unwrap();
1076
1077        let loaded = store.load_from("load-from-signed", 2).await.unwrap();
1078        assert_eq!(loaded.len(), 1);
1079        assert_eq!(loaded[0].sequence, 2);
1080    }
1081
1082    #[tokio::test]
1083    async fn test_integrity_stream_all_verifies_per_aggregate() {
1084        let store = setup_integrity_test_store().await;
1085        store
1086            .append(
1087                "signed-a",
1088                VersionCheck::New,
1089                vec![stamped_event(
1090                    "User",
1091                    "signed-a",
1092                    1,
1093                    "UserCreated",
1094                    json!({}),
1095                )],
1096            )
1097            .await
1098            .unwrap();
1099        store
1100            .append(
1101                "signed-b",
1102                VersionCheck::New,
1103                vec![stamped_event(
1104                    "User",
1105                    "signed-b",
1106                    1,
1107                    "UserCreated",
1108                    json!({}),
1109                )],
1110            )
1111            .await
1112            .unwrap();
1113        store
1114            .append(
1115                "signed-a",
1116                VersionCheck::Expected(1),
1117                vec![stamped_event(
1118                    "User",
1119                    "signed-a",
1120                    2,
1121                    "ProfileUpdated",
1122                    json!({}),
1123                )],
1124            )
1125            .await
1126            .unwrap();
1127
1128        let loaded = store.stream_all(0).await.unwrap();
1129        assert_eq!(loaded.len(), 3);
1130    }
1131
1132    #[tokio::test]
1133    async fn test_optimistic_concurrency_control() {
1134        let store = setup_test_store().await;
1135        store
1136            .append(
1137                "user-789",
1138                VersionCheck::New,
1139                vec![stamped_event(
1140                    "User",
1141                    "user-789",
1142                    1,
1143                    "UserCreated",
1144                    json!({}),
1145                )],
1146            )
1147            .await
1148            .unwrap();
1149        store
1150            .append(
1151                "user-789",
1152                VersionCheck::Expected(1),
1153                vec![stamped_event(
1154                    "User",
1155                    "user-789",
1156                    2,
1157                    "ProfileUpdated",
1158                    json!({}),
1159                )],
1160            )
1161            .await
1162            .unwrap();
1163        let result = store
1164            .append(
1165                "user-789",
1166                VersionCheck::Expected(1),
1167                vec![stamped_event(
1168                    "User",
1169                    "user-789",
1170                    3,
1171                    "EmailChanged",
1172                    json!({}),
1173                )],
1174            )
1175            .await;
1176        assert!(matches!(
1177            result,
1178            Err(EventStoreError::ConcurrencyConflict {
1179                expected: 1,
1180                actual: 2,
1181                ..
1182            })
1183        ));
1184    }
1185
1186    #[tokio::test]
1187    async fn test_invalid_sequence() {
1188        let store = setup_test_store().await;
1189        let result = store
1190            .append(
1191                "user-999",
1192                VersionCheck::New,
1193                vec![stamped_event(
1194                    "User",
1195                    "user-999",
1196                    5,
1197                    "UserCreated",
1198                    json!({}),
1199                )],
1200            )
1201            .await;
1202        assert!(matches!(
1203            result,
1204            Err(EventStoreError::InvalidSequence {
1205                expected: 1,
1206                actual: 5,
1207                ..
1208            })
1209        ));
1210    }
1211
1212    #[tokio::test]
1213    async fn test_load_from_sequence() {
1214        let store = setup_test_store().await;
1215        let events = vec![
1216            stamped_event("Order", "order-1", 1, "OrderCreated", json!({})),
1217            stamped_event("Order", "order-1", 2, "ItemAdded", json!({})),
1218            stamped_event("Order", "order-1", 3, "ItemAdded", json!({})),
1219            stamped_event("Order", "order-1", 4, "OrderShipped", json!({})),
1220        ];
1221        store
1222            .append("order-1", VersionCheck::New, events)
1223            .await
1224            .unwrap();
1225        let loaded = store.load_from("order-1", 3).await.unwrap();
1226        assert_eq!(loaded.len(), 2);
1227        assert_eq!(loaded[0].sequence, 3);
1228    }
1229
1230    #[tokio::test]
1231    async fn test_get_version() {
1232        let store = setup_test_store().await;
1233        assert_eq!(store.get_version("nope").await.unwrap(), 0);
1234        let events = vec![
1235            stamped_event("User", "u1", 1, "UserCreated", json!({})),
1236            stamped_event("User", "u1", 2, "ProfileUpdated", json!({})),
1237            stamped_event("User", "u1", 3, "EmailChanged", json!({})),
1238        ];
1239        store.append("u1", VersionCheck::New, events).await.unwrap();
1240        assert_eq!(store.get_version("u1").await.unwrap(), 3);
1241    }
1242
1243    #[tokio::test]
1244    async fn test_stream_all() {
1245        let store = setup_test_store().await;
1246        store
1247            .append(
1248                "user-1",
1249                VersionCheck::New,
1250                vec![
1251                    stamped_event("User", "user-1", 1, "UserCreated", json!({})),
1252                    stamped_event("User", "user-1", 2, "ProfileUpdated", json!({})),
1253                ],
1254            )
1255            .await
1256            .unwrap();
1257        store
1258            .append(
1259                "order-1",
1260                VersionCheck::New,
1261                vec![
1262                    stamped_event("Order", "order-1", 1, "OrderCreated", json!({})),
1263                    stamped_event("Order", "order-1", 2, "OrderShipped", json!({})),
1264                ],
1265            )
1266            .await
1267            .unwrap();
1268        assert_eq!(store.stream_all(0).await.unwrap().len(), 4);
1269    }
1270
1271    #[tokio::test]
1272    async fn test_empty_aggregate() {
1273        let store = setup_test_store().await;
1274        assert_eq!(store.load("nothing").await.unwrap().len(), 0);
1275    }
1276
1277    #[tokio::test]
1278    async fn test_audit_roundtrip_preserves_all_fields() {
1279        let store = setup_test_store().await;
1280        let mut audit = AuditMetadata::test_default();
1281        audit.actor_id = "user-uuid-42".to_string();
1282        audit.actor_session_id = Some("sess-XYZ".to_string());
1283        audit.source_ip = Some("10.0.0.42".to_string());
1284        audit.user_agent = Some("Mozilla/5.0 (test)".to_string());
1285        audit.causation_id = Some(Uuid::new_v4());
1286        let expected_corr = audit.correlation_id;
1287        let expected_caus = audit.causation_id;
1288
1289        let event = Event::new(NewEvent {
1290            aggregate_type: "User",
1291            aggregate_id: "u-audit",
1292            sequence: 1,
1293            event_type: "UserCreated",
1294            payload: json!({}),
1295        })
1296        .with_audit(audit.clone());
1297
1298        store
1299            .append("u-audit", VersionCheck::New, vec![event])
1300            .await
1301            .unwrap();
1302        let loaded = store.load("u-audit").await.unwrap();
1303
1304        assert_eq!(loaded[0].audit.actor_id, "user-uuid-42");
1305        assert_eq!(
1306            loaded[0].audit.actor_session_id.as_deref(),
1307            Some("sess-XYZ")
1308        );
1309        assert_eq!(loaded[0].audit.source_ip.as_deref(), Some("10.0.0.42"));
1310        assert_eq!(
1311            loaded[0].audit.user_agent.as_deref(),
1312            Some("Mozilla/5.0 (test)")
1313        );
1314        assert_eq!(loaded[0].audit.correlation_id, expected_corr);
1315        assert_eq!(loaded[0].audit.causation_id, expected_caus);
1316        assert!(loaded[0].audit.timestamp_utc_us > 0);
1317    }
1318
1319    #[tokio::test]
1320    async fn test_append_rejects_pending_audit() {
1321        let store = setup_test_store().await;
1322        // Built without with_audit — audit stays pending.
1323        let event = Event::new(NewEvent {
1324            aggregate_type: "User",
1325            aggregate_id: "u-bad",
1326            sequence: 1,
1327            event_type: "UserCreated",
1328            payload: json!({}),
1329        });
1330        let err = store
1331            .append("u-bad", VersionCheck::New, vec![event])
1332            .await
1333            .unwrap_err();
1334        assert!(matches!(err, EventStoreError::InvalidAudit { .. }));
1335
1336        // No row should have been written.
1337        assert_eq!(store.load("u-bad").await.unwrap().len(), 0);
1338    }
1339
1340    #[tokio::test]
1341    async fn test_actor_id_index_used() {
1342        let store = setup_test_store().await;
1343        let mut a = AuditMetadata::test_default();
1344        a.actor_id = "alice-uuid".to_string();
1345        let event = Event::new(NewEvent {
1346            aggregate_type: "User",
1347            aggregate_id: "u1",
1348            sequence: 1,
1349            event_type: "UserCreated",
1350            payload: json!({}),
1351        })
1352        .with_audit(a);
1353        store
1354            .append("u1", VersionCheck::New, vec![event])
1355            .await
1356            .unwrap();
1357
1358        // EXPLAIN QUERY PLAN must show an index search on actor_id
1359        let pool = store.pool.clone();
1360        let plan = tokio::task::spawn_blocking(move || -> EventStoreResult<Vec<String>> {
1361            let mut conn = pool
1362                .get()
1363                .map_err(|e| EventStoreError::database(e.to_string()))?;
1364            let plan: Vec<(i32, i32, i32, String)> = diesel::sql_query(
1365                "EXPLAIN QUERY PLAN SELECT * FROM events WHERE actor_id = 'alice-uuid'",
1366            )
1367            .load::<ExplainRow>(&mut *conn)
1368            .map_err(|e| EventStoreError::database(e.to_string()))?
1369            .into_iter()
1370            .map(|r| (r.id, r.parent, r.notused, r.detail))
1371            .collect();
1372            Ok(plan.into_iter().map(|(_, _, _, d)| d).collect())
1373        })
1374        .await
1375        .unwrap()
1376        .unwrap();
1377
1378        assert!(
1379            plan.iter().any(|d| d.contains("idx_events_actor_id")),
1380            "actor_id query did not use index; plan: {:?}",
1381            plan
1382        );
1383    }
1384
1385    #[derive(QueryableByName, Debug)]
1386    struct ExplainRow {
1387        #[diesel(sql_type = diesel::sql_types::Integer)]
1388        id: i32,
1389        #[diesel(sql_type = diesel::sql_types::Integer)]
1390        parent: i32,
1391        #[diesel(sql_type = diesel::sql_types::Integer)]
1392        notused: i32,
1393        #[diesel(sql_type = diesel::sql_types::Text)]
1394        detail: String,
1395    }
1396
1397    #[tokio::test]
1398    async fn test_concurrent_appends() {
1399        let store = setup_test_store().await;
1400        store
1401            .append(
1402                "uc",
1403                VersionCheck::New,
1404                vec![stamped_event("User", "uc", 1, "UserCreated", json!({}))],
1405            )
1406            .await
1407            .unwrap();
1408
1409        let s1 = store.clone();
1410        let s2 = store.clone();
1411        let h1 = tokio::spawn(async move {
1412            s1.append(
1413                "uc",
1414                VersionCheck::Expected(1),
1415                vec![stamped_event("User", "uc", 2, "U1", json!({}))],
1416            )
1417            .await
1418        });
1419        let h2 = tokio::spawn(async move {
1420            s2.append(
1421                "uc",
1422                VersionCheck::Expected(1),
1423                vec![stamped_event("User", "uc", 2, "U2", json!({}))],
1424            )
1425            .await
1426        });
1427        let r1 = h1.await.unwrap();
1428        let r2 = h2.await.unwrap();
1429        assert!(r1.is_ok() != r2.is_ok());
1430    }
1431
1432    #[tokio::test]
1433    async fn test_sequence_above_i32_max_roundtrips_without_truncation() {
1434        // Pre-fix bug: sequence was cast to i32 on insert and read back as i32.
1435        // A value above i32::MAX (2_147_483_647) would silently overflow.
1436        // After widening, this round-trips intact.
1437        let store = setup_test_store().await;
1438        let pool = store.pool.clone();
1439
1440        let huge_seq: i64 = (i32::MAX as i64) + 1234;
1441        let huge_ts: i64 = 9_999_999_999; // year 2286 — would never fit in i32
1442
1443        let inserted = tokio::task::spawn_blocking(move || -> EventStoreResult<usize> {
1444            let mut conn = pool
1445                .get()
1446                .map_err(|e| EventStoreError::database(e.to_string()))?;
1447            diesel::sql_query(format!(
1448                "INSERT INTO events (event_id, aggregate_type, aggregate_id, sequence,
1449                    event_type, payload, timestamp,
1450                    actor_id, timestamp_utc_us, correlation_id)
1451                 VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'User', 'u-big', {seq},
1452                    'Event', '{{}}', {ts},
1453                    'tester', {seq}, '00000000-0000-0000-0000-000000000001')",
1454                seq = huge_seq,
1455                ts = huge_ts,
1456            ))
1457            .execute(&mut *conn)
1458            .map_err(|e| EventStoreError::database(e.to_string()))
1459        })
1460        .await
1461        .unwrap()
1462        .unwrap();
1463        assert_eq!(inserted, 1);
1464
1465        let loaded = store.load("u-big").await.expect("load");
1466        assert_eq!(loaded.len(), 1);
1467        assert_eq!(loaded[0].sequence, huge_seq, "sequence must not truncate");
1468        // timestamp stored as seconds; round-trip back to milliseconds in Event::timestamp
1469        assert_eq!(loaded[0].timestamp, (huge_ts as u64) * 1000);
1470
1471        let v = store.get_version("u-big").await.expect("version");
1472        assert_eq!(v, huge_seq, "get_version must not truncate either");
1473    }
1474
1475    #[tokio::test]
1476    async fn test_legacy_backfilled_row_roundtrips() {
1477        // Simulates a row written before HIPAA-1, then backfilled by the migration:
1478        // actor_id='legacy-pre-hipaa', timestamp_utc_us derived from seconds*1_000_000,
1479        // correlation_id = nil UUID. The row must load without panicking.
1480        let store = setup_test_store().await;
1481        let pool = store.pool.clone();
1482
1483        let inserted_count = tokio::task::spawn_blocking(move || -> EventStoreResult<usize> {
1484            let mut conn = pool
1485                .get()
1486                .map_err(|e| EventStoreError::database(e.to_string()))?;
1487            diesel::sql_query(
1488                "INSERT INTO events (event_id, aggregate_type, aggregate_id, sequence,
1489                    event_type, payload, timestamp,
1490                    actor_id, timestamp_utc_us, correlation_id)
1491                 VALUES ('11111111-1111-1111-1111-111111111111', 'User', 'u-legacy', 1,
1492                    'UserCreated', '{}', 1700000000,
1493                    'legacy-pre-hipaa', 1700000000000000,
1494                    '00000000-0000-0000-0000-000000000000')",
1495            )
1496            .execute(&mut *conn)
1497            .map_err(|e| EventStoreError::database(e.to_string()))
1498        })
1499        .await
1500        .unwrap()
1501        .unwrap();
1502        assert_eq!(inserted_count, 1);
1503
1504        let loaded = store.load("u-legacy").await.expect("legacy row must load");
1505        assert_eq!(loaded.len(), 1);
1506        assert_eq!(loaded[0].audit.actor_id, "legacy-pre-hipaa");
1507        assert_eq!(loaded[0].audit.timestamp_utc_us, 1_700_000_000_000_000);
1508        assert_eq!(loaded[0].audit.correlation_id, Uuid::nil());
1509        assert!(loaded[0].audit.causation_id.is_none());
1510    }
1511
1512    #[tokio::test]
1513    async fn test_load_rejects_malformed_correlation_uuid() {
1514        let store = setup_test_store().await;
1515        let pool = store.pool.clone();
1516
1517        tokio::task::spawn_blocking(move || -> EventStoreResult<()> {
1518            let mut conn = pool
1519                .get()
1520                .map_err(|e| EventStoreError::database(e.to_string()))?;
1521            diesel::sql_query(
1522                "INSERT INTO events (event_id, aggregate_type, aggregate_id, sequence,
1523                    event_type, payload, timestamp,
1524                    actor_id, timestamp_utc_us, correlation_id)
1525                 VALUES ('22222222-2222-2222-2222-222222222222', 'User', 'u-bad', 1,
1526                    'X', '{}', 1700000000,
1527                    'tester', 1700000000000000,
1528                    'not-a-uuid')",
1529            )
1530            .execute(&mut *conn)
1531            .map_err(|e| EventStoreError::database(e.to_string()))?;
1532            Ok(())
1533        })
1534        .await
1535        .unwrap()
1536        .unwrap();
1537
1538        let err = store.load("u-bad").await.unwrap_err();
1539        assert!(
1540            matches!(err, EventStoreError::SerializationError { ref message } if message.contains("Invalid correlation UUID")),
1541            "expected SerializationError on malformed correlation_id, got {:?}",
1542            err
1543        );
1544    }
1545
1546    #[tokio::test]
1547    async fn test_caused_by_chain_roundtrips_through_sqlite() {
1548        use arc_core::aggregate::{Aggregate, Command};
1549        use arc_core::command_bus::{CommandBus, CommandContext};
1550        use arc_core::event::Event as CoreEvent;
1551        use arc_core::event_bus::InProcessEventBus;
1552
1553        // Trivial aggregate so we can exercise CommandBus + SqliteEventStore together.
1554        #[derive(Default)]
1555        struct Counter {
1556            v: i64,
1557        }
1558        struct Cmd {
1559            id: String,
1560        }
1561        impl Command for Cmd {
1562            fn aggregate_id(&self) -> &str {
1563                &self.id
1564            }
1565        }
1566        #[derive(Debug, thiserror::Error)]
1567        #[error("never")]
1568        struct Never;
1569        #[async_trait]
1570        impl Aggregate for Counter {
1571            type Command = Cmd;
1572            type Event = ();
1573            type Error = Never;
1574            fn aggregate_type() -> &'static str {
1575                "Counter"
1576            }
1577            fn version(&self) -> i64 {
1578                self.v
1579            }
1580            async fn handle(&self, c: Self::Command) -> Result<Vec<CoreEvent>, Self::Error> {
1581                Ok(vec![CoreEvent::new(NewEvent {
1582                    aggregate_type: "Counter",
1583                    aggregate_id: &c.id,
1584                    sequence: self.v + 1,
1585                    event_type: "Incremented",
1586                    payload: serde_json::json!({}),
1587                })])
1588            }
1589            fn apply(&mut self, e: &CoreEvent) {
1590                self.v = e.sequence;
1591            }
1592        }
1593
1594        let store = setup_test_store().await;
1595        let bus =
1596            CommandBus::<Counter>::new(Box::new(store.clone()), Box::new(InProcessEventBus::new()));
1597
1598        let first_ctx = CommandContext::for_actor("alice");
1599        let trigger_corr = first_ctx.correlation_id;
1600        let triggers = bus
1601            .dispatch(Cmd { id: "c1".into() }, first_ctx)
1602            .await
1603            .unwrap();
1604
1605        let follow_ctx = CommandContext::caused_by("worker", &triggers[0]);
1606        let follow_corr = follow_ctx.correlation_id;
1607        let _ = bus
1608            .dispatch(Cmd { id: "c2".into() }, follow_ctx)
1609            .await
1610            .unwrap();
1611
1612        // Reload from SQLite — chain must survive the trip
1613        let loaded_c2 = store.load("c2").await.unwrap();
1614        assert_eq!(loaded_c2.len(), 1);
1615        assert_eq!(loaded_c2[0].audit.correlation_id, trigger_corr);
1616        assert_eq!(loaded_c2[0].audit.correlation_id, follow_corr);
1617        assert_eq!(loaded_c2[0].audit.causation_id, Some(triggers[0].event_id));
1618    }
1619
1620    #[tokio::test]
1621    async fn test_event_ordering_within_aggregate() {
1622        let store = setup_test_store().await;
1623        store
1624            .append(
1625                "uo",
1626                VersionCheck::New,
1627                vec![
1628                    stamped_event("User", "uo", 1, "UserCreated", json!({})),
1629                    stamped_event("User", "uo", 2, "EmailChanged", json!({})),
1630                ],
1631            )
1632            .await
1633            .unwrap();
1634        store
1635            .append(
1636                "uo",
1637                VersionCheck::Expected(2),
1638                vec![
1639                    stamped_event("User", "uo", 3, "ProfileUpdated", json!({})),
1640                    stamped_event("User", "uo", 4, "PasswordChanged", json!({})),
1641                ],
1642            )
1643            .await
1644            .unwrap();
1645        let loaded = store.load("uo").await.unwrap();
1646        for (i, e) in loaded.iter().enumerate() {
1647            assert_eq!(e.sequence, (i + 1) as i64);
1648        }
1649    }
1650
1651    #[tokio::test]
1652    async fn test_save_then_load_snapshot() {
1653        let store = setup_test_store().await;
1654        let snap = Snapshot::new("agg-1", "User", 5, json!({ "name": "Alice" }));
1655        store.save_snapshot(&snap).await.unwrap();
1656        let loaded = store.load_snapshot("agg-1").await.unwrap();
1657        assert_eq!(loaded, Some(snap));
1658    }
1659
1660    #[tokio::test]
1661    async fn test_load_snapshot_unknown_returns_none() {
1662        let store = setup_test_store().await;
1663        assert_eq!(store.load_snapshot("missing").await.unwrap(), None);
1664    }
1665
1666    #[tokio::test]
1667    async fn test_save_snapshot_upserts_newer_version() {
1668        let store = setup_test_store().await;
1669        store
1670            .save_snapshot(&Snapshot::new("agg-2", "User", 3, json!({ "v": 3 })))
1671            .await
1672            .unwrap();
1673        let newer = Snapshot::new("agg-2", "User", 9, json!({ "v": 9 }));
1674        store.save_snapshot(&newer).await.unwrap();
1675        let loaded = store.load_snapshot("agg-2").await.unwrap().unwrap();
1676        assert_eq!(loaded.version, 9);
1677        assert_eq!(loaded.state["v"], 9);
1678    }
1679}