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