Skip to main content
The UDF macros are enabled by better-duck-core’s feature = "udf" (which also turns on columnar). You never depend on better-duck-macros directly — import everything from better_duck_core::…. Registration is always on a core Connection; through better-duck-diesel’s udf feature, register on the underlying connection via conn.inner_mut().
The UDF macros turn an ordinary Rust function into a DuckDB function. Parameter and return types are inferred from the signature via DuckLogicalType — any type that round-trips through AppendAble (every integer width, floats, String / &str, bool, …) works. No unsafe, no manual vector handling.

Scalar functions

#[duckdb_scalar] — one value per row, for a SELECT list or WHERE.

Table functions

#[duckdb_table_function] — rows and columns for a FROM clause.

Aggregate functions

#[duckdb_aggregate] — fold many rows into one value.

Cast functions

#[duckdb_cast] — a custom CAST / TRY_CAST.
Plus #[duckdb_entrypoint] for packaging functions into a loadable extension.

The generated register

Each function macro generates a register(&mut Connection) associated function in a module named after your function, so you register with my_fn::register(&mut conn)?. The annotated function also remains an ordinary, directly callable Rust fn.

Shared semantics

An Option<T> parameter or return maps to DuckDB NULL. Any Option<T> parameter also auto-enables special_handling, so the function is still invoked for NULL inputs (a non-Option parameter instead short-circuits NULL rows to a NULL result without calling your body).
A Result<T, E> return fails the query with E’s message on Err (E: Into<Box<dyn Error + Send + Sync>>). Prefer this over panicking.
The duck_bail!("…") macro is return Err(format!(…).into()) for a fallible body — see helper macros.

The panic caveat

Callback panics are caught and reported to DuckDB as a query error only when the crate is built with panic = "unwind" (the default for dev and test). Under a panic = "abort" build — many release profiles — a panicking UDF aborts the process: catch_unwind cannot catch once unwinding is compiled out. Return Err (or duck_bail!) from a fallible function rather than panicking; the error path never depends on unwinding.

Low-level traits

Every macro compiles down to a trait implementation you can also write by hand and register directly: Each function page documents its trait and the by-hand path. The macros are the recommended surface; the traits exist for cases the macros don’t cover (multiple overloads, bind-time constant folding, per-worker local state).
Replacement scans (udf::ReplacementScan, which rewrite a bare FROM identifier into a table-function call) are an experimental part of the same udf feature but have no attribute macro. Register one per-database with database.register_replacement_scan::<T>().

See also

  • Columnar overview — the DataChunkHandle / vector API callbacks use.
  • Values overview — the type inference that drives parameter/return mapping.
  • Helper macros — duck_bail!, duck_state!, duck_projection!, duck_extra_info!.