Skip to main content

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}