Skip to main content

duckdb/
error.rs

1use arrow::datatypes::DataType;
2
3use super::Result;
4use crate::{
5    ffi,
6    types::{FromSqlError, Type},
7};
8use std::{error, ffi::CStr, fmt, path::PathBuf, str};
9
10/// Enum listing possible errors from duckdb.
11#[derive(Debug)]
12#[allow(clippy::enum_variant_names)]
13#[non_exhaustive]
14pub enum Error {
15    /// An error from an underlying DuckDB call.
16    DuckDBFailure(ffi::Error, Option<String>),
17
18    /// Error when the value of a particular column is requested, but it cannot
19    /// be converted to the requested Rust type.
20    FromSqlConversionFailure(usize, Type, Box<dyn error::Error + Send + Sync + 'static>),
21
22    /// Error when DuckDB gives us an integral value outside the range of the
23    /// requested type (e.g., trying to get the value 1000 into a `u8`).
24    /// The associated `usize` is the column index,
25    /// and the associated `i128` is the value returned by DuckDB.
26    IntegralValueOutOfRange(usize, i128),
27
28    /// Error when DuckDB gives us an unsigned integral value outside the range
29    /// of the requested type. The associated `usize` is the column index, and
30    /// the associated `u128` is the value returned by DuckDB.
31    UnsignedIntegralValueOutOfRange(usize, u128),
32
33    /// Error converting a string to UTF-8.
34    Utf8Error(str::Utf8Error),
35
36    /// Error converting a string to a C-compatible string because it contained
37    /// an embedded nul.
38    NulError(::std::ffi::NulError),
39
40    /// Error when using SQL named parameters and passing a parameter name not
41    /// present in the SQL.
42    InvalidParameterName(String),
43
44    /// Error converting a file path to a string.
45    InvalidPath(PathBuf),
46
47    /// Error returned when an [`execute`](crate::Connection::execute) call
48    /// returns rows.
49    ExecuteReturnedResults,
50
51    /// Error when a query that was expected to return at least one row (e.g.,
52    /// for [`query_row`](crate::Connection::query_row)) did not return any.
53    QueryReturnedNoRows,
54
55    /// Error when a query that was expected to return only one row (e.g.,
56    /// for [`Statement::query_one`](crate::Statement::query_one)) did return
57    /// more than one.
58    QueryReturnedMoreThanOneRow,
59
60    /// Error when the value of a particular column is requested, but the index
61    /// is out of range for the statement.
62    InvalidColumnIndex(usize),
63
64    /// Error when the value of a named column is requested, but no column
65    /// matches the name for the statement.
66    InvalidColumnName(String),
67
68    /// Error when the value of a particular column is requested, but the type
69    /// of the result in that column cannot be converted to the requested
70    /// Rust type.
71    InvalidColumnType(usize, String, Type),
72
73    /// Error when datatype to duckdb type
74    ArrowTypeToDuckdbType(String, DataType),
75
76    /// Error when a query that was expected to insert one row did not insert
77    /// any or insert many.
78    StatementChangedRows(usize),
79
80    /// Error available for the implementors of the
81    /// [`ToSql`](crate::types::ToSql) trait.
82    ToSqlConversionFailure(Box<dyn error::Error + Send + Sync + 'static>),
83
84    /// Error when the SQL is not a `SELECT`, is not read-only.
85    InvalidQuery,
86
87    /// Error when the SQL contains multiple statements.
88    MultipleStatement,
89
90    /// Error when the number of bound parameters does not match the number of
91    /// parameters in the query. The first `usize` is how many parameters were
92    /// given, the 2nd is how many were expected.
93    InvalidParameterCount(usize, usize),
94
95    /// Error when a parameter is requested, but the index is out of range
96    /// for the statement.
97    InvalidParameterIndex(usize),
98
99    /// Append Error
100    AppendError,
101}
102
103impl PartialEq for Error {
104    fn eq(&self, other: &Self) -> bool {
105        match (self, other) {
106            (Self::DuckDBFailure(e1, s1), Self::DuckDBFailure(e2, s2)) => e1 == e2 && s1 == s2,
107            (Self::IntegralValueOutOfRange(i1, n1), Self::IntegralValueOutOfRange(i2, n2)) => i1 == i2 && n1 == n2,
108            (Self::UnsignedIntegralValueOutOfRange(i1, n1), Self::UnsignedIntegralValueOutOfRange(i2, n2)) => {
109                i1 == i2 && n1 == n2
110            }
111            (Self::Utf8Error(e1), Self::Utf8Error(e2)) => e1 == e2,
112            (Self::NulError(e1), Self::NulError(e2)) => e1 == e2,
113            (Self::InvalidParameterName(n1), Self::InvalidParameterName(n2)) => n1 == n2,
114            (Self::InvalidPath(p1), Self::InvalidPath(p2)) => p1 == p2,
115            (Self::ExecuteReturnedResults, Self::ExecuteReturnedResults) => true,
116            (Self::QueryReturnedNoRows, Self::QueryReturnedNoRows) => true,
117            (Self::QueryReturnedMoreThanOneRow, Self::QueryReturnedMoreThanOneRow) => true,
118            (Self::InvalidColumnIndex(i1), Self::InvalidColumnIndex(i2)) => i1 == i2,
119            (Self::InvalidColumnName(n1), Self::InvalidColumnName(n2)) => n1 == n2,
120            (Self::InvalidColumnType(i1, n1, t1), Self::InvalidColumnType(i2, n2, t2)) => {
121                i1 == i2 && t1 == t2 && n1 == n2
122            }
123            (Self::StatementChangedRows(n1), Self::StatementChangedRows(n2)) => n1 == n2,
124            (Self::InvalidParameterCount(i1, n1), Self::InvalidParameterCount(i2, n2)) => i1 == i2 && n1 == n2,
125            (Self::InvalidParameterIndex(i1), Self::InvalidParameterIndex(i2)) => i1 == i2,
126            (..) => false,
127        }
128    }
129}
130
131impl From<str::Utf8Error> for Error {
132    #[cold]
133    fn from(err: str::Utf8Error) -> Self {
134        Self::Utf8Error(err)
135    }
136}
137
138impl From<::std::ffi::NulError> for Error {
139    #[cold]
140    fn from(err: ::std::ffi::NulError) -> Self {
141        Self::NulError(err)
142    }
143}
144
145const UNKNOWN_COLUMN: usize = usize::MAX;
146
147/// The conversion isn't precise, but it's convenient to have it
148/// to allow use of `get_raw(…).as_…()?` in callbacks that take `Error`.
149impl From<FromSqlError> for Error {
150    #[cold]
151    fn from(err: FromSqlError) -> Self {
152        // The error type requires index and type fields, but they aren't known in this
153        // context.
154        match err {
155            FromSqlError::OutOfRange(val) => Self::IntegralValueOutOfRange(UNKNOWN_COLUMN, val),
156            FromSqlError::OutOfRangeUnsigned(val) => Self::UnsignedIntegralValueOutOfRange(UNKNOWN_COLUMN, val),
157            #[cfg(feature = "uuid")]
158            FromSqlError::InvalidUuidSize(_) => {
159                Self::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Blob, Box::new(err))
160            }
161            FromSqlError::Other(source) => Self::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, source),
162            _ => Self::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, Box::new(err)),
163        }
164    }
165}
166
167impl fmt::Display for Error {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match *self {
170            Self::DuckDBFailure(ref err, None) => err.fmt(f),
171            Self::DuckDBFailure(_, Some(ref s)) => write!(f, "{s}"),
172            Self::FromSqlConversionFailure(i, ref t, ref err) => {
173                if i != UNKNOWN_COLUMN {
174                    write!(f, "Conversion error from type {t} at index: {i}, {err}")
175                } else {
176                    err.fmt(f)
177                }
178            }
179            Self::IntegralValueOutOfRange(col, val) => {
180                if col != UNKNOWN_COLUMN {
181                    write!(f, "Integer {val} out of range at index {col}")
182                } else {
183                    write!(f, "Integer {val} out of range")
184                }
185            }
186            Self::UnsignedIntegralValueOutOfRange(col, val) => {
187                if col != UNKNOWN_COLUMN {
188                    write!(f, "Unsigned integer {val} out of range at index {col}")
189                } else {
190                    write!(f, "Unsigned integer {val} out of range")
191                }
192            }
193            Self::Utf8Error(ref err) => err.fmt(f),
194            Self::NulError(ref err) => err.fmt(f),
195            Self::InvalidParameterName(ref name) => write!(f, "Invalid parameter name: {name}"),
196            Self::InvalidPath(ref p) => write!(f, "Invalid path: {}", p.to_string_lossy()),
197            Self::ExecuteReturnedResults => {
198                write!(f, "Execute returned results - did you mean to call query?")
199            }
200            Self::QueryReturnedNoRows => write!(f, "Query returned no rows"),
201            Self::QueryReturnedMoreThanOneRow => write!(f, "Query returned more than one row"),
202            Self::InvalidColumnIndex(i) => write!(f, "Invalid column index: {i}"),
203            Self::InvalidColumnName(ref name) => write!(f, "Invalid column name: {name}"),
204            Self::InvalidColumnType(i, ref name, ref t) => {
205                write!(f, "Invalid column type {t} at index: {i}, name: {name}")
206            }
207            Self::ArrowTypeToDuckdbType(ref name, ref t) => {
208                write!(f, "Invalid column type {t} , name: {name}")
209            }
210            Self::InvalidParameterCount(i1, n1) => {
211                write!(f, "Wrong number of parameters passed to query. Got {i1}, needed {n1}")
212            }
213            Self::InvalidParameterIndex(i) => write!(f, "Invalid parameter index: {i}"),
214            Self::StatementChangedRows(i) => write!(f, "Query changed {i} rows"),
215            Self::ToSqlConversionFailure(ref err) => err.fmt(f),
216            Self::InvalidQuery => write!(f, "Query is not read-only"),
217            Self::MultipleStatement => write!(f, "Multiple statements provided"),
218            Self::AppendError => write!(f, "Append error"),
219        }
220    }
221}
222
223impl error::Error for Error {
224    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
225        match *self {
226            Self::DuckDBFailure(ref err, _) => Some(err),
227            Self::Utf8Error(ref err) => Some(err),
228            Self::NulError(ref err) => Some(err),
229
230            Self::IntegralValueOutOfRange(..)
231            | Self::UnsignedIntegralValueOutOfRange(..)
232            | Self::InvalidParameterName(_)
233            | Self::ExecuteReturnedResults
234            | Self::QueryReturnedNoRows
235            | Self::QueryReturnedMoreThanOneRow
236            | Self::InvalidColumnIndex(_)
237            | Self::InvalidColumnName(_)
238            | Self::InvalidColumnType(..)
239            | Self::InvalidPath(_)
240            | Self::InvalidParameterCount(..)
241            | Self::InvalidParameterIndex(_)
242            | Self::StatementChangedRows(_)
243            | Self::InvalidQuery
244            | Self::AppendError
245            | Self::ArrowTypeToDuckdbType(..)
246            | Self::MultipleStatement => None,
247            Self::FromSqlConversionFailure(_, _, ref err) | Self::ToSqlConversionFailure(ref err) => Some(&**err),
248        }
249    }
250}
251
252// These are public but not re-exported by lib.rs, so only visible within crate.
253
254#[inline]
255fn result_from_duckdb_state(code: ffi::duckdb_state, message: Option<String>) -> Result<()> {
256    Err(duckdb_failure_from_state(code, message))
257}
258
259#[inline]
260pub(crate) fn duckdb_failure_from_state(code: ffi::duckdb_state, message: Option<String>) -> Error {
261    Error::DuckDBFailure(ffi::Error::new(code), message)
262}
263
264#[inline]
265fn duckdb_failure_from_error_type(error_type: ffi::duckdb_error_type, message: Option<String>) -> Error {
266    Error::DuckDBFailure(ffi::Error::new(error_type as ffi::duckdb_state), message)
267}
268
269#[inline]
270pub(crate) fn duckdb_failure_from_message(message: impl Into<String>) -> Error {
271    duckdb_failure_from_state(ffi::DuckDBError, Some(message.into()))
272}
273
274#[inline]
275pub(crate) fn arrow_conversion_failure(context: &str, err: impl fmt::Display) -> Error {
276    duckdb_failure_from_message(format!("{context}: {err}"))
277}
278
279#[inline]
280fn appender_error_message(appender: ffi::duckdb_appender) -> Option<String> {
281    if appender.is_null() {
282        return Some("appender is null".to_string());
283    }
284    unsafe {
285        let c_err = ffi::duckdb_appender_error(appender);
286        if c_err.is_null() {
287            None
288        } else {
289            Some(CStr::from_ptr(c_err).to_string_lossy().into_owned())
290        }
291    }
292}
293
294#[inline]
295pub(crate) fn error_from_appender_code(code: ffi::duckdb_state, appender: ffi::duckdb_appender) -> Error {
296    Error::DuckDBFailure(ffi::Error::new(code), appender_error_message(appender))
297}
298
299#[cold]
300#[inline]
301pub fn result_from_duckdb_appender(code: ffi::duckdb_state, appender: *mut ffi::duckdb_appender) -> Result<()> {
302    if code == ffi::DuckDBSuccess {
303        return Ok(());
304    }
305    unsafe {
306        let message = appender_error_message(*appender);
307        if !(*appender).is_null() {
308            ffi::duckdb_appender_destroy(appender);
309        }
310        result_from_duckdb_state(code, message)
311    }
312}
313
314#[cold]
315#[inline]
316pub fn result_from_duckdb_prepare(code: ffi::duckdb_state, mut prepare: ffi::duckdb_prepared_statement) -> Result<()> {
317    if code == ffi::DuckDBSuccess {
318        return Ok(());
319    }
320    unsafe {
321        let message = if prepare.is_null() {
322            Some("prepare is null".to_string())
323        } else {
324            let c_err = ffi::duckdb_prepare_error(prepare);
325            let message = Some(CStr::from_ptr(c_err).to_string_lossy().to_string());
326            ffi::duckdb_destroy_prepare(&mut prepare);
327            message
328        };
329        result_from_duckdb_state(code, message)
330    }
331}
332
333#[cold]
334#[inline]
335pub fn result_from_duckdb_result(code: ffi::duckdb_state, result: *mut ffi::duckdb_result) -> Result<()> {
336    if code == ffi::DuckDBSuccess {
337        return Ok(());
338    }
339    unsafe {
340        if result.is_null() {
341            result_from_duckdb_state(code, Some("result is null".to_string()))
342        } else {
343            let message = result_error_message(result);
344            ffi::duckdb_destroy_result(result);
345            result_from_duckdb_state(code, message)
346        }
347    }
348}
349
350/// Returns the current DuckDB error message for a result handle.
351///
352/// # Safety
353///
354/// `result` must be a live DuckDB result handle.
355pub(crate) unsafe fn result_error_message(result: *mut ffi::duckdb_result) -> Option<String> {
356    unsafe {
357        debug_assert!(!result.is_null());
358        let c_err = ffi::duckdb_result_error(result);
359        if c_err.is_null() {
360            None
361        } else {
362            Some(CStr::from_ptr(c_err).to_string_lossy().to_string())
363        }
364    }
365}
366
367/// Converts a DuckDB error-data handle into this crate's `Result`.
368///
369/// # Safety
370///
371/// `error_data` must be null or a valid handle allocated by DuckDB. This
372/// function takes ownership of non-null handles and always destroys them before
373/// returning.
374#[inline]
375pub(crate) unsafe fn result_from_duckdb_error_data(mut error_data: ffi::duckdb_error_data) -> Result<()> {
376    unsafe {
377        if error_data.is_null() {
378            return Ok(());
379        }
380        if !ffi::duckdb_error_data_has_error(error_data) {
381            ffi::duckdb_destroy_error_data(&mut error_data);
382            return Ok(());
383        }
384
385        let error_type = ffi::duckdb_error_data_error_type(error_data);
386        let message = {
387            let c_err = ffi::duckdb_error_data_message(error_data);
388            if c_err.is_null() {
389                None
390            } else {
391                Some(CStr::from_ptr(c_err).to_string_lossy().to_string())
392            }
393        };
394        ffi::duckdb_destroy_error_data(&mut error_data);
395        Err(duckdb_failure_from_error_type(error_type, message))
396    }
397}
398
399#[cold]
400#[inline]
401pub fn result_from_duckdb_extract(
402    num_statements: ffi::idx_t,
403    mut extracted: ffi::duckdb_extracted_statements,
404) -> Result<()> {
405    if num_statements > 0 {
406        return Ok(());
407    }
408    unsafe {
409        let message = if extracted.is_null() {
410            Some("extracted statements are null".to_string())
411        } else {
412            let c_err = ffi::duckdb_extract_statements_error(extracted);
413            let message = if c_err.is_null() {
414                None
415            } else {
416                Some(CStr::from_ptr(c_err).to_string_lossy().to_string())
417            };
418            ffi::duckdb_destroy_extracted(&mut extracted);
419            message
420        };
421        result_from_duckdb_state(ffi::DuckDBError, message)
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use std::{ffi::CString, ptr};
428
429    use super::{Error, result_from_duckdb_error_data};
430    use crate::ffi;
431
432    #[test]
433    fn result_from_duckdb_error_data_accepts_null() {
434        unsafe {
435            result_from_duckdb_error_data(ptr::null_mut()).unwrap();
436        }
437    }
438
439    #[test]
440    fn result_from_duckdb_error_data_accepts_no_error_handle() {
441        unsafe {
442            let mut db = ptr::null_mut();
443            assert_eq!(ffi::DuckDBSuccess, ffi::duckdb_open(ptr::null(), &mut db));
444
445            let mut con = ptr::null_mut();
446            assert_eq!(ffi::DuckDBSuccess, ffi::duckdb_connect(db, &mut con));
447
448            let sql = CString::new("CREATE TABLE t(i INTEGER)").unwrap();
449            let mut result: ffi::duckdb_result = std::mem::zeroed();
450            assert_eq!(ffi::DuckDBSuccess, ffi::duckdb_query(con, sql.as_ptr(), &mut result));
451            ffi::duckdb_destroy_result(&mut result);
452
453            let table = CString::new("t").unwrap();
454            let mut appender = ptr::null_mut();
455            assert_eq!(
456                ffi::DuckDBSuccess,
457                ffi::duckdb_appender_create(con, ptr::null(), table.as_ptr(), &mut appender)
458            );
459
460            let error_data = ffi::duckdb_appender_error_data(appender);
461            assert!(!error_data.is_null());
462            assert!(!ffi::duckdb_error_data_has_error(error_data));
463
464            result_from_duckdb_error_data(error_data).unwrap();
465
466            assert_eq!(ffi::DuckDBSuccess, ffi::duckdb_appender_destroy(&mut appender));
467            ffi::duckdb_disconnect(&mut con);
468            ffi::duckdb_close(&mut db);
469        }
470    }
471
472    #[test]
473    fn result_from_duckdb_error_data_returns_error_message() {
474        let message = CString::new("synthetic error").unwrap();
475        let error_data =
476            unsafe { ffi::duckdb_create_error_data(ffi::duckdb_error_type_DUCKDB_ERROR_INTERNAL, message.as_ptr()) };
477
478        let err = unsafe { result_from_duckdb_error_data(error_data) }.unwrap_err();
479
480        match err {
481            Error::DuckDBFailure(err, Some(message)) => {
482                assert_eq!(
483                    err.extended_code,
484                    ffi::duckdb_error_type_DUCKDB_ERROR_INTERNAL as ffi::duckdb_state
485                );
486                assert_eq!(message, "synthetic error");
487            }
488            err => panic!("unexpected error: {err:?}"),
489        }
490    }
491}