voltdb_client_rust/
error.rs

1//! Hierarchical error types for VoltDB client.
2//!
3//! This module provides structured error types organized by category:
4//! - `ConnectionError` - Network and connection-related errors
5//! - `ProtocolError` - Wire protocol parsing and encoding errors
6//! - `QueryError` - Query execution and result handling errors
7//!
8//! The main `VoltError` type wraps these subcategories for backward compatibility.
9
10use std::fmt;
11use std::io;
12use std::str::Utf8Error;
13use std::sync::PoisonError;
14
15use crate::response::VoltResponseInfo;
16
17/// Connection-related errors.
18#[derive(Debug)]
19pub enum ConnectionError {
20    /// I/O error during network operations
21    Io(io::Error),
22    /// Connection is not available or has been closed
23    NotAvailable,
24    /// Connection timeout occurred
25    Timeout,
26    /// Authentication failed
27    AuthFailed,
28    /// Invalid connection configuration
29    InvalidConfig,
30}
31
32impl fmt::Display for ConnectionError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            ConnectionError::Io(e) => write!(f, "I/O error: {}", e),
36            ConnectionError::NotAvailable => write!(f, "Connection not available"),
37            ConnectionError::Timeout => write!(f, "Connection timeout"),
38            ConnectionError::AuthFailed => write!(f, "Authentication failed"),
39            ConnectionError::InvalidConfig => write!(f, "Invalid configuration"),
40        }
41    }
42}
43
44impl std::error::Error for ConnectionError {
45    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46        match self {
47            ConnectionError::Io(e) => Some(e),
48            _ => None,
49        }
50    }
51}
52
53impl From<io::Error> for ConnectionError {
54    fn from(err: io::Error) -> Self {
55        ConnectionError::Io(err)
56    }
57}
58
59/// Protocol-related errors (wire format, parsing).
60#[derive(Debug)]
61pub enum ProtocolError {
62    /// Invalid column type encountered
63    InvalidColumnType(i8),
64    /// Negative number of tables in response
65    NegativeNumTables(i16),
66    /// Bad return status on table
67    BadReturnStatusOnTable(i8),
68    /// UTF-8 encoding/decoding error
69    Utf8(Utf8Error),
70    /// Generic protocol error
71    Other(String),
72}
73
74impl fmt::Display for ProtocolError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            ProtocolError::InvalidColumnType(t) => write!(f, "Invalid column type: {}", t),
78            ProtocolError::NegativeNumTables(n) => write!(f, "Negative number of tables: {}", n),
79            ProtocolError::BadReturnStatusOnTable(s) => {
80                write!(f, "Bad return status on table: {}", s)
81            }
82            ProtocolError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
83            ProtocolError::Other(s) => write!(f, "Protocol error: {}", s),
84        }
85    }
86}
87
88impl std::error::Error for ProtocolError {
89    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90        match self {
91            ProtocolError::Utf8(e) => Some(e),
92            _ => None,
93        }
94    }
95}
96
97impl From<Utf8Error> for ProtocolError {
98    fn from(err: Utf8Error) -> Self {
99        ProtocolError::Utf8(err)
100    }
101}
102
103/// Query execution and result handling errors.
104#[derive(Debug)]
105pub enum QueryError {
106    /// Server returned an error response
107    ExecuteFail(VoltResponseInfo),
108    /// Requested column/value not found
109    NoValue(String),
110    /// Unexpected NULL value in non-nullable column
111    UnexpectedNull(String),
112    /// Channel receive error
113    RecvError(std::sync::mpsc::RecvError),
114}
115
116impl fmt::Display for QueryError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            QueryError::ExecuteFail(info) => write!(f, "Query execution failed: {:?}", info),
120            QueryError::NoValue(col) => write!(f, "No value found for: {}", col),
121            QueryError::UnexpectedNull(col) => {
122                write!(
123                    f,
124                    "Unexpected NULL in column '{}'. Use Option<T> for nullable columns.",
125                    col
126                )
127            }
128            QueryError::RecvError(e) => write!(f, "Receive error: {}", e),
129        }
130    }
131}
132
133impl std::error::Error for QueryError {
134    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135        match self {
136            QueryError::RecvError(e) => Some(e),
137            _ => None,
138        }
139    }
140}
141
142impl From<std::sync::mpsc::RecvError> for QueryError {
143    fn from(err: std::sync::mpsc::RecvError) -> Self {
144        QueryError::RecvError(err)
145    }
146}
147
148/// Concurrency-related errors.
149#[derive(Debug)]
150pub struct ConcurrencyError(pub String);
151
152impl fmt::Display for ConcurrencyError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "Concurrency error: {}", self.0)
155    }
156}
157
158impl std::error::Error for ConcurrencyError {}
159
160impl<T> From<PoisonError<T>> for ConcurrencyError {
161    fn from(err: PoisonError<T>) -> Self {
162        ConcurrencyError(err.to_string())
163    }
164}
165
166/// Helper trait to convert subcategory errors to VoltError.
167pub trait IntoVoltError {
168    fn into_volt_error(self) -> crate::encode::VoltError;
169}
170
171impl IntoVoltError for ConnectionError {
172    fn into_volt_error(self) -> crate::encode::VoltError {
173        match self {
174            ConnectionError::Io(e) => crate::encode::VoltError::Io(e),
175            ConnectionError::NotAvailable => crate::encode::VoltError::ConnectionNotAvailable,
176            ConnectionError::Timeout => crate::encode::VoltError::Timeout,
177            ConnectionError::AuthFailed => crate::encode::VoltError::AuthFailed,
178            ConnectionError::InvalidConfig => crate::encode::VoltError::InvalidConfig,
179        }
180    }
181}
182
183impl IntoVoltError for ProtocolError {
184    fn into_volt_error(self) -> crate::encode::VoltError {
185        match self {
186            ProtocolError::InvalidColumnType(t) => crate::encode::VoltError::InvalidColumnType(t),
187            ProtocolError::NegativeNumTables(n) => crate::encode::VoltError::NegativeNumTables(n),
188            ProtocolError::BadReturnStatusOnTable(s) => {
189                crate::encode::VoltError::BadReturnStatusOnTable(s)
190            }
191            ProtocolError::Utf8(e) => crate::encode::VoltError::Utf8Error(e),
192            ProtocolError::Other(s) => crate::encode::VoltError::Other(s),
193        }
194    }
195}
196
197impl IntoVoltError for QueryError {
198    fn into_volt_error(self) -> crate::encode::VoltError {
199        match self {
200            QueryError::ExecuteFail(info) => crate::encode::VoltError::ExecuteFail(info),
201            QueryError::NoValue(s) => crate::encode::VoltError::NoValue(s),
202            QueryError::UnexpectedNull(s) => crate::encode::VoltError::UnexpectedNull(s),
203            QueryError::RecvError(e) => crate::encode::VoltError::RecvError(e),
204        }
205    }
206}
207
208impl IntoVoltError for ConcurrencyError {
209    fn into_volt_error(self) -> crate::encode::VoltError {
210        crate::encode::VoltError::PoisonError(self.0)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use std::error::Error;
218
219    // ConnectionError tests
220    #[test]
221    fn test_connection_error_display() {
222        let err = ConnectionError::NotAvailable;
223        assert_eq!(format!("{}", err), "Connection not available");
224    }
225
226    #[test]
227    fn test_connection_error_timeout() {
228        let err = ConnectionError::Timeout;
229        assert_eq!(format!("{}", err), "Connection timeout");
230    }
231
232    #[test]
233    fn test_connection_error_auth_failed() {
234        let err = ConnectionError::AuthFailed;
235        assert_eq!(format!("{}", err), "Authentication failed");
236    }
237
238    #[test]
239    fn test_connection_error_invalid_config() {
240        let err = ConnectionError::InvalidConfig;
241        assert_eq!(format!("{}", err), "Invalid configuration");
242    }
243
244    #[test]
245    fn test_connection_error_from_io() {
246        let io_err = io::Error::new(io::ErrorKind::ConnectionRefused, "refused");
247        let conn_err: ConnectionError = io_err.into();
248        assert!(matches!(conn_err, ConnectionError::Io(_)));
249        assert!(format!("{}", conn_err).contains("I/O error"));
250    }
251
252    // ProtocolError tests
253    #[test]
254    fn test_protocol_error_display() {
255        let err = ProtocolError::InvalidColumnType(99);
256        assert_eq!(format!("{}", err), "Invalid column type: 99");
257    }
258
259    #[test]
260    fn test_protocol_error_negative_tables() {
261        let err = ProtocolError::NegativeNumTables(-5);
262        assert!(format!("{}", err).contains("-5"));
263    }
264
265    #[test]
266    fn test_protocol_error_bad_status() {
267        let err = ProtocolError::BadReturnStatusOnTable(-1);
268        assert!(format!("{}", err).contains("-1"));
269    }
270
271    #[test]
272    fn test_protocol_error_other() {
273        let err = ProtocolError::Other("custom error".to_string());
274        assert!(format!("{}", err).contains("custom error"));
275    }
276
277    // QueryError tests
278    #[test]
279    fn test_query_error_display() {
280        let err = QueryError::UnexpectedNull("my_column".to_string());
281        assert!(format!("{}", err).contains("my_column"));
282    }
283
284    #[test]
285    fn test_query_error_no_value() {
286        let err = QueryError::NoValue("missing_col".to_string());
287        assert!(format!("{}", err).contains("missing_col"));
288    }
289
290    #[test]
291    fn test_query_error_unexpected_null_message() {
292        let err = QueryError::UnexpectedNull("test_col".to_string());
293        let msg = format!("{}", err);
294        assert!(msg.contains("test_col"));
295        assert!(msg.contains("Option<T>"));
296    }
297
298    // ConcurrencyError tests
299    #[test]
300    fn test_concurrency_error_display() {
301        let err = ConcurrencyError("lock poisoned".to_string());
302        assert!(format!("{}", err).contains("lock poisoned"));
303    }
304
305    // IntoVoltError trait tests
306    #[test]
307    fn test_connection_error_into_volt_error() {
308        let conn_err = ConnectionError::NotAvailable;
309        let volt_err = conn_err.into_volt_error();
310        assert!(matches!(
311            volt_err,
312            crate::encode::VoltError::ConnectionNotAvailable
313        ));
314    }
315
316    #[test]
317    fn test_connection_timeout_into_volt_error() {
318        let conn_err = ConnectionError::Timeout;
319        let volt_err = conn_err.into_volt_error();
320        assert!(matches!(volt_err, crate::encode::VoltError::Timeout));
321    }
322
323    #[test]
324    fn test_protocol_error_into_volt_error() {
325        let proto_err = ProtocolError::InvalidColumnType(42);
326        let volt_err = proto_err.into_volt_error();
327        match volt_err {
328            crate::encode::VoltError::InvalidColumnType(t) => assert_eq!(t, 42),
329            _ => panic!("Expected InvalidColumnType"),
330        }
331    }
332
333    #[test]
334    fn test_query_error_into_volt_error() {
335        let query_err = QueryError::NoValue("col".to_string());
336        let volt_err = query_err.into_volt_error();
337        match volt_err {
338            crate::encode::VoltError::NoValue(s) => assert_eq!(s, "col"),
339            _ => panic!("Expected NoValue"),
340        }
341    }
342
343    #[test]
344    fn test_concurrency_error_into_volt_error() {
345        let conc_err = ConcurrencyError("poison".to_string());
346        let volt_err = conc_err.into_volt_error();
347        match volt_err {
348            crate::encode::VoltError::PoisonError(s) => assert_eq!(s, "poison"),
349            _ => panic!("Expected PoisonError"),
350        }
351    }
352
353    // std::error::Error trait tests
354    #[test]
355    fn test_connection_error_source() {
356        let io_err = io::Error::new(io::ErrorKind::Other, "test");
357        let conn_err = ConnectionError::Io(io_err);
358        assert!(conn_err.source().is_some());
359
360        let conn_err = ConnectionError::NotAvailable;
361        assert!(conn_err.source().is_none());
362    }
363
364    #[test]
365    fn test_protocol_error_source() {
366        let proto_err = ProtocolError::InvalidColumnType(1);
367        assert!(proto_err.source().is_none());
368    }
369
370    #[test]
371    fn test_query_error_source() {
372        let query_err = QueryError::NoValue("x".to_string());
373        assert!(query_err.source().is_none());
374    }
375}