Skip to main content
The Appender (better_duck_core::Appender) is DuckDB’s fast bulk-ingest path — far faster than row-by-row INSERT. Create one from a Connection. This page covers the everyday lifecycle; see appender advanced for projected columns, catalog/query appenders, raw chunk ingestion, and poisoning.

Lifecycle at a glance

1

Create

conn.appender("items", "main")? — (table, schema).
2

Append rows

One append call per column value, left to right, row by row.
3

Flush

save() (flush, keep using) or finish() (flush, close, report errors).
Rows map to columns in order; supply exactly one value per column per row.

Appending rows

append<T: AppendAble>(&mut self, row: &mut T) -> Result<()> (#[must_use]). Every DuckValue and the primitive Rust types implement AppendAble. Internally the call opens a row, appends the value, and closes the row with a guard that ends the row even on an early return or panic — so a half-written row can never bleed into the next call.
For a struct-per-row path, derive #[derive(ToRow)] and append the whole struct at once:

Flushing: save vs finish vs drop

Prefer finish() over relying on drop when you need the flush/close error reported — drop cannot return a Result, so it swallows failures (logging only).

Appenders and the connection

  • An Appender keeps its originating connection alive and appends inside any transaction open on that connection.
  • A second connection (try_clone) can read the rows after save().
  • Connection::close errors while an appender from that connection is still alive; the connection closes once the last appender drops.

See also

  • Appender advanced — projected columns, catalog/query appenders, raw chunk ingestion, and poisoning semantics.
  • ToRow derive — append a struct as a row.
  • Data chunks — build a chunk and bulk-append it.