> ## Documentation Index
> Fetch the complete documentation index at: https://better-duck.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Appender — Bulk Insert

> Load rows fast with DuckDB's Appender: creating one, appending rows, flushing with save/finish, drop behavior, and struct-per-row ergonomics.

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`](/core/opening-connections). This page covers
the everyday lifecycle; see [appender advanced](/core/appender-advanced) for projected columns,
catalog/query appenders, raw chunk ingestion, and poisoning.

## Lifecycle at a glance

<Steps>
  <Step title="Create">
    `conn.appender("items", "main")?` — `(table, schema)`.
  </Step>

  <Step title="Append rows">
    One `append` call per column value, left to right, row by row.
  </Step>

  <Step title="Flush">
    `save()` (flush, keep using) or `finish()` (flush, close, report errors).
  </Step>
</Steps>

```rust theme={null}
use better_duck_core::types::value::DuckValue;

conn.execute_batch("CREATE TABLE items (id INTEGER, name VARCHAR)")?;

let mut app = conn.appender("items", "main")?;    // (table, schema)
app.append(&mut DuckValue::Int(1))?;               // row 1, column 0
app.append(&mut DuckValue::text("a"))?;            // row 1, column 1
app.append(&mut DuckValue::Int(2))?;               // row 2, column 0
app.append(&mut DuckValue::text("b"))?;            // row 2, column 1
app.save()?;                                        // flush buffered rows
```

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`](/core/values-overview) and the primitive Rust types implement
[`AppendAble`](/core/values-overview#the-conversion-traits). 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.

<Tip>
  For a struct-per-row path, derive [`#[derive(ToRow)]`](/macros/fromrow-torow#derive-torow) and
  append the whole struct at once:

  ```rust theme={null}
  use better_duck_core::ToRow;
  #[derive(ToRow)]
  struct NewItem { id: i32, name: String }

  app.append(&mut NewItem { id: 3, name: "c".into() })?;
  ```
</Tip>

## Flushing: save vs finish vs drop

| Call | Behavior |
| - | - |
| `save(&mut self) -> Result<()>` | Flush buffered rows; the appender stays usable. `#[must_use]`. |
| `finish(self) -> Result<()>` | Flush, close, and **report** any error; consumes the appender. `#[must_use]`. |
| drop | Best-effort flush; a failure is only **logged**, not returned. |

```rust theme={null}
app.append(&mut DuckValue::Int(4))?;
app.finish()?;                                      // surfaces a flush/close error
```

<Warning>
  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).
</Warning>

## 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`](/core/opening-connections#cloning-a-connection)) can read the
  rows after `save()`.
* [`Connection::close`](/core/opening-connections#closing) errors while an appender from that
  connection is still alive; the connection closes once the last appender drops.

## See also

* [Appender advanced](/core/appender-advanced) — projected columns, catalog/query appenders, raw
  chunk ingestion, and poisoning semantics.
* [ToRow derive](/macros/fromrow-torow#derive-torow) — append a struct as a row.
* [Data chunks](/core/data-chunks) — build a chunk and bulk-append it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.