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