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
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#[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 pub fn message(&self) -> &str {
91 &self.message
92 }
93
94 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}