use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("transport: {0}")]
Transport(#[source] TransportError),
#[error(transparent)]
Server(ServerError),
#[error("authentication failed: {0}")]
Auth(String),
#[error("protocol: {0}")]
Protocol(#[source] ProtocolError),
#[error("decode column {column:?}: {source}")]
Decode {
column: Option<String>,
#[source]
source: DecodeError,
},
#[error("pool exhausted (timeout {timeout:?})")]
PoolExhausted {
timeout: Duration,
},
#[error("operation cancelled")]
Cancelled,
#[error("internal error: {0}")]
Internal(String),
}
#[derive(Debug, thiserror::Error)]
pub enum TransportError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("connection closed by peer")]
Closed,
}
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("malformed JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("response correlation mismatch: expected id {expected}, got {got}")]
CorrelationMismatch {
expected: String,
got: String,
},
#[error("unknown response type: {0}")]
UnknownResponseType(String),
}
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
#[error("serde: {0}")]
Serde(String),
#[error("column not found: {0}")]
MissingColumn(String),
}
#[derive(Debug, Clone)]
pub struct ServerError {
pub message: String,
pub sqlstate: Option<String>,
pub sqlcode: Option<i32>,
pub job_name: Option<String>,
pub diagnostics: Vec<DiagnosticItem>,
}
impl std::fmt::Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.job_name {
Some(job) => write!(
f,
"server error [sqlstate={:?} sqlcode={:?} job={job}]: {}",
self.sqlstate, self.sqlcode, self.message
),
None => write!(
f,
"server error [sqlstate={:?} sqlcode={:?}]: {}",
self.sqlstate, self.sqlcode, self.message
),
}
}
}
impl std::error::Error for ServerError {}
impl ServerError {
#[must_use]
pub fn is_transient(&self) -> bool {
match self.sqlstate.as_deref() {
Some(s) if s.starts_with("08") => true,
Some("40001" | "57033") => true,
_ => false,
}
}
#[must_use]
pub fn is_constraint_violation(&self) -> bool {
self.sqlstate
.as_deref()
.is_some_and(|s| s.starts_with("23"))
}
#[must_use]
pub fn is_authorization(&self) -> bool {
match self.sqlstate.as_deref() {
Some(s) if s.starts_with("28") => true,
Some("42501") => true,
_ => false,
}
}
#[must_use]
pub fn is_object_not_found(&self) -> bool {
matches!(self.sqlstate.as_deref(), Some("42704" | "42S02"))
}
#[must_use]
pub fn is_data_type_mismatch(&self) -> bool {
self.sqlstate
.as_deref()
.is_some_and(|s| s.starts_with("22"))
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticItem {
pub message_id: Option<String>,
pub text: String,
}
impl From<TransportError> for Error {
fn from(value: TransportError) -> Self {
Self::Transport(value)
}
}
impl From<ProtocolError> for Error {
fn from(value: ProtocolError) -> Self {
Self::Protocol(value)
}
}
impl From<ServerError> for Error {
fn from(value: ServerError) -> Self {
Self::Server(value)
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Transport(TransportError::Io(value))
}
}
impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Self {
Self::Protocol(ProtocolError::Json(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn srv(sqlstate: Option<&str>) -> ServerError {
ServerError {
message: "x".into(),
sqlstate: sqlstate.map(String::from),
sqlcode: None,
job_name: None,
diagnostics: vec![],
}
}
#[test]
fn is_transient_classifies() {
assert!(srv(Some("08001")).is_transient());
assert!(srv(Some("08S01")).is_transient());
assert!(srv(Some("40001")).is_transient());
assert!(srv(Some("57033")).is_transient());
assert!(!srv(Some("23000")).is_transient());
assert!(!srv(None).is_transient());
}
#[test]
fn is_constraint_violation_classifies() {
assert!(srv(Some("23000")).is_constraint_violation());
assert!(srv(Some("23505")).is_constraint_violation());
assert!(!srv(Some("22000")).is_constraint_violation());
}
#[test]
fn is_authorization_classifies() {
assert!(srv(Some("28000")).is_authorization());
assert!(srv(Some("42501")).is_authorization());
assert!(!srv(Some("23000")).is_authorization());
}
#[test]
fn is_object_not_found_classifies() {
assert!(srv(Some("42704")).is_object_not_found());
assert!(srv(Some("42S02")).is_object_not_found());
assert!(!srv(Some("42501")).is_object_not_found());
}
#[test]
fn is_data_type_mismatch_classifies() {
assert!(srv(Some("22001")).is_data_type_mismatch());
assert!(srv(Some("22018")).is_data_type_mismatch());
assert!(!srv(Some("23000")).is_data_type_mismatch());
}
#[test]
fn server_error_display() {
let e = srv(Some("23505"));
let s = format!("{e}");
assert!(s.contains("23505"));
assert!(s.contains('x'));
}
#[test]
fn server_error_display_includes_job_name_when_present() {
let mut e = srv(Some("23505"));
e.job_name = Some("QZDASOINIT/QUSER/123456".into());
let s = format!("{e}");
assert!(s.contains("QZDASOINIT/QUSER/123456"));
assert!(s.contains("23505"));
}
#[test]
fn error_server_display_is_transparent() {
let inner = srv(Some("23505"));
let err: Error = inner.into();
let s = format!("{err}");
assert!(
s.contains("23505"),
"expected sqlstate in transparent display, got: {s}"
);
}
#[test]
fn from_io_error_classifies_as_transport() {
let io = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "bye");
let err: Error = io.into();
assert!(matches!(err, Error::Transport(TransportError::Io(_))));
}
#[test]
fn from_serde_json_error_classifies_as_protocol() {
let parse_err = serde_json::from_str::<i32>("not json").unwrap_err();
let err: Error = parse_err.into();
assert!(matches!(err, Error::Protocol(ProtocolError::Json(_))));
}
}