@@ -4,7 +4,7 @@ use std::{ffi::c_void, fmt, os::raw::c_char};
44use 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) ]
261330mod 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 ( ) ?;
0 commit comments