Skip to main content

quack_rs/table/
init_data.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! Type-safe init data management for table functions.
7//!
8//! `DuckDB` table functions have two init phases:
9//!
10//! - **Global init** (`init`): Called once per query. Use [`FfiInitData`] to store
11//!   global scan state (e.g., a file handle, row counter shared across threads).
12//! - **Local init** (`local_init`): Called once per thread. Use [`FfiLocalInitData`]
13//!   to store per-thread scan state (e.g., a thread-local buffer or offset).
14//!
15//! # Example
16//!
17//! ```rust,no_run
18//! use quack_rs::table::{FfiInitData, FfiLocalInitData};
19//! use libduckdb_sys::{duckdb_init_info, duckdb_function_info};
20//!
21//! struct GlobalState { rows_remaining: u64 }
22//! struct LocalState  { thread_offset: u64 }
23//!
24//! unsafe extern "C" fn my_init(info: duckdb_init_info) {
25//!     unsafe { FfiInitData::<GlobalState>::set(info, GlobalState { rows_remaining: 1000 }); }
26//! }
27//!
28//! unsafe extern "C" fn my_local_init(info: duckdb_init_info) {
29//!     unsafe { FfiLocalInitData::<LocalState>::set(info, LocalState { thread_offset: 0 }); }
30//! }
31//!
32//! unsafe extern "C" fn my_scan(info: duckdb_function_info, _output: libduckdb_sys::duckdb_data_chunk) {
33//!     let _global = unsafe { FfiInitData::<GlobalState>::get(info) };
34//!     let _local  = unsafe { FfiLocalInitData::<LocalState>::get(info) };
35//! }
36//! ```
37
38use std::os::raw::c_void;
39
40use libduckdb_sys::{
41    duckdb_function_get_init_data, duckdb_function_get_local_init_data, duckdb_function_info,
42    duckdb_init_info, duckdb_init_set_init_data,
43};
44
45/// Type-safe global init data for `DuckDB` table functions.
46///
47/// Set in the global `init` callback; retrieved in `scan`.
48pub struct FfiInitData<T: 'static> {
49    _marker: std::marker::PhantomData<T>,
50}
51
52impl<T: 'static> FfiInitData<T> {
53    /// Stores `data` as the global init data for this query.
54    ///
55    /// Call inside your global `init` callback.
56    ///
57    /// # Safety
58    ///
59    /// - `info` must be a valid `duckdb_init_info`.
60    /// - Must be called at most once per init invocation.
61    pub unsafe fn set(info: duckdb_init_info, data: T) {
62        let raw = Box::into_raw(Box::new(data)).cast::<c_void>();
63        // SAFETY: info is valid; raw is a heap allocation; destroy is a valid fn pointer.
64        unsafe {
65            duckdb_init_set_init_data(info, raw, Some(Self::destroy));
66        }
67    }
68
69    /// Retrieves a shared reference to the global init data from a scan callback.
70    ///
71    /// Returns `None` if no init data was set.
72    ///
73    /// # Safety
74    ///
75    /// - `info` must be a valid `duckdb_function_info` from a scan callback.
76    /// - No mutable reference to the same data must exist simultaneously.
77    pub unsafe fn get<'a>(info: duckdb_function_info) -> Option<&'a T> {
78        // SAFETY: info is valid per caller's contract.
79        let raw = unsafe { duckdb_function_get_init_data(info) };
80        if raw.is_null() {
81            return None;
82        }
83        // SAFETY: raw was created by set() via Box::into_raw.
84        Some(unsafe { &*raw.cast::<T>() })
85    }
86
87    /// Retrieves a mutable reference to the global init data from a scan callback.
88    ///
89    /// Returns `None` if no init data was set.
90    ///
91    /// # Safety
92    ///
93    /// - `info` must be a valid `duckdb_function_info` from a scan callback.
94    /// - No other reference to the same data must exist simultaneously.
95    pub unsafe fn get_mut<'a>(info: duckdb_function_info) -> Option<&'a mut T> {
96        let raw = unsafe { duckdb_function_get_init_data(info) };
97        if raw.is_null() {
98            return None;
99        }
100        Some(unsafe { &mut *raw.cast::<T>() })
101    }
102
103    /// Destroy callback: drops the `Box<T>`.
104    ///
105    /// # Safety
106    ///
107    /// `ptr` must have been allocated by [`set`][FfiInitData::set].
108    pub unsafe extern "C" fn destroy(ptr: *mut c_void) {
109        if !ptr.is_null() {
110            unsafe { drop(Box::from_raw(ptr.cast::<T>())) };
111        }
112    }
113}
114
115/// Type-safe per-thread local init data for `DuckDB` table functions.
116///
117/// Set in the `local_init` callback; retrieved in `scan`.
118pub struct FfiLocalInitData<T: 'static> {
119    _marker: std::marker::PhantomData<T>,
120}
121
122impl<T: 'static> FfiLocalInitData<T> {
123    /// Stores `data` as the per-thread local init data.
124    ///
125    /// Call inside your `local_init` callback.
126    ///
127    /// # Safety
128    ///
129    /// - `info` must be a valid `duckdb_init_info`.
130    /// - Must be called at most once per `local_init` invocation.
131    pub unsafe fn set(info: duckdb_init_info, data: T) {
132        let raw = Box::into_raw(Box::new(data)).cast::<c_void>();
133        // SAFETY: info is valid; raw is non-null. The same duckdb_init_set_init_data
134        // function is used for both global and local init; DuckDB tracks which
135        // phase is active when the callback is invoked.
136        unsafe {
137            duckdb_init_set_init_data(info, raw, Some(Self::destroy));
138        }
139    }
140
141    /// Retrieves a shared reference to the per-thread local init data.
142    ///
143    /// Returns `None` if no local init data was set.
144    ///
145    /// # Safety
146    ///
147    /// - `info` must be a valid `duckdb_function_info`.
148    /// - No mutable reference to the same data must exist simultaneously.
149    pub unsafe fn get<'a>(info: duckdb_function_info) -> Option<&'a T> {
150        let raw = unsafe { duckdb_function_get_local_init_data(info) };
151        if raw.is_null() {
152            return None;
153        }
154        Some(unsafe { &*raw.cast::<T>() })
155    }
156
157    /// Retrieves a mutable reference to the per-thread local init data.
158    ///
159    /// Returns `None` if no local init data was set.
160    ///
161    /// # Safety
162    ///
163    /// - `info` must be a valid `duckdb_function_info`.
164    /// - No other reference to the same data must exist simultaneously.
165    pub unsafe fn get_mut<'a>(info: duckdb_function_info) -> Option<&'a mut T> {
166        let raw = unsafe { duckdb_function_get_local_init_data(info) };
167        if raw.is_null() {
168            return None;
169        }
170        Some(unsafe { &mut *raw.cast::<T>() })
171    }
172
173    /// Destroy callback: drops the `Box<T>`.
174    ///
175    /// # Safety
176    ///
177    /// `ptr` must have been allocated by [`set`][FfiLocalInitData::set].
178    pub unsafe extern "C" fn destroy(ptr: *mut c_void) {
179        if !ptr.is_null() {
180            unsafe { drop(Box::from_raw(ptr.cast::<T>())) };
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[allow(dead_code)]
190    struct MyState {
191        counter: u64,
192    }
193
194    #[test]
195    fn destroy_null_is_noop() {
196        unsafe { FfiInitData::<MyState>::destroy(std::ptr::null_mut()) };
197        unsafe { FfiLocalInitData::<MyState>::destroy(std::ptr::null_mut()) };
198    }
199
200    #[test]
201    fn destroy_allocated_drops() {
202        let raw = Box::into_raw(Box::new(MyState { counter: 7 })).cast::<c_void>();
203        unsafe { FfiInitData::<MyState>::destroy(raw) };
204
205        let raw2 = Box::into_raw(Box::new(MyState { counter: 3 })).cast::<c_void>();
206        unsafe { FfiLocalInitData::<MyState>::destroy(raw2) };
207    }
208}