Skip to main content

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}