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