Skip to main content

mz_deploy/lsp/
functions.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Registry of Materialize SQL functions for LSP completion and hover.
11//!
12//! Built at first use from the canonical builtin registries in
13//! [`mz_sql::func`] so every overload the planner supports is automatically
14//! discoverable. Descriptions are not sourced from upstream — the LSP only
15//! shows kind, name, and synthesized signature strings.
16
17use 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/// The kind of a SQL function.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum FunctionKind {
28    /// A scalar function that returns a single value per input row.
29    Scalar,
30    /// An aggregate function that combines multiple rows into one result.
31    Aggregate,
32    /// A window function that computes values across related rows.
33    Window,
34    /// A table function that returns a set of rows.
35    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/// Metadata about a single SQL function, collapsing all overloads for a name.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct FunctionInfo {
52    /// The function name (lowercase).
53    pub name: String,
54    /// One entry per overload, e.g. `abs(int2) -> int2`.
55    pub signatures: Vec<String>,
56    /// Whether this is a scalar, aggregate, window, or table function. When a
57    /// name appears in multiple registries, the first-seen kind wins.
58    pub kind: FunctionKind,
59}
60
61/// All documented Materialize SQL functions, sorted alphabetically by name.
62///
63/// Merged across `pg_catalog`, `mz_catalog`, `information_schema`, and
64/// `mz_internal`. When the same name appears in multiple registries,
65/// overloads are appended and the first-seen `kind` is retained (name
66/// collisions across registries with different kinds are not expected in
67/// practice).
68pub 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
112/// Look up a function by exact name (case-insensitive).
113///
114/// Returns the merged entry containing every overload across registries.
115pub 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
120/// Return all functions whose name starts with the given prefix (case-insensitive).
121pub 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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
133    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
150    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
157    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
164    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
171    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
178    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
187    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
193    #[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)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
210    #[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}