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}