Overloading with Function Sets
A DuckDB aggregate function can have several signatures under one name, registered
together as a function set. This page shows how to overload an aggregate
function in Rust with AggregateFunctionSetBuilder, including variadic aggregates
such as retention(c1, c2, ..., c32) and overloads with different return types.
Known DuckDB limitation. C-API aggregates — including every overload in a set — read out of bounds under
agg(x) OVER ()(whole-partition window frames) andagg(x ORDER BY y). This is a DuckDB C API defect; see Aggregate Functions for the details and DuckDB source lines. Do not use C-API aggregates in those two query shapes.Reported upstream as duckdb/duckdb#26109.
Note: For scalar function overloads, see
ScalarFunctionSetBuilder.
When to use function sets
Use AggregateFunctionSetBuilder when you need:
- Multiple type signatures for the same function name (e.g.,
my_agg(INT)andmy_agg(BIGINT)) - Variadic arity under one name (e.g.,
retention(2 columns),retention(3 columns), ...) - Overloads that return different types (see Per-overload return types)
For a single signature, use AggregateFunctionBuilder directly.
Registration
#![allow(unused)] fn main() { use libduckdb_sys::{duckdb_aggregate_state, duckdb_bind_info, duckdb_connection, duckdb_data_chunk, duckdb_function_info, duckdb_init_info, duckdb_vector, idx_t}; use quack_rs::prelude::*; #[derive(Default)] struct RetentionState { hits: u32 } impl AggregateState for RetentionState {} unsafe extern "C" fn update(_: duckdb_function_info, _: duckdb_data_chunk, _: *mut duckdb_aggregate_state) {} unsafe extern "C" fn combine(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: *mut duckdb_aggregate_state, _: idx_t) {} unsafe extern "C" fn finalize(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: duckdb_vector, _: idx_t, _: idx_t) {} use quack_rs::aggregate::AggregateFunctionSetBuilder; use quack_rs::types::TypeId; unsafe fn register(con: duckdb_connection) -> Result<(), ExtensionError> { unsafe { AggregateFunctionSetBuilder::new("retention") .returns(TypeId::Varchar) .overloads(2..=3, |n, builder| { // Each overload gets `n` BOOLEAN parameters let b = (0..n).fold(builder, |b, _| b.param(TypeId::Boolean)); b.ffi_state::<RetentionState>() // state_size + init + destructor .update(update) .combine(combine) .finalize(finalize) }) .register(con)?; } Ok(()) } }
overloads takes a RangeInclusive<usize> and a closure, called once per arity
n with a fresh AggregateOverloadBuilder, that returns the configured overload.
The set builder gives every member the set's name when it registers them.
Per-overload return types
DuckDB resolves an aggregate overload from its parameter types and arity
alone — the return type plays no part in resolution. Members of one set are
therefore free to return different types. This is how DuckDB's own arg_max
works:
arg_max(ANY, ANY) -> ANY
arg_max(ANY, ANY, ANY) -> ANY[]
Set the return type on the overload with AggregateOverloadBuilder::returns (or
returns_logical), and add each one with overload:
#![allow(unused)] fn main() { use libduckdb_sys::{duckdb_aggregate_state, duckdb_bind_info, duckdb_connection, duckdb_data_chunk, duckdb_function_info, duckdb_init_info, duckdb_vector, idx_t}; use quack_rs::prelude::*; #[derive(Default)] struct IntState { sum: i64 } impl AggregateState for IntState {} unsafe extern "C" fn int_update(_: duckdb_function_info, _: duckdb_data_chunk, _: *mut duckdb_aggregate_state) {} unsafe extern "C" fn int_combine(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: *mut duckdb_aggregate_state, _: idx_t) {} unsafe extern "C" fn int_finalize(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: duckdb_vector, _: idx_t, _: idx_t) {} #[derive(Default)] struct StrState { longest: String } impl AggregateState for StrState {} unsafe extern "C" fn str_update(_: duckdb_function_info, _: duckdb_data_chunk, _: *mut duckdb_aggregate_state) {} unsafe extern "C" fn str_combine(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: *mut duckdb_aggregate_state, _: idx_t) {} unsafe extern "C" fn str_finalize(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: duckdb_vector, _: idx_t, _: idx_t) {} unsafe fn demo(con: duckdb_connection) -> Result<(), ExtensionError> { use quack_rs::aggregate::{AggregateFunctionSetBuilder, AggregateOverloadBuilder}; use quack_rs::types::TypeId; unsafe { AggregateFunctionSetBuilder::new("my_agg") .overload( AggregateOverloadBuilder::new() .param(TypeId::Integer) .returns(TypeId::Integer) // my_agg(INTEGER) -> INTEGER .ffi_state::<IntState>() .update(int_update) .combine(int_combine) .finalize(int_finalize), ) .overload( AggregateOverloadBuilder::new() .param(TypeId::Varchar) .returns(TypeId::Varchar) // my_agg(VARCHAR) -> VARCHAR .ffi_state::<StrState>() .update(str_update) .combine(str_combine) .finalize(str_finalize), ) .register(con)?; } Ok(()) } }
Each overload carries its own callbacks, so overloads with different parameter
types can use different state types: call ffi_state::<T>() on each overload
with that overload's state type (AggregateFunctionSetBuilder itself has no
ffi_state), and read it in that overload's update, combine and finalize
with FfiState::<T>::with_state / with_state_mut for the same T.
Which return type wins
For each overload, in order:
AggregateOverloadBuilder::returns_logicalAggregateOverloadBuilder::returnsAggregateFunctionSetBuilder::returns_logical(the set-level default)AggregateFunctionSetBuilder::returns(the set-level default)
Registration fails, naming the overload index, if an overload reaches the end of that list with nothing set or is missing a required callback. It also fails if two overloads take the same parameter types. All of this is checked for every overload before any DuckDB handle is created.
Other per-overload settings
Each overload can also carry its own extra_info
(AggregateOverloadBuilder::extra_info), read in that overload's callbacks with
AggregateFunctionInfo::get_extra_info. Ownership works as on
AggregateFunctionBuilder: DuckDB frees it once registration has handed it over,
even if registration then fails; the builder frees it if it never gets that far.
AggregateOverloadBuilder::null_handling sets an overload's
NULL handling.
overload and overloads may be mixed on one builder; overloads register in
the order they were added.
Note:
AggregateOverloadBuilderwas calledOverloadBuilderbefore v0.18.0. The old name is still exported as a deprecated alias atquack_rs::aggregate::builder::OverloadBuilder.
The silent name bug — solved
Pitfall L6: When using a function set, the name must be set on each individual
duckdb_aggregate_functionviaduckdb_aggregate_function_set_name, not just on the set. If any member lacks a name, it is silently not registered — no error is returned.DuckDB's C API documentation does not mention this. It was found by reading DuckDB's C++ test code at
test/api/capi/test_capi_aggregate_functions.cpp. Induckdb-behavioral, 6 of 7 functions failed to register silently due to this bug.
AggregateFunctionSetBuilder::register calls duckdb_aggregate_function_set_name
on every member, whether it was added with overload or overloads.
See Pitfall L6.
Complex return types
If all overloads share one complex return type, set it once on the set builder as a default, rather than repeating it on every overload:
#![allow(unused)] fn main() { use libduckdb_sys::{duckdb_aggregate_state, duckdb_bind_info, duckdb_connection, duckdb_data_chunk, duckdb_function_info, duckdb_init_info, duckdb_vector, idx_t}; use quack_rs::prelude::*; #[derive(Default)] struct RetentionState { hits: u32 } impl AggregateState for RetentionState {} unsafe extern "C" fn update(_: duckdb_function_info, _: duckdb_data_chunk, _: *mut duckdb_aggregate_state) {} unsafe extern "C" fn combine(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: *mut duckdb_aggregate_state, _: idx_t) {} unsafe extern "C" fn finalize(_: duckdb_function_info, _: *mut duckdb_aggregate_state, _: duckdb_vector, _: idx_t, _: idx_t) {} unsafe fn demo(con: duckdb_connection) -> Result<(), ExtensionError> { use quack_rs::aggregate::AggregateFunctionSetBuilder; use quack_rs::types::{LogicalType, TypeId}; unsafe { AggregateFunctionSetBuilder::new("retention") .returns_logical(LogicalType::list(TypeId::Boolean)) // default for every overload .overloads(2..=32, |n, builder| { (0..n).fold(builder, |b, _| b.param(TypeId::Boolean)) .ffi_state::<RetentionState>() .update(update) .combine(combine) .finalize(finalize) }) .register(con)?; } Ok(()) } }
Individual overloads can also use param_logical for complex parameter types:
#![allow(unused)] fn main() { use libduckdb_sys::{duckdb_aggregate_state, duckdb_bind_info, duckdb_connection, duckdb_data_chunk, duckdb_function_info, duckdb_init_info, duckdb_vector, idx_t}; use quack_rs::prelude::*; fn demo() { let _ = AggregateFunctionSetBuilder::new("retention") .overloads(2..=8, |n, builder| { builder .param(TypeId::Interval) .param_logical(LogicalType::list(TypeId::Timestamp)) // LIST(TIMESTAMP) parameter // ... }) ; } }
Why not varargs?
DuckDB's C API has no duckdb_aggregate_function_set_varargs. A variadic aggregate
must therefore be registered as one overload per supported arity, which overloads
does in one call.
Note: Scalar functions do support varargs, through
ScalarFunctionBuilder::varargs()(stable C API, no feature flag needed).