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.

Opening one connection per thread or per request is straightforward, but it comes with overhead: each call to Connection::open(path) opens a new database file handle, and each call to Connection::open_in_memory() creates an entirely isolated database. The pool feature addresses both of these costs. It provides an r2d2-backed connection pool where every slot in the pool shares one underlying Database — so connections are created quickly (a single duckdb_connect call), and for in-memory databases, all connections observe the same data.

Enabling the pool Feature

[dependencies]
better-duck-core = { version = "0.1.0-beta.4", features = ["pool"] }
This adds r2d2 as a dependency. The DuckDbConnectionManager, Pool, and related type aliases are available under better_duck_core::pool.

Creating a Pool

1

Create a DuckDbConnectionManager

The manager holds one Database handle. All connections it creates are derived from that handle via db.connect().
use better_duck_core::pool::DuckDbConnectionManager;

// In-memory pool (all connections share one database)
let manager = DuckDbConnectionManager::memory()?;

// File-based pool
let manager = DuckDbConnectionManager::file("data.duckdb")?;

// File-based with extra DuckDB config
use better_duck_core::config::Config;
let config = Config::default().with("threads", "4")?;
let manager = DuckDbConnectionManager::file_with_flags("data.duckdb", config)?;
2

Build the pool with r2d2

Pass the manager to Pool::builder() and configure the pool size, timeout, and other r2d2 settings before calling .build():
use better_duck_core::pool::Pool;

let pool = Pool::builder()
    .max_size(8)
    .build(manager)?;
3

Check out connections

Call pool.get() to check out a PooledConnection. The checkout blocks until a connection is available (or the configured timeout elapses). The connection is returned to the pool automatically when the PooledConnection is dropped.
let mut conn = pool.get()?;
conn.execute_batch("SELECT 1")?;
// conn is returned to the pool when it goes out of scope

DuckDbConnectionManager Constructors

ConstructorDescription
DuckDbConnectionManager::memory() -> Result<DuckDbConnectionManager>Shared in-memory database
DuckDbConnectionManager::memory_with_flags(config) -> Result<DuckDbConnectionManager>Shared in-memory with DuckDB config
DuckDbConnectionManager::file(path) -> Result<DuckDbConnectionManager>File-based database
DuckDbConnectionManager::file_with_flags(path, config) -> Result<DuckDbConnectionManager>File-based with DuckDB config
DuckDbConnectionManager::new(database) -> DuckDbConnectionManagerWrap an already-open Database
You can also retrieve the underlying Database from an existing manager with manager.database().

Checking Out Connections

pool.get() returns a PooledConnection — a smart pointer that derefs to Connection. Use it exactly like a regular Connection and drop it when done to return the slot to the pool:
use better_duck_core::pool::{DuckDbConnectionManager, Pool};

fn main() -> better_duck_core::error::Result<()> {
    let manager = DuckDbConnectionManager::memory()?;
    let pool = Pool::builder().max_size(4).build(manager)?;

    // Check out and use the first connection
    let mut conn = pool.get()?;
    conn.execute_batch("CREATE TABLE t (id INTEGER)")?;
    drop(conn); // return to pool

    // Check out a second connection — it sees the table created above
    let mut conn2 = pool.get()?;
    conn2.execute_batch("INSERT INTO t VALUES (1)")?;
    let n = conn2.execute("SELECT count(*) AS c FROM t")?.changes();
    println!("rows: {}", n);

    Ok(())
}
All connections checked out of the same pool share one underlying Database. For in-memory pools this means they observe consistent data — creating a table on one checkout is immediately visible to the next checkout. This is the key difference from opening N independent connections.

Full Example

use better_duck_core::pool::{DuckDbConnectionManager, Pool};
use better_duck_core::types::value::DuckValue;

fn main() -> better_duck_core::error::Result<()> {
    // Build an 8-connection in-memory pool
    let manager = DuckDbConnectionManager::memory()?;
    let pool = Pool::builder().max_size(8).build(manager)?;

    // Schema setup
    {
        let mut conn = pool.get()?;
        conn.execute_batch("CREATE TABLE events (id INTEGER, label TEXT)")?;
    }

    // Insert via one connection
    {
        let mut conn = pool.get()?;
        conn.execute_batch("INSERT INTO events VALUES (1, 'alpha'), (2, 'beta')")?;
    }

    // Query via another connection
    {
        let mut conn = pool.get()?;
        let mut threshold = DuckValue::Int(1);
        let mut result = conn.execute_with(
            "SELECT id, label FROM events WHERE id > $1",
            &mut [&mut threshold],
        )?;
        for row in result {
            let row = row?;
            println!("{:?}  {:?}", row.get("id"), row.get("label"));
        }
    }

    Ok(())
}

Pool Types

TypeDescription
Poolr2d2::Pool<DuckDbConnectionManager> — the pool itself
PooledConnectionr2d2::PooledConnection<DuckDbConnectionManager> — a checked-out connection
PoolBuilderr2d2::Builder — builder returned by Pool::builder() (re-exported as-is; parameterized by DuckDbConnectionManager at use-site)
PoolErrorr2d2::Error — error type from checkout operations
PoolStater2d2::State — idle/active connection counts
All are re-exported from better_duck_core::pool so you don’t need to depend on r2d2 directly.

r2d2 Pool Validation

The DuckDbConnectionManager implements r2d2::ManageConnection with two validation hooks:
  • is_valid — runs SELECT 1 on a connection before handing it to the caller. Connections that fail this check are discarded and replaced.
  • has_broken — returns true if conn.is_open() is false. Broken connections are immediately evicted.
These hooks run transparently; you never need to call them yourself.

Using with AsyncPool

When both async and pool features are enabled, AsyncPool wraps an r2d2 Pool and provides async methods. Import AsyncPool from better_duck_core:
[dependencies]
better-duck-core = { version = "0.1.0-beta.4", features = ["async", "pool"] }
use better_duck_core::{AsyncPool, pool::{DuckDbConnectionManager, Pool}};

#[tokio::main]
async fn main() -> better_duck_core::error::Result<()> {
    let manager = DuckDbConnectionManager::memory()?;
    let pool = Pool::builder().max_size(4).build(manager)?;
    let async_pool = AsyncPool::new(pool);

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

    // Fan out concurrent inserts
    let mut handles = Vec::new();
    for i in 0..16i32 {
        let p = async_pool.clone();
        handles.push(tokio::spawn(async move {
            p.execute_batch(format!("INSERT INTO t VALUES ({i})")).await.unwrap();
        }));
    }
    for h in handles {
        h.await.unwrap();
    }

    let result = async_pool.execute("SELECT count(*) AS c FROM t").await?;
    println!("{:?}", result.rows()[0].get("c")); // BigInt(16)
    Ok(())
}
AsyncPool::with() is the correct place for transactions — the closure acquires a checkout and runs entirely on a blocking thread before the await resolves, so the connection is never held across an .await point:
async_pool.with(|conn| {
    conn.execute_batch("BEGIN")?;
    conn.execute_batch("INSERT INTO t VALUES (99)")?;
    conn.execute_batch("COMMIT")?;
    Ok(())
}).await?;
For the full async API, see the Async guide.

Build docs developers (and LLMs) love