1use thiserror::Error;
2
3#[derive(Debug, Error)]
4#[non_exhaustive]
5pub enum StoreError {
6 #[error("I/O error")]
7 Io(#[from] std::io::Error),
8
9 #[error("serialization/deserialization error")]
10 Serialization(#[source] Box<dyn std::error::Error + Send + Sync>),
11
12 #[error("data validation failed: {0}")]
16 Validation(String),
17
18 #[error("database connection error")]
19 Connection(#[source] Box<dyn std::error::Error + Send + Sync>),
20
21 #[error("database operation error")]
22 Database(#[source] Box<dyn std::error::Error + Send + Sync>),
23
24 #[error("database operation '{op}' exhausted retries")]
25 RetriesExhausted { op: String },
26
27 #[error("migration error")]
28 Migration(#[source] Box<dyn std::error::Error + Send + Sync>),
29
30 #[error("store configuration is invalid: {0}")]
31 InvalidConfig(String),
32
33 #[error("device with ID {0} not found")]
34 DeviceNotFound(i32),
35}
36
37impl StoreError {
38 pub fn is_database_busy_or_locked(&self) -> bool {
47 let mut layer: &dyn std::error::Error = self;
48 loop {
49 let s = layer.to_string().to_lowercase();
50 if s.contains("locked") || s.contains("busy") {
51 return true;
52 }
53 match layer.source() {
54 Some(inner) => layer = inner,
55 None => return false,
56 }
57 }
58 }
59}
60
61pub type Result<T> = std::result::Result<T, StoreError>;
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[derive(Debug, thiserror::Error)]
68 #[error("synthetic backend error: {0}")]
69 struct DummyBackendError(&'static str);
70
71 #[test]
72 fn database_preserves_typed_source_via_downcast() {
73 let inner = DummyBackendError("bang");
74 let se = StoreError::Database(Box::new(inner));
75 let src = std::error::Error::source(&se).expect("source preserved");
76 let downcast = src
77 .downcast_ref::<DummyBackendError>()
78 .expect("downcasts to DummyBackendError");
79 assert_eq!(downcast.0, "bang");
80 }
81
82 #[test]
83 fn is_busy_or_locked_walks_chain() {
84 let inner = DummyBackendError("database is locked");
85 let se = StoreError::Database(Box::new(inner));
86 assert!(se.is_database_busy_or_locked());
87 }
88
89 #[test]
90 fn is_busy_or_locked_negative() {
91 let inner = DummyBackendError("permission denied");
92 let se = StoreError::Database(Box::new(inner));
93 assert!(!se.is_database_busy_or_locked());
94 }
95
96 #[test]
97 fn is_busy_or_locked_is_case_insensitive() {
98 for msg in [
102 "database is LOCKED",
103 "SQLITE_BUSY: write contention",
104 "Busy",
105 "Locked",
106 ] {
107 let se = StoreError::Database(Box::new(DummyBackendError(msg)));
108 assert!(
109 se.is_database_busy_or_locked(),
110 "expected {msg:?} to be detected"
111 );
112 }
113 }
114}