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 /// SQL Browser service instance resolution failed.
144 #[error("SQL Browser resolution failed for instance '{instance}': {reason}")]
145 BrowserResolution {
146 /// The instance name that was being resolved.
147 instance: String,
148 /// Description of what went wrong.
149 reason: String,
150 },
151
152 /// FILESTREAM operation failed.
153 ///
154 /// This error occurs when opening or accessing a FILESTREAM BLOB fails,
155 /// typically due to a missing driver DLL, invalid path, or permission issue.
156 #[cfg(all(windows, feature = "filestream"))]
157 #[error("FILESTREAM error: {0}")]
158 FileStream(String),
159
160 /// Always Encrypted operation failed.
161 ///
162 /// This error occurs during CEK decryption, column value decryption, or
163 /// parameter encryption. Key material is never included in the error message.
164 #[cfg(feature = "always-encrypted")]
165 #[error("encryption error: {0}")]
166 Encryption(String),
167}
168
169// Note: From<mssql_tls::TlsError> and From<tds_protocol::ProtocolError> are
170// derived via #[from] on the enum variants above, preserving the full error chain.
171
172/// A cloneable wrapper around `std::io::Error` that preserves the error source chain.
173///
174/// `Arc<io::Error>` does not implement `std::error::Error`, which breaks
175/// `source()` chain traversal used by libraries like `anyhow` and `eyre`.
176/// This newtype bridges the gap.
177#[derive(Debug, Clone)]
178pub struct SharedIoError(Arc<std::io::Error>);
179
180impl std::fmt::Display for SharedIoError {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 self.0.fmt(f)
183 }
184}
185
186impl std::error::Error for SharedIoError {
187 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
188 self.0.source()
189 }
190}
191
192impl From<std::io::Error> for Error {
193 fn from(e: std::io::Error) -> Self {
194 Error::Io(SharedIoError(Arc::new(e)))
195 }
196}
197
198#[cfg(feature = "always-encrypted")]
199impl From<mssql_auth::EncryptionError> for Error {
200 fn from(e: mssql_auth::EncryptionError) -> Self {
201 // SECURITY: Do NOT include key material in the error message.
202 // EncryptionError::Display does not log keys, but we convert to
203 // String to ensure no internal state leaks.
204 Error::Encryption(e.to_string())
205 }
206}
207
208impl Error {
209 /// Check if this error is transient and may succeed on retry.
210 ///
211 /// Transient errors include timeouts, connection issues, and
212 /// certain server errors that may resolve themselves.
213 ///
214 /// Per ADR-009, the following server error codes are considered transient:
215 /// - 1205: Deadlock victim
216 /// - -2: Timeout
217 /// - 10928, 10929: Resource limit (Azure)
218 /// - 40197: Service error (Azure)
219 /// - 40501: Service busy (Azure)
220 /// - 40613: Database unavailable (Azure)
221 /// - 49918, 49919, 49920: Cannot process request (Azure)
222 /// - 4060: Cannot open database (may be transient during failover)
223 /// - 18456: Login failed (may be transient in Azure during failover)
224 #[must_use]
225 pub fn is_transient(&self) -> bool {
226 match self {
227 Self::ConnectTimeout { .. }
228 | Self::TlsTimeout { .. }
229 | Self::LoginTimeout { .. }
230 | Self::CommandTimeout
231 | Self::ConnectionClosed
232 | Self::Connection(_)
233 | Self::Routing { .. }
234 | Self::PoolExhausted
235 | Self::Io(_) => true,
236 Self::Server { number, .. } => Self::is_transient_server_error(*number),
237 _ => false,
238 }
239 }
240
241 /// Check if a server error number is transient (may succeed on retry).
242 ///
243 /// This follows the error codes specified in ADR-009.
244 ///
245 /// # Extending with custom error codes
246 ///
247 /// Applications with domain-specific transient error codes can compose
248 /// this method with their own logic:
249 ///
250 /// ```rust
251 /// use mssql_client::Error;
252 ///
253 /// fn is_transient_for_my_app(err: &Error) -> bool {
254 /// // Check built-in transient codes first
255 /// if err.is_transient() {
256 /// return true;
257 /// }
258 /// // Add application-specific transient server errors
259 /// if let Error::Server { number, .. } = err {
260 /// matches!(number, 50001 | 50002) // custom app error codes
261 /// } else {
262 /// false
263 /// }
264 /// }
265 /// ```
266 #[must_use]
267 pub fn is_transient_server_error(number: i32) -> bool {
268 matches!(
269 number,
270 1205 | // Deadlock victim
271 -2 | // Timeout
272 10928 | // Resource limit (Azure)
273 10929 | // Resource limit (Azure)
274 40197 | // Service error (Azure)
275 40501 | // Service busy (Azure)
276 40613 | // Database unavailable (Azure)
277 49918 | // Cannot process request (Azure)
278 49919 | // Cannot process create/update (Azure)
279 49920 | // Cannot process request (Azure)
280 4060 | // Cannot open database
281 18456 // Login failed (may be transient in Azure)
282 )
283 }
284
285 /// Check if this is a terminal error that will never succeed on retry.
286 ///
287 /// Terminal errors include syntax errors, constraint violations, and
288 /// other errors that indicate programmer error or data issues.
289 ///
290 /// Per ADR-009, the following server error codes are terminal:
291 /// - 102: Syntax error
292 /// - 207: Invalid column
293 /// - 208: Invalid object
294 /// - 547: Constraint violation
295 /// - 2627: Unique constraint violation
296 /// - 2601: Duplicate key
297 #[must_use]
298 pub fn is_terminal(&self) -> bool {
299 match self {
300 Self::Config(_)
301 | Self::InvalidIdentifier(_)
302 | Self::Protocol(_)
303 | Self::ProtocolError(_)
304 | Self::Tls(_)
305 | Self::Authentication(_)
306 | Self::Cancel(_) => true,
307 Self::Server { number, .. } => Self::is_terminal_server_error(*number),
308 _ => false,
309 }
310 }
311
312 /// Check if a server error number is terminal (will never succeed on retry).
313 ///
314 /// This follows the error codes specified in ADR-009.
315 #[must_use]
316 pub fn is_terminal_server_error(number: i32) -> bool {
317 matches!(
318 number,
319 102 | // Syntax error
320 207 | // Invalid column
321 208 | // Invalid object
322 547 | // Constraint violation
323 2627 | // Unique constraint violation
324 2601 // Duplicate key
325 )
326 }
327
328 /// Check if this error indicates a protocol/driver bug.
329 ///
330 /// Protocol errors typically indicate a bug in the driver implementation
331 /// rather than a user error or server issue. These are always terminal.
332 #[must_use]
333 pub fn is_protocol_error(&self) -> bool {
334 matches!(self, Self::Protocol(_) | Self::ProtocolError(_))
335 }
336
337 /// Check if this is a TLS/encryption error.
338 ///
339 /// TLS errors indicate certificate, handshake, or encryption failures.
340 /// These are terminal — TLS timeouts are reported as [`Error::TlsTimeout`] instead.
341 #[must_use]
342 pub fn is_tls_error(&self) -> bool {
343 matches!(self, Self::Tls(_) | Self::TlsTimeout { .. })
344 }
345
346 /// Check if this is an authentication error.
347 #[must_use]
348 pub fn is_authentication_error(&self) -> bool {
349 matches!(self, Self::Authentication(_))
350 }
351
352 /// Check if this is a configuration error.
353 ///
354 /// Configuration errors are always terminal — they indicate invalid
355 /// settings that cannot be resolved by retrying.
356 #[must_use]
357 pub fn is_config_error(&self) -> bool {
358 matches!(self, Self::Config(_))
359 }
360
361 /// Check if this is a server error with a specific number.
362 #[must_use]
363 pub fn is_server_error(&self, number: i32) -> bool {
364 matches!(self, Self::Server { number: n, .. } if *n == number)
365 }
366
367 /// Get the error class/severity if this is a server error.
368 ///
369 /// SQL Server error classes range from 0-25:
370 /// - 0-10: Informational
371 /// - 11-16: User errors
372 /// - 17-19: Resource/hardware errors
373 /// - 20-25: System errors (connection terminating)
374 #[must_use]
375 pub fn class(&self) -> Option<u8> {
376 match self {
377 Self::Server { class, .. } => Some(*class),
378 _ => None,
379 }
380 }
381
382 /// Alias for `class()` - returns error severity.
383 #[must_use]
384 pub fn severity(&self) -> Option<u8> {
385 self.class()
386 }
387}
388
389/// Format the server/procedure/line suffix for server error Display.
390fn format_server_location(
391 server: &Option<String>,
392 procedure: &Option<String>,
393 line: &u32,
394) -> String {
395 let mut parts = Vec::new();
396 if let Some(srv) = server {
397 if !srv.is_empty() {
398 parts.push(format!("server: {srv}"));
399 }
400 }
401 if let Some(proc) = procedure {
402 if !proc.is_empty() {
403 parts.push(format!("procedure: {proc}"));
404 }
405 }
406 if *line > 0 {
407 parts.push(format!("line: {line}"));
408 }
409 if parts.is_empty() {
410 String::new()
411 } else {
412 format!(" [{}]", parts.join(", "))
413 }
414}
415
416/// Result type for client operations.
417pub type Result<T> = std::result::Result<T, Error>;
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used)]
421mod tests {
422 use super::*;
423 use std::sync::Arc;
424
425 fn make_server_error(number: i32) -> Error {
426 Error::Server {
427 number,
428 class: 16,
429 state: 1,
430 message: "Test error".to_string(),
431 server: None,
432 procedure: None,
433 line: 1,
434 }
435 }
436
437 #[test]
438 fn test_is_transient_connection_errors() {
439 assert!(
440 Error::ConnectTimeout {
441 host: "test".into(),
442 port: 1433
443 }
444 .is_transient()
445 );
446 assert!(
447 Error::TlsTimeout {
448 host: "test".into(),
449 port: 1433
450 }
451 .is_transient()
452 );
453 assert!(
454 Error::LoginTimeout {
455 host: "test".into(),
456 port: 1433
457 }
458 .is_transient()
459 );
460 assert!(Error::CommandTimeout.is_transient());
461 assert!(Error::ConnectionClosed.is_transient());
462 assert!(Error::PoolExhausted.is_transient());
463 assert!(
464 Error::Routing {
465 host: "test".into(),
466 port: 1433,
467 }
468 .is_transient()
469 );
470 }
471
472 #[test]
473 fn test_is_transient_io_error() {
474 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
475 assert!(Error::Io(SharedIoError(Arc::new(io_err))).is_transient());
476 }
477
478 #[test]
479 fn test_is_transient_server_errors_deadlock() {
480 // 1205 - Deadlock victim
481 assert!(make_server_error(1205).is_transient());
482 }
483
484 #[test]
485 fn test_is_transient_server_errors_timeout() {
486 // -2 - Timeout
487 assert!(make_server_error(-2).is_transient());
488 }
489
490 #[test]
491 fn test_is_transient_server_errors_azure() {
492 // Azure-specific transient errors
493 assert!(make_server_error(10928).is_transient()); // Resource limit
494 assert!(make_server_error(10929).is_transient()); // Resource limit
495 assert!(make_server_error(40197).is_transient()); // Service error
496 assert!(make_server_error(40501).is_transient()); // Service busy
497 assert!(make_server_error(40613).is_transient()); // Database unavailable
498 assert!(make_server_error(49918).is_transient()); // Cannot process request
499 assert!(make_server_error(49919).is_transient()); // Cannot process create/update
500 assert!(make_server_error(49920).is_transient()); // Cannot process request
501 }
502
503 #[test]
504 fn test_is_transient_server_errors_other() {
505 // Other transient errors
506 assert!(make_server_error(4060).is_transient()); // Cannot open database
507 assert!(make_server_error(18456).is_transient()); // Login failed (Azure failover)
508 }
509
510 #[test]
511 fn test_is_not_transient() {
512 // Non-transient errors
513 assert!(!Error::Config("bad config".into()).is_transient());
514 assert!(!Error::Query("syntax error".into()).is_transient());
515 assert!(!Error::InvalidIdentifier("bad id".into()).is_transient());
516 assert!(!make_server_error(102).is_transient()); // Syntax error
517 }
518
519 #[test]
520 fn test_is_terminal_server_errors() {
521 // Terminal SQL errors per ADR-009
522 assert!(make_server_error(102).is_terminal()); // Syntax error
523 assert!(make_server_error(207).is_terminal()); // Invalid column
524 assert!(make_server_error(208).is_terminal()); // Invalid object
525 assert!(make_server_error(547).is_terminal()); // Constraint violation
526 assert!(make_server_error(2627).is_terminal()); // Unique constraint violation
527 assert!(make_server_error(2601).is_terminal()); // Duplicate key
528 }
529
530 #[test]
531 fn test_is_terminal_config_errors() {
532 assert!(Error::Config("bad config".into()).is_terminal());
533 assert!(Error::InvalidIdentifier("bad id".into()).is_terminal());
534 }
535
536 #[test]
537 fn test_is_not_terminal() {
538 // Non-terminal errors (may be transient or other)
539 assert!(
540 !Error::ConnectTimeout {
541 host: "test".into(),
542 port: 1433
543 }
544 .is_terminal()
545 );
546 assert!(!make_server_error(1205).is_terminal()); // Deadlock - transient, not terminal
547 assert!(!make_server_error(40501).is_terminal()); // Service busy - transient
548 }
549
550 #[test]
551 fn test_transient_server_error_static() {
552 // Test the static helper function
553 assert!(Error::is_transient_server_error(1205));
554 assert!(Error::is_transient_server_error(40501));
555 assert!(!Error::is_transient_server_error(102));
556 }
557
558 #[test]
559 fn test_terminal_server_error_static() {
560 // Test the static helper function
561 assert!(Error::is_terminal_server_error(102));
562 assert!(Error::is_terminal_server_error(2627));
563 assert!(!Error::is_terminal_server_error(1205));
564 }
565
566 #[test]
567 fn test_error_class() {
568 let err = make_server_error(102);
569 assert_eq!(err.class(), Some(16));
570 assert_eq!(err.severity(), Some(16));
571
572 assert_eq!(
573 Error::ConnectTimeout {
574 host: "test".into(),
575 port: 1433
576 }
577 .class(),
578 None
579 );
580 }
581
582 #[test]
583 fn test_is_server_error() {
584 let err = make_server_error(102);
585 assert!(err.is_server_error(102));
586 assert!(!err.is_server_error(103));
587
588 assert!(
589 !Error::ConnectTimeout {
590 host: "test".into(),
591 port: 1433
592 }
593 .is_server_error(102)
594 );
595 }
596}