use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StoreError {
#[error("I/O error")]
Io(#[from] std::io::Error),
#[error("serialization/deserialization error")]
Serialization(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("data validation failed: {0}")]
Validation(String),
#[error("database connection error")]
Connection(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("database operation error")]
Database(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("database operation '{op}' exhausted retries")]
RetriesExhausted { op: String },
#[error("migration error")]
Migration(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("store configuration is invalid: {0}")]
InvalidConfig(String),
#[error("device with ID {0} not found")]
DeviceNotFound(i32),
}
impl StoreError {
pub fn is_database_busy_or_locked(&self) -> bool {
let mut layer: &dyn std::error::Error = self;
loop {
let s = layer.to_string().to_lowercase();
if s.contains("locked") || s.contains("busy") {
return true;
}
match layer.source() {
Some(inner) => layer = inner,
None => return false,
}
}
}
}
pub type Result<T> = std::result::Result<T, StoreError>;
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("synthetic backend error: {0}")]
struct DummyBackendError(&'static str);
#[test]
fn database_preserves_typed_source_via_downcast() {
let inner = DummyBackendError("bang");
let se = StoreError::Database(Box::new(inner));
let src = std::error::Error::source(&se).expect("source preserved");
let downcast = src
.downcast_ref::<DummyBackendError>()
.expect("downcasts to DummyBackendError");
assert_eq!(downcast.0, "bang");
}
#[test]
fn is_busy_or_locked_walks_chain() {
let inner = DummyBackendError("database is locked");
let se = StoreError::Database(Box::new(inner));
assert!(se.is_database_busy_or_locked());
}
#[test]
fn is_busy_or_locked_negative() {
let inner = DummyBackendError("permission denied");
let se = StoreError::Database(Box::new(inner));
assert!(!se.is_database_busy_or_locked());
}
#[test]
fn is_busy_or_locked_is_case_insensitive() {
for msg in [
"database is LOCKED",
"SQLITE_BUSY: write contention",
"Busy",
"Locked",
] {
let se = StoreError::Database(Box::new(DummyBackendError(msg)));
assert!(
se.is_database_busy_or_locked(),
"expected {msg:?} to be detected"
);
}
}
}