quack_rs/vector/struct_reader.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 reader for STRUCT input vectors.
7//!
8//! [`StructReader`] pre-creates [`VectorReader`]s for every field at construction,
9//! then exposes typed `read_*` methods that take `(row, field_idx)`.
10//! This is the read-side counterpart to [`StructWriter`][super::StructWriter].
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use quack_rs::vector::StructReader;
16//! use libduckdb_sys::duckdb_vector;
17//!
18//! // Inside a scan callback, given a STRUCT input vector with 3 fields:
19//! // let sr = unsafe { StructReader::new(struct_vec, 3, row_count) };
20//! // for row in 0..row_count {
21//! // let name = unsafe { sr.read_str(row, 0) };
22//! // let age = unsafe { sr.read_i32(row, 1) };
23//! // let active = unsafe { sr.read_bool(row, 2) };
24//! // }
25//! ```
26
27use libduckdb_sys::duckdb_vector;
28
29use crate::interval::DuckInterval;
30use crate::vector::complex::StructVector;
31use crate::vector::VectorReader;
32
33/// A batched reader for STRUCT input vectors.
34///
35/// Pre-creates a [`VectorReader`] for every field at construction, allowing
36/// direct typed reads without repeated `duckdb_struct_vector_get_child` calls.
37pub struct StructReader {
38 vector: duckdb_vector,
39 fields: Vec<VectorReader>,
40}
41
42impl StructReader {
43 /// Creates a new `StructReader` for a STRUCT vector with `field_count` fields.
44 ///
45 /// # Safety
46 ///
47 /// - `vector` must be a valid `DuckDB` STRUCT vector.
48 /// - `field_count` must match the number of fields in the STRUCT type.
49 /// - `row_count` must match the number of rows in the parent chunk.
50 /// - The vector must remain valid for the lifetime of this reader.
51 pub unsafe fn new(vector: duckdb_vector, field_count: usize, row_count: usize) -> Self {
52 let mut fields = Vec::with_capacity(field_count);
53 for idx in 0..field_count {
54 // SAFETY: caller guarantees vector is valid STRUCT with field_count fields.
55 fields.push(unsafe { StructVector::field_reader(vector, idx, row_count) });
56 }
57 Self { vector, fields }
58 }
59
60 /// Returns the number of fields in this struct reader.
61 #[mutants::skip]
62 #[must_use]
63 #[inline]
64 pub fn field_count(&self) -> usize {
65 self.fields.len()
66 }
67
68 /// Returns a reference to the [`VectorReader`] for the given field.
69 ///
70 /// # Panics
71 ///
72 /// Panics if `field_idx >= field_count`.
73 #[must_use]
74 #[inline]
75 pub fn field(&self, field_idx: usize) -> &VectorReader {
76 &self.fields[field_idx]
77 }
78
79 /// Returns the raw `duckdb_vector` handle for the given field.
80 ///
81 /// Use this when a struct field has a complex type (LIST, MAP, ARRAY) that
82 /// requires operations beyond simple scalar reads — for example, calling
83 /// [`ListVector::get_entry`][crate::vector::complex::ListVector::get_entry] or
84 /// [`ListVector::child_reader`][crate::vector::complex::ListVector::child_reader].
85 ///
86 /// # Safety
87 ///
88 /// - `field_idx` must be a valid field index (0 ≤ `field_idx` < `field_count`).
89 /// - The returned vector is borrowed from the parent STRUCT vector and must
90 /// not outlive it.
91 #[must_use]
92 #[inline]
93 pub unsafe fn child_vector(&self, field_idx: usize) -> duckdb_vector {
94 unsafe { StructVector::get_child(self.vector, field_idx) }
95 }
96
97 /// Returns `true` if the value at `row` in field `field_idx` is not NULL.
98 ///
99 /// # Safety
100 ///
101 /// `row` must be less than the row count.
102 ///
103 /// # Panics
104 ///
105 /// Panics if `field_idx >= field_count`.
106 #[inline]
107 pub unsafe fn is_valid(&self, row: usize, field_idx: usize) -> bool {
108 unsafe { self.fields[field_idx].is_valid(row) }
109 }
110
111 /// Reads a `bool` (BOOLEAN) value from field `field_idx` at row `row`.
112 ///
113 /// # Safety
114 ///
115 /// - `row` must be less than the row count.
116 /// - The field at `field_idx` must have `BOOLEAN` type.
117 ///
118 /// # Panics
119 ///
120 /// Panics if `field_idx >= field_count`.
121 #[inline]
122 pub unsafe fn read_bool(&self, row: usize, field_idx: usize) -> bool {
123 unsafe { self.fields[field_idx].read_bool(row) }
124 }
125
126 /// Reads a VARCHAR value from field `field_idx` at row `row`.
127 ///
128 /// # Safety
129 ///
130 /// - `row` must be less than the row count.
131 /// - The field at `field_idx` must have `VARCHAR` type.
132 ///
133 /// # Panics
134 ///
135 /// Panics if `field_idx >= field_count`.
136 #[inline]
137 pub unsafe fn read_str(&self, row: usize, field_idx: usize) -> &str {
138 unsafe { self.fields[field_idx].read_str(row) }
139 }
140
141 /// Reads an `i8` (TINYINT) value from field `field_idx` at row `row`.
142 ///
143 /// # Safety
144 ///
145 /// See [`read_bool`][Self::read_bool].
146 #[inline]
147 pub unsafe fn read_i8(&self, row: usize, field_idx: usize) -> i8 {
148 unsafe { self.fields[field_idx].read_i8(row) }
149 }
150
151 /// Reads an `i16` (SMALLINT) value.
152 ///
153 /// # Safety
154 ///
155 /// See [`read_bool`][Self::read_bool].
156 #[inline]
157 pub unsafe fn read_i16(&self, row: usize, field_idx: usize) -> i16 {
158 unsafe { self.fields[field_idx].read_i16(row) }
159 }
160
161 /// Reads an `i32` (INTEGER) value.
162 ///
163 /// # Safety
164 ///
165 /// See [`read_bool`][Self::read_bool].
166 #[inline]
167 pub unsafe fn read_i32(&self, row: usize, field_idx: usize) -> i32 {
168 unsafe { self.fields[field_idx].read_i32(row) }
169 }
170
171 /// Reads an `i64` (BIGINT) value.
172 ///
173 /// # Safety
174 ///
175 /// See [`read_bool`][Self::read_bool].
176 #[inline]
177 pub unsafe fn read_i64(&self, row: usize, field_idx: usize) -> i64 {
178 unsafe { self.fields[field_idx].read_i64(row) }
179 }
180
181 /// Reads an `i128` (HUGEINT) value.
182 ///
183 /// # Safety
184 ///
185 /// See [`read_bool`][Self::read_bool].
186 #[inline]
187 pub unsafe fn read_i128(&self, row: usize, field_idx: usize) -> i128 {
188 unsafe { self.fields[field_idx].read_i128(row) }
189 }
190
191 /// Reads a `u8` (UTINYINT) value.
192 ///
193 /// # Safety
194 ///
195 /// See [`read_bool`][Self::read_bool].
196 #[inline]
197 pub unsafe fn read_u8(&self, row: usize, field_idx: usize) -> u8 {
198 unsafe { self.fields[field_idx].read_u8(row) }
199 }
200
201 /// Reads a `u16` (USMALLINT) value.
202 ///
203 /// # Safety
204 ///
205 /// See [`read_bool`][Self::read_bool].
206 #[inline]
207 pub unsafe fn read_u16(&self, row: usize, field_idx: usize) -> u16 {
208 unsafe { self.fields[field_idx].read_u16(row) }
209 }
210
211 /// Reads a `u32` (UINTEGER) value.
212 ///
213 /// # Safety
214 ///
215 /// See [`read_bool`][Self::read_bool].
216 #[inline]
217 pub unsafe fn read_u32(&self, row: usize, field_idx: usize) -> u32 {
218 unsafe { self.fields[field_idx].read_u32(row) }
219 }
220
221 /// Reads a `u64` (UBIGINT) value.
222 ///
223 /// # Safety
224 ///
225 /// See [`read_bool`][Self::read_bool].
226 #[inline]
227 pub unsafe fn read_u64(&self, row: usize, field_idx: usize) -> u64 {
228 unsafe { self.fields[field_idx].read_u64(row) }
229 }
230
231 /// Reads an `f32` (FLOAT) value.
232 ///
233 /// # Safety
234 ///
235 /// See [`read_bool`][Self::read_bool].
236 #[inline]
237 pub unsafe fn read_f32(&self, row: usize, field_idx: usize) -> f32 {
238 unsafe { self.fields[field_idx].read_f32(row) }
239 }
240
241 /// Reads an `f64` (DOUBLE) value.
242 ///
243 /// # Safety
244 ///
245 /// See [`read_bool`][Self::read_bool].
246 #[inline]
247 pub unsafe fn read_f64(&self, row: usize, field_idx: usize) -> f64 {
248 unsafe { self.fields[field_idx].read_f64(row) }
249 }
250
251 /// Reads an INTERVAL value.
252 ///
253 /// # Safety
254 ///
255 /// See [`read_bool`][Self::read_bool].
256 #[inline]
257 pub unsafe fn read_interval(&self, row: usize, field_idx: usize) -> DuckInterval {
258 unsafe { self.fields[field_idx].read_interval(row) }
259 }
260
261 /// Reads a DATE value (days since epoch).
262 ///
263 /// # Safety
264 ///
265 /// See [`read_bool`][Self::read_bool].
266 #[inline]
267 pub unsafe fn read_date(&self, row: usize, field_idx: usize) -> i32 {
268 unsafe { self.fields[field_idx].read_date(row) }
269 }
270
271 /// Reads a TIMESTAMP value (microseconds since epoch).
272 ///
273 /// # Safety
274 ///
275 /// See [`read_bool`][Self::read_bool].
276 #[inline]
277 pub unsafe fn read_timestamp(&self, row: usize, field_idx: usize) -> i64 {
278 unsafe { self.fields[field_idx].read_timestamp(row) }
279 }
280
281 /// Reads a TIME value (microseconds since midnight).
282 ///
283 /// # Safety
284 ///
285 /// See [`read_bool`][Self::read_bool].
286 #[inline]
287 pub unsafe fn read_time(&self, row: usize, field_idx: usize) -> i64 {
288 unsafe { self.fields[field_idx].read_time(row) }
289 }
290
291 /// Reads a `BLOB` (binary) value from field `field_idx` at row `row`.
292 ///
293 /// # Safety
294 ///
295 /// See [`read_bool`][Self::read_bool].
296 #[inline]
297 pub unsafe fn read_blob(&self, row: usize, field_idx: usize) -> &[u8] {
298 unsafe { self.fields[field_idx].read_blob(row) }
299 }
300
301 /// Reads a `UUID` value (as i128) from field `field_idx` at row `row`.
302 ///
303 /// # Safety
304 ///
305 /// See [`read_bool`][Self::read_bool].
306 #[inline]
307 pub unsafe fn read_uuid(&self, row: usize, field_idx: usize) -> i128 {
308 unsafe { self.fields[field_idx].read_uuid(row) }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn struct_reader_field_count() {
318 let sr = StructReader {
319 vector: std::ptr::null_mut(),
320 fields: Vec::new(),
321 };
322 assert_eq!(sr.field_count(), 0);
323 }
324
325 #[test]
326 fn size_of_struct_reader() {
327 assert_eq!(
328 std::mem::size_of::<StructReader>(),
329 4 * std::mem::size_of::<usize>() // vector ptr + Vec (ptr + len + cap)
330 );
331 }
332}