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
123impl From<mssql_tls::TlsError> for Error {
124    fn from(e: mssql_tls::TlsError) -> Self {
125        Error::Tls(e.to_string())
126    }
127}
128
129impl From<tds_protocol::ProtocolError> for Error {
130    fn from(e: tds_protocol::ProtocolError) -> Self {
131        Error::Protocol(e.to_string())
132    }
133}
134
135impl From<std::io::Error> for Error {
136    fn from(e: std::io::Error) -> Self {
137        Error::Io(Arc::new(e))
138    }
139}
140
141impl Error {
142    /// Check if this error is transient and may succeed on retry.
143    ///
144    /// Transient errors include timeouts, connection issues, and
145    /// certain server errors that may resolve themselves.
146    ///
147    /// Per ADR-009, the following server error codes are considered transient:
148    /// - 1205: Deadlock victim
149    /// - -2: Timeout
150    /// - 10928, 10929: Resource limit (Azure)
151    /// - 40197: Service error (Azure)
152    /// - 40501: Service busy (Azure)
153    /// - 40613: Database unavailable (Azure)
154    /// - 49918, 49919, 49920: Cannot process request (Azure)
155    /// - 4060: Cannot open database (may be transient during failover)
156    /// - 18456: Login failed (may be transient in Azure during failover)
157    #[must_use]
158    pub fn is_transient(&self) -> bool {
159        match self {
160            Self::ConnectTimeout
161            | Self::TlsTimeout
162            | Self::ConnectionTimeout
163            | Self::CommandTimeout
164            | Self::ConnectionClosed
165            | Self::Routing { .. }
166            | Self::PoolExhausted
167            | Self::Io(_) => true,
168            Self::Server { number, .. } => Self::is_transient_server_error(*number),
169            _ => false,
170        }
171    }
172
173    /// Check if a server error number is transient (may succeed on retry).
174    ///
175    /// This follows the error codes specified in ADR-009.
176    #[must_use]
177    pub fn is_transient_server_error(number: i32) -> bool {
178        matches!(
179            number,
180            1205 |      // Deadlock victim
181            -2 |        // Timeout
182            10928 |     // Resource limit (Azure)
183            10929 |     // Resource limit (Azure)
184            40197 |     // Service error (Azure)
185            40501 |     // Service busy (Azure)
186            40613 |     // Database unavailable (Azure)
187            49918 |     // Cannot process request (Azure)
188            49919 |     // Cannot process create/update (Azure)
189            49920 |     // Cannot process request (Azure)
190            4060 |      // Cannot open database
191            18456 // Login failed (may be transient in Azure)
192        )
193    }
194
195    /// Check if this is a terminal error that will never succeed on retry.
196    ///
197    /// Terminal errors include syntax errors, constraint violations, and
198    /// other errors that indicate programmer error or data issues.
199    ///
200    /// Per ADR-009, the following server error codes are terminal:
201    /// - 102: Syntax error
202    /// - 207: Invalid column
203    /// - 208: Invalid object
204    /// - 547: Constraint violation
205    /// - 2627: Unique constraint violation
206    /// - 2601: Duplicate key
207    #[must_use]
208    pub fn is_terminal(&self) -> bool {
209        match self {
210            Self::Config(_) | Self::InvalidIdentifier(_) => true,
211            Self::Server { number, .. } => Self::is_terminal_server_error(*number),
212            _ => false,
213        }
214    }
215
216    /// Check if a server error number is terminal (will never succeed on retry).
217    ///
218    /// This follows the error codes specified in ADR-009.
219    #[must_use]
220    pub fn is_terminal_server_error(number: i32) -> bool {
221        matches!(
222            number,
223            102 |       // Syntax error
224            207 |       // Invalid column
225            208 |       // Invalid object
226            547 |       // Constraint violation
227            2627 |      // Unique constraint violation
228            2601 // Duplicate key
229        )
230    }
231
232    /// Check if this error indicates a protocol/driver bug.
233    ///
234    /// Protocol errors typically indicate a bug in the driver implementation
235    /// rather than a user error or server issue.
236    #[must_use]
237    pub fn is_protocol_error(&self) -> bool {
238        matches!(self, Self::Protocol(_))
239    }
240
241    /// Check if this is a server error with a specific number.
242    #[must_use]
243    pub fn is_server_error(&self, number: i32) -> bool {
244        matches!(self, Self::Server { number: n, .. } if *n == number)
245    }
246
247    /// Get the error class/severity if this is a server error.
248    ///
249    /// SQL Server error classes range from 0-25:
250    /// - 0-10: Informational
251    /// - 11-16: User errors
252    /// - 17-19: Resource/hardware errors
253    /// - 20-25: System errors (connection terminating)
254    #[must_use]
255    pub fn class(&self) -> Option<u8> {
256        match self {
257            Self::Server { class, .. } => Some(*class),
258            _ => None,
259        }
260    }
261
262    /// Alias for `class()` - returns error severity.
263    #[must_use]
264    pub fn severity(&self) -> Option<u8> {
265        self.class()
266    }
267}
268
269/// Result type for client operations.
270pub type Result<T> = std::result::Result<T, Error>;
271
272#[cfg(test)]
273#[allow(clippy::unwrap_used)]
274mod tests {
275    use super::*;
276    use std::sync::Arc;
277
278    fn make_server_error(number: i32) -> Error {
279        Error::Server {
280            number,
281            class: 16,
282            state: 1,
283            message: "Test error".to_string(),
284            server: None,
285            procedure: None,
286            line: 1,
287        }
288    }
289
290    #[test]
291    fn test_is_transient_connection_errors() {
292        assert!(Error::ConnectionTimeout.is_transient());
293        assert!(Error::CommandTimeout.is_transient());
294        assert!(Error::ConnectionClosed.is_transient());
295        assert!(Error::PoolExhausted.is_transient());
296        assert!(
297            Error::Routing {
298                host: "test".into(),
299                port: 1433,
300            }
301            .is_transient()
302        );
303    }
304
305    #[test]
306    fn test_is_transient_io_error() {
307        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
308        assert!(Error::Io(Arc::new(io_err)).is_transient());
309    }
310
311    #[test]
312    fn test_is_transient_server_errors_deadlock() {
313        // 1205 - Deadlock victim
314        assert!(make_server_error(1205).is_transient());
315    }
316
317    #[test]
318    fn test_is_transient_server_errors_timeout() {
319        // -2 - Timeout
320        assert!(make_server_error(-2).is_transient());
321    }
322
323    #[test]
324    fn test_is_transient_server_errors_azure() {
325        // Azure-specific transient errors
326        assert!(make_server_error(10928).is_transient()); // Resource limit
327        assert!(make_server_error(10929).is_transient()); // Resource limit
328        assert!(make_server_error(40197).is_transient()); // Service error
329        assert!(make_server_error(40501).is_transient()); // Service busy
330        assert!(make_server_error(40613).is_transient()); // Database unavailable
331        assert!(make_server_error(49918).is_transient()); // Cannot process request
332        assert!(make_server_error(49919).is_transient()); // Cannot process create/update
333        assert!(make_server_error(49920).is_transient()); // Cannot process request
334    }
335
336    #[test]
337    fn test_is_transient_server_errors_other() {
338        // Other transient errors
339        assert!(make_server_error(4060).is_transient()); // Cannot open database
340        assert!(make_server_error(18456).is_transient()); // Login failed (Azure failover)
341    }
342
343    #[test]
344    fn test_is_not_transient() {
345        // Non-transient errors
346        assert!(!Error::Config("bad config".into()).is_transient());
347        assert!(!Error::Query("syntax error".into()).is_transient());
348        assert!(!Error::InvalidIdentifier("bad id".into()).is_transient());
349        assert!(!make_server_error(102).is_transient()); // Syntax error
350    }
351
352    #[test]
353    fn test_is_terminal_server_errors() {
354        // Terminal SQL errors per ADR-009
355        assert!(make_server_error(102).is_terminal()); // Syntax error
356        assert!(make_server_error(207).is_terminal()); // Invalid column
357        assert!(make_server_error(208).is_terminal()); // Invalid object
358        assert!(make_server_error(547).is_terminal()); // Constraint violation
359        assert!(make_server_error(2627).is_terminal()); // Unique constraint violation
360        assert!(make_server_error(2601).is_terminal()); // Duplicate key
361    }
362
363    #[test]
364    fn test_is_terminal_config_errors() {
365        assert!(Error::Config("bad config".into()).is_terminal());
366        assert!(Error::InvalidIdentifier("bad id".into()).is_terminal());
367    }
368
369    #[test]
370    fn test_is_not_terminal() {
371        // Non-terminal errors (may be transient or other)
372        assert!(!Error::ConnectionTimeout.is_terminal());
373        assert!(!make_server_error(1205).is_terminal()); // Deadlock - transient, not terminal
374        assert!(!make_server_error(40501).is_terminal()); // Service busy - transient
375    }
376
377    #[test]
378    fn test_transient_server_error_static() {
379        // Test the static helper function
380        assert!(Error::is_transient_server_error(1205));
381        assert!(Error::is_transient_server_error(40501));
382        assert!(!Error::is_transient_server_error(102));
383    }
384
385    #[test]
386    fn test_terminal_server_error_static() {
387        // Test the static helper function
388        assert!(Error::is_terminal_server_error(102));
389        assert!(Error::is_terminal_server_error(2627));
390        assert!(!Error::is_terminal_server_error(1205));
391    }
392
393    #[test]
394    fn test_error_class() {
395        let err = make_server_error(102);
396        assert_eq!(err.class(), Some(16));
397        assert_eq!(err.severity(), Some(16));
398
399        assert_eq!(Error::ConnectionTimeout.class(), None);
400    }
401
402    #[test]
403    fn test_is_server_error() {
404        let err = make_server_error(102);
405        assert!(err.is_server_error(102));
406        assert!(!err.is_server_error(103));
407
408        assert!(!Error::ConnectionTimeout.is_server_error(102));
409    }
410}