Skip to main content

fletch_orm/
error.rs

1//! Error types for fletch database operations.
2
3use sqlx::error::DatabaseError;
4
5/// Errors that can occur during database operations.
6#[derive(Debug, thiserror::Error)]
7pub enum FletchError {
8    /// The requested row was not found.
9    #[error("not found")]
10    NotFound,
11
12    /// A unique or primary-key constraint was violated.
13    #[error("unique violation: {0}")]
14    UniqueViolation(String),
15
16    /// A database error occurred.
17    #[error("database error: {0}")]
18    Database(String),
19
20    /// The connection pool is closed.
21    #[error("pool closed")]
22    PoolClosed,
23
24    /// Failed to encode a value for the database.
25    #[error("encode error: {0}")]
26    Encode(String),
27
28    /// Failed to decode a value from the database.
29    #[error("decode error: {0}")]
30    Decode(String),
31
32    /// A configuration error occurred.
33    #[error("configuration error: {0}")]
34    Configuration(String),
35}
36
37impl FletchError {
38    /// Return true when `err` represents a unique or primary-key constraint violation.
39    pub fn is_unique_violation(err: &Self) -> bool {
40        match err {
41            Self::UniqueViolation(_) => true,
42            Self::Database(msg) => is_unique_violation_message(msg),
43            _ => false,
44        }
45    }
46
47    /// Convert from a `sqlx::Error` into a `FletchError`.
48    pub fn from_sqlx(err: sqlx::Error) -> Self {
49        match err {
50            sqlx::Error::RowNotFound => Self::NotFound,
51            sqlx::Error::PoolClosed => Self::PoolClosed,
52            sqlx::Error::Configuration(e) => Self::Configuration(e.to_string()),
53            sqlx::Error::ColumnDecode { source, .. } => Self::Decode(source.to_string()),
54            sqlx::Error::Database(db) => {
55                let msg = db.to_string();
56                if is_sqlx_database_unique_violation(db.as_ref()) {
57                    Self::UniqueViolation(msg)
58                } else {
59                    Self::Database(msg)
60                }
61            }
62            other => Self::Database(other.to_string()),
63        }
64    }
65}
66
67impl From<sqlx::Error> for FletchError {
68    fn from(err: sqlx::Error) -> Self {
69        Self::from_sqlx(err)
70    }
71}
72
73/// Return true when `db_err` represents a unique or primary-key constraint violation.
74fn is_sqlx_database_unique_violation(db_err: &dyn DatabaseError) -> bool {
75    if db_err.is_unique_violation() {
76        return true;
77    }
78
79    if db_err
80        .code()
81        .is_some_and(|code| matches!(code.as_ref(), "2067" | "23505" | "1062"))
82    {
83        return true;
84    }
85
86    is_unique_violation_message(db_err.message())
87}
88
89/// Return true when `msg` looks like a unique or primary-key constraint violation.
90fn is_unique_violation_message(msg: &str) -> bool {
91    msg.contains("UNIQUE constraint failed")
92        || msg.contains("duplicate key value violates unique constraint")
93        || msg.contains("Duplicate entry")
94        || msg.contains("23505")
95        || msg.contains("1062")
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn not_found_displays_message() {
104        let err = FletchError::NotFound;
105        assert_eq!(err.to_string(), "not found");
106    }
107
108    #[test]
109    fn unique_violation_displays_message() {
110        let err = FletchError::UniqueViolation("duplicate key".into());
111        assert_eq!(err.to_string(), "unique violation: duplicate key");
112    }
113
114    #[test]
115    fn database_displays_message() {
116        let err = FletchError::Database("connection refused".into());
117        assert_eq!(err.to_string(), "database error: connection refused");
118    }
119
120    #[test]
121    fn pool_closed_displays_message() {
122        let err = FletchError::PoolClosed;
123        assert_eq!(err.to_string(), "pool closed");
124    }
125
126    #[test]
127    fn encode_displays_message() {
128        let err = FletchError::Encode("invalid type".into());
129        assert_eq!(err.to_string(), "encode error: invalid type");
130    }
131
132    #[test]
133    fn decode_displays_message() {
134        let err = FletchError::Decode("unexpected null".into());
135        assert_eq!(err.to_string(), "decode error: unexpected null");
136    }
137
138    #[test]
139    fn configuration_displays_message() {
140        let err = FletchError::Configuration("bad url".into());
141        assert_eq!(err.to_string(), "configuration error: bad url");
142    }
143
144    #[test]
145    fn fletch_error_is_send_sync() {
146        fn assert_send_sync<T: Send + Sync>() {}
147        assert_send_sync::<FletchError>();
148    }
149
150    #[test]
151    fn from_sqlx_row_not_found() {
152        let err = FletchError::from(sqlx::Error::RowNotFound);
153        assert!(matches!(err, FletchError::NotFound));
154    }
155
156    #[test]
157    fn from_sqlx_pool_closed() {
158        let err = FletchError::from(sqlx::Error::PoolClosed);
159        assert!(matches!(err, FletchError::PoolClosed));
160    }
161
162    #[test]
163    fn is_unique_violation_detects_variant() {
164        let err = FletchError::UniqueViolation("duplicate".into());
165        assert!(FletchError::is_unique_violation(&err));
166    }
167
168    #[test]
169    fn is_unique_violation_detects_database_message_fallback() {
170        let err = FletchError::Database(
171            "error returned from database: duplicate key value violates unique constraint".into(),
172        );
173        assert!(FletchError::is_unique_violation(&err));
174    }
175
176    #[tokio::test]
177    async fn from_sqlx_sqlite_unique_violation_uses_code() {
178        let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
179        sqlx::query("CREATE TABLE users (email TEXT UNIQUE NOT NULL)")
180            .execute(&pool)
181            .await
182            .unwrap();
183        sqlx::query("INSERT INTO users (email) VALUES ('a@example.com')")
184            .execute(&pool)
185            .await
186            .unwrap();
187
188        let sqlx_err = sqlx::query("INSERT INTO users (email) VALUES ('a@example.com')")
189            .execute(&pool)
190            .await
191            .unwrap_err();
192
193        let err = FletchError::from_sqlx(sqlx_err);
194        assert!(matches!(err, FletchError::UniqueViolation(_)));
195        assert!(FletchError::is_unique_violation(&err));
196    }
197}