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}