1use prax_query::QueryError;
4use thiserror::Error;
5
6pub type SqlxResult<T> = Result<T, SqlxError>;
8
9#[derive(Error, Debug)]
11pub enum SqlxError {
12 #[error("Database error: {0}")]
14 Sqlx(#[from] sqlx::Error),
15
16 #[error("Configuration error: {0}")]
18 Config(String),
19
20 #[error("Connection error: {0}")]
22 Connection(String),
23
24 #[error("Query error: {0}")]
26 Query(String),
27
28 #[error("Deserialization error: {0}")]
30 Deserialization(String),
31
32 #[error("Type conversion error: {0}")]
34 TypeConversion(String),
35
36 #[error("Pool error: {0}")]
38 Pool(String),
39
40 #[error("Operation timed out after {0}ms")]
42 Timeout(u64),
43
44 #[error("Migration error: {0}")]
46 Migration(String),
47
48 #[error("Internal error: {0}")]
50 Internal(String),
51}
52
53impl From<SqlxError> for QueryError {
54 fn from(err: SqlxError) -> Self {
55 match err {
56 SqlxError::Sqlx(e) => match &e {
57 sqlx::Error::Io(_) | sqlx::Error::PoolTimedOut | sqlx::Error::PoolClosed => {
61 QueryError::connection(e.to_string())
62 }
63 sqlx::Error::RowNotFound => QueryError::not_found("row"),
64 sqlx::Error::Database(db_err) => match db_err.code().as_deref() {
67 Some("23505") | Some("23503") | Some("23514") => {
69 QueryError::constraint_violation("", e.to_string())
70 }
71 Some("23502") => QueryError::invalid_input("", e.to_string()),
73 Some("0A000")
79 if db_err
80 .message()
81 .contains("cached plan must not change result type") =>
82 {
83 QueryError::stale_plan(e.to_string())
84 }
85 _ => QueryError::database(e.to_string()),
86 },
87 _ => QueryError::database(e.to_string()),
88 },
89 SqlxError::Config(msg) => QueryError::connection(msg),
90 SqlxError::Connection(msg) => QueryError::connection(msg),
91 SqlxError::Query(msg) => QueryError::database(msg),
92 SqlxError::Deserialization(msg) => QueryError::serialization(msg),
93 SqlxError::TypeConversion(msg) => QueryError::serialization(msg),
94 SqlxError::Pool(msg) => QueryError::connection(msg),
95 SqlxError::Timeout(ms) => QueryError::timeout(ms),
96 SqlxError::Migration(msg) => QueryError::database(msg),
97 SqlxError::Internal(msg) => QueryError::internal(msg),
98 }
99 }
100}
101
102impl SqlxError {
103 pub fn config(msg: impl Into<String>) -> Self {
105 Self::Config(msg.into())
106 }
107
108 pub fn connection(msg: impl Into<String>) -> Self {
110 Self::Connection(msg.into())
111 }
112
113 pub fn query(msg: impl Into<String>) -> Self {
115 Self::Query(msg.into())
116 }
117
118 pub fn pool(msg: impl Into<String>) -> Self {
120 Self::Pool(msg.into())
121 }
122
123 pub fn timeout(ms: u64) -> Self {
125 Self::Timeout(ms)
126 }
127
128 pub fn type_conversion(msg: impl Into<String>) -> Self {
130 Self::TypeConversion(msg.into())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_error_creation() {
140 let err = SqlxError::config("test config error");
141 assert!(matches!(err, SqlxError::Config(_)));
142
143 let err = SqlxError::connection("test connection error");
144 assert!(matches!(err, SqlxError::Connection(_)));
145
146 let err = SqlxError::timeout(5000);
147 assert!(matches!(err, SqlxError::Timeout(5000)));
148 }
149
150 #[test]
151 fn test_error_to_query_error() {
152 let err = SqlxError::connection("connection failed");
153 let query_err: QueryError = err.into();
154 assert!(query_err.to_string().contains("connection"));
155
156 let err = SqlxError::timeout(1000);
157 let query_err: QueryError = err.into();
158 assert!(
159 query_err.to_string().contains("timeout") || query_err.to_string().contains("1000")
160 );
161 }
162
163 #[derive(Debug)]
166 struct MockDbError {
167 message: String,
168 code: Option<String>,
169 }
170
171 impl std::fmt::Display for MockDbError {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 f.write_str(&self.message)
174 }
175 }
176
177 impl std::error::Error for MockDbError {}
178
179 impl sqlx::error::DatabaseError for MockDbError {
180 fn message(&self) -> &str {
181 &self.message
182 }
183
184 fn code(&self) -> Option<std::borrow::Cow<'_, str>> {
185 self.code.as_deref().map(std::borrow::Cow::Borrowed)
186 }
187
188 fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
189 self
190 }
191
192 fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
193 self
194 }
195
196 fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
197 self
198 }
199
200 fn kind(&self) -> sqlx::error::ErrorKind {
201 sqlx::error::ErrorKind::Other
202 }
203 }
204
205 #[test]
206 fn test_sqlx_database_error_code_mapping() {
207 let db_err = MockDbError {
209 message: "db says no".to_string(),
210 code: Some("23505".to_string()),
211 };
212 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
213 let query_err: QueryError = err.into();
214 assert_eq!(query_err.code, prax_query::ErrorCode::UniqueConstraint);
215
216 let db_err = MockDbError {
218 message: "db says no".to_string(),
219 code: Some("23502".to_string()),
220 };
221 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
222 let query_err: QueryError = err.into();
223 assert_eq!(query_err.code, prax_query::ErrorCode::InvalidParameter);
224
225 let db_err = MockDbError {
227 message: "deadlock detected".to_string(),
228 code: Some("40P01".to_string()),
229 };
230 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
231 let query_err: QueryError = err.into();
232 assert_eq!(query_err.code, prax_query::ErrorCode::DatabaseError);
233 }
234
235 #[test]
236 fn test_sqlx_check_violation_maps_to_constraint() {
237 let db_err = MockDbError {
239 message: "new row violates check constraint".to_string(),
240 code: Some("23514".to_string()),
241 };
242 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
243 let query_err: QueryError = err.into();
244 assert!(query_err.is_constraint_violation());
245 }
246
247 #[test]
248 fn test_sqlx_stale_cached_plan_is_retryable() {
249 let db_err = MockDbError {
251 message: "cached plan must not change result type".to_string(),
252 code: Some("0A000".to_string()),
253 };
254 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
255 let query_err: QueryError = err.into();
256 assert_eq!(query_err.code, prax_query::ErrorCode::SerializationFailure);
257 assert!(query_err.is_retryable());
258 }
259
260 #[test]
261 fn test_sqlx_other_0a000_stays_generic() {
262 let db_err = MockDbError {
264 message: "cannot insert into a view".to_string(),
265 code: Some("0A000".to_string()),
266 };
267 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
268 let query_err: QueryError = err.into();
269 assert_eq!(query_err.code, prax_query::ErrorCode::DatabaseError);
270 assert!(!query_err.is_retryable());
271 }
272
273 #[test]
274 fn test_sqlx_io_error_maps_to_connection() {
275 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
278 let err = SqlxError::from(sqlx::Error::Io(io_err));
279 let query_err: QueryError = err.into();
280 assert_eq!(query_err.code, prax_query::ErrorCode::ConnectionFailed);
281 }
282}