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