1use std::fmt;
8use thiserror::Error;
9
10pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum ErrorCategory {
16 Connection,
18 Query,
20 Transaction,
22 Constraint,
24 TypeConversion,
26 Timeout,
28 Deadlock,
30 Authentication,
32 Configuration,
34 PoolExhausted,
36 Schema,
38 Other,
40}
41
42impl ErrorCategory {
43 #[inline]
45 pub const fn is_retriable(self) -> bool {
46 matches!(
47 self,
48 Self::Connection | Self::Timeout | Self::Deadlock | Self::PoolExhausted
49 )
50 }
51}
52
53#[derive(Error, Debug)]
55#[allow(missing_docs)]
56pub enum Error {
57 #[error("connection error: {message}")]
59 Connection {
60 message: String,
61 #[source]
62 source: Option<Box<dyn std::error::Error + Send + Sync>>,
63 },
64
65 #[error("query error: {message}")]
67 Query {
68 message: String,
69 sql: Option<String>,
70 #[source]
71 source: Option<Box<dyn std::error::Error + Send + Sync>>,
72 },
73
74 #[error("transaction error: {message}")]
76 Transaction {
77 message: String,
78 #[source]
79 source: Option<Box<dyn std::error::Error + Send + Sync>>,
80 },
81
82 #[error("constraint violation: {constraint_name} - {message}")]
84 Constraint {
85 constraint_name: String,
86 message: String,
87 },
88
89 #[error("type conversion error: {message}")]
91 TypeConversion { message: String },
92
93 #[error("timeout: {message}")]
95 Timeout { message: String },
96
97 #[error("deadlock detected")]
99 Deadlock,
100
101 #[error("authentication failed: {message}")]
103 Authentication { message: String },
104
105 #[error("configuration error: {message}")]
107 Configuration { message: String },
108
109 #[error("pool exhausted: {message}")]
111 PoolExhausted { message: String },
112
113 #[error("schema error: {message}")]
115 Schema { message: String },
116
117 #[error("prepared statement not found: {name}")]
119 PreparedStatementNotFound { name: String },
120
121 #[error("table not found: {table}")]
123 TableNotFound { table: String },
124
125 #[error("column not found: {column} in table {table}")]
127 ColumnNotFound { table: String, column: String },
128
129 #[error("unsupported: {message}")]
131 Unsupported { message: String },
132
133 #[error("internal error: {message}")]
135 Internal { message: String },
136}
137
138impl Error {
139 pub fn category(&self) -> ErrorCategory {
141 match self {
142 Self::Connection { .. } => ErrorCategory::Connection,
143 Self::Query { .. } => ErrorCategory::Query,
144 Self::Transaction { .. } => ErrorCategory::Transaction,
145 Self::Constraint { .. } => ErrorCategory::Constraint,
146 Self::TypeConversion { .. } => ErrorCategory::TypeConversion,
147 Self::Timeout { .. } => ErrorCategory::Timeout,
148 Self::Deadlock => ErrorCategory::Deadlock,
149 Self::Authentication { .. } => ErrorCategory::Authentication,
150 Self::Configuration { .. } => ErrorCategory::Configuration,
151 Self::PoolExhausted { .. } => ErrorCategory::PoolExhausted,
152 Self::Schema { .. } | Self::TableNotFound { .. } | Self::ColumnNotFound { .. } => {
153 ErrorCategory::Schema
154 }
155 Self::PreparedStatementNotFound { .. } => ErrorCategory::Query,
156 Self::Unsupported { .. } | Self::Internal { .. } => ErrorCategory::Other,
157 }
158 }
159
160 #[inline]
162 pub fn is_retriable(&self) -> bool {
163 self.category().is_retriable()
164 }
165
166 pub fn connection(message: impl Into<String>) -> Self {
168 Self::Connection {
169 message: message.into(),
170 source: None,
171 }
172 }
173
174 pub fn connection_with_source(
176 message: impl Into<String>,
177 source: impl std::error::Error + Send + Sync + 'static,
178 ) -> Self {
179 Self::Connection {
180 message: message.into(),
181 source: Some(Box::new(source)),
182 }
183 }
184
185 pub fn query(message: impl Into<String>) -> Self {
187 Self::Query {
188 message: message.into(),
189 sql: None,
190 source: None,
191 }
192 }
193
194 pub fn query_with_sql(message: impl Into<String>, sql: impl Into<String>) -> Self {
196 Self::Query {
197 message: message.into(),
198 sql: Some(sql.into()),
199 source: None,
200 }
201 }
202
203 pub fn timeout(message: impl Into<String>) -> Self {
205 Self::Timeout {
206 message: message.into(),
207 }
208 }
209
210 pub fn config(message: impl Into<String>) -> Self {
212 Self::Configuration {
213 message: message.into(),
214 }
215 }
216
217 pub fn type_conversion(message: impl Into<String>) -> Self {
219 Self::TypeConversion {
220 message: message.into(),
221 }
222 }
223
224 pub fn schema(message: impl Into<String>) -> Self {
226 Self::Schema {
227 message: message.into(),
228 }
229 }
230
231 pub fn transaction(message: impl Into<String>) -> Self {
233 Self::Transaction {
234 message: message.into(),
235 source: None,
236 }
237 }
238
239 pub fn execution(message: impl Into<String>) -> Self {
241 Self::Query {
242 message: message.into(),
243 sql: None,
244 source: None,
245 }
246 }
247
248 pub fn unsupported(message: impl Into<String>) -> Self {
250 Self::Unsupported {
251 message: message.into(),
252 }
253 }
254}
255
256impl fmt::Display for ErrorCategory {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 Self::Connection => write!(f, "connection"),
260 Self::Query => write!(f, "query"),
261 Self::Transaction => write!(f, "transaction"),
262 Self::Constraint => write!(f, "constraint"),
263 Self::TypeConversion => write!(f, "type_conversion"),
264 Self::Timeout => write!(f, "timeout"),
265 Self::Deadlock => write!(f, "deadlock"),
266 Self::Authentication => write!(f, "authentication"),
267 Self::Configuration => write!(f, "configuration"),
268 Self::PoolExhausted => write!(f, "pool_exhausted"),
269 Self::Schema => write!(f, "schema"),
270 Self::Other => write!(f, "other"),
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_error_category_retriable() {
281 assert!(ErrorCategory::Connection.is_retriable());
282 assert!(ErrorCategory::Timeout.is_retriable());
283 assert!(ErrorCategory::Deadlock.is_retriable());
284 assert!(ErrorCategory::PoolExhausted.is_retriable());
285
286 assert!(!ErrorCategory::Constraint.is_retriable());
287 assert!(!ErrorCategory::TypeConversion.is_retriable());
288 assert!(!ErrorCategory::Query.is_retriable());
289 }
290
291 #[test]
292 fn test_error_is_retriable() {
293 assert!(Error::connection("failed").is_retriable());
294 assert!(Error::timeout("timed out").is_retriable());
295 assert!(Error::Deadlock.is_retriable());
296
297 assert!(!Error::Constraint {
298 constraint_name: "pk".into(),
299 message: "duplicate".into()
300 }
301 .is_retriable());
302 }
303
304 #[test]
305 fn test_error_display() {
306 let err = Error::connection("connection refused");
307 assert!(err.to_string().contains("connection refused"));
308
309 let err = Error::query_with_sql("syntax error", "SELECT * FORM users");
310 assert!(err.to_string().contains("syntax error"));
311 }
312}