duckfn
Write DuckDB extensions in plain Rust.
duckfn is a runtime framework for building DuckDB extensions on top of
DuckDB's C Extension API. Together with duckfn-macro,
a single attribute turns an ordinary Rust function into a DuckDB scalar, aggregate or
table function, a SQL macro, a replacement scan, a type cast, or a nested type — no C/C++ glue
code, and no local DuckDB build required.
- Repository: https://github.com/shijianjs/duckfn
- Built on
quack-rs·libduckdb-sys - No
unsafeto write: nounsafe fn, no raw pointers in your function bodies - No DuckDB build required, no C/C++ code
- Attribute-driven registration through
inventory - Panic-safe: Rust panics become DuckDB errors instead of unwinding across the FFI boundary
- Works with DuckDB's official multi-platform extension CI
Status: early / experimental. APIs may change before
1.0.
Installation
[]
= "0.0.2"
# duckfn itself is built on these two crates; add them explicitly when you use
# their types or builders directly.
= "0.16.0"
# `loadable-extension` dispatches through DuckDB's API table instead of linking
# libduckdb, which is what keeps a local DuckDB build unnecessary.
# (headers only — no linked library)
= { = ">=1.4.4, <2", = ["loadable-extension"] }
If you prefer the macros without the runtime, depend on
duckfn-macro directly; otherwise the macros are
re-exported by duckfn and no extra dependency is needed.
Quick start
use ;
/// ```sql
/// SELECT double_it(21); -- 42
/// SELECT double_it(NULL); -- NULL
/// SELECT double_it(13); -- error: unlucky input
/// ```
// Generate the extension entry point (name must be lowercase + underscores).
duckfn_entrypoint!;
The wrapper, the logical types and the registration are all generated, so the snippet above is
entirely safe Rust — no unsafe fn, no raw pointers, no DuckDB C types. The only place unsafe
shows up is manual registration: #[duck_custom_register] calls quack-rs'
unsafe fn register_scalar / register_aggregate / register_table.
Attributes
| Attribute | Purpose |
|---|---|
#[duck_scalar_function] |
Register a scalar function. |
#[duck_aggregate_function] |
Register an aggregate function. |
#[duck_cast_function] |
Register a type cast (CAST(x AS T) / TRY_CAST). The single argument is the source value and the return type is the target type; supports Option<T> input, implicit_cost = N and auto_register = false. |
#[duck_table_function] |
Register a table function. |
#[duck_replacement_scan] |
Redirect an unresolved table name (usually a file path) to a table function, i.e. SELECT * FROM 'data.points'. Return Option<String> / Option<&'static str> / DuckOptionResult<...>; the path is passed as the first VARCHAR parameter. |
#[duck_sql_macro] |
Register a SQL macro. Return SqlMacro / DuckResult<SqlMacro>, or a SQL string (String / &'static str / DuckResult<...>) which is executed directly. |
#[duck_custom_register] |
Manually register builders, signature fn(&Connection) -> DuckResult<()>. |
#[derive(DuckStruct)] |
Map a struct to a DuckDB STRUCT. |
duckfn_entrypoint!("name") |
Generate the extension entry point. |
Common macro arguments:
auto_register = false— only generate builders (scalar_function_builder(),scalar_overload_builder(), ...) instead of auto-registering; pair it with#[duck_custom_register].named_param_from = "field"— where named arguments start for table functions.
#[duck_scalar_function] makes the function available as a module of the same name, exposing the
generated builders so you can register overloads or function sets yourself.
Type mapping
| DuckDB | Rust |
|---|---|
BOOLEAN |
bool |
TINYINT / SMALLINT / INTEGER / BIGINT |
i8 / i16 / i32 / i64 |
UTINYINT / USMALLINT / UINTEGER / UBIGINT |
u8 / u16 / u32 / u64 |
HUGEINT / UHUGEINT |
i128 / u128 |
FLOAT / DOUBLE |
f32 / f64 |
VARCHAR |
String |
NULL |
Option<T> |
LIST(T) |
Vec<T>, nestable (Vec<Option<Vec<Option<T>>>> ...) |
MAP(K, V) |
IndexMap<K, V> |
ARRAY(T, N) |
[T; N] (DuckArray) / [Option<T>; N] (DuckOptionArray) |
STRUCT(...) |
#[derive(DuckStruct)], nested structs and lists supported |
Nullability follows the Rust signature:
- a non-
Optionargument short-circuits the row toNULLwhen the input isNULL— the function body is not called; - an
Option<T>argument receivesNoneand decides the semantics itself.
Error handling and panics
Return DuckOptionResult<T> (i.e. Result<Option<T>, ExtensionError>) to emit NULL or fail the
query with duck_error("..."). Panics inside a function body are caught and converted into a
DuckDB error rather than unwinding across the FFI boundary.
A scalar function may use any of these return shapes:
// never NULL
// None -> SQL NULL
Example extension
A complete example extension (rusty_quack) covering every feature lives in the repository:
https://github.com/shijianjs/duckfn
License
Licensed under the MIT License.