Skip to main content

better_duck_core/raw/
appender.rs

1use std::ffi::{c_char, CString};
2use std::ptr;
3use std::sync::Arc;
4
5use crate::error::{EngineError, Error, Result};
6use crate::ffi::{
7    duckdb_append_data_chunk, duckdb_append_default, duckdb_append_default_to_chunk,
8    duckdb_appender, duckdb_appender_add_column, duckdb_appender_begin_row,
9    duckdb_appender_clear_columns, duckdb_appender_close, duckdb_appender_column_count,
10    duckdb_appender_column_type, duckdb_appender_create, duckdb_appender_create_ext,
11    duckdb_appender_create_query, duckdb_appender_destroy, duckdb_appender_end_row,
12    duckdb_appender_error_data, duckdb_appender_flush, duckdb_data_chunk_get_column_count,
13    duckdb_data_chunk_get_size, duckdb_logical_type, idx_t, DuckDBSuccess,
14};
15use crate::helpers::duck_result::result_from_duckdb_appender;
16use crate::raw::connection::ConnectionInner;
17use crate::raw::data_chunk::DataChunk;
18use crate::raw::error_data::ErrorData;
19use crate::types::appendable::AppendAble;
20use crate::types::LogicalType;
21
22/// Lifecycle state of an [`Appender`].
23///
24/// DuckDB invalidates an appender the moment a flush, `end_row`, or close fails:
25/// its docs say "all data is invalidated ... it is not possible to append more
26/// values", and the only legal follow-up is `duckdb_appender_error_data` then
27/// `duckdb_appender_destroy`. Re-flushing an invalidated appender — which the
28/// previous `Drop` did — can *deadlock* DuckDB when the target table carries an
29/// ART index (e.g. `PRIMARY KEY`). This state machine makes that unrepresentable:
30/// once `Poisoned`, no further flush/close is ever issued.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum AppenderState {
33    /// Accepting rows; flush/close are legal.
34    Ready,
35    /// A DuckDB operation failed and invalidated the appender. No further
36    /// append/flush/close may run; the handle may only be destroyed.
37    Poisoned,
38    /// Explicitly finished via [`Appender::finish`]; the handle has been closed
39    /// and only awaits destruction. `Drop` is a bare destroy.
40    Closed,
41}
42
43/// A DuckDB appender for bulk-inserting rows into a table without going through
44/// the SQL parser.
45///
46/// # Lifecycle
47///
48/// Call [`append`](Appender::append) for each row, then either [`finish`](Appender::finish)
49/// (consuming, reports flush errors) or let the appender drop (best-effort flush,
50/// errors logged). A failed [`append`](Appender::append)/[`save`](Appender::save)
51/// *poisons* the appender: DuckDB has invalidated all buffered data, so subsequent
52/// calls fail fast without touching the C handle, and drop skips the flush entirely
53/// (re-flushing an invalidated appender can deadlock DuckDB on indexed tables).
54pub struct Appender {
55    /// Keeps the connection this appender was created on open for at least as long
56    /// as the appender, and ties appended rows to that connection's transaction.
57    _connection: Arc<ConnectionInner>,
58    inn: duckdb_appender,
59    state: AppenderState,
60    /// Whether any row has been appended yet. The active-column-list builders
61    /// ([`add_column`](Appender::add_column)/[`clear_columns`](Appender::clear_columns))
62    /// must run *before* the first row, so this gates them.
63    rows_appended: bool,
64}
65
66impl Appender {
67    /// Creates a new `Appender` for the given table and schema.
68    ///
69    /// Takes the shared connection owner rather than a `RawConnection` so that rows
70    /// are appended on the *caller's* connection, and therefore inside any
71    /// transaction open on it.
72    ///
73    /// Crate-internal because [`ConnectionInner`] is: external callers construct
74    /// appenders through [`Connection::appender`](crate::connection::Connection::appender).
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the table does not exist or the DuckDB appender cannot
79    /// be created.
80    pub(crate) fn new(
81        connection: Arc<ConnectionInner>,
82        table: &str,
83        schema: &str,
84    ) -> Result<Appender> {
85        let mut appender: duckdb_appender = ptr::null_mut();
86        let c_table = CString::new(table)?;
87        let c_schema = CString::new(schema)?;
88        // SAFETY: `connection`'s handle is a valid open duckdb_connection, kept alive by
89        // the `Arc` this appender retains. `c_schema` and `c_table` are valid
90        // null-terminated C strings. `appender` is a valid output pointer.
91        let res = unsafe {
92            duckdb_appender_create(
93                connection.handle(),
94                c_schema.as_ptr() as *const c_char,
95                c_table.as_ptr() as *const c_char,
96                &mut appender,
97            )
98        };
99        result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
100            _connection: connection,
101            inn: appender,
102            state: AppenderState::Ready,
103            rows_appended: false,
104        })
105    }
106
107    /// Creates an `Appender` for `[catalog.]schema.table`, addressing a table in a
108    /// specific attached catalog. A `None` catalog uses DuckDB's default.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if a name contains an interior NUL, or if DuckDB cannot
113    /// create the appender (e.g. the table or catalog does not exist).
114    pub(crate) fn new_ext(
115        connection: Arc<ConnectionInner>,
116        catalog: Option<&str>,
117        schema: &str,
118        table: &str,
119    ) -> Result<Appender> {
120        let c_catalog = catalog.map(CString::new).transpose()?;
121        let c_schema = CString::new(schema)?;
122        let c_table = CString::new(table)?;
123        let catalog_ptr = c_catalog.as_ref().map_or(ptr::null(), |c| c.as_ptr());
124        let mut appender: duckdb_appender = ptr::null_mut();
125        // SAFETY: `connection`'s handle is valid and kept alive by the retained `Arc`.
126        // The (optional) catalog/schema/table pointers are valid null-terminated
127        // strings (or null for the default catalog) that outlive the call; `appender`
128        // is a valid out-pointer.
129        let res = unsafe {
130            duckdb_appender_create_ext(
131                connection.handle(),
132                catalog_ptr,
133                c_schema.as_ptr() as *const c_char,
134                c_table.as_ptr() as *const c_char,
135                &mut appender,
136            )
137        };
138        result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
139            _connection: connection,
140            inn: appender,
141            state: AppenderState::Ready,
142            rows_appended: false,
143        })
144    }
145
146    /// Creates a *query* appender: rows appended to it feed `query` (an `INSERT`,
147    /// `UPDATE`, `DELETE`, or `MERGE INTO`), which refers to the appended data by
148    /// `table_name` (default `"appended_data"`). `types` gives the appended columns'
149    /// types; `column_names` optionally names them (default `col1`, `col2`, …).
150    ///
151    /// # Errors
152    ///
153    /// Returns an error on an interior NUL in any string, or if DuckDB rejects the
154    /// query/columns.
155    pub(crate) fn new_query(
156        connection: Arc<ConnectionInner>,
157        query: &str,
158        types: &[LogicalType],
159        table_name: Option<&str>,
160        column_names: Option<&[&str]>,
161    ) -> Result<Appender> {
162        let c_query = CString::new(query)?;
163        let c_table = table_name.map(CString::new).transpose()?;
164        let table_ptr = c_table.as_ref().map_or(ptr::null(), |c| c.as_ptr());
165
166        // DuckDB copies the type handles; collect the raw pointers into a temporary.
167        let mut raw_types: Vec<duckdb_logical_type> =
168            types.iter().map(LogicalType::as_raw).collect();
169
170        // Optional column names: build owned CStrings, then a pointer array over them.
171        let c_names: Option<Vec<CString>> = column_names
172            .map(|names| names.iter().map(|n| CString::new(*n)).collect::<Result<_, _>>())
173            .transpose()?;
174        let mut name_ptrs: Option<Vec<*const c_char>> =
175            c_names.as_ref().map(|cs| cs.iter().map(|c| c.as_ptr()).collect());
176        let names_ptr = name_ptrs.as_mut().map_or(ptr::null_mut(), |v| v.as_mut_ptr());
177
178        let mut appender: duckdb_appender = ptr::null_mut();
179        // SAFETY: `connection`'s handle is valid (kept alive by the retained `Arc`).
180        // `c_query`/`table_ptr` are valid (or null) null-terminated strings; `raw_types`
181        // holds `types.len()` valid handles DuckDB copies; `names_ptr` is null or points
182        // at `types.len()`-ish valid name pointers that outlive the call; `appender` is a
183        // valid out-pointer.
184        let res = unsafe {
185            duckdb_appender_create_query(
186                connection.handle(),
187                c_query.as_ptr(),
188                raw_types.len() as idx_t,
189                raw_types.as_mut_ptr(),
190                table_ptr,
191                names_ptr,
192                &mut appender,
193            )
194        };
195        result_from_duckdb_appender(res, &mut appender).map(|_| Appender {
196            _connection: connection,
197            inn: appender,
198            state: AppenderState::Ready,
199            rows_appended: false,
200        })
201    }
202
203    /// The number of columns in the appender's active column list (or, with no
204    /// projection set, the receiving table's column count).
205    #[must_use]
206    pub fn column_count(&self) -> u64 {
207        // SAFETY: `self.inn` is a valid, non-null duckdb_appender.
208        unsafe { duckdb_appender_column_count(self.inn) as u64 }
209    }
210
211    /// The logical type of the appender column at `col_idx`, or `None` if DuckDB
212    /// returns no type (e.g. index out of range).
213    #[must_use]
214    pub fn column_type(
215        &self,
216        col_idx: u64,
217    ) -> Option<LogicalType> {
218        // SAFETY: `self.inn` is valid; `duckdb_appender_column_type` returns an owned
219        // logical type (destroy once) that the RAII `LogicalType` wraps; null → None.
220        LogicalType::from_raw(unsafe { duckdb_appender_column_type(self.inn, col_idx as idx_t) })
221            .ok()
222    }
223
224    /// Adds `name` to the appender's *active column list*, so subsequent rows supply
225    /// only the projected columns (the rest take their `DEFAULT`).
226    ///
227    /// Must be called before the first row is appended.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if a row has already been appended, on an interior NUL in
232    /// `name`, or if DuckDB rejects the column.
233    pub fn add_column(
234        &mut self,
235        name: &str,
236    ) -> Result<()> {
237        self.ensure_configurable()?;
238        let c_name = CString::new(name)?;
239        // SAFETY: `self.inn` is valid; `c_name` is a valid null-terminated string that
240        // outlives the call and is not retained.
241        let rc = unsafe { duckdb_appender_add_column(self.inn, c_name.as_ptr()) };
242        self.check(rc)
243    }
244
245    /// Clears any active column-list projection, so subsequent rows supply every
246    /// column of the receiving table again.
247    ///
248    /// Must be called before the first row is appended.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if a row has already been appended, or if DuckDB reports a
253    /// failure.
254    pub fn clear_columns(&mut self) -> Result<()> {
255        self.ensure_configurable()?;
256        // SAFETY: `self.inn` is a valid, non-null duckdb_appender.
257        let rc = unsafe { duckdb_appender_clear_columns(self.inn) };
258        self.check(rc)
259    }
260
261    /// Appends one row in which every column takes its `DEFAULT` value.
262    ///
263    /// Fills the active column list with `duckdb_append_default` (a column whose
264    /// table has no `DEFAULT` becomes `NULL`). Opens and closes the row like
265    /// [`append`](Appender::append), so a failure part-way never leaves a half-open
266    /// row.
267    ///
268    /// # Errors
269    ///
270    /// Returns an error if the appender is poisoned/closed or DuckDB rejects a
271    /// default.
272    pub fn append_default_row(&mut self) -> Result<()> {
273        self.ensure_ready()?;
274        let columns = self.column_count();
275
276        // SAFETY: `self.inn` is a valid duckdb_appender created by a constructor.
277        let begin = unsafe { duckdb_appender_begin_row(self.inn) };
278        self.check(begin)?;
279
280        // The guard ends the row on every exit path (early `?`/panic).
281        let guard = RowGuard { appender: self, ended: false };
282        for _ in 0..columns {
283            // SAFETY: `guard.appender.inn` is valid with a row open; each call appends
284            // the current column's default and advances the column cursor.
285            let rc = unsafe { duckdb_append_default(guard.appender.inn) };
286            guard.appender.check(rc)?;
287        }
288        guard.end()?;
289        self.rows_appended = true;
290        Ok(())
291    }
292
293    /// Appends every row of `chunk` to the appender in one call
294    /// (`duckdb_append_data_chunk`).
295    ///
296    /// `chunk` is only read — the caller keeps ownership and it is destroyed on drop
297    /// as usual. The chunk's column count must match the appender's active column
298    /// list; this is validated Rust-side before the FFI call.
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the appender is poisoned/closed, the chunk's column count
303    /// disagrees with the appender's, or DuckDB rejects the chunk (e.g. type
304    /// mismatch).
305    pub fn append_chunk(
306        &mut self,
307        chunk: &DataChunk,
308    ) -> Result<()> {
309        self.ensure_ready()?;
310        let appender_cols = self.column_count();
311        // SAFETY: `chunk.0` is a valid duckdb_data_chunk owned by `chunk`.
312        let chunk_cols = unsafe { duckdb_data_chunk_get_column_count(chunk.0) } as u64;
313        if chunk_cols != appender_cols {
314            return Err(Error::Engine(EngineError::unavailable(Some(format!(
315                "data chunk has {chunk_cols} columns but the appender expects {appender_cols}"
316            )))));
317        }
318        // SAFETY: `self.inn` is a valid appender; `chunk.0` is a valid data chunk whose
319        // column count matches. DuckDB reads the chunk and does not take ownership.
320        let rc = unsafe { duckdb_append_data_chunk(self.inn, chunk.0) };
321        self.check(rc)?;
322        // SAFETY: `chunk.0` is valid; a non-empty chunk means rows now exist.
323        if unsafe { duckdb_data_chunk_get_size(chunk.0) } > 0 {
324            self.rows_appended = true;
325        }
326        Ok(())
327    }
328
329    /// Writes the `DEFAULT` value of appender column `col` into `chunk` at
330    /// `(col, row)` (`duckdb_append_default_to_chunk`); a column with no `DEFAULT`
331    /// becomes `NULL`.
332    ///
333    /// `col` must be within the chunk's column count (validated Rust-side).
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if the appender is poisoned/closed, `col` is out of range, or
338    /// DuckDB reports a failure.
339    pub fn append_default_to_chunk(
340        &mut self,
341        chunk: &mut DataChunk,
342        col: u64,
343        row: u64,
344    ) -> Result<()> {
345        self.ensure_ready()?;
346        // SAFETY: `chunk.0` is a valid duckdb_data_chunk owned by `chunk`.
347        let chunk_cols = unsafe { duckdb_data_chunk_get_column_count(chunk.0) } as u64;
348        if col >= chunk_cols {
349            return Err(Error::Engine(EngineError::unavailable(Some(format!(
350                "column {col} out of range for a {chunk_cols}-column chunk"
351            )))));
352        }
353        // SAFETY: `self.inn` is a valid appender; `chunk.0` is a valid chunk; `col` is
354        // in range. DuckDB writes the default into the chunk cell.
355        let rc = unsafe {
356            duckdb_append_default_to_chunk(self.inn, chunk.0, col as idx_t, row as idx_t)
357        };
358        self.check(rc)
359    }
360
361    /// Returns `Ok(())` only if the active column list may still be reconfigured —
362    /// i.e. the appender is Ready and no row has been appended yet.
363    fn ensure_configurable(&self) -> Result<()> {
364        self.ensure_ready()?;
365        if self.rows_appended {
366            return Err(Error::Engine(EngineError::unavailable(Some(
367                "appender columns must be configured before the first row is appended".to_owned(),
368            ))));
369        }
370        Ok(())
371    }
372
373    /// Appends a row to the table.
374    ///
375    /// Opens a row (`duckdb_appender_begin_row`), appends the value, then closes it
376    /// (`duckdb_appender_end_row`). A `RowGuard` closes the row even if appending
377    /// the value returns early or panics, so a half-written row can never bleed into
378    /// the next call.
379    ///
380    /// # Errors
381    ///
382    /// Returns an error if the appender is poisoned or closed, or if the row cannot
383    /// be appended. Engine-side failures carry DuckDB's typed classification via
384    /// [`Error::Engine`]; any failure poisons the appender.
385    #[must_use = "append result should be checked"]
386    #[allow(dead_code)]
387    pub fn append<T: AppendAble>(
388        &mut self,
389        row: &mut T,
390    ) -> Result<()> {
391        self.ensure_ready()?;
392
393        // SAFETY: `self.inn` is a valid duckdb_appender created in `new`.
394        let begin = unsafe { duckdb_appender_begin_row(self.inn) };
395        self.check(begin)?;
396
397        // The guard ends the row on every exit path — including an early `?` return
398        // or a panic inside `appender_append` — so DuckDB is never left mid-row.
399        let guard = RowGuard { appender: self, ended: false };
400        guard.appender.row_result(row.appender_append(guard.appender.inn))?;
401        guard.end()?;
402        // A row now exists, so the active column list may no longer be reconfigured.
403        self.rows_appended = true;
404        Ok(())
405    }
406
407    /// Flushes all buffered rows to the database.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error if the appender is poisoned or closed, or if the flush
412    /// fails (which additionally poisons the appender).
413    #[must_use = "save result should be checked"]
414    #[allow(dead_code)]
415    pub fn save(&mut self) -> Result<()> {
416        self.ensure_ready()?;
417        self.flush()
418    }
419
420    /// Flushes and closes the appender, consuming it and reporting any error.
421    ///
422    /// Unlike dropping, this surfaces a flush/close failure to the caller. On
423    /// success the handle is closed and its `Drop` becomes a bare destroy.
424    ///
425    /// # Errors
426    ///
427    /// Returns an error if the appender is already poisoned, or if the final
428    /// flush/close fails (which poisons it).
429    #[allow(dead_code)]
430    pub fn finish(mut self) -> Result<()> {
431        self.ensure_ready()?;
432        // SAFETY: `self.inn` is a valid, non-null duckdb_appender in the Ready state.
433        let rc = unsafe { duckdb_appender_close(self.inn) };
434        self.check(rc)?;
435        self.state = AppenderState::Closed;
436        Ok(())
437    }
438
439    /// Flushes the appender's internal buffer, poisoning it on failure.
440    fn flush(&mut self) -> Result<()> {
441        // SAFETY: `self.inn` is a valid, non-null duckdb_appender (upheld by the
442        // constructor and the Drop null-guard) and is Ready (checked by callers).
443        let res = unsafe { duckdb_appender_flush(self.inn) };
444        self.check(res)
445    }
446
447    /// Returns `Ok(())` only if the appender can still accept operations.
448    fn ensure_ready(&self) -> Result<()> {
449        match self.state {
450            AppenderState::Ready => Ok(()),
451            AppenderState::Poisoned => Err(Error::Engine(EngineError::unavailable(Some(
452                "appender was invalidated by an earlier failure".to_owned(),
453            )))),
454            AppenderState::Closed => Err(Error::Engine(EngineError::unavailable(Some(
455                "appender has been closed".to_owned(),
456            )))),
457        }
458    }
459
460    /// Maps a DuckDB appender status into a typed `Result`, poisoning the appender
461    /// on failure.
462    ///
463    /// On failure this reads the appender's [`ErrorData`], which carries DuckDB's
464    /// own error classification, instead of the deprecated bare-string
465    /// `duckdb_appender_error`. The handle stays valid (for destruction) but the
466    /// appender is marked [`Poisoned`](AppenderState::Poisoned) so no further
467    /// flush/close is ever issued against invalidated data.
468    fn check(
469        &mut self,
470        code: crate::ffi::duckdb_state,
471    ) -> Result<()> {
472        if code == DuckDBSuccess {
473            return Ok(());
474        }
475        let engine = self.error_data().unwrap_or_else(|| {
476            EngineError::unavailable(Some(
477                "appender reported failure without error data".to_owned(),
478            ))
479        });
480        self.state = AppenderState::Poisoned;
481        Err(Error::Engine(engine))
482    }
483
484    /// Poisons the appender and returns the original Rust-side error unchanged.
485    ///
486    /// Used when a value fails to append *before* reaching DuckDB (e.g. a
487    /// conversion error): the row is abandoned, so the appender must not be reused.
488    fn row_result(
489        &mut self,
490        result: Result<()>,
491    ) -> Result<()> {
492        if result.is_err() {
493            self.state = AppenderState::Poisoned;
494        }
495        result
496    }
497
498    /// Reads DuckDB's typed error for this appender, if any is set.
499    ///
500    /// Returns `None` when the appender is not in an error state. The returned
501    /// error is fully owned: the underlying `duckdb_error_data` handle is
502    /// destroyed before this returns.
503    fn error_data(&mut self) -> Option<EngineError> {
504        // SAFETY: `self.inn` is a valid, non-null duckdb_appender. DuckDB returns an
505        // owned `duckdb_error_data` handle (or null); `ErrorData` takes ownership and
506        // destroys it, and `to_engine_error` copies every field out first.
507        let data = unsafe { ErrorData::from_raw(duckdb_appender_error_data(self.inn)) }?;
508        data.has_error().then(|| data.to_engine_error())
509    }
510}
511
512/// Ends the currently open appender row when it goes out of scope.
513///
514/// Guarantees `duckdb_appender_end_row` runs even if the value append returns
515/// early or unwinds, so DuckDB is never left with a half-open row. If the guard
516/// is dropped without an explicit [`end`](RowGuard::end) (i.e. via `?` or a
517/// panic), it ends the row on a best-effort basis and poisons the appender,
518/// since the row's contents are indeterminate.
519struct RowGuard<'a> {
520    appender: &'a mut Appender,
521    ended: bool,
522}
523
524impl RowGuard<'_> {
525    /// Ends the row explicitly, propagating any DuckDB error (which poisons).
526    fn end(mut self) -> Result<()> {
527        self.ended = true;
528        // SAFETY: `self.appender.inn` is a valid duckdb_appender with a row open.
529        let rc = unsafe { duckdb_appender_end_row(self.appender.inn) };
530        self.appender.check(rc)
531    }
532}
533
534impl Drop for RowGuard<'_> {
535    fn drop(&mut self) {
536        if self.ended {
537            return;
538        }
539        // Early return or panic mid-row: close the row so DuckDB is not left
540        // mid-row, and poison the appender because the row is incomplete.
541        // SAFETY: `self.appender.inn` is a valid duckdb_appender with a row open.
542        unsafe { duckdb_appender_end_row(self.appender.inn) };
543        self.appender.state = AppenderState::Poisoned;
544    }
545}
546
547impl Drop for Appender {
548    fn drop(&mut self) {
549        if self.inn.is_null() {
550            return;
551        }
552
553        // A poisoned appender has been invalidated by DuckDB: re-flushing it is
554        // illegal and can deadlock on indexed tables, so go straight to destroy.
555        // A Closed appender was already flushed+closed by `finish`. Only a Ready
556        // appender still owns unflushed rows worth a best-effort flush.
557        if self.state == AppenderState::Ready {
558            // [err-result-over-panic] — log on flush failure; never panic in Drop.
559            if let Err(e) = self.flush() {
560                eprintln!("[better-duck] appender flush on drop failed: {e}");
561            }
562        }
563
564        // SAFETY: `self.inn` is a valid, non-null duckdb_appender (null guard above).
565        // `duckdb_appender_destroy` de-allocates the handle regardless of state and
566        // is the documented cleanup for an invalidated appender. After destroy the
567        // handle is invalid and will not be used again.
568        unsafe {
569            duckdb_appender_destroy(&mut self.inn);
570        }
571    }
572}
573
574#[cfg(test)]
575mod appender_tests {
576    use crate::{
577        error::{DuckDBConversionError, Error},
578        ffi::{duckdb_append_int32, duckdb_append_varchar, duckdb_bind_int32, duckdb_bind_varchar},
579        raw::connection::RawConnection,
580        types::value::DuckValue,
581    };
582
583    use super::*;
584    use crate::{config::Config, helpers::path::path_to_cstring};
585
586    const ROW_FAILURE: &str = "intentional row failure";
587
588    #[derive(Debug)]
589    struct Row(i32, &'static str);
590
591    struct FailingRow;
592
593    impl AppendAble for FailingRow {
594        fn appender_append(
595            &mut self,
596            _appender: duckdb_appender,
597        ) -> Result<()> {
598            Err(Error::ConversionError(DuckDBConversionError::ConversionError(
599                ROW_FAILURE.to_owned(),
600            )))
601        }
602
603        fn stmt_append(
604            &mut self,
605            _idx: u64,
606            _stmt: crate::ffi::duckdb_prepared_statement,
607        ) -> Result<()> {
608            unreachable!("FailingRow is only used with Appender")
609        }
610    }
611
612    impl AppendAble for Row {
613        fn appender_append(
614            &mut self,
615            appender: duckdb_appender,
616        ) -> crate::error::Result<()> {
617            // SAFETY: `appender` is a valid duckdb_appender from `Appender::new`;
618            // we are inside a begin_row/end_row pair. The int32 and varchar values are
619            // valid for their respective columns.
620            unsafe {
621                duckdb_append_int32(appender, self.0);
622                let st = CString::new(self.1)
623                    .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
624                    .unwrap();
625                duckdb_append_varchar(appender, st.as_ptr());
626            }
627            Ok(())
628        }
629        fn stmt_append(
630            &mut self,
631            idx: u64,
632            stmt: crate::ffi::duckdb_prepared_statement,
633        ) -> Result<()> {
634            // SAFETY: `stmt` is a valid prepared statement; `idx` is a 1-based parameter
635            // index within the statement's parameter count.
636            unsafe {
637                duckdb_bind_int32(stmt, idx, self.0);
638                let st = CString::new(self.1)
639                    .map_err(|e| DuckDBConversionError::ConversionError(e.to_string()))
640                    .unwrap();
641                duckdb_bind_varchar(stmt, idx + 1, st.as_ptr());
642            }
643            Ok(())
644        }
645    }
646
647    fn get_test_connection() -> RawConnection {
648        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
649        let config = Config::default().with("duckdb_api", "rust").unwrap();
650        RawConnection::open_with_flags(&c_path, config).unwrap()
651    }
652
653    fn single_int(
654        con: &RawConnection,
655        sql: &str,
656        column: &str,
657    ) -> i32 {
658        let mut result = con.prepare(sql).unwrap().execute().unwrap();
659        let row = result.next().expect("expected one row").unwrap();
660        match row.get(column).unwrap() {
661            DuckValue::Int(value) => *value,
662            other => panic!("Expected Int for '{column}', got {other:?}"),
663        }
664    }
665
666    fn assert_row_exists(
667        con: &RawConnection,
668        table: &str,
669        id: i32,
670    ) {
671        let sql = format!("SELECT id FROM {table} WHERE id = {id}");
672        assert_eq!(single_int(con, &sql, "id"), id);
673    }
674
675    #[test]
676    fn test_appender_create_and_drop() {
677        let mut con = get_test_connection();
678
679        let create_sql = "CREATE TABLE test_appender (id INTEGER, name VARCHAR)";
680        let _ = con.query(create_sql).unwrap();
681
682        let appender = con.appender("test_appender", "main");
683        assert!(appender.is_ok());
684    }
685
686    #[test]
687    fn test_appender_append_and_flush() {
688        let mut con = get_test_connection();
689
690        let _ = con.query("CREATE TABLE test_append (id INTEGER, name VARCHAR)").unwrap();
691
692        let mut appender = con.appender("test_append", "main").unwrap();
693        let mut row = Row(1, "Alice");
694        let mut row2 = Row(2, "Sara");
695        let mut row3 = Row(3, "Charlie");
696
697        appender.append(&mut row).unwrap();
698        appender.append(&mut row2).unwrap();
699        appender.append(&mut row3).unwrap();
700        appender.save().unwrap();
701
702        let mut stmt = con.prepare("SELECT id,name FROM test_append WHERE id=123").unwrap();
703        let mut rows = stmt.execute().unwrap();
704        assert!(rows.next().is_none(), "Row with id=123 should not exist");
705
706        let mut stmt = con.prepare("SELECT id,name FROM test_append").unwrap();
707        let rows = stmt.execute().unwrap();
708        for row in rows {
709            assert!(row.is_ok());
710            let row = row.unwrap();
711            let id = match row.get("id").unwrap() {
712                DuckValue::Int(id) => id,
713                other => panic!("Expected Int for 'id', got {:?}", other),
714            };
715            assert!([1, 2, 3].contains(id), "Row with id={} should exist", id);
716            let name = match row.get("name").unwrap() {
717                DuckValue::Text(name) => name.as_str(),
718                other => panic!("Expected Str for 'name', got {:?}", other),
719            };
720            match id {
721                1 => assert_eq!(name, "Alice"),
722                2 => assert_eq!(name, "Sara"),
723                3 => assert_eq!(name, "Charlie"),
724                _ => panic!("Unexpected row id: {}", id),
725            }
726        }
727    }
728
729    #[test]
730    fn test_appender_rejects_interior_nul_table_name() {
731        let error = get_test_connection()
732            .appender("test\0table", "main")
733            .err()
734            .expect("interior NUL table name should fail");
735        assert!(matches!(error, Error::NulError(_)));
736    }
737
738    #[test]
739    fn test_appender_rejects_interior_nul_schema_name() {
740        let error = get_test_connection()
741            .appender("test_table", "ma\0in")
742            .err()
743            .expect("interior NUL schema name should fail");
744        assert!(matches!(error, Error::NulError(_)));
745    }
746
747    #[test]
748    fn test_appender_error_on_nonexistent_schema() {
749        let mut con = get_test_connection();
750        con.query("CREATE TABLE test_table (id INTEGER)").unwrap();
751
752        let appender = con.appender("test_table", "nonexistent_schema");
753        assert!(appender.is_err());
754    }
755
756    #[test]
757    fn test_appender_error_on_nonexistent_table() {
758        let mut con = get_test_connection();
759
760        let appender = con.appender("nonexistent_table", "main");
761        assert!(appender.is_err());
762    }
763
764    #[test]
765    fn test_save_flushes_rows_for_another_connection() {
766        let mut con = get_test_connection();
767        con.query("CREATE TABLE saved_rows (id INTEGER, name VARCHAR)").unwrap();
768        let reader = con.try_clone().unwrap();
769        let mut appender = con.appender("saved_rows", "main").unwrap();
770
771        appender.append(&mut Row(11, "saved")).unwrap();
772        appender.save().unwrap();
773
774        assert_row_exists(&reader, "saved_rows", 11);
775    }
776
777    #[test]
778    fn test_drop_flushes_rows_for_another_connection() {
779        let mut con = get_test_connection();
780        con.query("CREATE TABLE dropped_rows (id INTEGER, name VARCHAR)").unwrap();
781        let reader = con.try_clone().unwrap();
782
783        {
784            let mut appender = con.appender("dropped_rows", "main").unwrap();
785            appender.append(&mut Row(12, "dropped")).unwrap();
786        }
787
788        assert_row_exists(&reader, "dropped_rows", 12);
789    }
790
791    #[test]
792    fn test_append_propagates_row_error_and_connection_remains_usable() {
793        let mut con = get_test_connection();
794        con.query("CREATE TABLE failed_rows (id INTEGER, name VARCHAR)").unwrap();
795        let mut appender = con.appender("failed_rows", "main").unwrap();
796
797        let error = appender.append(&mut FailingRow).unwrap_err();
798        assert!(matches!(
799            error,
800            Error::ConversionError(DuckDBConversionError::ConversionError(ref message))
801                if message == ROW_FAILURE
802        ));
803        drop(appender);
804
805        con.query("INSERT INTO failed_rows VALUES (13, 'usable')").unwrap();
806        assert_row_exists(&con, "failed_rows", 13);
807    }
808
809    /// An engine-side append failure surfaces as a typed [`Error::Engine`] carrying
810    /// DuckDB's classification and message, read via `duckdb_appender_error_data`.
811    #[test]
812    fn engine_append_failure_surfaces_typed_error_data() {
813        use crate::error::EngineErrorKind;
814
815        let mut con = get_test_connection();
816        con.query("CREATE TABLE typed_fail (id INTEGER CHECK (id > 0))").unwrap();
817        let mut appender = con.appender("typed_fail", "main").unwrap();
818
819        // Append a value the CHECK rejects. DuckDB defers the constraint check, so
820        // the failure appears when the buffered row is flushed.
821        appender.append(&mut DuckValue::Int(-1)).unwrap();
822        let error = appender.save().unwrap_err();
823
824        let engine = match error {
825            Error::Engine(engine) => engine,
826            other => panic!("expected a typed engine error, got {other:?}"),
827        };
828        // DuckDB classifies this rather than leaving it Unavailable, and the driver
829        // reports that classification instead of parsing the message text.
830        assert_ne!(engine.kind, EngineErrorKind::Unavailable, "kind should be typed by DuckDB");
831        assert!(engine.message.is_some(), "DuckDB should supply a message");
832    }
833
834    // NOTE: a regression test for a failed flush on a `PRIMARY KEY` (ART-indexed)
835    // table is deliberately *not* included here. That scenario deadlocks inside
836    // DuckDB's own `duckdb_appender_destroy`. Through the wrapper — which always
837    // destroys the handle — it deadlocks *deterministically* (hangs on the first
838    // iteration of every run); an earlier pure-FFI probe that varied the call
839    // sequence saw it intermittently, hence an older "nondeterministic" wording.
840    // It is an upstream C++ defect on DuckDB's own documented cleanup path, not a
841    // wrapper defect, and no Rust-side ordering avoids it; including the test would
842    // hang CI. The fix (never re-flushing a poisoned appender) is covered
843    // below with `CHECK`-constraint failures, which poison identically but build
844    // no index and so tear down cleanly. The upstream defect is documented above.
845
846    /// Once poisoned, every further operation fails fast without touching the C
847    /// handle, and does not re-enter DuckDB.
848    #[test]
849    fn poisoned_appender_rejects_further_operations() {
850        let mut con = get_test_connection();
851        con.query("CREATE TABLE reuse_fail (id INTEGER CHECK (id > 0))").unwrap();
852        let mut appender = con.appender("reuse_fail", "main").unwrap();
853
854        appender.append(&mut DuckValue::Int(-1)).unwrap();
855        appender.save().unwrap_err(); // poisons
856
857        // Subsequent append and save both fail fast with the poisoned error.
858        let err = appender.append(&mut DuckValue::Int(5)).unwrap_err();
859        assert!(
860            matches!(
861                err,
862                Error::Engine(ref e) if e.message.as_deref() == Some("appender was invalidated by an earlier failure")
863            ),
864            "unexpected error: {err:?}"
865        );
866        assert!(appender.save().is_err());
867    }
868
869    /// A Rust-side value failure mid-row poisons the appender rather than leaving a
870    /// row open for the next append to corrupt.
871    #[test]
872    fn row_side_failure_poisons_and_leaves_connection_usable() {
873        let mut con = get_test_connection();
874        con.query("CREATE TABLE row_poison (id INTEGER, name VARCHAR)").unwrap();
875        let mut appender = con.appender("row_poison", "main").unwrap();
876
877        appender.append(&mut FailingRow).unwrap_err();
878        // The appender is poisoned; a following append is rejected.
879        assert!(appender.append(&mut Row(1, "later")).is_err());
880        drop(appender);
881
882        // The connection itself is unharmed.
883        con.query("INSERT INTO row_poison VALUES (7, 'ok')").unwrap();
884        assert_row_exists(&con, "row_poison", 7);
885    }
886
887    /// `finish` flushes, closes, and reports success; the rows are visible and the
888    /// consumed appender's drop is a bare destroy.
889    #[test]
890    fn finish_commits_rows_and_reports_success() {
891        let mut con = get_test_connection();
892        con.query("CREATE TABLE finished (id INTEGER, name VARCHAR)").unwrap();
893        let reader = con.try_clone().unwrap();
894
895        let mut appender = con.appender("finished", "main").unwrap();
896        appender.append(&mut Row(21, "done")).unwrap();
897        appender.finish().unwrap();
898
899        assert_row_exists(&reader, "finished", 21);
900    }
901
902    /// `finish` surfaces a flush/close failure to the caller instead of only
903    /// logging it as drop does.
904    #[test]
905    fn finish_reports_flush_failure() {
906        let mut con = get_test_connection();
907        con.query("CREATE TABLE finish_fail (id INTEGER CHECK (id > 0))").unwrap();
908        let mut appender = con.appender("finish_fail", "main").unwrap();
909
910        appender.append(&mut DuckValue::Int(-5)).unwrap();
911        let error = appender.finish().unwrap_err();
912        assert!(matches!(error, Error::Engine(_)), "expected typed engine error, got {error:?}");
913    }
914
915    /// `finish` on an already-poisoned appender fails fast at the state check,
916    /// before touching the C handle — it must not attempt a close on invalidated
917    /// data.
918    #[test]
919    fn finish_on_poisoned_appender_reports_poison_without_reclose() {
920        let mut con = get_test_connection();
921        con.query("CREATE TABLE finish_poisoned (id INTEGER CHECK (id > 0))").unwrap();
922        let mut appender = con.appender("finish_poisoned", "main").unwrap();
923
924        appender.append(&mut DuckValue::Int(-1)).unwrap();
925        appender.save().unwrap_err(); // poisons
926
927        let error = appender.finish().unwrap_err();
928        assert!(
929            matches!(error, Error::Engine(ref e)
930                if e.message.as_deref() == Some("appender was invalidated by an earlier failure")),
931            "finish on a poisoned appender must report the poison, got {error:?}"
932        );
933    }
934
935    /// Dropping a `Ready` appender that has an unflushed constraint-violating row
936    /// runs the best-effort flush in `Drop`, which fails and is logged (never
937    /// panics), and leaves the connection usable.
938    #[test]
939    fn drop_with_pending_violation_logs_and_leaves_connection_usable() {
940        let mut con = get_test_connection();
941        con.query("CREATE TABLE drop_fail (id INTEGER CHECK (id > 0))").unwrap();
942        {
943            let mut appender = con.appender("drop_fail", "main").unwrap();
944            // Buffered but not saved: the violation surfaces during Drop's flush.
945            appender.append(&mut DuckValue::Int(-9)).unwrap();
946            // appender drops here — Drop flushes (Ready state), fails, logs, destroys.
947        }
948        // The connection is unharmed and the bad row was not committed.
949        con.query("INSERT INTO drop_fail VALUES (3)").unwrap();
950        assert_row_exists(&con, "drop_fail", 3);
951        let mut rows = con.query("SELECT count(*) AS n FROM drop_fail").unwrap();
952        let row = rows.next().unwrap().unwrap();
953        assert_eq!(row.get("n").unwrap(), &DuckValue::BigInt(1), "only the valid row persisted");
954    }
955
956    // Catalog-aware / query appenders, schema introspection, projected columns.
957    mod builders {
958        use crate::connection::Connection;
959        use crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER;
960        use crate::types::value::DuckValue;
961        use crate::types::LogicalType;
962
963        #[test]
964        fn appender_ext_default_and_named_catalog() {
965            let mut conn = Connection::open_in_memory().unwrap();
966            conn.execute_batch("CREATE TABLE t (id INTEGER)").unwrap();
967
968            // Default catalog (None).
969            {
970                let mut app = conn.appender_ext(None, "main", "t").unwrap();
971                app.append(&mut DuckValue::Int(1)).unwrap();
972                app.save().unwrap();
973            }
974            // Explicit in-memory catalog name.
975            {
976                let mut app = conn.appender_ext(Some("memory"), "main", "t").unwrap();
977                app.append(&mut DuckValue::Int(2)).unwrap();
978                app.save().unwrap();
979            }
980            let rows: Vec<_> = conn
981                .execute("SELECT id FROM t ORDER BY id")
982                .unwrap()
983                .collect::<Result<_, _>>()
984                .unwrap();
985            assert_eq!(rows.len(), 2);
986            assert_eq!(rows[0].get("id"), Some(&DuckValue::Int(1)));
987            assert_eq!(rows[1].get("id"), Some(&DuckValue::Int(2)));
988        }
989
990        #[test]
991        fn column_count_and_type_reflect_the_table() {
992            let mut conn = Connection::open_in_memory().unwrap();
993            conn.execute_batch("CREATE TABLE t (id INTEGER, name VARCHAR)").unwrap();
994            let app = conn.appender("t", "main").unwrap();
995            assert_eq!(app.column_count(), 2);
996            let ty = app.column_type(0).expect("column 0 type");
997            assert_eq!(ty.type_id(), DUCKDB_TYPE_DUCKDB_TYPE_INTEGER);
998        }
999
1000        #[test]
1001        fn add_column_projects_and_fills_defaults() {
1002            let mut conn = Connection::open_in_memory().unwrap();
1003            conn.execute_batch("CREATE TABLE t (a INTEGER, b INTEGER DEFAULT 99)").unwrap();
1004            {
1005                let mut app = conn.appender("t", "main").unwrap();
1006                // Project only `a`; `b` should take its DEFAULT.
1007                app.add_column("a").unwrap();
1008                assert_eq!(app.column_count(), 1, "active column list is just `a`");
1009                app.append(&mut DuckValue::Int(7)).unwrap();
1010                app.save().unwrap();
1011            }
1012            let mut rows = conn.execute("SELECT a, b FROM t").unwrap();
1013            let row = rows.next().unwrap().unwrap();
1014            assert_eq!(row.get("a"), Some(&DuckValue::Int(7)));
1015            assert_eq!(
1016                row.get("b"),
1017                Some(&DuckValue::Int(99)),
1018                "DEFAULT filled the unprojected column"
1019            );
1020        }
1021
1022        #[test]
1023        fn clear_columns_restores_full_projection() {
1024            let mut conn = Connection::open_in_memory().unwrap();
1025            conn.execute_batch("CREATE TABLE t (a INTEGER, b INTEGER)").unwrap();
1026            let mut app = conn.appender("t", "main").unwrap();
1027            app.add_column("a").unwrap();
1028            assert_eq!(app.column_count(), 1);
1029            app.clear_columns().unwrap();
1030            assert_eq!(app.column_count(), 2, "clear_columns restores every column");
1031        }
1032
1033        #[test]
1034        fn configuration_after_first_row_is_rejected() {
1035            let mut conn = Connection::open_in_memory().unwrap();
1036            // Single column so a bare-value row supplies every column and succeeds.
1037            conn.execute_batch("CREATE TABLE t (a INTEGER)").unwrap();
1038            let mut app = conn.appender("t", "main").unwrap();
1039            app.append(&mut DuckValue::Int(1)).unwrap();
1040            // A row exists, so the active column list can no longer be reconfigured.
1041            assert!(app.add_column("a").is_err(), "add_column after a row must be rejected");
1042            assert!(app.clear_columns().is_err(), "clear_columns after a row must be rejected");
1043        }
1044
1045        #[test]
1046        fn appender_query_feeds_an_insert() {
1047            let mut conn = Connection::open_in_memory().unwrap();
1048            conn.execute_batch("CREATE TABLE dest (v INTEGER)").unwrap();
1049            let types = [LogicalType::of::<i32>().unwrap()];
1050            {
1051                let mut app = conn
1052                    .appender_query(
1053                        "INSERT INTO dest SELECT * FROM appended_data",
1054                        &types,
1055                        None,
1056                        None,
1057                    )
1058                    .unwrap();
1059                app.append(&mut DuckValue::Int(42)).unwrap();
1060                app.append(&mut DuckValue::Int(43)).unwrap();
1061                app.save().unwrap();
1062            }
1063            let rows: Vec<_> = conn
1064                .execute("SELECT v FROM dest ORDER BY v")
1065                .unwrap()
1066                .collect::<Result<_, _>>()
1067                .unwrap();
1068            assert_eq!(rows.len(), 2);
1069            assert_eq!(rows[0].get("v"), Some(&DuckValue::Int(42)));
1070            assert_eq!(rows[1].get("v"), Some(&DuckValue::Int(43)));
1071        }
1072    }
1073
1074    // DEFAULT rows/cells and whole-chunk ingestion.
1075    mod chunk_ingestion {
1076        use crate::connection::Connection;
1077        use crate::raw::data_chunk::DataChunk;
1078        use crate::types::value::DuckValue;
1079        use crate::types::LogicalType;
1080
1081        #[test]
1082        fn append_default_row_fills_table_defaults_and_nulls() {
1083            let mut conn = Connection::open_in_memory().unwrap();
1084            // `a` has a DEFAULT, `b` does not (so it becomes NULL).
1085            conn.execute_batch("CREATE TABLE t (a INTEGER DEFAULT 5, b INTEGER)").unwrap();
1086            {
1087                let mut app = conn.appender("t", "main").unwrap();
1088                app.append_default_row().unwrap();
1089                app.save().unwrap();
1090            }
1091            let mut rows = conn.execute("SELECT a, b FROM t").unwrap();
1092            let row = rows.next().unwrap().unwrap();
1093            assert_eq!(row.get("a"), Some(&DuckValue::Int(5)), "DEFAULT applied");
1094            assert_eq!(row.get("b"), Some(&DuckValue::Null), "no DEFAULT -> NULL");
1095        }
1096
1097        #[test]
1098        fn append_chunk_copies_rows_and_validates_column_count() {
1099            let mut conn = Connection::open_in_memory().unwrap();
1100            conn.execute_batch("CREATE TABLE src (v INTEGER)").unwrap();
1101            conn.execute_batch("INSERT INTO src VALUES (1), (2), (3)").unwrap();
1102            conn.execute_batch("CREATE TABLE dst (v INTEGER)").unwrap();
1103
1104            // Fetch a chunk of the source rows (owned; independent of the result).
1105            let chunk = {
1106                let result = conn.execute("SELECT v FROM src ORDER BY v").unwrap();
1107                DataChunk::from_result(&result).expect("a chunk").unwrap()
1108            };
1109
1110            {
1111                let mut app = conn.appender("dst", "main").unwrap();
1112                app.append_chunk(&chunk).unwrap();
1113                app.save().unwrap();
1114            }
1115            let rows: Vec<_> = conn
1116                .execute("SELECT v FROM dst ORDER BY v")
1117                .unwrap()
1118                .collect::<Result<_, _>>()
1119                .unwrap();
1120            assert_eq!(rows.len(), 3);
1121            assert_eq!(rows[0].get("v"), Some(&DuckValue::Int(1)));
1122            assert_eq!(rows[2].get("v"), Some(&DuckValue::Int(3)));
1123
1124            // A chunk whose column count disagrees with the appender is rejected
1125            // Rust-side (dst2 has two columns, the chunk has one).
1126            conn.execute_batch("CREATE TABLE dst2 (v INTEGER, w INTEGER)").unwrap();
1127            let mut app2 = conn.appender("dst2", "main").unwrap();
1128            assert!(app2.append_chunk(&chunk).is_err(), "column-count mismatch must error");
1129        }
1130
1131        #[test]
1132        fn append_default_to_chunk_fills_a_cell() {
1133            let mut conn = Connection::open_in_memory().unwrap();
1134            conn.execute_batch("CREATE TABLE t (a INTEGER DEFAULT 42)").unwrap();
1135
1136            // Build a one-column INTEGER chunk to receive the default.
1137            let int_ty = LogicalType::of::<i32>().unwrap();
1138            let mut raw_types = [int_ty.as_raw()];
1139            // SAFETY: `raw_types` holds one valid logical type handle that outlives the
1140            // call; DuckDB copies it. The returned chunk is wrapped in RAII (`DataChunk`)
1141            // so it is destroyed exactly once.
1142            let raw = unsafe { crate::ffi::duckdb_create_data_chunk(raw_types.as_mut_ptr(), 1) };
1143            let mut chunk = DataChunk::new(raw).unwrap();
1144            drop(int_ty);
1145
1146            {
1147                let mut app = conn.appender("t", "main").unwrap();
1148                // Write column 0's DEFAULT (42) into chunk cell (0, 0), then size it.
1149                app.append_default_to_chunk(&mut chunk, 0, 0).unwrap();
1150                // SAFETY: `chunk` is valid; one row is now populated.
1151                unsafe { crate::ffi::duckdb_data_chunk_set_size(*chunk, 1) };
1152                app.append_chunk(&chunk).unwrap();
1153                app.save().unwrap();
1154
1155                // Out-of-range column is rejected Rust-side.
1156                assert!(app.append_default_to_chunk(&mut chunk, 5, 0).is_err());
1157            }
1158            let mut rows = conn.execute("SELECT a FROM t").unwrap();
1159            let row = rows.next().unwrap().unwrap();
1160            assert_eq!(row.get("a"), Some(&DuckValue::Int(42)));
1161        }
1162    }
1163}