Skip to main content

better_duck_core/
error.rs

1#![allow(dead_code)]
2// Direct copy from DuckDB
3
4use crate::ffi::{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/// Enum listing possible errors from duckdb.
26#[derive(Debug)]
27#[allow(clippy::enum_variant_names)]
28#[non_exhaustive]
29pub enum Error {
30    /// An error from an underlying DuckDB call.
31    DuckDBFailure(FFIError, Option<String>),
32
33    /// Error when the value of a particular column is requested, but it cannot
34    /// be converted to the requested Rust type.
35    // FromSqlConversionFailure(usize, Type, Box<dyn error::Error + Send + Sync + 'static>),
36
37    /// Error when DuckDB gives us an integral value outside the range of the
38    /// requested type (e.g., trying to get the value 1000 into a `u8`).
39    /// The associated `usize` is the column index,
40    /// and the associated `i64` is the value returned by DuckDB.
41    IntegralValueOutOfRange(usize, i128),
42
43    /// Error converting a string to UTF-8.
44    Utf8Error(str::Utf8Error),
45
46    /// Error converting a string to a C-compatible string because it contained
47    /// an embedded nul.
48    NulError(::std::ffi::NulError),
49
50    /// Error when using SQL named parameters and passing a parameter name not
51    /// present in the SQL.
52    InvalidParameterName(String),
53
54    /// Error converting a file path to a string.
55    InvalidPath(PathBuf),
56
57    /// Error returned when an [`execute`](crate::connection::Connection::execute) call
58    /// returns rows.
59    ExecuteReturnedResults,
60
61    /// Error when a query that was expected to return at least one row did not
62    /// return any.
63    QueryReturnedNoRows,
64
65    /// Error when the value of a particular column is requested, but the index
66    /// is out of range for the statement.
67    InvalidColumnIndex(usize),
68
69    /// Error when the value of a named column is requested, but no column
70    /// matches the name for the statement.
71    InvalidColumnName(String),
72
73    /// Error when the value of a particular column is requested, but the type
74    /// of the result in that column cannot be converted to the requested
75    /// Rust type.
76    // InvalidColumnType(usize, String, Type),
77
78    /// Error when a query that was expected to insert one row did not insert
79    /// any or inserted many.
80    StatementChangedRows(usize),
81
82    /// Error available for the implementors of the
83    /// [`AppendAble`](crate::types::appendable::AppendAble) trait.
84    ToSqlConversionFailure(Box<dyn error::Error + Send + Sync + 'static>),
85
86    /// Error when the SQL is not a `SELECT`, is not read-only.
87    InvalidQuery,
88
89    /// Error when the SQL contains multiple statements.
90    MultipleStatement,
91
92    /// Error when the number of bound parameters does not match the number of
93    /// parameters in the query. The first `usize` is how many parameters were
94    /// given, the 2nd is how many were expected.
95    InvalidParameterCount(usize, usize),
96
97    /// An error occurred while appending a value via the DuckDB appender API.
98    AppendError,
99
100    /// A value conversion error.
101    ConversionError(DuckDBConversionError),
102
103    /// An unexpected error with no more specific classification.
104    #[allow(non_camel_case_types)]
105    UNKNOWN(Box<dyn ::std::error::Error + Send + Sync + 'static>),
106
107    /// A background blocking task panicked or was cancelled before it produced a result.
108    BackgroundTaskFailed(String),
109
110    /// A connection pool operation failed (checkout timeout, manager error).
111    Pool(String),
112}
113
114/// A typedef of the result returned by many methods.
115pub type Result<T, E = Error> = result::Result<T, E>;
116
117impl PartialEq for Error {
118    fn eq(
119        &self,
120        other: &Error,
121    ) -> bool {
122        match (self, other) {
123            (Error::DuckDBFailure(e1, s1), Error::DuckDBFailure(e2, s2)) => e1 == e2 && s1 == s2,
124            (Error::IntegralValueOutOfRange(i1, n1), Error::IntegralValueOutOfRange(i2, n2)) => {
125                i1 == i2 && n1 == n2
126            },
127            (Error::Utf8Error(e1), Error::Utf8Error(e2)) => e1 == e2,
128            (Error::NulError(e1), Error::NulError(e2)) => e1 == e2,
129            (Error::InvalidParameterName(n1), Error::InvalidParameterName(n2)) => n1 == n2,
130            (Error::InvalidPath(p1), Error::InvalidPath(p2)) => p1 == p2,
131            (Error::ExecuteReturnedResults, Error::ExecuteReturnedResults) => true,
132            (Error::QueryReturnedNoRows, Error::QueryReturnedNoRows) => true,
133            (Error::InvalidColumnIndex(i1), Error::InvalidColumnIndex(i2)) => i1 == i2,
134            (Error::InvalidColumnName(n1), Error::InvalidColumnName(n2)) => n1 == n2,
135            // (Error::InvalidColumnType(i1, n1, t1), Error::InvalidColumnType(i2, n2, t2)) => {
136            //     i1 == i2 && t1 == t2 && n1 == n2
137            // }
138            (Error::StatementChangedRows(n1), Error::StatementChangedRows(n2)) => n1 == n2,
139            (Error::InvalidParameterCount(i1, n1), Error::InvalidParameterCount(i2, n2)) => {
140                i1 == i2 && n1 == n2
141            },
142            (..) => false,
143        }
144    }
145}
146
147impl From<str::Utf8Error> for Error {
148    #[cold]
149    fn from(err: str::Utf8Error) -> Error {
150        Error::Utf8Error(err)
151    }
152}
153
154impl From<::std::ffi::NulError> for Error {
155    #[cold]
156    fn from(err: ::std::ffi::NulError) -> Error {
157        Error::NulError(err)
158    }
159}
160
161const UNKNOWN_COLUMN: usize = usize::MAX;
162
163/// The conversion isn't precise, but it's convenient to have it
164/// to allow use of `get_raw(…).as_…()?` in callbacks that take `Error`.
165/// ```rust,ignore
166/// impl From<FromSqlError> for Error {
167///     #[cold]
168///     fn from(err: FromSqlError) -> Error {
169///         // The error type requires index and type fields, but they aren't known in this
170///         // context.
171///         match err {
172///             FromSqlError::OutOfRange(val) => Error::IntegralValueOutOfRange(UNKNOWN_COLUMN, val),
173///             #[cfg(feature = "uuid")]
174///             FromSqlError::InvalidUuidSize(_) => {
175///                 Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Blob, Box::new(err))
176///             }
177///             FromSqlError::Other(source) => {
178///                 Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, source)
179///             }
180///             _ => Error::FromSqlConversionFailure(UNKNOWN_COLUMN, Type::Null, Box::new(err)),
181///         }
182///     }
183/// }
184/// ```
185///
186impl fmt::Display for Error {
187    fn fmt(
188        &self,
189        f: &mut fmt::Formatter<'_>,
190    ) -> fmt::Result {
191        match self {
192            Error::DuckDBFailure(ref err, None) => err.fmt(f),
193            Error::DuckDBFailure(_, Some(ref s)) => write!(f, "{s}"),
194            // Error::FromSqlConversionFailure(i, ref t, ref err) => {
195            //     if i != UNKNOWN_COLUMN {
196            //         write!(f, "Conversion error from type {t} at index: {i}, {err}")
197            //     } else {
198            //         err.fmt(f)
199            //     }
200            // }
201            Error::IntegralValueOutOfRange(col, val) => {
202                if *col != UNKNOWN_COLUMN {
203                    write!(f, "Integer {val} out of range at index {col}")
204                } else {
205                    write!(f, "Integer {val} out of range")
206                }
207            },
208            Error::Utf8Error(ref err) => err.fmt(f),
209            Error::NulError(ref err) => err.fmt(f),
210            Error::InvalidParameterName(ref name) => write!(f, "Invalid parameter name: {name}"),
211            Error::InvalidPath(ref p) => write!(f, "Invalid path: {}", p.to_string_lossy()),
212            Error::ExecuteReturnedResults => {
213                write!(f, "Execute returned results - did you mean to call query?")
214            },
215            Error::QueryReturnedNoRows => write!(f, "Query returned no rows"),
216            Error::InvalidColumnIndex(i) => write!(f, "Invalid column index: {i}"),
217            Error::InvalidColumnName(ref name) => write!(f, "Invalid column name: {name}"),
218            // Error::InvalidColumnType(i, ref name, ref t) => {
219            //     write!(f, "Invalid column type {t} at index: {i}, name: {name}")
220            // }
221            // Error::ArrowTypeToDuckdbType(ref name, ref t) => {
222            //     write!(f, "Invalid column type {t} , name: {name}")
223            // }
224            Error::InvalidParameterCount(i1, n1) => {
225                write!(f, "Wrong number of parameters passed to query. Got {i1}, needed {n1}")
226            },
227            Error::StatementChangedRows(i) => write!(f, "Query changed {i} rows"),
228            Error::ToSqlConversionFailure(ref err) => err.fmt(f),
229            Error::InvalidQuery => write!(f, "Query is not read-only"),
230            Error::MultipleStatement => write!(f, "Multiple statements provided"),
231            Error::AppendError => write!(f, "Append error"),
232            Error::ConversionError(ref err) => match err {
233                DuckDBConversionError::TypeMismatch { expected, found } => {
234                    write!(f, "Type mismatch: expected {expected}, found {found}")
235                },
236                DuckDBConversionError::ConversionError(ref msg) => {
237                    write!(f, "Conversion error: {msg}")
238                },
239                DuckDBConversionError::NullValue => write!(f, "Null value encountered"),
240                DuckDBConversionError::PrecisionLoss(ref msg) => write!(f, "Precision loss: {msg}"),
241            },
242            Error::UNKNOWN(e) => write!(f, "Unknown error: {e}"),
243            Error::BackgroundTaskFailed(ref msg) => write!(f, "Background task failed: {msg}"),
244            Error::Pool(ref msg) => write!(f, "Connection pool error: {msg}"),
245        }
246    }
247}
248
249impl error::Error for Error {
250    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
251        match self {
252            Error::DuckDBFailure(ref err, _) => Some(err),
253            Error::Utf8Error(ref err) => Some(err),
254            Error::NulError(ref err) => Some(err),
255
256            Error::IntegralValueOutOfRange(..)
257            | Error::InvalidParameterName(_)
258            | Error::ExecuteReturnedResults
259            | Error::QueryReturnedNoRows
260            | Error::InvalidColumnIndex(_)
261            | Error::InvalidColumnName(_)
262            // | Error::InvalidColumnType(..)
263            | Error::InvalidPath(_)
264            | Error::InvalidParameterCount(..)
265            | Error::StatementChangedRows(_)
266            | Error::InvalidQuery
267            | Error::AppendError
268            // | Error::ArrowTypeToDuckdbType(..)
269            | Error::MultipleStatement
270            | Error::ConversionError(_) => None,
271            // Error::FromSqlConversionFailure(_, _, ref err)
272            Error::ToSqlConversionFailure(ref err) => Some(&**err),
273            Error::UNKNOWN(e) => Some(e.as_ref()),
274            Error::BackgroundTaskFailed(_) | Error::Pool(_) => None,
275        }
276    }
277}