1use sqlx::error::DatabaseError;
4
5#[derive(Debug, thiserror::Error)]
7pub enum FletchError {
8 #[error("not found")]
10 NotFound,
11
12 #[error("unique violation: {0}")]
14 UniqueViolation(String),
15
16 #[error("database error: {0}")]
18 Database(String),
19
20 #[error("pool closed")]
22 PoolClosed,
23
24 #[error("encode error: {0}")]
26 Encode(String),
27
28 #[error("decode error: {0}")]
30 Decode(String),
31
32 #[error("configuration error: {0}")]
34 Configuration(String),
35}
36
37impl FletchError {
38 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 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
73fn 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
89fn 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}