1use std::sync::{Arc, Mutex};
30
31use mempill_core::ports::pending_adjudication::{PendingAdjudicationPort, PendingAdjudicationRow};
32use mempill_core::ports::persistence::PersistencePort;
33use mempill_types::{
34 claim::{Cardinality, Claim, Confidence, Criticality, Fact},
35 edge::{ClaimEdge, EdgeKind},
36 identity::{AgentId, ClaimRef},
37 ledger::{LedgerEntry, LedgerEventKind},
38 provenance::{ExternalAnchor, ExternalKind, ProvenanceLabel},
39 time::{date_granularity_to_str, str_to_date_granularity, TransactionTime, ValidTime},
40 validity::{AssertionKind, ValidityAssertion},
41};
42use rusqlite::Connection;
43
44use crate::{txn::SqliteTxn, SqliteStoreError};
45
46pub struct SqlitePersistenceStore {
54 conn: Arc<Mutex<Option<Box<Connection>>>>,
56}
57
58impl SqlitePersistenceStore {
59 pub fn new(conn: Connection) -> Self {
61 Self {
62 conn: Arc::new(Mutex::new(Some(Box::new(conn)))),
63 }
64 }
65
66 pub fn pending_store(&self) -> SqlitePendingStore {
77 SqlitePendingStore::new(Arc::clone(&self.conn))
78 }
79}
80
81unsafe impl Send for SqlitePersistenceStore {}
83unsafe impl Sync for SqlitePersistenceStore {}
84
85fn provenance_to_str(p: &ProvenanceLabel) -> &'static str {
91 match p {
92 ProvenanceLabel::ModelDerived => "ModelDerived",
93 ProvenanceLabel::RecallReEntry => "RecallReEntry",
94 ProvenanceLabel::External(ExternalKind::UserAsserted) => "External_UserAsserted",
95 ProvenanceLabel::External(ExternalKind::ExternalFirstHand) => "External_ExternalFirstHand",
96 _ => "Unknown",
98 }
99}
100
101fn str_to_provenance(s: &str) -> Result<ProvenanceLabel, SqliteStoreError> {
104 match s {
105 "ModelDerived" => Ok(ProvenanceLabel::ModelDerived),
106 "RecallReEntry" => Ok(ProvenanceLabel::RecallReEntry),
107 "External_UserAsserted" => {
108 Ok(ProvenanceLabel::External(ExternalKind::UserAsserted))
109 }
110 "External_ExternalFirstHand" => {
111 Ok(ProvenanceLabel::External(ExternalKind::ExternalFirstHand))
112 }
113 other => Err(SqliteStoreError::Mapping(format!(
114 "unknown provenance_label value: {other}"
115 ))),
116 }
117}
118
119fn cardinality_to_str(c: &Cardinality) -> &'static str {
120 match c {
121 Cardinality::Functional => "Functional",
122 Cardinality::SetValued => "SetValued",
123 Cardinality::Unknown => "Unknown",
124 }
125}
126
127fn str_to_cardinality(s: &str) -> Result<Cardinality, SqliteStoreError> {
128 match s {
129 "Functional" => Ok(Cardinality::Functional),
130 "SetValued" => Ok(Cardinality::SetValued),
131 "Unknown" => Ok(Cardinality::Unknown),
132 other => Err(SqliteStoreError::Mapping(format!(
133 "unknown cardinality value: {other}"
134 ))),
135 }
136}
137
138fn criticality_to_str(c: &Criticality) -> &'static str {
139 match c {
140 Criticality::Low => "Low",
141 Criticality::Medium => "Medium",
142 Criticality::High => "High",
143 Criticality::Critical => "Critical",
144 }
145}
146
147fn str_to_criticality(s: &str) -> Result<Criticality, SqliteStoreError> {
148 match s {
149 "Low" => Ok(Criticality::Low),
150 "Medium" => Ok(Criticality::Medium),
151 "High" => Ok(Criticality::High),
152 "Critical" => Ok(Criticality::Critical),
153 other => Err(SqliteStoreError::Mapping(format!(
154 "unknown criticality value: {other}"
155 ))),
156 }
157}
158
159fn edge_kind_to_str(k: &EdgeKind) -> &'static str {
160 match k {
161 EdgeKind::DerivedFrom => "DerivedFrom",
162 EdgeKind::Supersedes => "Supersedes",
163 EdgeKind::DependsOn => "DependsOn",
164 EdgeKind::MutualExclusion => "MutualExclusion",
165 _ => "Unknown",
167 }
168}
169
170fn str_to_edge_kind(s: &str) -> Result<EdgeKind, SqliteStoreError> {
171 match s {
172 "DerivedFrom" => Ok(EdgeKind::DerivedFrom),
173 "Supersedes" => Ok(EdgeKind::Supersedes),
174 "DependsOn" => Ok(EdgeKind::DependsOn),
175 "MutualExclusion" => Ok(EdgeKind::MutualExclusion),
176 other => Err(SqliteStoreError::Mapping(format!(
177 "unknown edge_kind value: {other}"
178 ))),
179 }
180}
181
182fn ledger_event_kind_to_str(k: &LedgerEventKind) -> &'static str {
183 match k {
185 LedgerEventKind::ClaimCommitted => "ClaimCommitted",
186 LedgerEventKind::ValidityAsserted => "ValidityAsserted",
187 LedgerEventKind::AdjudicationRequested => "AdjudicationRequested",
188 LedgerEventKind::AdjudicationResolved => "AdjudicationResolved",
189 LedgerEventKind::RecallReEntryDetected => "RecallReEntryDetected",
190 LedgerEventKind::Quarantined => "Quarantined",
191 LedgerEventKind::DependentFlaggedPendingReview => "DependentFlaggedPendingReview",
192 LedgerEventKind::ServedAsInjected => "ServedAsInjected",
193 LedgerEventKind::AdjudicationExpired => "AdjudicationExpired",
194 _ => "Unknown",
196 }
197}
198
199fn str_to_ledger_event_kind(s: &str) -> Result<LedgerEventKind, SqliteStoreError> {
200 match s {
201 "ClaimCommitted" => Ok(LedgerEventKind::ClaimCommitted),
202 "ValidityAsserted" => Ok(LedgerEventKind::ValidityAsserted),
203 "AdjudicationRequested" => Ok(LedgerEventKind::AdjudicationRequested),
204 "AdjudicationResolved" => Ok(LedgerEventKind::AdjudicationResolved),
205 "RecallReEntryDetected" => Ok(LedgerEventKind::RecallReEntryDetected),
206 "Quarantined" => Ok(LedgerEventKind::Quarantined),
207 "DependentFlaggedPendingReview" => Ok(LedgerEventKind::DependentFlaggedPendingReview),
208 "ServedAsInjected" => Ok(LedgerEventKind::ServedAsInjected),
209 "AdjudicationExpired" => Ok(LedgerEventKind::AdjudicationExpired),
210 other => Err(SqliteStoreError::Mapping(format!(
211 "unknown ledger event_kind value: {other}"
212 ))),
213 }
214}
215
216fn disposition_to_str(d: &mempill_types::disposition::Disposition) -> &'static str {
217 use mempill_types::disposition::Disposition;
218 match d {
219 Disposition::CommittedCheap => "CommittedCheap",
220 Disposition::CommittedInferred => "CommittedInferred",
221 Disposition::QueuedForAdjudication => "QueuedForAdjudication",
222 Disposition::Contested => "Contested",
223 Disposition::PendingConflict => "PendingConflict",
224 Disposition::PendingReview => "PendingReview",
225 Disposition::PendingLowConfidence => "PendingLowConfidence",
226 Disposition::Quarantined => "Quarantined",
227 Disposition::Superseded => "Superseded",
228 Disposition::Invalidated => "Invalidated",
229 Disposition::Reinstated => "Reinstated",
230 Disposition::Rejected => "Rejected",
231 _ => "Unknown",
233 }
234}
235
236fn str_to_disposition(s: &str) -> Result<mempill_types::disposition::Disposition, SqliteStoreError> {
237 use mempill_types::disposition::Disposition;
238 match s {
239 "CommittedCheap" => Ok(Disposition::CommittedCheap),
240 "CommittedInferred" => Ok(Disposition::CommittedInferred),
241 "QueuedForAdjudication" => Ok(Disposition::QueuedForAdjudication),
242 "Contested" => Ok(Disposition::Contested),
243 "PendingConflict" => Ok(Disposition::PendingConflict),
244 "PendingReview" => Ok(Disposition::PendingReview),
245 "PendingLowConfidence" => Ok(Disposition::PendingLowConfidence),
246 "Quarantined" => Ok(Disposition::Quarantined),
247 "Superseded" => Ok(Disposition::Superseded),
248 "Invalidated" => Ok(Disposition::Invalidated),
249 "Reinstated" => Ok(Disposition::Reinstated),
250 "Rejected" => Ok(Disposition::Rejected),
251 other => Err(SqliteStoreError::Mapping(format!(
252 "unknown disposition value: {other}"
253 ))),
254 }
255}
256
257fn row_to_claim(row: &rusqlite::Row<'_>) -> Result<Claim, rusqlite::Error> {
283 let claim_id_str: String = row.get(0)?;
286 let agent_id_str: String = row.get(1)?;
287 let subject: String = row.get(2)?;
288 let predicate: String = row.get(3)?;
289 let value_json: String = row.get(4)?;
290 let cardinality_str: String = row.get(5)?;
291 let provenance_str: String = row.get(6)?;
292 let nearest_anchor_str: Option<String> = row.get(7)?;
293 let derivation_depth: i64 = row.get(8)?;
294 let tx_time_str: String = row.get(9)?;
295 let valid_time_start_str: Option<String> = row.get(10)?;
296 let valid_time_end_str: Option<String> = row.get(11)?;
297 let valid_time_confidence: f64 = row.get(12)?;
298 let value_confidence: f64 = row.get(13)?;
299 let criticality_str: String = row.get(14)?;
300 let derived_from_json: String = row.get(15)?;
301 let metadata_json: Option<String> = row.get(16)?;
302 let snapshot_schema_version_raw: Option<i64> = row.get(17)?;
303 let start_granularity_str: Option<String> = row.get(18)?;
304 let end_granularity_str: Option<String> = row.get(19)?;
305
306 let to_rusqlite_err = |msg: String| rusqlite::Error::InvalidColumnType(
309 0,
310 msg,
311 rusqlite::types::Type::Text,
312 );
313
314 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
315 .map_err(|e| to_rusqlite_err(format!("claim_id UUID parse: {e}")))?;
316
317 let value: serde_json::Value = serde_json::from_str(&value_json)
318 .map_err(|e| to_rusqlite_err(format!("value JSON parse: {e}")))?;
319
320 let cardinality = str_to_cardinality(&cardinality_str)
321 .map_err(|e| to_rusqlite_err(e.to_string()))?;
322
323 let provenance = str_to_provenance(&provenance_str)
324 .map_err(|e| to_rusqlite_err(e.to_string()))?;
325
326 let nearest_external_anchor: Option<ClaimRef> = nearest_anchor_str
327 .map(|s| {
328 uuid::Uuid::parse_str(&s)
329 .map(ClaimRef)
330 .map_err(|e| to_rusqlite_err(format!("anchor UUID parse: {e}")))
331 })
332 .transpose()?;
333
334 let tx_time = chrono::DateTime::parse_from_rfc3339(&tx_time_str)
335 .map(|dt| dt.with_timezone(&chrono::Utc))
336 .map_err(|e| to_rusqlite_err(format!("tx_time parse: {e}")))?;
337
338 let valid_time_start = valid_time_start_str
339 .map(|s| {
340 chrono::DateTime::parse_from_rfc3339(&s)
341 .map(|dt| dt.with_timezone(&chrono::Utc))
342 .map_err(|e| to_rusqlite_err(format!("valid_time_start parse: {e}")))
343 })
344 .transpose()?;
345
346 let valid_time_end = valid_time_end_str
347 .map(|s| {
348 chrono::DateTime::parse_from_rfc3339(&s)
349 .map(|dt| dt.with_timezone(&chrono::Utc))
350 .map_err(|e| to_rusqlite_err(format!("valid_time_end parse: {e}")))
351 })
352 .transpose()?;
353
354 let criticality = str_to_criticality(&criticality_str)
355 .map_err(|e| to_rusqlite_err(e.to_string()))?;
356
357 let derived_from_uuids: Vec<String> = serde_json::from_str(&derived_from_json)
358 .map_err(|e| to_rusqlite_err(format!("derived_from JSON parse: {e}")))?;
359
360 let derived_from: Vec<ClaimRef> = derived_from_uuids
361 .iter()
362 .map(|s| {
363 uuid::Uuid::parse_str(s)
364 .map(ClaimRef)
365 .map_err(|e| to_rusqlite_err(format!("derived_from UUID parse: {e}")))
366 })
367 .collect::<Result<_, _>>()?;
368
369 let metadata: Option<serde_json::Value> = metadata_json
370 .map(|s| {
371 serde_json::from_str(&s)
372 .map_err(|e| to_rusqlite_err(format!("metadata JSON parse: {e}")))
373 })
374 .transpose()?;
375
376 let snapshot_schema_version: Option<u32> =
377 snapshot_schema_version_raw.map(|v| v as u32);
378
379 Ok(Claim::new(
380 ClaimRef(claim_id),
381 AgentId(agent_id_str),
382 Fact { subject, predicate, value },
383 cardinality,
384 provenance,
385 ExternalAnchor {
386 nearest_external_anchor,
387 derivation_depth: derivation_depth as u32,
388 },
389 TransactionTime(tx_time),
390 ValidTime {
391 start: valid_time_start,
392 end: valid_time_end,
393 valid_time_confidence: valid_time_confidence as f32,
394 start_granularity: start_granularity_str
395 .as_deref()
396 .and_then(str_to_date_granularity),
397 end_granularity: end_granularity_str
398 .as_deref()
399 .and_then(str_to_date_granularity),
400 },
401 Confidence {
402 value_confidence: value_confidence as f32,
403 valid_time_confidence: valid_time_confidence as f32,
404 },
405 criticality,
406 derived_from,
407 metadata,
408 snapshot_schema_version,
409 ))
410}
411
412const CLAIM_SELECT_COLS: &str = "
415 claim_id, agent_id, subject, predicate, value, cardinality,
416 provenance_label, nearest_external_anchor_id, derivation_depth,
417 tx_time, valid_time_start, valid_time_end, valid_time_confidence,
418 value_confidence, criticality, derived_from,
419 metadata, snapshot_schema_version,
420 valid_time_start_granularity, valid_time_end_granularity
421";
422
423fn row_to_edge(row: &rusqlite::Row<'_>) -> Result<ClaimEdge, rusqlite::Error> {
425 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
426 0, msg, rusqlite::types::Type::Text,
427 );
428
429 let edge_id_str: String = row.get(0)?;
430 let agent_id_str: String = row.get(1)?;
431 let from_claim_str: String = row.get(2)?;
432 let to_claim_str: String = row.get(3)?;
433 let kind_str: String = row.get(4)?;
434 let created_at_str: String = row.get(5)?;
435
436 let edge_id = uuid::Uuid::parse_str(&edge_id_str)
437 .map_err(|e| to_err(format!("edge_id UUID: {e}")))?;
438 let from_claim = uuid::Uuid::parse_str(&from_claim_str)
439 .map(ClaimRef)
440 .map_err(|e| to_err(format!("from_claim UUID: {e}")))?;
441 let to_claim = uuid::Uuid::parse_str(&to_claim_str)
442 .map(ClaimRef)
443 .map_err(|e| to_err(format!("to_claim UUID: {e}")))?;
444 let kind = str_to_edge_kind(&kind_str)
445 .map_err(|e| to_err(e.to_string()))?;
446 let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
447 .map(|dt| dt.with_timezone(&chrono::Utc))
448 .map_err(|e| to_err(format!("created_at parse: {e}")))?;
449
450 Ok(ClaimEdge {
451 edge_id,
452 agent_id: AgentId(agent_id_str),
453 from_claim,
454 to_claim,
455 kind,
456 created_at: TransactionTime(created_at),
457 })
458}
459
460impl PersistencePort for SqlitePersistenceStore {
463 type Transaction = SqliteTxn;
464 type Error = SqliteStoreError;
465
466 fn begin_atomic(&self, agent_id: &AgentId) -> Result<SqliteTxn, SqliteStoreError> {
473 let mut slot = self.conn.lock().expect("SqlitePersistenceStore: mutex poisoned");
474 let conn = slot.take().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
475 SqliteTxn::begin(agent_id.clone(), conn)
476 }
477
478 fn commit(&self, txn: SqliteTxn) -> Result<(), SqliteStoreError> {
480 let conn = txn.commit_and_return()?;
481 let mut slot = self.conn.lock().expect("SqlitePersistenceStore: mutex poisoned");
482 *slot = Some(conn);
483 Ok(())
484 }
485
486 fn rollback(&self, txn: SqliteTxn) -> Result<(), SqliteStoreError> {
489 let conn = txn.rollback_and_return()?;
490 let mut slot = self.conn.lock().expect("SqlitePersistenceStore: mutex poisoned");
491 *slot = Some(conn);
492 Ok(())
493 }
494
495 fn append_claim(
506 &self,
507 txn: &mut SqliteTxn,
508 claim: &Claim,
509 ) -> Result<ClaimRef, SqliteStoreError> {
510 let conn = txn.conn();
511
512 let claim_id = claim.claim_ref().0.to_string();
513 let agent_id = claim.agent_id().0.as_str();
514 let fact = claim.fact();
515 let value_json = serde_json::to_string(&fact.value)
516 .map_err(|e| SqliteStoreError::Mapping(format!("value serialization: {e}")))?;
517 let cardinality = cardinality_to_str(claim.cardinality());
518 let provenance = provenance_to_str(claim.provenance());
519 let anchor = claim.external_anchor();
520 let nearest_anchor: Option<String> =
521 anchor.nearest_external_anchor.as_ref().map(|r| r.0.to_string());
522 let derivation_depth = anchor.derivation_depth as i64;
523 let tx_time = claim.transaction_time().0.to_rfc3339();
524 let vt = claim.valid_time();
525 let valid_time_start: Option<String> = vt.start.map(|dt| dt.to_rfc3339());
526 let valid_time_end: Option<String> = vt.end.map(|dt| dt.to_rfc3339());
527 let valid_time_confidence = vt.valid_time_confidence as f64;
528 let valid_time_start_granularity: Option<&'static str> =
529 vt.start_granularity.map(date_granularity_to_str);
530 let valid_time_end_granularity: Option<&'static str> =
531 vt.end_granularity.map(date_granularity_to_str);
532 let conf = claim.confidence();
533 let value_confidence = conf.value_confidence as f64;
534 let criticality = criticality_to_str(claim.criticality());
535 let derived_from_refs: Vec<String> =
536 claim.derived_from().iter().map(|r| r.0.to_string()).collect();
537 let derived_from_json = serde_json::to_string(&derived_from_refs)
538 .map_err(|e| SqliteStoreError::Mapping(format!("derived_from serialization: {e}")))?;
539 let metadata: Option<String> = claim
540 .metadata()
541 .map(|v| {
542 serde_json::to_string(v)
543 .map_err(|e| SqliteStoreError::Mapping(format!("metadata serialization: {e}")))
544 })
545 .transpose()?;
546 let snapshot_schema_version: Option<i64> =
547 claim.snapshot_schema_version().map(|v| v as i64);
548
549 conn.execute(
550 "INSERT INTO claims (
551 claim_id, agent_id, subject, predicate, value, cardinality,
552 provenance_label, nearest_external_anchor_id, derivation_depth,
553 tx_time, valid_time_start, valid_time_end, valid_time_confidence,
554 value_confidence, criticality, derived_from,
555 metadata, snapshot_schema_version, embedding_model_id,
556 valid_time_start_granularity, valid_time_end_granularity
557 ) VALUES (
558 ?1, ?2, ?3, ?4, ?5, ?6,
559 ?7, ?8, ?9,
560 ?10, ?11, ?12, ?13,
561 ?14, ?15, ?16,
562 ?17, ?18, NULL,
563 ?19, ?20
564 )",
565 rusqlite::params![
566 claim_id,
567 agent_id,
568 fact.subject.as_str(),
569 fact.predicate.as_str(),
570 value_json.as_str(),
571 cardinality,
572 provenance,
573 nearest_anchor,
574 derivation_depth,
575 tx_time.as_str(),
576 valid_time_start,
577 valid_time_end,
578 valid_time_confidence,
579 value_confidence,
580 criticality,
581 derived_from_json.as_str(),
582 metadata,
583 snapshot_schema_version,
584 valid_time_start_granularity,
585 valid_time_end_granularity,
586 ],
587 )?;
588
589 Ok(claim.claim_ref().clone())
590 }
591
592 fn append_validity_assertion(
594 &self,
595 txn: &mut SqliteTxn,
596 assertion: &ValidityAssertion,
597 ) -> Result<(), SqliteStoreError> {
598 let conn = txn.conn();
599
600 let assertion_id = assertion.assertion_ref.to_string();
601 let agent_id = assertion.agent_id.0.as_str();
602 let target_claim_id = assertion.target_claim.0.to_string();
603 let provenance = provenance_to_str(&assertion.provenance);
604 let value_confidence = assertion.confidence.value_confidence as f64;
605 let valid_time_confidence = assertion.confidence.valid_time_confidence as f64;
606 let asserted_at = assertion.asserted_at.0.to_rfc3339();
607
608 let (assertion_kind, bound_at, reopen_at): (&str, Option<String>, Option<String>) =
609 match &assertion.kind {
610 AssertionKind::Bound { bound_at } => {
611 ("Bound", Some(bound_at.to_rfc3339()), None)
612 }
613 AssertionKind::Reopen { reopen_at } => {
614 ("Reopen", None, Some(reopen_at.to_rfc3339()))
615 }
616 _ => ("Unknown", None, None),
618 };
619
620 conn.execute(
621 "INSERT INTO validity_assertions (
622 assertion_id, agent_id, target_claim_id,
623 assertion_kind, bound_at, reopen_at,
624 provenance_label, value_confidence, valid_time_confidence, asserted_at
625 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
626 rusqlite::params![
627 assertion_id.as_str(),
628 agent_id,
629 target_claim_id.as_str(),
630 assertion_kind,
631 bound_at,
632 reopen_at,
633 provenance,
634 value_confidence,
635 valid_time_confidence,
636 asserted_at.as_str(),
637 ],
638 )?;
639
640 Ok(())
641 }
642
643 fn append_ledger_entry(
645 &self,
646 txn: &mut SqliteTxn,
647 entry: &LedgerEntry,
648 ) -> Result<(), SqliteStoreError> {
649 let conn = txn.conn();
650
651 let entry_id = entry.entry_id.to_string();
652 let agent_id = entry.agent_id.0.as_str();
653 let claim_id = entry.claim_ref.0.to_string();
654 let event_kind = ledger_event_kind_to_str(&entry.event_kind);
655 let disposition = disposition_to_str(&entry.disposition);
656 let rationale: Option<String> = entry
657 .rationale
658 .as_ref()
659 .map(|v| {
660 serde_json::to_string(v)
661 .map_err(|e| SqliteStoreError::Mapping(format!("rationale serialization: {e}")))
662 })
663 .transpose()?;
664 let recorded_at = entry.recorded_at.0.to_rfc3339();
665
666 conn.execute(
667 "INSERT INTO ledger_entries (
668 entry_id, agent_id, claim_id, event_kind, disposition, rationale, recorded_at
669 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
670 rusqlite::params![
671 entry_id.as_str(),
672 agent_id,
673 claim_id.as_str(),
674 event_kind,
675 disposition,
676 rationale,
677 recorded_at.as_str(),
678 ],
679 )?;
680
681 Ok(())
682 }
683
684 fn append_claim_edge(
686 &self,
687 txn: &mut SqliteTxn,
688 edge: &ClaimEdge,
689 ) -> Result<(), SqliteStoreError> {
690 let conn = txn.conn();
691
692 let edge_id = edge.edge_id.to_string();
693 let agent_id = edge.agent_id.0.as_str();
694 let from_claim_id = edge.from_claim.0.to_string();
695 let to_claim_id = edge.to_claim.0.to_string();
696 let edge_kind = edge_kind_to_str(&edge.kind);
697 let created_at = edge.created_at.0.to_rfc3339();
698
699 conn.execute(
700 "INSERT INTO claim_edges (
701 edge_id, agent_id, from_claim_id, to_claim_id, edge_kind, created_at
702 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
703 rusqlite::params![
704 edge_id.as_str(),
705 agent_id,
706 from_claim_id.as_str(),
707 to_claim_id.as_str(),
708 edge_kind,
709 created_at.as_str(),
710 ],
711 )?;
712
713 Ok(())
714 }
715
716 fn load_subject_line(
725 &self,
726 agent_id: &AgentId,
727 subject: &str,
728 predicate: &str,
729 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
730 ) -> Result<Vec<Claim>, SqliteStoreError> {
731 let slot = self.conn.lock().expect("mutex poisoned");
732 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
733
734 if let Some(cutoff) = as_of_tx_time {
735 let sql = format!(
736 "SELECT {CLAIM_SELECT_COLS} FROM claims
737 WHERE agent_id = ?1 AND subject = ?2 AND predicate = ?3
738 AND tx_time <= ?4
739 ORDER BY tx_time ASC"
740 );
741 let cutoff_str = cutoff.to_rfc3339();
742 let mut stmt = conn.prepare(&sql)?;
743 let rows = stmt.query_map(
744 rusqlite::params![agent_id.0.as_str(), subject, predicate, cutoff_str],
745 row_to_claim,
746 )?;
747 let mut claims = Vec::new();
748 for row in rows {
749 claims.push(row?);
750 }
751 Ok(claims)
752 } else {
753 let sql = format!(
754 "SELECT {CLAIM_SELECT_COLS} FROM claims
755 WHERE agent_id = ?1 AND subject = ?2 AND predicate = ?3
756 ORDER BY tx_time ASC"
757 );
758 let mut stmt = conn.prepare(&sql)?;
759 let rows = stmt.query_map(
760 rusqlite::params![agent_id.0.as_str(), subject, predicate],
761 row_to_claim,
762 )?;
763 let mut claims = Vec::new();
764 for row in rows {
765 claims.push(row?);
766 }
767 Ok(claims)
768 }
769 }
770
771 fn load_claim(
773 &self,
774 agent_id: &AgentId,
775 claim_ref: &ClaimRef,
776 ) -> Result<Option<Claim>, SqliteStoreError> {
777 let slot = self.conn.lock().expect("mutex poisoned");
778 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
779
780 let sql = format!(
781 "SELECT {CLAIM_SELECT_COLS} FROM claims
782 WHERE agent_id = ?1 AND claim_id = ?2"
783 );
784 let mut stmt = conn.prepare(&sql)?;
785 let mut rows = stmt.query_map(
786 rusqlite::params![agent_id.0.as_str(), claim_ref.0.to_string()],
787 row_to_claim,
788 )?;
789
790 match rows.next() {
791 None => Ok(None),
792 Some(row) => Ok(Some(row?)),
793 }
794 }
795
796 fn load_validity_assertions_for(
800 &self,
801 agent_id: &AgentId,
802 claim_ref: &ClaimRef,
803 ) -> Result<Vec<ValidityAssertion>, SqliteStoreError> {
804 let slot = self.conn.lock().expect("mutex poisoned");
805 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
806
807 let mut stmt = conn.prepare(
808 "SELECT assertion_id, agent_id, target_claim_id,
809 assertion_kind, bound_at, reopen_at,
810 provenance_label, value_confidence, valid_time_confidence, asserted_at
811 FROM validity_assertions
812 WHERE agent_id = ?1 AND target_claim_id = ?2
813 ORDER BY asserted_at ASC",
814 )?;
815
816 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
817 0, msg, rusqlite::types::Type::Text,
818 );
819
820 let rows = stmt.query_map(
821 rusqlite::params![agent_id.0.as_str(), claim_ref.0.to_string()],
822 |row| {
823 let assertion_id_str: String = row.get(0)?;
824 let agent_id_str: String = row.get(1)?;
825 let target_claim_str: String = row.get(2)?;
826 let kind_str: String = row.get(3)?;
827 let bound_at_str: Option<String> = row.get(4)?;
828 let reopen_at_str: Option<String> = row.get(5)?;
829 let prov_str: String = row.get(6)?;
830 let value_confidence: f64 = row.get(7)?;
831 let valid_time_confidence: f64 = row.get(8)?;
832 let asserted_at_str: String = row.get(9)?;
833
834 let assertion_ref = uuid::Uuid::parse_str(&assertion_id_str)
835 .map_err(|e| to_err(format!("assertion_id UUID: {e}")))?;
836 let target_claim = uuid::Uuid::parse_str(&target_claim_str)
837 .map(ClaimRef)
838 .map_err(|e| to_err(format!("target_claim UUID: {e}")))?;
839 let provenance = str_to_provenance(&prov_str)
840 .map_err(|e| to_err(e.to_string()))?;
841 let asserted_at = chrono::DateTime::parse_from_rfc3339(&asserted_at_str)
842 .map(|dt| dt.with_timezone(&chrono::Utc))
843 .map_err(|e| to_err(format!("asserted_at parse: {e}")))?;
844
845 let kind = match kind_str.as_str() {
846 "Bound" => {
847 let s = bound_at_str.ok_or_else(|| to_err("bound_at is NULL for Bound assertion".into()))?;
848 let dt = chrono::DateTime::parse_from_rfc3339(&s)
849 .map(|dt| dt.with_timezone(&chrono::Utc))
850 .map_err(|e| to_err(format!("bound_at parse: {e}")))?;
851 AssertionKind::Bound { bound_at: dt }
852 }
853 "Reopen" => {
854 let s = reopen_at_str.ok_or_else(|| to_err("reopen_at is NULL for Reopen assertion".into()))?;
855 let dt = chrono::DateTime::parse_from_rfc3339(&s)
856 .map(|dt| dt.with_timezone(&chrono::Utc))
857 .map_err(|e| to_err(format!("reopen_at parse: {e}")))?;
858 AssertionKind::Reopen { reopen_at: dt }
859 }
860 other => return Err(to_err(format!("unknown assertion_kind: {other}"))),
861 };
862
863 Ok(ValidityAssertion {
864 assertion_ref,
865 agent_id: AgentId(agent_id_str),
866 target_claim,
867 kind,
868 provenance,
869 confidence: Confidence {
870 value_confidence: value_confidence as f32,
871 valid_time_confidence: valid_time_confidence as f32,
872 },
873 asserted_at: TransactionTime(asserted_at),
874 })
875 },
876 )?;
877
878 let mut assertions = Vec::new();
879 for row in rows {
880 assertions.push(row?);
881 }
882 Ok(assertions)
883 }
884
885 fn load_ledger(
890 &self,
891 agent_id: &AgentId,
892 from: Option<&TransactionTime>,
893 limit: usize,
894 ) -> Result<Vec<LedgerEntry>, SqliteStoreError> {
895 let slot = self.conn.lock().expect("mutex poisoned");
896 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
897
898 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
899 0, msg, rusqlite::types::Type::Text,
900 );
901
902 let from_str: Option<String> = from.map(|t| t.0.to_rfc3339());
903 let limit_i64 = limit as i64;
904
905 let map_row = |row: &rusqlite::Row<'_>| {
906 let entry_id_str: String = row.get(0)?;
907 let agent_id_str: String = row.get(1)?;
908 let claim_id_str: String = row.get(2)?;
909 let event_kind_str: String = row.get(3)?;
910 let disposition_str: String = row.get(4)?;
911 let rationale_json: Option<String> = row.get(5)?;
912 let recorded_at_str: String = row.get(6)?;
913
914 let entry_id = uuid::Uuid::parse_str(&entry_id_str)
915 .map_err(|e| to_err(format!("entry_id UUID: {e}")))?;
916 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
917 .map(ClaimRef)
918 .map_err(|e| to_err(format!("claim_id UUID: {e}")))?;
919 let event_kind = str_to_ledger_event_kind(&event_kind_str)
920 .map_err(|e| to_err(e.to_string()))?;
921 let disposition = str_to_disposition(&disposition_str)
922 .map_err(|e| to_err(e.to_string()))?;
923 let rationale: Option<serde_json::Value> = rationale_json
924 .map(|s| serde_json::from_str(&s).map_err(|e| to_err(format!("rationale JSON: {e}"))))
925 .transpose()?;
926 let recorded_at = chrono::DateTime::parse_from_rfc3339(&recorded_at_str)
927 .map(|dt| dt.with_timezone(&chrono::Utc))
928 .map_err(|e| to_err(format!("recorded_at parse: {e}")))?;
929
930 Ok(LedgerEntry {
931 entry_id,
932 agent_id: AgentId(agent_id_str),
933 claim_ref: claim_id,
934 event_kind,
935 disposition,
936 rationale,
937 recorded_at: TransactionTime(recorded_at),
938 })
939 };
940
941 let mut entries = Vec::new();
942
943 if let Some(ref from_val) = from_str {
944 let mut stmt = conn.prepare(
945 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale, recorded_at
946 FROM ledger_entries
947 WHERE agent_id = ?1 AND recorded_at >= ?2
948 ORDER BY recorded_at ASC
949 LIMIT ?3",
950 )?;
951 let rows = stmt.query_map(
952 rusqlite::params![agent_id.0.as_str(), from_val.as_str(), limit_i64],
953 map_row,
954 )?;
955 for row in rows {
956 entries.push(row?);
957 }
958 } else {
959 let mut stmt = conn.prepare(
960 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale, recorded_at
961 FROM ledger_entries
962 WHERE agent_id = ?1
963 ORDER BY recorded_at ASC
964 LIMIT ?2",
965 )?;
966 let rows = stmt.query_map(
967 rusqlite::params![agent_id.0.as_str(), limit_i64],
968 map_row,
969 )?;
970 for row in rows {
971 entries.push(row?);
972 }
973 }
974
975 Ok(entries)
976 }
977
978 fn load_ledger_for_claims(
984 &self,
985 agent_id: &AgentId,
986 claim_refs: &[ClaimRef],
987 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
988 ) -> Result<Vec<LedgerEntry>, SqliteStoreError> {
989 if claim_refs.is_empty() {
990 return Ok(vec![]);
991 }
992
993 let slot = self.conn.lock().expect("mutex poisoned");
994 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
995
996 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
997 0, msg, rusqlite::types::Type::Text,
998 );
999
1000 let map_row = |row: &rusqlite::Row<'_>| {
1001 let entry_id_str: String = row.get(0)?;
1002 let agent_id_str: String = row.get(1)?;
1003 let claim_id_str: String = row.get(2)?;
1004 let event_kind_str: String = row.get(3)?;
1005 let disposition_str: String = row.get(4)?;
1006 let rationale_json: Option<String> = row.get(5)?;
1007 let recorded_at_str: String = row.get(6)?;
1008
1009 let entry_id = uuid::Uuid::parse_str(&entry_id_str)
1010 .map_err(|e| to_err(format!("entry_id UUID: {e}")))?;
1011 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
1012 .map(ClaimRef)
1013 .map_err(|e| to_err(format!("claim_id UUID: {e}")))?;
1014 let event_kind = str_to_ledger_event_kind(&event_kind_str)
1015 .map_err(|e| to_err(e.to_string()))?;
1016 let disposition = str_to_disposition(&disposition_str)
1017 .map_err(|e| to_err(e.to_string()))?;
1018 let rationale: Option<serde_json::Value> = rationale_json
1019 .map(|s| serde_json::from_str(&s).map_err(|e| to_err(format!("rationale JSON: {e}"))))
1020 .transpose()?;
1021 let recorded_at = chrono::DateTime::parse_from_rfc3339(&recorded_at_str)
1022 .map(|dt| dt.with_timezone(&chrono::Utc))
1023 .map_err(|e| to_err(format!("recorded_at parse: {e}")))?;
1024
1025 Ok(LedgerEntry {
1026 entry_id,
1027 agent_id: AgentId(agent_id_str),
1028 claim_ref: claim_id,
1029 event_kind,
1030 disposition,
1031 rationale,
1032 recorded_at: TransactionTime(recorded_at),
1033 })
1034 };
1035
1036 let mut all_entries = Vec::new();
1037 const CHUNK: usize = 900;
1040
1041 let as_of_str: Option<String> = as_of_tx_time.map(|t| t.to_rfc3339());
1043
1044 for chunk in claim_refs.chunks(CHUNK) {
1045 let id_start = 2usize;
1048 let placeholders: Vec<String> = (id_start..=chunk.len() + id_start - 1)
1049 .map(|i| format!("?{i}"))
1050 .collect();
1051 let as_of_clause = if as_of_str.is_some() {
1052 format!(" AND recorded_at <= ?{}", chunk.len() + id_start)
1053 } else {
1054 String::new()
1055 };
1056 let sql = format!(
1057 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale, recorded_at
1058 FROM ledger_entries
1059 WHERE agent_id = ?1 AND claim_id IN ({}){}
1060 ORDER BY recorded_at ASC",
1061 placeholders.join(", "),
1062 as_of_clause
1063 );
1064
1065 let mut stmt = conn.prepare(&sql)?;
1066 let agent_str = agent_id.0.as_str();
1068 let id_strings: Vec<String> = chunk.iter().map(|r| r.0.to_string()).collect();
1069
1070 let mut params: Vec<&dyn rusqlite::types::ToSql> =
1072 Vec::with_capacity(1 + id_strings.len() + usize::from(as_of_str.is_some()));
1073 params.push(&agent_str);
1074 for s in &id_strings {
1075 params.push(s);
1076 }
1077 if let Some(ref s) = as_of_str {
1078 params.push(s);
1079 }
1080
1081 let rows = stmt.query_map(params.as_slice(), map_row)?;
1082 for row in rows {
1083 all_entries.push(row?);
1084 }
1085 }
1086
1087 Ok(all_entries)
1088 }
1089
1090 fn load_edges_for(
1095 &self,
1096 agent_id: &AgentId,
1097 claim_ref: &ClaimRef,
1098 ) -> Result<Vec<ClaimEdge>, SqliteStoreError> {
1099 let slot = self.conn.lock().expect("mutex poisoned");
1100 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1101
1102 let claim_id_str = claim_ref.0.to_string();
1103
1104 let mut stmt = conn.prepare(
1105 "SELECT edge_id, agent_id, from_claim_id, to_claim_id, edge_kind, created_at
1106 FROM claim_edges
1107 WHERE agent_id = ?1
1108 AND (from_claim_id = ?2 OR to_claim_id = ?2)
1109 ORDER BY created_at ASC",
1110 )?;
1111
1112 let rows = stmt.query_map(
1113 rusqlite::params![agent_id.0.as_str(), claim_id_str.as_str()],
1114 row_to_edge,
1115 )?;
1116
1117 let mut edges = Vec::new();
1118 for row in rows {
1119 edges.push(row?);
1120 }
1121 Ok(edges)
1122 }
1123
1124 fn load_injected_claims(
1129 &self,
1130 agent_id: &AgentId,
1131 ) -> Result<Vec<ClaimRef>, SqliteStoreError> {
1132 let slot = self.conn.lock().expect("mutex poisoned");
1133 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1134
1135 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
1136 0, msg, rusqlite::types::Type::Text,
1137 );
1138
1139 let mut stmt = conn.prepare(
1140 "SELECT claim_id
1141 FROM ledger_entries
1142 WHERE agent_id = ?1 AND event_kind = 'ServedAsInjected'
1143 GROUP BY claim_id
1144 ORDER BY MIN(recorded_at) ASC",
1145 )?;
1146
1147 let rows = stmt.query_map(
1148 rusqlite::params![agent_id.0.as_str()],
1149 |row| {
1150 let claim_id_str: String = row.get(0)?;
1151 uuid::Uuid::parse_str(&claim_id_str)
1152 .map(ClaimRef)
1153 .map_err(|e| to_err(format!("claim_id UUID: {e}")))
1154 },
1155 )?;
1156
1157 let mut refs = Vec::new();
1158 for row in rows {
1159 refs.push(row?);
1160 }
1161 Ok(refs)
1162 }
1163
1164 fn load_lineage(
1172 &self,
1173 agent_id: &AgentId,
1174 claim_ref: &ClaimRef,
1175 ) -> Result<Vec<ClaimEdge>, SqliteStoreError> {
1176 let slot = self.conn.lock().expect("mutex poisoned");
1177 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1178
1179 let start_id = claim_ref.0.to_string();
1180
1181 let mut stmt = conn.prepare(
1185 "WITH RECURSIVE lineage(edge_id, depth) AS (
1186 -- Base case: all DerivedFrom edges leaving from our starting claim
1187 SELECT ce.edge_id, 1
1188 FROM claim_edges ce
1189 WHERE ce.agent_id = ?1
1190 AND ce.from_claim_id = ?2
1191 AND ce.edge_kind = 'DerivedFrom'
1192 UNION ALL
1193 -- Recursive case: follow the to_claim of the previous edge onward
1194 SELECT ce2.edge_id, l.depth + 1
1195 FROM claim_edges ce2
1196 JOIN lineage l ON ce2.from_claim_id = (
1197 SELECT to_claim_id FROM claim_edges WHERE edge_id = l.edge_id
1198 )
1199 WHERE ce2.agent_id = ?1
1200 AND ce2.edge_kind = 'DerivedFrom'
1201 AND l.depth < 64
1202 )
1203 SELECT ce.edge_id, ce.agent_id, ce.from_claim_id, ce.to_claim_id,
1204 ce.edge_kind, ce.created_at,
1205 l.depth
1206 FROM claim_edges ce
1207 JOIN lineage l ON ce.edge_id = l.edge_id
1208 ORDER BY l.depth ASC, ce.created_at ASC",
1209 )?;
1210
1211 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
1212 0, msg, rusqlite::types::Type::Text,
1213 );
1214
1215 let rows = stmt.query_map(
1216 rusqlite::params![agent_id.0.as_str(), start_id.as_str()],
1217 |row| {
1218 let edge_id_str: String = row.get(0)?;
1219 let agent_id_str: String = row.get(1)?;
1220 let from_claim_str: String = row.get(2)?;
1221 let to_claim_str: String = row.get(3)?;
1222 let kind_str: String = row.get(4)?;
1223 let created_at_str: String = row.get(5)?;
1224 let edge_id = uuid::Uuid::parse_str(&edge_id_str)
1227 .map_err(|e| to_err(format!("edge_id UUID: {e}")))?;
1228 let from_claim = uuid::Uuid::parse_str(&from_claim_str)
1229 .map(ClaimRef)
1230 .map_err(|e| to_err(format!("from_claim UUID: {e}")))?;
1231 let to_claim = uuid::Uuid::parse_str(&to_claim_str)
1232 .map(ClaimRef)
1233 .map_err(|e| to_err(format!("to_claim UUID: {e}")))?;
1234 let kind = str_to_edge_kind(&kind_str)
1235 .map_err(|e| to_err(e.to_string()))?;
1236 let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
1237 .map(|dt| dt.with_timezone(&chrono::Utc))
1238 .map_err(|e| to_err(format!("created_at parse: {e}")))?;
1239
1240 Ok(ClaimEdge {
1241 edge_id,
1242 agent_id: AgentId(agent_id_str),
1243 from_claim,
1244 to_claim,
1245 kind,
1246 created_at: TransactionTime(created_at),
1247 })
1248 },
1249 )?;
1250
1251 let mut edges = Vec::new();
1252 for row in rows {
1253 edges.push(row?);
1254 }
1255 Ok(edges)
1256 }
1257
1258 fn list_predicates_for_subject(
1267 &self,
1268 agent_id: &AgentId,
1269 subject: &str,
1270 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
1271 ) -> Result<Vec<String>, SqliteStoreError> {
1272 let slot = self.conn.lock().expect("mutex poisoned");
1273 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1274
1275 let predicates = if let Some(cutoff) = as_of_tx_time {
1276 let cutoff_str = cutoff.to_rfc3339();
1277 let mut stmt = conn.prepare(
1278 "SELECT DISTINCT predicate
1279 FROM claims
1280 WHERE agent_id = ?1 AND subject = ?2 AND tx_time <= ?3",
1281 )?;
1282 let rows = stmt.query_map(
1283 rusqlite::params![agent_id.0.as_str(), subject, cutoff_str.as_str()],
1284 |row| row.get::<_, String>(0),
1285 )?;
1286 let mut preds = Vec::new();
1287 for row in rows {
1288 preds.push(row?);
1289 }
1290 preds
1291 } else {
1292 let mut stmt = conn.prepare(
1293 "SELECT DISTINCT predicate
1294 FROM claims
1295 WHERE agent_id = ?1 AND subject = ?2",
1296 )?;
1297 let rows = stmt.query_map(
1298 rusqlite::params![agent_id.0.as_str(), subject],
1299 |row| row.get::<_, String>(0),
1300 )?;
1301 let mut preds = Vec::new();
1302 for row in rows {
1303 preds.push(row?);
1304 }
1305 preds
1306 };
1307
1308 Ok(predicates)
1309 }
1310}
1311
1312pub struct SqlitePendingStore {
1324 conn: Arc<Mutex<Option<Box<Connection>>>>,
1325}
1326
1327impl SqlitePendingStore {
1328 pub fn new(conn: Arc<Mutex<Option<Box<Connection>>>>) -> Self {
1330 Self { conn }
1331 }
1332}
1333
1334unsafe impl Send for SqlitePendingStore {}
1336unsafe impl Sync for SqlitePendingStore {}
1337
1338impl PendingAdjudicationPort for SqlitePendingStore {
1339 type Error = SqliteStoreError;
1340
1341 fn insert_pending(&self, row: &PendingAdjudicationRow) -> Result<(), SqliteStoreError> {
1342 let slot = self.conn.lock().expect("mutex poisoned");
1343 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1344
1345 let request_payload = serde_json::to_string(&row.request_payload)
1346 .map_err(|e| SqliteStoreError::Mapping(format!("request_payload serialization: {e}")))?;
1347 let queued_at = row.queued_at.to_rfc3339();
1348 let expires_at: Option<String> = row.expires_at.map(|dt| dt.to_rfc3339());
1349
1350 conn.execute(
1351 "INSERT INTO pending_adjudications (
1352 handle_id, agent_id, subject, predicate,
1353 challenger_claim_ref, incumbent_claim_ref,
1354 request_payload, queued_at, expires_at, status
1355 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
1356 rusqlite::params![
1357 row.handle_id.to_string(),
1358 row.agent_id.0.as_str(),
1359 row.subject.as_str(),
1360 row.predicate.as_str(),
1361 row.challenger_claim_ref.0.to_string(),
1362 row.incumbent_claim_ref.0.to_string(),
1363 request_payload.as_str(),
1364 queued_at.as_str(),
1365 expires_at,
1366 row.status.as_str(),
1367 ],
1368 )?;
1369 Ok(())
1370 }
1371
1372 fn get_pending(&self, handle_id: uuid::Uuid) -> Result<Option<PendingAdjudicationRow>, SqliteStoreError> {
1373 let slot = self.conn.lock().expect("mutex poisoned");
1374 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1375
1376 let mut stmt = conn.prepare(
1377 "SELECT handle_id, agent_id, subject, predicate,
1378 challenger_claim_ref, incumbent_claim_ref,
1379 request_payload, queued_at, expires_at, status
1380 FROM pending_adjudications
1381 WHERE handle_id = ?1",
1382 )?;
1383
1384 let mut rows = stmt.query_map(
1385 rusqlite::params![handle_id.to_string()],
1386 row_to_pending,
1387 )?;
1388
1389 match rows.next() {
1390 None => Ok(None),
1391 Some(row) => Ok(Some(row.map_err(|e| SqliteStoreError::Mapping(e.to_string()))?)),
1392 }
1393 }
1394
1395 fn list_pending(&self, agent_id: Option<&mempill_types::AgentId>) -> Result<Vec<PendingAdjudicationRow>, SqliteStoreError> {
1396 let slot = self.conn.lock().expect("mutex poisoned");
1397 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1398
1399 let rows = if let Some(aid) = agent_id {
1400 let mut stmt = conn.prepare(
1401 "SELECT handle_id, agent_id, subject, predicate,
1402 challenger_claim_ref, incumbent_claim_ref,
1403 request_payload, queued_at, expires_at, status
1404 FROM pending_adjudications
1405 WHERE agent_id = ?1 AND status = 'pending'
1406 ORDER BY queued_at ASC",
1407 )?;
1408 let mapped = stmt.query_map(rusqlite::params![aid.0.as_str()], row_to_pending)?;
1409 let mut result = Vec::new();
1410 for r in mapped {
1411 result.push(r.map_err(|e| SqliteStoreError::Mapping(e.to_string()))?);
1412 }
1413 result
1414 } else {
1415 let mut stmt = conn.prepare(
1416 "SELECT handle_id, agent_id, subject, predicate,
1417 challenger_claim_ref, incumbent_claim_ref,
1418 request_payload, queued_at, expires_at, status
1419 FROM pending_adjudications
1420 WHERE status = 'pending'
1421 ORDER BY queued_at ASC",
1422 )?;
1423 let mapped = stmt.query_map([], row_to_pending)?;
1424 let mut result = Vec::new();
1425 for r in mapped {
1426 result.push(r.map_err(|e| SqliteStoreError::Mapping(e.to_string()))?);
1427 }
1428 result
1429 };
1430 Ok(rows)
1431 }
1432
1433 fn list_expired(&self, now: chrono::DateTime<chrono::Utc>) -> Result<Vec<PendingAdjudicationRow>, SqliteStoreError> {
1434 let slot = self.conn.lock().expect("mutex poisoned");
1435 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1436
1437 let now_str = now.to_rfc3339();
1438 let mut stmt = conn.prepare(
1439 "SELECT handle_id, agent_id, subject, predicate,
1440 challenger_claim_ref, incumbent_claim_ref,
1441 request_payload, queued_at, expires_at, status
1442 FROM pending_adjudications
1443 WHERE expires_at IS NOT NULL AND expires_at <= ?1 AND status = 'pending'
1444 ORDER BY expires_at ASC",
1445 )?;
1446 let mapped = stmt.query_map(rusqlite::params![now_str.as_str()], row_to_pending)?;
1447 let mut result = Vec::new();
1448 for r in mapped {
1449 result.push(r.map_err(|e| SqliteStoreError::Mapping(e.to_string()))?);
1450 }
1451 Ok(result)
1452 }
1453
1454 fn mark_resolved(&self, handle_id: uuid::Uuid) -> Result<(), SqliteStoreError> {
1455 let slot = self.conn.lock().expect("mutex poisoned");
1456 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1457
1458 conn.execute(
1459 "UPDATE pending_adjudications SET status = 'resolved' WHERE handle_id = ?1",
1460 rusqlite::params![handle_id.to_string()],
1461 )?;
1462 Ok(())
1463 }
1464
1465 fn mark_expired(&self, handle_id: uuid::Uuid) -> Result<(), SqliteStoreError> {
1466 let slot = self.conn.lock().expect("mutex poisoned");
1467 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1468
1469 conn.execute(
1470 "UPDATE pending_adjudications SET status = 'expired' WHERE handle_id = ?1",
1471 rusqlite::params![handle_id.to_string()],
1472 )?;
1473 Ok(())
1474 }
1475
1476 fn list_queued_orphan_claims(
1486 &self,
1487 ) -> Result<Vec<mempill_core::ports::pending_adjudication::OrphanedQueuedClaim>, SqliteStoreError> {
1488 let slot = self.conn.lock().expect("mutex poisoned");
1489 let conn = slot.as_ref().ok_or(SqliteStoreError::TxnAlreadyOpen)?;
1490
1491 let mut stmt = conn.prepare(
1494 "SELECT l.agent_id, l.claim_id, c.subject, c.predicate
1495 FROM ledger_entries l
1496 JOIN claims c ON c.claim_id = l.claim_id AND c.agent_id = l.agent_id
1497 WHERE l.disposition = 'QueuedForAdjudication'
1498 AND l.recorded_at = (
1499 SELECT MAX(l2.recorded_at) FROM ledger_entries l2
1500 WHERE l2.claim_id = l.claim_id AND l2.agent_id = l.agent_id
1501 )
1502 AND NOT EXISTS (
1503 SELECT 1 FROM pending_adjudications pa
1504 WHERE pa.challenger_claim_ref = l.claim_id
1505 AND pa.agent_id = l.agent_id
1506 AND pa.status = 'pending'
1507 )",
1508 )?;
1509
1510 let orphan_rows: Vec<(String, String, String, String)> = stmt
1511 .query_map([], |row| {
1512 Ok((
1513 row.get::<_, String>(0)?,
1514 row.get::<_, String>(1)?,
1515 row.get::<_, String>(2)?,
1516 row.get::<_, String>(3)?,
1517 ))
1518 })?
1519 .filter_map(|r| r.ok())
1520 .collect();
1521
1522 let mut results = Vec::new();
1523 for (agent_id_str, challenger_str, subject, predicate) in orphan_rows {
1524 use mempill_types::ClaimRef;
1525
1526 let challenger_ref = uuid::Uuid::parse_str(&challenger_str)
1527 .map(ClaimRef)
1528 .map_err(|e| SqliteStoreError::Mapping(format!("challenger_claim_ref UUID: {e}")))?;
1529
1530 let incumbent_ref = find_committed_cheap_claim(conn, &agent_id_str, &subject, &predicate)?;
1532
1533 results.push(mempill_core::ports::pending_adjudication::OrphanedQueuedClaim {
1534 agent_id: mempill_types::AgentId(agent_id_str),
1535 challenger_claim_ref: challenger_ref,
1536 incumbent_claim_ref: incumbent_ref,
1537 subject,
1538 predicate,
1539 });
1540 }
1541
1542 Ok(results)
1543 }
1544}
1545
1546fn row_to_pending(row: &rusqlite::Row<'_>) -> Result<PendingAdjudicationRow, rusqlite::Error> {
1560 let to_err = |msg: String| rusqlite::Error::InvalidColumnType(
1561 0, msg, rusqlite::types::Type::Text,
1562 );
1563
1564 let handle_id_str: String = row.get(0)?;
1565 let agent_id_str: String = row.get(1)?;
1566 let subject: String = row.get(2)?;
1567 let predicate: String = row.get(3)?;
1568 let challenger_str: String = row.get(4)?;
1569 let incumbent_str: String = row.get(5)?;
1570 let payload_json: String = row.get(6)?;
1571 let queued_at_str: String = row.get(7)?;
1572 let expires_at_str: Option<String> = row.get(8)?;
1573 let status: String = row.get(9)?;
1574
1575 let handle_id = uuid::Uuid::parse_str(&handle_id_str)
1576 .map_err(|e| to_err(format!("handle_id UUID: {e}")))?;
1577 let challenger_claim_ref = uuid::Uuid::parse_str(&challenger_str)
1578 .map(ClaimRef)
1579 .map_err(|e| to_err(format!("challenger_claim_ref UUID: {e}")))?;
1580 let incumbent_claim_ref = uuid::Uuid::parse_str(&incumbent_str)
1581 .map(ClaimRef)
1582 .map_err(|e| to_err(format!("incumbent_claim_ref UUID: {e}")))?;
1583 let request_payload: mempill_types::AdjudicationRequest =
1584 serde_json::from_str(&payload_json)
1585 .map_err(|e| to_err(format!("request_payload JSON: {e}")))?;
1586 let queued_at = chrono::DateTime::parse_from_rfc3339(&queued_at_str)
1587 .map(|dt| dt.with_timezone(&chrono::Utc))
1588 .map_err(|e| to_err(format!("queued_at parse: {e}")))?;
1589 let expires_at = expires_at_str
1590 .map(|s| {
1591 chrono::DateTime::parse_from_rfc3339(&s)
1592 .map(|dt| dt.with_timezone(&chrono::Utc))
1593 .map_err(|e| to_err(format!("expires_at parse: {e}")))
1594 })
1595 .transpose()?;
1596
1597 Ok(PendingAdjudicationRow {
1598 handle_id,
1599 agent_id: AgentId(agent_id_str),
1600 subject,
1601 predicate,
1602 challenger_claim_ref,
1603 incumbent_claim_ref,
1604 request_payload,
1605 queued_at,
1606 expires_at,
1607 status,
1608 })
1609}
1610
1611fn find_committed_cheap_claim(
1619 conn: &Connection,
1620 agent_id: &str,
1621 subject: &str,
1622 predicate: &str,
1623) -> Result<Option<mempill_types::ClaimRef>, SqliteStoreError> {
1624 let mut stmt = conn.prepare(
1626 "SELECT l.claim_id
1627 FROM ledger_entries l
1628 JOIN claims c ON c.claim_id = l.claim_id AND c.agent_id = l.agent_id
1629 WHERE l.agent_id = ?1
1630 AND c.subject = ?2
1631 AND c.predicate = ?3
1632 AND l.disposition = 'CommittedCheap'
1633 AND l.recorded_at = (
1634 SELECT MAX(l2.recorded_at) FROM ledger_entries l2
1635 WHERE l2.claim_id = l.claim_id AND l2.agent_id = l.agent_id
1636 )
1637 ORDER BY l.recorded_at DESC
1638 LIMIT 1",
1639 )?;
1640
1641 let mut rows = stmt.query_map(rusqlite::params![agent_id, subject, predicate], |row| {
1642 row.get::<_, String>(0)
1643 })?;
1644
1645 if let Some(Ok(ref_str)) = rows.next() {
1646 let claim_ref = uuid::Uuid::parse_str(&ref_str)
1647 .map(mempill_types::ClaimRef)
1648 .map_err(|e| SqliteStoreError::Mapping(format!("incumbent_claim_ref UUID: {e}")))?;
1649 Ok(Some(claim_ref))
1650 } else {
1651 Ok(None)
1652 }
1653}
1654
1655#[cfg(test)]
1658mod tests {
1659 use super::*;
1660 use crate::connection::open_in_memory;
1661 use chrono::Utc;
1662 use mempill_types::{
1663 claim::{Cardinality, Claim, Confidence, Criticality, Fact},
1664 disposition::Disposition,
1665 identity::AgentId,
1666 ledger::LedgerEventKind,
1667 provenance::{ExternalAnchor, ExternalKind, ProvenanceLabel},
1668 time::{TransactionTime, ValidTime},
1669 validity::AssertionKind,
1670 };
1671 use uuid::Uuid;
1672
1673 fn make_store() -> SqlitePersistenceStore {
1674 let conn = open_in_memory().expect("in-memory connection should open");
1675 SqlitePersistenceStore::new(conn)
1676 }
1677
1678 fn make_agent() -> AgentId {
1679 AgentId("test-agent-1".into())
1680 }
1681
1682 fn make_claim(agent_id: &AgentId) -> Claim {
1683 Claim::new(
1684 ClaimRef(Uuid::new_v4()),
1685 agent_id.clone(),
1686 Fact {
1687 subject: "user".into(),
1688 predicate: "favourite_colour".into(),
1689 value: serde_json::json!("blue"),
1690 },
1691 Cardinality::Functional,
1692 ProvenanceLabel::External(ExternalKind::UserAsserted),
1693 ExternalAnchor { nearest_external_anchor: None, derivation_depth: 0 },
1694 TransactionTime(Utc::now()),
1695 ValidTime { start: None, end: None, valid_time_confidence: 0.0 , start_granularity: None, end_granularity: None},
1696 Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
1697 Criticality::Low,
1698 vec![],
1699 None,
1700 None,
1701 )
1702 }
1703
1704 fn make_ledger_entry(
1705 agent_id: &AgentId,
1706 claim_ref: &ClaimRef,
1707 ) -> LedgerEntry {
1708 LedgerEntry {
1709 entry_id: Uuid::new_v4(),
1710 agent_id: agent_id.clone(),
1711 claim_ref: claim_ref.clone(),
1712 event_kind: LedgerEventKind::ClaimCommitted,
1713 disposition: Disposition::CommittedCheap,
1714 rationale: None,
1715 recorded_at: TransactionTime(Utc::now()),
1716 }
1717 }
1718
1719 #[test]
1724 fn write_round_trip_claim_persists_after_commit() {
1725 let store = make_store();
1726 let agent = make_agent();
1727 let claim = make_claim(&agent);
1728 let claim_id = claim.claim_ref().0.to_string();
1729
1730 let mut txn = store.begin_atomic(&agent).expect("begin_atomic should succeed");
1731 store.append_claim(&mut txn, &claim).expect("append_claim should succeed");
1732 store.commit(txn).expect("commit should succeed");
1733
1734 let slot = store.conn.lock().unwrap();
1736 let conn = slot.as_ref().expect("connection must be back after commit");
1737 let count: i64 = conn
1738 .query_row(
1739 "SELECT COUNT(*) FROM claims WHERE claim_id = ?1",
1740 [claim_id.as_str()],
1741 |r| r.get(0),
1742 )
1743 .expect("SELECT should succeed");
1744 assert_eq!(count, 1, "claim row must exist after commit");
1745 }
1746
1747 #[test]
1749 fn write_round_trip_provenance_not_null() {
1750 let store = make_store();
1751 let agent = make_agent();
1752 let claim = make_claim(&agent);
1753 let claim_id = claim.claim_ref().0.to_string();
1754
1755 let mut txn = store.begin_atomic(&agent).expect("begin_atomic should succeed");
1756 store.append_claim(&mut txn, &claim).expect("append_claim should succeed");
1757 store.commit(txn).expect("commit should succeed");
1758
1759 let slot = store.conn.lock().unwrap();
1760 let conn = slot.as_ref().unwrap();
1761
1762 let prov: String = conn
1764 .query_row(
1765 "SELECT provenance_label FROM claims WHERE claim_id = ?1",
1766 [claim_id.as_str()],
1767 |r| r.get(0),
1768 )
1769 .expect("provenance_label must be selectable");
1770 assert_eq!(
1771 prov, "External_UserAsserted",
1772 "provenance_label column must be non-NULL and correct"
1773 );
1774
1775 let tx_time: String = conn
1777 .query_row(
1778 "SELECT tx_time FROM claims WHERE claim_id = ?1",
1779 [claim_id.as_str()],
1780 |r| r.get(0),
1781 )
1782 .expect("tx_time must be selectable");
1783 assert!(!tx_time.is_empty(), "tx_time must be non-NULL");
1784 }
1785
1786 #[test]
1791 fn atomicity_rollback_leaves_zero_rows() {
1792 let store = make_store();
1793 let agent = make_agent();
1794 let claim = make_claim(&agent);
1795 let claim_ref = claim.claim_ref().clone();
1796 let claim_id = claim_ref.0.to_string();
1797
1798 let assertion = mempill_types::validity::ValidityAssertion {
1799 assertion_ref: Uuid::new_v4(),
1800 agent_id: agent.clone(),
1801 target_claim: claim_ref.clone(),
1802 kind: AssertionKind::Bound { bound_at: Utc::now() },
1803 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
1804 confidence: mempill_types::claim::Confidence {
1805 value_confidence: 0.9,
1806 valid_time_confidence: 0.9,
1807 },
1808 asserted_at: TransactionTime(Utc::now()),
1809 };
1810 let assertion_id = assertion.assertion_ref.to_string();
1811
1812 let ledger_entry = make_ledger_entry(&agent, &claim_ref);
1813 let entry_id = ledger_entry.entry_id.to_string();
1814
1815 let mut txn = store.begin_atomic(&agent).expect("begin_atomic should succeed");
1816 store.append_claim(&mut txn, &claim).expect("append_claim in txn should succeed");
1817 store
1818 .append_validity_assertion(&mut txn, &assertion)
1819 .expect("append_validity_assertion in txn should succeed");
1820 store
1821 .append_ledger_entry(&mut txn, &ledger_entry)
1822 .expect("append_ledger_entry in txn should succeed");
1823
1824 store.rollback(txn).expect("rollback should succeed");
1826
1827 let slot = store.conn.lock().unwrap();
1828 let conn = slot.as_ref().expect("connection must be back after rollback");
1829
1830 let claim_count: i64 = conn
1831 .query_row(
1832 "SELECT COUNT(*) FROM claims WHERE claim_id = ?1",
1833 [claim_id.as_str()],
1834 |r| r.get(0),
1835 )
1836 .unwrap();
1837 let assertion_count: i64 = conn
1838 .query_row(
1839 "SELECT COUNT(*) FROM validity_assertions WHERE assertion_id = ?1",
1840 [assertion_id.as_str()],
1841 |r| r.get(0),
1842 )
1843 .unwrap();
1844 let ledger_count: i64 = conn
1845 .query_row(
1846 "SELECT COUNT(*) FROM ledger_entries WHERE entry_id = ?1",
1847 [entry_id.as_str()],
1848 |r| r.get(0),
1849 )
1850 .unwrap();
1851
1852 assert_eq!(claim_count, 0, "claim row must not exist after rollback");
1853 assert_eq!(assertion_count, 0, "validity_assertion row must not exist after rollback");
1854 assert_eq!(ledger_count, 0, "ledger_entry row must not exist after rollback");
1855 }
1856
1857 #[test]
1860 fn write_round_trip_validity_assertion() {
1861 let store = make_store();
1862 let agent = make_agent();
1863 let claim = make_claim(&agent);
1864 let claim_ref = claim.claim_ref().clone();
1865
1866 let assertion = mempill_types::validity::ValidityAssertion {
1867 assertion_ref: Uuid::new_v4(),
1868 agent_id: agent.clone(),
1869 target_claim: claim_ref.clone(),
1870 kind: AssertionKind::Bound { bound_at: Utc::now() },
1871 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
1872 confidence: mempill_types::claim::Confidence {
1873 value_confidence: 0.95,
1874 valid_time_confidence: 0.8,
1875 },
1876 asserted_at: TransactionTime(Utc::now()),
1877 };
1878 let assertion_id = assertion.assertion_ref.to_string();
1879
1880 let mut txn = store.begin_atomic(&agent).unwrap();
1881 store.append_claim(&mut txn, &claim).unwrap();
1882 store.append_validity_assertion(&mut txn, &assertion).unwrap();
1883 store.commit(txn).unwrap();
1884
1885 let slot = store.conn.lock().unwrap();
1886 let conn = slot.as_ref().unwrap();
1887 let count: i64 = conn
1888 .query_row(
1889 "SELECT COUNT(*) FROM validity_assertions WHERE assertion_id = ?1",
1890 [assertion_id.as_str()],
1891 |r| r.get(0),
1892 )
1893 .unwrap();
1894 assert_eq!(count, 1, "validity_assertion row must exist after commit");
1895 }
1896
1897 #[test]
1900 fn write_round_trip_ledger_entry() {
1901 let store = make_store();
1902 let agent = make_agent();
1903 let claim = make_claim(&agent);
1904 let claim_ref = claim.claim_ref().clone();
1905 let entry = make_ledger_entry(&agent, &claim_ref);
1906 let entry_id = entry.entry_id.to_string();
1907
1908 let mut txn = store.begin_atomic(&agent).unwrap();
1909 store.append_claim(&mut txn, &claim).unwrap();
1910 store.append_ledger_entry(&mut txn, &entry).unwrap();
1911 store.commit(txn).unwrap();
1912
1913 let slot = store.conn.lock().unwrap();
1914 let conn = slot.as_ref().unwrap();
1915 let count: i64 = conn
1916 .query_row(
1917 "SELECT COUNT(*) FROM ledger_entries WHERE entry_id = ?1",
1918 [entry_id.as_str()],
1919 |r| r.get(0),
1920 )
1921 .unwrap();
1922 assert_eq!(count, 1, "ledger_entry row must exist after commit");
1923 }
1924
1925 #[test]
1928 fn write_round_trip_claim_edge() {
1929 let store = make_store();
1930 let agent = make_agent();
1931 let from_claim = make_claim(&agent);
1932 let to_claim = make_claim(&agent);
1933 let from_ref = from_claim.claim_ref().clone();
1934 let to_ref = to_claim.claim_ref().clone();
1935
1936 let edge = ClaimEdge {
1937 edge_id: Uuid::new_v4(),
1938 agent_id: agent.clone(),
1939 from_claim: from_ref.clone(),
1940 to_claim: to_ref.clone(),
1941 kind: EdgeKind::DerivedFrom,
1942 created_at: TransactionTime(Utc::now()),
1943 };
1944 let edge_id = edge.edge_id.to_string();
1945
1946 let mut txn = store.begin_atomic(&agent).unwrap();
1947 store.append_claim(&mut txn, &from_claim).unwrap();
1948 store.append_claim(&mut txn, &to_claim).unwrap();
1949 store.append_claim_edge(&mut txn, &edge).unwrap();
1950 store.commit(txn).unwrap();
1951
1952 let slot = store.conn.lock().unwrap();
1953 let conn = slot.as_ref().unwrap();
1954 let count: i64 = conn
1955 .query_row(
1956 "SELECT COUNT(*) FROM claim_edges WHERE edge_id = ?1",
1957 [edge_id.as_str()],
1958 |r| r.get(0),
1959 )
1960 .unwrap();
1961 assert_eq!(count, 1, "claim_edge row must exist after commit");
1962 }
1963
1964 #[test]
1968 fn read_load_claim_round_trip() {
1969 let store = make_store();
1970 let agent = make_agent();
1971 let claim = make_claim(&agent);
1972 let claim_ref = claim.claim_ref().clone();
1973
1974 let mut txn = store.begin_atomic(&agent).unwrap();
1975 store.append_claim(&mut txn, &claim).unwrap();
1976 store.commit(txn).unwrap();
1977
1978 let loaded = store.load_claim(&agent, &claim_ref).unwrap();
1979 assert!(loaded.is_some(), "load_claim must return Some for existing claim");
1980 let loaded = loaded.unwrap();
1981 assert_eq!(loaded.claim_ref(), &claim_ref);
1982 assert_eq!(loaded.agent_id(), &agent);
1983 assert_eq!(loaded.fact().subject, "user");
1984 assert_eq!(loaded.fact().predicate, "favourite_colour");
1985 assert_eq!(loaded.fact().value, serde_json::json!("blue"));
1986 assert_eq!(loaded.provenance(), claim.provenance());
1987 assert_eq!(loaded.cardinality(), claim.cardinality());
1988 assert_eq!(loaded.criticality(), claim.criticality());
1989 }
1990
1991 #[test]
1993 fn read_load_claim_missing_returns_none() {
1994 let store = make_store();
1995 let agent = make_agent();
1996 let missing_ref = ClaimRef(Uuid::new_v4());
1997 let result = store.load_claim(&agent, &missing_ref).unwrap();
1998 assert!(result.is_none(), "load_claim must return None for unknown claim_ref");
1999 }
2000
2001 #[test]
2003 fn read_load_subject_line_round_trip() {
2004 let store = make_store();
2005 let agent = make_agent();
2006 let claim = make_claim(&agent);
2007 let claim_ref = claim.claim_ref().clone();
2008
2009 let mut txn = store.begin_atomic(&agent).unwrap();
2010 store.append_claim(&mut txn, &claim).unwrap();
2011 store.commit(txn).unwrap();
2012
2013 let claims = store.load_subject_line(&agent, "user", "favourite_colour", None).unwrap();
2014 assert_eq!(claims.len(), 1, "load_subject_line must return the single written claim");
2015 assert_eq!(claims[0].claim_ref(), &claim_ref);
2016 }
2017
2018 #[test]
2020 fn read_load_subject_line_empty_when_no_match() {
2021 let store = make_store();
2022 let agent = make_agent();
2023 let claims = store.load_subject_line(&agent, "nonexistent", "pred", None).unwrap();
2024 assert!(claims.is_empty(), "load_subject_line must return empty vec for unknown subject-line");
2025 }
2026
2027 #[test]
2029 fn read_load_validity_assertions_round_trip() {
2030 let store = make_store();
2031 let agent = make_agent();
2032 let claim = make_claim(&agent);
2033 let claim_ref = claim.claim_ref().clone();
2034
2035 let assertion = mempill_types::validity::ValidityAssertion {
2036 assertion_ref: Uuid::new_v4(),
2037 agent_id: agent.clone(),
2038 target_claim: claim_ref.clone(),
2039 kind: AssertionKind::Bound { bound_at: Utc::now() },
2040 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
2041 confidence: mempill_types::claim::Confidence {
2042 value_confidence: 0.9,
2043 valid_time_confidence: 0.8,
2044 },
2045 asserted_at: TransactionTime(Utc::now()),
2046 };
2047
2048 let mut txn = store.begin_atomic(&agent).unwrap();
2049 store.append_claim(&mut txn, &claim).unwrap();
2050 store.append_validity_assertion(&mut txn, &assertion).unwrap();
2051 store.commit(txn).unwrap();
2052
2053 let loaded = store.load_validity_assertions_for(&agent, &claim_ref).unwrap();
2054 assert_eq!(loaded.len(), 1, "must return one validity assertion");
2055 assert_eq!(loaded[0].assertion_ref, assertion.assertion_ref);
2056 assert_eq!(loaded[0].target_claim, claim_ref);
2057 assert!(matches!(loaded[0].kind, AssertionKind::Bound { .. }));
2058 }
2059
2060 #[test]
2062 fn read_load_validity_assertions_empty_when_none() {
2063 let store = make_store();
2064 let agent = make_agent();
2065 let claim = make_claim(&agent);
2066 let claim_ref = claim.claim_ref().clone();
2067
2068 let mut txn = store.begin_atomic(&agent).unwrap();
2069 store.append_claim(&mut txn, &claim).unwrap();
2070 store.commit(txn).unwrap();
2071
2072 let loaded = store.load_validity_assertions_for(&agent, &claim_ref).unwrap();
2073 assert!(loaded.is_empty(), "must return empty vec when no assertions");
2074 }
2075
2076 #[test]
2078 fn read_load_ledger_round_trip() {
2079 let store = make_store();
2080 let agent = make_agent();
2081 let claim = make_claim(&agent);
2082 let claim_ref = claim.claim_ref().clone();
2083 let entry = make_ledger_entry(&agent, &claim_ref);
2084
2085 let mut txn = store.begin_atomic(&agent).unwrap();
2086 store.append_claim(&mut txn, &claim).unwrap();
2087 store.append_ledger_entry(&mut txn, &entry).unwrap();
2088 store.commit(txn).unwrap();
2089
2090 let loaded = store.load_ledger(&agent, None, 100).unwrap();
2091 assert_eq!(loaded.len(), 1, "must return one ledger entry");
2092 assert_eq!(loaded[0].entry_id, entry.entry_id);
2093 assert_eq!(loaded[0].claim_ref, claim_ref);
2094 assert_eq!(loaded[0].event_kind, LedgerEventKind::ClaimCommitted);
2095 }
2096
2097 #[test]
2099 fn read_load_ledger_from_bound_filters_earlier_entries() {
2100 let store = make_store();
2101 let agent = make_agent();
2102
2103 let claim_early = make_claim(&agent);
2105 let claim_late = make_claim(&agent);
2106 let ref_early = claim_early.claim_ref().clone();
2107 let ref_late = claim_late.claim_ref().clone();
2108
2109 let t_early = TransactionTime(Utc::now() - chrono::Duration::seconds(10));
2110 let t_late = TransactionTime(Utc::now());
2111
2112 let entry_early = mempill_types::ledger::LedgerEntry {
2113 entry_id: Uuid::new_v4(),
2114 agent_id: agent.clone(),
2115 claim_ref: ref_early.clone(),
2116 event_kind: LedgerEventKind::ClaimCommitted,
2117 disposition: mempill_types::disposition::Disposition::CommittedCheap,
2118 rationale: None,
2119 recorded_at: t_early.clone(),
2120 };
2121 let entry_late = mempill_types::ledger::LedgerEntry {
2122 entry_id: Uuid::new_v4(),
2123 agent_id: agent.clone(),
2124 claim_ref: ref_late.clone(),
2125 event_kind: LedgerEventKind::ClaimCommitted,
2126 disposition: mempill_types::disposition::Disposition::CommittedCheap,
2127 rationale: None,
2128 recorded_at: t_late.clone(),
2129 };
2130
2131 let mut txn = store.begin_atomic(&agent).unwrap();
2132 store.append_claim(&mut txn, &claim_early).unwrap();
2133 store.append_claim(&mut txn, &claim_late).unwrap();
2134 store.append_ledger_entry(&mut txn, &entry_early).unwrap();
2135 store.append_ledger_entry(&mut txn, &entry_late).unwrap();
2136 store.commit(txn).unwrap();
2137
2138 let loaded = store.load_ledger(&agent, Some(&t_late), 100).unwrap();
2140 assert_eq!(loaded.len(), 1, "only the late entry must be returned when from=t_late");
2141 assert_eq!(loaded[0].entry_id, entry_late.entry_id);
2142 }
2143
2144 #[test]
2146 fn read_load_ledger_empty_when_none() {
2147 let store = make_store();
2148 let agent = make_agent();
2149 let loaded = store.load_ledger(&agent, None, 100).unwrap();
2150 assert!(loaded.is_empty(), "must return empty vec when no ledger entries");
2151 }
2152
2153 #[test]
2155 fn read_load_edges_for_ordering_created_at_asc() {
2156 let store = make_store();
2157 let agent = make_agent();
2158
2159 let claim_a = make_claim(&agent);
2160 let claim_b = make_claim(&agent);
2161 let claim_c = make_claim(&agent);
2162 let ref_a = claim_a.claim_ref().clone();
2163 let ref_b = claim_b.claim_ref().clone();
2164 let ref_c = claim_c.claim_ref().clone();
2165
2166 let t1 = TransactionTime(Utc::now() - chrono::Duration::seconds(5));
2168 let t2 = TransactionTime(Utc::now());
2169
2170 let edge_ab = ClaimEdge {
2171 edge_id: Uuid::new_v4(),
2172 agent_id: agent.clone(),
2173 from_claim: ref_a.clone(),
2174 to_claim: ref_b.clone(),
2175 kind: EdgeKind::DependsOn,
2176 created_at: t1,
2177 };
2178 let edge_ac = ClaimEdge {
2179 edge_id: Uuid::new_v4(),
2180 agent_id: agent.clone(),
2181 from_claim: ref_a.clone(),
2182 to_claim: ref_c.clone(),
2183 kind: EdgeKind::DependsOn,
2184 created_at: t2,
2185 };
2186
2187 let mut txn = store.begin_atomic(&agent).unwrap();
2188 store.append_claim(&mut txn, &claim_a).unwrap();
2190 store.append_claim(&mut txn, &claim_b).unwrap();
2191 store.append_claim(&mut txn, &claim_c).unwrap();
2192 store.append_claim_edge(&mut txn, &edge_ac).unwrap(); store.append_claim_edge(&mut txn, &edge_ab).unwrap(); store.commit(txn).unwrap();
2195
2196 let loaded = store.load_edges_for(&agent, &ref_a).unwrap();
2197 assert_eq!(loaded.len(), 2, "must return both edges");
2198 assert_eq!(loaded[0].to_claim, ref_b, "earlier edge (A→B) must be first");
2200 assert_eq!(loaded[1].to_claim, ref_c, "later edge (A→C) must be second");
2201 }
2202
2203 #[test]
2205 fn read_load_edges_for_empty_when_none() {
2206 let store = make_store();
2207 let agent = make_agent();
2208 let claim = make_claim(&agent);
2209 let claim_ref = claim.claim_ref().clone();
2210
2211 let mut txn = store.begin_atomic(&agent).unwrap();
2212 store.append_claim(&mut txn, &claim).unwrap();
2213 store.commit(txn).unwrap();
2214
2215 let loaded = store.load_edges_for(&agent, &claim_ref).unwrap();
2216 assert!(loaded.is_empty(), "must return empty vec when no edges");
2217 }
2218
2219 #[test]
2221 fn read_load_injected_claims_round_trip() {
2222 use mempill_types::disposition::Disposition;
2223
2224 let store = make_store();
2225 let agent = make_agent();
2226 let claim = make_claim(&agent);
2227 let claim_ref = claim.claim_ref().clone();
2228
2229 let injected_entry = mempill_types::ledger::LedgerEntry {
2230 entry_id: Uuid::new_v4(),
2231 agent_id: agent.clone(),
2232 claim_ref: claim_ref.clone(),
2233 event_kind: LedgerEventKind::ServedAsInjected,
2234 disposition: Disposition::CommittedCheap,
2235 rationale: None,
2236 recorded_at: TransactionTime(Utc::now()),
2237 };
2238
2239 let mut txn = store.begin_atomic(&agent).unwrap();
2240 store.append_claim(&mut txn, &claim).unwrap();
2241 store.append_ledger_entry(&mut txn, &injected_entry).unwrap();
2242 store.commit(txn).unwrap();
2243
2244 let loaded = store.load_injected_claims(&agent).unwrap();
2245 assert_eq!(loaded.len(), 1, "must return one injected claim ref");
2246 assert_eq!(loaded[0], claim_ref);
2247 }
2248
2249 #[test]
2251 fn read_load_injected_claims_empty_when_none() {
2252 let store = make_store();
2253 let agent = make_agent();
2254 let loaded = store.load_injected_claims(&agent).unwrap();
2255 assert!(loaded.is_empty(), "must return empty vec when no injected claims");
2256 }
2257
2258 #[test]
2260 fn read_load_lineage_multi_hop_derived_from() {
2261 let store = make_store();
2262 let agent = make_agent();
2263
2264 let claim_a = make_claim(&agent);
2267 let claim_b = make_claim(&agent);
2268 let claim_c = make_claim(&agent);
2269 let ref_a = claim_a.claim_ref().clone();
2270 let ref_b = claim_b.claim_ref().clone();
2271 let ref_c = claim_c.claim_ref().clone();
2272
2273 let edge_ab = ClaimEdge {
2274 edge_id: Uuid::new_v4(),
2275 agent_id: agent.clone(),
2276 from_claim: ref_a.clone(),
2277 to_claim: ref_b.clone(),
2278 kind: EdgeKind::DerivedFrom,
2279 created_at: TransactionTime(Utc::now() - chrono::Duration::seconds(2)),
2280 };
2281 let edge_bc = ClaimEdge {
2282 edge_id: Uuid::new_v4(),
2283 agent_id: agent.clone(),
2284 from_claim: ref_b.clone(),
2285 to_claim: ref_c.clone(),
2286 kind: EdgeKind::DerivedFrom,
2287 created_at: TransactionTime(Utc::now() - chrono::Duration::seconds(1)),
2288 };
2289
2290 let mut txn = store.begin_atomic(&agent).unwrap();
2291 store.append_claim(&mut txn, &claim_a).unwrap();
2292 store.append_claim(&mut txn, &claim_b).unwrap();
2293 store.append_claim(&mut txn, &claim_c).unwrap();
2294 store.append_claim_edge(&mut txn, &edge_ab).unwrap();
2295 store.append_claim_edge(&mut txn, &edge_bc).unwrap();
2296 store.commit(txn).unwrap();
2297
2298 let lineage = store.load_lineage(&agent, &ref_a).unwrap();
2299 assert_eq!(lineage.len(), 2, "lineage must contain both DerivedFrom hops A→B and B→C");
2300
2301 assert_eq!(lineage[0].from_claim, ref_a, "first edge must start from A");
2303 assert_eq!(lineage[0].to_claim, ref_b, "first edge must point to B");
2304 assert_eq!(lineage[1].from_claim, ref_b, "second edge must start from B");
2306 assert_eq!(lineage[1].to_claim, ref_c, "second edge must point to C");
2307 }
2308
2309 #[test]
2311 fn read_load_lineage_empty_when_no_derived_from_edges() {
2312 let store = make_store();
2313 let agent = make_agent();
2314 let claim = make_claim(&agent);
2315 let claim_ref = claim.claim_ref().clone();
2316
2317 let mut txn = store.begin_atomic(&agent).unwrap();
2318 store.append_claim(&mut txn, &claim).unwrap();
2319 store.commit(txn).unwrap();
2320
2321 let lineage = store.load_lineage(&agent, &claim_ref).unwrap();
2322 assert!(lineage.is_empty(), "load_lineage must return empty vec when no DerivedFrom edges");
2323 }
2324
2325 #[test]
2328 fn begin_atomic_while_txn_open_returns_error() {
2329 let store = make_store();
2330 let agent = make_agent();
2331
2332 let _txn = store.begin_atomic(&agent).expect("first begin_atomic should succeed");
2333 let result = store.begin_atomic(&agent);
2334 assert!(
2335 matches!(result, Err(SqliteStoreError::TxnAlreadyOpen)),
2336 "second begin_atomic must return TxnAlreadyOpen"
2337 );
2338 }
2339
2340 #[test]
2345 fn atomic_unit_all_four_rows_on_commit() {
2346 let store = make_store();
2347 let agent = make_agent();
2348 let claim_a = make_claim(&agent);
2349 let claim_b = make_claim(&agent);
2350 let claim_ref_a = claim_a.claim_ref().clone();
2351 let claim_ref_b = claim_b.claim_ref().clone();
2352
2353 let assertion = mempill_types::validity::ValidityAssertion {
2354 assertion_ref: Uuid::new_v4(),
2355 agent_id: agent.clone(),
2356 target_claim: claim_ref_a.clone(),
2357 kind: AssertionKind::Bound { bound_at: Utc::now() },
2358 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
2359 confidence: mempill_types::claim::Confidence {
2360 value_confidence: 0.9,
2361 valid_time_confidence: 0.9,
2362 },
2363 asserted_at: TransactionTime(Utc::now()),
2364 };
2365 let ledger = make_ledger_entry(&agent, &claim_ref_a);
2366 let edge = ClaimEdge {
2367 edge_id: Uuid::new_v4(),
2368 agent_id: agent.clone(),
2369 from_claim: claim_ref_a.clone(),
2370 to_claim: claim_ref_b.clone(),
2371 kind: EdgeKind::Supersedes,
2372 created_at: TransactionTime(Utc::now()),
2373 };
2374
2375 let mut txn = store.begin_atomic(&agent).unwrap();
2376 store.append_claim(&mut txn, &claim_a).unwrap();
2377 store.append_claim(&mut txn, &claim_b).unwrap();
2378 store.append_validity_assertion(&mut txn, &assertion).unwrap();
2379 store.append_ledger_entry(&mut txn, &ledger).unwrap();
2380 store.append_claim_edge(&mut txn, &edge).unwrap();
2381 store.commit(txn).unwrap();
2382
2383 let slot = store.conn.lock().unwrap();
2384 let conn = slot.as_ref().unwrap();
2385
2386 let claims: i64 = conn
2387 .query_row("SELECT COUNT(*) FROM claims", [], |r| r.get(0))
2388 .unwrap();
2389 let assertions: i64 = conn
2390 .query_row("SELECT COUNT(*) FROM validity_assertions", [], |r| r.get(0))
2391 .unwrap();
2392 let ledger_count: i64 = conn
2393 .query_row("SELECT COUNT(*) FROM ledger_entries", [], |r| r.get(0))
2394 .unwrap();
2395 let edges: i64 = conn
2396 .query_row("SELECT COUNT(*) FROM claim_edges", [], |r| r.get(0))
2397 .unwrap();
2398
2399 assert_eq!(claims, 2, "two claim rows must exist");
2400 assert_eq!(assertions, 1, "one validity_assertion row must exist");
2401 assert_eq!(ledger_count, 1, "one ledger_entry row must exist");
2402 assert_eq!(edges, 1, "one claim_edge row must exist");
2403 }
2404
2405 use mempill_core::ports::pending_adjudication::{PendingAdjudicationPort, PendingAdjudicationRow};
2408 use mempill_types::{
2409 AdjudicationRequest, Belief, CurrencySignal, CurrencyState, OverturnReason, SubjectLineRef,
2410 };
2411
2412 fn make_adj_request(agent: &AgentId) -> AdjudicationRequest {
2413 let claim_ref = ClaimRef(Uuid::new_v4());
2414 let now = TransactionTime(Utc::now());
2415 AdjudicationRequest {
2416 subject_line: SubjectLineRef {
2417 agent_id: agent.clone(),
2418 subject: "user".into(),
2419 predicate: "city".into(),
2420 },
2421 incumbent: Belief {
2422 claim_ref: claim_ref.clone(),
2423 fact: mempill_types::Fact {
2424 subject: "user".into(),
2425 predicate: "city".into(),
2426 value: serde_json::json!("Berlin"),
2427 },
2428 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
2429 valid_time: ValidTime { start: None, end: None, valid_time_confidence: 0.0 , start_granularity: None, end_granularity: None},
2430 transaction_time: now.clone(),
2431 confidence: Confidence { value_confidence: 0.9, valid_time_confidence: 0.0 },
2432 currency_signal: CurrencySignal {
2433 last_refreshed_at: now.clone(),
2434 state: CurrencyState::Fresh,
2435 corroboration_count: 0,
2436 },
2437 criticality: Criticality::Low,
2438 },
2439 challenger: make_claim(agent),
2440 criticality: Criticality::Low,
2441 reason: OverturnReason::ExternalContradiction,
2442 }
2443 }
2444
2445 fn make_pending_row(agent: &AgentId) -> PendingAdjudicationRow {
2446 PendingAdjudicationRow {
2447 handle_id: Uuid::new_v4(),
2448 agent_id: agent.clone(),
2449 subject: "user".into(),
2450 predicate: "city".into(),
2451 challenger_claim_ref: ClaimRef(Uuid::new_v4()),
2452 incumbent_claim_ref: ClaimRef(Uuid::new_v4()),
2453 request_payload: make_adj_request(agent),
2454 queued_at: Utc::now(),
2455 expires_at: None,
2456 status: "pending".to_string(),
2457 }
2458 }
2459
2460 #[test]
2462 fn w3_sqlite_pending_insert_and_get_round_trip() {
2463 let store = make_store();
2464 let pending = store.pending_store();
2465 let agent = make_agent();
2466 let row = make_pending_row(&agent);
2467 let handle_id = row.handle_id;
2468
2469 pending.insert_pending(&row).expect("insert_pending must succeed");
2470
2471 let fetched = pending.get_pending(handle_id).expect("get_pending must succeed");
2472 let fetched = fetched.expect("row must be present");
2473 assert_eq!(fetched.handle_id, handle_id);
2474 assert_eq!(fetched.agent_id, agent);
2475 assert_eq!(fetched.subject, "user");
2476 assert_eq!(fetched.predicate, "city");
2477 assert_eq!(fetched.challenger_claim_ref, row.challenger_claim_ref);
2478 assert_eq!(fetched.incumbent_claim_ref, row.incumbent_claim_ref);
2479 assert_eq!(fetched.status, "pending");
2480 assert!(fetched.expires_at.is_none());
2481 }
2482
2483 #[test]
2485 fn w3_sqlite_pending_get_nonexistent_returns_none() {
2486 let store = make_store();
2487 let pending = store.pending_store();
2488 let result = pending.get_pending(Uuid::new_v4()).expect("get_pending must not error");
2489 assert!(result.is_none(), "unknown handle_id must return None");
2490 }
2491
2492 #[test]
2494 fn w3_sqlite_pending_list_pending_by_agent() {
2495 let store = make_store();
2496 let pending = store.pending_store();
2497 let agent = make_agent();
2498 let agent2 = AgentId("other-agent".into());
2499
2500 let row1 = make_pending_row(&agent);
2501 let row2 = make_pending_row(&agent);
2502 let row3 = make_pending_row(&agent2);
2503
2504 pending.insert_pending(&row1).unwrap();
2505 pending.insert_pending(&row2).unwrap();
2506 pending.insert_pending(&row3).unwrap();
2507
2508 let agent_rows = pending.list_pending(Some(&agent)).unwrap();
2509 assert_eq!(agent_rows.len(), 2, "must return exactly 2 rows for agent");
2510
2511 let all_rows = pending.list_pending(None).unwrap();
2512 assert_eq!(all_rows.len(), 3, "list_pending(None) must return all 3 rows");
2513 }
2514
2515 #[test]
2517 fn w3_sqlite_pending_mark_resolved() {
2518 let store = make_store();
2519 let pending = store.pending_store();
2520 let agent = make_agent();
2521 let row = make_pending_row(&agent);
2522 let handle_id = row.handle_id;
2523
2524 pending.insert_pending(&row).unwrap();
2525 pending.mark_resolved(handle_id).unwrap();
2526
2527 let fetched = pending.get_pending(handle_id).unwrap().unwrap();
2529 assert_eq!(fetched.status, "resolved", "status must be 'resolved' after mark_resolved");
2530
2531 let pending_rows = pending.list_pending(Some(&agent)).unwrap();
2533 assert!(pending_rows.is_empty(), "resolved row must not appear in list_pending");
2534 }
2535
2536 #[test]
2542 fn w3_sqlite_pending_durability_shared_arc() {
2543 let conn = open_in_memory().expect("in-memory connection must open");
2544 let persistence = SqlitePersistenceStore::new(conn);
2545 let pending = persistence.pending_store();
2546 let agent = make_agent();
2547 let row = make_pending_row(&agent);
2548 let handle_id = row.handle_id;
2549
2550 pending.insert_pending(&row).unwrap();
2551 drop(pending); let pending2 = persistence.pending_store();
2555 let fetched = pending2.get_pending(handle_id).unwrap();
2556 assert!(fetched.is_some(), "pending row must survive store handle drop (durability via shared Arc)");
2557 assert_eq!(fetched.unwrap().handle_id, handle_id);
2558 }
2559}