Skip to main content

better_duck_core/udf/scalar/
mod.rs

1//! DuckDB scalar functions: row-wise functions used in a `SELECT` list or
2//! `WHERE` clause, e.g. `SELECT my_func(x) FROM t`.
3
4mod function;
5
6use std::ffi::CString;
7
8use crate::{
9    connection::Connection,
10    error::Result,
11    ffi::{duckdb_data_chunk, duckdb_function_info, duckdb_vector},
12};
13
14use self::function::{ScalarFunction, ScalarFunctionInfo, ScalarFunctionSet};
15use super::{
16    callback::contain_callback, data_chunk::DataChunkHandle, logical_type::LogicalType,
17    vector::VectorMut,
18};
19
20/// A DuckDB scalar function: computes one value per row.
21///
22/// See the callback containment contract in [`crate::udf`].
23pub trait VScalar: Sized {
24    /// State set at registration time, shared across every invocation and every
25    /// worker thread. Persists for the lifetime of the catalog entry, so it must
26    /// be `'static`; any interior mutation must be synchronized.
27    type State: Send + Sync + 'static;
28
29    /// The possible signatures of this function. Each becomes a DuckDB overload;
30    /// [`VScalar::invoke`] must be able to handle every one of them.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if a signature's logical type cannot be built.
35    fn signatures() -> Result<Vec<ScalarSignature>>;
36
37    /// Computes `output[row]` for every `row` in `0..input.len()`.
38    ///
39    /// DuckDB guarantees `input` and `output` stay live for the duration of this
40    /// call, and that `output`'s capacity is at least `input.len()`.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error to fail the query with that message.
45    fn invoke(
46        state: &Self::State,
47        input: &DataChunkHandle,
48        output: &mut VectorMut<'_>,
49    ) -> super::UdfResult<()>;
50
51    /// Whether this function is volatile — re-evaluated for every row even with
52    /// no parameters, rather than optimized to a constant. Needed for functions
53    /// like random-number or UUID generators.
54    fn volatile() -> bool {
55        false
56    }
57
58    /// Whether this function should be invoked for rows containing `NULL`
59    /// parameters. By default DuckDB substitutes `NULL` as the result for any
60    /// row with a `NULL` argument without calling [`VScalar::invoke`] at all;
61    /// returning `true` here disables that shortcut.
62    fn special_handling() -> bool {
63        false
64    }
65}
66
67/// The parameter shape of one [`ScalarSignature`].
68enum ScalarParams {
69    /// A fixed list of parameter types.
70    Exact(Vec<LogicalType>),
71    /// Any number of arguments of a single type.
72    Variadic(LogicalType),
73}
74
75/// One overload of a scalar function: a parameter shape and a return type.
76pub struct ScalarSignature {
77    parameters: Option<ScalarParams>,
78    return_type: LogicalType,
79}
80
81impl ScalarSignature {
82    /// A signature with a fixed list of parameter types.
83    pub fn exact(
84        parameters: Vec<LogicalType>,
85        return_type: LogicalType,
86    ) -> Self {
87        Self { parameters: Some(ScalarParams::Exact(parameters)), return_type }
88    }
89
90    /// A signature accepting any number of arguments of `parameter`'s type.
91    pub fn variadic(
92        parameter: LogicalType,
93        return_type: LogicalType,
94    ) -> Self {
95        Self { parameters: Some(ScalarParams::Variadic(parameter)), return_type }
96    }
97
98    fn apply(
99        &self,
100        f: &ScalarFunction,
101    ) {
102        f.set_return_type(&self.return_type);
103        match &self.parameters {
104            Some(ScalarParams::Exact(params)) => {
105                for p in params {
106                    f.add_parameter(p);
107                }
108            },
109            Some(ScalarParams::Variadic(p)) => f.set_varargs(p),
110            None => {},
111        }
112    }
113}
114
115/// The C trampoline installed via `duckdb_scalar_function_set_function`.
116///
117/// Nothing above the `contain_callback` call may panic or carry a `Drop` impl:
118/// since Rust 1.81 a panic escaping a plain `extern "C"` frame aborts the
119/// process regardless of panic strategy, so containment must be the outermost
120/// thing here. See [`crate::udf`] for the full `panic = "abort"` caveat.
121unsafe extern "C" fn scalar_trampoline<S: VScalar>(
122    info: duckdb_function_info,
123    input: duckdb_data_chunk,
124    output: duckdb_vector,
125) {
126    let sink = ScalarFunctionInfo::from(info);
127    contain_callback(&sink, || {
128        // SAFETY: DuckDB owns `input` and guarantees it stays live and unmutated
129        // by other code for the duration of this call.
130        let chunk = unsafe { DataChunkHandle::borrowed(input) };
131        // SAFETY: DuckDB owns `output`, guarantees it stays live for the
132        // duration of this call, and that no other code writes through it
133        // concurrently.
134        let mut out = unsafe { VectorMut::new(output) };
135        // SAFETY: `register_scalar_function`/`register_scalar_function_with_state`
136        // always call `ScalarFunction::set_extra_info::<S::State>`, so the state
137        // stored for this catalog entry always has type `S::State`.
138        let state = unsafe { sink.state::<S::State>() };
139        S::invoke(state, &chunk, &mut out)
140    });
141}
142
143impl Connection {
144    /// Registers `S` as a scalar function named `name`, using `S::State`'s
145    /// default value as the shared state for every overload.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if `name` contains a NUL byte, a signature's logical
150    /// type cannot be built, or DuckDB rejects the registration (e.g. a name
151    /// conflict).
152    pub fn register_scalar_function<S: VScalar>(
153        &mut self,
154        name: &str,
155    ) -> Result<()>
156    where
157        S::State: Default,
158    {
159        // A fresh default per overload, rather than one shared value cloned —
160        // avoids requiring `S::State: Clone` in addition to `Default`.
161        register_scalar_function_impl::<S>(self, name, S::State::default)
162    }
163
164    /// Registers `S` as a scalar function named `name`, with explicit shared
165    /// state. The state is cloned once per overload.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if `name` contains a NUL byte, a signature's logical
170    /// type cannot be built, or DuckDB rejects the registration (e.g. a name
171    /// conflict).
172    pub fn register_scalar_function_with_state<S: VScalar>(
173        &mut self,
174        name: &str,
175        state: S::State,
176    ) -> Result<()>
177    where
178        S::State: Clone,
179    {
180        register_scalar_function_impl::<S>(self, name, move || state.clone())
181    }
182}
183
184fn register_scalar_function_impl<S: VScalar>(
185    conn: &mut Connection,
186    name: &str,
187    mut make_state: impl FnMut() -> S::State,
188) -> Result<()> {
189    let c_name = CString::new(name)?;
190    let set = ScalarFunctionSet::new(&c_name);
191    for signature in S::signatures()? {
192        let f = ScalarFunction::new(&c_name);
193        signature.apply(&f);
194        f.set_function(Some(scalar_trampoline::<S>));
195        if S::volatile() {
196            f.set_volatile();
197        }
198        if S::special_handling() {
199            f.set_special_handling();
200        }
201        f.set_extra_info(make_state());
202        set.add_function(&f)?;
203    }
204    set.register(conn.raw_con(), name)
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::connection::Connection;
211
212    /// A hand-written `VScalar`, exercising the trait directly (not through the
213    /// `#[duckdb_scalar]` macro, which lands in a later phase).
214    struct AddOne;
215
216    impl VScalar for AddOne {
217        type State = ();
218
219        fn signatures() -> Result<Vec<ScalarSignature>> {
220            Ok(vec![ScalarSignature::exact(
221                vec![LogicalType::of::<i32>()?],
222                LogicalType::of::<i32>()?,
223            )])
224        }
225
226        fn invoke(
227            _state: &(),
228            input: &DataChunkHandle,
229            output: &mut VectorMut<'_>,
230        ) -> super::super::UdfResult<()> {
231            let col = input.vector(0)?;
232            for row in 0..input.len() {
233                let v: i32 = col.get(row)?;
234                output.set(row, v + 1)?;
235            }
236            Ok(())
237        }
238    }
239
240    #[test]
241    fn register_and_call_scalar_function() {
242        let mut conn = Connection::open_in_memory().unwrap();
243        conn.register_scalar_function::<AddOne>("add_one").unwrap();
244        conn.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
245        conn.execute_batch("INSERT INTO t VALUES (1), (2), (41)").unwrap();
246        let result = conn.execute("SELECT add_one(v) AS r FROM t ORDER BY v").unwrap();
247        let rows: Vec<_> = result.collect::<Result<_>>().unwrap();
248        assert_eq!(rows.len(), 3);
249        assert_eq!(rows[2].get("r"), Some(&crate::types::value::DuckValue::Int(42)));
250    }
251
252    /// A `VScalar` whose `invoke` always errors, verifying the error surfaces as
253    /// a normal query error and the connection stays usable afterward.
254    struct AlwaysFails;
255
256    impl VScalar for AlwaysFails {
257        type State = ();
258
259        fn signatures() -> Result<Vec<ScalarSignature>> {
260            Ok(vec![ScalarSignature::exact(
261                vec![LogicalType::of::<i32>()?],
262                LogicalType::of::<i32>()?,
263            )])
264        }
265
266        fn invoke(
267            _state: &(),
268            _input: &DataChunkHandle,
269            _output: &mut VectorMut<'_>,
270        ) -> super::super::UdfResult<()> {
271            Err("deliberate failure".into())
272        }
273    }
274
275    #[test]
276    fn invoke_error_surfaces_as_query_error_and_connection_stays_usable() {
277        let mut conn = Connection::open_in_memory().unwrap();
278        conn.register_scalar_function::<AlwaysFails>("always_fails").unwrap();
279        conn.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
280        conn.execute_batch("INSERT INTO t VALUES (1)").unwrap();
281        let err = match conn.execute("SELECT always_fails(v) FROM t") {
282            Ok(_) => panic!("expected an error"),
283            Err(e) => e,
284        };
285        assert!(err.to_string().contains("deliberate failure"), "{err}");
286        // The connection must still be usable after a UDF error.
287        conn.execute_batch("INSERT INTO t VALUES (2)").unwrap();
288    }
289
290    /// A panicking `VScalar`, verifying panic containment.
291    struct AlwaysPanics;
292
293    impl VScalar for AlwaysPanics {
294        type State = ();
295
296        fn signatures() -> Result<Vec<ScalarSignature>> {
297            Ok(vec![ScalarSignature::exact(
298                vec![LogicalType::of::<i32>()?],
299                LogicalType::of::<i32>()?,
300            )])
301        }
302
303        fn invoke(
304            _state: &(),
305            _input: &DataChunkHandle,
306            _output: &mut VectorMut<'_>,
307        ) -> super::super::UdfResult<()> {
308            panic!("deliberate panic")
309        }
310    }
311
312    #[test]
313    #[cfg(panic = "unwind")]
314    fn invoke_panic_is_contained_and_connection_stays_usable() {
315        let mut conn = Connection::open_in_memory().unwrap();
316        conn.register_scalar_function::<AlwaysPanics>("always_panics").unwrap();
317        conn.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
318        conn.execute_batch("INSERT INTO t VALUES (1)").unwrap();
319        let err = match conn.execute("SELECT always_panics(v) FROM t") {
320            Ok(_) => panic!("expected an error"),
321            Err(e) => e,
322        };
323        assert!(err.to_string().contains("deliberate panic"), "{err}");
324        conn.execute_batch("INSERT INTO t VALUES (2)").unwrap();
325    }
326}