Skip to main content

better_duck_core/udf/table/
mod.rs

1//! DuckDB table functions: functions used in a `FROM` clause that produce rows
2//! and columns, e.g. `SELECT * FROM my_func(1, 2)`.
3
4mod function;
5mod row;
6
7use std::ffi::CString;
8
9use crate::{
10    connection::Connection,
11    error::Result,
12    ffi::{duckdb_bind_info, duckdb_data_chunk, duckdb_function_info, duckdb_init_info},
13};
14
15use self::function::TableFunction;
16pub use self::function::{BindInfo, InitInfo, TableFunctionInfo};
17pub use self::row::{run_table_func, TableInitData, TableRow};
18use super::{callback::contain_callback, data_chunk::DataChunkHandle, logical_type::LogicalType};
19
20/// A DuckDB table function: produces rows and columns for use in a `FROM`
21/// clause, e.g. `SELECT * FROM my_func(1, 2)`.
22///
23/// See the callback containment contract in [`crate::udf`].
24pub trait VTab: Sized {
25    /// Data produced once by [`VTab::bind`] and shared, read-only, by every
26    /// later call to [`VTab::init`] and [`VTab::func`] for this query.
27    type BindData: Send + Sync;
28
29    /// Data produced once by [`VTab::init`], shared across every worker thread
30    /// executing this query. Any interior mutation must be synchronized.
31    type InitData: Send + Sync;
32
33    /// The positional parameter types this function accepts, in order. Empty by
34    /// default (no positional parameters).
35    ///
36    /// # Errors
37    ///
38    /// Returns an error if a parameter's logical type cannot be built.
39    fn parameters() -> Result<Vec<LogicalType>> {
40        Ok(Vec::new())
41    }
42
43    /// Determines the function's output schema (via
44    /// [`BindInfo::add_result_column`]) and produces the data shared by every
45    /// later call for this query.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error to fail the query with that message.
50    fn bind(bind: &BindInfo) -> super::UdfResult<Self::BindData>;
51
52    /// Produces data shared across every worker thread executing this query,
53    /// e.g. the initial position of a cursor.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error to fail the query with that message.
58    fn init(init: &InitInfo<Self>) -> super::UdfResult<Self::InitData>;
59
60    /// Writes up to `output.capacity()` rows into `output`, then calls
61    /// `output.set_len(k)` with the number actually written. Called repeatedly
62    /// until `set_len(0)` (or `output.len() == 0` on entry, if untouched) signals
63    /// the scan is complete.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error to fail the query with that message.
68    fn func(
69        func: &TableFunctionInfo<Self>,
70        output: &mut DataChunkHandle,
71    ) -> super::UdfResult<()>;
72}
73
74/// The C trampoline installed via `duckdb_table_function_set_bind`.
75///
76/// See [`scalar_trampoline`](super::scalar) for why containment must be the
77/// outermost thing here.
78unsafe extern "C" fn table_bind_trampoline<T: VTab>(info: duckdb_bind_info) {
79    let bind = BindInfo::from(info);
80    contain_callback(&bind, || {
81        let data = T::bind(&bind)?;
82        bind.set_bind_data(data);
83        Ok(())
84    });
85}
86
87/// The C trampoline installed via `duckdb_table_function_set_init`.
88unsafe extern "C" fn table_init_trampoline<T: VTab>(info: duckdb_init_info) {
89    let init = InitInfo::<T>::from(info);
90    contain_callback(&init, || {
91        let data = T::init(&init)?;
92        init.set_init_data(data);
93        Ok(())
94    });
95}
96
97/// The C trampoline installed via `duckdb_table_function_set_function`.
98unsafe extern "C" fn table_func_trampoline<T: VTab>(
99    info: duckdb_function_info,
100    output: duckdb_data_chunk,
101) {
102    let func = TableFunctionInfo::<T>::from(info);
103    contain_callback(&func, || {
104        // SAFETY: DuckDB owns `output` and guarantees it stays live and
105        // unmutated by other code for the duration of this call.
106        let mut chunk = unsafe { DataChunkHandle::borrowed(output) };
107        T::func(&func, &mut chunk)
108    });
109}
110
111impl Connection {
112    /// Registers `T` as a table function named `name`.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if `name` contains a NUL byte, a parameter's logical
117    /// type cannot be built, or DuckDB rejects the registration (e.g. a name
118    /// conflict).
119    pub fn register_table_function<T: VTab>(
120        &mut self,
121        name: &str,
122    ) -> Result<()> {
123        let c_name = CString::new(name)?;
124        let f = TableFunction::new(&c_name);
125        for param in T::parameters()? {
126            f.add_parameter(&param);
127        }
128        f.set_bind(Some(table_bind_trampoline::<T>));
129        f.set_init(Some(table_init_trampoline::<T>));
130        f.set_function(Some(table_func_trampoline::<T>));
131        f.register(self.raw_con(), name)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::connection::Connection;
139    use crate::types::value::DuckValue;
140
141    /// `series(start, stop)`: the integers in `[start, stop)`, one per row,
142    /// exercising `bind` (result schema + cardinality), `init` (cursor state
143    /// shared across the scan), and chunked `func` output.
144    struct Series;
145
146    struct SeriesBind {
147        start: i64,
148        stop: i64,
149    }
150
151    struct SeriesInit {
152        next: std::sync::atomic::AtomicI64,
153    }
154
155    impl VTab for Series {
156        type BindData = SeriesBind;
157        type InitData = SeriesInit;
158
159        fn parameters() -> Result<Vec<LogicalType>> {
160            Ok(vec![LogicalType::of::<i64>()?, LogicalType::of::<i64>()?])
161        }
162
163        fn bind(bind: &BindInfo) -> super::super::UdfResult<Self::BindData> {
164            bind.add_result_column("n", &LogicalType::of::<i64>()?)?;
165            let start: i64 = bind.get_parameter(0)?;
166            let stop: i64 = bind.get_parameter(1)?;
167            bind.set_cardinality((stop - start).max(0) as u64, true);
168            Ok(SeriesBind { start, stop })
169        }
170
171        fn init(init: &InitInfo<Self>) -> super::super::UdfResult<Self::InitData> {
172            Ok(SeriesInit { next: std::sync::atomic::AtomicI64::new(init.bind_data().start) })
173        }
174
175        fn func(
176            func: &TableFunctionInfo<Self>,
177            output: &mut DataChunkHandle,
178        ) -> super::super::UdfResult<()> {
179            let stop = func.bind_data().stop;
180            let init = func.init_data();
181            let cap = output.capacity();
182            let mut written = 0usize;
183            {
184                let mut col = output.vector_mut(0)?;
185                while written < cap {
186                    let n = init.next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
187                    if n >= stop {
188                        init.next.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
189                        break;
190                    }
191                    col.set(written, n)?;
192                    written += 1;
193                }
194            }
195            output.set_len(written)?;
196            Ok(())
197        }
198    }
199
200    #[test]
201    fn register_and_scan_table_function() {
202        let mut conn = Connection::open_in_memory().unwrap();
203        conn.register_table_function::<Series>("series").unwrap();
204        let result = conn.execute("SELECT n FROM series(1, 6) ORDER BY n").unwrap();
205        let rows: Vec<_> = result.collect::<Result<_>>().unwrap();
206        let got: Vec<i64> = rows
207            .iter()
208            .map(|r| match r.get("n").unwrap() {
209                DuckValue::BigInt(n) => *n,
210                other => panic!("expected BigInt, got {other:?}"),
211            })
212            .collect();
213        assert_eq!(got, vec![1, 2, 3, 4, 5]);
214    }
215
216    #[test]
217    fn aggregate_over_table_function_matches_expected_sum() {
218        let mut conn = Connection::open_in_memory().unwrap();
219        conn.register_table_function::<Series>("series").unwrap();
220        let mut result = conn.execute("SELECT sum(n) AS total FROM series(1, 101)").unwrap();
221        let row = result.next().unwrap().unwrap();
222        assert_eq!(row.get("total"), Some(&DuckValue::HugeInt(5050)));
223    }
224
225    /// A large enough range to force multiple chunks through `func`, proving the
226    /// cursor state in `InitData` survives across repeated calls.
227    #[test]
228    fn scan_spanning_multiple_chunks() {
229        let mut conn = Connection::open_in_memory().unwrap();
230        conn.register_table_function::<Series>("series").unwrap();
231        let result = conn.execute("SELECT count(*) AS n FROM series(0, 10000)").unwrap();
232        let rows: Vec<_> = result.collect::<Result<_>>().unwrap();
233        assert_eq!(rows[0].get("n"), Some(&DuckValue::BigInt(10000)));
234    }
235
236    /// A `VTab` whose `bind` always errors, verifying it surfaces as a normal
237    /// query error and the connection stays usable afterward.
238    struct AlwaysFailsBind;
239    impl VTab for AlwaysFailsBind {
240        type BindData = ();
241        type InitData = ();
242
243        fn bind(_bind: &BindInfo) -> super::super::UdfResult<Self::BindData> {
244            Err("deliberate bind failure".into())
245        }
246
247        fn init(_init: &InitInfo<Self>) -> super::super::UdfResult<Self::InitData> {
248            Ok(())
249        }
250
251        fn func(
252            _func: &TableFunctionInfo<Self>,
253            output: &mut DataChunkHandle,
254        ) -> super::super::UdfResult<()> {
255            output.set_len(0)?;
256            Ok(())
257        }
258    }
259
260    #[test]
261    fn bind_error_surfaces_as_query_error_and_connection_stays_usable() {
262        let mut conn = Connection::open_in_memory().unwrap();
263        conn.register_table_function::<AlwaysFailsBind>("always_fails_bind").unwrap();
264        let err = match conn.execute("SELECT * FROM always_fails_bind()") {
265            Ok(_) => panic!("expected an error"),
266            Err(e) => e,
267        };
268        assert!(err.to_string().contains("deliberate bind failure"), "{err}");
269        conn.execute_batch("CREATE TABLE t (v INTEGER)").unwrap();
270    }
271}