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