Skip to main content

mnemefusion_core/
error.rs

1//! Error types for MnemeFusion
2//!
3//! This module defines all error types that can occur during MnemeFusion operations.
4//! Uses thiserror for ergonomic error handling.
5
6/// Main error type for MnemeFusion operations
7#[derive(Debug, thiserror::Error)]
8pub enum Error {
9    /// Storage engine error (redb)
10    #[error("Storage error: {0}")]
11    Storage(#[from] redb::Error),
12
13    /// Database error
14    #[error("Database error: {0}")]
15    Database(String),
16
17    /// Table error
18    #[error("Table error: {0}")]
19    Table(String),
20
21    /// Storage transaction error
22    #[error("Transaction error: {0}")]
23    Transaction(String),
24
25    /// Storage commit error
26    #[error("Commit error: {0}")]
27    Commit(String),
28
29    /// Vector index error
30    #[error("Vector index error: {0}")]
31    VectorIndex(String),
32
33    /// Invalid file format
34    #[error("Invalid file format: {0}")]
35    InvalidFormat(&'static str),
36
37    /// Unsupported version
38    #[error("Unsupported version: {0} (current version: {1})")]
39    UnsupportedVersion(u32, u32),
40
41    /// Memory not found
42    #[error("Memory not found: {0}")]
43    MemoryNotFound(String),
44
45    /// Entity not found
46    #[error("Entity not found: {0}")]
47    EntityNotFound(String),
48
49    /// Invalid embedding dimension
50    #[error("Invalid embedding dimension: expected {expected}, got {got}")]
51    InvalidEmbeddingDimension { expected: usize, got: usize },
52
53    /// Serialization error
54    #[error("Serialization error: {0}")]
55    Serialization(String),
56
57    /// Deserialization error
58    #[error("Deserialization error: {0}")]
59    Deserialization(String),
60
61    /// IO error
62    #[error("IO error: {0}")]
63    Io(#[from] std::io::Error),
64
65    /// Invalid memory ID format
66    #[error("Invalid memory ID: {0}")]
67    InvalidMemoryId(String),
68
69    /// Invalid timestamp
70    #[error("Invalid timestamp: {0}")]
71    InvalidTimestamp(String),
72
73    /// Invalid source
74    #[error("Invalid source: {0}")]
75    InvalidSource(String),
76
77    /// Configuration error
78    #[error("Configuration error: {0}")]
79    Configuration(String),
80
81    /// Invalid parameter
82    #[error("Invalid parameter: {0}")]
83    InvalidParameter(String),
84
85    /// Database already exists
86    #[error("Database already exists at path: {0}")]
87    DatabaseExists(String),
88
89    /// Database not found
90    #[error("Database not found at path: {0}")]
91    DatabaseNotFound(String),
92
93    /// Namespace mismatch
94    #[error("Namespace mismatch: expected '{expected}', found '{found}'")]
95    NamespaceMismatch { expected: String, found: String },
96
97    /// Database corruption detected
98    #[error("Database corruption detected: {0}")]
99    DatabaseCorruption(String),
100
101    /// File truncated or incomplete
102    #[error("File truncated or incomplete: {0}")]
103    FileTruncated(String),
104
105    /// SLM feature not available (compiled without 'slm' feature)
106    #[error("SLM feature not available - compile with 'slm' feature to enable")]
107    SlmNotAvailable,
108
109    /// SLM initialization error
110    #[error("SLM initialization error: {0}")]
111    SlmInitialization(String),
112
113    /// SLM inference error
114    #[error("SLM inference error: {0}")]
115    SlmInference(String),
116
117    /// SLM timeout error
118    #[error("SLM inference timeout after {0}ms")]
119    SlmTimeout(u64),
120
121    /// Model file not found (entity extraction)
122    #[error("Model not found: {0}")]
123    ModelNotFound(String),
124
125    /// Native inference error (llama.cpp)
126    #[error("Inference error: {0}")]
127    InferenceError(String),
128
129    /// Entity extraction feature not available
130    #[error("Entity extraction feature not available - compile with 'entity-extraction' feature")]
131    EntityExtractionNotAvailable,
132
133    /// Embedding engine not configured — cannot auto-compute embeddings
134    #[error(
135        "No embedding engine configured. Either pass an explicit embedding vector \
136         or set 'embedding_model' in Config to enable automatic embedding."
137    )]
138    NoEmbeddingEngine,
139
140    /// Embedding engine feature not available
141    #[error("Embedding feature not available - compile with 'embedding-onnx' feature")]
142    EmbeddingNotAvailable,
143}
144
145/// Result type alias for MnemeFusion operations
146pub type Result<T> = std::result::Result<T, Error>;
147
148impl Error {
149    /// Check if this error is recoverable
150    ///
151    /// Recoverable errors can typically be retried or worked around.
152    /// Non-recoverable errors indicate serious problems that require user intervention.
153    pub fn is_recoverable(&self) -> bool {
154        match self {
155            // Recoverable errors
156            Error::MemoryNotFound(_) => true,
157            Error::EntityNotFound(_) => true,
158            Error::NamespaceMismatch { .. } => true,
159            Error::InvalidParameter(_) => true,
160            Error::InvalidEmbeddingDimension { .. } => true,
161
162            // Non-recoverable errors
163            Error::DatabaseCorruption(_) => false,
164            Error::FileTruncated(_) => false,
165            Error::InvalidFormat(_) => false,
166            Error::UnsupportedVersion(..) => false,
167
168            // Storage/IO errors might be recoverable
169            Error::Storage(_) => true,
170            Error::Io(_) => true,
171
172            // Other errors are situational
173            _ => false,
174        }
175    }
176
177    /// Get a user-friendly error message with troubleshooting hints
178    pub fn user_message(&self) -> String {
179        match self {
180            Error::InvalidEmbeddingDimension { expected, got } => {
181                format!(
182                    "Embedding dimension mismatch: expected {} dimensions, but got {}.\n\
183                     Hint: Ensure all embeddings use the same model and dimension size.\n\
184                     You can set the dimension in Config with .with_embedding_dim({})",
185                    expected, got, expected
186                )
187            }
188            Error::DatabaseCorruption(msg) => {
189                format!(
190                    "Database corruption detected: {}\n\
191                     The database file may be corrupted or incomplete.\n\
192                     Hint: Try restoring from a backup if available, or create a new database.",
193                    msg
194                )
195            }
196            Error::FileTruncated(msg) => {
197                format!(
198                    "Database file is truncated: {}\n\
199                     The file may have been corrupted during a previous operation.\n\
200                     Hint: Restore from backup or delete the file to start fresh.",
201                    msg
202                )
203            }
204            Error::UnsupportedVersion(found, current) => {
205                if found > current {
206                    format!(
207                        "Database version {} is newer than supported version {}.\n\
208                         Hint: Update MnemeFusion to the latest version.",
209                        found, current
210                    )
211                } else {
212                    format!(
213                        "Database version {} is older than current version {}.\n\
214                         Migration may be required.",
215                        found, current
216                    )
217                }
218            }
219            Error::NamespaceMismatch { expected, found } => {
220                format!(
221                    "Namespace mismatch: operation expected '{}' but memory is in '{}'.\n\
222                     Hint: Verify you're using the correct namespace for this operation.",
223                    expected, found
224                )
225            }
226            Error::VectorIndex(msg) => {
227                format!(
228                    "Vector index error: {}\n\
229                     Hint: This may indicate corrupted index data. Try reopening the database.",
230                    msg
231                )
232            }
233            _ => self.to_string(),
234        }
235    }
236
237    /// Check if this is a corruption-related error
238    pub fn is_corruption(&self) -> bool {
239        matches!(
240            self,
241            Error::DatabaseCorruption(_) | Error::FileTruncated(_) | Error::InvalidFormat(_)
242        )
243    }
244
245    /// Check if this is a version-related error
246    pub fn is_version_error(&self) -> bool {
247        matches!(self, Error::UnsupportedVersion(..))
248    }
249}
250
251// Convert redb specific errors to our Error type
252impl From<redb::DatabaseError> for Error {
253    fn from(err: redb::DatabaseError) -> Self {
254        Error::Database(err.to_string())
255    }
256}
257
258impl From<redb::TransactionError> for Error {
259    fn from(err: redb::TransactionError) -> Self {
260        Error::Transaction(err.to_string())
261    }
262}
263
264impl From<redb::TableError> for Error {
265    fn from(err: redb::TableError) -> Self {
266        Error::Table(err.to_string())
267    }
268}
269
270impl From<redb::CommitError> for Error {
271    fn from(err: redb::CommitError) -> Self {
272        Error::Commit(err.to_string())
273    }
274}
275
276impl From<redb::StorageError> for Error {
277    fn from(err: redb::StorageError) -> Self {
278        Error::Storage(redb::Error::from(err))
279    }
280}
281
282impl From<serde_json::Error> for Error {
283    fn from(err: serde_json::Error) -> Self {
284        if err.is_data() {
285            Error::Deserialization(err.to_string())
286        } else {
287            Error::Serialization(err.to_string())
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_error_display() {
298        let err = Error::InvalidFormat("bad magic number");
299        assert_eq!(err.to_string(), "Invalid file format: bad magic number");
300
301        let err = Error::InvalidEmbeddingDimension {
302            expected: 384,
303            got: 512,
304        };
305        assert_eq!(
306            err.to_string(),
307            "Invalid embedding dimension: expected 384, got 512"
308        );
309    }
310
311    #[test]
312    fn test_error_conversion() {
313        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
314        let err: Error = io_err.into();
315        assert!(matches!(err, Error::Io(_)));
316    }
317
318    #[test]
319    fn test_is_recoverable() {
320        // Recoverable errors
321        assert!(Error::MemoryNotFound("id".to_string()).is_recoverable());
322        assert!(Error::EntityNotFound("id".to_string()).is_recoverable());
323        assert!(Error::InvalidParameter("test".to_string()).is_recoverable());
324        assert!(Error::InvalidEmbeddingDimension {
325            expected: 384,
326            got: 512
327        }
328        .is_recoverable());
329
330        // Non-recoverable errors
331        assert!(!Error::DatabaseCorruption("test".to_string()).is_recoverable());
332        assert!(!Error::FileTruncated("test".to_string()).is_recoverable());
333        assert!(!Error::InvalidFormat("test").is_recoverable());
334        assert!(!Error::UnsupportedVersion(2, 1).is_recoverable());
335    }
336
337    #[test]
338    fn test_is_corruption() {
339        assert!(Error::DatabaseCorruption("test".to_string()).is_corruption());
340        assert!(Error::FileTruncated("test".to_string()).is_corruption());
341        assert!(Error::InvalidFormat("test").is_corruption());
342
343        assert!(!Error::MemoryNotFound("id".to_string()).is_corruption());
344        assert!(!Error::InvalidParameter("test".to_string()).is_corruption());
345    }
346
347    #[test]
348    fn test_is_version_error() {
349        assert!(Error::UnsupportedVersion(2, 1).is_version_error());
350        assert!(!Error::DatabaseCorruption("test".to_string()).is_version_error());
351    }
352
353    #[test]
354    fn test_user_message() {
355        // Test dimension error message includes hint
356        let err = Error::InvalidEmbeddingDimension {
357            expected: 384,
358            got: 512,
359        };
360        let msg = err.user_message();
361        assert!(msg.contains("expected 384"));
362        assert!(msg.contains("got 512"));
363        assert!(msg.contains("Hint"));
364
365        // Test corruption error includes recovery suggestion
366        let err = Error::DatabaseCorruption("bad data".to_string());
367        let msg = err.user_message();
368        assert!(msg.contains("corruption"));
369        assert!(msg.contains("backup"));
370
371        // Test version error distinguishes newer vs older
372        let err = Error::UnsupportedVersion(5, 1);
373        let msg = err.user_message();
374        assert!(msg.contains("newer"));
375        assert!(msg.contains("Update"));
376    }
377
378    #[test]
379    fn test_unsupported_version_messages() {
380        // Newer version
381        let err = Error::UnsupportedVersion(5, 1);
382        let msg = err.user_message();
383        assert!(msg.contains("newer"));
384
385        // Older version (edge case, but handled)
386        let err = Error::UnsupportedVersion(1, 5);
387        let msg = err.user_message();
388        assert!(msg.contains("older") || msg.contains("Migration"));
389    }
390}