**Progress: 1,178 → 57 test errors (95% reduction)** ## Status Summary - ✅ Production code: Compiles cleanly (0 errors) - ⚠️ Test code: 57 errors remain (massive improvement) - ⚙️ All services build successfully - 📊 Warning count: 253 (target: <20) - AGENTS WILL FIX ## Remaining Test Errors (57 total) ### Primary Issues: 1. 23× E0308 mismatched types 2. 17× E0433 undeclared Decimal 3. 15× E0433 compliance module not found 4. 6× E0624 private method access 5. Various import and type issues ## Next Phase: Wave 33-2 Launch 10+ parallel agents to: - Fix remaining 57 test compilation errors - Reduce 253 warnings to <20 - Achieve 95% test coverage - Ensure all tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
12 KiB
Rust
341 lines
12 KiB
Rust
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<sqlx::Error> 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<serde_json::Error> for DatabaseError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
DatabaseError::Serialization {
|
|
message: err.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert from config errors
|
|
impl From<config::error::ConfigError> for DatabaseError {
|
|
fn from(err: config::error::ConfigError) -> Self {
|
|
DatabaseError::Configuration {
|
|
message: err.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result type alias for database operations
|
|
pub type DatabaseResult<T> = Result<T, DatabaseError>;
|
|
|
|
/// Error context helper for adding additional information
|
|
pub trait ErrorContext<T> {
|
|
fn with_context(self, context: &str) -> DatabaseResult<T>;
|
|
fn with_query_context(self, query: &str) -> DatabaseResult<T>;
|
|
}
|
|
|
|
impl<T> ErrorContext<T> for DatabaseResult<T> {
|
|
fn with_context(self, context: &str) -> DatabaseResult<T> {
|
|
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<T> {
|
|
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<T, sqlx::Error>
|
|
impl<T> ErrorContext<T> for Result<T, sqlx::Error> {
|
|
fn with_context(self, context: &str) -> DatabaseResult<T> {
|
|
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<T> {
|
|
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<i32> = 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"),
|
|
}
|
|
}
|
|
}
|