mssql_client/
error.rs

1//! Client error types.
2
3use std::sync::Arc;
4
5use thiserror::Error;
6
7/// Errors that can occur during client operations.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum Error {
11    /// Connection failed.
12    #[error("connection failed: {0}")]
13    Connection(String),
14
15    /// Connection closed unexpectedly.
16    #[error("connection closed")]
17    ConnectionClosed,
18
19    /// Authentication failed.
20    #[error("authentication failed: {0}")]
21    Authentication(#[from] mssql_auth::AuthError),
22
23    /// TLS error (string for flexibility in connection code).
24    #[error("TLS error: {0}")]
25    Tls(String),
26
27    /// Protocol error (string for flexibility in connection code).
28    #[error("protocol error: {0}")]
29    Protocol(String),
30
31    /// Codec error.
32    #[error("codec error: {0}")]
33    Codec(#[from] mssql_codec::CodecError),
34
35    /// Type conversion error.
36    #[error("type error: {0}")]
37    Type(#[from] mssql_types::TypeError),
38
39    /// Query execution error.
40    #[error("query error: {0}")]
41    Query(String),
42
43    /// Server returned an error.
44    #[error("server error {number}: {message}")]
45    Server {
46        /// Error number.
47        number: i32,
48        /// Error class/severity (0-25).
49        class: u8,
50        /// Error state.
51        state: u8,
52        /// Error message.
53        message: String,
54        /// Server name where error occurred.
55        server: Option<String>,
56        /// Stored procedure name (if applicable).
57        procedure: Option<String>,
58        /// Line number in the SQL batch or procedure.
59        line: u32,
60    },
61
62    /// Transaction error.
63    #[error("transaction error: {0}")]
64    Transaction(String),
65
66    /// Configuration error.
67    #[error("configuration error: {0}")]
68    Config(String),
69
70    /// TCP connection timeout occurred.
71    #[error("connection timed out")]
72    ConnectTimeout,
73
74    /// TLS handshake timeout occurred.
75    #[error("TLS handshake timed out")]
76    TlsTimeout,
77
78    /// Connection timeout occurred (alias for backwards compatibility).
79    #[error("connection timed out")]
80    ConnectionTimeout,
81
82    /// Command execution timeout occurred.
83    #[error("command timed out")]
84    CommandTimeout,
85
86    /// Connection routing required (Azure SQL).
87    #[error("routing required to {host}:{port}")]
88    Routing {
89        /// Target host.
90        host: String,
91        /// Target port.
92        port: u16,
93    },
94
95    /// Too many redirects during connection.
96    #[error("too many redirects (max {max})")]
97    TooManyRedirects {
98        /// Maximum redirects allowed.
99        max: u8,
100    },
101
102    /// IO error (wrapped in Arc for Clone support).
103    #[error("IO error: {0}")]
104    Io(Arc<std::io::Error>),
105
106    /// Invalid identifier (potential SQL injection attempt).
107    #[error("invalid identifier: {0}")]
108    InvalidIdentifier(String),
109
110    /// Connection pool exhausted.
111    #[error("connection pool exhausted")]
112    PoolExhausted,
113
114    /// Query cancellation error.
115    #[error("query cancellation failed: {0}")]
116    Cancel(String),
117
118    /// Query was cancelled by user request.
119    #[error("query cancelled")]
120    Cancelled,
121}
122
123#[cfg(feature = "tls")]
124impl From<mssql_tls::TlsError> for Error {
125    fn from(e: mssql_tls::TlsError) -> Self {
126        Error::Tls(e.to_string())
127    }
128}
129
130impl From<tds_protocol::ProtocolError> for Error {
131    fn from(e: tds_protocol::ProtocolError) -> Self {
132        Error::Protocol(e.to_string())
133    }
134}
135
136impl From<std::io::Error> for Error {
137    fn from(e: std::io::Error) -> Self {
138        Error::Io(Arc::new(e))
139    }
140}
141
142impl Error {
143    /// Check if this error is transient and may succeed on retry.
144    ///
145    /// Transient errors include timeouts, connection issues, and
146    /// certain server errors that may resolve themselves.
147    ///
148    /// Per ADR-009, the following server error codes are considered transient:
149    /// - 1205: Deadlock victim
150    /// - -2: Timeout
151    /// - 10928, 10929: Resource limit (Azure)
152    /// - 40197: Service error (Azure)
153    /// - 40501: Service busy (Azure)
154    /// - 40613: Database unavailable (Azure)
155    /// - 49918, 49919, 49920: Cannot process request (Azure)
156    /// - 4060: Cannot open database (may be transient during failover)
157    /// - 18456: Login failed (may be transient in Azure during failover)
158    #[must_use]
159    pub fn is_transient(&self) -> bool {
160        match self {
161            Self::ConnectTimeout
162            | Self::TlsTimeout
163            | Self::ConnectionTimeout
164            | Self::CommandTimeout
165            | Self::ConnectionClosed
166            | Self::Routing { .. }
167            | Self::PoolExhausted
168            | Self::Io(_) => true,
169            Self::Server { number, .. } => Self::is_transient_server_error(*number),
170            _ => false,
171        }
172    }
173
174    /// Check if a server error number is transient (may succeed on retry).
175    ///
176    /// This follows the error codes specified in ADR-009.
177    #[must_use]
178    pub fn is_transient_server_error(number: i32) -> bool {
179        matches!(
180            number,
181            1205 |      // Deadlock victim
182            -2 |        // Timeout
183            10928 |     // Resource limit (Azure)
184            10929 |     // Resource limit (Azure)
185            40197 |     // Service error (Azure)
186            40501 |     // Service busy (Azure)
187            40613 |     // Database unavailable (Azure)
188            49918 |     // Cannot process request (Azure)
189            49919 |     // Cannot process create/update (Azure)
190            49920 |     // Cannot process request (Azure)
191            4060 |      // Cannot open database
192            18456 // Login failed (may be transient in Azure)
193        )
194    }
195
196    /// Check if this is a terminal error that will never succeed on retry.
197    ///
198    /// Terminal errors include syntax errors, constraint violations, and
199    /// other errors that indicate programmer error or data issues.
200    ///
201    /// Per ADR-009, the following server error codes are terminal:
202    /// - 102: Syntax error
203    /// - 207: Invalid column
204    /// - 208: Invalid object
205    /// - 547: Constraint violation
206    /// - 2627: Unique constraint violation
207    /// - 2601: Duplicate key
208    #[must_use]
209    pub fn is_terminal(&self) -> bool {
210        match self {
211            Self::Config(_) | Self::InvalidIdentifier(_) => true,
212            Self::Server { number, .. } => Self::is_terminal_server_error(*number),
213            _ => false,
214        }
215    }
216
217    /// Check if a server error number is terminal (will never succeed on retry).
218    ///
219    /// This follows the error codes specified in ADR-009.
220    #[must_use]
221    pub fn is_terminal_server_error(number: i32) -> bool {
222        matches!(
223            number,
224            102 |       // Syntax error
225            207 |       // Invalid column
226            208 |       // Invalid object
227            547 |       // Constraint violation
228            2627 |      // Unique constraint violation
229            2601 // Duplicate key
230        )
231    }
232
233    /// Check if this error indicates a protocol/driver bug.
234    ///
235    /// Protocol errors typically indicate a bug in the driver implementation
236    /// rather than a user error or server issue.
237    #[must_use]
238    pub fn is_protocol_error(&self) -> bool {
239        matches!(self, Self::Protocol(_))
240    }
241
242    /// Check if this is a server error with a specific number.
243    #[must_use]
244    pub fn is_server_error(&self, number: i32) -> bool {
245        matches!(self, Self::Server { number: n, .. } if *n == number)
246    }
247
248    /// Get the error class/severity if this is a server error.
249    ///
250    /// SQL Server error classes range from 0-25:
251    /// - 0-10: Informational
252    /// - 11-16: User errors
253    /// - 17-19: Resource/hardware errors
254    /// - 20-25: System errors (connection terminating)
255    #[must_use]
256    pub fn class(&self) -> Option<u8> {
257        match self {
258            Self::Server { class, .. } => Some(*class),
259            _ => None,
260        }
261    }
262
263    /// Alias for `class()` - returns error severity.
264    #[must_use]
265    pub fn severity(&self) -> Option<u8> {
266        self.class()
267    }
268}
269
270/// Result type for client operations.
271pub type Result<T> = std::result::Result<T, Error>;
272
273#[cfg(test)]
274#[allow(clippy::unwrap_used)]
275mod tests {
276    use super::*;
277    use std::sync::Arc;
278
279    fn make_server_error(number: i32) -> Error {
280        Error::Server {
281            number,
282            class: 16,
283            state: 1,
284            message: "Test error".to_string(),
285            server: None,
286            procedure: None,
287            line: 1,
288        }
289    }
290
291    #[test]
292    fn test_is_transient_connection_errors() {
293        assert!(Error::ConnectionTimeout.is_transient());
294        assert!(Error::CommandTimeout.is_transient());
295        assert!(Error::ConnectionClosed.is_transient());
296        assert!(Error::PoolExhausted.is_transient());
297        assert!(
298            Error::Routing {
299                host: "test".into(),
300                port: 1433,
301            }
302            .is_transient()
303        );
304    }
305
306    #[test]
307    fn test_is_transient_io_error() {
308        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
309        assert!(Error::Io(Arc::new(io_err)).is_transient());
310    }
311
312    #[test]
313    fn test_is_transient_server_errors_deadlock() {
314        // 1205 - Deadlock victim
315        assert!(make_server_error(1205).is_transient());
316    }
317
318    #[test]
319    fn test_is_transient_server_errors_timeout() {
320        // -2 - Timeout
321        assert!(make_server_error(-2).is_transient());
322    }
323
324    #[test]
325    fn test_is_transient_server_errors_azure() {
326        // Azure-specific transient errors
327        assert!(make_server_error(10928).is_transient()); // Resource limit
328        assert!(make_server_error(10929).is_transient()); // Resource limit
329        assert!(make_server_error(40197).is_transient()); // Service error
330        assert!(make_server_error(40501).is_transient()); // Service busy
331        assert!(make_server_error(40613).is_transient()); // Database unavailable
332        assert!(make_server_error(49918).is_transient()); // Cannot process request
333        assert!(make_server_error(49919).is_transient()); // Cannot process create/update
334        assert!(make_server_error(49920).is_transient()); // Cannot process request
335    }
336
337    #[test]
338    fn test_is_transient_server_errors_other() {
339        // Other transient errors
340        assert!(make_server_error(4060).is_transient()); // Cannot open database
341        assert!(make_server_error(18456).is_transient()); // Login failed (Azure failover)
342    }
343
344    #[test]
345    fn test_is_not_transient() {
346        // Non-transient errors
347        assert!(!Error::Config("bad config".into()).is_transient());
348        assert!(!Error::Query("syntax error".into()).is_transient());
349        assert!(!Error::InvalidIdentifier("bad id".into()).is_transient());
350        assert!(!make_server_error(102).is_transient()); // Syntax error
351    }
352
353    #[test]
354    fn test_is_terminal_server_errors() {
355        // Terminal SQL errors per ADR-009
356        assert!(make_server_error(102).is_terminal()); // Syntax error
357        assert!(make_server_error(207).is_terminal()); // Invalid column
358        assert!(make_server_error(208).is_terminal()); // Invalid object
359        assert!(make_server_error(547).is_terminal()); // Constraint violation
360        assert!(make_server_error(2627).is_terminal()); // Unique constraint violation
361        assert!(make_server_error(2601).is_terminal()); // Duplicate key
362    }
363
364    #[test]
365    fn test_is_terminal_config_errors() {
366        assert!(Error::Config("bad config".into()).is_terminal());
367        assert!(Error::InvalidIdentifier("bad id".into()).is_terminal());
368    }
369
370    #[test]
371    fn test_is_not_terminal() {
372        // Non-terminal errors (may be transient or other)
373        assert!(!Error::ConnectionTimeout.is_terminal());
374        assert!(!make_server_error(1205).is_terminal()); // Deadlock - transient, not terminal
375        assert!(!make_server_error(40501).is_terminal()); // Service busy - transient
376    }
377
378    #[test]
379    fn test_transient_server_error_static() {
380        // Test the static helper function
381        assert!(Error::is_transient_server_error(1205));
382        assert!(Error::is_transient_server_error(40501));
383        assert!(!Error::is_transient_server_error(102));
384    }
385
386    #[test]
387    fn test_terminal_server_error_static() {
388        // Test the static helper function
389        assert!(Error::is_terminal_server_error(102));
390        assert!(Error::is_terminal_server_error(2627));
391        assert!(!Error::is_terminal_server_error(1205));
392    }
393
394    #[test]
395    fn test_error_class() {
396        let err = make_server_error(102);
397        assert_eq!(err.class(), Some(16));
398        assert_eq!(err.severity(), Some(16));
399
400        assert_eq!(Error::ConnectionTimeout.class(), None);
401    }
402
403    #[test]
404    fn test_is_server_error() {
405        let err = make_server_error(102);
406        assert!(err.is_server_error(102));
407        assert!(!err.is_server_error(103));
408
409        assert!(!Error::ConnectionTimeout.is_server_error(102));
410    }
411}