use cratestack_core::{CoolError, DbErrorInfo};
use crate::sqlx;
pub fn cool_error_from_sqlx(error: sqlx::Error) -> CoolError {
match error {
sqlx::Error::Database(db_err) => {
let detail = db_err.to_string();
let sqlstate = db_err.code().map(|c| c.into_owned());
let constraint = db_err.constraint().map(ToOwned::to_owned);
CoolError::DatabaseTyped(DbErrorInfo {
detail,
sqlstate,
constraint,
})
}
sqlx::Error::RowNotFound => CoolError::NotFound("not found".to_owned()),
other => CoolError::Database(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn row_not_found_maps_to_not_found() {
let err = cool_error_from_sqlx(sqlx::Error::RowNotFound);
assert!(
matches!(err, CoolError::NotFound(_)),
"RowNotFound should map to CoolError::NotFound",
);
assert_eq!(err.status_code().as_u16(), 404);
assert_eq!(err.db_sqlstate(), None);
assert_eq!(err.db_constraint(), None);
}
#[test]
fn non_database_sqlx_error_produces_legacy_variant() {
let err = cool_error_from_sqlx(sqlx::Error::Protocol(
"unexpected EOF from server".to_owned(),
));
assert!(
matches!(err, CoolError::Database(_)),
"Protocol error should fall back to CoolError::Database",
);
assert!(
err.detail().is_some(),
"detail() must not be empty for non-database errors",
);
assert_eq!(err.db_sqlstate(), None);
assert_eq!(err.db_constraint(), None);
}
#[test]
fn database_typed_accessors() {
let info = DbErrorInfo {
detail: "ERROR: duplicate key value violates unique constraint \"accounts_email_key\""
.to_owned(),
sqlstate: Some("23505".to_owned()),
constraint: Some("accounts_email_key".to_owned()),
};
let err = CoolError::DatabaseTyped(info);
assert_eq!(err.db_sqlstate(), Some("23505"));
assert_eq!(err.db_constraint(), Some("accounts_email_key"));
assert_eq!(err.code(), "DATABASE_ERROR");
let status = err.status_code();
assert_eq!(status.as_u16(), 500);
assert_eq!(err.public_message(), "internal error");
assert!(err.detail().unwrap().contains("duplicate key"));
}
#[test]
fn database_typed_detail_preserved_for_retry_logic() {
let info = DbErrorInfo {
detail: "Database(PgDatabaseError { code: \"40001\", message: \"could not serialize access\" })"
.to_owned(),
sqlstate: Some("40001".to_owned()),
constraint: None,
};
let err = CoolError::DatabaseTyped(info);
let detail = err.detail().unwrap_or_default();
assert!(
detail.contains("40001") || detail.contains("serialize"),
"detail must still surface retriable substrings: {detail}",
);
}
#[test]
fn database_typed_empty_detail_returns_none() {
let err = CoolError::DatabaseTyped(DbErrorInfo::default());
assert_eq!(err.detail(), None);
}
#[test]
fn database_typed_into_response_does_not_leak_detail() {
let info = DbErrorInfo {
detail: "SELECT * FROM secrets".to_owned(),
sqlstate: Some("23505".to_owned()),
constraint: None,
};
let response = CoolError::DatabaseTyped(info).into_response();
assert_eq!(response.code, "DATABASE_ERROR");
assert_eq!(response.message, "internal error");
assert!(!response.message.contains("secrets"));
assert!(response.details.is_none());
}
}