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