better_duck_core/udf/table/
mod.rs1mod 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
20pub trait VTab: Sized {
25 type BindData: Send + Sync;
28
29 type InitData: Send + Sync;
32
33 fn parameters() -> Result<Vec<LogicalType>> {
40 Ok(Vec::new())
41 }
42
43 fn bind(bind: &BindInfo) -> super::UdfResult<Self::BindData>;
51
52 fn init(init: &InitInfo<Self>) -> super::UdfResult<Self::InitData>;
59
60 fn func(
69 func: &TableFunctionInfo<Self>,
70 output: &mut DataChunkHandle,
71 ) -> super::UdfResult<()>;
72}
73
74unsafe 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
87unsafe 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
97unsafe 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 let mut chunk = unsafe { DataChunkHandle::borrowed(output) };
107 T::func(&func, &mut chunk)
108 });
109}
110
111impl Connection {
112 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(¶m);
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 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 #[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 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}