use std::fmt;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConstraintKind {
Unique,
ForeignKey,
NotNull,
Check,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Busy,
Constraint(ConstraintKind),
Connection,
PoolTimeout,
Decode,
Encode,
NotFound,
Misuse,
Other,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{kind:?}: {source}")]
Turso {
kind: ErrorKind,
#[source]
source: turso::Error,
},
#[cfg(feature = "serverless")]
#[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
#[error("{kind:?}: {source}")]
Remote {
kind: ErrorKind,
#[source]
source: turso_serverless::Error,
},
#[error("timed out waiting for a pooled connection")]
PoolTimeout,
#[error("cannot decode column {column} as {ty}: {reason}")]
Decode {
column: String,
ty: &'static str,
reason: String,
},
#[error("cannot encode value: {0}")]
Encode(String),
#[error("no row returned")]
NotFound,
#[error("invalid connect options: {0}")]
InvalidOptions(String),
#[error("misuse: {0}")]
Misuse(String),
#[error("{0}")]
Custom(String),
}
impl Error {
pub fn kind(&self) -> ErrorKind {
match self {
Error::Turso { kind, .. } => *kind,
#[cfg(feature = "serverless")]
Error::Remote { kind, .. } => *kind,
Error::PoolTimeout => ErrorKind::PoolTimeout,
Error::Decode { .. } => ErrorKind::Decode,
Error::Encode(_) => ErrorKind::Encode,
Error::NotFound => ErrorKind::NotFound,
Error::InvalidOptions(_) | Error::Misuse(_) => ErrorKind::Misuse,
Error::Custom(_) => ErrorKind::Other,
}
}
pub fn is_busy(&self) -> bool {
self.kind() == ErrorKind::Busy
}
pub fn constraint(&self) -> Option<ConstraintKind> {
match self.kind() {
ErrorKind::Constraint(k) => Some(k),
_ => None,
}
}
pub fn decode(column: impl fmt::Display, ty: &'static str, reason: impl fmt::Display) -> Self {
Error::Decode {
column: column.to_string(),
ty,
reason: reason.to_string(),
}
}
}
impl From<turso::Error> for Error {
fn from(source: turso::Error) -> Self {
let kind = classify(&source);
Error::Turso { kind, source }
}
}
fn classify(err: &turso::Error) -> ErrorKind {
match err {
turso::Error::Busy(_) | turso::Error::BusySnapshot(_) => ErrorKind::Busy,
turso::Error::Constraint(msg) => ErrorKind::Constraint(classify_constraint(msg)),
turso::Error::Misuse(_) => ErrorKind::Misuse,
turso::Error::Error(msg) if msg.to_ascii_lowercase().contains("conflict") => {
ErrorKind::Busy
}
turso::Error::IoError(..) | turso::Error::NotAdb(_) | turso::Error::Corrupt(_) => {
ErrorKind::Connection
}
_ => ErrorKind::Other,
}
}
#[cfg(feature = "serverless")]
impl From<turso_serverless::Error> for Error {
fn from(source: turso_serverless::Error) -> Self {
use turso_serverless::Error as E;
let kind = match &source {
E::Busy(_) | E::BusySnapshot(_) => ErrorKind::Busy,
E::Constraint(msg) => ErrorKind::Constraint(classify_constraint(msg)),
E::Misuse(_) => ErrorKind::Misuse,
E::Error(msg) if msg.to_ascii_lowercase().contains("conflict") => ErrorKind::Busy,
E::Http(_) | E::NotAdb(_) | E::Corrupt(_) => ErrorKind::Connection,
_ => ErrorKind::Other,
};
Error::Remote { kind, source }
}
}
fn classify_constraint(msg: &str) -> ConstraintKind {
let lower = msg.to_ascii_lowercase();
if lower.contains("unique") || lower.contains("primary key") {
ConstraintKind::Unique
} else if lower.contains("foreign key") {
ConstraintKind::ForeignKey
} else if lower.contains("not null") {
ConstraintKind::NotNull
} else if lower.contains("check") {
ConstraintKind::Check
} else {
ConstraintKind::Other
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies() {
let e: Error = turso::Error::Constraint("UNIQUE constraint failed: t.a".into()).into();
assert_eq!(e.constraint(), Some(ConstraintKind::Unique));
let e: Error = turso::Error::Busy("database is locked".into()).into();
assert!(e.is_busy());
let e: Error = turso::Error::Error("write-write conflict".into()).into();
assert!(e.is_busy());
let e: Error = turso::Error::Error("syntax error".into()).into();
assert_eq!(e.kind(), ErrorKind::Other);
}
}