1use 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#[derive(Debug, Default, Clone, PartialEq, Eq)]
25pub struct ItemReferences {
26 pub ids: BTreeSet<String>,
28 pub named_relations: BTreeSet<UnresolvedItemName>,
33 pub named_funcs: BTreeSet<UnresolvedItemName>,
34 pub named_types: BTreeSet<UnresolvedItemName>,
35 pub named_array_elements: BTreeSet<UnresolvedItemName>,
38}
39
40pub 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 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 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 RawDataType::Other {
120 name: RawItemName::Name(name),
121 typ_mod: _,
122 } => {
123 self.refs.named_array_elements.insert(name.clone());
124 }
125 RawDataType::Other {
131 name: RawItemName::Id(..),
132 typ_mod: _,
133 } => {}
134 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 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 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 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 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 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 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 assert_eq!(refs.named_types, names(&[&["int4"]]));
323 }
324
325 #[mz_ore::test]
326 fn qualified_cte_lookalike_not_excluded() {
327 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 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 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 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}