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