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    /// Writes a `bool` value to field `field_idx` at row `row`.
121    ///
122    /// # Safety
123    ///
124    /// - `row` must be within the vector's capacity.
125    /// - The field at `field_idx` must have `BOOLEAN` type.
126    ///
127    /// # Panics
128    ///
129    /// Panics if `field_idx >= field_count`.
130    #[inline]
131    pub unsafe fn write_bool(&mut self, row: usize, field_idx: usize, value: bool) {
132        // SAFETY: caller guarantees row is in bounds and field type is BOOLEAN.
133        unsafe { self.fields[field_idx].write_bool(row, value) };
134    }
135
136    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
137    ///
138    /// # Safety
139    ///
140    /// - `row` must be within the vector's capacity.
141    /// - The field at `field_idx` must have `VARCHAR` type.
142    ///
143    /// # Panics
144    ///
145    /// Panics if `field_idx >= field_count`.
146    #[inline]
147    pub unsafe fn write_varchar(&mut self, row: usize, field_idx: usize, value: &str) {
148        // SAFETY: caller guarantees row is in bounds and field type is VARCHAR.
149        unsafe { self.fields[field_idx].write_varchar(row, value) };
150    }
151
152    /// Writes an `i8` (TINYINT) value to field `field_idx` at row `row`.
153    ///
154    /// # Safety
155    ///
156    /// - `row` must be within the vector's capacity.
157    /// - The field at `field_idx` must have `TINYINT` type.
158    ///
159    /// # Panics
160    ///
161    /// Panics if `field_idx >= field_count`.
162    #[inline]
163    pub unsafe fn write_i8(&mut self, row: usize, field_idx: usize, value: i8) {
164        unsafe { self.fields[field_idx].write_i8(row, value) };
165    }
166
167    /// Writes an `i16` (SMALLINT) value to field `field_idx` at row `row`.
168    ///
169    /// # Safety
170    ///
171    /// See [`write_i8`][Self::write_i8].
172    #[inline]
173    pub unsafe fn write_i16(&mut self, row: usize, field_idx: usize, value: i16) {
174        unsafe { self.fields[field_idx].write_i16(row, value) };
175    }
176
177    /// Writes an `i32` (INTEGER) value to field `field_idx` at row `row`.
178    ///
179    /// # Safety
180    ///
181    /// See [`write_i8`][Self::write_i8].
182    #[inline]
183    pub unsafe fn write_i32(&mut self, row: usize, field_idx: usize, value: i32) {
184        unsafe { self.fields[field_idx].write_i32(row, value) };
185    }
186
187    /// Writes an `i64` (BIGINT) value to field `field_idx` at row `row`.
188    ///
189    /// # Safety
190    ///
191    /// See [`write_i8`][Self::write_i8].
192    #[inline]
193    pub unsafe fn write_i64(&mut self, row: usize, field_idx: usize, value: i64) {
194        unsafe { self.fields[field_idx].write_i64(row, value) };
195    }
196
197    /// Writes an `i128` (HUGEINT) value to field `field_idx` at row `row`.
198    ///
199    /// # Safety
200    ///
201    /// See [`write_i8`][Self::write_i8].
202    #[inline]
203    pub unsafe fn write_i128(&mut self, row: usize, field_idx: usize, value: i128) {
204        unsafe { self.fields[field_idx].write_i128(row, value) };
205    }
206
207    /// Writes a `u8` (UTINYINT) value to field `field_idx` at row `row`.
208    ///
209    /// # Safety
210    ///
211    /// See [`write_i8`][Self::write_i8].
212    #[inline]
213    pub unsafe fn write_u8(&mut self, row: usize, field_idx: usize, value: u8) {
214        unsafe { self.fields[field_idx].write_u8(row, value) };
215    }
216
217    /// Writes a `u16` (USMALLINT) value to field `field_idx` at row `row`.
218    ///
219    /// # Safety
220    ///
221    /// See [`write_i8`][Self::write_i8].
222    #[inline]
223    pub unsafe fn write_u16(&mut self, row: usize, field_idx: usize, value: u16) {
224        unsafe { self.fields[field_idx].write_u16(row, value) };
225    }
226
227    /// Writes a `u32` (UINTEGER) value to field `field_idx` at row `row`.
228    ///
229    /// # Safety
230    ///
231    /// See [`write_i8`][Self::write_i8].
232    #[inline]
233    pub unsafe fn write_u32(&mut self, row: usize, field_idx: usize, value: u32) {
234        unsafe { self.fields[field_idx].write_u32(row, value) };
235    }
236
237    /// Writes a `u64` (UBIGINT) value to field `field_idx` at row `row`.
238    ///
239    /// # Safety
240    ///
241    /// See [`write_i8`][Self::write_i8].
242    #[inline]
243    pub unsafe fn write_u64(&mut self, row: usize, field_idx: usize, value: u64) {
244        unsafe { self.fields[field_idx].write_u64(row, value) };
245    }
246
247    /// Writes an `f32` (FLOAT) value to field `field_idx` at row `row`.
248    ///
249    /// # Safety
250    ///
251    /// See [`write_i8`][Self::write_i8].
252    #[inline]
253    pub unsafe fn write_f32(&mut self, row: usize, field_idx: usize, value: f32) {
254        unsafe { self.fields[field_idx].write_f32(row, value) };
255    }
256
257    /// Writes an `f64` (DOUBLE) value to field `field_idx` at row `row`.
258    ///
259    /// # Safety
260    ///
261    /// See [`write_i8`][Self::write_i8].
262    #[inline]
263    pub unsafe fn write_f64(&mut self, row: usize, field_idx: usize, value: f64) {
264        unsafe { self.fields[field_idx].write_f64(row, value) };
265    }
266
267    /// Writes an INTERVAL value to field `field_idx` at row `row`.
268    ///
269    /// # Safety
270    ///
271    /// See [`write_i8`][Self::write_i8].
272    #[inline]
273    pub unsafe fn write_interval(&mut self, row: usize, field_idx: usize, value: DuckInterval) {
274        unsafe { self.fields[field_idx].write_interval(row, value) };
275    }
276
277    /// Writes a `BLOB` (binary) value to field `field_idx` at row `row`.
278    ///
279    /// # Safety
280    ///
281    /// See [`write_i8`][Self::write_i8].
282    #[inline]
283    pub unsafe fn write_blob(&mut self, row: usize, field_idx: usize, value: &[u8]) {
284        unsafe { self.fields[field_idx].write_blob(row, value) };
285    }
286
287    /// Writes a `UUID` value (as i128) to field `field_idx` at row `row`.
288    ///
289    /// # Safety
290    ///
291    /// See [`write_i8`][Self::write_i8].
292    #[inline]
293    pub unsafe fn write_uuid(&mut self, row: usize, field_idx: usize, value: i128) {
294        unsafe { self.fields[field_idx].write_uuid(row, value) };
295    }
296
297    /// Writes a VARCHAR string value to field `field_idx` at row `row`.
298    ///
299    /// Alias for [`write_varchar`][Self::write_varchar].
300    ///
301    /// # Safety
302    ///
303    /// See [`write_varchar`][Self::write_varchar].
304    #[inline]
305    pub unsafe fn write_str(&mut self, row: usize, field_idx: usize, value: &str) {
306        unsafe { self.write_varchar(row, field_idx, value) };
307    }
308
309    /// Writes a `DATE` value (days since epoch) to field `field_idx` at row `row`.
310    ///
311    /// Semantic alias for [`write_i32`][Self::write_i32].
312    ///
313    /// # Safety
314    ///
315    /// See [`write_i8`][Self::write_i8].
316    #[inline]
317    pub unsafe fn write_date(&mut self, row: usize, field_idx: usize, days_since_epoch: i32) {
318        unsafe { self.write_i32(row, field_idx, days_since_epoch) };
319    }
320
321    /// Writes a `TIMESTAMP` value (microseconds since epoch) to field `field_idx` at row `row`.
322    ///
323    /// Semantic alias for [`write_i64`][Self::write_i64].
324    ///
325    /// # Safety
326    ///
327    /// See [`write_i8`][Self::write_i8].
328    #[inline]
329    pub unsafe fn write_timestamp(
330        &mut self,
331        row: usize,
332        field_idx: usize,
333        micros_since_epoch: i64,
334    ) {
335        unsafe { self.write_i64(row, field_idx, micros_since_epoch) };
336    }
337
338    /// Writes a `TIME` value (microseconds since midnight) to field `field_idx` at row `row`.
339    ///
340    /// Semantic alias for [`write_i64`][Self::write_i64].
341    ///
342    /// # Safety
343    ///
344    /// See [`write_i8`][Self::write_i8].
345    #[inline]
346    pub unsafe fn write_time(&mut self, row: usize, field_idx: usize, micros_since_midnight: i64) {
347        unsafe { self.write_i64(row, field_idx, micros_since_midnight) };
348    }
349
350    /// Marks field `field_idx` at row `row` as NULL.
351    ///
352    /// # Safety
353    ///
354    /// - `row` must be within the vector's capacity.
355    ///
356    /// # Panics
357    ///
358    /// Panics if `field_idx >= field_count`.
359    #[inline]
360    pub unsafe fn set_null(&mut self, row: usize, field_idx: usize) {
361        // SAFETY: caller guarantees row is in bounds.
362        unsafe { self.fields[field_idx].set_null(row) };
363    }
364
365    /// Marks field `field_idx` at row `row` as valid (non-NULL).
366    ///
367    /// Use this to undo a previous [`set_null`][Self::set_null] call.
368    ///
369    /// # Safety
370    ///
371    /// - `row` must be within the vector's capacity.
372    ///
373    /// # Panics
374    ///
375    /// Panics if `field_idx >= field_count`.
376    #[inline]
377    pub unsafe fn set_valid(&mut self, row: usize, field_idx: usize) {
378        // SAFETY: caller guarantees row is in bounds.
379        unsafe { self.fields[field_idx].set_valid(row) };
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn struct_writer_field_count() {
389        // We can't create a real StructWriter without DuckDB, but we can verify
390        // the Vec-based field storage works correctly.
391        let sw = StructWriter { fields: Vec::new() };
392        assert_eq!(sw.field_count(), 0);
393    }
394
395    #[test]
396    fn size_of_struct_writer() {
397        // StructWriter is a Vec<VectorWriter> = 3 * usize (ptr, len, cap)
398        assert_eq!(
399            std::mem::size_of::<StructWriter>(),
400            3 * std::mem::size_of::<usize>()
401        );
402    }
403}