Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nimdeveloper/better-duck/llms.txt

Use this file to discover all available pages before exploring further.

Table user-defined functions produce rows and columns and are used in a FROM clause — SELECT * FROM my_func(arg1, arg2). Unlike scalar functions, which compute one value per row, a table function is an iterator: it is driven lazily by DuckDB in chunks and can return any number of rows. With better-duck’s udf feature, the #[duckdb_table_function] attribute macro turns any Rust function returning impl Iterator<Item = T> + Send into a fully registered DuckDB table function. No unsafe code, no manual BindInfo/InitInfo plumbing, and no manual vector handling is required.

Enabling UDF Support

Table functions, like scalar functions, require the udf feature:
[dependencies]
better-duck-core = { version = "0.1.0-beta.4", features = ["udf"] }

Basic Table Function

Annotate a function that returns impl Iterator<Item = T> + Send with #[duckdb_table_function]. Call <fn_name>::register(&mut conn) before querying. Use the columns(...) option to name the output column(s); for a single-column function it defaults to the SQL function name.
use better_duck_core::{connection::Connection, duckdb_table_function};

#[duckdb_table_function(name = "series", columns("n"))]
fn series(start: i64, stop: i64) -> impl Iterator<Item = i64> + Send {
    start..stop
}

let mut conn = Connection::open_in_memory()?;
series::register(&mut conn)?;

let mut result = conn.execute("SELECT sum(n) AS total FROM series(1, 101)")?;
// total = 5050
The iterator is driven lazily in DuckDB’s internal chunk size (typically 2048 rows). Large iterators spanning multiple chunks work correctly — the init callback boxes the iterator once and the func callback advances it on each pull.
Like #[duckdb_scalar], the original Rust function is preserved and remains directly callable. The generated <fn_name> module exposing register is placed alongside it in the same scope.

Named (Keyword) Parameters

Declare one or more function parameters as SQL named (keyword) parameters with the named_params(...) option. Named parameters are bound with := syntax in SQL, not by position. A non-Option named parameter is required; an Option<T> named parameter is optional (returns None when omitted).
use better_duck_core::{connection::Connection, duckdb_table_function};

/// `start` is positional; `step` is a required SQL named (keyword) parameter.
#[duckdb_table_function(columns("n"), named_params("step"))]
fn stepped(start: i64, step: i64) -> impl Iterator<Item = i64> + Send {
    (0..5).map(move |i| start + i * step)
}

let mut conn = Connection::open_in_memory()?;
stepped::register(&mut conn)?;

let result = conn.execute("SELECT n FROM stepped(10, step := 2) ORDER BY n")?;
// n = 10, 12, 14, 16, 18
Names in named_params(...) must exactly match the Rust parameter identifiers — the macro validates this at compile time and emits a descriptive error if they don’t match.
An optional named parameter uses Option<T> in the Rust signature. If the caller omits it, the parameter receives None. A non-Option named parameter that is omitted fails the query with a "missing required named parameter" message naming the absent field.

Projection Pushdown

Add the projection_pushdown flag to tell DuckDB that this function honors the optimizer’s request to skip computing columns the query doesn’t need. Inside the function body, call duck_projection!() to retrieve a Vec<usize> of the 0-based column indices the current query actually wants.
use better_duck_core::{connection::Connection, duck_projection, duckdb_table_function};

#[duckdb_table_function(columns("a", "b"), projection_pushdown)]
fn two_cols() -> impl Iterator<Item = (i32, i32)> + Send {
    let wanted_columns = duck_projection!(); // Vec<usize> — indices into ["a", "b"]
    // Produce values only for the columns in `wanted_columns`.
    // Here we always produce both for simplicity.
    let _ = wanted_columns;
    std::iter::once((1, 2))
}

let mut conn = Connection::open_in_memory()?;
two_cols::register(&mut conn)?;

conn.execute("SELECT a FROM two_cols()")?;    // DuckDB may push down [0]
conn.execute("SELECT a, b FROM two_cols()")?; // DuckDB requests both columns
duck_projection!() returns an empty Vec when DuckDB’s optimizer does not trigger pushdown for a given query shape. Always treat an empty list as “all columns are needed”. The projection_pushdown flag must be set for the returned list to ever be non-empty; without it, duck_projection!() will always return an empty Vec.

Extra Info with duck_extra_info!

Use extra_info(Type, init_expr) to attach shared, read-only context to a table function at registration time. The value is initialized once with init_expr and is accessible inside the function body via duck_extra_info!(Type). The type must be Clone.
use better_duck_core::{connection::Connection, duck_extra_info, duckdb_table_function};

#[duckdb_table_function(columns("n"), extra_info(i64, 100))]
fn with_extra_info() -> impl Iterator<Item = i64> + Send {
    let limit = duck_extra_info!(i64); // 100
    (0..limit)
}

let mut conn = Connection::open_in_memory()?;
with_extra_info::register(&mut conn)?;

let result = conn.execute("SELECT count(*) AS c FROM with_extra_info()")?;
// c = 100
Extra info is scoped to the registration: every call to the function on any connection sharing the same database sees the same value. Unlike duck_state! (which is for scalar functions), duck_extra_info! is for table functions.

Multi-Column Tables

Return tuples from the iterator to produce multiple columns. Name them with columns("col1", "col2", ...). The number of names must exactly match the tuple arity — the macro validates this at compile time.
use better_duck_core::{connection::Connection, duckdb_table_function};

/// Splits text on whitespace, yielding (index, word) pairs.
#[duckdb_table_function(columns("idx", "word"))]
fn words(text: String) -> impl Iterator<Item = (i64, String)> + Send {
    text.split_whitespace()
        .map(String::from)
        .enumerate()
        .map(|(i, w)| (i as i64, w))
        .collect::<Vec<_>>()
        .into_iter()
}

let mut conn = Connection::open_in_memory()?;
words::register(&mut conn)?;

let result = conn.execute(
    "SELECT idx, word FROM words('hello there world') ORDER BY idx"
)?;
// (0, "hello"), (1, "there"), (2, "world")
Column types are inferred from the Rust types in the tuple — the same DuckLogicalType inference used for scalar parameters and return types applies here.

NULL and Error Handling

1

NULL items

Yield Option<T> items (or Option<(A, B)> for multi-column rows) to produce NULL values. A None item sets the entire row’s columns to NULL.
2

Fallible bind

Return Result<impl Iterator<Item = T> + Send, E> instead of a bare iterator to fail the query at bind time (before any rows are pulled). This is useful for argument validation:
#[duckdb_table_function(name = "checked_series", columns("n"))]
fn checked_series(
    start: i64,
    stop: i64,
) -> Result<impl Iterator<Item = i64> + Send, String> {
    if stop < start {
        return Err(format!("stop ({stop}) must be >= start ({start})"));
    }
    Ok(start..stop)
}
On Err, the query fails with the error message. The connection remains fully usable.
varargs (variable-arity table functions) and LIST/STRUCT column types are not yet supported via the #[duckdb_table_function] macro. For those cases, implement the VTab trait directly.

Complete Attribute Reference

OptionDescription
name = "sql_name"Override the SQL function name (defaults to the Rust function name)
crate = ::pathRe-export escape hatch for the generated code’s ::better_duck_core path
columns("a", "b", ...)Output column names (defaults: the function name for one column, column_0, column_1, … for several)
named_params("a", "b")Bind the named Rust parameters as SQL keyword parameters instead of positional ones
projection_pushdownDeclare that this function honors duck_projection!() for optimizer-driven column skipping
extra_info(Type, init_expr)Attach read-only registration-time context, readable inside the body via duck_extra_info!(Type)

Build docs developers (and LLMs) love