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.

By default, every call to Connection::open_in_memory() creates a completely separate DuckDB database. Two connections opened this way cannot see each other’s tables or data — they are fully isolated. When you need multiple connections to observe the same data (including in-memory data), you need the Database handle: open one Database, then call db.connect() to derive as many connections as you need. All of them share the single underlying DuckDB database instance.

The Database Handle

Database is a thin, cheap Arc-backed wrapper around the raw DuckDB database handle (duckdb_database). Cloning it is an atomic reference-count bump; no new DuckDB objects are created. The underlying database is destroyed only when the last Arc referencing it is dropped — which means it outlives individual connections, and connections can be dropped and reopened freely without losing data.

Database::open(path)

Open or create an on-disk DuckDB database

Database::open_in_memory()

Create an in-memory database shared by all connections derived from it

db.connect()

Spawn a new Connection attached to this database

Method signatures

MethodDescription
Database::open<P: AsRef<Path>>(path: P) -> Result<Database>Open (or create) an on-disk database
Database::open_with_flags<P: AsRef<Path>>(path: P, config: Config) -> Result<Database>Same, with extra DuckDB config
Database::open_in_memory() -> Result<Database>Create a shared in-memory database
Database::open_in_memory_with_flags(config: Config) -> Result<Database>Same, with extra DuckDB config
db.connect() -> Result<Connection>Spawn a new connection to this database

Shared In-Memory Database

Open one Database and call db.connect() twice — both connections see the same tables and rows:
use better_duck_core::database::Database;

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

    let mut a = db.connect()?;
    let mut b = db.connect()?;

    // `a` creates a table
    a.execute_batch("CREATE TABLE t (id INTEGER)")?;

    // `b` sees the table `a` created and inserts into it
    b.execute_batch("INSERT INTO t VALUES (1)")?;

    // `a` reads the row `b` inserted
    let mut result = a.execute("SELECT count(*) AS c FROM t")?;
    let row = result.next().unwrap()?;
    println!("{:?}", row.get("c")); // Some(BigInt(1))

    Ok(())
}
All connections derived from the same Database — including clones of the Database handle itself — share one consistent view of the data. This applies whether the database is in-memory or file-based.

Independent In-Memory Connections

For contrast, opening two connections with Connection::open_in_memory() gives each its own isolated database:
use better_duck_core::connection::Connection;

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

    x.execute_batch("CREATE TABLE t (id INTEGER)")?;

    // `y` has its own database; `t` does not exist there
    let result = y.execute_batch("INSERT INTO t VALUES (1)");
    assert!(result.is_err(), "y cannot see x's table");

    Ok(())
}
Use Connection::open_in_memory() when you want complete isolation — for example, in a test suite where each test should start from a clean slate. Use Database::open_in_memory() when multiple actors need to share state.

Getting a Database from a Connection

You don’t always start from a Database. If you already have a Connection, call conn.database() to retrieve the Arc-backed Database handle. From there, use db.connect() to spawn sibling connections that share the same underlying database.
use better_duck_core::connection::Connection;

fn main() -> better_duck_core::error::Result<()> {
    // Start with a plain connection
    let conn = Connection::open_in_memory()?;

    // Retrieve the shared database handle (Arc bump — no DuckDB call)
    let db = conn.database();

    // Spawn siblings that share the same database
    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 (99)")?;

    // The original `conn` also sees the change
    let mut result = conn.database().connect()?.execute("SELECT id FROM t")?;
    let row = result.next().unwrap()?;
    println!("{:?}", row.get("id")); // Some(Int(99))

    Ok(())
}
MethodDescription
conn.database() -> DatabaseReturn the Arc-backed handle for the database backing this connection

Connection Lifetime vs. Database Lifetime

Because Database is Arc-backed, the underlying DuckDB database instance lives as long as there is at least one Database handle or Connection referencing it — whichever outlives the other. You can safely drop the Database you used to open the database while connections derived from it are still in use:
use better_duck_core::database::Database;

let mut conn = {
    let db = Database::open_in_memory()?;
    db.connect()? // conn holds an Arc to the database internally
}; // `db` is dropped here, but the underlying database lives on through `conn`

// Safe: `conn` still works because it keeps the Arc alive
conn.execute_batch("CREATE TABLE t (id INTEGER)")?;

Thread Safety

Database is Send + Sync. You can clone a Database handle and share the clone across threads, or move it into a tokio::spawn task, without any additional synchronization:
use better_duck_core::database::Database;
use std::thread;

let db = Database::open_in_memory()?;
db.connect()?.execute_batch("CREATE TABLE t (id INTEGER)")?;

let db_clone = db.clone(); // cheap Arc bump
let handle = thread::spawn(move || {
    let mut conn = db_clone.connect().unwrap();
    conn.execute_batch("INSERT INTO t VALUES (1)").unwrap();
});

handle.join().unwrap();
Connection is Send but not Sync. A single Connection may not be shared across threads concurrently — each thread should hold its own connection derived from the shared Database. For async workloads, see the Async and Pooling guides.

Build docs developers (and LLMs) love