use std::fmt;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
NotFound,
TableMissing {
table: String,
},
UniqueViolation {
constraint: Option<String>,
},
ForeignKeyViolation {
constraint: Option<String>,
},
NotNullViolation {
constraint: Option<String>,
},
CheckViolation {
constraint: Option<String>,
},
Conflict,
Timeout,
Connection(String),
Decode(String),
Invalid(String),
Database {
code: Option<String>,
message: String,
},
Aborted(Box<dyn std::error::Error + Send + Sync>),
Hook(Box<dyn std::error::Error + Send + Sync>),
}
impl Error {
pub fn abort(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Error::Aborted(err.into())
}
pub fn is_retryable(&self) -> bool {
matches!(self, Error::Conflict)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let constraint =
|c: &Option<String>| c.as_deref().map(|c| format!(" ({c})")).unwrap_or_default();
match self {
Error::NotFound => write!(f, "row not found"),
Error::TableMissing { table } => write!(
f,
"table `{table}` does not exist — run `rango makemigrations` and `rango migrate`"
),
Error::UniqueViolation { constraint: c } => {
write!(f, "unique violation{}", constraint(c))
}
Error::ForeignKeyViolation { constraint: c } => {
write!(f, "foreign key violation{}", constraint(c))
}
Error::NotNullViolation { constraint: c } => {
write!(f, "not null violation{}", constraint(c))
}
Error::CheckViolation { constraint: c } => {
write!(f, "check violation{}", constraint(c))
}
Error::Conflict => write!(
f,
"transaction conflict (serialization failure or deadlock)"
),
Error::Timeout => write!(f, "timed out"),
Error::Connection(m) => write!(f, "connection error: {m}"),
Error::Decode(m) => write!(f, "decode error: {m}"),
Error::Invalid(m) => write!(f, "invalid: {m}"),
Error::Database {
code: Some(c),
message,
} => write!(f, "database error {c}: {message}"),
Error::Database {
code: None,
message,
} => write!(f, "database error: {message}"),
Error::Aborted(e) => write!(f, "aborted: {e}"),
Error::Hook(e) => write!(f, "hook failed: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Aborted(e) | Error::Hook(e) => Some(e.as_ref()),
_ => None,
}
}
}
impl From<crate::RowError> for Error {
fn from(e: crate::RowError) -> Self {
Error::Decode(e.to_string())
}
}
impl From<crate::FieldError> for Error {
fn from(e: crate::FieldError) -> Self {
Error::Invalid(e.0)
}
}
#[cfg(test)]
mod tests {
use super::Error;
#[test]
fn field_error_converts_with_question_mark() {
fn f() -> super::Result<crate::FieldVarchar<1, 3>> {
Ok(crate::FieldVarchar::new("too long")?)
}
assert!(matches!(f(), Err(Error::Invalid(m)) if m.contains("too long")));
}
#[test]
fn display_and_retryable() {
let e = Error::UniqueViolation {
constraint: Some("user_email_key".into()),
};
assert_eq!(e.to_string(), "unique violation (user_email_key)");
assert_eq!(
Error::TableMissing {
table: "user".into()
}
.to_string(),
"table `user` does not exist — run `rango makemigrations` and `rango migrate`"
);
assert!(Error::Conflict.is_retryable());
assert!(!Error::NotFound.is_retryable());
assert_eq!(Error::abort("no funds").to_string(), "aborted: no funds");
assert_eq!(Error::Hook("nope".into()).to_string(), "hook failed: nope");
}
}