1#[derive(Debug, thiserror::Error)]
8pub enum Error {
9 #[error("Storage error: {0}")]
11 Storage(#[from] redb::Error),
12
13 #[error("Database error: {0}")]
15 Database(String),
16
17 #[error("Table error: {0}")]
19 Table(String),
20
21 #[error("Transaction error: {0}")]
23 Transaction(String),
24
25 #[error("Commit error: {0}")]
27 Commit(String),
28
29 #[error("Vector index error: {0}")]
31 VectorIndex(String),
32
33 #[error("Invalid file format: {0}")]
35 InvalidFormat(&'static str),
36
37 #[error("Unsupported version: {0} (current version: {1})")]
39 UnsupportedVersion(u32, u32),
40
41 #[error("Memory not found: {0}")]
43 MemoryNotFound(String),
44
45 #[error("Entity not found: {0}")]
47 EntityNotFound(String),
48
49 #[error("Invalid embedding dimension: expected {expected}, got {got}")]
51 InvalidEmbeddingDimension { expected: usize, got: usize },
52
53 #[error("Serialization error: {0}")]
55 Serialization(String),
56
57 #[error("Deserialization error: {0}")]
59 Deserialization(String),
60
61 #[error("IO error: {0}")]
63 Io(#[from] std::io::Error),
64
65 #[error("Invalid memory ID: {0}")]
67 InvalidMemoryId(String),
68
69 #[error("Invalid timestamp: {0}")]
71 InvalidTimestamp(String),
72
73 #[error("Invalid source: {0}")]
75 InvalidSource(String),
76
77 #[error("Configuration error: {0}")]
79 Configuration(String),
80
81 #[error("Invalid parameter: {0}")]
83 InvalidParameter(String),
84
85 #[error("Database already exists at path: {0}")]
87 DatabaseExists(String),
88
89 #[error("Database not found at path: {0}")]
91 DatabaseNotFound(String),
92
93 #[error("Namespace mismatch: expected '{expected}', found '{found}'")]
95 NamespaceMismatch { expected: String, found: String },
96
97 #[error("Database corruption detected: {0}")]
99 DatabaseCorruption(String),
100
101 #[error("File truncated or incomplete: {0}")]
103 FileTruncated(String),
104
105 #[error("SLM feature not available - compile with 'slm' feature to enable")]
107 SlmNotAvailable,
108
109 #[error("SLM initialization error: {0}")]
111 SlmInitialization(String),
112
113 #[error("SLM inference error: {0}")]
115 SlmInference(String),
116
117 #[error("SLM inference timeout after {0}ms")]
119 SlmTimeout(u64),
120
121 #[error("Model not found: {0}")]
123 ModelNotFound(String),
124
125 #[error("Inference error: {0}")]
127 InferenceError(String),
128
129 #[error("Entity extraction feature not available - compile with 'entity-extraction' feature")]
131 EntityExtractionNotAvailable,
132
133 #[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 #[error("Embedding feature not available - compile with 'embedding-onnx' feature")]
142 EmbeddingNotAvailable,
143}
144
145pub type Result<T> = std::result::Result<T, Error>;
147
148impl Error {
149 pub fn is_recoverable(&self) -> bool {
154 match self {
155 Error::MemoryNotFound(_) => true,
157 Error::EntityNotFound(_) => true,
158 Error::NamespaceMismatch { .. } => true,
159 Error::InvalidParameter(_) => true,
160 Error::InvalidEmbeddingDimension { .. } => true,
161
162 Error::DatabaseCorruption(_) => false,
164 Error::FileTruncated(_) => false,
165 Error::InvalidFormat(_) => false,
166 Error::UnsupportedVersion(..) => false,
167
168 Error::Storage(_) => true,
170 Error::Io(_) => true,
171
172 _ => false,
174 }
175 }
176
177 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 pub fn is_corruption(&self) -> bool {
239 matches!(
240 self,
241 Error::DatabaseCorruption(_) | Error::FileTruncated(_) | Error::InvalidFormat(_)
242 )
243 }
244
245 pub fn is_version_error(&self) -> bool {
247 matches!(self, Error::UnsupportedVersion(..))
248 }
249}
250
251impl 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 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 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 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 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 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 let err = Error::UnsupportedVersion(5, 1);
382 let msg = err.user_message();
383 assert!(msg.contains("newer"));
384
385 let err = Error::UnsupportedVersion(1, 5);
387 let msg = err.user_message();
388 assert!(msg.contains("older") || msg.contains("Migration"));
389 }
390}