Skip to main content

subcog/storage/traits/
persistence.rs

1//! Persistence backend trait.
2//!
3//! The persistence layer is the authoritative source of truth for all memories.
4//! It handles durable storage, ensuring memories survive process restarts.
5//!
6//! # Available Implementations
7//!
8//! | Backend | Use Case | Trade-offs |
9//! |---------|----------|------------|
10//! | `SqliteBackend` | Primary; embedded, ACID | Single-process access |
11//! | `PostgresBackend` | Multi-user, ACID | Requires PostgreSQL server |
12//! | `FilesystemBackend` | Fallback; simple | No transactional guarantees |
13//!
14//! # Error Modes and Guarantees
15//!
16//! All backends return `Result<T>` with errors propagated via [`crate::Error`].
17//!
18//! ## Transactional Behavior
19//!
20//! | Backend | Atomicity | Isolation | Durability |
21//! |---------|-----------|-----------|------------|
22//! | `SQLite` | Full ACID | Serializable | On commit (WAL) |
23//! | PostgreSQL | Full ACID | Serializable | On commit |
24//! | Filesystem | None | None | On fsync |
25//!
26//! ## Error Recovery
27//!
28//! | Error Type | Recovery Strategy |
29//! |------------|-------------------|
30//! | `Error::Storage` | Retry with exponential backoff |
31//! | `Error::NotFound` | Expected for missing IDs; handle gracefully |
32//! | `Error::InvalidInput` | Validate input before calling |
33//! | `Error::OperationFailed` | Log and surface to user |
34//!
35//! ## Consistency Guarantees
36//!
37//! - **Read-after-write**: Guaranteed for all backends
38//! - **Concurrent writes**: `SQLite` uses WAL mode with busy timeout; `PostgreSQL` uses transactions
39//! - **Partial failures**: `SQLite` rolls back; `PostgreSQL` rolls back
40
41use crate::Result;
42use crate::models::{Memory, MemoryId};
43
44/// Trait for persistence layer backends.
45///
46/// Persistence backends are the authoritative source of truth for memories.
47/// They handle long-term storage and retrieval.
48///
49/// # Implementor Notes
50///
51/// - Methods use `&self` to enable sharing via `Arc<dyn PersistenceBackend>`
52/// - Use interior mutability (e.g., `Mutex`) for mutable state
53/// - All methods must be thread-safe (`Send + Sync` bound)
54/// - Prefer returning `Error::NotFound` over `None` for missing IDs
55/// - Use structured error variants from [`crate::Error`]
56pub trait PersistenceBackend: Send + Sync {
57    /// Stores a memory.
58    ///
59    /// Uses interior mutability for thread-safe concurrent access.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the storage operation fails.
64    fn store(&self, memory: &Memory) -> Result<()>;
65
66    /// Retrieves a memory by ID.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the retrieval operation fails.
71    fn get(&self, id: &MemoryId) -> Result<Option<Memory>>;
72
73    /// Deletes a memory by ID.
74    ///
75    /// Uses interior mutability for thread-safe concurrent access.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the deletion operation fails.
80    fn delete(&self, id: &MemoryId) -> Result<bool>;
81
82    /// Lists all memory IDs.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the list operation fails.
87    fn list_ids(&self) -> Result<Vec<MemoryId>>;
88
89    /// Retrieves multiple memories by their IDs in a single batch operation.
90    ///
91    /// This method avoids N+1 queries by fetching all requested memories
92    /// in a single database round-trip (where supported by the backend).
93    ///
94    /// # Default Implementation
95    ///
96    /// Falls back to calling `get()` for each ID. Backends should override
97    /// this with an optimized batch query (e.g., `SELECT ... WHERE id IN (...)`).
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if any retrieval operation fails.
102    fn get_batch(&self, ids: &[MemoryId]) -> Result<Vec<Memory>> {
103        ids.iter()
104            .filter_map(|id| self.get(id).transpose())
105            .collect()
106    }
107
108    /// Checks if a memory exists.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the existence check fails.
113    fn exists(&self, id: &MemoryId) -> Result<bool> {
114        Ok(self.get(id)?.is_some())
115    }
116
117    /// Returns the total count of memories.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the count operation fails.
122    fn count(&self) -> Result<usize> {
123        Ok(self.list_ids()?.len())
124    }
125}