use std::error::Error;
use std::fmt;
use std::io;
use std::sync::{Arc, OnceLock, RwLock};
type ErrorHook = Arc<dyn Fn(&DbError) + Send + Sync>;
static GLOBAL_ERROR_HOOK: OnceLock<RwLock<Option<ErrorHook>>> = OnceLock::new();
fn error_hook_storage() -> &'static RwLock<Option<ErrorHook>> {
GLOBAL_ERROR_HOOK.get_or_init(|| RwLock::new(None))
}
pub fn set_error_hook(hook: ErrorHook) {
*error_hook_storage().write().unwrap() = Some(hook);
}
pub fn trigger_error_hook(err: &DbError) {
if let Ok(storage) = error_hook_storage().read() {
if let Some(ref hook) = *storage {
hook(err);
}
}
}
#[derive(Debug)]
pub enum DbError {
QueryError(String),
ConnectionError(String),
ConnectionRefused(String),
ConnectionTimeout(String),
PoolError(PoolError),
CacheError(CacheError),
TxError(TxError),
MigrationError(String),
Unsupported(String),
ConfigError(String),
SerdeError(String),
NotFound(String),
AlreadyExists(String),
ConstraintViolation(String),
UniqueViolation(String),
ForeignKeyViolation(String),
NullValue(String),
InvalidInput(String),
Internal(String),
IoError(String),
Hook(String),
TenantError(String),
Validation(String),
Contextual {
source: Box<DbError>,
context: ErrorContext,
},
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub context: String,
pub span: Option<String>,
pub previous: Option<Box<ErrorContext>>,
}
impl ErrorContext {
pub fn new(context: impl Into<String>) -> Self {
Self {
context: context.into(),
span: None,
previous: None,
}
}
pub fn with_previous(mut self, prev: ErrorContext) -> Self {
self.previous = Some(Box::new(prev));
self
}
pub fn with_span(mut self, span: impl Into<String>) -> Self {
self.span = Some(span.into());
self
}
pub fn iter(&self) -> impl Iterator<Item = &ErrorContext> {
let mut current = Some(self);
std::iter::from_fn(move || {
let node = current?;
let result = node;
current = node.previous.as_deref();
Some(result)
})
}
pub fn format_chain(&self) -> String {
self.iter()
.enumerate()
.map(|(i, ctx)| {
if let Some(ref span) = ctx.span {
format!(" [{}] {} (span: {})", i, ctx.context, span)
} else {
format!(" [{}] {}", i, ctx.context)
}
})
.collect::<Vec<_>>()
.join("\n")
}
}
impl DbError {
pub fn query(s: impl Into<String>) -> Self {
DbError::QueryError(s.into())
}
pub fn connection(s: impl Into<String>) -> Self {
DbError::ConnectionError(s.into())
}
pub fn not_found(s: impl Into<String>) -> Self {
DbError::NotFound(s.into())
}
pub fn with_context(self, context: impl Into<String>) -> Self {
let new_ctx = ErrorContext::new(context);
match self {
DbError::Contextual {
source,
context: existing_ctx,
} => {
let new_ctx = new_ctx.with_previous(existing_ctx);
DbError::Contextual {
source,
context: new_ctx,
}
}
other => DbError::Contextual {
source: Box::new(other),
context: new_ctx,
},
}
}
pub fn with_context_in_span(self, context: impl Into<String>, span: impl Into<String>) -> Self {
let new_ctx = ErrorContext::new(context).with_span(span);
match self {
DbError::Contextual {
source,
context: existing_ctx,
} => {
let new_ctx = new_ctx.with_previous(existing_ctx);
DbError::Contextual {
source,
context: new_ctx,
}
}
other => DbError::Contextual {
source: Box::new(other),
context: new_ctx,
},
}
}
pub fn context(&self) -> Option<&ErrorContext> {
match self {
DbError::Contextual { context, .. } => Some(context),
_ => None,
}
}
pub fn format_context_chain(&self) -> String {
match self {
DbError::Contextual { context, .. } => context.format_chain(),
_ => String::new(),
}
}
pub fn root_cause(&self) -> &DbError {
match self {
DbError::Contextual { source, .. } => source.root_cause(),
other => other,
}
}
pub fn is_retryable(&self) -> bool {
self.root_cause_is_retryable()
}
fn root_cause_is_retryable(&self) -> bool {
match self {
DbError::Contextual { source, .. } => source.root_cause_is_retryable(),
DbError::ConnectionError(_)
| DbError::ConnectionTimeout(_)
| DbError::PoolError(PoolError::Timeout) => true,
_ => false,
}
}
pub fn error_code(&self) -> &'static str {
match self {
DbError::Contextual { source, .. } => source.error_code(),
DbError::QueryError(_) => "DB001",
DbError::ConnectionError(_) => "DB002",
DbError::ConnectionRefused(_) => "DB003",
DbError::ConnectionTimeout(_) => "DB004",
DbError::PoolError(e) => e.error_code(),
DbError::CacheError(e) => e.error_code(),
DbError::TxError(_) => "DB007",
DbError::MigrationError(_) => "DB008",
DbError::Unsupported(_) => "DB009",
DbError::ConfigError(_) => "DB010",
DbError::SerdeError(_) => "DB011",
DbError::NotFound(_) => "DB012",
DbError::AlreadyExists(_) => "DB013",
DbError::ConstraintViolation(_) => "DB014",
DbError::UniqueViolation(_) => "DB022",
DbError::ForeignKeyViolation(_) => "DB023",
DbError::NullValue(_) => "DB015",
DbError::InvalidInput(_) => "DB016",
DbError::Internal(_) => "DB017",
DbError::IoError(_) => "DB018",
DbError::Hook(_) => "DB019",
DbError::TenantError(_) => "DB020",
DbError::Validation(_) => "DB021",
}
}
pub fn http_status(&self) -> u16 {
match self {
DbError::Contextual { source, .. } => source.http_status(),
DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 400,
DbError::NotFound(_) => 404,
DbError::AlreadyExists(_)
| DbError::ConstraintViolation(_)
| DbError::UniqueViolation(_)
| DbError::ForeignKeyViolation(_)
| DbError::NullValue(_) => 409,
DbError::SerdeError(_) => 422,
DbError::Unsupported(_) => 501,
DbError::ConnectionError(_) | DbError::ConnectionRefused(_) => 502,
DbError::ConnectionTimeout(_) => 504,
DbError::PoolError(e) => match e {
PoolError::Timeout => 504,
PoolError::Exhausted | PoolError::Closed | PoolError::ConnectionFailed(_) => 503,
_ => 500,
},
DbError::CacheError(_) => 503,
_ => 500,
}
}
pub fn grpc_status_code(&self) -> u32 {
match self {
DbError::Contextual { source, .. } => source.grpc_status_code(),
DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 3,
DbError::ConnectionTimeout(_) => 4,
DbError::PoolError(PoolError::Timeout) => 4,
DbError::NotFound(_) => 5,
DbError::AlreadyExists(_) | DbError::UniqueViolation(_) => 6,
DbError::TenantError(_) => 7,
DbError::PoolError(PoolError::Exhausted) | DbError::PoolError(PoolError::Closed) => 8,
DbError::CacheError(_) => 8,
DbError::ConstraintViolation(_)
| DbError::ForeignKeyViolation(_)
| DbError::NullValue(_)
| DbError::TxError(_) => 9,
DbError::Unsupported(_) => 12,
DbError::SerdeError(_) => 13,
DbError::ConnectionError(_)
| DbError::ConnectionRefused(_)
| DbError::PoolError(PoolError::ConnectionFailed(_)) => 14,
_ => 2,
}
}
}
impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DbError::QueryError(s) => write!(f, "Query error: {}", s),
DbError::ConnectionError(s) => write!(f, "Connection error: {}", s),
DbError::ConnectionRefused(s) => write!(f, "Connection refused: {}", s),
DbError::ConnectionTimeout(s) => write!(f, "Connection timeout: {}", s),
DbError::PoolError(e) => write!(f, "Pool error: {}", e),
DbError::CacheError(e) => write!(f, "Cache error: {}", e),
DbError::TxError(e) => write!(f, "Transaction error: {}", e),
DbError::MigrationError(s) => write!(f, "Migration error: {}", s),
DbError::Unsupported(s) => write!(f, "Unsupported: {}", s),
DbError::ConfigError(s) => write!(f, "Configuration error: {}", s),
DbError::SerdeError(s) => write!(f, "Serialization error: {}", s),
DbError::NotFound(s) => write!(f, "Not found: {}", s),
DbError::AlreadyExists(s) => write!(f, "Already exists: {}", s),
DbError::ConstraintViolation(s) => write!(f, "Constraint violation: {}", s),
DbError::UniqueViolation(s) => write!(f, "Unique constraint violation: {}", s),
DbError::ForeignKeyViolation(s) => write!(f, "Foreign key constraint violation: {}", s),
DbError::NullValue(s) => write!(f, "Null value: {}", s),
DbError::InvalidInput(s) => write!(f, "Invalid input: {}", s),
DbError::Internal(s) => write!(f, "Internal error: {}", s),
DbError::IoError(s) => write!(f, "IO error: {}", s),
DbError::Hook(s) => write!(f, "Hook error: {}", s),
DbError::TenantError(s) => write!(f, "Tenant error: {}", s),
DbError::Validation(s) => write!(f, "Validation error: {}", s),
DbError::Contextual {
context, source, ..
} => write!(f, "{}: {}", context.context, source),
}
}
}
impl Error for DbError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
DbError::PoolError(e) => Some(e),
DbError::CacheError(e) => Some(e),
DbError::TxError(e) => Some(e),
DbError::Contextual { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl From<io::Error> for DbError {
fn from(err: io::Error) -> Self {
DbError::IoError(err.to_string())
}
}
impl From<serde_json::Error> for DbError {
fn from(err: serde_json::Error) -> Self {
DbError::SerdeError(err.to_string())
}
}
impl From<std::num::TryFromIntError> for DbError {
fn from(err: std::num::TryFromIntError) -> Self {
DbError::Internal(err.to_string())
}
}
impl From<std::string::FromUtf8Error> for DbError {
fn from(err: std::string::FromUtf8Error) -> Self {
DbError::Internal(err.to_string())
}
}
impl<T> From<std::sync::PoisonError<T>> for DbError {
fn from(err: std::sync::PoisonError<T>) -> Self {
DbError::Internal(format!("RwLock/Mutex poisoned: {}", err))
}
}
#[derive(Debug)]
pub enum PoolError {
Exhausted,
Timeout,
AlreadyAcquired,
NotAcquired,
InvalidConfig(String),
Internal(String),
Closed,
ConnectionFailed(String),
CircuitOpen,
RateLimited { remaining: u64, reset_at: i64 },
}
impl PoolError {
pub fn error_code(&self) -> &'static str {
match self {
PoolError::Exhausted => "PL001",
PoolError::Timeout => "PL002",
PoolError::AlreadyAcquired => "PL003",
PoolError::NotAcquired => "PL004",
PoolError::InvalidConfig(_) => "PL005",
PoolError::Internal(_) => "PL006",
PoolError::Closed => "PL007",
PoolError::ConnectionFailed(_) => "PL008",
PoolError::CircuitOpen => "PL009",
PoolError::RateLimited { .. } => "PL010",
}
}
}
impl fmt::Display for PoolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PoolError::Exhausted => write!(f, "Connection pool exhausted"),
PoolError::Timeout => write!(f, "Connection acquire timeout"),
PoolError::AlreadyAcquired => write!(f, "Connection already acquired"),
PoolError::NotAcquired => write!(f, "Connection not acquired"),
PoolError::InvalidConfig(s) => write!(f, "Invalid pool config: {}", s),
PoolError::Internal(s) => write!(f, "Internal pool error: {}", s),
PoolError::Closed => write!(f, "Connection pool closed"),
PoolError::ConnectionFailed(s) => write!(f, "Connection failed: {}", s),
PoolError::CircuitOpen => write!(f, "Circuit breaker open"),
PoolError::RateLimited {
remaining,
reset_at,
} => write!(
f,
"Rate limited (remaining: {}, reset_at: {})",
remaining, reset_at
),
}
}
}
impl Error for PoolError {}
#[derive(Debug, Clone)]
pub enum CacheError {
NotFound(String),
SerializationError(String),
DeserializationError(String),
ConnectionError(String),
Timeout(String),
Internal(String),
}
impl CacheError {
pub fn error_code(&self) -> &'static str {
match self {
CacheError::NotFound(_) => "CH001",
CacheError::SerializationError(_) => "CH002",
CacheError::DeserializationError(_) => "CH003",
CacheError::ConnectionError(_) => "CH004",
CacheError::Timeout(_) => "CH005",
CacheError::Internal(_) => "CH006",
}
}
}
impl fmt::Display for CacheError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CacheError::NotFound(s) => write!(f, "Cache key not found: {}", s),
CacheError::SerializationError(s) => write!(f, "Cache serialization error: {}", s),
CacheError::DeserializationError(s) => write!(f, "Cache deserialization error: {}", s),
CacheError::ConnectionError(s) => write!(f, "Cache connection error: {}", s),
CacheError::Timeout(s) => write!(f, "Cache timeout: {}", s),
CacheError::Internal(s) => write!(f, "Cache internal error: {}", s),
}
}
}
impl Error for CacheError {}
impl<T> From<std::sync::PoisonError<T>> for CacheError {
fn from(err: std::sync::PoisonError<T>) -> Self {
CacheError::Internal(format!("RwLock poisoned: {}", err))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TransactionState {
#[default]
Active,
Committed,
RolledBack,
}
impl fmt::Display for TransactionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransactionState::Active => write!(f, "Active"),
TransactionState::Committed => write!(f, "Committed"),
TransactionState::RolledBack => write!(f, "RolledBack"),
}
}
}
#[derive(Debug)]
pub enum TxError {
NotStarted,
AlreadyStarted,
CommitFailed(String),
RollbackFailed(String),
SavepointError(String),
NestedNotSupported,
NotActive(TransactionState),
InvalidSavepointName(String),
ConnectionTaken,
MaxNestingDepthExceeded { current_depth: u32, max_depth: u32 },
DeadlockDetected { attempt: u32, max_attempts: u32 },
}
impl fmt::Display for TxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TxError::NotStarted => write!(f, "Transaction not started"),
TxError::AlreadyStarted => write!(f, "Transaction already started"),
TxError::CommitFailed(s) => write!(f, "Transaction commit failed: {}", s),
TxError::RollbackFailed(s) => write!(f, "Transaction rollback failed: {}", s),
TxError::SavepointError(s) => write!(f, "Savepoint error: {}", s),
TxError::NestedNotSupported => write!(f, "Nested transactions not supported"),
TxError::NotActive(state) => {
write!(f, "Transaction not active (current state: {})", state)
}
TxError::InvalidSavepointName(name) => {
write!(
f,
"Invalid savepoint name '{}': must be non-empty, start with a letter or underscore, and contain only ASCII alphanumeric or underscore",
name
)
}
TxError::ConnectionTaken => write!(f, "Transaction connection already taken"),
TxError::MaxNestingDepthExceeded {
current_depth,
max_depth,
} => write!(
f,
"Transaction nesting depth {} exceeds maximum allowed {}",
current_depth, max_depth
),
TxError::DeadlockDetected {
attempt,
max_attempts,
} => write!(
f,
"Deadlock detected on attempt {} of {}",
attempt, max_attempts
),
}
}
}
impl Error for TxError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_error_display() {
let err = DbError::query("test");
assert_eq!(format!("{}", err), "Query error: test");
let err = DbError::not_found("user");
assert_eq!(format!("{}", err), "Not found: user");
}
#[test]
fn test_db_error_code() {
let err = DbError::query("test");
assert_eq!(err.error_code(), "DB001");
let err = DbError::PoolError(PoolError::Timeout);
assert_eq!(err.error_code(), "PL002");
}
#[test]
fn test_db_error_source() {
let err = DbError::PoolError(PoolError::Timeout);
assert!(err.source().is_some());
}
#[test]
fn test_db_error_contextual_source_chain() {
let root = DbError::QueryError("table not found".to_string());
let wrapped = root.with_context("fetching user");
let outer = wrapped.with_context("user_service.fetch");
let source1 = outer.source().expect("outer should have source");
assert!(source1.source().is_none());
let ctx_chain = outer.context().expect("outer should have context");
assert_eq!(ctx_chain.context, "user_service.fetch");
let inner_ctx = ctx_chain
.previous
.as_ref()
.expect("should have previous context");
assert_eq!(inner_ctx.context, "fetching user");
assert!(inner_ctx.previous.is_none());
let root_cause = outer.root_cause();
assert!(matches!(root_cause, DbError::QueryError(_)));
}
#[test]
fn test_pool_error() {
let err = PoolError::Timeout;
assert_eq!(format!("{}", err), "Connection acquire timeout");
assert_eq!(err.error_code(), "PL002");
}
#[test]
fn test_cache_error() {
let err = CacheError::NotFound("key".to_string());
assert_eq!(format!("{}", err), "Cache key not found: key");
assert_eq!(err.error_code(), "CH001");
}
#[test]
fn test_error_hook_set_and_trigger() {
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let c = counter.clone();
set_error_hook(Arc::new(move |_err: &DbError| {
c.fetch_add(1, Ordering::SeqCst);
}));
let err = DbError::query("hook test");
trigger_error_hook(&err);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn test_error_hook_no_hook_silent() {
let err = DbError::query("no hook");
trigger_error_hook(&err);
}
#[test]
fn test_http_status_bad_request() {
assert_eq!(DbError::InvalidInput("bad".into()).http_status(), 400);
assert_eq!(DbError::Validation("fail".into()).http_status(), 400);
assert_eq!(DbError::ConfigError("cfg".into()).http_status(), 400);
}
#[test]
fn test_http_status_not_found() {
assert_eq!(DbError::NotFound("user".into()).http_status(), 404);
}
#[test]
fn test_http_status_conflict() {
assert_eq!(DbError::AlreadyExists("x".into()).http_status(), 409);
assert_eq!(DbError::ConstraintViolation("c".into()).http_status(), 409);
assert_eq!(DbError::UniqueViolation("u".into()).http_status(), 409);
assert_eq!(DbError::ForeignKeyViolation("f".into()).http_status(), 409);
assert_eq!(DbError::NullValue("n".into()).http_status(), 409);
}
#[test]
fn test_http_status_unprocessable() {
assert_eq!(DbError::SerdeError("s".into()).http_status(), 422);
}
#[test]
fn test_http_status_internal_server_error() {
assert_eq!(DbError::QueryError("q".into()).http_status(), 500);
assert_eq!(DbError::Internal("i".into()).http_status(), 500);
assert_eq!(DbError::Hook("h".into()).http_status(), 500);
assert_eq!(DbError::MigrationError("m".into()).http_status(), 500);
assert_eq!(DbError::IoError("io".into()).http_status(), 500);
assert_eq!(DbError::TxError(TxError::NotStarted).http_status(), 500);
assert_eq!(DbError::TenantError("t".into()).http_status(), 500);
}
#[test]
fn test_http_status_not_implemented() {
assert_eq!(DbError::Unsupported("feat".into()).http_status(), 501);
}
#[test]
fn test_http_status_bad_gateway() {
assert_eq!(DbError::ConnectionError("c".into()).http_status(), 502);
assert_eq!(DbError::ConnectionRefused("r".into()).http_status(), 502);
}
#[test]
fn test_http_status_service_unavailable() {
assert_eq!(DbError::PoolError(PoolError::Exhausted).http_status(), 503);
assert_eq!(DbError::PoolError(PoolError::Closed).http_status(), 503);
assert_eq!(
DbError::PoolError(PoolError::ConnectionFailed("f".into())).http_status(),
503
);
assert_eq!(
DbError::CacheError(CacheError::Internal("e".into())).http_status(),
503
);
}
#[test]
fn test_http_status_gateway_timeout() {
assert_eq!(DbError::ConnectionTimeout("t".into()).http_status(), 504);
assert_eq!(DbError::PoolError(PoolError::Timeout).http_status(), 504);
}
#[test]
fn test_grpc_status_invalid_argument() {
assert_eq!(DbError::InvalidInput("bad".into()).grpc_status_code(), 3);
assert_eq!(DbError::Validation("fail".into()).grpc_status_code(), 3);
assert_eq!(DbError::ConfigError("cfg".into()).grpc_status_code(), 3);
}
#[test]
fn test_grpc_status_deadline_exceeded() {
assert_eq!(DbError::ConnectionTimeout("t".into()).grpc_status_code(), 4);
assert_eq!(DbError::PoolError(PoolError::Timeout).grpc_status_code(), 4);
}
#[test]
fn test_grpc_status_not_found() {
assert_eq!(DbError::NotFound("user".into()).grpc_status_code(), 5);
}
#[test]
fn test_grpc_status_already_exists() {
assert_eq!(DbError::AlreadyExists("x".into()).grpc_status_code(), 6);
assert_eq!(DbError::UniqueViolation("u".into()).grpc_status_code(), 6);
}
#[test]
fn test_grpc_status_permission_denied() {
assert_eq!(DbError::TenantError("t".into()).grpc_status_code(), 7);
}
#[test]
fn test_grpc_status_resource_exhausted() {
assert_eq!(
DbError::PoolError(PoolError::Exhausted).grpc_status_code(),
8
);
assert_eq!(DbError::PoolError(PoolError::Closed).grpc_status_code(), 8);
assert_eq!(
DbError::CacheError(CacheError::Internal("e".into())).grpc_status_code(),
8
);
}
#[test]
fn test_grpc_status_failed_precondition() {
assert_eq!(
DbError::ConstraintViolation("c".into()).grpc_status_code(),
9
);
assert_eq!(
DbError::ForeignKeyViolation("f".into()).grpc_status_code(),
9
);
assert_eq!(DbError::NullValue("n".into()).grpc_status_code(), 9);
assert_eq!(DbError::TxError(TxError::NotStarted).grpc_status_code(), 9);
}
#[test]
fn test_grpc_status_unimplemented() {
assert_eq!(DbError::Unsupported("feat".into()).grpc_status_code(), 12);
}
#[test]
fn test_grpc_status_internal() {
assert_eq!(DbError::SerdeError("s".into()).grpc_status_code(), 13);
}
#[test]
fn test_grpc_status_unavailable() {
assert_eq!(DbError::ConnectionError("c".into()).grpc_status_code(), 14);
assert_eq!(
DbError::ConnectionRefused("r".into()).grpc_status_code(),
14
);
assert_eq!(
DbError::PoolError(PoolError::ConnectionFailed("f".into())).grpc_status_code(),
14
);
}
#[test]
fn test_grpc_status_unknown() {
assert_eq!(DbError::QueryError("q".into()).grpc_status_code(), 2);
assert_eq!(DbError::Internal("i".into()).grpc_status_code(), 2);
assert_eq!(DbError::Hook("h".into()).grpc_status_code(), 2);
assert_eq!(DbError::MigrationError("m".into()).grpc_status_code(), 2);
assert_eq!(DbError::IoError("io".into()).grpc_status_code(), 2);
assert_eq!(
DbError::PoolError(PoolError::AlreadyAcquired).grpc_status_code(),
2
);
assert_eq!(
DbError::PoolError(PoolError::NotAcquired).grpc_status_code(),
2
);
assert_eq!(
DbError::PoolError(PoolError::InvalidConfig("x".into())).grpc_status_code(),
2
);
assert_eq!(
DbError::PoolError(PoolError::Internal("y".into())).grpc_status_code(),
2
);
}
}