quack_rs/value.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//! RAII wrapper around `DuckDB` values (`duckdb_value`).
7//!
8//! [`Value`] provides safe, typed access to `DuckDB` values returned from bind
9//! parameter extraction, configuration options, and other APIs. It automatically
10//! calls [`duckdb_destroy_value`] on drop, eliminating the manual cleanup that
11//! every extension author currently has to remember.
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use quack_rs::value::Value;
17//! use quack_rs::table::BindInfo;
18//! use libduckdb_sys::duckdb_bind_info;
19//!
20//! unsafe extern "C" fn my_bind(info: duckdb_bind_info) {
21//! let bind = unsafe { BindInfo::new(info) };
22//! // RAII: Value is destroyed automatically when it goes out of scope.
23//! let val = unsafe { Value::from_raw(bind.get_parameter(0)) };
24//! if let Ok(s) = val.as_str() {
25//! // use s...
26//! }
27//! }
28//! ```
29
30mod blob;
31
32use std::ffi::CStr;
33use std::os::raw::c_char;
34
35#[cfg(feature = "duckdb-1-5")]
36use libduckdb_sys::{
37 duckdb_create_time_ns, duckdb_get_time_ns, duckdb_time_ns, duckdb_value_to_string,
38};
39use libduckdb_sys::{
40 duckdb_destroy_value, duckdb_free, duckdb_get_bool, duckdb_get_double, duckdb_get_float,
41 duckdb_get_hugeint, duckdb_get_int16, duckdb_get_int32, duckdb_get_int64, duckdb_get_int8,
42 duckdb_get_uint16, duckdb_get_uint32, duckdb_get_uint64, duckdb_get_uint8, duckdb_get_varchar,
43 duckdb_value,
44};
45
46use crate::error::ExtensionError;
47
48/// An owned, RAII-managed `DuckDB` value.
49///
50/// When dropped, the underlying `duckdb_value` handle is destroyed via
51/// [`duckdb_destroy_value`]. This eliminates the manual `duckdb_destroy_value`
52/// calls that are easy to forget and lead to memory leaks.
53///
54/// # Creation
55///
56/// Obtain a `Value` from:
57/// - [`BindInfo::get_parameter_value`][crate::table::BindInfo::get_parameter_value]
58/// - [`BindInfo::get_named_parameter_value`][crate::table::BindInfo::get_named_parameter_value]
59/// - [`Value::from_raw`] (escape hatch for raw `duckdb_value` handles)
60///
61/// # Extraction
62///
63/// Use typed accessors to extract the underlying data:
64/// - [`as_str`][Value::as_str] — VARCHAR → `String`
65/// - [`as_blob`][Value::as_blob] — BLOB → `Vec<u8>`
66/// - [`as_i32`][Value::as_i32] — INTEGER → `i32`
67/// - [`as_i64`][Value::as_i64] — BIGINT → `i64`
68/// - [`as_f32`][Value::as_f32] — FLOAT → `f32`
69/// - [`as_f64`][Value::as_f64] — DOUBLE → `f64`
70/// - [`as_bool`][Value::as_bool] — BOOLEAN → `bool`
71pub struct Value {
72 raw: duckdb_value,
73}
74
75impl Value {
76 /// Wraps a raw `duckdb_value` handle.
77 ///
78 /// The returned `Value` takes ownership and will call `duckdb_destroy_value`
79 /// on drop.
80 ///
81 /// # Safety
82 ///
83 /// `raw` must be a valid `duckdb_value` obtained from a `DuckDB` API call
84 /// (e.g., `duckdb_bind_get_parameter`). The caller must not destroy the
85 /// value after passing it to this function.
86 #[inline]
87 #[must_use]
88 pub const unsafe fn from_raw(raw: duckdb_value) -> Self {
89 Self { raw }
90 }
91
92 /// Extracts the value as a `String` (VARCHAR).
93 ///
94 /// Internally calls `duckdb_get_varchar` and frees the returned C string
95 /// with `duckdb_free`. Returns an error if the string is not valid UTF-8
96 /// or if the value handle is null.
97 ///
98 /// # Embedded NUL bytes
99 ///
100 /// `duckdb_get_varchar` returns a NUL-terminated `char *`, so a value whose
101 /// text contains an interior NUL is **truncated at the first one**. `DuckDB`
102 /// itself stores the full bytes; only this read path is limited. If the text
103 /// may contain NULs, keep it in a `BLOB` and use
104 /// [`as_blob`][Self::as_blob].
105 ///
106 /// # Errors
107 ///
108 /// Returns `ExtensionError` if the value is null or contains invalid UTF-8.
109 pub fn as_str(&self) -> Result<String, ExtensionError> {
110 if self.raw.is_null() {
111 return Err(ExtensionError::new("Value is null"));
112 }
113 // SAFETY: self.raw is a valid duckdb_value per constructor contract.
114 let c_str: *mut c_char = unsafe { duckdb_get_varchar(self.raw) };
115 if c_str.is_null() {
116 return Err(ExtensionError::new("duckdb_get_varchar returned null"));
117 }
118 // SAFETY: c_str is a valid null-terminated C string allocated by DuckDB.
119 let result = unsafe { CStr::from_ptr(c_str) }
120 .to_str()
121 .map(str::to_owned)
122 .map_err(|_| ExtensionError::new("Value contains invalid UTF-8"));
123 // SAFETY: c_str was allocated by DuckDB and must be freed with duckdb_free.
124 unsafe { duckdb_free(c_str.cast()) };
125 result
126 }
127
128 /// Extracts the value as an `i32` (INTEGER).
129 ///
130 /// `DuckDB` will attempt to cast the value to INTEGER. If the value is not
131 /// numeric, this returns 0.
132 #[inline]
133 #[must_use]
134 pub fn as_i32(&self) -> i32 {
135 // SAFETY: self.raw is valid per constructor contract.
136 unsafe { duckdb_get_int32(self.raw) }
137 }
138
139 /// Extracts the value as an `i64` (BIGINT).
140 ///
141 /// `DuckDB` will attempt to cast the value to BIGINT. If the value is not
142 /// numeric, this returns 0.
143 #[inline]
144 #[must_use]
145 pub fn as_i64(&self) -> i64 {
146 // SAFETY: self.raw is valid per constructor contract.
147 unsafe { duckdb_get_int64(self.raw) }
148 }
149
150 /// Extracts the value as an `f32` (FLOAT).
151 ///
152 /// `DuckDB` will attempt to cast the value to FLOAT. If the value is not
153 /// numeric, this returns 0.0.
154 #[inline]
155 #[must_use]
156 pub fn as_f32(&self) -> f32 {
157 // SAFETY: self.raw is valid per constructor contract.
158 unsafe { duckdb_get_float(self.raw) }
159 }
160
161 /// Extracts the value as an `f64` (DOUBLE).
162 ///
163 /// `DuckDB` will attempt to cast the value to DOUBLE. If the value is not
164 /// numeric, this returns 0.0.
165 #[inline]
166 #[must_use]
167 pub fn as_f64(&self) -> f64 {
168 // SAFETY: self.raw is valid per constructor contract.
169 unsafe { duckdb_get_double(self.raw) }
170 }
171
172 /// Extracts the value as a `bool` (BOOLEAN).
173 ///
174 /// `DuckDB` will attempt to cast the value to BOOLEAN. If the value is not
175 /// convertible, this returns `false`.
176 #[inline]
177 #[must_use]
178 pub fn as_bool(&self) -> bool {
179 // SAFETY: self.raw is valid per constructor contract.
180 unsafe { duckdb_get_bool(self.raw) }
181 }
182
183 /// Extracts the value as an `i8` (TINYINT).
184 ///
185 /// `DuckDB` will attempt to cast the value to TINYINT. If the value is not
186 /// numeric, this returns 0.
187 #[inline]
188 #[must_use]
189 pub fn as_i8(&self) -> i8 {
190 // SAFETY: self.raw is valid per constructor contract.
191 unsafe { duckdb_get_int8(self.raw) }
192 }
193
194 /// Extracts the value as an `i16` (SMALLINT).
195 ///
196 /// `DuckDB` will attempt to cast the value to SMALLINT. If the value is not
197 /// numeric, this returns 0.
198 #[inline]
199 #[must_use]
200 pub fn as_i16(&self) -> i16 {
201 // SAFETY: self.raw is valid per constructor contract.
202 unsafe { duckdb_get_int16(self.raw) }
203 }
204
205 /// Extracts the value as a `u8` (UTINYINT).
206 ///
207 /// `DuckDB` will attempt to cast the value to UTINYINT. If the value is not
208 /// numeric, this returns 0.
209 #[inline]
210 #[must_use]
211 pub fn as_u8(&self) -> u8 {
212 // SAFETY: self.raw is valid per constructor contract.
213 unsafe { duckdb_get_uint8(self.raw) }
214 }
215
216 /// Extracts the value as a `u16` (USMALLINT).
217 ///
218 /// `DuckDB` will attempt to cast the value to USMALLINT. If the value is not
219 /// numeric, this returns 0.
220 #[inline]
221 #[must_use]
222 pub fn as_u16(&self) -> u16 {
223 // SAFETY: self.raw is valid per constructor contract.
224 unsafe { duckdb_get_uint16(self.raw) }
225 }
226
227 /// Extracts the value as a `u32` (UINTEGER).
228 ///
229 /// `DuckDB` will attempt to cast the value to UINTEGER. If the value is not
230 /// numeric, this returns 0.
231 #[inline]
232 #[must_use]
233 pub fn as_u32(&self) -> u32 {
234 // SAFETY: self.raw is valid per constructor contract.
235 unsafe { duckdb_get_uint32(self.raw) }
236 }
237
238 /// Extracts the value as a `u64` (UBIGINT).
239 ///
240 /// `DuckDB` will attempt to cast the value to UBIGINT. If the value is not
241 /// numeric, this returns 0.
242 #[inline]
243 #[must_use]
244 pub fn as_u64(&self) -> u64 {
245 // SAFETY: self.raw is valid per constructor contract.
246 unsafe { duckdb_get_uint64(self.raw) }
247 }
248
249 /// Extracts the value as an `i128` (HUGEINT).
250 ///
251 /// `DuckDB` returns HUGEINT as `{ lower: u64, upper: i64 }`. This method
252 /// reconstructs the full `i128` value.
253 #[inline]
254 #[must_use]
255 pub fn as_i128(&self) -> i128 {
256 // SAFETY: self.raw is valid per constructor contract.
257 let h = unsafe { duckdb_get_hugeint(self.raw) };
258 #[allow(clippy::cast_lossless)]
259 let result = (h.upper as i128) << 64 | (h.lower as i128);
260 result
261 }
262
263 /// Extracts the value as a `String`, returning `default` on failure.
264 ///
265 /// Convenience for `val.as_str().unwrap_or_else(|_| default.to_owned())`.
266 #[inline]
267 #[must_use]
268 pub fn as_str_or(&self, default: &str) -> String {
269 self.as_str().unwrap_or_else(|_| default.to_owned())
270 }
271
272 /// Extracts the value as a `String`, returning an empty string on failure.
273 ///
274 /// Convenience for `val.as_str().unwrap_or_default()`.
275 #[inline]
276 #[must_use]
277 pub fn as_str_or_default(&self) -> String {
278 self.as_str().unwrap_or_default()
279 }
280
281 /// Extracts the value as an `i32`, returning `default` if the handle is null.
282 #[inline]
283 #[must_use]
284 pub fn as_i32_or(&self, default: i32) -> i32 {
285 if self.is_null() {
286 default
287 } else {
288 self.as_i32()
289 }
290 }
291
292 /// Extracts the value as an `i64`, returning `default` if the handle is null.
293 #[inline]
294 #[must_use]
295 pub fn as_i64_or(&self, default: i64) -> i64 {
296 if self.is_null() {
297 default
298 } else {
299 self.as_i64()
300 }
301 }
302
303 /// Extracts the value as an `f32`, returning `default` if the handle is null.
304 #[inline]
305 #[must_use]
306 pub fn as_f32_or(&self, default: f32) -> f32 {
307 if self.is_null() {
308 default
309 } else {
310 self.as_f32()
311 }
312 }
313
314 /// Extracts the value as an `f64`, returning `default` if the handle is null.
315 #[inline]
316 #[must_use]
317 pub fn as_f64_or(&self, default: f64) -> f64 {
318 if self.is_null() {
319 default
320 } else {
321 self.as_f64()
322 }
323 }
324
325 /// Extracts the value as a `bool`, returning `default` if the handle is null.
326 #[inline]
327 #[must_use]
328 pub fn as_bool_or(&self, default: bool) -> bool {
329 if self.is_null() {
330 default
331 } else {
332 self.as_bool()
333 }
334 }
335
336 /// Extracts the value as an `i8`, returning `default` if the handle is null.
337 #[inline]
338 #[must_use]
339 pub fn as_i8_or(&self, default: i8) -> i8 {
340 if self.is_null() {
341 default
342 } else {
343 self.as_i8()
344 }
345 }
346
347 /// Extracts the value as an `i16`, returning `default` if the handle is null.
348 #[inline]
349 #[must_use]
350 pub fn as_i16_or(&self, default: i16) -> i16 {
351 if self.is_null() {
352 default
353 } else {
354 self.as_i16()
355 }
356 }
357
358 /// Extracts the value as a `u8`, returning `default` if the handle is null.
359 #[inline]
360 #[must_use]
361 pub fn as_u8_or(&self, default: u8) -> u8 {
362 if self.is_null() {
363 default
364 } else {
365 self.as_u8()
366 }
367 }
368
369 /// Extracts the value as a `u16`, returning `default` if the handle is null.
370 #[inline]
371 #[must_use]
372 pub fn as_u16_or(&self, default: u16) -> u16 {
373 if self.is_null() {
374 default
375 } else {
376 self.as_u16()
377 }
378 }
379
380 /// Extracts the value as a `u32`, returning `default` if the handle is null.
381 #[inline]
382 #[must_use]
383 pub fn as_u32_or(&self, default: u32) -> u32 {
384 if self.is_null() {
385 default
386 } else {
387 self.as_u32()
388 }
389 }
390
391 /// Extracts the value as a `u64`, returning `default` if the handle is null.
392 #[inline]
393 #[must_use]
394 pub fn as_u64_or(&self, default: u64) -> u64 {
395 if self.is_null() {
396 default
397 } else {
398 self.as_u64()
399 }
400 }
401
402 /// Extracts the value as an `i128`, returning `default` if the handle is null.
403 #[inline]
404 #[must_use]
405 pub fn as_i128_or(&self, default: i128) -> i128 {
406 if self.is_null() {
407 default
408 } else {
409 self.as_i128()
410 }
411 }
412
413 /// Creates a `TIME_NS` value (time of day with nanosecond precision) from a
414 /// raw nanosecond count (`DuckDB` 1.5.0+).
415 ///
416 /// Pairs with [`as_time_ns`][Value::as_time_ns] and the
417 /// [`TypeId::TimeNs`][crate::types::TypeId::TimeNs] column type.
418 #[cfg(feature = "duckdb-1-5")]
419 #[inline]
420 #[must_use]
421 pub fn time_ns(nanos: i64) -> Self {
422 // SAFETY: duckdb_create_time_ns accepts any nanosecond count and returns
423 // an owned duckdb_value.
424 let raw = unsafe { duckdb_create_time_ns(duckdb_time_ns { nanos }) };
425 Self { raw }
426 }
427
428 /// Extracts the value as a `TIME_NS` nanosecond count (`DuckDB` 1.5.0+).
429 ///
430 /// Returns 0 if the value is not a `TIME_NS`.
431 #[cfg(feature = "duckdb-1-5")]
432 #[inline]
433 #[must_use]
434 pub fn as_time_ns(&self) -> i64 {
435 // SAFETY: self.raw is valid per constructor contract.
436 unsafe { duckdb_get_time_ns(self.raw) }.nanos
437 }
438
439 /// Returns the **SQL literal** representation of this value, as `DuckDB`
440 /// would render it (`DuckDB` 1.5.0+).
441 ///
442 /// Note "SQL literal", not "text". A VARCHAR comes back quoted and typed
443 /// values carry an explicit cast:
444 ///
445 /// | Value | `display_string()` |
446 /// |-------|--------------------|
447 /// | `Value::varchar("hello")` | `'hello'` |
448 /// | `Value::bigint(-42)` | `-42` |
449 /// | `Value::date(0)` | `'1970-01-01'::DATE` |
450 /// | `Value::timestamp(0)` | `'1970-01-01 00:00:00'::TIMESTAMP` |
451 ///
452 /// Use [`as_str`][Self::as_str] for a VARCHAR's contents. This is for
453 /// diagnostics and error messages, where it works for any value type.
454 ///
455 /// Returns `None` if the handle is null or the rendered text is not valid
456 /// UTF-8.
457 #[cfg(feature = "duckdb-1-5")]
458 #[must_use]
459 pub fn display_string(&self) -> Option<String> {
460 if self.raw.is_null() {
461 return None;
462 }
463 // SAFETY: self.raw is a valid duckdb_value per constructor contract.
464 let c_str: *mut c_char = unsafe { duckdb_value_to_string(self.raw) };
465 if c_str.is_null() {
466 return None;
467 }
468 // SAFETY: c_str is a valid null-terminated string allocated by DuckDB.
469 let result = unsafe { CStr::from_ptr(c_str) }
470 .to_str()
471 .ok()
472 .map(str::to_owned);
473 // SAFETY: c_str was allocated by DuckDB and must be freed with duckdb_free.
474 unsafe { duckdb_free(c_str.cast()) };
475 result
476 }
477
478 // ── Temporal, DECIMAL and UUID extraction ────────────────────────────
479 //
480 // A table function declared with `.named_param("since", TypeId::Timestamp)`
481 // hands the bind callback a `duckdb_value`, and until now the only way to
482 // read it was `as_str()` plus reparsing DuckDB's rendering. These are the
483 // `duckdb_get_*` counterparts, all in the stable prefix of the C API.
484
485 /// Extracts a `DATE` as days since 1970-01-01.
486 ///
487 /// Returns 0 if the value is not a `DATE`. Decode it with
488 /// [`datetime::date_from_days`][crate::datetime::date_from_days].
489 #[inline]
490 #[must_use]
491 pub fn as_date(&self) -> i32 {
492 // SAFETY: self.raw is valid per constructor contract.
493 unsafe { libduckdb_sys::duckdb_get_date(self.raw) }.days
494 }
495
496 /// Extracts a `TIME` as microseconds since midnight.
497 ///
498 /// Returns 0 if the value is not a `TIME`.
499 #[inline]
500 #[must_use]
501 pub fn as_time(&self) -> i64 {
502 // SAFETY: self.raw is valid per constructor contract.
503 unsafe { libduckdb_sys::duckdb_get_time(self.raw) }.micros
504 }
505
506 /// Extracts a `TIMETZ` as `DuckDB`'s packed 64-bit representation.
507 ///
508 /// Decode it with
509 /// [`datetime::time_tz_from_bits`][crate::datetime::time_tz_from_bits].
510 #[inline]
511 #[must_use]
512 pub fn as_time_tz(&self) -> u64 {
513 // SAFETY: self.raw is valid per constructor contract.
514 unsafe { libduckdb_sys::duckdb_get_time_tz(self.raw) }.bits
515 }
516
517 /// Extracts a `TIMESTAMP` as microseconds since the epoch.
518 ///
519 /// Returns 0 if the value is not a `TIMESTAMP`.
520 #[inline]
521 #[must_use]
522 pub fn as_timestamp(&self) -> i64 {
523 // SAFETY: self.raw is valid per constructor contract.
524 unsafe { libduckdb_sys::duckdb_get_timestamp(self.raw) }.micros
525 }
526
527 /// Extracts a `TIMESTAMPTZ` as microseconds since the epoch, in UTC.
528 #[inline]
529 #[must_use]
530 pub fn as_timestamp_tz(&self) -> i64 {
531 // SAFETY: self.raw is valid per constructor contract.
532 unsafe { libduckdb_sys::duckdb_get_timestamp_tz(self.raw) }.micros
533 }
534
535 /// Extracts a `TIMESTAMP_S` as seconds since the epoch.
536 #[inline]
537 #[must_use]
538 pub fn as_timestamp_s(&self) -> i64 {
539 // SAFETY: self.raw is valid per constructor contract.
540 unsafe { libduckdb_sys::duckdb_get_timestamp_s(self.raw) }.seconds
541 }
542
543 /// Extracts a `TIMESTAMP_MS` as milliseconds since the epoch.
544 #[inline]
545 #[must_use]
546 pub fn as_timestamp_ms(&self) -> i64 {
547 // SAFETY: self.raw is valid per constructor contract.
548 unsafe { libduckdb_sys::duckdb_get_timestamp_ms(self.raw) }.millis
549 }
550
551 /// Extracts a `TIMESTAMP_NS` as nanoseconds since the epoch.
552 #[inline]
553 #[must_use]
554 pub fn as_timestamp_ns(&self) -> i64 {
555 // SAFETY: self.raw is valid per constructor contract.
556 unsafe { libduckdb_sys::duckdb_get_timestamp_ns(self.raw) }.nanos
557 }
558
559 /// Extracts an `INTERVAL`.
560 #[inline]
561 #[must_use]
562 pub fn as_interval(&self) -> crate::interval::DuckInterval {
563 // SAFETY: self.raw is valid per constructor contract.
564 let raw = unsafe { libduckdb_sys::duckdb_get_interval(self.raw) };
565 crate::interval::DuckInterval {
566 months: raw.months,
567 days: raw.days,
568 micros: raw.micros,
569 }
570 }
571
572 /// Extracts a `UUID` as its **textual** 128 bits, matching
573 /// [`VectorReader::read_uuid`][crate::vector::VectorReader::read_uuid]
574 /// and [`uuid`][Self::uuid].
575 ///
576 /// `DuckDB` undoes its internal top-bit flip itself here, so this is the
577 /// value the UUID renders as — not the raw `HUGEINT` a `UUID` vector holds.
578 #[inline]
579 #[must_use]
580 pub fn as_uuid(&self) -> u128 {
581 // SAFETY: self.raw is valid per constructor contract.
582 let raw = unsafe { libduckdb_sys::duckdb_get_uuid(self.raw) };
583 (u128::from(raw.upper) << 64) | u128::from(raw.lower)
584 }
585
586 /// Extracts a `DECIMAL` as its width, scale and unscaled value.
587 ///
588 /// The represented number is `value / 10^scale`.
589 #[inline]
590 #[must_use]
591 pub fn as_decimal(&self) -> crate::datetime::Decimal {
592 // SAFETY: self.raw is valid per constructor contract.
593 let raw = unsafe { libduckdb_sys::duckdb_get_decimal(self.raw) };
594 crate::datetime::Decimal {
595 width: raw.width,
596 scale: raw.scale,
597 value: (i128::from(raw.value.upper) << 64) | i128::from(raw.value.lower),
598 }
599 }
600
601 /// Extracts a `UHUGEINT` as a `u128`.
602 #[inline]
603 #[must_use]
604 pub fn as_u128(&self) -> u128 {
605 // SAFETY: self.raw is valid per constructor contract.
606 let raw = unsafe { libduckdb_sys::duckdb_get_uhugeint(self.raw) };
607 (u128::from(raw.upper) << 64) | u128::from(raw.lower)
608 }
609
610 // ── LIST / STRUCT / MAP extraction ───────────────────────────────────
611
612 /// Number of elements in a `LIST` value.
613 ///
614 /// Returns 0 for non-`LIST` values.
615 #[inline]
616 #[must_use]
617 pub fn list_len(&self) -> usize {
618 // SAFETY: self.raw is valid per constructor contract.
619 usize::try_from(unsafe { libduckdb_sys::duckdb_get_list_size(self.raw) }).unwrap_or(0)
620 }
621
622 /// Element `index` of a `LIST` value, or `None` if out of range.
623 ///
624 /// The returned [`Value`] owns its handle.
625 #[must_use]
626 pub fn list_child(&self, index: usize) -> Option<Self> {
627 if index >= self.list_len() {
628 return None;
629 }
630 // SAFETY: `index` was bounds-checked against `list_len`.
631 let raw = unsafe {
632 libduckdb_sys::duckdb_get_list_child(self.raw, index as libduckdb_sys::idx_t)
633 };
634 (!raw.is_null()).then(|| Self { raw })
635 }
636
637 /// Collects a `LIST` value into a `Vec` of owned [`Value`]s.
638 ///
639 /// # Example
640 ///
641 /// ```rust,no_run
642 /// # use quack_rs::value::Value;
643 /// # fn demo(paths: &Value) {
644 /// let files: Vec<String> = paths
645 /// .list_items()
646 /// .iter()
647 /// .filter_map(|v| v.as_str().ok())
648 /// .collect();
649 /// # }
650 /// ```
651 #[must_use]
652 pub fn list_items(&self) -> Vec<Self> {
653 (0..self.list_len())
654 .filter_map(|i| self.list_child(i))
655 .collect()
656 }
657
658 /// Field `index` of a `STRUCT` value, or `None` if the handle is null or the
659 /// index is out of range.
660 ///
661 /// Field names come from the value's `LogicalType`, not from the value
662 /// itself; `DuckDB`'s C API exposes children by position.
663 #[must_use]
664 pub fn struct_child(&self, index: usize) -> Option<Self> {
665 if self.raw.is_null() {
666 return None;
667 }
668 // SAFETY: self.raw is valid; DuckDB returns null for an out-of-range index.
669 let raw = unsafe {
670 libduckdb_sys::duckdb_get_struct_child(self.raw, index as libduckdb_sys::idx_t)
671 };
672 (!raw.is_null()).then(|| Self { raw })
673 }
674
675 /// Number of key/value pairs in a `MAP` value.
676 #[inline]
677 #[must_use]
678 pub fn map_len(&self) -> usize {
679 // SAFETY: self.raw is valid per constructor contract.
680 usize::try_from(unsafe { libduckdb_sys::duckdb_get_map_size(self.raw) }).unwrap_or(0)
681 }
682
683 /// Key at `index` of a `MAP` value, or `None` if out of range.
684 #[must_use]
685 pub fn map_key(&self, index: usize) -> Option<Self> {
686 if index >= self.map_len() {
687 return None;
688 }
689 // SAFETY: `index` was bounds-checked against `map_len`.
690 let raw =
691 unsafe { libduckdb_sys::duckdb_get_map_key(self.raw, index as libduckdb_sys::idx_t) };
692 (!raw.is_null()).then(|| Self { raw })
693 }
694
695 /// Value at `index` of a `MAP` value, or `None` if out of range.
696 #[must_use]
697 pub fn map_value(&self, index: usize) -> Option<Self> {
698 if index >= self.map_len() {
699 return None;
700 }
701 // SAFETY: `index` was bounds-checked against `map_len`.
702 let raw =
703 unsafe { libduckdb_sys::duckdb_get_map_value(self.raw, index as libduckdb_sys::idx_t) };
704 (!raw.is_null()).then(|| Self { raw })
705 }
706
707 // ── Construction ─────────────────────────────────────────────────────
708 //
709 // Needed for `ConfigOptionBuilder::default_value` and anywhere else DuckDB
710 // wants a `duckdb_value` rather than a Rust scalar.
711
712 /// Creates a `BOOLEAN` value.
713 #[inline]
714 #[must_use]
715 pub fn boolean(value: bool) -> Self {
716 // SAFETY: duckdb_create_bool accepts any bool and returns an owned value.
717 Self {
718 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
719 // the returned handle is owned by this `Value`.
720 raw: unsafe { libduckdb_sys::duckdb_create_bool(value) },
721 }
722 }
723
724 /// Creates a `BIGINT` value.
725 #[inline]
726 #[must_use]
727 pub fn bigint(value: i64) -> Self {
728 // SAFETY: duckdb_create_int64 accepts any i64 and returns an owned value.
729 Self {
730 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
731 // the returned handle is owned by this `Value`.
732 raw: unsafe { libduckdb_sys::duckdb_create_int64(value) },
733 }
734 }
735
736 /// Creates a `DOUBLE` value.
737 #[inline]
738 #[must_use]
739 pub fn double(value: f64) -> Self {
740 // SAFETY: duckdb_create_double accepts any f64 and returns an owned value.
741 Self {
742 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
743 // the returned handle is owned by this `Value`.
744 raw: unsafe { libduckdb_sys::duckdb_create_double(value) },
745 }
746 }
747
748 /// Creates a `DATE` value from days since 1970-01-01.
749 #[inline]
750 #[must_use]
751 pub fn date(days: i32) -> Self {
752 // SAFETY: duckdb_create_date accepts any day count.
753 Self {
754 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
755 // the returned handle is owned by this `Value`.
756 raw: unsafe { libduckdb_sys::duckdb_create_date(libduckdb_sys::duckdb_date { days }) },
757 }
758 }
759
760 /// Creates a `TIMESTAMP` value from microseconds since the epoch.
761 #[inline]
762 #[must_use]
763 pub fn timestamp(micros: i64) -> Self {
764 // SAFETY: duckdb_create_timestamp accepts any microsecond count.
765 Self {
766 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
767 // the returned handle is owned by this `Value`.
768 raw: unsafe {
769 libduckdb_sys::duckdb_create_timestamp(libduckdb_sys::duckdb_timestamp { micros })
770 },
771 }
772 }
773
774 /// Creates a `VARCHAR` value.
775 ///
776 /// The length is passed explicitly, so no `CString` conversion can fail and
777 /// `DuckDB` stores every byte — but note that [`as_str`][Self::as_str] reads
778 /// back through a NUL-terminated C string and will truncate at an interior
779 /// NUL.
780 #[must_use]
781 pub fn varchar(value: &str) -> Self {
782 // SAFETY: `value` is valid for the duration of the call; DuckDB copies it.
783 let raw = unsafe {
784 libduckdb_sys::duckdb_create_varchar_length(
785 value.as_ptr().cast::<c_char>(),
786 libduckdb_sys::idx_t::try_from(value.len()).unwrap_or(libduckdb_sys::idx_t::MAX),
787 )
788 };
789 Self { raw }
790 }
791
792 /// Creates a `UUID` value from its **textual** 128 bits, matching
793 /// [`VectorWriter::write_uuid`][crate::vector::VectorWriter::write_uuid]
794 /// and [`as_uuid`][Self::as_uuid].
795 ///
796 /// `DuckDB` applies its internal top-bit flip itself here, so these are the
797 /// bits the value renders as — not the raw `HUGEINT` a `UUID` vector holds.
798 #[inline]
799 #[must_use]
800 pub fn uuid(bits: u128) -> Self {
801 let raw = libduckdb_sys::duckdb_uhugeint {
802 #[allow(clippy::cast_possible_truncation)]
803 lower: bits as u64,
804 #[allow(clippy::cast_possible_truncation)]
805 upper: (bits >> 64) as u64,
806 };
807 // SAFETY: duckdb_create_uuid accepts any 128-bit pattern.
808 Self {
809 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
810 // the returned handle is owned by this `Value`.
811 raw: unsafe { libduckdb_sys::duckdb_create_uuid(raw) },
812 }
813 }
814
815 /// Creates a SQL `NULL` value.
816 #[inline]
817 #[must_use]
818 pub fn null_value() -> Self {
819 // SAFETY: duckdb_create_null_value takes no arguments and returns an
820 // owned SQLNULL value.
821 Self {
822 // SAFETY: the argument is a plain value DuckDB accepts unconditionally, and
823 // the returned handle is owned by this `Value`.
824 raw: unsafe { libduckdb_sys::duckdb_create_null_value() },
825 }
826 }
827
828 /// Returns the [`TypeId`][crate::types::TypeId] this value actually holds.
829 ///
830 /// Every `as_*` accessor *reinterprets* the value as a chosen physical
831 /// type without checking: reading a `VARCHAR` with
832 /// [`as_i64`][Self::as_i64] returns garbage rather than an error. This is
833 /// the check that makes those accessors safe to use on a value whose type
834 /// you did not choose — a named parameter, a bound constant, a config
835 /// option.
836 ///
837 /// Returns `None` for a null handle, and for a type id introduced by a
838 /// newer `DuckDB` than this build of quack-rs knows.
839 ///
840 /// # Example
841 ///
842 /// ```rust,no_run
843 /// use quack_rs::types::TypeId;
844 /// use quack_rs::value::Value;
845 ///
846 /// # fn demo(value: &Value) -> Option<i64> {
847 /// match value.type_id()? {
848 /// TypeId::BigInt => Some(value.as_i64()),
849 /// TypeId::Integer => Some(i64::from(value.as_i32())),
850 /// _ => None,
851 /// }
852 /// # }
853 /// ```
854 #[must_use]
855 pub fn type_id(&self) -> Option<crate::types::TypeId> {
856 if self.raw.is_null() {
857 return None;
858 }
859 // SAFETY: `self.raw` is a valid duckdb_value per the constructor
860 // contract. The returned logical type is owned by the value — duckdb.h
861 // states "The type itself must not be destroyed" — so it is read
862 // directly rather than wrapped in `LogicalType`, which frees on drop.
863 let logical = unsafe { libduckdb_sys::duckdb_get_value_type(self.raw) };
864 if logical.is_null() {
865 return None;
866 }
867 // SAFETY: `logical` is non-null and valid for as long as `self` is.
868 let raw_id = unsafe { libduckdb_sys::duckdb_get_type_id(logical) };
869 crate::types::TypeId::try_from_duckdb_type(raw_id)
870 }
871
872 /// Returns `true` if the underlying handle is null.
873 #[inline]
874 #[must_use]
875 pub const fn is_null(&self) -> bool {
876 self.raw.is_null()
877 }
878
879 /// Returns the raw `duckdb_value` handle without consuming the `Value`.
880 ///
881 /// The `Value` still owns the handle and will destroy it on drop.
882 #[inline]
883 #[must_use]
884 pub const fn as_raw(&self) -> duckdb_value {
885 self.raw
886 }
887
888 /// Consumes the `Value` and returns the raw `duckdb_value` handle.
889 ///
890 /// The caller takes ownership and is responsible for calling
891 /// `duckdb_destroy_value` when done.
892 #[inline]
893 #[must_use]
894 pub const fn into_raw(self) -> duckdb_value {
895 let raw = self.raw;
896 std::mem::forget(self);
897 raw
898 }
899}
900
901impl Drop for Value {
902 fn drop(&mut self) {
903 if !self.raw.is_null() {
904 // SAFETY: self.raw is a valid duckdb_value that we own.
905 unsafe { duckdb_destroy_value(&raw mut self.raw) };
906 }
907 }
908}
909
910impl core::fmt::Debug for Value {
911 /// Prints the value's type and, where `DuckDB` can render it, its contents.
912 ///
913 /// Like [`LogicalType`][crate::types::LogicalType]'s impl this calls into
914 /// `DuckDB`, so it avoids every path that could panic while formatting.
915 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
916 if self.raw.is_null() {
917 return f.write_str("Value(<null handle>)");
918 }
919 let mut out = f.debug_struct("Value");
920 match self.type_id() {
921 Some(type_id) => out.field("type", &type_id),
922 None => out.field("type", &"<unknown>"),
923 };
924 #[cfg(feature = "duckdb-1-5")]
925 if let Some(rendered) = self.display_string() {
926 out.field("value", &rendered);
927 }
928 out.finish()
929 }
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935
936 #[test]
937 fn null_value_is_null() {
938 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
939 assert!(val.is_null());
940 }
941
942 #[test]
943 fn null_value_as_str_returns_error() {
944 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
945 assert!(val.as_str().is_err());
946 }
947
948 #[test]
949 fn into_raw_prevents_double_free() {
950 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
951 let raw = val.into_raw();
952 assert!(raw.is_null());
953 // No double-free: Value was forgotten via into_raw.
954 }
955
956 #[test]
957 fn size_of_value() {
958 assert_eq!(std::mem::size_of::<Value>(), std::mem::size_of::<usize>());
959 }
960
961 #[test]
962 fn as_str_or_returns_default_for_null() {
963 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
964 assert_eq!(val.as_str_or("fallback"), "fallback");
965 }
966
967 #[test]
968 fn as_str_or_default_returns_empty_for_null() {
969 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
970 assert_eq!(val.as_str_or_default(), "");
971 }
972
973 #[test]
974 fn as_i64_or_returns_default_for_null() {
975 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
976 assert_eq!(val.as_i64_or(99), 99);
977 }
978
979 #[test]
980 fn as_i32_or_returns_default_for_null() {
981 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
982 assert_eq!(val.as_i32_or(42), 42);
983 }
984
985 #[test]
986 fn as_bool_or_returns_default_for_null() {
987 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
988 assert!(val.as_bool_or(true));
989 assert!(!val.as_bool_or(false));
990 }
991
992 #[test]
993 fn as_f64_or_returns_default_for_null() {
994 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
995 assert!((val.as_f64_or(2.72) - 2.72).abs() < f64::EPSILON);
996 }
997
998 #[test]
999 fn as_f32_or_returns_default_for_null() {
1000 let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
1001 assert!((val.as_f32_or(2.5) - 2.5).abs() < f32::EPSILON);
1002 }
1003}
1004
1005/// `Value` accessors exercised against a live `DuckDB`.
1006///
1007/// These go through real `duckdb_value` handles produced by SQL, which is the
1008/// only way to be sure each `duckdb_get_*` is paired with the right SQL type.
1009#[cfg(all(test, feature = "_duckdb-testing"))]
1010mod live_tests {
1011 use super::Value;
1012 use crate::datetime;
1013 use crate::testing::InMemoryDb;
1014
1015 /// Evaluates `expr` and returns the result as an owned `Value`.
1016 ///
1017 /// Uses `duckdb_create_*` round-tripping through SQL is not possible from
1018 /// the C API, so this builds the value with the constructors under test and
1019 /// checks it against `DuckDB`'s own rendering.
1020 #[cfg(feature = "duckdb-1-5")]
1021 fn rendered(value: &Value) -> String {
1022 value
1023 .display_string()
1024 .expect("DuckDB should render every value")
1025 }
1026
1027 /// `display_string` wraps `duckdb_value_to_string`, which lives past the
1028 /// stable prefix, so this assertion set needs `duckdb-1-5`.
1029 #[cfg(feature = "duckdb-1-5")]
1030 #[test]
1031 fn scalar_constructors_render_as_duckdb_would() {
1032 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1033 assert_eq!(rendered(&Value::boolean(true)), "true");
1034 assert_eq!(rendered(&Value::bigint(-42)), "-42");
1035 // `duckdb_value_to_string` renders a SQL *literal*, so a VARCHAR is
1036 // quoted. This is easy to trip over when using it for diagnostics.
1037 assert_eq!(rendered(&Value::varchar("hello")), "'hello'");
1038 // Typed values carry an explicit cast, which is what makes this a SQL
1039 // *literal* rather than a display string.
1040 assert_eq!(rendered(&Value::date(0)), "'1970-01-01'::DATE");
1041 assert_eq!(
1042 rendered(&Value::timestamp(0)),
1043 "'1970-01-01 00:00:00'::TIMESTAMP"
1044 );
1045 assert!(Value::null_value().display_string().is_some());
1046 }
1047
1048 #[test]
1049 fn temporal_constructors_round_trip_through_accessors() {
1050 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1051
1052 // 2026-08-18 is 20685 days after the epoch; check against the calendar
1053 // conversion rather than restating the number.
1054 // SAFETY: InMemoryDb::open() initialised the dispatch table.
1055 let days = unsafe {
1056 datetime::date_to_days(datetime::Date {
1057 year: 2026,
1058 month: 8,
1059 day: 18,
1060 })
1061 };
1062 assert_eq!(Value::date(days).as_date(), days);
1063
1064 let micros = 1_700_000_000_000_000_i64;
1065 assert_eq!(Value::timestamp(micros).as_timestamp(), micros);
1066 assert_eq!(Value::bigint(i64::MIN).as_i64(), i64::MIN);
1067 assert_eq!(Value::bigint(i64::MAX).as_i64(), i64::MAX);
1068 }
1069
1070 #[test]
1071 fn as_str_truncates_at_an_embedded_nul() {
1072 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1073 // `duckdb_create_varchar_length` stores all three bytes, but
1074 // `duckdb_get_varchar` hands back a NUL-terminated C string, so the read
1075 // path truncates. Pinned so the documented caveat stays accurate.
1076 let value = Value::varchar("a\0b");
1077 assert_eq!(value.as_str().expect("utf8"), "a");
1078 }
1079
1080 #[test]
1081 fn uuid_round_trips_including_the_high_bit() {
1082 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1083 // `duckdb_get_uuid` returns an *unsigned* hugeint. Assembling its halves
1084 // directly as i128 overflows once the upper half's high bit is set —
1085 // which is true for half of all UUIDs, and panics in a debug build.
1086 for bits in [0_u128, 1, u128::MAX, 1 << 127, (1 << 127) - 1] {
1087 assert_eq!(
1088 Value::uuid(bits).as_uuid(),
1089 bits,
1090 "round trip for {bits:#034x}"
1091 );
1092 }
1093 }
1094
1095 #[test]
1096 fn list_and_map_accessors_bounds_check() {
1097 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1098 // A scalar is not a list; the accessors must report that rather than
1099 // reading out of range.
1100 let scalar = Value::bigint(1);
1101 assert_eq!(scalar.list_len(), 0);
1102 assert!(scalar.list_child(0).is_none());
1103 assert_eq!(scalar.map_len(), 0);
1104 assert!(scalar.map_key(0).is_none());
1105 assert!(scalar.map_value(0).is_none());
1106 }
1107
1108 #[test]
1109 fn accessors_on_a_null_handle_do_not_crash() {
1110 let _db = InMemoryDb::open().expect("open in-memory DuckDB");
1111 // SAFETY: a null handle is explicitly part of `Value`'s contract — it is
1112 // what `duckdb_bind_get_named_parameter` returns for an absent parameter.
1113 let value = unsafe { Value::from_raw(std::ptr::null_mut()) };
1114 assert!(value.is_null());
1115 assert!(value.as_str().is_err());
1116 #[cfg(feature = "duckdb-1-5")]
1117 assert!(value.display_string().is_none());
1118 assert!(value.struct_child(0).is_none());
1119 }
1120}