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}