Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/backend/
errors.rs

1//! Database error types for the Eidetica backend.
2//!
3//! This module defines structured error types for database operations,
4//! providing better error context and type safety compared to string-based errors.
5
6use thiserror::Error;
7
8use crate::entry::ID;
9
10/// Errors that can occur during database operations.
11///
12/// # Stability
13///
14/// - New variants may be added in minor versions (enum is `#[non_exhaustive]`)
15/// - Existing variants will not be removed in minor versions
16/// - Field additions/changes require a major version bump
17/// - Helper methods like `is_*()` provide stable APIs
18#[non_exhaustive]
19#[derive(Debug, Error)]
20pub enum BackendError {
21    /// Entry not found by ID.
22    #[error("Entry not found: {id}")]
23    EntryNotFound {
24        /// The ID of the entry that was not found
25        id: ID,
26    },
27
28    /// Entry failed structural validation.
29    #[error("Entry {entry_id} failed validation: {reason}")]
30    EntryValidationFailed {
31        /// The ID of the entry that failed validation
32        entry_id: ID,
33        /// The reason for validation failure
34        reason: String,
35    },
36
37    /// Verification status not found for entry.
38    #[error("Verification status not found for entry: {id}")]
39    VerificationStatusNotFound {
40        /// The ID of the entry whose verification status was not found
41        id: ID,
42    },
43
44    /// Entry is not part of the specified tree.
45    #[error("Entry {entry_id} is not in tree {tree_id}")]
46    EntryNotInTree {
47        /// The ID of the entry
48        entry_id: ID,
49        /// The ID of the tree
50        tree_id: ID,
51    },
52
53    /// Entry is not part of the specified subtree.
54    #[error("Entry {entry_id} is not in subtree {subtree} of tree {tree_id}")]
55    EntryNotInSubtree {
56        /// The ID of the entry
57        entry_id: ID,
58        /// The ID of the tree
59        tree_id: ID,
60        /// The name of the subtree
61        subtree: String,
62    },
63
64    /// Cycle detected in DAG structure.
65    #[error("Cycle detected in DAG while traversing from {entry_id}")]
66    CycleDetected {
67        /// The entry ID where cycle was detected
68        entry_id: ID,
69    },
70
71    /// No common ancestor found for given entries.
72    #[error("No common ancestor found for entries: {entry_ids:?}")]
73    NoCommonAncestor {
74        /// The entry IDs that have no common ancestor
75        entry_ids: Vec<ID>,
76    },
77
78    /// Empty entry list provided where non-empty list required.
79    #[error("No entry IDs provided for {operation}")]
80    EmptyEntryList {
81        /// The operation that required a non-empty list
82        operation: String,
83    },
84
85    /// Data corruption detected during height calculation.
86    #[error("Height calculation corruption: {reason}")]
87    HeightCalculationCorruption {
88        /// Description of the corruption detected
89        reason: String,
90    },
91
92    /// Private key not found.
93    #[error("Private key not found: {key_name}")]
94    PrivateKeyNotFound {
95        /// The name of the private key that was not found
96        key_name: String,
97    },
98
99    /// Serialization failed.
100    #[error("Serialization failed")]
101    SerializationFailed {
102        /// The underlying serialization error
103        #[source]
104        source: serde_json::Error,
105    },
106
107    /// Deserialization failed.
108    #[error("Deserialization failed")]
109    DeserializationFailed {
110        /// The underlying deserialization error
111        #[source]
112        source: serde_json::Error,
113    },
114
115    /// File I/O error.
116    #[error("File I/O error")]
117    FileIo {
118        /// The underlying I/O error
119        #[source]
120        source: std::io::Error,
121    },
122
123    /// CRDT cache operation failed.
124    #[error("CRDT cache operation failed: {reason}")]
125    CrdtCacheError {
126        /// Description of the cache operation failure
127        reason: String,
128    },
129
130    /// Database integrity violation detected.
131    #[error("Database integrity violation: {reason}")]
132    TreeIntegrityViolation {
133        /// Description of the integrity violation
134        reason: String,
135    },
136
137    /// Invalid tree reference or tree ID.
138    #[error("Invalid tree reference: {tree_id}")]
139    InvalidTreeReference {
140        /// The invalid tree ID
141        tree_id: String,
142    },
143
144    /// Database state inconsistency detected.
145    #[error("Database state inconsistency: {reason}")]
146    StateInconsistency {
147        /// Description of the state inconsistency
148        reason: String,
149    },
150
151    /// Cache miss or cache corruption.
152    #[error("Cache operation failed: {reason}")]
153    CacheError {
154        /// Description of the cache error
155        reason: String,
156    },
157
158    /// SQL database error (sqlx).
159    #[cfg(any(feature = "sqlite", feature = "postgres"))]
160    #[error("SQL error: {reason}")]
161    SqlxError {
162        /// Description of the SQL error
163        reason: String,
164        /// The underlying sqlx error, if available
165        #[source]
166        source: Option<sqlx::Error>,
167    },
168}
169
170impl BackendError {
171    /// Check if this error indicates a resource was not found.
172    pub fn is_not_found(&self) -> bool {
173        matches!(
174            self,
175            BackendError::EntryNotFound { .. }
176                | BackendError::VerificationStatusNotFound { .. }
177                | BackendError::PrivateKeyNotFound { .. }
178        )
179    }
180
181    /// Check if this error indicates a data integrity issue.
182    pub fn is_integrity_error(&self) -> bool {
183        matches!(
184            self,
185            BackendError::EntryValidationFailed { .. }
186                | BackendError::CycleDetected { .. }
187                | BackendError::HeightCalculationCorruption { .. }
188                | BackendError::TreeIntegrityViolation { .. }
189                | BackendError::StateInconsistency { .. }
190        )
191    }
192
193    /// Check if this error is related to I/O operations.
194    pub fn is_io_error(&self) -> bool {
195        #[cfg(any(feature = "sqlite", feature = "postgres"))]
196        if matches!(self, BackendError::SqlxError { .. }) {
197            return true;
198        }
199        matches!(
200            self,
201            BackendError::FileIo { .. }
202                | BackendError::SerializationFailed { .. }
203                | BackendError::DeserializationFailed { .. }
204        )
205    }
206
207    /// Check if this error is related to SQL database operations.
208    #[cfg(any(feature = "sqlite", feature = "postgres"))]
209    pub fn is_sql_error(&self) -> bool {
210        matches!(self, BackendError::SqlxError { .. })
211    }
212
213    /// Check if this error is related to cache operations.
214    pub fn is_cache_error(&self) -> bool {
215        matches!(
216            self,
217            BackendError::CrdtCacheError { .. } | BackendError::CacheError { .. }
218        )
219    }
220
221    /// Check if this error indicates a logical inconsistency.
222    pub fn is_logical_error(&self) -> bool {
223        matches!(
224            self,
225            BackendError::EntryNotInTree { .. }
226                | BackendError::EntryNotInSubtree { .. }
227                | BackendError::NoCommonAncestor { .. }
228                | BackendError::EmptyEntryList { .. }
229        )
230    }
231
232    /// Get the entry ID if this error is about a specific entry.
233    pub fn entry_id(&self) -> Option<&ID> {
234        match self {
235            BackendError::EntryNotFound { id }
236            | BackendError::VerificationStatusNotFound { id }
237            | BackendError::EntryValidationFailed { entry_id: id, .. }
238            | BackendError::CycleDetected { entry_id: id }
239            | BackendError::EntryNotInTree { entry_id: id, .. }
240            | BackendError::EntryNotInSubtree { entry_id: id, .. } => Some(id),
241            _ => None,
242        }
243    }
244
245    /// Get the tree ID if this error is about a specific tree.
246    pub fn tree_id(&self) -> Option<String> {
247        match self {
248            BackendError::EntryNotInTree { tree_id, .. }
249            | BackendError::EntryNotInSubtree { tree_id, .. } => Some(tree_id.to_string()),
250            BackendError::InvalidTreeReference { tree_id } => Some(tree_id.clone()),
251            _ => None,
252        }
253    }
254}
255
256// Conversion from DatabaseError to the main Error type
257impl From<BackendError> for crate::Error {
258    fn from(err: BackendError) -> Self {
259        // Use the new structured Backend variant
260        crate::Error::Backend(err)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_error_helpers() {
270        let err = BackendError::EntryNotFound {
271            id: ID::from("test-entry"),
272        };
273        assert!(err.is_not_found());
274        assert_eq!(err.entry_id(), Some(&ID::from("test-entry")));
275
276        let err = BackendError::CycleDetected {
277            entry_id: ID::from("cycle-entry"),
278        };
279        assert!(err.is_integrity_error());
280        assert_eq!(err.entry_id(), Some(&ID::from("cycle-entry")));
281
282        let err = BackendError::FileIo {
283            source: std::io::Error::new(std::io::ErrorKind::NotFound, "test"),
284        };
285        assert!(err.is_io_error());
286
287        let err = BackendError::CacheError {
288            reason: "test".to_string(),
289        };
290        assert!(err.is_cache_error());
291
292        let err = BackendError::EmptyEntryList {
293            operation: "test".to_string(),
294        };
295        assert!(err.is_logical_error());
296    }
297
298    #[test]
299    fn test_error_conversion() {
300        let db_err = BackendError::EntryNotFound {
301            id: ID::from("test"),
302        };
303        let err: crate::Error = db_err.into();
304        match err {
305            crate::Error::Backend(BackendError::EntryNotFound { id }) => {
306                assert_eq!(id.to_string(), "test")
307            }
308            _ => panic!("Unexpected error variant"),
309        }
310    }
311}