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}