use std::fmt;
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Error>;
#[non_exhaustive]
#[derive(Debug)]
pub enum Error {
AlreadyExists(PathBuf),
ReadOnly(ReadOnly),
UnsupportedSchema {
found: i64,
writes: i64,
reads: i64,
},
EncoderMismatch {
source_id: i64,
wrote: String,
reading: String,
},
TimelineBackwards {
source_id: i64,
ts: i64,
floor: i64,
},
InUse {
what: String,
},
NotAnArchive {
what: String,
reason: String,
},
Encoder {
stream: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
EncoderContract {
stream: String,
detail: String,
},
Writer(std::sync::Arc<Error>),
WriterGone,
Sqlite {
context: String,
source: rusqlite::Error,
},
Message(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReadOnly {
Media,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::AlreadyExists(p) => {
write!(f, "{} already exists", p.display())
}
Error::ReadOnly(ReadOnly::Media) => write!(
f,
"this archive is on read-only media; open it with `Archive::open` to read it"
),
Error::UnsupportedSchema {
found,
writes,
reads,
} => write!(
f,
"unsupported archive schema version {found}: this build writes \
v{writes} and reads v{reads}"
),
Error::EncoderMismatch {
source_id,
wrote,
reading,
} => write!(
f,
"source {source_id} was written with encoder version {wrote:?} and is being \
read with {reading:?}; the rows are the encoder's bytes, so a different \
version is not guaranteed to decode them the same way"
),
Error::TimelineBackwards {
source_id,
ts,
floor,
} => write!(
f,
"source {source_id}: timestamp {ts} is not after the newest row its previous \
writer session left ({floor}); the clock went backwards across the restart, \
and rows would collide or run backwards"
),
Error::InUse { what } => write!(
f,
"{what} is held by another connection: an exclusive open needs the file to \
itself, and nothing can open a file an exclusive handle holds"
),
Error::NotAnArchive { what, reason } => {
write!(f, "{what}: not a dendro archive: {reason}")
}
Error::Encoder { stream, source } => {
write!(f, "failed to encode a {stream} segment: {source}")
}
Error::EncoderContract { stream, detail } => write!(
f,
"the encoder returned a segment for {stream} that does not \
describe the rows it was given: {detail}"
),
Error::Writer(e) => write!(f, "{e}"),
Error::WriterGone => write!(
f,
"the archive writer thread exited before the source finished"
),
Error::Sqlite { context, source } if context.is_empty() => write!(f, "{source}"),
Error::Sqlite { context, source } => write!(f, "{context}: {source}"),
Error::Message(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Encoder { source, .. } => Some(&**source),
Error::Writer(e) => Some(&**e),
Error::Sqlite { source, .. } => Some(source),
_ => None,
}
}
}
impl From<String> for Error {
fn from(m: String) -> Self {
Error::Message(m)
}
}
impl From<&str> for Error {
fn from(m: &str) -> Self {
Error::Message(m.to_string())
}
}
impl From<rusqlite::Error> for Error {
fn from(source: rusqlite::Error) -> Self {
Error::Sqlite {
context: String::new(),
source,
}
}
}
impl Error {
pub fn sqlite(context: impl Into<String>) -> impl FnOnce(rusqlite::Error) -> Error {
let context = context.into();
move |source| Error::Sqlite { context, source }
}
pub fn sqlite_code(&self) -> Option<rusqlite::ErrorCode> {
match self.root() {
Error::Sqlite {
source: rusqlite::Error::SqliteFailure(e, _),
..
} => Some(e.code),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
use rusqlite::ErrorCode::*;
matches!(
self.sqlite_code(),
Some(
DatabaseBusy
| DatabaseLocked
| DiskFull
| SystemIoFailure
| OutOfMemory
| OperationInterrupted
| SchemaChanged
)
)
}
pub fn is_constraint(&self) -> bool {
matches!(
self.sqlite_code(),
Some(rusqlite::ErrorCode::ConstraintViolation)
)
}
pub fn root(&self) -> &Error {
match self {
Error::Writer(inner) => inner.root(),
other => other,
}
}
}
impl From<Error> for String {
fn from(e: Error) -> String {
e.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
fn sqlite(code: i32) -> Error {
Error::from(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(code),
None,
))
}
#[test]
fn classifies_sqlite_result_codes() {
for code in [
rusqlite::ffi::SQLITE_BUSY,
rusqlite::ffi::SQLITE_LOCKED,
rusqlite::ffi::SQLITE_FULL,
rusqlite::ffi::SQLITE_IOERR,
rusqlite::ffi::SQLITE_NOMEM,
rusqlite::ffi::SQLITE_INTERRUPT,
rusqlite::ffi::SQLITE_SCHEMA,
] {
let e = sqlite(code);
assert!(e.is_retryable(), "{code}: {e}");
assert!(!e.is_constraint(), "{code}: {e}");
}
let constraint = sqlite(rusqlite::ffi::SQLITE_CONSTRAINT);
assert!(constraint.is_constraint() && !constraint.is_retryable());
let pk = sqlite(rusqlite::ffi::SQLITE_CONSTRAINT_PRIMARYKEY);
assert!(pk.is_constraint());
for code in [
rusqlite::ffi::SQLITE_CORRUPT,
rusqlite::ffi::SQLITE_READONLY,
rusqlite::ffi::SQLITE_MISUSE,
rusqlite::ffi::SQLITE_NOTADB,
] {
let e = sqlite(code);
assert!(!e.is_retryable() && !e.is_constraint(), "{code}: {e}");
}
assert!(!Error::Message("bad json".into()).is_retryable());
assert_eq!(Error::Message("x".into()).sqlite_code(), None);
}
#[test]
fn context_and_writer_wrapping_keep_the_code() {
let wrapped = Error::sqlite("inserting a row")(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
None,
));
assert!(wrapped.is_retryable());
assert!(
wrapped.to_string().starts_with("inserting a row: "),
"{wrapped}"
);
assert!(wrapped.source().is_some(), "the SQLite error is the source");
let via_writer = Error::Writer(std::sync::Arc::new(wrapped));
assert!(via_writer.is_retryable());
assert_eq!(
via_writer.sqlite_code(),
Some(rusqlite::ErrorCode::DatabaseBusy)
);
let bare = sqlite(rusqlite::ffi::SQLITE_FULL);
assert!(!bare.to_string().starts_with(": "), "{bare}");
}
}