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
178fn check_timestamp(args: &[TypedExpr]) -> Result<(), PlannerError> {
179    for arg in args {
180        if !matches!(
181            arg.resolved_type,
182            ResolvedType::Timestamp | ResolvedType::Null
183        ) {
184            return Err(PlannerError::type_mismatch(
185                "Timestamp",
186                arg.resolved_type.type_name(),
187                arg.span,
188            ));
189        }
190    }
191    Ok(())
192}
193
194fn check_text_timestamp(args: &[TypedExpr]) -> Result<(), PlannerError> {
195    check_text(&args[..1])?;
196    check_timestamp(&args[1..])
197}
198
199fn check_timestamp_text(args: &[TypedExpr]) -> Result<(), PlannerError> {
200    check_timestamp(&args[..1])?;
201    check_text(&args[1..])
202}
203
204fn check_to_timestamp(args: &[TypedExpr]) -> Result<(), PlannerError> {
205    if args.len() == 1 {
206        let arg = &args[0];
207        if is_numeric(&arg.resolved_type)
208            || matches!(arg.resolved_type, ResolvedType::Text | ResolvedType::Null)
209        {
210            return Ok(());
211        }
212        return Err(PlannerError::type_mismatch(
213            "Numeric or Text",
214            arg.resolved_type.type_name(),
215            arg.span,
216        ));
217    }
218    check_text(args)
219}
220
221pub fn check_bigint(args: &[TypedExpr]) -> Result<(), PlannerError> {
222    for arg in args {
223        if !matches!(arg.resolved_type, ResolvedType::BigInt | ResolvedType::Null) {
224            return Err(PlannerError::type_mismatch(
225                "BigInt",
226                arg.resolved_type.type_name(),
227                arg.span,
228            ));
229        }
230    }
231    Ok(())
232}
233
234fn check_integer(args: &[TypedExpr]) -> Result<(), PlannerError> {
235    for arg in args {
236        if !matches!(
237            arg.resolved_type,
238            ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
239        ) {
240            return Err(PlannerError::type_mismatch(
241                "Integer",
242                arg.resolved_type.type_name(),
243                arg.span,
244            ));
245        }
246    }
247    Ok(())
248}
249
250pub fn check_blob_text(args: &[TypedExpr]) -> Result<(), PlannerError> {
251    if let Some(first) = args.first()
252        && !matches!(first.resolved_type, ResolvedType::Blob | ResolvedType::Null)
253    {
254        return Err(PlannerError::type_mismatch(
255            "Blob",
256            first.resolved_type.type_name(),
257            first.span,
258        ));
259    }
260    check_text(&args[1..])
261}
262
263pub fn check_any(args: &[TypedExpr]) -> Result<(), PlannerError> {
264    if args.is_empty() {
265        return Err(PlannerError::invalid_expression(
266            "at least one argument is required",
267        ));
268    }
269    Ok(())
270}
271
272pub fn check_no_args(_args: &[TypedExpr]) -> Result<(), PlannerError> {
273    Ok(())
274}
275
276fn check_vector_one(args: &[TypedExpr]) -> Result<(), PlannerError> {
277    match args.first().map(|arg| &arg.resolved_type) {
278        Some(ResolvedType::Vector { .. } | ResolvedType::Null) => Ok(()),
279        Some(ty) => Err(PlannerError::type_mismatch(
280            "Vector",
281            ty.type_name(),
282            args[0].span,
283        )),
284        None => Ok(()),
285    }
286}
287
288fn check_vector_triplet(args: &[TypedExpr]) -> Result<(), PlannerError> {
289    let first = match &args[0].resolved_type {
290        ResolvedType::Vector { dimension, .. } => *dimension,
291        ty => {
292            return Err(PlannerError::type_mismatch(
293                "Vector",
294                ty.type_name(),
295                args[0].span,
296            ));
297        }
298    };
299    let second = match &args[1].resolved_type {
300        ResolvedType::Vector { dimension, .. } => *dimension,
301        ty => {
302            return Err(PlannerError::type_mismatch(
303                "Vector",
304                ty.type_name(),
305                args[1].span,
306            ));
307        }
308    };
309    if first != second {
310        return Err(PlannerError::vector_dimension_mismatch(
311            first,
312            second,
313            args[1].span,
314        ));
315    }
316    match &args[2].resolved_type {
317        ResolvedType::Text => {
318            if let TypedExprKind::Literal(Literal::String(value)) = &args[2].kind
319                && !matches!(
320                    value.to_ascii_lowercase().as_str(),
321                    "cosine" | "l2" | "inner"
322                )
323            {
324                return Err(PlannerError::invalid_metric(value, args[2].span));
325            }
326            Ok(())
327        }
328        ty => Err(PlannerError::type_mismatch(
329            "Text (metric)",
330            ty.type_name(),
331            args[2].span,
332        )),
333    }
334}
335
336fn numeric_return(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
337    let mut result = ResolvedType::Null;
338    for ty in types {
339        if matches!(ty, ResolvedType::Null) {
340            continue;
341        }
342        if !is_numeric(ty) {
343            return Err(PlannerError::type_mismatch(
344                "Numeric",
345                ty.type_name(),
346                Span::default(),
347            ));
348        }
349        result = match (&result, ty) {
350            (ResolvedType::Null, _) => ty.clone(),
351            (ResolvedType::Double, _) | (_, ResolvedType::Double) => ResolvedType::Double,
352            (ResolvedType::Float, ResolvedType::BigInt)
353            | (ResolvedType::BigInt, ResolvedType::Float) => ResolvedType::Double,
354            (ResolvedType::Float, _) | (_, ResolvedType::Float) => ResolvedType::Float,
355            (ResolvedType::BigInt, _) | (_, ResolvedType::BigInt) => ResolvedType::BigInt,
356            _ => ResolvedType::Integer,
357        };
358    }
359    Ok(result)
360}
361
362fn return_arg0(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
363    Ok(types.first().cloned().unwrap_or(ResolvedType::Null))
364}
365
366fn return_first_non_null(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
367    Ok(types
368        .iter()
369        .find(|ty| !matches!(ty, ResolvedType::Null))
370        .cloned()
371        .unwrap_or(ResolvedType::Null))
372}
373
374fn return_numeric(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
375    numeric_return(types)
376}
377
378fn return_arg0_numeric(types: &[ResolvedType]) -> Result<ResolvedType, PlannerError> {
379    return_arg0(types)
380}
381
382const fn sig(
383    name: &'static str,
384    arity: Arity,
385    check: fn(&[TypedExpr]) -> Result<(), PlannerError>,
386    ret: ReturnRule,
387) -> ScalarSignature {
388    ScalarSignature {
389        name,
390        arity,
391        check,
392        ret,
393        meta: PURE_META,
394    }
395}
396
397const fn sig_meta(
398    name: &'static str,
399    arity: Arity,
400    check: fn(&[TypedExpr]) -> Result<(), PlannerError>,
401    ret: ReturnRule,
402    meta: FnMeta,
403) -> ScalarSignature {
404    ScalarSignature {
405        name,
406        arity,
407        check,
408        ret,
409        meta,
410    }
411}
412
413static SIGNATURES: &[ScalarSignature] = &[
414    sig(
415        "vector_similarity",
416        Arity::Exact(3),
417        check_vector_triplet,
418        ReturnRule::Fixed(ResolvedType::Double),
419    ),
420    sig(
421        "vector_distance",
422        Arity::Exact(3),
423        check_vector_triplet,
424        ReturnRule::Fixed(ResolvedType::Double),
425    ),
426    sig(
427        "vector_dims",
428        Arity::Exact(1),
429        check_vector_one,
430        ReturnRule::Fixed(ResolvedType::Integer),
431    ),
432    sig(
433        "vector_norm",
434        Arity::Exact(1),
435        check_vector_one,
436        ReturnRule::Fixed(ResolvedType::Double),
437    ),
438    sig(
439        "abs",
440        Arity::Exact(1),
441        check_numeric,
442        ReturnRule::FromArgs(return_arg0_numeric),
443    ),
444    sig(
445        "sign",
446        Arity::Exact(1),
447        check_numeric,
448        ReturnRule::Fixed(ResolvedType::Integer),
449    ),
450    sig(
451        "round",
452        Arity::Range(1, 2),
453        check_numeric,
454        ReturnRule::FromArgs(return_arg0_numeric),
455    ),
456    sig(
457        "floor",
458        Arity::Exact(1),
459        check_numeric,
460        ReturnRule::FromArgs(return_arg0_numeric),
461    ),
462    sig(
463        "ceil",
464        Arity::Exact(1),
465        check_numeric,
466        ReturnRule::FromArgs(return_arg0_numeric),
467    ),
468    sig(
469        "ceiling",
470        Arity::Exact(1),
471        check_numeric,
472        ReturnRule::FromArgs(return_arg0_numeric),
473    ),
474    sig(
475        "trunc",
476        Arity::Range(1, 2),
477        check_numeric,
478        ReturnRule::FromArgs(return_arg0_numeric),
479    ),
480    sig(
481        "mod",
482        Arity::Exact(2),
483        check_numeric,
484        ReturnRule::FromArgs(return_numeric),
485    ),
486    sig(
487        "power",
488        Arity::Exact(2),
489        check_numeric,
490        ReturnRule::Fixed(ResolvedType::Double),
491    ),
492    sig(
493        "pow",
494        Arity::Exact(2),
495        check_numeric,
496        ReturnRule::Fixed(ResolvedType::Double),
497    ),
498    sig(
499        "sqrt",
500        Arity::Exact(1),
501        check_numeric,
502        ReturnRule::Fixed(ResolvedType::Double),
503    ),
504    sig(
505        "exp",
506        Arity::Exact(1),
507        check_numeric,
508        ReturnRule::Fixed(ResolvedType::Double),
509    ),
510    sig(
511        "ln",
512        Arity::Exact(1),
513        check_numeric,
514        ReturnRule::Fixed(ResolvedType::Double),
515    ),
516    sig(
517        "log",
518        Arity::Range(1, 2),
519        check_numeric,
520        ReturnRule::Fixed(ResolvedType::Double),
521    ),
522    sig(
523        "log10",
524        Arity::Exact(1),
525        check_numeric,
526        ReturnRule::Fixed(ResolvedType::Double),
527    ),
528    sig(
529        "cbrt",
530        Arity::Exact(1),
531        check_numeric,
532        ReturnRule::Fixed(ResolvedType::Double),
533    ),
534    sig(
535        "cot",
536        Arity::Exact(1),
537        check_numeric,
538        ReturnRule::Fixed(ResolvedType::Double),
539    ),
540    sig(
541        "log2",
542        Arity::Exact(1),
543        check_numeric,
544        ReturnRule::Fixed(ResolvedType::Double),
545    ),
546    sig(
547        "acosh",
548        Arity::Exact(1),
549        check_numeric,
550        ReturnRule::Fixed(ResolvedType::Double),
551    ),
552    sig(
553        "asinh",
554        Arity::Exact(1),
555        check_numeric,
556        ReturnRule::Fixed(ResolvedType::Double),
557    ),
558    sig(
559        "atanh",
560        Arity::Exact(1),
561        check_numeric,
562        ReturnRule::Fixed(ResolvedType::Double),
563    ),
564    sig(
565        "cosh",
566        Arity::Exact(1),
567        check_numeric,
568        ReturnRule::Fixed(ResolvedType::Double),
569    ),
570    sig(
571        "sinh",
572        Arity::Exact(1),
573        check_numeric,
574        ReturnRule::Fixed(ResolvedType::Double),
575    ),
576    sig(
577        "tanh",
578        Arity::Exact(1),
579        check_numeric,
580        ReturnRule::Fixed(ResolvedType::Double),
581    ),
582    sig(
583        "isnan",
584        Arity::Exact(1),
585        check_numeric,
586        ReturnRule::Fixed(ResolvedType::Boolean),
587    ),
588    sig_meta(
589        "random",
590        Arity::Exact(0),
591        check_numeric,
592        ReturnRule::Fixed(ResolvedType::Double),
593        RANDOM_META,
594    ),
595    sig_meta(
596        "now",
597        Arity::Range(0, 1),
598        check_numeric,
599        ReturnRule::Fixed(ResolvedType::Timestamp),
600        STATEMENT_STABLE_META,
601    ),
602    sig_meta(
603        "current_timestamp",
604        Arity::Range(0, 1),
605        check_numeric,
606        ReturnRule::Fixed(ResolvedType::Timestamp),
607        STATEMENT_STABLE_META,
608    ),
609    sig(
610        "extract",
611        Arity::Exact(2),
612        check_text_timestamp,
613        ReturnRule::Fixed(ResolvedType::Double),
614    ),
615    sig(
616        "date_part",
617        Arity::Exact(2),
618        check_text_timestamp,
619        ReturnRule::Fixed(ResolvedType::Double),
620    ),
621    sig(
622        "date_trunc",
623        Arity::Exact(2),
624        check_text_timestamp,
625        ReturnRule::Fixed(ResolvedType::Timestamp),
626    ),
627    sig(
628        "to_char",
629        Arity::Exact(2),
630        check_timestamp_text,
631        ReturnRule::Fixed(ResolvedType::Text),
632    ),
633    sig(
634        "to_timestamp",
635        Arity::Range(1, 2),
636        check_to_timestamp,
637        ReturnRule::Fixed(ResolvedType::Timestamp),
638    ),
639    sig(
640        "strftime",
641        Arity::Exact(2),
642        check_text_timestamp,
643        ReturnRule::Fixed(ResolvedType::Text),
644    ),
645    sig(
646        "julianday",
647        Arity::Exact(1),
648        check_timestamp,
649        ReturnRule::Fixed(ResolvedType::Double),
650    ),
651    sig(
652        "unixepoch",
653        Arity::Exact(1),
654        check_timestamp,
655        ReturnRule::Fixed(ResolvedType::BigInt),
656    ),
657    sig(
658        "sin",
659        Arity::Exact(1),
660        check_numeric,
661        ReturnRule::Fixed(ResolvedType::Double),
662    ),
663    sig(
664        "cos",
665        Arity::Exact(1),
666        check_numeric,
667        ReturnRule::Fixed(ResolvedType::Double),
668    ),
669    sig(
670        "tan",
671        Arity::Exact(1),
672        check_numeric,
673        ReturnRule::Fixed(ResolvedType::Double),
674    ),
675    sig(
676        "asin",
677        Arity::Exact(1),
678        check_numeric,
679        ReturnRule::Fixed(ResolvedType::Double),
680    ),
681    sig(
682        "acos",
683        Arity::Exact(1),
684        check_numeric,
685        ReturnRule::Fixed(ResolvedType::Double),
686    ),
687    sig(
688        "atan",
689        Arity::Exact(1),
690        check_numeric,
691        ReturnRule::Fixed(ResolvedType::Double),
692    ),
693    sig(
694        "atan2",
695        Arity::Exact(2),
696        check_numeric,
697        ReturnRule::Fixed(ResolvedType::Double),
698    ),
699    sig(
700        "degrees",
701        Arity::Exact(1),
702        check_numeric,
703        ReturnRule::Fixed(ResolvedType::Double),
704    ),
705    sig(
706        "radians",
707        Arity::Exact(1),
708        check_numeric,
709        ReturnRule::Fixed(ResolvedType::Double),
710    ),
711    sig(
712        "pi",
713        Arity::Exact(0),
714        check_numeric,
715        ReturnRule::Fixed(ResolvedType::Double),
716    ),
717    sig(
718        "sha256",
719        Arity::Exact(1),
720        check_text_or_blob,
721        ReturnRule::Fixed(ResolvedType::Blob),
722    ),
723    sig(
724        "md5",
725        Arity::Exact(1),
726        check_text_or_blob,
727        ReturnRule::Fixed(ResolvedType::Text),
728    ),
729    sig(
730        "simhash",
731        Arity::Exact(1),
732        check_text,
733        ReturnRule::Fixed(ResolvedType::BigInt),
734    ),
735    sig(
736        "hamming_distance",
737        Arity::Exact(2),
738        check_bigint,
739        ReturnRule::Fixed(ResolvedType::Integer),
740    ),
741    sig_meta(
742        "gen_random_uuid",
743        Arity::Exact(0),
744        check_no_args,
745        ReturnRule::Fixed(ResolvedType::Text),
746        RANDOM_META,
747    ),
748    sig_meta(
749        "uuidv7",
750        Arity::Exact(0),
751        check_no_args,
752        ReturnRule::Fixed(ResolvedType::Text),
753        RANDOM_META,
754    ),
755    sig(
756        "hex",
757        Arity::Exact(1),
758        check_text_or_blob,
759        ReturnRule::Fixed(ResolvedType::Text),
760    ),
761    sig(
762        "unhex",
763        Arity::Exact(1),
764        check_text,
765        ReturnRule::Fixed(ResolvedType::Blob),
766    ),
767    sig(
768        "encode",
769        Arity::Exact(2),
770        check_blob_text,
771        ReturnRule::Fixed(ResolvedType::Text),
772    ),
773    sig(
774        "decode",
775        Arity::Exact(2),
776        check_text,
777        ReturnRule::Fixed(ResolvedType::Blob),
778    ),
779    sig(
780        "length",
781        Arity::Exact(1),
782        check_text_or_blob,
783        ReturnRule::Fixed(ResolvedType::Integer),
784    ),
785    sig(
786        "char_length",
787        Arity::Exact(1),
788        check_text,
789        ReturnRule::Fixed(ResolvedType::Integer),
790    ),
791    sig(
792        "octet_length",
793        Arity::Exact(1),
794        check_text_or_blob,
795        ReturnRule::Fixed(ResolvedType::Integer),
796    ),
797    sig(
798        "ascii",
799        Arity::Exact(1),
800        check_text,
801        ReturnRule::Fixed(ResolvedType::Integer),
802    ),
803    sig(
804        "chr",
805        Arity::Exact(1),
806        check_integer,
807        ReturnRule::Fixed(ResolvedType::Text),
808    ),
809    sig(
810        "bit_length",
811        Arity::Exact(1),
812        check_text_or_blob,
813        ReturnRule::Fixed(ResolvedType::Integer),
814    ),
815    sig(
816        "starts_with",
817        Arity::Exact(2),
818        check_text,
819        ReturnRule::Fixed(ResolvedType::Boolean),
820    ),
821    sig(
822        "ends_with",
823        Arity::Exact(2),
824        check_text,
825        ReturnRule::Fixed(ResolvedType::Boolean),
826    ),
827    sig(
828        "translate",
829        Arity::Exact(3),
830        check_text,
831        ReturnRule::Fixed(ResolvedType::Text),
832    ),
833    sig(
834        "levenshtein",
835        Arity::Exact(2),
836        check_text,
837        ReturnRule::Fixed(ResolvedType::Integer),
838    ),
839    sig(
840        "upper",
841        Arity::Exact(1),
842        check_text,
843        ReturnRule::Fixed(ResolvedType::Text),
844    ),
845    sig(
846        "lower",
847        Arity::Exact(1),
848        check_text,
849        ReturnRule::Fixed(ResolvedType::Text),
850    ),
851    sig(
852        "initcap",
853        Arity::Exact(1),
854        check_text,
855        ReturnRule::Fixed(ResolvedType::Text),
856    ),
857    sig(
858        "substr",
859        Arity::Range(2, 3),
860        check_numeric_or_text_substr,
861        ReturnRule::Fixed(ResolvedType::Text),
862    ),
863    sig(
864        "left",
865        Arity::Exact(2),
866        check_text_numeric,
867        ReturnRule::Fixed(ResolvedType::Text),
868    ),
869    sig(
870        "right",
871        Arity::Exact(2),
872        check_text_numeric,
873        ReturnRule::Fixed(ResolvedType::Text),
874    ),
875    sig(
876        "trim",
877        Arity::Range(1, 2),
878        check_text,
879        ReturnRule::Fixed(ResolvedType::Text),
880    ),
881    sig(
882        "ltrim",
883        Arity::Range(1, 2),
884        check_text,
885        ReturnRule::Fixed(ResolvedType::Text),
886    ),
887    sig(
888        "rtrim",
889        Arity::Range(1, 2),
890        check_text,
891        ReturnRule::Fixed(ResolvedType::Text),
892    ),
893    sig(
894        "replace",
895        Arity::Exact(3),
896        check_text,
897        ReturnRule::Fixed(ResolvedType::Text),
898    ),
899    sig(
900        "instr",
901        Arity::Exact(2),
902        check_text,
903        ReturnRule::Fixed(ResolvedType::Integer),
904    ),
905    sig(
906        "strpos",
907        Arity::Exact(2),
908        check_text,
909        ReturnRule::Fixed(ResolvedType::Integer),
910    ),
911    sig(
912        "concat",
913        Arity::Variadic(0),
914        check_text,
915        ReturnRule::Fixed(ResolvedType::Text),
916    ),
917    sig(
918        "concat_ws",
919        Arity::Variadic(1),
920        check_text,
921        ReturnRule::Fixed(ResolvedType::Text),
922    ),
923    sig(
924        "repeat",
925        Arity::Exact(2),
926        check_text_numeric,
927        ReturnRule::Fixed(ResolvedType::Text),
928    ),
929    sig(
930        "reverse",
931        Arity::Exact(1),
932        check_text,
933        ReturnRule::Fixed(ResolvedType::Text),
934    ),
935    sig(
936        "lpad",
937        Arity::Range(2, 3),
938        check_pad,
939        ReturnRule::Fixed(ResolvedType::Text),
940    ),
941    sig(
942        "rpad",
943        Arity::Range(2, 3),
944        check_pad,
945        ReturnRule::Fixed(ResolvedType::Text),
946    ),
947    sig(
948        "split_part",
949        Arity::Exact(3),
950        check_text_text_numeric,
951        ReturnRule::Fixed(ResolvedType::Text),
952    ),
953    sig(
954        "regexp_replace",
955        Arity::Exact(3),
956        check_text,
957        ReturnRule::Fixed(ResolvedType::Text),
958    ),
959    sig(
960        "regexp_match",
961        Arity::Exact(2),
962        check_text,
963        ReturnRule::Fixed(ResolvedType::Text),
964    ),
965    sig(
966        "regexp_matches",
967        Arity::Range(2, 3),
968        check_text,
969        ReturnRule::Fixed(ResolvedType::Text),
970    ),
971    sig(
972        "regexp_like",
973        Arity::Range(2, 3),
974        check_text,
975        ReturnRule::Fixed(ResolvedType::Boolean),
976    ),
977    sig(
978        "coalesce",
979        Arity::Variadic(1),
980        check_compatible,
981        ReturnRule::FromArgs(return_first_non_null),
982    ),
983    sig(
984        "nullif",
985        Arity::Exact(2),
986        check_compatible,
987        ReturnRule::FromArgs(return_first_non_null),
988    ),
989    sig(
990        "ifnull",
991        Arity::Exact(2),
992        check_compatible,
993        ReturnRule::FromArgs(return_first_non_null),
994    ),
995    sig(
996        "iif",
997        Arity::Exact(3),
998        check_iif,
999        ReturnRule::FromArgs(return_first_non_null),
1000    ),
1001    sig(
1002        "greatest",
1003        Arity::Variadic(1),
1004        check_numeric,
1005        ReturnRule::FromArgs(return_numeric),
1006    ),
1007    sig(
1008        "least",
1009        Arity::Variadic(1),
1010        check_numeric,
1011        ReturnRule::FromArgs(return_numeric),
1012    ),
1013    sig(
1014        "typeof",
1015        Arity::Exact(1),
1016        check_any,
1017        ReturnRule::Fixed(ResolvedType::Text),
1018    ),
1019    sig(
1020        "pg_typeof",
1021        Arity::Exact(1),
1022        check_any,
1023        ReturnRule::Fixed(ResolvedType::Text),
1024    ),
1025    sig(
1026        "quote",
1027        Arity::Exact(1),
1028        check_any,
1029        ReturnRule::Fixed(ResolvedType::Text),
1030    ),
1031    sig_meta(
1032        "memory_stats",
1033        Arity::Exact(0),
1034        check_no_args,
1035        ReturnRule::Fixed(ResolvedType::Text),
1036        SYSTEM_META,
1037    ),
1038    sig_meta(
1039        "io_stats",
1040        Arity::Exact(0),
1041        check_no_args,
1042        ReturnRule::Fixed(ResolvedType::Text),
1043        SYSTEM_META,
1044    ),
1045    sig_meta(
1046        "clear_cache",
1047        Arity::Exact(0),
1048        check_no_args,
1049        ReturnRule::Fixed(ResolvedType::BigInt),
1050        SYSTEM_SIDE_EFFECT_META,
1051    ),
1052];
1053
1054fn check_numeric_or_text_substr(args: &[TypedExpr]) -> Result<(), PlannerError> {
1055    if args.is_empty() {
1056        return Ok(());
1057    }
1058    check_text(&args[..1])?;
1059    check_numeric(&args[1..])
1060}
1061
1062fn check_text_numeric(args: &[TypedExpr]) -> Result<(), PlannerError> {
1063    if !args.is_empty() {
1064        check_text(&args[..1])?;
1065    }
1066    if args.len() >= 2 {
1067        check_numeric(&args[1..2])?;
1068    }
1069    Ok(())
1070}
1071
1072fn check_pad(args: &[TypedExpr]) -> Result<(), PlannerError> {
1073    check_text_numeric(&args[..2])?;
1074    if args.len() == 3 {
1075        check_text(&args[2..3])?;
1076    }
1077    Ok(())
1078}
1079
1080fn check_text_text_numeric(args: &[TypedExpr]) -> Result<(), PlannerError> {
1081    check_text(&args[..2])?;
1082    check_numeric(&args[2..3])
1083}
1084
1085fn check_compatible(args: &[TypedExpr]) -> Result<(), PlannerError> {
1086    let mut expected: Option<&ResolvedType> = None;
1087    for arg in args {
1088        if matches!(arg.resolved_type, ResolvedType::Null) {
1089            continue;
1090        }
1091        if let Some(first) = expected {
1092            if !(is_numeric(first) && is_numeric(&arg.resolved_type)) && first != &arg.resolved_type
1093            {
1094                return Err(PlannerError::type_mismatch(
1095                    first.type_name(),
1096                    arg.resolved_type.type_name(),
1097                    arg.span,
1098                ));
1099            }
1100        } else {
1101            expected = Some(&arg.resolved_type);
1102        }
1103    }
1104    Ok(())
1105}
1106
1107fn check_iif(args: &[TypedExpr]) -> Result<(), PlannerError> {
1108    if !matches!(
1109        args.first().map(|a| &a.resolved_type),
1110        Some(ResolvedType::Boolean | ResolvedType::Null)
1111    ) {
1112        let arg = args
1113            .first()
1114            .expect("arity is validated before type checking");
1115        return Err(PlannerError::type_mismatch(
1116            "Boolean",
1117            arg.resolved_type.type_name(),
1118            arg.span,
1119        ));
1120    }
1121    check_compatible(&args[1..])
1122}
1123
1124pub fn signatures() -> &'static [ScalarSignature] {
1125    SIGNATURES
1126}
1127
1128pub fn signature(name: &str) -> Option<&'static ScalarSignature> {
1129    let lower = name.to_ascii_lowercase();
1130    SIGNATURES.iter().find(|sig| sig.name == lower)
1131}
1132
1133pub fn is_numeric_type(ty: &ResolvedType) -> bool {
1134    is_numeric(ty)
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140
1141    #[test]
1142    fn names_are_normalized_and_unique() {
1143        let mut names: Vec<_> = signatures().iter().map(|s| s.name).collect();
1144        names.sort_unstable();
1145        names.dedup();
1146        assert_eq!(names.len(), signatures().len());
1147        assert!(
1148            signatures()
1149                .iter()
1150                .all(|s| s.name == s.name.to_ascii_lowercase())
1151        );
1152    }
1153
1154    #[test]
1155    fn random_metadata_is_volatile() {
1156        let random = signature("RANDOM").unwrap();
1157        assert!(!random.meta.deterministic);
1158        assert!(random.meta.volatile);
1159        assert!(!random.meta.foldable);
1160        assert!(!random.meta.cacheable);
1161    }
1162
1163    #[test]
1164    fn scalar_names_do_not_overlap_aggregate_names() {
1165        let aggregates = [
1166            "count",
1167            "sum",
1168            "total",
1169            "avg",
1170            "min",
1171            "max",
1172            "group_concat",
1173            "string_agg",
1174        ];
1175        assert!(aggregates.iter().all(|name| signature(name).is_none()));
1176    }
1177}