odbc_api/
error.rs

1use std::io;
2
3use thiserror::Error as ThisError;
4
5use crate::handles::{log_diagnostics, Diagnostics, Record as DiagnosticRecord, SqlResult};
6
7/// Error indicating a failed allocation for a column buffer
8#[derive(Debug)]
9pub struct TooLargeBufferSize {
10    /// Number of elements supposed to be in the buffer.
11    pub num_elements: usize,
12    /// Element size in the buffer in bytes.
13    pub element_size: usize,
14}
15
16impl TooLargeBufferSize {
17    /// Map the column allocation error to an [`crate::Error`] adding the context of which
18    /// column caused the allocation error.
19    pub fn add_context(self, buffer_index: u16) -> Error {
20        Error::TooLargeColumnBufferSize {
21            buffer_index,
22            num_elements: self.num_elements,
23            element_size: self.element_size,
24        }
25    }
26}
27
28#[cfg(feature = "odbc_version_3_5")]
29const ODBC_VERSION_STRING: &str = "3.5";
30#[cfg(not(feature = "odbc_version_3_5"))]
31const ODBC_VERSION_STRING: &str = "3.80";
32
33#[derive(Debug, ThisError)]
34/// Error type used to indicate a low level ODBC call returned with SQL_ERROR.
35pub enum Error {
36    /// Setting connection pooling option failed. Exclusively emitted by
37    /// [`crate::Environment::set_connection_pooling`].
38    #[error("Failed to set connection pooling.")]
39    FailedSettingConnectionPooling,
40    /// Allocating the environment itself fails. Further diagnostics are not available, as they
41    /// would be retrieved using the envirorment handle. Exclusively emitted by
42    /// [`crate::Environment::new`].
43    #[error("Failed to allocate ODBC Environment.")]
44    FailedAllocatingEnvironment,
45    /// This should never happen, given that ODBC driver manager and ODBC driver do not have any
46    /// Bugs. Since we may link vs a bunch of these, better to be on the safe side.
47    #[error(
48        "No Diagnostics available. The ODBC function call to {} returned an error. Sadly neither \
49        the ODBC driver manager, nor the driver were polite enough to leave a diagnostic record \
50        specifying what exactly went wrong.",
51        function
52    )]
53    NoDiagnostics {
54        /// ODBC API call which returned error without producing a diagnostic record.
55        function: &'static str,
56    },
57    /// SQL Error had been returned by a low level ODBC function call. A Diagnostic record is
58    /// obtained and associated with this error.
59    #[error("ODBC emitted an error calling '{function}':\n{record}")]
60    Diagnostics {
61        /// Diagnostic record returned by the ODBC driver manager
62        record: DiagnosticRecord,
63        /// ODBC API call which produced the diagnostic record
64        function: &'static str,
65    },
66    /// A user dialog to complete the connection string has been aborted.
67    #[error("The dialog shown to provide or complete the connection string has been aborted.")]
68    AbortedConnectionStringCompletion,
69    /// An error returned if we fail to set the ODBC version
70    #[error(
71        "The ODBC diver manager installed in your system does not seem to support ODBC API version \
72        {ODBC_VERSION_STRING}. Which is required by this application. Most likely you need to \
73        update your driver manager. Your driver manager is most likely unixODBC if you run on a \
74        Linux. Diagnostic record returned by SQLSetEnvAttr:\n{0}"
75    )]
76    UnsupportedOdbcApiVersion(DiagnosticRecord),
77    /// An error emitted by an `std::io::ReadBuf` implementation used as an input argument.
78    #[error("Sending data to the database at statement execution time failed. IO error:\n{0}")]
79    FailedReadingInput(io::Error),
80    /// Driver returned "invalid attribute" then setting the row array size. Most likely the array
81    /// size is too large. Instead of returing "option value changed (SQLSTATE 01S02)" as suggested
82    /// in <https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlsetstmtattr-function> the
83    /// driver returned an error instead.
84    #[error(
85        "An invalid row array size (aka. batch size) has been set. The ODBC drivers should just \
86        emit a warning and emmit smaller batches, but not all do (yours does not at least). Try \
87        fetching data from the database in smaller batches.\nRow array size (aka. batch size): \
88        {size}\n Diagnostic record returned by SQLSetEnvAttr:\n{record}"
89    )]
90    InvalidRowArraySize {
91        record: DiagnosticRecord,
92        size: usize,
93    },
94    #[error(
95        "Tried to retrieve a value from the database. The value turned out to be `NULL` yet this \
96        turned out to not be representable. So the application is written as if the value could \
97        never be `NULL` in the datasource, yet the in actuallity a `NULL` has been returned. \
98        Diagnostic record returned:\n{0}"
99    )]
100    UnableToRepresentNull(DiagnosticRecord),
101    /// There are plenty of issues in the net about Oracle ODBC driver not supporting 64Bit. This
102    /// message, should make it easier identify what is going on, since the message emmitted by,
103    /// Oracles ODBC driver is a bit cryptic: `[Oracle][ODBC]Invalid SQL data type <-25>`.
104    #[error(
105        "SQLFetch came back with an error indicating you specified an invalid SQL Type. You very \
106        likely did not do that however. Actually SQLFetch is not supposed to return that error \
107        type.  You should have received it back than you were still binding columns or parameters. \
108        All this is circumstancial evidence that you are using an Oracle Database and want to use \
109        64Bit integers, which are not supported by Oracles ODBC driver manager. In case this \
110        diagnose is wrong the original error is:\n{0}."
111    )]
112    OracleOdbcDriverDoesNotSupport64Bit(DiagnosticRecord),
113    #[error(
114        "There is not enough memory to allocate enough memory for a column buffer. Number of \
115        elements requested for the column buffer: {num_elements}; Size needed to hold the largest \
116        possible element: {element_size}."
117    )]
118    TooLargeColumnBufferSize {
119        /// Zero based column buffer index. Note that this is different from the 1 based column
120        /// index.
121        buffer_index: u16,
122        num_elements: usize,
123        /// `usize::MAX` may be used to indicate a missing aupper bound of an element.
124        element_size: usize,
125    },
126    #[error(
127        "A value (at least one) is too large to be written into the allocated buffer without \
128        truncation. Size in bytes indicated by ODBC driver: {indicator:?}"
129    )]
130    TooLargeValueForBuffer {
131        /// Length of the complete value in bytes as reported by the ODBC driver. If the length is
132        /// not known, this is `None`.
133        indicator: Option<usize>,
134        /// Index of the buffer in which the truncation occurred.
135        buffer_index: usize,
136    },
137}
138
139impl Error {
140    /// Allows for mapping the error variant from the "catch all" diagnostic to a more specific one
141    /// offering the oppertunity to provide context in the error message.
142    fn provide_context_for_diagnostic<F>(self, f: F) -> Self
143    where
144        F: FnOnce(DiagnosticRecord, &'static str) -> Error,
145    {
146        if let Error::Diagnostics { record, function } = self {
147            f(record, function)
148        } else {
149            self
150        }
151    }
152}
153
154/// Convinience for easily providing more context to errors without an additional call to `map_err`
155pub(crate) trait ExtendResult {
156    fn provide_context_for_diagnostic<F>(self, f: F) -> Self
157    where
158        F: FnOnce(DiagnosticRecord, &'static str) -> Error;
159}
160
161impl<T> ExtendResult for Result<T, Error> {
162    fn provide_context_for_diagnostic<F>(self, f: F) -> Self
163    where
164        F: FnOnce(DiagnosticRecord, &'static str) -> Error,
165    {
166        self.map_err(|error| error.provide_context_for_diagnostic(f))
167    }
168}
169
170impl SqlResult<()> {
171    /// Use this instead of [`Self::into_result`] if you expect [`SqlResult::NoData`] to be a
172    /// valid value. [`SqlResult::NoData`] is mapped to `Ok(false)`, all other success values are
173    /// `Ok(true)`.
174    pub fn into_result_bool(self, handle: &impl Diagnostics) -> Result<bool, Error> {
175        self.on_success(|| true)
176            .into_result_with(handle, Some(false), None)
177    }
178}
179
180// Define that here rather than in `sql_result` mod to keep the `handles` module entirely agnostic
181// about the top level `Error` type.
182impl<T> SqlResult<T> {
183    /// [`Self::Success`] and [`Self::SuccessWithInfo`] are mapped to Ok. In case of
184    /// [`Self::SuccessWithInfo`] any diagnostics are logged. [`Self::Error`] is mapped to error.
185    pub fn into_result(self, handle: &impl Diagnostics) -> Result<T, Error> {
186        self.into_result_with(handle, None, None)
187    }
188
189    /// Like [`Self::into_result`], but [`SqlResult::NoData`] is mapped to `None`, and any success
190    /// is mapped to `Some`.
191    pub fn into_result_option(self, handle: &impl Diagnostics) -> Result<Option<T>, Error> {
192        self.map(Some).into_result_with(handle, Some(None), None)
193    }
194
195    /// Most flexible way of converting an `SqlResult` to an idiomatic `Result`.
196    ///
197    /// # Parameters
198    ///
199    /// * `handle`: This handle is used to extract diagnostics in case `self` is
200    ///   [`SqlResult::SuccessWithInfo`] or [`SqlResult::Error`].
201    /// * `error_for_truncation`: Intended to be used to be used after bulk fetching into a buffer.
202    ///   If `error_for_truncation` is `true` any diagnostics are inspected for truncation. If any
203    ///   truncation is found an error is returned.
204    /// * `no_data`: Controls the behaviour for [`SqlResult::NoData`]. `None` indicates that the
205    ///   result is never expected to be [`SqlResult::NoData`] and would panic in that case.
206    ///   `Some(value)` would cause [`SqlResult::NoData`] to be mapped to `Ok(value)`.
207    /// * `need_data`: Controls the behaviour for [`SqlResult::NeedData`]. `None` indicates that the
208    ///   result is never expected to be [`SqlResult::NeedData`] and would panic in that case.
209    ///   `Some(value)` would cause [`SqlResult::NeedData`] to be mapped to `Ok(value)`.
210    pub fn into_result_with(
211        self,
212        handle: &impl Diagnostics,
213        no_data: Option<T>,
214        need_data: Option<T>,
215    ) -> Result<T, Error> {
216        match self {
217            // The function has been executed successfully. Holds result.
218            SqlResult::Success(value) => Ok(value),
219            // The function has been executed successfully. There have been warnings. Holds result.
220            SqlResult::SuccessWithInfo(value) => {
221                log_diagnostics(handle);
222                Ok(value)
223            }
224            SqlResult::Error { function } => {
225                let mut record = DiagnosticRecord::with_capacity(512);
226                if record.fill_from(handle, 1) {
227                    log_diagnostics(handle);
228                    Err(Error::Diagnostics { record, function })
229                } else {
230                    // Anecdotal ways to reach this code paths:
231                    //
232                    // * Inserting a 64Bit integers into an Oracle Database.
233                    // * Specifying invalid drivers (e.g. missing .so the driver itself depends on)
234                    Err(Error::NoDiagnostics { function })
235                }
236            }
237            SqlResult::NoData => {
238                Ok(no_data.expect("Unexepcted SQL_NO_DATA returned by ODBC function"))
239            }
240            SqlResult::NeedData => {
241                Ok(need_data.expect("Unexepcted SQL_NEED_DATA returned by ODBC function"))
242            }
243            SqlResult::StillExecuting => panic!(
244                "SqlResult must not be converted to result while the function is still executing."
245            ),
246        }
247    }
248}