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 the raw `duckdb_vector` handle for the given field.
77    ///
78    /// Use this when a struct field has a complex type (LIST, MAP, ARRAY) that
79    /// requires operations beyond simple scalar writes — for example, calling
80    /// [`ListVector::set_entry`][crate::vector::complex::ListVector::set_entry] or
81    /// [`ListVector::reserve`][crate::vector::complex::ListVector::reserve].
82    ///
83    /// # Example
84    ///
85    /// ```rust,no_run
86    /// use quack_rs::vector::{StructWriter, VectorWriter, complex::ListVector};
87    /// use libduckdb_sys::duckdb_vector;
88    ///
89    /// // Given a STRUCT output vector where field 1 is LIST<VARCHAR>:
90    /// // let mut sw = unsafe { StructWriter::new(struct_vec, 3) };
91    /// // sw.write_varchar(0, 0, "name");               // scalar field
92    /// // let list_vec = sw.child_vector(1);             // LIST field
93    /// // unsafe { ListVector::reserve(list_vec, 10) };  // complex ops
94    /// // unsafe { ListVector::set_entry(list_vec, 0, 0, 3) };
95    /// // let mut elem = unsafe { ListVector::child_writer(list_vec) };
96    /// // unsafe { elem.write_varchar(0, "a") };
97    /// // unsafe { ListVector::set_size(list_vec, 3) };
98    /// ```
99    ///
100    /// # Panics
101    ///
102    /// Panics if `field_idx >= field_count`.
103    #[must_use]
104    #[inline]
105    pub fn child_vector(&self, field_idx: usize) -> duckdb_vector {
106        self.fields[field_idx].as_raw()
107    }
108
109    /// Returns a mutable reference to the [`VectorWriter`] for the given field.
110    ///
111    /// # Panics
112    ///
113    /// Panics if `field_idx >= field_count`.
114    #[must_use]
115    #[inline]
116    pub fn field_mut(&mut self, field_idx: usize) -> &mut VectorWriter {
117        &mut self.fields[field_idx]
118    }
119
120    /// Returns the raw `duckdb_vector` handle for a LIST-typed field.
121    ///
122    /// This is a semantic alias for [`child_vector`][Self::child_vector] that
123    /// makes the intent clear when a struct field has `LIST` type. Use the
124    /// returned handle with [`ListVector`][crate::vector::complex::ListVector]
125    /// methods (`reserve`, `set_entry`, `set_size`, `child_writer`, etc.).
126    ///
127    /// # Example
128    ///
129    /// ```rust,no_run
130    /// use quack_rs::vector::{StructWriter, VectorWriter, complex::ListVector};
131    /// use libduckdb_sys::duckdb_vector;
132    ///
133    /// // Given a STRUCT output vector where field 2 is LIST<VARCHAR>:
134    /// // let mut sw = unsafe { StructWriter::new(struct_vec, 4) };
135    /// // let list_vec = sw.child_list_vector(2);
136    /// // unsafe { ListVector::reserve(list_vec, 10) };
137    /// // unsafe { ListVector::set_entry(list_vec, 0, 0, 3) };
138    /// // let mut elem_writer = unsafe { ListVector::child_writer(list_vec) };
139    /// // unsafe {
140    /// //     elem_writer.write_varchar(0, "a");
141    /// //     elem_writer.write_varchar(1, "b");
142    /// //     elem_writer.write_varchar(2, "c");
143    /// // }
144    /// // unsafe { ListVector::set_size(list_vec, 3) };
145    /// ```
146    ///
147    /// # Panics
148    ///
149    /// Panics if `field_idx >= field_count`.
150    #[must_use]
151    #[inline]
152    pub fn child_list_vector(&self, field_idx: usize) -> duckdb_vector {
153        self.child_vector(field_idx)
154    }
155
156    /// Writes a `bool` value to field `field_idx` at row `row`.
157    ///
158    /// # Safety
159    ///
160    /// - `row` must be within the vector's capacity.
161    /// - The field at `field_idx` must have `BOOLEAN` type.
162    ///
163    /// # Panics
164    ///
165    /// Panics if `field_idx >= field_count`.
166    #[inline]
167    pub unsafe fn write_bool(&mut self, row: usize, field_idx: usize, value: bool) {
168        // SAFETY: caller guarantees row is in bounds and field type is BOOLEAN.
169        unsafe { self.fields[field_idx].write_bool(row, value) };
170    }
171
172    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
173    ///
174    /// # Safety
175    ///
176    /// - `row` must be within the vector's capacity.
177    /// - The field at `field_idx` must have `VARCHAR` type.
178    ///
179    /// # Panics
180    ///
181    /// Panics if `field_idx >= field_count`.
182    #[inline]
183    pub unsafe fn write_varchar(&mut self, row: usize, field_idx: usize, value: &str) {
184        // SAFETY: caller guarantees row is in bounds and field type is VARCHAR.
185        unsafe { self.fields[field_idx].write_varchar(row, value) };
186    }
187
188    /// Writes an `i8` (TINYINT) value to field `field_idx` at row `row`.
189    ///
190    /// # Safety
191    ///
192    /// - `row` must be within the vector's capacity.
193    /// - The field at `field_idx` must have `TINYINT` type.
194    ///
195    /// # Panics
196    ///
197    /// Panics if `field_idx >= field_count`.
198    #[inline]
199    pub unsafe fn write_i8(&mut self, row: usize, field_idx: usize, value: i8) {
200        unsafe { self.fields[field_idx].write_i8(row, value) };
201    }
202
203    /// Writes an `i16` (SMALLINT) value to field `field_idx` at row `row`.
204    ///
205    /// # Safety
206    ///
207    /// See [`write_i8`][Self::write_i8].
208    #[inline]
209    pub unsafe fn write_i16(&mut self, row: usize, field_idx: usize, value: i16) {
210        unsafe { self.fields[field_idx].write_i16(row, value) };
211    }
212
213    /// Writes an `i32` (INTEGER) value to field `field_idx` at row `row`.
214    ///
215    /// # Safety
216    ///
217    /// See [`write_i8`][Self::write_i8].
218    #[inline]
219    pub unsafe fn write_i32(&mut self, row: usize, field_idx: usize, value: i32) {
220        unsafe { self.fields[field_idx].write_i32(row, value) };
221    }
222
223    /// Writes an `i64` (BIGINT) value to field `field_idx` at row `row`.
224    ///
225    /// # Safety
226    ///
227    /// See [`write_i8`][Self::write_i8].
228    #[inline]
229    pub unsafe fn write_i64(&mut self, row: usize, field_idx: usize, value: i64) {
230        unsafe { self.fields[field_idx].write_i64(row, value) };
231    }
232
233    /// Writes an `i128` (HUGEINT) value to field `field_idx` at row `row`.
234    ///
235    /// # Safety
236    ///
237    /// See [`write_i8`][Self::write_i8].
238    #[inline]
239    pub unsafe fn write_i128(&mut self, row: usize, field_idx: usize, value: i128) {
240        unsafe { self.fields[field_idx].write_i128(row, value) };
241    }
242
243    /// Writes a `u8` (UTINYINT) value to field `field_idx` at row `row`.
244    ///
245    /// # Safety
246    ///
247    /// See [`write_i8`][Self::write_i8].
248    #[inline]
249    pub unsafe fn write_u8(&mut self, row: usize, field_idx: usize, value: u8) {
250        unsafe { self.fields[field_idx].write_u8(row, value) };
251    }
252
253    /// Writes a `u16` (USMALLINT) value to field `field_idx` at row `row`.
254    ///
255    /// # Safety
256    ///
257    /// See [`write_i8`][Self::write_i8].
258    #[inline]
259    pub unsafe fn write_u16(&mut self, row: usize, field_idx: usize, value: u16) {
260        unsafe { self.fields[field_idx].write_u16(row, value) };
261    }
262
263    /// Writes a `u32` (UINTEGER) value to field `field_idx` at row `row`.
264    ///
265    /// # Safety
266    ///
267    /// See [`write_i8`][Self::write_i8].
268    #[inline]
269    pub unsafe fn write_u32(&mut self, row: usize, field_idx: usize, value: u32) {
270        unsafe { self.fields[field_idx].write_u32(row, value) };
271    }
272
273    /// Writes a `u64` (UBIGINT) value to field `field_idx` at row `row`.
274    ///
275    /// # Safety
276    ///
277    /// See [`write_i8`][Self::write_i8].
278    #[inline]
279    pub unsafe fn write_u64(&mut self, row: usize, field_idx: usize, value: u64) {
280        unsafe { self.fields[field_idx].write_u64(row, value) };
281    }
282
283    /// Writes an `f32` (FLOAT) value to field `field_idx` at row `row`.
284    ///
285    /// # Safety
286    ///
287    /// See [`write_i8`][Self::write_i8].
288    #[inline]
289    pub unsafe fn write_f32(&mut self, row: usize, field_idx: usize, value: f32) {
290        unsafe { self.fields[field_idx].write_f32(row, value) };
291    }
292
293    /// Writes an `f64` (DOUBLE) value to field `field_idx` at row `row`.
294    ///
295    /// # Safety
296    ///
297    /// See [`write_i8`][Self::write_i8].
298    #[inline]
299    pub unsafe fn write_f64(&mut self, row: usize, field_idx: usize, value: f64) {
300        unsafe { self.fields[field_idx].write_f64(row, value) };
301    }
302
303    /// Writes an INTERVAL value to field `field_idx` at row `row`.
304    ///
305    /// # Safety
306    ///
307    /// See [`write_i8`][Self::write_i8].
308    #[inline]
309    pub unsafe fn write_interval(&mut self, row: usize, field_idx: usize, value: DuckInterval) {
310        unsafe { self.fields[field_idx].write_interval(row, value) };
311    }
312
313    /// Writes a `BLOB` (binary) value to field `field_idx` at row `row`.
314    ///
315    /// # Safety
316    ///
317    /// See [`write_i8`][Self::write_i8].
318    #[inline]
319    pub unsafe fn write_blob(&mut self, row: usize, field_idx: usize, value: &[u8]) {
320        unsafe { self.fields[field_idx].write_blob(row, value) };
321    }
322
323    /// Writes a `UUID` value (as i128) to field `field_idx` at row `row`.
324    ///
325    /// # Safety
326    ///
327    /// See [`write_i8`][Self::write_i8].
328    #[inline]
329    pub unsafe fn write_uuid(&mut self, row: usize, field_idx: usize, value: i128) {
330        unsafe { self.fields[field_idx].write_uuid(row, value) };
331    }
332
333    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
334    ///
335    /// Alias for [`write_varchar`][Self::write_varchar].
336    ///
337    /// # Safety
338    ///
339    /// See [`write_varchar`][Self::write_varchar].
340    #[inline]
341    pub unsafe fn write_str(&mut self, row: usize, field_idx: usize, value: &str) {
342        unsafe { self.write_varchar(row, field_idx, value) };
343    }
344
345    /// Writes a `DATE` value (days since epoch) to field `field_idx` at row `row`.
346    ///
347    /// Semantic alias for [`write_i32`][Self::write_i32].
348    ///
349    /// # Safety
350    ///
351    /// See [`write_i8`][Self::write_i8].
352    #[inline]
353    pub unsafe fn write_date(&mut self, row: usize, field_idx: usize, days_since_epoch: i32) {
354        unsafe { self.write_i32(row, field_idx, days_since_epoch) };
355    }
356
357    /// Writes a `TIMESTAMP` value (microseconds since epoch) to field `field_idx` at row `row`.
358    ///
359    /// Semantic alias for [`write_i64`][Self::write_i64].
360    ///
361    /// # Safety
362    ///
363    /// See [`write_i8`][Self::write_i8].
364    #[inline]
365    pub unsafe fn write_timestamp(
366        &mut self,
367        row: usize,
368        field_idx: usize,
369        micros_since_epoch: i64,
370    ) {
371        unsafe { self.write_i64(row, field_idx, micros_since_epoch) };
372    }
373
374    /// Writes a `TIME` value (microseconds since midnight) to field `field_idx` at row `row`.
375    ///
376    /// Semantic alias for [`write_i64`][Self::write_i64].
377    ///
378    /// # Safety
379    ///
380    /// See [`write_i8`][Self::write_i8].
381    #[inline]
382    pub unsafe fn write_time(&mut self, row: usize, field_idx: usize, micros_since_midnight: i64) {
383        unsafe { self.write_i64(row, field_idx, micros_since_midnight) };
384    }
385
386    /// Marks field `field_idx` at row `row` as NULL.
387    ///
388    /// # Safety
389    ///
390    /// - `row` must be within the vector's capacity.
391    ///
392    /// # Panics
393    ///
394    /// Panics if `field_idx >= field_count`.
395    #[inline]
396    pub unsafe fn set_null(&mut self, row: usize, field_idx: usize) {
397        // SAFETY: caller guarantees row is in bounds.
398        unsafe { self.fields[field_idx].set_null(row) };
399    }
400
401    /// Marks field `field_idx` at row `row` as valid (non-NULL).
402    ///
403    /// Use this to undo a previous [`set_null`][Self::set_null] call.
404    ///
405    /// # Safety
406    ///
407    /// - `row` must be within the vector's capacity.
408    ///
409    /// # Panics
410    ///
411    /// Panics if `field_idx >= field_count`.
412    #[inline]
413    pub unsafe fn set_valid(&mut self, row: usize, field_idx: usize) {
414        // SAFETY: caller guarantees row is in bounds.
415        unsafe { self.fields[field_idx].set_valid(row) };
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn struct_writer_field_count() {
425        // We can't create a real StructWriter without DuckDB, but we can verify
426        // the Vec-based field storage works correctly.
427        let sw = StructWriter { fields: Vec::new() };
428        assert_eq!(sw.field_count(), 0);
429    }
430
431    #[test]
432    fn size_of_struct_writer() {
433        // StructWriter is a Vec<VectorWriter> = 3 * usize (ptr, len, cap)
434        assert_eq!(
435            std::mem::size_of::<StructWriter>(),
436            3 * std::mem::size_of::<usize>()
437        );
438    }
439}