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
8pub type Result<T, E = OdbcError> = std::result::Result<T, E>;
10
11#[derive(Debug, thiserror::Error)]
13pub enum OdbcError {
14 #[error(transparent)]
16 Database(#[from] OdbcDatabaseError),
17
18 #[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#[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 pub fn message(&self) -> &str {
84 &self.message
85 }
86
87 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}