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);