use thiserror::Error;
#[derive(Debug, Error)]
pub enum SqlError {
#[error("ODBC error [{code}]: {message}")]
Odbc { code: i32, message: String },
#[error("Connection pool exhausted (timeout: {timeout_ms}ms)")]
PoolExhausted { timeout_ms: u64 },
#[error("Query timeout after {elapsed_ms}ms")]
QueryTimeout { elapsed_ms: u64 },
#[error("Operation cancelled")]
Cancelled,
#[error("Redis error: {0}")]
Cache(#[from] redis::RedisError),
#[error("Serialization error: {0}")]
Serialize(#[from] serde_json::Error),
#[error("Deadlock detected, retries exhausted ({attempts})")]
DeadlockRetryExhausted { attempts: u8 },
#[error("Transaction already committed or rolled back")]
InvalidTransactionState,
#[error("Config error: {0}")]
Config(String),
#[error("Row mapping error on column '{column}': {reason}")]
RowMapping {
column: String,
reason: String,
},
#[error("Not found: {table} with pk={pk}")]
NotFound { table: &'static str, pk: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl SqlError {
pub fn odbc(code: i32, message: impl Into<String>) -> Self {
Self::Odbc { code, message: message.into() }
}
pub fn config(msg: impl Into<String>) -> Self {
Self::Config(msg.into())
}
pub fn row_mapping(column: &'static str, reason: impl Into<String>) -> Self {
Self::RowMapping { column: column.to_string(), reason: reason.into() }
}
pub fn row_mapping_dynamic(column: impl Into<String>, reason: impl Into<String>) -> Self {
Self::RowMapping { column: column.into(), reason: reason.into() }
}
pub fn is_deadlock(&self) -> bool {
matches!(self, Self::Odbc { code: 1205, .. })
}
pub fn error_code(&self) -> &'static str {
match self {
Self::Odbc { .. } => "odbc_error",
Self::PoolExhausted { .. } => "pool_exhausted",
Self::QueryTimeout { .. } => "query_timeout",
Self::Cancelled => "cancelled",
Self::Cache(_) => "cache_error",
Self::Serialize(_) => "serialization_error",
Self::DeadlockRetryExhausted { .. } => "deadlock_retry_exhausted",
Self::InvalidTransactionState => "invalid_transaction_state",
Self::Config(_) => "config_error",
Self::RowMapping { .. } => "row_mapping_error",
Self::NotFound { .. } => "not_found",
Self::Io(_) => "io_error",
}
}
pub fn safe_message(&self) -> String {
match self {
Self::NotFound { table, pk } => format!("{table} with pk={pk} not found"),
Self::Cancelled => "Operation cancelled".to_string(),
Self::PoolExhausted { timeout_ms } => format!("Service busy, pool timeout after {timeout_ms}ms"),
Self::QueryTimeout { elapsed_ms } => format!("Query timed out after {elapsed_ms}ms"),
Self::DeadlockRetryExhausted { attempts} => format!("Deadlock retries exhausted ({attempts})"),
Self::InvalidTransactionState => "Invalid transaction state".to_string(),
_ => "An internal database error occurred".to_string(),
}
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for SqlError {
fn into_response(self) -> axum::response::Response {
use axum::http::StatusCode;
use axum::Json;
use serde_json::json;
let status = match &self {
SqlError::NotFound { .. } => StatusCode::NOT_FOUND,
SqlError::Cancelled => StatusCode::from_u16(499).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
SqlError::PoolExhausted { .. }
| SqlError::DeadlockRetryExhausted { .. } => StatusCode::SERVICE_UNAVAILABLE,
SqlError::QueryTimeout { .. } => StatusCode::GATEWAY_TIMEOUT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
let body = Json(json!({
"error_code": self.error_code(),
"message": self.safe_message(),
}));
(status, body).into_response()
}
}
#[cfg(feature = "tauri")]
impl serde::Serialize for SqlError {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("SqlError", 2)?;
s.serialize_field("error_code", self.error_code())?;
s.serialize_field("message", &self.safe_message())?;
s.end()
}
}