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