Skip to main content

alopex_sql/scalar/
mod.rs

1//! Shared scalar-function signatures.
2//!
3//! This module is the single source of truth for scalar names, arity, type
4//! contracts, return rules, and optimizer metadata. Evaluation functions are
5//! attached by `executor::evaluator::registry`.
6//!
7//! The v0.7.4 catalog includes the v0.5.3 scalar set, v0.5.1 hash/encoding
8//! functions, and v0.5.2 system functions. `memory_stats`, `io_stats`, and
9//! `clear_cache` are resolved by the executor because they require store
10//! access; their signatures still participate in planner validation here.
11
12use crate::PlannerError;
13use crate::ast::expr::Literal;
14use crate::ast::span::Span;
15use crate::planner::typed_expr::{TypedExpr, TypedExprKind};
16use crate::planner::types::ResolvedType;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Arity {
20    Exact(usize),
21    Range(usize, usize),
22    Variadic(usize),
23}
24
25impl Arity {
26    pub fn accepts(self, count: usize) -> bool {
27        match self {
28            Self::Exact(n) => count == n,
29            Self::Range(min, max) => (min..=max).contains(&count),
30            Self::Variadic(min) => count >= min,
31        }
32    }
33
34    pub fn describe(self) -> String {
35        match self {
36            Self::Exact(n) => n.to_string(),
37            Self::Range(min, max) => format!("{min}..={max}"),
38            Self::Variadic(min) => format!("{min} or more"),
39        }
40    }
41
42    pub fn validate(self, name: &str, count: usize, _span: Span) -> Result<(), PlannerError> {
43        if self.accepts(count) {
44            Ok(())
45        } else {
46            Err(PlannerError::invalid_expression(format!(
47                "function '{name}' expects {} argument(s), got {count}",
48                self.describe()
49            )))
50        }
51    }
52}
53
54#[derive(Debug, Clone)]
55pub enum ReturnRule {
56    Fixed(ResolvedType),
57    FromArgs(fn(&[ResolvedType]) -> Result<ResolvedType, PlannerError>),
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct FnMeta {
62    pub deterministic: bool,
63    pub volatile: bool,
64    pub side_effecting: bool,
65    pub foldable: bool,
66    pub cacheable: bool,
67    pub reorderable: bool,
68}
69
70pub const PURE_META: FnMeta = FnMeta {
71    deterministic: true,
72    volatile: false,
73    side_effecting: false,
74    foldable: true,
75    cacheable: true,
76    reorderable: true,
77};
78
79pub const RANDOM_META: FnMeta = FnMeta {
80    deterministic: false,
81    volatile: true,
82    side_effecting: false,
83    foldable: false,
84    cacheable: false,
85    reorderable: true,
86};
87
88pub const SYSTEM_META: FnMeta = FnMeta {
89    deterministic: false,
90    volatile: true,
91    side_effecting: false,
92    foldable: false,
93    cacheable: false,
94    reorderable: false,
95};
96
97pub const SYSTEM_SIDE_EFFECT_META: FnMeta = FnMeta {
98    deterministic: false,
99    volatile: true,
100    side_effecting: true,
101    foldable: false,
102    cacheable: false,
103    reorderable: false,
104};
105
106#[derive(Debug, Clone)]
107pub struct ScalarSignature {
108    pub name: &'static str,
109    pub arity: Arity,
110    pub check: fn(&[TypedExpr]) -> Result<(), PlannerError>,
111    pub ret: ReturnRule,
112    pub meta: FnMeta,
113}
114
115fn is_numeric(ty: &ResolvedType) -> bool {
116    matches!(
117        ty,
118        ResolvedType::Integer
119            | ResolvedType::BigInt
120            | ResolvedType::Float
121            | ResolvedType::Double
122            | ResolvedType::Null
123    )
124}
125
126pub fn check_numeric(args: &[TypedExpr]) -> Result<(), PlannerError> {
127    for arg in args {
128        if !is_numeric(&arg.resolved_type) {
129            return Err(PlannerError::type_mismatch(
130                "Numeric",
131                arg.resolved_type.type_name(),
132                arg.span,
133            ));
134        }
135    }
136    Ok(())
137}
138
139pub fn check_text(args: &[TypedExpr]) -> Result<(), PlannerError> {
140    for arg in args {
141        if !matches!(arg.resolved_type, ResolvedType::Text | ResolvedType::Null) {
142            return Err(PlannerError::type_mismatch(
143                "Text",
144                arg.resolved_type.type_name(),
145                arg.span,
146            ));
147        }
148    }
149    Ok(())
150}
151
152pub fn check_text_or_blob(args: &[TypedExpr]) -> Result<(), PlannerError> {
153    for arg in args {
154        if !matches!(
155            arg.resolved_type,
156            ResolvedType::Text | ResolvedType::Blob | ResolvedType::Null
157        ) {
158            return Err(PlannerError::type_mismatch(
159                "Text or Blob",
160                arg.resolved_type.type_name(),
161                arg.span,
162            ));
163        }
164    }
165    Ok(())
166}
167
168pub fn check_bigint(args: &[TypedExpr]) -> Result<(), PlannerError> {
169    for arg in args {
170        if !matches!(arg.resolved_type, ResolvedType::BigInt | ResolvedType::Null) {
171            return Err(PlannerError::type_mismatch(
172                "BigInt",
173                arg.resolved_type.type_name(),
174                arg.span,
175            ));
176        }
177    }
178    Ok(())
179}
180
181pub fn check_blob_text(args: &[TypedExpr]) -> Result<(), PlannerError> {
182    if let Some(first) = args.first()
183        && !matches!(first.resolved_type, ResolvedType::Blob | ResolvedType::Null)
184    {
185        return Err(PlannerError::type_mismatch(
186            "Blob",
187            first.resolved_type.type_name(),
188            first.span,
189        ));
190    }
191    check_text(&args[1..])
192}
193
194pub fn check_any(args: &[TypedExpr]) -> Result<(), PlannerError> {
195    if args.is_empty() {
196        return Err(PlannerError::invalid_expression(
197            "at least one argument is required",
198        ));
199    }
200    Ok(())
201}
202
203pub fn check_no_args(_args: &[TypedExpr]) -> Result<(), PlannerError> {
204    Ok(())
205}
206
207fn check_vector_one(args: &[TypedExpr]) -> Result<(), PlannerError> {
208    match args.first().map(|arg| &arg.resolved_type) {
209        Some(ResolvedType::Vector { .. } | ResolvedType::Null) => Ok(()),
210        Some(ty) => Err(PlannerError::type_mismatch(
211            "Vector",
212            ty.type_name(),
213            args[0].span,
214        )),
215        None => Ok(()),
216    }
217}
218
219fn check_vector_triplet(args: &[TypedExpr]) -> Result<(), PlannerError> {
220    let first = match &args[0].resolved_type {
221        ResolvedType::Vector { dimension, .. } => *dimension,
222        ty => {
223            return Err(PlannerError::type_mismatch(
224                "Vector",
225                ty.type_name(),
226                args[0].span,
227            ));
228        }
229    };
230    let second = match &args[1].resolved_type {
231        ResolvedType::Vector { dimension, .. } => *dimension,
232        ty => {
233            return Err(PlannerError::type_mismatch(
234                "Vector",
235                ty.type_name(),
236                args[1].span,
237            ));
238        }
239    };
240    if first != second {
241        return Err(PlannerError::vector_dimension_mismatch(
242            first,
243            second,
244            args[1].span,
245        ));
246    }
247    match &args[2].resolved_type {
248        ResolvedType::Text => {
249            if let TypedExprKind::Literal(Literal::String(value)) = &args[2].kind
250                && !matches!(
251                    value.to_ascii_lowercase().as_str(),
252                    "cosine" | "l2" | "inner"
253                )
254            {
255                return Err(PlannerError::invalid_metric(value, args[2].span));
256            }
257            Ok(())
258        }
259        ty => Err(PlannerError::type_mismatch(
260            "Text (metric)",
261            ty.type_name(),
262            args[2].span,
263        )),
264    }
265}
266
267fn numeric_return(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
268    let mut result = ResolvedType::Null;
269    for ty in types {
270        if matches!(ty, ResolvedType::Null) {
271            continue;
272        }
273        if !is_numeric(ty) {
274            return Err(PlannerError::type_mismatch(
275                "Numeric",
276                ty.type_name(),
277                Span::default(),
278            ));
279        }
280        result = match (&result, ty) {
281            (ResolvedType::Null, _) => ty.clone(),
282            (ResolvedType::Double, _) | (_, ResolvedType::Double) => ResolvedType::Double,
283            (ResolvedType::Float, ResolvedType::BigInt)
284            | (ResolvedType::BigInt, ResolvedType::Float) => ResolvedType::Double,
285            (ResolvedType::Float, _) | (_, ResolvedType::Float) => ResolvedType::Float,
286            (ResolvedType::BigInt, _) | (_, ResolvedType::BigInt) => ResolvedType::BigInt,
287            _ => ResolvedType::Integer,
288        };
289    }
290    Ok(result)
291}
292
293fn return_arg0(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
294    Ok(types.first().cloned().unwrap_or(ResolvedType::Null))
295}
296
297fn return_first_non_null(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
298    Ok(types
299        .iter()
300        .find(|ty| !matches!(ty, ResolvedType::Null))
301        .cloned()
302        .unwrap_or(ResolvedType::Null))
303}
304
305fn return_numeric(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
306    numeric_return(types)
307}
308
309fn return_arg0_numeric(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
310    return_arg0(types)
311}
312
313const fn sig(
314    name: &'static str,
315    arity: Arity,
316    check: fn(&[TypedExpr]) -> Result<(), PlannerError>,
317    ret: ReturnRule,
318) -> ScalarSignature {
319    ScalarSignature {
320        name,
321        arity,
322        check,
323        ret,
324        meta: PURE_META,
325    }
326}
327
328const fn sig_meta(
329    name: &'static str,
330    arity: Arity,
331    check: fn(&[TypedExpr]) -> Result<(), PlannerError>,
332    ret: ReturnRule,
333    meta: FnMeta,
334) -> ScalarSignature {
335    ScalarSignature {
336        name,
337        arity,
338        check,
339        ret,
340        meta,
341    }
342}
343
344static SIGNATURES: &[ScalarSignature] = &[
345    sig(
346        "vector_similarity",
347        Arity::Exact(3),
348        check_vector_triplet,
349        ReturnRule::Fixed(ResolvedType::Double),
350    ),
351    sig(
352        "vector_distance",
353        Arity::Exact(3),
354        check_vector_triplet,
355        ReturnRule::Fixed(ResolvedType::Double),
356    ),
357    sig(
358        "vector_dims",
359        Arity::Exact(1),
360        check_vector_one,
361        ReturnRule::Fixed(ResolvedType::Integer),
362    ),
363    sig(
364        "vector_norm",
365        Arity::Exact(1),
366        check_vector_one,
367        ReturnRule::Fixed(ResolvedType::Double),
368    ),
369    sig(
370        "abs",
371        Arity::Exact(1),
372        check_numeric,
373        ReturnRule::FromArgs(return_arg0_numeric),
374    ),
375    sig(
376        "sign",
377        Arity::Exact(1),
378        check_numeric,
379        ReturnRule::Fixed(ResolvedType::Integer),
380    ),
381    sig(
382        "round",
383        Arity::Range(1, 2),
384        check_numeric,
385        ReturnRule::FromArgs(return_arg0_numeric),
386    ),
387    sig(
388        "floor",
389        Arity::Exact(1),
390        check_numeric,
391        ReturnRule::FromArgs(return_arg0_numeric),
392    ),
393    sig(
394        "ceil",
395        Arity::Exact(1),
396        check_numeric,
397        ReturnRule::FromArgs(return_arg0_numeric),
398    ),
399    sig(
400        "ceiling",
401        Arity::Exact(1),
402        check_numeric,
403        ReturnRule::FromArgs(return_arg0_numeric),
404    ),
405    sig(
406        "trunc",
407        Arity::Range(1, 2),
408        check_numeric,
409        ReturnRule::FromArgs(return_arg0_numeric),
410    ),
411    sig(
412        "mod",
413        Arity::Exact(2),
414        check_numeric,
415        ReturnRule::FromArgs(return_numeric),
416    ),
417    sig(
418        "power",
419        Arity::Exact(2),
420        check_numeric,
421        ReturnRule::Fixed(ResolvedType::Double),
422    ),
423    sig(
424        "pow",
425        Arity::Exact(2),
426        check_numeric,
427        ReturnRule::Fixed(ResolvedType::Double),
428    ),
429    sig(
430        "sqrt",
431        Arity::Exact(1),
432        check_numeric,
433        ReturnRule::Fixed(ResolvedType::Double),
434    ),
435    sig(
436        "exp",
437        Arity::Exact(1),
438        check_numeric,
439        ReturnRule::Fixed(ResolvedType::Double),
440    ),
441    sig(
442        "ln",
443        Arity::Exact(1),
444        check_numeric,
445        ReturnRule::Fixed(ResolvedType::Double),
446    ),
447    sig(
448        "log",
449        Arity::Range(1, 2),
450        check_numeric,
451        ReturnRule::Fixed(ResolvedType::Double),
452    ),
453    sig(
454        "log10",
455        Arity::Exact(1),
456        check_numeric,
457        ReturnRule::Fixed(ResolvedType::Double),
458    ),
459    sig_meta(
460        "random",
461        Arity::Exact(0),
462        check_numeric,
463        ReturnRule::Fixed(ResolvedType::Double),
464        RANDOM_META,
465    ),
466    sig(
467        "sin",
468        Arity::Exact(1),
469        check_numeric,
470        ReturnRule::Fixed(ResolvedType::Double),
471    ),
472    sig(
473        "cos",
474        Arity::Exact(1),
475        check_numeric,
476        ReturnRule::Fixed(ResolvedType::Double),
477    ),
478    sig(
479        "tan",
480        Arity::Exact(1),
481        check_numeric,
482        ReturnRule::Fixed(ResolvedType::Double),
483    ),
484    sig(
485        "asin",
486        Arity::Exact(1),
487        check_numeric,
488        ReturnRule::Fixed(ResolvedType::Double),
489    ),
490    sig(
491        "acos",
492        Arity::Exact(1),
493        check_numeric,
494        ReturnRule::Fixed(ResolvedType::Double),
495    ),
496    sig(
497        "atan",
498        Arity::Exact(1),
499        check_numeric,
500        ReturnRule::Fixed(ResolvedType::Double),
501    ),
502    sig(
503        "atan2",
504        Arity::Exact(2),
505        check_numeric,
506        ReturnRule::Fixed(ResolvedType::Double),
507    ),
508    sig(
509        "degrees",
510        Arity::Exact(1),
511        check_numeric,
512        ReturnRule::Fixed(ResolvedType::Double),
513    ),
514    sig(
515        "radians",
516        Arity::Exact(1),
517        check_numeric,
518        ReturnRule::Fixed(ResolvedType::Double),
519    ),
520    sig(
521        "pi",
522        Arity::Exact(0),
523        check_numeric,
524        ReturnRule::Fixed(ResolvedType::Double),
525    ),
526    sig(
527        "sha256",
528        Arity::Exact(1),
529        check_text_or_blob,
530        ReturnRule::Fixed(ResolvedType::Blob),
531    ),
532    sig(
533        "md5",
534        Arity::Exact(1),
535        check_text_or_blob,
536        ReturnRule::Fixed(ResolvedType::Text),
537    ),
538    sig(
539        "simhash",
540        Arity::Exact(1),
541        check_text,
542        ReturnRule::Fixed(ResolvedType::BigInt),
543    ),
544    sig(
545        "hamming_distance",
546        Arity::Exact(2),
547        check_bigint,
548        ReturnRule::Fixed(ResolvedType::Integer),
549    ),
550    sig_meta(
551        "gen_random_uuid",
552        Arity::Exact(0),
553        check_no_args,
554        ReturnRule::Fixed(ResolvedType::Text),
555        RANDOM_META,
556    ),
557    sig_meta(
558        "uuidv7",
559        Arity::Exact(0),
560        check_no_args,
561        ReturnRule::Fixed(ResolvedType::Text),
562        RANDOM_META,
563    ),
564    sig(
565        "hex",
566        Arity::Exact(1),
567        check_text_or_blob,
568        ReturnRule::Fixed(ResolvedType::Text),
569    ),
570    sig(
571        "unhex",
572        Arity::Exact(1),
573        check_text,
574        ReturnRule::Fixed(ResolvedType::Blob),
575    ),
576    sig(
577        "encode",
578        Arity::Exact(2),
579        check_blob_text,
580        ReturnRule::Fixed(ResolvedType::Text),
581    ),
582    sig(
583        "decode",
584        Arity::Exact(2),
585        check_text,
586        ReturnRule::Fixed(ResolvedType::Blob),
587    ),
588    sig(
589        "length",
590        Arity::Exact(1),
591        check_text_or_blob,
592        ReturnRule::Fixed(ResolvedType::Integer),
593    ),
594    sig(
595        "char_length",
596        Arity::Exact(1),
597        check_text,
598        ReturnRule::Fixed(ResolvedType::Integer),
599    ),
600    sig(
601        "octet_length",
602        Arity::Exact(1),
603        check_text_or_blob,
604        ReturnRule::Fixed(ResolvedType::Integer),
605    ),
606    sig(
607        "upper",
608        Arity::Exact(1),
609        check_text,
610        ReturnRule::Fixed(ResolvedType::Text),
611    ),
612    sig(
613        "lower",
614        Arity::Exact(1),
615        check_text,
616        ReturnRule::Fixed(ResolvedType::Text),
617    ),
618    sig(
619        "initcap",
620        Arity::Exact(1),
621        check_text,
622        ReturnRule::Fixed(ResolvedType::Text),
623    ),
624    sig(
625        "substr",
626        Arity::Range(2, 3),
627        check_numeric_or_text_substr,
628        ReturnRule::Fixed(ResolvedType::Text),
629    ),
630    sig(
631        "left",
632        Arity::Exact(2),
633        check_text_numeric,
634        ReturnRule::Fixed(ResolvedType::Text),
635    ),
636    sig(
637        "right",
638        Arity::Exact(2),
639        check_text_numeric,
640        ReturnRule::Fixed(ResolvedType::Text),
641    ),
642    sig(
643        "trim",
644        Arity::Range(1, 2),
645        check_text,
646        ReturnRule::Fixed(ResolvedType::Text),
647    ),
648    sig(
649        "ltrim",
650        Arity::Range(1, 2),
651        check_text,
652        ReturnRule::Fixed(ResolvedType::Text),
653    ),
654    sig(
655        "rtrim",
656        Arity::Range(1, 2),
657        check_text,
658        ReturnRule::Fixed(ResolvedType::Text),
659    ),
660    sig(
661        "replace",
662        Arity::Exact(3),
663        check_text,
664        ReturnRule::Fixed(ResolvedType::Text),
665    ),
666    sig(
667        "instr",
668        Arity::Exact(2),
669        check_text,
670        ReturnRule::Fixed(ResolvedType::Integer),
671    ),
672    sig(
673        "strpos",
674        Arity::Exact(2),
675        check_text,
676        ReturnRule::Fixed(ResolvedType::Integer),
677    ),
678    sig(
679        "concat",
680        Arity::Variadic(0),
681        check_text,
682        ReturnRule::Fixed(ResolvedType::Text),
683    ),
684    sig(
685        "concat_ws",
686        Arity::Variadic(1),
687        check_text,
688        ReturnRule::Fixed(ResolvedType::Text),
689    ),
690    sig(
691        "repeat",
692        Arity::Exact(2),
693        check_text_numeric,
694        ReturnRule::Fixed(ResolvedType::Text),
695    ),
696    sig(
697        "reverse",
698        Arity::Exact(1),
699        check_text,
700        ReturnRule::Fixed(ResolvedType::Text),
701    ),
702    sig(
703        "lpad",
704        Arity::Range(2, 3),
705        check_pad,
706        ReturnRule::Fixed(ResolvedType::Text),
707    ),
708    sig(
709        "rpad",
710        Arity::Range(2, 3),
711        check_pad,
712        ReturnRule::Fixed(ResolvedType::Text),
713    ),
714    sig(
715        "split_part",
716        Arity::Exact(3),
717        check_text_text_numeric,
718        ReturnRule::Fixed(ResolvedType::Text),
719    ),
720    sig(
721        "regexp_replace",
722        Arity::Exact(3),
723        check_text,
724        ReturnRule::Fixed(ResolvedType::Text),
725    ),
726    sig(
727        "regexp_match",
728        Arity::Exact(2),
729        check_text,
730        ReturnRule::Fixed(ResolvedType::Text),
731    ),
732    sig(
733        "regexp_matches",
734        Arity::Range(2, 3),
735        check_text,
736        ReturnRule::Fixed(ResolvedType::Text),
737    ),
738    sig(
739        "coalesce",
740        Arity::Variadic(1),
741        check_compatible,
742        ReturnRule::FromArgs(return_first_non_null),
743    ),
744    sig(
745        "nullif",
746        Arity::Exact(2),
747        check_compatible,
748        ReturnRule::FromArgs(return_first_non_null),
749    ),
750    sig(
751        "ifnull",
752        Arity::Exact(2),
753        check_compatible,
754        ReturnRule::FromArgs(return_first_non_null),
755    ),
756    sig(
757        "iif",
758        Arity::Exact(3),
759        check_iif,
760        ReturnRule::FromArgs(return_first_non_null),
761    ),
762    sig(
763        "greatest",
764        Arity::Variadic(1),
765        check_numeric,
766        ReturnRule::FromArgs(return_numeric),
767    ),
768    sig(
769        "least",
770        Arity::Variadic(1),
771        check_numeric,
772        ReturnRule::FromArgs(return_numeric),
773    ),
774    sig(
775        "typeof",
776        Arity::Exact(1),
777        check_any,
778        ReturnRule::Fixed(ResolvedType::Text),
779    ),
780    sig(
781        "pg_typeof",
782        Arity::Exact(1),
783        check_any,
784        ReturnRule::Fixed(ResolvedType::Text),
785    ),
786    sig(
787        "quote",
788        Arity::Exact(1),
789        check_any,
790        ReturnRule::Fixed(ResolvedType::Text),
791    ),
792    sig_meta(
793        "memory_stats",
794        Arity::Exact(0),
795        check_no_args,
796        ReturnRule::Fixed(ResolvedType::Text),
797        SYSTEM_META,
798    ),
799    sig_meta(
800        "io_stats",
801        Arity::Exact(0),
802        check_no_args,
803        ReturnRule::Fixed(ResolvedType::Text),
804        SYSTEM_META,
805    ),
806    sig_meta(
807        "clear_cache",
808        Arity::Exact(0),
809        check_no_args,
810        ReturnRule::Fixed(ResolvedType::BigInt),
811        SYSTEM_SIDE_EFFECT_META,
812    ),
813];
814
815fn check_numeric_or_text_substr(args: &[TypedExpr]) -> Result<(), PlannerError> {
816    if args.is_empty() {
817        return Ok(());
818    }
819    check_text(&args[..1])?;
820    check_numeric(&args[1..])
821}
822
823fn check_text_numeric(args: &[TypedExpr]) -> Result<(), PlannerError> {
824    if !args.is_empty() {
825        check_text(&args[..1])?;
826    }
827    if args.len() >= 2 {
828        check_numeric(&args[1..2])?;
829    }
830    Ok(())
831}
832
833fn check_pad(args: &[TypedExpr]) -> Result<(), PlannerError> {
834    check_text_numeric(&args[..2])?;
835    if args.len() == 3 {
836        check_text(&args[2..3])?;
837    }
838    Ok(())
839}
840
841fn check_text_text_numeric(args: &[TypedExpr]) -> Result<(), PlannerError> {
842    check_text(&args[..2])?;
843    check_numeric(&args[2..3])
844}
845
846fn check_compatible(args: &[TypedExpr]) -> Result<(), PlannerError> {
847    let mut expected: Option<&ResolvedType> = None;
848    for arg in args {
849        if matches!(arg.resolved_type, ResolvedType::Null) {
850            continue;
851        }
852        if let Some(first) = expected {
853            if !(is_numeric(first) && is_numeric(&arg.resolved_type)) && first != &arg.resolved_type
854            {
855                return Err(PlannerError::type_mismatch(
856                    first.type_name(),
857                    arg.resolved_type.type_name(),
858                    arg.span,
859                ));
860            }
861        } else {
862            expected = Some(&arg.resolved_type);
863        }
864    }
865    Ok(())
866}
867
868fn check_iif(args: &[TypedExpr]) -> Result<(), PlannerError> {
869    if !matches!(
870        args.first().map(|a| &a.resolved_type),
871        Some(ResolvedType::Boolean | ResolvedType::Null)
872    ) {
873        let arg = args
874            .first()
875            .expect("arity is validated before type checking");
876        return Err(PlannerError::type_mismatch(
877            "Boolean",
878            arg.resolved_type.type_name(),
879            arg.span,
880        ));
881    }
882    check_compatible(&args[1..])
883}
884
885pub fn signatures() -> &'static [ScalarSignature] {
886    SIGNATURES
887}
888
889pub fn signature(name: &str) -> Option<&'static ScalarSignature> {
890    let lower = name.to_ascii_lowercase();
891    SIGNATURES.iter().find(|sig| sig.name == lower)
892}
893
894pub fn is_numeric_type(ty: &ResolvedType) -> bool {
895    is_numeric(ty)
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901
902    #[test]
903    fn names_are_normalized_and_unique() {
904        let mut names: Vec<_> = signatures().iter().map(|s| s.name).collect();
905        names.sort_unstable();
906        names.dedup();
907        assert_eq!(names.len(), signatures().len());
908        assert!(
909            signatures()
910                .iter()
911                .all(|s| s.name == s.name.to_ascii_lowercase())
912        );
913    }
914
915    #[test]
916    fn random_metadata_is_volatile() {
917        let random = signature("RANDOM").unwrap();
918        assert!(!random.meta.deterministic);
919        assert!(random.meta.volatile);
920        assert!(!random.meta.foldable);
921        assert!(!random.meta.cacheable);
922    }
923
924    #[test]
925    fn scalar_names_do_not_overlap_aggregate_names() {
926        let aggregates = [
927            "count",
928            "sum",
929            "total",
930            "avg",
931            "min",
932            "max",
933            "group_concat",
934            "string_agg",
935        ];
936        assert!(aggregates.iter().all(|name| signature(name).is_none()));
937    }
938}