⌘K ctrl+k
Search Shortcut cmd + k | ctrl + k
Import Data

Overview

The Rust client offers two ways to get data into DuckDB in bulk: the high-performance Appender for inserting rows from Rust, and reading directly from data files such as Parquet, CSV, and JSON. Both are described below.

Appender

The Appender is the fastest way to insert rows generated in Rust. Create one from a Connection with appender(), passing the target table name, then push rows with append_row(). Each row is a parameter list built with the params! macro:

use duckdb::{params, Connection, Result};

fn insert_rows(conn: &Connection) -> Result<()> {
    conn.execute_batch("CREATE TABLE foo (a INTEGER, b INTEGER)")?;

    let mut app = conn.appender("foo")?;
    app.append_row(params![1, 2])?;
    app.append_row(params![3, 4])?;
    app.flush()?;
    Ok(())
}

To append many rows from an iterator of parameter lists in one call, use append_rows():

app.append_rows([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]])?;

Additional constructors target tables outside the default schema: appender_to_db() takes a schema name, and appender_to_catalog_and_db() takes a catalog and a schema. To append only a subset of columns, use appender_with_columns() (and its schema and catalog variants).

Warning DuckDB buffers appended rows and checks constraints such as NOT NULL and foreign keys only when the buffer is flushed, so append_row() may return Ok for a row that later fails a constraint check. Always call flush() explicitly and check its result before the appender is dropped. Dropping an appender flushes any remaining rows, but errors raised during that implicit flush are discarded, because Rust's Drop cannot report them.

The Appender is Sync but not Send, because it borrows the Connection. To append from another thread, move the Connection to that thread and create the appender there.

Appending Inside a Transaction

For a large load, wrapping the appends in a transaction lets DuckDB commit them as a single unit. Begin a transaction with Connection::transaction(), set it to commit on drop, and create the appender from the transaction. The following bulk-inserts ten million rows, adapted from the crate's appender example:

use duckdb::{params, Connection, DropBehavior, Result};

fn main() -> Result<()> {
    let mut db = Connection::open_in_memory()?;
    db.execute_batch(
        "CREATE TABLE test (id INTEGER NOT NULL, area CHAR(6), age TINYINT NOT NULL, active TINYINT NOT NULL)",
    )?;

    let row_count = 10_000_000;
    {
        let mut tx = db.transaction()?;
        tx.set_drop_behavior(DropBehavior::Commit);
        let mut app = tx.appender("test")?;
        for i in 0..row_count {
            app.append_row(params![i, "123456", 15, 1])?;
        }
    } // the appender flushes and the transaction commits here, on drop

    let count = db.query_row("SELECT count(*) FROM test", [], |row| row.get::<_, i64>(0))?;
    assert_eq!(count, row_count);
    Ok(())
}

A Transaction rolls back by default when dropped. Call set_drop_behavior(DropBehavior::Commit) to commit on drop instead, or call tx.commit() explicitly. The other DropBehavior variants are Rollback (the default), Ignore (leave the transaction open), and Panic.

Appending Arrow Data

With the appender-arrow feature enabled, an Appender can append an Apache Arrow RecordBatch directly, which avoids building a parameter list per row. See Handle Results for the Arrow integration.

Reading Data Files

DuckDB can read many file formats directly in SQL, which is often the simplest way to load external data. The Rust client just sends the query. For Parquet, enable the parquet feature and query read_parquet(), adapted from the crate's parquet example:

use duckdb::{Connection, Result};
use duckdb::arrow::record_batch::RecordBatch;

let conn = Connection::open_in_memory()?;
conn.execute_batch("INSTALL parquet; LOAD parquet;")?;

let batches: Vec<RecordBatch> = conn
    .prepare("SELECT * FROM read_parquet(?)")?
    .query_arrow(["data.parquet"])?
    .collect();

The same pattern reads CSV with read_csv() and JSON with read_json() (the latter needs the json feature). To load a file into a table rather than query it in place, wrap the reader in a CREATE TABLE ... AS SELECT or COPY statement sent with execute().

Further Reading

  • Appender — the engine-level Appender interface that the Rust Appender wraps.
  • Data Import — DuckDB's full set of bulk-loading options.
  • Handle Results — the Apache Arrow integration used to append and read record batches.
  • Run Queries — sending the CREATE TABLE and COPY statements the file readers build on.
© 2026 DuckDB Foundation, Amsterdam NL
DuckDB Home Code of Conduct Trademark Use Blog