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