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.

The Connection type is the primary entry point to better-duck. It wraps a RawConnection — which in turn owns a single DuckDB C-level connection — and exposes a fully safe, ergonomic API for opening databases, executing SQL, creating bulk appenders, and spawning sibling connections. Every FFI call is encapsulated behind the safe wrapper; no unsafe code ever surfaces in user code.

Opening a Connection

better-duck provides four constructor variants covering the two most common scenarios: in-memory (great for tests and one-shot scripts) and file-based (persistent on-disk databases), each with an optional Config argument for fine-grained DuckDB settings.
use better_duck_core::connection::Connection;

let mut conn = Connection::open_in_memory()?;
Each call to open_in_memory() creates a completely independent in-memory database. If you need multiple connections that see the same data, use Database::open_in_memory() instead.

Method signatures

MethodDescription
Connection::open_in_memory() -> Result<Connection>Open a standalone in-memory database
Connection::open_in_memory_with_flags(config: Config) -> Result<Connection>Same, with extra DuckDB config
Connection::open<P: AsRef<Path>>(path: P) -> Result<Connection>Open (or create) an on-disk database
Connection::open_with_flags<P: AsRef<Path>>(path: P, config: Config) -> Result<Connection>Same, with extra DuckDB config
All four constructors are annotated #[must_use = "connection should be used or explicitly dropped"]. Rust will warn you if the returned Connection is silently discarded — make sure to bind it to a variable or explicitly drop it when you are done.

Connection Lifecycle

A Connection is open from the moment it is created until either close() is called or it goes out of scope. The underlying DuckDB connection is destroyed in Drop, so resources are always reclaimed even if you never call close() explicitly.
use better_duck_core::connection::Connection;

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

    assert!(conn.is_open());

    // Explicit close — useful when you need to handle any close errors
    conn.close()?;

    assert!(!conn.is_open());
    Ok(())
}

Lifecycle methods

MethodDescription
conn.is_open() -> boolReturns true if the underlying DuckDB connection handle is non-null
conn.close() -> Result<()>Closes the connection explicitly; also called automatically on drop
close() is annotated #[must_use = "close result should be checked"]. Always propagate or handle the Result it returns; ignoring it silently discards any error that occurred during shutdown.

Cloning a Connection

try_clone() opens a second, independent connection to the same underlying database as cheaply as a single duckdb_connect FFI call — no file I/O. Both connections share the same data (including in-memory databases), and each can execute queries independently.
use better_duck_core::connection::Connection;

fn main() -> better_duck_core::error::Result<()> {
    let mut a = Connection::open_in_memory()?;
    a.execute_batch("CREATE TABLE t (id INTEGER)")?;

    let mut b = a.try_clone()?;
    b.execute_batch("INSERT INTO t VALUES (1)")?;

    // `a` sees the row inserted by `b`
    let mut result = a.execute("SELECT count(*) AS c FROM t")?;
    let row = result.next().unwrap()?;
    println!("{:?}", row.get("c")); // Some(BigInt(1))
    Ok(())
}
MethodDescription
conn.try_clone() -> Result<Connection>Open a second connection to the same database

Accessing the Database Handle

Every Connection keeps an Arc-backed reference to its underlying Database. Calling conn.database() returns that handle as a cheap Arc bump — no new DuckDB objects are created. Once you have the Database handle, you can call db.connect() to spawn as many additional connections as you need, all sharing the same underlying data.
use better_duck_core::connection::Connection;

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

    // Retrieve the shared database handle
    let db = conn.database();

    // Spawn further connections from it
    let mut sibling_a = db.connect()?;
    let mut sibling_b = db.connect()?;

    sibling_a.execute_batch("CREATE TABLE t (id INTEGER)")?;
    sibling_b.execute_batch("INSERT INTO t VALUES (42)")?;

    // The original `conn`, `sibling_a`, and `sibling_b` all see the same data
    Ok(())
}
MethodDescription
conn.database() -> DatabaseReturn the Arc-backed Database handle for this connection
For full details on the Database type, shared in-memory databases, and thread safety, see the Database Sharing guide.

Execute Methods

Once you have a connection you can run SQL immediately. The three core execute methods are:
MethodBest for
conn.execute_batch(sql) -> Result<()>DDL and fire-and-forget DML; result discarded
conn.execute(sql) -> Result<DuckResult>Any statement where you want rows or change counts
conn.execute_with(sql, &mut [...]) -> Result<DuckResult>Parameterized queries with positional $1, $2, … binds
See the Querying guide for full examples and row-reading patterns.

Build docs developers (and LLMs) love