odbc_api/handles/diagnostics.rs
1use crate::handles::slice_to_cow_utf8;
2
3use super::{
4 SqlChar,
5 any_handle::AnyHandle,
6 buffer::{clamp_small_int, mut_buf_ptr},
7};
8use log::warn;
9use odbc_sys::{SQLSTATE_SIZE, SqlReturn};
10use std::fmt;
11
12// Starting with odbc 5 we may be able to specify utf8 encoding. Until then, we may need to fall
13// back on the 'W' wide function calls.
14#[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
15use odbc_sys::SQLGetDiagRecW as sql_get_diag_rec;
16
17#[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
18use odbc_sys::SQLGetDiagRec as sql_get_diag_rec;
19
20/// A buffer large enough to hold an `SOLState` for diagnostics
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub struct State(pub [u8; SQLSTATE_SIZE]);
23
24impl State {
25 /// Can be returned from SQLDisconnect
26 pub const INVALID_STATE_TRANSACTION: State = State(*b"25000");
27 /// Given the specified Attribute value, an invalid value was specified in ValuePtr.
28 pub const INVALID_ATTRIBUTE_VALUE: State = State(*b"HY024");
29 /// An invalid data type has been bound to a statement. Is also returned by SQLFetch if trying
30 /// to fetch into a 64Bit Integer Buffer.
31 pub const INVALID_SQL_DATA_TYPE: State = State(*b"HY004");
32 /// String or binary data returned for a column resulted in the truncation of nonblank character
33 /// or non-NULL binary data. If it was a string value, it was right-truncated.
34 pub const STRING_DATA_RIGHT_TRUNCATION: State = State(*b"01004");
35 /// StrLen_or_IndPtr was a null pointer and NULL data was retrieved.
36 pub const INDICATOR_VARIABLE_REQUIRED_BUT_NOT_SUPPLIED: State = State(*b"22002");
37 /// Can be returned by SQLSetStmtAttr function. We expect it in case the array set size is
38 /// rejected.
39 pub const OPTION_VALUE_CHANGED: State = State(*b"01S02");
40 /// One of two things:
41 ///
42 /// * The value specified for the argument Attribute was not valid for the version of ODBC
43 /// supported by the driver.
44 /// * The value specified for the argument Attribute was a read-only attribute.
45 ///
46 /// Both are emitted by the driver manager, rather than the driver itself.
47 ///
48 /// One example of this error code emitted is when using `mdbtools`. When the driver builds its
49 /// dispatch table, it comments out `SQLSetStmtAttr` triggering unixODBC to use the fallback
50 /// behavior implemented against `SQLSetStmtOption`. `SQLSetStmtOption` does not have any notion
51 /// of array parameters, so setting the parameter size triggers this error code.
52 pub const INVALID_ATTRIBUTE_OR_OPTION_IDENTIFIER: State = State(*b"HY092");
53
54 /// Drops terminating zero and changes char type, if required
55 pub fn from_chars_with_nul(code: &[SqlChar; SQLSTATE_SIZE + 1]) -> Self {
56 // `SQLGetDiagRecW` returns ODBC state as wide characters. This constructor converts the
57 // wide characters to narrow and drops the terminating zero.
58
59 let mut ascii = [0; SQLSTATE_SIZE];
60 for (index, letter) in code[..SQLSTATE_SIZE].iter().copied().enumerate() {
61 // Then using wide character set, convert to ASCII first
62 #[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
63 let letter = letter as u8;
64 ascii[index] = letter;
65 }
66 State(ascii)
67 }
68
69 /// View status code as string slice for displaying. Must always succeed as ODBC status code
70 /// always consist of ASCII characters.
71 pub fn as_str(&self) -> &str {
72 std::str::from_utf8(&self.0).unwrap()
73 }
74}
75
76/// Result of [`Diagnostics::diagnostic_record`].
77#[derive(Debug, Clone, Copy)]
78pub struct DiagnosticResult {
79 /// A five-character SQLSTATE code (and terminating NULL) for the diagnostic record
80 /// `rec_number`. The first two characters indicate the class; the next three indicate the
81 /// subclass. For more information, see
82 /// [SQLSTATEs](https://docs.microsoft.com/sql/odbc/reference/develop-app/sqlstates).
83 pub state: State,
84 /// Native error code specific to the data source.
85 pub native_error: i32,
86 /// The length of the diagnostic message reported by ODBC. This is supposed to be the size in
87 /// characters (excluding the terminating zero). For narrow encodings this is the size in bytes;
88 /// For wide encodings this is the size in 16-bit double words. Some drivers however report
89 /// larger values (e.g. they add additional padding `\0` bytes to the message and include the
90 /// padding in the length). Other drivers (IBM i Access ODBC driver, see
91 /// [issue #898](https://github.com/pacman82/odbc-api/issues/898>) underreport the text length.
92 /// They report only one "character" for each UTF-8 code point even if they consist of multiple
93 /// bytes.
94 pub text_length: i16,
95}
96
97/// Report diagnostics from the last call to an ODBC function using a handle.
98pub trait Diagnostics {
99 /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
100 ///
101 /// Returns the current values of multiple fields of a diagnostic record that contains error,
102 /// warning, and status information
103 ///
104 /// See: [Diagnostic Messages][1]
105 ///
106 /// # Arguments
107 ///
108 /// * `rec_number` - Indicates the status record from which the application seeks information.
109 /// Status records are numbered from 1. Function panics for values smaller < 1.
110 /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
111 /// number of characters to return is greater than the buffer length, the message is
112 /// truncated. To determine that a truncation occurred, the application must compare the
113 /// buffer length to the actual number of bytes available, which is found in
114 /// [`self::DiagnosticResult::text_length]`
115 ///
116 /// # Result
117 ///
118 /// * `Some(rec)` - The function successfully returned diagnostic information. message. No
119 /// diagnostic records were generated.
120 /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
121 /// the specified Handle. The function also returns `NoData` for any positive `rec_number` if
122 /// there are no diagnostic records available.
123 ///
124 /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
125 fn diagnostic_record(
126 &self,
127 rec_number: i16,
128 message_text: &mut [SqlChar],
129 ) -> Option<DiagnosticResult>;
130
131 /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
132 /// This method builds on top of [`Self::diagnostic_record`], if the message does not fit in the
133 /// buffer, it will grow the message buffer and extract it again.
134 ///
135 /// See: [Diagnostic Messages][1]
136 ///
137 /// # Arguments
138 ///
139 /// * `rec_number` - Indicates the status record from which the application seeks information.
140 /// Status records are numbered from 1. Function panics for values smaller < 1.
141 /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
142 /// number of characters to return is greater than the buffer length, the buffer will be grown
143 /// to be large enough to hold it.
144 ///
145 /// # Result
146 ///
147 /// * `Some(rec)` - The function successfully returned diagnostic information. message. No
148 /// diagnostic records were generated. To determine that a truncation occurred, the
149 /// application must compare the buffer length to the actual number of bytes available, which
150 /// is found in [`self::DiagnosticResult::text_length]`.
151 /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
152 /// the specified Handle. The function also returns `NoData` for any positive `rec_number` if
153 /// there are no diagnostic records available.
154 ///
155 /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
156 fn diagnostic_record_vec(
157 &self,
158 rec_number: i16,
159 message_text: &mut Vec<SqlChar>,
160 ) -> Option<DiagnosticResult> {
161 // Use all the memory available in the buffer, but don't allocate any extra.
162 let cap = message_text.capacity();
163 message_text.resize(cap, 0);
164
165 self.diagnostic_record(rec_number, message_text)
166 .map(|mut result| {
167 let mut text_length = result.text_length.try_into().unwrap();
168
169 // Check if the buffer has been large enough to hold the message and terminating
170 // zero.
171 if text_length + 1 > message_text.len() {
172 // The `message_text` buffer was too small to hold the requested diagnostic
173 // message.
174
175 // Resize with +1 to account for terminating zero
176 message_text.resize(text_length + 1, 0);
177
178 // Call diagnostics again with the larger buffer. Should be a success this time
179 // if driver is not buggy.
180 result = self.diagnostic_record(rec_number, message_text).unwrap();
181 }
182 // Now `message_text` should have been large enough to hold the entire message.
183
184 // For a well behaved driver, we expect the last character to not be a terminating
185 // zero, followed by a terminating zero.
186 if text_length != 0
187 && (message_text[text_length - 1] == 0 || message_text[text_length] != 0)
188 {
189 text_length = 0;
190 // Driver is not well behaved. Let's scan for the terminating zero instead to
191 // determine the message length..
192 while text_length < message_text.len() && message_text[text_length] != 0 {
193 text_length += 1;
194 }
195 }
196
197 // Resize Vec to hold exactly the message.
198 message_text.resize(text_length, 0);
199
200 result
201 })
202 }
203}
204
205impl<T: AnyHandle + ?Sized> Diagnostics for T {
206 fn diagnostic_record(
207 &self,
208 rec_number: i16,
209 message_text: &mut [SqlChar],
210 ) -> Option<DiagnosticResult> {
211 // Diagnostic records in ODBC are indexed starting with 1
212 assert!(rec_number > 0);
213
214 // The total number of characters (excluding the terminating NULL) available to return in
215 // `message_text`.
216 let mut text_length = 0;
217 let mut state = [0; SQLSTATE_SIZE + 1];
218 let mut native_error = 0;
219 let ret = unsafe {
220 sql_get_diag_rec(
221 self.handle_type(),
222 self.as_handle(),
223 rec_number,
224 state.as_mut_ptr(),
225 &mut native_error,
226 mut_buf_ptr(message_text),
227 clamp_small_int(message_text.len()),
228 &mut text_length,
229 )
230 };
231
232 let result = DiagnosticResult {
233 state: State::from_chars_with_nul(&state),
234 native_error,
235 text_length,
236 };
237
238 match ret {
239 SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Some(result),
240 SqlReturn::NO_DATA => None,
241 SqlReturn::ERROR => panic!("rec_number argument of diagnostics must be > 0."),
242 unexpected => panic!("SQLGetDiagRec returned: {unexpected:?}"),
243 }
244 }
245}
246
247/// ODBC Diagnostic Record
248///
249/// The `description` method of the `std::error::Error` trait only returns the message. Use
250/// `std::fmt::Display` to retrieve status code and other information.
251#[derive(Default)]
252pub struct Record {
253 /// All elements but the last one, may not be null. The last one must be null.
254 pub state: State,
255 /// Error code returned by Driver manager or driver
256 pub native_error: i32,
257 /// Buffer containing the error message. The buffer already has the correct size, and there is
258 /// no terminating zero at the end.
259 pub message: Vec<SqlChar>,
260}
261
262impl Record {
263 /// Creates an empty diagnostic record with at least the specified capacity for the message.
264 /// Using a buffer with a size different from zero then filling the diagnostic record may safe a
265 /// second function call to `SQLGetDiagRec`.
266 pub fn with_capacity(capacity: usize) -> Self {
267 Self {
268 message: Vec::with_capacity(capacity),
269 ..Default::default()
270 }
271 }
272
273 /// Fill this diagnostic `Record` from any ODBC handle.
274 ///
275 /// # Return
276 ///
277 /// `true` if a record has been found, `false` if not.
278 pub fn fill_from(&mut self, handle: &(impl Diagnostics + ?Sized), record_number: i16) -> bool {
279 match handle.diagnostic_record_vec(record_number, &mut self.message) {
280 Some(result) => {
281 self.state = result.state;
282 self.native_error = result.native_error;
283 true
284 }
285 None => false,
286 }
287 }
288}
289
290impl fmt::Display for Record {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 let message = slice_to_cow_utf8(&self.message);
293
294 write!(
295 f,
296 "State: {}, Native error: {}, Message: {}",
297 self.state.as_str(),
298 self.native_error,
299 message,
300 )
301 }
302}
303
304impl fmt::Debug for Record {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 fmt::Display::fmt(self, f)
307 }
308}
309
310/// Used to iterate over all the diagnostics after a call to an ODBC function.
311///
312/// Fills the same [`Record`] with all the diagnostic records associated with the handle.
313pub struct DiagnosticStream<'d, D: ?Sized> {
314 /// We use this to store the contents of the current diagnostic record.
315 record: Record,
316 /// One based index of the current diagnostic record
317 record_number: i16,
318 /// A borrowed handle to the diagnostics. Used to access n-th diagnostic record.
319 diagnostics: &'d D,
320}
321
322impl<'d, D> DiagnosticStream<'d, D>
323where
324 D: Diagnostics + ?Sized,
325{
326 pub fn new(diagnostics: &'d D) -> Self {
327 Self {
328 record: Record::with_capacity(512),
329 record_number: 0,
330 diagnostics,
331 }
332 }
333
334 // We can not implement iterator, since we return a borrowed member in the result.
335 #[allow(clippy::should_implement_trait)]
336 /// The next diagnostic record. `None` if all records are exhausted.
337 pub fn next(&mut self) -> Option<&Record> {
338 if self.record_number == i16::MAX {
339 // Prevent overflow. This is not that unlikely to happen, since some `execute` or
340 // `fetch` calls can cause diagnostic messages for each row
341 #[cfg(not(feature = "structured_logging"))]
342 warn!(
343 "Too many diagnostic records were generated. Ignoring the remaining to prevent \
344 overflowing the 16Bit integer counting them."
345 );
346 #[cfg(feature = "structured_logging")]
347 warn!(
348 target: "odbc_api",
349 "Diagnostic record limit reached"
350 );
351 return None;
352 }
353 self.record_number += 1;
354 self.record
355 .fill_from(self.diagnostics, self.record_number)
356 .then_some(&self.record)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362
363 use std::cell::RefCell;
364
365 use crate::handles::{
366 DiagnosticStream, Diagnostics, SqlChar,
367 diagnostics::{DiagnosticResult, State},
368 };
369
370 use super::Record;
371
372 #[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
373 fn to_vec_sql_char(text: &str) -> Vec<u16> {
374 text.encode_utf16().collect()
375 }
376
377 #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
378 fn to_vec_sql_char(text: &str) -> Vec<u8> {
379 text.bytes().collect()
380 }
381
382 #[test]
383 fn formatting() {
384 // build diagnostic record
385 let message = to_vec_sql_char("[Microsoft][ODBC Driver Manager] Function sequence error");
386 let rec = Record {
387 state: State(*b"HY010"),
388 message,
389 ..Record::default()
390 };
391
392 // test formatting
393 assert_eq!(
394 format!("{rec}"),
395 "State: HY010, Native error: 0, Message: [Microsoft][ODBC Driver Manager] \
396 Function sequence error"
397 );
398 }
399
400 struct InfiniteDiagnostics {
401 times_called: RefCell<usize>,
402 }
403
404 impl InfiniteDiagnostics {
405 fn new() -> InfiniteDiagnostics {
406 Self {
407 times_called: RefCell::new(0),
408 }
409 }
410
411 fn num_calls(&self) -> usize {
412 *self.times_called.borrow()
413 }
414 }
415
416 impl Diagnostics for InfiniteDiagnostics {
417 fn diagnostic_record(
418 &self,
419 _rec_number: i16,
420 _message_text: &mut [SqlChar],
421 ) -> Option<DiagnosticResult> {
422 *self.times_called.borrow_mut() += 1;
423 Some(DiagnosticResult {
424 state: State([0, 0, 0, 0, 0]),
425 native_error: 0,
426 text_length: 0,
427 })
428 }
429 }
430
431 /// This test is inspired by a bug caused from a fetch statement generating a lot of diagnostic
432 /// messages.
433 #[test]
434 fn more_than_i16_max_diagnostic_records() {
435 let diagnostics = InfiniteDiagnostics::new();
436
437 let mut stream = DiagnosticStream::new(&diagnostics);
438 while let Some(_rec) = stream.next() {}
439
440 assert_eq!(diagnostics.num_calls(), i16::MAX as usize)
441 }
442
443 #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
444 #[test]
445 fn driver_pads_diagnostic_message_text_with_zeroes() {
446 struct DiagnosticStub;
447
448 impl Diagnostics for DiagnosticStub {
449 fn diagnostic_record(
450 &self,
451 _rec_number: i16,
452 message_text: &mut [SqlChar],
453 ) -> Option<DiagnosticResult> {
454 let message = "Hello, World!";
455 message_text[..message.len()].copy_from_slice(message.as_bytes());
456 message_text[message.len()..].fill(0);
457 Some(DiagnosticResult {
458 state: State([0, 0, 0, 0, 0]),
459 native_error: 0,
460 // Overreport: length is actually 13
461 text_length: 20,
462 })
463 }
464 }
465
466 let mut message_text = Vec::with_capacity(50);
467 DiagnosticStub.diagnostic_record_vec(0, &mut message_text);
468
469 assert_eq!("Hello, World!", String::from_utf8(message_text).unwrap())
470 }
471
472 /// IBM i Access ODBC driver only reports one "character" for each UTF-8 code point even if they
473 /// consist of multiple bytes
474 ///
475 /// See: <https://github.com/pacman82/odbc-api/issues/898> underreport the text length.
476 #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
477 #[test]
478 fn driver_understates_length_of_message_text() {
479 struct DiagnosticStub;
480
481 impl Diagnostics for DiagnosticStub {
482 fn diagnostic_record(
483 &self,
484 _rec_number: i16,
485 message_text: &mut [SqlChar],
486 ) -> Option<DiagnosticResult> {
487 let message = "Hällö, Wörld!";
488 message_text[..message.len()].copy_from_slice(message.as_bytes());
489 message_text[message.len()] = 0;
490 Some(DiagnosticResult {
491 state: State([0, 0, 0, 0, 0]),
492 native_error: 0,
493 // Underreport: length in codepoints not bytes
494 text_length: 13,
495 })
496 }
497 }
498
499 let mut message_text = Vec::with_capacity(50);
500 DiagnosticStub.diagnostic_record_vec(0, &mut message_text);
501
502 assert_eq!("Hällö, Wörld!", String::from_utf8(message_text).unwrap())
503 }
504}