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