use stateset_core::{CommerceError, DbError};
#[cfg(feature = "sqlite")]
pub mod sqlite {
use super::{CommerceError, DbError};
use rusqlite::Error as SqliteError;
#[must_use]
pub fn map_error(
table: &'static str,
operation: &'static str,
err: SqliteError,
) -> CommerceError {
match &err {
SqliteError::SqliteFailure(ffi_err, msg) => {
match ffi_err.code {
rusqlite::ErrorCode::ConstraintViolation => {
let constraint =
msg.as_ref().map_or("unknown", std::string::String::as_str);
CommerceError::Database(DbError::ConstraintViolation {
table,
constraint: extract_constraint_name(constraint).to_string(),
message: err.to_string(),
})
}
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked => {
CommerceError::Database(DbError::TransactionFailed {
message: format!("Database busy/locked during {operation}: {err}"),
})
}
rusqlite::ErrorCode::CannotOpen => {
CommerceError::Database(DbError::ConnectionFailed {
url: "sqlite".to_string(),
message: err.to_string(),
})
}
_ => CommerceError::Database(DbError::QueryFailed {
table,
operation,
message: err.to_string(),
}),
}
}
SqliteError::QueryReturnedNoRows => {
CommerceError::NotFound
}
SqliteError::InvalidColumnType(col, col_name, expected) => {
CommerceError::Database(DbError::SerializationError {
field: col_name.clone(),
message: format!(
"Column {col_name} has invalid type at index {col}, expected {expected:?}"
),
})
}
SqliteError::InvalidColumnName(name) => CommerceError::Database(DbError::QueryFailed {
table,
operation,
message: format!("Invalid column name: {name}"),
}),
SqliteError::FromSqlConversionFailure(col, _, err) => {
CommerceError::Database(DbError::SerializationError {
field: format!("column_{col}"),
message: err.to_string(),
})
}
_ => CommerceError::Database(DbError::QueryFailed {
table,
operation,
message: err.to_string(),
}),
}
}
#[must_use]
pub fn map_pool_error(_err: r2d2::Error) -> CommerceError {
CommerceError::Database(DbError::PoolExhausted {
timeout_ms: 30000, })
}
pub fn map_connection_error<E: std::fmt::Display>(err: E) -> CommerceError {
CommerceError::Database(DbError::ConnectionFailed {
url: "sqlite-pool".to_string(),
message: err.to_string(),
})
}
fn extract_constraint_name(msg: &str) -> &str {
if let Some(idx) = msg.find(':') { msg[..idx].trim() } else { msg }
}
#[macro_export]
macro_rules! map_sqlite_err {
($table:expr, $op:expr, $result:expr) => {
$result.map_err(|e| $crate::error_helpers::sqlite::map_error($table, $op, e))
};
}
}
#[cfg(feature = "postgres")]
pub mod postgres {
use super::*;
use sqlx::Error as PgError;
pub fn map_error(table: &'static str, operation: &'static str, err: PgError) -> CommerceError {
match &err {
PgError::Database(db_err) => {
if let Some(code) = db_err.code() {
let code_str = code.as_ref();
if code_str.starts_with("23") {
let constraint = db_err.constraint().unwrap_or("unknown").to_string();
return CommerceError::Database(DbError::ConstraintViolation {
table,
constraint,
message: db_err.message().to_string(),
});
}
if code_str.starts_with("08") {
return CommerceError::Database(DbError::ConnectionFailed {
url: "postgres".to_string(),
message: db_err.message().to_string(),
});
}
if code_str.starts_with("40") {
return CommerceError::Database(DbError::TransactionFailed {
message: db_err.message().to_string(),
});
}
}
CommerceError::Database(DbError::QueryFailed {
table,
operation,
message: db_err.message().to_string(),
})
}
PgError::RowNotFound => CommerceError::NotFound,
PgError::PoolTimedOut => {
CommerceError::Database(DbError::PoolExhausted {
timeout_ms: 30000, })
}
PgError::PoolClosed => CommerceError::Database(DbError::ConnectionFailed {
url: "postgres-pool".to_string(),
message: "Connection pool is closed".to_string(),
}),
PgError::Io(io_err) => CommerceError::Database(DbError::ConnectionFailed {
url: "postgres".to_string(),
message: io_err.to_string(),
}),
PgError::Decode(decode_err) => CommerceError::Database(DbError::SerializationError {
field: "unknown".to_string(),
message: decode_err.to_string(),
}),
_ => CommerceError::Database(DbError::QueryFailed {
table,
operation,
message: err.to_string(),
}),
}
}
pub fn map_migration_error(version: i32, err: impl std::fmt::Display) -> CommerceError {
CommerceError::Database(DbError::MigrationFailed { version, message: err.to_string() })
}
#[macro_export]
macro_rules! map_pg_err {
($table:expr, $op:expr, $result:expr) => {
$result.map_err(|e| $crate::error_helpers::postgres::map_error($table, $op, e))
};
}
}
pub fn map_json_error(field: &str, err: impl std::fmt::Display) -> CommerceError {
CommerceError::Database(DbError::SerializationError {
field: field.to_string(),
message: err.to_string(),
})
}
pub fn map_db_error(msg: impl Into<String>) -> CommerceError {
CommerceError::Database(DbError::Other(msg.into()))
}
pub fn migration_failed(version: i32, msg: impl Into<String>) -> CommerceError {
CommerceError::Database(DbError::MigrationFailed { version, message: msg.into() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "sqlite")]
fn test_map_sqlite_not_found() {
let err = rusqlite::Error::QueryReturnedNoRows;
let result = sqlite::map_error("orders", "get", err);
assert!(result.is_not_found());
}
#[test]
fn test_map_json_error() {
let err = map_json_error("metadata", "invalid JSON");
assert!(err.is_database());
}
#[test]
fn test_map_json_error_contains_field_name() {
let err = map_json_error("settings", "parse failed");
let msg = format!("{err}");
assert!(msg.contains("settings"), "Error should mention the field: {msg}");
}
#[test]
fn test_map_db_error_produces_database_error() {
let err = map_db_error("something went wrong");
assert!(err.is_database());
let msg = format!("{err}");
assert!(msg.contains("something went wrong"));
}
#[test]
fn test_map_db_error_with_string() {
let err = map_db_error(String::from("owned message"));
assert!(err.is_database());
}
#[test]
fn test_migration_failed_produces_database_error() {
let err = migration_failed(42, "column missing");
assert!(err.is_database());
let msg = format!("{err}");
assert!(msg.contains("42"), "Should contain version: {msg}");
assert!(msg.contains("column missing"), "Should contain message: {msg}");
}
#[test]
fn test_migration_failed_version_zero() {
let err = migration_failed(0, "initial");
assert!(err.is_database());
}
#[test]
fn test_migration_failed_negative_version() {
let err = migration_failed(-1, "rollback");
assert!(err.is_database());
}
#[test]
fn test_map_json_error_empty_field() {
let err = map_json_error("", "no field");
assert!(err.is_database());
}
#[cfg(feature = "sqlite")]
mod sqlite_tests {
use super::*;
use rusqlite::{Connection, Error as SqliteError};
#[test]
fn test_constraint_violation_unique() {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)", [])
.unwrap();
conn.execute("INSERT INTO users (email) VALUES ('a@b.com')", []).unwrap();
let raw_err =
conn.execute("INSERT INTO users (email) VALUES ('a@b.com')", []).unwrap_err();
let mapped = sqlite::map_error("users", "insert", raw_err);
assert!(mapped.is_database());
match mapped {
CommerceError::Database(DbError::ConstraintViolation { table, .. }) => {
assert_eq!(table, "users");
}
other => panic!("Expected ConstraintViolation, got: {other:?}"),
}
}
#[test]
fn test_invalid_column_name() {
let err = SqliteError::InvalidColumnName("bogus_col".into());
let mapped = sqlite::map_error("orders", "select", err);
assert!(mapped.is_database());
let msg = format!("{mapped}");
assert!(msg.contains("bogus_col"), "Should mention column: {msg}");
}
#[test]
fn test_invalid_column_type() {
let err =
SqliteError::InvalidColumnType(0, "amount".into(), rusqlite::types::Type::Text);
let mapped = sqlite::map_error("invoices", "get", err);
assert!(mapped.is_database());
let msg = format!("{mapped}");
assert!(msg.contains("amount"), "Should mention column name: {msg}");
}
#[test]
fn test_from_sql_conversion_failure() {
let inner = Box::new(std::fmt::Error); let err =
SqliteError::FromSqlConversionFailure(3, rusqlite::types::Type::Integer, inner);
let mapped = sqlite::map_error("payments", "get", err);
assert!(mapped.is_database());
}
#[test]
fn test_generic_sqlite_error_maps_to_query_failed() {
let err = SqliteError::InvalidParameterCount(3, 5);
let mapped = sqlite::map_error("products", "update", err);
assert!(mapped.is_database());
}
#[test]
fn test_map_pool_error() {
}
#[test]
fn test_map_connection_error_with_string() {
let err = sqlite::map_connection_error("connection refused");
assert!(err.is_database());
let msg = format!("{err}");
assert!(msg.contains("connection refused"));
}
#[test]
fn test_map_connection_error_with_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
let err = sqlite::map_connection_error(io_err);
assert!(err.is_database());
}
#[test]
fn test_extract_constraint_name_with_colon() {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE t (v TEXT UNIQUE)", []).unwrap();
conn.execute("INSERT INTO t VALUES ('x')", []).unwrap();
let raw_err = conn.execute("INSERT INTO t VALUES ('x')", []).unwrap_err();
let mapped = sqlite::map_error("t", "insert", raw_err);
match mapped {
CommerceError::Database(DbError::ConstraintViolation { constraint, .. }) => {
assert!(
!constraint.contains(':'),
"Constraint should not contain colon: {constraint}"
);
assert!(constraint.contains("UNIQUE"), "Should contain UNIQUE: {constraint}");
}
other => panic!("Expected ConstraintViolation, got: {other:?}"),
}
}
#[test]
fn test_database_busy_maps_to_transaction_failed() {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE t (id INTEGER)", []).unwrap();
let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0)).unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_query_returned_no_rows_maps_to_not_found() {
let err = SqliteError::QueryReturnedNoRows;
let mapped = sqlite::map_error("shipments", "get_by_id", err);
assert!(mapped.is_not_found());
}
#[test]
fn test_multiple_tables_constraint_violations() {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE a (id INTEGER PRIMARY KEY)", []).unwrap();
conn.execute(
"CREATE TABLE b (id INTEGER PRIMARY KEY, a_id INTEGER REFERENCES a(id))",
[],
)
.unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
let raw_err = conn.execute("INSERT INTO b (id, a_id) VALUES (1, 999)", []).unwrap_err();
let mapped = sqlite::map_error("b", "insert", raw_err);
assert!(mapped.is_database());
}
}
}