Skip to main content

quack_rs/
error_data.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Structured error data (`DuckDB` 1.5.0+).
7//!
8//! [`ErrorData`] is an RAII wrapper around `DuckDB`'s `duckdb_error_data` handle —
9//! the structured error type returned by several 1.5.0 C API surfaces, including
10//! expression folding ([`Expression::fold`][crate::expression::Expression::fold]),
11//! the file system API ([`crate::file_system`]), and the appender error accessor
12//! ([`Appender::error_data`][crate::appender::Appender::error_data]).
13//!
14//! Unlike a bare error string, an `ErrorData` carries both a human-readable
15//! message and a machine-readable [`DuckDbErrorType`] category, so an extension
16//! can branch on the kind of failure (e.g. distinguishing `IO` from
17//! `OutOfMemory`).
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use quack_rs::error_data::{DuckDbErrorType, ErrorData};
23//!
24//! // Construct a structured error to hand back to DuckDB.
25//! let err = ErrorData::new(DuckDbErrorType::InvalidInput, "row index out of range");
26//! assert!(err.has_error());
27//! assert_eq!(err.error_type(), DuckDbErrorType::InvalidInput);
28//! ```
29
30use std::ffi::{CStr, CString};
31use std::fmt;
32
33use libduckdb_sys::{
34    duckdb_create_error_data, duckdb_destroy_error_data, duckdb_error_data,
35    duckdb_error_data_error_type, duckdb_error_data_has_error, duckdb_error_data_message,
36    duckdb_error_type, duckdb_error_type_DUCKDB_ERROR_BINDER,
37    duckdb_error_type_DUCKDB_ERROR_CATALOG, duckdb_error_type_DUCKDB_ERROR_CONNECTION,
38    duckdb_error_type_DUCKDB_ERROR_CONSTRAINT, duckdb_error_type_DUCKDB_ERROR_CONVERSION,
39    duckdb_error_type_DUCKDB_ERROR_DECIMAL, duckdb_error_type_DUCKDB_ERROR_DEPENDENCY,
40    duckdb_error_type_DUCKDB_ERROR_DIVIDE_BY_ZERO, duckdb_error_type_DUCKDB_ERROR_EXECUTOR,
41    duckdb_error_type_DUCKDB_ERROR_EXPRESSION, duckdb_error_type_DUCKDB_ERROR_FATAL,
42    duckdb_error_type_DUCKDB_ERROR_HTTP, duckdb_error_type_DUCKDB_ERROR_INDEX,
43    duckdb_error_type_DUCKDB_ERROR_INTERNAL, duckdb_error_type_DUCKDB_ERROR_INTERRUPT,
44    duckdb_error_type_DUCKDB_ERROR_INVALID, duckdb_error_type_DUCKDB_ERROR_INVALID_INPUT,
45    duckdb_error_type_DUCKDB_ERROR_INVALID_TYPE, duckdb_error_type_DUCKDB_ERROR_IO,
46    duckdb_error_type_DUCKDB_ERROR_MISMATCH_TYPE, duckdb_error_type_DUCKDB_ERROR_MISSING_EXTENSION,
47    duckdb_error_type_DUCKDB_ERROR_NETWORK, duckdb_error_type_DUCKDB_ERROR_NOT_IMPLEMENTED,
48    duckdb_error_type_DUCKDB_ERROR_NULL_POINTER, duckdb_error_type_DUCKDB_ERROR_OBJECT_SIZE,
49    duckdb_error_type_DUCKDB_ERROR_OPTIMIZER, duckdb_error_type_DUCKDB_ERROR_OUT_OF_MEMORY,
50    duckdb_error_type_DUCKDB_ERROR_OUT_OF_RANGE,
51    duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_ALLOWED,
52    duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_RESOLVED, duckdb_error_type_DUCKDB_ERROR_PARSER,
53    duckdb_error_type_DUCKDB_ERROR_PERMISSION, duckdb_error_type_DUCKDB_ERROR_PLANNER,
54    duckdb_error_type_DUCKDB_ERROR_SCHEDULER, duckdb_error_type_DUCKDB_ERROR_SERIALIZATION,
55    duckdb_error_type_DUCKDB_ERROR_SETTINGS, duckdb_error_type_DUCKDB_ERROR_STAT,
56    duckdb_error_type_DUCKDB_ERROR_SYNTAX, duckdb_error_type_DUCKDB_ERROR_TRANSACTION,
57    duckdb_error_type_DUCKDB_ERROR_UNKNOWN_TYPE, duckdb_valid_utf8_check, idx_t,
58};
59
60use crate::error::ExtensionError;
61
62/// The category of a `DuckDB` error, mirroring `duckdb_error_type`.
63///
64/// Unknown or future error categories map to [`DuckDbErrorType::Invalid`].
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum DuckDbErrorType {
68    /// No specific category / invalid.
69    Invalid,
70    /// Value out of the representable range.
71    OutOfRange,
72    /// A type conversion failed.
73    Conversion,
74    /// An unknown type was encountered.
75    UnknownType,
76    /// A decimal-specific error.
77    Decimal,
78    /// A type mismatch.
79    MismatchType,
80    /// Division by zero.
81    DivideByZero,
82    /// An object-size error.
83    ObjectSize,
84    /// An invalid type was supplied.
85    InvalidType,
86    /// A (de)serialization error.
87    Serialization,
88    /// A transaction error.
89    Transaction,
90    /// The requested feature is not implemented.
91    NotImplemented,
92    /// An error in an expression.
93    Expression,
94    /// A catalog error (e.g. missing table).
95    Catalog,
96    /// A parser error.
97    Parser,
98    /// A planner error.
99    Planner,
100    /// A scheduler error.
101    Scheduler,
102    /// An executor error.
103    Executor,
104    /// A constraint violation.
105    Constraint,
106    /// An index error.
107    Index,
108    /// A statistics error.
109    Stat,
110    /// A connection error.
111    Connection,
112    /// A syntax error.
113    Syntax,
114    /// A settings error.
115    Settings,
116    /// A binder error.
117    Binder,
118    /// A network error.
119    Network,
120    /// An optimizer error.
121    Optimizer,
122    /// A null-pointer error.
123    NullPointer,
124    /// An I/O error.
125    Io,
126    /// The query was interrupted.
127    Interrupt,
128    /// A fatal error — the database is in an unusable state.
129    Fatal,
130    /// An internal error (a `DuckDB` bug).
131    Internal,
132    /// Invalid input was supplied.
133    InvalidInput,
134    /// Out of memory.
135    OutOfMemory,
136    /// A permission error.
137    Permission,
138    /// A prepared-statement parameter could not be resolved.
139    ParameterNotResolved,
140    /// A prepared-statement parameter is not allowed here.
141    ParameterNotAllowed,
142    /// A dependency error.
143    Dependency,
144    /// An HTTP error.
145    Http,
146    /// A required extension is missing.
147    MissingExtension,
148}
149
150impl DuckDbErrorType {
151    /// Converts to the `DuckDB` C API constant.
152    #[must_use]
153    pub(crate) const fn to_raw(self) -> duckdb_error_type {
154        match self {
155            Self::Invalid => duckdb_error_type_DUCKDB_ERROR_INVALID,
156            Self::OutOfRange => duckdb_error_type_DUCKDB_ERROR_OUT_OF_RANGE,
157            Self::Conversion => duckdb_error_type_DUCKDB_ERROR_CONVERSION,
158            Self::UnknownType => duckdb_error_type_DUCKDB_ERROR_UNKNOWN_TYPE,
159            Self::Decimal => duckdb_error_type_DUCKDB_ERROR_DECIMAL,
160            Self::MismatchType => duckdb_error_type_DUCKDB_ERROR_MISMATCH_TYPE,
161            Self::DivideByZero => duckdb_error_type_DUCKDB_ERROR_DIVIDE_BY_ZERO,
162            Self::ObjectSize => duckdb_error_type_DUCKDB_ERROR_OBJECT_SIZE,
163            Self::InvalidType => duckdb_error_type_DUCKDB_ERROR_INVALID_TYPE,
164            Self::Serialization => duckdb_error_type_DUCKDB_ERROR_SERIALIZATION,
165            Self::Transaction => duckdb_error_type_DUCKDB_ERROR_TRANSACTION,
166            Self::NotImplemented => duckdb_error_type_DUCKDB_ERROR_NOT_IMPLEMENTED,
167            Self::Expression => duckdb_error_type_DUCKDB_ERROR_EXPRESSION,
168            Self::Catalog => duckdb_error_type_DUCKDB_ERROR_CATALOG,
169            Self::Parser => duckdb_error_type_DUCKDB_ERROR_PARSER,
170            Self::Planner => duckdb_error_type_DUCKDB_ERROR_PLANNER,
171            Self::Scheduler => duckdb_error_type_DUCKDB_ERROR_SCHEDULER,
172            Self::Executor => duckdb_error_type_DUCKDB_ERROR_EXECUTOR,
173            Self::Constraint => duckdb_error_type_DUCKDB_ERROR_CONSTRAINT,
174            Self::Index => duckdb_error_type_DUCKDB_ERROR_INDEX,
175            Self::Stat => duckdb_error_type_DUCKDB_ERROR_STAT,
176            Self::Connection => duckdb_error_type_DUCKDB_ERROR_CONNECTION,
177            Self::Syntax => duckdb_error_type_DUCKDB_ERROR_SYNTAX,
178            Self::Settings => duckdb_error_type_DUCKDB_ERROR_SETTINGS,
179            Self::Binder => duckdb_error_type_DUCKDB_ERROR_BINDER,
180            Self::Network => duckdb_error_type_DUCKDB_ERROR_NETWORK,
181            Self::Optimizer => duckdb_error_type_DUCKDB_ERROR_OPTIMIZER,
182            Self::NullPointer => duckdb_error_type_DUCKDB_ERROR_NULL_POINTER,
183            Self::Io => duckdb_error_type_DUCKDB_ERROR_IO,
184            Self::Interrupt => duckdb_error_type_DUCKDB_ERROR_INTERRUPT,
185            Self::Fatal => duckdb_error_type_DUCKDB_ERROR_FATAL,
186            Self::Internal => duckdb_error_type_DUCKDB_ERROR_INTERNAL,
187            Self::InvalidInput => duckdb_error_type_DUCKDB_ERROR_INVALID_INPUT,
188            Self::OutOfMemory => duckdb_error_type_DUCKDB_ERROR_OUT_OF_MEMORY,
189            Self::Permission => duckdb_error_type_DUCKDB_ERROR_PERMISSION,
190            Self::ParameterNotResolved => duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_RESOLVED,
191            Self::ParameterNotAllowed => duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_ALLOWED,
192            Self::Dependency => duckdb_error_type_DUCKDB_ERROR_DEPENDENCY,
193            Self::Http => duckdb_error_type_DUCKDB_ERROR_HTTP,
194            Self::MissingExtension => duckdb_error_type_DUCKDB_ERROR_MISSING_EXTENSION,
195        }
196    }
197
198    /// Converts from the `DuckDB` C API constant. Unknown values map to
199    /// [`Invalid`][DuckDbErrorType::Invalid].
200    #[must_use]
201    pub(crate) const fn from_raw(raw: duckdb_error_type) -> Self {
202        match raw {
203            x if x == duckdb_error_type_DUCKDB_ERROR_OUT_OF_RANGE => Self::OutOfRange,
204            x if x == duckdb_error_type_DUCKDB_ERROR_CONVERSION => Self::Conversion,
205            x if x == duckdb_error_type_DUCKDB_ERROR_UNKNOWN_TYPE => Self::UnknownType,
206            x if x == duckdb_error_type_DUCKDB_ERROR_DECIMAL => Self::Decimal,
207            x if x == duckdb_error_type_DUCKDB_ERROR_MISMATCH_TYPE => Self::MismatchType,
208            x if x == duckdb_error_type_DUCKDB_ERROR_DIVIDE_BY_ZERO => Self::DivideByZero,
209            x if x == duckdb_error_type_DUCKDB_ERROR_OBJECT_SIZE => Self::ObjectSize,
210            x if x == duckdb_error_type_DUCKDB_ERROR_INVALID_TYPE => Self::InvalidType,
211            x if x == duckdb_error_type_DUCKDB_ERROR_SERIALIZATION => Self::Serialization,
212            x if x == duckdb_error_type_DUCKDB_ERROR_TRANSACTION => Self::Transaction,
213            x if x == duckdb_error_type_DUCKDB_ERROR_NOT_IMPLEMENTED => Self::NotImplemented,
214            x if x == duckdb_error_type_DUCKDB_ERROR_EXPRESSION => Self::Expression,
215            x if x == duckdb_error_type_DUCKDB_ERROR_CATALOG => Self::Catalog,
216            x if x == duckdb_error_type_DUCKDB_ERROR_PARSER => Self::Parser,
217            x if x == duckdb_error_type_DUCKDB_ERROR_PLANNER => Self::Planner,
218            x if x == duckdb_error_type_DUCKDB_ERROR_SCHEDULER => Self::Scheduler,
219            x if x == duckdb_error_type_DUCKDB_ERROR_EXECUTOR => Self::Executor,
220            x if x == duckdb_error_type_DUCKDB_ERROR_CONSTRAINT => Self::Constraint,
221            x if x == duckdb_error_type_DUCKDB_ERROR_INDEX => Self::Index,
222            x if x == duckdb_error_type_DUCKDB_ERROR_STAT => Self::Stat,
223            x if x == duckdb_error_type_DUCKDB_ERROR_CONNECTION => Self::Connection,
224            x if x == duckdb_error_type_DUCKDB_ERROR_SYNTAX => Self::Syntax,
225            x if x == duckdb_error_type_DUCKDB_ERROR_SETTINGS => Self::Settings,
226            x if x == duckdb_error_type_DUCKDB_ERROR_BINDER => Self::Binder,
227            x if x == duckdb_error_type_DUCKDB_ERROR_NETWORK => Self::Network,
228            x if x == duckdb_error_type_DUCKDB_ERROR_OPTIMIZER => Self::Optimizer,
229            x if x == duckdb_error_type_DUCKDB_ERROR_NULL_POINTER => Self::NullPointer,
230            x if x == duckdb_error_type_DUCKDB_ERROR_IO => Self::Io,
231            x if x == duckdb_error_type_DUCKDB_ERROR_INTERRUPT => Self::Interrupt,
232            x if x == duckdb_error_type_DUCKDB_ERROR_FATAL => Self::Fatal,
233            x if x == duckdb_error_type_DUCKDB_ERROR_INTERNAL => Self::Internal,
234            x if x == duckdb_error_type_DUCKDB_ERROR_INVALID_INPUT => Self::InvalidInput,
235            x if x == duckdb_error_type_DUCKDB_ERROR_OUT_OF_MEMORY => Self::OutOfMemory,
236            x if x == duckdb_error_type_DUCKDB_ERROR_PERMISSION => Self::Permission,
237            x if x == duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_RESOLVED => {
238                Self::ParameterNotResolved
239            }
240            x if x == duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_ALLOWED => {
241                Self::ParameterNotAllowed
242            }
243            x if x == duckdb_error_type_DUCKDB_ERROR_DEPENDENCY => Self::Dependency,
244            x if x == duckdb_error_type_DUCKDB_ERROR_HTTP => Self::Http,
245            x if x == duckdb_error_type_DUCKDB_ERROR_MISSING_EXTENSION => Self::MissingExtension,
246            _ => Self::Invalid,
247        }
248    }
249
250    /// Returns a short, human-readable label for this error category.
251    #[must_use]
252    pub const fn as_str(self) -> &'static str {
253        match self {
254            Self::Invalid => "invalid",
255            Self::OutOfRange => "out of range",
256            Self::Conversion => "conversion",
257            Self::UnknownType => "unknown type",
258            Self::Decimal => "decimal",
259            Self::MismatchType => "type mismatch",
260            Self::DivideByZero => "divide by zero",
261            Self::ObjectSize => "object size",
262            Self::InvalidType => "invalid type",
263            Self::Serialization => "serialization",
264            Self::Transaction => "transaction",
265            Self::NotImplemented => "not implemented",
266            Self::Expression => "expression",
267            Self::Catalog => "catalog",
268            Self::Parser => "parser",
269            Self::Planner => "planner",
270            Self::Scheduler => "scheduler",
271            Self::Executor => "executor",
272            Self::Constraint => "constraint",
273            Self::Index => "index",
274            Self::Stat => "statistics",
275            Self::Connection => "connection",
276            Self::Syntax => "syntax",
277            Self::Settings => "settings",
278            Self::Binder => "binder",
279            Self::Network => "network",
280            Self::Optimizer => "optimizer",
281            Self::NullPointer => "null pointer",
282            Self::Io => "I/O",
283            Self::Interrupt => "interrupt",
284            Self::Fatal => "fatal",
285            Self::Internal => "internal",
286            Self::InvalidInput => "invalid input",
287            Self::OutOfMemory => "out of memory",
288            Self::Permission => "permission",
289            Self::ParameterNotResolved => "parameter not resolved",
290            Self::ParameterNotAllowed => "parameter not allowed",
291            Self::Dependency => "dependency",
292            Self::Http => "HTTP",
293            Self::MissingExtension => "missing extension",
294        }
295    }
296}
297
298impl fmt::Display for DuckDbErrorType {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.write_str(self.as_str())
301    }
302}
303
304/// RAII wrapper for a `duckdb_error_data` handle.
305///
306/// Automatically destroyed when dropped. Carries a structured error category
307/// ([`DuckDbErrorType`]) and a human-readable message.
308///
309/// Implements [`Display`][std::fmt::Display] and [`std::error::Error`], and
310/// converts into [`ExtensionError`] via [`From`] (or
311/// [`into_extension_error`][ErrorData::into_extension_error]) so it can be
312/// propagated with `?`.
313pub struct ErrorData {
314    raw: duckdb_error_data,
315}
316
317impl ErrorData {
318    /// Creates a new structured error with the given category and message.
319    ///
320    /// If `message` contains an interior null byte it is truncated at that point.
321    #[must_use]
322    pub fn new(error_type: DuckDbErrorType, message: &str) -> Self {
323        let c_msg = CString::new(message).unwrap_or_else(|_| {
324            let pos = message
325                .bytes()
326                .position(|b| b == 0)
327                .unwrap_or(message.len());
328            CString::new(&message.as_bytes()[..pos]).unwrap_or_default()
329        });
330        // SAFETY: error_type.to_raw() is a valid duckdb_error_type and c_msg is a
331        // valid null-terminated string for the duration of the call.
332        let raw = unsafe { duckdb_create_error_data(error_type.to_raw(), c_msg.as_ptr()) };
333        Self { raw }
334    }
335
336    /// Wraps a raw `duckdb_error_data` handle, taking ownership.
337    ///
338    /// # Safety
339    ///
340    /// `raw` must be a `duckdb_error_data` returned by a `DuckDB` API call (it may
341    /// be null). The caller must not destroy the handle after this call.
342    #[inline]
343    #[must_use]
344    pub const unsafe fn from_raw(raw: duckdb_error_data) -> Self {
345        Self { raw }
346    }
347
348    /// Returns the raw handle without consuming the `ErrorData`.
349    #[inline]
350    #[must_use]
351    pub const fn as_raw(&self) -> duckdb_error_data {
352        self.raw
353    }
354
355    /// Returns `true` if the underlying handle is null.
356    #[inline]
357    #[must_use]
358    pub const fn is_null(&self) -> bool {
359        self.raw.is_null()
360    }
361
362    /// Returns `true` if this handle represents an actual error.
363    #[must_use]
364    pub fn has_error(&self) -> bool {
365        if self.raw.is_null() {
366            return false;
367        }
368        // SAFETY: self.raw is a non-null, valid duckdb_error_data.
369        unsafe { duckdb_error_data_has_error(self.raw) }
370    }
371
372    /// Returns the error category.
373    #[must_use]
374    pub fn error_type(&self) -> DuckDbErrorType {
375        if self.raw.is_null() {
376            return DuckDbErrorType::Invalid;
377        }
378        // SAFETY: self.raw is a non-null, valid duckdb_error_data.
379        let raw = unsafe { duckdb_error_data_error_type(self.raw) };
380        DuckDbErrorType::from_raw(raw)
381    }
382
383    /// Returns the error message, or `None` if the handle is null or the message
384    /// is not valid UTF-8.
385    ///
386    /// The returned string is owned by `DuckDB` and copied into a Rust `String`.
387    #[must_use]
388    pub fn message(&self) -> Option<String> {
389        if self.raw.is_null() {
390            return None;
391        }
392        // SAFETY: self.raw is a non-null, valid duckdb_error_data. The returned
393        // pointer is owned by the error data and remains valid while it lives.
394        let ptr = unsafe { duckdb_error_data_message(self.raw) };
395        if ptr.is_null() {
396            return None;
397        }
398        // SAFETY: ptr is a valid null-terminated string owned by the error data.
399        unsafe { CStr::from_ptr(ptr) }
400            .to_str()
401            .ok()
402            .map(String::from)
403    }
404
405    /// Consumes the `ErrorData` and converts it into an [`ExtensionError`].
406    ///
407    /// Useful for propagating a structured `DuckDB` error through `?`.
408    #[must_use]
409    pub fn into_extension_error(self) -> ExtensionError {
410        let msg = self
411            .message()
412            .unwrap_or_else(|| "unknown DuckDB error".to_owned());
413        ExtensionError::new(msg)
414    }
415
416    /// Consumes the `ErrorData` and returns the raw handle.
417    ///
418    /// The caller takes ownership and must call `duckdb_destroy_error_data`.
419    #[inline]
420    #[must_use]
421    pub const fn into_raw(self) -> duckdb_error_data {
422        let raw = self.raw;
423        std::mem::forget(self);
424        raw
425    }
426}
427
428impl Drop for ErrorData {
429    fn drop(&mut self) {
430        if !self.raw.is_null() {
431            // SAFETY: self.raw is a valid duckdb_error_data that we own.
432            unsafe { duckdb_destroy_error_data(&raw mut self.raw) };
433        }
434    }
435}
436
437impl fmt::Debug for ErrorData {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        f.debug_struct("ErrorData")
440            .field("error_type", &self.error_type())
441            .field("message", &self.message())
442            .finish()
443    }
444}
445
446impl fmt::Display for ErrorData {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        match self.message() {
449            Some(msg) => write!(f, "{}: {msg}", self.error_type()),
450            None => f.write_str(self.error_type().as_str()),
451        }
452    }
453}
454
455impl std::error::Error for ErrorData {}
456
457impl From<ErrorData> for ExtensionError {
458    #[inline]
459    fn from(err: ErrorData) -> Self {
460        err.into_extension_error()
461    }
462}
463
464/// Checks whether `bytes` form a valid UTF-8 string according to `DuckDB`'s
465/// validator (`DuckDB` 1.5.0+).
466///
467/// `DuckDB` enforces stricter rules than Rust in some cases (e.g. rejecting
468/// certain code points), so this is useful when validating externally-sourced
469/// bytes before handing them to `DuckDB` string APIs.
470///
471/// # Errors
472///
473/// Returns the structured [`ErrorData`] describing the first validation failure.
474pub fn check_valid_utf8(bytes: &[u8]) -> Result<(), ErrorData> {
475    let len = idx_t::try_from(bytes.len()).unwrap_or(idx_t::MAX);
476    // SAFETY: bytes.as_ptr() is valid for `len` bytes; DuckDB only reads them.
477    let raw = unsafe { duckdb_valid_utf8_check(bytes.as_ptr().cast(), len) };
478    // SAFETY: duckdb_valid_utf8_check returns an owned duckdb_error_data handle.
479    let err = unsafe { ErrorData::from_raw(raw) };
480    if err.has_error() {
481        Err(err)
482    } else {
483        Ok(())
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    const ALL_VARIANTS: [DuckDbErrorType; 40] = [
492        DuckDbErrorType::Invalid,
493        DuckDbErrorType::OutOfRange,
494        DuckDbErrorType::Conversion,
495        DuckDbErrorType::UnknownType,
496        DuckDbErrorType::Decimal,
497        DuckDbErrorType::MismatchType,
498        DuckDbErrorType::DivideByZero,
499        DuckDbErrorType::ObjectSize,
500        DuckDbErrorType::InvalidType,
501        DuckDbErrorType::Serialization,
502        DuckDbErrorType::Transaction,
503        DuckDbErrorType::NotImplemented,
504        DuckDbErrorType::Expression,
505        DuckDbErrorType::Catalog,
506        DuckDbErrorType::Parser,
507        DuckDbErrorType::Planner,
508        DuckDbErrorType::Scheduler,
509        DuckDbErrorType::Executor,
510        DuckDbErrorType::Constraint,
511        DuckDbErrorType::Index,
512        DuckDbErrorType::Stat,
513        DuckDbErrorType::Connection,
514        DuckDbErrorType::Syntax,
515        DuckDbErrorType::Settings,
516        DuckDbErrorType::Binder,
517        DuckDbErrorType::Network,
518        DuckDbErrorType::Optimizer,
519        DuckDbErrorType::NullPointer,
520        DuckDbErrorType::Io,
521        DuckDbErrorType::Interrupt,
522        DuckDbErrorType::Fatal,
523        DuckDbErrorType::Internal,
524        DuckDbErrorType::InvalidInput,
525        DuckDbErrorType::OutOfMemory,
526        DuckDbErrorType::Permission,
527        DuckDbErrorType::ParameterNotResolved,
528        DuckDbErrorType::ParameterNotAllowed,
529        DuckDbErrorType::Dependency,
530        DuckDbErrorType::Http,
531        DuckDbErrorType::MissingExtension,
532    ];
533
534    #[test]
535    fn error_type_round_trip_all_variants() {
536        for variant in ALL_VARIANTS {
537            let raw = variant.to_raw();
538            assert_eq!(
539                DuckDbErrorType::from_raw(raw),
540                variant,
541                "round-trip failed for {variant:?}"
542            );
543        }
544    }
545
546    #[test]
547    fn error_type_unknown_raw_maps_to_invalid() {
548        assert_eq!(DuckDbErrorType::from_raw(9999), DuckDbErrorType::Invalid);
549    }
550
551    #[test]
552    fn error_type_distinct_raw_values() {
553        for (i, a) in ALL_VARIANTS.iter().enumerate() {
554            for b in ALL_VARIANTS.iter().skip(i + 1) {
555                assert_ne!(
556                    a.to_raw(),
557                    b.to_raw(),
558                    "variants {a:?} and {b:?} share a raw value"
559                );
560            }
561        }
562    }
563
564    #[test]
565    fn null_error_data_has_no_error() {
566        let err = unsafe { ErrorData::from_raw(std::ptr::null_mut()) };
567        assert!(err.is_null());
568        assert!(!err.has_error());
569        assert_eq!(err.error_type(), DuckDbErrorType::Invalid);
570        assert!(err.message().is_none());
571    }
572
573    #[test]
574    fn into_raw_forgets_handle() {
575        let err = unsafe { ErrorData::from_raw(std::ptr::null_mut()) };
576        let raw = err.into_raw();
577        assert!(raw.is_null());
578    }
579
580    #[test]
581    fn error_type_display_matches_as_str() {
582        for variant in ALL_VARIANTS {
583            assert!(!variant.as_str().is_empty(), "{variant:?} has empty label");
584            assert_eq!(format!("{variant}"), variant.as_str());
585        }
586    }
587
588    #[test]
589    fn error_type_labels_are_distinct() {
590        for (i, a) in ALL_VARIANTS.iter().enumerate() {
591            for b in ALL_VARIANTS.iter().skip(i + 1) {
592                assert_ne!(a.as_str(), b.as_str(), "{a:?} and {b:?} share a label");
593            }
594        }
595    }
596
597    #[test]
598    fn null_error_data_debug_and_display() {
599        // Null handle: error_type()/message() short-circuit without FFI calls,
600        // so Debug/Display are safe to exercise in a unit test.
601        let err = unsafe { ErrorData::from_raw(std::ptr::null_mut()) };
602        assert_eq!(err.to_string(), "invalid");
603        let dbg = format!("{err:?}");
604        assert!(dbg.contains("ErrorData"), "debug was: {dbg}");
605        assert!(dbg.contains("Invalid"), "debug was: {dbg}");
606    }
607
608    #[test]
609    fn from_error_data_for_extension_error() {
610        let err = unsafe { ErrorData::from_raw(std::ptr::null_mut()) };
611        let ext: ExtensionError = err.into();
612        assert_eq!(ext.as_str(), "unknown DuckDB error");
613    }
614
615    #[test]
616    fn error_data_usable_as_std_error() {
617        fn takes_error(_e: &dyn std::error::Error) {}
618        let err = unsafe { ErrorData::from_raw(std::ptr::null_mut()) };
619        takes_error(&err);
620    }
621}