Skip to main content

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}