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