Skip to main content

geode_client/
error.rs

1//! Error types for the Geode client.
2
3use thiserror::Error;
4
5/// Result type alias for Geode operations
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Error types that can occur when using the Geode client
9#[derive(Error, Debug)]
10pub enum Error {
11    /// Connection error
12    #[error("Connection error: {0}")]
13    Connection(String),
14
15    /// Query execution error
16    #[error("Query error: {code} - {message}")]
17    Query { code: String, message: String },
18
19    /// Authentication error
20    #[error("Authentication error: {0}")]
21    Auth(String),
22
23    /// I/O error
24    #[error("I/O error: {0}")]
25    Io(#[from] std::io::Error),
26
27    /// JSON serialization/deserialization error
28    #[error("JSON error: {0}")]
29    Json(#[from] serde_json::Error),
30
31    /// QUIC connection error
32    #[error("QUIC error: {0}")]
33    Quic(String),
34
35    /// TLS error
36    #[error("TLS error: {0}")]
37    Tls(String),
38
39    /// Invalid DSN format
40    #[error("Invalid DSN: {0}")]
41    InvalidDsn(String),
42
43    /// Type conversion error
44    #[error("Type error: {0}")]
45    Type(String),
46
47    /// Timeout error
48    #[error("Operation timed out")]
49    Timeout,
50
51    /// Pool error
52    #[error("Pool error: {0}")]
53    Pool(String),
54
55    /// Input validation error
56    #[error("Validation error: {0}")]
57    Validation(String),
58
59    /// Client-side limit exceeded (frame size, rows, depth, etc.)
60    #[error("Limit exceeded: {0}")]
61    Limit(String),
62
63    /// Invalid identifier (contains forbidden characters)
64    #[error("Invalid identifier: {0}")]
65    InvalidIdent(String),
66
67    /// Invalid string literal (contains forbidden characters)
68    #[error("Invalid string literal: {0}")]
69    InvalidString(String),
70
71    /// No rows returned by a query expected to return at least one row.
72    #[error("no rows in result set")]
73    NoRows,
74
75    /// Generic error
76    #[error("{0}")]
77    Other(String),
78}
79
80impl Error {
81    /// Create a connection error
82    pub fn connection<S: Into<String>>(msg: S) -> Self {
83        Error::Connection(msg.into())
84    }
85
86    /// Create a query error
87    pub fn query<S: Into<String>>(msg: S) -> Self {
88        Error::Query {
89            code: "QUERY_ERROR".to_string(),
90            message: msg.into(),
91        }
92    }
93
94    /// Create a protocol error
95    pub fn protocol<S: Into<String>>(msg: S) -> Self {
96        Error::Connection(format!("Protocol error: {}", msg.into()))
97    }
98
99    /// Create a transaction error
100    pub fn transaction<S: Into<String>>(msg: S) -> Self {
101        Error::Connection(format!("Transaction error: {}", msg.into()))
102    }
103
104    /// Create a timeout error
105    pub fn timeout() -> Self {
106        Error::Timeout
107    }
108
109    /// Create an auth error
110    pub fn auth<S: Into<String>>(msg: S) -> Self {
111        Error::Auth(msg.into())
112    }
113
114    /// Create a QUIC error
115    pub fn quic<S: Into<String>>(msg: S) -> Self {
116        Error::Quic(msg.into())
117    }
118
119    /// Create a TLS error
120    pub fn tls<S: Into<String>>(msg: S) -> Self {
121        Error::Tls(msg.into())
122    }
123
124    /// Create a type error
125    pub fn type_error<S: Into<String>>(msg: S) -> Self {
126        Error::Type(msg.into())
127    }
128
129    /// Create a pool error
130    pub fn pool<S: Into<String>>(msg: S) -> Self {
131        Error::Pool(msg.into())
132    }
133
134    /// Create a validation error
135    pub fn validation<S: Into<String>>(msg: S) -> Self {
136        Error::Validation(msg.into())
137    }
138
139    /// Create a client limit error
140    pub fn limit<S: Into<String>>(msg: S) -> Self {
141        Error::Limit(msg.into())
142    }
143
144    /// Create an invalid DSN error
145    pub fn invalid_dsn<S: Into<String>>(msg: S) -> Self {
146        Error::InvalidDsn(msg.into())
147    }
148
149    /// Create an invalid identifier error
150    pub fn invalid_ident<S: Into<String>>(msg: S) -> Self {
151        Error::InvalidIdent(msg.into())
152    }
153
154    /// Create an invalid string literal error
155    pub fn invalid_string<S: Into<String>>(msg: S) -> Self {
156        Error::InvalidString(msg.into())
157    }
158
159    /// Check if the error is retryable
160    ///
161    /// Retryable errors are transient failures that may succeed on retry:
162    /// - Connection errors (network issues)
163    /// - Timeout errors
164    /// - QUIC errors (connection reset, etc.)
165    /// - Query errors with serialization failure codes (40001, 40P01)
166    #[inline]
167    pub fn is_retryable(&self) -> bool {
168        match self {
169            Error::Connection(_) => true,
170            Error::Timeout => true,
171            Error::Quic(_) => true,
172            Error::Query { code, .. } => {
173                // ISO/IEC 39075 retryable codes
174                code == "40001" || code == "40P01" || code == "40502"
175            }
176            Error::Pool(_) => true,
177            _ => false,
178        }
179    }
180
181    /// Create a no-rows error.
182    pub fn no_rows() -> Self {
183        Error::NoRows
184    }
185
186    /// Get the error code if this is a query error
187    pub fn code(&self) -> Option<&str> {
188        match self {
189            Error::Query { code, .. } => Some(code),
190            _ => None,
191        }
192    }
193
194    /// Returns the ISO/IEC 39075 status class for query errors.
195    fn status_class(&self) -> Option<&str> {
196        match self {
197            Error::Query { code, .. } => Some(code.as_str()),
198            _ => None,
199        }
200    }
201
202    /// Returns true if this error represents an authentication/authorization
203    /// failure (status class 28000 or the Geode-specific 08P01), or an
204    /// [`Error::Auth`] variant.
205    pub fn is_auth_error(&self) -> bool {
206        if matches!(self, Error::Auth(_)) {
207            return true;
208        }
209        matches!(self.status_class(), Some("28000") | Some("08P01"))
210    }
211
212    /// Returns true if this error represents a syntax error (status class 42000).
213    pub fn is_syntax_error(&self) -> bool {
214        self.status_class() == Some("42000")
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use std::io;
222
223    #[test]
224    fn test_error_display_connection() {
225        let err = Error::Connection("connection refused".to_string());
226        assert_eq!(err.to_string(), "Connection error: connection refused");
227    }
228
229    #[test]
230    fn test_error_display_query() {
231        let err = Error::Query {
232            code: "42000".to_string(),
233            message: "syntax error".to_string(),
234        };
235        assert_eq!(err.to_string(), "Query error: 42000 - syntax error");
236    }
237
238    #[test]
239    fn test_error_display_auth() {
240        let err = Error::Auth("invalid credentials".to_string());
241        assert_eq!(err.to_string(), "Authentication error: invalid credentials");
242    }
243
244    #[test]
245    fn test_error_display_io() {
246        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
247        let err = Error::Io(io_err);
248        assert!(err.to_string().starts_with("I/O error:"));
249    }
250
251    #[test]
252    fn test_error_display_json() {
253        let json_err: serde_json::Error = serde_json::from_str::<i32>("invalid").unwrap_err();
254        let err = Error::Json(json_err);
255        assert!(err.to_string().starts_with("JSON error:"));
256    }
257
258    #[test]
259    fn test_error_display_quic() {
260        let err = Error::Quic("connection reset".to_string());
261        assert_eq!(err.to_string(), "QUIC error: connection reset");
262    }
263
264    #[test]
265    fn test_error_display_tls() {
266        let err = Error::Tls("certificate expired".to_string());
267        assert_eq!(err.to_string(), "TLS error: certificate expired");
268    }
269
270    #[test]
271    fn test_error_display_invalid_dsn() {
272        let err = Error::InvalidDsn("missing host".to_string());
273        assert_eq!(err.to_string(), "Invalid DSN: missing host");
274    }
275
276    #[test]
277    fn test_error_display_type() {
278        let err = Error::Type("cannot convert int to string".to_string());
279        assert_eq!(err.to_string(), "Type error: cannot convert int to string");
280    }
281
282    #[test]
283    fn test_error_display_timeout() {
284        let err = Error::Timeout;
285        assert_eq!(err.to_string(), "Operation timed out");
286    }
287
288    #[test]
289    fn test_error_display_pool() {
290        let err = Error::Pool("pool exhausted".to_string());
291        assert_eq!(err.to_string(), "Pool error: pool exhausted");
292    }
293
294    #[test]
295    fn test_error_display_limit() {
296        let err = Error::Limit("frame too large".to_string());
297        assert_eq!(err.to_string(), "Limit exceeded: frame too large");
298    }
299
300    #[test]
301    fn test_error_display_other() {
302        let err = Error::Other("unknown error".to_string());
303        assert_eq!(err.to_string(), "unknown error");
304    }
305
306    #[test]
307    fn test_error_from_io() {
308        let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "refused");
309        let err: Error = io_err.into();
310        assert!(matches!(err, Error::Io(_)));
311    }
312
313    #[test]
314    fn test_error_from_json() {
315        let json_err: serde_json::Error = serde_json::from_str::<i32>("not_a_number").unwrap_err();
316        let err: Error = json_err.into();
317        assert!(matches!(err, Error::Json(_)));
318    }
319
320    #[test]
321    fn test_error_helper_connection() {
322        let err = Error::connection("test connection error");
323        assert!(matches!(err, Error::Connection(msg) if msg == "test connection error"));
324    }
325
326    #[test]
327    fn test_error_helper_query() {
328        let err = Error::query("test query error");
329        assert!(matches!(err, Error::Query { code, message }
330            if code == "QUERY_ERROR" && message == "test query error"));
331    }
332
333    #[test]
334    fn test_error_helper_protocol() {
335        let err = Error::protocol("invalid frame");
336        assert!(matches!(err, Error::Connection(msg) if msg.contains("Protocol error")));
337    }
338
339    #[test]
340    fn test_error_helper_transaction() {
341        let err = Error::transaction("rollback failed");
342        assert!(matches!(err, Error::Connection(msg) if msg.contains("Transaction error")));
343    }
344
345    #[test]
346    fn test_error_helper_timeout() {
347        let err = Error::timeout();
348        assert!(matches!(err, Error::Timeout));
349    }
350
351    #[test]
352    fn test_error_helper_auth() {
353        let err = Error::auth("bad token");
354        assert!(matches!(err, Error::Auth(msg) if msg == "bad token"));
355    }
356
357    #[test]
358    fn test_error_helper_quic() {
359        let err = Error::quic("stream closed");
360        assert!(matches!(err, Error::Quic(msg) if msg == "stream closed"));
361    }
362
363    #[test]
364    fn test_error_helper_tls() {
365        let err = Error::tls("handshake failed");
366        assert!(matches!(err, Error::Tls(msg) if msg == "handshake failed"));
367    }
368
369    #[test]
370    fn test_error_helper_type_error() {
371        let err = Error::type_error("invalid cast");
372        assert!(matches!(err, Error::Type(msg) if msg == "invalid cast"));
373    }
374
375    #[test]
376    fn test_error_helper_pool() {
377        let err = Error::pool("no connections available");
378        assert!(matches!(err, Error::Pool(msg) if msg == "no connections available"));
379    }
380
381    #[test]
382    fn test_error_helper_limit() {
383        let err = Error::limit("max rows exceeded");
384        assert!(matches!(err, Error::Limit(msg) if msg == "max rows exceeded"));
385    }
386
387    #[test]
388    fn test_error_is_retryable_connection() {
389        let err = Error::Connection("network error".to_string());
390        assert!(err.is_retryable());
391    }
392
393    #[test]
394    fn test_error_is_retryable_timeout() {
395        let err = Error::Timeout;
396        assert!(err.is_retryable());
397    }
398
399    #[test]
400    fn test_error_is_retryable_quic() {
401        let err = Error::Quic("reset".to_string());
402        assert!(err.is_retryable());
403    }
404
405    #[test]
406    fn test_error_is_retryable_pool() {
407        let err = Error::Pool("exhausted".to_string());
408        assert!(err.is_retryable());
409    }
410
411    #[test]
412    fn test_error_is_retryable_serialization_failure() {
413        let err = Error::Query {
414            code: "40001".to_string(),
415            message: "serialization failure".to_string(),
416        };
417        assert!(err.is_retryable());
418    }
419
420    #[test]
421    fn test_error_is_retryable_deadlock() {
422        let err = Error::Query {
423            code: "40P01".to_string(),
424            message: "deadlock detected".to_string(),
425        };
426        assert!(err.is_retryable());
427    }
428
429    #[test]
430    fn test_error_is_retryable_transaction_deadlock() {
431        let err = Error::Query {
432            code: "40502".to_string(),
433            message: "transaction deadlock".to_string(),
434        };
435        assert!(err.is_retryable());
436    }
437
438    #[test]
439    fn test_error_not_retryable_syntax() {
440        let err = Error::Query {
441            code: "42000".to_string(),
442            message: "syntax error".to_string(),
443        };
444        assert!(!err.is_retryable());
445    }
446
447    #[test]
448    fn test_error_not_retryable_auth() {
449        let err = Error::Auth("invalid".to_string());
450        assert!(!err.is_retryable());
451    }
452
453    #[test]
454    fn test_error_not_retryable_tls() {
455        let err = Error::Tls("cert error".to_string());
456        assert!(!err.is_retryable());
457    }
458
459    #[test]
460    fn test_error_not_retryable_dsn() {
461        let err = Error::InvalidDsn("bad format".to_string());
462        assert!(!err.is_retryable());
463    }
464
465    #[test]
466    fn test_error_not_retryable_type() {
467        let err = Error::Type("cast failed".to_string());
468        assert!(!err.is_retryable());
469    }
470
471    #[test]
472    fn test_error_code_query() {
473        let err = Error::Query {
474            code: "42000".to_string(),
475            message: "syntax error".to_string(),
476        };
477        assert_eq!(err.code(), Some("42000"));
478    }
479
480    #[test]
481    fn test_error_code_non_query() {
482        let err = Error::Connection("test".to_string());
483        assert_eq!(err.code(), None);
484    }
485
486    #[test]
487    fn test_result_type_alias() {
488        fn returns_result() -> Result<i32> {
489            Ok(42)
490        }
491        assert_eq!(returns_result().unwrap(), 42);
492    }
493
494    #[test]
495    fn test_result_type_alias_error() {
496        fn returns_error() -> Result<i32> {
497            Err(Error::Other("test".to_string()))
498        }
499        assert!(returns_error().is_err());
500    }
501
502    #[test]
503    fn test_error_no_rows() {
504        let err = Error::no_rows();
505        assert!(matches!(err, Error::NoRows));
506        assert_eq!(err.to_string(), "no rows in result set");
507    }
508
509    #[test]
510    fn test_is_auth_error() {
511        assert!(
512            Error::Query {
513                code: "28000".into(),
514                message: "x".into()
515            }
516            .is_auth_error()
517        );
518        assert!(
519            Error::Query {
520                code: "08P01".into(),
521                message: "x".into()
522            }
523            .is_auth_error()
524        );
525        assert!(
526            !Error::Query {
527                code: "42000".into(),
528                message: "x".into()
529            }
530            .is_auth_error()
531        );
532        assert!(!Error::Connection("x".into()).is_auth_error());
533        // Auth variant also classifies as auth error.
534        assert!(Error::Auth("bad".into()).is_auth_error());
535    }
536
537    #[test]
538    fn test_is_syntax_error() {
539        assert!(
540            Error::Query {
541                code: "42000".into(),
542                message: "x".into()
543            }
544            .is_syntax_error()
545        );
546        assert!(
547            !Error::Query {
548                code: "28000".into(),
549                message: "x".into()
550            }
551            .is_syntax_error()
552        );
553        assert!(!Error::Connection("x".into()).is_syntax_error());
554    }
555
556    #[test]
557    fn test_error_string_conversion() {
558        // Test that Into<String> works for &str
559        let err = Error::connection("test");
560        assert!(matches!(err, Error::Connection(_)));
561
562        // Test that Into<String> works for String
563        let err = Error::connection(String::from("test"));
564        assert!(matches!(err, Error::Connection(_)));
565    }
566
567    #[test]
568    fn test_error_debug() {
569        let err = Error::Connection("test".to_string());
570        let debug_str = format!("{:?}", err);
571        assert!(debug_str.contains("Connection"));
572        assert!(debug_str.contains("test"));
573    }
574}