engram/store.rs
1//! `FactStore` trait — the primary storage interface for Engram.
2//!
3//! All persistence implementations (SQLite, Postgres, in-memory) must
4//! implement this trait. The trait is `async_trait`-annotated and requires
5//! `Send + Sync` so it can be used across task boundaries.
6
7use crate::fact::{Fact, FactFilter, FactId, FactPatch};
8use crate::scope::Scope;
9use async_trait::async_trait;
10use serde::Serialize;
11use thiserror::Error;
12
13// ---------------------------------------------------------------------------
14// MemoryError
15// ---------------------------------------------------------------------------
16
17/// Errors that can be returned by `FactStore` operations.
18#[derive(Debug, Error)]
19pub enum MemoryError {
20 #[error("database error: {0}")]
21 Database(String),
22
23 #[error("serialization error: {0}")]
24 Serialization(String),
25
26 #[error("not found: {0}")]
27 NotFound(String),
28
29 #[error("embedding error: {0}")]
30 Embedding(String),
31
32 #[error("graph error: {0}")]
33 Graph(String),
34}
35
36// ---------------------------------------------------------------------------
37// StoreStats
38// ---------------------------------------------------------------------------
39
40/// Aggregate statistics for a `FactStore` instance.
41#[derive(Debug, Clone, Default, Serialize)]
42pub struct StoreStats {
43 pub total_facts: u64,
44 pub valid_facts: u64,
45 pub invalidated_facts: u64,
46 pub total_entities: u64,
47 pub total_relationships: u64,
48}
49
50// ---------------------------------------------------------------------------
51// FactStore trait
52// ---------------------------------------------------------------------------
53
54/// Primary storage interface for Engram facts.
55///
56/// Implementations MUST be `Send + Sync` so that `Arc<dyn FactStore>` can be
57/// shared across async tasks. All mutation methods are fallible and return
58/// `Result<_, MemoryError>`.
59#[async_trait]
60pub trait FactStore: Send + Sync {
61 /// Persist a new fact. The `fact.id` must be unique; implementations SHOULD
62 /// return `MemoryError::Database` if a duplicate id is detected.
63 async fn insert_fact(&self, fact: Fact) -> Result<FactId, MemoryError>;
64
65 /// Retrieve a single fact by id.
66 async fn get_fact(&self, id: FactId) -> Result<Fact, MemoryError>;
67
68 /// Apply a partial patch to an existing fact.
69 /// Only fields set to `Some(…)` in `patch` are updated.
70 async fn update_fact(&self, id: FactId, patch: FactPatch) -> Result<Fact, MemoryError>;
71
72 /// List facts matching the given filter.
73 async fn list_facts(&self, filter: &FactFilter) -> Result<Vec<Fact>, MemoryError>;
74
75 /// Mark a fact as invalid as of `now` (sets `invalid_at` to the current
76 /// timestamp). Does not delete the record; historical queries still see it.
77 async fn invalidate_fact(&self, id: FactId) -> Result<(), MemoryError>;
78
79 /// Delete ALL data (facts, entities, relationships) belonging to `scope`.
80 /// This is a hard delete and is typically used for GDPR / right-to-erasure
81 /// requests. Returns the number of facts deleted.
82 async fn delete_scope_data(&self, scope: &Scope) -> Result<u64, MemoryError>;
83
84 /// Export all facts matching `filter` as a JSON-serialisable vector.
85 /// Implementations SHOULD stream or batch internally to avoid loading
86 /// unbounded data into memory when the result set is large.
87 async fn export(&self, filter: &FactFilter) -> Result<Vec<Fact>, MemoryError>;
88
89 /// Import a batch of facts (e.g. from a previous `export`).
90 /// Existing facts with the same id SHOULD be skipped (upsert-or-ignore).
91 /// Returns the number of facts successfully imported.
92 async fn import(&self, facts: Vec<Fact>) -> Result<u64, MemoryError>;
93
94 /// Return aggregate statistics for this store.
95 async fn stats(&self) -> Result<StoreStats, MemoryError>;
96
97 /// Record that a fact was accessed (increments `access_count`,
98 /// updates `last_accessed`). Implementations MAY do this
99 /// asynchronously / fire-and-forget; callers SHOULD NOT depend on
100 /// the update being immediately visible.
101 async fn record_access(&self, id: FactId) -> Result<(), MemoryError>;
102
103 /// Full-text keyword search over fact text (BM25 ranking).
104 async fn keyword_search(
105 &self,
106 query: &str,
107 scope: &Scope,
108 top_k: usize,
109 ) -> Result<Vec<Fact>, MemoryError>;
110}