mz_deploy/lsp/
functions.rs1use std::collections::BTreeMap;
18use std::sync::LazyLock;
19
20use mz_sql::func::{
21 Func, FuncImplCatalogDetails, INFORMATION_SCHEMA_BUILTINS, MZ_CATALOG_BUILTINS,
22 MZ_INTERNAL_BUILTINS, PG_CATALOG_BUILTINS,
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum FunctionKind {
28 Scalar,
30 Aggregate,
32 Window,
34 Table,
36}
37
38impl FunctionKind {
39 fn from_func(func: &Func) -> Self {
40 match func {
41 Func::Scalar(_) => FunctionKind::Scalar,
42 Func::Aggregate(_) => FunctionKind::Aggregate,
43 Func::Table(_) => FunctionKind::Table,
44 Func::ScalarWindow(_) | Func::ValueWindow(_) => FunctionKind::Window,
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct FunctionInfo {
52 pub name: String,
54 pub signatures: Vec<String>,
56 pub kind: FunctionKind,
59}
60
61pub static FUNCTIONS: LazyLock<Vec<FunctionInfo>> = LazyLock::new(build_functions);
69
70fn build_functions() -> Vec<FunctionInfo> {
71 let registries: &[&LazyLock<BTreeMap<&'static str, Func>>] = &[
72 &PG_CATALOG_BUILTINS,
73 &MZ_CATALOG_BUILTINS,
74 &INFORMATION_SCHEMA_BUILTINS,
75 &MZ_INTERNAL_BUILTINS,
76 ];
77
78 let mut by_name: BTreeMap<String, FunctionInfo> = BTreeMap::new();
79 for registry in registries {
80 for (name, func) in registry.iter() {
81 let kind = FunctionKind::from_func(func);
82 let entry = by_name
83 .entry((*name).to_string())
84 .or_insert_with(|| FunctionInfo {
85 name: (*name).to_string(),
86 signatures: Vec::new(),
87 kind,
88 });
89 for details in func.func_impls() {
90 entry.signatures.push(format_signature(name, &details));
91 }
92 }
93 }
94
95 by_name.into_values().collect()
96}
97
98fn format_signature(name: &str, details: &FuncImplCatalogDetails) -> String {
99 let mut args: Vec<String> = details.arg_typs.iter().map(|t| (*t).to_string()).collect();
100 if let Some(variadic) = details.variadic_typ {
101 args.push(format!("VARIADIC {variadic}"));
102 }
103 let args_str = args.join(", ");
104 let return_str = match (details.return_typ, details.return_is_set) {
105 (Some(t), true) => format!(" -> setof {t}"),
106 (Some(t), false) => format!(" -> {t}"),
107 (None, _) => String::new(),
108 };
109 format!("{name}({args_str}){return_str}")
110}
111
112pub fn lookup(name: &str) -> Option<&'static FunctionInfo> {
116 let name_lower = name.to_lowercase();
117 FUNCTIONS.iter().find(|f| f.name == name_lower)
118}
119
120pub fn search_prefix(prefix: &str) -> impl Iterator<Item = &'static FunctionInfo> {
122 let prefix_lower = prefix.to_lowercase();
123 FUNCTIONS
124 .iter()
125 .filter(move |f| f.name.starts_with(&prefix_lower))
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[cfg_attr(miri, ignore)] #[mz_ore::test]
134 fn lookup_finds_core_scalar() {
135 let f = lookup("abs").expect("abs should exist");
136 assert_eq!(f.name, "abs");
137 assert_eq!(f.kind, FunctionKind::Scalar);
138 assert!(
139 !f.signatures.is_empty(),
140 "abs should have at least one overload"
141 );
142 assert!(
143 f.signatures.iter().all(|s| s.starts_with("abs(")),
144 "every signature should start with the function name, got {:?}",
145 f.signatures,
146 );
147 }
148
149 #[cfg_attr(miri, ignore)] #[mz_ore::test]
151 fn lookup_is_case_insensitive() {
152 assert!(lookup("ABS").is_some());
153 assert!(lookup("Abs").is_some());
154 }
155
156 #[cfg_attr(miri, ignore)] #[mz_ore::test]
158 fn lookup_classifies_aggregate() {
159 let f = lookup("sum").expect("sum should exist");
160 assert_eq!(f.kind, FunctionKind::Aggregate);
161 }
162
163 #[cfg_attr(miri, ignore)] #[mz_ore::test]
165 fn lookup_classifies_table() {
166 let f = lookup("generate_series").expect("generate_series should exist");
167 assert_eq!(f.kind, FunctionKind::Table);
168 }
169
170 #[cfg_attr(miri, ignore)] #[mz_ore::test]
172 fn lookup_classifies_window() {
173 let f = lookup("row_number").expect("row_number should exist");
174 assert_eq!(f.kind, FunctionKind::Window);
175 }
176
177 #[cfg_attr(miri, ignore)] #[mz_ore::test]
179 fn lookup_finds_materialize_specific() {
180 assert!(
181 lookup("mz_now").is_some(),
182 "mz_now missing — MZ_CATALOG_BUILTINS not wired in?"
183 );
184 }
185
186 #[cfg_attr(miri, ignore)] #[mz_ore::test]
188 fn lookup_unknown_returns_none() {
189 assert!(lookup("this_function_does_not_exist").is_none());
190 }
191
192 #[cfg_attr(miri, ignore)] #[mz_ore::test]
194 fn search_prefix_returns_sorted_unique_names() {
195 let names: Vec<&str> = search_prefix("arr").map(|f| f.name.as_str()).collect();
196 assert!(!names.is_empty(), "expected at least one 'arr*' function");
197 assert!(
198 names.iter().all(|n| n.starts_with("arr")),
199 "non-matching name in prefix results: {:?}",
200 names,
201 );
202 let mut sorted = names.clone();
203 sorted.sort();
204 assert_eq!(names, sorted, "prefix results not sorted: {:?}", names);
205 sorted.dedup();
206 assert_eq!(names.len(), sorted.len(), "duplicate names: {:?}", names);
207 }
208
209 #[cfg_attr(miri, ignore)] #[mz_ore::test]
211 fn signature_format_includes_return_type() {
212 let f = lookup("lower").expect("lower should exist");
213 assert!(
214 f.signatures.iter().any(|s| s.contains(" -> ")),
215 "expected ' -> ' in at least one signature, got {:?}",
216 f.signatures,
217 );
218 }
219}