odbc_api/handles/
diagnostics.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
use crate::handles::slice_to_cow_utf8;

use super::{
    as_handle::AsHandle,
    buffer::{clamp_small_int, mut_buf_ptr},
    SqlChar,
};
use odbc_sys::{SqlReturn, SQLSTATE_SIZE};
use std::fmt;

// Starting with odbc 5 we may be able to specify utf8 encoding. Until then, we may need to fall
// back on the 'W' wide function calls.
#[cfg(not(feature = "narrow"))]
use odbc_sys::SQLGetDiagRecW as sql_get_diag_rec;

#[cfg(feature = "narrow")]
use odbc_sys::SQLGetDiagRec as sql_get_diag_rec;

/// A buffer large enough to hold an `SOLState` for diagnostics
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State(pub [u8; SQLSTATE_SIZE]);

impl State {
    /// Can be returned from SQLDisconnect
    pub const INVALID_STATE_TRANSACTION: State = State(*b"25000");
    /// Given the specified Attribute value, an invalid value was specified in ValuePtr.
    pub const INVALID_ATTRIBUTE_VALUE: State = State(*b"HY024");
    /// An invalid data type has been bound to a statement. Is also returned by SQLFetch if trying
    /// to fetch into a 64Bit Integer Buffer.
    pub const INVALID_SQL_DATA_TYPE: State = State(*b"HY004");
    /// String or binary data returned for a column resulted in the truncation of nonblank character
    /// or non-NULL binary data. If it was a string value, it was right-truncated.
    pub const STRING_DATA_RIGHT_TRUNCATION: State = State(*b"01004");
    /// StrLen_or_IndPtr was a null pointer and NULL data was retrieved.
    pub const INDICATOR_VARIABLE_REQUIRED_BUT_NOT_SUPPLIED: State = State(*b"22002");

    /// Drops terminating zero and changes char type, if required
    pub fn from_chars_with_nul(code: &[SqlChar; SQLSTATE_SIZE + 1]) -> Self {
        // `SQLGetDiagRecW` returns ODBC state as wide characters. This constructor converts the
        //  wide characters to narrow and drops the terminating zero.

        let mut ascii = [0; SQLSTATE_SIZE];
        for (index, letter) in code[..SQLSTATE_SIZE].iter().copied().enumerate() {
            ascii[index] = letter as u8;
        }
        State(ascii)
    }

    /// View status code as string slice for displaying. Must always succeed as ODBC status code
    /// always consist of ASCII characters.
    pub fn as_str(&self) -> &str {
        std::str::from_utf8(&self.0).unwrap()
    }
}

/// Result of [`Diagnostic::diagnostic_record`].
#[derive(Debug, Clone, Copy)]
pub struct DiagnosticResult {
    /// A five-character SQLSTATE code (and terminating NULL) for the diagnostic record
    /// `rec_number`. The first two characters indicate the class; the next three indicate the
    /// subclass. For more information, see [SQLSTATE][1]s.
    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/sqlstates
    pub state: State,
    /// Native error code specific to the data source.
    pub native_error: i32,
    /// The length of the diagnostic message reported by ODBC (excluding the terminating zero).
    pub text_length: i16,
}

/// Report diagnostics from the last call to an ODBC function using a handle.
pub trait Diagnostics {
    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
    ///
    /// Returns the current values of multiple fields of a diagnostic record that contains error,
    /// warning, and status information
    ///
    /// See: [Diagnostic Messages][1]
    ///
    /// # Arguments
    ///
    /// * `rec_number` - Indicates the status record from which the application seeks information.
    ///   Status records are numbered from 1. Function panics for values smaller < 1.
    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
    ///   number of characters to return is greater than the buffer length, the message is
    ///   truncated. To determine that a truncation occurred, the application must compare the
    ///   buffer length to the actual number of bytes available, which is found in
    ///   [`self::DiagnosticResult::text_length]`
    ///
    /// # Result
    ///
    /// * `Some(rec)` - The function successfully returned diagnostic information.
    ///   message. No diagnostic records were generated.
    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
    ///   there are no diagnostic records available.
    ///
    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
    fn diagnostic_record(
        &self,
        rec_number: i16,
        message_text: &mut [SqlChar],
    ) -> Option<DiagnosticResult>;

    /// Call this method to retrieve diagnostic information for the last call to an ODBC function.
    /// This method builds on top of [`Self::diagnostic_record`], if the message does not fit in the
    /// buffer, it will grow the message buffer and extract it again.
    ///
    /// See: [Diagnostic Messages][1]
    ///
    /// # Arguments
    ///
    /// * `rec_number` - Indicates the status record from which the application seeks information.
    ///   Status records are numbered from 1. Function panics for values smaller < 1.
    /// * `message_text` - Buffer in which to return the diagnostic message text string. If the
    ///   number of characters to return is greater than the buffer length, the buffer will be grown
    ///   to be large enough to hold it.
    ///
    /// # Result
    ///
    /// * `Some(rec)` - The function successfully returned diagnostic information.
    ///   message. No diagnostic records were generated. To determine that a truncation occurred,
    ///   the application must compare the buffer length to the actual number of bytes available,
    ///   which is found in [`self::DiagnosticResult::text_length]`.
    /// * `None` - `rec_number` was greater than the number of diagnostic records that existed for
    ///   the specified Handle. The function also returns `NoData` for any positive `rec_number` if
    ///   there are no diagnostic records available.
    ///
    /// [1]: https://docs.microsoft.com/sql/odbc/reference/develop-app/diagnostic-messages
    fn diagnostic_record_vec(
        &self,
        rec_number: i16,
        message_text: &mut Vec<SqlChar>,
    ) -> Option<DiagnosticResult> {
        // Use all the memory available in the buffer, but don't allocate any extra.
        let cap = message_text.capacity();
        message_text.resize(cap, 0);

        self.diagnostic_record(rec_number, message_text)
            .map(|mut result| {
                let mut text_length = result.text_length.try_into().unwrap();

                // Check if the buffer has been large enough to hold the message.
                if text_length > message_text.len() {
                    // The `message_text` buffer was too small to hold the requested diagnostic message.
                    // No diagnostic records were generated. To determine that a truncation occurred,
                    // the application must compare the buffer length to the actual number of bytes
                    // available, which is found in `DiagnosticResult::text_length`.

                    // Resize with +1 to account for terminating zero
                    message_text.resize(text_length + 1, 0);

                    // Call diagnostics again with the larger buffer. Should be a success this time if
                    // driver isn't buggy.
                    result = self.diagnostic_record(rec_number, message_text).unwrap();
                }
                // Now `message_text` has been large enough to hold the entire message.

                // Some drivers pad the message with null-chars (which is still a valid C string,
                // but not a valid Rust string).
                while text_length > 0 && message_text[text_length - 1] == 0 {
                    text_length -= 1;
                }
                // Resize Vec to hold exactly the message.
                message_text.resize(text_length, 0);

                result
            })
    }
}

impl<T: AsHandle + ?Sized> Diagnostics for T {
    fn diagnostic_record(
        &self,
        rec_number: i16,
        message_text: &mut [SqlChar],
    ) -> Option<DiagnosticResult> {
        // Diagnostic records in ODBC are indexed starting with 1
        assert!(rec_number > 0);

        // The total number of characters (excluding the terminating NULL) available to return in
        // `message_text`.
        let mut text_length = 0;
        let mut state = [0; SQLSTATE_SIZE + 1];
        let mut native_error = 0;
        let ret = unsafe {
            sql_get_diag_rec(
                self.handle_type(),
                self.as_handle(),
                rec_number,
                state.as_mut_ptr(),
                &mut native_error,
                mut_buf_ptr(message_text),
                clamp_small_int(message_text.len()),
                &mut text_length,
            )
        };

        let result = DiagnosticResult {
            state: State::from_chars_with_nul(&state),
            native_error,
            text_length,
        };

        match ret {
            SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Some(result),
            SqlReturn::NO_DATA => None,
            SqlReturn::ERROR => panic!("rec_number argument of diagnostics must be > 0."),
            unexpected => panic!("SQLGetDiagRec returned: {unexpected:?}"),
        }
    }
}

/// ODBC Diagnostic Record
///
/// The `description` method of the `std::error::Error` trait only returns the message. Use
/// `std::fmt::Display` to retrieve status code and other information.
#[derive(Default)]
pub struct Record {
    /// All elements but the last one, may not be null. The last one must be null.
    pub state: State,
    /// Error code returned by Driver manager or driver
    pub native_error: i32,
    /// Buffer containing the error message. The buffer already has the correct size, and there is
    /// no terminating zero at the end.
    pub message: Vec<SqlChar>,
}

impl Record {
    /// Creates an empty diagnostic record with at least the specified capacity for the message.
    /// Using a buffer with a size different from zero then filling the diagnostic record may safe a
    /// second function call to `SQLGetDiagRec`.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            message: Vec::with_capacity(capacity),
            ..Default::default()
        }
    }

    /// Fill this diagnostic `Record` from any ODBC handle.
    ///
    /// # Return
    ///
    /// `true` if a record has been found, `false` if not.
    pub fn fill_from(&mut self, handle: &(impl Diagnostics + ?Sized), record_number: i16) -> bool {
        match handle.diagnostic_record_vec(record_number, &mut self.message) {
            Some(result) => {
                self.state = result.state;
                self.native_error = result.native_error;
                true
            }
            None => false,
        }
    }
}

impl fmt::Display for Record {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = slice_to_cow_utf8(&self.message);

        write!(
            f,
            "State: {}, Native error: {}, Message: {}",
            self.state.as_str(),
            self.native_error,
            message,
        )
    }
}

impl fmt::Debug for Record {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

#[cfg(test)]
mod tests {

    use crate::handles::diagnostics::State;

    use super::Record;

    #[cfg(not(feature = "narrow"))]
    fn to_vec_sql_char(text: &str) -> Vec<u16> {
        text.encode_utf16().collect()
    }

    #[cfg(feature = "narrow")]
    fn to_vec_sql_char(text: &str) -> Vec<u8> {
        text.bytes().collect()
    }

    #[test]
    fn formatting() {
        // build diagnostic record
        let message = to_vec_sql_char("[Microsoft][ODBC Driver Manager] Function sequence error");
        let rec = Record {
            state: State(*b"HY010"),
            message,
            ..Record::default()
        };

        // test formatting
        assert_eq!(
            format!("{rec}"),
            "State: HY010, Native error: 0, Message: [Microsoft][ODBC Driver Manager] \
             Function sequence error"
        );
    }
}