use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StatementKind {
Query,
Batch,
StoredProcedure,
TransactionControl,
}
impl fmt::Display for StatementKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
StatementKind::Query => "query",
StatementKind::Batch => "batch",
StatementKind::StoredProcedure => "stored procedure",
StatementKind::TransactionControl => "transaction control statement",
})
}
}
struct Statement<'a>(&'a str);
impl Statement<'_> {
const MAX_CHARS: usize = 200;
}
impl fmt::Display for Statement<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = self.0.split_whitespace().collect::<Vec<_>>().join(" ");
if text.is_empty() {
return Ok(());
}
if text.chars().count() > Self::MAX_CHARS {
let head: String = text.chars().take(Self::MAX_CHARS).collect();
write!(f, " [{head}...]")
} else {
write!(f, " [{text}]")
}
}
}
struct Detail<'a>(&'a tds_driver::error::Error);
impl fmt::Display for Detail<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
tds_driver::error::Error::Server(token) => {
write!(f, ": {}", token.message())?;
if !token.procedure().is_empty() {
write!(f, " (in {})", token.procedure())?;
}
write!(
f,
" [error {}, state {}, severity {}, line {}]",
token.code(),
token.state(),
token.class(),
token.line()
)
}
other => write!(f, ": {other}"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("failed to connect to {server}{}", Detail(.source))]
Connect {
server: String,
#[source]
source: Box<tds_driver::error::Error>,
},
#[error("network I/O error")]
Io(#[from] std::io::Error),
#[error("{kind} failed{}{}", Statement(.statement), Detail(.source))]
#[non_exhaustive]
Query {
kind: StatementKind,
statement: String,
parameters: usize,
#[source]
source: Box<tds_driver::error::Error>,
},
#[error("column '{0}' not found")]
ColumnNotFound(String),
#[error("column index {index} out of range ({len} columns)")]
ColumnIndexOutOfRange {
index: usize,
len: usize,
},
#[error("expected exactly one row, found {found}")]
UnexpectedRowCount {
found: usize,
},
#[error("cannot convert column '{column}' from {actual} to {target}")]
Conversion {
column: String,
actual: &'static str,
target: &'static str,
},
#[error("invalid decimal from server (value {value}, scale {scale})")]
Decimal {
value: i128,
scale: u8,
},
#[error("invalid configuration: {0}")]
Config(String),
#[error("connection is closed")]
ConnectionClosed,
#[error("the blocking client cannot be used from inside an async runtime")]
BlockingInAsync,
#[error("invalid savepoint name: {0:?}")]
InvalidSavepointName(String),
}
impl Error {
pub(crate) fn with_statement(
self,
kind: StatementKind,
statement: &str,
parameters: usize,
) -> Self {
match self {
Error::Query {
statement: known,
source,
..
} if known.is_empty() => Error::Query {
kind,
statement: statement.to_string(),
parameters,
source,
},
other => other,
}
}
pub fn statement(&self) -> Option<&str> {
match self {
Error::Query { statement, .. } if !statement.is_empty() => Some(statement),
_ => None,
}
}
pub fn statement_kind(&self) -> Option<StatementKind> {
match self {
Error::Query { kind, .. } => Some(*kind),
_ => None,
}
}
pub fn parameter_count(&self) -> Option<usize> {
match self {
Error::Query { parameters, .. } => Some(*parameters),
_ => None,
}
}
fn token(&self) -> Option<&tds_driver::error::TokenError> {
let source = match self {
Error::Connect { source, .. } | Error::Query { source, .. } => source.as_ref(),
_ => return None,
};
match source {
tds_driver::error::Error::Server(token) => Some(token),
_ => None,
}
}
pub fn code(&self) -> Option<u32> {
self.token().map(|t| t.code())
}
pub fn server_message(&self) -> Option<&str> {
self.token().map(|t| t.message())
}
pub fn severity(&self) -> Option<u8> {
self.token().map(|t| t.class())
}
pub fn state(&self) -> Option<u8> {
self.token().map(|t| t.state())
}
pub fn line(&self) -> Option<u32> {
self.token().map(|t| t.line())
}
pub fn procedure(&self) -> Option<&str> {
self.token()
.map(|t| t.procedure())
.filter(|p| !p.is_empty())
}
pub fn server_name(&self) -> Option<&str> {
self.token().map(|t| t.server()).filter(|s| !s.is_empty())
}
pub fn is_deadlock(&self) -> bool {
self.code() == Some(1205)
}
}
impl From<tds_driver::error::Error> for Error {
fn from(e: tds_driver::error::Error) -> Self {
Error::Query {
kind: StatementKind::Query,
statement: String::new(),
parameters: 0,
source: Box::new(e),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
fn assert_traits<T: std::error::Error + Send + Sync + 'static>() {}
#[test]
fn error_is_send_sync_static() {
assert_traits::<Error>();
}
#[test]
fn messages_carry_context() {
let e = Error::ColumnNotFound("id".into());
assert_eq!(e.to_string(), "column 'id' not found");
let e = Error::UnexpectedRowCount { found: 3 };
assert_eq!(e.to_string(), "expected exactly one row, found 3");
let e = Error::Conversion {
column: "amount".into(),
actual: "Text",
target: "i32",
};
assert_eq!(
e.to_string(),
"cannot convert column 'amount' from Text to i32"
);
}
fn driver_error() -> tds_driver::error::Error {
tds_driver::error::Error::Protocol("stream desync".into())
}
fn query_error(kind: StatementKind, statement: &str, parameters: usize) -> Error {
Error::from(driver_error()).with_statement(kind, statement, parameters)
}
#[test]
fn query_error_names_the_statement() {
let e = query_error(
StatementKind::Query,
"SELECT id FROM users WHERE id > @P1",
1,
);
assert_eq!(
e.to_string(),
"query failed [SELECT id FROM users WHERE id > @P1]: Protocol error: stream desync"
);
assert_eq!(e.statement(), Some("SELECT id FROM users WHERE id > @P1"));
assert_eq!(e.statement_kind(), Some(StatementKind::Query));
assert_eq!(e.parameter_count(), Some(1));
}
#[test]
fn statement_kind_names_the_route() {
let e = query_error(StatementKind::StoredProcedure, "dbo.sp_upsert_order", 2);
assert!(
e.to_string()
.starts_with("stored procedure failed [dbo.sp_upsert_order]")
);
let e = query_error(StatementKind::Batch, "CREATE VIEW v AS SELECT 1 AS x", 0);
assert!(e.to_string().starts_with("batch failed ["));
let e = query_error(StatementKind::TransactionControl, "COMMIT TRANSACTION", 0);
assert!(
e.to_string()
.starts_with("transaction control statement failed [")
);
}
#[test]
fn multi_line_statements_collapse_onto_one_line() {
let e = query_error(StatementKind::Batch, "SELECT 1\n FROM dual\n", 0);
assert_eq!(
e.to_string(),
"batch failed [SELECT 1 FROM dual]: Protocol error: stream desync"
);
}
#[test]
fn long_statements_are_truncated_in_the_message() {
let long = format!("SELECT {}", "x".repeat(500));
let e = query_error(StatementKind::Query, &long, 0);
let message = e.to_string();
assert!(message.contains("..."), "{message}");
assert!(message.len() < long.len(), "{message}");
assert_eq!(e.statement(), Some(long.as_str()));
}
#[test]
fn context_is_only_filled_in_once() {
let e = query_error(StatementKind::Query, "SELECT 1", 0).with_statement(
StatementKind::Batch,
"SELECT 2",
7,
);
assert_eq!(e.statement(), Some("SELECT 1"));
assert_eq!(e.parameter_count(), Some(0));
let e = Error::UnexpectedRowCount { found: 3 }.with_statement(
StatementKind::Query,
"SELECT 1",
0,
);
assert!(matches!(e, Error::UnexpectedRowCount { found: 3 }));
}
#[test]
fn server_fields_are_absent_when_the_server_never_answered() {
let e = query_error(StatementKind::Query, "SELECT 1", 0);
assert_eq!(e.code(), None);
assert_eq!(e.server_message(), None);
assert_eq!(e.severity(), None);
assert_eq!(e.state(), None);
assert_eq!(e.line(), None);
assert_eq!(e.procedure(), None);
assert_eq!(e.server_name(), None);
assert!(!e.is_deadlock());
let e = Error::ConnectionClosed;
assert_eq!(e.code(), None);
assert_eq!(e.statement(), None);
assert_eq!(e.statement_kind(), None);
assert_eq!(e.parameter_count(), None);
}
#[test]
fn connect_errors_carry_the_driver_detail() {
let e = Error::Connect {
server: "localhost:1433".into(),
source: Box::new(driver_error()),
};
assert_eq!(
e.to_string(),
"failed to connect to localhost:1433: Protocol error: stream desync"
);
}
}