quack_rs/vector/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//! Safe typed reading from `DuckDB` data vectors.
7//!
8//! [`VectorReader`] provides safe access to the typed data in a `DuckDB` vector
9//! without requiring direct raw pointer manipulation.
10//!
11//! # Pitfalls solved
12//!
13//! - **L5**: Booleans are read as `u8 != 0`, never as `bool`, because `DuckDB`'s
14//! C API does not guarantee the Rust `bool` invariant (must be 0 or 1).
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use quack_rs::vector::VectorReader;
20//! use libduckdb_sys::{duckdb_data_chunk, duckdb_data_chunk_get_vector,
21//! duckdb_data_chunk_get_size};
22//!
23//! // Inside a DuckDB aggregate `update` callback:
24//! // let reader = unsafe { VectorReader::new(chunk, 0) };
25//! // for row in 0..reader.row_count() {
26//! // if reader.is_valid(row) {
27//! // let val = unsafe { reader.read_i64(row) };
28//! // }
29//! // }
30//! ```
31
32use libduckdb_sys::{
33 duckdb_data_chunk, duckdb_data_chunk_get_size, duckdb_data_chunk_get_vector,
34 duckdb_validity_row_is_valid, duckdb_vector, duckdb_vector_get_data,
35 duckdb_vector_get_validity, idx_t,
36};
37
38/// A typed reader for a single column in a `DuckDB` data chunk.
39///
40/// `VectorReader` wraps a pointer to a `DuckDB` vector's data buffer and
41/// provides ergonomic, type-checked access methods for common `DuckDB` types.
42///
43/// # Lifetimes
44///
45/// The reader borrows from the data chunk. Do not call `duckdb_destroy_data_chunk`
46/// while a `VectorReader` that references it is live.
47#[derive(Debug)]
48pub struct VectorReader {
49 data: *const u8,
50 validity: *mut u64,
51 row_count: usize,
52}
53
54impl VectorReader {
55 /// Creates a new `VectorReader` for the given column in a data chunk.
56 ///
57 /// # Safety
58 ///
59 /// - `chunk` must be a valid `duckdb_data_chunk` for the duration of this reader's lifetime.
60 /// - `col_idx` must be a valid column index in the chunk.
61 pub unsafe fn new(chunk: duckdb_data_chunk, col_idx: usize) -> Self {
62 // SAFETY: Caller guarantees chunk is valid.
63 let row_count = usize::try_from(unsafe { duckdb_data_chunk_get_size(chunk) }).unwrap_or(0);
64 // SAFETY: col_idx is valid per caller's contract.
65 let vector = unsafe { duckdb_data_chunk_get_vector(chunk, col_idx as idx_t) };
66 // SAFETY: vector is non-null for valid column indices.
67 let data = unsafe { duckdb_vector_get_data(vector) }.cast::<u8>();
68 // SAFETY: may be null if all values are valid (no NULLs); checked in is_valid.
69 let validity = unsafe { duckdb_vector_get_validity(vector) };
70 Self {
71 data,
72 validity,
73 row_count,
74 }
75 }
76
77 /// Creates a `VectorReader` directly from a raw `duckdb_vector` handle.
78 ///
79 /// Use this when you already have a child vector (e.g., from
80 /// [`StructVector::get_child`][crate::vector::complex::StructVector::get_child] or
81 /// [`ListVector::get_child`][crate::vector::complex::ListVector::get_child]).
82 ///
83 /// # Safety
84 ///
85 /// - `vector` must be a valid `duckdb_vector` for the duration of this reader's lifetime.
86 /// - `row_count` must equal the number of valid rows in the vector.
87 pub unsafe fn from_vector(vector: duckdb_vector, row_count: usize) -> Self {
88 // SAFETY: vector is valid per caller's contract.
89 let data = unsafe { duckdb_vector_get_data(vector) }.cast::<u8>();
90 let validity = unsafe { duckdb_vector_get_validity(vector) };
91 Self {
92 data,
93 validity,
94 row_count,
95 }
96 }
97
98 /// Returns the number of rows in this vector.
99 #[mutants::skip]
100 #[must_use]
101 #[inline]
102 pub const fn row_count(&self) -> usize {
103 self.row_count
104 }
105
106 /// Returns `true` if the value at row `idx` is not NULL.
107 ///
108 /// # Safety
109 ///
110 /// `idx` must be less than `self.row_count()`.
111 #[inline]
112 pub unsafe fn is_valid(&self, idx: usize) -> bool {
113 if self.validity.is_null() {
114 return true;
115 }
116 // SAFETY: validity is non-null and idx is in bounds per caller's contract.
117 unsafe { duckdb_validity_row_is_valid(self.validity, idx as idx_t) }
118 }
119
120 /// Reads an `i8` (TINYINT) value at row `idx`.
121 ///
122 /// # Safety
123 ///
124 /// - `idx` must be less than `self.row_count()`.
125 /// - The column must contain `TINYINT` data.
126 /// - The value at `idx` must not be NULL (check with [`is_valid`][Self::is_valid]).
127 #[inline]
128 pub const unsafe fn read_i8(&self, idx: usize) -> i8 {
129 // SAFETY: data points to valid TINYINT array, idx is in bounds.
130 unsafe { core::ptr::read_unaligned(self.data.add(idx).cast::<i8>()) }
131 }
132
133 /// Reads an `i16` (SMALLINT) value at row `idx`.
134 ///
135 /// # Safety
136 ///
137 /// - `idx` must be less than `self.row_count()`.
138 /// - The column must contain `SMALLINT` data.
139 #[inline]
140 pub const unsafe fn read_i16(&self, idx: usize) -> i16 {
141 // SAFETY: 2-byte read from valid SMALLINT vector.
142 unsafe { core::ptr::read_unaligned(self.data.add(idx * 2).cast::<i16>()) }
143 }
144
145 /// Reads an `i32` (INTEGER) value at row `idx`.
146 ///
147 /// # Safety
148 ///
149 /// See [`read_i8`][Self::read_i8].
150 #[inline]
151 pub const unsafe fn read_i32(&self, idx: usize) -> i32 {
152 // SAFETY: 4-byte read from valid INTEGER vector.
153 unsafe { core::ptr::read_unaligned(self.data.add(idx * 4).cast::<i32>()) }
154 }
155
156 /// Reads an `i64` (BIGINT / TIMESTAMP) value at row `idx`.
157 ///
158 /// # Safety
159 ///
160 /// See [`read_i8`][Self::read_i8].
161 #[inline]
162 pub const unsafe fn read_i64(&self, idx: usize) -> i64 {
163 // SAFETY: 8-byte read from valid BIGINT/TIMESTAMP vector.
164 unsafe { core::ptr::read_unaligned(self.data.add(idx * 8).cast::<i64>()) }
165 }
166
167 /// Reads a `u8` (UTINYINT) value at row `idx`.
168 ///
169 /// # Safety
170 ///
171 /// See [`read_i8`][Self::read_i8].
172 #[inline]
173 pub const unsafe fn read_u8(&self, idx: usize) -> u8 {
174 // SAFETY: 1-byte read from valid UTINYINT vector.
175 unsafe { *self.data.add(idx) }
176 }
177
178 /// Reads a `u16` (USMALLINT) value at row `idx`.
179 ///
180 /// # Safety
181 ///
182 /// See [`read_i8`][Self::read_i8].
183 #[inline]
184 pub const unsafe fn read_u16(&self, idx: usize) -> u16 {
185 // SAFETY: 2-byte read from valid USMALLINT vector.
186 unsafe { core::ptr::read_unaligned(self.data.add(idx * 2).cast::<u16>()) }
187 }
188
189 /// Reads a `u32` (UINTEGER) value at row `idx`.
190 ///
191 /// # Safety
192 ///
193 /// See [`read_i8`][Self::read_i8].
194 #[inline]
195 pub const unsafe fn read_u32(&self, idx: usize) -> u32 {
196 // SAFETY: 4-byte read from valid UINTEGER vector.
197 unsafe { core::ptr::read_unaligned(self.data.add(idx * 4).cast::<u32>()) }
198 }
199
200 /// Reads a `u64` (UBIGINT) value at row `idx`.
201 ///
202 /// # Safety
203 ///
204 /// See [`read_i8`][Self::read_i8].
205 #[inline]
206 pub const unsafe fn read_u64(&self, idx: usize) -> u64 {
207 // SAFETY: 8-byte read from valid UBIGINT vector.
208 unsafe { core::ptr::read_unaligned(self.data.add(idx * 8).cast::<u64>()) }
209 }
210
211 /// Reads an `f32` (FLOAT) value at row `idx`.
212 ///
213 /// # Safety
214 ///
215 /// See [`read_i8`][Self::read_i8].
216 #[inline]
217 pub const unsafe fn read_f32(&self, idx: usize) -> f32 {
218 // SAFETY: 4-byte read from valid FLOAT vector.
219 unsafe { core::ptr::read_unaligned(self.data.add(idx * 4).cast::<f32>()) }
220 }
221
222 /// Reads an `f64` (DOUBLE) value at row `idx`.
223 ///
224 /// # Safety
225 ///
226 /// See [`read_i8`][Self::read_i8].
227 #[inline]
228 pub const unsafe fn read_f64(&self, idx: usize) -> f64 {
229 // SAFETY: 8-byte read from valid DOUBLE vector.
230 unsafe { core::ptr::read_unaligned(self.data.add(idx * 8).cast::<f64>()) }
231 }
232
233 /// Reads a `bool` (BOOLEAN) value at row `idx`.
234 ///
235 /// # Pitfall L5: Defensive boolean reading
236 ///
237 /// This method reads the underlying byte as `u8` and compares with `!= 0`,
238 /// rather than casting directly to `bool`. `DuckDB`'s C API does not guarantee
239 /// the Rust `bool` invariant (must be exactly 0 or 1), so a direct cast could
240 /// cause undefined behaviour.
241 ///
242 /// # Safety
243 ///
244 /// - `idx` must be less than `self.row_count()`.
245 /// - The column must contain `BOOLEAN` data.
246 #[inline]
247 pub const unsafe fn read_bool(&self, idx: usize) -> bool {
248 // SAFETY: BOOLEAN data is stored as 1 byte per value.
249 // We read as u8 (not bool) to avoid UB if DuckDB sets non-0/1 values.
250 // This is Pitfall L5: always read boolean as u8 then compare != 0.
251 unsafe { *self.data.add(idx) != 0 }
252 }
253
254 /// Reads an `i128` (HUGEINT) value at row `idx`.
255 ///
256 /// `DuckDB` stores HUGEINT as `{ lower: u64, upper: i64 }` in little-endian
257 /// layout, totaling 16 bytes per value.
258 ///
259 /// # Safety
260 ///
261 /// - `idx` must be less than `self.row_count()`.
262 /// - The column must contain `HUGEINT` data.
263 /// - The value at `idx` must not be NULL (check with [`is_valid`][Self::is_valid]).
264 #[inline]
265 pub const unsafe fn read_i128(&self, idx: usize) -> i128 {
266 // SAFETY: HUGEINT is stored as { lower: u64, upper: i64 } = 16 bytes.
267 // DuckDB lays this out in little-endian order: lower at offset 0, upper at offset 8.
268 let base = unsafe { self.data.add(idx * 16) };
269 let lower = unsafe { core::ptr::read_unaligned(base.cast::<u64>()) };
270 let upper = unsafe { core::ptr::read_unaligned(base.add(8).cast::<i64>()) };
271 // Widening casts: u64→i128 and i64→i128 are always lossless.
272 #[allow(clippy::cast_lossless)]
273 let result = (upper as i128) << 64 | (lower as i128);
274 result
275 }
276
277 /// Reads a `u128` (UHUGEINT) value at row `idx`.
278 ///
279 /// `DuckDB` stores UHUGEINT as `{ lower: u64, upper: u64 }` in little-endian
280 /// layout, totalling 16 bytes per value.
281 ///
282 /// # Safety
283 ///
284 /// - `idx` must be less than `self.row_count()`.
285 /// - The column must contain `UHUGEINT` data.
286 #[inline]
287 pub const unsafe fn read_u128(&self, idx: usize) -> u128 {
288 // SAFETY: UHUGEINT = { lower: u64, upper: u64 } = 16 bytes.
289 let base = unsafe { self.data.add(idx * 16) };
290 let lower = unsafe { core::ptr::read_unaligned(base.cast::<u64>()) };
291 let upper = unsafe { core::ptr::read_unaligned(base.add(8).cast::<u64>()) };
292 ((upper as u128) << 64) | (lower as u128)
293 }
294
295 /// Reads a `TIMESTAMP WITH TIME ZONE` value at row `idx`, as microseconds
296 /// since the Unix epoch in UTC.
297 ///
298 /// # Safety
299 ///
300 /// - `idx` must be less than `self.row_count()`.
301 /// - The column must contain `TIMESTAMPTZ` data.
302 #[inline]
303 pub const unsafe fn read_timestamp_tz(&self, idx: usize) -> i64 {
304 // SAFETY: TIMESTAMPTZ shares TIMESTAMP's i64 storage.
305 unsafe { self.read_i64(idx) }
306 }
307
308 /// Reads a `TIMESTAMP_S` value at row `idx`, as seconds since the epoch.
309 ///
310 /// # Safety
311 ///
312 /// - `idx` must be less than `self.row_count()`.
313 /// - The column must contain `TIMESTAMP_S` data.
314 #[inline]
315 pub const unsafe fn read_timestamp_s(&self, idx: usize) -> i64 {
316 // SAFETY: TIMESTAMP_S is stored as i64 seconds.
317 unsafe { self.read_i64(idx) }
318 }
319
320 /// Reads a `TIMESTAMP_MS` value at row `idx`, as milliseconds since the
321 /// epoch.
322 ///
323 /// # Safety
324 ///
325 /// - `idx` must be less than `self.row_count()`.
326 /// - The column must contain `TIMESTAMP_MS` data.
327 #[inline]
328 pub const unsafe fn read_timestamp_ms(&self, idx: usize) -> i64 {
329 // SAFETY: TIMESTAMP_MS is stored as i64 milliseconds.
330 unsafe { self.read_i64(idx) }
331 }
332
333 /// Reads a `TIMESTAMP_NS` value at row `idx`, as nanoseconds since the
334 /// epoch.
335 ///
336 /// # Safety
337 ///
338 /// - `idx` must be less than `self.row_count()`.
339 /// - The column must contain `TIMESTAMP_NS` data.
340 #[inline]
341 pub const unsafe fn read_timestamp_ns(&self, idx: usize) -> i64 {
342 // SAFETY: TIMESTAMP_NS is stored as i64 nanoseconds.
343 unsafe { self.read_i64(idx) }
344 }
345
346 /// Reads a `TIME WITH TIME ZONE` value at row `idx` as `DuckDB`'s packed
347 /// 64-bit representation.
348 ///
349 /// Decode it with
350 /// [`datetime::time_tz_from_bits`][crate::datetime::time_tz_from_bits].
351 ///
352 /// # Safety
353 ///
354 /// - `idx` must be less than `self.row_count()`.
355 /// - The column must contain `TIMETZ` data.
356 #[inline]
357 pub const unsafe fn read_time_tz(&self, idx: usize) -> u64 {
358 // SAFETY: TIMETZ is stored as a 64-bit packed value.
359 unsafe { self.read_u64(idx) }
360 }
361
362 /// Reads a `DECIMAL` value at row `idx` as its unscaled integer.
363 ///
364 /// `DuckDB` stores a `DECIMAL` in the narrowest integer that fits its
365 /// declared width — `i16` up to 4 digits, `i32` up to 9, `i64` up to 18, and
366 /// `i128` up to 38 — so `width` must be the column's declared width. Get it
367 /// from [`LogicalType::decimal_width`][crate::types::LogicalType::decimal_width].
368 ///
369 /// The represented number is `result / 10^scale`.
370 ///
371 /// # Safety
372 ///
373 /// - `idx` must be less than `self.row_count()`.
374 /// - The column must contain `DECIMAL` data with exactly this `width`.
375 #[inline]
376 pub const unsafe fn read_decimal(&self, idx: usize, width: u8) -> i128 {
377 // SAFETY: the caller guarantees `width` matches the column's declared
378 // width, which fixes the physical storage type.
379 unsafe {
380 if width <= 4 {
381 self.read_i16(idx) as i128
382 } else if width <= 9 {
383 self.read_i32(idx) as i128
384 } else if width <= 18 {
385 self.read_i64(idx) as i128
386 } else {
387 self.read_i128(idx)
388 }
389 }
390 }
391
392 /// Returns `true` if `idx` addresses a row of this vector.
393 ///
394 /// Every `read_*` method requires `idx < row_count()`; this is the check to
395 /// pair with them when the index comes from somewhere other than a
396 /// `0..row_count()` loop.
397 #[must_use]
398 #[inline]
399 pub const fn contains(&self, idx: usize) -> bool {
400 idx < self.row_count
401 }
402
403 /// Reads a VARCHAR value at row `idx`.
404 ///
405 /// Returns an empty string if the data is not valid UTF-8 or if the internal
406 /// string pointer is null.
407 ///
408 /// # Pitfall P7
409 ///
410 /// `DuckDB` stores strings in a 16-byte `duckdb_string_t` with two formats
411 /// (inline for ≤ 12 bytes, pointer otherwise). This method handles both.
412 ///
413 /// # Safety
414 ///
415 /// - `idx` must be less than `self.row_count()`.
416 /// - The column must contain `VARCHAR` data.
417 /// - For pointer-format strings, the pointed-to heap memory must be valid
418 /// for the lifetime of the returned `&str`.
419 pub unsafe fn read_str(&self, idx: usize) -> &str {
420 // SAFETY: Caller guarantees data is a VARCHAR vector and idx is in bounds.
421 unsafe { crate::vector::string::read_duck_string(self.data, idx) }
422 }
423
424 /// Reads a `BLOB` (binary) value at row `idx`.
425 ///
426 /// `DuckDB` stores BLOBs using the same 16-byte `duckdb_string_t` layout as
427 /// VARCHAR (inline for ≤12 bytes, pointer for larger values). The returned
428 /// slice borrows from the vector's data buffer.
429 ///
430 /// The bytes are returned without UTF-8 validation.
431 ///
432 /// # Safety
433 ///
434 /// - `idx` must be less than `self.row_count()`.
435 /// - The column must contain `BLOB` data.
436 /// - The pointed-to memory must be valid for the lifetime of the returned slice.
437 pub unsafe fn read_blob(&self, idx: usize) -> &[u8] {
438 // SAFETY: BLOB uses the same duckdb_string_t layout as VARCHAR.
439 unsafe { crate::vector::string::read_duck_blob(self.data, idx) }
440 }
441
442 /// Reads a `UUID` value at row `idx` as an `i128`.
443 ///
444 /// Reads the UUID's **textual** 128 bits — the value the column renders,
445 /// and what every Rust `Uuid` type holds.
446 ///
447 /// A `UUID` column is physically a `HUGEINT`, but `DuckDB` stores it with
448 /// the top bit flipped so that signed integer ordering matches UUID string
449 /// ordering, so the raw storage of
450 /// `'11111111-2222-3333-4444-555555555555'` is `0x9111...`, not `0x1111...`.
451 /// This undoes that. Use [`read_i128`][Self::read_i128] for the raw storage,
452 /// and [`uuid_from_storage`][crate::vector::uuid_from_storage] to convert.
453 ///
454 /// # Safety
455 ///
456 /// - `idx` must be less than `self.row_count()`.
457 /// - The column must contain `UUID` data.
458 #[inline]
459 pub const unsafe fn read_uuid(&self, idx: usize) -> u128 {
460 // SAFETY: UUID is stored as HUGEINT; undo DuckDB's top-bit flip.
461 unsafe { crate::vector::uuid::uuid_from_storage(self.read_i128(idx)) }
462 }
463
464 /// Reads a `DATE` value at row `idx` as days since the Unix epoch.
465 ///
466 /// `DuckDB` stores DATE as a 4-byte `i32` representing the number of days
467 /// since 1970-01-01. This is a semantic alias for [`read_i32`][Self::read_i32].
468 ///
469 /// # Safety
470 ///
471 /// - `idx` must be less than `self.row_count()`.
472 /// - The column must contain `DATE` data.
473 #[inline]
474 pub const unsafe fn read_date(&self, idx: usize) -> i32 {
475 // SAFETY: DATE is stored as i32 (days since epoch).
476 unsafe { self.read_i32(idx) }
477 }
478
479 /// Reads a `TIMESTAMP` value at row `idx` as microseconds since the Unix epoch.
480 ///
481 /// `DuckDB` stores TIMESTAMP as an 8-byte `i64` representing microseconds
482 /// since 1970-01-01 00:00:00 UTC. This is a semantic alias for
483 /// [`read_i64`][Self::read_i64].
484 ///
485 /// # Safety
486 ///
487 /// - `idx` must be less than `self.row_count()`.
488 /// - The column must contain `TIMESTAMP` data.
489 #[inline]
490 pub const unsafe fn read_timestamp(&self, idx: usize) -> i64 {
491 // SAFETY: TIMESTAMP is stored as i64 (microseconds since epoch).
492 unsafe { self.read_i64(idx) }
493 }
494
495 /// Reads a `TIME` value at row `idx` as microseconds since midnight.
496 ///
497 /// `DuckDB` stores TIME as an 8-byte `i64` representing microseconds since
498 /// midnight. This is a semantic alias for [`read_i64`][Self::read_i64].
499 ///
500 /// # Safety
501 ///
502 /// - `idx` must be less than `self.row_count()`.
503 /// - The column must contain `TIME` data.
504 #[inline]
505 pub const unsafe fn read_time(&self, idx: usize) -> i64 {
506 // SAFETY: TIME is stored as i64 (microseconds since midnight).
507 unsafe { self.read_i64(idx) }
508 }
509
510 /// Reads an `INTERVAL` value at row `idx`.
511 ///
512 /// Returns a [`DuckInterval`][crate::interval::DuckInterval] struct.
513 ///
514 /// # Pitfall P8
515 ///
516 /// The `INTERVAL` struct is 16 bytes: `{ months: i32, days: i32, micros: i64 }`.
517 /// This method handles the layout correctly using [`read_interval_at`][crate::interval::read_interval_at].
518 ///
519 /// # Safety
520 ///
521 /// - `idx` must be less than `self.row_count()`.
522 /// - The column must contain `INTERVAL` data.
523 #[inline]
524 pub const unsafe fn read_interval(&self, idx: usize) -> crate::interval::DuckInterval {
525 // SAFETY: data is a valid INTERVAL vector and idx is in bounds.
526 unsafe { crate::interval::read_interval_at(self.data, idx) }
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 /// Verify that `VectorReader` handles the boolean-as-u8 pattern correctly.
535 #[test]
536 fn bool_read_u8_pattern() {
537 // Simulate a DuckDB BOOLEAN vector with a non-standard value (e.g., 2)
538 // to verify we use != 0 comparison rather than transmuting to bool.
539 let data: [u8; 4] = [0, 1, 2, 255];
540
541 // Directly test the read_bool logic by checking values
542 // (We can't easily create a real VectorReader without DuckDB, so we test
543 // the underlying invariant: any non-zero byte is `true`.)
544 let as_bools: Vec<bool> = data.iter().map(|&b| b != 0).collect();
545 assert_eq!(as_bools, [false, true, true, true]);
546 }
547
548 #[test]
549 fn row_count_is_zero_for_empty_state() {
550 // This exercises the struct layout; actual DuckDB integration is in tests/
551 let reader = VectorReader {
552 data: std::ptr::null(),
553 validity: std::ptr::null_mut(),
554 row_count: 0,
555 };
556 assert_eq!(reader.row_count(), 0);
557 }
558
559 #[test]
560 fn contains_bounds_checks_against_row_count() {
561 let reader = VectorReader {
562 data: std::ptr::null(),
563 validity: std::ptr::null_mut(),
564 row_count: 3,
565 };
566 assert!(reader.contains(0));
567 assert!(reader.contains(2));
568 assert!(!reader.contains(3));
569 assert!(!reader.contains(usize::MAX));
570 }
571
572 #[test]
573 fn decimal_width_thresholds_match_duckdb_storage() {
574 // DuckDB picks the physical type from the declared width:
575 // <=4 -> INT16, <=9 -> INT32, <=18 -> INT64, <=38 -> INT128
576 // (duckdb/common/types/decimal.hpp). Reading with the wrong width reads
577 // the wrong number of bytes, so pin the boundaries.
578 let mut buf = [0u8; 16];
579 buf[..2].copy_from_slice(&(-1234_i16).to_le_bytes());
580 let reader = VectorReader {
581 data: buf.as_ptr(),
582 validity: std::ptr::null_mut(),
583 row_count: 1,
584 };
585 // SAFETY: `buf` holds one INT16 at index 0.
586 assert_eq!(unsafe { reader.read_decimal(0, 4) }, -1234);
587
588 let mut buf = [0u8; 16];
589 buf[..4].copy_from_slice(&(-123_456_789_i32).to_le_bytes());
590 let reader = VectorReader {
591 data: buf.as_ptr(),
592 validity: std::ptr::null_mut(),
593 row_count: 1,
594 };
595 // SAFETY: `buf` holds one INT32 at index 0.
596 assert_eq!(unsafe { reader.read_decimal(0, 9) }, -123_456_789);
597
598 let mut buf = [0u8; 16];
599 buf[..8].copy_from_slice(&(-1_234_567_890_123_456_789_i64).to_le_bytes());
600 let reader = VectorReader {
601 data: buf.as_ptr(),
602 validity: std::ptr::null_mut(),
603 row_count: 1,
604 };
605 // SAFETY: `buf` holds one INT64 at index 0.
606 assert_eq!(
607 unsafe { reader.read_decimal(0, 18) },
608 -1_234_567_890_123_456_789
609 );
610
611 let value: i128 = -170_141_183_460_469_231_731_687_303_715_884_105_727;
612 let buf = value.to_le_bytes();
613 let reader = VectorReader {
614 data: buf.as_ptr(),
615 validity: std::ptr::null_mut(),
616 row_count: 1,
617 };
618 // SAFETY: `buf` holds one INT128 at index 0.
619 assert_eq!(unsafe { reader.read_decimal(0, 38) }, value);
620 }
621
622 #[test]
623 fn u128_reads_little_endian_halves() {
624 let value: u128 = (0xdead_beef_u128 << 64) | 0x1234_5678;
625 let buf = value.to_le_bytes();
626 let reader = VectorReader {
627 data: buf.as_ptr(),
628 validity: std::ptr::null_mut(),
629 row_count: 1,
630 };
631 // SAFETY: `buf` holds one UHUGEINT at index 0.
632 assert_eq!(unsafe { reader.read_u128(0) }, value);
633 }
634
635 #[test]
636 fn is_valid_when_validity_null() {
637 // When validity is null, all rows are considered valid
638 let reader = VectorReader {
639 data: std::ptr::null(),
640 validity: std::ptr::null_mut(),
641 row_count: 5,
642 };
643 // SAFETY: row 0 is in bounds (row_count = 5), validity is null (all valid)
644 assert!(unsafe { reader.is_valid(0) });
645 assert!(unsafe { reader.is_valid(4) });
646 }
647}