Skip to main content

pulsedb/
error.rs

1//! Error types for PulseDB.
2//!
3//! PulseDB uses a hierarchical error system:
4//! - `PulseDBError` is the top-level error returned by all public APIs
5//! - Specific error types (`StorageError`, `ValidationError`) provide detail
6//!
7//! # Error Handling Pattern
8//! ```rust
9//! use pulsedb::{PulseDB, Config, Result};
10//!
11//! fn example() -> Result<()> {
12//!     let dir = tempfile::tempdir().unwrap();
13//!     let db = PulseDB::open(dir.path().join("test.db"), Config::default())?;
14//!     // ... operations that may fail ...
15//!     db.close()?;
16//!     Ok(())
17//! }
18//! ```
19
20use std::path::PathBuf;
21use thiserror::Error;
22
23/// Result type alias for PulseDB operations.
24pub type Result<T> = std::result::Result<T, PulseDBError>;
25
26/// Top-level error enum for all PulseDB operations.
27///
28/// This is the only error type returned by public APIs.
29/// Use pattern matching to handle specific error cases.
30#[derive(Debug, Error)]
31pub enum PulseDBError {
32    /// Storage layer error (I/O, corruption, transactions).
33    #[error("Storage error: {0}")]
34    Storage(#[from] StorageError),
35
36    /// Input validation error.
37    #[error("Validation error: {0}")]
38    Validation(#[from] ValidationError),
39
40    /// Configuration error.
41    #[error("Configuration error: {reason}")]
42    Config {
43        /// Description of what's wrong with the configuration.
44        reason: String,
45    },
46
47    /// Requested entity not found.
48    #[error("{0}")]
49    NotFound(#[from] NotFoundError),
50
51    /// General I/O error.
52    #[error("I/O error: {0}")]
53    Io(#[from] std::io::Error),
54
55    /// Embedding generation/validation error.
56    #[error("Embedding error: {0}")]
57    Embedding(String),
58
59    /// Vector index error (HNSW operations).
60    #[error("Vector index error: {0}")]
61    Vector(String),
62
63    /// Watch system error (subscription or event delivery).
64    #[error("Watch error: {0}")]
65    Watch(String),
66
67    /// Internal error (e.g., async runtime failure, task join error).
68    #[error("Internal error: {0}")]
69    Internal(String),
70}
71
72impl PulseDBError {
73    /// Creates a configuration error with the given reason.
74    pub fn config(reason: impl Into<String>) -> Self {
75        Self::Config {
76            reason: reason.into(),
77        }
78    }
79
80    /// Creates an embedding error with the given message.
81    pub fn embedding(msg: impl Into<String>) -> Self {
82        Self::Embedding(msg.into())
83    }
84
85    /// Creates a vector index error with the given message.
86    pub fn vector(msg: impl Into<String>) -> Self {
87        Self::Vector(msg.into())
88    }
89
90    /// Creates a watch system error with the given message.
91    pub fn watch(msg: impl Into<String>) -> Self {
92        Self::Watch(msg.into())
93    }
94
95    /// Creates an internal error with the given message.
96    pub fn internal(msg: impl Into<String>) -> Self {
97        Self::Internal(msg.into())
98    }
99
100    /// Returns true if this is a "not found" error.
101    pub fn is_not_found(&self) -> bool {
102        matches!(self, Self::NotFound(_))
103    }
104
105    /// Returns true if this is a validation error.
106    pub fn is_validation(&self) -> bool {
107        matches!(self, Self::Validation(_))
108    }
109
110    /// Returns true if this is a storage error.
111    pub fn is_storage(&self) -> bool {
112        matches!(self, Self::Storage(_))
113    }
114
115    /// Returns true if this is a vector index error.
116    pub fn is_vector(&self) -> bool {
117        matches!(self, Self::Vector(_))
118    }
119
120    /// Returns true if this is a watch system error.
121    pub fn is_watch(&self) -> bool {
122        matches!(self, Self::Watch(_))
123    }
124
125    /// Returns true if this is an embedding error.
126    pub fn is_embedding(&self) -> bool {
127        matches!(self, Self::Embedding(_))
128    }
129
130    /// Returns true if this is an internal error.
131    pub fn is_internal(&self) -> bool {
132        matches!(self, Self::Internal(_))
133    }
134
135    /// Returns true if this is a configuration error.
136    pub fn is_config(&self) -> bool {
137        matches!(self, Self::Config { .. })
138    }
139
140    /// Returns true if this is an I/O error.
141    pub fn is_io(&self) -> bool {
142        matches!(self, Self::Io(_))
143    }
144}
145
146/// Storage-related errors.
147///
148/// These errors indicate problems with the underlying storage layer.
149#[derive(Debug, Error)]
150pub enum StorageError {
151    /// Database file or data is corrupted.
152    #[error("Database corrupted: {0}")]
153    Corrupted(String),
154
155    /// Database file not found at expected path.
156    #[error("Database not found: {0}")]
157    DatabaseNotFound(PathBuf),
158
159    /// Database is locked by another process.
160    #[error("Database is locked by another writer")]
161    DatabaseLocked,
162
163    /// Transaction failed (commit, rollback, etc.).
164    #[error("Transaction failed: {0}")]
165    Transaction(String),
166
167    /// Serialization/deserialization error.
168    #[error("Serialization error: {0}")]
169    Serialization(String),
170
171    /// Error from the redb storage engine.
172    #[error("Storage engine error: {0}")]
173    Redb(String),
174
175    /// Database schema version doesn't match expected version.
176    #[error("Schema version mismatch: expected {expected}, found {found}")]
177    SchemaVersionMismatch {
178        /// Expected schema version.
179        expected: u32,
180        /// Actual schema version found in database.
181        found: u32,
182    },
183
184    /// Table not found in database.
185    #[error("Table not found: {0}")]
186    TableNotFound(String),
187}
188
189impl StorageError {
190    /// Creates a corruption error with the given message.
191    pub fn corrupted(msg: impl Into<String>) -> Self {
192        Self::Corrupted(msg.into())
193    }
194
195    /// Creates a transaction error with the given message.
196    pub fn transaction(msg: impl Into<String>) -> Self {
197        Self::Transaction(msg.into())
198    }
199
200    /// Creates a serialization error with the given message.
201    pub fn serialization(msg: impl Into<String>) -> Self {
202        Self::Serialization(msg.into())
203    }
204
205    /// Creates a redb error with the given message.
206    pub fn redb(msg: impl Into<String>) -> Self {
207        Self::Redb(msg.into())
208    }
209}
210
211// Conversions from redb error types
212impl From<redb::Error> for StorageError {
213    fn from(err: redb::Error) -> Self {
214        StorageError::Redb(err.to_string())
215    }
216}
217
218impl From<redb::DatabaseError> for StorageError {
219    fn from(err: redb::DatabaseError) -> Self {
220        StorageError::Redb(err.to_string())
221    }
222}
223
224impl From<redb::TransactionError> for StorageError {
225    fn from(err: redb::TransactionError) -> Self {
226        StorageError::Transaction(err.to_string())
227    }
228}
229
230impl From<redb::CommitError> for StorageError {
231    fn from(err: redb::CommitError) -> Self {
232        StorageError::Transaction(format!("Commit failed: {}", err))
233    }
234}
235
236impl From<redb::TableError> for StorageError {
237    fn from(err: redb::TableError) -> Self {
238        StorageError::Redb(format!("Table error: {}", err))
239    }
240}
241
242impl From<redb::StorageError> for StorageError {
243    fn from(err: redb::StorageError) -> Self {
244        StorageError::Redb(format!("Storage error: {}", err))
245    }
246}
247
248// Convert bincode errors to StorageError
249impl From<bincode::Error> for StorageError {
250    fn from(err: bincode::Error) -> Self {
251        StorageError::Serialization(err.to_string())
252    }
253}
254
255// Also allow direct conversion to PulseDBError for convenience
256impl From<redb::Error> for PulseDBError {
257    fn from(err: redb::Error) -> Self {
258        PulseDBError::Storage(StorageError::from(err))
259    }
260}
261
262impl From<redb::DatabaseError> for PulseDBError {
263    fn from(err: redb::DatabaseError) -> Self {
264        PulseDBError::Storage(StorageError::from(err))
265    }
266}
267
268impl From<redb::TransactionError> for PulseDBError {
269    fn from(err: redb::TransactionError) -> Self {
270        PulseDBError::Storage(StorageError::from(err))
271    }
272}
273
274impl From<redb::CommitError> for PulseDBError {
275    fn from(err: redb::CommitError) -> Self {
276        PulseDBError::Storage(StorageError::from(err))
277    }
278}
279
280impl From<redb::TableError> for PulseDBError {
281    fn from(err: redb::TableError) -> Self {
282        PulseDBError::Storage(StorageError::from(err))
283    }
284}
285
286impl From<redb::StorageError> for PulseDBError {
287    fn from(err: redb::StorageError) -> Self {
288        PulseDBError::Storage(StorageError::from(err))
289    }
290}
291
292impl From<bincode::Error> for PulseDBError {
293    fn from(err: bincode::Error) -> Self {
294        PulseDBError::Storage(StorageError::from(err))
295    }
296}
297
298/// Validation errors for input data.
299///
300/// These errors indicate problems with data provided by the caller.
301#[derive(Debug, Error)]
302pub enum ValidationError {
303    /// Embedding dimension doesn't match collective's configured dimension.
304    #[error("Embedding dimension mismatch: expected {expected}, got {got}")]
305    DimensionMismatch {
306        /// Expected dimension from collective configuration.
307        expected: usize,
308        /// Actual dimension provided.
309        got: usize,
310    },
311
312    /// A field has an invalid value.
313    #[error("Invalid field '{field}': {reason}")]
314    InvalidField {
315        /// Name of the invalid field.
316        field: String,
317        /// Why the value is invalid.
318        reason: String,
319    },
320
321    /// Content exceeds maximum allowed size.
322    #[error("Content too large: {size} bytes (max: {max} bytes)")]
323    ContentTooLarge {
324        /// Actual content size in bytes.
325        size: usize,
326        /// Maximum allowed size in bytes.
327        max: usize,
328    },
329
330    /// A required field is missing or empty.
331    #[error("Required field missing: {field}")]
332    RequiredField {
333        /// Name of the missing field.
334        field: String,
335    },
336
337    /// Too many items in a collection field.
338    #[error("Too many items in '{field}': {count} (max: {max})")]
339    TooManyItems {
340        /// Name of the field.
341        field: String,
342        /// Actual count.
343        count: usize,
344        /// Maximum allowed.
345        max: usize,
346    },
347}
348
349impl ValidationError {
350    /// Creates a dimension mismatch error.
351    pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
352        Self::DimensionMismatch { expected, got }
353    }
354
355    /// Creates an invalid field error.
356    pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
357        Self::InvalidField {
358            field: field.into(),
359            reason: reason.into(),
360        }
361    }
362
363    /// Creates a content too large error.
364    pub fn content_too_large(size: usize, max: usize) -> Self {
365        Self::ContentTooLarge { size, max }
366    }
367
368    /// Creates a required field error.
369    pub fn required_field(field: impl Into<String>) -> Self {
370        Self::RequiredField {
371            field: field.into(),
372        }
373    }
374
375    /// Creates a too many items error.
376    pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
377        Self::TooManyItems {
378            field: field.into(),
379            count,
380            max,
381        }
382    }
383}
384
385/// Not found errors for specific entity types.
386#[derive(Debug, Error)]
387pub enum NotFoundError {
388    /// Collective with given ID not found.
389    #[error("Collective not found: {0}")]
390    Collective(String),
391
392    /// Experience with given ID not found.
393    #[error("Experience not found: {0}")]
394    Experience(String),
395
396    /// Relation with given ID not found.
397    #[error("Relation not found: {0}")]
398    Relation(String),
399
400    /// Insight with given ID not found.
401    #[error("Insight not found: {0}")]
402    Insight(String),
403
404    /// Activity not found for given agent/collective pair.
405    #[error("Activity not found: {0}")]
406    Activity(String),
407}
408
409impl NotFoundError {
410    /// Creates a collective not found error.
411    pub fn collective(id: impl ToString) -> Self {
412        Self::Collective(id.to_string())
413    }
414
415    /// Creates an experience not found error.
416    pub fn experience(id: impl ToString) -> Self {
417        Self::Experience(id.to_string())
418    }
419
420    /// Creates a relation not found error.
421    pub fn relation(id: impl ToString) -> Self {
422        Self::Relation(id.to_string())
423    }
424
425    /// Creates an insight not found error.
426    pub fn insight(id: impl ToString) -> Self {
427        Self::Insight(id.to_string())
428    }
429
430    /// Creates an activity not found error.
431    pub fn activity(id: impl ToString) -> Self {
432        Self::Activity(id.to_string())
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_error_display() {
442        let err = PulseDBError::config("Invalid dimension");
443        assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
444    }
445
446    #[test]
447    fn test_storage_error_display() {
448        let err = StorageError::SchemaVersionMismatch {
449            expected: 2,
450            found: 1,
451        };
452        assert_eq!(
453            err.to_string(),
454            "Schema version mismatch: expected 2, found 1"
455        );
456    }
457
458    #[test]
459    fn test_validation_error_display() {
460        let err = ValidationError::dimension_mismatch(384, 768);
461        assert_eq!(
462            err.to_string(),
463            "Embedding dimension mismatch: expected 384, got 768"
464        );
465    }
466
467    #[test]
468    fn test_not_found_error_display() {
469        let err = NotFoundError::collective("abc-123");
470        assert_eq!(err.to_string(), "Collective not found: abc-123");
471    }
472
473    #[test]
474    fn test_is_not_found() {
475        let err: PulseDBError = NotFoundError::collective("test").into();
476        assert!(err.is_not_found());
477        assert!(!err.is_validation());
478    }
479
480    #[test]
481    fn test_is_validation() {
482        let err: PulseDBError = ValidationError::required_field("content").into();
483        assert!(err.is_validation());
484        assert!(!err.is_not_found());
485    }
486
487    #[test]
488    fn test_vector_error_display() {
489        let err = PulseDBError::vector("HNSW insert failed");
490        assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
491        assert!(err.is_vector());
492        assert!(!err.is_storage());
493    }
494
495    #[test]
496    fn test_error_conversion_chain() {
497        // Simulate a storage error propagating up
498        fn inner() -> Result<()> {
499            Err(StorageError::corrupted("test corruption"))?
500        }
501
502        let result = inner();
503        assert!(result.is_err());
504        assert!(result.unwrap_err().is_storage());
505    }
506
507    #[test]
508    fn test_watch_error_display() {
509        let err = PulseDBError::watch("subscribers lock poisoned");
510        assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
511    }
512
513    #[test]
514    fn test_watch_constructor() {
515        let err = PulseDBError::watch("test");
516        assert!(err.is_watch());
517        assert!(!err.is_storage());
518    }
519
520    #[test]
521    fn test_is_watch() {
522        let err = PulseDBError::watch("test");
523        assert!(err.is_watch());
524        assert!(!err.is_not_found());
525    }
526
527    #[test]
528    fn test_is_embedding() {
529        let err = PulseDBError::embedding("model load failed");
530        assert!(err.is_embedding());
531        assert!(!err.is_vector());
532    }
533
534    #[test]
535    fn test_is_internal() {
536        let err = PulseDBError::internal("task join failed");
537        assert!(err.is_internal());
538        assert!(!err.is_storage());
539    }
540
541    #[test]
542    fn test_is_config() {
543        let err = PulseDBError::config("invalid dimension");
544        assert!(err.is_config());
545        assert!(!err.is_validation());
546    }
547
548    #[test]
549    fn test_is_io() {
550        let err = PulseDBError::Io(std::io::Error::new(
551            std::io::ErrorKind::NotFound,
552            "file missing",
553        ));
554        assert!(err.is_io());
555        assert!(!err.is_storage());
556    }
557}