Skip to main content

Community extension docs

Every community extension gets a page on duckdb.org/community_extensions that is generated from the extension binary: DuckDB loads it, diffs duckdb_functions() before and after, and renders what the extension added.

That last part is the catch — the page shows whatever the catalog reports, and DuckDB's C extension API has no way to set a function's description or examples. It exposes duckdb_scalar_function_set_name, _set_return_type, _set_varargs, _set_volatile, _set_special_handling, _set_extra_info, _set_bind and _set_function, and that is it. There is no duckdb_scalar_function_set_description, no _add_example. So a duckfn extension cannot carry that text into the catalog, and without help the "Added Functions" table on its page is a list of bare names.

The way out is a CSV. duckdb/community-extensions' scripts/generate_md.sh looks for extensions/<name>/docs/function_descriptions.csv and, when it is there, LEFT JOINs it on function_name == other.function to override the description, comment and examples columns of both functions and functions_overloads. Rows that match nothing are ignored, so the file is purely an override table.

Write the text on the attribute​

duckfn lets you keep that text next to the function it describes:

use duckfn::duck_scalar_function;

/// Doubles an INTEGER.
#[duck_scalar_function(
description = "Doubles an INTEGER",
comment = "NULL in, NULL out",
examples = ["SELECT double_it(21)", "SELECT double_it(x) FROM t"]
)]
pub fn double_it(v: Option<i64>) -> Option<i64> {
v.map(|x| x * 2)
}

Three keys, all optional:

KeyCSV columnNotes
descriptiondescriptionOne-line summary. This is what shows up in the table.
commentcommentExtra remarks, shown alongside the description.
example / examplesexampleOne example (example = "SELECT ...") or several (examples = ["...", "..."]). The two are mutually exclusive — writing both is a compile error.

They work on every attribute that registers a function: #[duck_scalar_function], #[duck_aggregate_function], #[duck_table_function], #[duck_cast_function], #[duck_copy_function], #[duck_copy_from_function] and #[duck_sql_macro]. #[duck_replacement_scan] and #[duck_custom_register] do not take them, because nothing named after the Rust function ends up in the catalog.

Three things worth knowing about the text itself:

  • Several examples are joined with "; ", and a trailing ; on each is dropped. The CSV's example column is a single string — the generator only wraps it in [...], it never splits it — and real SQL is full of commas (FROM (VALUES (1, 'a'), (2, NULL)) v(i, s)), so a comma join would be unreadable. "; " is SQL's own statement separator, and dropping the trailing semicolon keeps ["SELECT 1", "SELECT 2;"] from coming out as SELECT 1; SELECT 2;;.
  • Line breaks collapse to a single space when the CSV is written. The generated page is a Markdown table, where a newline inside a cell ends the row, and DuckDB's read_csv() reads a bare newline inside a quoted field back as \r\n anyway. Commas, double quotes and non-ASCII text pass through unchanged; only newlines are flattened.
  • Curly braces are your problem to write and the generator's to escape. generate_md.sh runs both columns through a jekyll_format_function macro that wraps every {{ and }} in {% raw %}…{% endraw %}, so example = "SELECT f('{{x}}')" is fine as written.

The attributes do not touch registration — they are collected into an inventory entry and only used when the CSV is exported.

Generate the CSV​

cargo run --bin duckfn -- function_descriptions # -> target/function_descriptions.csv
cargo run --bin duckfn -- function_descriptions --all # -> target/function_descriptions_all.csv

or, if you prefer just:

just docs_csv

The default export writes one row per function that has a description, comment or example. --all writes every registered function instead, leaving the columns empty for the ones you have not documented yet — handy as a checklist, and the file name gets an _all suffix so the two do not collide. The path is always <project>/target/, fixed, so nothing downstream has to guess where to find it.

This is a plain in-memory operation over what the macros recorded at compile time: no extension is loaded, duckdb_functions() is never queried, and DuckDB is not involved at all.

Setting it up in a new project​

Two pieces, both copied from the duckfn repository:

  1. Cargo.toml — turn on duckfn's cli feature:

    duckfn = { version = "x.y.z", features = ["duckdb-1-5", "cli"] }
  2. src/bin/duckfn.rs — the entry point:

    //! duckfn 命令行工具入口:cargo run --bin duckfn -- function_descriptions
    #[path = "../extension/mod.rs"]
    mod extension;

    fn main() -> std::process::ExitCode {
    duckfn::cli::run(env!("CARGO_MANIFEST_DIR"))
    }

    The #[path] inclusion is deliberate: #[duck_*] metadata is collected by inventory's static constructors, which only fire for object files that are really linked into the final binary. Merely depending on the library lets the linker drop those modules, and the CSV would come out empty without any error.

Submit it​

The file is read from the community-extensions repository, not from your own: when you open the pull request that adds extensions/<name>/description.yml, add extensions/<name>/docs/function_descriptions.csv next to it. Keeping a copy in your own repo is handy for regeneration — re-run the command whenever you add a function.