Skip to main content

polyglot_sql_function_catalogs/
lib.rs

1#![forbid(unsafe_code)]
2
3#[cfg(feature = "dialect-clickhouse")]
4mod clickhouse;
5#[cfg(feature = "dialect-duckdb")]
6mod duckdb;
7
8/// Function-name casing behavior for lookup.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum FunctionNameCase {
11    /// Function names are compared case-insensitively.
12    #[default]
13    Insensitive,
14    /// Function names are compared with exact case.
15    Sensitive,
16}
17
18/// Function signature metadata used by semantic validation.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct FunctionSignature {
21    /// Minimum number of positional arguments.
22    pub min_arity: usize,
23    /// Maximum number of positional arguments.
24    /// `None` means unbounded/variadic.
25    pub max_arity: Option<usize>,
26}
27
28impl FunctionSignature {
29    /// Build an exact-arity signature.
30    pub const fn exact(arity: usize) -> Self {
31        Self {
32            min_arity: arity,
33            max_arity: Some(arity),
34        }
35    }
36
37    /// Build a bounded arity range signature.
38    pub const fn range(min_arity: usize, max_arity: usize) -> Self {
39        Self {
40            min_arity,
41            max_arity: Some(max_arity),
42        }
43    }
44
45    /// Build a variadic signature with a minimum arity.
46    pub const fn variadic(min_arity: usize) -> Self {
47        Self {
48            min_arity,
49            max_arity: None,
50        }
51    }
52}
53
54/// Sink used by this crate to emit feature-enabled dialect function catalogs.
55///
56/// The sink abstraction keeps this crate independent of `polyglot-sql`.
57pub trait CatalogSink {
58    /// Set default function-name casing behavior for a dialect key.
59    fn set_dialect_name_case(&mut self, dialect: &'static str, name_case: FunctionNameCase);
60
61    /// Set optional per-function casing override for a dialect key.
62    fn set_function_name_case(
63        &mut self,
64        dialect: &'static str,
65        function_name: &str,
66        name_case: FunctionNameCase,
67    );
68
69    /// Register function signatures for a dialect key.
70    fn register(
71        &mut self,
72        dialect: &'static str,
73        function_name: &str,
74        signatures: Vec<FunctionSignature>,
75    );
76}
77
78/// Register all catalogs enabled via crate features into a sink.
79#[allow(unused_variables)]
80pub fn register_enabled_catalogs<S: CatalogSink>(sink: &mut S) {
81    #[cfg(feature = "dialect-clickhouse")]
82    clickhouse::register(sink);
83    #[cfg(feature = "dialect-duckdb")]
84    duckdb::register(sink);
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::collections::HashMap;
91
92    #[derive(Default)]
93    struct TestSink {
94        dialect_name_case: HashMap<&'static str, FunctionNameCase>,
95        entries: HashMap<&'static str, HashMap<String, Vec<FunctionSignature>>>,
96    }
97
98    impl CatalogSink for TestSink {
99        fn set_dialect_name_case(&mut self, dialect: &'static str, name_case: FunctionNameCase) {
100            self.dialect_name_case.insert(dialect, name_case);
101        }
102
103        fn set_function_name_case(
104            &mut self,
105            _dialect: &'static str,
106            _function_name: &str,
107            _name_case: FunctionNameCase,
108        ) {
109        }
110
111        fn register(
112            &mut self,
113            dialect: &'static str,
114            function_name: &str,
115            signatures: Vec<FunctionSignature>,
116        ) {
117            self.entries
118                .entry(dialect)
119                .or_default()
120                .insert(function_name.to_string(), signatures);
121        }
122    }
123
124    #[cfg(feature = "dialect-clickhouse")]
125    #[test]
126    fn clickhouse_catalog_exposes_common_functions() {
127        let mut sink = TestSink::default();
128        register_enabled_catalogs(&mut sink);
129
130        assert_eq!(
131            sink.dialect_name_case.get("clickhouse"),
132            Some(&FunctionNameCase::Insensitive)
133        );
134        let if_signatures = sink
135            .entries
136            .get("clickhouse")
137            .and_then(|entries| entries.get("if"))
138            .expect("expected clickhouse function 'if'");
139        assert!(if_signatures
140            .iter()
141            .any(|sig| sig.min_arity == 3 && sig.max_arity == Some(3)));
142    }
143
144    #[cfg(feature = "dialect-duckdb")]
145    #[test]
146    fn duckdb_catalog_exposes_common_functions() {
147        let mut sink = TestSink::default();
148        register_enabled_catalogs(&mut sink);
149
150        assert_eq!(
151            sink.dialect_name_case.get("duckdb"),
152            Some(&FunctionNameCase::Insensitive)
153        );
154        let abs_signatures = sink
155            .entries
156            .get("duckdb")
157            .and_then(|entries| entries.get("abs"))
158            .expect("expected duckdb function 'abs'");
159        assert!(abs_signatures
160            .iter()
161            .any(|sig| sig.min_arity == 1 && sig.max_arity == Some(1)));
162    }
163}