odbc_api/handles/
diagnostics.rs

1use crate::handles::slice_to_cow_utf8;
2
3use super::{
4    as_handle::AsHandle,
5    buffer::{clamp_small_int, mut_buf_ptr},
6    SqlChar,
7};
8use odbc_sys::{SqlReturn, SQLSTATE_SIZE};
9use std::fmt;
10
11// Starting with odbc 5 we may be able to specify utf8 encoding. Until then, we may need to fall
12// back on the 'W' wide function calls.
13#[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
14use odbc_sys::SQLGetDiagRecW as sql_get_diag_rec;
15
16#[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
17use odbc_sys::SQLGetDiagRec as sql_get_diag_rec;
18
19/// A buffer large enough to hold an `SOLState` for diagnostics
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub struct State(pub [u8; SQLSTATE_SIZE]);
22
23impl State {
24    /// Can be returned from SQLDisconnect
25    pub const INVALID_STATE_TRANSACTION: State = State(*b"25000");
26    /// Given the specified Attribute value, an invalid value was specified in ValuePtr.
27    pub const INVALID_ATTRIBUTE_VALUE: State = State(*b"HY024");
28    /// An invalid data type has been bound to a statement. Is also returned by SQLFetch if trying
29    /// to fetch into a 64Bit Integer Buffer.
30    pub const INVALID_SQL_DATA_TYPE: State = State(*b"HY004");
31    /// String or binary data returned for a column resulted in the truncation of nonblank character
32    /// or non-NULL binary data. If it was a string value, it was right-truncated.
33    pub const STRING_DATA_RIGHT_TRUNCATION: State = State(*b"01004");
34    /// StrLen_or_IndPtr was a null pointer and NULL data was retrieved.
35    pub const INDICATOR_VARIABLE_REQUIRED_BUT_NOT_SUPPLIED: State = State(*b"22002");
36
37    /// Drops terminating zero and changes char type, if required
38    pub fn from_chars_with_nul(code: &[SqlChar; SQLSTATE_SIZE + 1]) -> Self {
39        // `SQLGetDiagRecW` returns ODBC state as wide characters. This constructor converts the
40        //  wide characters to narrow and drops the terminating zero.
41
42        let mut ascii = [0; SQLSTATE_SIZE];
43        for (index, letter) in code[..SQLSTATE_SIZE].iter().copied().enumerate() {
44            ascii[index] = letter as u8;
45        }
46        State(ascii)
47    }
48
49    /// View status code as string slice for displaying. Must always succeed as ODBC status code
50    /// always consist of ASCII characters.
51    pub fn as_str(&self) -> &str {
52        std::str::from_utf8(&self.0).unwrap()
53    }
54}
55
56/// Result of [`Diagnostic::diagnostic_record`].
57#[derive(Debug, Clone, Copy)]
58pub struct DiagnosticResult {
59    /// A five-character SQLSTATE code (and terminating NULL) for the diagnostic record
60    /// `rec_number`. The first two characters indicate the class; the next three indicate the
61    /// subclass. For more information, see [SQLSTATE][1]s.
62    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/sqlstates
63    pub state: State,
64    /// Native error code specific to the data source.
65    pub native_error: i32,
66    /// The length of the diagnostic message reported by ODBC (excluding the terminating zero).
67    pub text_length: i16,
68}
69
70/// Report diagnostics from the last call to an ODBC function using a handle.
71pub trait Diagnostics {
72    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
73    ///
74    /// Returns the current values of multiple fields of a diagnostic record that contains error,
75    /// warning, and status information
76    ///
77    /// See: [Diagnostic Messages][1]
78    ///
79    /// # Arguments
80    ///
81    /// * `rec_number` - Indicates the status record from which the application seeks information.
82    ///   Status records are numbered from 1. Function panics for values smaller < 1.
83    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
84    ///   number of characters to return is greater than the buffer length, the message is
85    ///   truncated. To determine that a truncation occurred, the application must compare the
86    ///   buffer length to the actual number of bytes available, which is found in
87    ///   [`self::DiagnosticResult::text_length]`
88    ///
89    /// # Result
90    ///
91    /// * `Some(rec)` - The function successfully returned diagnostic information.
92    ///   message. No diagnostic records were generated.
93    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
94    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
95    ///   there are no diagnostic records available.
96    ///
97    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
98    fn diagnostic_record(
99        &self,
100        rec_number: i16,
101        message_text: &mut [SqlChar],
102    ) -> Option<DiagnosticResult>;
103
104    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
105    /// This method builds on top of [`Self::diagnostic_record`], if the message does not fit in the
106    /// buffer, it will grow the message buffer and extract it again.
107    ///
108    /// See: [Diagnostic Messages][1]
109    ///
110    /// # Arguments
111    ///
112    /// * `rec_number` - Indicates the status record from which the application seeks information.
113    ///   Status records are numbered from 1. Function panics for values smaller < 1.
114    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
115    ///   number of characters to return is greater than the buffer length, the buffer will be grown
116    ///   to be large enough to hold it.
117    ///
118    /// # Result
119    ///
120    /// * `Some(rec)` - The function successfully returned diagnostic information.
121    ///   message. No diagnostic records were generated. To determine that a truncation occurred,
122    ///   the application must compare the buffer length to the actual number of bytes available,
123    ///   which is found in [`self::DiagnosticResult::text_length]`.
124    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
125    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
126    ///   there are no diagnostic records available.
127    ///
128    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
129    fn diagnostic_record_vec(
130        &self,
131        rec_number: i16,
132        message_text: &mut Vec<SqlChar>,
133    ) -> Option<DiagnosticResult> {
134        // Use all the memory available in the buffer, but don't allocate any extra.
135        let cap = message_text.capacity();
136        message_text.resize(cap, 0);
137
138        self.diagnostic_record(rec_number, message_text)
139            .map(|mut result| {
140                let mut text_length = result.text_length.try_into().unwrap();
141
142                // Check if the buffer has been large enough to hold the message.
143                if text_length > message_text.len() {
144                    // The `message_text` buffer was too small to hold the requested diagnostic message.
145                    // No diagnostic records were generated. To determine that a truncation occurred,
146                    // the application must compare the buffer length to the actual number of bytes
147                    // available, which is found in `DiagnosticResult::text_length`.
148
149                    // Resize with +1 to account for terminating zero
150                    message_text.resize(text_length + 1, 0);
151
152                    // Call diagnostics again with the larger buffer. Should be a success this time if
153                    // driver isn't buggy.
154                    result = self.diagnostic_record(rec_number, message_text).unwrap();
155                }
156                // Now `message_text` has been large enough to hold the entire message.
157
158                // Some drivers pad the message with null-chars (which is still a valid C string,
159                // but not a valid Rust string).
160                while text_length > 0 && message_text[text_length - 1] == 0 {
161                    text_length -= 1;
162                }
163                // Resize Vec to hold exactly the message.
164                message_text.resize(text_length, 0);
165
166                result
167            })
168    }
169}
170
171impl<T: AsHandle + ?Sized> Diagnostics for T {
172    fn diagnostic_record(
173        &self,
174        rec_number: i16,
175        message_text: &mut [SqlChar],
176    ) -> Option<DiagnosticResult> {
177        // Diagnostic records in ODBC are indexed starting with 1
178        assert!(rec_number > 0);
179
180        // The total number of characters (excluding the terminating NULL) available to return in
181        // `message_text`.
182        let mut text_length = 0;
183        let mut state = [0; SQLSTATE_SIZE + 1];
184        let mut native_error = 0;
185        let ret = unsafe {
186            sql_get_diag_rec(
187                self.handle_type(),
188                self.as_handle(),
189                rec_number,
190                state.as_mut_ptr(),
191                &mut native_error,
192                mut_buf_ptr(message_text),
193                clamp_small_int(message_text.len()),
194                &mut text_length,
195            )
196        };
197
198        let result = DiagnosticResult {
199            state: State::from_chars_with_nul(&state),
200            native_error,
201            text_length,
202        };
203
204        match ret {
205            SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Some(result),
206            SqlReturn::NO_DATA => None,
207            SqlReturn::ERROR => panic!("rec_number argument of diagnostics must be > 0."),
208            unexpected => panic!("SQLGetDiagRec returned: {unexpected:?}"),
209        }
210    }
211}
212
213/// ODBC Diagnostic Record
214///
215/// The `description` method of the `std::error::Error` trait only returns the message. Use
216/// `std::fmt::Display` to retrieve status code and other information.
217#[derive(Default)]
218pub struct Record {
219    /// All elements but the last one, may not be null. The last one must be null.
220    pub state: State,
221    /// Error code returned by Driver manager or driver
222    pub native_error: i32,
223    /// Buffer containing the error message. The buffer already has the correct size, and there is
224    /// no terminating zero at the end.
225    pub message: Vec<SqlChar>,
226}
227
228impl Record {
229    /// Creates an empty diagnostic record with at least the specified capacity for the message.
230    /// Using a buffer with a size different from zero then filling the diagnostic record may safe a
231    /// second function call to `SQLGetDiagRec`.
232    pub fn with_capacity(capacity: usize) -> Self {
233        Self {
234            message: Vec::with_capacity(capacity),
235            ..Default::default()
236        }
237    }
238
239    /// Fill this diagnostic `Record` from any ODBC handle.
240    ///
241    /// # Return
242    ///
243    /// `true` if a record has been found, `false` if not.
244    pub fn fill_from(&mut self, handle: &(impl Diagnostics + ?Sized), record_number: i16) -> bool {
245        match handle.diagnostic_record_vec(record_number, &mut self.message) {
246            Some(result) => {
247                self.state = result.state;
248                self.native_error = result.native_error;
249                true
250            }
251            None => false,
252        }
253    }
254}
255
256impl fmt::Display for Record {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        let message = slice_to_cow_utf8(&self.message);
259
260        write!(
261            f,
262            "State: {}, Native error: {}, Message: {}",
263            self.state.as_str(),
264            self.native_error,
265            message,
266        )
267    }
268}
269
270impl fmt::Debug for Record {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        fmt::Display::fmt(self, f)
273    }
274}
275
276#[cfg(test)]
277mod tests {
278
279    use crate::handles::diagnostics::State;
280
281    use super::Record;
282
283    #[cfg(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows")))]
284    fn to_vec_sql_char(text: &str) -> Vec<u16> {
285        text.encode_utf16().collect()
286    }
287
288    #[cfg(not(any(feature = "wide", all(not(feature = "narrow"), target_os = "windows"))))]
289    fn to_vec_sql_char(text: &str) -> Vec<u8> {
290        text.bytes().collect()
291    }
292
293    #[test]
294    fn formatting() {
295        // build diagnostic record
296        let message = to_vec_sql_char("[Microsoft][ODBC Driver Manager] Function sequence error");
297        let rec = Record {
298            state: State(*b"HY010"),
299            message,
300            ..Record::default()
301        };
302
303        // test formatting
304        assert_eq!(
305            format!("{rec}"),
306            "State: HY010, Native error: 0, Message: [Microsoft][ODBC Driver Manager] \
307             Function sequence error"
308        );
309    }
310}