Skip to main content

better_duck_core/raw/
owned_value.rs

1//! RAII wrapper for a standalone, owned `duckdb_value`, with introspection.
2//!
3//! Most values in this driver are read straight out of result vectors into
4//! [`DuckValue`], but DuckDB also has a *standalone value* object
5//! (`duckdb_value`) — the currency of the scalar-value C API. [`OwnedValue`] owns
6//! one (destroying it exactly once on drop) and exposes the introspection the C API
7//! offers on any value: whether it is SQL `NULL`, its SQL string rendering, and —
8//! for a `STRUCT` value — its child fields by index (each returned child is itself
9//! an owned `OwnedValue`, so nothing is leaked or double-freed).
10//!
11//! Obtain one with [`DuckValue::to_owned_value`](crate::types::value::DuckValue::to_owned_value).
12// FFI pointer args are used safely inside `unsafe` blocks.
13#![allow(clippy::not_unsafe_ptr_arg_deref)]
14
15use std::ffi::{c_void, CStr};
16
17use crate::{
18    error::{DuckDBConversionError, Result},
19    ffi::{
20        duckdb_destroy_value, duckdb_free, duckdb_get_date, duckdb_get_hugeint,
21        duckdb_get_interval, duckdb_get_struct_child, duckdb_get_time, duckdb_get_time_ns,
22        duckdb_get_time_tz, duckdb_get_timestamp, duckdb_get_timestamp_ms, duckdb_get_timestamp_ns,
23        duckdb_get_timestamp_s, duckdb_get_timestamp_tz, duckdb_get_type_id, duckdb_get_uhugeint,
24        duckdb_get_uuid, duckdb_get_value_type, duckdb_is_null_value, duckdb_type, duckdb_value,
25        duckdb_value_to_string, idx_t, DUCKDB_TYPE_DUCKDB_TYPE_DATE,
26        DUCKDB_TYPE_DUCKDB_TYPE_HUGEINT, DUCKDB_TYPE_DUCKDB_TYPE_INTERVAL,
27        DUCKDB_TYPE_DUCKDB_TYPE_TIME, DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP,
28        DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_MS, DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_NS,
29        DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_S, DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_TZ,
30        DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS, DUCKDB_TYPE_DUCKDB_TYPE_TIME_TZ,
31        DUCKDB_TYPE_DUCKDB_TYPE_UHUGEINT, DUCKDB_TYPE_DUCKDB_TYPE_UUID,
32    },
33    types::numeric::{i128_from_hugeint, u128_from_uhugeint},
34    types::uuid::DuckUuid,
35};
36
37/// A broken-down DuckDB `INTERVAL` (months, days, microseconds).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct IntervalParts {
40    /// Whole months.
41    pub months: i32,
42    /// Whole days.
43    pub days: i32,
44    /// Sub-day microseconds.
45    pub micros: i64,
46}
47
48/// An owned standalone `duckdb_value` (destroyed once on drop) with introspection.
49pub struct OwnedValue {
50    value: duckdb_value,
51}
52
53impl OwnedValue {
54    /// Takes ownership of a raw `duckdb_value`, or `None` if null.
55    ///
56    /// # Safety
57    ///
58    /// `value` must be a live `duckdb_value` whose ownership is transferred here
59    /// (destroyed on drop).
60    pub(crate) unsafe fn from_raw(value: duckdb_value) -> Option<OwnedValue> {
61        if value.is_null() {
62            return None;
63        }
64        Some(OwnedValue { value })
65    }
66
67    /// Whether the value's type is SQL `NULL`.
68    #[must_use]
69    pub fn is_null(&self) -> bool {
70        // SAFETY: `self.value` is a valid, non-null duckdb_value owned by `self`.
71        unsafe { duckdb_is_null_value(self.value) }
72    }
73
74    /// The raw handle, borrowed for the duration of `&self` (e.g. to make a vector
75    /// reference this value). The caller must not destroy it.
76    #[allow(dead_code)]
77    pub(crate) fn raw(&self) -> duckdb_value {
78        self.value
79    }
80
81    /// The value's SQL string rendering (e.g. `42`, `'text'`), or `None` if DuckDB
82    /// produces none.
83    #[must_use]
84    pub fn to_sql_string(&self) -> Option<String> {
85        // SAFETY: `self.value` is valid; `duckdb_value_to_string` returns a heap
86        // `char*` (or null) that must be freed with `duckdb_free`.
87        let ptr = unsafe { duckdb_value_to_string(self.value) };
88        if ptr.is_null() {
89            return None;
90        }
91        // SAFETY: `ptr` is a valid, non-null, null-terminated C string.
92        let s = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
93        // SAFETY: `ptr` was allocated by DuckDB and ownership transferred to us.
94        unsafe { duckdb_free(ptr as *mut c_void) };
95        Some(s)
96    }
97
98    /// The child field at `index` of a `STRUCT` value, as an owned [`OwnedValue`],
99    /// or `None` if out of range (or the value is not a struct).
100    #[must_use]
101    pub fn struct_child(
102        &self,
103        index: u64,
104    ) -> Option<OwnedValue> {
105        // SAFETY: `self.value` is valid; `duckdb_get_struct_child` returns a newly
106        // allocated child value (destroyed by the returned `OwnedValue`) or null.
107        unsafe { OwnedValue::from_raw(duckdb_get_struct_child(self.value, index as idx_t)) }
108    }
109
110    /// The value's top-level `duckdb_type` id.
111    #[must_use]
112    fn type_id(&self) -> duckdb_type {
113        // SAFETY: `self.value` is valid; `duckdb_get_value_type` returns a *borrowed*
114        // logical type owned by the value — read only, never destroyed.
115        let lt = unsafe { duckdb_get_value_type(self.value) };
116        // SAFETY: `lt` is a valid (borrowed) logical type.
117        unsafe { duckdb_get_type_id(lt) }
118    }
119
120    /// Errors unless the value's type id is `expected`.
121    fn expect_type(
122        &self,
123        expected: duckdb_type,
124        what: &str,
125    ) -> Result<(), DuckDBConversionError> {
126        let actual = self.type_id();
127        if actual == expected {
128            Ok(())
129        } else {
130            Err(DuckDBConversionError::ConversionError(format!(
131                "value is type id {actual}, not {what} (id {expected})"
132            )))
133        }
134    }
135
136    /// The `HUGEINT` value as an `i128`.
137    ///
138    /// # Errors
139    /// Errors if the value is not a `HUGEINT`.
140    pub fn get_hugeint(&self) -> Result<i128, DuckDBConversionError> {
141        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_HUGEINT, "HUGEINT")?;
142        // SAFETY: type checked as HUGEINT above; `self.value` is valid.
143        Ok(i128_from_hugeint(unsafe { duckdb_get_hugeint(self.value) }))
144    }
145
146    /// The `UHUGEINT` value as a `u128`.
147    ///
148    /// # Errors
149    /// Errors if the value is not a `UHUGEINT`.
150    pub fn get_uhugeint(&self) -> Result<u128, DuckDBConversionError> {
151        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_UHUGEINT, "UHUGEINT")?;
152        // SAFETY: type checked as UHUGEINT above; `self.value` is valid.
153        Ok(u128_from_uhugeint(unsafe { duckdb_get_uhugeint(self.value) }))
154    }
155
156    /// The `DATE` value as a day count since the epoch (1970-01-01).
157    ///
158    /// # Errors
159    /// Errors if the value is not a `DATE`.
160    pub fn get_date_days(&self) -> Result<i32, DuckDBConversionError> {
161        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_DATE, "DATE")?;
162        // SAFETY: type checked as DATE above; `self.value` is valid.
163        Ok(unsafe { duckdb_get_date(self.value) }.days)
164    }
165
166    /// The `TIME` value as microseconds since midnight.
167    ///
168    /// # Errors
169    /// Errors if the value is not a `TIME`.
170    pub fn get_time_micros(&self) -> Result<i64, DuckDBConversionError> {
171        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIME, "TIME")?;
172        // SAFETY: type checked as TIME above; `self.value` is valid.
173        Ok(unsafe { duckdb_get_time(self.value) }.micros)
174    }
175
176    /// The `TIME_NS` value as nanoseconds since midnight.
177    ///
178    /// # Errors
179    /// Errors if the value is not a `TIME_NS`.
180    pub fn get_time_ns_nanos(&self) -> Result<i64, DuckDBConversionError> {
181        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIME_NS, "TIME_NS")?;
182        // SAFETY: type checked as TIME_NS above; `self.value` is valid.
183        Ok(unsafe { duckdb_get_time_ns(self.value) }.nanos)
184    }
185
186    /// The `TIME WITH TIME ZONE` value as its packed 64-bit representation
187    /// (micros + offset bits, per DuckDB's `duckdb_time_tz` layout).
188    ///
189    /// # Errors
190    /// Errors if the value is not a `TIME_TZ`.
191    pub fn get_time_tz_bits(&self) -> Result<u64, DuckDBConversionError> {
192        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIME_TZ, "TIME_TZ")?;
193        // SAFETY: type checked as TIME_TZ above; `self.value` is valid.
194        Ok(unsafe { duckdb_get_time_tz(self.value) }.bits)
195    }
196
197    /// The `TIMESTAMP` value as microseconds since the epoch.
198    ///
199    /// # Errors
200    /// Errors if the value is not a `TIMESTAMP`.
201    pub fn get_timestamp_micros(&self) -> Result<i64, DuckDBConversionError> {
202        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP, "TIMESTAMP")?;
203        // SAFETY: type checked as TIMESTAMP above; `self.value` is valid.
204        Ok(unsafe { duckdb_get_timestamp(self.value) }.micros)
205    }
206
207    /// The `TIMESTAMP WITH TIME ZONE` value as microseconds since the epoch (UTC).
208    ///
209    /// # Errors
210    /// Errors if the value is not a `TIMESTAMP_TZ`.
211    pub fn get_timestamp_tz_micros(&self) -> Result<i64, DuckDBConversionError> {
212        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_TZ, "TIMESTAMP_TZ")?;
213        // SAFETY: type checked as TIMESTAMP_TZ above; `self.value` is valid.
214        Ok(unsafe { duckdb_get_timestamp_tz(self.value) }.micros)
215    }
216
217    /// The `TIMESTAMP_S` value as seconds since the epoch.
218    ///
219    /// # Errors
220    /// Errors if the value is not a `TIMESTAMP_S`.
221    pub fn get_timestamp_seconds(&self) -> Result<i64, DuckDBConversionError> {
222        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_S, "TIMESTAMP_S")?;
223        // SAFETY: type checked as TIMESTAMP_S above; `self.value` is valid.
224        Ok(unsafe { duckdb_get_timestamp_s(self.value) }.seconds)
225    }
226
227    /// The `TIMESTAMP_MS` value as milliseconds since the epoch.
228    ///
229    /// # Errors
230    /// Errors if the value is not a `TIMESTAMP_MS`.
231    pub fn get_timestamp_millis(&self) -> Result<i64, DuckDBConversionError> {
232        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_MS, "TIMESTAMP_MS")?;
233        // SAFETY: type checked as TIMESTAMP_MS above; `self.value` is valid.
234        Ok(unsafe { duckdb_get_timestamp_ms(self.value) }.millis)
235    }
236
237    /// The `TIMESTAMP_NS` value as nanoseconds since the epoch.
238    ///
239    /// # Errors
240    /// Errors if the value is not a `TIMESTAMP_NS`.
241    pub fn get_timestamp_nanos(&self) -> Result<i64, DuckDBConversionError> {
242        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_TIMESTAMP_NS, "TIMESTAMP_NS")?;
243        // SAFETY: type checked as TIMESTAMP_NS above; `self.value` is valid.
244        Ok(unsafe { duckdb_get_timestamp_ns(self.value) }.nanos)
245    }
246
247    /// The `INTERVAL` value as its (months, days, micros) parts.
248    ///
249    /// # Errors
250    /// Errors if the value is not an `INTERVAL`.
251    pub fn get_interval(&self) -> Result<IntervalParts, DuckDBConversionError> {
252        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_INTERVAL, "INTERVAL")?;
253        // SAFETY: type checked as INTERVAL above; `self.value` is valid.
254        let iv = unsafe { duckdb_get_interval(self.value) };
255        Ok(IntervalParts { months: iv.months, days: iv.days, micros: iv.micros })
256    }
257
258    /// The `UUID` value.
259    ///
260    /// # Errors
261    /// Errors if the value is not a `UUID`.
262    pub fn get_uuid(&self) -> Result<DuckUuid, DuckDBConversionError> {
263        self.expect_type(DUCKDB_TYPE_DUCKDB_TYPE_UUID, "UUID")?;
264        // SAFETY: type checked as UUID above; `self.value` is valid. The value API
265        // returns the logical uhugeint (sign-bit flip already undone), so build the
266        // `DuckUuid` directly from its two halves.
267        let raw = unsafe { duckdb_get_uuid(self.value) };
268        Ok(DuckUuid(((raw.upper as u128) << 64) | (raw.lower as u128)))
269    }
270}
271
272impl Drop for OwnedValue {
273    fn drop(&mut self) {
274        if !self.value.is_null() {
275            // SAFETY: `self.value` is a valid, non-null duckdb_value owned by `self`
276            // and not yet destroyed; destroyed exactly once here.
277            unsafe { duckdb_destroy_value(&mut self.value) };
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use crate::types::value::DuckValue;
285    use std::collections::HashMap;
286
287    #[test]
288    fn null_and_scalar_values_report_correctly() {
289        assert!(DuckValue::Null.to_owned_value().unwrap().is_null());
290        let int = DuckValue::Int(42).to_owned_value().unwrap();
291        assert!(!int.is_null());
292        assert_eq!(int.to_sql_string().as_deref(), Some("42"));
293    }
294
295    #[test]
296    fn text_value_renders_as_sql_string() {
297        let v = DuckValue::text("hi").to_owned_value().unwrap();
298        // DuckDB renders a VARCHAR value quoted.
299        assert_eq!(v.to_sql_string().as_deref(), Some("'hi'"));
300    }
301
302    #[test]
303    fn struct_children_are_owned_and_introspectable() {
304        // A single-field struct so child index 0 is deterministic.
305        let s = DuckValue::Struct(HashMap::from([("n".to_owned(), DuckValue::Int(7))]));
306        let owned = s.to_owned_value().unwrap();
307        let child = owned.struct_child(0).expect("struct child 0");
308        assert!(!child.is_null());
309        assert_eq!(child.to_sql_string().as_deref(), Some("7"));
310        // Out-of-range child index yields None.
311        assert!(owned.struct_child(5).is_none());
312    }
313
314    #[test]
315    fn typed_scalar_getters_read_matching_values_and_reject_mismatches() {
316        use crate::types::uuid::DuckUuid;
317
318        // HUGEINT / UHUGEINT.
319        assert_eq!(DuckValue::HugeInt(-170).to_owned_value().unwrap().get_hugeint().unwrap(), -170);
320        assert_eq!(DuckValue::UHugeInt(340).to_owned_value().unwrap().get_uhugeint().unwrap(), 340);
321
322        // UUID round-trips through the value API.
323        let uuid = DuckUuid(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
324        assert_eq!(DuckValue::Uuid(uuid).to_owned_value().unwrap().get_uuid().unwrap(), uuid);
325
326        // Exact type check: a HUGEINT getter on a UHUGEINT value errors.
327        assert!(DuckValue::UHugeInt(1).to_owned_value().unwrap().get_hugeint().is_err());
328        // ...and on an integer value too.
329        assert!(DuckValue::Int(1).to_owned_value().unwrap().get_timestamp_micros().is_err());
330    }
331
332    #[cfg(feature = "chrono")]
333    #[test]
334    fn interval_getter_reads_parts() {
335        // 2 pure days → (0 months, 0 days-field, 2*86400*1e6 micros) — DuckDB keeps a
336        // day-precision interval in the micros field when built from a µs Duration.
337        let iv = DuckValue::Interval(chrono::Duration::microseconds(2 * 86_400 * 1_000_000))
338            .to_owned_value()
339            .unwrap()
340            .get_interval()
341            .unwrap();
342        assert_eq!((iv.months, iv.days), (0, 0));
343        assert_eq!(iv.micros, 2 * 86_400 * 1_000_000);
344    }
345
346    #[cfg(feature = "chrono")]
347    #[test]
348    fn temporal_getters_read_each_resolution() {
349        use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
350
351        // DATE → day count (2024-03-15).
352        let date = DuckValue::Date(NaiveDate::from_ymd_opt(2024, 3, 15).unwrap());
353        let days = date.to_owned_value().unwrap().get_date_days().unwrap();
354        assert_eq!(
355            days,
356            (NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
357                - NaiveDate::from_ymd_opt(1970, 1, 1).unwrap())
358            .num_days() as i32
359        );
360
361        // TIME → micros since midnight (01:02:03).
362        let time = DuckValue::Time(NaiveTime::from_hms_opt(1, 2, 3).unwrap());
363        assert_eq!(
364            time.to_owned_value().unwrap().get_time_micros().unwrap(),
365            (3600 + 2 * 60 + 3) * 1_000_000
366        );
367
368        // TIMESTAMP → micros since epoch.
369        let ts = NaiveDateTime::new(
370            NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
371            NaiveTime::from_hms_opt(0, 0, 1).unwrap(),
372        );
373        assert_eq!(
374            DuckValue::Timestamp(ts).to_owned_value().unwrap().get_timestamp_micros().unwrap(),
375            1_000_000
376        );
377    }
378}