use sqlx::error::DatabaseError;
#[derive(Debug, thiserror::Error)]
pub enum FletchError {
#[error("not found")]
NotFound,
#[error("unique violation: {0}")]
UniqueViolation(String),
#[error("database error: {0}")]
Database(String),
#[error("pool closed")]
PoolClosed,
#[error("encode error: {0}")]
Encode(String),
#[error("decode error: {0}")]
Decode(String),
#[error("configuration error: {0}")]
Configuration(String),
}
impl FletchError {
pub fn is_unique_violation(err: &Self) -> bool {
match err {
Self::UniqueViolation(_) => true,
Self::Database(msg) => is_unique_violation_message(msg),
_ => false,
}
}
pub fn from_sqlx(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => Self::NotFound,
sqlx::Error::PoolClosed => Self::PoolClosed,
sqlx::Error::Configuration(e) => Self::Configuration(e.to_string()),
sqlx::Error::ColumnDecode { source, .. } => Self::Decode(source.to_string()),
sqlx::Error::Database(db) => {
let msg = db.to_string();
if is_sqlx_database_unique_violation(db.as_ref()) {
Self::UniqueViolation(msg)
} else {
Self::Database(msg)
}
}
other => Self::Database(other.to_string()),
}
}
}
impl From<sqlx::Error> for FletchError {
fn from(err: sqlx::Error) -> Self {
Self::from_sqlx(err)
}
}
fn is_sqlx_database_unique_violation(db_err: &dyn DatabaseError) -> bool {
if db_err.is_unique_violation() {
return true;
}
if db_err
.code()
.is_some_and(|code| matches!(code.as_ref(), "2067" | "23505" | "1062"))
{
return true;
}
is_unique_violation_message(db_err.message())
}
fn is_unique_violation_message(msg: &str) -> bool {
msg.contains("UNIQUE constraint failed")
|| msg.contains("duplicate key value violates unique constraint")
|| msg.contains("Duplicate entry")
|| msg.contains("23505")
|| msg.contains("1062")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_found_displays_message() {
let err = FletchError::NotFound;
assert_eq!(err.to_string(), "not found");
}
#[test]
fn unique_violation_displays_message() {
let err = FletchError::UniqueViolation("duplicate key".into());
assert_eq!(err.to_string(), "unique violation: duplicate key");
}
#[test]
fn database_displays_message() {
let err = FletchError::Database("connection refused".into());
assert_eq!(err.to_string(), "database error: connection refused");
}
#[test]
fn pool_closed_displays_message() {
let err = FletchError::PoolClosed;
assert_eq!(err.to_string(), "pool closed");
}
#[test]
fn encode_displays_message() {
let err = FletchError::Encode("invalid type".into());
assert_eq!(err.to_string(), "encode error: invalid type");
}
#[test]
fn decode_displays_message() {
let err = FletchError::Decode("unexpected null".into());
assert_eq!(err.to_string(), "decode error: unexpected null");
}
#[test]
fn configuration_displays_message() {
let err = FletchError::Configuration("bad url".into());
assert_eq!(err.to_string(), "configuration error: bad url");
}
#[test]
fn fletch_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<FletchError>();
}
#[test]
fn from_sqlx_row_not_found() {
let err = FletchError::from(sqlx::Error::RowNotFound);
assert!(matches!(err, FletchError::NotFound));
}
#[test]
fn from_sqlx_pool_closed() {
let err = FletchError::from(sqlx::Error::PoolClosed);
assert!(matches!(err, FletchError::PoolClosed));
}
#[test]
fn is_unique_violation_detects_variant() {
let err = FletchError::UniqueViolation("duplicate".into());
assert!(FletchError::is_unique_violation(&err));
}
#[test]
fn is_unique_violation_detects_database_message_fallback() {
let err = FletchError::Database(
"error returned from database: duplicate key value violates unique constraint".into(),
);
assert!(FletchError::is_unique_violation(&err));
}
#[tokio::test]
async fn from_sqlx_sqlite_unique_violation_uses_code() {
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::query("CREATE TABLE users (email TEXT UNIQUE NOT NULL)")
.execute(&pool)
.await
.unwrap();
sqlx::query("INSERT INTO users (email) VALUES ('a@example.com')")
.execute(&pool)
.await
.unwrap();
let sqlx_err = sqlx::query("INSERT INTO users (email) VALUES ('a@example.com')")
.execute(&pool)
.await
.unwrap_err();
let err = FletchError::from_sqlx(sqlx_err);
assert!(matches!(err, FletchError::UniqueViolation(_)));
assert!(FletchError::is_unique_violation(&err));
}
}