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
38pub(crate) fn database_error_with_context(
39    error: OdbcApiError,
40    context: impl Into<String>,
41) -> OdbcError {
42    OdbcError::Database(OdbcDatabaseError::with_context(error, context))
43}
44
45/// Database error details extracted from ODBC diagnostics.
46#[derive(Debug)]
47pub struct OdbcDatabaseError {
48    error: OdbcApiError,
49    message: String,
50    code: Option<String>,
51}
52
53impl OdbcDatabaseError {
54    fn with_context(error: OdbcApiError, context: impl Into<String>) -> Self {
55        let context = context.into();
56        let mut database_error = Self::from(error);
57        database_error.message = format!("{context}: {}", database_error.message);
58        database_error
59    }
60
61    fn diagnostic_record(error: &OdbcApiError) -> Option<&Record> {
62        match error {
63            OdbcApiError::Diagnostics { record, .. } => Some(record),
64            OdbcApiError::InvalidRowArraySize { record, .. } => Some(record),
65            OdbcApiError::UnsupportedOdbcApiVersion(record) => Some(record),
66            OdbcApiError::UnableToRepresentNull(record) => Some(record),
67            OdbcApiError::OracleOdbcDriverDoesNotSupport64Bit(record) => Some(record),
68            _ => None,
69        }
70    }
71
72    fn diagnostic_code(record: &Record) -> Option<String> {
73        let code = record.state.as_str();
74
75        if code.as_bytes().iter().all(|&byte| byte == 0) {
76            None
77        } else {
78            Some(code.to_owned())
79        }
80    }
81
82    /// Primary diagnostic message.
83    pub fn message(&self) -> &str {
84        &self.message
85    }
86
87    /// ODBC SQLSTATE code, if available.
88    pub fn code(&self) -> Option<Cow<'_, str>> {
89        self.code.as_deref().map(Cow::Borrowed)
90    }
91}
92
93impl From<OdbcApiError> for OdbcDatabaseError {
94    fn from(error: OdbcApiError) -> Self {
95        let record = Self::diagnostic_record(&error);
96        let message = record
97            .map(|record| slice_to_cow_utf8(&record.message).into_owned())
98            .filter(|message| !message.is_empty())
99            .unwrap_or_else(|| error.to_string());
100        let code = record.and_then(Self::diagnostic_code);
101
102        Self {
103            error,
104            message,
105            code,
106        }
107    }
108}
109
110impl Display for OdbcDatabaseError {
111    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
112        f.write_str(&self.message)
113    }
114}
115
116impl std::error::Error for OdbcDatabaseError {
117    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118        Some(&self.error)
119    }
120}
121
122impl sqlx_core::error::DatabaseError for OdbcDatabaseError {
123    fn message(&self) -> &str {
124        self.message()
125    }
126
127    fn code(&self) -> Option<Cow<'_, str>> {
128        self.code()
129    }
130
131    fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
132        self
133    }
134
135    fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
136        self
137    }
138
139    fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
140        self
141    }
142
143    fn kind(&self) -> sqlx_core::error::ErrorKind {
144        sqlx_core::error::ErrorKind::Other
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use odbc_api::handles::{Record, SqlChar, State};
152
153    fn sql_chars(text: &str) -> Vec<SqlChar> {
154        text.bytes().collect()
155    }
156
157    #[test]
158    fn database_error_uses_odbc_diagnostics_for_message_and_code() {
159        let error = OdbcDatabaseError::from(OdbcApiError::Diagnostics {
160            function: "SQLExecDirect",
161            record: Record {
162                state: State(*b"HY000"),
163                native_error: 1234,
164                message: sql_chars("syntax error near FROM"),
165            },
166        });
167
168        assert_eq!(error.message(), "syntax error near FROM");
169        assert_eq!(error.code().as_deref(), Some("HY000"));
170    }
171
172    #[test]
173    fn database_error_context_is_included_in_message_and_display() {
174        let error = OdbcDatabaseError::with_context(
175            OdbcApiError::Diagnostics {
176                function: "SQLSetStmtAttr",
177                record: Record {
178                    state: State(*b"HY092"),
179                    native_error: 0,
180                    message: sql_chars("invalid attribute option identifier"),
181                },
182            },
183            "ODBC buffered fetching could not be enabled",
184        );
185
186        assert_eq!(
187            error.message(),
188            "ODBC buffered fetching could not be enabled: invalid attribute option identifier"
189        );
190        assert_eq!(error.to_string(), error.message());
191        assert_eq!(error.code().as_deref(), Some("HY092"));
192    }
193}