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