use thiserror::Error;
impl From<sea_orm::DbErr> for Error {
fn from(err: sea_orm::DbErr) -> Self {
crate::internal::translate_error(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::Internal {
message: err.to_string(),
}
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Conversion {
message: err.to_string(),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("Record not found: {message}")]
NotFound {
message: String,
context: Option<Box<ErrorContext>>,
},
#[error("Connection error: {message}")]
Connection {
message: String,
},
#[error("Query error: {message}")]
Query {
message: String,
context: Option<Box<ErrorContext>>,
},
#[error("Validation error: {field} - {message}")]
Validation {
field: String,
message: String,
},
#[error("Conversion error: {message}")]
Conversion {
message: String,
},
#[error("Transaction error: {message}")]
Transaction {
message: String,
},
#[error("Configuration error: {message}")]
Configuration {
message: String,
},
#[error("Internal error: {message}")]
Internal {
message: String,
},
#[error("Backend not supported: {message}")]
BackendNotSupported {
message: String,
backend: String,
},
#[error("Primary key not set: {message}")]
PrimaryKeyNotSet {
message: String,
model: String,
},
#[error("Insert returning not supported: {message}")]
InsertReturningNotSupported {
message: String,
backend: String,
},
#[error("Tokenization error: {message}")]
Tokenization {
message: String,
},
#[error("Invalid token: {message}")]
InvalidToken {
message: String,
},
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub table: Option<String>,
pub column: Option<String>,
pub conditions: Vec<String>,
pub operator_chain: Option<String>,
pub query: Option<String>,
}
impl std::fmt::Display for ErrorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
if let Some(ref table) = self.table {
parts.push(format!("table: {}", table));
}
if let Some(ref column) = self.column {
parts.push(format!("column: {}", column));
}
if !self.conditions.is_empty() {
parts.push(format!("conditions: {}", self.conditions.join(" | ")));
}
if let Some(ref operator_chain) = self.operator_chain {
parts.push(format!("operator_chain: {}", operator_chain));
}
if let Some(ref query) = self.query {
parts.push(format!("query: {}", query));
}
write!(f, "{}", parts.join(", "))
}
}
impl ErrorContext {
pub fn new() -> Self {
Self {
table: None,
column: None,
conditions: Vec::new(),
operator_chain: None,
query: None,
}
}
pub fn table(mut self, table: impl Into<String>) -> Self {
self.table = Some(table.into());
self
}
pub fn column(mut self, column: impl Into<String>) -> Self {
self.column = Some(column.into());
self
}
pub fn condition(mut self, condition: impl Into<String>) -> Self {
self.conditions.push(condition.into());
self
}
pub fn conditions(mut self, conditions: Vec<String>) -> Self {
self.conditions = conditions;
self
}
pub fn operator_chain(mut self, operator_chain: impl Into<String>) -> Self {
self.operator_chain = Some(operator_chain.into());
self
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
}
impl Default for ErrorContext {
fn default() -> Self {
Self::new()
}
}
impl Error {
pub fn not_found(message: impl Into<String>) -> Self {
Self::NotFound {
message: message.into(),
context: None,
}
}
pub fn not_found_with_context(message: impl Into<String>, context: ErrorContext) -> Self {
Self::NotFound {
message: message.into(),
context: Some(Box::new(context)),
}
}
pub fn connection(message: impl Into<String>) -> Self {
Self::Connection {
message: message.into(),
}
}
pub fn query(message: impl Into<String>) -> Self {
Self::Query {
message: message.into(),
context: None,
}
}
pub fn query_with_context(message: impl Into<String>, context: ErrorContext) -> Self {
Self::Query {
message: message.into(),
context: Some(Box::new(context)),
}
}
pub fn validation(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::Validation {
field: field.into(),
message: message.into(),
}
}
pub fn conversion(message: impl Into<String>) -> Self {
Self::Conversion {
message: message.into(),
}
}
pub fn transaction(message: impl Into<String>) -> Self {
Self::Transaction {
message: message.into(),
}
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn backend_not_supported(message: impl Into<String>, backend: impl Into<String>) -> Self {
Self::BackendNotSupported {
message: message.into(),
backend: backend.into(),
}
}
pub fn primary_key_not_set(message: impl Into<String>, model: impl Into<String>) -> Self {
Self::PrimaryKeyNotSet {
message: message.into(),
model: model.into(),
}
}
pub fn insert_returning_not_supported(
message: impl Into<String>,
backend: impl Into<String>,
) -> Self {
Self::InsertReturningNotSupported {
message: message.into(),
backend: backend.into(),
}
}
pub fn tokenization(message: impl Into<String>) -> Self {
Self::Tokenization {
message: message.into(),
}
}
pub fn invalid_token(message: impl Into<String>) -> Self {
Self::InvalidToken {
message: message.into(),
}
}
pub fn invalid_query(message: impl Into<String>) -> Self {
Self::Query {
message: message.into(),
context: None,
}
}
pub fn context(&self) -> Option<&ErrorContext> {
match self {
Self::NotFound { context, .. } => context.as_deref(),
Self::Query { context, .. } => context.as_deref(),
_ => None,
}
}
pub fn with_context(self, ctx: ErrorContext) -> Self {
match self {
Self::NotFound { message, .. } => Self::NotFound {
message,
context: Some(Box::new(ctx)),
},
Self::Query { message, .. } => Self::Query {
message,
context: Some(Box::new(ctx)),
},
other => other,
}
}
pub fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound { .. })
}
pub fn is_connection_error(&self) -> bool {
matches!(self, Self::Connection { .. })
}
pub fn is_validation_error(&self) -> bool {
matches!(self, Self::Validation { .. })
}
pub fn is_query_error(&self) -> bool {
matches!(self, Self::Query { .. })
}
pub fn is_transaction_error(&self) -> bool {
matches!(self, Self::Transaction { .. })
}
pub fn is_configuration_error(&self) -> bool {
matches!(self, Self::Configuration { .. })
}
pub fn is_backend_not_supported(&self) -> bool {
matches!(self, Self::BackendNotSupported { .. })
}
pub fn is_primary_key_not_set(&self) -> bool {
matches!(self, Self::PrimaryKeyNotSet { .. })
}
pub fn is_insert_returning_not_supported(&self) -> bool {
matches!(self, Self::InsertReturningNotSupported { .. })
}
pub fn suggestion(&self) -> String {
match self {
Self::NotFound { message, .. } => {
if message.contains("find") || message.contains("Find") {
"Check that the ID exists. Use `Model::exists(id).await?` to verify before find.".to_string()
} else {
"Verify the record exists and hasn't been soft-deleted. Use `.with_trashed()` to include deleted records.".to_string()
}
}
Self::Connection { message } => {
if message.contains("refused") || message.contains("Refused") {
"Database server is not running or not accepting connections. Check that:\n\
1. The database server is running\n\
2. The host and port are correct\n\
3. Firewall allows the connection".to_string()
} else if message.contains("password") || message.contains("authentication") {
"Check your database credentials in the connection URL.".to_string()
} else if message.contains("does not exist") || message.contains("unknown database") {
"The database doesn't exist. Create it first: CREATE DATABASE dbname;".to_string()
} else if message.contains("timeout") || message.contains("Timeout") {
"Connection timed out. Check network connectivity and increase `connect_timeout` if needed.".to_string()
} else if message.contains("pool") || message.contains("Pool") {
"Connection pool exhausted. Consider:\n\
1. Increasing `max_connections` in TideConfig\n\
2. Reducing connection hold time\n\
3. Using `acquire_timeout` to wait for connections".to_string()
} else {
"Verify your database URL format: postgres://user:pass@host:5432/database".to_string()
}
}
Self::Query { message, context } => {
let base_suggestion = if message.contains("syntax") || message.contains("Syntax") {
"SQL syntax error. Check column names and query structure."
} else if message.contains("duplicate") || message.contains("unique") {
"Duplicate key violation. The value already exists in a unique column."
} else if message.contains("foreign key") || message.contains("violates foreign key") {
"Foreign key constraint violation. The referenced record doesn't exist or can't be deleted."
} else if message.contains("null") || message.contains("NOT NULL") {
"NULL value not allowed. Ensure all required fields are provided."
} else if message.contains("column") && message.contains("does not exist") {
"Column doesn't exist. Check spelling and run migrations if needed."
} else if message.contains("table") && message.contains("does not exist") {
"Table doesn't exist. Run migrations: `TideConfig::init().run_migrations(true).connect().await?`"
} else if message.contains("permission") || message.contains("denied") {
"Permission denied. Check database user privileges."
} else if message.contains("deadlock") {
"Deadlock detected. Retry the transaction or review query ordering."
} else {
"Check the SQL query and ensure all referenced columns/tables exist."
};
if let Some(ctx) = context {
if let Some(ref query) = ctx.query {
format!("{}\n\nQuery: {}", base_suggestion, query)
} else {
base_suggestion.to_string()
}
} else {
base_suggestion.to_string()
}
}
Self::Validation { field, message: _ } => {
format!("Validate the '{}' field before saving. Use Model::validate() for custom validation.", field)
}
Self::Conversion { message } => {
if message.contains("type") {
"Type mismatch. Check that Rust types match database column types.".to_string()
} else {
"Data conversion failed. Verify the data format matches expected type.".to_string()
}
}
Self::Transaction { message } => {
if message.contains("timeout") {
"Transaction timed out. Split into smaller transactions or increase timeout.".to_string()
} else if message.contains("rollback") || message.contains("aborted") {
"Transaction was rolled back. Check for errors in transaction body.".to_string()
} else {
"Transaction failed. Ensure all operations in the transaction are valid.".to_string()
}
}
Self::Configuration { message } => {
if message.contains("initialized") || message.contains("not set") {
"Database not initialized. Call `TideConfig::init().database(url).connect().await?` first.".to_string()
} else if message.contains("already") {
"Configuration already set. TideConfig::init() should only be called once.".to_string()
} else {
format!("Check your TideConfig settings: {}", message)
}
}
Self::Internal { .. } => {
"Internal error. Please report this issue at https://github.com/mohamadzoh/tideorm/issues".to_string()
}
Self::BackendNotSupported { backend, message } => {
format!(
"Operation not supported on {} backend. {}\n\
Consider using a database-agnostic approach or checking backend with `db.backend()`.",
backend, message
)
}
Self::PrimaryKeyNotSet { model, .. } => {
format!(
"Set the primary key on your {} instance before this operation.\n\
Use `Model::find(id)` to load an existing record, or ensure auto-increment is configured.",
model
)
}
Self::InsertReturningNotSupported { backend, .. } => {
format!(
"{} does not support INSERT ... RETURNING syntax.\n\
Options:\n\
1. Use separate insert() and find() calls\n\
2. For MySQL, use last_insert_id() after insert\n\
3. Consider using PostgreSQL which supports RETURNING",
backend
)
}
Self::Tokenization { message } => {
format!(
"Tokenization failed: {}\n\
Ensure:\n\
1. An encryption key is configured via TideConfig::encryption_key()\n\
2. The model has tokenization enabled via #[tideorm(tokenize)]\n\
3. The record has a valid primary key",
message
)
}
Self::InvalidToken { message } => {
format!(
"Invalid token: {}\n\
Possible causes:\n\
1. Token was tampered with or corrupted\n\
2. Token is for a different model type\n\
3. Encryption key has changed since token was created",
message
)
}
}
}
pub fn code(&self) -> &'static str {
match self {
Self::NotFound { .. } => "TIDE_NOT_FOUND",
Self::Connection { .. } => "TIDE_CONNECTION",
Self::Query { .. } => "TIDE_QUERY",
Self::Validation { .. } => "TIDE_VALIDATION",
Self::Conversion { .. } => "TIDE_CONVERSION",
Self::Transaction { .. } => "TIDE_TRANSACTION",
Self::Configuration { .. } => "TIDE_CONFIG",
Self::Internal { .. } => "TIDE_INTERNAL",
Self::BackendNotSupported { .. } => "TIDE_BACKEND_NOT_SUPPORTED",
Self::PrimaryKeyNotSet { .. } => "TIDE_PRIMARY_KEY_NOT_SET",
Self::InsertReturningNotSupported { .. } => "TIDE_INSERT_RETURNING_NOT_SUPPORTED",
Self::Tokenization { .. } => "TIDE_TOKENIZATION",
Self::InvalidToken { .. } => "TIDE_INVALID_TOKEN",
}
}
pub fn http_status(&self) -> u16 {
match self {
Self::NotFound { .. } => 404,
Self::Connection { .. } => 503, Self::Query { .. } => 400, Self::Validation { .. } => 422, Self::Conversion { .. } => 400,
Self::Transaction { .. } => 409, Self::Configuration { .. } => 500,
Self::Internal { .. } => 500,
Self::BackendNotSupported { .. } => 501, Self::PrimaryKeyNotSet { .. } => 400, Self::InsertReturningNotSupported { .. } => 501, Self::Tokenization { .. } => 400, Self::InvalidToken { .. } => 401, }
}
pub fn is_retryable(&self) -> bool {
match self {
Self::Connection { message } => {
message.contains("timeout")
|| message.contains("pool")
|| message.contains("refused")
}
Self::Query { message, .. } => {
message.contains("deadlock")
|| message.contains("lock")
|| message.contains("timeout")
}
Self::Transaction { message } => {
message.contains("deadlock")
|| message.contains("timeout")
|| message.contains("serialization")
}
_ => false,
}
}
pub fn log_format(&self) -> String {
let mut output = format!("[{}] {}", self.code(), self);
if let Some(ctx) = self.context() {
if let Some(ref table) = ctx.table {
output.push_str(&format!("\n Table: {}", table));
}
if let Some(ref column) = ctx.column {
output.push_str(&format!("\n Column: {}", column));
}
if !ctx.conditions.is_empty() {
output.push_str(&format!("\n Conditions: {}", ctx.conditions.join(" | ")));
}
if let Some(ref operator_chain) = ctx.operator_chain {
output.push_str(&format!("\n Operator chain: {}", operator_chain));
}
if let Some(ref query) = ctx.query {
output.push_str(&format!("\n Query: {}", query));
}
}
output.push_str(&format!("\n Suggestion: {}", self.suggestion()));
output
}
}
#[cfg(test)]
#[path = "testing/error_tests.rs"]
mod tests;