use std::fmt;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCategory {
Connection,
Query,
Transaction,
Constraint,
TypeConversion,
Timeout,
Deadlock,
Authentication,
Configuration,
PoolExhausted,
Schema,
Other,
}
impl ErrorCategory {
#[inline]
pub const fn is_retriable(self) -> bool {
matches!(
self,
Self::Connection | Self::Timeout | Self::Deadlock | Self::PoolExhausted
)
}
}
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
#[error("connection error: {message}")]
Connection {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("query error: {message}")]
Query {
message: String,
sql: Option<String>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("transaction error: {message}")]
Transaction {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("constraint violation: {constraint_name} - {message}")]
Constraint {
constraint_name: String,
message: String,
},
#[error("type conversion error: {message}")]
TypeConversion { message: String },
#[error("timeout: {message}")]
Timeout { message: String },
#[error("deadlock detected")]
Deadlock,
#[error("authentication failed: {message}")]
Authentication { message: String },
#[error("configuration error: {message}")]
Configuration { message: String },
#[error("pool exhausted: {message}")]
PoolExhausted { message: String },
#[error("schema error: {message}")]
Schema { message: String },
#[error("prepared statement not found: {name}")]
PreparedStatementNotFound { name: String },
#[error("table not found: {table}")]
TableNotFound { table: String },
#[error("column not found: {column} in table {table}")]
ColumnNotFound { table: String, column: String },
#[error("unsupported: {message}")]
Unsupported { message: String },
#[error("internal error: {message}")]
Internal { message: String },
}
impl Error {
pub fn category(&self) -> ErrorCategory {
match self {
Self::Connection { .. } => ErrorCategory::Connection,
Self::Query { .. } => ErrorCategory::Query,
Self::Transaction { .. } => ErrorCategory::Transaction,
Self::Constraint { .. } => ErrorCategory::Constraint,
Self::TypeConversion { .. } => ErrorCategory::TypeConversion,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::Deadlock => ErrorCategory::Deadlock,
Self::Authentication { .. } => ErrorCategory::Authentication,
Self::Configuration { .. } => ErrorCategory::Configuration,
Self::PoolExhausted { .. } => ErrorCategory::PoolExhausted,
Self::Schema { .. } | Self::TableNotFound { .. } | Self::ColumnNotFound { .. } => {
ErrorCategory::Schema
}
Self::PreparedStatementNotFound { .. } => ErrorCategory::Query,
Self::Unsupported { .. } | Self::Internal { .. } => ErrorCategory::Other,
}
}
#[inline]
pub fn is_retriable(&self) -> bool {
self.category().is_retriable()
}
pub fn connection(message: impl Into<String>) -> Self {
Self::Connection {
message: message.into(),
source: None,
}
}
pub fn connection_with_source(
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Connection {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn query(message: impl Into<String>) -> Self {
Self::Query {
message: message.into(),
sql: None,
source: None,
}
}
pub fn query_with_sql(message: impl Into<String>, sql: impl Into<String>) -> Self {
Self::Query {
message: message.into(),
sql: Some(sql.into()),
source: None,
}
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::Timeout {
message: message.into(),
}
}
pub fn config(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
}
}
pub fn type_conversion(message: impl Into<String>) -> Self {
Self::TypeConversion {
message: message.into(),
}
}
pub fn schema(message: impl Into<String>) -> Self {
Self::Schema {
message: message.into(),
}
}
pub fn transaction(message: impl Into<String>) -> Self {
Self::Transaction {
message: message.into(),
source: None,
}
}
pub fn execution(message: impl Into<String>) -> Self {
Self::Query {
message: message.into(),
sql: None,
source: None,
}
}
pub fn unsupported(message: impl Into<String>) -> Self {
Self::Unsupported {
message: message.into(),
}
}
}
impl fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Connection => write!(f, "connection"),
Self::Query => write!(f, "query"),
Self::Transaction => write!(f, "transaction"),
Self::Constraint => write!(f, "constraint"),
Self::TypeConversion => write!(f, "type_conversion"),
Self::Timeout => write!(f, "timeout"),
Self::Deadlock => write!(f, "deadlock"),
Self::Authentication => write!(f, "authentication"),
Self::Configuration => write!(f, "configuration"),
Self::PoolExhausted => write!(f, "pool_exhausted"),
Self::Schema => write!(f, "schema"),
Self::Other => write!(f, "other"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_category_retriable() {
assert!(ErrorCategory::Connection.is_retriable());
assert!(ErrorCategory::Timeout.is_retriable());
assert!(ErrorCategory::Deadlock.is_retriable());
assert!(ErrorCategory::PoolExhausted.is_retriable());
assert!(!ErrorCategory::Constraint.is_retriable());
assert!(!ErrorCategory::TypeConversion.is_retriable());
assert!(!ErrorCategory::Query.is_retriable());
}
#[test]
fn test_error_is_retriable() {
assert!(Error::connection("failed").is_retriable());
assert!(Error::timeout("timed out").is_retriable());
assert!(Error::Deadlock.is_retriable());
assert!(!Error::Constraint {
constraint_name: "pk".into(),
message: "duplicate".into()
}
.is_retriable());
}
#[test]
fn test_error_display() {
let err = Error::connection("connection refused");
assert!(err.to_string().contains("connection refused"));
let err = Error::query_with_sql("syntax error", "SELECT * FORM users");
assert!(err.to_string().contains("syntax error"));
}
}