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    /// # Errors
99    ///
100    /// Returns `ExtensionError` if the value is null or contains invalid UTF-8.
101    pub fn as_str(&self) -> Result<String, ExtensionError> {
102        if self.raw.is_null() {
103            return Err(ExtensionError::new("Value is null"));
104        }
105        // SAFETY: self.raw is a valid duckdb_value per constructor contract.
106        let c_str: *mut c_char = unsafe { duckdb_get_varchar(self.raw) };
107        if c_str.is_null() {
108            return Err(ExtensionError::new("duckdb_get_varchar returned null"));
109        }
110        // SAFETY: c_str is a valid null-terminated C string allocated by DuckDB.
111        let result = unsafe { CStr::from_ptr(c_str) }
112            .to_str()
113            .map(str::to_owned)
114            .map_err(|_| ExtensionError::new("Value contains invalid UTF-8"));
115        // SAFETY: c_str was allocated by DuckDB and must be freed with duckdb_free.
116        unsafe { duckdb_free(c_str.cast()) };
117        result
118    }
119
120    /// Extracts the value as an `i32` (INTEGER).
121    ///
122    /// `DuckDB` will attempt to cast the value to INTEGER. If the value is not
123    /// numeric, this returns 0.
124    #[inline]
125    #[must_use]
126    pub fn as_i32(&self) -> i32 {
127        // SAFETY: self.raw is valid per constructor contract.
128        unsafe { duckdb_get_int32(self.raw) }
129    }
130
131    /// Extracts the value as an `i64` (BIGINT).
132    ///
133    /// `DuckDB` will attempt to cast the value to BIGINT. If the value is not
134    /// numeric, this returns 0.
135    #[inline]
136    #[must_use]
137    pub fn as_i64(&self) -> i64 {
138        // SAFETY: self.raw is valid per constructor contract.
139        unsafe { duckdb_get_int64(self.raw) }
140    }
141
142    /// Extracts the value as an `f32` (FLOAT).
143    ///
144    /// `DuckDB` will attempt to cast the value to FLOAT. If the value is not
145    /// numeric, this returns 0.0.
146    #[inline]
147    #[must_use]
148    pub fn as_f32(&self) -> f32 {
149        // SAFETY: self.raw is valid per constructor contract.
150        unsafe { duckdb_get_float(self.raw) }
151    }
152
153    /// Extracts the value as an `f64` (DOUBLE).
154    ///
155    /// `DuckDB` will attempt to cast the value to DOUBLE. If the value is not
156    /// numeric, this returns 0.0.
157    #[inline]
158    #[must_use]
159    pub fn as_f64(&self) -> f64 {
160        // SAFETY: self.raw is valid per constructor contract.
161        unsafe { duckdb_get_double(self.raw) }
162    }
163
164    /// Extracts the value as a `bool` (BOOLEAN).
165    ///
166    /// `DuckDB` will attempt to cast the value to BOOLEAN. If the value is not
167    /// convertible, this returns `false`.
168    #[inline]
169    #[must_use]
170    pub fn as_bool(&self) -> bool {
171        // SAFETY: self.raw is valid per constructor contract.
172        unsafe { duckdb_get_bool(self.raw) }
173    }
174
175    /// Extracts the value as an `i8` (TINYINT).
176    ///
177    /// `DuckDB` will attempt to cast the value to TINYINT. If the value is not
178    /// numeric, this returns 0.
179    #[inline]
180    #[must_use]
181    pub fn as_i8(&self) -> i8 {
182        // SAFETY: self.raw is valid per constructor contract.
183        unsafe { duckdb_get_int8(self.raw) }
184    }
185
186    /// Extracts the value as an `i16` (SMALLINT).
187    ///
188    /// `DuckDB` will attempt to cast the value to SMALLINT. If the value is not
189    /// numeric, this returns 0.
190    #[inline]
191    #[must_use]
192    pub fn as_i16(&self) -> i16 {
193        // SAFETY: self.raw is valid per constructor contract.
194        unsafe { duckdb_get_int16(self.raw) }
195    }
196
197    /// Extracts the value as a `u8` (UTINYINT).
198    ///
199    /// `DuckDB` will attempt to cast the value to UTINYINT. If the value is not
200    /// numeric, this returns 0.
201    #[inline]
202    #[must_use]
203    pub fn as_u8(&self) -> u8 {
204        // SAFETY: self.raw is valid per constructor contract.
205        unsafe { duckdb_get_uint8(self.raw) }
206    }
207
208    /// Extracts the value as a `u16` (USMALLINT).
209    ///
210    /// `DuckDB` will attempt to cast the value to USMALLINT. If the value is not
211    /// numeric, this returns 0.
212    #[inline]
213    #[must_use]
214    pub fn as_u16(&self) -> u16 {
215        // SAFETY: self.raw is valid per constructor contract.
216        unsafe { duckdb_get_uint16(self.raw) }
217    }
218
219    /// Extracts the value as a `u32` (UINTEGER).
220    ///
221    /// `DuckDB` will attempt to cast the value to UINTEGER. If the value is not
222    /// numeric, this returns 0.
223    #[inline]
224    #[must_use]
225    pub fn as_u32(&self) -> u32 {
226        // SAFETY: self.raw is valid per constructor contract.
227        unsafe { duckdb_get_uint32(self.raw) }
228    }
229
230    /// Extracts the value as a `u64` (UBIGINT).
231    ///
232    /// `DuckDB` will attempt to cast the value to UBIGINT. If the value is not
233    /// numeric, this returns 0.
234    #[inline]
235    #[must_use]
236    pub fn as_u64(&self) -> u64 {
237        // SAFETY: self.raw is valid per constructor contract.
238        unsafe { duckdb_get_uint64(self.raw) }
239    }
240
241    /// Extracts the value as an `i128` (HUGEINT).
242    ///
243    /// `DuckDB` returns HUGEINT as `{ lower: u64, upper: i64 }`. This method
244    /// reconstructs the full `i128` value.
245    #[inline]
246    #[must_use]
247    pub fn as_i128(&self) -> i128 {
248        // SAFETY: self.raw is valid per constructor contract.
249        let h = unsafe { duckdb_get_hugeint(self.raw) };
250        #[allow(clippy::cast_lossless)]
251        let result = (h.upper as i128) << 64 | (h.lower as i128);
252        result
253    }
254
255    /// Extracts the value as a `String`, returning `default` on failure.
256    ///
257    /// Convenience for `val.as_str().unwrap_or_else(|_| default.to_owned())`.
258    #[inline]
259    #[must_use]
260    pub fn as_str_or(&self, default: &str) -> String {
261        self.as_str().unwrap_or_else(|_| default.to_owned())
262    }
263
264    /// Extracts the value as a `String`, returning an empty string on failure.
265    ///
266    /// Convenience for `val.as_str().unwrap_or_default()`.
267    #[inline]
268    #[must_use]
269    pub fn as_str_or_default(&self) -> String {
270        self.as_str().unwrap_or_default()
271    }
272
273    /// Extracts the value as an `i32`, returning `default` if the handle is null.
274    #[inline]
275    #[must_use]
276    pub fn as_i32_or(&self, default: i32) -> i32 {
277        if self.is_null() {
278            default
279        } else {
280            self.as_i32()
281        }
282    }
283
284    /// Extracts the value as an `i64`, returning `default` if the handle is null.
285    #[inline]
286    #[must_use]
287    pub fn as_i64_or(&self, default: i64) -> i64 {
288        if self.is_null() {
289            default
290        } else {
291            self.as_i64()
292        }
293    }
294
295    /// Extracts the value as an `f32`, returning `default` if the handle is null.
296    #[inline]
297    #[must_use]
298    pub fn as_f32_or(&self, default: f32) -> f32 {
299        if self.is_null() {
300            default
301        } else {
302            self.as_f32()
303        }
304    }
305
306    /// Extracts the value as an `f64`, returning `default` if the handle is null.
307    #[inline]
308    #[must_use]
309    pub fn as_f64_or(&self, default: f64) -> f64 {
310        if self.is_null() {
311            default
312        } else {
313            self.as_f64()
314        }
315    }
316
317    /// Extracts the value as a `bool`, returning `default` if the handle is null.
318    #[inline]
319    #[must_use]
320    pub fn as_bool_or(&self, default: bool) -> bool {
321        if self.is_null() {
322            default
323        } else {
324            self.as_bool()
325        }
326    }
327
328    /// Extracts the value as an `i8`, returning `default` if the handle is null.
329    #[inline]
330    #[must_use]
331    pub fn as_i8_or(&self, default: i8) -> i8 {
332        if self.is_null() {
333            default
334        } else {
335            self.as_i8()
336        }
337    }
338
339    /// Extracts the value as an `i16`, returning `default` if the handle is null.
340    #[inline]
341    #[must_use]
342    pub fn as_i16_or(&self, default: i16) -> i16 {
343        if self.is_null() {
344            default
345        } else {
346            self.as_i16()
347        }
348    }
349
350    /// Extracts the value as a `u8`, returning `default` if the handle is null.
351    #[inline]
352    #[must_use]
353    pub fn as_u8_or(&self, default: u8) -> u8 {
354        if self.is_null() {
355            default
356        } else {
357            self.as_u8()
358        }
359    }
360
361    /// Extracts the value as a `u16`, returning `default` if the handle is null.
362    #[inline]
363    #[must_use]
364    pub fn as_u16_or(&self, default: u16) -> u16 {
365        if self.is_null() {
366            default
367        } else {
368            self.as_u16()
369        }
370    }
371
372    /// Extracts the value as a `u32`, returning `default` if the handle is null.
373    #[inline]
374    #[must_use]
375    pub fn as_u32_or(&self, default: u32) -> u32 {
376        if self.is_null() {
377            default
378        } else {
379            self.as_u32()
380        }
381    }
382
383    /// Extracts the value as a `u64`, returning `default` if the handle is null.
384    #[inline]
385    #[must_use]
386    pub fn as_u64_or(&self, default: u64) -> u64 {
387        if self.is_null() {
388            default
389        } else {
390            self.as_u64()
391        }
392    }
393
394    /// Extracts the value as an `i128`, returning `default` if the handle is null.
395    #[inline]
396    #[must_use]
397    pub fn as_i128_or(&self, default: i128) -> i128 {
398        if self.is_null() {
399            default
400        } else {
401            self.as_i128()
402        }
403    }
404
405    /// Creates a `TIME_NS` value (time of day with nanosecond precision) from a
406    /// raw nanosecond count (`DuckDB` 1.5.0+).
407    ///
408    /// Pairs with [`as_time_ns`][Value::as_time_ns] and the
409    /// [`TypeId::TimeNs`][crate::types::TypeId::TimeNs] column type.
410    #[cfg(feature = "duckdb-1-5")]
411    #[inline]
412    #[must_use]
413    pub fn time_ns(nanos: i64) -> Self {
414        // SAFETY: duckdb_create_time_ns accepts any nanosecond count and returns
415        // an owned duckdb_value.
416        let raw = unsafe { duckdb_create_time_ns(duckdb_time_ns { nanos }) };
417        Self { raw }
418    }
419
420    /// Extracts the value as a `TIME_NS` nanosecond count (`DuckDB` 1.5.0+).
421    ///
422    /// Returns 0 if the value is not a `TIME_NS`.
423    #[cfg(feature = "duckdb-1-5")]
424    #[inline]
425    #[must_use]
426    pub fn as_time_ns(&self) -> i64 {
427        // SAFETY: self.raw is valid per constructor contract.
428        unsafe { duckdb_get_time_ns(self.raw) }.nanos
429    }
430
431    /// Returns the canonical string representation of this value, as `DuckDB`
432    /// would render it (`DuckDB` 1.5.0+).
433    ///
434    /// Returns `None` if the handle is null or the rendered text is not valid
435    /// UTF-8. This is primarily useful for diagnostics and error messages, where
436    /// it works for any value type (not just VARCHAR).
437    #[cfg(feature = "duckdb-1-5")]
438    #[must_use]
439    pub fn display_string(&self) -> Option<String> {
440        if self.raw.is_null() {
441            return None;
442        }
443        // SAFETY: self.raw is a valid duckdb_value per constructor contract.
444        let c_str: *mut c_char = unsafe { duckdb_value_to_string(self.raw) };
445        if c_str.is_null() {
446            return None;
447        }
448        // SAFETY: c_str is a valid null-terminated string allocated by DuckDB.
449        let result = unsafe { CStr::from_ptr(c_str) }
450            .to_str()
451            .ok()
452            .map(str::to_owned);
453        // SAFETY: c_str was allocated by DuckDB and must be freed with duckdb_free.
454        unsafe { duckdb_free(c_str.cast()) };
455        result
456    }
457
458    /// Returns `true` if the underlying handle is null.
459    #[inline]
460    #[must_use]
461    pub const fn is_null(&self) -> bool {
462        self.raw.is_null()
463    }
464
465    /// Returns the raw `duckdb_value` handle without consuming the `Value`.
466    ///
467    /// The `Value` still owns the handle and will destroy it on drop.
468    #[inline]
469    #[must_use]
470    pub const fn as_raw(&self) -> duckdb_value {
471        self.raw
472    }
473
474    /// Consumes the `Value` and returns the raw `duckdb_value` handle.
475    ///
476    /// The caller takes ownership and is responsible for calling
477    /// `duckdb_destroy_value` when done.
478    #[inline]
479    #[must_use]
480    pub const fn into_raw(self) -> duckdb_value {
481        let raw = self.raw;
482        std::mem::forget(self);
483        raw
484    }
485}
486
487impl Drop for Value {
488    fn drop(&mut self) {
489        if !self.raw.is_null() {
490            // SAFETY: self.raw is a valid duckdb_value that we own.
491            unsafe { duckdb_destroy_value(&raw mut self.raw) };
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn null_value_is_null() {
502        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
503        assert!(val.is_null());
504    }
505
506    #[test]
507    fn null_value_as_str_returns_error() {
508        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
509        assert!(val.as_str().is_err());
510    }
511
512    #[test]
513    fn into_raw_prevents_double_free() {
514        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
515        let raw = val.into_raw();
516        assert!(raw.is_null());
517        // No double-free: Value was forgotten via into_raw.
518    }
519
520    #[test]
521    fn size_of_value() {
522        assert_eq!(std::mem::size_of::<Value>(), std::mem::size_of::<usize>());
523    }
524
525    #[test]
526    fn as_str_or_returns_default_for_null() {
527        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
528        assert_eq!(val.as_str_or("fallback"), "fallback");
529    }
530
531    #[test]
532    fn as_str_or_default_returns_empty_for_null() {
533        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
534        assert_eq!(val.as_str_or_default(), "");
535    }
536
537    #[test]
538    fn as_i64_or_returns_default_for_null() {
539        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
540        assert_eq!(val.as_i64_or(99), 99);
541    }
542
543    #[test]
544    fn as_i32_or_returns_default_for_null() {
545        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
546        assert_eq!(val.as_i32_or(42), 42);
547    }
548
549    #[test]
550    fn as_bool_or_returns_default_for_null() {
551        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
552        assert!(val.as_bool_or(true));
553        assert!(!val.as_bool_or(false));
554    }
555
556    #[test]
557    fn as_f64_or_returns_default_for_null() {
558        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
559        assert!((val.as_f64_or(2.72) - 2.72).abs() < f64::EPSILON);
560    }
561
562    #[test]
563    fn as_f32_or_returns_default_for_null() {
564        let val = unsafe { Value::from_raw(std::ptr::null_mut()) };
565        assert!((val.as_f32_or(2.5) - 2.5).abs() < f32::EPSILON);
566    }
567}