Skip to main content

polyglot_sql/
function_catalog.rs

1use crate::dialects::DialectType;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Declarative, per-dialect input for the existing runtime catalog.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase", deny_unknown_fields)]
8pub struct FunctionCatalogSpec {
9    #[serde(default)]
10    pub name_case: FunctionNameCase,
11    pub functions: Vec<FunctionCatalogEntry>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct FunctionCatalogEntry {
17    pub name: String,
18    pub signatures: Vec<FunctionSignature>,
19    #[serde(default)]
20    pub name_case: Option<FunctionNameCase>,
21}
22
23impl FunctionCatalogSpec {
24    /// Compile a specification for the call's dialect. An explicit catalog
25    /// replaces, rather than supplements, the embedded catalog.
26    pub fn build(&self, dialect: DialectType) -> Result<HashMapFunctionCatalog, String> {
27        let mut catalog = HashMapFunctionCatalog::default();
28        catalog.set_dialect_name_case(dialect, self.name_case);
29        let mut cases = HashMap::new();
30        for function in &self.functions {
31            if function.name.trim().is_empty() || function.signatures.is_empty() {
32                return Err("Catalog functions require a nonempty name and signatures".into());
33            }
34            for signature in &function.signatures {
35                if signature
36                    .max_arity
37                    .is_some_and(|max| max < signature.min_arity)
38                {
39                    return Err(format!("Invalid arity range for '{}'", function.name));
40                }
41            }
42            let case = function.name_case.unwrap_or(self.name_case);
43            if cases
44                .insert(function.name.to_lowercase(), case)
45                .is_some_and(|old| old != case)
46            {
47                return Err(format!("Conflicting nameCase for '{}'", function.name));
48            }
49            catalog.set_function_name_case(dialect, &function.name, case);
50            catalog.register(dialect, &function.name, function.signatures.clone());
51        }
52        Ok(catalog)
53    }
54}
55
56/// Function-name casing behavior for lookup.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum FunctionNameCase {
60    /// Function names are compared case-insensitively.
61    #[default]
62    Insensitive,
63    /// Function names are compared with exact case.
64    Sensitive,
65}
66
67/// Function signature metadata used by semantic validation.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct FunctionSignature {
71    /// Minimum number of positional arguments.
72    pub min_arity: usize,
73    /// Maximum number of positional arguments.
74    /// `None` means unbounded/variadic.
75    pub max_arity: Option<usize>,
76}
77
78impl FunctionSignature {
79    /// Build an exact-arity signature.
80    pub const fn exact(arity: usize) -> Self {
81        Self {
82            min_arity: arity,
83            max_arity: Some(arity),
84        }
85    }
86
87    /// Build a bounded arity range signature.
88    pub const fn range(min_arity: usize, max_arity: usize) -> Self {
89        Self {
90            min_arity,
91            max_arity: Some(max_arity),
92        }
93    }
94
95    /// Build a variadic signature with a minimum arity.
96    pub const fn variadic(min_arity: usize) -> Self {
97        Self {
98            min_arity,
99            max_arity: None,
100        }
101    }
102
103    /// Whether an observed arity matches this signature.
104    pub fn matches_arity(&self, arity: usize) -> bool {
105        if arity < self.min_arity {
106            return false;
107        }
108        match self.max_arity {
109            Some(max) => arity <= max,
110            None => true,
111        }
112    }
113
114    /// Render a human-readable arity descriptor.
115    pub fn describe_arity(&self) -> String {
116        match self.max_arity {
117            Some(max) if max == self.min_arity => self.min_arity.to_string(),
118            Some(max) => format!("{}..{}", self.min_arity, max),
119            None => format!("{}+", self.min_arity),
120        }
121    }
122}
123
124/// Catalog abstraction for dialect-specific function metadata.
125///
126/// Implementations can be backed by generated files, external crates, or runtime-loaded assets.
127pub trait FunctionCatalog: Send + Sync {
128    /// Lookup overloads for a function name in a given dialect.
129    ///
130    /// `raw_function_name` should preserve user query casing.
131    /// `normalized_name` should be canonicalized/lowercased by the caller.
132    fn lookup(
133        &self,
134        dialect: DialectType,
135        raw_function_name: &str,
136        normalized_name: &str,
137    ) -> Option<&[FunctionSignature]>;
138}
139
140/// Minimal in-memory catalog implementation for runtime registration and tests.
141#[derive(Debug, Clone, Default)]
142pub struct HashMapFunctionCatalog {
143    entries_normalized: HashMap<DialectType, HashMap<String, Vec<FunctionSignature>>>,
144    entries_exact: HashMap<DialectType, HashMap<String, Vec<FunctionSignature>>>,
145    dialect_name_case: HashMap<DialectType, FunctionNameCase>,
146    function_name_case_overrides: HashMap<DialectType, HashMap<String, FunctionNameCase>>,
147}
148
149impl HashMapFunctionCatalog {
150    /// Set default function-name casing behavior for a dialect.
151    pub fn set_dialect_name_case(&mut self, dialect: DialectType, name_case: FunctionNameCase) {
152        self.dialect_name_case.insert(dialect, name_case);
153    }
154
155    /// Set optional per-function casing behavior override for a dialect.
156    ///
157    /// The override key is normalized to lowercase.
158    pub fn set_function_name_case(
159        &mut self,
160        dialect: DialectType,
161        function_name: impl Into<String>,
162        name_case: FunctionNameCase,
163    ) {
164        self.function_name_case_overrides
165            .entry(dialect)
166            .or_default()
167            .insert(function_name.into().to_lowercase(), name_case);
168    }
169
170    /// Register overloads for a function in a dialect.
171    pub fn register(
172        &mut self,
173        dialect: DialectType,
174        function_name: impl Into<String>,
175        signatures: Vec<FunctionSignature>,
176    ) {
177        let function_name = function_name.into();
178        let normalized_name = function_name.to_lowercase();
179
180        let normalized_entry = self
181            .entries_normalized
182            .entry(dialect)
183            .or_default()
184            .entry(normalized_name)
185            .or_default();
186        let exact_entry = self
187            .entries_exact
188            .entry(dialect)
189            .or_default()
190            .entry(function_name)
191            .or_default();
192
193        for sig in signatures {
194            if !normalized_entry.contains(&sig) {
195                normalized_entry.push(sig.clone());
196            }
197            if !exact_entry.contains(&sig) {
198                exact_entry.push(sig);
199            }
200        }
201    }
202
203    fn effective_name_case(&self, dialect: DialectType, normalized_name: &str) -> FunctionNameCase {
204        if let Some(overrides) = self.function_name_case_overrides.get(&dialect) {
205            if let Some(name_case) = overrides.get(normalized_name) {
206                return *name_case;
207            }
208        }
209        self.dialect_name_case
210            .get(&dialect)
211            .copied()
212            .unwrap_or_default()
213    }
214}
215
216impl FunctionCatalog for HashMapFunctionCatalog {
217    fn lookup(
218        &self,
219        dialect: DialectType,
220        raw_function_name: &str,
221        normalized_name: &str,
222    ) -> Option<&[FunctionSignature]> {
223        match self.effective_name_case(dialect, normalized_name) {
224            FunctionNameCase::Insensitive => self
225                .entries_normalized
226                .get(&dialect)
227                .and_then(|entries| entries.get(normalized_name))
228                .map(|v| v.as_slice()),
229            FunctionNameCase::Sensitive => self
230                .entries_exact
231                .get(&dialect)
232                .and_then(|entries| entries.get(raw_function_name))
233                .map(|v| v.as_slice()),
234        }
235    }
236}