quack_rs/expression.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//! Bound expressions (`DuckDB` 1.5.0+).
7//!
8//! [`Expression`] is an RAII wrapper around `DuckDB`'s `duckdb_expression` handle.
9//! Extension authors obtain one from a scalar function's *bind* callback via
10//! [`ScalarBindInfo::argument`][crate::scalar::ScalarBindInfo::argument], which
11//! lets the bind phase inspect each argument's static type and — when the
12//! argument is a constant — fold it to a concrete [`Value`].
13//!
14//! This is the canonical way to implement scalar functions whose behaviour
15//! depends on a constant argument (for example a format string or a precision)
16//! that should be validated or pre-computed once at bind time rather than on
17//! every row.
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use quack_rs::scalar::ScalarBindInfo;
23//! use libduckdb_sys::duckdb_bind_info;
24//!
25//! unsafe extern "C" fn my_bind(info: duckdb_bind_info) {
26//! let bind = unsafe { ScalarBindInfo::new(info) };
27//! if let Some(arg) = unsafe { bind.argument(0) } {
28//! // Inspect the argument's static return type at bind time.
29//! let _ty = arg.return_type();
30//! if arg.is_foldable() {
31//! // With a `ClientContext`, `arg.fold(&ctx)` pre-computes the constant.
32//! }
33//! }
34//! }
35//! ```
36
37use libduckdb_sys::{
38 duckdb_destroy_expression, duckdb_expression, duckdb_expression_fold,
39 duckdb_expression_is_foldable, duckdb_expression_return_type, duckdb_value,
40};
41
42use crate::client_context::ClientContext;
43use crate::error_data::ErrorData;
44use crate::types::LogicalType;
45use crate::value::Value;
46
47/// RAII wrapper for a `duckdb_expression`.
48///
49/// Automatically destroyed when dropped.
50pub struct Expression {
51 raw: duckdb_expression,
52}
53
54impl Expression {
55 /// Wraps a raw `duckdb_expression` handle, taking ownership.
56 ///
57 /// # Safety
58 ///
59 /// `raw` must be a valid `duckdb_expression` returned by a `DuckDB` API call
60 /// (e.g. `duckdb_scalar_function_bind_get_argument`). The caller must not
61 /// destroy the handle after this call.
62 #[inline]
63 #[must_use]
64 pub const unsafe fn from_raw(raw: duckdb_expression) -> Self {
65 Self { raw }
66 }
67
68 /// Returns the raw handle without consuming the `Expression`.
69 #[inline]
70 #[must_use]
71 pub const fn as_raw(&self) -> duckdb_expression {
72 self.raw
73 }
74
75 /// Returns `true` if the underlying handle is null.
76 #[inline]
77 #[must_use]
78 pub const fn is_null(&self) -> bool {
79 self.raw.is_null()
80 }
81
82 /// Returns the static return type of this expression, or `None` if the
83 /// handle is null.
84 #[must_use]
85 pub fn return_type(&self) -> Option<LogicalType> {
86 if self.raw.is_null() {
87 return None;
88 }
89 // SAFETY: self.raw is a non-null, valid duckdb_expression. The returned
90 // logical type is owned by the caller and freed by LogicalType on drop.
91 let raw = unsafe { duckdb_expression_return_type(self.raw) };
92 if raw.is_null() {
93 return None;
94 }
95 // SAFETY: raw is a non-null logical type handle owned by the caller.
96 Some(unsafe { LogicalType::from_raw(raw) })
97 }
98
99 /// Returns `true` if this expression is *foldable* — i.e. it is constant and
100 /// can be evaluated to a single [`Value`] via [`fold`][Expression::fold]
101 /// without per-row input.
102 #[must_use]
103 pub fn is_foldable(&self) -> bool {
104 if self.raw.is_null() {
105 return false;
106 }
107 // SAFETY: self.raw is a non-null, valid duckdb_expression.
108 unsafe { duckdb_expression_is_foldable(self.raw) }
109 }
110
111 /// Folds this (constant) expression into a single [`Value`].
112 ///
113 /// Only valid when [`is_foldable`][Expression::is_foldable] returns `true`.
114 ///
115 /// # Errors
116 ///
117 /// Returns the structured [`ErrorData`] if folding fails (for example because
118 /// the expression is not constant).
119 pub fn fold(&self, context: &ClientContext) -> Result<Value, ErrorData> {
120 let mut out_value: duckdb_value = std::ptr::null_mut();
121 // SAFETY: self.raw and context.as_raw() are valid; out_value is a valid
122 // out-pointer that DuckDB writes an owned duckdb_value into.
123 let err_raw =
124 unsafe { duckdb_expression_fold(context.as_raw(), self.raw, &raw mut out_value) };
125 // SAFETY: duckdb_expression_fold returns an owned duckdb_error_data.
126 let err = unsafe { ErrorData::from_raw(err_raw) };
127 if err.has_error() {
128 // SAFETY: out_value may have been left null/invalid; destroy any value.
129 if !out_value.is_null() {
130 drop(unsafe { Value::from_raw(out_value) });
131 }
132 return Err(err);
133 }
134 // SAFETY: folding succeeded, so out_value is an owned duckdb_value.
135 Ok(unsafe { Value::from_raw(out_value) })
136 }
137}
138
139impl Drop for Expression {
140 fn drop(&mut self) {
141 if !self.raw.is_null() {
142 // SAFETY: self.raw is a valid duckdb_expression that we own.
143 unsafe { duckdb_destroy_expression(&raw mut self.raw) };
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn null_expression_is_null() {
154 let expr = unsafe { Expression::from_raw(std::ptr::null_mut()) };
155 assert!(expr.is_null());
156 assert!(!expr.is_foldable());
157 assert!(expr.return_type().is_none());
158 }
159
160 #[test]
161 fn size_of_expression_is_one_pointer() {
162 assert_eq!(
163 std::mem::size_of::<Expression>(),
164 std::mem::size_of::<usize>()
165 );
166 }
167}