reddb_server/storage/schema/function_catalog.rs
1//! Function catalog — static table of built-in scalar / aggregate
2//! function signatures used by the Fase 3 expression typer to
3//! resolve `Expr::FunctionCall` nodes to a concrete return type.
4//!
5//! Mirrors PostgreSQL's `pg_proc` catalog with a deliberately
6//! narrow row shape: a single (name, arg_types, return_type, kind)
7//! entry per overload. Multiple entries may share the same name —
8//! the resolver picks the one whose argument types match (after
9//! implicit coercion) using the `func_select_candidate` heuristic
10//! described in the roadmap (parte 4 of the plan file).
11//!
12//! The table is `const &[FunctionEntry]` so it lives in the
13//! read-only segment and lookups stay cache-friendly. Linear
14//! scan is fine for the ~30 entries the catalog covers today.
15//! Future weeks can switch to a `HashMap<&'static str, &[…]>`
16//! grouped by name when the table grows past ~500 entries.
17//!
18//! ## Coverage today
19//!
20//! Aggregates: COUNT, SUM, AVG, MIN, MAX (the five SQL-standard
21//! ones). Each has multiple overloads for the numeric category.
22//!
23//! Scalars covered:
24//!
25//! - String: UPPER, LOWER, LENGTH, COALESCE
26//! - Math: ABS, ROUND, FLOOR, CEIL
27//! - Time: NOW, CURRENT_TIMESTAMP, CURRENT_DATE, TIME_BUCKET
28//! - Geo: GEO_DISTANCE, GEO_BEARING, HAVERSINE
29//! - Misc: VERIFY_PASSWORD
30//!
31//! Variadic functions (COALESCE, GREATEST, LEAST, CONCAT) are
32//! marked with `variadic: true` and the resolver treats their
33//! `arg_types` slice as a description of the *uniform* element
34//! type — the catalog can't enumerate every arity, so the typer
35//! checks each call-site argument against `arg_types[0]` instead.
36//!
37//! ## What's NOT in this catalog
38//!
39//! - User-defined functions (CREATE FUNCTION) — separate runtime
40//! table, queried after the static catalog yields no match.
41//! - Polymorphic signatures (anyelement, anyarray) — Fase 3 W4.
42//! - Operator functions backing `+`, `-`, `*` — those go in
43//! `pg_operator` equivalent which we haven't built yet.
44
45use super::types::DataType;
46
47/// Function classification — affects resolver behavior and
48/// downstream planner cost estimation.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum FunctionKind {
51 /// Pure scalar function: same input → same output, no side
52 /// effects, no row-context dependency. The planner is free
53 /// to constant-fold or push-down through joins.
54 Scalar,
55 /// Aggregate function: consumes a Vec of input values, produces
56 /// a single output. Only valid in projection lists / HAVING /
57 /// window frames.
58 Aggregate,
59 /// Window function: like aggregate but evaluates over an
60 /// ORDER BY frame. Stays in projection lists only.
61 Window,
62 /// Side-effecting / time-dependent function: NOW(), RANDOM().
63 /// The planner cannot cache results across rows.
64 Volatile,
65}
66
67/// One signature in the static function catalog.
68#[derive(Debug, Clone, Copy)]
69pub struct FunctionEntry {
70 pub name: &'static str,
71 pub arg_types: &'static [DataType],
72 pub return_type: DataType,
73 pub kind: FunctionKind,
74 /// When true, the catalog's `arg_types` describes the element
75 /// type of a variadic argument list. Resolver matches each
76 /// call-site argument against `arg_types[0]` and ignores
77 /// arity entirely.
78 pub variadic: bool,
79}
80
81const fn entry(
82 name: &'static str,
83 arg_types: &'static [DataType],
84 return_type: DataType,
85 kind: FunctionKind,
86 variadic: bool,
87) -> FunctionEntry {
88 FunctionEntry {
89 name,
90 arg_types,
91 return_type,
92 kind,
93 variadic,
94 }
95}
96
97// ── Argument-list constants used by multiple entries ──
98// These are static slices so the FunctionEntry::arg_types pointer
99// stays the same across overloads sharing identical signatures.
100// The compiler interns the slice symbols so there's no duplicate
101// storage even if multiple entries reference the same array.
102
103const ARGS_INT: &[DataType] = &[DataType::Integer];
104const ARGS_FLOAT: &[DataType] = &[DataType::Float];
105const ARGS_BIGINT: &[DataType] = &[DataType::BigInt];
106const ARGS_TEXT: &[DataType] = &[DataType::Text];
107const ARGS_TWO_TEXT: &[DataType] = &[DataType::Text, DataType::Text];
108const ARGS_TEXT_INT: &[DataType] = &[DataType::Text, DataType::Integer];
109const ARGS_TEXT_TWO_INT: &[DataType] = &[DataType::Text, DataType::Integer, DataType::Integer];
110const ARGS_NONE: &[DataType] = &[];
111const ARGS_TWO_FLOATS: &[DataType] = &[DataType::Float, DataType::Float];
112const ARGS_GEO_PAIR: &[DataType] = &[DataType::GeoPoint, DataType::GeoPoint];
113const ARGS_FOUR_FLOATS: &[DataType] = &[
114 DataType::Float,
115 DataType::Float,
116 DataType::Float,
117 DataType::Float,
118];
119const ARGS_TIME_BUCKET: &[DataType] = &[DataType::Text, DataType::Timestamp];
120const ARGS_VERIFY_PWD: &[DataType] = &[DataType::Password, DataType::Text];
121const ARGS_MONEY: &[DataType] = &[DataType::Money];
122const ARGS_ONE_TEXT: &[DataType] = &[DataType::Text];
123const ARGS_TWO_TEXT_ANY: &[DataType] = &[DataType::Text, DataType::Text];
124
125// JSON function signatures (Phase 1.4 PG parity).
126//
127// RedDB accepts both Text and Json inputs interchangeably — the evaluator
128// parses text on the fly. We list the Text overload in the catalog so
129// existing text-column calls resolve without a cast; an explicit Json
130// overload is included for schemas that actually use the Json type.
131const ARGS_JSON_TEXT: &[DataType] = &[DataType::Text];
132const ARGS_JSON_JSON: &[DataType] = &[DataType::Json];
133const ARGS_JSON_EXTRACT_TEXT: &[DataType] = &[DataType::Text, DataType::Text];
134const ARGS_JSON_EXTRACT_JSON: &[DataType] = &[DataType::Json, DataType::Text];
135// JSON_SET takes (json, path, value). Value type is Unknown — the evaluator
136// coerces any scalar at runtime (int, float, bool, text, or another json).
137const ARGS_JSON_SET_TEXT: &[DataType] = &[DataType::Text, DataType::Text, DataType::Unknown];
138const ARGS_JSON_SET_JSON: &[DataType] = &[DataType::Json, DataType::Text, DataType::Unknown];
139
140/// The static function catalog. Append-only; removing a row is a
141/// breaking change that may invalidate cached plans referencing
142/// the function. Each block is grouped by category for readability.
143pub const FUNCTION_CATALOG: &[FunctionEntry] = &[
144 // ─────────────────────────────────────────────────────────────
145 // Aggregate functions
146 // ─────────────────────────────────────────────────────────────
147 //
148 // COUNT(*) and COUNT(col) are both modelled as `Integer →
149 // Integer` here; the parser distinguishes the star form via
150 // a separate Projection variant so the catalog doesn't need
151 // a magic asterisk overload.
152 entry(
153 "COUNT",
154 ARGS_INT,
155 DataType::Integer,
156 FunctionKind::Aggregate,
157 false,
158 ),
159 entry(
160 "COUNT",
161 ARGS_TEXT,
162 DataType::Integer,
163 FunctionKind::Aggregate,
164 false,
165 ),
166 entry(
167 "COUNT",
168 ARGS_FLOAT,
169 DataType::Integer,
170 FunctionKind::Aggregate,
171 false,
172 ),
173 entry(
174 "SUM",
175 ARGS_INT,
176 DataType::Integer,
177 FunctionKind::Aggregate,
178 false,
179 ),
180 entry(
181 "SUM",
182 ARGS_BIGINT,
183 DataType::BigInt,
184 FunctionKind::Aggregate,
185 false,
186 ),
187 entry(
188 "SUM",
189 ARGS_FLOAT,
190 DataType::Float,
191 FunctionKind::Aggregate,
192 false,
193 ),
194 entry(
195 "AVG",
196 ARGS_INT,
197 DataType::Float,
198 FunctionKind::Aggregate,
199 false,
200 ),
201 entry(
202 "AVG",
203 ARGS_FLOAT,
204 DataType::Float,
205 FunctionKind::Aggregate,
206 false,
207 ),
208 entry(
209 "MIN",
210 ARGS_INT,
211 DataType::Integer,
212 FunctionKind::Aggregate,
213 false,
214 ),
215 entry(
216 "MIN",
217 ARGS_FLOAT,
218 DataType::Float,
219 FunctionKind::Aggregate,
220 false,
221 ),
222 entry(
223 "MIN",
224 ARGS_TEXT,
225 DataType::Text,
226 FunctionKind::Aggregate,
227 false,
228 ),
229 entry(
230 "MAX",
231 ARGS_INT,
232 DataType::Integer,
233 FunctionKind::Aggregate,
234 false,
235 ),
236 entry(
237 "MAX",
238 ARGS_FLOAT,
239 DataType::Float,
240 FunctionKind::Aggregate,
241 false,
242 ),
243 entry(
244 "MAX",
245 ARGS_TEXT,
246 DataType::Text,
247 FunctionKind::Aggregate,
248 false,
249 ),
250 entry(
251 "STDDEV",
252 ARGS_FLOAT,
253 DataType::Float,
254 FunctionKind::Aggregate,
255 false,
256 ),
257 entry(
258 "VARIANCE",
259 ARGS_FLOAT,
260 DataType::Float,
261 FunctionKind::Aggregate,
262 false,
263 ),
264 entry(
265 "GROUP_CONCAT",
266 ARGS_TWO_TEXT,
267 DataType::Text,
268 FunctionKind::Aggregate,
269 false,
270 ),
271 entry(
272 "STRING_AGG",
273 ARGS_TWO_TEXT,
274 DataType::Text,
275 FunctionKind::Aggregate,
276 false,
277 ),
278 // ─────────────────────────────────────────────────────────────
279 // Scalar — string
280 // ─────────────────────────────────────────────────────────────
281 entry(
282 "UPPER",
283 ARGS_TEXT,
284 DataType::Text,
285 FunctionKind::Scalar,
286 false,
287 ),
288 entry(
289 "LOWER",
290 ARGS_TEXT,
291 DataType::Text,
292 FunctionKind::Scalar,
293 false,
294 ),
295 entry(
296 "LENGTH",
297 ARGS_TEXT,
298 DataType::Integer,
299 FunctionKind::Scalar,
300 false,
301 ),
302 entry(
303 "CHAR_LENGTH",
304 ARGS_TEXT,
305 DataType::Integer,
306 FunctionKind::Scalar,
307 false,
308 ),
309 entry(
310 "CHARACTER_LENGTH",
311 ARGS_TEXT,
312 DataType::Integer,
313 FunctionKind::Scalar,
314 false,
315 ),
316 entry(
317 "OCTET_LENGTH",
318 ARGS_TEXT,
319 DataType::Integer,
320 FunctionKind::Scalar,
321 false,
322 ),
323 entry(
324 "BIT_LENGTH",
325 ARGS_TEXT,
326 DataType::Integer,
327 FunctionKind::Scalar,
328 false,
329 ),
330 entry(
331 "SUBSTRING",
332 ARGS_TWO_TEXT,
333 DataType::Text,
334 FunctionKind::Scalar,
335 false,
336 ),
337 entry(
338 "SUBSTRING",
339 ARGS_TEXT_INT,
340 DataType::Text,
341 FunctionKind::Scalar,
342 false,
343 ),
344 entry(
345 "SUBSTRING",
346 ARGS_TEXT_TWO_INT,
347 DataType::Text,
348 FunctionKind::Scalar,
349 false,
350 ),
351 entry(
352 "SUBSTR",
353 ARGS_TEXT_INT,
354 DataType::Text,
355 FunctionKind::Scalar,
356 false,
357 ),
358 entry(
359 "SUBSTR",
360 ARGS_TEXT_TWO_INT,
361 DataType::Text,
362 FunctionKind::Scalar,
363 false,
364 ),
365 entry(
366 "POSITION",
367 ARGS_TWO_TEXT,
368 DataType::Integer,
369 FunctionKind::Scalar,
370 false,
371 ),
372 entry(
373 "TRIM",
374 ARGS_TEXT,
375 DataType::Text,
376 FunctionKind::Scalar,
377 false,
378 ),
379 entry(
380 "TRIM",
381 ARGS_TWO_TEXT,
382 DataType::Text,
383 FunctionKind::Scalar,
384 false,
385 ),
386 entry(
387 "LTRIM",
388 ARGS_TEXT,
389 DataType::Text,
390 FunctionKind::Scalar,
391 false,
392 ),
393 entry(
394 "LTRIM",
395 ARGS_TWO_TEXT,
396 DataType::Text,
397 FunctionKind::Scalar,
398 false,
399 ),
400 entry(
401 "RTRIM",
402 ARGS_TEXT,
403 DataType::Text,
404 FunctionKind::Scalar,
405 false,
406 ),
407 entry(
408 "RTRIM",
409 ARGS_TWO_TEXT,
410 DataType::Text,
411 FunctionKind::Scalar,
412 false,
413 ),
414 entry(
415 "BTRIM",
416 ARGS_TEXT,
417 DataType::Text,
418 FunctionKind::Scalar,
419 false,
420 ),
421 entry(
422 "BTRIM",
423 ARGS_TWO_TEXT,
424 DataType::Text,
425 FunctionKind::Scalar,
426 false,
427 ),
428 entry(
429 "CONCAT",
430 ARGS_TEXT,
431 DataType::Text,
432 FunctionKind::Scalar,
433 true,
434 ),
435 entry(
436 "CONCAT_WS",
437 ARGS_TEXT,
438 DataType::Text,
439 FunctionKind::Scalar,
440 true,
441 ),
442 entry(
443 "REVERSE",
444 ARGS_TEXT,
445 DataType::Text,
446 FunctionKind::Scalar,
447 false,
448 ),
449 entry(
450 "LEFT",
451 ARGS_TEXT_INT,
452 DataType::Text,
453 FunctionKind::Scalar,
454 false,
455 ),
456 entry(
457 "RIGHT",
458 ARGS_TEXT_INT,
459 DataType::Text,
460 FunctionKind::Scalar,
461 false,
462 ),
463 entry(
464 "QUOTE_LITERAL",
465 ARGS_TEXT,
466 DataType::Text,
467 FunctionKind::Scalar,
468 false,
469 ),
470 // COALESCE is variadic over a uniform element type. The
471 // resolver matches each call-site arg against arg_types[0]
472 // (any concrete type), and the return type is propagated
473 // from the first non-null argument's type at typing time.
474 entry(
475 "COALESCE",
476 ARGS_TEXT,
477 DataType::Text,
478 FunctionKind::Scalar,
479 true,
480 ),
481 // ─────────────────────────────────────────────────────────────
482 // Scalar — math
483 // ─────────────────────────────────────────────────────────────
484 entry(
485 "ABS",
486 ARGS_INT,
487 DataType::Integer,
488 FunctionKind::Scalar,
489 false,
490 ),
491 entry(
492 "ABS",
493 ARGS_FLOAT,
494 DataType::Float,
495 FunctionKind::Scalar,
496 false,
497 ),
498 entry(
499 "ROUND",
500 ARGS_FLOAT,
501 DataType::Float,
502 FunctionKind::Scalar,
503 false,
504 ),
505 entry(
506 "FLOOR",
507 ARGS_FLOAT,
508 DataType::Float,
509 FunctionKind::Scalar,
510 false,
511 ),
512 entry(
513 "CEIL",
514 ARGS_FLOAT,
515 DataType::Float,
516 FunctionKind::Scalar,
517 false,
518 ),
519 // ─────────────────────────────────────────────────────────────
520 // Scalar — time
521 // ─────────────────────────────────────────────────────────────
522 //
523 // NOW / CURRENT_TIMESTAMP / CURRENT_DATE are no-arg volatile
524 // scalars that read the wall clock at evaluation time. The
525 // planner must not constant-fold them across rows.
526 entry(
527 "NOW",
528 ARGS_NONE,
529 DataType::TimestampMs,
530 FunctionKind::Volatile,
531 false,
532 ),
533 entry(
534 "CURRENT_TIMESTAMP",
535 ARGS_NONE,
536 DataType::TimestampMs,
537 FunctionKind::Volatile,
538 false,
539 ),
540 entry(
541 "CURRENT_DATE",
542 ARGS_NONE,
543 DataType::Date,
544 FunctionKind::Volatile,
545 false,
546 ),
547 // Phase 2.5.3 multi-tenancy + PG session identity scalars. Marked
548 // Volatile so the planner cannot constant-fold them across rows
549 // — the thread-local value is hot-swappable per statement.
550 entry(
551 "CURRENT_TENANT",
552 ARGS_NONE,
553 DataType::Text,
554 FunctionKind::Volatile,
555 false,
556 ),
557 entry(
558 "CURRENT_USER",
559 ARGS_NONE,
560 DataType::Text,
561 FunctionKind::Volatile,
562 false,
563 ),
564 entry(
565 "SESSION_USER",
566 ARGS_NONE,
567 DataType::Text,
568 FunctionKind::Volatile,
569 false,
570 ),
571 entry(
572 "CURRENT_ROLE",
573 ARGS_NONE,
574 DataType::Text,
575 FunctionKind::Volatile,
576 false,
577 ),
578 entry(
579 "TIME_BUCKET",
580 ARGS_TIME_BUCKET,
581 DataType::TimestampMs,
582 FunctionKind::Scalar,
583 false,
584 ),
585 // ─────────────────────────────────────────────────────────────
586 // Scalar — geo
587 // ─────────────────────────────────────────────────────────────
588 entry(
589 "GEO_DISTANCE",
590 ARGS_GEO_PAIR,
591 DataType::Float,
592 FunctionKind::Scalar,
593 false,
594 ),
595 entry(
596 "GEO_DISTANCE",
597 ARGS_FOUR_FLOATS,
598 DataType::Float,
599 FunctionKind::Scalar,
600 false,
601 ),
602 entry(
603 "GEO_BEARING",
604 ARGS_FOUR_FLOATS,
605 DataType::Float,
606 FunctionKind::Scalar,
607 false,
608 ),
609 entry(
610 "HAVERSINE",
611 ARGS_FOUR_FLOATS,
612 DataType::Float,
613 FunctionKind::Scalar,
614 false,
615 ),
616 entry(
617 "VINCENTY",
618 ARGS_FOUR_FLOATS,
619 DataType::Float,
620 FunctionKind::Scalar,
621 false,
622 ),
623 // ─────────────────────────────────────────────────────────────
624 // Scalar — security
625 // ─────────────────────────────────────────────────────────────
626 //
627 // VERIFY_PASSWORD takes a hashed Password column + a candidate
628 // plaintext Text and returns Boolean. Marked Volatile because
629 // the underlying argon2id verify is intentionally slow and the
630 // planner should not cache results.
631 entry(
632 "VERIFY_PASSWORD",
633 ARGS_VERIFY_PWD,
634 DataType::Boolean,
635 FunctionKind::Volatile,
636 false,
637 ),
638 entry(
639 "MONEY",
640 ARGS_ONE_TEXT,
641 DataType::Money,
642 FunctionKind::Scalar,
643 false,
644 ),
645 entry(
646 "MONEY",
647 ARGS_TWO_TEXT_ANY,
648 DataType::Money,
649 FunctionKind::Scalar,
650 false,
651 ),
652 entry(
653 "MONEY_ASSET",
654 ARGS_MONEY,
655 DataType::AssetCode,
656 FunctionKind::Scalar,
657 false,
658 ),
659 entry(
660 "MONEY_MINOR",
661 ARGS_MONEY,
662 DataType::BigInt,
663 FunctionKind::Scalar,
664 false,
665 ),
666 entry(
667 "MONEY_SCALE",
668 ARGS_MONEY,
669 DataType::Integer,
670 FunctionKind::Scalar,
671 false,
672 ),
673 // Two-floats variant used by some places (legacy dual-arg form).
674 entry(
675 "POWER",
676 ARGS_TWO_FLOATS,
677 DataType::Float,
678 FunctionKind::Scalar,
679 false,
680 ),
681 // ─────────────────────────────────────────────────────────────
682 // Scalar — JSON (Phase 1.4 PG parity)
683 // ─────────────────────────────────────────────────────────────
684 //
685 // JSON_EXTRACT(json_or_text, path) → Text
686 // Returns the value at `path` serialised as JSON text. Scalar strings
687 // come back with surrounding quotes; scalar numbers/booleans come back
688 // as bare tokens. Returns NULL when the path does not resolve.
689 entry(
690 "JSON_EXTRACT",
691 ARGS_JSON_EXTRACT_TEXT,
692 DataType::Text,
693 FunctionKind::Scalar,
694 false,
695 ),
696 entry(
697 "JSON_EXTRACT",
698 ARGS_JSON_EXTRACT_JSON,
699 DataType::Text,
700 FunctionKind::Scalar,
701 false,
702 ),
703 // JSON_EXTRACT_TEXT(json_or_text, path) → Text
704 // Same as JSON_EXTRACT but strings come back unquoted (PG `->>` style).
705 entry(
706 "JSON_EXTRACT_TEXT",
707 ARGS_JSON_EXTRACT_TEXT,
708 DataType::Text,
709 FunctionKind::Scalar,
710 false,
711 ),
712 entry(
713 "JSON_EXTRACT_TEXT",
714 ARGS_JSON_EXTRACT_JSON,
715 DataType::Text,
716 FunctionKind::Scalar,
717 false,
718 ),
719 // JSON_SET(json_or_text, path, value) → Json
720 // Immutable update — returns a new JSON blob with `value` written at `path`.
721 // Creates missing parent objects/arrays on the path.
722 entry(
723 "JSON_SET",
724 ARGS_JSON_SET_TEXT,
725 DataType::Json,
726 FunctionKind::Scalar,
727 false,
728 ),
729 entry(
730 "JSON_SET",
731 ARGS_JSON_SET_JSON,
732 DataType::Json,
733 FunctionKind::Scalar,
734 false,
735 ),
736 // JSON_ARRAY_LENGTH(json) → Integer
737 entry(
738 "JSON_ARRAY_LENGTH",
739 ARGS_JSON_TEXT,
740 DataType::Integer,
741 FunctionKind::Scalar,
742 false,
743 ),
744 entry(
745 "JSON_ARRAY_LENGTH",
746 ARGS_JSON_JSON,
747 DataType::Integer,
748 FunctionKind::Scalar,
749 false,
750 ),
751 // JSON_TYPEOF(json) → Text ("null" | "boolean" | "number" | "string" | "array" | "object")
752 entry(
753 "JSON_TYPEOF",
754 ARGS_JSON_TEXT,
755 DataType::Text,
756 FunctionKind::Scalar,
757 false,
758 ),
759 entry(
760 "JSON_TYPEOF",
761 ARGS_JSON_JSON,
762 DataType::Text,
763 FunctionKind::Scalar,
764 false,
765 ),
766 // JSON_VALID(text) → Boolean — returns true if the argument parses.
767 entry(
768 "JSON_VALID",
769 ARGS_TEXT,
770 DataType::Boolean,
771 FunctionKind::Scalar,
772 false,
773 ),
774 // JSON_ARRAY(any*) → Json — variadic array constructor.
775 // Resolver uses arg_types[0] as the template; Unknown means "any scalar".
776 entry(
777 "JSON_ARRAY",
778 &[DataType::Unknown],
779 DataType::Json,
780 FunctionKind::Scalar,
781 true,
782 ),
783 // JSON_OBJECT(key, value, ...) → Json — variadic k/v pair object constructor.
784 entry(
785 "JSON_OBJECT",
786 &[DataType::Unknown],
787 DataType::Json,
788 FunctionKind::Scalar,
789 true,
790 ),
791];
792
793/// Look up a function by name, returning the slice of overloads
794/// (possibly empty). The resolver walks this slice and picks the
795/// best match using its own coercion logic.
796pub fn lookup(name: &str) -> Vec<&'static FunctionEntry> {
797 FUNCTION_CATALOG
798 .iter()
799 .filter(|e| e.name.eq_ignore_ascii_case(name))
800 .collect()
801}
802
803/// Resolve a function call to the best-matching overload. Returns
804/// `None` when no overload matches the call-site argument types
805/// (after implicit coercion).
806///
807/// **As of issue #82, this function delegates to
808/// `coercion_spine::resolve_function`**. The spine owns the
809/// coercion-aware overload-picking rule that used to live inline
810/// here; the catalog itself is now a pure registry of `pg_proc`-style
811/// rows. Callers that need the per-argument implicit-cast list
812/// (which the runtime evaluator must apply before dispatch) should
813/// call `coercion_spine::resolve_function` directly — this wrapper
814/// only returns the entry to preserve the existing call-site
815/// signature.
816pub fn resolve(name: &str, arg_types: &[DataType]) -> Option<&'static FunctionEntry> {
817 super::coercion_spine::resolve_function(name, arg_types).map(|(entry, _)| entry)
818}