Skip to main content

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.
24    #[cfg(feature = "tls")]
25    #[error("TLS error: {0}")]
26    Tls(#[from] mssql_tls::TlsError),
27
28    /// TLS error (when TLS feature is disabled, stores the message).
29    #[cfg(not(feature = "tls"))]
30    #[error("TLS error: {0}")]
31    Tls(String),
32
33    /// Protocol error from the TDS layer (preserves the source error chain).
34    #[error("protocol error: {0}")]
35    ProtocolError(#[from] tds_protocol::ProtocolError),
36
37    /// Protocol violation with a descriptive message.
38    #[error("protocol error: {0}")]
39    Protocol(String),
40
41    /// Codec error.
42    #[error("codec error: {0}")]
43    Codec(#[from] mssql_codec::CodecError),
44
45    /// Type conversion error.
46    #[error("type error: {0}")]
47    Type(#[from] mssql_types::TypeError),
48
49    /// Query execution error.
50    #[error("query error: {0}")]
51    Query(String),
52
53    /// Server returned an error.
54    #[error("server error {number} (severity {class}, state {state}): {message}{}", format_server_location(.server, .procedure, .line))]
55    Server {
56        /// Error number.
57        number: i32,
58        /// Error class/severity (0-25).
59        class: u8,
60        /// Error state.
61        state: u8,
62        /// Error message.
63        message: String,
64        /// Server name where error occurred.
65        server: Option<String>,
66        /// Stored procedure name (if applicable).
67        procedure: Option<String>,
68        /// Line number in the SQL batch or procedure.
69        line: u32,
70    },
71
72    /// Configuration error.
73    #[error("configuration error: {0}")]
74    Config(String),
75
76    /// TCP connection timeout occurred.
77    #[error("TCP connection timed out connecting to {host}:{port}")]
78    ConnectTimeout {
79        /// Target host.
80        host: String,
81        /// Target port.
82        port: u16,
83    },
84
85    /// TLS handshake timeout occurred.
86    #[error("TLS handshake timed out with {host}:{port}")]
87    TlsTimeout {
88        /// Target host.
89        host: String,
90        /// Target port.
91        port: u16,
92    },
93
94    /// Login/authentication response timeout occurred.
95    #[error("login timed out for {host}:{port}")]
96    LoginTimeout {
97        /// Target host.
98        host: String,
99        /// Target port.
100        port: u16,
101    },
102
103    /// Command execution timeout occurred.
104    #[error("command timed out")]
105    CommandTimeout,
106
107    /// Connection routing required (Azure SQL).
108    #[error("routing required to {host}:{port}")]
109    Routing {
110        /// Target host.
111        host: String,
112        /// Target port.
113        port: u16,
114    },
115
116    /// Too many redirects during connection.
117    #[error("too many redirects (max {max})")]
118    TooManyRedirects {
119        /// Maximum redirects allowed.
120        max: u8,
121    },
122
123    /// IO error (wrapped in Arc for Clone support).
124    #[error("IO error: {0}")]
125    Io(#[source] SharedIoError),
126
127    /// Invalid identifier (potential SQL injection attempt).
128    #[error("invalid identifier: {0}")]
129    InvalidIdentifier(String),
130
131    /// Connection pool exhausted.
132    #[error("connection pool exhausted")]
133    PoolExhausted,
134
135    /// Query cancellation error.
136    #[error("query cancellation failed: {0}")]
137    Cancel(String),
138
139    /// Query was cancelled by user request.
140    #[error("query cancelled")]
141    Cancelled,
142}
143
144// Note: From<mssql_tls::TlsError> and From<tds_protocol::ProtocolError> are
145// derived via #[from] on the enum variants above, preserving the full error chain.
146
147/// A cloneable wrapper around `std::io::Error` that preserves the error source chain.
148///
149/// `Arc<io::Error>` does not implement `std::error::Error`, which breaks
150/// `source()` chain traversal used by libraries like `anyhow` and `eyre`.
151/// This newtype bridges the gap.
152#[derive(Debug, Clone)]
153pub struct SharedIoError(Arc<std::io::Error>);
154
155impl std::fmt::Display for SharedIoError {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        self.0.fmt(f)
158    }
159}
160
161impl std::error::Error for SharedIoError {
162    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
163        self.0.source()
164    }
165}
166
167impl From<std::io::Error> for Error {
168    fn from(e: std::io::Error) -> Self {
169        Error::Io(SharedIoError(Arc::new(e)))
170    }
171}
172
173impl Error {
174    /// Check if this error is transient and may succeed on retry.
175    ///
176    /// Transient errors include timeouts, connection issues, and
177    /// certain server errors that may resolve themselves.
178    ///
179    /// Per ADR-009, the following server error codes are considered transient:
180    /// - 1205: Deadlock victim
181    /// - -2: Timeout
182    /// - 10928, 10929: Resource limit (Azure)
183    /// - 40197: Service error (Azure)
184    /// - 40501: Service busy (Azure)
185    /// - 40613: Database unavailable (Azure)
186    /// - 49918, 49919, 49920: Cannot process request (Azure)
187    /// - 4060: Cannot open database (may be transient during failover)
188    /// - 18456: Login failed (may be transient in Azure during failover)
189    #[must_use]
190    pub fn is_transient(&self) -> bool {
191        match self {
192            Self::ConnectTimeout { .. }
193            | Self::TlsTimeout { .. }
194            | Self::LoginTimeout { .. }
195            | Self::CommandTimeout
196            | Self::ConnectionClosed
197            | Self::Connection(_)
198            | Self::Routing { .. }
199            | Self::PoolExhausted
200            | Self::Io(_) => true,
201            Self::Server { number, .. } => Self::is_transient_server_error(*number),
202            _ => false,
203        }
204    }
205
206    /// Check if a server error number is transient (may succeed on retry).
207    ///
208    /// This follows the error codes specified in ADR-009.
209    ///
210    /// # Extending with custom error codes
211    ///
212    /// Applications with domain-specific transient error codes can compose
213    /// this method with their own logic:
214    ///
215    /// ```rust
216    /// use mssql_client::Error;
217    ///
218    /// fn is_transient_for_my_app(err: &Error) -> bool {
219    ///     // Check built-in transient codes first
220    ///     if err.is_transient() {
221    ///         return true;
222    ///     }
223    ///     // Add application-specific transient server errors
224    ///     if let Error::Server { number, .. } = err {
225    ///         matches!(number, 50001 | 50002) // custom app error codes
226    ///     } else {
227    ///         false
228    ///     }
229    /// }
230    /// ```
231    #[must_use]
232    pub fn is_transient_server_error(number: i32) -> bool {
233        matches!(
234            number,
235            1205 |      // Deadlock victim
236            -2 |        // Timeout
237            10928 |     // Resource limit (Azure)
238            10929 |     // Resource limit (Azure)
239            40197 |     // Service error (Azure)
240            40501 |     // Service busy (Azure)
241            40613 |     // Database unavailable (Azure)
242            49918 |     // Cannot process request (Azure)
243            49919 |     // Cannot process create/update (Azure)
244            49920 |     // Cannot process request (Azure)
245            4060 |      // Cannot open database
246            18456 // Login failed (may be transient in Azure)
247        )
248    }
249
250    /// Check if this is a terminal error that will never succeed on retry.
251    ///
252    /// Terminal errors include syntax errors, constraint violations, and
253    /// other errors that indicate programmer error or data issues.
254    ///
255    /// Per ADR-009, the following server error codes are terminal:
256    /// - 102: Syntax error
257    /// - 207: Invalid column
258    /// - 208: Invalid object
259    /// - 547: Constraint violation
260    /// - 2627: Unique constraint violation
261    /// - 2601: Duplicate key
262    #[must_use]
263    pub fn is_terminal(&self) -> bool {
264        match self {
265            Self::Config(_)
266            | Self::InvalidIdentifier(_)
267            | Self::Protocol(_)
268            | Self::ProtocolError(_)
269            | Self::Tls(_)
270            | Self::Authentication(_)
271            | Self::Cancel(_) => true,
272            Self::Server { number, .. } => Self::is_terminal_server_error(*number),
273            _ => false,
274        }
275    }
276
277    /// Check if a server error number is terminal (will never succeed on retry).
278    ///
279    /// This follows the error codes specified in ADR-009.
280    #[must_use]
281    pub fn is_terminal_server_error(number: i32) -> bool {
282        matches!(
283            number,
284            102 |       // Syntax error
285            207 |       // Invalid column
286            208 |       // Invalid object
287            547 |       // Constraint violation
288            2627 |      // Unique constraint violation
289            2601 // Duplicate key
290        )
291    }
292
293    /// Check if this error indicates a protocol/driver bug.
294    ///
295    /// Protocol errors typically indicate a bug in the driver implementation
296    /// rather than a user error or server issue. These are always terminal.
297    #[must_use]
298    pub fn is_protocol_error(&self) -> bool {
299        matches!(self, Self::Protocol(_) | Self::ProtocolError(_))
300    }
301
302    /// Check if this is a TLS/encryption error.
303    ///
304    /// TLS errors indicate certificate, handshake, or encryption failures.
305    /// These are terminal — TLS timeouts are reported as [`Error::TlsTimeout`] instead.
306    #[must_use]
307    pub fn is_tls_error(&self) -> bool {
308        matches!(self, Self::Tls(_) | Self::TlsTimeout { .. })
309    }
310
311    /// Check if this is an authentication error.
312    #[must_use]
313    pub fn is_authentication_error(&self) -> bool {
314        matches!(self, Self::Authentication(_))
315    }
316
317    /// Check if this is a configuration error.
318    ///
319    /// Configuration errors are always terminal — they indicate invalid
320    /// settings that cannot be resolved by retrying.
321    #[must_use]
322    pub fn is_config_error(&self) -> bool {
323        matches!(self, Self::Config(_))
324    }
325
326    /// Check if this is a server error with a specific number.
327    #[must_use]
328    pub fn is_server_error(&self, number: i32) -> bool {
329        matches!(self, Self::Server { number: n, .. } if *n == number)
330    }
331
332    /// Get the error class/severity if this is a server error.
333    ///
334    /// SQL Server error classes range from 0-25:
335    /// - 0-10: Informational
336    /// - 11-16: User errors
337    /// - 17-19: Resource/hardware errors
338    /// - 20-25: System errors (connection terminating)
339    #[must_use]
340    pub fn class(&self) -> Option<u8> {
341        match self {
342            Self::Server { class, .. } => Some(*class),
343            _ => None,
344        }
345    }
346
347    /// Alias for `class()` - returns error severity.
348    #[must_use]
349    pub fn severity(&self) -> Option<u8> {
350        self.class()
351    }
352}
353
354/// Format the server/procedure/line suffix for server error Display.
355fn format_server_location(
356    server: &Option<String>,
357    procedure: &Option<String>,
358    line: &u32,
359) -> String {
360    let mut parts = Vec::new();
361    if let Some(srv) = server {
362        if !srv.is_empty() {
363            parts.push(format!("server: {srv}"));
364        }
365    }
366    if let Some(proc) = procedure {
367        if !proc.is_empty() {
368            parts.push(format!("procedure: {proc}"));
369        }
370    }
371    if *line > 0 {
372        parts.push(format!("line: {line}"));
373    }
374    if parts.is_empty() {
375        String::new()
376    } else {
377        format!(" [{}]", parts.join(", "))
378    }
379}
380
381/// Result type for client operations.
382pub type Result<T> = std::result::Result<T, Error>;
383
384#[cfg(test)]
385#[allow(clippy::unwrap_used)]
386mod tests {
387    use super::*;
388    use std::sync::Arc;
389
390    fn make_server_error(number: i32) -> Error {
391        Error::Server {
392            number,
393            class: 16,
394            state: 1,
395            message: "Test error".to_string(),
396            server: None,
397            procedure: None,
398            line: 1,
399        }
400    }
401
402    #[test]
403    fn test_is_transient_connection_errors() {
404        assert!(
405            Error::ConnectTimeout {
406                host: "test".into(),
407                port: 1433
408            }
409            .is_transient()
410        );
411        assert!(
412            Error::TlsTimeout {
413                host: "test".into(),
414                port: 1433
415            }
416            .is_transient()
417        );
418        assert!(
419            Error::LoginTimeout {
420                host: "test".into(),
421                port: 1433
422            }
423            .is_transient()
424        );
425        assert!(Error::CommandTimeout.is_transient());
426        assert!(Error::ConnectionClosed.is_transient());
427        assert!(Error::PoolExhausted.is_transient());
428        assert!(
429            Error::Routing {
430                host: "test".into(),
431                port: 1433,
432            }
433            .is_transient()
434        );
435    }
436
437    #[test]
438    fn test_is_transient_io_error() {
439        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
440        assert!(Error::Io(SharedIoError(Arc::new(io_err))).is_transient());
441    }
442
443    #[test]
444    fn test_is_transient_server_errors_deadlock() {
445        // 1205 - Deadlock victim
446        assert!(make_server_error(1205).is_transient());
447    }
448
449    #[test]
450    fn test_is_transient_server_errors_timeout() {
451        // -2 - Timeout
452        assert!(make_server_error(-2).is_transient());
453    }
454
455    #[test]
456    fn test_is_transient_server_errors_azure() {
457        // Azure-specific transient errors
458        assert!(make_server_error(10928).is_transient()); // Resource limit
459        assert!(make_server_error(10929).is_transient()); // Resource limit
460        assert!(make_server_error(40197).is_transient()); // Service error
461        assert!(make_server_error(40501).is_transient()); // Service busy
462        assert!(make_server_error(40613).is_transient()); // Database unavailable
463        assert!(make_server_error(49918).is_transient()); // Cannot process request
464        assert!(make_server_error(49919).is_transient()); // Cannot process create/update
465        assert!(make_server_error(49920).is_transient()); // Cannot process request
466    }
467
468    #[test]
469    fn test_is_transient_server_errors_other() {
470        // Other transient errors
471        assert!(make_server_error(4060).is_transient()); // Cannot open database
472        assert!(make_server_error(18456).is_transient()); // Login failed (Azure failover)
473    }
474
475    #[test]
476    fn test_is_not_transient() {
477        // Non-transient errors
478        assert!(!Error::Config("bad config".into()).is_transient());
479        assert!(!Error::Query("syntax error".into()).is_transient());
480        assert!(!Error::InvalidIdentifier("bad id".into()).is_transient());
481        assert!(!make_server_error(102).is_transient()); // Syntax error
482    }
483
484    #[test]
485    fn test_is_terminal_server_errors() {
486        // Terminal SQL errors per ADR-009
487        assert!(make_server_error(102).is_terminal()); // Syntax error
488        assert!(make_server_error(207).is_terminal()); // Invalid column
489        assert!(make_server_error(208).is_terminal()); // Invalid object
490        assert!(make_server_error(547).is_terminal()); // Constraint violation
491        assert!(make_server_error(2627).is_terminal()); // Unique constraint violation
492        assert!(make_server_error(2601).is_terminal()); // Duplicate key
493    }
494
495    #[test]
496    fn test_is_terminal_config_errors() {
497        assert!(Error::Config("bad config".into()).is_terminal());
498        assert!(Error::InvalidIdentifier("bad id".into()).is_terminal());
499    }
500
501    #[test]
502    fn test_is_not_terminal() {
503        // Non-terminal errors (may be transient or other)
504        assert!(
505            !Error::ConnectTimeout {
506                host: "test".into(),
507                port: 1433
508            }
509            .is_terminal()
510        );
511        assert!(!make_server_error(1205).is_terminal()); // Deadlock - transient, not terminal
512        assert!(!make_server_error(40501).is_terminal()); // Service busy - transient
513    }
514
515    #[test]
516    fn test_transient_server_error_static() {
517        // Test the static helper function
518        assert!(Error::is_transient_server_error(1205));
519        assert!(Error::is_transient_server_error(40501));
520        assert!(!Error::is_transient_server_error(102));
521    }
522
523    #[test]
524    fn test_terminal_server_error_static() {
525        // Test the static helper function
526        assert!(Error::is_terminal_server_error(102));
527        assert!(Error::is_terminal_server_error(2627));
528        assert!(!Error::is_terminal_server_error(1205));
529    }
530
531    #[test]
532    fn test_error_class() {
533        let err = make_server_error(102);
534        assert_eq!(err.class(), Some(16));
535        assert_eq!(err.severity(), Some(16));
536
537        assert_eq!(
538            Error::ConnectTimeout {
539                host: "test".into(),
540                port: 1433
541            }
542            .class(),
543            None
544        );
545    }
546
547    #[test]
548    fn test_is_server_error() {
549        let err = make_server_error(102);
550        assert!(err.is_server_error(102));
551        assert!(!err.is_server_error(103));
552
553        assert!(
554            !Error::ConnectTimeout {
555                host: "test".into(),
556                port: 1433
557            }
558            .is_server_error(102)
559        );
560    }
561}