fsqlite_func/scalar.rs
1//! Scalar (row-level) function trait.
2//!
3//! Scalar functions compute a single output value from zero or more input
4//! values. They are stateless across rows: each invocation is independent.
5//!
6//! This trait is **open** (user-implementable), unlike the sealed pager/btree
7//! traits. Extension authors implement `ScalarFunction` to register custom
8//! SQL functions.
9//!
10//! # Send + Sync
11//!
12//! Scalar functions may be shared across threads via `Arc` for use by
13//! concurrent query executors. Implementations must be thread-safe.
14//!
15//! # Cx Exception
16//!
17//! `invoke` does **not** take `&Cx` because deterministic scalar functions
18//! are pure computations (§9 cross-cutting rule: "Pure computation
19//! exceptions: deterministic ScalarFunction::invoke without I/O need not
20//! take Cx").
21#![allow(clippy::unnecessary_literal_bound)]
22
23use fsqlite_error::Result;
24use fsqlite_types::SqliteValue;
25
26use crate::{FunctionArity, collation::CollationFunction};
27
28/// A scalar (row-level) SQL function.
29///
30/// Scalar functions are invoked once per row and return a single value.
31/// They are stored in the [`FunctionRegistry`](crate::FunctionRegistry) as
32/// `Arc<dyn ScalarFunction>`.
33///
34/// # Error Handling
35///
36/// - Return [`FrankenError::FunctionError`](fsqlite_error::FrankenError::FunctionError)
37/// for domain errors (e.g. `abs(i64::MIN)`).
38/// - Return [`FrankenError::TooBig`](fsqlite_error::FrankenError::TooBig)
39/// if the result exceeds `SQLITE_MAX_LENGTH`.
40/// SQLite's JSON subtype tag (`'J'` = 74), attached to TEXT/BLOB values
41/// produced by JSON functions so downstream JSON constructors embed them as
42/// parsed JSON rather than quoting them as ordinary strings.
43pub const JSON_SUBTYPE: u32 = 74;
44
45pub trait ScalarFunction: Send + Sync {
46 /// Execute this function on the given arguments.
47 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue>;
48
49 /// Execute this function with knowledge of each argument's value subtype
50 /// (e.g. [`JSON_SUBTYPE`] for values returned by JSON functions).
51 ///
52 /// The default ignores subtypes and delegates to [`Self::invoke`]. JSON
53 /// constructors (`json_object`, `json_array`, the JSON mutators) override
54 /// this so a JSON-subtyped TEXT argument is embedded as a JSON value
55 /// instead of being quoted as a plain string — matching C SQLite, which
56 /// keys this behaviour off `sqlite3_value_subtype()`.
57 fn invoke_with_arg_subtypes(
58 &self,
59 args: &[SqliteValue],
60 _arg_subtypes: &[u32],
61 ) -> Result<SqliteValue> {
62 self.invoke(args)
63 }
64
65 /// Whether this function consumes the SQL collation selected from its
66 /// arguments (for example built-in `nullif`, scalar `min`, and scalar
67 /// `max`). Custom functions default to collation-opaque semantics.
68 fn consumes_argument_collation(&self) -> bool {
69 false
70 }
71
72 /// Invoke with the selected SQL collation, when this implementation
73 /// advertises [`Self::consumes_argument_collation`].
74 ///
75 /// The default deliberately ignores the collation so a custom function
76 /// that happens to replace a collation-consuming built-in keeps its own
77 /// semantics.
78 fn invoke_with_collation(
79 &self,
80 args: &[SqliteValue],
81 _collation: Option<&dyn CollationFunction>,
82 ) -> Result<SqliteValue> {
83 self.invoke(args)
84 }
85
86 /// The subtype this function tags onto its result value, if any.
87 ///
88 /// Returns [`JSON_SUBTYPE`] for functions whose result is JSON text so the
89 /// engine can propagate the tag to the destination register. Defaults to
90 /// `None` (no subtype).
91 fn result_subtype(&self) -> Option<u32> {
92 None
93 }
94
95 /// Whether this function is deterministic (same inputs → same output).
96 ///
97 /// Deterministic functions enable constant folding and other query
98 /// planner optimizations. Defaults to `true`.
99 fn is_deterministic(&self) -> bool {
100 true
101 }
102
103 /// The number of arguments this function accepts.
104 ///
105 /// `-1` means variadic (any number of arguments).
106 fn num_args(&self) -> i32;
107
108 /// Minimum accepted argument count for a variadic function.
109 ///
110 /// The default is zero. Fixed-arity functions are matched directly from
111 /// [`Self::num_args`] and do not consult this method.
112 fn min_args(&self) -> i32 {
113 0
114 }
115
116 /// Maximum accepted argument count for a variadic function.
117 ///
118 /// The default is unbounded. Fixed-arity functions are matched directly
119 /// from [`Self::num_args`] and do not consult this method.
120 fn max_args(&self) -> Option<i32> {
121 None
122 }
123
124 /// Return the complete SQL-visible arity contract in one metadata call.
125 ///
126 /// Registries use this method exactly once before publishing a function,
127 /// so a reentrant or stateful [`Self::num_args`] implementation cannot
128 /// produce a key and bounds from different observations. Implementations
129 /// with dynamically-computed metadata may override this method directly.
130 fn arity(&self) -> FunctionArity {
131 let declared = self.num_args();
132 FunctionArity::from_declared_args(declared, || (self.min_args(), self.max_args()))
133 }
134
135 /// The function name, used in error messages and EXPLAIN output.
136 fn name(&self) -> &str;
137}
138
139#[cfg(test)]
140mod tests {
141 use std::sync::Arc;
142
143 use fsqlite_error::FrankenError;
144
145 use super::*;
146
147 // -- Mock: add_one(x) -> x + 1 --
148
149 struct AddOne;
150
151 impl ScalarFunction for AddOne {
152 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
153 match &args[0] {
154 SqliteValue::Integer(i) => Ok(SqliteValue::Integer(i + 1)),
155 SqliteValue::Float(f) => Ok(SqliteValue::Float(f + 1.0)),
156 SqliteValue::Null => Ok(SqliteValue::Null),
157 SqliteValue::Text(s) => {
158 let n: i64 = s.parse().unwrap_or(0);
159 Ok(SqliteValue::Integer(n + 1))
160 }
161 SqliteValue::Blob(_) => Ok(SqliteValue::Integer(1)),
162 }
163 }
164
165 fn num_args(&self) -> i32 {
166 1
167 }
168
169 fn name(&self) -> &str {
170 "add_one"
171 }
172 }
173
174 // -- Mock: non-deterministic --
175
176 struct NonDeterministic;
177
178 impl ScalarFunction for NonDeterministic {
179 fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
180 Ok(SqliteValue::Integer(42))
181 }
182
183 fn is_deterministic(&self) -> bool {
184 false
185 }
186
187 fn num_args(&self) -> i32 {
188 0
189 }
190
191 fn name(&self) -> &str {
192 "random_ish"
193 }
194 }
195
196 // -- Mock: variadic concat --
197
198 struct Concat;
199
200 impl ScalarFunction for Concat {
201 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
202 let mut result = String::new();
203 for arg in args {
204 result.push_str(&arg.to_text());
205 }
206 Ok(SqliteValue::Text(result.into()))
207 }
208
209 fn num_args(&self) -> i32 {
210 -1
211 }
212
213 fn name(&self) -> &str {
214 "concat"
215 }
216 }
217
218 // -- Mock: domain error --
219
220 struct SafeAbs;
221
222 impl ScalarFunction for SafeAbs {
223 fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
224 match &args[0] {
225 SqliteValue::Integer(i) => {
226 if *i == i64::MIN {
227 return Err(FrankenError::function_error("abs(i64::MIN) would overflow"));
228 }
229 Ok(SqliteValue::Integer(i.abs()))
230 }
231 _ => Ok(args[0].clone()),
232 }
233 }
234
235 fn num_args(&self) -> i32 {
236 1
237 }
238
239 fn name(&self) -> &str {
240 "safe_abs"
241 }
242 }
243
244 // -- Mock: too-big error --
245
246 struct BigResult;
247
248 impl ScalarFunction for BigResult {
249 fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
250 Err(FrankenError::TooBig)
251 }
252
253 fn num_args(&self) -> i32 {
254 0
255 }
256
257 fn name(&self) -> &str {
258 "big_result"
259 }
260 }
261
262 // -- Tests --
263
264 #[test]
265 fn test_scalar_function_invoke_basic() {
266 let f = AddOne;
267 // Integer
268 assert_eq!(
269 f.invoke(&[SqliteValue::Integer(41)]).unwrap(),
270 SqliteValue::Integer(42)
271 );
272 // Float
273 assert_eq!(
274 f.invoke(&[SqliteValue::Float(1.5)]).unwrap(),
275 SqliteValue::Float(2.5)
276 );
277 // Null
278 assert!(f.invoke(&[SqliteValue::Null]).unwrap().is_null());
279 // Text (numeric coercion)
280 assert_eq!(
281 f.invoke(&[SqliteValue::Text("99".into())]).unwrap(),
282 SqliteValue::Integer(100)
283 );
284 }
285
286 #[test]
287 fn test_scalar_function_deterministic_flag() {
288 let det = AddOne;
289 assert!(det.is_deterministic());
290
291 let non_det = NonDeterministic;
292 assert!(!non_det.is_deterministic());
293 }
294
295 #[test]
296 fn test_scalar_function_variadic() {
297 let f = Concat;
298 assert_eq!(f.num_args(), -1);
299 assert_eq!(f.min_args(), 0);
300 assert_eq!(f.max_args(), None);
301 assert!(f.arity().accepts(0));
302 assert!(f.arity().accepts(3));
303
304 // 0 args
305 assert_eq!(f.invoke(&[]).unwrap(), SqliteValue::Text("".into()));
306
307 // 1 arg
308 assert_eq!(
309 f.invoke(&[SqliteValue::Text("hello".into())]).unwrap(),
310 SqliteValue::Text("hello".into())
311 );
312
313 // many args
314 assert_eq!(
315 f.invoke(&[
316 SqliteValue::Text("a".into()),
317 SqliteValue::Text("b".into()),
318 SqliteValue::Text("c".into()),
319 ])
320 .unwrap(),
321 SqliteValue::Text("abc".into())
322 );
323 }
324
325 #[test]
326 fn test_scalar_function_error_domain() {
327 let f = SafeAbs;
328 let err = f.invoke(&[SqliteValue::Integer(i64::MIN)]).unwrap_err();
329 assert!(
330 matches!(err, FrankenError::FunctionError(ref msg) if msg.contains("overflow")),
331 "expected FunctionError, got {err:?}"
332 );
333 }
334
335 #[test]
336 fn test_scalar_function_too_big_error() {
337 let f = BigResult;
338 let err = f.invoke(&[]).unwrap_err();
339 assert!(matches!(err, FrankenError::TooBig));
340 }
341
342 #[test]
343 fn test_scalar_send_sync() {
344 fn assert_send_sync<T: Send + Sync>() {}
345 assert_send_sync::<AddOne>();
346
347 // Can be stored in Arc
348 let f: Arc<dyn ScalarFunction> = Arc::new(AddOne);
349 let f2 = Arc::clone(&f);
350 let handle = std::thread::spawn(move || f2.invoke(&[SqliteValue::Integer(0)]));
351 let _ = f.invoke(&[SqliteValue::Integer(1)]);
352 handle.join().unwrap().unwrap();
353 }
354}