Skip to main content

rlm_rs/
error.rs

1//! Error types for RLM-RS operations.
2//!
3//! This module provides a comprehensive error hierarchy using `thiserror` for
4//! all RLM operations including storage, chunking, I/O, and CLI commands.
5
6use thiserror::Error;
7
8/// Result type alias for RLM operations.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Comprehensive error types for RLM operations.
12#[derive(Error, Debug)]
13pub enum Error {
14    /// Storage-related errors (database operations).
15    #[error("storage error: {0}")]
16    Storage(#[from] StorageError),
17
18    /// Chunking-related errors (text processing).
19    #[error("chunking error: {0}")]
20    Chunking(#[from] ChunkingError),
21
22    /// I/O errors (file operations).
23    #[error("I/O error: {0}")]
24    Io(#[from] IoError),
25
26    /// CLI command errors.
27    #[error("command error: {0}")]
28    Command(#[from] CommandError),
29
30    /// Search errors.
31    #[error("search error: {0}")]
32    Search(#[from] SearchError),
33
34    /// Invalid state errors.
35    #[error("invalid state: {message}")]
36    InvalidState {
37        /// Description of the invalid state.
38        message: String,
39    },
40
41    /// Configuration errors.
42    #[error("configuration error: {message}")]
43    Config {
44        /// Description of the configuration error.
45        message: String,
46    },
47}
48
49/// Storage-specific errors for database operations.
50#[derive(Error, Debug)]
51pub enum StorageError {
52    /// Database connection or query error.
53    #[error("database error: {0}")]
54    Database(String),
55
56    /// Storage not initialized (init command not run).
57    #[error("RLM not initialized. Run: rlm-cli init")]
58    NotInitialized,
59
60    /// Context not found in storage.
61    #[error("context not found")]
62    ContextNotFound,
63
64    /// Buffer not found by ID or name.
65    #[error("buffer not found: {identifier}")]
66    BufferNotFound {
67        /// Buffer ID or name that was not found.
68        identifier: String,
69    },
70
71    /// Chunk not found by ID.
72    #[error("chunk not found: {id}")]
73    ChunkNotFound {
74        /// Chunk ID that was not found.
75        id: i64,
76    },
77
78    /// Schema migration error.
79    #[error("migration error: {0}")]
80    Migration(String),
81
82    /// Transaction error.
83    #[error("transaction error: {0}")]
84    Transaction(String),
85
86    /// Serialization/deserialization error.
87    #[error("serialization error: {0}")]
88    Serialization(String),
89
90    /// Vector search error (feature-gated).
91    #[cfg(feature = "usearch-hnsw")]
92    #[error("vector search error: {0}")]
93    VectorSearch(String),
94
95    /// Embedding error (feature-gated).
96    #[cfg(feature = "fastembed-embeddings")]
97    #[error("embedding error: {0}")]
98    Embedding(String),
99}
100
101/// Chunking-specific errors for text processing.
102#[derive(Error, Debug)]
103pub enum ChunkingError {
104    /// Invalid UTF-8 encountered at specific byte offset.
105    #[error("invalid UTF-8 at byte offset {offset}")]
106    InvalidUtf8 {
107        /// Byte offset where invalid UTF-8 was found.
108        offset: usize,
109    },
110
111    /// Chunk size exceeds maximum allowed.
112    #[error("chunk size {size} exceeds maximum {max}")]
113    ChunkTooLarge {
114        /// Actual chunk size.
115        size: usize,
116        /// Maximum allowed size.
117        max: usize,
118    },
119
120    /// Invalid chunk configuration.
121    #[error("invalid chunk configuration: {reason}")]
122    InvalidConfig {
123        /// Reason the configuration is invalid.
124        reason: String,
125    },
126
127    /// Overlap exceeds chunk size.
128    #[error("overlap {overlap} must be less than chunk size {size}")]
129    OverlapTooLarge {
130        /// Overlap size.
131        overlap: usize,
132        /// Chunk size.
133        size: usize,
134    },
135
136    /// Parallel processing error.
137    #[error("parallel processing failed: {reason}")]
138    ParallelFailed {
139        /// Reason for failure.
140        reason: String,
141    },
142
143    /// Semantic analysis error.
144    #[error("semantic analysis failed: {0}")]
145    SemanticFailed(String),
146
147    /// Regex compilation error.
148    #[error("regex error: {0}")]
149    Regex(String),
150
151    /// Unknown chunking strategy.
152    #[error("unknown chunking strategy: {name}")]
153    UnknownStrategy {
154        /// Name of the unknown strategy.
155        name: String,
156    },
157}
158
159/// I/O-specific errors for file operations.
160#[derive(Error, Debug)]
161pub enum IoError {
162    /// File not found.
163    #[error("file not found: {path}")]
164    FileNotFound {
165        /// Path to the file that was not found.
166        path: String,
167    },
168
169    /// Failed to read file.
170    #[error("failed to read file: {path}: {reason}")]
171    ReadFailed {
172        /// Path to the file.
173        path: String,
174        /// Reason for failure.
175        reason: String,
176    },
177
178    /// Failed to write file.
179    #[error("failed to write file: {path}: {reason}")]
180    WriteFailed {
181        /// Path to the file.
182        path: String,
183        /// Reason for failure.
184        reason: String,
185    },
186
187    /// Memory mapping error.
188    #[error("memory mapping failed: {path}: {reason}")]
189    MmapFailed {
190        /// Path to the file.
191        path: String,
192        /// Reason for failure.
193        reason: String,
194    },
195
196    /// Directory creation error.
197    #[error("failed to create directory: {path}: {reason}")]
198    DirectoryFailed {
199        /// Path to the directory.
200        path: String,
201        /// Reason for failure.
202        reason: String,
203    },
204
205    /// Path traversal security error.
206    #[error("path traversal denied: {path}")]
207    PathTraversal {
208        /// Path that was denied.
209        path: String,
210    },
211
212    /// Generic I/O error wrapper.
213    #[error("I/O error: {0}")]
214    Generic(String),
215}
216
217/// Search-specific errors for vector search operations.
218#[derive(Error, Debug)]
219pub enum SearchError {
220    /// HNSW index operation failed.
221    #[error("index error: {message}")]
222    IndexError {
223        /// Error message.
224        message: String,
225    },
226
227    /// Vector dimension mismatch.
228    #[error("dimension mismatch: expected {expected}, got {got}")]
229    DimensionMismatch {
230        /// Expected dimensions.
231        expected: usize,
232        /// Actual dimensions.
233        got: usize,
234    },
235
236    /// Feature not enabled.
237    #[error("feature not enabled: {feature}")]
238    FeatureNotEnabled {
239        /// Name of the required feature.
240        feature: String,
241    },
242
243    /// Query error.
244    #[error("query error: {message}")]
245    QueryError {
246        /// Error message.
247        message: String,
248    },
249}
250
251/// CLI command-specific errors.
252#[derive(Error, Debug)]
253pub enum CommandError {
254    /// Unknown command.
255    #[error("unknown command: {0}")]
256    UnknownCommand(String),
257
258    /// Invalid argument provided.
259    #[error("invalid argument: {0}")]
260    InvalidArgument(String),
261
262    /// Missing required argument.
263    #[error("missing required argument: {0}")]
264    MissingArgument(String),
265
266    /// Command execution failed.
267    #[error("command execution failed: {0}")]
268    ExecutionFailed(String),
269
270    /// User cancelled operation.
271    #[error("operation cancelled by user")]
272    Cancelled,
273
274    /// Output format error.
275    #[error("output format error: {0}")]
276    OutputFormat(String),
277}
278
279// Implement From traits for standard library errors
280
281impl From<std::io::Error> for Error {
282    fn from(err: std::io::Error) -> Self {
283        Self::Io(IoError::Generic(err.to_string()))
284    }
285}
286
287impl From<rusqlite::Error> for Error {
288    fn from(err: rusqlite::Error) -> Self {
289        Self::Storage(StorageError::Database(err.to_string()))
290    }
291}
292
293impl From<rusqlite::Error> for StorageError {
294    fn from(err: rusqlite::Error) -> Self {
295        Self::Database(err.to_string())
296    }
297}
298
299impl From<regex::Error> for ChunkingError {
300    fn from(err: regex::Error) -> Self {
301        Self::Regex(err.to_string())
302    }
303}
304
305impl From<serde_json::Error> for StorageError {
306    fn from(err: serde_json::Error) -> Self {
307        Self::Serialization(err.to_string())
308    }
309}
310
311impl From<std::string::FromUtf8Error> for ChunkingError {
312    fn from(err: std::string::FromUtf8Error) -> Self {
313        Self::InvalidUtf8 {
314            offset: err.utf8_error().valid_up_to(),
315        }
316    }
317}
318
319impl From<std::str::Utf8Error> for ChunkingError {
320    fn from(err: std::str::Utf8Error) -> Self {
321        Self::InvalidUtf8 {
322            offset: err.valid_up_to(),
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_error_display() {
333        let err = Error::InvalidState {
334            message: "test error".to_string(),
335        };
336        assert_eq!(err.to_string(), "invalid state: test error");
337    }
338
339    #[test]
340    fn test_storage_error_display() {
341        let err = StorageError::NotInitialized;
342        assert_eq!(err.to_string(), "RLM not initialized. Run: rlm-cli init");
343
344        let err = StorageError::BufferNotFound {
345            identifier: "test-buffer".to_string(),
346        };
347        assert_eq!(err.to_string(), "buffer not found: test-buffer");
348    }
349
350    #[test]
351    fn test_chunking_error_display() {
352        let err = ChunkingError::InvalidUtf8 { offset: 42 };
353        assert_eq!(err.to_string(), "invalid UTF-8 at byte offset 42");
354
355        let err = ChunkingError::OverlapTooLarge {
356            overlap: 100,
357            size: 50,
358        };
359        assert_eq!(
360            err.to_string(),
361            "overlap 100 must be less than chunk size 50"
362        );
363    }
364
365    #[test]
366    fn test_io_error_display() {
367        let err = IoError::FileNotFound {
368            path: "/tmp/test.txt".to_string(),
369        };
370        assert_eq!(err.to_string(), "file not found: /tmp/test.txt");
371    }
372
373    #[test]
374    fn test_command_error_display() {
375        let err = CommandError::MissingArgument("--file".to_string());
376        assert_eq!(err.to_string(), "missing required argument: --file");
377    }
378
379    #[test]
380    fn test_error_from_io() {
381        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
382        let err: Error = io_err.into();
383        assert!(matches!(err, Error::Io(_)));
384    }
385
386    #[test]
387    fn test_error_from_storage() {
388        let storage_err = StorageError::NotInitialized;
389        let err: Error = storage_err.into();
390        assert!(matches!(err, Error::Storage(_)));
391    }
392
393    #[test]
394    fn test_error_from_chunking() {
395        let chunk_err = ChunkingError::InvalidUtf8 { offset: 0 };
396        let err: Error = chunk_err.into();
397        assert!(matches!(err, Error::Chunking(_)));
398    }
399
400    #[test]
401    fn test_error_from_command() {
402        let cmd_err = CommandError::Cancelled;
403        let err: Error = cmd_err.into();
404        assert!(matches!(err, Error::Command(_)));
405    }
406
407    #[test]
408    fn test_error_config() {
409        let err = Error::Config {
410            message: "bad config".to_string(),
411        };
412        assert_eq!(err.to_string(), "configuration error: bad config");
413    }
414
415    #[test]
416    fn test_storage_error_variants() {
417        let err = StorageError::Database("connection failed".to_string());
418        assert!(err.to_string().contains("connection failed"));
419
420        let err = StorageError::ContextNotFound;
421        assert_eq!(err.to_string(), "context not found");
422
423        let err = StorageError::ChunkNotFound { id: 42 };
424        assert_eq!(err.to_string(), "chunk not found: 42");
425
426        let err = StorageError::Migration("schema error".to_string());
427        assert!(err.to_string().contains("schema error"));
428
429        let err = StorageError::Transaction("rollback".to_string());
430        assert!(err.to_string().contains("rollback"));
431
432        let err = StorageError::Serialization("invalid json".to_string());
433        assert!(err.to_string().contains("invalid json"));
434    }
435
436    #[test]
437    fn test_chunking_error_variants() {
438        let err = ChunkingError::ChunkTooLarge {
439            size: 1000,
440            max: 500,
441        };
442        assert!(err.to_string().contains("1000"));
443        assert!(err.to_string().contains("500"));
444
445        let err = ChunkingError::InvalidConfig {
446            reason: "bad overlap".to_string(),
447        };
448        assert!(err.to_string().contains("bad overlap"));
449
450        let err = ChunkingError::ParallelFailed {
451            reason: "thread panic".to_string(),
452        };
453        assert!(err.to_string().contains("thread panic"));
454
455        let err = ChunkingError::SemanticFailed("model error".to_string());
456        assert!(err.to_string().contains("model error"));
457
458        let err = ChunkingError::Regex("invalid pattern".to_string());
459        assert!(err.to_string().contains("invalid pattern"));
460
461        let err = ChunkingError::UnknownStrategy {
462            name: "foobar".to_string(),
463        };
464        assert!(err.to_string().contains("foobar"));
465    }
466
467    #[test]
468    fn test_io_error_variants() {
469        let err = IoError::ReadFailed {
470            path: "/tmp/test".to_string(),
471            reason: "permission denied".to_string(),
472        };
473        assert!(err.to_string().contains("/tmp/test"));
474        assert!(err.to_string().contains("permission denied"));
475
476        let err = IoError::WriteFailed {
477            path: "/tmp/out".to_string(),
478            reason: "disk full".to_string(),
479        };
480        assert!(err.to_string().contains("disk full"));
481
482        let err = IoError::MmapFailed {
483            path: "/tmp/big".to_string(),
484            reason: "out of memory".to_string(),
485        };
486        assert!(err.to_string().contains("memory mapping"));
487
488        let err = IoError::DirectoryFailed {
489            path: "/tmp/dir".to_string(),
490            reason: "exists".to_string(),
491        };
492        assert!(err.to_string().contains("directory"));
493
494        let err = IoError::PathTraversal {
495            path: "../etc/passwd".to_string(),
496        };
497        assert!(err.to_string().contains("traversal"));
498
499        let err = IoError::Generic("unknown error".to_string());
500        assert!(err.to_string().contains("unknown error"));
501    }
502
503    #[test]
504    fn test_command_error_variants() {
505        let err = CommandError::UnknownCommand("foo".to_string());
506        assert!(err.to_string().contains("unknown command"));
507
508        let err = CommandError::InvalidArgument("--bad".to_string());
509        assert!(err.to_string().contains("invalid argument"));
510
511        let err = CommandError::ExecutionFailed("timeout".to_string());
512        assert!(err.to_string().contains("execution failed"));
513
514        let err = CommandError::Cancelled;
515        assert!(err.to_string().contains("cancelled"));
516
517        let err = CommandError::OutputFormat("json error".to_string());
518        assert!(err.to_string().contains("output format"));
519    }
520
521    #[test]
522    fn test_from_rusqlite_error_to_error() {
523        let rusqlite_err = rusqlite::Error::InvalidQuery;
524        let err: Error = rusqlite_err.into();
525        assert!(matches!(err, Error::Storage(StorageError::Database(_))));
526    }
527
528    #[test]
529    fn test_from_rusqlite_error_to_storage_error() {
530        let rusqlite_err = rusqlite::Error::InvalidQuery;
531        let err: StorageError = rusqlite_err.into();
532        assert!(matches!(err, StorageError::Database(_)));
533    }
534
535    #[test]
536    #[allow(clippy::invalid_regex)]
537    fn test_from_regex_error_to_chunking_error() {
538        let regex_err = regex::Regex::new("[invalid").unwrap_err();
539        let err: ChunkingError = regex_err.into();
540        assert!(matches!(err, ChunkingError::Regex(_)));
541    }
542
543    #[test]
544    fn test_from_serde_json_error_to_storage_error() {
545        let json_err: serde_json::Error = serde_json::from_str::<i32>("invalid").unwrap_err();
546        let err: StorageError = json_err.into();
547        assert!(matches!(err, StorageError::Serialization(_)));
548    }
549
550    #[test]
551    fn test_from_string_utf8_error_to_chunking_error() {
552        // Create invalid UTF-8 bytes
553        let invalid_bytes = vec![0xff, 0xfe];
554        let utf8_err = String::from_utf8(invalid_bytes).unwrap_err();
555        let err: ChunkingError = utf8_err.into();
556        assert!(matches!(err, ChunkingError::InvalidUtf8 { .. }));
557    }
558
559    #[test]
560    fn test_from_str_utf8_error_to_chunking_error() {
561        // Create invalid UTF-8 bytes at runtime to avoid lint warning
562        let invalid_bytes: Vec<u8> = vec![0xff, 0xfe];
563        let utf8_err = std::str::from_utf8(&invalid_bytes).unwrap_err();
564        let err: ChunkingError = utf8_err.into();
565        assert!(matches!(err, ChunkingError::InvalidUtf8 { .. }));
566    }
567}