1use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
13pub enum Error {
14 #[error("storage error: {0}")]
16 Storage(#[from] StorageError),
17
18 #[error("chunking error: {0}")]
20 Chunking(#[from] ChunkingError),
21
22 #[error("I/O error: {0}")]
24 Io(#[from] IoError),
25
26 #[error("command error: {0}")]
28 Command(#[from] CommandError),
29
30 #[error("search error: {0}")]
32 Search(#[from] SearchError),
33
34 #[error("invalid state: {message}")]
36 InvalidState {
37 message: String,
39 },
40
41 #[error("configuration error: {message}")]
43 Config {
44 message: String,
46 },
47}
48
49#[derive(Error, Debug)]
51pub enum StorageError {
52 #[error("database error: {0}")]
54 Database(String),
55
56 #[error("RLM not initialized. Run: rlm-cli init")]
58 NotInitialized,
59
60 #[error("context not found")]
62 ContextNotFound,
63
64 #[error("buffer not found: {identifier}")]
66 BufferNotFound {
67 identifier: String,
69 },
70
71 #[error("chunk not found: {id}")]
73 ChunkNotFound {
74 id: i64,
76 },
77
78 #[error("migration error: {0}")]
80 Migration(String),
81
82 #[error("transaction error: {0}")]
84 Transaction(String),
85
86 #[error("serialization error: {0}")]
88 Serialization(String),
89
90 #[cfg(feature = "usearch-hnsw")]
92 #[error("vector search error: {0}")]
93 VectorSearch(String),
94
95 #[cfg(feature = "fastembed-embeddings")]
97 #[error("embedding error: {0}")]
98 Embedding(String),
99}
100
101#[derive(Error, Debug)]
103pub enum ChunkingError {
104 #[error("invalid UTF-8 at byte offset {offset}")]
106 InvalidUtf8 {
107 offset: usize,
109 },
110
111 #[error("chunk size {size} exceeds maximum {max}")]
113 ChunkTooLarge {
114 size: usize,
116 max: usize,
118 },
119
120 #[error("invalid chunk configuration: {reason}")]
122 InvalidConfig {
123 reason: String,
125 },
126
127 #[error("overlap {overlap} must be less than chunk size {size}")]
129 OverlapTooLarge {
130 overlap: usize,
132 size: usize,
134 },
135
136 #[error("parallel processing failed: {reason}")]
138 ParallelFailed {
139 reason: String,
141 },
142
143 #[error("semantic analysis failed: {0}")]
145 SemanticFailed(String),
146
147 #[error("regex error: {0}")]
149 Regex(String),
150
151 #[error("unknown chunking strategy: {name}")]
153 UnknownStrategy {
154 name: String,
156 },
157}
158
159#[derive(Error, Debug)]
161pub enum IoError {
162 #[error("file not found: {path}")]
164 FileNotFound {
165 path: String,
167 },
168
169 #[error("failed to read file: {path}: {reason}")]
171 ReadFailed {
172 path: String,
174 reason: String,
176 },
177
178 #[error("failed to write file: {path}: {reason}")]
180 WriteFailed {
181 path: String,
183 reason: String,
185 },
186
187 #[error("memory mapping failed: {path}: {reason}")]
189 MmapFailed {
190 path: String,
192 reason: String,
194 },
195
196 #[error("failed to create directory: {path}: {reason}")]
198 DirectoryFailed {
199 path: String,
201 reason: String,
203 },
204
205 #[error("path traversal denied: {path}")]
207 PathTraversal {
208 path: String,
210 },
211
212 #[error("I/O error: {0}")]
214 Generic(String),
215}
216
217#[derive(Error, Debug)]
219pub enum SearchError {
220 #[error("index error: {message}")]
222 IndexError {
223 message: String,
225 },
226
227 #[error("dimension mismatch: expected {expected}, got {got}")]
229 DimensionMismatch {
230 expected: usize,
232 got: usize,
234 },
235
236 #[error("feature not enabled: {feature}")]
238 FeatureNotEnabled {
239 feature: String,
241 },
242
243 #[error("query error: {message}")]
245 QueryError {
246 message: String,
248 },
249}
250
251#[derive(Error, Debug)]
253pub enum CommandError {
254 #[error("unknown command: {0}")]
256 UnknownCommand(String),
257
258 #[error("invalid argument: {0}")]
260 InvalidArgument(String),
261
262 #[error("missing required argument: {0}")]
264 MissingArgument(String),
265
266 #[error("command execution failed: {0}")]
268 ExecutionFailed(String),
269
270 #[error("operation cancelled by user")]
272 Cancelled,
273
274 #[error("output format error: {0}")]
276 OutputFormat(String),
277}
278
279impl 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 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 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}