Skip to main content

quack_rs/vector/
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//! Safe typed writing to `DuckDB` result vectors.
7//!
8//! [`VectorWriter`] provides safe methods for writing typed values and NULL
9//! flags to a `DuckDB` output vector from within a `finalize` callback.
10//!
11//! # Pitfall L4: `ensure_validity_writable`
12//!
13//! When writing NULL values, you must call `duckdb_vector_ensure_validity_writable`
14//! before `duckdb_vector_get_validity`. If you skip this call, `get_validity`
15//! returns an uninitialized pointer that will cause a segfault or silent corruption.
16//!
17//! [`VectorWriter::set_null`] calls `ensure_validity_writable` automatically.
18
19use libduckdb_sys::{
20    duckdb_validity_set_row_invalid, duckdb_validity_set_row_valid, duckdb_vector,
21    duckdb_vector_assign_string_element_len, duckdb_vector_ensure_validity_writable,
22    duckdb_vector_get_data, duckdb_vector_get_validity, idx_t,
23};
24
25/// A typed writer for a `DuckDB` output vector in a `finalize` callback.
26///
27/// # Example
28///
29/// ```rust,no_run
30/// use quack_rs::vector::VectorWriter;
31/// use libduckdb_sys::duckdb_vector;
32///
33/// // Inside finalize:
34/// // let mut writer = unsafe { VectorWriter::new(result_vector) };
35/// // for row in 0..count {
36/// //     if let Some(val) = compute_result(row) {
37/// //         unsafe { writer.write_i64(row, val) };
38/// //     } else {
39/// //         unsafe { writer.set_null(row) };
40/// //     }
41/// // }
42/// ```
43#[derive(Debug)]
44pub struct VectorWriter {
45    vector: duckdb_vector,
46    data: *mut u8,
47    /// Lazily-resolved validity bitmap.
48    ///
49    /// `duckdb_vector_ensure_validity_writable` allocates the mask on first use
50    /// and is a no-op afterwards, and the resulting pointer is stable for the
51    /// vector's lifetime. Caching it turns "two FFI calls per NULL" into "two
52    /// FFI calls per vector", which matters when a column is mostly NULL: a full
53    /// 2048-row vector went from 4096 calls to 2.
54    validity: *mut u64,
55}
56
57impl VectorWriter {
58    /// Creates a new `VectorWriter` for the given result vector.
59    ///
60    /// # Safety
61    ///
62    /// `vector` must be a valid `DuckDB` output vector obtained in a `finalize`
63    /// callback. The vector must not be destroyed while this writer is live.
64    pub unsafe fn new(vector: duckdb_vector) -> Self {
65        // SAFETY: Caller guarantees vector is valid.
66        let data = unsafe { duckdb_vector_get_data(vector) }.cast::<u8>();
67        Self {
68            vector,
69            data,
70            validity: core::ptr::null_mut(),
71        }
72    }
73
74    /// Creates a `VectorWriter` directly from a raw `duckdb_vector` handle.
75    ///
76    /// Use this when you need to write into a child vector (e.g., a STRUCT field
77    /// or LIST element vector) obtained from
78    /// [`StructVector::get_child`][crate::vector::complex::StructVector::get_child] or
79    /// [`ListVector::get_child`][crate::vector::complex::ListVector::get_child].
80    ///
81    /// # Safety
82    ///
83    /// `vector` must be a valid, writable `duckdb_vector`. The vector must not be
84    /// destroyed while this writer is live.
85    pub unsafe fn from_vector(vector: duckdb_vector) -> Self {
86        // SAFETY: caller guarantees vector is valid.
87        let data = unsafe { duckdb_vector_get_data(vector) }.cast::<u8>();
88        Self {
89            vector,
90            data,
91            validity: core::ptr::null_mut(),
92        }
93    }
94
95    /// Writes an `i8` (TINYINT) value at row `idx`.
96    ///
97    /// # Safety
98    ///
99    /// - `idx` must be within the vector's capacity.
100    /// - The vector must have `TINYINT` type.
101    #[inline]
102    pub const unsafe fn write_i8(&mut self, idx: usize, value: i8) {
103        // SAFETY: data points to a valid writable TINYINT array. idx is in bounds.
104        unsafe { core::ptr::write_unaligned(self.data.add(idx).cast::<i8>(), value) };
105    }
106
107    /// Writes an `i16` (SMALLINT) value at row `idx`.
108    ///
109    /// # Safety
110    ///
111    /// See [`write_i8`][Self::write_i8].
112    #[inline]
113    pub const unsafe fn write_i16(&mut self, idx: usize, value: i16) {
114        // SAFETY: 2-byte aligned write to valid SMALLINT vector.
115        unsafe { core::ptr::write_unaligned(self.data.add(idx * 2).cast::<i16>(), value) };
116    }
117
118    /// Writes an `i32` (INTEGER) value at row `idx`.
119    ///
120    /// # Safety
121    ///
122    /// See [`write_i8`][Self::write_i8].
123    #[inline]
124    pub const unsafe fn write_i32(&mut self, idx: usize, value: i32) {
125        // SAFETY: 4-byte aligned write to valid INTEGER vector.
126        unsafe { core::ptr::write_unaligned(self.data.add(idx * 4).cast::<i32>(), value) };
127    }
128
129    /// Writes an `i64` (BIGINT / TIMESTAMP) value at row `idx`.
130    ///
131    /// # Safety
132    ///
133    /// See [`write_i8`][Self::write_i8].
134    #[inline]
135    pub const unsafe fn write_i64(&mut self, idx: usize, value: i64) {
136        // SAFETY: 8-byte aligned write to valid BIGINT vector.
137        unsafe { core::ptr::write_unaligned(self.data.add(idx * 8).cast::<i64>(), value) };
138    }
139
140    /// Writes a `u8` (UTINYINT) value at row `idx`.
141    ///
142    /// # Safety
143    ///
144    /// See [`write_i8`][Self::write_i8].
145    #[inline]
146    pub const unsafe fn write_u8(&mut self, idx: usize, value: u8) {
147        // SAFETY: 1-byte write to valid UTINYINT vector.
148        unsafe { *self.data.add(idx) = value };
149    }
150
151    /// Writes a `u32` (UINTEGER) value at row `idx`.
152    ///
153    /// # Safety
154    ///
155    /// See [`write_i8`][Self::write_i8].
156    #[inline]
157    pub const unsafe fn write_u32(&mut self, idx: usize, value: u32) {
158        // SAFETY: 4-byte aligned write to valid UINTEGER vector.
159        unsafe { core::ptr::write_unaligned(self.data.add(idx * 4).cast::<u32>(), value) };
160    }
161
162    /// Writes a `u64` (UBIGINT) value at row `idx`.
163    ///
164    /// # Safety
165    ///
166    /// See [`write_i8`][Self::write_i8].
167    #[inline]
168    pub const unsafe fn write_u64(&mut self, idx: usize, value: u64) {
169        // SAFETY: 8-byte aligned write to valid UBIGINT vector.
170        unsafe { core::ptr::write_unaligned(self.data.add(idx * 8).cast::<u64>(), value) };
171    }
172
173    /// Writes an `f32` (FLOAT) value at row `idx`.
174    ///
175    /// # Safety
176    ///
177    /// See [`write_i8`][Self::write_i8].
178    #[inline]
179    pub const unsafe fn write_f32(&mut self, idx: usize, value: f32) {
180        // SAFETY: 4-byte aligned write to valid FLOAT vector.
181        unsafe { core::ptr::write_unaligned(self.data.add(idx * 4).cast::<f32>(), value) };
182    }
183
184    /// Writes an `f64` (DOUBLE) value at row `idx`.
185    ///
186    /// # Safety
187    ///
188    /// See [`write_i8`][Self::write_i8].
189    #[inline]
190    pub const unsafe fn write_f64(&mut self, idx: usize, value: f64) {
191        // SAFETY: 8-byte aligned write to valid DOUBLE vector.
192        unsafe { core::ptr::write_unaligned(self.data.add(idx * 8).cast::<f64>(), value) };
193    }
194
195    /// Writes a `bool` (BOOLEAN) value at row `idx`.
196    ///
197    /// Booleans are stored as a single byte: `1` for `true`, `0` for `false`.
198    ///
199    /// # Safety
200    ///
201    /// - `idx` must be within the vector's capacity.
202    /// - The vector must have `BOOLEAN` type.
203    #[inline]
204    pub unsafe fn write_bool(&mut self, idx: usize, value: bool) {
205        // SAFETY: BOOLEAN stored as 1 byte.
206        unsafe { *self.data.add(idx) = u8::from(value) };
207    }
208
209    /// Writes an `i128` (HUGEINT) value at row `idx`.
210    ///
211    /// `DuckDB` stores HUGEINT as `{ lower: u64, upper: i64 }` in little-endian
212    /// layout, totaling 16 bytes per value.
213    ///
214    /// # Safety
215    ///
216    /// - `idx` must be within the vector's capacity.
217    /// - The vector must have `HUGEINT` type.
218    #[inline]
219    pub const unsafe fn write_i128(&mut self, idx: usize, value: i128) {
220        // SAFETY: HUGEINT = { lower: u64, upper: i64 } = 16 bytes.
221        let base = unsafe { self.data.add(idx * 16) };
222        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
223        let lower = value as u64;
224        #[allow(clippy::cast_possible_truncation)]
225        let upper = (value >> 64) as i64;
226        unsafe {
227            core::ptr::write_unaligned(base.cast::<u64>(), lower);
228            core::ptr::write_unaligned(base.add(8).cast::<i64>(), upper);
229        }
230    }
231
232    /// Writes a `u16` (USMALLINT) value at row `idx`.
233    ///
234    /// # Safety
235    ///
236    /// See [`write_i8`][Self::write_i8].
237    #[inline]
238    pub const unsafe fn write_u16(&mut self, idx: usize, value: u16) {
239        // SAFETY: 2-byte aligned write to valid USMALLINT vector.
240        unsafe { core::ptr::write_unaligned(self.data.add(idx * 2).cast::<u16>(), value) };
241    }
242
243    /// Writes a VARCHAR string value at row `idx`.
244    ///
245    /// This uses `duckdb_vector_assign_string_element_len` which handles both
246    /// the inline (≤12 bytes) and pointer (>12 bytes) storage formats
247    /// automatically. `DuckDB` manages the memory for the string data.
248    ///
249    /// # Note on very long strings
250    ///
251    /// If `value.len()` exceeds `idx_t::MAX` (2^64 − 1 on 64-bit platforms),
252    /// the length is silently clamped to `idx_t::MAX`. In practice, this limit
253    /// is unreachable on any current hardware (≈18 exabytes), so no explicit
254    /// error path is provided.
255    ///
256    /// # Safety
257    ///
258    /// - `idx` must be within the vector's capacity.
259    /// - The vector must have `VARCHAR` type.
260    pub unsafe fn write_varchar(&mut self, idx: usize, value: &str) {
261        // SAFETY: self.vector is valid per constructor's contract.
262        // duckdb_vector_assign_string_element_len copies the string data.
263        unsafe {
264            duckdb_vector_assign_string_element_len(
265                self.vector,
266                idx as idx_t,
267                value.as_ptr().cast::<std::os::raw::c_char>(),
268                idx_t::try_from(value.len()).unwrap_or(idx_t::MAX),
269            );
270        }
271    }
272
273    /// Writes a `DATE` value at row `idx` as days since the Unix epoch.
274    ///
275    /// `DuckDB` stores DATE as a 4-byte `i32`. This is a semantic alias for
276    /// [`write_i32`][Self::write_i32].
277    ///
278    /// # Safety
279    ///
280    /// - `idx` must be within the vector's capacity.
281    /// - The vector must have `DATE` type.
282    #[inline]
283    pub const unsafe fn write_date(&mut self, idx: usize, days_since_epoch: i32) {
284        // SAFETY: DATE is stored as i32.
285        unsafe { self.write_i32(idx, days_since_epoch) };
286    }
287
288    /// Writes a `TIMESTAMP` value at row `idx` as microseconds since the Unix epoch.
289    ///
290    /// `DuckDB` stores TIMESTAMP as an 8-byte `i64`. This is a semantic alias for
291    /// [`write_i64`][Self::write_i64].
292    ///
293    /// # Safety
294    ///
295    /// - `idx` must be within the vector's capacity.
296    /// - The vector must have `TIMESTAMP` type.
297    #[inline]
298    pub const unsafe fn write_timestamp(&mut self, idx: usize, micros_since_epoch: i64) {
299        // SAFETY: TIMESTAMP is stored as i64.
300        unsafe { self.write_i64(idx, micros_since_epoch) };
301    }
302
303    /// Writes a `TIME` value at row `idx` as microseconds since midnight.
304    ///
305    /// `DuckDB` stores TIME as an 8-byte `i64`. This is a semantic alias for
306    /// [`write_i64`][Self::write_i64].
307    ///
308    /// # Safety
309    ///
310    /// - `idx` must be within the vector's capacity.
311    /// - The vector must have `TIME` type.
312    #[inline]
313    pub const unsafe fn write_time(&mut self, idx: usize, micros_since_midnight: i64) {
314        // SAFETY: TIME is stored as i64.
315        unsafe { self.write_i64(idx, micros_since_midnight) };
316    }
317
318    /// Writes an INTERVAL value at row `idx`.
319    ///
320    /// `DuckDB` stores INTERVAL as `{ months: i32, days: i32, micros: i64 }` in a
321    /// 16-byte layout. This method writes all three components at the correct offsets.
322    ///
323    /// # Safety
324    ///
325    /// - `idx` must be within the vector's capacity.
326    /// - The vector must have `INTERVAL` type.
327    #[inline]
328    pub const unsafe fn write_interval(
329        &mut self,
330        idx: usize,
331        value: crate::interval::DuckInterval,
332    ) {
333        // SAFETY: INTERVAL = { months: i32 @ 0, days: i32 @ 4, micros: i64 @ 8 } = 16 bytes.
334        let base = unsafe { self.data.add(idx * 16) };
335        unsafe {
336            core::ptr::write_unaligned(base.cast::<i32>(), value.months);
337            core::ptr::write_unaligned(base.add(4).cast::<i32>(), value.days);
338            core::ptr::write_unaligned(base.add(8).cast::<i64>(), value.micros);
339        }
340    }
341
342    /// Writes a `BLOB` (binary) value at row `idx`.
343    ///
344    /// This uses the same underlying storage as VARCHAR — `DuckDB` stores BLOBs
345    /// using `duckdb_vector_assign_string_element_len`, which copies the data.
346    ///
347    /// # Safety
348    ///
349    /// - `idx` must be within the vector's capacity.
350    /// - The vector must have `BLOB` type.
351    pub unsafe fn write_blob(&mut self, idx: usize, value: &[u8]) {
352        // SAFETY: BLOB uses the same storage as VARCHAR.
353        unsafe {
354            duckdb_vector_assign_string_element_len(
355                self.vector,
356                idx as idx_t,
357                value.as_ptr().cast::<std::os::raw::c_char>(),
358                idx_t::try_from(value.len()).unwrap_or(idx_t::MAX),
359            );
360        }
361    }
362
363    /// Writes a `UUID` value at row `idx`.
364    ///
365    /// Writes `bits` — the UUID's **textual** 128 bits, as every Rust `Uuid`
366    /// type holds them — at row `idx`.
367    ///
368    /// A `UUID` column is physically a `HUGEINT`, but `DuckDB` stores it with
369    /// the top bit flipped so that signed integer ordering matches UUID string
370    /// ordering. This applies that flip, so the value you pass is the value the
371    /// column renders. Use [`write_i128`][Self::write_i128] to write the raw
372    /// storage instead, and [`uuid_to_storage`][crate::vector::uuid_to_storage]
373    /// to convert between the two.
374    ///
375    /// # Safety
376    ///
377    /// - `idx` must be within the vector's capacity.
378    /// - The vector must have `UUID` type.
379    #[inline]
380    pub const unsafe fn write_uuid(&mut self, idx: usize, bits: u128) {
381        // SAFETY: UUID is stored as HUGEINT, with DuckDB's top-bit flip applied.
382        unsafe { self.write_i128(idx, crate::vector::uuid::uuid_to_storage(bits)) };
383    }
384
385    /// Writes a VARCHAR string value at row `idx`.
386    ///
387    /// This is an alias for [`write_varchar`][VectorWriter::write_varchar] provided
388    /// for discoverability — extension authors often look for `write_str` first.
389    ///
390    /// # Safety
391    ///
392    /// - `idx` must be within the vector's capacity.
393    /// - The vector must have `VARCHAR` type.
394    #[inline]
395    pub unsafe fn write_str(&mut self, idx: usize, value: &str) {
396        // SAFETY: Delegates to write_varchar; same contract.
397        unsafe { self.write_varchar(idx, value) };
398    }
399
400    /// Writes a `u128` (UHUGEINT) value at row `idx`.
401    ///
402    /// `DuckDB` stores UHUGEINT as `{ lower: u64, upper: u64 }` in little-endian
403    /// layout, totalling 16 bytes per value.
404    ///
405    /// # Safety
406    ///
407    /// - `idx` must be within the vector's capacity.
408    /// - The vector must have `UHUGEINT` type.
409    #[inline]
410    pub const unsafe fn write_u128(&mut self, idx: usize, value: u128) {
411        // SAFETY: UHUGEINT = { lower: u64, upper: u64 } = 16 bytes.
412        let base = unsafe { self.data.add(idx * 16) };
413        #[allow(clippy::cast_possible_truncation)]
414        let lower = value as u64;
415        #[allow(clippy::cast_possible_truncation)]
416        let upper = (value >> 64) as u64;
417        unsafe {
418            core::ptr::write_unaligned(base.cast::<u64>(), lower);
419            core::ptr::write_unaligned(base.add(8).cast::<u64>(), upper);
420        }
421    }
422
423    /// Writes a `TIMESTAMP WITH TIME ZONE` value at row `idx`, as microseconds
424    /// since the Unix epoch in UTC.
425    ///
426    /// `TIMESTAMPTZ` shares `TIMESTAMP`'s 8-byte `i64` storage; only the logical
427    /// type differs.
428    ///
429    /// # Safety
430    ///
431    /// - `idx` must be within the vector's capacity.
432    /// - The vector must have `TIMESTAMPTZ` type.
433    #[inline]
434    pub const unsafe fn write_timestamp_tz(&mut self, idx: usize, micros_since_epoch: i64) {
435        // SAFETY: TIMESTAMPTZ is stored as i64 microseconds.
436        unsafe { self.write_i64(idx, micros_since_epoch) };
437    }
438
439    /// Writes a `TIMESTAMP_S` value at row `idx`, as seconds since the epoch.
440    ///
441    /// # Safety
442    ///
443    /// - `idx` must be within the vector's capacity.
444    /// - The vector must have `TIMESTAMP_S` type.
445    #[inline]
446    pub const unsafe fn write_timestamp_s(&mut self, idx: usize, seconds_since_epoch: i64) {
447        // SAFETY: TIMESTAMP_S is stored as i64 seconds.
448        unsafe { self.write_i64(idx, seconds_since_epoch) };
449    }
450
451    /// Writes a `TIMESTAMP_MS` value at row `idx`, as milliseconds since the
452    /// epoch.
453    ///
454    /// # Safety
455    ///
456    /// - `idx` must be within the vector's capacity.
457    /// - The vector must have `TIMESTAMP_MS` type.
458    #[inline]
459    pub const unsafe fn write_timestamp_ms(&mut self, idx: usize, millis_since_epoch: i64) {
460        // SAFETY: TIMESTAMP_MS is stored as i64 milliseconds.
461        unsafe { self.write_i64(idx, millis_since_epoch) };
462    }
463
464    /// Writes a `TIMESTAMP_NS` value at row `idx`, as nanoseconds since the
465    /// epoch.
466    ///
467    /// # Safety
468    ///
469    /// - `idx` must be within the vector's capacity.
470    /// - The vector must have `TIMESTAMP_NS` type.
471    #[inline]
472    pub const unsafe fn write_timestamp_ns(&mut self, idx: usize, nanos_since_epoch: i64) {
473        // SAFETY: TIMESTAMP_NS is stored as i64 nanoseconds.
474        unsafe { self.write_i64(idx, nanos_since_epoch) };
475    }
476
477    /// Writes a `TIME WITH TIME ZONE` value at row `idx`.
478    ///
479    /// `bits` is `DuckDB`'s packed representation; build one with
480    /// [`datetime::time_tz_bits`][crate::datetime::time_tz_bits] rather than
481    /// assembling it by hand.
482    ///
483    /// # Safety
484    ///
485    /// - `idx` must be within the vector's capacity.
486    /// - The vector must have `TIMETZ` type.
487    #[inline]
488    pub const unsafe fn write_time_tz(&mut self, idx: usize, bits: u64) {
489        // SAFETY: TIMETZ is stored as a 64-bit packed value.
490        unsafe { self.write_u64(idx, bits) };
491    }
492
493    /// Writes a `DECIMAL` value at row `idx` from its unscaled representation.
494    ///
495    /// `DuckDB` stores a `DECIMAL` in the narrowest integer that fits its
496    /// declared width — `i16` up to 4 digits, `i32` up to 9, `i64` up to 18, and
497    /// `i128` beyond — so the width must match the column's type. Get it from
498    /// [`LogicalType::decimal_width`][crate::types::LogicalType::decimal_width].
499    ///
500    /// # Safety
501    ///
502    /// - `idx` must be within the vector's capacity.
503    /// - The vector must have `DECIMAL` type with exactly this `width`.
504    #[inline]
505    pub const unsafe fn write_decimal(&mut self, idx: usize, width: u8, unscaled: i128) {
506        // SAFETY: the caller guarantees `width` matches the column's declared
507        // width, which fixes the physical storage type.
508        unsafe {
509            #[allow(clippy::cast_possible_truncation)]
510            if width <= 4 {
511                self.write_i16(idx, unscaled as i16);
512            } else if width <= 9 {
513                self.write_i32(idx, unscaled as i32);
514            } else if width <= 18 {
515                self.write_i64(idx, unscaled as i64);
516            } else {
517                self.write_i128(idx, unscaled);
518            }
519        }
520    }
521
522    /// Marks row `idx` as NULL in the output vector.
523    ///
524    /// # Pitfall L4: `ensure_validity_writable`
525    ///
526    /// This method calls `duckdb_vector_ensure_validity_writable` before
527    /// `duckdb_vector_get_validity`, which is required before writing any NULL
528    /// flags. Forgetting this call returns an uninitialized pointer.
529    ///
530    /// # Safety
531    ///
532    /// - `idx` must be within the vector's capacity.
533    pub unsafe fn set_null(&mut self, idx: usize) {
534        // SAFETY: self.vector is valid per constructor's contract.
535        let validity = unsafe { self.writable_validity() };
536        // SAFETY: validity is now initialized and idx is in bounds per caller's contract.
537        unsafe {
538            duckdb_validity_set_row_invalid(validity, idx as idx_t);
539        }
540    }
541
542    /// Marks every row in `range` as NULL.
543    ///
544    /// Equivalent to calling [`set_null`][Self::set_null] for each index, but
545    /// resolves the validity bitmap once.
546    ///
547    /// # Safety
548    ///
549    /// Every index in `range` must be within the vector's capacity.
550    pub unsafe fn set_null_range(&mut self, range: core::ops::Range<usize>) {
551        if range.is_empty() {
552            return;
553        }
554        // SAFETY: self.vector is valid per constructor's contract.
555        let validity = unsafe { self.writable_validity() };
556        for idx in range {
557            // SAFETY: idx is in bounds per caller's contract.
558            unsafe { duckdb_validity_set_row_invalid(validity, idx as idx_t) };
559        }
560    }
561
562    /// Resolves (once) and returns the writable validity bitmap pointer.
563    ///
564    /// # Pitfall L4: `ensure_validity_writable`
565    ///
566    /// `duckdb_vector_get_validity` returns an unusable pointer until
567    /// `duckdb_vector_ensure_validity_writable` has allocated the mask. This
568    /// does both, then caches the result — `EnsureWritable` is a no-op after the
569    /// first call and the pointer is stable for the vector's lifetime.
570    ///
571    /// # Safety
572    ///
573    /// `self.vector` must still be a valid, flat, writable vector.
574    unsafe fn writable_validity(&mut self) -> *mut u64 {
575        if self.validity.is_null() {
576            // SAFETY: self.vector is valid per constructor's contract.
577            unsafe { duckdb_vector_ensure_validity_writable(self.vector) };
578            // SAFETY: the mask was just allocated, so the pointer is usable.
579            self.validity = unsafe { duckdb_vector_get_validity(self.vector) };
580        }
581        self.validity
582    }
583
584    /// Marks row `idx` as valid (non-NULL) in the output vector.
585    ///
586    /// Use this to undo a previous [`set_null`][Self::set_null] call for a row,
587    /// or to explicitly mark a row as valid after writing its value.
588    ///
589    /// Like [`set_null`][Self::set_null], this calls `ensure_validity_writable`
590    /// before modifying the validity bitmap.
591    ///
592    /// # Safety
593    ///
594    /// - `idx` must be within the vector's capacity.
595    pub unsafe fn set_valid(&mut self, idx: usize) {
596        // SAFETY: self.vector is valid per constructor's contract.
597        let validity = unsafe { self.writable_validity() };
598        // SAFETY: validity is now initialized and idx is in bounds per caller's contract.
599        unsafe {
600            duckdb_validity_set_row_valid(validity, idx as idx_t);
601        }
602    }
603
604    /// Returns the underlying raw vector handle.
605    #[must_use]
606    #[inline]
607    pub const fn as_raw(&self) -> duckdb_vector {
608        self.vector
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    // Functional tests for VectorWriter require a live DuckDB instance and are
615    // located in tests/integration_test.rs. Unit tests here verify the struct
616    // layout and any pure-Rust logic.
617
618    #[test]
619    fn size_of_vector_writer() {
620        use super::VectorWriter;
621        use std::mem::size_of;
622        // vector + data + cached validity pointer
623        assert_eq!(size_of::<VectorWriter>(), 3 * size_of::<usize>());
624    }
625}