Contributing
Prerequisites
| Rust | 1.86 or newer — the workspace sets rust-version = "1.86" and uses edition 2024. |
| Python 3 + network | Only for make configure, which builds the sqllogictest runner's virtualenv. |
make | Drives the DuckDB extension-ci-tools makefiles. |
just (optional) | The Justfile wraps the common commands. |
| DuckDB CLI | For loading the extension by hand, and for debugging. |
extension-ci-tools/ is a git submodule and the Makefile includes makefiles from it, so after a
fresh clone:
git submodule update --init --recursive
make configure
Windows
Run make from Git Bash, not PowerShell or cmd: the makefiles and their helper scripts assume
a POSIX shell. Anything make reports as missing can usually be installed with
Scoop:
scoop install make python
Plain cargo commands (cargo build, cargo clippy, cargo test) work in any shell, so Git Bash
is only needed for the make targets — make configure, make test, make debug — and the just
recipes that call them (just build, just sql, just repl, just test).
The workspace
| Member | Published | Notes |
|---|---|---|
/ (duckfn) | yes | The runtime framework, the example extension and the workspace root. |
duckfn-macro/ | yes | The procedural macros; depends on the runtime for nothing, only on darling, syn, quote. |
test/extension/, test/sql/ | shipped, never compiled | The example extension (duckfn) with its sqllogictest suite: part of the duckfn package, switched on by the quack feature. |
The root manifest pins duckfn-macro = "=0.0.18", so the two crates always ship together.
An extension project has three crate roots — see Project structure — and this repository has two: there is no separate wasm root, because the example extension is part of the package and its library is what gets compiled for WebAssembly.
Why the example is part of this package
Cargo never packages a subdirectory that contains its own Cargo.toml, so an example extension kept
as a crate of its own could never ship inside duckfn. Folding it into the package is what lets
the crate carry a complete worked example — the module tree in test/extension/, the CLI at
src/bin/duckfn.rs and the sqllogictest suite in test/sql/ — which is the point of the
arrangement (see Build and release for the exact file list).
What compiles it is the quack feature, off by default: make debug passes it on through
TARGET_INFO += --features quack in the root Makefile, and just build / just build_wasm do
the same. A bare cargo build skips the target silently and hands back a cdylib with no entry-point
symbol, which DuckDB only rejects at LOAD. A dependency on duckfn is unaffected: the sources
are in the package, the feature is off, and the dependency tree is unchanged. Use
cargo test -p duckfn for the runtime's own tests, or cargo test --workspace for everything.
How this repository's lib differs from a plugin's
Two things follow from the example living inside the library, and both differ from what an extension project does:
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
crate-type cannot be overridden per target, and the package is taken as three things:
cdylib for the native extension DuckDB loads, staticlib for WebAssembly (where emcc does the
final link and wants a .a), and rlib for dependents. Listing all three is also why no
[[example]] wasm root is needed — the example tree is compiled once, by the library, so the entry
symbol and the inventory registrations exist exactly once. A second copy would register every
function twice, and on wasm the duplicate entry symbol fails to link outright.
Two naming details follow from the same arrangement:
- The entry symbol lives alone in
test/extension/entry.rs. The library provides it, and the CLI links that library while re-includingextension/mod.rsthrough#[path]— defining it a second time there would be a duplicate definition, an outrightLNK2005on Windows. - The CLI's target is
duckfn-cli, though its file issrc/bin/duckfn.rs: this package's cdylib also produces aduckfnartefact and the two Windows.pdbfiles would collide. A downstream project has a different package name, no collision, and keeps the plainduckfn.
Day-to-day commands
make debug # build the extension
just sql "SELECT double_it5(21);" # rebuild and run one statement
make test # run the sqllogictest suite
just doc # build the rustdoc for duckfn
make test runs make configure debug test through just test.
.cargo/config.toml statically links the C runtime on x86_64-pc-windows-msvc; nothing else needs
to be configured per platform.
Debugging
The extension code runs inside the duckdb process, so attach the debugger to that process
instead of launching something yourself:
- Build with debug symbols —
make debug, orjust build(which runsmake configure && make debug). - Start DuckDB and keep the session alive, for example
duckdb -unsigned. LOAD '/path/to/my_ext.duckdb_extension';in that session.- In the IDE, attach to the running
duckdbprocess — in RustRover that is Attach to process. - Set a breakpoint in your function and run the SQL that calls it, e.g.
SELECT double_it(21);.
The shared library only enters the process at LOAD, so a breakpoint set earlier starts resolving
from that point on. just sql "<SQL>" is a quick way to run one statement by hand while the
debugger is attached.
Tests
DuckDB's own sqllogictest runner is the practical choice: it exercises the extension through SQL,
exactly the way DuckDB calls it, and it is what CI runs. It needs the make flow to be set up
(make configure once, then make test).
Tests are sqllogictest files under test/sql/, mirroring the source layout:
test/sql/demo/ <source file>.test
test/sql/functions/ <source file>.test
test/sql/types/ <type>_scalar_echo.test, <type>_table_echo.test
A file starts by requiring the extension, then pairs SQL with its expected output:
require duckfn
query I
SELECT double_it5(21);
----
42
statement error
SELECT CAST('abc' AS INTEGER);
----
not an integer: "abc"
When you add a function, add the matching .test file: the expected values there are what the
documentation quotes, so they are the source of truth for behaviour. Type codes used in the files
include I (integer), T (text), R (real) and combinations such as IT; list, map, struct and
array values are compared as text after CAST(… AS VARCHAR).
Documentation
The site lives in docs/. Every English page under docs/docs/ needs its Simplified Chinese
counterpart at the same path under
docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/:
- Translate the body and the reader-facing front matter (
title,description). - Keep
sidebar_positionidentical so both sidebars stay in the same order. - Link between pages with relative file paths (
./types.md,../guide/types.md) so each language links to its own pages.
cd docs
npm start # http://localhost:3000
npm start -- --locale zh-Hans
npm run build # must pass for both locales; broken links fail the build
npm test # run every runnable SQL block in DuckDB-Wasm
npm test is the docs' own test suite: it collects each runnable block and runs
it with the site's extension preloaded, so a renamed function or a changed default
shows up here instead of in a reader's browser. A block that demonstrates a
failure has to declare it ("expect": "error"), otherwise it counts as breakage —
see Testing the examples.
Conventions
- Match the style of the surrounding code. The workspace is not
rustfmt-clean today, so runningcargo fmtacross the tree would rewrite files unrelated to your change — format only what you touched. Keepcargo clippyquiet. - Error messages start with the name of the function that produced them, e.g.
dfn_table_checked: n must be >= 0. - User-facing code stays free of
unsafe; the only accepted exceptions are the explicit registration paths, which needunsafe { c.register_scalar(…) }and friends. - New attribute arguments go into the argument struct of the macro that actually needs them
(
duckfn-macro/src/<macro>.rs); each macro declares only its own keys and no longer forwards its arguments to the derive macros. - When behaviour changes, update the sqllogictest expectation first, then the docs page that quotes
it, then the READMEs (
README.mdandREADME.zh-CN.md).
Next
- Architecture — where the code you are about to change lives.
- Build and release — the CI and release flow.