Skip to content

Commit c55117e

Browse files
committed
Replace register_volatile_scalar_function with volatile trait method
This trait-based approach is more idiomatic: `VScalar` implementations declare volatility via `volatile()` instead of using separate registration methods.
1 parent c3f0d38 commit c55117e

File tree

3 files changed

+78
-87
lines changed

3 files changed

+78
-87
lines changed

crates/duckdb/src/vscalar/arrow.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ pub trait VArrowScalar: Sized {
8484
/// The possible signatures of the scalar function. These will result in DuckDB scalar function overloads.
8585
/// The invoke method should be able to handle all of these signatures.
8686
fn signatures() -> Vec<ArrowFunctionSignature>;
87+
88+
/// Whether the scalar function is volatile.
89+
///
90+
/// Volatile functions are re-evaluated for each row, even if they have no parameters.
91+
/// This is useful for functions that generate random or unique values, such as random
92+
/// number generators, UUID generators, or fake data generators.
93+
///
94+
/// By default, DuckDB optimizes zero-argument scalar functions as constants, evaluating
95+
/// them only once. Returning true from this method prevents this optimization.
96+
///
97+
/// # Default
98+
/// Returns `false` by default, meaning the function is not volatile.
99+
fn volatile() -> bool {
100+
false
101+
}
87102
}
88103

89104
impl<T> VScalar for T

crates/duckdb/src/vscalar/function.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,6 @@ impl ScalarFunction {
121121
///
122122
/// By default, DuckDB optimizes zero-argument scalar functions as constants, evaluating
123123
/// them only once. Setting a function as volatile prevents this optimization.
124-
///
125-
/// # Example
126-
/// ```no_run
127-
/// use duckdb::vscalar::ScalarFunction;
128-
/// use duckdb::core::LogicalTypeHandle;
129-
/// use libduckdb_sys::LogicalTypeId;
130-
///
131-
/// let func = ScalarFunction::new("my_random_func").unwrap();
132-
/// func.set_return_type(&LogicalTypeHandle::from(LogicalTypeId::Varchar))
133-
/// .set_volatile() // Mark as volatile so it's evaluated per row
134-
/// .set_function(Some(my_random_impl));
135-
/// ```
136124
pub fn set_volatile(&self) -> &Self {
137125
unsafe {
138126
duckdb_scalar_function_set_volatile(self.ptr);

crates/duckdb/src/vscalar/mod.rs

Lines changed: 63 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ pub trait VScalar: Sized {
4545
/// These will result in DuckDB scalar function overloads.
4646
/// The invoke method should be able to handle all of these signatures.
4747
fn signatures() -> Vec<ScalarFunctionSignature>;
48+
49+
/// Whether the scalar function is volatile.
50+
///
51+
/// Volatile functions are re-evaluated for each row, even if they have no parameters.
52+
/// This is useful for functions that generate random or unique values, such as random
53+
/// number generators, UUID generators, or fake data generators.
54+
///
55+
/// By default, DuckDB optimizes zero-argument scalar functions as constants, evaluating
56+
/// them only once. Returning true from this method prevents this optimization.
57+
///
58+
/// # Default
59+
/// Returns `false` by default, meaning the function is not volatile.
60+
fn volatile() -> bool {
61+
false
62+
}
4863
}
4964

5065
/// Duckdb scalar function parameters
@@ -144,6 +159,9 @@ impl Connection {
144159
let scalar_function = ScalarFunction::new(name)?;
145160
signature.register_with_scalar(&scalar_function);
146161
scalar_function.set_function(Some(scalar_func::<S>));
162+
if S::volatile() {
163+
scalar_function.set_volatile();
164+
}
147165
scalar_function.set_extra_info(S::State::default());
148166
set.add_function(scalar_function)?;
149167
}
@@ -163,73 +181,9 @@ impl Connection {
163181
let scalar_function = ScalarFunction::new(name)?;
164182
signature.register_with_scalar(&scalar_function);
165183
scalar_function.set_function(Some(scalar_func::<S>));
166-
scalar_function.set_extra_info(state.clone());
167-
set.add_function(scalar_function)?;
168-
}
169-
self.db.borrow_mut().register_scalar_function_set(set)
170-
}
171-
172-
/// Register the given ScalarFunction with default state, marked as volatile.
173-
///
174-
/// Volatile functions are re-evaluated for each row, even if they have no parameters.
175-
/// This is useful for functions that generate random or unique values per row, such as:
176-
/// - Random number generators
177-
/// - UUID generators
178-
/// - Fake data generators
179-
/// - Current timestamp functions
180-
///
181-
/// By default, DuckDB optimizes zero-argument scalar functions as constants.
182-
/// Use this method when you need the function to be evaluated independently for each row.
183-
///
184-
/// # Example
185-
/// ```no_run
186-
/// use duckdb::Connection;
187-
/// // Assume RandomUUID implements VScalar
188-
/// let conn = Connection::open_in_memory()?;
189-
/// conn.register_volatile_scalar_function::<RandomUUID>("random_uuid")?;
190-
///
191-
/// // Each row gets a unique UUID
192-
/// let mut stmt = conn.prepare("SELECT random_uuid() FROM generate_series(1, 10)")?;
193-
/// # Ok::<(), duckdb::Error>(())
194-
/// ```
195-
#[inline]
196-
pub fn register_volatile_scalar_function<S: VScalar>(&self, name: &str) -> crate::Result<()>
197-
where
198-
S::State: Default,
199-
{
200-
let set = ScalarFunctionSet::new(name);
201-
for signature in S::signatures() {
202-
let scalar_function = ScalarFunction::new(name)?;
203-
signature.register_with_scalar(&scalar_function);
204-
scalar_function.set_function(Some(scalar_func::<S>));
205-
scalar_function.set_volatile(); // Mark as volatile
206-
scalar_function.set_extra_info(S::State::default());
207-
set.add_function(scalar_function)?;
208-
}
209-
self.db.borrow_mut().register_scalar_function_set(set)
210-
}
211-
212-
/// Register the given ScalarFunction with custom state, marked as volatile.
213-
///
214-
/// Volatile functions are re-evaluated for each row, even if they have no parameters.
215-
/// This is the volatile variant of `register_scalar_function_with_state`.
216-
///
217-
/// See [`register_volatile_scalar_function`](Self::register_volatile_scalar_function) for more details on volatile functions.
218-
#[inline]
219-
pub fn register_volatile_scalar_function_with_state<S: VScalar>(
220-
&self,
221-
name: &str,
222-
state: &S::State,
223-
) -> crate::Result<()>
224-
where
225-
S::State: Clone,
226-
{
227-
let set = ScalarFunctionSet::new(name);
228-
for signature in S::signatures() {
229-
let scalar_function = ScalarFunction::new(name)?;
230-
signature.register_with_scalar(&scalar_function);
231-
scalar_function.set_function(Some(scalar_func::<S>));
232-
scalar_function.set_volatile(); // Mark as volatile
184+
if S::volatile() {
185+
scalar_function.set_volatile();
186+
}
233187
scalar_function.set_extra_info(state.clone());
234188
set.add_function(scalar_function)?;
235189
}
@@ -442,9 +396,10 @@ mod test {
442396
Ok(())
443397
}
444398

445-
// Counter for testing volatile functions
399+
// Counters for testing volatile functions
446400
use std::sync::atomic::{AtomicU64, Ordering};
447-
static COUNTER: AtomicU64 = AtomicU64::new(0);
401+
static VOLATILE_COUNTER: AtomicU64 = AtomicU64::new(0);
402+
static NON_VOLATILE_COUNTER: AtomicU64 = AtomicU64::new(0);
448403

449404
struct CounterScalar {}
450405

@@ -461,7 +416,7 @@ mod test {
461416
let data = output_vec.as_mut_slice::<i64>();
462417

463418
for i in 0..len {
464-
let count = COUNTER.fetch_add(1, Ordering::SeqCst);
419+
let count = NON_VOLATILE_COUNTER.fetch_add(1, Ordering::SeqCst);
465420
data[i] = count as i64;
466421
}
467422
Ok(())
@@ -475,15 +430,48 @@ mod test {
475430
}
476431
}
477432

433+
struct VolatileCounterScalar {}
434+
435+
impl VScalar for VolatileCounterScalar {
436+
type State = ();
437+
438+
unsafe fn invoke(
439+
_: &Self::State,
440+
input: &mut DataChunkHandle,
441+
output: &mut dyn WritableVector,
442+
) -> Result<(), Box<dyn std::error::Error>> {
443+
let len = input.len();
444+
let mut output_vec = output.flat_vector();
445+
let data = output_vec.as_mut_slice::<i64>();
446+
447+
for i in 0..len {
448+
let count = VOLATILE_COUNTER.fetch_add(1, Ordering::SeqCst);
449+
data[i] = count as i64;
450+
}
451+
Ok(())
452+
}
453+
454+
fn signatures() -> Vec<ScalarFunctionSignature> {
455+
vec![ScalarFunctionSignature::exact(
456+
vec![],
457+
LogicalTypeHandle::from(LogicalTypeId::Bigint),
458+
)]
459+
}
460+
461+
fn volatile() -> bool {
462+
true
463+
}
464+
}
465+
478466
#[test]
479467
fn test_volatile_scalar() -> Result<(), Box<dyn Error>> {
480468
let conn = Connection::open_in_memory()?;
481469

482470
// Reset counter
483-
COUNTER.store(0, Ordering::SeqCst);
471+
VOLATILE_COUNTER.store(0, Ordering::SeqCst);
484472

485-
// Register as volatile
486-
conn.register_volatile_scalar_function::<CounterScalar>("volatile_counter")?;
473+
// Register volatile counter
474+
conn.register_scalar_function::<VolatileCounterScalar>("volatile_counter")?;
487475

488476
// Query should get different values for each row
489477
let mut stmt = conn.prepare("SELECT volatile_counter() FROM generate_series(1, 5)")?;
@@ -509,9 +497,9 @@ mod test {
509497
let conn = Connection::open_in_memory()?;
510498

511499
// Reset counter
512-
COUNTER.store(100, Ordering::SeqCst);
500+
NON_VOLATILE_COUNTER.store(0, Ordering::SeqCst);
513501

514-
// Register WITHOUT volatile flag
502+
// Register non-volatile counter
515503
conn.register_scalar_function::<CounterScalar>("non_volatile_counter")?;
516504

517505
// Query should get the SAME value for all rows (optimized as constant)

0 commit comments

Comments
 (0)