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}