Skip to main content

sqlx_odbc/
error.rs

1use odbc_api::{
2    handles::{slice_to_cow_utf8, Record},
3    Error as OdbcApiError,
4};
5use std::borrow::Cow;
6use std::fmt::{Display, Formatter, Result as FmtResult};
7
8/// Result alias for this crate.
9pub type Result<T, E = OdbcError> = std::result::Result<T, E>;
10
11/// Error type returned by this crate while the SQLx driver port is in progress.
12#[derive(Debug, thiserror::Error)]
13pub enum OdbcError {
14    /// ODBC driver-manager or database error.
15    #[error(transparent)]
16    Database(#[from] OdbcDatabaseError),
17
18    /// Invalid local configuration.
19    #[error("ODBC configuration error: {0}")]
20    Configuration(String),
21}
22
23impl From<OdbcApiError> for OdbcError {
24    fn from(error: OdbcApiError) -> Self {
25        Self::Database(OdbcDatabaseError::from(error))
26    }
27}
28
29impl From<OdbcError> for sqlx_core::Error {
30    fn from(error: OdbcError) -> Self {
31        match error {
32            OdbcError::Database(error) => sqlx_core::Error::Database(Box::new(error)),
33            OdbcError::Configuration(message) => sqlx_core::Error::Configuration(message.into()),
34        }
35    }
36}
37
38/// Database error details extracted from ODBC diagnostics.
39#[derive(Debug)]
40pub struct OdbcDatabaseError {
41    error: OdbcApiError,
42    message: String,
43    code: Option<String>,
44}
45
46impl OdbcDatabaseError {
47    fn diagnostic_record(error: &OdbcApiError) -> Option<&Record> {
48        match error {
49            OdbcApiError::Diagnostics { record, .. } => Some(record),
50            OdbcApiError::InvalidRowArraySize { record, .. } => Some(record),
51            OdbcApiError::UnsupportedOdbcApiVersion(record) => Some(record),
52            OdbcApiError::UnableToRepresentNull(record) => Some(record),
53            OdbcApiError::OracleOdbcDriverDoesNotSupport64Bit(record) => Some(record),
54            _ => None,
55        }
56    }
57
58    fn diagnostic_code(record: &Record) -> Option<String> {
59        let code = record.state.as_str();
60
61        if code.as_bytes().iter().all(|&byte| byte == 0) {
62            None
63        } else {
64            Some(code.to_owned())
65        }
66    }
67
68    /// Primary diagnostic message.
69    pub fn message(&self) -> &str {
70        &self.message
71    }
72
73    /// ODBC SQLSTATE code, if available.
74    pub fn code(&self) -> Option<Cow<'_, str>> {
75        self.code.as_deref().map(Cow::Borrowed)
76    }
77}
78
79impl From<OdbcApiError> for OdbcDatabaseError {
80    fn from(error: OdbcApiError) -> Self {
81        let record = Self::diagnostic_record(&error);
82        let message = record
83            .map(|record| slice_to_cow_utf8(&record.message).into_owned())
84            .filter(|message| !message.is_empty())
85            .unwrap_or_else(|| error.to_string());
86        let code = record.and_then(Self::diagnostic_code);
87
88        Self {
89            error,
90            message,
91            code,
92        }
93    }
94}
95
96impl Display for OdbcDatabaseError {
97    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
98        Display::fmt(&self.error, f)
99    }
100}
101
102impl std::error::Error for OdbcDatabaseError {}
103
104impl sqlx_core::error::DatabaseError for OdbcDatabaseError {
105    fn message(&self) -> &str {
106        self.message()
107    }
108
109    fn code(&self) -> Option<Cow<'_, str>> {
110        self.code()
111    }
112
113    fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
114        self
115    }
116
117    fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
118        self
119    }
120
121    fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
122        self
123    }
124
125    fn kind(&self) -> sqlx_core::error::ErrorKind {
126        sqlx_core::error::ErrorKind::Other
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use odbc_api::handles::{Record, SqlChar, State};
134
135    fn sql_chars(text: &str) -> Vec<SqlChar> {
136        text.bytes().collect()
137    }
138
139    #[test]
140    fn database_error_uses_odbc_diagnostics_for_message_and_code() {
141        let error = OdbcDatabaseError::from(OdbcApiError::Diagnostics {
142            function: "SQLExecDirect",
143            record: Record {
144                state: State(*b"HY000"),
145                native_error: 1234,
146                message: sql_chars("syntax error near FROM"),
147            },
148        });
149
150        assert_eq!(error.message(), "syntax error near FROM");
151        assert_eq!(error.code().as_deref(), Some("HY000"));
152    }
153}