Skip to main content

better_duck_core/
error.rs

1#![allow(dead_code)]
2// Direct copy from DuckDB
3
4use crate::ffi::{duckdb_error_type, duckdb_type, Error as FFIError};
5use std::{error, fmt, path::PathBuf, result, str};
6
7/// Describes why a value conversion from or to DuckDB failed.
8#[derive(Debug)]
9pub enum DuckDBConversionError {
10    /// The DuckDB column type did not match the expected Rust type.
11    TypeMismatch {
12        /// The type that was expected.
13        expected: duckdb_type,
14        /// The type that was actually found.
15        found: duckdb_type,
16    },
17    /// A general conversion error with a description.
18    ConversionError(String),
19    /// A null value was encountered where a non-null value was required.
20    NullValue,
21    /// The conversion would lose precision (e.g. Decimal scale overflow).
22    PrecisionLoss(String),
23}
24
25/// How DuckDB itself classified an engine error.
26///
27/// Mirrors `duckdb_error_type`. Marked `#[non_exhaustive]` because DuckDB adds
28/// error types across releases: a value this build does not recognise is kept
29/// verbatim in [`EngineErrorKind::Unknown`] rather than being flattened into a
30/// catch-all, so no information is lost and matching stays forward-compatible.
31///
32/// [`EngineErrorKind::Unavailable`] is distinct from `Unknown`: it means the
33/// DuckDB API in question exposes only a status code and a message, with no
34/// typed classification at all. This driver never invents a concrete kind by
35/// parsing message text.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum EngineErrorKind {
39    /// `DUCKDB_ERROR_INVALID`
40    Invalid,
41    /// `DUCKDB_ERROR_OUT_OF_RANGE`
42    OutOfRange,
43    /// `DUCKDB_ERROR_CONVERSION`
44    Conversion,
45    /// `DUCKDB_ERROR_UNKNOWN_TYPE`
46    UnknownType,
47    /// `DUCKDB_ERROR_DECIMAL`
48    Decimal,
49    /// `DUCKDB_ERROR_MISMATCH_TYPE`
50    MismatchType,
51    /// `DUCKDB_ERROR_DIVIDE_BY_ZERO`
52    DivideByZero,
53    /// `DUCKDB_ERROR_OBJECT_SIZE`
54    ObjectSize,
55    /// `DUCKDB_ERROR_INVALID_TYPE`
56    InvalidType,
57    /// `DUCKDB_ERROR_SERIALIZATION`
58    Serialization,
59    /// `DUCKDB_ERROR_TRANSACTION`
60    Transaction,
61    /// `DUCKDB_ERROR_NOT_IMPLEMENTED`
62    NotImplemented,
63    /// `DUCKDB_ERROR_EXPRESSION`
64    Expression,
65    /// `DUCKDB_ERROR_CATALOG`
66    Catalog,
67    /// `DUCKDB_ERROR_PARSER`
68    Parser,
69    /// `DUCKDB_ERROR_PLANNER`
70    Planner,
71    /// `DUCKDB_ERROR_SCHEDULER`
72    Scheduler,
73    /// `DUCKDB_ERROR_EXECUTOR`
74    Executor,
75    /// `DUCKDB_ERROR_CONSTRAINT` — DuckDB does not say *which* constraint.
76    Constraint,
77    /// `DUCKDB_ERROR_INDEX`
78    Index,
79    /// `DUCKDB_ERROR_STAT`
80    Stat,
81    /// `DUCKDB_ERROR_CONNECTION`
82    Connection,
83    /// `DUCKDB_ERROR_SYNTAX`
84    Syntax,
85    /// `DUCKDB_ERROR_SETTINGS`
86    Settings,
87    /// `DUCKDB_ERROR_BINDER`
88    Binder,
89    /// `DUCKDB_ERROR_NETWORK`
90    Network,
91    /// `DUCKDB_ERROR_OPTIMIZER`
92    Optimizer,
93    /// `DUCKDB_ERROR_NULL_POINTER`
94    NullPointer,
95    /// `DUCKDB_ERROR_IO`
96    Io,
97    /// `DUCKDB_ERROR_INTERRUPT`
98    Interrupt,
99    /// `DUCKDB_ERROR_FATAL`
100    Fatal,
101    /// `DUCKDB_ERROR_INTERNAL`
102    Internal,
103    /// `DUCKDB_ERROR_INVALID_INPUT`
104    InvalidInput,
105    /// `DUCKDB_ERROR_OUT_OF_MEMORY`
106    OutOfMemory,
107    /// `DUCKDB_ERROR_PERMISSION`
108    Permission,
109    /// `DUCKDB_ERROR_PARAMETER_NOT_RESOLVED`
110    ParameterNotResolved,
111    /// `DUCKDB_ERROR_PARAMETER_NOT_ALLOWED`
112    ParameterNotAllowed,
113    /// `DUCKDB_ERROR_DEPENDENCY`
114    Dependency,
115    /// `DUCKDB_ERROR_HTTP`
116    Http,
117    /// `DUCKDB_ERROR_MISSING_EXTENSION`
118    MissingExtension,
119    /// `DUCKDB_ERROR_AUTOLOAD`
120    Autoload,
121    /// `DUCKDB_ERROR_SEQUENCE`
122    Sequence,
123    /// `DUCKDB_INVALID_CONFIGURATION`
124    InvalidConfiguration,
125    /// DuckDB reported a classification this build does not know; the raw value
126    /// is preserved so nothing is lost across DuckDB upgrades.
127    Unknown(duckdb_error_type),
128    /// The originating DuckDB API offers no typed classification — only a status
129    /// code and a message (prepare, extracted statements, pending, table
130    /// description). Not a guess: an explicit absence.
131    Unavailable,
132}
133
134impl EngineErrorKind {
135    /// Every kind that maps 1:1 onto a `duckdb_error_type` this build knows.
136    ///
137    /// Excludes [`Unknown`](EngineErrorKind::Unknown) and
138    /// [`Unavailable`](EngineErrorKind::Unavailable), which have no single raw value.
139    pub const KNOWN: &'static [EngineErrorKind] = &[
140        EngineErrorKind::Invalid,
141        EngineErrorKind::OutOfRange,
142        EngineErrorKind::Conversion,
143        EngineErrorKind::UnknownType,
144        EngineErrorKind::Decimal,
145        EngineErrorKind::MismatchType,
146        EngineErrorKind::DivideByZero,
147        EngineErrorKind::ObjectSize,
148        EngineErrorKind::InvalidType,
149        EngineErrorKind::Serialization,
150        EngineErrorKind::Transaction,
151        EngineErrorKind::NotImplemented,
152        EngineErrorKind::Expression,
153        EngineErrorKind::Catalog,
154        EngineErrorKind::Parser,
155        EngineErrorKind::Planner,
156        EngineErrorKind::Scheduler,
157        EngineErrorKind::Executor,
158        EngineErrorKind::Constraint,
159        EngineErrorKind::Index,
160        EngineErrorKind::Stat,
161        EngineErrorKind::Connection,
162        EngineErrorKind::Syntax,
163        EngineErrorKind::Settings,
164        EngineErrorKind::Binder,
165        EngineErrorKind::Network,
166        EngineErrorKind::Optimizer,
167        EngineErrorKind::NullPointer,
168        EngineErrorKind::Io,
169        EngineErrorKind::Interrupt,
170        EngineErrorKind::Fatal,
171        EngineErrorKind::Internal,
172        EngineErrorKind::InvalidInput,
173        EngineErrorKind::OutOfMemory,
174        EngineErrorKind::Permission,
175        EngineErrorKind::ParameterNotResolved,
176        EngineErrorKind::ParameterNotAllowed,
177        EngineErrorKind::Dependency,
178        EngineErrorKind::Http,
179        EngineErrorKind::MissingExtension,
180        EngineErrorKind::Autoload,
181        EngineErrorKind::Sequence,
182        EngineErrorKind::InvalidConfiguration,
183    ];
184
185    /// Classifies a raw `duckdb_error_type`, preserving unrecognised values.
186    #[must_use]
187    pub fn from_raw(raw: duckdb_error_type) -> EngineErrorKind {
188        use crate::ffi as f;
189        match raw {
190            f::duckdb_error_type_DUCKDB_ERROR_INVALID => EngineErrorKind::Invalid,
191            f::duckdb_error_type_DUCKDB_ERROR_OUT_OF_RANGE => EngineErrorKind::OutOfRange,
192            f::duckdb_error_type_DUCKDB_ERROR_CONVERSION => EngineErrorKind::Conversion,
193            f::duckdb_error_type_DUCKDB_ERROR_UNKNOWN_TYPE => EngineErrorKind::UnknownType,
194            f::duckdb_error_type_DUCKDB_ERROR_DECIMAL => EngineErrorKind::Decimal,
195            f::duckdb_error_type_DUCKDB_ERROR_MISMATCH_TYPE => EngineErrorKind::MismatchType,
196            f::duckdb_error_type_DUCKDB_ERROR_DIVIDE_BY_ZERO => EngineErrorKind::DivideByZero,
197            f::duckdb_error_type_DUCKDB_ERROR_OBJECT_SIZE => EngineErrorKind::ObjectSize,
198            f::duckdb_error_type_DUCKDB_ERROR_INVALID_TYPE => EngineErrorKind::InvalidType,
199            f::duckdb_error_type_DUCKDB_ERROR_SERIALIZATION => EngineErrorKind::Serialization,
200            f::duckdb_error_type_DUCKDB_ERROR_TRANSACTION => EngineErrorKind::Transaction,
201            f::duckdb_error_type_DUCKDB_ERROR_NOT_IMPLEMENTED => EngineErrorKind::NotImplemented,
202            f::duckdb_error_type_DUCKDB_ERROR_EXPRESSION => EngineErrorKind::Expression,
203            f::duckdb_error_type_DUCKDB_ERROR_CATALOG => EngineErrorKind::Catalog,
204            f::duckdb_error_type_DUCKDB_ERROR_PARSER => EngineErrorKind::Parser,
205            f::duckdb_error_type_DUCKDB_ERROR_PLANNER => EngineErrorKind::Planner,
206            f::duckdb_error_type_DUCKDB_ERROR_SCHEDULER => EngineErrorKind::Scheduler,
207            f::duckdb_error_type_DUCKDB_ERROR_EXECUTOR => EngineErrorKind::Executor,
208            f::duckdb_error_type_DUCKDB_ERROR_CONSTRAINT => EngineErrorKind::Constraint,
209            f::duckdb_error_type_DUCKDB_ERROR_INDEX => EngineErrorKind::Index,
210            f::duckdb_error_type_DUCKDB_ERROR_STAT => EngineErrorKind::Stat,
211            f::duckdb_error_type_DUCKDB_ERROR_CONNECTION => EngineErrorKind::Connection,
212            f::duckdb_error_type_DUCKDB_ERROR_SYNTAX => EngineErrorKind::Syntax,
213            f::duckdb_error_type_DUCKDB_ERROR_SETTINGS => EngineErrorKind::Settings,
214            f::duckdb_error_type_DUCKDB_ERROR_BINDER => EngineErrorKind::Binder,
215            f::duckdb_error_type_DUCKDB_ERROR_NETWORK => EngineErrorKind::Network,
216            f::duckdb_error_type_DUCKDB_ERROR_OPTIMIZER => EngineErrorKind::Optimizer,
217            f::duckdb_error_type_DUCKDB_ERROR_NULL_POINTER => EngineErrorKind::NullPointer,
218            f::duckdb_error_type_DUCKDB_ERROR_IO => EngineErrorKind::Io,
219            f::duckdb_error_type_DUCKDB_ERROR_INTERRUPT => EngineErrorKind::Interrupt,
220            f::duckdb_error_type_DUCKDB_ERROR_FATAL => EngineErrorKind::Fatal,
221            f::duckdb_error_type_DUCKDB_ERROR_INTERNAL => EngineErrorKind::Internal,
222            f::duckdb_error_type_DUCKDB_ERROR_INVALID_INPUT => EngineErrorKind::InvalidInput,
223            f::duckdb_error_type_DUCKDB_ERROR_OUT_OF_MEMORY => EngineErrorKind::OutOfMemory,
224            f::duckdb_error_type_DUCKDB_ERROR_PERMISSION => EngineErrorKind::Permission,
225            f::duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_RESOLVED => {
226                EngineErrorKind::ParameterNotResolved
227            },
228            f::duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_ALLOWED => {
229                EngineErrorKind::ParameterNotAllowed
230            },
231            f::duckdb_error_type_DUCKDB_ERROR_DEPENDENCY => EngineErrorKind::Dependency,
232            f::duckdb_error_type_DUCKDB_ERROR_HTTP => EngineErrorKind::Http,
233            f::duckdb_error_type_DUCKDB_ERROR_MISSING_EXTENSION => {
234                EngineErrorKind::MissingExtension
235            },
236            f::duckdb_error_type_DUCKDB_ERROR_AUTOLOAD => EngineErrorKind::Autoload,
237            f::duckdb_error_type_DUCKDB_ERROR_SEQUENCE => EngineErrorKind::Sequence,
238            f::duckdb_error_type_DUCKDB_INVALID_CONFIGURATION => {
239                EngineErrorKind::InvalidConfiguration
240            },
241            other => EngineErrorKind::Unknown(other),
242        }
243    }
244
245    /// Returns the raw `duckdb_error_type` for this kind.
246    ///
247    /// [`Unavailable`](EngineErrorKind::Unavailable) has no DuckDB counterpart and
248    /// maps to `DUCKDB_ERROR_INVALID`, which is what DuckDB itself uses for an
249    /// unclassified error.
250    #[must_use]
251    pub fn to_raw(self) -> duckdb_error_type {
252        use crate::ffi as f;
253        match self {
254            EngineErrorKind::Invalid => f::duckdb_error_type_DUCKDB_ERROR_INVALID,
255            EngineErrorKind::OutOfRange => f::duckdb_error_type_DUCKDB_ERROR_OUT_OF_RANGE,
256            EngineErrorKind::Conversion => f::duckdb_error_type_DUCKDB_ERROR_CONVERSION,
257            EngineErrorKind::UnknownType => f::duckdb_error_type_DUCKDB_ERROR_UNKNOWN_TYPE,
258            EngineErrorKind::Decimal => f::duckdb_error_type_DUCKDB_ERROR_DECIMAL,
259            EngineErrorKind::MismatchType => f::duckdb_error_type_DUCKDB_ERROR_MISMATCH_TYPE,
260            EngineErrorKind::DivideByZero => f::duckdb_error_type_DUCKDB_ERROR_DIVIDE_BY_ZERO,
261            EngineErrorKind::ObjectSize => f::duckdb_error_type_DUCKDB_ERROR_OBJECT_SIZE,
262            EngineErrorKind::InvalidType => f::duckdb_error_type_DUCKDB_ERROR_INVALID_TYPE,
263            EngineErrorKind::Serialization => f::duckdb_error_type_DUCKDB_ERROR_SERIALIZATION,
264            EngineErrorKind::Transaction => f::duckdb_error_type_DUCKDB_ERROR_TRANSACTION,
265            EngineErrorKind::NotImplemented => f::duckdb_error_type_DUCKDB_ERROR_NOT_IMPLEMENTED,
266            EngineErrorKind::Expression => f::duckdb_error_type_DUCKDB_ERROR_EXPRESSION,
267            EngineErrorKind::Catalog => f::duckdb_error_type_DUCKDB_ERROR_CATALOG,
268            EngineErrorKind::Parser => f::duckdb_error_type_DUCKDB_ERROR_PARSER,
269            EngineErrorKind::Planner => f::duckdb_error_type_DUCKDB_ERROR_PLANNER,
270            EngineErrorKind::Scheduler => f::duckdb_error_type_DUCKDB_ERROR_SCHEDULER,
271            EngineErrorKind::Executor => f::duckdb_error_type_DUCKDB_ERROR_EXECUTOR,
272            EngineErrorKind::Constraint => f::duckdb_error_type_DUCKDB_ERROR_CONSTRAINT,
273            EngineErrorKind::Index => f::duckdb_error_type_DUCKDB_ERROR_INDEX,
274            EngineErrorKind::Stat => f::duckdb_error_type_DUCKDB_ERROR_STAT,
275            EngineErrorKind::Connection => f::duckdb_error_type_DUCKDB_ERROR_CONNECTION,
276            EngineErrorKind::Syntax => f::duckdb_error_type_DUCKDB_ERROR_SYNTAX,
277            EngineErrorKind::Settings => f::duckdb_error_type_DUCKDB_ERROR_SETTINGS,
278            EngineErrorKind::Binder => f::duckdb_error_type_DUCKDB_ERROR_BINDER,
279            EngineErrorKind::Network => f::duckdb_error_type_DUCKDB_ERROR_NETWORK,
280            EngineErrorKind::Optimizer => f::duckdb_error_type_DUCKDB_ERROR_OPTIMIZER,
281            EngineErrorKind::NullPointer => f::duckdb_error_type_DUCKDB_ERROR_NULL_POINTER,
282            EngineErrorKind::Io => f::duckdb_error_type_DUCKDB_ERROR_IO,
283            EngineErrorKind::Interrupt => f::duckdb_error_type_DUCKDB_ERROR_INTERRUPT,
284            EngineErrorKind::Fatal => f::duckdb_error_type_DUCKDB_ERROR_FATAL,
285            EngineErrorKind::Internal => f::duckdb_error_type_DUCKDB_ERROR_INTERNAL,
286            EngineErrorKind::InvalidInput => f::duckdb_error_type_DUCKDB_ERROR_INVALID_INPUT,
287            EngineErrorKind::OutOfMemory => f::duckdb_error_type_DUCKDB_ERROR_OUT_OF_MEMORY,
288            EngineErrorKind::Permission => f::duckdb_error_type_DUCKDB_ERROR_PERMISSION,
289            EngineErrorKind::ParameterNotResolved => {
290                f::duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_RESOLVED
291            },
292            EngineErrorKind::ParameterNotAllowed => {
293                f::duckdb_error_type_DUCKDB_ERROR_PARAMETER_NOT_ALLOWED
294            },
295            EngineErrorKind::Dependency => f::duckdb_error_type_DUCKDB_ERROR_DEPENDENCY,
296            EngineErrorKind::Http => f::duckdb_error_type_DUCKDB_ERROR_HTTP,
297            EngineErrorKind::MissingExtension => {
298                f::duckdb_error_type_DUCKDB_ERROR_MISSING_EXTENSION
299            },
300            EngineErrorKind::Autoload => f::duckdb_error_type_DUCKDB_ERROR_AUTOLOAD,
301            EngineErrorKind::Sequence => f::duckdb_error_type_DUCKDB_ERROR_SEQUENCE,
302            EngineErrorKind::InvalidConfiguration => {
303                f::duckdb_error_type_DUCKDB_INVALID_CONFIGURATION
304            },
305            EngineErrorKind::Unknown(raw) => raw,
306            EngineErrorKind::Unavailable => f::duckdb_error_type_DUCKDB_ERROR_INVALID,
307        }
308    }
309}
310
311impl fmt::Display for EngineErrorKind {
312    fn fmt(
313        &self,
314        f: &mut fmt::Formatter<'_>,
315    ) -> fmt::Result {
316        match self {
317            EngineErrorKind::Unknown(raw) => write!(f, "unknown engine error type {raw}"),
318            EngineErrorKind::Unavailable => write!(f, "unclassified engine error"),
319            other => write!(f, "{other:?}"),
320        }
321    }
322}
323
324/// A fully owned engine error: DuckDB's own classification plus its message.
325///
326/// Pure Rust — every field is copied out of DuckDB memory when the error is
327/// built, so an `EngineError` stays valid after the handle it came from is
328/// destroyed and after the connection is closed.
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct EngineError {
331    /// How DuckDB classified the failure.
332    pub kind: EngineErrorKind,
333    /// DuckDB's message, if it supplied one.
334    pub message: Option<String>,
335}
336
337impl EngineError {
338    /// Builds an engine error with no typed classification available.
339    ///
340    /// Use for DuckDB APIs that expose only a status code and a message, so the
341    /// absence of a kind is explicit rather than guessed.
342    #[must_use]
343    pub fn unavailable(message: Option<String>) -> EngineError {
344        EngineError { kind: EngineErrorKind::Unavailable, message }
345    }
346}
347
348impl fmt::Display for EngineError {
349    fn fmt(
350        &self,
351        f: &mut fmt::Formatter<'_>,
352    ) -> fmt::Result {
353        match &self.message {
354            Some(message) => write!(f, "{}: {message}", self.kind),
355            None => write!(f, "{}", self.kind),
356        }
357    }
358}
359
360impl error::Error for EngineError {}
361
362/// Enum listing possible errors from duckdb.
363#[derive(Debug)]
364#[allow(clippy::enum_variant_names)]
365#[non_exhaustive]
366pub enum Error {
367    /// An error from an underlying DuckDB call.
368    DuckDBFailure(FFIError, Option<String>),
369
370    /// A typed engine error carrying DuckDB's own classification.
371    Engine(EngineError),
372
373    /// Error when the value of a particular column is requested, but it cannot
374    /// be converted to the requested Rust type.
375    // FromSqlConversionFailure(usize, Type, Box<dyn error::Error + Send + Sync + 'static>),
376
377    /// Error when DuckDB gives us an integral value outside the range of the
378    /// requested type (e.g., trying to get the value 1000 into a `u8`).
379    /// The associated `usize` is the column index,
380    /// and the associated `i64` is the value returned by DuckDB.
381    IntegralValueOutOfRange(usize, i128),
382
383    /// Error converting a string to UTF-8.
384    Utf8Error(str::Utf8Error),
385
386    /// Error converting a string to a C-compatible string because it contained
387    /// an embedded nul.
388    NulError(::std::ffi::NulError),
389
390    /// Error when using SQL named parameters and passing a parameter name not
391    /// present in the SQL.
392    InvalidParameterName(String),
393
394    /// Error converting a file path to a string.
395    InvalidPath(PathBuf),
396
397    /// Error returned when an [`execute`](crate::connection::Connection::execute) call
398    /// returns rows.
399    ExecuteReturnedResults,
400
401    /// Error when a query that was expected to return at least one row did not
402    /// return any.
403    QueryReturnedNoRows,
404
405    /// Error when the value of a particular column is requested, but the index
406    /// is out of range for the statement.
407    InvalidColumnIndex(usize),
408
409    /// Error when the value of a named column is requested, but no column
410    /// matches the name for the statement.
411    InvalidColumnName(String),
412
413    /// Error when the value of a particular column is requested, but the type
414    /// of the result in that column cannot be converted to the requested
415    /// Rust type.
416    // InvalidColumnType(usize, String, Type),
417
418    /// Error when a query that was expected to insert one row did not insert
419    /// any or inserted many.
420    StatementChangedRows(usize),
421
422    /// Error available for the implementors of the
423    /// [`AppendAble`](crate::types::appendable::AppendAble) trait.
424    ToSqlConversionFailure(Box<dyn error::Error + Send + Sync + 'static>),
425
426    /// Error when the SQL is not a `SELECT`, is not read-only.
427    InvalidQuery,
428
429    /// Error when the SQL contains multiple statements.
430    MultipleStatement,
431
432    /// Error when the number of bound parameters does not match the number of
433    /// parameters in the query. The first `usize` is how many parameters were
434    /// given, the 2nd is how many were expected.
435    InvalidParameterCount(usize, usize),
436
437    /// An error occurred while appending a value via the DuckDB appender API.
438    AppendError,
439
440    /// A value conversion error.
441    ConversionError(DuckDBConversionError),
442
443    /// An unexpected error with no more specific classification.
444    #[allow(non_camel_case_types)]
445    UNKNOWN(Box<dyn ::std::error::Error + Send + Sync + 'static>),
446
447    /// A background blocking task panicked or was cancelled before it produced a result.
448    BackgroundTaskFailed(String),
449
450    /// A connection pool operation failed (checkout timeout, manager error).
451    Pool(String),
452}
453
454/// A typedef of the result returned by many methods.
455pub type Result<T, E = Error> = result::Result<T, E>;
456
457impl PartialEq for Error {
458    fn eq(
459        &self,
460        other: &Error,
461    ) -> bool {
462        match (self, other) {
463            (Error::DuckDBFailure(e1, s1), Error::DuckDBFailure(e2, s2)) => e1 == e2 && s1 == s2,
464            (Error::Engine(a), Error::Engine(b)) => a == b,
465            (Error::IntegralValueOutOfRange(i1, n1), Error::IntegralValueOutOfRange(i2, n2)) => {
466                i1 == i2 && n1 == n2
467            },
468            (Error::Utf8Error(e1), Error::Utf8Error(e2)) => e1 == e2,
469            (Error::NulError(e1), Error::NulError(e2)) => e1 == e2,
470            (Error::InvalidParameterName(n1), Error::InvalidParameterName(n2)) => n1 == n2,
471            (Error::InvalidPath(p1), Error::InvalidPath(p2)) => p1 == p2,
472            (Error::ExecuteReturnedResults, Error::ExecuteReturnedResults) => true,
473            (Error::QueryReturnedNoRows, Error::QueryReturnedNoRows) => true,
474            (Error::InvalidColumnIndex(i1), Error::InvalidColumnIndex(i2)) => i1 == i2,
475            (Error::InvalidColumnName(n1), Error::InvalidColumnName(n2)) => n1 == n2,
476            // (Error::InvalidColumnType(i1, n1, t1), Error::InvalidColumnType(i2, n2, t2)) => {
477            //     i1 == i2 && t1 == t2 && n1 == n2
478            // }
479            (Error::StatementChangedRows(n1), Error::StatementChangedRows(n2)) => n1 == n2,
480            (Error::InvalidParameterCount(i1, n1), Error::InvalidParameterCount(i2, n2)) => {
481                i1 == i2 && n1 == n2
482            },
483            (..) => false,
484        }
485    }
486}
487
488impl From<str::Utf8Error> for Error {
489    #[cold]
490    fn from(err: str::Utf8Error) -> Error {
491        Error::Utf8Error(err)
492    }
493}
494
495impl From<::std::ffi::NulError> for Error {
496    #[cold]
497    fn from(err: ::std::ffi::NulError) -> Error {
498        Error::NulError(err)
499    }
500}
501
502const UNKNOWN_COLUMN: usize = usize::MAX;
503
504/// The conversion isn't precise, but it's convenient to have it
505/// to allow use of `get_raw(…).as_…()?` in callbacks that take `Error`.
506/// ```rust,ignore
507/// impl From<FromSqlError> for Error {
508///     #[cold]
509///     fn from(err: FromSqlError) -> Error {
510///         // The error type requires index and type fields, but they aren't known in this
511///         // context.
512///         match err {
513///             FromSqlError::OutOfRange(val) => Error::IntegralValueOutOfRange(UNKNOWN_COLUMN, val),
514///             #[cfg(feature = "uuid")]
515///             FromSqlError::InvalidUuidSize(_) => {
516///                 Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Blob, Box::new(err))
517///             }
518///             FromSqlError::Other(source) => {
519///                 Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, source)
520///             }
521///             _ => Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, Box::new(err)),
522///         }
523///     }
524/// }
525/// ```
526///
527impl fmt::Display for Error {
528    fn fmt(
529        &self,
530        f: &mut fmt::Formatter<'_>,
531    ) -> fmt::Result {
532        match self {
533            Error::DuckDBFailure(ref err, None) => err.fmt(f),
534            Error::DuckDBFailure(_, Some(ref s)) => write!(f, "{s}"),
535            Error::Engine(ref err) => err.fmt(f),
536            // Error::FromSqlConversionFailure(i, ref t, ref err) => {
537            //     if i != UNKNOWN_COLUMN {
538            //         write!(f, "Conversion error from type {t} at index: {i}, {err}")
539            //     } else {
540            //         err.fmt(f)
541            //     }
542            // }
543            Error::IntegralValueOutOfRange(col, val) => {
544                if *col != UNKNOWN_COLUMN {
545                    write!(f, "Integer {val} out of range at index {col}")
546                } else {
547                    write!(f, "Integer {val} out of range")
548                }
549            },
550            Error::Utf8Error(ref err) => err.fmt(f),
551            Error::NulError(ref err) => err.fmt(f),
552            Error::InvalidParameterName(ref name) => write!(f, "Invalid parameter name: {name}"),
553            Error::InvalidPath(ref p) => write!(f, "Invalid path: {}", p.to_string_lossy()),
554            Error::ExecuteReturnedResults => {
555                write!(f, "Execute returned results - did you mean to call query?")
556            },
557            Error::QueryReturnedNoRows => write!(f, "Query returned no rows"),
558            Error::InvalidColumnIndex(i) => write!(f, "Invalid column index: {i}"),
559            Error::InvalidColumnName(ref name) => write!(f, "Invalid column name: {name}"),
560            // Error::InvalidColumnType(i, ref name, ref t) => {
561            //     write!(f, "Invalid column type {t} at index: {i}, name: {name}")
562            // }
563            // Error::ArrowTypeToDuckdbType(ref name, ref t) => {
564            //     write!(f, "Invalid column type {t} , name: {name}")
565            // }
566            Error::InvalidParameterCount(i1, n1) => {
567                write!(f, "Wrong number of parameters passed to query. Got {i1}, needed {n1}")
568            },
569            Error::StatementChangedRows(i) => write!(f, "Query changed {i} rows"),
570            Error::ToSqlConversionFailure(ref err) => err.fmt(f),
571            Error::InvalidQuery => write!(f, "Query is not read-only"),
572            Error::MultipleStatement => write!(f, "Multiple statements provided"),
573            Error::AppendError => write!(f, "Append error"),
574            Error::ConversionError(ref err) => match err {
575                DuckDBConversionError::TypeMismatch { expected, found } => {
576                    write!(f, "Type mismatch: expected {expected}, found {found}")
577                },
578                DuckDBConversionError::ConversionError(ref msg) => {
579                    write!(f, "Conversion error: {msg}")
580                },
581                DuckDBConversionError::NullValue => write!(f, "Null value encountered"),
582                DuckDBConversionError::PrecisionLoss(ref msg) => write!(f, "Precision loss: {msg}"),
583            },
584            Error::UNKNOWN(e) => write!(f, "Unknown error: {e}"),
585            Error::BackgroundTaskFailed(ref msg) => write!(f, "Background task failed: {msg}"),
586            Error::Pool(ref msg) => write!(f, "Connection pool error: {msg}"),
587        }
588    }
589}
590
591impl error::Error for Error {
592    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
593        match self {
594            Error::DuckDBFailure(ref err, _) => Some(err),
595            Error::Engine(ref err) => Some(err),
596            Error::Utf8Error(ref err) => Some(err),
597            Error::NulError(ref err) => Some(err),
598
599            Error::IntegralValueOutOfRange(..)
600            | Error::InvalidParameterName(_)
601            | Error::ExecuteReturnedResults
602            | Error::QueryReturnedNoRows
603            | Error::InvalidColumnIndex(_)
604            | Error::InvalidColumnName(_)
605            // | Error::InvalidColumnType(..)
606            | Error::InvalidPath(_)
607            | Error::InvalidParameterCount(..)
608            | Error::StatementChangedRows(_)
609            | Error::InvalidQuery
610            | Error::AppendError
611            // | Error::ArrowTypeToDuckdbType(..)
612            | Error::MultipleStatement
613            | Error::ConversionError(_) => None,
614            // Error::FromSqlConversionFailure(_, _, ref err)
615            Error::ToSqlConversionFailure(ref err) => Some(&**err),
616            Error::UNKNOWN(e) => Some(e.as_ref()),
617            Error::BackgroundTaskFailed(_) | Error::Pool(_) => None,
618        }
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use crate::ffi::{DuckDBError, DuckDBSuccess};
626    use std::{error::Error as _, ffi::CString, io};
627
628    fn invalid_utf8() -> str::Utf8Error {
629        let bytes = vec![0xff];
630        str::from_utf8(&bytes).unwrap_err()
631    }
632
633    #[test]
634    fn equality_covers_comparable_variants_and_rejects_others() {
635        assert_eq!(
636            Error::DuckDBFailure(FFIError::new(DuckDBError), Some("context".into())),
637            Error::DuckDBFailure(FFIError::new(DuckDBError), Some("context".into()))
638        );
639        assert_ne!(
640            Error::DuckDBFailure(FFIError::new(DuckDBSuccess), None),
641            Error::DuckDBFailure(FFIError::new(DuckDBError), None)
642        );
643        assert_eq!(Error::IntegralValueOutOfRange(2, 300), Error::IntegralValueOutOfRange(2, 300));
644        assert_ne!(Error::IntegralValueOutOfRange(2, 300), Error::IntegralValueOutOfRange(3, 300));
645        assert_eq!(Error::Utf8Error(invalid_utf8()), Error::Utf8Error(invalid_utf8()));
646        assert_eq!(
647            Error::NulError(CString::new("a\0b").unwrap_err()),
648            Error::NulError(CString::new("a\0b").unwrap_err())
649        );
650        assert_eq!(
651            Error::InvalidParameterName("p".into()),
652            Error::InvalidParameterName("p".into())
653        );
654        assert_eq!(
655            Error::InvalidPath(PathBuf::from("file.db")),
656            Error::InvalidPath(PathBuf::from("file.db"))
657        );
658        assert_eq!(Error::ExecuteReturnedResults, Error::ExecuteReturnedResults);
659        assert_eq!(Error::QueryReturnedNoRows, Error::QueryReturnedNoRows);
660        assert_eq!(Error::InvalidColumnIndex(4), Error::InvalidColumnIndex(4));
661        assert_eq!(
662            Error::InvalidColumnName("value".into()),
663            Error::InvalidColumnName("value".into())
664        );
665        assert_eq!(Error::StatementChangedRows(3), Error::StatementChangedRows(3));
666        assert_eq!(Error::InvalidParameterCount(1, 2), Error::InvalidParameterCount(1, 2));
667        assert_ne!(
668            Error::ToSqlConversionFailure(Box::new(io::Error::other("same"))),
669            Error::ToSqlConversionFailure(Box::new(io::Error::other("same")))
670        );
671        assert_ne!(Error::InvalidQuery, Error::InvalidQuery);
672        assert_ne!(Error::MultipleStatement, Error::MultipleStatement);
673        assert_ne!(Error::AppendError, Error::AppendError);
674        assert_ne!(
675            Error::ConversionError(DuckDBConversionError::TypeMismatch {
676                expected: crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER,
677                found: crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_VARCHAR
678            }),
679            Error::ConversionError(DuckDBConversionError::TypeMismatch {
680                expected: crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER,
681                found: crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_VARCHAR
682            })
683        );
684        assert_ne!(
685            Error::ConversionError(DuckDBConversionError::NullValue),
686            Error::ConversionError(DuckDBConversionError::NullValue)
687        );
688        assert_ne!(
689            Error::BackgroundTaskFailed("same".into()),
690            Error::BackgroundTaskFailed("same".into())
691        );
692        assert_ne!(Error::Pool("same".into()), Error::Pool("same".into()));
693        assert_ne!(Error::InvalidQuery, Error::MultipleStatement);
694    }
695
696    #[test]
697    fn utf8_and_nul_errors_convert_and_compare() {
698        let utf8 = invalid_utf8();
699        let converted: Error = utf8.into();
700        assert_eq!(converted, Error::Utf8Error(utf8));
701
702        let nul = CString::new("a\0b").unwrap_err();
703        let expected = CString::new("a\0b").unwrap_err();
704        let converted: Error = nul.into();
705        assert_eq!(converted, Error::NulError(expected));
706    }
707
708    #[test]
709    fn display_formats_error_context() {
710        assert_eq!(
711            Error::IntegralValueOutOfRange(2, 300).to_string(),
712            "Integer 300 out of range at index 2"
713        );
714        assert_eq!(
715            Error::IntegralValueOutOfRange(usize::MAX, 300).to_string(),
716            "Integer 300 out of range"
717        );
718        assert_eq!(
719            Error::ConversionError(DuckDBConversionError::ConversionError("bad value".into()))
720                .to_string(),
721            "Conversion error: bad value"
722        );
723        assert_eq!(
724            Error::ConversionError(DuckDBConversionError::NullValue).to_string(),
725            "Null value encountered"
726        );
727        assert_eq!(
728            Error::ConversionError(DuckDBConversionError::PrecisionLoss("rounded".into()))
729                .to_string(),
730            "Precision loss: rounded"
731        );
732        assert_eq!(Error::AppendError.to_string(), "Append error");
733        assert_eq!(Error::InvalidQuery.to_string(), "Query is not read-only");
734        assert_eq!(Error::MultipleStatement.to_string(), "Multiple statements provided");
735        assert_eq!(
736            Error::BackgroundTaskFailed("worker stopped".into()).to_string(),
737            "Background task failed: worker stopped"
738        );
739        assert_eq!(Error::Pool("timed out".into()).to_string(), "Connection pool error: timed out");
740        assert_eq!(
741            Error::DuckDBFailure(FFIError::new(DuckDBError), Some("query failed".into()))
742                .to_string(),
743            "query failed"
744        );
745        assert_eq!(
746            Error::DuckDBFailure(FFIError::new(DuckDBError), None).to_string(),
747            "DuckDB call failed with result code 1"
748        );
749    }
750
751    #[test]
752    fn engine_error_kinds_round_trip_and_preserve_unknown_values() {
753        // Every known kind survives a raw round trip.
754        for kind in EngineErrorKind::KNOWN {
755            assert_eq!(
756                EngineErrorKind::from_raw(kind.to_raw()),
757                *kind,
758                "kind {kind:?} did not round-trip"
759            );
760        }
761
762        // A value this build does not know is kept verbatim rather than flattened.
763        let future = EngineErrorKind::from_raw(9_999);
764        assert_eq!(future, EngineErrorKind::Unknown(9_999));
765        assert_eq!(future.to_raw(), 9_999);
766        assert_eq!(future.to_string(), "unknown engine error type 9999");
767
768        // `Unavailable` is an explicit absence, distinct from `Unknown`.
769        assert_ne!(EngineErrorKind::Unavailable, EngineErrorKind::Unknown(0));
770        assert_eq!(EngineErrorKind::Unavailable.to_string(), "unclassified engine error");
771    }
772
773    #[test]
774    fn engine_errors_display_and_compare_by_value() {
775        let with_message =
776            EngineError { kind: EngineErrorKind::Catalog, message: Some("no such table".into()) };
777        assert_eq!(with_message.to_string(), "Catalog: no such table");
778        assert_eq!(with_message, with_message.clone());
779
780        let without = EngineError { kind: EngineErrorKind::Binder, message: None };
781        assert_eq!(without.to_string(), "Binder");
782
783        assert_eq!(EngineError::unavailable(None).kind, EngineErrorKind::Unavailable);
784
785        // The wrapping `Error` delegates Display and exposes the engine error as source.
786        let wrapped = Error::Engine(with_message.clone());
787        assert_eq!(wrapped.to_string(), "Catalog: no such table");
788        assert!(wrapped.source().is_some());
789        assert_eq!(wrapped, Error::Engine(with_message));
790        assert_ne!(
791            Error::Engine(EngineError::unavailable(None)),
792            Error::Engine(EngineError { kind: EngineErrorKind::Io, message: None })
793        );
794    }
795
796    #[test]
797    fn sources_are_exposed_only_for_wrapped_errors() {
798        let utf8 = Error::Utf8Error(invalid_utf8());
799        let nul = Error::NulError(CString::new("a\0b").unwrap_err());
800        let ffi = Error::DuckDBFailure(FFIError::new(DuckDBError), None);
801        let sql = Error::ToSqlConversionFailure(Box::new(io::Error::other("sql")));
802        let unknown = Error::UNKNOWN(Box::new(io::Error::other("unknown")));
803
804        assert!(ffi.source().is_some());
805        assert!(utf8.source().is_some());
806        assert!(nul.source().is_some());
807        assert_eq!(sql.source().unwrap().to_string(), "sql");
808        assert_eq!(unknown.source().unwrap().to_string(), "unknown");
809        assert!(Error::AppendError.source().is_none());
810        assert!(Error::ConversionError(DuckDBConversionError::NullValue).source().is_none());
811        assert!(Error::BackgroundTaskFailed("stopped".into()).source().is_none());
812        assert!(Error::Pool("timeout".into()).source().is_none());
813    }
814}