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