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}