1use thiserror::Error;
7
8use crate::entry::ID;
9
10#[non_exhaustive]
19#[derive(Debug, Error)]
20pub enum BackendError {
21 #[error(
23 "Eidetica storage `{namespace}` is already owned; connect through the owning Eidetica service instead"
24 )]
25 StorageAlreadyOwned {
26 namespace: String,
28 },
29 #[error("Store-state records are not supported by this backend")]
31 StoreStateStorageUnsupported,
32
33 #[error("Invalid or expired Store-state staging token")]
35 InvalidStoreStateStagingToken,
36
37 #[error("Store-state view is not ready (cleared or never published)")]
44 InvalidStoreStateView,
45
46 #[error("Published derived Store state is immutable")]
48 StoreStateNamespaceImmutable,
49
50 #[error("Store-state record is too large for the service protocol ({encoded_bytes} bytes)")]
52 RecordTooLarge {
53 encoded_bytes: usize,
55 },
56
57 #[error("Entry not found: {id}")]
59 EntryNotFound {
60 id: ID,
62 },
63
64 #[error("Entry {entry_id} failed validation: {reason}")]
66 EntryValidationFailed {
67 entry_id: ID,
69 reason: String,
71 },
72
73 #[error("Verification status not found for entry: {id}")]
75 VerificationStatusNotFound {
76 id: ID,
78 },
79
80 #[error("Entry {entry_id} is not in tree {tree_id}")]
82 EntryNotInTree {
83 entry_id: ID,
85 tree_id: ID,
87 },
88
89 #[error("Entry {entry_id} is not in subtree {subtree} of tree {tree_id}")]
91 EntryNotInSubtree {
92 entry_id: ID,
94 tree_id: ID,
96 subtree: String,
98 },
99
100 #[error("Cycle detected in DAG while traversing from {entry_id}")]
102 CycleDetected {
103 entry_id: ID,
105 },
106
107 #[error("No common ancestor found for entries: {entry_ids:?}")]
116 NoCommonAncestor {
117 entry_ids: Vec<ID>,
119 },
120
121 #[error("No entry IDs provided for {operation}")]
123 EmptyEntryList {
124 operation: String,
126 },
127
128 #[error("Height calculation corruption: {reason}")]
130 HeightCalculationCorruption {
131 reason: String,
133 },
134
135 #[error("Private key not found: {key_name}")]
137 PrivateKeyNotFound {
138 key_name: String,
140 },
141
142 #[error("Serialization failed")]
144 SerializationFailed {
145 #[source]
147 source: serde_json::Error,
148 },
149
150 #[error("Deserialization failed")]
152 DeserializationFailed {
153 #[source]
155 source: serde_json::Error,
156 },
157
158 #[error("File I/O error")]
160 FileIo {
161 #[source]
163 source: std::io::Error,
164 },
165
166 #[error("CRDT cache operation failed: {reason}")]
168 CrdtCacheError {
169 reason: String,
171 },
172
173 #[error("Database integrity violation: {reason}")]
175 TreeIntegrityViolation {
176 reason: String,
178 },
179
180 #[error("Invalid tree reference: {tree_id}")]
182 InvalidTreeReference {
183 tree_id: ID,
185 },
186
187 #[error("Database state inconsistency: {reason}")]
189 StateInconsistency {
190 reason: String,
192 },
193
194 #[error("Cache operation failed: {reason}")]
196 CacheError {
197 reason: String,
199 },
200
201 #[cfg(any(feature = "sqlite", feature = "postgres"))]
203 #[error("SQL error: {reason}")]
204 SqlxError {
205 reason: String,
207 #[source]
209 source: Option<sqlx::Error>,
210 },
211}
212
213impl BackendError {
214 pub fn is_not_found(&self) -> bool {
216 matches!(
217 self,
218 BackendError::EntryNotFound { .. }
219 | BackendError::VerificationStatusNotFound { .. }
220 | BackendError::PrivateKeyNotFound { .. }
221 )
222 }
223
224 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 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 #[cfg(any(feature = "sqlite", feature = "postgres"))]
252 pub fn is_sql_error(&self) -> bool {
253 matches!(self, BackendError::SqlxError { .. })
254 }
255
256 pub fn is_cache_error(&self) -> bool {
258 matches!(
259 self,
260 BackendError::CrdtCacheError { .. } | BackendError::CacheError { .. }
261 )
262 }
263
264 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 pub fn is_unsupported_store_state(&self) -> bool {
282 matches!(self, BackendError::StoreStateStorageUnsupported)
283 }
284
285 pub fn is_invalid_store_state_view(&self) -> bool {
288 matches!(self, BackendError::InvalidStoreStateView)
289 }
290
291 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 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
315impl From<BackendError> for crate::Error {
317 fn from(err: BackendError) -> Self {
318 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}