use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum SqliteStoreError {
#[error("sqlite store io error: {0}")]
Io(#[from] std::io::Error),
#[error("sqlite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error(
"schema for domain `{domain}` is from the future: file has version {found}, \
this binary supports up to {supported}"
)]
SchemaFromTheFuture {
domain: String,
found: i64,
supported: i64,
},
#[error("migration {version} (`{name}`) for domain `{domain}` failed: {source}")]
MigrationFailed {
domain: String,
version: i64,
name: String,
#[source]
source: rusqlite::Error,
},
#[error(
"migration {version} (`{name}`) for domain `{domain}` ended the runner's transaction; \
migration bodies must not COMMIT or ROLLBACK"
)]
MigrationBrokeTransaction {
domain: String,
version: i64,
name: String,
},
#[error("meerkat_schema ledger is malformed: {detail}")]
LedgerMalformed { detail: String },
#[error("could not establish journal_mode=WAL on `{path}`: effective mode is `{actual}`")]
WalNotEstablished { path: PathBuf, actual: String },
#[error("domain `{domain}` registered an invalid migration list: {detail}")]
InvalidMigrationList { domain: String, detail: String },
#[error("cannot open `{path}` with profile {profile}: {detail}")]
OpenRefused {
path: PathBuf,
profile: &'static str,
detail: String,
},
#[error("maintenance fence is held for `{path}`; storage is under offline maintenance")]
MaintenanceFenceHeld { path: PathBuf },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqliteErrorClass {
Transient,
Corrupt,
Other,
}
pub fn classify_sqlite_error(error: &rusqlite::Error) -> SqliteErrorClass {
use rusqlite::ErrorCode;
match error {
rusqlite::Error::SqliteFailure(f, _) => match f.code {
ErrorCode::DatabaseBusy
| ErrorCode::DatabaseLocked
| ErrorCode::OperationInterrupted => SqliteErrorClass::Transient,
ErrorCode::DatabaseCorrupt | ErrorCode::NotADatabase => SqliteErrorClass::Corrupt,
_ => SqliteErrorClass::Other,
},
_ => SqliteErrorClass::Other,
}
}
pub fn is_busy_or_locked(error: &rusqlite::Error) -> bool {
use rusqlite::ErrorCode;
matches!(
error,
rusqlite::Error::SqliteFailure(f, _)
if matches!(f.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked)
)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
fn sqlite_failure(code: rusqlite::ErrorCode) -> rusqlite::Error {
rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error {
code,
extended_code: 0,
},
None,
)
}
#[test]
fn busy_and_locked_classify_transient() {
for code in [
rusqlite::ErrorCode::DatabaseBusy,
rusqlite::ErrorCode::DatabaseLocked,
] {
let err = sqlite_failure(code);
assert_eq!(classify_sqlite_error(&err), SqliteErrorClass::Transient);
assert!(is_busy_or_locked(&err));
}
}
#[test]
fn corruption_classifies_corrupt() {
for code in [
rusqlite::ErrorCode::DatabaseCorrupt,
rusqlite::ErrorCode::NotADatabase,
] {
let err = sqlite_failure(code);
assert_eq!(classify_sqlite_error(&err), SqliteErrorClass::Corrupt);
assert!(!is_busy_or_locked(&err));
}
}
#[test]
fn constraint_violation_classifies_other() {
let err = sqlite_failure(rusqlite::ErrorCode::ConstraintViolation);
assert_eq!(classify_sqlite_error(&err), SqliteErrorClass::Other);
assert!(!is_busy_or_locked(&err));
}
}