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") => {
69 QueryError::constraint_violation("", e.to_string())
70 }
71 Some("23502") => QueryError::invalid_input("", e.to_string()),
73 _ => QueryError::database(e.to_string()),
74 },
75 _ => QueryError::database(e.to_string()),
76 },
77 SqlxError::Config(msg) => QueryError::connection(msg),
78 SqlxError::Connection(msg) => QueryError::connection(msg),
79 SqlxError::Query(msg) => QueryError::database(msg),
80 SqlxError::Deserialization(msg) => QueryError::serialization(msg),
81 SqlxError::TypeConversion(msg) => QueryError::serialization(msg),
82 SqlxError::Pool(msg) => QueryError::connection(msg),
83 SqlxError::Timeout(ms) => QueryError::timeout(ms),
84 SqlxError::Migration(msg) => QueryError::database(msg),
85 SqlxError::Internal(msg) => QueryError::internal(msg),
86 }
87 }
88}
89
90impl SqlxError {
91 pub fn config(msg: impl Into<String>) -> Self {
93 Self::Config(msg.into())
94 }
95
96 pub fn connection(msg: impl Into<String>) -> Self {
98 Self::Connection(msg.into())
99 }
100
101 pub fn query(msg: impl Into<String>) -> Self {
103 Self::Query(msg.into())
104 }
105
106 pub fn pool(msg: impl Into<String>) -> Self {
108 Self::Pool(msg.into())
109 }
110
111 pub fn timeout(ms: u64) -> Self {
113 Self::Timeout(ms)
114 }
115
116 pub fn type_conversion(msg: impl Into<String>) -> Self {
118 Self::TypeConversion(msg.into())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_error_creation() {
128 let err = SqlxError::config("test config error");
129 assert!(matches!(err, SqlxError::Config(_)));
130
131 let err = SqlxError::connection("test connection error");
132 assert!(matches!(err, SqlxError::Connection(_)));
133
134 let err = SqlxError::timeout(5000);
135 assert!(matches!(err, SqlxError::Timeout(5000)));
136 }
137
138 #[test]
139 fn test_error_to_query_error() {
140 let err = SqlxError::connection("connection failed");
141 let query_err: QueryError = err.into();
142 assert!(query_err.to_string().contains("connection"));
143
144 let err = SqlxError::timeout(1000);
145 let query_err: QueryError = err.into();
146 assert!(
147 query_err.to_string().contains("timeout") || query_err.to_string().contains("1000")
148 );
149 }
150
151 #[derive(Debug)]
154 struct MockDbError {
155 message: String,
156 code: Option<String>,
157 }
158
159 impl std::fmt::Display for MockDbError {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.write_str(&self.message)
162 }
163 }
164
165 impl std::error::Error for MockDbError {}
166
167 impl sqlx::error::DatabaseError for MockDbError {
168 fn message(&self) -> &str {
169 &self.message
170 }
171
172 fn code(&self) -> Option<std::borrow::Cow<'_, str>> {
173 self.code.as_deref().map(std::borrow::Cow::Borrowed)
174 }
175
176 fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
177 self
178 }
179
180 fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
181 self
182 }
183
184 fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
185 self
186 }
187
188 fn kind(&self) -> sqlx::error::ErrorKind {
189 sqlx::error::ErrorKind::Other
190 }
191 }
192
193 #[test]
194 fn test_sqlx_database_error_code_mapping() {
195 let db_err = MockDbError {
197 message: "db says no".to_string(),
198 code: Some("23505".to_string()),
199 };
200 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
201 let query_err: QueryError = err.into();
202 assert_eq!(query_err.code, prax_query::ErrorCode::UniqueConstraint);
203
204 let db_err = MockDbError {
206 message: "db says no".to_string(),
207 code: Some("23502".to_string()),
208 };
209 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
210 let query_err: QueryError = err.into();
211 assert_eq!(query_err.code, prax_query::ErrorCode::InvalidParameter);
212
213 let db_err = MockDbError {
215 message: "deadlock detected".to_string(),
216 code: Some("40P01".to_string()),
217 };
218 let err = SqlxError::from(sqlx::Error::Database(Box::new(db_err)));
219 let query_err: QueryError = err.into();
220 assert_eq!(query_err.code, prax_query::ErrorCode::DatabaseError);
221 }
222
223 #[test]
224 fn test_sqlx_io_error_maps_to_connection() {
225 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
228 let err = SqlxError::from(sqlx::Error::Io(io_err));
229 let query_err: QueryError = err.into();
230 assert_eq!(query_err.code, prax_query::ErrorCode::ConnectionFailed);
231 }
232}