1use std::sync::Arc;
29
30use mempill_core::{
31 ports::pending_adjudication::{PendingAdjudicationPort, PendingAdjudicationRow},
32 ports::persistence::PersistencePort,
33 EngineConfig, EngineHandle,
34};
35use mempill_types::{
36 claim::{Cardinality, Claim, Confidence, Criticality, Fact},
37 disposition::Disposition,
38 edge::{ClaimEdge, EdgeKind},
39 identity::{AgentId, ClaimRef},
40 ledger::{LedgerEntry, LedgerEventKind},
41 provenance::{ExternalAnchor, ExternalKind, ProvenanceLabel},
42 time::{date_granularity_to_str, str_to_date_granularity, TransactionTime, ValidTime},
43 validity::{AssertionKind, ValidityAssertion},
44};
45
46use crate::{
47 connection::{PostgresPersistenceStore, PostgresStoreError},
48 txn::PostgresTxn,
49};
50
51fn provenance_to_str(p: &ProvenanceLabel) -> &'static str {
54 match p {
55 ProvenanceLabel::ModelDerived => "ModelDerived",
56 ProvenanceLabel::RecallReEntry => "RecallReEntry",
57 ProvenanceLabel::External(ExternalKind::UserAsserted) => "External_UserAsserted",
58 ProvenanceLabel::External(ExternalKind::ExternalFirstHand) => "External_ExternalFirstHand",
59 _ => "Unknown",
60 }
61}
62
63fn str_to_provenance(s: &str) -> Result<ProvenanceLabel, PostgresStoreError> {
64 match s {
65 "ModelDerived" => Ok(ProvenanceLabel::ModelDerived),
66 "RecallReEntry" => Ok(ProvenanceLabel::RecallReEntry),
67 "External_UserAsserted" => Ok(ProvenanceLabel::External(ExternalKind::UserAsserted)),
68 "External_ExternalFirstHand" => Ok(ProvenanceLabel::External(ExternalKind::ExternalFirstHand)),
69 other => Err(PostgresStoreError::Mapping(format!("unknown provenance_label: {other}"))),
70 }
71}
72
73fn cardinality_to_str(c: &Cardinality) -> &'static str {
74 match c {
75 Cardinality::Functional => "Functional",
76 Cardinality::SetValued => "SetValued",
77 Cardinality::Unknown => "Unknown",
78 }
79}
80
81fn str_to_cardinality(s: &str) -> Result<Cardinality, PostgresStoreError> {
82 match s {
83 "Functional" => Ok(Cardinality::Functional),
84 "SetValued" => Ok(Cardinality::SetValued),
85 "Unknown" => Ok(Cardinality::Unknown),
86 other => Err(PostgresStoreError::Mapping(format!("unknown cardinality: {other}"))),
87 }
88}
89
90fn criticality_to_str(c: &Criticality) -> &'static str {
91 match c {
92 Criticality::Low => "Low",
93 Criticality::Medium => "Medium",
94 Criticality::High => "High",
95 Criticality::Critical => "Critical",
96 }
97}
98
99fn str_to_criticality(s: &str) -> Result<Criticality, PostgresStoreError> {
100 match s {
101 "Low" => Ok(Criticality::Low),
102 "Medium" => Ok(Criticality::Medium),
103 "High" => Ok(Criticality::High),
104 "Critical" => Ok(Criticality::Critical),
105 other => Err(PostgresStoreError::Mapping(format!("unknown criticality: {other}"))),
106 }
107}
108
109fn edge_kind_to_str(k: &EdgeKind) -> &'static str {
110 match k {
111 EdgeKind::DerivedFrom => "DerivedFrom",
112 EdgeKind::Supersedes => "Supersedes",
113 EdgeKind::DependsOn => "DependsOn",
114 EdgeKind::MutualExclusion => "MutualExclusion",
115 _ => "Unknown",
117 }
118}
119
120fn str_to_edge_kind(s: &str) -> Result<EdgeKind, PostgresStoreError> {
121 match s {
122 "DerivedFrom" => Ok(EdgeKind::DerivedFrom),
123 "Supersedes" => Ok(EdgeKind::Supersedes),
124 "DependsOn" => Ok(EdgeKind::DependsOn),
125 "MutualExclusion" => Ok(EdgeKind::MutualExclusion),
126 other => Err(PostgresStoreError::Mapping(format!("unknown edge_kind: {other}"))),
127 }
128}
129
130fn ledger_event_kind_to_str(k: &LedgerEventKind) -> &'static str {
131 match k {
132 LedgerEventKind::ClaimCommitted => "ClaimCommitted",
133 LedgerEventKind::ValidityAsserted => "ValidityAsserted",
134 LedgerEventKind::AdjudicationRequested => "AdjudicationRequested",
135 LedgerEventKind::AdjudicationResolved => "AdjudicationResolved",
136 LedgerEventKind::RecallReEntryDetected => "RecallReEntryDetected",
137 LedgerEventKind::Quarantined => "Quarantined",
138 LedgerEventKind::DependentFlaggedPendingReview => "DependentFlaggedPendingReview",
139 LedgerEventKind::ServedAsInjected => "ServedAsInjected",
140 LedgerEventKind::AdjudicationExpired => "AdjudicationExpired",
141 _ => "Unknown",
143 }
144}
145
146fn str_to_ledger_event_kind(s: &str) -> Result<LedgerEventKind, PostgresStoreError> {
147 match s {
148 "ClaimCommitted" => Ok(LedgerEventKind::ClaimCommitted),
149 "ValidityAsserted" => Ok(LedgerEventKind::ValidityAsserted),
150 "AdjudicationRequested" => Ok(LedgerEventKind::AdjudicationRequested),
151 "AdjudicationResolved" => Ok(LedgerEventKind::AdjudicationResolved),
152 "RecallReEntryDetected" => Ok(LedgerEventKind::RecallReEntryDetected),
153 "Quarantined" => Ok(LedgerEventKind::Quarantined),
154 "DependentFlaggedPendingReview" => Ok(LedgerEventKind::DependentFlaggedPendingReview),
155 "ServedAsInjected" => Ok(LedgerEventKind::ServedAsInjected),
156 "AdjudicationExpired" => Ok(LedgerEventKind::AdjudicationExpired),
157 other => Err(PostgresStoreError::Mapping(format!("unknown ledger event_kind: {other}"))),
158 }
159}
160
161fn disposition_to_str(d: &Disposition) -> &'static str {
162 match d {
163 Disposition::CommittedCheap => "CommittedCheap",
164 Disposition::CommittedInferred => "CommittedInferred",
165 Disposition::QueuedForAdjudication => "QueuedForAdjudication",
166 Disposition::Contested => "Contested",
167 Disposition::PendingConflict => "PendingConflict",
168 Disposition::PendingReview => "PendingReview",
169 Disposition::PendingLowConfidence => "PendingLowConfidence",
170 Disposition::Quarantined => "Quarantined",
171 Disposition::Superseded => "Superseded",
172 Disposition::Invalidated => "Invalidated",
173 Disposition::Reinstated => "Reinstated",
174 Disposition::Rejected => "Rejected",
175 _ => "Unknown",
177 }
178}
179
180fn str_to_disposition(s: &str) -> Result<Disposition, PostgresStoreError> {
181 match s {
182 "CommittedCheap" => Ok(Disposition::CommittedCheap),
183 "CommittedInferred" => Ok(Disposition::CommittedInferred),
184 "QueuedForAdjudication" => Ok(Disposition::QueuedForAdjudication),
185 "Contested" => Ok(Disposition::Contested),
186 "PendingConflict" => Ok(Disposition::PendingConflict),
187 "PendingReview" => Ok(Disposition::PendingReview),
188 "PendingLowConfidence" => Ok(Disposition::PendingLowConfidence),
189 "Quarantined" => Ok(Disposition::Quarantined),
190 "Superseded" => Ok(Disposition::Superseded),
191 "Invalidated" => Ok(Disposition::Invalidated),
192 "Reinstated" => Ok(Disposition::Reinstated),
193 "Rejected" => Ok(Disposition::Rejected),
194 other => Err(PostgresStoreError::Mapping(format!("unknown disposition: {other}"))),
195 }
196}
197
198const CLAIM_SELECT_COLS: &str = "
208 claim_id, agent_id, subject, predicate, value::text, cardinality,
209 provenance_label, nearest_external_anchor_id, derivation_depth,
210 tx_time, valid_time_start, valid_time_end, valid_time_confidence,
211 value_confidence, criticality, derived_from,
212 metadata::text, snapshot_schema_version,
213 valid_time_start_granularity, valid_time_end_granularity
214";
215
216fn row_to_claim(row: &postgres::Row) -> Result<Claim, PostgresStoreError> {
240 let claim_id_str: String = row.get(0);
241 let agent_id_str: String = row.get(1);
242 let subject: String = row.get(2);
243 let predicate: String = row.get(3);
244 let value_json: String = row.get(4);
245 let cardinality_str: String = row.get(5);
246 let provenance_str: String = row.get(6);
247 let nearest_anchor_str: Option<String> = row.get(7);
248 let derivation_depth: i32 = row.get(8);
249 let tx_time_str: String = row.get(9);
250 let valid_time_start_str: Option<String> = row.get(10);
251 let valid_time_end_str: Option<String> = row.get(11);
252 let valid_time_confidence: f64 = row.get(12);
253 let value_confidence: f64 = row.get(13);
254 let criticality_str: String = row.get(14);
255 let derived_from_json: String = row.get(15);
256 let metadata_json: Option<String> = row.get(16);
257 let snapshot_schema_version_raw: Option<i32> = row.get(17);
258 let start_granularity_str: Option<String> = row.get(18);
259 let end_granularity_str: Option<String> = row.get(19);
260
261 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
262 .map_err(|e| PostgresStoreError::Mapping(format!("claim_id UUID: {e}")))?;
263
264 let value: serde_json::Value = serde_json::from_str(&value_json)
265 .map_err(|e| PostgresStoreError::Mapping(format!("value JSON: {e}")))?;
266
267 let cardinality = str_to_cardinality(&cardinality_str)?;
268 let provenance = str_to_provenance(&provenance_str)?;
269
270 let nearest_external_anchor: Option<ClaimRef> = nearest_anchor_str
271 .map(|s| {
272 uuid::Uuid::parse_str(&s)
273 .map(ClaimRef)
274 .map_err(|e| PostgresStoreError::Mapping(format!("anchor UUID: {e}")))
275 })
276 .transpose()?;
277
278 let tx_time = chrono::DateTime::parse_from_rfc3339(&tx_time_str)
279 .map(|dt| dt.with_timezone(&chrono::Utc))
280 .map_err(|e| PostgresStoreError::Mapping(format!("tx_time parse: {e}")))?;
281
282 let valid_time_start = valid_time_start_str
283 .map(|s| {
284 chrono::DateTime::parse_from_rfc3339(&s)
285 .map(|dt| dt.with_timezone(&chrono::Utc))
286 .map_err(|e| PostgresStoreError::Mapping(format!("valid_time_start: {e}")))
287 })
288 .transpose()?;
289
290 let valid_time_end = valid_time_end_str
291 .map(|s| {
292 chrono::DateTime::parse_from_rfc3339(&s)
293 .map(|dt| dt.with_timezone(&chrono::Utc))
294 .map_err(|e| PostgresStoreError::Mapping(format!("valid_time_end: {e}")))
295 })
296 .transpose()?;
297
298 let criticality = str_to_criticality(&criticality_str)?;
299
300 let derived_from_uuids: Vec<String> = serde_json::from_str(&derived_from_json)
301 .map_err(|e| PostgresStoreError::Mapping(format!("derived_from JSON: {e}")))?;
302
303 let derived_from: Vec<ClaimRef> = derived_from_uuids
304 .iter()
305 .map(|s| {
306 uuid::Uuid::parse_str(s)
307 .map(ClaimRef)
308 .map_err(|e| PostgresStoreError::Mapping(format!("derived_from UUID: {e}")))
309 })
310 .collect::<Result<_, _>>()?;
311
312 let metadata: Option<serde_json::Value> = metadata_json
313 .map(|s| {
314 serde_json::from_str(&s)
315 .map_err(|e| PostgresStoreError::Mapping(format!("metadata JSON: {e}")))
316 })
317 .transpose()?;
318
319 let snapshot_schema_version: Option<u32> = snapshot_schema_version_raw.map(|v| v as u32);
320
321 Ok(Claim::new(
322 ClaimRef(claim_id),
323 AgentId(agent_id_str),
324 Fact { subject, predicate, value },
325 cardinality,
326 provenance,
327 ExternalAnchor {
328 nearest_external_anchor,
329 derivation_depth: derivation_depth as u32,
330 },
331 TransactionTime(tx_time),
332 ValidTime {
333 start: valid_time_start,
334 end: valid_time_end,
335 valid_time_confidence: valid_time_confidence as f32,
336 start_granularity: start_granularity_str
337 .as_deref()
338 .and_then(str_to_date_granularity),
339 end_granularity: end_granularity_str
340 .as_deref()
341 .and_then(str_to_date_granularity),
342 },
343 Confidence {
344 value_confidence: value_confidence as f32,
345 valid_time_confidence: valid_time_confidence as f32,
346 },
347 criticality,
348 derived_from,
349 metadata,
350 snapshot_schema_version,
351 ))
352}
353
354fn row_to_edge(row: &postgres::Row) -> Result<ClaimEdge, PostgresStoreError> {
356 let edge_id_str: String = row.get(0);
357 let agent_id_str: String = row.get(1);
358 let from_claim_str: String = row.get(2);
359 let to_claim_str: String = row.get(3);
360 let kind_str: String = row.get(4);
361 let created_at_str: String = row.get(5);
362
363 let edge_id = uuid::Uuid::parse_str(&edge_id_str)
364 .map_err(|e| PostgresStoreError::Mapping(format!("edge_id UUID: {e}")))?;
365 let from_claim = uuid::Uuid::parse_str(&from_claim_str)
366 .map(ClaimRef)
367 .map_err(|e| PostgresStoreError::Mapping(format!("from_claim UUID: {e}")))?;
368 let to_claim = uuid::Uuid::parse_str(&to_claim_str)
369 .map(ClaimRef)
370 .map_err(|e| PostgresStoreError::Mapping(format!("to_claim UUID: {e}")))?;
371 let kind = str_to_edge_kind(&kind_str)?;
372 let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
373 .map(|dt| dt.with_timezone(&chrono::Utc))
374 .map_err(|e| PostgresStoreError::Mapping(format!("created_at parse: {e}")))?;
375
376 Ok(ClaimEdge {
377 edge_id,
378 agent_id: AgentId(agent_id_str),
379 from_claim,
380 to_claim,
381 kind,
382 created_at: TransactionTime(created_at),
383 })
384}
385
386impl PostgresPersistenceStore {
389 pub fn pending_store(&self) -> PostgresPendingStore {
395 PostgresPendingStore::new(self.pool.clone())
396 }
397}
398
399impl PersistencePort for PostgresPersistenceStore {
400 type Transaction = PostgresTxn;
401 type Error = PostgresStoreError;
402
403 fn begin_atomic(&self, agent_id: &AgentId) -> Result<PostgresTxn, PostgresStoreError> {
410 let conn = self.pool.get()?;
411 PostgresTxn::begin(agent_id.clone(), conn)
412 }
413
414 fn commit(&self, txn: PostgresTxn) -> Result<(), PostgresStoreError> {
416 txn.commit_and_drop()
417 }
418
419 fn rollback(&self, txn: PostgresTxn) -> Result<(), PostgresStoreError> {
421 txn.rollback_and_drop()
422 }
423
424 fn append_claim(
430 &self,
431 txn: &mut PostgresTxn,
432 claim: &Claim,
433 ) -> Result<ClaimRef, PostgresStoreError> {
434 let claim_id = claim.claim_ref().0.to_string();
435 let agent_id = claim.agent_id().0.clone();
436 let fact = claim.fact();
437 let value_jsonb: &serde_json::Value = &fact.value;
442 let cardinality = cardinality_to_str(claim.cardinality()).to_owned();
443 let provenance = provenance_to_str(claim.provenance()).to_owned();
444 let anchor = claim.external_anchor();
445 let nearest_anchor: Option<String> =
446 anchor.nearest_external_anchor.as_ref().map(|r| r.0.to_string());
447 let derivation_depth = anchor.derivation_depth as i32;
448 let tx_time = claim.transaction_time().0.to_rfc3339();
449 let vt = claim.valid_time();
450 let valid_time_start: Option<String> = vt.start.map(|dt| dt.to_rfc3339());
451 let valid_time_end: Option<String> = vt.end.map(|dt| dt.to_rfc3339());
452 let valid_time_confidence = vt.valid_time_confidence as f64;
453 let valid_time_start_granularity: Option<&'static str> =
454 vt.start_granularity.map(date_granularity_to_str);
455 let valid_time_end_granularity: Option<&'static str> =
456 vt.end_granularity.map(date_granularity_to_str);
457 let conf = claim.confidence();
458 let value_confidence = conf.value_confidence as f64;
459 let criticality = criticality_to_str(claim.criticality()).to_owned();
460 let derived_from_refs: Vec<String> =
461 claim.derived_from().iter().map(|r| r.0.to_string()).collect();
462 let derived_from_json = serde_json::to_string(&derived_from_refs)
463 .map_err(|e| PostgresStoreError::Mapping(format!("derived_from serialization: {e}")))?;
464 let metadata_jsonb: Option<serde_json::Value> = claim.metadata().cloned();
466 let snapshot_schema_version: Option<i32> =
467 claim.snapshot_schema_version().map(|v| v as i32);
468
469 txn.client().execute(
470 "INSERT INTO claims (
471 claim_id, agent_id, subject, predicate, value, cardinality,
472 provenance_label, nearest_external_anchor_id, derivation_depth,
473 tx_time, valid_time_start, valid_time_end, valid_time_confidence,
474 value_confidence, criticality, derived_from,
475 metadata, snapshot_schema_version, embedding_model_id,
476 valid_time_start_granularity, valid_time_end_granularity
477 ) VALUES (
478 $1, $2, $3, $4, $5, $6,
479 $7, $8, $9,
480 $10, $11, $12, $13,
481 $14, $15, $16,
482 $17, $18, NULL,
483 $19, $20
484 )",
485 &[
486 &claim_id,
487 &agent_id,
488 &fact.subject.as_str(),
489 &fact.predicate.as_str(),
490 &value_jsonb,
491 &cardinality,
492 &provenance,
493 &nearest_anchor,
494 &derivation_depth,
495 &tx_time,
496 &valid_time_start,
497 &valid_time_end,
498 &valid_time_confidence,
499 &value_confidence,
500 &criticality,
501 &derived_from_json,
502 &metadata_jsonb,
503 &snapshot_schema_version,
504 &valid_time_start_granularity,
505 &valid_time_end_granularity,
506 ],
507 )?;
508
509 Ok(claim.claim_ref().clone())
510 }
511
512 fn append_validity_assertion(
514 &self,
515 txn: &mut PostgresTxn,
516 assertion: &ValidityAssertion,
517 ) -> Result<(), PostgresStoreError> {
518 let assertion_id = assertion.assertion_ref.to_string();
519 let agent_id = assertion.agent_id.0.clone();
520 let target_claim_id = assertion.target_claim.0.to_string();
521 let provenance = provenance_to_str(&assertion.provenance).to_owned();
522 let value_confidence = assertion.confidence.value_confidence as f64;
523 let valid_time_confidence = assertion.confidence.valid_time_confidence as f64;
524 let asserted_at = assertion.asserted_at.0.to_rfc3339();
525
526 let (assertion_kind, bound_at, reopen_at): (&str, Option<String>, Option<String>) =
527 match &assertion.kind {
528 AssertionKind::Bound { bound_at } => ("Bound", Some(bound_at.to_rfc3339()), None),
529 AssertionKind::Reopen { reopen_at } => ("Reopen", None, Some(reopen_at.to_rfc3339())),
530 _ => ("Unknown", None, None),
532 };
533
534 txn.client().execute(
535 "INSERT INTO validity_assertions (
536 assertion_id, agent_id, target_claim_id,
537 assertion_kind, bound_at, reopen_at,
538 provenance_label, value_confidence, valid_time_confidence, asserted_at
539 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
540 &[
541 &assertion_id,
542 &agent_id,
543 &target_claim_id,
544 &assertion_kind,
545 &bound_at,
546 &reopen_at,
547 &provenance,
548 &value_confidence,
549 &valid_time_confidence,
550 &asserted_at,
551 ],
552 )?;
553
554 Ok(())
555 }
556
557 fn append_ledger_entry(
566 &self,
567 txn: &mut PostgresTxn,
568 entry: &LedgerEntry,
569 ) -> Result<(), PostgresStoreError> {
570 let entry_id = entry.entry_id.to_string();
571 let agent_id = entry.agent_id.0.clone();
572 let claim_id = entry.claim_ref.0.to_string();
573 let event_kind = ledger_event_kind_to_str(&entry.event_kind).to_owned();
574 let disposition = disposition_to_str(&entry.disposition).to_owned();
575 let rationale_jsonb: Option<serde_json::Value> = entry.rationale.clone();
578 let recorded_at = entry.recorded_at.0.to_rfc3339();
579
580 let row = txn.client().query_one(
582 "SELECT COALESCE(MAX(stream_seq), 0) + 1 FROM ledger_entries WHERE agent_id = $1",
583 &[&agent_id],
584 )?;
585 let stream_seq: i64 = row.get(0);
586
587 txn.client().execute(
588 "INSERT INTO ledger_entries (
589 entry_id, agent_id, claim_id, event_kind, disposition, rationale, recorded_at, stream_seq
590 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
591 &[
592 &entry_id,
593 &agent_id,
594 &claim_id,
595 &event_kind,
596 &disposition,
597 &rationale_jsonb,
598 &recorded_at,
599 &stream_seq,
600 ],
601 )?;
602
603 Ok(())
604 }
605
606 fn append_claim_edge(
608 &self,
609 txn: &mut PostgresTxn,
610 edge: &ClaimEdge,
611 ) -> Result<(), PostgresStoreError> {
612 let edge_id = edge.edge_id.to_string();
613 let agent_id = edge.agent_id.0.clone();
614 let from_claim_id = edge.from_claim.0.to_string();
615 let to_claim_id = edge.to_claim.0.to_string();
616 let edge_kind = edge_kind_to_str(&edge.kind).to_owned();
617 let created_at = edge.created_at.0.to_rfc3339();
618
619 txn.client().execute(
620 "INSERT INTO claim_edges (
621 edge_id, agent_id, from_claim_id, to_claim_id, edge_kind, created_at
622 ) VALUES ($1, $2, $3, $4, $5, $6)",
623 &[
624 &edge_id,
625 &agent_id,
626 &from_claim_id,
627 &to_claim_id,
628 &edge_kind,
629 &created_at,
630 ],
631 )?;
632
633 Ok(())
634 }
635
636 fn load_subject_line(
644 &self,
645 agent_id: &AgentId,
646 subject: &str,
647 predicate: &str,
648 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
649 ) -> Result<Vec<Claim>, PostgresStoreError> {
650 let mut conn = self.pool.get()?;
651 if let Some(cutoff) = as_of_tx_time {
652 let cutoff_str = cutoff.to_rfc3339();
658 let sql = format!(
659 "SELECT {CLAIM_SELECT_COLS} FROM claims
660 WHERE agent_id = $1 AND subject = $2 AND predicate = $3
661 AND tx_time <= $4
662 ORDER BY tx_time ASC"
663 );
664 let rows = conn.query(
665 &sql,
666 &[&agent_id.0.as_str(), &subject, &predicate, &cutoff_str.as_str()],
667 )?;
668 rows.iter().map(row_to_claim).collect()
669 } else {
670 let sql = format!(
671 "SELECT {CLAIM_SELECT_COLS} FROM claims
672 WHERE agent_id = $1 AND subject = $2 AND predicate = $3
673 ORDER BY tx_time ASC"
674 );
675 let rows = conn.query(
676 &sql,
677 &[&agent_id.0.as_str(), &subject, &predicate],
678 )?;
679 rows.iter().map(row_to_claim).collect()
680 }
681 }
682
683 fn load_claim(
685 &self,
686 agent_id: &AgentId,
687 claim_ref: &ClaimRef,
688 ) -> Result<Option<Claim>, PostgresStoreError> {
689 let mut conn = self.pool.get()?;
690 let claim_id_str = claim_ref.0.to_string();
691 let sql = format!(
692 "SELECT {CLAIM_SELECT_COLS} FROM claims WHERE agent_id = $1 AND claim_id = $2"
693 );
694 let rows = conn.query(&sql, &[&agent_id.0.as_str(), &claim_id_str.as_str()])?;
695 match rows.first() {
696 None => Ok(None),
697 Some(row) => Ok(Some(row_to_claim(row)?)),
698 }
699 }
700
701 fn load_validity_assertions_for(
703 &self,
704 agent_id: &AgentId,
705 claim_ref: &ClaimRef,
706 ) -> Result<Vec<ValidityAssertion>, PostgresStoreError> {
707 let mut conn = self.pool.get()?;
708 let claim_id_str = claim_ref.0.to_string();
709 let rows = conn.query(
710 "SELECT assertion_id, agent_id, target_claim_id,
711 assertion_kind, bound_at, reopen_at,
712 provenance_label, value_confidence, valid_time_confidence, asserted_at
713 FROM validity_assertions
714 WHERE agent_id = $1 AND target_claim_id = $2
715 ORDER BY asserted_at ASC",
716 &[&agent_id.0.as_str(), &claim_id_str.as_str()],
717 )?;
718
719 rows.iter()
720 .map(|row| {
721 let assertion_id_str: String = row.get(0);
722 let agent_id_str: String = row.get(1);
723 let target_claim_str: String = row.get(2);
724 let kind_str: String = row.get(3);
725 let bound_at_str: Option<String> = row.get(4);
726 let reopen_at_str: Option<String> = row.get(5);
727 let prov_str: String = row.get(6);
728 let value_confidence: f64 = row.get(7);
729 let valid_time_confidence: f64 = row.get(8);
730 let asserted_at_str: String = row.get(9);
731
732 let assertion_ref = uuid::Uuid::parse_str(&assertion_id_str)
733 .map_err(|e| PostgresStoreError::Mapping(format!("assertion_id UUID: {e}")))?;
734 let target_claim = uuid::Uuid::parse_str(&target_claim_str)
735 .map(ClaimRef)
736 .map_err(|e| PostgresStoreError::Mapping(format!("target_claim UUID: {e}")))?;
737 let provenance = str_to_provenance(&prov_str)?;
738 let asserted_at = chrono::DateTime::parse_from_rfc3339(&asserted_at_str)
739 .map(|dt| dt.with_timezone(&chrono::Utc))
740 .map_err(|e| PostgresStoreError::Mapping(format!("asserted_at: {e}")))?;
741
742 let kind = match kind_str.as_str() {
743 "Bound" => {
744 let s = bound_at_str.ok_or_else(|| {
745 PostgresStoreError::Mapping("bound_at is NULL for Bound assertion".into())
746 })?;
747 let dt = chrono::DateTime::parse_from_rfc3339(&s)
748 .map(|dt| dt.with_timezone(&chrono::Utc))
749 .map_err(|e| PostgresStoreError::Mapping(format!("bound_at: {e}")))?;
750 AssertionKind::Bound { bound_at: dt }
751 }
752 "Reopen" => {
753 let s = reopen_at_str.ok_or_else(|| {
754 PostgresStoreError::Mapping("reopen_at is NULL for Reopen assertion".into())
755 })?;
756 let dt = chrono::DateTime::parse_from_rfc3339(&s)
757 .map(|dt| dt.with_timezone(&chrono::Utc))
758 .map_err(|e| PostgresStoreError::Mapping(format!("reopen_at: {e}")))?;
759 AssertionKind::Reopen { reopen_at: dt }
760 }
761 other => {
762 return Err(PostgresStoreError::Mapping(format!(
763 "unknown assertion_kind: {other}"
764 )))
765 }
766 };
767
768 Ok(ValidityAssertion {
769 assertion_ref,
770 agent_id: AgentId(agent_id_str),
771 target_claim,
772 kind,
773 provenance,
774 confidence: Confidence {
775 value_confidence: value_confidence as f32,
776 valid_time_confidence: valid_time_confidence as f32,
777 },
778 asserted_at: TransactionTime(asserted_at),
779 })
780 })
781 .collect()
782 }
783
784 fn load_ledger(
787 &self,
788 agent_id: &AgentId,
789 from: Option<&TransactionTime>,
790 limit: usize,
791 ) -> Result<Vec<LedgerEntry>, PostgresStoreError> {
792 let mut conn = self.pool.get()?;
793 let limit_i64 = limit as i64;
794
795 let map_row = |row: &postgres::Row| -> Result<LedgerEntry, PostgresStoreError> {
796 let entry_id_str: String = row.get(0);
797 let agent_id_str: String = row.get(1);
798 let claim_id_str: String = row.get(2);
799 let event_kind_str: String = row.get(3);
800 let disposition_str: String = row.get(4);
801 let rationale_json: Option<String> = row.get(5);
802 let recorded_at_str: String = row.get(6);
803
804 let entry_id = uuid::Uuid::parse_str(&entry_id_str)
805 .map_err(|e| PostgresStoreError::Mapping(format!("entry_id UUID: {e}")))?;
806 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
807 .map(ClaimRef)
808 .map_err(|e| PostgresStoreError::Mapping(format!("claim_id UUID: {e}")))?;
809 let event_kind = str_to_ledger_event_kind(&event_kind_str)?;
810 let disposition = str_to_disposition(&disposition_str)?;
811 let rationale: Option<serde_json::Value> = rationale_json
812 .map(|s| {
813 serde_json::from_str(&s)
814 .map_err(|e| PostgresStoreError::Mapping(format!("rationale JSON: {e}")))
815 })
816 .transpose()?;
817 let recorded_at = chrono::DateTime::parse_from_rfc3339(&recorded_at_str)
818 .map(|dt| dt.with_timezone(&chrono::Utc))
819 .map_err(|e| PostgresStoreError::Mapping(format!("recorded_at: {e}")))?;
820
821 Ok(LedgerEntry {
822 entry_id,
823 agent_id: AgentId(agent_id_str),
824 claim_ref: claim_id,
825 event_kind,
826 disposition,
827 rationale,
828 recorded_at: TransactionTime(recorded_at),
829 })
830 };
831
832 let rows = if let Some(from_time) = from {
833 let from_str = from_time.0.to_rfc3339();
834 conn.query(
835 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale::text, recorded_at
836 FROM ledger_entries
837 WHERE agent_id = $1 AND recorded_at >= $2
838 ORDER BY recorded_at ASC
839 LIMIT $3",
840 &[&agent_id.0.as_str(), &from_str.as_str(), &limit_i64],
841 )?
842 } else {
843 conn.query(
844 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale::text, recorded_at
845 FROM ledger_entries
846 WHERE agent_id = $1
847 ORDER BY recorded_at ASC
848 LIMIT $2",
849 &[&agent_id.0.as_str(), &limit_i64],
850 )?
851 };
852
853 rows.iter().map(map_row).collect()
854 }
855
856 fn load_ledger_for_claims(
860 &self,
861 agent_id: &AgentId,
862 claim_refs: &[ClaimRef],
863 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
864 ) -> Result<Vec<LedgerEntry>, PostgresStoreError> {
865 if claim_refs.is_empty() {
866 return Ok(vec![]);
867 }
868
869 let mut conn = self.pool.get()?;
870
871 let map_row = |row: &postgres::Row| -> Result<LedgerEntry, PostgresStoreError> {
872 let entry_id_str: String = row.get(0);
873 let agent_id_str: String = row.get(1);
874 let claim_id_str: String = row.get(2);
875 let event_kind_str: String = row.get(3);
876 let disposition_str: String = row.get(4);
877 let rationale_json: Option<String> = row.get(5);
878 let recorded_at_str: String = row.get(6);
879
880 let entry_id = uuid::Uuid::parse_str(&entry_id_str)
881 .map_err(|e| PostgresStoreError::Mapping(format!("entry_id UUID: {e}")))?;
882 let claim_id = uuid::Uuid::parse_str(&claim_id_str)
883 .map(ClaimRef)
884 .map_err(|e| PostgresStoreError::Mapping(format!("claim_id UUID: {e}")))?;
885 let event_kind = str_to_ledger_event_kind(&event_kind_str)?;
886 let disposition = str_to_disposition(&disposition_str)?;
887 let rationale: Option<serde_json::Value> = rationale_json
888 .map(|s| {
889 serde_json::from_str(&s)
890 .map_err(|e| PostgresStoreError::Mapping(format!("rationale JSON: {e}")))
891 })
892 .transpose()?;
893 let recorded_at = chrono::DateTime::parse_from_rfc3339(&recorded_at_str)
894 .map(|dt| dt.with_timezone(&chrono::Utc))
895 .map_err(|e| PostgresStoreError::Mapping(format!("recorded_at: {e}")))?;
896
897 Ok(LedgerEntry {
898 entry_id,
899 agent_id: AgentId(agent_id_str),
900 claim_ref: claim_id,
901 event_kind,
902 disposition,
903 rationale,
904 recorded_at: TransactionTime(recorded_at),
905 })
906 };
907
908 let id_strings: Vec<String> = claim_refs.iter().map(|r| r.0.to_string()).collect();
910 let ids_ref: Vec<&str> = id_strings.iter().map(|s| s.as_str()).collect();
911
912 let rows = if let Some(as_of) = as_of_tx_time {
915 let as_of_str = as_of.to_rfc3339();
916 conn.query(
917 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale::text, recorded_at
918 FROM ledger_entries
919 WHERE agent_id = $1 AND claim_id = ANY($2) AND recorded_at <= $3
920 ORDER BY recorded_at ASC",
921 &[&agent_id.0.as_str(), &ids_ref.as_slice(), &as_of_str.as_str()],
922 )?
923 } else {
924 conn.query(
925 "SELECT entry_id, agent_id, claim_id, event_kind, disposition, rationale::text, recorded_at
926 FROM ledger_entries
927 WHERE agent_id = $1 AND claim_id = ANY($2)
928 ORDER BY recorded_at ASC",
929 &[&agent_id.0.as_str(), &ids_ref.as_slice()],
930 )?
931 };
932
933 rows.iter().map(map_row).collect()
934 }
935
936 fn load_edges_for(
938 &self,
939 agent_id: &AgentId,
940 claim_ref: &ClaimRef,
941 ) -> Result<Vec<ClaimEdge>, PostgresStoreError> {
942 let mut conn = self.pool.get()?;
943 let claim_id_str = claim_ref.0.to_string();
944
945 let rows = conn.query(
946 "SELECT edge_id, agent_id, from_claim_id, to_claim_id, edge_kind, created_at
947 FROM claim_edges
948 WHERE agent_id = $1
949 AND (from_claim_id = $2 OR to_claim_id = $2)
950 ORDER BY created_at ASC",
951 &[&agent_id.0.as_str(), &claim_id_str.as_str()],
952 )?;
953
954 rows.iter().map(row_to_edge).collect()
955 }
956
957 fn load_injected_claims(
959 &self,
960 agent_id: &AgentId,
961 ) -> Result<Vec<ClaimRef>, PostgresStoreError> {
962 let mut conn = self.pool.get()?;
963
964 let rows = conn.query(
965 "SELECT claim_id
966 FROM ledger_entries
967 WHERE agent_id = $1 AND event_kind = 'ServedAsInjected'
968 GROUP BY claim_id
969 ORDER BY MIN(recorded_at) ASC",
970 &[&agent_id.0.as_str()],
971 )?;
972
973 rows.iter()
974 .map(|row| {
975 let claim_id_str: String = row.get(0);
976 uuid::Uuid::parse_str(&claim_id_str)
977 .map(ClaimRef)
978 .map_err(|e| PostgresStoreError::Mapping(format!("claim_id UUID: {e}")))
979 })
980 .collect()
981 }
982
983 fn load_lineage(
989 &self,
990 agent_id: &AgentId,
991 claim_ref: &ClaimRef,
992 ) -> Result<Vec<ClaimEdge>, PostgresStoreError> {
993 let mut conn = self.pool.get()?;
994 let start_id = claim_ref.0.to_string();
995
996 let rows = conn.query(
997 "WITH RECURSIVE lineage(edge_id, depth) AS (
998 -- Base case: all DerivedFrom edges leaving from our starting claim
999 SELECT ce.edge_id, 1
1000 FROM claim_edges ce
1001 WHERE ce.agent_id = $1
1002 AND ce.from_claim_id = $2
1003 AND ce.edge_kind = 'DerivedFrom'
1004 UNION ALL
1005 -- Recursive case: follow the to_claim of the previous edge onward
1006 SELECT ce2.edge_id, l.depth + 1
1007 FROM claim_edges ce2
1008 JOIN lineage l ON ce2.from_claim_id = (
1009 SELECT to_claim_id FROM claim_edges WHERE edge_id = l.edge_id
1010 )
1011 WHERE ce2.agent_id = $1
1012 AND ce2.edge_kind = 'DerivedFrom'
1013 AND l.depth < 64
1014 )
1015 SELECT ce.edge_id, ce.agent_id, ce.from_claim_id, ce.to_claim_id,
1016 ce.edge_kind, ce.created_at,
1017 l.depth
1018 FROM claim_edges ce
1019 JOIN lineage l ON ce.edge_id = l.edge_id
1020 ORDER BY l.depth ASC, ce.created_at ASC",
1021 &[&agent_id.0.as_str(), &start_id.as_str()],
1022 )?;
1023
1024 rows.iter()
1025 .map(|row| {
1026 let edge_id_str: String = row.get(0);
1027 let agent_id_str: String = row.get(1);
1028 let from_claim_str: String = row.get(2);
1029 let to_claim_str: String = row.get(3);
1030 let kind_str: String = row.get(4);
1031 let created_at_str: String = row.get(5);
1032 let edge_id = uuid::Uuid::parse_str(&edge_id_str)
1035 .map_err(|e| PostgresStoreError::Mapping(format!("edge_id UUID: {e}")))?;
1036 let from_claim = uuid::Uuid::parse_str(&from_claim_str)
1037 .map(ClaimRef)
1038 .map_err(|e| PostgresStoreError::Mapping(format!("from_claim UUID: {e}")))?;
1039 let to_claim = uuid::Uuid::parse_str(&to_claim_str)
1040 .map(ClaimRef)
1041 .map_err(|e| PostgresStoreError::Mapping(format!("to_claim UUID: {e}")))?;
1042 let kind = str_to_edge_kind(&kind_str)?;
1043 let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
1044 .map(|dt| dt.with_timezone(&chrono::Utc))
1045 .map_err(|e| PostgresStoreError::Mapping(format!("created_at: {e}")))?;
1046
1047 Ok(ClaimEdge {
1048 edge_id,
1049 agent_id: AgentId(agent_id_str),
1050 from_claim,
1051 to_claim,
1052 kind,
1053 created_at: TransactionTime(created_at),
1054 })
1055 })
1056 .collect()
1057 }
1058
1059 fn list_predicates_for_subject(
1069 &self,
1070 agent_id: &AgentId,
1071 subject: &str,
1072 as_of_tx_time: Option<chrono::DateTime<chrono::Utc>>,
1073 ) -> Result<Vec<String>, PostgresStoreError> {
1074 let mut conn = self.pool.get()?;
1075
1076 let predicates = if let Some(cutoff) = as_of_tx_time {
1077 let cutoff_str = cutoff.to_rfc3339();
1078 let rows = conn.query(
1079 "SELECT DISTINCT predicate
1080 FROM claims
1081 WHERE agent_id = $1 AND subject = $2 AND tx_time <= $3",
1082 &[&agent_id.0.as_str(), &subject, &cutoff_str.as_str()],
1083 )?;
1084 rows.iter().map(|row| Ok(row.get::<_, String>(0))).collect::<Result<Vec<_>, PostgresStoreError>>()?
1085 } else {
1086 let rows = conn.query(
1087 "SELECT DISTINCT predicate
1088 FROM claims
1089 WHERE agent_id = $1 AND subject = $2",
1090 &[&agent_id.0.as_str(), &subject],
1091 )?;
1092 rows.iter().map(|row| Ok(row.get::<_, String>(0))).collect::<Result<Vec<_>, PostgresStoreError>>()?
1093 };
1094
1095 Ok(predicates)
1096 }
1097
1098 fn requires_global_write_serialization(&self) -> bool {
1100 false
1101 }
1102}
1103
1104pub struct PostgresPendingStore {
1112 pool: r2d2::Pool<r2d2_postgres::PostgresConnectionManager<postgres::NoTls>>,
1113}
1114
1115impl PostgresPendingStore {
1116 pub fn new(pool: r2d2::Pool<r2d2_postgres::PostgresConnectionManager<postgres::NoTls>>) -> Self {
1118 Self { pool }
1119 }
1120}
1121
1122impl PendingAdjudicationPort for PostgresPendingStore {
1123 type Error = PostgresStoreError;
1124
1125 fn insert_pending(&self, row: &PendingAdjudicationRow) -> Result<(), PostgresStoreError> {
1126 let mut conn = self.pool.get()?;
1127 let request_payload = serde_json::to_string(&row.request_payload)
1128 .map_err(|e| PostgresStoreError::Mapping(format!("request_payload serialization: {e}")))?;
1129 let queued_at: chrono::DateTime<chrono::Utc> = row.queued_at;
1133 let expires_at: Option<chrono::DateTime<chrono::Utc>> = row.expires_at;
1134 conn.execute(
1135 "INSERT INTO pending_adjudications (
1136 handle_id, agent_id, subject, predicate,
1137 challenger_claim_ref, incumbent_claim_ref,
1138 request_payload, queued_at, expires_at, status
1139 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
1140 &[
1141 &row.handle_id.to_string(),
1142 &row.agent_id.0.as_str(),
1143 &row.subject.as_str(),
1144 &row.predicate.as_str(),
1145 &row.challenger_claim_ref.0.to_string(),
1146 &row.incumbent_claim_ref.0.to_string(),
1147 &request_payload.as_str(),
1148 &queued_at,
1149 &expires_at,
1150 &row.status.as_str(),
1151 ],
1152 )?;
1153 Ok(())
1154 }
1155
1156 fn get_pending(&self, handle_id: uuid::Uuid) -> Result<Option<PendingAdjudicationRow>, PostgresStoreError> {
1157 let mut conn = self.pool.get()?;
1158 let rows = conn.query(
1159 "SELECT handle_id, agent_id, subject, predicate,
1160 challenger_claim_ref, incumbent_claim_ref,
1161 request_payload, queued_at, expires_at, status
1162 FROM pending_adjudications
1163 WHERE handle_id = $1",
1164 &[&handle_id.to_string()],
1165 )?;
1166 match rows.into_iter().next() {
1167 None => Ok(None),
1168 Some(row) => Ok(Some(pg_row_to_pending(&row)?)),
1169 }
1170 }
1171
1172 fn list_pending(&self, agent_id: Option<&AgentId>) -> Result<Vec<PendingAdjudicationRow>, PostgresStoreError> {
1173 let mut conn = self.pool.get()?;
1174 let rows = if let Some(aid) = agent_id {
1175 conn.query(
1176 "SELECT handle_id, agent_id, subject, predicate,
1177 challenger_claim_ref, incumbent_claim_ref,
1178 request_payload, queued_at, expires_at, status
1179 FROM pending_adjudications
1180 WHERE agent_id = $1 AND status = 'pending'
1181 ORDER BY queued_at ASC",
1182 &[&aid.0.as_str()],
1183 )?
1184 } else {
1185 conn.query(
1186 "SELECT handle_id, agent_id, subject, predicate,
1187 challenger_claim_ref, incumbent_claim_ref,
1188 request_payload, queued_at, expires_at, status
1189 FROM pending_adjudications
1190 WHERE status = 'pending'
1191 ORDER BY queued_at ASC",
1192 &[],
1193 )?
1194 };
1195 rows.iter().map(pg_row_to_pending).collect()
1196 }
1197
1198 fn list_expired(&self, now: chrono::DateTime<chrono::Utc>) -> Result<Vec<PendingAdjudicationRow>, PostgresStoreError> {
1199 let mut conn = self.pool.get()?;
1200 let rows = conn.query(
1202 "SELECT handle_id, agent_id, subject, predicate,
1203 challenger_claim_ref, incumbent_claim_ref,
1204 request_payload, queued_at, expires_at, status
1205 FROM pending_adjudications
1206 WHERE expires_at IS NOT NULL AND expires_at <= $1 AND status = 'pending'
1207 ORDER BY expires_at ASC",
1208 &[&now],
1209 )?;
1210 rows.iter().map(pg_row_to_pending).collect()
1211 }
1212
1213 fn mark_resolved(&self, handle_id: uuid::Uuid) -> Result<(), PostgresStoreError> {
1214 let mut conn = self.pool.get()?;
1215 conn.execute(
1216 "UPDATE pending_adjudications SET status = 'resolved' WHERE handle_id = $1",
1217 &[&handle_id.to_string()],
1218 )?;
1219 Ok(())
1220 }
1221
1222 fn mark_expired(&self, handle_id: uuid::Uuid) -> Result<(), PostgresStoreError> {
1223 let mut conn = self.pool.get()?;
1224 conn.execute(
1225 "UPDATE pending_adjudications SET status = 'expired' WHERE handle_id = $1",
1226 &[&handle_id.to_string()],
1227 )?;
1228 Ok(())
1229 }
1230
1231 fn list_queued_orphan_claims(
1235 &self,
1236 ) -> Result<Vec<mempill_core::ports::pending_adjudication::OrphanedQueuedClaim>, PostgresStoreError> {
1237 let mut conn = self.pool.get()?;
1238
1239 let orphan_rows = conn.query(
1243 "SELECT l.agent_id, l.claim_id, c.subject, c.predicate
1244 FROM ledger_entries l
1245 JOIN claims c ON c.claim_id = l.claim_id AND c.agent_id = l.agent_id
1246 WHERE l.disposition = 'QueuedForAdjudication'
1247 AND l.recorded_at = (
1248 SELECT MAX(l2.recorded_at) FROM ledger_entries l2
1249 WHERE l2.claim_id = l.claim_id AND l2.agent_id = l.agent_id
1250 )
1251 AND NOT EXISTS (
1252 SELECT 1 FROM pending_adjudications pa
1253 WHERE pa.challenger_claim_ref = l.claim_id
1254 AND pa.agent_id = l.agent_id
1255 AND pa.status = 'pending'
1256 )",
1257 &[],
1258 )?;
1259
1260 let mut results = Vec::new();
1261 for row in &orphan_rows {
1262 let agent_id_str: String = row.get(0);
1263 let challenger_str: String = row.get(1);
1264 let subject: String = row.get(2);
1265 let predicate: String = row.get(3);
1266
1267 let challenger_ref = uuid::Uuid::parse_str(&challenger_str)
1268 .map(mempill_types::ClaimRef)
1269 .map_err(|e| PostgresStoreError::Mapping(format!("challenger_claim_ref UUID: {e}")))?;
1270
1271 let incumbent_rows = conn.query(
1274 "SELECT l.claim_id
1275 FROM ledger_entries l
1276 JOIN claims c ON c.claim_id = l.claim_id AND c.agent_id = l.agent_id
1277 WHERE l.agent_id = $1
1278 AND c.subject = $2
1279 AND c.predicate = $3
1280 AND l.disposition = 'CommittedCheap'
1281 AND l.recorded_at = (
1282 SELECT MAX(l2.recorded_at) FROM ledger_entries l2
1283 WHERE l2.claim_id = l.claim_id AND l2.agent_id = l.agent_id
1284 )
1285 ORDER BY l.recorded_at DESC
1286 LIMIT 1",
1287 &[&agent_id_str.as_str(), &subject.as_str(), &predicate.as_str()],
1288 )?;
1289
1290 let incumbent_ref = incumbent_rows.first()
1291 .map(|ir| {
1292 let ref_str: String = ir.get(0);
1293 uuid::Uuid::parse_str(&ref_str)
1294 .map(mempill_types::ClaimRef)
1295 .map_err(|e| PostgresStoreError::Mapping(format!("incumbent UUID: {e}")))
1296 })
1297 .transpose()?;
1298
1299 results.push(mempill_core::ports::pending_adjudication::OrphanedQueuedClaim {
1300 agent_id: mempill_types::AgentId(agent_id_str),
1301 challenger_claim_ref: challenger_ref,
1302 incumbent_claim_ref: incumbent_ref,
1303 subject,
1304 predicate,
1305 });
1306 }
1307
1308 Ok(results)
1309 }
1310}
1311
1312fn pg_row_to_pending(row: &postgres::Row) -> Result<PendingAdjudicationRow, PostgresStoreError> {
1318 let handle_id_str: String = row.get(0);
1319 let agent_id_str: String = row.get(1);
1320 let subject: String = row.get(2);
1321 let predicate: String = row.get(3);
1322 let challenger_str: String = row.get(4);
1323 let incumbent_str: String = row.get(5);
1324 let payload_json: String = row.get(6);
1325 let queued_at: chrono::DateTime<chrono::Utc> = row.get(7);
1327 let expires_at: Option<chrono::DateTime<chrono::Utc>> = row.get(8);
1328 let status: String = row.get(9);
1329
1330 let handle_id = uuid::Uuid::parse_str(&handle_id_str)
1331 .map_err(|e| PostgresStoreError::Mapping(format!("handle_id UUID: {e}")))?;
1332 let challenger_claim_ref = uuid::Uuid::parse_str(&challenger_str)
1333 .map(ClaimRef)
1334 .map_err(|e| PostgresStoreError::Mapping(format!("challenger_claim_ref UUID: {e}")))?;
1335 let incumbent_claim_ref = uuid::Uuid::parse_str(&incumbent_str)
1336 .map(ClaimRef)
1337 .map_err(|e| PostgresStoreError::Mapping(format!("incumbent_claim_ref UUID: {e}")))?;
1338 let request_payload: mempill_types::AdjudicationRequest =
1339 serde_json::from_str(&payload_json)
1340 .map_err(|e| PostgresStoreError::Mapping(format!("request_payload JSON: {e}")))?;
1341
1342 Ok(PendingAdjudicationRow {
1343 handle_id,
1344 agent_id: AgentId(agent_id_str),
1345 subject,
1346 predicate,
1347 challenger_claim_ref,
1348 incumbent_claim_ref,
1349 request_payload,
1350 queued_at,
1351 expires_at,
1352 status,
1353 })
1354}
1355
1356pub fn open_postgres<O, V>(
1363 connection_string: &str,
1364 oracle: Option<Arc<O>>,
1365 vector: Option<Arc<V>>,
1366 config: EngineConfig,
1367) -> Result<EngineHandle<PostgresPersistenceStore, O, V>, PostgresStoreError>
1368where
1369 O: mempill_core::ports::OraclePort + Send + Sync + 'static,
1370 V: mempill_core::ports::VectorPort + Send + Sync + 'static,
1371{
1372 let store = PostgresPersistenceStore::new(connection_string)?;
1373 Ok(EngineHandle::new(Arc::new(store), oracle, vector, config))
1374}
1375
1376pub fn open_postgres_with_oracle<O, V>(
1385 connection_string: &str,
1386 oracle: Arc<O>,
1387 vector: Option<Arc<V>>,
1388 config: EngineConfig,
1389) -> Result<EngineHandle<PostgresPersistenceStore, O, V>, PostgresStoreError>
1390where
1391 O: mempill_core::ports::OraclePort + Send + Sync + 'static,
1392 V: mempill_core::ports::VectorPort + Send + Sync + 'static,
1393{
1394 let store = PostgresPersistenceStore::new(connection_string)?;
1395 let store_arc = Arc::new(store);
1396 let pending_store: Arc<dyn mempill_core::ErasedPendingStore> = Arc::new(
1397 mempill_core::ErasedPendingStoreAdapter::new(store_arc.pending_store()),
1398 );
1399 Ok(EngineHandle::new_with_pending_store::<()>(
1400 store_arc,
1401 Some(oracle),
1402 vector,
1403 pending_store,
1404 config,
1405 ))
1406}