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