odbc_api/handles/diagnostics.rs
1use crate::handles::slice_to_cow_utf8;
2
3use super::{
4 as_handle::AsHandle,
5 buffer::{clamp_small_int, mut_buf_ptr},
6 SqlChar,
7};
8use odbc_sys::{SqlReturn, SQLSTATE_SIZE};
9use std::fmt;
10
11// Starting with odbc 5 we may be able to specify utf8 encoding. Until then, we may need to fall
12// back on the 'W' wide function calls.
13#[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
14use odbc_sys::SQLGetDiagRecW as sql_get_diag_rec;
15
16#[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
17use odbc_sys::SQLGetDiagRec as sql_get_diag_rec;
18
19/// A buffer large enough to hold an `SOLState` for diagnostics
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub struct State(pub [u8; SQLSTATE_SIZE]);
22
23impl State {
24 /// Can be returned from SQLDisconnect
25 pub const INVALID_STATE_TRANSACTION: State = State(*b"25000");
26 /// Given the specified Attribute value, an invalid value was specified in ValuePtr.
27 pub const INVALID_ATTRIBUTE_VALUE: State = State(*b"HY024");
28 /// An invalid data type has been bound to a statement. Is also returned by SQLFetch if trying
29 /// to fetch into a 64Bit Integer Buffer.
30 pub const INVALID_SQL_DATA_TYPE: State = State(*b"HY004");
31 /// String or binary data returned for a column resulted in the truncation of nonblank character
32 /// or non-NULL binary data. If it was a string value, it was right-truncated.
33 pub const STRING_DATA_RIGHT_TRUNCATION: State = State(*b"01004");
34 /// StrLen_or_IndPtr was a null pointer and NULL data was retrieved.
35 pub const INDICATOR_VARIABLE_REQUIRED_BUT_NOT_SUPPLIED: State = State(*b"22002");
36
37 /// Drops terminating zero and changes char type, if required
38 pub fn from_chars_with_nul(code: &[SqlChar; SQLSTATE_SIZE + 1]) -> Self {
39 // `SQLGetDiagRecW` returns ODBC state as wide characters. This constructor converts the
40 // wide characters to narrow and drops the terminating zero.
41
42 let mut ascii = [0; SQLSTATE_SIZE];
43 for (index, letter) in code[..SQLSTATE_SIZE].iter().copied().enumerate() {
44 ascii[index] = letter as u8;
45 }
46 State(ascii)
47 }
48
49 /// View status code as string slice for displaying. Must always succeed as ODBC status code
50 /// always consist of ASCII characters.
51 pub fn as_str(&self) -> &str {
52 std::str::from_utf8(&self.0).unwrap()
53 }
54}
55
56/// Result of [`Diagnostic::diagnostic_record`].
57#[derive(Debug, Clone, Copy)]
58pub struct DiagnosticResult {
59 /// A five-character SQLSTATE code (and terminating NULL) for the diagnostic record
60 /// `rec_number`. The first two characters indicate the class; the next three indicate the
61 /// subclass. For more information, see [SQLSTATE][1]s.
62 /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/sqlstates
63 pub state: State,
64 /// Native error code specific to the data source.
65 pub native_error: i32,
66 /// The length of the diagnostic message reported by ODBC (excluding the terminating zero).
67 pub text_length: i16,
68}
69
70/// Report diagnostics from the last call to an ODBC function using a handle.
71pub trait Diagnostics {
72 /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
73 ///
74 /// Returns the current values of multiple fields of a diagnostic record that contains error,
75 /// warning, and status information
76 ///
77 /// See: [Diagnostic Messages][1]
78 ///
79 /// # Arguments
80 ///
81 /// * `rec_number` - Indicates the status record from which the application seeks information.
82 /// Status records are numbered from 1. Function panics for values smaller < 1.
83 /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
84 /// number of characters to return is greater than the buffer length, the message is
85 /// truncated. To determine that a truncation occurred, the application must compare the
86 /// buffer length to the actual number of bytes available, which is found in
87 /// [`self::DiagnosticResult::text_length]`
88 ///
89 /// # Result
90 ///
91 /// * `Some(rec)` - The function successfully returned diagnostic information.
92 /// message. No diagnostic records were generated.
93 /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
94 /// the specified Handle. The function also returns `NoData` for any positive `rec_number` if
95 /// there are no diagnostic records available.
96 ///
97 /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
98 fn diagnostic_record(
99 &self,
100 rec_number: i16,
101 message_text: &mut [SqlChar],
102 ) -> Option<DiagnosticResult>;
103
104 /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
105 /// This method builds on top of [`Self::diagnostic_record`], if the message does not fit in the
106 /// buffer, it will grow the message buffer and extract it again.
107 ///
108 /// See: [Diagnostic Messages][1]
109 ///
110 /// # Arguments
111 ///
112 /// * `rec_number` - Indicates the status record from which the application seeks information.
113 /// Status records are numbered from 1. Function panics for values smaller < 1.
114 /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
115 /// number of characters to return is greater than the buffer length, the buffer will be grown
116 /// to be large enough to hold it.
117 ///
118 /// # Result
119 ///
120 /// * `Some(rec)` - The function successfully returned diagnostic information.
121 /// message. No diagnostic records were generated. To determine that a truncation occurred,
122 /// the application must compare the buffer length to the actual number of bytes available,
123 /// which is found in [`self::DiagnosticResult::text_length]`.
124 /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
125 /// the specified Handle. The function also returns `NoData` for any positive `rec_number` if
126 /// there are no diagnostic records available.
127 ///
128 /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
129 fn diagnostic_record_vec(
130 &self,
131 rec_number: i16,
132 message_text: &mut Vec<SqlChar>,
133 ) -> Option<DiagnosticResult> {
134 // Use all the memory available in the buffer, but don't allocate any extra.
135 let cap = message_text.capacity();
136 message_text.resize(cap, 0);
137
138 self.diagnostic_record(rec_number, message_text)
139 .map(|mut result| {
140 let mut text_length = result.text_length.try_into().unwrap();
141
142 // Check if the buffer has been large enough to hold the message.
143 if text_length > message_text.len() {
144 // The `message_text` buffer was too small to hold the requested diagnostic message.
145 // No diagnostic records were generated. To determine that a truncation occurred,
146 // the application must compare the buffer length to the actual number of bytes
147 // available, which is found in `DiagnosticResult::text_length`.
148
149 // Resize with +1 to account for terminating zero
150 message_text.resize(text_length + 1, 0);
151
152 // Call diagnostics again with the larger buffer. Should be a success this time if
153 // driver isn't buggy.
154 result = self.diagnostic_record(rec_number, message_text).unwrap();
155 }
156 // Now `message_text` has been large enough to hold the entire message.
157
158 // Some drivers pad the message with null-chars (which is still a valid C string,
159 // but not a valid Rust string).
160 while text_length > 0 && message_text[text_length - 1] == 0 {
161 text_length -= 1;
162 }
163 // Resize Vec to hold exactly the message.
164 message_text.resize(text_length, 0);
165
166 result
167 })
168 }
169}
170
171impl<T: AsHandle + ?Sized> Diagnostics for T {
172 fn diagnostic_record(
173 &self,
174 rec_number: i16,
175 message_text: &mut [SqlChar],
176 ) -> Option<DiagnosticResult> {
177 // Diagnostic records in ODBC are indexed starting with 1
178 assert!(rec_number > 0);
179
180 // The total number of characters (excluding the terminating NULL) available to return in
181 // `message_text`.
182 let mut text_length = 0;
183 let mut state = [0; SQLSTATE_SIZE + 1];
184 let mut native_error = 0;
185 let ret = unsafe {
186 sql_get_diag_rec(
187 self.handle_type(),
188 self.as_handle(),
189 rec_number,
190 state.as_mut_ptr(),
191 &mut native_error,
192 mut_buf_ptr(message_text),
193 clamp_small_int(message_text.len()),
194 &mut text_length,
195 )
196 };
197
198 let result = DiagnosticResult {
199 state: State::from_chars_with_nul(&state),
200 native_error,
201 text_length,
202 };
203
204 match ret {
205 SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Some(result),
206 SqlReturn::NO_DATA => None,
207 SqlReturn::ERROR => panic!("rec_number argument of diagnostics must be > 0."),
208 unexpected => panic!("SQLGetDiagRec returned: {unexpected:?}"),
209 }
210 }
211}
212
213/// ODBC Diagnostic Record
214///
215/// The `description` method of the `std::error::Error` trait only returns the message. Use
216/// `std::fmt::Display` to retrieve status code and other information.
217#[derive(Default)]
218pub struct Record {
219 /// All elements but the last one, may not be null. The last one must be null.
220 pub state: State,
221 /// Error code returned by Driver manager or driver
222 pub native_error: i32,
223 /// Buffer containing the error message. The buffer already has the correct size, and there is
224 /// no terminating zero at the end.
225 pub message: Vec<SqlChar>,
226}
227
228impl Record {
229 /// Creates an empty diagnostic record with at least the specified capacity for the message.
230 /// Using a buffer with a size different from zero then filling the diagnostic record may safe a
231 /// second function call to `SQLGetDiagRec`.
232 pub fn with_capacity(capacity: usize) -> Self {
233 Self {
234 message: Vec::with_capacity(capacity),
235 ..Default::default()
236 }
237 }
238
239 /// Fill this diagnostic `Record` from any ODBC handle.
240 ///
241 /// # Return
242 ///
243 /// `true` if a record has been found, `false` if not.
244 pub fn fill_from(&mut self, handle: &(impl Diagnostics + ?Sized), record_number: i16) -> bool {
245 match handle.diagnostic_record_vec(record_number, &mut self.message) {
246 Some(result) => {
247 self.state = result.state;
248 self.native_error = result.native_error;
249 true
250 }
251 None => false,
252 }
253 }
254}
255
256impl fmt::Display for Record {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 let message = slice_to_cow_utf8(&self.message);
259
260 write!(
261 f,
262 "State: {}, Native error: {}, Message: {}",
263 self.state.as_str(),
264 self.native_error,
265 message,
266 )
267 }
268}
269
270impl fmt::Debug for Record {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 fmt::Display::fmt(self, f)
273 }
274}
275
276#[cfg(test)]
277mod tests {
278
279 use crate::handles::diagnostics::State;
280
281 use super::Record;
282
283 #[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
284 fn to_vec_sql_char(text: &str) -> Vec<u16> {
285 text.encode_utf16().collect()
286 }
287
288 #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
289 fn to_vec_sql_char(text: &str) -> Vec<u8> {
290 text.bytes().collect()
291 }
292
293 #[test]
294 fn formatting() {
295 // build diagnostic record
296 let message = to_vec_sql_char("[Microsoft][ODBC Driver Manager] Function sequence error");
297 let rec = Record {
298 state: State(*b"HY010"),
299 message,
300 ..Record::default()
301 };
302
303 // test formatting
304 assert_eq!(
305 format!("{rec}"),
306 "State: HY010, Native error: 0, Message: [Microsoft][ODBC Driver Manager] \
307 Function sequence error"
308 );
309 }
310}