Skip to main content

mz_sql_parser/ast/
item_refs.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 shall be governed
8// by the Apache License, Version 2.0.
9
10//! Extraction of catalog item references from a raw SQL AST.
11
12use std::collections::BTreeSet;
13
14use crate::ast::visit::Visit;
15use crate::ast::{
16    CteBlock, Function, Ident, Query, Raw, RawDataType, RawItemName, Statement, UnresolvedItemName,
17};
18
19/// The catalog item references appearing in a statement.
20///
21/// User statements are normalized and have IDs in its relation and type references (e.g. `[u1 AS
22/// db.schema.name]`). For references that don't have IDs, we create separate `named_*` buckets that
23/// contain their qualified names.
24#[derive(Debug, Default, Clone, PartialEq, Eq)]
25pub struct ItemReferences {
26    /// Catalog IDs from `RawItemName::Id` references
27    pub ids: BTreeSet<String>,
28    /// Named relations that don't carry an ID. Most commonly seen in builtin statements since they
29    /// are not normalized. CTE names are excluded: a name bound by a WITH clause refers to the
30    /// query-local binding rather than a catalog item, so recording it would fabricate an edge to
31    /// any catalog object that happens to share the name.
32    pub named_relations: BTreeSet<UnresolvedItemName>,
33    pub named_funcs: BTreeSet<UnresolvedItemName>,
34    pub named_types: BTreeSet<UnresolvedItemName>,
35    /// Data-type references written `T[]`. These are
36    /// reported separately from `named_types` since `T[]` maps to the named type `_T`.
37    pub named_array_elements: BTreeSet<UnresolvedItemName>,
38}
39
40/// Collects all catalog item references from a statement.
41pub fn collect_item_references(stmt: &Statement<Raw>) -> ItemReferences {
42    let mut collector = ReferenceCollector {
43        refs: ItemReferences::default(),
44        cte_names: Vec::new(),
45    };
46    collector.visit_statement(stmt);
47    collector.refs
48}
49
50struct ReferenceCollector {
51    refs: ItemReferences,
52    /// Stack of CTE names currently in scope.
53    cte_names: Vec<Ident>,
54}
55
56impl ReferenceCollector {
57    fn record(&mut self, name: &RawItemName, position: Position) {
58        match name {
59            RawItemName::Id(id, _, _) => {
60                self.refs.ids.insert(id.clone());
61            }
62            RawItemName::Name(name) => match position {
63                Position::Relation => {
64                    // Filter out CTEs.
65                    if let [only] = &name.0[..] {
66                        if self.cte_names.contains(only) {
67                            return;
68                        }
69                    }
70                    self.refs.named_relations.insert(name.clone());
71                }
72                Position::Func => {
73                    self.refs.named_funcs.insert(name.clone());
74                }
75                Position::Type => {
76                    self.refs.named_types.insert(name.clone());
77                }
78            },
79        }
80    }
81}
82
83enum Position {
84    Relation,
85    Func,
86    Type,
87}
88
89impl<'ast> Visit<'ast, Raw> for ReferenceCollector {
90    fn visit_item_name(&mut self, node: &'ast RawItemName) {
91        self.record(node, Position::Relation);
92    }
93
94    fn visit_function(&mut self, node: &'ast Function<Raw>) {
95        let Function {
96            name,
97            args,
98            filter,
99            over,
100            distinct: _,
101        } = node;
102        self.record(name, Position::Func);
103        self.visit_function_args(args);
104        if let Some(filter) = filter {
105            self.visit_expr(filter);
106        }
107        if let Some(over) = over {
108            self.visit_window_spec(over);
109        }
110    }
111
112    fn visit_data_type(&mut self, node: &'ast RawDataType) {
113        match node {
114            RawDataType::Array(elem_type) => match &**elem_type {
115                // `T[]` references the array type paired with `T`, not `T`
116                // itself. That pairing lives in `T`'s catalog details, which
117                // the parser cannot read, so the element is reported on its
118                // own field for the caller to map.
119                RawDataType::Other {
120                    name: RawItemName::Name(name),
121                    typ_mod: _,
122                } => {
123                    self.refs.named_array_elements.insert(name.clone());
124                }
125                // `[<id> AS <name>][]`. The id in hand names the element, and
126                // mapping it to the array type needs the catalog, so nothing
127                // is reported rather than an edge to the wrong type. Stored
128                // SQL never carries this spelling: name resolution prints a
129                // resolved array type as a single id reference.
130                RawDataType::Other {
131                    name: RawItemName::Id(..),
132                    typ_mod: _,
133                } => {}
134                // An array of a list, map, or array. Name resolution rejects
135                // the first two and the parser collapses `T[][]` into a 1D
136                // array, so none reach stored SQL. Recurse anyway so that no
137                // reference is dropped unreported.
138                elem_type => self.visit_data_type(elem_type),
139            },
140            RawDataType::List(elem_type) => self.visit_data_type(elem_type),
141            RawDataType::Map {
142                key_type,
143                value_type,
144            } => {
145                self.visit_data_type(key_type);
146                self.visit_data_type(value_type);
147            }
148            RawDataType::Other { name, typ_mod: _ } => self.record(name, Position::Type),
149        }
150    }
151
152    fn visit_query(&mut self, node: &'ast Query<Raw>) {
153        let Query {
154            ctes,
155            body,
156            order_by,
157            limit,
158            offset,
159        } = node;
160
161        let scope_depth = self.cte_names.len();
162        // Keep track of CTEs such that we don't record them
163        // in `named_relations`
164        match ctes {
165            CteBlock::Simple(ctes) => {
166                for cte in ctes {
167                    self.visit_cte(cte);
168                    self.cte_names.push(cte.alias.name.clone());
169                }
170            }
171            CteBlock::MutuallyRecursive(block) => {
172                for cte in &block.ctes {
173                    self.cte_names.push(cte.name.clone());
174                }
175                self.visit_mut_rec_block(block);
176            }
177        }
178        self.visit_set_expr(body);
179        for order_by in order_by {
180            self.visit_order_by_expr(order_by);
181        }
182        if let Some(limit) = limit {
183            self.visit_limit(limit);
184        }
185        if let Some(offset) = offset {
186            self.visit_expr(offset);
187        }
188        // Pop CTEs once visited.
189        self.cte_names.truncate(scope_depth);
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::parser::parse_statements;
197
198    #[track_caller]
199    fn collect(sql: &str) -> ItemReferences {
200        let stmts = parse_statements(sql).expect("valid sql");
201        assert_eq!(stmts.len(), 1, "expected a single statement");
202        collect_item_references(&stmts.into_iter().next().unwrap().ast)
203    }
204
205    fn names(parts: &[&[&str]]) -> BTreeSet<UnresolvedItemName> {
206        parts
207            .iter()
208            .map(|name| UnresolvedItemName(name.iter().map(|p| Ident::new_unchecked(*p)).collect()))
209            .collect()
210    }
211
212    fn strings(items: &[&str]) -> BTreeSet<String> {
213        items.iter().map(|s| s.to_string()).collect()
214    }
215
216    #[mz_ore::test]
217    fn ids_from_bracketed_references() {
218        let refs = collect(
219            r#"CREATE VIEW v AS
220               SELECT a::[s20 AS "pg_catalog"."int4"] FROM [u1 AS "materialize"."public"."t"]"#,
221        );
222        assert_eq!(refs.ids, strings(&["s20", "u1"]));
223        assert!(refs.named_relations.is_empty());
224        assert!(refs.named_types.is_empty());
225    }
226
227    #[mz_ore::test]
228    fn versioned_id_reference() {
229        let refs = collect(
230            r#"CREATE VIEW v AS SELECT * FROM [u3 AS "materialize"."public"."t" VERSION 2]"#,
231        );
232        assert_eq!(refs.ids, strings(&["u3"]));
233    }
234
235    #[mz_ore::test]
236    fn function_names_classified() {
237        let refs = collect(
238            r#"CREATE VIEW v AS SELECT "pg_catalog"."abs"(1), count(*) FROM "mz_catalog"."mz_tables""#,
239        );
240        assert_eq!(
241            refs.named_funcs,
242            names(&[&["pg_catalog", "abs"], &["count"]])
243        );
244        assert_eq!(refs.named_relations, names(&[&["mz_catalog", "mz_tables"]]));
245    }
246
247    #[mz_ore::test]
248    fn function_args_still_visited() {
249        let refs = collect(
250            r#"CREATE VIEW v AS SELECT "pg_catalog"."abs"(a::"pg_catalog"."int8")
251               FROM "mz_catalog"."mz_tables""#,
252        );
253        assert_eq!(refs.named_funcs, names(&[&["pg_catalog", "abs"]]));
254        assert_eq!(refs.named_types, names(&[&["pg_catalog", "int8"]]));
255    }
256
257    #[mz_ore::test]
258    fn nested_data_types() {
259        let refs = collect(
260            r#"CREATE TABLE t (a "pg_catalog"."int4"[], b "pg_catalog"."text" list,
261               c map["pg_catalog"."text" => "pg_catalog"."int8" list])"#,
262        );
263        // int4 sits under `[]`, so it is reported as an array element rather
264        // than as a type reference in its own right.
265        assert_eq!(
266            refs.named_types,
267            names(&[&["pg_catalog", "text"], &["pg_catalog", "int8"]])
268        );
269        assert_eq!(refs.named_array_elements, names(&[&["pg_catalog", "int4"]]));
270    }
271
272    #[mz_ore::test]
273    fn array_nested_in_list() {
274        // A list of arrays still references the array type.
275        let refs = collect(r#"CREATE TABLE t (a "pg_catalog"."int4"[] list)"#);
276        assert_eq!(refs.named_array_elements, names(&[&["pg_catalog", "int4"]]));
277        assert!(refs.named_types.is_empty());
278    }
279
280    #[mz_ore::test]
281    fn array_of_id_reference_reports_nothing() {
282        // Mapping the element id to its array type needs the catalog, so
283        // nothing is reported rather than an edge to the element.
284        let refs = collect(r#"CREATE TABLE t (a [s20 AS "pg_catalog"."int4"][])"#);
285        assert!(refs.named_array_elements.is_empty());
286        assert!(refs.named_types.is_empty());
287        assert!(refs.ids.is_empty());
288    }
289
290    #[mz_ore::test]
291    fn array_of_list_still_visits_the_inner_type() {
292        // Name resolution rejects `T list[]`, but the collector must not drop
293        // the inner reference unreported.
294        let refs = collect(r#"CREATE TABLE t (a "pg_catalog"."int4" list[])"#);
295        assert_eq!(refs.named_types, names(&[&["pg_catalog", "int4"]]));
296        assert!(refs.named_array_elements.is_empty());
297    }
298
299    #[mz_ore::test]
300    fn cte_names_excluded() {
301        let refs = collect(
302            r#"CREATE VIEW v AS
303               WITH c AS (SELECT * FROM "mz_catalog"."mz_tables")
304               SELECT * FROM c JOIN "mz_catalog"."mz_views" ON true"#,
305        );
306        assert_eq!(
307            refs.named_relations,
308            names(&[&["mz_catalog", "mz_tables"], &["mz_catalog", "mz_views"]])
309        );
310    }
311
312    #[mz_ore::test]
313    fn mut_rec_cte_names_excluded() {
314        let refs = collect(
315            r#"CREATE VIEW v AS
316               WITH MUTUALLY RECURSIVE
317                   reach (a int) AS (SELECT a FROM "mz_catalog"."mz_tables", reach)
318               SELECT * FROM reach"#,
319        );
320        assert_eq!(refs.named_relations, names(&[&["mz_catalog", "mz_tables"]]));
321        // The int column type on the WMR CTE is still a type reference.
322        assert_eq!(refs.named_types, names(&[&["int4"]]));
323    }
324
325    #[mz_ore::test]
326    fn qualified_cte_lookalike_not_excluded() {
327        // Only 1-part references can refer to a CTE.
328        let refs = collect(
329            r#"CREATE VIEW v AS
330               WITH mz_tables AS (SELECT 1 AS a)
331               SELECT * FROM mz_tables JOIN "mz_catalog"."mz_tables" ON true"#,
332        );
333        assert_eq!(refs.named_relations, names(&[&["mz_catalog", "mz_tables"]]));
334    }
335
336    #[mz_ore::test]
337    fn cte_scoping_is_lexical() {
338        // The CTE binding is scoped to the subquery that declares it: the
339        // same name in a sibling scope is a real relation reference.
340        let refs = collect(
341            r#"CREATE VIEW v AS
342               SELECT * FROM (WITH tbl AS (SELECT 1 AS a) SELECT * FROM tbl) x
343               JOIN tbl ON true"#,
344        );
345        assert_eq!(refs.named_relations, names(&[&["tbl"]]));
346    }
347
348    #[mz_ore::test]
349    fn simple_cte_definition_does_not_see_itself() {
350        // Mirroring the name resolver: a plain WITH binds a CTE's name only
351        // after its own definition, so the inner `tbl` is a real relation.
352        let refs = collect(
353            r#"CREATE VIEW v AS
354               WITH tbl AS (SELECT * FROM tbl) SELECT * FROM tbl"#,
355        );
356        assert_eq!(refs.named_relations, names(&[&["tbl"]]));
357    }
358
359    #[mz_ore::test]
360    fn later_simple_cte_sees_earlier() {
361        let refs = collect(
362            r#"CREATE VIEW v AS
363               WITH a AS (SELECT 1 AS x), b AS (SELECT * FROM a)
364               SELECT * FROM b"#,
365        );
366        assert!(refs.named_relations.is_empty());
367    }
368
369    #[mz_ore::test]
370    fn connection_and_secret_references() {
371        let refs = collect(
372            r#"CREATE CONNECTION kc TO KAFKA (
373                BROKER 'kafka:9092',
374                SSH TUNNEL [u5 AS "materialize"."public"."ssh"],
375                SASL PASSWORD = SECRET [u7 AS "materialize"."public"."pw"]
376            )"#,
377        );
378        assert_eq!(refs.ids, strings(&["u5", "u7"]));
379    }
380
381    #[mz_ore::test]
382    fn doc_on_references() {
383        // DOC ON TYPE and DOC ON COLUMN references persist with ids, so a
384        // sink's dependency on a commented type is recovered from the ids
385        // bucket like any other id reference.
386        let refs = collect(
387            r#"CREATE SINK s
388               FROM [u1 AS "materialize"."public"."t"]
389               INTO KAFKA CONNECTION [u2 AS "materialize"."public"."kc"] (TOPIC 'top')
390               FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION [u3 AS "materialize"."public"."csr"]
391               (DOC ON TYPE [u4 AS "materialize"."public"."point"] = 'point doc',
392                DOC ON COLUMN [u4 AS "materialize"."public"."point"].x = 'x doc')
393               ENVELOPE UPSERT"#,
394        );
395        assert_eq!(refs.ids, strings(&["u1", "u2", "u3", "u4"]));
396        assert!(refs.named_relations.is_empty());
397        assert!(refs.named_types.is_empty());
398    }
399
400    #[mz_ore::test]
401    fn index_references() {
402        let refs = collect(
403            r#"CREATE INDEX i IN CLUSTER [u1] ON [u2 AS "materialize"."public"."t"] ("pg_catalog"."abs"(a))"#,
404        );
405        assert_eq!(refs.ids, strings(&["u2"]));
406        assert_eq!(refs.named_funcs, names(&[&["pg_catalog", "abs"]]));
407    }
408}