Create a project
Three decisions come before writing any code: what to start from, what to write with, and how to build.
Start from a template
Both templates below are the same skeleton — DuckDB's CI, extension-ci-tools, a cdylib crate
root, a sqllogictest directory. They differ in how much is already wired up for duckfn.
The duckfn template
shijianjs/duckfn-extension-template is
this project's own template: the official skeleton with the duckfn-specific work already done.
git clone https://github.com/shijianjs/duckfn-extension-template my_ext
cd my_ext
rm -rf .git && git init # optional: drop the template's history and start your own
just rename my_ext
just rename (that is, scripts/rename.sh) is the reason the template exists. The extension name
has to agree in several places at once, and missing one is not a compile error — it is an extension
that will not load, because DuckDB derives the entry-point symbol from the file name, so a mismatch
fails at LOAD with nothing useful to go on. The script rewrites all of them in one pass:
[package] nameand the[[example]] nameinCargo.toml;EXTENSION_NAMEin theMakefile;- the name passed to
duckfn_entrypoint!(…), which becomes the entry-point symbolmy_ext_init_c_api; extension_namein theJustfileand in the CI workflow;- the path examples in the README and the documentation site, plus the
Cargo.lockentry.
It ends by printing what is still left for a human — replacing the two sample functions being the
main item. Everything else is there already: duckfn in the dependency list with
loadable-extension enabled, src/lib.rs and src/wasm_lib.rs declaring the same set of modules, a
scalar and an aggregate sample function with SQLLogicTest files for both, the Justfile, a
two-language Docusaurus site under docs/ (deletable — nothing else depends on it), the release
scripts, and the community-extension/ staging files. Its README.md and DEVELOPMENT.md describe
the loop, and its AGENTS.md is already the one from
Brief your coding agent.
The official template
duckdb/extension-template-rs is DuckDB's own
Rust extension template, and the base both templates derive from. Start here if you would rather do
the wiring yourself — but keep it in mind either way, because it is where the makefiles and the CI
ultimately come from:
- It ships a complete GitHub Actions pipeline that builds and tests the extension for every platform DuckDB supports and publishes the binaries on a version tag. There is nothing to write.
- It wires up
extension-ci-tools, which providesmake configure/make debug/make release/make test— the same flow DuckDB itself uses, and the one the sqllogictest runner needs. - The repository you are reading is a template like that, with the functions replaced.
git clone --recurse-submodules https://github.com/duckdb/extension-template-rs my_ext
cd my_ext
extension-ci-tools is a git submodule, so clone with --recurse-submodules, or run
git submodule update --init --recursive before the first build.
Then rename what the template hard-codes — EXTENSION_NAME in the Makefile, the [[example]]
target if you also build for WebAssembly, and the name passed to duckfn_entrypoint! — which is the
same list just rename automates, plus the Justfile and CI entries it would not know about yet.
Whichever template you start from, leave the crate roots alone: src/lib.rs, src/wasm_lib.rs and
src/bin/duckfn.rs all point at the same src/extension/ module, and that is the one rule the
layout follows — see Project structure for why, and what error[E0583]
means when it is broken.
Write with duckfn and quack-rs
duckfn is the layer this repository provides. It sits on top of
quack-rs and turns an ordinary Rust function into an
extension function with a single attribute:
#[duck_scalar_function]
pub fn double_it(v: Option<i64>) -> DuckOptionResult<i64> {
Ok(v.map(|x| x * 2))
}
Why not use the official duckdb crate directly? Because its extension API only covers two kinds of
functions — scalar functions behind the vscalar feature and table functions behind vtab, which is
the entire surface of this line:
duckdb = { version = "~1.10505.0", features = ["loadable-extension", "vscalar"] }
Aggregate functions, SQL macros, replacement scans, casts and nested types have no registration API
at all. That gap is what duckfn fills: one attribute system covering scalar functions, aggregate
functions, table functions, SQL macros, replacement scans and casts.
Reach for quack-rs directly when you need something the attributes do not expose — a hand-built
LogicalType, a vector-level operation, a corner of the C API. It is already in your dependency
list.
Same functions, two ways takes four functions from the official
template and the quack-rs example and writes each one both ways.
Build with the official toolchain
For day-to-day work, use DuckDB's own extension-ci-tools makefiles — the same flow CI and the
sqllogictest runner use:
make configure # once: creates the Python venv the test runner uses
make debug # -> build/debug/my_ext.duckdb_extension
make debug builds the cdylib, appends the extension metadata and lays the result out where DuckDB
expects it, so the file you load locally is the same artifact the test runner and CI consume:
duckdb -unsigned -c "
LOAD './build/debug/my_ext.duckdb_extension';
SELECT double_it(21);
"
The duckfn template wraps this flow in a
Justfile: just build
(which is make configure + make debug), just sql "SELECT …", just repl, just test,
just ci-release, plus the docs_* and release recipes. just rename has already set
extension_name to whatever duckfn_entrypoint! declares. See
Quick start.
Running make on Windows
The make flow needs Git Bash on Windows: run make (and the just recipes that call it) from Git
Bash rather than PowerShell — see
Contributing.
Brief your coding agent
If an AI agent writes the extension, give it a project-level AGENTS.md. The
duckfn template ships one: fill in its two placeholders — what the extension
does and where the duckfn clone lives — and that is the whole setup. Starting from anywhere else,
copy that file:
AGENTS.md.
It exists because of what a dependency does and does not carry. cargo unpacks duckfn and
duckfn-macro into the local registry, so the runtime and the macro implementations — the ground
truth for which attribute accepts which arguments and which return shapes it allows — are on disk
and readable. The published duckfn package is more than that: it also carries the documentation
sources under docs/docs/** with their Simplified Chinese translations and the example extension —
test/extension/** together with its sqllogictest suite under test/sql/ — so the guide, the example
page and a complete worked extension can all be read straight out of the unpacked crate. See
Build and release for the exact file list.
What the crate does not carry is the repository around them: the issue history, the CI workflows, the
documentation site's own tooling, and the AGENTS.md file itself. That is why the AGENTS.md points
the agent at a local clone of this repository and tells it to stop and ask rather than invent an
attribute it cannot find — the clone's path is one of the two placeholders to fill in.
Nothing that can be read out of the code is written down in it — extension name, crate name, duckfn version — so it cannot go stale.
Next
- Installation — add duckfn to the crate.
- Quick start — write, build and load the extension.
- Build and release — what the template's CI does on a version tag.