Skip to content

Commit 009281a

Browse files
authored
Stop panicking when binding/appending unsupported Value variants (#749)
A safe Rust API should not panic in that case. Stopgap for #680 and #533 (that also clarifies current gaps).
2 parents 03e8b2b + 8f16ea2 commit 009281a

8 files changed

Lines changed: 457 additions & 49 deletions

File tree

crates/duckdb/src/appender/mod.rs

Lines changed: 186 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{ffi::c_void, fmt, os::raw::c_char};
44
use crate::{
55
Error,
66
error::result_from_duckdb_appender,
7-
types::{ToSql, ToSqlOutput},
7+
types::{ToSql, ToSqlOutput, value_ref_from_value},
88
};
99

1010
/// Appender for fast import data
@@ -80,7 +80,7 @@ impl Appender<'_> {
8080
///
8181
/// # Failure
8282
///
83-
/// Will return `Err` if append column count not the same with the table schema
83+
/// Returns `Err` if a row cannot be appended.
8484
#[inline]
8585
pub fn append_rows<P, I>(&mut self, rows: I) -> Result<()>
8686
where
@@ -108,38 +108,61 @@ impl Appender<'_> {
108108
///
109109
/// # Failure
110110
///
111-
/// Will return `Err` if append column count not the same with the table schema
111+
/// Returns `Err` if the row cannot be appended.
112112
#[inline]
113113
pub fn append_row<P: AppenderParams>(&mut self, params: P) -> Result<()> {
114-
let _ = unsafe { ffi::duckdb_appender_begin_row(self.app) };
115-
params.__bind_in(self)?;
116-
// NOTE: we only check end_row return value
117-
let rc = unsafe { ffi::duckdb_appender_end_row(self.app) };
118-
result_from_duckdb_appender(rc, &mut self.app)
114+
// AppenderParams normalizes arrays, tuples, slices, and iterators into
115+
// append_parameter_row, which validates up-front, then wraps binding
116+
// with begin_row/end_row.
117+
params.__bind_in(self)
119118
}
120119

121120
#[inline]
122-
pub(crate) fn bind_parameters<P>(&mut self, params: P) -> Result<()>
121+
pub(crate) fn append_parameter_row<P>(&mut self, params: P) -> Result<()>
123122
where
124123
P: IntoIterator,
125124
P::Item: ToSql,
126125
{
127-
for p in params.into_iter() {
128-
self.bind_parameter(&p)?;
126+
let params = params.into_iter().collect::<Vec<_>>();
127+
let values = params
128+
.iter()
129+
.map(ToSql::to_sql)
130+
.collect::<Result<Vec<ToSqlOutput<'_>>>>()?;
131+
132+
self.validate_parameter_values(&values)?;
133+
134+
let _ = unsafe { ffi::duckdb_appender_begin_row(self.app) };
135+
if let Err(err) = self.bind_parameter_values(&values) {
136+
// validate_parameter_values catches unsupported types up-front; this guards
137+
// against an unmapped variant slipping through bind_parameter.
138+
let rc = unsafe { ffi::duckdb_appender_end_row(self.app) };
139+
// Prefer cleanup failure here: result_from_duckdb_appender destroys
140+
// an appender that DuckDB reports as invalid after a partial row.
141+
result_from_duckdb_appender(rc, &mut self.app)?;
142+
return Err(err);
143+
}
144+
let rc = unsafe { ffi::duckdb_appender_end_row(self.app) };
145+
result_from_duckdb_appender(rc, &mut self.app)
146+
}
147+
148+
fn validate_parameter_values(&self, values: &[ToSqlOutput<'_>]) -> Result<()> {
149+
for value in values {
150+
let value = to_value_ref(value)?;
151+
validate_appender_value_ref(value)?;
129152
}
130153
Ok(())
131154
}
132155

133-
fn bind_parameter<P: ?Sized + ToSql>(&self, param: &P) -> Result<()> {
134-
let value = param.to_sql()?;
156+
fn bind_parameter_values(&self, values: &[ToSqlOutput<'_>]) -> Result<()> {
157+
for value in values {
158+
self.bind_parameter(value)?;
159+
}
160+
Ok(())
161+
}
135162

163+
fn bind_parameter(&self, value: &ToSqlOutput<'_>) -> Result<()> {
136164
let ptr = self.app;
137-
let value = match value {
138-
ToSqlOutput::Borrowed(v) => v,
139-
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
140-
};
141-
// NOTE: we ignore the return value here
142-
// because if anything failed, end_row will fail
165+
let value = to_value_ref(value)?;
143166
// TODO: append more
144167
let rc = match value {
145168
ValueRef::Null => unsafe { ffi::duckdb_append_null(ptr) },
@@ -193,7 +216,11 @@ impl Appender<'_> {
193216
ffi::duckdb_destroy_value(&mut value);
194217
res
195218
},
196-
_ => unreachable!("not supported"),
219+
_ => {
220+
return Err(Error::ToSqlConversionFailure(
221+
appending_unsupported_value(value.data_type()).into(),
222+
));
223+
}
197224
};
198225
if rc != 0 {
199226
return Err(Error::AppendError);
@@ -257,6 +284,48 @@ impl fmt::Debug for Appender<'_> {
257284
}
258285
}
259286

287+
fn appending_unsupported_value(value_type: impl fmt::Display) -> String {
288+
format!("appending {value_type} values is not yet supported")
289+
}
290+
291+
fn to_value_ref<'value, 'output>(value: &'value ToSqlOutput<'output>) -> Result<ValueRef<'value>>
292+
where
293+
'output: 'value,
294+
{
295+
match *value {
296+
ToSqlOutput::Borrowed(v) => Ok(v),
297+
ToSqlOutput::Owned(ref v) => value_ref_from_value(v, appending_unsupported_value),
298+
}
299+
}
300+
301+
fn validate_appender_value_ref(value: ValueRef<'_>) -> Result<()> {
302+
match value {
303+
ValueRef::Null
304+
| ValueRef::Boolean(_)
305+
| ValueRef::TinyInt(_)
306+
| ValueRef::SmallInt(_)
307+
| ValueRef::Int(_)
308+
| ValueRef::BigInt(_)
309+
| ValueRef::HugeInt(_)
310+
| ValueRef::UTinyInt(_)
311+
| ValueRef::USmallInt(_)
312+
| ValueRef::UInt(_)
313+
| ValueRef::UBigInt(_)
314+
| ValueRef::Float(_)
315+
| ValueRef::Double(_)
316+
| ValueRef::Text(_)
317+
| ValueRef::Timestamp(_, _)
318+
| ValueRef::Blob(_)
319+
| ValueRef::Date32(_)
320+
| ValueRef::Time64(_, _)
321+
| ValueRef::Interval { .. }
322+
| ValueRef::Decimal(_) => Ok(()),
323+
_ => Err(Error::ToSqlConversionFailure(
324+
appending_unsupported_value(value.data_type()).into(),
325+
)),
326+
}
327+
}
328+
260329
#[cfg(test)]
261330
mod test {
262331
use rust_decimal::Decimal;
@@ -278,6 +347,103 @@ mod test {
278347
Ok(())
279348
}
280349

350+
#[test]
351+
fn test_append_unsupported_container_type_returns_error() -> Result<()> {
352+
use arrow::{array::ListArray, datatypes::Int32Type};
353+
354+
use crate::{
355+
ToSql,
356+
types::{ListType, ToSqlOutput, Value, ValueRef},
357+
};
358+
359+
struct OwnedList;
360+
impl ToSql for OwnedList {
361+
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
362+
Ok(ToSqlOutput::Owned(Value::List(vec![Value::Int(1), Value::Int(2)])))
363+
}
364+
}
365+
366+
struct BorrowedList(ListArray);
367+
impl BorrowedList {
368+
fn new() -> Self {
369+
Self(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![Some(vec![
370+
Some(1),
371+
Some(2),
372+
])]))
373+
}
374+
}
375+
impl ToSql for BorrowedList {
376+
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
377+
Ok(ToSqlOutput::Borrowed(ValueRef::List(ListType::Regular(&self.0), 0)))
378+
}
379+
}
380+
381+
fn assert_unsupported_list_error(err: Error) {
382+
match err {
383+
Error::ToSqlConversionFailure(e) => {
384+
assert!(
385+
e.to_string().contains("appending List values is not yet supported"),
386+
"unexpected message: {e}"
387+
);
388+
}
389+
other => panic!("expected ToSqlConversionFailure, got {other:?}"),
390+
}
391+
}
392+
393+
let db = Connection::open_in_memory()?;
394+
db.execute_batch("CREATE TABLE foo(id INTEGER, name TEXT)")?;
395+
396+
let list = OwnedList;
397+
let mut app = db.appender("foo")?;
398+
app.append_row(params![10, "before"])?;
399+
app.append_row(params![11, "also before"])?;
400+
let err = app.append_row(params![1, list]).unwrap_err();
401+
assert_unsupported_list_error(err);
402+
403+
let borrowed_list = BorrowedList::new();
404+
let err = app.append_row(params![3, borrowed_list]).unwrap_err();
405+
assert_unsupported_list_error(err);
406+
app.append_row(params![2, "ok"])?;
407+
app.flush()?;
408+
409+
let rows = db
410+
.prepare("SELECT id, name FROM foo ORDER BY id")?
411+
.query_map([], |row| Ok((row.get::<_, i32>(0)?, row.get::<_, String>(1)?)))?
412+
.collect::<Result<Vec<_>>>()?;
413+
assert_eq!(
414+
rows,
415+
vec![
416+
(2, "ok".to_string()),
417+
(10, "before".to_string()),
418+
(11, "also before".to_string())
419+
]
420+
);
421+
let count: i32 = db.query_row("SELECT COUNT(*) FROM foo", [], |row| row.get(0))?;
422+
assert_eq!(count, 3);
423+
Ok(())
424+
}
425+
426+
#[test]
427+
fn test_append_bind_failure_prefers_cleanup_error() -> Result<()> {
428+
let db = Connection::open_in_memory()?;
429+
db.execute_batch("CREATE TABLE foo(id INTEGER, value UUID)")?;
430+
431+
let mut app = db.appender("foo")?;
432+
let err = app.append_row(params![1, 2]).unwrap_err();
433+
434+
match err {
435+
Error::DuckDBFailure(_, Some(msg)) => {
436+
assert!(
437+
msg.contains("Call to EndRow before all columns have been appended to"),
438+
"unexpected message: {msg}"
439+
);
440+
}
441+
other => panic!("expected DuckDBFailure from appender cleanup, got {other:?}"),
442+
}
443+
assert!(app.app.is_null());
444+
Ok(())
445+
}
446+
281447
#[test]
282448
fn test_append_rows() -> Result<()> {
283449
let db = Connection::open_in_memory()?;

crates/duckdb/src/appender_params.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ use sealed::Sealed;
118118
pub trait AppenderParams: Sealed {
119119
// XXX not public api, might not need to expose.
120120
//
121-
// Binds the parameters to the statement. It is unlikely calling this
122-
// explicitly will do what you want. Please use `Statement::query` or
121+
// Binds the parameters to the appender. It is unlikely calling this
122+
// explicitly will do what you want. Please use `Appender::append_row` or
123123
// similar directly.
124124
//
125125
// For now, just hide the function in the docs...
@@ -134,18 +134,17 @@ impl Sealed for [&dyn ToSql; 0] {}
134134
impl AppenderParams for [&dyn ToSql; 0] {
135135
#[inline]
136136
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
137-
// Note: Can't just return `Ok(())` — `Statement::bind_parameters`
138-
// checks that the right number of params were passed too.
139-
// TODO: we should have tests for `Error::InvalidParameterCount`...
140-
stmt.bind_parameters(&[] as &[&dyn ToSql])
137+
// Route through the normal append path so DuckDB can reject empty rows
138+
// for tables that require values.
139+
stmt.append_parameter_row(&[] as &[&dyn ToSql])
141140
}
142141
}
143142

144143
impl Sealed for &[&dyn ToSql] {}
145144
impl AppenderParams for &[&dyn ToSql] {
146145
#[inline]
147146
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
148-
stmt.bind_parameters(self)
147+
stmt.append_parameter_row(self)
149148
}
150149
}
151150

@@ -156,14 +155,14 @@ macro_rules! impl_for_array_ref {
156155
impl<T: ToSql + ?Sized> Sealed for &[&T; $N] {}
157156
impl<T: ToSql + ?Sized> AppenderParams for &[&T; $N] {
158157
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
159-
stmt.bind_parameters(self)
158+
stmt.append_parameter_row(self)
160159
}
161160
}
162161
impl<T: ToSql> Sealed for [T; $N] {}
163162
impl<T: ToSql> AppenderParams for [T; $N] {
164163
#[inline]
165164
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
166-
stmt.bind_parameters(&self)
165+
stmt.append_parameter_row(&self)
167166
}
168167
}
169168
)+};
@@ -189,7 +188,7 @@ macro_rules! impl_appender_params_for_tuple {
189188
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
190189
#[allow(non_snake_case)]
191190
let ($($T,)+) = &self;
192-
stmt.bind_parameters(&[$($T as &dyn ToSql),+])
191+
stmt.append_parameter_row([$($T as &dyn ToSql),+])
193192
}
194193
}
195194
};
@@ -340,7 +339,7 @@ where
340339
{
341340
#[inline]
342341
fn __bind_in(self, stmt: &mut Appender<'_>) -> Result<()> {
343-
stmt.bind_parameters(self.0)
342+
stmt.append_parameter_row(self.0)
344343
}
345344
}
346345

crates/duckdb/src/pragma.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
Connection, DatabaseName, Result, Row,
77
error::Error,
88
ffi,
9-
types::{ToSql, ToSqlOutput, ValueRef},
9+
types::{ToSql, ToSqlOutput, ValueRef, binding_unsupported_value, value_ref_from_value},
1010
};
1111

1212
pub struct Sql {
@@ -60,7 +60,7 @@ impl Sql {
6060
let value = value.to_sql()?;
6161
let value = match value {
6262
ToSqlOutput::Borrowed(v) => v,
63-
ToSqlOutput::Owned(ref v) => ValueRef::from(v),
63+
ToSqlOutput::Owned(ref v) => value_ref_from_value(v, binding_unsupported_value)?,
6464
};
6565
match value {
6666
ValueRef::BigInt(i) => {
@@ -76,7 +76,7 @@ impl Sql {
7676
_ => {
7777
return Err(Error::DuckDBFailure(
7878
ffi::Error::new(ffi::DuckDBError),
79-
Some(format!("Unsupported value \"{value:?}\"")),
79+
Some(format!("Unsupported value type {}", value.data_type())),
8080
));
8181
}
8282
};
@@ -305,6 +305,27 @@ mod test {
305305
Ok(())
306306
}
307307

308+
#[test]
309+
fn pragma_rejects_unsupported_container_type() -> Result<()> {
310+
use crate::{Error, types::Value};
311+
312+
let db = Connection::open_in_memory()?;
313+
let err = db
314+
.pragma(None, "table_info", &Value::List(vec![Value::Int(1)]), |_| Ok(()))
315+
.unwrap_err();
316+
317+
match err {
318+
Error::ToSqlConversionFailure(e) => {
319+
assert!(
320+
e.to_string().contains("binding List parameters is not yet supported"),
321+
"unexpected message: {e}"
322+
);
323+
}
324+
other => panic!("expected ToSqlConversionFailure, got {other:?}"),
325+
}
326+
Ok(())
327+
}
328+
308329
#[test]
309330
fn pragma_update() -> Result<()> {
310331
let db = Connection::open_in_memory()?;

0 commit comments

Comments
 (0)