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}