duckfn 0.0.2

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 | 简体中文

crates.io docs.rs License: MIT zread

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 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: 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

[dependencies]

duckfn = "0.0.2"



# 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.

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 — 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-Option argument short-circuits the row to NULL when the input is NULL — the function body is not called;
  • an Option<T> argument receives None and 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:

#[duck_scalar_function] fn plain(i: i32) -> i32 { i * 2 }                 // never NULL
#[duck_scalar_function] fn maybe(i: i32) -> Option<i32> { Some(i) }       // None -> SQL NULL
#[duck_scalar_function] fn checked(i: i32) -> duckfn::DuckOptionResult<i32> { Ok(Some(i)) }

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.