Skip to content

Commit 0b98c35

Browse files
authored
Support volatile scalar functions (#639)
Based on #636 by @tobilg, but more idiomatic using a new trait method.
2 parents d79a185 + e585283 commit 0b98c35

File tree

3 files changed

+152
-1
lines changed

3 files changed

+152
-1
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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ use libduckdb_sys::{
5353
duckdb_create_scalar_function_set, duckdb_data_chunk, duckdb_delete_callback_t, duckdb_destroy_scalar_function,
5454
duckdb_function_info, duckdb_scalar_function, duckdb_scalar_function_add_parameter, duckdb_scalar_function_set,
5555
duckdb_scalar_function_set_extra_info, duckdb_scalar_function_set_function, duckdb_scalar_function_set_name,
56-
duckdb_scalar_function_set_return_type, duckdb_scalar_function_set_varargs, duckdb_vector, DuckDBSuccess,
56+
duckdb_scalar_function_set_return_type, duckdb_scalar_function_set_varargs, duckdb_scalar_function_set_volatile,
57+
duckdb_vector, DuckDBSuccess,
5758
};
5859

5960
use crate::{core::LogicalTypeHandle, Error};
@@ -112,6 +113,21 @@ impl ScalarFunction {
112113
self
113114
}
114115

116+
/// Marks the scalar function as volatile.
117+
///
118+
/// Volatile functions are re-evaluated for each row, even if they have no parameters.
119+
/// This is useful for functions that generate random or unique values, such as random
120+
/// number generators, UUID generators, or fake data generators.
121+
///
122+
/// By default, DuckDB optimizes zero-argument scalar functions as constants, evaluating
123+
/// them only once. Setting a function as volatile prevents this optimization.
124+
pub fn set_volatile(&self) -> &Self {
125+
unsafe {
126+
duckdb_scalar_function_set_volatile(self.ptr);
127+
}
128+
self
129+
}
130+
115131
/// Assigns extra information to the scalar function using raw pointers.
116132
///
117133
/// For most use cases, prefer [`set_extra_info`](Self::set_extra_info) which handles memory management automatically.

crates/duckdb/src/vscalar/mod.rs

Lines changed: 120 additions & 0 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,6 +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>));
184+
if S::volatile() {
185+
scalar_function.set_volatile();
186+
}
166187
scalar_function.set_extra_info(state.clone());
167188
set.add_function(scalar_function)?;
168189
}
@@ -374,4 +395,103 @@ mod test {
374395

375396
Ok(())
376397
}
398+
399+
// Counters for testing volatile functions
400+
use std::sync::atomic::{AtomicU64, Ordering};
401+
static VOLATILE_COUNTER: AtomicU64 = AtomicU64::new(0);
402+
static NON_VOLATILE_COUNTER: AtomicU64 = AtomicU64::new(0);
403+
404+
struct CounterScalar {}
405+
406+
impl VScalar for CounterScalar {
407+
type State = ();
408+
409+
unsafe fn invoke(
410+
_: &Self::State,
411+
input: &mut DataChunkHandle,
412+
output: &mut dyn WritableVector,
413+
) -> Result<(), Box<dyn std::error::Error>> {
414+
let len = input.len();
415+
let mut output_vec = output.flat_vector();
416+
let data = output_vec.as_mut_slice::<i64>();
417+
418+
for item in data.iter_mut().take(len) {
419+
*item = NON_VOLATILE_COUNTER.fetch_add(1, Ordering::SeqCst) as i64;
420+
}
421+
Ok(())
422+
}
423+
424+
fn signatures() -> Vec<ScalarFunctionSignature> {
425+
vec![ScalarFunctionSignature::exact(
426+
vec![],
427+
LogicalTypeHandle::from(LogicalTypeId::Bigint),
428+
)]
429+
}
430+
}
431+
432+
struct VolatileCounterScalar {}
433+
434+
impl VScalar for VolatileCounterScalar {
435+
type State = ();
436+
437+
unsafe fn invoke(
438+
_: &Self::State,
439+
input: &mut DataChunkHandle,
440+
output: &mut dyn WritableVector,
441+
) -> Result<(), Box<dyn std::error::Error>> {
442+
let len = input.len();
443+
let mut output_vec = output.flat_vector();
444+
let data = output_vec.as_mut_slice::<i64>();
445+
446+
for item in data.iter_mut().take(len) {
447+
*item = VOLATILE_COUNTER.fetch_add(1, Ordering::SeqCst) as i64;
448+
}
449+
Ok(())
450+
}
451+
452+
fn signatures() -> Vec<ScalarFunctionSignature> {
453+
vec![ScalarFunctionSignature::exact(
454+
vec![],
455+
LogicalTypeHandle::from(LogicalTypeId::Bigint),
456+
)]
457+
}
458+
459+
fn volatile() -> bool {
460+
true
461+
}
462+
}
463+
464+
#[test]
465+
fn test_volatile_scalar() -> Result<(), Box<dyn Error>> {
466+
let conn = Connection::open_in_memory()?;
467+
468+
VOLATILE_COUNTER.store(0, Ordering::SeqCst);
469+
conn.register_scalar_function::<VolatileCounterScalar>("volatile_counter")?;
470+
471+
let values: Vec<i64> = conn
472+
.prepare("SELECT volatile_counter() FROM generate_series(1, 5)")?
473+
.query_map([], |row| row.get(0))?
474+
.collect::<Result<_, _>>()?;
475+
476+
assert_eq!(values, [0, 1, 2, 3, 4]);
477+
478+
Ok(())
479+
}
480+
481+
#[test]
482+
fn test_non_volatile_scalar() -> Result<(), Box<dyn Error>> {
483+
let conn = Connection::open_in_memory()?;
484+
485+
NON_VOLATILE_COUNTER.store(0, Ordering::SeqCst);
486+
conn.register_scalar_function::<CounterScalar>("non_volatile_counter")?;
487+
488+
// Constant folding should make every row identical
489+
let distinct_count: i64 = conn
490+
.prepare("SELECT COUNT(DISTINCT non_volatile_counter()) FROM generate_series(1, 5)")?
491+
.query_row([], |row| row.get(0))?;
492+
493+
assert_eq!(distinct_count, 1);
494+
495+
Ok(())
496+
}
377497
}

0 commit comments

Comments
 (0)