Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nimdeveloper/better-duck/llms.txt

Use this file to discover all available pages before exploring further.

better-duck-core gives you a direct, no-ORM DuckDB connection with a straightforward API: open a database, execute SQL, and iterate typed rows — all in safe Rust with no Arrow dependency and no system library required. This guide walks you through the five essential steps from adding the crate to your project all the way to running a parameterized query, using real working code you can paste straight into main.rs.
1

Add the dependency to Cargo.toml

Open your project’s Cargo.toml and add better-duck-core under [dependencies]:
[dependencies]
better-duck-core = "0.1.0-beta.4"
If you also want the Diesel ORM backend, add both crates:
[dependencies]
better-duck-core   = "0.1.0-beta.4"
better-duck-diesel = "0.1.0-beta.4"
Cargo’s default SemVer range (e.g. "0.1") excludes pre-release versions like -beta.4. You must pin the exact pre-release string as shown above, or use the CLI:
cargo add better-duck-core --version 0.1.0-beta.4
The first build will compile DuckDB from the vendored source archive inside better-duck-sys. This takes a few minutes on a cold cache; subsequent builds use Cargo’s incremental cache and are fast.
2

Open a connection

Connection is the entry point for all database access. You can open an in-memory database (perfect for tests and one-shot scripts) or a persistent on-disk file:
use better_duck_core::connection::Connection;

// In-memory — a fresh, independent database, gone when the connection is dropped.
let mut conn = Connection::open_in_memory()?;

// On-disk — creates or reopens a DuckDB file at the given path.
let mut conn = Connection::open("my_database.duckdb")?;
Both methods return better_duck_core::error::Result<Connection>, so they compose naturally with the ? operator.
3

Create tables and insert data with execute_batch

execute_batch runs one or more semicolon-separated SQL statements and discards any result sets. It is ideal for DDL and multi-statement setup scripts:
conn.execute_batch(
    "CREATE TABLE events (id INTEGER, label TEXT, score DOUBLE);
     INSERT INTO events VALUES (1, 'alpha', 9.5), (2, 'beta', 7.2);",
)?;
4

Execute a SELECT and iterate rows

execute returns a DuckResult, which implements Iterator<Item = Result<DuckRow>>. Call row.get(column_name) on each row to retrieve a DuckValue for any column by name:
use better_duck_core::{connection::Connection, types::value::DuckValue};

fn main() -> better_duck_core::error::Result<()> {
    let mut conn = Connection::open_in_memory()?;

    conn.execute_batch(
        "CREATE TABLE events (id INTEGER, label TEXT, score DOUBLE);
         INSERT INTO events VALUES (1, 'alpha', 9.5), (2, 'beta', 7.2);",
    )?;

    let mut result = conn.execute("SELECT id, label, score FROM events ORDER BY id")?;
    for row in result {
        let row = row?;
        println!(
            "id={:?}  label={:?}  score={:?}",
            row.get("id"),
            row.get("label"),
            row.get("score"),
        );
    }
    Ok(())
}
Each call to row.get(name) returns Option<&DuckValue>None if no column with that name exists, or Some(&value) otherwise. You can match on the inner variant to extract the underlying Rust type:
use better_duck_core::types::value::DuckValue;

match value {
    DuckValue::Int(n)    => println!("integer: {n}"),
    DuckValue::Text(s)   => println!("text: {s}"),
    DuckValue::Double(f) => println!("float: {f}"),
    DuckValue::Null      => println!("null"),
    _ => println!("other: {value:?}"),
}
DuckValue is #[non_exhaustive] — always include a _ arm to stay forward-compatible as new DuckDB types are added.
5

Run a parameterized query with execute_with

For queries with runtime values, use execute_with. Parameters are positional ($1, $2, …) and passed as a mutable slice of &mut dyn AppendAble references. Any type that implements AppendAble — including all DuckValue variants and Rust primitives — can be used as a bind parameter:
use better_duck_core::types::value::DuckValue;

let mut threshold = DuckValue::Double(8.0);
let mut rows = conn.execute_with(
    "SELECT id, label FROM events WHERE score > $1",
    &mut [&mut threshold],
)?;
for row in rows {
    let row = row?;
    println!("{:?}", row);
}
This prints only the alpha row (score 9.5), since beta (7.2) is below the threshold.
Ready to go deeper? Explore these topics next:
  • Installation — feature flags, optional dependencies, and build requirements in full detail.
  • Connections — shared databases, Database::connect() for multi-connection access, and the async AsyncConnection facade.
  • Type System — the complete DuckValue hierarchy, AppendAble trait, and how DuckDB types map to Rust types.
  • Bulk Appender — streaming thousands of rows with Connection::appender for high-throughput inserts.

Build docs developers (and LLMs) love