The crate map
Most applications depend onbetter-duck-core alone (plus better-duck-diesel if you want the
ORM). Everything else is either pulled in transitively or used only in a specific context.
The crate published as
better-duck on crates.io is better-duck-core. Throughout these docs,
“the core crate” means better-duck-core, and imports are written better_duck_core::….How the layers stack
better-duck-diesel and better-duck-tauri both build on core rather than wrapping it opaquely —
you can always drop to the underlying core Connection (Diesel exposes it via
inner_mut(); the Tauri plugin runs core directly) for the appender, columnar reads, UDF
registration, or raw SQL that the higher layers don’t surface.
The FFI safety model
DuckDB ships a C API. better-duck’s central promise is that nothingunsafe leaks into your
code while still exposing the full type system. A few principles make that hold:
Every FFI call is wrapped and validated Rust-side
Every FFI call is wrapped and validated Rust-side
Handles are owned by Rust types that free them on drop (
Connection, Database, Appender,
Statement, DataChunkHandle, result handles). Inputs that the C API cannot represent are
rejected before the call: a &str or path with an interior NUL byte becomes
Error::NulError, an out-of-range column index becomes
Error::InvalidColumnIndex, a wrong parameter count becomes Error::InvalidParameterCount.Errors are copied out of DuckDB memory, so they outlive the handle
Errors are copied out of DuckDB memory, so they outlive the handle
A failed query surfaces as
Error::Engine(EngineError { kind, message }),
where every field is copied out of DuckDB’s own memory when the error is built. The error stays
valid after the result — and the connection — are gone, and kind preserves DuckDB’s typed
classification (never guessed by parsing message text).The borrow checker closes aliasing holes
The borrow checker closes aliasing holes
A
VectorMut is built from &mut DataChunkHandle, so the compiler guarantees
it is the only live view of that column — an aliasing hole present in some other DuckDB bindings.
A DuckResult is consumed in exactly one mode (row-by-row or
chunk-by-chunk); mixing them is a clean Error::MixedIterationMode, not undefined behavior.Failures poison rather than corrupt
Failures poison rather than corrupt
When DuckDB invalidates a resource mid-operation — most notably a failed flush on the
Appender — the Rust wrapper poisons it: every later call fails fast
without re-entering DuckDB, and drop skips the dangerous re-flush. The originating connection
stays usable.The handful of unsafe entry points
The safe surface is the default; unsafe appears only where the caller must uphold a contract the
compiler cannot check, and each is clearly marked:
Connection::open_from_raw(db)— wrap aduckdb_databaseowned by a host process (loadable extensions); it never closes that database.Connection::register_arrow(view_name, stream)— register a caller-ownedArrowArrayStreampointer; the stream must stay valid until the returnedArrowViewguard drops.
Bundled DuckDB (embedded-first)
DuckDB is compiled from vendored source and statically linked into your binary bybetter-duck-sys. There is no system package to install and no shared library to ship at runtime.
This is what lets better-duck run inside Tauri desktop apps, iOS cross-builds, and anywhere a system
library isn’t an option.
How features flow down to the sys crate
The extension feature flags onbetter-duck-core are forwarded to
better-duck-sys, which compiles that extension straight into the static library — so json,
parquet, and icu need no runtime INSTALL / LOAD. Extensions without a bundling feature
(for example spatial) are instead loaded at runtime through the
extension API. The DuckDB version itself is pinned in better-duck-sys;
better_duck_core::library_version() returns the linked version
string at runtime.
The loadable-extension inversion
The experimentalloadable-extension feature flips the model: the crate
links no DuckDB of its own and routes every FFI call through a host DuckDB’s dynamically-loaded
API table, building a cdylib. It pairs with the #[duckdb_entrypoint] macro and is
for writing a DuckDB extension in Rust — not for a normal embedded application.
Where to next
Feature flags
Every Cargo feature, grouped, with its implications.
Type system
How DuckDB types arrive as
DuckValue, feature by feature.Error handling
How failures surface, from
NulError to EngineError.Versioning & compatibility
Beta policy, MSRV, platforms, and the vendored DuckDB version.