use sqlx::error::ErrorKind;
use turnframe_store::error::{StoreError, invalid_record};
const CLASS_CONNECTION_EXCEPTION: &str = "08";
const CLASS_INSUFFICIENT_RESOURCES: &str = "53";
const QUERY_CANCELED: &str = "57014";
const ADMIN_SHUTDOWN: &str = "57P01";
const SERIALIZATION_FAILURE: &str = "40001";
const DEADLOCK_DETECTED: &str = "40P01";
pub const POSTGRES_CODE_PREFIX: &str = "turnframe.store.postgres.";
#[must_use]
pub fn store_error(error: &sqlx::Error) -> StoreError {
match error {
sqlx::Error::RowNotFound => StoreError::NotFound,
sqlx::Error::Database(database) => database_error(database.as_ref()),
sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed => StoreError::Unavailable,
sqlx::Error::Io(_)
| sqlx::Error::Tls(_)
| sqlx::Error::Protocol(_)
| sqlx::Error::WorkerCrashed => StoreError::Unavailable,
sqlx::Error::ColumnDecode { .. }
| sqlx::Error::Decode(_)
| sqlx::Error::ColumnNotFound(_)
| sqlx::Error::ColumnIndexOutOfBounds { .. }
| sqlx::Error::TypeNotFound { .. } => StoreError::Corrupt,
sqlx::Error::Encode(_) => StoreError::Serialization,
_ => StoreError::Other {
code: format!("{POSTGRES_CODE_PREFIX}driver"),
},
}
}
#[must_use]
pub fn commit_failed(error: &sqlx::Error) -> StoreError {
match error {
sqlx::Error::Database(_) => store_error(error),
_ => StoreError::Timeout,
}
}
fn database_error(database: &dyn sqlx::error::DatabaseError) -> StoreError {
match database.kind() {
ErrorKind::UniqueViolation | ErrorKind::ForeignKeyViolation => return StoreError::Conflict,
ErrorKind::CheckViolation | ErrorKind::NotNullViolation => return invalid_record(),
_ => {}
}
let Some(sqlstate) = database.code() else {
return StoreError::Other {
code: format!("{POSTGRES_CODE_PREFIX}unknown"),
};
};
by_sqlstate(sqlstate.as_ref())
}
fn by_sqlstate(sqlstate: &str) -> StoreError {
match sqlstate {
SERIALIZATION_FAILURE | DEADLOCK_DETECTED => StoreError::Conflict,
QUERY_CANCELED => StoreError::Timeout,
ADMIN_SHUTDOWN => StoreError::Unavailable,
_ if sqlstate.starts_with(CLASS_CONNECTION_EXCEPTION)
|| sqlstate.starts_with(CLASS_INSUFFICIENT_RESOURCES) =>
{
StoreError::Unavailable
}
_ => StoreError::Other {
code: format!("{POSTGRES_CODE_PREFIX}{sqlstate}"),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transport_failures_report_that_nothing_was_written() {
let io = sqlx::Error::Io(std::io::Error::other("boom"));
assert_eq!(store_error(&io), StoreError::Unavailable);
assert_eq!(
store_error(&sqlx::Error::PoolTimedOut),
StoreError::Unavailable
);
assert_eq!(
store_error(&sqlx::Error::PoolClosed),
StoreError::Unavailable
);
}
#[test]
fn a_commit_that_does_not_answer_is_indeterminate() {
let io = sqlx::Error::Io(std::io::Error::other("boom"));
assert_eq!(commit_failed(&io), StoreError::Timeout);
}
#[test]
fn undecodable_values_are_corrupt_not_conflicts() {
let decode = sqlx::Error::ColumnDecode {
index: "status".to_owned(),
source: "not a status".into(),
};
assert_eq!(store_error(&decode), StoreError::Corrupt);
assert_eq!(
store_error(&sqlx::Error::ColumnNotFound("status".to_owned())),
StoreError::Corrupt
);
}
#[test]
fn sqlstates_map_to_the_contract() {
assert_eq!(by_sqlstate(SERIALIZATION_FAILURE), StoreError::Conflict);
assert_eq!(by_sqlstate(DEADLOCK_DETECTED), StoreError::Conflict);
assert_eq!(by_sqlstate(QUERY_CANCELED), StoreError::Timeout);
assert_eq!(by_sqlstate("08006"), StoreError::Unavailable);
assert_eq!(by_sqlstate("53300"), StoreError::Unavailable);
assert_eq!(
by_sqlstate("22012"),
StoreError::Other {
code: "turnframe.store.postgres.22012".to_owned()
}
);
}
#[test]
fn no_server_message_reaches_the_error_surface() {
let rendered = by_sqlstate("22012").to_string();
assert_eq!(rendered, "store failure turnframe.store.postgres.22012");
}
}