Files
foxhunt/database/src/error.rs
jgrusewski 030a15ee05 🔧 Emergency Fix: Resolve catastrophic _i32 suffix corruption (463→0 errors)
- Fixed systematic array indexing corruption: [0_i32] → [0]
- Fixed numeric literal suffixes across 835 files
- Fixed iterator patterns on RwLockReadGuard (.iter() required)
- Fixed float type annotations (365.25_f64 for sqrt)
- Fixed missing semicolons in position manager
- Fixed reference dereferencing in data loader

Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices
Impact: Complete compilation failure (463 errors)
Resolution: Automated regex + targeted fixes
Result: 100% compilation success (0 errors)

Validated: cargo check --workspace passes
Ready for: Production deployment
2025-10-10 23:05:26 +02:00

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_u64, // 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"),
}
}
}