Skip to main content

rivven_rdbc/
error.rs

1//! Error types for rivven-rdbc
2//!
3//! Provides granular error classification for proper retry handling:
4//! - Retriable errors (connection, timeout, deadlock)
5//! - Non-retriable errors (constraint violations, type errors)
6
7use std::fmt;
8use thiserror::Error;
9
10/// Result type for rivven-rdbc operations
11pub type Result<T> = std::result::Result<T, Error>;
12
13/// Error categories for classification
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum ErrorCategory {
16    /// Connection-related errors (retriable)
17    Connection,
18    /// Query execution errors
19    Query,
20    /// Transaction errors
21    Transaction,
22    /// Constraint violation (not retriable)
23    Constraint,
24    /// Type conversion errors (not retriable)
25    TypeConversion,
26    /// Timeout errors (retriable)
27    Timeout,
28    /// Deadlock detected (retriable)
29    Deadlock,
30    /// Authentication failure
31    Authentication,
32    /// Configuration error
33    Configuration,
34    /// Pool exhausted (retriable with backoff)
35    PoolExhausted,
36    /// Schema-related errors
37    Schema,
38    /// Unknown/other errors
39    Other,
40}
41
42impl ErrorCategory {
43    /// Whether errors in this category are generally retriable
44    #[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/// Main error type for rivven-rdbc
54#[derive(Error, Debug)]
55#[allow(missing_docs)]
56pub enum Error {
57    /// Connection failed
58    #[error("connection error: {message}")]
59    Connection {
60        message: String,
61        #[source]
62        source: Option<Box<dyn std::error::Error + Send + Sync>>,
63    },
64
65    /// Query execution failed
66    #[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    /// Transaction error
75    #[error("transaction error: {message}")]
76    Transaction {
77        message: String,
78        #[source]
79        source: Option<Box<dyn std::error::Error + Send + Sync>>,
80    },
81
82    /// Constraint violation (PK, FK, unique, check)
83    #[error("constraint violation: {constraint_name} - {message}")]
84    Constraint {
85        constraint_name: String,
86        message: String,
87    },
88
89    /// Type conversion failed
90    #[error("type conversion error: {message}")]
91    TypeConversion { message: String },
92
93    /// Operation timed out
94    #[error("timeout: {message}")]
95    Timeout { message: String },
96
97    /// Deadlock detected
98    #[error("deadlock detected")]
99    Deadlock,
100
101    /// Authentication failed
102    #[error("authentication failed: {message}")]
103    Authentication { message: String },
104
105    /// Configuration error
106    #[error("configuration error: {message}")]
107    Configuration { message: String },
108
109    /// Connection pool exhausted
110    #[error("pool exhausted: {message}")]
111    PoolExhausted { message: String },
112
113    /// Schema error (table not found, column mismatch)
114    #[error("schema error: {message}")]
115    Schema { message: String },
116
117    /// Prepared statement not found
118    #[error("prepared statement not found: {name}")]
119    PreparedStatementNotFound { name: String },
120
121    /// Table not found
122    #[error("table not found: {table}")]
123    TableNotFound { table: String },
124
125    /// Column not found
126    #[error("column not found: {column} in table {table}")]
127    ColumnNotFound { table: String, column: String },
128
129    /// Unsupported operation for this backend
130    #[error("unsupported: {message}")]
131    Unsupported { message: String },
132
133    /// Internal error
134    #[error("internal error: {message}")]
135    Internal { message: String },
136}
137
138impl Error {
139    /// Get the error category
140    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    /// Whether this error is retriable
161    #[inline]
162    pub fn is_retriable(&self) -> bool {
163        self.category().is_retriable()
164    }
165
166    /// Create a connection error
167    pub fn connection(message: impl Into<String>) -> Self {
168        Self::Connection {
169            message: message.into(),
170            source: None,
171        }
172    }
173
174    /// Create a connection error with source
175    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    /// Create a query error
186    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    /// Create a query error with SQL
195    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    /// Create a timeout error
204    pub fn timeout(message: impl Into<String>) -> Self {
205        Self::Timeout {
206            message: message.into(),
207        }
208    }
209
210    /// Create a configuration error
211    pub fn config(message: impl Into<String>) -> Self {
212        Self::Configuration {
213            message: message.into(),
214        }
215    }
216
217    /// Create a type conversion error
218    pub fn type_conversion(message: impl Into<String>) -> Self {
219        Self::TypeConversion {
220            message: message.into(),
221        }
222    }
223
224    /// Create a schema error
225    pub fn schema(message: impl Into<String>) -> Self {
226        Self::Schema {
227            message: message.into(),
228        }
229    }
230
231    /// Create a transaction error
232    pub fn transaction(message: impl Into<String>) -> Self {
233        Self::Transaction {
234            message: message.into(),
235            source: None,
236        }
237    }
238
239    /// Create an execution error (alias for query)
240    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    /// Create an unsupported operation error
249    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}