Skip to main content

quack_rs/vector/
struct_writer.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//! Batched, typed writer for STRUCT output vectors.
7//!
8//! [`StructWriter`] pre-creates [`VectorWriter`]s for every field at construction,
9//! then exposes typed `write_*` methods that take `(row, field_idx, value)`.
10//! This eliminates the repetitive `duckdb_struct_vector_get_child` + manual
11//! `VectorWriter` creation that extension authors currently need for every field.
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use quack_rs::vector::StructWriter;
17//! use libduckdb_sys::duckdb_vector;
18//!
19//! // Inside a scan callback, given a STRUCT output vector with 5 fields:
20//! // let mut sw = unsafe { StructWriter::new(struct_vec, 5) };
21//! // unsafe {
22//! //     sw.write_bool(0, 0, result.success);
23//! //     sw.write_varchar(0, 1, &result.data);
24//! //     sw.write_i64(0, 2, result.lease);
25//! //     sw.write_bool(0, 3, result.renewable);
26//! //     sw.write_varchar(0, 4, &result.message);
27//! // }
28//! ```
29//!
30//! # Estimated impact
31//!
32//! Eliminates ~120 raw `duckdb_struct_vector_get_child` calls across typical
33//! extensions, reducing unsafe surface area by ~30%.
34
35use libduckdb_sys::duckdb_vector;
36
37use crate::interval::DuckInterval;
38use crate::vector::complex::StructVector;
39use crate::vector::VectorWriter;
40
41/// A batched writer for STRUCT output vectors.
42///
43/// Pre-creates a [`VectorWriter`] for every field at construction, allowing
44/// direct typed writes without repeated `duckdb_struct_vector_get_child` calls.
45pub struct StructWriter {
46    fields: Vec<VectorWriter>,
47}
48
49impl StructWriter {
50    /// Creates a new `StructWriter` for a STRUCT vector with `field_count` fields.
51    ///
52    /// This pre-creates a [`VectorWriter`] for each field index `0..field_count`.
53    ///
54    /// # Safety
55    ///
56    /// - `vector` must be a valid, writable `DuckDB` STRUCT vector.
57    /// - `field_count` must match the number of fields in the STRUCT type.
58    /// - The vector must remain valid for the lifetime of this writer.
59    pub unsafe fn new(vector: duckdb_vector, field_count: usize) -> Self {
60        let mut fields = Vec::with_capacity(field_count);
61        for idx in 0..field_count {
62            // SAFETY: caller guarantees vector is valid STRUCT with field_count fields.
63            fields.push(unsafe { StructVector::field_writer(vector, idx) });
64        }
65        Self { fields }
66    }
67
68    /// Returns the number of fields in this struct writer.
69    #[mutants::skip]
70    #[must_use]
71    #[inline]
72    pub fn field_count(&self) -> usize {
73        self.fields.len()
74    }
75
76    /// Returns a mutable reference to the [`VectorWriter`] for the given field.
77    ///
78    /// # Panics
79    ///
80    /// Panics if `field_idx >= field_count`.
81    #[must_use]
82    #[inline]
83    pub fn field_mut(&mut self, field_idx: usize) -> &mut VectorWriter {
84        &mut self.fields[field_idx]
85    }
86
87    /// Writes a `bool` value to field `field_idx` at row `row`.
88    ///
89    /// # Safety
90    ///
91    /// - `row` must be within the vector's capacity.
92    /// - The field at `field_idx` must have `BOOLEAN` type.
93    ///
94    /// # Panics
95    ///
96    /// Panics if `field_idx >= field_count`.
97    #[inline]
98    pub unsafe fn write_bool(&mut self, row: usize, field_idx: usize, value: bool) {
99        // SAFETY: caller guarantees row is in bounds and field type is BOOLEAN.
100        unsafe { self.fields[field_idx].write_bool(row, value) };
101    }
102
103    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
104    ///
105    /// # Safety
106    ///
107    /// - `row` must be within the vector's capacity.
108    /// - The field at `field_idx` must have `VARCHAR` type.
109    ///
110    /// # Panics
111    ///
112    /// Panics if `field_idx >= field_count`.
113    #[inline]
114    pub unsafe fn write_varchar(&mut self, row: usize, field_idx: usize, value: &str) {
115        // SAFETY: caller guarantees row is in bounds and field type is VARCHAR.
116        unsafe { self.fields[field_idx].write_varchar(row, value) };
117    }
118
119    /// Writes an `i8` (TINYINT) value to field `field_idx` at row `row`.
120    ///
121    /// # Safety
122    ///
123    /// - `row` must be within the vector's capacity.
124    /// - The field at `field_idx` must have `TINYINT` type.
125    ///
126    /// # Panics
127    ///
128    /// Panics if `field_idx >= field_count`.
129    #[inline]
130    pub unsafe fn write_i8(&mut self, row: usize, field_idx: usize, value: i8) {
131        unsafe { self.fields[field_idx].write_i8(row, value) };
132    }
133
134    /// Writes an `i16` (SMALLINT) value to field `field_idx` at row `row`.
135    ///
136    /// # Safety
137    ///
138    /// See [`write_i8`][Self::write_i8].
139    #[inline]
140    pub unsafe fn write_i16(&mut self, row: usize, field_idx: usize, value: i16) {
141        unsafe { self.fields[field_idx].write_i16(row, value) };
142    }
143
144    /// Writes an `i32` (INTEGER) value to field `field_idx` at row `row`.
145    ///
146    /// # Safety
147    ///
148    /// See [`write_i8`][Self::write_i8].
149    #[inline]
150    pub unsafe fn write_i32(&mut self, row: usize, field_idx: usize, value: i32) {
151        unsafe { self.fields[field_idx].write_i32(row, value) };
152    }
153
154    /// Writes an `i64` (BIGINT) value to field `field_idx` at row `row`.
155    ///
156    /// # Safety
157    ///
158    /// See [`write_i8`][Self::write_i8].
159    #[inline]
160    pub unsafe fn write_i64(&mut self, row: usize, field_idx: usize, value: i64) {
161        unsafe { self.fields[field_idx].write_i64(row, value) };
162    }
163
164    /// Writes an `i128` (HUGEINT) value to field `field_idx` at row `row`.
165    ///
166    /// # Safety
167    ///
168    /// See [`write_i8`][Self::write_i8].
169    #[inline]
170    pub unsafe fn write_i128(&mut self, row: usize, field_idx: usize, value: i128) {
171        unsafe { self.fields[field_idx].write_i128(row, value) };
172    }
173
174    /// Writes a `u8` (UTINYINT) value to field `field_idx` at row `row`.
175    ///
176    /// # Safety
177    ///
178    /// See [`write_i8`][Self::write_i8].
179    #[inline]
180    pub unsafe fn write_u8(&mut self, row: usize, field_idx: usize, value: u8) {
181        unsafe { self.fields[field_idx].write_u8(row, value) };
182    }
183
184    /// Writes a `u16` (USMALLINT) value to field `field_idx` at row `row`.
185    ///
186    /// # Safety
187    ///
188    /// See [`write_i8`][Self::write_i8].
189    #[inline]
190    pub unsafe fn write_u16(&mut self, row: usize, field_idx: usize, value: u16) {
191        unsafe { self.fields[field_idx].write_u16(row, value) };
192    }
193
194    /// Writes a `u32` (UINTEGER) value to field `field_idx` at row `row`.
195    ///
196    /// # Safety
197    ///
198    /// See [`write_i8`][Self::write_i8].
199    #[inline]
200    pub unsafe fn write_u32(&mut self, row: usize, field_idx: usize, value: u32) {
201        unsafe { self.fields[field_idx].write_u32(row, value) };
202    }
203
204    /// Writes a `u64` (UBIGINT) value to field `field_idx` at row `row`.
205    ///
206    /// # Safety
207    ///
208    /// See [`write_i8`][Self::write_i8].
209    #[inline]
210    pub unsafe fn write_u64(&mut self, row: usize, field_idx: usize, value: u64) {
211        unsafe { self.fields[field_idx].write_u64(row, value) };
212    }
213
214    /// Writes an `f32` (FLOAT) value to field `field_idx` at row `row`.
215    ///
216    /// # Safety
217    ///
218    /// See [`write_i8`][Self::write_i8].
219    #[inline]
220    pub unsafe fn write_f32(&mut self, row: usize, field_idx: usize, value: f32) {
221        unsafe { self.fields[field_idx].write_f32(row, value) };
222    }
223
224    /// Writes an `f64` (DOUBLE) value to field `field_idx` at row `row`.
225    ///
226    /// # Safety
227    ///
228    /// See [`write_i8`][Self::write_i8].
229    #[inline]
230    pub unsafe fn write_f64(&mut self, row: usize, field_idx: usize, value: f64) {
231        unsafe { self.fields[field_idx].write_f64(row, value) };
232    }
233
234    /// Writes an INTERVAL value to field `field_idx` at row `row`.
235    ///
236    /// # Safety
237    ///
238    /// See [`write_i8`][Self::write_i8].
239    #[inline]
240    pub unsafe fn write_interval(&mut self, row: usize, field_idx: usize, value: DuckInterval) {
241        unsafe { self.fields[field_idx].write_interval(row, value) };
242    }
243
244    /// Writes a `BLOB` (binary) value to field `field_idx` at row `row`.
245    ///
246    /// # Safety
247    ///
248    /// See [`write_i8`][Self::write_i8].
249    #[inline]
250    pub unsafe fn write_blob(&mut self, row: usize, field_idx: usize, value: &[u8]) {
251        unsafe { self.fields[field_idx].write_blob(row, value) };
252    }
253
254    /// Writes a `UUID` value (as i128) to field `field_idx` at row `row`.
255    ///
256    /// # Safety
257    ///
258    /// See [`write_i8`][Self::write_i8].
259    #[inline]
260    pub unsafe fn write_uuid(&mut self, row: usize, field_idx: usize, value: i128) {
261        unsafe { self.fields[field_idx].write_uuid(row, value) };
262    }
263
264    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
265    ///
266    /// Alias for [`write_varchar`][Self::write_varchar].
267    ///
268    /// # Safety
269    ///
270    /// See [`write_varchar`][Self::write_varchar].
271    #[inline]
272    pub unsafe fn write_str(&mut self, row: usize, field_idx: usize, value: &str) {
273        unsafe { self.write_varchar(row, field_idx, value) };
274    }
275
276    /// Writes a `DATE` value (days since epoch) to field `field_idx` at row `row`.
277    ///
278    /// Semantic alias for [`write_i32`][Self::write_i32].
279    ///
280    /// # Safety
281    ///
282    /// See [`write_i8`][Self::write_i8].
283    #[inline]
284    pub unsafe fn write_date(&mut self, row: usize, field_idx: usize, days_since_epoch: i32) {
285        unsafe { self.write_i32(row, field_idx, days_since_epoch) };
286    }
287
288    /// Writes a `TIMESTAMP` value (microseconds since epoch) to field `field_idx` at row `row`.
289    ///
290    /// Semantic alias for [`write_i64`][Self::write_i64].
291    ///
292    /// # Safety
293    ///
294    /// See [`write_i8`][Self::write_i8].
295    #[inline]
296    pub unsafe fn write_timestamp(
297        &mut self,
298        row: usize,
299        field_idx: usize,
300        micros_since_epoch: i64,
301    ) {
302        unsafe { self.write_i64(row, field_idx, micros_since_epoch) };
303    }
304
305    /// Writes a `TIME` value (microseconds since midnight) to field `field_idx` at row `row`.
306    ///
307    /// Semantic alias for [`write_i64`][Self::write_i64].
308    ///
309    /// # Safety
310    ///
311    /// See [`write_i8`][Self::write_i8].
312    #[inline]
313    pub unsafe fn write_time(&mut self, row: usize, field_idx: usize, micros_since_midnight: i64) {
314        unsafe { self.write_i64(row, field_idx, micros_since_midnight) };
315    }
316
317    /// Marks field `field_idx` at row `row` as NULL.
318    ///
319    /// # Safety
320    ///
321    /// - `row` must be within the vector's capacity.
322    ///
323    /// # Panics
324    ///
325    /// Panics if `field_idx >= field_count`.
326    #[inline]
327    pub unsafe fn set_null(&mut self, row: usize, field_idx: usize) {
328        // SAFETY: caller guarantees row is in bounds.
329        unsafe { self.fields[field_idx].set_null(row) };
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn struct_writer_field_count() {
339        // We can't create a real StructWriter without DuckDB, but we can verify
340        // the Vec-based field storage works correctly.
341        let sw = StructWriter { fields: Vec::new() };
342        assert_eq!(sw.field_count(), 0);
343    }
344
345    #[test]
346    fn size_of_struct_writer() {
347        // StructWriter is a Vec<VectorWriter> = 3 * usize (ptr, len, cap)
348        assert_eq!(
349            std::mem::size_of::<StructWriter>(),
350            3 * std::mem::size_of::<usize>()
351        );
352    }
353}