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}