Skip to main content

linkrs_query/executor/expression/
functions.rs

1//! Expression Function Module
2//!
3//! Provide the function definitions and implementations during the evaluation of expressions, including both built-in functions and custom functions.
4//!
5//! ## Module Structure
6//!
7//! - `signature.rs` - type signature system
8//! - `registry.rs` - function registry
9//! - `builtin/` – Implementation of built-in functions
10//! - `custom.rs` - custom function definitions
11//! - `table.rs` - table function definitions
12//!
13//! ## How to use it
14//!
15//! ```rust
16//! use linkrs_core::value::Value;
17//! use linkrs_query::executor::expression::functions::{BuiltinFunction, MathFunction};
18//!
19//! let func = BuiltinFunction::Math(MathFunction::Abs);
20//! let result = func.execute(&[Value::Int(-5)]).expect("abs should succeed");
21//! assert_eq!(result, Value::Int(5));
22//! ```
23
24pub mod builtin;
25pub mod registry;
26pub mod signature;
27pub mod udf;
28
29mod custom;
30mod table;
31
32pub use custom::{
33    AggregateFinalCallback, AggregateStepCallback, CFunctionContext, CustomFunction,
34    CustomFunctionImpl, ScalarFunctionCallback,
35};
36pub use table::{BuiltinTableFunction, TableFunction};
37
38// Full-text search functions
39pub mod fulltext;
40pub use fulltext::{FulltextExecutionContext, FulltextFunction};
41
42// Vector functions (re-export from builtin)
43pub use builtin::vector::VectorFunction;
44
45// Sequence functions (re-export from builtin)
46pub use builtin::sequence::SequenceFunction;
47
48pub use registry::{global_registry, global_registry_ref, FunctionRegistry};
49pub use signature::ValueType;
50pub use udf::{SharedPlugin, UdfError, UdfPlugin};
51
52// Reexport the function types from the built-in submodule.
53pub use builtin::container::ContainerFunction;
54pub use builtin::conversion::ConversionFunction;
55pub use builtin::datetime::DateTimeFunction;
56pub use builtin::geography::GeographyFunction;
57pub use builtin::graph::GraphFunction;
58pub use builtin::math::MathFunction;
59pub use builtin::path::PathFunction;
60pub use builtin::regex::RegexFunction;
61pub use builtin::string::StringFunction;
62pub use builtin::utility::UtilityFunction;
63
64use crate::executor::expression::evaluation_context::graph_storage::GraphStorageRef;
65use crate::executor::expression::{ExpressionError, ExpressionErrorType};
66use linkrs_core::types::operators::AggregateFunction;
67use linkrs_core::Value;
68
69/// Function reference enumeration, used to reference functions in expressions
70#[derive(Debug, Clone)]
71pub enum FunctionRef<'a> {
72    /// Reference to built-in functions
73    Builtin(&'a BuiltinFunction),
74    /// Reference to a custom function
75    Custom(&'a CustomFunction),
76}
77
78/// A function reference that possesses ownership
79#[derive(Debug, Clone)]
80pub enum OwnedFunctionRef {
81    /// Reference to an internal function (with ownership)
82    Builtin(BuiltinFunction),
83    /// Reference to a custom function (with ownership)
84    Custom(CustomFunction),
85}
86
87impl<'a> From<FunctionRef<'a>> for OwnedFunctionRef {
88    fn from(func_ref: FunctionRef<'a>) -> Self {
89        match func_ref {
90            FunctionRef::Builtin(f) => OwnedFunctionRef::Builtin(f.clone()),
91            FunctionRef::Custom(f) => OwnedFunctionRef::Custom(f.clone()),
92        }
93    }
94}
95
96impl OwnedFunctionRef {
97    pub fn name(&self) -> &str {
98        match self {
99            OwnedFunctionRef::Builtin(f) => f.name(),
100            OwnedFunctionRef::Custom(f) => f.name(),
101        }
102    }
103
104    pub fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError> {
105        match self {
106            OwnedFunctionRef::Builtin(f) => f.execute(args),
107            OwnedFunctionRef::Custom(f) => f.execute(args),
108        }
109    }
110
111    pub fn execute_with_storage(
112        &self,
113        args: &[Value],
114        storage: &GraphStorageRef,
115    ) -> Result<Value, ExpressionError> {
116        match self {
117            OwnedFunctionRef::Builtin(f) => f.execute_with_storage(args, storage),
118            OwnedFunctionRef::Custom(f) => f.execute(args),
119        }
120    }
121
122    pub fn execute_with_cache(
123        &self,
124        args: &[Value],
125        _cache: &mut (),
126    ) -> Result<Value, ExpressionError> {
127        match self {
128            OwnedFunctionRef::Builtin(f) => f.execute_with_cache(args, _cache),
129            OwnedFunctionRef::Custom(f) => f.execute(args), // Custom functions don't have cache
130        }
131    }
132}
133
134/// Expression function characteristics
135pub trait ExpressionFunction: Send + Sync {
136    /// Obtain the function name
137    fn name(&self) -> &str;
138
139    /// Determine the number of parameters
140    fn arity(&self) -> usize;
141
142    /// Check whether variable parameters are accepted.
143    fn is_variadic(&self) -> bool;
144
145    /// Execute the function
146    fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError>;
147
148    /// Obtain the function description
149    fn description(&self) -> &str;
150}
151
152/// Built-in function types to avoid dynamic distribution.
153#[derive(Debug, Clone)]
154pub enum BuiltinFunction {
155    /// Mathematical functions
156    Math(MathFunction),
157    /// String functions
158    String(StringFunction),
159    /// Regular Expression Functions
160    Regex(RegexFunction),
161    /// Aggregate functions
162    Aggregate(AggregateFunction),
163    /// Type conversion functions
164    Conversion(ConversionFunction),
165    /// Date and time functions
166    DateTime(DateTimeFunction),
167    /// Geospatial functions
168    Geography(GeographyFunction),
169    /// Practical functions
170    Utility(UtilityFunction),
171    /// Graph-related functions
172    Graph(GraphFunction),
173    /// Container operation functions
174    Container(ContainerFunction),
175    /// Path function
176    Path(PathFunction),
177    /// Full-text search functions
178    Fulltext(FulltextFunction),
179    /// Vector functions
180    Vector(VectorFunction),
181    /// Window functions
182    Window(crate::executor::expression::functions::builtin::window::WindowFunction),
183    /// Sequence functions (curr_val, next_val)
184    Sequence(SequenceFunction),
185}
186
187impl BuiltinFunction {
188    /// Get function name
189    pub fn name(&self) -> &str {
190        match self {
191            BuiltinFunction::Math(f) => f.name(),
192            BuiltinFunction::String(f) => f.name(),
193            BuiltinFunction::Regex(f) => f.name(),
194            BuiltinFunction::Aggregate(f) => f.name(),
195            BuiltinFunction::Conversion(f) => f.name(),
196            BuiltinFunction::DateTime(f) => f.name(),
197            BuiltinFunction::Geography(f) => f.name(),
198            BuiltinFunction::Utility(f) => f.name(),
199            BuiltinFunction::Graph(f) => f.name(),
200            BuiltinFunction::Container(f) => f.name(),
201            BuiltinFunction::Path(f) => f.name(),
202            BuiltinFunction::Fulltext(f) => f.name(),
203            BuiltinFunction::Vector(f) => f.name(),
204            BuiltinFunction::Window(f) => f.name(),
205            BuiltinFunction::Sequence(f) => f.name(),
206        }
207    }
208
209    /// Get the number of parameters
210    pub fn arity(&self) -> usize {
211        match self {
212            BuiltinFunction::Math(f) => f.arity(),
213            BuiltinFunction::String(f) => f.arity(),
214            BuiltinFunction::Regex(f) => f.arity(),
215            BuiltinFunction::Aggregate(f) => f.arity(),
216            BuiltinFunction::Conversion(f) => f.arity(),
217            BuiltinFunction::DateTime(f) => f.arity(),
218            BuiltinFunction::Geography(f) => f.arity(),
219            BuiltinFunction::Utility(f) => f.arity(),
220            BuiltinFunction::Graph(f) => f.arity(),
221            BuiltinFunction::Container(f) => f.arity(),
222            BuiltinFunction::Path(f) => f.arity(),
223            BuiltinFunction::Fulltext(f) => f.arity(),
224            BuiltinFunction::Vector(f) => f.arity(),
225            BuiltinFunction::Window(f) => f.arity(),
226            BuiltinFunction::Sequence(f) => f.arity(),
227        }
228    }
229
230    /// Check if variable parameters are accepted
231    pub fn is_variadic(&self) -> bool {
232        match self {
233            BuiltinFunction::Math(f) => f.is_variadic(),
234            BuiltinFunction::String(f) => f.is_variadic(),
235            BuiltinFunction::Regex(f) => f.is_variadic(),
236            BuiltinFunction::Aggregate(f) => f.is_variadic(),
237            BuiltinFunction::Conversion(f) => f.is_variadic(),
238            BuiltinFunction::DateTime(f) => f.is_variadic(),
239            BuiltinFunction::Geography(f) => f.is_variadic(),
240            BuiltinFunction::Utility(f) => f.is_variadic(),
241            BuiltinFunction::Graph(f) => f.is_variadic(),
242            BuiltinFunction::Container(f) => f.is_variadic(),
243            BuiltinFunction::Path(f) => f.is_variadic(),
244            BuiltinFunction::Fulltext(f) => f.is_variadic(),
245            BuiltinFunction::Vector(f) => f.is_variadic(),
246            BuiltinFunction::Window(f) => f.is_variadic(),
247            BuiltinFunction::Sequence(f) => f.is_variadic(),
248        }
249    }
250
251    /// Whether the function is deterministic and free of side effects.
252    ///
253    /// Purity gates constant folding: a function classified as pure may be
254    /// evaluated once at plan time and its result baked into the plan as a
255    /// literal. Classifying a non-deterministic or context-dependent
256    /// function as pure would fold a stale result into a (possibly cached)
257    /// plan, so the classification is a **whitelist**: categories that are
258    /// pure for every variant return `true`; mixed categories enumerate the
259    /// pure variants explicitly; anything not listed — including future
260    /// variants — defaults to `false`.
261    ///
262    /// Never folded:
263    /// - non-deterministic: `rand*`, `now`, `timestamp`, `current_date`,
264    ///   `current_timestamp`, `gen_random_uuid`
265    /// - session-dependent: `current_user`, `current_database` (currently
266    ///   stubbed, but conceptually per-session)
267    /// - need row-group / storage / index context: aggregates, window
268    ///   functions, graph functions, fulltext search, vector functions
269    pub fn is_pure(&self) -> bool {
270        match self {
271            // Every variant of these categories is a deterministic
272            // value-level operator.
273            BuiltinFunction::String(_)
274            | BuiltinFunction::Regex(_)
275            | BuiltinFunction::Conversion(_)
276            | BuiltinFunction::Geography(_)
277            | BuiltinFunction::Container(_)
278            | BuiltinFunction::Path(_)
279            | BuiltinFunction::Vector(_) => true,
280
281            // Mixed categories: whitelist the pure variants; anything not
282            // listed (including future variants) is conservatively non-pure.
283            BuiltinFunction::Math(f) => Self::pure_math(f),
284            BuiltinFunction::DateTime(f) => Self::pure_datetime(f),
285            BuiltinFunction::Utility(f) => Self::pure_utility(f),
286
287            // Need row-group / storage / index / session context: never fold.
288            BuiltinFunction::Aggregate(_)
289            | BuiltinFunction::Graph(_)
290            | BuiltinFunction::Fulltext(_)
291            | BuiltinFunction::Window(_)
292            | BuiltinFunction::Sequence(_) => false,
293        }
294    }
295
296    /// Whitelist of deterministic math functions.
297    ///
298    /// `rand*` and future variants are not listed and therefore never fold.
299    fn pure_math(f: &MathFunction) -> bool {
300        matches!(
301            f,
302            MathFunction::Abs
303                | MathFunction::Sqrt
304                | MathFunction::Pow
305                | MathFunction::Log
306                | MathFunction::Log10
307                | MathFunction::Sin
308                | MathFunction::Cos
309                | MathFunction::Tan
310                | MathFunction::Round
311                | MathFunction::Ceil
312                | MathFunction::Floor
313                | MathFunction::Asin
314                | MathFunction::Acos
315                | MathFunction::Atan
316                | MathFunction::Cbrt
317                | MathFunction::Hypot
318                | MathFunction::Sign
319                | MathFunction::E
320                | MathFunction::Pi
321                | MathFunction::Exp2
322                | MathFunction::Log2
323                | MathFunction::Radians
324                | MathFunction::BitAnd
325                | MathFunction::BitOr
326                | MathFunction::BitXor
327                | MathFunction::Atan2
328                | MathFunction::Sinh
329                | MathFunction::Cosh
330                | MathFunction::Tanh
331                | MathFunction::Degrees
332                | MathFunction::Gcd
333                | MathFunction::Lcm
334                | MathFunction::Factorial
335                | MathFunction::Gamma
336                | MathFunction::Lgamma
337                | MathFunction::Negate
338                | MathFunction::Even
339                | MathFunction::BitShiftLeft
340                | MathFunction::BitShiftRight
341        )
342    }
343
344    /// Whitelist of deterministic date/time functions.
345    ///
346    /// `now`, `timestamp`, `current_date`, `current_timestamp` and future
347    /// variants are not listed and therefore never fold.
348    fn pure_datetime(f: &DateTimeFunction) -> bool {
349        matches!(
350            f,
351            DateTimeFunction::Date
352                | DateTimeFunction::Time
353                | DateTimeFunction::DateTime
354                | DateTimeFunction::Year
355                | DateTimeFunction::Month
356                | DateTimeFunction::Day
357                | DateTimeFunction::Hour
358                | DateTimeFunction::Minute
359                | DateTimeFunction::Second
360                | DateTimeFunction::DateAdd
361                | DateTimeFunction::DateSub
362                | DateTimeFunction::DateDiff
363                | DateTimeFunction::DateTrunc
364                | DateTimeFunction::ToChar
365                | DateTimeFunction::ToDate
366                | DateTimeFunction::Age
367                | DateTimeFunction::LastDay
368                | DateTimeFunction::GenerateSeries
369                | DateTimeFunction::ToYears
370                | DateTimeFunction::ToMonths
371                | DateTimeFunction::ToDays
372                | DateTimeFunction::ToHours
373                | DateTimeFunction::ToMinutes
374                | DateTimeFunction::ToSeconds
375                | DateTimeFunction::ToMilliseconds
376                | DateTimeFunction::ToMicroseconds
377                | DateTimeFunction::Century
378                | DateTimeFunction::EpochMs
379                | DateTimeFunction::ToTimestamp
380                | DateTimeFunction::ToEpochMs
381                | DateTimeFunction::DatePart
382                | DateTimeFunction::DayName
383                | DateTimeFunction::MonthName
384        )
385    }
386
387    /// Whitelist of deterministic utility functions.
388    ///
389    /// `gen_random_uuid` is non-deterministic; `current_user` /
390    /// `current_database` depend on session state (even though the current
391    /// implementation stubs them); future variants are not listed and
392    /// therefore never fold.
393    fn pure_utility(f: &UtilityFunction) -> bool {
394        matches!(
395            f,
396            UtilityFunction::Coalesce
397                | UtilityFunction::Hash
398                | UtilityFunction::JsonExtract
399                | UtilityFunction::JsonBuildObject
400                | UtilityFunction::JsonBuildArray
401                | UtilityFunction::JsonObjectKeys
402                | UtilityFunction::NullIf
403                | UtilityFunction::Greatest
404                | UtilityFunction::Least
405                | UtilityFunction::JsonEach
406                | UtilityFunction::JsonTypeOf
407                | UtilityFunction::JsonStripNulls
408                | UtilityFunction::IfNull
409                | UtilityFunction::TypeOf
410                | UtilityFunction::Version
411                | UtilityFunction::Corr
412                | UtilityFunction::CovarPop
413                | UtilityFunction::CovarSamp
414                | UtilityFunction::OctetLength
415                | UtilityFunction::Encode
416                | UtilityFunction::Decode
417                | UtilityFunction::UnionValue
418                | UtilityFunction::UnionTag
419                | UtilityFunction::UnionExtract
420        )
421    }
422
423    /// Get function description
424    pub fn description(&self) -> &str {
425        match self {
426            BuiltinFunction::Math(f) => f.description(),
427            BuiltinFunction::String(f) => f.description(),
428            BuiltinFunction::Regex(f) => f.description(),
429            BuiltinFunction::Aggregate(f) => f.description(),
430            BuiltinFunction::Conversion(f) => f.description(),
431            BuiltinFunction::DateTime(f) => f.description(),
432            BuiltinFunction::Geography(f) => f.description(),
433            BuiltinFunction::Utility(f) => f.description(),
434            BuiltinFunction::Graph(f) => f.description(),
435            BuiltinFunction::Container(f) => f.description(),
436            BuiltinFunction::Path(f) => f.description(),
437            BuiltinFunction::Fulltext(f) => f.description(),
438            BuiltinFunction::Vector(f) => f.description(),
439            BuiltinFunction::Window(f) => f.description(),
440            BuiltinFunction::Sequence(f) => f.description(),
441        }
442    }
443
444    /// executable function
445    pub fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError> {
446        if !self.is_variadic() && args.len() != self.arity() {
447            return Err(ExpressionError::invalid_arity(
448                self.name(),
449                self.arity(),
450                args.len(),
451            ));
452        }
453        match self {
454            BuiltinFunction::Math(f) => f.execute(args),
455            BuiltinFunction::String(f) => f.execute(args),
456            BuiltinFunction::Regex(f) => f.execute(args),
457            BuiltinFunction::Aggregate(_) => Err(ExpressionError::new(
458                ExpressionErrorType::InvalidOperation,
459                "Aggregation functions need to be executed within the aggregation context"
460                    .to_string(),
461            )),
462            BuiltinFunction::Conversion(f) => f.execute(args),
463            BuiltinFunction::DateTime(f) => f.execute(args),
464            BuiltinFunction::Geography(f) => f.execute(args),
465            BuiltinFunction::Utility(f) => f.execute(args),
466            BuiltinFunction::Graph(f) => f.execute(args),
467            BuiltinFunction::Container(f) => f.execute(args),
468            BuiltinFunction::Path(f) => f.execute(args),
469            BuiltinFunction::Fulltext(_f) => {
470                // Fulltext functions require execution context
471                // This is a placeholder - actual execution happens in the executor
472                Err(ExpressionError::new(
473                    ExpressionErrorType::InvalidOperation,
474                    "The full-text search function needs to be executed within the context of full-text search".to_string(),
475                ))
476            }
477            BuiltinFunction::Vector(f) => f.execute(args),
478            BuiltinFunction::Window(f) => f.execute(args),
479            BuiltinFunction::Sequence(f) => f.execute(args),
480        }
481    }
482
483    /// Execute function with graph storage access.
484    /// Graph functions use this to perform storage-backed operations.
485    pub fn execute_with_storage(
486        &self,
487        args: &[Value],
488        storage: &GraphStorageRef,
489    ) -> Result<Value, ExpressionError> {
490        match self {
491            BuiltinFunction::Graph(f) => f.execute_with_storage(args, storage),
492            _ => self.execute(args),
493        }
494    }
495
496    /// Execution function (with cache)
497    ///
498    /// The caching function has been removed; this method directly calls `execute`.
499    pub fn execute_with_cache(
500        &self,
501        args: &[Value],
502        _cache: &mut (),
503    ) -> Result<Value, ExpressionError> {
504        self.execute(args)
505    }
506}
507
508impl ExpressionFunction for BuiltinFunction {
509    fn name(&self) -> &str {
510        self.name()
511    }
512
513    fn arity(&self) -> usize {
514        self.arity()
515    }
516
517    fn is_variadic(&self) -> bool {
518        self.is_variadic()
519    }
520
521    fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError> {
522        self.execute(args)
523    }
524
525    fn description(&self) -> &str {
526        self.description()
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    /// All explicitly known non-deterministic / context-dependent functions
535    /// must be classified as non-pure (never constant-folded).
536    #[test]
537    fn test_is_pure_impure_functions_never_fold() {
538        let registry = global_registry();
539        for name in [
540            "rand",
541            "rand32",
542            "rand64",
543            "now",
544            "timestamp",
545            "current_date",
546            "current_timestamp",
547            "gen_random_uuid",
548            "current_user",
549            "current_database",
550        ] {
551            let f = registry
552                .get_builtin(name)
553                .unwrap_or_else(|| panic!("function `{name}` must be registered"));
554            assert!(!f.is_pure(), "`{name}` is non-pure and must never fold");
555        }
556    }
557
558    /// Representative deterministic value operators must be foldable.
559    #[test]
560    fn test_is_pure_representative_pure_functions() {
561        let registry = global_registry();
562        for name in [
563            "abs",
564            "sqrt",
565            "pow",
566            "upper",
567            "length",
568            "regex_match",
569            "to_string",
570            "to_int",
571            "year",
572            "to_char",
573            "st_distance",
574            "coalesce",
575            "json_extract",
576            "version",
577            "keys",
578            "nodes",
579            "cosine_similarity",
580        ] {
581            let f = registry
582                .get_builtin(name)
583                .unwrap_or_else(|| panic!("function `{name}` must be registered"));
584            assert!(f.is_pure(), "`{name}` is a deterministic value operator");
585        }
586    }
587
588    /// Every registered builtin function must be consciously classified.
589    ///
590    /// The non-pure set is exactly {context-bound categories} ∪ {explicitly
591    /// listed non-pure variants}. Adding a function without updating either
592    /// `is_pure` or this table fails the test, so new functions can never
593    /// silently inherit a wrong purity default.
594    #[test]
595    fn test_is_pure_classification_is_explicit_for_all_registered() {
596        let registry = global_registry();
597        for name in registry.function_names() {
598            let f = registry
599                .get_builtin(name)
600                .unwrap_or_else(|| panic!("function `{name}` must be registered"));
601            let impure_by_category = matches!(
602                f,
603                BuiltinFunction::Aggregate(_)
604                    | BuiltinFunction::Graph(_)
605                    | BuiltinFunction::Fulltext(_)
606                    | BuiltinFunction::Window(_)
607                    | BuiltinFunction::Sequence(_)
608            );
609            let impure_by_variant = matches!(
610                f,
611                BuiltinFunction::Math(f)
612                    if matches!(f, MathFunction::Rand | MathFunction::Rand32 | MathFunction::Rand64 | MathFunction::SetSeed)
613            ) || matches!(
614                f,
615                BuiltinFunction::DateTime(f)
616                    if matches!(
617                        f,
618                        DateTimeFunction::Now
619                            | DateTimeFunction::TimeStamp
620                            | DateTimeFunction::CurrentDate
621                            | DateTimeFunction::CurrentTimestamp
622                    )
623            ) || matches!(
624                f,
625                BuiltinFunction::Utility(f)
626                    if matches!(
627                        f,
628                        UtilityFunction::GenRandomUuid
629                            | UtilityFunction::CurrentUser
630                            | UtilityFunction::CurrentDatabase
631                    )
632            );
633            assert_eq!(
634                f.is_pure(),
635                !(impure_by_category || impure_by_variant),
636                "function `{name}` is not explicitly classified as pure or non-pure"
637            );
638        }
639    }
640
641    /// Fixed-arity functions reject wrong argument counts at the central
642    /// `BuiltinFunction::execute` entry with a unified error.
643    #[test]
644    fn test_central_arity_fixed_functions() {
645        let registry = global_registry();
646        // Correct calls still succeed.
647        assert!(registry.execute("length", &[Value::string("hi")]).is_ok());
648        assert!(registry
649            .execute(
650                "substring",
651                &[Value::string("hi"), Value::Int(0), Value::Int(1)]
652            )
653            .is_ok());
654        // One sample per category: too few and too many arguments.
655        let cases: &[(&str, Vec<Value>, Vec<Value>)] = &[
656            ("length", vec![], vec![Value::Int(1), Value::Int(2)]),
657            ("size", vec![], vec![Value::Int(1), Value::Int(2)]),
658            (
659                "upper",
660                vec![],
661                vec![Value::string("a"), Value::string("b")],
662            ),
663            (
664                "substring",
665                vec![Value::string("a")],
666                vec![
667                    Value::string("a"),
668                    Value::Int(0),
669                    Value::Int(1),
670                    Value::Int(2),
671                ],
672            ),
673            ("abs", vec![], vec![Value::Int(1), Value::Int(2)]),
674            ("pow", vec![Value::Int(1)], vec![Value::Int(1); 3]),
675            ("head", vec![], vec![Value::Int(1), Value::Int(2)]),
676            ("id", vec![], vec![Value::Int(1), Value::Int(2)]),
677            (
678                "row_number",
679                vec![Value::Int(1)],
680                vec![Value::Int(1), Value::Int(2)],
681            ),
682            (
683                "regex_match",
684                vec![Value::string("a")],
685                vec![Value::string("a"); 3],
686            ),
687            (
688                "cosine_similarity",
689                vec![Value::string("a")],
690                vec![Value::string("a"); 3],
691            ),
692            ("to_string", vec![], vec![Value::Int(1), Value::Int(2)]),
693            ("year", vec![], vec![Value::Int(1), Value::Int(2)]),
694            (
695                "octet_length",
696                vec![],
697                vec![Value::string("a"), Value::string("b")],
698            ),
699            (
700                "curr_val",
701                vec![],
702                vec![Value::string("a"), Value::string("b")],
703            ),
704            ("lead", vec![Value::Int(1)], vec![Value::Int(1); 3]),
705        ];
706        for (name, too_few, too_many) in cases {
707            for args in [too_few, too_many] {
708                let err = registry.execute(name, args).unwrap_err();
709                assert_eq!(
710                    err.error_type,
711                    ExpressionErrorType::InvalidArgumentCount,
712                    "`{name}` with wrong arity must report InvalidArgumentCount"
713                );
714                assert!(
715                    err.message.contains(*name),
716                    "`{name}` arity error must name the function"
717                );
718            }
719        }
720    }
721
722    /// The unified arity error carries the function name and both counts.
723    #[test]
724    fn test_central_arity_error_format() {
725        let registry = global_registry();
726        let err = registry.execute("length", &[]).unwrap_err();
727        assert_eq!(err.error_type, ExpressionErrorType::InvalidArgumentCount);
728        assert_eq!(
729            err.message,
730            "function 'length' expects 1 argument(s), got 0"
731        );
732        let err = registry
733            .execute("substring", &[Value::string("a"), Value::Int(0)])
734            .unwrap_err();
735        assert_eq!(err.error_type, ExpressionErrorType::InvalidArgumentCount);
736        assert!(err.message.contains("'substring'"));
737        assert!(err.message.contains("3"));
738        assert!(err.message.contains("2"));
739    }
740
741    /// Variadic functions skip central validation; their own range checks stay.
742    #[test]
743    fn test_central_arity_variadic_boundaries() {
744        let registry = global_registry();
745        // Zero-argument calls that handlers accept.
746        assert!(registry.execute("concat", &[]).is_ok());
747        assert!(registry.execute("coalesce", &[]).is_ok());
748        assert!(registry.execute("date", &[]).is_ok());
749        assert!(registry.execute("rand32", &[]).is_ok());
750        assert!(registry
751            .execute("rand32", &[Value::Int(1), Value::Int(5)])
752            .is_ok());
753        // Handler-level range checks still fire.
754        assert!(registry.execute("range", &[Value::Int(1)]).is_err());
755        assert!(registry
756            .execute("concat_ws", &[Value::string(",")])
757            .is_err());
758        assert!(registry.execute("format", &[Value::string("{0}")]).is_err());
759        assert!(registry.execute("list_concat", &[Value::Int(1)]).is_err());
760        assert!(registry
761            .execute("struct_pack", &[Value::string("k")])
762            .is_err());
763        assert!(registry
764            .execute("rand32", &[Value::Int(1), Value::Int(2), Value::Int(3)])
765            .is_err());
766        assert!(registry
767            .execute(
768                "range",
769                &[Value::Int(1), Value::Int(5), Value::Int(1), Value::Int(2)]
770            )
771            .is_err());
772    }
773
774    /// Arity errors take precedence over context errors for aggregate and
775    /// fulltext functions.
776    #[test]
777    fn test_central_arity_before_context_interception() {
778        let count = BuiltinFunction::Aggregate(AggregateFunction::Count);
779        let err = count.execute(&[]).unwrap_err();
780        assert_eq!(err.error_type, ExpressionErrorType::InvalidArgumentCount);
781        let err = count.execute(&[Value::Int(1)]).unwrap_err();
782        assert_eq!(err.error_type, ExpressionErrorType::InvalidOperation);
783
784        let score = BuiltinFunction::Fulltext(FulltextFunction::Score);
785        let err = score.execute(&[Value::Int(1)]).unwrap_err();
786        assert_eq!(err.error_type, ExpressionErrorType::InvalidArgumentCount);
787        let err = score.execute(&[]).unwrap_err();
788        assert_eq!(err.error_type, ExpressionErrorType::InvalidOperation);
789    }
790}