1use 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
27pub 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#[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 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#[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#[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 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 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 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 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 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 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 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; 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 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 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 #[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 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}