Skip to main content

FAQ

Quick answers to the questions that come up most while using duckfn — why it works the way it does, and what to do when a function does not behave. For upstream bugs and platform quirks with workarounds, see Known issues instead.

Why doesn't duckfn need a local DuckDB build?​

Because nothing is linked. libduckdb-sys is compiled with the loadable-extension feature, which uses DuckDB's headers but resolves every API function through a pointer table that the host DuckDB installs when it loads the extension. The trade-off is that the extension is tied to the DuckDB version it was built against — see Architecture and DuckDB version compatibility.

Why do I have to load the extension with -unsigned?​

The extension is not signed by DuckDB, and it is marked as using DuckDB's unstable C API, so DuckDB refuses to load it unless unsigned extensions are allowed. Passing -unsigned on the command line does that.

How is this different from duckdb-rs?​

They work in opposite directions. duckdb-rs (the duckdb crate) is a client binding: you embed a DuckDB database inside a Rust program and call it. duckfn produces an extension: a shared library that an existing DuckDB process loads with LOAD, adding functions to that process.

duckfn is built on quack-rs and libduckdb-sys rather than on duckdb-rs.

My function does not exist in SQL. What now?​

Check, in order:

  1. LOAD succeeded — a failed LOAD is easy to miss in a script.
  2. The function is not registered with auto_register = false. Those only exist once something calls their builder; see Attributes.
  3. The entry point symbol matches the extension name: duckfn_entrypoint!("my_ext") exports my_ext_init_c_api. A mismatch means the extension loads but registers nothing.
  4. With overloads_name = "…", the branch functions are not registered under their own names — only the set name exists.
  5. The parameter or return types are ones the macro accepts. An unsupported shape is a compile error, but an unsupported argument combination shows up as No function matches the given name and argument types.

Why doesn't my constant NULL reach the function body?​

DuckDB folds constant expressions at bind time, so NULL::INTEGER and NULL::INTEGER + 0 never reach the callback. Set special_null_handling = true if the body needs to see them; column values already arrive as None. See Scalar functions.

Why does a NULL inside Vec<i32> behave differently in a scalar and a table function?​

A scalar function reads arguments row by row, so a NULL element makes the whole row NULL — unless you declare Vec<Option<i32>>. A table function's arguments are bind parameters read from a Value, and there Vec<T> with a NULL element is an error (Vec<T> value is None). Again, use the Option variant when NULL elements are expected.

Why can't an ARRAY be a table function argument?​

DuckDB cannot bind a Value to an ARRAY type: Bind value to array type is not supported. Arrays work fine as scalar function arguments, as list elements, and as struct fields.

Can a function return Result<T, ExtensionError>?​

No. The accepted shapes are T, Option<T> and DuckOptionResult<T> for scalar-like functions; return Ok(Some(value)) instead of Ok(value).

How do I return a STRUCT?​

Derive DuckStruct on the type you return:

#[derive(Clone, Debug, DuckStruct)]
pub struct Point {
x: i64,
y: i64,
}

Its fields become the struct's fields at every level, including inside lists, maps and arrays.

Can I add a logical type duckfn does not support?​

Yes. DuckValueType is a public, unsealed trait, so you implement it for your own type outside duckfn: three methods are enough for a type that reuses an existing physical representation, and DuckValueReader / DuckValueWriter expose the raw DuckDB vector for the cases quack-rs has no accessor for. See Custom types.

Some DuckDB 1.5 types only need a Cargo feature instead — TIME_NS ships behind duckdb-1-5.

Why are named arguments ignored on scalar functions?​

duckfn registers scalar functions by position, so DuckDB binds the values in the order written and ignores the names — f(b := 2, a := 1) passes 2 first. named_param_from is a table-function-only key; the other attribute macros reject it.

How do I test an extension?​

With sqllogictest files under test/sql/, run by make test (just test). The file sets up the extension with require duckfn, then pairs statements with their expected output. See Contributing.

The extension loads but calls fail with a version error​

The extension is compiled against a specific DuckDB version (TARGET_DUCKDB_VERSION, currently v1.5.6) and uses the unstable C API, so it only works with a compatible DuckDB. Load it into the matching version, or rebuild against the version you are running. What "compatible" means — and how the same error reads under the stable ABI — is in DuckDB version compatibility.

Where do tooling problems go?​

Problems with the project layout — the crate roots, error[E0583] once you nest modules, the IDE flagging a separate wasm root — are in Project structure. The Rust 1.86 pin in the official CI's WebAssembly job and the upstream bugs worth knowing about are in Known issues.