Skip to main content

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}