use std::fmt; use thiserror::Error; /// Database error types covering all database operations #[derive(Error, Debug)] pub enum DatabaseError { /// Connection pool related errors #[error("Connection pool error: {message}")] ConnectionPool { message: String }, /// Connection establishment errors #[error("Connection failed: {message}")] Connection { message: String }, /// Query execution errors #[error("Query execution failed: {query} - {message}")] Query { query: String, message: String }, /// Transaction related errors #[error("Transaction error: {message}")] Transaction { message: String }, /// Configuration errors #[error("Configuration error: {message}")] Configuration { message: String }, /// Migration errors #[error("Migration error: {message}")] Migration { message: String }, /// Serialization/Deserialization errors #[error("Serialization error: {message}")] Serialization { message: String }, /// Timeout errors #[error("Operation timeout: {operation} exceeded {timeout_secs}s")] Timeout { operation: String, timeout_secs: u64, }, /// Validation errors #[error("Validation error: {field} - {message}")] Validation { field: String, message: String }, /// Resource not found errors #[error("Resource not found: {resource_type} with {identifier}")] NotFound { resource_type: String, identifier: String, }, /// Constraint violation errors #[error("Constraint violation: {constraint} - {message}")] ConstraintViolation { constraint: String, message: String }, /// Unknown/Generic errors #[error("Database error: {message}")] Unknown { message: String }, } impl DatabaseError { /// Check if this error is retryable (transient) pub fn is_retryable(&self) -> bool { match self { DatabaseError::ConnectionPool { .. } => true, DatabaseError::Connection { .. } => true, DatabaseError::Timeout { .. } => true, DatabaseError::Query { message, .. } => { // Check for specific PostgreSQL error codes that are retryable message.contains("connection") || message.contains("timeout") || message.contains("deadlock") || message.contains("serialization_failure") }, _ => false, } } /// Get error category for metrics/logging pub fn category(&self) -> &'static str { match self { DatabaseError::ConnectionPool { .. } => "connection_pool", DatabaseError::Connection { .. } => "connection", DatabaseError::Query { .. } => "query", DatabaseError::Transaction { .. } => "transaction", DatabaseError::Configuration { .. } => "configuration", DatabaseError::Migration { .. } => "migration", DatabaseError::Serialization { .. } => "serialization", DatabaseError::Timeout { .. } => "timeout", DatabaseError::Validation { .. } => "validation", DatabaseError::NotFound { .. } => "not_found", DatabaseError::ConstraintViolation { .. } => "constraint_violation", DatabaseError::Unknown { .. } => "unknown", } } /// Get error severity level pub fn severity(&self) -> ErrorSeverity { match self { DatabaseError::Configuration { .. } => ErrorSeverity::Critical, DatabaseError::Migration { .. } => ErrorSeverity::Critical, DatabaseError::ConnectionPool { .. } => ErrorSeverity::High, DatabaseError::Connection { .. } => ErrorSeverity::High, DatabaseError::Transaction { .. } => ErrorSeverity::High, DatabaseError::ConstraintViolation { .. } => ErrorSeverity::Medium, DatabaseError::Query { .. } => ErrorSeverity::Medium, DatabaseError::Timeout { .. } => ErrorSeverity::Medium, DatabaseError::Validation { .. } => ErrorSeverity::Low, DatabaseError::NotFound { .. } => ErrorSeverity::Low, DatabaseError::Serialization { .. } => ErrorSeverity::Low, DatabaseError::Unknown { .. } => ErrorSeverity::Medium, } } } /// Error severity levels #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorSeverity { Low, Medium, High, Critical, } impl fmt::Display for ErrorSeverity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorSeverity::Low => std::write!(f, "LOW"), ErrorSeverity::Medium => std::write!(f, "MEDIUM"), ErrorSeverity::High => std::write!(f, "HIGH"), ErrorSeverity::Critical => write!(f, "CRITICAL"), } } } /// Convert SQLx errors to our domain errors impl From for DatabaseError { fn from(err: sqlx::Error) -> Self { match err { sqlx::Error::Configuration(msg) => DatabaseError::Configuration { message: msg.to_string(), }, sqlx::Error::Database(db_err) => { let code = db_err.code().unwrap_or_default(); let message = db_err.message().to_string(); // PostgreSQL error codes match code.as_ref() { "23505" | "23503" | "23502" | "23514" => DatabaseError::ConstraintViolation { constraint: code.to_string(), message, }, "40001" | "40P01" => DatabaseError::Transaction { message: format!("Serialization failure: {}", message), }, _ => DatabaseError::Query { query: "unknown".to_string(), message, }, } }, sqlx::Error::Io(io_err) => DatabaseError::Connection { message: io_err.to_string(), }, sqlx::Error::Tls(tls_err) => DatabaseError::Connection { message: format!("TLS error: {}", tls_err), }, sqlx::Error::Protocol(msg) => DatabaseError::Connection { message: format!("Protocol error: {}", msg), }, sqlx::Error::RowNotFound => DatabaseError::NotFound { resource_type: "row".to_string(), identifier: "unknown".to_string(), }, sqlx::Error::TypeNotFound { type_name } => DatabaseError::Serialization { message: format!("Type not found: {}", type_name), }, sqlx::Error::ColumnIndexOutOfBounds { index, len } => DatabaseError::Query { query: "unknown".to_string(), message: format!("Column index {} out of bounds (len: {})", index, len), }, sqlx::Error::ColumnNotFound(col) => DatabaseError::Query { query: "unknown".to_string(), message: format!("Column not found: {}", col), }, sqlx::Error::ColumnDecode { index, source } => DatabaseError::Serialization { message: format!("Column decode error at index {}: {}", index, source), }, sqlx::Error::Decode(decode_err) => DatabaseError::Serialization { message: decode_err.to_string(), }, sqlx::Error::PoolTimedOut => DatabaseError::Timeout { operation: "pool_acquire".to_string(), timeout_secs: 30, // Default timeout }, sqlx::Error::PoolClosed => DatabaseError::ConnectionPool { message: "Connection pool is closed".to_string(), }, sqlx::Error::WorkerCrashed => DatabaseError::ConnectionPool { message: "Database worker crashed".to_string(), }, _ => DatabaseError::Unknown { message: err.to_string(), }, } } } /// Convert from serde_json errors impl From for DatabaseError { fn from(err: serde_json::Error) -> Self { DatabaseError::Serialization { message: err.to_string(), } } } /// Convert from config errors impl From for DatabaseError { fn from(err: config::error::ConfigError) -> Self { DatabaseError::Configuration { message: err.to_string(), } } } /// Result type alias for database operations pub type DatabaseResult = Result; /// Error context helper for adding additional information pub trait ErrorContext { fn with_context(self, context: &str) -> DatabaseResult; fn with_query_context(self, query: &str) -> DatabaseResult; } impl ErrorContext for DatabaseResult { fn with_context(self, context: &str) -> DatabaseResult { self.map_err(|err| match err { DatabaseError::Unknown { message } => DatabaseError::Unknown { message: format!("{}: {}", context, message), }, other => other, }) } fn with_query_context(self, query: &str) -> DatabaseResult { self.map_err(|err| match err { DatabaseError::Query { message, .. } => DatabaseError::Query { query: query.to_string(), message, }, DatabaseError::Unknown { message } => DatabaseError::Query { query: query.to_string(), message, }, other => other, }) } } /// Implement ErrorContext for Result impl ErrorContext for Result { fn with_context(self, context: &str) -> DatabaseResult { self.map_err(|err| { let db_err = DatabaseError::from(err); match db_err { DatabaseError::Unknown { message } => DatabaseError::Unknown { message: format!("{}: {}", context, message), }, other => other, } }) } fn with_query_context(self, query: &str) -> DatabaseResult { self.map_err(|err| { let db_err = DatabaseError::from(err); match db_err { DatabaseError::Query { message, .. } => DatabaseError::Query { query: query.to_string(), message, }, DatabaseError::Unknown { message } => DatabaseError::Query { query: query.to_string(), message, }, other => other, } }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_retryable() { let retryable = DatabaseError::Connection { message: "Connection refused".to_string(), }; assert!(retryable.is_retryable()); let not_retryable = DatabaseError::Validation { field: "email".to_string(), message: "Invalid format".to_string(), }; assert!(!not_retryable.is_retryable()); } #[test] fn test_error_severity() { let critical = DatabaseError::Configuration { message: "Invalid config".to_string(), }; assert_eq!(critical.severity(), ErrorSeverity::Critical); let low = DatabaseError::NotFound { resource_type: "user".to_string(), identifier: "123".to_string(), }; assert_eq!(low.severity(), ErrorSeverity::Low); } #[test] fn test_error_context() { let result: DatabaseResult = Err(DatabaseError::Unknown { message: "Something went wrong".to_string(), }); let with_context = result.with_context("User operation failed"); match with_context { Err(DatabaseError::Unknown { message }) => { assert!(message.contains("User operation failed")); }, _ => panic!("Expected Unknown error with context"), } } }