better_duck_core/udf/table/function.rs
1//! RAII builders and callback-info wrappers around the `duckdb_table_function`/
2//! `duckdb_bind_info`/`duckdb_init_info`/`duckdb_function_info` C API.
3
4use std::{
5 ffi::{CStr, CString},
6 marker::PhantomData,
7};
8
9use crate::{
10 error::{Error, Result},
11 ffi::{
12 duckdb_bind_add_result_column, duckdb_bind_get_extra_info, duckdb_bind_get_named_parameter,
13 duckdb_bind_get_parameter, duckdb_bind_get_parameter_count, duckdb_bind_info,
14 duckdb_bind_set_bind_data, duckdb_bind_set_cardinality, duckdb_bind_set_error,
15 duckdb_connection, duckdb_create_table_function, duckdb_destroy_table_function,
16 duckdb_destroy_value, duckdb_function_get_bind_data, duckdb_function_get_extra_info,
17 duckdb_function_get_init_data, duckdb_function_get_local_init_data, duckdb_function_info,
18 duckdb_function_set_error, duckdb_init_get_bind_data, duckdb_init_get_column_count,
19 duckdb_init_get_column_index, duckdb_init_get_extra_info, duckdb_init_info,
20 duckdb_init_set_error, duckdb_init_set_init_data, duckdb_init_set_max_threads,
21 duckdb_register_table_function, duckdb_table_function,
22 duckdb_table_function_add_named_parameter, duckdb_table_function_add_parameter,
23 duckdb_table_function_bind_t, duckdb_table_function_get_client_context,
24 duckdb_table_function_init_t, duckdb_table_function_set_bind,
25 duckdb_table_function_set_extra_info, duckdb_table_function_set_function,
26 duckdb_table_function_set_init, duckdb_table_function_set_local_init,
27 duckdb_table_function_set_name, duckdb_table_function_supports_projection_pushdown,
28 duckdb_table_function_t,
29 },
30 types::DuckDialect,
31};
32
33use super::super::{
34 callback::{drop_boxed, CallbackErrorSink},
35 LogicalType,
36};
37use super::VTab;
38
39/// An in-progress table function, built up via its setters and registered.
40pub(crate) struct TableFunction {
41 ptr: duckdb_table_function,
42}
43
44impl TableFunction {
45 pub(crate) fn new(name: &CStr) -> Self {
46 // SAFETY: always safe to call; returns a freshly allocated, empty function.
47 let ptr = unsafe { duckdb_create_table_function() };
48 // SAFETY: `ptr` was just allocated above.
49 unsafe { duckdb_table_function_set_name(ptr, name.as_ptr()) };
50 Self { ptr }
51 }
52
53 pub(crate) fn add_parameter(
54 &self,
55 ty: &LogicalType,
56 ) {
57 // SAFETY: `self.ptr` is valid; `ty.as_raw()` is a valid logical type owned
58 // by `ty` for the duration of this call, which copies it.
59 unsafe { duckdb_table_function_add_parameter(self.ptr, ty.as_raw()) };
60 }
61
62 pub(crate) fn set_bind(
63 &self,
64 f: duckdb_table_function_bind_t,
65 ) {
66 // SAFETY: `self.ptr` is valid; `f`, if `Some`, is a valid `extern "C"`
67 // function pointer with the expected signature.
68 unsafe { duckdb_table_function_set_bind(self.ptr, f) };
69 }
70
71 pub(crate) fn set_init(
72 &self,
73 f: duckdb_table_function_init_t,
74 ) {
75 // SAFETY: `self.ptr` is valid; `f`, if `Some`, is a valid `extern "C"`
76 // function pointer with the expected signature.
77 unsafe { duckdb_table_function_set_init(self.ptr, f) };
78 }
79
80 pub(crate) fn set_function(
81 &self,
82 f: duckdb_table_function_t,
83 ) {
84 // SAFETY: `self.ptr` is valid; `f`, if `Some`, is a valid `extern "C"`
85 // function pointer with the expected signature.
86 unsafe { duckdb_table_function_set_function(self.ptr, f) };
87 }
88
89 /// Registers the thread-local ("local") init callback, invoked once per
90 /// worker thread that executes this table function, in addition to the
91 /// single global [`set_init`](Self::set_init). Its result is retrievable
92 /// via [`TableFunctionInfo::local_init_data`] from that same thread.
93 pub(crate) fn set_local_init(
94 &self,
95 f: duckdb_table_function_init_t,
96 ) {
97 // SAFETY: `self.ptr` is valid; `f`, if `Some`, is a valid `extern "C"`
98 // function pointer with the expected signature (the C API reuses
99 // `duckdb_table_function_init_t` for both the global and local init
100 // callbacks).
101 unsafe { duckdb_table_function_set_local_init(self.ptr, f) };
102 }
103
104 /// Declares a named (keyword) parameter in addition to the positional ones
105 /// added via [`add_parameter`](Self::add_parameter).
106 ///
107 /// # Errors
108 ///
109 /// Returns an error if `name` contains a NUL byte.
110 pub(crate) fn add_named_parameter(
111 &self,
112 name: &str,
113 ty: &LogicalType,
114 ) -> Result<()> {
115 let c_name = CString::new(name)?;
116 // SAFETY: `self.ptr` is valid; `c_name` is a valid NUL-terminated C
117 // string for the duration of this call; `ty.as_raw()` is valid and
118 // copied by this function.
119 unsafe {
120 duckdb_table_function_add_named_parameter(self.ptr, c_name.as_ptr(), ty.as_raw())
121 };
122 Ok(())
123 }
124
125 /// Declares whether this function can consume a projected column list
126 /// (i.e. honors [`InitInfo::column_indices`] to skip writing columns the
127 /// query doesn't need). Defaults to `false`.
128 pub(crate) fn set_supports_projection_pushdown(
129 &self,
130 supports: bool,
131 ) {
132 // SAFETY: `self.ptr` is valid.
133 unsafe { duckdb_table_function_supports_projection_pushdown(self.ptr, supports) };
134 }
135
136 /// Stores `info`, retrievable via [`BindInfo::extra_info`]/
137 /// [`InitInfo::extra_info`]/[`TableFunctionInfo::extra_info`]. Freed
138 /// automatically when DuckDB drops the catalog entry.
139 pub(crate) fn set_extra_info<T: Send + Sync + 'static>(
140 &self,
141 info: T,
142 ) {
143 let ptr = Box::into_raw(Box::new(info)).cast::<std::ffi::c_void>();
144 // SAFETY: `ptr` was just created by `Box::into_raw::<T>` above;
145 // `drop_boxed::<T>` frees it with the matching type, exactly once, when
146 // DuckDB calls the delete callback.
147 unsafe { duckdb_table_function_set_extra_info(self.ptr, ptr, Some(drop_boxed::<T>)) };
148 }
149
150 /// Registers this function with `con`.
151 ///
152 /// # Errors
153 ///
154 /// Returns an error if registration fails, e.g. a name conflict, or if the
155 /// function is missing a required piece (bind/init/function callback).
156 pub(crate) fn register(
157 &self,
158 con: duckdb_connection,
159 name: &str,
160 ) -> Result<()> {
161 // SAFETY: `con` is a valid connection handle; `self.ptr` is a valid,
162 // fully configured table function.
163 let rc = unsafe { duckdb_register_table_function(con, self.ptr) };
164 if rc != crate::ffi::DuckDBSuccess {
165 return Err(Error::DuckDBFailure(
166 crate::ffi::Error::new(rc),
167 Some(format!(
168 "failed to register table function `{name}` (name conflict, or invalid signature)"
169 )),
170 ));
171 }
172 Ok(())
173 }
174}
175
176impl Drop for TableFunction {
177 fn drop(&mut self) {
178 if !self.ptr.is_null() {
179 // SAFETY: `self.ptr` is a valid, non-null table function allocated by
180 // `duckdb_create_table_function` and not yet destroyed; destroyed
181 // exactly once here. `duckdb_register_table_function` copies it, so
182 // destroying our own handle afterward is correct.
183 unsafe { duckdb_destroy_table_function(&mut self.ptr) };
184 }
185 }
186}
187
188/// An interface to store and retrieve data during the table function's bind stage.
189pub struct BindInfo {
190 ptr: duckdb_bind_info,
191}
192
193impl BindInfo {
194 pub(crate) fn from(ptr: duckdb_bind_info) -> Self {
195 Self { ptr }
196 }
197
198 /// Adds a result column to the output schema of the table function.
199 ///
200 /// # Errors
201 ///
202 /// Returns an error if `column_name` contains a NUL byte.
203 pub fn add_result_column(
204 &self,
205 column_name: &str,
206 column_type: &LogicalType,
207 ) -> Result<()> {
208 let c_name = CString::new(column_name)?;
209 // SAFETY: `self.ptr` is valid; `c_name` is a valid, NUL-terminated C
210 // string for the duration of this call; `column_type.as_raw()` is valid
211 // and copied by this function.
212 unsafe { duckdb_bind_add_result_column(self.ptr, c_name.as_ptr(), column_type.as_raw()) };
213 Ok(())
214 }
215
216 /// The number of positional parameters passed to this call of the function.
217 pub fn parameter_count(&self) -> u64 {
218 // SAFETY: `self.ptr` is valid.
219 unsafe { duckdb_bind_get_parameter_count(self.ptr) }
220 }
221
222 /// The [`ClientContext`](crate::ClientContext) of the connection executing this bind, exposing its
223 /// stable connection id. The returned context borrows `self`, so it cannot
224 /// outlive the bind call. Returns `None` if no context is available.
225 #[must_use]
226 pub fn client_context(&self) -> Option<crate::raw::client_context::ClientContext<'_>> {
227 let mut ctx: crate::ffi::duckdb_client_context = std::ptr::null_mut();
228 // SAFETY: `self.ptr` is a valid bind info; `ctx` is a valid out-pointer DuckDB
229 // writes an owned client-context handle into.
230 unsafe { duckdb_table_function_get_client_context(self.ptr, &mut ctx) };
231 // SAFETY: `ctx` is null or an owned handle whose owner (this bind call)
232 // outlives the returned borrow.
233 unsafe { crate::raw::client_context::ClientContext::from_raw(ctx) }
234 }
235
236 /// Reads the positional parameter at `index` as `T`.
237 ///
238 /// # Errors
239 ///
240 /// Returns an error if `index` is out of range or the parameter cannot be
241 /// converted to `T`.
242 pub fn get_parameter<T: DuckDialect>(
243 &self,
244 index: u64,
245 ) -> Result<T> {
246 // SAFETY: `self.ptr` is valid; `index` is caller-guaranteed to be within
247 // `[0, parameter_count())`. The returned value is owned by us and
248 // destroyed below.
249 let mut value = unsafe { duckdb_bind_get_parameter(self.ptr, index) };
250 if value.is_null() {
251 return Err(Error::InvalidColumnIndex(index as usize));
252 }
253 let result = T::from_duck(value).map_err(Error::ConversionError);
254 // SAFETY: `value` was allocated by `duckdb_bind_get_parameter` above and
255 // is destroyed exactly once here.
256 unsafe { duckdb_destroy_value(&mut value) };
257 result
258 }
259
260 /// Reads the named (keyword) parameter `name` as `T`.
261 ///
262 /// Unlike [`get_parameter`](Self::get_parameter), a missing named
263 /// parameter is not an error — named parameters are optional by nature —
264 /// so this returns `Ok(None)` when `name` wasn't passed by the caller.
265 ///
266 /// # Errors
267 ///
268 /// Returns an error if `name` contains a NUL byte, or if the parameter was
269 /// provided but cannot be converted to `T`.
270 pub fn get_named_parameter<T: DuckDialect>(
271 &self,
272 name: &str,
273 ) -> Result<Option<T>> {
274 let c_name = CString::new(name)?;
275 // SAFETY: `self.ptr` is valid; `c_name` is a valid, NUL-terminated C
276 // string for the duration of this call. The returned value, if
277 // non-null, is owned by us and destroyed below.
278 let mut value = unsafe { duckdb_bind_get_named_parameter(self.ptr, c_name.as_ptr()) };
279 if value.is_null() {
280 return Ok(None);
281 }
282 let result = T::from_duck(value).map_err(Error::ConversionError).map(Some);
283 // SAFETY: `value` was allocated by `duckdb_bind_get_named_parameter`
284 // above (checked non-null) and is destroyed exactly once here.
285 unsafe { duckdb_destroy_value(&mut value) };
286 result
287 }
288
289 /// The registration-time "extra info" set via a table function's
290 /// `set_extra_info`, if any, shared read-only across every call to
291 /// `bind`/`init`/`func` for this function.
292 ///
293 /// Returns `None` if no extra info was configured for this function.
294 ///
295 /// # Safety
296 ///
297 /// The caller must request the same `T` that was stored at registration
298 /// time; there is no runtime type check.
299 pub fn extra_info<T>(&self) -> Option<&T> {
300 // SAFETY: `self.ptr` is valid for the duration of the callback.
301 let raw = unsafe { duckdb_bind_get_extra_info(self.ptr) };
302 if raw.is_null() {
303 return None;
304 }
305 // SAFETY: `raw`, if non-null, was produced by
306 // `TableFunction::set_extra_info::<T>` for this same registration (the
307 // only place that sets it), and is kept alive by DuckDB until the
308 // catalog entry is dropped, which outlives every call using it. The
309 // caller is responsible for requesting the matching `T`.
310 Some(unsafe { &*raw.cast::<T>() })
311 }
312
313 /// Sets the estimated (or exact) number of rows this call will produce, used
314 /// by the query optimizer.
315 pub fn set_cardinality(
316 &self,
317 cardinality: u64,
318 is_exact: bool,
319 ) {
320 // SAFETY: `self.ptr` is valid.
321 unsafe { duckdb_bind_set_cardinality(self.ptr, cardinality, is_exact) };
322 }
323
324 /// Stores `data`, retrievable during init/execution via
325 /// [`TableFunctionInfo::bind_data`] and [`InitInfo::bind_data`].
326 pub(crate) fn set_bind_data<T: Send + Sync>(
327 &self,
328 data: T,
329 ) {
330 let ptr = Box::into_raw(Box::new(data)).cast::<std::ffi::c_void>();
331 // SAFETY: `ptr` was just created by `Box::into_raw::<T>` above;
332 // `drop_boxed::<T>` frees it with the matching type, exactly once, when
333 // DuckDB destroys the bind data (at the end of the query, or on bind
334 // failure).
335 unsafe { duckdb_bind_set_bind_data(self.ptr, ptr, Some(drop_boxed::<T>)) };
336 }
337}
338
339impl CallbackErrorSink for BindInfo {
340 fn set_c_error(
341 &self,
342 error: &CStr,
343 ) {
344 // SAFETY: `self.ptr` is valid for the duration of the callback; `error`
345 // is a valid, NUL-terminated C string.
346 unsafe { duckdb_bind_set_error(self.ptr, error.as_ptr()) };
347 }
348}
349
350/// An interface to store and retrieve data during the table function's init stage.
351pub struct InitInfo<V: VTab> {
352 ptr: duckdb_init_info,
353 _marker: PhantomData<fn() -> V>,
354}
355
356impl<V: VTab> InitInfo<V> {
357 pub(crate) fn from(ptr: duckdb_init_info) -> Self {
358 Self { ptr, _marker: PhantomData }
359 }
360
361 /// The bind data produced by [`VTab::bind`] for this call.
362 ///
363 /// Read-only: for tracking state across calls to the execution callback,
364 /// store it in the init data instead.
365 pub fn bind_data(&self) -> &V::BindData {
366 // SAFETY: `self.ptr` is valid for the duration of the callback.
367 let raw = unsafe { duckdb_init_get_bind_data(self.ptr) };
368 // SAFETY: `raw` was produced by `BindInfo::set_bind_data::<V::BindData>`
369 // for this same registration (the only place that calls it), and has not
370 // been freed (guaranteed by DuckDB: it outlives every init/execute call
371 // for this query).
372 unsafe { &*raw.cast::<V::BindData>() }
373 }
374
375 /// Stores `data`, retrievable during execution via [`TableFunctionInfo::init_data`].
376 pub(crate) fn set_init_data(
377 &self,
378 data: V::InitData,
379 ) {
380 let ptr = Box::into_raw(Box::new(data)).cast::<std::ffi::c_void>();
381 // SAFETY: `ptr` was just created by `Box::into_raw::<V::InitData>`
382 // above; `drop_boxed::<V::InitData>` frees it with the matching type,
383 // exactly once, when DuckDB destroys the init data.
384 unsafe { duckdb_init_set_init_data(self.ptr, ptr, Some(drop_boxed::<V::InitData>)) };
385 }
386
387 /// Stores `data` as this worker thread's *local* init data, retrievable
388 /// during execution on the same thread via
389 /// [`TableFunctionInfo::local_init_data`].
390 ///
391 /// Only meaningful when called from the local-init callback registered via
392 /// `VTabLocalInit` — DuckDB itself distinguishes the global vs. local init
393 /// data by which callback set it, both through this same C entry point.
394 pub(crate) fn set_local_init_data<T: Send + Sync>(
395 &self,
396 data: T,
397 ) {
398 let ptr = Box::into_raw(Box::new(data)).cast::<std::ffi::c_void>();
399 // SAFETY: `ptr` was just created by `Box::into_raw::<T>` above;
400 // `drop_boxed::<T>` frees it with the matching type, exactly once, when
401 // DuckDB destroys this thread's local init data.
402 unsafe { duckdb_init_set_init_data(self.ptr, ptr, Some(drop_boxed::<T>)) };
403 }
404
405 /// Sets how many threads may call the execution callback for this query in
406 /// parallel. Defaults to `1` (single-threaded execution).
407 pub fn set_max_threads(
408 &self,
409 max_threads: u64,
410 ) {
411 // SAFETY: `self.ptr` is valid.
412 unsafe { duckdb_init_set_max_threads(self.ptr, max_threads) };
413 }
414
415 /// The 0-based indices, into the function's full result schema, of the
416 /// columns the query actually needs — only meaningful when the function
417 /// declared `supports_projection_pushdown()`. Empty if pushdown wasn't
418 /// requested or the function didn't opt in, in which case every column
419 /// should be written.
420 pub fn column_indices(&self) -> Vec<usize> {
421 // SAFETY: `self.ptr` is valid for the duration of the callback.
422 let count = unsafe { duckdb_init_get_column_count(self.ptr) };
423 (0..count)
424 .map(|i| {
425 // SAFETY: `self.ptr` is valid; `i` is within `[0, count)`.
426 unsafe { duckdb_init_get_column_index(self.ptr, i) as usize }
427 })
428 .collect()
429 }
430
431 /// The registration-time "extra info", if any — see [`BindInfo::extra_info`].
432 ///
433 /// # Safety
434 ///
435 /// The caller must request the same `T` that was stored at registration
436 /// time; there is no runtime type check.
437 pub fn extra_info<T>(&self) -> Option<&T> {
438 // SAFETY: `self.ptr` is valid for the duration of the callback.
439 let raw = unsafe { duckdb_init_get_extra_info(self.ptr) };
440 if raw.is_null() {
441 return None;
442 }
443 // SAFETY: see `BindInfo::extra_info` — same invariants, same source.
444 Some(unsafe { &*raw.cast::<T>() })
445 }
446}
447
448impl<V: VTab> CallbackErrorSink for InitInfo<V> {
449 fn set_c_error(
450 &self,
451 error: &CStr,
452 ) {
453 // SAFETY: `self.ptr` is valid for the duration of the callback; `error`
454 // is a valid, NUL-terminated C string.
455 unsafe { duckdb_init_set_error(self.ptr, error.as_ptr()) };
456 }
457}
458
459/// An interface to store and retrieve data during the table function's
460/// execution stage.
461pub struct TableFunctionInfo<V: VTab> {
462 ptr: duckdb_function_info,
463 _marker: PhantomData<fn() -> V>,
464}
465
466impl<V: VTab> TableFunctionInfo<V> {
467 pub(crate) fn from(ptr: duckdb_function_info) -> Self {
468 Self { ptr, _marker: PhantomData }
469 }
470
471 /// The bind data produced by [`VTab::bind`] for this call. Read-only.
472 pub fn bind_data(&self) -> &V::BindData {
473 // SAFETY: `self.ptr` is valid for the duration of the callback.
474 let raw = unsafe { duckdb_function_get_bind_data(self.ptr) };
475 // SAFETY: `raw` was produced by `BindInfo::set_bind_data::<V::BindData>`
476 // for this same registration, and has not been freed (guaranteed by
477 // DuckDB: it outlives every execute call for this query).
478 unsafe { &*raw.cast::<V::BindData>() }
479 }
480
481 /// The init data produced by [`VTab::init`] for this call. Shared across
482 /// every worker thread executing this query, so any interior mutation must
483 /// be synchronized.
484 pub fn init_data(&self) -> &V::InitData {
485 // SAFETY: `self.ptr` is valid for the duration of the callback.
486 let raw = unsafe { duckdb_function_get_init_data(self.ptr) };
487 // SAFETY: `raw` was produced by `InitInfo::set_init_data::<V::InitData>`
488 // for this same registration, and has not been freed (guaranteed by
489 // DuckDB: it outlives every execute call for this query).
490 unsafe { &*raw.cast::<V::InitData>() }
491 }
492
493 /// The data produced by `VTabLocalInit::local_init` for *this worker
494 /// thread*, if the function registered a local-init callback. Returns
495 /// `None` if no local-init callback was registered.
496 ///
497 /// # Safety
498 ///
499 /// The caller must request the same `T` that was stored by the local-init
500 /// callback; there is no runtime type check.
501 pub fn local_init_data<T>(&self) -> Option<&T> {
502 // SAFETY: `self.ptr` is valid for the duration of the callback.
503 let raw = unsafe { duckdb_function_get_local_init_data(self.ptr) };
504 if raw.is_null() {
505 return None;
506 }
507 // SAFETY: `raw`, if non-null, was produced by
508 // `InitInfo::set_local_init_data::<T>` on this same worker thread, and
509 // has not been freed (guaranteed by DuckDB: it outlives every execute
510 // call on this thread for this query). The caller is responsible for
511 // requesting the matching `T`.
512 Some(unsafe { &*raw.cast::<T>() })
513 }
514
515 /// The registration-time "extra info", if any — see [`BindInfo::extra_info`].
516 ///
517 /// # Safety
518 ///
519 /// The caller must request the same `T` that was stored at registration
520 /// time; there is no runtime type check.
521 pub fn extra_info<T>(&self) -> Option<&T> {
522 // SAFETY: `self.ptr` is valid for the duration of the callback.
523 let raw = unsafe { duckdb_function_get_extra_info(self.ptr) };
524 if raw.is_null() {
525 return None;
526 }
527 // SAFETY: see `BindInfo::extra_info` — same invariants, same source.
528 Some(unsafe { &*raw.cast::<T>() })
529 }
530}
531
532impl<V: VTab> CallbackErrorSink for TableFunctionInfo<V> {
533 fn set_c_error(
534 &self,
535 error: &CStr,
536 ) {
537 // SAFETY: `self.ptr` is valid for the duration of the callback; `error`
538 // is a valid, NUL-terminated C string.
539 unsafe { duckdb_function_set_error(self.ptr, error.as_ptr()) };
540 }
541}