Attributes
The attributes
| Attribute | Registers | Accepted return shapes |
|---|---|---|
#[duck_scalar_function] | a scalar function | T, Option<T>, DuckOptionResult<T> |
#[duck_aggregate_function] | an aggregate function | row handler returns () or DuckResult<()>; the output comes from the state |
#[duck_table_function] | a table function | impl Iterator<Item = Row>, DuckResult<impl Iterator<Item = Row>>, DuckFullIteratorResult<Row> |
#[duck_copy_function] | a COPY ... TO file format | fn(&mut Writer, &[DuckDynamicRow]) -> DuckResult<()> (the writer type implements DuckCopyToWriter) |
#[duck_copy_from_function] | a COPY ... FROM file format | fn(&mut Reader, usize) -> DuckResult<Vec<DuckDynamicRow>> (the reader type implements DuckCopyFromReader) |
#[duck_cast_function] | a type cast | T, Option<T>, DuckOptionResult<T> |
#[duck_replacement_scan] | a replacement scan | Option<String>, Option<&'static str>, DuckOptionResult<String>, DuckOptionResult<&'static str> |
#[duck_sql_macro] | a SQL macro | SqlMacro, DuckResult<SqlMacro>, String, &'static str, or DuckResult of those |
#[duck_custom_register] | whatever the function registers itself | fn(&Connection) -> DuckResult<()> |
#[derive(DuckStruct)] | — | maps a struct to a DuckDB STRUCT |
#[derive(DuckEnum)] | — | maps a unit-variant enum to a DuckDB ENUM (optionally creates the type at load time) |
duckfn_entrypoint!("name") | the extension entry point | — |
duck_sql_macro_files!("a.sql", …) | macros defined in SQL files | — |
Anything outside the listed return shapes is a compile error, with a message naming the shapes that are supported.
Arguments
Each macro declares only the arguments it actually uses; a key that the macro does not know is a
compile error. #[duck_custom_register] takes no arguments at all.
| Argument | Used by | Default | Meaning |
|---|---|---|---|
auto_register | every #[duck_*] attribute | true | false generates the builders but does not register the function. |
named_param_from | #[duck_table_function], #[derive(DuckStruct)] | — | For table functions: the argument from which on everything is a named parameter. |
description | every #[duck_*] attribute that registers a function | — | One-line summary of the function, exported to function_descriptions.csv. See Community extension docs. |
comment | every #[duck_*] attribute that registers a function | — | Extra remarks, exported as the CSV's comment column. |
example | every #[duck_*] attribute that registers a function | — | A single usage example: example = "SELECT ...". |
examples | every #[duck_*] attribute that registers a function | — | Several usage examples: examples = ["SELECT ...", "..."]. Cannot be combined with example. |
special_null_handling | #[duck_scalar_function], #[duck_aggregate_function] | false | Ask DuckDB to hand NULL arguments to the callback instead of folding them away. See Scalar functions. |
volatile | #[duck_scalar_function] | false | Mark the function volatile, so registration calls duckdb_scalar_function_set_volatile and DuckDB neither caches nor reuses calls with the same arguments. Requires the duckdb-1-5 feature and cannot be combined with overloads_name. See Scalar functions. |
varargs | #[duck_scalar_function] | false | Enable variadic arguments. The last parameter must be Vec<T> and T's logical type is passed to duckdb_scalar_function_set_varargs. Requires the duckdb-1-5 feature and cannot be combined with overloads_name. See Scalar functions. |
batch | #[duck_scalar_function] | false | Hand the whole batch of rows to the function instead of walking it row by row. The single parameter is Vec<MyRow> (only the non-NULL rows) or Vec<Option<MyRow>>, MyRow being a #[derive(DuckStruct)] struct that becomes the argument type; the return value is Vec<T> / Vec<Option<T>> / DuckOptionResult<Vec<…>>. Cannot be combined with varargs. See Scalar functions. |
implicit_cost | #[duck_cast_function] | — | For casts: the implicit conversion cost. |
overloads_name | #[duck_scalar_function], #[duck_aggregate_function] | — | Register as an overload of this function set instead of under the function's own name. |
#[derive(DuckStruct)] has its own argument set (named_param_from, plus sql_name / create_type
from "Named types in the catalog" below) declared through #[duck(...)] on the struct, which is how
struct-based table functions declare where their named parameters start:
#[derive(Default, Debug, Clone, DuckStruct)]
#[duck(named_param_from = "start")]
struct CountDownS {
start: i64,
}
#[derive(DuckEnum)] maps a unit-variant-only enum onto a DuckDB ENUM (dictionary = declaration
order). It has one argument of its own — rename_all (lowercase, UPPERCASE, snake_case,
SCREAMING_SNAKE_CASE, camelCase, PascalCase, kebab-case, SCREAMING-KEBAB-CASE, default
verbatim) — plus #[duck(rename = "...")] on a single variant:
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, DuckEnum)]
#[duck(rename_all = "lowercase")]
pub enum Priority {
#[default]
Low,
Medium,
High,
}
The enum is then usable anywhere a value type is expected — function arguments, return values,
STRUCT fields, container elements — and Option<Priority> makes it nullable. A non-nullable
argument additionally needs Default (the generated argument struct derives it), which is why the
example derives Default and marks a #[default] variant; with Option<Priority> it is not needed.
Named types in the catalog
Both derives also accept:
| Argument | Default | Meaning |
|---|---|---|
sql_name | the type name in snake_case | The SQL-side type name (Priority → priority, Ticket → ticket). |
create_type | false | true runs CREATE TYPE IF NOT EXISTS <sql_name> AS <type>; when the extension loads, "replace" runs CREATE OR REPLACE TYPE <sql_name> AS <type>; instead (overwriting an existing type of that name), "print" only prints the IF NOT EXISTS statement (to stderr), false does nothing. |
With true — and with "print", which renders the same statement — loading the extension twice is
fine and an existing type of that name is left untouched; "replace" is the flavour that overwrites
it. Either way execution goes through the same path as the SQL macros (duckdb_query). An
enum becomes ENUM(...); a struct becomes STRUCT(...), and its field types are rendered from
DuckDB's own logical types, so nested enums and structs, LIST / ARRAY / MAP and hand-written
custom field types come along automatically:
#[derive(Clone, Debug, Default, DuckStruct)]
#[duck(sql_name = "ticket", create_type = true)]
pub struct Ticket {
pub id: i64,
pub priority: Priority,
pub labels: Vec<String>,
}
// CREATE TYPE IF NOT EXISTS "ticket" AS
// STRUCT("id" BIGINT, "priority" ENUM('low', 'medium', 'high'), "labels" VARCHAR[]);
With create_type = "print" the macro renders that very same statement but creates nothing. The DDL
is queued and printed once, after every registration has run, so an extension with a dozen
print-mode types still gets a single notice instead of a dozen hints. The block is framed by
-- [duckfn] comments saying it was not executed, which keeps it copy-pasteable as SQL and makes a
bare DDL line impossible to mistake for something that actually happened:
#[derive(Clone, Debug, Default, DuckStruct)]
#[duck(sql_name = "ticket", create_type = "print")]
pub struct Ticket {
pub id: i64,
pub priority: Priority,
}
-- [duckfn] create_type = "print": the statement below was NOT executed.
-- [duckfn] Copy it and run it yourself if you want the type created.
CREATE TYPE IF NOT EXISTS "ticket" AS STRUCT("id" BIGINT, "priority" ENUM('low', 'medium', 'high'));
-- [duckfn] end - nothing above was executed.
Use create_type = "replace" when the extension should own a name that may already exist: the same
statement is rendered with OR REPLACE, so the existing definition is overwritten on every load
instead of being left alone. Handy while iterating on a type definition, and for an extension whose
type is the source of truth:
#[derive(Clone, Debug, Default, DuckStruct)]
#[duck(sql_name = "ticket", create_type = "replace")]
pub struct Ticket {
pub id: i64,
pub priority: Priority,
}
// CREATE OR REPLACE TYPE "ticket" AS
// STRUCT("id" BIGINT, "priority" ENUM('low', 'medium', 'high'));
Keep create_type = false (the default) when the macro should stay out of the way entirely, and
print the DDL yourself — from #[duck_custom_register], with wording of your own:
#[duck_custom_register]
fn show_the_create_type_ddl(_connection: &Connection) -> DuckResult<()> {
// `create_type = false`: showing the DDL — and what it means — is up to you
let ddl = duckfn::named_type_ddl("ticket", &Ticket::logical_type())?;
duckfn::print_sql_preview(
"my extension will NOT create this type",
&ddl,
"end - copy the statement above and run it yourself if you want it",
);
Ok(())
}
An enum works the same way — duckfn::named_type_ddl("priority", &Priority::logical_type()) (its
logical_type() already carries the dictionary), or duckfn::register_enum_type /
duckfn::queue_enum_type_ddl for a hand-written label list.
The same helpers come in a conflict-aware flavour: pass duckfn::TypeConflict::Replace to
named_type_ddl_with / register_named_type_with / register_enum_type_with /
queue_named_type_ddl_with / queue_enum_type_ddl_with to render (or run) CREATE OR REPLACE TYPE
instead — that is exactly what create_type = "replace" calls, so a hand-written registrar can get
the same behaviour.
SQL can then use ticket as a type — a column type or a cast target — even though the functions are
registered with the equivalent structural type; the two are interchangeable. See
Types → Enums and Types → Structs.
What a macro generates
A function attribute expands to the original function plus a module named after that function:
#[duck_scalar_function]
fn dfn_scalar_reg_manual(i: i32) -> i32 {
i + 1
}
becomes approximately:
fn dfn_scalar_reg_manual(i: i32) -> i32 {
i + 1
}
mod dfn_scalar_reg_manual {
use super::*;
// One field per argument; the derive maps the struct to a STRUCT.
#[derive(duckfn::DuckStruct, Debug, Clone, Default)]
pub struct DuckArgsImpl {
pub i: i32,
}
pub struct ScalarFunctionImpl;
impl duckfn::ScalarFunctionAdapter for ScalarFunctionImpl {
const NAME: &'static str = "dfn_scalar_reg_manual";
type Args = DuckArgsImpl;
type Output = i32;
// …
}
pub fn scalar_function_builder() -> quack_rs::prelude::ScalarFunctionBuilder { /* … */ }
pub fn scalar_overload_builder() -> quack_rs::prelude::ScalarOverloadBuilder { /* … */ }
}
The module inherits the function's visibility, so a pub fn gets a pub mod. Inside it, each macro
provides:
| Macro | Generated items |
|---|---|
#[duck_scalar_function] | ScalarFunctionImpl, SQL_NAME, scalar_function_builder(), scalar_overload_builder() — with batch = true, DuckArgsImpl is an alias for the row struct rather than a generated struct |
#[duck_aggregate_function] | AggregateFunctionImpl, SQL_NAME, aggregate_function_builder(), aggregate_overload_builder(builder), aggregate_function_guard() |
#[duck_table_function] | TableFunctionImpl, SQL_NAME, table_function_builder() (returns a DuckResult) |
#[duck_copy_function] | CopyFunctionImpl, copy_function_builder() (returns a DuckResult), copy_function_register(connection) — no DuckArgsImpl |
#[duck_cast_function] | CastFunctionImpl, cast_function_builder(), cast_function_register(connection) |
#[duck_replacement_scan] | ReplacementScanImpl, replacement_scan_register(connection) — no DuckArgsImpl |
SQL_NAME is the name SQL actually uses: the function name, or the function-set name when the
signature is registered through overloads_name. It differs from the adapter's NAME, which is the
Rust function name and only identifies the callback. Read SQL_NAME when an error message or a log
line has to carry the function name, instead of repeating the attribute's string literal.
#[duck_custom_register] and #[duck_sql_macro] generate no module at all: they keep the function
as written and submit it to the registry.
Automatic vs. manual registration
With the default auto_register = true the macro submits itself to an inventory registry, and the
entry point generated by duckfn_entrypoint! registers everything it finds when DuckDB loads the
extension. There is no registration boilerplate to write.
With auto_register = false the function is generated but never registered, which makes it
invisible to SQL until you register it yourself:
#[duck_scalar_function(auto_register = false)]
fn dfn_scalar_reg_manual(i: i32) -> i32 {
i + 1
}
#[duck_custom_register]
fn dfn_scalar_reg_manual_register(c: &Connection) -> DuckResult<()> {
unsafe { c.register_scalar(dfn_scalar_reg_manual::scalar_function_builder()) }
}
#[duck_custom_register] takes the function as-is, so its signature must be exactly
fn(&Connection) -> DuckResult<()>. Registering the other kinds follows the same pattern:
unsafe { c.register_aggregate(dfn_agg_reg_manual::aggregate_function_builder()) } // aggregate
unsafe { c.register_table(dfn_table_reg_manual::table_function_builder()?) } // table
dfn_cast_manual::cast_function_register(c) // cast
dfn_scan_manual::replacement_scan_register(c) // replacement scan
Note that the table builder returns a DuckResult, so it is ?-ed; the cast and scan helpers take
the connection directly.
Manual registration is also how you assemble a function set by hand:
#[duck_custom_register]
fn dfn_scalar_reg_over_register(c: &Connection) -> DuckResult<()> {
unsafe {
c.register_scalar_set(
ScalarFunctionSetBuilder::new("dfn_scalar_reg_overload")
.overload(dfn_scalar_reg_over_int::scalar_overload_builder())
.overload(dfn_scalar_reg_over_varchar::scalar_overload_builder()),
)
}
}
ScalarFunctionSetBuilder, Connection and the register methods come from quack-rs; see
Installation for why you add that dependency yourself.
The entry point
duckfn_entrypoint!("my_ext");
This generates the symbol my_ext_init_c_api, which is what DuckDB looks up when loading the
extension. The name must be non-empty and contain only lowercase ASCII letters, digits and
underscores; anything else is rejected at compile time, and a wrong symbol name makes the extension
fail to load.
SQL macros from files
duck_sql_macro_files! registers macros kept in .sql files instead of Rust string literals:
duck_sql_macro_files!(
"sql/macro_files_a.sql",
"sql/macro_files_b.sql",
"sql/macro_files_c.sql"
);
Paths are resolved relative to the .rs file that invokes the macro, inlined with include_str!
at compile time, and executed in the order written. A single file may define any number of macros;
see SQL macros.
Source and tests
duckfn-macro/src/— one file per macro holding its own arguments and expansion, pluscommon.rsfor the shared scaffoldingtest/extension/functions/scalar_function.rsandtest/sql/functions/scalar_function.test— the manual-registration examples