1use 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 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}