Skip to main content

quack_rs/
appender.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Bulk data appending.
7//!
8//! [`Appender`] is an RAII wrapper around `DuckDB`'s appender — the fastest way
9//! to bulk-insert rows into an existing table, and considerably faster than
10//! issuing `INSERT` statements.
11//!
12//! # Two ways to append
13//!
14//! **Row at a time.** Call one `append_*` per column, then
15//! [`end_row`][Appender::end_row] — or let [`row`][Appender::row] call it for
16//! you, which is the difference between a forgotten `end_row` being a compile
17//! -time non-issue and a silently short table:
18//!
19//! ```rust,no_run
20//! use quack_rs::appender::Appender;
21//! # use libduckdb_sys::duckdb_connection;
22//! # unsafe fn demo(con: duckdb_connection) -> Result<(), quack_rs::appender::AppendError> {
23//! // SAFETY: `con` is a valid, open connection.
24//! let appender = unsafe { Appender::new(con, None, c"measurements") }?;
25//! for (sensor, reading) in [("a", 1.5_f64), ("b", 2.5)] {
26//!     appender.row(|row| {
27//!         row.append_str(sensor)?;
28//!         row.append_f64(reading)
29//!     })?;
30//! }
31//! appender.close()?;
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! **A chunk at a time.** Build a [`DataChunk`] and hand it over with
37//! [`append_chunk`][Appender::append_chunk]. Fewer FFI crossings, and the
38//! natural fit when the data already lives in vectors.
39//!
40//! # Errors and the appender's lifecycle
41//!
42//! Appended rows are buffered. A constraint violation therefore surfaces at
43//! [`flush`][Appender::flush] or [`close`][Appender::close], not at the
44//! `append_*` call that caused it, and it **invalidates every buffered row**.
45//!
46//! Dropping an `Appender` closes it, and a failure there has nowhere to go —
47//! `DuckDB`'s own header is explicit that after destruction "it is no longer
48//! possible to obtain the specific error message". Call
49//! [`close`][Appender::close] explicitly whenever the outcome matters.
50//!
51//! # Feature flags
52//!
53//! The appender is available **without** any feature flag: `DuckDB` has kept
54//! `duckdb_appender_*` in the frozen stable prefix of the extension API
55//! (slots 281–291 and 330–356) since v1.2.0, so using it does not push an
56//! extension onto the version-pinned unstable ABI. Three methods are the
57//! exception and are gated on `duckdb-1-5`:
58//! [`error_data`][Appender::error_data], [`clear`][Appender::clear] and
59//! [`append_default_to_chunk`][Appender::append_default_to_chunk].
60//!
61//! That gate also picks the error type — see [`AppendError`].
62
63use std::ffi::CStr;
64
65use libduckdb_sys::{
66    duckdb_append_blob, duckdb_append_data_chunk, duckdb_append_date, duckdb_append_default,
67    duckdb_append_hugeint, duckdb_append_interval, duckdb_append_null, duckdb_append_time,
68    duckdb_append_timestamp, duckdb_append_uhugeint, duckdb_append_value,
69    duckdb_append_varchar_length, duckdb_appender, duckdb_appender_add_column,
70    duckdb_appender_clear_columns, duckdb_appender_close, duckdb_appender_column_count,
71    duckdb_appender_column_type, duckdb_appender_create, duckdb_appender_create_ext,
72    duckdb_appender_destroy, duckdb_appender_end_row, duckdb_appender_error, duckdb_appender_flush,
73    duckdb_connection, duckdb_date, duckdb_hugeint, duckdb_interval, duckdb_state, duckdb_time,
74    duckdb_timestamp, duckdb_uhugeint, idx_t, DuckDBSuccess,
75};
76#[cfg(feature = "duckdb-1-5")]
77use libduckdb_sys::{duckdb_append_default_to_chunk, duckdb_appender_clear};
78
79use crate::data_chunk::DataChunk;
80#[cfg(feature = "duckdb-1-5")]
81use crate::error_data::ErrorData;
82use crate::interval::DuckInterval;
83use crate::types::LogicalType;
84use crate::value::Value;
85
86/// The error type every fallible [`Appender`] operation reports.
87///
88/// `DuckDB` exposes the appender's error two ways, and only one of them is in
89/// the stable prefix:
90///
91/// | Feature | Type | C API |
92/// |---------|------|-------|
93/// | `duckdb-1-5` on | [`ErrorData`] — message **and** machine-readable category | `duckdb_appender_error_data` (unstable slot 408) |
94/// | `duckdb-1-5` off | [`ExtensionError`][crate::error::ExtensionError] — message only | `duckdb_appender_error` (stable slot 285) |
95///
96/// Enabling `duckdb-1-5` therefore upgrades the error type in place; it does
97/// not change any method's shape.
98#[cfg(feature = "duckdb-1-5")]
99pub type AppendError = ErrorData;
100
101/// The error type every fallible [`Appender`] operation reports.
102///
103/// See the `duckdb-1-5` variant of this alias for the full explanation: without
104/// that feature the appender reports errors through the stable
105/// `duckdb_appender_error`, which carries a message but no category.
106#[cfg(not(feature = "duckdb-1-5"))]
107pub type AppendError = crate::error::ExtensionError;
108
109/// `duckdb_append_varchar_length` narrows its length argument to `uint32_t`
110/// with `UnsafeNumericCast`, which is a plain `static_cast` in the release
111/// builds `DuckDB` ships. A longer string would be silently truncated to its
112/// low 32 bits, so it is refused here instead.
113const MAX_VARCHAR_LEN: usize = u32::MAX as usize;
114
115/// Converts an optional `&CStr` into a (possibly null) C string pointer.
116#[inline]
117fn opt_ptr(s: Option<&CStr>) -> *const std::os::raw::c_char {
118    s.map_or(std::ptr::null(), CStr::as_ptr)
119}
120
121/// RAII wrapper for a `duckdb_appender`.
122///
123/// The appender is closed and destroyed automatically on drop. To surface any
124/// error from the final flush, call [`close`][Appender::close] explicitly
125/// beforehand.
126///
127/// See the [module docs][crate::appender] for the two append styles and the
128/// buffering rules that decide when an error appears.
129pub struct Appender {
130    appender: duckdb_appender,
131}
132
133impl Appender {
134    // ── Construction ────────────────────────────────────────────────────
135
136    /// Creates an appender for `table` in the given `schema` (or the default
137    /// schema when `schema` is `None`).
138    ///
139    /// # Errors
140    ///
141    /// Returns an [`AppendError`] if the appender cannot be created — most
142    /// often because the table does not exist.
143    ///
144    /// # Safety
145    ///
146    /// `con` must be a valid, open `duckdb_connection`.
147    pub unsafe fn new(
148        con: duckdb_connection,
149        schema: Option<&CStr>,
150        table: &CStr,
151    ) -> Result<Self, AppendError> {
152        let mut raw: duckdb_appender = std::ptr::null_mut();
153        // SAFETY: con is valid per caller's contract; the string pointers are
154        // valid for the call; raw is a valid out-pointer.
155        let state =
156            unsafe { duckdb_appender_create(con, opt_ptr(schema), table.as_ptr(), &raw mut raw) };
157        // DuckDB allocates the wrapper and writes it to `raw` *before* it can
158        // fail, precisely so the error is readable, so this must be constructed
159        // either way — and it must be dropped on the error path, which is what
160        // returning it inside `Err` via `last_error` arranges.
161        let appender = Self { appender: raw };
162        if state == DuckDBSuccess {
163            Ok(appender)
164        } else {
165            Err(appender.last_error())
166        }
167    }
168
169    /// Creates an appender for `table`, fully qualified by optional `catalog`
170    /// and `schema`.
171    ///
172    /// # Errors
173    ///
174    /// Returns an [`AppendError`] if the appender cannot be created.
175    ///
176    /// # Safety
177    ///
178    /// `con` must be a valid, open `duckdb_connection`.
179    pub unsafe fn with_catalog(
180        con: duckdb_connection,
181        catalog: Option<&CStr>,
182        schema: Option<&CStr>,
183        table: &CStr,
184    ) -> Result<Self, AppendError> {
185        let mut raw: duckdb_appender = std::ptr::null_mut();
186        // SAFETY: con is valid per caller's contract; the string pointers are
187        // valid for the call; raw is a valid out-pointer.
188        let state = unsafe {
189            duckdb_appender_create_ext(
190                con,
191                opt_ptr(catalog),
192                opt_ptr(schema),
193                table.as_ptr(),
194                &raw mut raw,
195            )
196        };
197        let appender = Self { appender: raw };
198        if state == DuckDBSuccess {
199            Ok(appender)
200        } else {
201            Err(appender.last_error())
202        }
203    }
204
205    // ── Schema ──────────────────────────────────────────────────────────
206
207    /// Number of columns the appender currently expects per row.
208    ///
209    /// This is the *active* column list, so it reflects any
210    /// [`add_column`][Self::add_column] calls rather than always matching the
211    /// table's width.
212    #[must_use]
213    pub fn column_count(&self) -> u64 {
214        // SAFETY: self.appender is valid; DuckDB returns 0 for a null or
215        // uninitialised appender.
216        unsafe { duckdb_appender_column_count(self.appender) }
217    }
218
219    /// Type of active column `index`, or `None` if the index is out of range.
220    #[must_use]
221    pub fn column_type(&self, index: u64) -> Option<LogicalType> {
222        // SAFETY: self.appender is valid; DuckDB bounds-checks `index` and
223        // returns null when it is out of range.
224        let raw = unsafe { duckdb_appender_column_type(self.appender, index as idx_t) };
225        if raw.is_null() {
226            None
227        } else {
228            // SAFETY: raw is a freshly allocated logical type that we now own.
229            Some(unsafe { LogicalType::from_raw(raw) })
230        }
231    }
232
233    /// Restricts appends to a named subset of the table's columns.
234    ///
235    /// Columns left out are filled with their `DEFAULT` (or NULL). Calling this
236    /// **flushes everything appended so far**.
237    ///
238    /// # Errors
239    ///
240    /// Returns an [`AppendError`] if the column does not exist, or if the
241    /// implicit flush fails.
242    pub fn add_column(&self, name: &CStr) -> Result<(), AppendError> {
243        // SAFETY: self.appender is valid and `name` is a NUL-terminated string
244        // that outlives the call.
245        let state = unsafe { duckdb_appender_add_column(self.appender, name.as_ptr()) };
246        self.check(state)
247    }
248
249    /// Resets the active column list so every table column is expected again.
250    ///
251    /// Also flushes everything appended so far.
252    ///
253    /// # Errors
254    ///
255    /// Returns an [`AppendError`] if the implicit flush fails.
256    pub fn clear_columns(&self) -> Result<(), AppendError> {
257        // SAFETY: self.appender is valid.
258        let state = unsafe { duckdb_appender_clear_columns(self.appender) };
259        self.check(state)
260    }
261
262    // ── Row-at-a-time appends ───────────────────────────────────────────
263
264    /// Appends one row, calling [`end_row`][Self::end_row] afterwards.
265    ///
266    /// The closure appends one value per active column. `end_row` runs only if
267    /// the closure succeeded, so a failed append does not leave a half-written
268    /// row behind.
269    ///
270    /// # Errors
271    ///
272    /// Returns whatever the closure returned, or the [`AppendError`] from
273    /// `end_row` — most often "call to `EndRow` before all columns have been
274    /// appended to".
275    pub fn row<F>(&self, append: F) -> Result<(), AppendError>
276    where
277        F: FnOnce(&Self) -> Result<(), AppendError>,
278    {
279        append(self)?;
280        self.end_row()
281    }
282
283    /// Finishes the current row.
284    ///
285    /// # Errors
286    ///
287    /// Returns an [`AppendError`] if fewer values were appended than the
288    /// appender has active columns.
289    pub fn end_row(&self) -> Result<(), AppendError> {
290        // SAFETY: self.appender is valid.
291        let state = unsafe { duckdb_appender_end_row(self.appender) };
292        self.check(state)
293    }
294
295    /// Appends SQL `NULL` to the current row, whatever the column's type.
296    ///
297    /// # Errors
298    ///
299    /// Returns an [`AppendError`] if the append fails.
300    pub fn append_null(&self) -> Result<(), AppendError> {
301        // SAFETY: self.appender is valid.
302        self.check(unsafe { duckdb_append_null(self.appender) })
303    }
304
305    /// Appends the column's `DEFAULT` value to the current row.
306    ///
307    /// # Errors
308    ///
309    /// Returns an [`AppendError`] if the column has no default, or the append
310    /// fails.
311    pub fn append_default(&self) -> Result<(), AppendError> {
312        // SAFETY: self.appender is valid.
313        self.check(unsafe { duckdb_append_default(self.appender) })
314    }
315
316    /// Appends a `VARCHAR`.
317    ///
318    /// Uses `duckdb_append_varchar_length`, so **interior NUL bytes are
319    /// preserved** — unlike the NUL-terminated `duckdb_append_varchar`, which
320    /// would stop at the first one.
321    ///
322    /// # Errors
323    ///
324    /// Returns an [`AppendError`] if the append fails, or if `value` is longer
325    /// than `u32::MAX` bytes — a length `DuckDB` narrows to 32 bits without
326    /// checking in its release builds.
327    pub fn append_str(&self, value: &str) -> Result<(), AppendError> {
328        self.append_bytes_as(value.as_bytes(), true)
329    }
330
331    /// Appends a `BLOB`.
332    ///
333    /// # Errors
334    ///
335    /// Returns an [`AppendError`] if the append fails.
336    pub fn append_bytes(&self, value: &[u8]) -> Result<(), AppendError> {
337        self.append_bytes_as(value, false)
338    }
339
340    fn append_bytes_as(&self, value: &[u8], varchar: bool) -> Result<(), AppendError> {
341        if varchar {
342            if value.len() > MAX_VARCHAR_LEN {
343                return Err(append_error(&format!(
344                    "VARCHAR of {} bytes exceeds DuckDB's {MAX_VARCHAR_LEN}-byte appender limit",
345                    value.len()
346                )));
347            }
348            // SAFETY: self.appender is valid; the pointer/length pair describes
349            // `value`, which outlives the call.
350            let state = unsafe {
351                duckdb_append_varchar_length(
352                    self.appender,
353                    value.as_ptr().cast::<std::os::raw::c_char>(),
354                    value.len() as idx_t,
355                )
356            };
357            return self.check(state);
358        }
359        // SAFETY: as above; DuckDB copies the bytes into a BLOB value.
360        let state = unsafe {
361            duckdb_append_blob(
362                self.appender,
363                value.as_ptr().cast::<std::os::raw::c_void>(),
364                value.len() as idx_t,
365            )
366        };
367        self.check(state)
368    }
369
370    /// Appends a `DATE` as days since 1970-01-01.
371    ///
372    /// # Errors
373    ///
374    /// Returns an [`AppendError`] if the append fails.
375    pub fn append_date(&self, days: i32) -> Result<(), AppendError> {
376        // SAFETY: self.appender is valid.
377        self.check(unsafe { duckdb_append_date(self.appender, duckdb_date { days }) })
378    }
379
380    /// Appends a `TIME` as microseconds since midnight.
381    ///
382    /// # Errors
383    ///
384    /// Returns an [`AppendError`] if the append fails.
385    pub fn append_time(&self, micros: i64) -> Result<(), AppendError> {
386        // SAFETY: self.appender is valid.
387        self.check(unsafe { duckdb_append_time(self.appender, duckdb_time { micros }) })
388    }
389
390    /// Appends a `TIMESTAMP` as microseconds since the epoch.
391    ///
392    /// # Errors
393    ///
394    /// Returns an [`AppendError`] if the append fails.
395    pub fn append_timestamp(&self, micros: i64) -> Result<(), AppendError> {
396        // SAFETY: self.appender is valid.
397        self.check(unsafe { duckdb_append_timestamp(self.appender, duckdb_timestamp { micros }) })
398    }
399
400    /// Appends an `INTERVAL`.
401    ///
402    /// # Errors
403    ///
404    /// Returns an [`AppendError`] if the append fails.
405    pub fn append_interval(&self, value: DuckInterval) -> Result<(), AppendError> {
406        let raw = duckdb_interval {
407            months: value.months,
408            days: value.days,
409            micros: value.micros,
410        };
411        // SAFETY: self.appender is valid.
412        self.check(unsafe { duckdb_append_interval(self.appender, raw) })
413    }
414
415    /// Appends an arbitrary [`Value`], letting `DuckDB` cast it to the column's
416    /// type.
417    ///
418    /// This is the escape hatch for types with no dedicated `append_*`:
419    /// `LIST`, `STRUCT`, `MAP`, `UUID`, `DECIMAL`, `ENUM`.
420    ///
421    /// # Errors
422    ///
423    /// Returns an [`AppendError`] if `value` holds a null handle — which
424    /// `duckdb_append_value` would dereference — or if the append fails.
425    pub fn append_value(&self, value: &Value) -> Result<(), AppendError> {
426        if value.as_raw().is_null() {
427            // duckdb_append_value dereferences its argument with no null check.
428            return Err(append_error("cannot append a null duckdb_value handle"));
429        }
430        // SAFETY: self.appender is valid and value.as_raw() is non-null.
431        self.check(unsafe { duckdb_append_value(self.appender, value.as_raw()) })
432    }
433
434    // ── Chunk appends ───────────────────────────────────────────────────
435
436    /// Appends an entire [`DataChunk`].
437    ///
438    /// The chunk's column types must match the appender's active columns; see
439    /// [`column_type`][Self::column_type] to discover them.
440    ///
441    /// # Errors
442    ///
443    /// Returns an [`AppendError`] if the append fails.
444    pub fn append_chunk(&self, chunk: &DataChunk) -> Result<(), AppendError> {
445        // SAFETY: self.appender and chunk.as_raw() are valid.
446        let state = unsafe { duckdb_append_data_chunk(self.appender, chunk.as_raw()) };
447        self.check(state)
448    }
449
450    /// Writes the table column `col`'s `DEFAULT` value into row `row` of
451    /// `chunk`.
452    ///
453    /// Useful when building a chunk to append: columns without an explicit
454    /// value can be filled with their schema default.
455    ///
456    /// # Errors
457    ///
458    /// Returns an [`AppendError`] if the default cannot be written.
459    #[cfg(feature = "duckdb-1-5")]
460    pub fn append_default_to_chunk(
461        &self,
462        chunk: &DataChunk,
463        col: u64,
464        row: u64,
465    ) -> Result<(), AppendError> {
466        // SAFETY: self.appender and chunk.as_raw() are valid.
467        let state =
468            unsafe { duckdb_append_default_to_chunk(self.appender, chunk.as_raw(), col, row) };
469        self.check(state)
470    }
471
472    // ── Lifecycle ───────────────────────────────────────────────────────
473
474    /// Flushes buffered rows to the table without closing the appender.
475    ///
476    /// # Errors
477    ///
478    /// Returns an [`AppendError`] if the flush fails — a constraint violation,
479    /// typically. On failure every buffered row is invalidated; with
480    /// `duckdb-1-5` they can be discarded with [`clear`][Self::clear].
481    pub fn flush(&self) -> Result<(), AppendError> {
482        // SAFETY: self.appender is valid.
483        let state = unsafe { duckdb_appender_flush(self.appender) };
484        self.check(state)
485    }
486
487    /// Flushes and closes the appender. No further rows may be appended.
488    ///
489    /// # Errors
490    ///
491    /// Returns an [`AppendError`] if the final flush fails.
492    pub fn close(&self) -> Result<(), AppendError> {
493        // SAFETY: self.appender is valid.
494        let state = unsafe { duckdb_appender_close(self.appender) };
495        self.check(state)
496    }
497
498    /// Discards all buffered, unflushed rows.
499    ///
500    /// Useful for recovering after a [`flush`][Self::flush] error without
501    /// re-appending the rows that were already committed.
502    ///
503    /// # Errors
504    ///
505    /// Returns an [`AppendError`] if the appender state is invalid.
506    #[cfg(feature = "duckdb-1-5")]
507    pub fn clear(&self) -> Result<(), AppendError> {
508        // SAFETY: self.appender is valid.
509        let state = unsafe { duckdb_appender_clear(self.appender) };
510        self.check(state)
511    }
512
513    // ── Errors ──────────────────────────────────────────────────────────
514
515    /// Returns the structured error from the most recent failed operation.
516    #[cfg(feature = "duckdb-1-5")]
517    #[must_use]
518    pub fn error_data(&self) -> ErrorData {
519        // SAFETY: self.appender may be null (a failed create); DuckDB handles
520        // that and returns an owned, empty error data handle.
521        let raw = unsafe { libduckdb_sys::duckdb_appender_error_data(self.appender) };
522        // SAFETY: raw is an owned duckdb_error_data (possibly null).
523        unsafe { ErrorData::from_raw(raw) }
524    }
525
526    /// Returns the message from the most recent failed operation, if any.
527    ///
528    /// Always available; with `duckdb-1-5` prefer
529    /// [`error_data`][Self::error_data], which also carries the error category.
530    #[must_use]
531    pub fn error_message(&self) -> Option<String> {
532        if self.appender.is_null() {
533            return None;
534        }
535        // SAFETY: self.appender is non-null; DuckDB returns null when there is
536        // no error, and otherwise a string it owns until the appender is
537        // destroyed — so it is copied out here rather than borrowed.
538        let ptr = unsafe { duckdb_appender_error(self.appender) };
539        if ptr.is_null() {
540            return None;
541        }
542        // SAFETY: ptr is a valid NUL-terminated string owned by the appender.
543        Some(
544            unsafe { CStr::from_ptr(ptr) }
545                .to_string_lossy()
546                .into_owned(),
547        )
548    }
549
550    /// Returns the raw handle.
551    #[inline]
552    #[must_use]
553    pub const fn as_raw(&self) -> duckdb_appender {
554        self.appender
555    }
556
557    /// Reads whichever error channel this build has.
558    #[cfg(feature = "duckdb-1-5")]
559    fn last_error(&self) -> AppendError {
560        self.error_data()
561    }
562
563    /// Reads whichever error channel this build has.
564    #[cfg(not(feature = "duckdb-1-5"))]
565    fn last_error(&self) -> AppendError {
566        self.error_message().map_or_else(
567            || append_error("appender operation failed"),
568            crate::error::ExtensionError::new,
569        )
570    }
571
572    /// Converts a `duckdb_state` into a `Result`, reading the appender's error
573    /// on failure.
574    fn check(&self, state: duckdb_state) -> Result<(), AppendError> {
575        if state == DuckDBSuccess {
576            Ok(())
577        } else {
578            Err(self.last_error())
579        }
580    }
581}
582
583/// Builds an [`AppendError`] for a failure quack-rs detected itself, before
584/// `DuckDB` was ever called.
585#[cfg(feature = "duckdb-1-5")]
586fn append_error(message: &str) -> AppendError {
587    ErrorData::new(crate::error_data::DuckDbErrorType::InvalidInput, message)
588}
589
590/// Builds an [`AppendError`] for a failure quack-rs detected itself, before
591/// `DuckDB` was ever called.
592#[cfg(not(feature = "duckdb-1-5"))]
593fn append_error(message: &str) -> AppendError {
594    crate::error::ExtensionError::new(message)
595}
596
597/// Generates the fixed-width numeric `append_*` methods, which differ only in
598/// the C function they call.
599macro_rules! append_scalar {
600    ($($(#[$attr:meta])* $name:ident($ty:ty) => $c_fn:ident),* $(,)?) => {
601        impl Appender {
602            $(
603                $(#[$attr])*
604                ///
605                /// # Errors
606                ///
607                /// Returns an [`AppendError`] if the append fails.
608                pub fn $name(&self, value: $ty) -> Result<(), AppendError> {
609                    // SAFETY: self.appender is valid.
610                    self.check(unsafe { libduckdb_sys::$c_fn(self.appender, value) })
611                }
612            )*
613        }
614    };
615}
616
617append_scalar! {
618    /// Appends a `BOOLEAN`.
619    append_bool(bool) => duckdb_append_bool,
620    /// Appends a `TINYINT`.
621    append_i8(i8) => duckdb_append_int8,
622    /// Appends a `SMALLINT`.
623    append_i16(i16) => duckdb_append_int16,
624    /// Appends an `INTEGER`.
625    append_i32(i32) => duckdb_append_int32,
626    /// Appends a `BIGINT`.
627    append_i64(i64) => duckdb_append_int64,
628    /// Appends a `UTINYINT`.
629    append_u8(u8) => duckdb_append_uint8,
630    /// Appends a `USMALLINT`.
631    append_u16(u16) => duckdb_append_uint16,
632    /// Appends a `UINTEGER`.
633    append_u32(u32) => duckdb_append_uint32,
634    /// Appends a `UBIGINT`.
635    append_u64(u64) => duckdb_append_uint64,
636    /// Appends a `FLOAT`.
637    append_f32(f32) => duckdb_append_float,
638    /// Appends a `DOUBLE`.
639    append_f64(f64) => duckdb_append_double,
640}
641
642impl Appender {
643    /// Appends a `HUGEINT`.
644    ///
645    /// # Errors
646    ///
647    /// Returns an [`AppendError`] if the append fails.
648    pub fn append_i128(&self, value: i128) -> Result<(), AppendError> {
649        let raw = duckdb_hugeint {
650            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
651            lower: value as u64,
652            #[allow(clippy::cast_possible_truncation)]
653            upper: (value >> 64) as i64,
654        };
655        // SAFETY: self.appender is valid.
656        self.check(unsafe { duckdb_append_hugeint(self.appender, raw) })
657    }
658
659    /// Appends a `UHUGEINT`.
660    ///
661    /// # Errors
662    ///
663    /// Returns an [`AppendError`] if the append fails.
664    pub fn append_u128(&self, value: u128) -> Result<(), AppendError> {
665        let raw = duckdb_uhugeint {
666            #[allow(clippy::cast_possible_truncation)]
667            lower: value as u64,
668            #[allow(clippy::cast_possible_truncation)]
669            upper: (value >> 64) as u64,
670        };
671        // SAFETY: self.appender is valid.
672        self.check(unsafe { duckdb_append_uhugeint(self.appender, raw) })
673    }
674}
675
676impl Drop for Appender {
677    fn drop(&mut self) {
678        if !self.appender.is_null() {
679            // SAFETY: self.appender is a valid handle that we own. Destroy
680            // closes (and so flushes) it first; the state is intentionally
681            // ignored here — `close` beforehand is how a final flush error is
682            // observed, because destruction also frees the error message.
683            unsafe { duckdb_appender_destroy(&raw mut self.appender) };
684        }
685    }
686}
687
688crate::debug_repr::impl_handle_debug!(Appender.appender);