duckdb-rs is an ergonomic Rust wrapper for DuckDB.
You can use it to:
- Query DuckDB with type-safe bindings and an API inspired by rusqlite.
- Read and write Arrow, Parquet, JSON, and CSV formats natively.
- Create DuckDB extensions in Rust with custom scalar and table functions.
Create a new project and add the duckdb crate:
cargo new quack-in-rust
cd quack-in-rust
cargo add duckdb -F bundledUpdate src/main.rs with the following code:
use duckdb::{params, Connection, Result};
struct Duck {
id: i32,
name: String,
}
fn main() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute(
"CREATE TABLE ducks (id INTEGER PRIMARY KEY, name TEXT)",
[], // empty list of parameters
)?;
conn.execute_batch(
r#"
INSERT INTO ducks (id, name) VALUES (1, 'Donald Duck');
INSERT INTO ducks (id, name) VALUES (2, 'Scrooge McDuck');
"#,
)?;
conn.execute(
"INSERT INTO ducks (id, name) VALUES (?, ?)",
params![3, "Darkwing Duck"],
)?;
let ducks = conn
.prepare("FROM ducks")?
.query_map([], |row| {
Ok(Duck {
id: row.get(0)?,
name: row.get(1)?,
})
})?
.collect::<Result<Vec<_>>>()?;
for duck in ducks {
println!("{}) {}", duck.id, duck.name);
}
Ok(())
}Execute the program with cargo run and watch DuckDB in action!
⚠ DuckDB's v2 C API is in rapid development. Consider
duckdb-neounstable. Feedback is welcome!
duckdb-neo is a second wrapper crate in this repository that targets DuckDB's new v2 C API (duckdb_v2.h) instead of the v1 API used by duckdb. Both crates share the same libduckdb-sys crate and therefore the same bundled DuckDB, build options, and version number.
cargo add duckdb-neouse duckdb_neo::{
Parameters,
environment::{Environment, StorageLocation},
};
fn main() -> duckdb_neo::Result<()> {
let environment = Environment::new()?;
let database = environment.open(StorageLocation::InMemory)?;
let connection = database.connect()?;
let mut result = connection.query("SELECT $1::INTEGER", Parameters::positional(&[&42]))?;
let chunk = result.next().transpose()?.expect("query returned no rows");
let values = chunk.get_vector_at::<i32>(0)?;
assert_eq!(values.get(0)?, Some(&42));
Ok(())
}libduckdb-sys exposes the v1 bindings as libduckdb_sys::v1 (also re-exported at the crate root) behind the default capi-v1 feature and, behind its capi-v2 feature, the v2 bindings as libduckdb_sys::v2. duckdb-neo disables the default features so only the v2 bindings are generated. duckdb-neo enables that feature itself. Its own capi-v2-p2 feature targets API surface that the bundled DuckDB does not provide yet and does not compile.
The following examples demonstrate various features and use cases of duckdb-rs:
- basic - Basic usage including creating tables, inserting data, and querying with and without Arrow.
- appender - Bulk data insertion using the appender API with transactions.
- arrow_vtab - Querying an in-memory Arrow RecordBatch from DuckDB SQL (requires the
vtab-arrowfeature). - parquet - Reading Parquet files directly using DuckDB's Parquet extension.
- vscalar - Defining and registering a custom scalar function (requires the
vscalarfeature). - vtab - Defining a custom table function returning primitive, LIST, and STRUCT columns (requires the
vtabfeature). - repl - Interactive SQL REPL.
- hello-ext - A DuckDB extension written in Rust (see Building and loading extensions).
Run an executable example using your configured DuckDB library:
cargo run --example basicPass each example's required feature when applicable:
cargo run --example arrow_vtab --features vtab-arrow
cargo run --example vscalar --features vscalar
cargo run --example vtab --features vtab
cargo run --example parquet --features parquetWithout bundled, the build links against the pinned prebuilt DuckDB it downloads (or a system library, see Notes on building). To compile DuckDB from source instead, add bundled to the feature list. For example:
cargo run --example basic --features bundled
cargo run --example arrow_vtab --features "bundled vtab-arrow"hello-ext is a library target rather than an executable, so build it instead of using cargo run:
cargo build --example hello-ext --features loadable-extensionThe duckdb crate provides a number of Cargo features that can be enabled to add functionality:
vtab- Base support for creating custom table functions and virtual tables.vtab-arrow- Apache Arrow integration for virtual tables. Enables conversion between Arrow RecordBatch and DuckDB data chunks.vtab-excel- Deprecated no-op retained for feature compatibility.vscalar- Create custom scalar functions that operate on individual values or rows.vscalar-arrow- Arrow-optimized scalar functions for vectorized operations. Requiresvscalar.
json- Enables reading and writing JSON files. Impliesbundled.parquet- Enables reading and writing Parquet files. Impliesbundled.
These extensions are only available through the CMake build backend and imply bundled-cmake.
autocomplete- DuckDB's autocomplete extension.httpfs- Statically links DuckDB's HTTP(S) and S3 filesystem extension (Linux and macOS only).icu- DuckDB's ICU extension for locale-aware operations.tpch- DuckDB's TPC-H benchmark extension.tpcds- DuckDB's TPC-DS benchmark extension.
appender-arrow- Efficient bulk insertion of Arrow data into DuckDB tables.polars- Integration with Polars DataFrames.rust_decimal- Compatibility impls forrust_decimal::Decimal. DuckDBDECIMALvalues useduckdb::types::Decimalas the core full-domain carrier; this feature restoresToSql/FromSqlconversions forrust_decimal::Decimalwhen values fit that crate's decimal domain, including compatibility reads fromFLOAT,DOUBLE, and text columns.
vtab-full- Enables virtual table features:vtab-arrowandappender-arrow; retains the deprecatedvtab-excelcompatibility flag.extensions-full- Enables all major extensions:json,parquet, andvtab-full.modern-full- Enables modern Rust ecosystem integrations:chrono,serde_json,url,r2d2,uuid,polars, andrust_decimal.
bundled- Uses a bundled version of DuckDB's source code and compiles it during build. This is the simplest way to get started and avoids needing DuckDB system libraries.bundled-cmake- Experimental. Builds DuckDB via its upstream CMake build system instead ofcc. Requires a duckdb-rs checkout (not available from crates.io). See step 2 below for details.buildtime_bindgen- Use bindgen at build time to generate fresh bindings instead of using pre-generated ones.capi-v1(libduckdb-sysonly, default) - Generate bindings for the v1 C API headerduckdb.h, exposed aslibduckdb_sys::v1and at the crate root. Required byduckdband byloadable-extension.capi-v2(libduckdb-sysonly) - Also generate bindings for the v2 C API headerduckdb_v2.h, exposed aslibduckdb_sys::v2.duckdb-neoenables this automatically. Withbuildtime_bindgen,duckdb_v2.hmust sit next toduckdb.hinDUCKDB_INCLUDE_DIR.loadable-extension- Experimental support for creating loadable DuckDB extensions. Includes procedural macros for extension development. Only enable this when building an extension, never for a client application - see Database client or extension?
Starting with DuckDB v1.5.0, the duckdb-rs version encodes the DuckDB version in its second semver component.
The format is 1.MAJOR_MINOR_PATCH.x, e.g., DuckDB v1.5.0 maps to duckdb-rs 1.10500.x.
Use a tilde requirement to receive duckdb-rs patch releases without automatically moving to a different bundled DuckDB version.
The recommended way to use duckdb-rs is to add it from crates.io:
cargo add duckdb -F bundledOr manually add it to your Cargo.toml:
[dependencies]
duckdb = { version = "~1.20000.0", features = ["bundled"] }To use the latest development version from the main branch, you can specify a git dependency in your Cargo.toml:
# Use a specific branch
duckdb = { git = "https://github.com/duckdb/duckdb-rs", branch = "main", features = ["bundled"] }
# Use a specific commit
duckdb = { git = "https://github.com/duckdb/duckdb-rs", rev = "abc123def", features = ["bundled"] }Note: Using the main branch of duckdb-rs means you'll get the latest Rust bindings and features, but you'll still be using whatever version of DuckDB core is bundled with that commit (when using the bundled feature).
Use the v1.4-andium branch for LTS releases that stay on DuckDB 1.4 Andium.
libduckdb-sys is a separate crate from duckdb-rs that provides the Rust
declarations for DuckDB's C API. By default, libduckdb-sys attempts to find a DuckDB library that already exists on your system using pkg-config, or a
Vcpkg installation for MSVC ABI builds.
You can adjust this behavior in a number of ways:
-
If you use the
bundledfeature,libduckdb-syswill use the cc crate to compile DuckDB from source and link against that. This source is embedded in thelibduckdb-syscrate and tracks the DuckDB version vendored for that duckdb-rs release. This is probably the simplest solution to any build problems. You can enable this by adding the following in yourCargo.tomlfile:cargo add duckdb --features bundled
Cargo.tomlwill be updated.[dependencies] duckdb = { version = "~1.20000.0", features = ["bundled"] }
-
If you use the
bundled-cmakefeature,libduckdb-syswill build DuckDB from the local checkout incrates/libduckdb-sys/duckdb-sourcesusing upstream CMake. This keeps plainbundledunchanged while allowing CMake-only extensions such ashttpfsandicu.Example:
[dependencies] duckdb = { git = "https://github.com/duckdb/duckdb-rs", branch = "main", features = ["bundled-cmake", "icu"] }
Notes:
bundled-cmakeis experimental and requires a git/workspace checkout. Published crates omit the fullduckdb-sourcestree.- It implies
bundledfor conditional-compilation gates, but uses CMake instead of theccbackend. Any CMake-only extension feature (e.g.icu) enables it automatically. - It always links DuckDB's default static extensions (
core_functionsandparquet) and therefore implies theparquetCargo feature. - Plain
bundleduses DuckDB's standard allocator and skips jemalloc.bundled-cmakeenables upstream jemalloc on supported 64-bit, non-musl Linux targets. Other targets follow DuckDB's CMake checks. SetDUCKDB_DISABLE_JEMALLOC=1to force the standard allocator. - Extension autoload/autoinstall are enabled to match
bundled, despite upstream CMake defaults. SetDUCKDB_DISABLE_EXTENSION_LOAD=1orDISABLE_EXTENSION_LOAD=1to disable external extension install/load support and force autoload/autoinstall off. Statically linked extensions remain available. - If
ninjais onPATH, the build uses Ninja by default. SetCMAKE_GENERATORto override. - DuckDB builds in
Releasemode by default, even for Rust debug builds, avoiding DuckDB's much slower debug/sanitizer profile. Override withDUCKDB_CMAKE_BUILD_TYPEorCMAKE_BUILD_TYPE.DUCKDB_CMAKE_BUILD_TYPEtakes precedence. httpfsrequires CMake, a C++ compiler, pkg-config, and the libcurl and OpenSSL development libraries. On Debian/Ubuntu, installcmake pkg-config libcurl4-openssl-dev libssl-dev; on macOS, installcmake pkg-config openssl@3with Homebrew (the macOS SDK provides libcurl). OpenSSL must be discoverable through pkg-config; for a custom installation, setPKG_CONFIG_PATHto itslib/pkgconfigdirectory. Network access is required at CMake configure time to fetch the HTTPFS extension sources. Windows is currently unsupported forhttpfs. Once built,LOAD httpfsneeds no downloaded extension artifact, including when external extension loading is disabled.DUCKDB_EXTENSION_CONFIGSis unsupported. Setting it fails fast.- Use
cargo build -vv -F bundled-cmakefor CMake configure/build logs. - On Windows (MSVC), keep the Cargo target directory short (e.g.
CARGO_TARGET_DIR=D:/t). DuckDB's unity build includes sources by a path relative to the build directory, and deep paths exceed MSVC's 260-character limit withC1083. Alternatively setDUCKDB_DISABLE_UNITY=1.
-
When linking against a DuckDB library already on the system (so not using any of the
bundledfeatures), you can set theDUCKDB_LIB_DIRenvironment variable to point to a directory containing the library. You can also set theDUCKDB_INCLUDE_DIRvariable to point to the directory containingduckdb.h(andduckdb_v2.hwhen usingcapi-v2withbuildtime_bindgen).The examples below fetch the same prebuilt build the crate pins in
crates/libduckdb-sys/.duckdb-release(see step 4). Once DuckDB publishes the v2.0.0 release, itsduckdb-shared-libs-<platform>.tar.gzassets on GitHub Releases work the same way. Each archive is a flat tarball with the shared library and the public headers.Linux ARM64 example:
source crates/libduckdb-sys/.duckdb-release # sets DUCKDB_RELEASE_URL mkdir libduckdb curl -fL "$DUCKDB_RELEASE_URL/duckdb-shared-libs-linux-arm64.tar.gz" | tar -xz -C libduckdb export DUCKDB_LIB_DIR=$PWD/libduckdb export DUCKDB_INCLUDE_DIR=$DUCKDB_LIB_DIR export LD_LIBRARY_PATH=$DUCKDB_LIB_DIR cargo build --examples
For x86-64 Linux, replace
arm64withamd64in the archive name.macOS example:
source crates/libduckdb-sys/.duckdb-release # sets DUCKDB_RELEASE_URL mkdir libduckdb curl -fL "$DUCKDB_RELEASE_URL/duckdb-shared-libs-osx-universal.tar.gz" | tar -xz -C libduckdb export DUCKDB_LIB_DIR=$PWD/libduckdb export DUCKDB_INCLUDE_DIR=$DUCKDB_LIB_DIR export DYLD_FALLBACK_LIBRARY_PATH=$DUCKDB_LIB_DIR cargo build --examples
On Windows the
windows-amd64andwindows-arm64archives containduckdb.dlland itsduckdb.libimport library; put both inDUCKDB_LIB_DIRand add that directory toPATH. -
When none of the
bundledfeatures is enabled andDUCKDB_LIB_DIRis not set, the build script downloads pre-built DuckDB binaries and links against the dynamic library in the archive, effectively automating the manual steps above. Which build it downloads is pinned incrates/libduckdb-sys/.duckdb-release: aDUCKDB_RELEASE_URLdirectory holding theduckdb-shared-libs-<platform>.tar.gzarchives, currently a staging build of a v2.0.0 preview commit, plus aDUCKDB_RELEASE_VERSIONlabel and theDUCKDB_RELEASE_COMMITthose libraries were built from../upgrade.sh --syncvendors that same commit into the bundled sources, sobundledand the download always agree. Without a pin file the build falls back to the GitHub release matching the DuckDB version encoded in the crate version.Set
DUCKDB_DOWNLOAD_LIB=0to opt out and probe the system (vcpkg, pkg-config) instead, orDUCKDB_DOWNLOAD_LIB=1to force the download. LeaveDUCKDB_STATICunset: setting it requests a staticduckdb_staticlibrary, which the downloaded archives do not contain. The archives are cached intarget/duckdb-download/<target>/<version>and that directory is automatically added to the linker search path.cargo test # downloads the pinned build on first use DUCKDB_DOWNLOAD_LIB=0 cargo test # use a system DuckDB instead
The download needs the
download-libfeature, which pulls an HTTP client (ureq/rustls) into the build script. It is on by default; consumers building withbundledor a system DuckDB can drop it (and the HTTP client) withdefault-features = false, in which case the build probes the system unlessDUCKDB_DOWNLOAD_LIB=1is set explicitly.The pinned staging archives do not ship
duckdb_v2.hyet, sobuildtime_bindgentogether withcapi-v2needsDUCKDB_INCLUDE_DIRor abundledbuild until the pin moves to a newer DuckDB build. -
Installing the DuckDB development packages will usually be all that is required, but the build helpers for pkg-config and vcpkg have some additional configuration options.
vcpkg-rsuses static libraries by default. To opt into dynamic linking, set theVCPKGRS_DYNAMIC=1environment variable before building.
When none of the options above are used, the build script falls back to this discovery path and will emit the appropriate cargo:rustc-link-lib directives if DuckDB is found on your system.
Cross-compiling with bundled is best-effort (not covered by CI): it compiles DuckDB from C++ source and so needs a working C++ cross-compiler for the target, not just the Rust toolchain. If you can't get one working, prefer one of the non-bundled options above (e.g. DUCKDB_LIB_DIR or DUCKDB_DOWNLOAD_LIB=1) and link a pre-built library for the target instead.
When using the bundled feature, the ICU extension is not included due to crates.io's 10MB package size limit. This means some date/time operations (like now() - interval '1 day' or ts::date casts) will fail. You can load ICU at runtime:
conn.execute_batch("INSTALL icu; LOAD icu;")?;Alternatively, link against a system libduckdb that was compiled with ICU (see build instructions above).
If you are working from a duckdb-rs checkout, you can also use bundled-cmake,icu to compile ICU in through DuckDB's CMake build.
We use bindgen to generate the Rust
declarations from DuckDB's C header file. bindgen
recommends
running this as part of the build process for libraries that use it. We tried
this briefly (duckdb 0.10.0, specifically), but it had some annoyances:
- The build time for
libduckdb-sys(and thereforeduckdb) increased dramatically. - Running
bindgenrequires a relatively recent version of Clang, which many systems do not have installed by default. - Running
bindgenalso requires the DuckDB header file to be present.
So we try to avoid running bindgen at build time by shipping
pregenerated bindings for DuckDB.
If you use the bundled feature, you will get pregenerated bindings for the
bundled version of DuckDB. If you want to run bindgen at build time to
produce your own bindings, use the buildtime_bindgen Cargo feature.
Three pregenerated files ship with libduckdb-sys: the v1 bindings, the
v1 loadable-extension bindings, and (for capi-v2) the v2 bindings. The
upgrade.sh script regenerates all of them.
A Connection is Send but not Sync: you may move it between threads, but you cannot share a single connection across threads at the same time. DuckDB does support concurrent access through multiple connections to the same database (see DuckDB's concurrency docs.
For multi-threaded applications, give each thread its own connection. The simplest way is a connection pool via the r2d2 feature:
use duckdb::{DuckdbConnectionManager, params};
let manager = DuckdbConnectionManager::file("file.db")?;
let pool = r2d2::Pool::new(manager)?;
// Each worker checks out its own connection from the pool.
let conn = pool.get()?;
conn.execute("INSERT INTO foo (bar) VALUES (?)", params![1])?;duckdb-rs covers two distinct use cases, and you normally pick exactly one:
- As a database client (most common): embed DuckDB in your Rust application to run queries, as shown in the Quickstart.
- For writing DuckDB extensions: build a loadable
.duckdb_extensionthat DuckDB loads at runtime. Enable theloadable-extensionfeature.
Do not enable loadable-extension for a regular client application. It replaces DuckDB's normal C API functions with extension wrappers that are only initialized when DuckDB loads your extension. Outside an extension context, calls such as Connection::open_in_memory() then panic with DuckDB API not initialized or DuckDB feature omitted.
The hello-ext example shows the extension API in action: it registers a hello(name) table function via the loadable-extension feature and the duckdb_entrypoint_c_api macro.
cargo build alone does not produce a loadable extension though. DuckDB only loads files ending in .duckdb_extension that carry a metadata footer matching the target DuckDB version. A bare shared library fails with The file is not a DuckDB extension. The metadata at the end of the file is invalid. Appending that footer requires DuckDB's extension build tooling.
So rather than wiring this up by hand, start from the official extension-template-rs template for a complete, ready-to-build Rust extension. It handles the metadata step, platform/version detection, tests, and CI. Its make debug builds the shared library and then appends the footer, producing a .duckdb_extension you load with duckdb -unsigned.
duckdb-rs is built and tested with stable Rust, and keeps a rolling MSRV (minimum supported Rust version) that may only be updated when the encoded DuckDB major/minor version changes, on a need basis (e.g. project dependencies bump their MSRV or a particular Rust feature is useful for us). The new MSRV will be at least 6 months old. DuckDB patch updates and crate patch releases (e.g. 1.10500.0 to 1.10500.1) are guaranteed to keep the same MSRV.
We welcome contributions! Take a look at CONTRIBUTING.md for more information.
Join our Discord to chat with the community in the #rust channel.
Copyright (c) Stichting DuckDB Foundation
Licensed under the MIT license.