Skip to main content

wacore/store/
error.rs

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    /// Validation failure with a descriptive message and no underlying typed
13    /// source — e.g. "Invalid foo length: 17". Prefer `Serialization` or a
14    /// dedicated typed variant if a real source exists.
15    #[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    /// Walks the error source chain and returns true if any layer's `Display`
39    /// indicates a SQLite busy/locked condition. Used by retry layers that
40    /// can't depend on a specific backend (Diesel, libsql, etc.) directly.
41    ///
42    /// Substring matching is necessary because SQLite reports BUSY/LOCKED
43    /// through `sqlite3_errmsg()` strings; the error code itself is mapped
44    /// to `Diesel::DatabaseError(Unknown, _)` (or similar) without further
45    /// discrimination.
46    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        // SQLite drivers in different ecosystems vary on casing for these
99        // diagnostic strings ("database is LOCKED", "Busy", etc.). The check
100        // must not depend on the exact casing the underlying driver chose.
101        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}