Skip to main content

better_duck_core/udf/
mod.rs

1//! User-defined DuckDB functions: scalar functions and table functions.
2//!
3//! Requires the `udf` feature.
4//!
5//! Two kinds of function can be registered on a [`Connection`](crate::connection::Connection):
6//!
7//! - **Scalar functions** compute one value per row, for use in a `SELECT` list
8//!   or `WHERE` clause: `SELECT my_func(x) FROM t`.
9//! - **Table functions** produce rows and columns, for use in a `FROM` clause:
10//!   `SELECT * FROM my_func(1, 2)`.
11//!
12//! The [`duckdb_scalar`](crate::duckdb_scalar) and
13//! [`duckdb_table_function`](crate::duckdb_table_function) attribute macros turn
14//! an ordinary Rust function into either kind without requiring any `unsafe` code
15//! or manual vector handling. Parameter and return types are inferred from the
16//! Rust signature via [`types::DuckLogicalType`](crate::types::DuckLogicalType) —
17//! any type that already round-trips through [`AppendAble`](crate::AppendAble)
18//! (all the integer widths, floats, `String`/`&str`, `bool`, …) works here too,
19//! with no extra type table for the macros to maintain.
20//!
21//! # Scalar functions
22//!
23//! ```
24//! use better_duck_core::{connection::Connection, duckdb_scalar};
25//!
26//! /// Repeats `s` `n` times.
27//! #[duckdb_scalar]
28//! fn repeat_str(s: &str, n: i32) -> String {
29//!     s.repeat(n.max(0) as usize)
30//! }
31//!
32//! /// `Option` parameters/returns propagate `NULL` explicitly.
33//! #[duckdb_scalar]
34//! fn double_or_null(x: Option<i32>) -> Option<i32> {
35//!     x.map(|v| v * 2)
36//! }
37//!
38//! /// A `Result<T, E>` return fails the query with `E`'s message on `Err`.
39//! #[duckdb_scalar(name = "to_int")]
40//! fn parse_int(s: &str) -> Result<i32, std::num::ParseIntError> {
41//!     s.parse()
42//! }
43//!
44//! # fn main() -> better_duck_core::error::Result<()> {
45//! let mut conn = Connection::open_in_memory()?;
46//! repeat_str::register(&mut conn)?;
47//! parse_int::register(&mut conn)?;
48//!
49//! let mut result = conn.execute("SELECT repeat_str('ab', 3) AS r")?;
50//! assert_eq!(result.next().unwrap()?.get("r"), Some(&better_duck_core::types::value::DuckValue::text("ababab")));
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! # Table functions
56//!
57//! The function returns `impl Iterator<Item = T> + Send` — `T` for a single
58//! column, or a tuple `(A, B, ...)` for several. DuckDB pulls rows in chunks, so
59//! the iterator is driven lazily and may be scanned across several calls.
60//!
61//! ```
62//! use better_duck_core::{connection::Connection, duckdb_table_function};
63//!
64//! /// The integers in `[start, stop)`.
65//! #[duckdb_table_function(name = "series", columns("n"))]
66//! fn series(start: i64, stop: i64) -> impl Iterator<Item = i64> + Send {
67//!     start..stop
68//! }
69//!
70//! # fn main() -> better_duck_core::error::Result<()> {
71//! let mut conn = Connection::open_in_memory()?;
72//! series::register(&mut conn)?;
73//! let mut result = conn.execute("SELECT sum(n) AS total FROM series(1, 101)")?;
74//! assert_eq!(result.next().unwrap()?.get("total"), Some(&better_duck_core::types::value::DuckValue::HugeInt(5050)));
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! # Attribute options
80//!
81//! | option | scalar | table | meaning |
82//! |---|---|---|---|
83//! | `name = "sql_name"` | yes | yes | the SQL function name (defaults to the Rust fn's name) |
84//! | `crate = ::path` | yes | yes | re-export escape hatch for generated code's `::better_duck_core` path |
85//! | `volatile` | yes | — | disables DuckDB's zero-argument constant-folding |
86//! | `state(Type, init_expr)` | yes | — | shared `VScalar::State`, readable via [`duck_state!`] |
87//! | `columns("a", "b")` | — | yes | result column names (defaults: the fn name for one column, `column_0`, `column_1`, … for several) |
88//! | `named_params("a", "b")` | — | yes | binds the named fn parameters as SQL named (keyword) parameters instead of positional; a non-`Option` field becomes a required named parameter, an `Option<T>` field an optional one |
89//! | `projection_pushdown` | — | yes | declares that this function honors [`duck_projection!`] to skip work for columns the query doesn't need |
90//! | `extra_info(Type, init_expr)` | — | yes | shared, registration-time context, readable via [`duck_extra_info!`] |
91//!
92//! `special_handling` (whether `NULL` inputs still invoke the function) is
93//! inferred automatically: present whenever any parameter is `Option<T>`.
94//!
95//! # Panics and `panic = "abort"`
96//!
97//! Callback panics are caught and reported to DuckDB as a query error whenever
98//! the crate is built with `panic = "unwind"` (the default for `dev` and `test`
99//! profiles). Under a `panic = "abort"` build — this workspace's own `release`
100//! profile, for instance — a panicking user-defined function aborts the process
101//! instead: `catch_unwind` cannot catch anything once unwinding itself has been
102//! compiled out. Prefer returning `Err` from a fallible user-defined function
103//! over panicking; the ordinary error path never depends on unwinding.
104#![allow(clippy::not_unsafe_ptr_arg_deref)]
105
106/// DuckDB scalar functions: row-wise functions used in a `SELECT` list or
107/// `WHERE` clause.
108pub mod aggregate;
109pub(crate) mod callback;
110pub mod cast;
111mod context;
112mod data_chunk;
113/// DuckDB replacement scans: rewrite an unresolved table reference into a
114/// table function call. **Experimental** — see the module docs.
115pub mod replacement;
116
117pub mod scalar;
118/// DuckDB table functions: functions used in a `FROM` clause that produce rows
119/// and columns.
120pub mod table;
121mod vector;
122
123/// An owned DuckDB logical type handle.
124pub use crate::types::LogicalType;
125/// The trait behind DuckDB scalar functions, and its signature type.
126pub use aggregate::{AggregateSetBuilder, VAggregate};
127pub use cast::VCast;
128/// An owned-or-borrowed DuckDB data chunk.
129pub use data_chunk::DataChunkHandle;
130/// The trait behind DuckDB replacement scans, and its callback-info type.
131/// **Experimental** — see the [`replacement`] module docs.
132pub use replacement::{ReplacementScan, ReplacementScanInfo};
133
134pub use scalar::{ScalarBindInfo, ScalarSignature, VScalar};
135/// Opt-in trait for a [`VTab`] with per-worker-thread ("local") init data.
136pub use table::VTabLocalInit;
137/// The row type produced by a `#[duckdb_table_function]`-generated function,
138/// and the shared init-data/execution-loop helpers it compiles down to.
139pub use table::{run_table_func, TableInitData, TableRow};
140/// The trait behind DuckDB table functions, and its callback-info types.
141pub use table::{BindInfo, InitInfo, TableFunctionInfo, VTab};
142/// Read and write views over a single column of a [`DataChunkHandle`], plus the
143/// [`ScalarArg`]/[`ScalarRet`] marshalling traits.
144pub use vector::{ScalarArg, ScalarRet, VectorMut, VectorRef};
145
146/// The error type returned by user-defined-function callbacks.
147///
148/// `+ Send + Sync` because callbacks may run on DuckDB worker threads.
149pub type UdfResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
150
151/// Returns early from a fallible user-defined-function body with a formatted
152/// error message.
153///
154/// Expands to `return Err(format!(...).into())` — the surrounding function's
155/// error type must implement `From<String>`, which is true for `String` itself
156/// and for `Box<dyn std::error::Error + Send + Sync>` (what `#[duckdb_scalar]`/
157/// `#[duckdb_table_function]` route a `Result<T, E>` return through).
158///
159/// # Examples
160///
161/// ```
162/// use better_duck_core::duck_bail;
163///
164/// fn check(stop: i64, start: i64) -> Result<i64, String> {
165///     if stop < start {
166///         duck_bail!("stop ({stop}) must be >= start ({start})");
167///     }
168///     Ok(stop - start)
169/// }
170/// assert!(check(1, 10).is_err());
171/// ```
172#[macro_export]
173macro_rules! duck_bail {
174    ($($arg:tt)*) => {
175        return Err(::std::format!($($arg)*).into())
176    };
177}
178
179/// Returns the 0-based column indices the current query actually needs, as a
180/// `Vec<usize>`, from inside a `#[duckdb_table_function(projection_pushdown)]`
181/// function body.
182///
183/// Empty if the query didn't request pushdown (every column is wanted).
184///
185/// # Panics
186///
187/// Panics if called outside a table function's `init` callback — contained by
188/// this crate's callback machinery, so it always surfaces as a normal query
189/// error, never undefined behavior.
190///
191/// # Examples
192///
193/// ```
194/// use better_duck_core::{connection::Connection, duck_projection, duckdb_table_function};
195///
196/// #[duckdb_table_function(columns("a", "b"), projection_pushdown)]
197/// fn two_cols() -> impl Iterator<Item = (i32, i32)> + Send {
198///     let wanted = duck_projection!();
199///     // `wanted` lists which of columns 0 (`a`) / 1 (`b`) the query needs.
200///     let _ = wanted;
201///     std::iter::once((1, 2))
202/// }
203/// # fn main() -> better_duck_core::error::Result<()> {
204/// let mut conn = Connection::open_in_memory()?;
205/// two_cols::register(&mut conn)?;
206/// # Ok(())
207/// # }
208/// ```
209#[macro_export]
210macro_rules! duck_projection {
211    () => {
212        $crate::udf::__private::current_projection()
213    };
214}
215
216/// Returns a clone of the current table function's registration-time "extra
217/// info" (see [`Connection::register_table_function_with_extra_info`](crate::connection::Connection::register_table_function_with_extra_info)),
218/// from inside a `#[duckdb_table_function]` function body.
219///
220/// `Type` must be `Clone` — the macro always returns an independently owned
221/// value, never a reference into ambient context.
222///
223/// # Panics
224///
225/// Panics if called outside a table function callback registered with extra
226/// info, or if `Type` doesn't match what was actually registered — contained
227/// by this crate's callback machinery, so it always surfaces as a normal
228/// query error, never undefined behavior.
229#[macro_export]
230macro_rules! duck_extra_info {
231    ($ty:ty) => {
232        $crate::udf::__private::current_table_extra_info::<$ty>()
233    };
234}
235
236/// Returns a clone of the current scalar function's `State`, from inside a
237/// `#[duckdb_scalar(state = ...)]` function body.
238///
239/// `Type` must be `Clone` — the macro always returns an independently owned
240/// value, never a reference into ambient context.
241///
242/// # Panics
243///
244/// Panics if called outside a scalar function callback, or if `Type` doesn't
245/// match the function's declared state type — contained by this crate's
246/// callback machinery, so it always surfaces as a normal query error, never
247/// undefined behavior.
248#[macro_export]
249macro_rules! duck_state {
250    ($ty:ty) => {
251        $crate::udf::__private::current_scalar_state::<$ty>()
252    };
253}
254
255/// Items referenced by code generated by the `#[duckdb_scalar]`/
256/// `#[duckdb_table_function]` attribute macros.
257///
258/// Not part of the public API: renamed or removed without notice. Referenced by
259/// generated code as `::better_duck_core::udf::__private::…` so that a user
260/// crate shadowing e.g. `Result` cannot break macro expansion.
261#[doc(hidden)]
262pub mod __private {
263    pub use crate::connection::Connection;
264    pub use crate::error::Result;
265    pub use crate::types::DuckLogicalType;
266    pub use crate::udf::context::{
267        current_projection, current_scalar_state, current_table_extra_info, ProjectionGuard,
268        ScalarStateGuard, TableExtraInfoGuard,
269    };
270    pub use crate::udf::{
271        run_table_func, BindInfo, DataChunkHandle, InitInfo, LogicalType, ScalarArg,
272        ScalarBindInfo, ScalarRet, ScalarSignature, TableFunctionInfo, TableInitData, TableRow,
273        UdfResult, VScalar, VTab, VTabLocalInit, VectorMut, VectorRef,
274    };
275    pub use std::boxed::Box;
276    pub use std::result::Result as StdResult;
277    pub use std::vec::Vec;
278
279    /// Boxes any suitable error into the UDF error type. Used by generated code
280    /// for a user function returning `Result<T, E>`.
281    pub fn boxed_error<E>(e: E) -> Box<dyn std::error::Error + Send + Sync + 'static>
282    where
283        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
284    {
285        e.into()
286    }
287}