Skip to main content

Contributing

Prerequisites​

Rust1.86 or newer — the workspace sets rust-version = "1.86" and uses edition 2024.
Python 3 + networkOnly for make configure, which builds the sqllogictest runner's virtualenv.
makeDrives the DuckDB extension-ci-tools makefiles.
just (optional)The Justfile wraps the common commands.
DuckDB CLIFor 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​

MemberPublishedNotes
/ (duckfn)yesThe runtime framework, the example extension and the workspace root.
duckfn-macro/yesThe procedural macros; depends on the runtime for nothing, only on darling, syn, quote.
test/extension/, test/sql/shipped, never compiledThe 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-including extension/mod.rs through #[path] — defining it a second time there would be a duplicate definition, an outright LNK2005 on Windows.
  • The CLI's target is duckfn-cli, though its file is src/bin/duckfn.rs: this package's cdylib also produces a duckfn artefact and the two Windows .pdb files would collide. A downstream project has a different package name, no collision, and keeps the plain duckfn.

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:

  1. Build with debug symbols — make debug, or just build (which runs make configure && make debug).
  2. Start DuckDB and keep the session alive, for example duckdb -unsigned.
  3. LOAD '/path/to/my_ext.duckdb_extension'; in that session.
  4. In the IDE, attach to the running duckdb process — in RustRover that is Attach to process.
  5. 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_position identical 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 running cargo fmt across the tree would rewrite files unrelated to your change — format only what you touched. Keep cargo clippy quiet.
  • 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 need unsafe { 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.md and README.zh-CN.md).

Next​