pub mod bulk_copy_errors;
use std::path::PathBuf;
pub use bulk_copy_errors::{BulkCopyAttentionTimeoutError, BulkCopyError, BulkCopyTimeoutError};
use crate::security::SecurityError;
use thiserror::Error;
use tokio::time::error::Elapsed;
#[derive(Debug, Clone)]
pub struct SqlErrorInfo {
pub message: String,
pub state: u8,
pub class: i32,
pub number: u32,
pub server_name: Option<String>,
pub proc_name: Option<String>,
pub line_number: Option<i32>,
}
impl std::fmt::Display for SqlErrorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Sql Error: {}: Class {}: State {}: {} on {} in {} at line {}",
self.number,
self.class,
self.state,
self.message,
self.server_name.as_deref().unwrap_or("Unknown"),
self.proc_name.as_deref().unwrap_or("Unknown"),
self.line_number.unwrap_or_default()
)
}
}
impl From<&crate::token::tokens::ErrorToken> for SqlErrorInfo {
fn from(token: &crate::token::tokens::ErrorToken) -> Self {
Self {
message: token.message.clone(),
state: token.state,
class: token.severity as i32,
number: token.number,
server_name: Some(token.server_name.clone()),
proc_name: Some(token.proc_name.clone()),
line_number: Some(token.line_number as i32),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SqlInfoMessage {
pub message: String,
pub state: u8,
pub class: i32,
pub number: u32,
pub server_name: Option<String>,
pub proc_name: Option<String>,
pub line_number: Option<i32>,
}
impl From<&crate::token::tokens::InfoToken> for SqlInfoMessage {
fn from(token: &crate::token::tokens::InfoToken) -> Self {
Self {
message: token.message.clone(),
state: token.state,
class: token.severity as i32,
number: token.number,
server_name: Some(token.server_name.clone()),
proc_name: Some(token.proc_name.clone()),
line_number: Some(token.line_number as i32),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SqlServerDiagnostics {
pub errors: Vec<SqlErrorInfo>,
pub info_messages: Vec<SqlInfoMessage>,
}
impl SqlServerDiagnostics {
pub fn new(errors: Vec<SqlErrorInfo>, info_messages: Vec<SqlInfoMessage>) -> Self {
Self {
errors,
info_messages,
}
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty() && self.info_messages.is_empty()
}
}
#[derive(Debug, Error)]
pub enum TimeoutErrorType {
#[error("Elapsed: {0}")]
Elapsed(Elapsed),
#[error("{0}")]
String(String),
}
#[derive(Debug, Error)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Server redirected the connection: {host}:{port} times")]
Redirection {
host: String,
port: u16,
},
#[error("Connection Error: {0}")]
ConnectionError(String),
#[error("Protocol Error: {0}")]
ProtocolError(String),
#[error("TLS Error: {0}")]
TlsError(#[from] native_tls::Error),
#[error(
"TLS handshake failed while connecting to '{expected_host}': {source}. Certificate SANs: {cert_sans}"
)]
TlsHandshakeError {
source: native_tls::Error,
expected_host: String,
cert_sans: String,
},
#[error("Timeout Error: {0}")]
TimeoutError(TimeoutErrorType),
#[error("Operation Cancelled Error: {0}")]
OperationCancelledError(String),
#[error("{}", SqlServerError::format_errors(&diagnostics.errors))]
SqlServerError {
diagnostics: SqlServerDiagnostics,
},
#[error("Usage Error: {0}")]
UsageError(String),
#[error("Unexpected Implementation Error: {0}")]
ImplementationError(String),
#[error("Unimplemented Feature: {feature} - {context}")]
UnimplementedFeature {
feature: String,
context: String,
},
#[error("Type Conversion Error: {0}")]
TypeConversionError(String),
#[error("Connection closed: {0}")]
ConnectionClosed(String),
#[error(
"Unsupported Encoding: LCID {lcid} (0x{lcid:04X}). Consider using NVARCHAR instead of VARCHAR/TEXT for better compatibility."
)]
UnsupportedEncoding {
lcid: u32,
},
#[error(
"Certificate file not found: {path}. Verify the ServerCertificate path is correct and the file exists."
)]
CertificateNotFound {
path: PathBuf,
},
#[error(
"Invalid certificate format in file: {path}. Ensure the file contains a valid DER or PEM encoded X.509 certificate."
)]
InvalidCertificateFormat {
path: PathBuf,
},
#[error(
"Server certificate has expired. The server's certificate is no longer valid. Contact your administrator."
)]
CertificateExpired,
#[error(
"Server certificate validation failed: Certificate mismatch. The server presented a different certificate than expected. Verify you are connecting to the correct server."
)]
CertificateMismatch,
#[error(
"Failed to read certificate file: {path}. Error: {error}. Check file permissions and ensure the file is not locked by another process."
)]
CertificateFileIoError {
path: PathBuf,
error: String,
},
#[error("No server certificate available during TLS handshake.")]
NoServerCertificate,
#[error("Bulk Copy Error: {0}")]
BulkCopyError(#[from] BulkCopyError),
#[error("Security error: {0}")]
Security(#[from] SecurityError),
#[error("Session recovery failed after {attempts} attempt(s): {message}")]
SessionRecoveryFailed {
attempts: u32,
message: String,
},
#[error("Session not recoverable: {0}")]
SessionNotRecoverable(String),
#[error("Reconnection validation failed: {0}")]
ReconnectionValidationFailed(String),
#[error("Column encryption error: {0}")]
ColumnEncryptionError(String),
#[error(
"The connection reset was not acknowledged by the server, so the session was not returned to its login defaults"
)]
ConnectionResetNotAcknowledged,
}
struct SqlServerError;
impl SqlServerError {
fn format_errors(errors: &[SqlErrorInfo]) -> String {
match errors.len() {
0 => "Sql Error: (no error details)".to_string(),
1 => errors[0].to_string(),
_ => errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n"),
}
}
}
const TRANSIENT_SQL_ERROR_NUMBERS: &[u32] = &[
233, 4060, 4221, 10928, 10929, 40143, 40197, 40501, 40540, 40613, 42108, 42109, 49918, 49919, 49920, ];
impl Error {
pub fn from_sql_error(error: SqlErrorInfo) -> Self {
Error::SqlServerError {
diagnostics: SqlServerDiagnostics::new(vec![error], Vec::new()),
}
}
pub fn from_sql_errors(errors: Vec<SqlErrorInfo>) -> Self {
Error::SqlServerError {
diagnostics: SqlServerDiagnostics::new(errors, Vec::new()),
}
}
pub fn from_sql_diagnostics(diagnostics: SqlServerDiagnostics) -> Self {
Error::SqlServerError { diagnostics }
}
pub(crate) fn is_transient_connect_error(&self) -> bool {
match self {
Error::Io(_)
| Error::ConnectionError(_)
| Error::TimeoutError(_)
| Error::ConnectionClosed(_) => true,
Error::SqlServerError { diagnostics } => diagnostics
.errors
.iter()
.any(|e| TRANSIENT_SQL_ERROR_NUMBERS.contains(&e.number)),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_timeout_error_type_string() {
let error = TimeoutErrorType::String("Test timeout".to_string());
assert_eq!(error.to_string(), "Test timeout");
}
#[test]
fn test_timeout_error_type_elapsed() {
let rt = tokio::runtime::Runtime::new().unwrap();
let elapsed = rt.block_on(async {
tokio::time::timeout(std::time::Duration::from_millis(1), async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
})
.await
.unwrap_err()
});
let error = TimeoutErrorType::Elapsed(elapsed);
assert!(error.to_string().contains("Elapsed"));
}
#[test]
fn test_io_error_conversion() {
let io_error = io::Error::new(io::ErrorKind::ConnectionRefused, "Connection refused");
let error = Error::from(io_error);
match error {
Error::Io(e) => assert_eq!(e.kind(), io::ErrorKind::ConnectionRefused),
_ => panic!("Expected IO error"),
}
}
#[test]
fn test_redirection_error() {
let error = Error::Redirection {
host: "example.com".to_string(),
port: 1433,
};
assert!(error.to_string().contains("example.com"));
assert!(error.to_string().contains("1433"));
}
#[test]
fn test_protocol_error() {
let error = Error::ProtocolError("Invalid packet".to_string());
assert_eq!(error.to_string(), "Protocol Error: Invalid packet");
}
#[test]
fn test_timeout_error() {
let timeout_type = TimeoutErrorType::String("Query timeout".to_string());
let error = Error::TimeoutError(timeout_type);
assert!(error.to_string().contains("Query timeout"));
}
#[test]
fn test_operation_cancelled_error() {
let error = Error::OperationCancelledError("User cancelled".to_string());
assert!(error.to_string().contains("User cancelled"));
}
#[test]
fn test_sql_server_error_full() {
let error = Error::from_sql_error(SqlErrorInfo {
message: "Login failed".to_string(),
state: 1,
class: 14,
number: 18456,
server_name: Some("SQLSERVER01".to_string()),
proc_name: Some("sp_login".to_string()),
line_number: Some(42),
});
let err_str = error.to_string();
assert!(err_str.contains("18456"));
assert!(err_str.contains("Login failed"));
assert!(err_str.contains("SQLSERVER01"));
assert!(err_str.contains("sp_login"));
assert!(err_str.contains("42"));
}
#[test]
fn test_sql_server_error_with_none_values() {
let error = Error::from_sql_error(SqlErrorInfo {
message: "Error occurred".to_string(),
state: 2,
class: 16,
number: 50000,
server_name: None,
proc_name: None,
line_number: None,
});
let err_str = error.to_string();
assert!(err_str.contains("50000"));
assert!(err_str.contains("Error occurred"));
assert!(err_str.contains("Unknown"));
}
#[test]
fn test_sql_server_error_multiple() {
let error = Error::from_sql_errors(vec![
SqlErrorInfo {
message: "First error".to_string(),
state: 1,
class: 16,
number: 50000,
server_name: Some("SRV".to_string()),
proc_name: None,
line_number: Some(1),
},
SqlErrorInfo {
message: "Second error".to_string(),
state: 1,
class: 16,
number: 50001,
server_name: Some("SRV".to_string()),
proc_name: None,
line_number: Some(2),
},
]);
let err_str = error.to_string();
assert!(err_str.contains("First error"));
assert!(err_str.contains("Second error"));
assert!(err_str.contains("50000"));
assert!(err_str.contains("50001"));
}
#[test]
fn test_usage_error() {
let error = Error::UsageError("Invalid connection string".to_string());
assert_eq!(error.to_string(), "Usage Error: Invalid connection string");
}
#[test]
fn test_implementation_error() {
let error = Error::ImplementationError("Not implemented yet".to_string());
assert_eq!(
error.to_string(),
"Unexpected Implementation Error: Not implemented yet"
);
}
#[test]
fn test_unimplemented_feature() {
let error = Error::UnimplementedFeature {
feature: "Always Encrypted".to_string(),
context: "Column encryption not supported".to_string(),
};
let err_str = error.to_string();
assert!(err_str.contains("Always Encrypted"));
assert!(err_str.contains("Column encryption not supported"));
}
#[test]
fn test_type_conversion_error() {
let error = Error::TypeConversionError("Cannot convert VARCHAR to INT".to_string());
assert_eq!(
error.to_string(),
"Type Conversion Error: Cannot convert VARCHAR to INT"
);
}
#[test]
fn test_error_debug_format() {
let error = Error::ProtocolError("Test".to_string());
let debug_str = format!("{error:?}");
assert!(debug_str.contains("ProtocolError"));
}
#[test]
fn io_error_is_transient() {
let err = Error::Io(io::Error::new(io::ErrorKind::ConnectionRefused, "refused"));
assert!(err.is_transient_connect_error());
}
#[test]
fn connection_error_is_transient() {
let err = Error::ConnectionError("failed to connect".to_string());
assert!(err.is_transient_connect_error());
}
#[test]
fn timeout_error_is_transient() {
let err = Error::TimeoutError(TimeoutErrorType::String("timed out".to_string()));
assert!(err.is_transient_connect_error());
}
#[test]
fn connection_closed_is_transient() {
let err = Error::ConnectionClosed("reset by peer".to_string());
assert!(err.is_transient_connect_error());
}
#[test]
fn permanent_sql_server_error_is_not_transient() {
let err = Error::from_sql_error(SqlErrorInfo {
message: "Login failed".to_string(),
state: 1,
class: 14,
number: 18456,
server_name: None,
proc_name: None,
line_number: None,
});
assert!(!err.is_transient_connect_error());
}
#[test]
fn transient_sql_server_errors_are_retried() {
for &code in &[
233, 4060, 4221, 10928, 10929, 40143, 40197, 40501, 40540, 40613, 42108, 42109, 49918,
49919, 49920,
] {
let err = Error::from_sql_error(SqlErrorInfo {
message: "transient".to_string(),
state: 1,
class: 16,
number: code,
server_name: None,
proc_name: None,
line_number: None,
});
assert!(
err.is_transient_connect_error(),
"SQL error {code} should be transient"
);
}
}
#[test]
fn mixed_sql_errors_transient_if_any_match() {
let err = Error::from_sql_errors(vec![
SqlErrorInfo {
message: "Login failed".to_string(),
state: 1,
class: 14,
number: 18456,
server_name: None,
proc_name: None,
line_number: None,
},
SqlErrorInfo {
message: "Service busy".to_string(),
state: 1,
class: 16,
number: 40501,
server_name: None,
proc_name: None,
line_number: None,
},
]);
assert!(err.is_transient_connect_error());
}
#[test]
fn protocol_error_is_not_transient() {
let err = Error::ProtocolError("bad packet".to_string());
assert!(!err.is_transient_connect_error());
}
#[test]
fn operation_cancelled_is_not_transient() {
let err = Error::OperationCancelledError("cancelled".to_string());
assert!(!err.is_transient_connect_error());
}
#[test]
fn usage_error_is_not_transient() {
let err = Error::UsageError("bad param".to_string());
assert!(!err.is_transient_connect_error());
}
#[test]
fn security_error_is_not_transient() {
let err = Error::Security(SecurityError::NotSupported("SSPI".to_string()));
assert!(!err.is_transient_connect_error());
}
#[test]
fn authentication_denied_is_not_transient() {
let err = Error::Security(SecurityError::AuthenticationDenied(
"access_denied".to_string(),
));
assert!(!err.is_transient_connect_error());
}
fn sample_error(number: u32, message: &str) -> SqlErrorInfo {
SqlErrorInfo {
message: message.to_string(),
state: 1,
class: 16,
number,
server_name: None,
proc_name: None,
line_number: None,
}
}
fn sample_info(number: u32, message: &str) -> SqlInfoMessage {
SqlInfoMessage {
message: message.to_string(),
state: 1,
class: 10,
number,
server_name: None,
proc_name: None,
line_number: None,
}
}
#[test]
fn diagnostics_helpers_report_contents() {
let empty = SqlServerDiagnostics::default();
assert!(empty.is_empty());
assert!(!empty.has_errors());
let info_only = SqlServerDiagnostics::new(vec![], vec![sample_info(5701, "db context")]);
assert!(!info_only.is_empty());
assert!(!info_only.has_errors());
let with_errors =
SqlServerDiagnostics::new(vec![sample_error(18456, "login failed")], vec![]);
assert!(!with_errors.is_empty());
assert!(with_errors.has_errors());
}
#[test]
fn from_sql_diagnostics_preserves_errors_and_info() {
let diagnostics = SqlServerDiagnostics::new(
vec![sample_error(18456, "login failed")],
vec![sample_info(5701, "changed database context")],
);
let err = Error::from_sql_diagnostics(diagnostics);
match &err {
Error::SqlServerError { diagnostics } => {
assert_eq!(diagnostics.errors.len(), 1);
assert_eq!(diagnostics.errors[0].number, 18456);
assert_eq!(diagnostics.info_messages.len(), 1);
assert_eq!(diagnostics.info_messages[0].number, 5701);
}
other => panic!("Expected SqlServerError, got: {other:?}"),
}
assert!(err.to_string().contains("login failed"));
}
#[test]
fn diagnostics_transient_detection_reads_errors() {
let err = Error::from_sql_diagnostics(SqlServerDiagnostics::new(
vec![sample_error(40501, "service busy")],
vec![sample_info(5701, "db context")],
));
assert!(err.is_transient_connect_error());
}
}