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}