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

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    /// Another Eidetica backend already owns this storage namespace.
22    #[error(
23        "Eidetica storage `{namespace}` is already owned; connect through the owning Eidetica service instead"
24    )]
25    StorageAlreadyOwned {
26        /// Credential-free description of the storage namespace.
27        namespace: String,
28    },
29    /// The backend does not cache Store state as records.
30    #[error("Store-state records are not supported by this backend")]
31    StoreStateStorageUnsupported,
32
33    /// A staging token does not identify a private unpublished build.
34    #[error("Invalid or expired Store-state staging token")]
35    InvalidStoreStateStagingToken,
36
37    /// A Store-state view does not identify a published record set.
38    ///
39    /// The record set was cleared after the view was minted, or the view never
40    /// identified a published record set. This is never a missing key or an
41    /// empty page: reads against a live view still report those as
42    /// `None` and empty pages.
43    #[error("Store-state view is not ready (cleared or never published)")]
44    InvalidStoreStateView,
45
46    /// A published record set cannot be changed after publication.
47    #[error("Published derived Store state is immutable")]
48    StoreStateNamespaceImmutable,
49
50    /// One encoded record cannot fit in a protocol frame.
51    #[error("Store-state record is too large for the service protocol ({encoded_bytes} bytes)")]
52    RecordTooLarge {
53        /// Encoded record size rejected before transmission.
54        encoded_bytes: usize,
55    },
56
57    /// Entry not found by ID.
58    #[error("Entry not found: {id}")]
59    EntryNotFound {
60        /// The ID of the entry that was not found
61        id: ID,
62    },
63
64    /// Entry failed structural validation.
65    #[error("Entry {entry_id} failed validation: {reason}")]
66    EntryValidationFailed {
67        /// The ID of the entry that failed validation
68        entry_id: ID,
69        /// The reason for validation failure
70        reason: String,
71    },
72
73    /// Verification status not found for entry.
74    #[error("Verification status not found for entry: {id}")]
75    VerificationStatusNotFound {
76        /// The ID of the entry whose verification status was not found
77        id: ID,
78    },
79
80    /// Entry is not part of the specified tree.
81    #[error("Entry {entry_id} is not in tree {tree_id}")]
82    EntryNotInTree {
83        /// The ID of the entry
84        entry_id: ID,
85        /// The ID of the tree
86        tree_id: ID,
87    },
88
89    /// Entry is not part of the specified subtree.
90    #[error("Entry {entry_id} is not in subtree {subtree} of tree {tree_id}")]
91    EntryNotInSubtree {
92        /// The ID of the entry
93        entry_id: ID,
94        /// The ID of the tree
95        tree_id: ID,
96        /// The name of the subtree
97        subtree: String,
98    },
99
100    /// Cycle detected in DAG structure.
101    #[error("Cycle detected in DAG while traversing from {entry_id}")]
102    CycleDetected {
103        /// The entry ID where cycle was detected
104        entry_id: ID,
105    },
106
107    /// No common ancestor found for given entries.
108    ///
109    /// This engine never produces this error: `find_merge_base` reports
110    /// disjoint histories as `Ok(None)` (the empty-base merge) rather than
111    /// failing. The variant is kept because peers running older versions
112    /// still emit it over the service wire (see the decode table in
113    /// `service::error`), and removing an existing variant is a breaking
114    /// change.
115    #[error("No common ancestor found for entries: {entry_ids:?}")]
116    NoCommonAncestor {
117        /// The entry IDs that have no common ancestor
118        entry_ids: Vec<ID>,
119    },
120
121    /// Empty entry list provided where non-empty list required.
122    #[error("No entry IDs provided for {operation}")]
123    EmptyEntryList {
124        /// The operation that required a non-empty list
125        operation: String,
126    },
127
128    /// Data corruption detected during height calculation.
129    #[error("Height calculation corruption: {reason}")]
130    HeightCalculationCorruption {
131        /// Description of the corruption detected
132        reason: String,
133    },
134
135    /// Private key not found.
136    #[error("Private key not found: {key_name}")]
137    PrivateKeyNotFound {
138        /// The name of the private key that was not found
139        key_name: String,
140    },
141
142    /// Serialization failed.
143    #[error("Serialization failed")]
144    SerializationFailed {
145        /// The underlying serialization error
146        #[source]
147        source: serde_json::Error,
148    },
149
150    /// Deserialization failed.
151    #[error("Deserialization failed")]
152    DeserializationFailed {
153        /// The underlying deserialization error
154        #[source]
155        source: serde_json::Error,
156    },
157
158    /// File I/O error.
159    #[error("File I/O error")]
160    FileIo {
161        /// The underlying I/O error
162        #[source]
163        source: std::io::Error,
164    },
165
166    /// CRDT cache operation failed.
167    #[error("CRDT cache operation failed: {reason}")]
168    CrdtCacheError {
169        /// Description of the cache operation failure
170        reason: String,
171    },
172
173    /// Database integrity violation detected.
174    #[error("Database integrity violation: {reason}")]
175    TreeIntegrityViolation {
176        /// Description of the integrity violation
177        reason: String,
178    },
179
180    /// Invalid tree reference or tree ID.
181    #[error("Invalid tree reference: {tree_id}")]
182    InvalidTreeReference {
183        /// The invalid tree ID
184        tree_id: ID,
185    },
186
187    /// Database state inconsistency detected.
188    #[error("Database state inconsistency: {reason}")]
189    StateInconsistency {
190        /// Description of the state inconsistency
191        reason: String,
192    },
193
194    /// Cache miss or cache corruption.
195    #[error("Cache operation failed: {reason}")]
196    CacheError {
197        /// Description of the cache error
198        reason: String,
199    },
200
201    /// SQL database error (sqlx).
202    #[cfg(any(feature = "sqlite", feature = "postgres"))]
203    #[error("SQL error: {reason}")]
204    SqlxError {
205        /// Description of the SQL error
206        reason: String,
207        /// The underlying sqlx error, if available
208        #[source]
209        source: Option<sqlx::Error>,
210    },
211}
212
213impl BackendError {
214    /// Check if this error indicates a resource was not found.
215    pub fn is_not_found(&self) -> bool {
216        matches!(
217            self,
218            BackendError::EntryNotFound { .. }
219                | BackendError::VerificationStatusNotFound { .. }
220                | BackendError::PrivateKeyNotFound { .. }
221        )
222    }
223
224    /// Check if this error indicates a data integrity issue.
225    pub fn is_integrity_error(&self) -> bool {
226        matches!(
227            self,
228            BackendError::EntryValidationFailed { .. }
229                | BackendError::CycleDetected { .. }
230                | BackendError::HeightCalculationCorruption { .. }
231                | BackendError::TreeIntegrityViolation { .. }
232                | BackendError::StateInconsistency { .. }
233        )
234    }
235
236    /// Check if this error is related to I/O operations.
237    pub fn is_io_error(&self) -> bool {
238        #[cfg(any(feature = "sqlite", feature = "postgres"))]
239        if matches!(self, BackendError::SqlxError { .. }) {
240            return true;
241        }
242        matches!(
243            self,
244            BackendError::FileIo { .. }
245                | BackendError::SerializationFailed { .. }
246                | BackendError::DeserializationFailed { .. }
247        )
248    }
249
250    /// Check if this error is related to SQL database operations.
251    #[cfg(any(feature = "sqlite", feature = "postgres"))]
252    pub fn is_sql_error(&self) -> bool {
253        matches!(self, BackendError::SqlxError { .. })
254    }
255
256    /// Check if this error is related to cache operations.
257    pub fn is_cache_error(&self) -> bool {
258        matches!(
259            self,
260            BackendError::CrdtCacheError { .. } | BackendError::CacheError { .. }
261        )
262    }
263
264    /// Check if this error indicates a logical inconsistency.
265    pub fn is_logical_error(&self) -> bool {
266        matches!(
267            self,
268            BackendError::EntryNotInTree { .. }
269                | BackendError::EntryNotInSubtree { .. }
270                | BackendError::NoCommonAncestor { .. }
271                | BackendError::EmptyEntryList { .. }
272        )
273    }
274
275    /// Whether this error means the backend does not cache Store state as
276    /// records.
277    ///
278    /// Only [`BackendError::StoreStateStorageUnsupported`] reports this. Every
279    /// other failure — invalid tokens, invalid views, storage I/O — is genuine
280    /// and must propagate, never fall back.
281    pub fn is_unsupported_store_state(&self) -> bool {
282        matches!(self, BackendError::StoreStateStorageUnsupported)
283    }
284
285    /// Whether this error means a Store-state view no longer identifies a
286    /// published record set (cleared after minting, or never published).
287    pub fn is_invalid_store_state_view(&self) -> bool {
288        matches!(self, BackendError::InvalidStoreStateView)
289    }
290
291    /// Get the entry ID if this error is about a specific entry.
292    pub fn entry_id(&self) -> Option<&ID> {
293        match self {
294            BackendError::EntryNotFound { id }
295            | BackendError::VerificationStatusNotFound { id }
296            | BackendError::EntryValidationFailed { entry_id: id, .. }
297            | BackendError::CycleDetected { entry_id: id }
298            | BackendError::EntryNotInTree { entry_id: id, .. }
299            | BackendError::EntryNotInSubtree { entry_id: id, .. } => Some(id),
300            _ => None,
301        }
302    }
303
304    /// Get the tree ID if this error is about a specific tree.
305    pub fn tree_id(&self) -> Option<&ID> {
306        match self {
307            BackendError::EntryNotInTree { tree_id, .. }
308            | BackendError::EntryNotInSubtree { tree_id, .. }
309            | BackendError::InvalidTreeReference { tree_id } => Some(tree_id),
310            _ => None,
311        }
312    }
313}
314
315// Conversion from DatabaseError to the main Error type
316impl From<BackendError> for crate::Error {
317    fn from(err: BackendError) -> Self {
318        // Use the new structured Backend variant
319        crate::Error::Backend(Box::new(err))
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_error_helpers() {
329        let err = BackendError::EntryNotFound {
330            id: ID::from_bytes("test-entry"),
331        };
332        assert!(err.is_not_found());
333        assert_eq!(err.entry_id(), Some(&ID::from_bytes("test-entry")));
334
335        let err = BackendError::CycleDetected {
336            entry_id: ID::from_bytes("cycle-entry"),
337        };
338        assert!(err.is_integrity_error());
339        assert_eq!(err.entry_id(), Some(&ID::from_bytes("cycle-entry")));
340
341        let err = BackendError::FileIo {
342            source: std::io::Error::new(std::io::ErrorKind::NotFound, "test"),
343        };
344        assert!(err.is_io_error());
345
346        let err = BackendError::CacheError {
347            reason: "test".to_string(),
348        };
349        assert!(err.is_cache_error());
350
351        let err = BackendError::EmptyEntryList {
352            operation: "test".to_string(),
353        };
354        assert!(err.is_logical_error());
355    }
356
357    #[test]
358    fn test_error_conversion() {
359        let db_err = BackendError::EntryNotFound {
360            id: ID::from_bytes("test"),
361        };
362        let err: crate::Error = db_err.into();
363        match err {
364            crate::Error::Backend(e) => match *e {
365                BackendError::EntryNotFound { id } => {
366                    assert_eq!(id, ID::from_bytes("test"))
367                }
368                _ => panic!("Unexpected error variant"),
369            },
370            _ => panic!("Unexpected error variant"),
371        }
372    }
373}