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 (excluding the terminating zero).
74    pub text_length: i16,
75}
76
77/// Report diagnostics from the last call to an ODBC function using a handle.
78pub trait Diagnostics {
79    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
80    ///
81    /// Returns the current values of multiple fields of a diagnostic record that contains error,
82    /// warning, and status information
83    ///
84    /// See: [Diagnostic Messages][1]
85    ///
86    /// # Arguments
87    ///
88    /// * `rec_number` - Indicates the status record from which the application seeks information.
89    ///   Status records are numbered from 1. Function panics for values smaller < 1.
90    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
91    ///   number of characters to return is greater than the buffer length, the message is
92    ///   truncated. To determine that a truncation occurred, the application must compare the
93    ///   buffer length to the actual number of bytes available, which is found in
94    ///   [`self::DiagnosticResult::text_length]`
95    ///
96    /// # Result
97    ///
98    /// * `Some(rec)` - The function successfully returned diagnostic information. message. No
99    ///   diagnostic records were generated.
100    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
101    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
102    ///   there are no diagnostic records available.
103    ///
104    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
105    fn diagnostic_record(
106        &self,
107        rec_number: i16,
108        message_text: &mut [SqlChar],
109    ) -> Option<DiagnosticResult>;
110
111    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
112    /// This method builds on top of [`Self::diagnostic_record`], if the message does not fit in the
113    /// buffer, it will grow the message buffer and extract it again.
114    ///
115    /// See: [Diagnostic Messages][1]
116    ///
117    /// # Arguments
118    ///
119    /// * `rec_number` - Indicates the status record from which the application seeks information.
120    ///   Status records are numbered from 1. Function panics for values smaller < 1.
121    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
122    ///   number of characters to return is greater than the buffer length, the buffer will be grown
123    ///   to be large enough to hold it.
124    ///
125    /// # Result
126    ///
127    /// * `Some(rec)` - The function successfully returned diagnostic information. message. No
128    ///   diagnostic records were generated. To determine that a truncation occurred, the
129    ///   application must compare the buffer length to the actual number of bytes available, which
130    ///   is found in [`self::DiagnosticResult::text_length]`.
131    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
132    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
133    ///   there are no diagnostic records available.
134    ///
135    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
136    fn diagnostic_record_vec(
137        &self,
138        rec_number: i16,
139        message_text: &mut Vec<SqlChar>,
140    ) -> Option<DiagnosticResult> {
141        // Use all the memory available in the buffer, but don't allocate any extra.
142        let cap = message_text.capacity();
143        message_text.resize(cap, 0);
144
145        self.diagnostic_record(rec_number, message_text)
146            .map(|mut result| {
147                let mut text_length = result.text_length.try_into().unwrap();
148
149                // Check if the buffer has been large enough to hold the message.
150                if text_length > message_text.len() {
151                    // The `message_text` buffer was too small to hold the requested diagnostic
152                    // message. No diagnostic records were generated. To
153                    // determine that a truncation occurred, the application
154                    // must compare the buffer length to the actual number of bytes
155                    // available, which is found in `DiagnosticResult::text_length`.
156
157                    // Resize with +1 to account for terminating zero
158                    message_text.resize(text_length + 1, 0);
159
160                    // Call diagnostics again with the larger buffer. Should be a success this time
161                    // if driver isn't buggy.
162                    result = self.diagnostic_record(rec_number, message_text).unwrap();
163                }
164                // Now `message_text` has been large enough to hold the entire message.
165
166                // Some drivers pad the message with null-chars (which is still a valid C string,
167                // but not a valid Rust string).
168                while text_length > 0 && message_text[text_length - 1] == 0 {
169                    text_length -= 1;
170                }
171                // Resize Vec to hold exactly the message.
172                message_text.resize(text_length, 0);
173
174                result
175            })
176    }
177}
178
179impl<T: AnyHandle + ?Sized> Diagnostics for T {
180    fn diagnostic_record(
181        &self,
182        rec_number: i16,
183        message_text: &mut [SqlChar],
184    ) -> Option<DiagnosticResult> {
185        // Diagnostic records in ODBC are indexed starting with 1
186        assert!(rec_number > 0);
187
188        // The total number of characters (excluding the terminating NULL) available to return in
189        // `message_text`.
190        let mut text_length = 0;
191        let mut state = [0; SQLSTATE_SIZE + 1];
192        let mut native_error = 0;
193        let ret = unsafe {
194            sql_get_diag_rec(
195                self.handle_type(),
196                self.as_handle(),
197                rec_number,
198                state.as_mut_ptr(),
199                &mut native_error,
200                mut_buf_ptr(message_text),
201                clamp_small_int(message_text.len()),
202                &mut text_length,
203            )
204        };
205
206        let result = DiagnosticResult {
207            state: State::from_chars_with_nul(&state),
208            native_error,
209            text_length,
210        };
211
212        match ret {
213            SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Some(result),
214            SqlReturn::NO_DATA => None,
215            SqlReturn::ERROR => panic!("rec_number argument of diagnostics must be > 0."),
216            unexpected => panic!("SQLGetDiagRec returned: {unexpected:?}"),
217        }
218    }
219}
220
221/// ODBC Diagnostic Record
222///
223/// The `description` method of the `std::error::Error` trait only returns the message. Use
224/// `std::fmt::Display` to retrieve status code and other information.
225#[derive(Default)]
226pub struct Record {
227    /// All elements but the last one, may not be null. The last one must be null.
228    pub state: State,
229    /// Error code returned by Driver manager or driver
230    pub native_error: i32,
231    /// Buffer containing the error message. The buffer already has the correct size, and there is
232    /// no terminating zero at the end.
233    pub message: Vec<SqlChar>,
234}
235
236impl Record {
237    /// Creates an empty diagnostic record with at least the specified capacity for the message.
238    /// Using a buffer with a size different from zero then filling the diagnostic record may safe a
239    /// second function call to `SQLGetDiagRec`.
240    pub fn with_capacity(capacity: usize) -> Self {
241        Self {
242            message: Vec::with_capacity(capacity),
243            ..Default::default()
244        }
245    }
246
247    /// Fill this diagnostic `Record` from any ODBC handle.
248    ///
249    /// # Return
250    ///
251    /// `true` if a record has been found, `false` if not.
252    pub fn fill_from(&mut self, handle: &(impl Diagnostics + ?Sized), record_number: i16) -> bool {
253        match handle.diagnostic_record_vec(record_number, &mut self.message) {
254            Some(result) => {
255                self.state = result.state;
256                self.native_error = result.native_error;
257                true
258            }
259            None => false,
260        }
261    }
262}
263
264impl fmt::Display for Record {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        let message = slice_to_cow_utf8(&self.message);
267
268        write!(
269            f,
270            "State: {}, Native error: {}, Message: {}",
271            self.state.as_str(),
272            self.native_error,
273            message,
274        )
275    }
276}
277
278impl fmt::Debug for Record {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        fmt::Display::fmt(self, f)
281    }
282}
283
284/// Used to iterate over all the diagnostics after a call to an ODBC function.
285///
286/// Fills the same [`Record`] with all the diagnostic records associated with the handle.
287pub struct DiagnosticStream<'d, D: ?Sized> {
288    /// We use this to store the contents of the current diagnostic record.
289    record: Record,
290    /// One based index of the current diagnostic record
291    record_number: i16,
292    /// A borrowed handle to the diagnostics. Used to access n-th diagnostic record.
293    diagnostics: &'d D,
294}
295
296impl<'d, D> DiagnosticStream<'d, D>
297where
298    D: Diagnostics + ?Sized,
299{
300    pub fn new(diagnostics: &'d D) -> Self {
301        Self {
302            record: Record::with_capacity(512),
303            record_number: 0,
304            diagnostics,
305        }
306    }
307
308    // We can not implement iterator, since we return a borrowed member in the result.
309    #[allow(clippy::should_implement_trait)]
310    /// The next diagnostic record. `None` if all records are exhausted.
311    pub fn next(&mut self) -> Option<&Record> {
312        if self.record_number == i16::MAX {
313            // Prevent overflow. This is not that unlikely to happen, since some `execute` or
314            // `fetch` calls can cause diagnostic messages for each row
315            #[cfg(not(feature = "structured_logging"))]
316            warn!(
317                "Too many diagnostic records were generated. Ignoring the remaining to prevent \
318                overflowing the 16Bit integer counting them."
319            );
320            #[cfg(feature = "structured_logging")]
321            warn!(
322                target: "odbc_api",
323                "Diagnostic record limit reached"
324            );
325            return None;
326        }
327        self.record_number += 1;
328        self.record
329            .fill_from(self.diagnostics, self.record_number)
330            .then_some(&self.record)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336
337    use std::cell::RefCell;
338
339    use crate::handles::{
340        DiagnosticStream, Diagnostics, SqlChar,
341        diagnostics::{DiagnosticResult, State},
342    };
343
344    use super::Record;
345
346    #[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
347    fn to_vec_sql_char(text: &str) -> Vec<u16> {
348        text.encode_utf16().collect()
349    }
350
351    #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
352    fn to_vec_sql_char(text: &str) -> Vec<u8> {
353        text.bytes().collect()
354    }
355
356    #[test]
357    fn formatting() {
358        // build diagnostic record
359        let message = to_vec_sql_char("[Microsoft][ODBC Driver Manager] Function sequence error");
360        let rec = Record {
361            state: State(*b"HY010"),
362            message,
363            ..Record::default()
364        };
365
366        // test formatting
367        assert_eq!(
368            format!("{rec}"),
369            "State: HY010, Native error: 0, Message: [Microsoft][ODBC Driver Manager] \
370             Function sequence error"
371        );
372    }
373
374    struct InfiniteDiagnostics {
375        times_called: RefCell<usize>,
376    }
377
378    impl InfiniteDiagnostics {
379        fn new() -> InfiniteDiagnostics {
380            Self {
381                times_called: RefCell::new(0),
382            }
383        }
384
385        fn num_calls(&self) -> usize {
386            *self.times_called.borrow()
387        }
388    }
389
390    impl Diagnostics for InfiniteDiagnostics {
391        fn diagnostic_record(
392            &self,
393            _rec_number: i16,
394            _message_text: &mut [SqlChar],
395        ) -> Option<DiagnosticResult> {
396            *self.times_called.borrow_mut() += 1;
397            Some(DiagnosticResult {
398                state: State([0, 0, 0, 0, 0]),
399                native_error: 0,
400                text_length: 0,
401            })
402        }
403    }
404
405    /// This test is inspired by a bug caused from a fetch statement generating a lot of diagnostic
406    /// messages.
407    #[test]
408    fn more_than_i16_max_diagnostic_records() {
409        let diagnostics = InfiniteDiagnostics::new();
410
411        let mut stream = DiagnosticStream::new(&diagnostics);
412        while let Some(_rec) = stream.next() {}
413
414        assert_eq!(diagnostics.num_calls(), i16::MAX as usize)
415    }
416}