duckfn 0.0.18

Write DuckDB extensions in plain Rust: attribute macros that turn ordinary functions into scalar/aggregate/table functions, SQL macros and nested LIST/MAP/ARRAY/STRUCT types.
Documentation

duckfn

English | 简体中文

GitHub Docs crates.io docs.rs License: MIT zread

Write DuckDB extensions in plain Rust.

duckfn is a Rust framework for building DuckDB extensions on top of DuckDB's C Extension API. A single attribute turns an ordinary Rust function into a DuckDB scalar, aggregate, table or copy 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
  • Crates: duckfn · duckfn-macro
  • Built on: quack-rs · libduckdb-sys
  • No unsafe to write: no unsafe fn, no raw pointers in your function bodies
  • No DuckDB build required, no C/C++ code
  • Attribute-driven registration through inventory
  • Panic-safe on native: Rust panics become DuckDB errors instead of unwinding across the FFI boundary. (On the browser this guard cannot work — a panic! cannot unwind across the JS boundary and surfaces as a stack overflow — so report errors with Err(duck_error(..)), not panic!.)
  • Host file system access (native): read and write through DuckDB's virtual file system (s3://, http(s):// with httpfs, in-memory) from any callback — aggregate functions included — plus one-line helpers such as duckfn::duck_vfs::read_string / write_string / append_string (the owned-connection feature; unreliable under DuckDB-Wasm, so it is not in all — see the feature notes below)
  • Interop with other crates: the time wrapper types (DuckDate, DuckTimestamp / _S / _Ms / _Ns, DuckTimestampTz, DuckTime) convert to and from chrono, DuckUuid ↔ uuid, and DuckDecimal<W, S> ↔ rust_decimal (chrono / uuid / rust_decimal features) — out-of-range values, DuckDB's infinity and digit-losing conversions come back as errors, never as a panic or a silent truncation
  • Function documentation: description / comment / example on any #[duck_*] attribute, exported to the function_descriptions.csv that DuckDB's community-extension pages read (cli feature)
  • Works with DuckDB's official multi-platform extension CI

Status: early / experimental. APIs may change before 1.0.

Repository layout

The repository is a Cargo workspace whose root is also the duckfn crate root:

Path Description Published to crates.io
/ (duckfn) Runtime framework: traits, type adapters, function registration. Also the workspace root. Yes
test/extension/, test/sql/ The example extension (duckfn): runnable SQL for every feature, its three entry points (src/lib.rs, src/bin/duckfn.rs) and its sqllogictest suite. Part of this package, compiled only with the quack feature. Yes — sources only, never compiled for a dependent
duckfn-macro/ Procedural macros: #[duck_scalar_function], #[derive(DuckStruct)], #[derive(DuckEnum)], ... Yes
docs/ Docusaurus documentation site: docs/docs/** (English) and docs/i18n/zh-Hans/** (Simplified Chinese). No, docs site — but its sources ship inside the duckfn package

The published duckfn package carries the runtime, its tests, this README, the license, the whole documentation source and the example extension with its sqllogictest suite, so the guide, the example page and a complete worked extension are readable without cloning the repository (cargo package --list in the repository root prints the exact file list). The example is compiled only when the quack feature is on — off by default — so a dependency on duckfn compiles none of it. Keeping the example inside this package is also what lets cargo ship it at all: cargo never packages a subdirectory that contains its own Cargo.toml.

Installation

[dependencies]
duckfn = "0.0.18"

# duckfn itself is built on these two crates; add them explicitly when you use
# their types or builders directly.
quack-rs = "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)
libduckdb-sys = { version = ">=1.4.4, <2", features = ["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.

duckfn has seven optional features. cli adds the command-line tool that exports a function_descriptions.csv for DuckDB's community-extension pages (cargo run --features quack --bin duckfn-cli -- function_descriptions in this repository); only an extension project's src/bin/duckfn.rs needs it. duckdb-1-5 enables what DuckDB's 1.5 C API added: the logical types from DuckDB 1.5 (currently TIME_NS) and copy functions. owned-connection builds on it to add host file-system access (duckfn::duck_vfs): read/write through DuckDB's VFS from any callback, aggregates included. The other three are interop and independent of each other: chrono converts the time wrapper types to and from chrono, uuid converts DuckUuid to and from uuid, and rust_decimal converts DuckDecimal<W, S> to and from rust_decimal — so the epoch / 128-bit / scaled-integer arithmetic lives in duckfn rather than in every extension:

duckfn = { version = "0.0.18", features = ["duckdb-1-5", "owned-connection", "chrono", "uuid", "rust_decimal"] }

all is the aggregate switch — every optional feature except owned-connection. duckfn normally sits at the end of the dependency tree, so features = ["all"] is the convenient spelling. The host file system is left out of all on purpose: it is unreliable under the browser / DuckDB-Wasm build (see the Troubleshooting / file-system docs), and on native DuckDB's own readers or the standard library cover it, so it is opt-in via owned-connection rather than charged to everyone. Pick individual features when you want a leaner tree.

quack is the one feature that is not meant for dependents: it compiles this package's own example extension (test/extension/), and depends on all plus owned-connection (the example demonstrates every capability). Asking for all never drags the example or its test functions into your build.

Quick start

use duckfn::{duck_error, duck_scalar_function, duckfn_entrypoint, DuckOptionResult};

/// ```sql
/// SELECT double_it(21);    -- 42
/// SELECT double_it(NULL);  -- NULL
/// SELECT double_it(13);    -- error: unlucky input
/// ```
#[duck_scalar_function]
pub fn double_it(v: Option<i64>) -> DuckOptionResult<i64> {
    if v == Some(13) {
        return Err(duck_error("unlucky input"));
    }
    Ok(v.map(|x| x * 2))
}

// Generate the extension entry point (name must be lowercase + underscores).
duckfn_entrypoint!("my_ext");

The wrapper, the logical types and the registration are all generated, so the snippet above is entirely safe Rust. The only place unsafe shows up is manual registration: #[duck_custom_register] calls quack-rs' unsafe fn register_scalar / register_aggregate / register_table.

Calling the generated module of a #[duck_scalar_function] exposes builders such as scalar_function_builder() and scalar_overload_builder(), so you can register overloads or function sets yourself.

Documentation

The full guide — every attribute and its arguments, the type mapping, the error model, and a runnable example extension — lives at https://shijianjs.github.io/duckfn/:

Page Contents
Introduction What duckfn is, and how the crates fit together.
Create a project Start from the duckfn extension template or DuckDB's official Rust one.
Project structure The crate roots, error[E0583], and the command-line tool's own root.
Installation Dependencies, MSRV, and why no DuckDB build is needed.
Quick start Write, build and load your first extension.
Attributes All attributes, shared arguments, and manual registration.
Scalar functions Return shapes, NULL handling, batch mode, overloads.
Aggregate functions Row handlers, state types, parallel aggregation.
Table functions Row structs, named parameters, streaming.
Copy functions Custom file formats for COPY ... TO / COPY ... FROM, built on runtime dynamic columns.
Type casts Overriding CAST for one source/target pair.
Replacement scans Making SELECT * FROM 'data.points' work.
SQL macros Macros from Rust or from .sql files.
Type mapping DuckDB ↔ Rust types, nullability and known gaps.
Community extension docs The description / comment / example attributes, and the CSV DuckDB's community-extension pages read.
Errors and panics · Architecture Error handling, expansion, registration and adapters.
Example extension duckfn, the example shipped with this package, with runnable SQL for every feature.
Build and release · Contributing · FAQ Local builds, CI, and troubleshooting.

中文文档:https://shijianjs.github.io/duckfn/zh-Hans/

Rust API reference: https://docs.rs/duckfn

License

Licensed under the MIT License.