Skip to main content

mz_sql/ast/
transform.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//! Provides a publicly available interface to transform our SQL ASTs.
11
12use std::collections::{BTreeMap, BTreeSet};
13
14use mz_ore::str::StrExt;
15use mz_repr::CatalogItemId;
16use mz_sql_parser::ast::CreateTableFromSourceStatement;
17
18use crate::ast::visit::{self, Visit};
19use crate::ast::visit_mut::{self, VisitMut};
20use crate::ast::{
21    AstInfo, CreateConnectionStatement, CreateIndexStatement, CreateMaterializedViewStatement,
22    CreateMetricSinkStatement, CreateSecretStatement, CreateSinkStatement, CreateSourceStatement,
23    CreateSubsourceStatement, CreateTableStatement, CreateViewStatement,
24    CreateWebhookSourceStatement, Expr, Ident, Query, Raw, RawDataType, RawItemName, Statement,
25    UnresolvedItemName, ViewDefinition,
26};
27use crate::names::FullItemName;
28
29/// Given a [`Statement`] rewrites all references of the schema name `cur_schema_name` to
30/// `new_schema_name`.
31pub fn create_stmt_rename_schema_refs(
32    create_stmt: &mut Statement<Raw>,
33    database: &str,
34    cur_schema: &str,
35    new_schema: &str,
36) -> Result<(), (String, String)> {
37    match create_stmt {
38        stmt @ Statement::CreateConnection(_)
39        | stmt @ Statement::CreateDatabase(_)
40        | stmt @ Statement::CreateSchema(_)
41        | stmt @ Statement::CreateWebhookSource(_)
42        | stmt @ Statement::CreateSource(_)
43        | stmt @ Statement::CreateSubsource(_)
44        | stmt @ Statement::CreateSink(_)
45        | stmt @ Statement::CreateMetricSink(_)
46        | stmt @ Statement::CreateView(_)
47        | stmt @ Statement::CreateMaterializedView(_)
48        | stmt @ Statement::CreateTable(_)
49        | stmt @ Statement::CreateTableFromSource(_)
50        | stmt @ Statement::CreateIndex(_)
51        | stmt @ Statement::CreateType(_)
52        | stmt @ Statement::CreateSecret(_) => {
53            let mut visitor = CreateSqlRewriteSchema {
54                database,
55                cur_schema,
56                new_schema,
57                error: None,
58            };
59            visitor.visit_statement_mut(stmt);
60
61            if let Some(e) = visitor.error.take() {
62                Err(e)
63            } else {
64                Ok(())
65            }
66        }
67        stmt => {
68            unreachable!("Internal error: only catalog items need to update item refs. {stmt:?}")
69        }
70    }
71}
72
73struct CreateSqlRewriteSchema<'a> {
74    database: &'a str,
75    cur_schema: &'a str,
76    new_schema: &'a str,
77    error: Option<(String, String)>,
78}
79
80impl<'a> CreateSqlRewriteSchema<'a> {
81    fn maybe_rewrite_idents(&mut self, name: &mut [Ident]) {
82        match name {
83            [schema, item] if schema.as_str() == self.cur_schema => {
84                // TODO(parkmycar): I _think_ when the database component is not specified we can
85                // always infer we're using the current database. But I'm not positive, so for now
86                // we'll bail in this case.
87                if self.error.is_none() {
88                    self.error = Some((schema.to_string(), item.to_string()));
89                }
90            }
91            [database, schema, _item] => {
92                if database.as_str() == self.database && schema.as_str() == self.cur_schema {
93                    *schema = Ident::new_unchecked(self.new_schema);
94                }
95            }
96            _ => (),
97        }
98    }
99}
100
101impl<'a, 'ast> VisitMut<'ast, Raw> for CreateSqlRewriteSchema<'a> {
102    fn visit_expr_mut(&mut self, e: &'ast mut Expr<Raw>) {
103        match e {
104            Expr::Identifier(id) => {
105                // The last ID component is a column name that should not be
106                // considered in the rewrite.
107                let i = id.len() - 1;
108                self.maybe_rewrite_idents(&mut id[..i]);
109            }
110            Expr::QualifiedWildcard(id) => {
111                self.maybe_rewrite_idents(id);
112            }
113            _ => visit_mut::visit_expr_mut(self, e),
114        }
115    }
116
117    fn visit_unresolved_item_name_mut(
118        &mut self,
119        unresolved_item_name: &'ast mut UnresolvedItemName,
120    ) {
121        self.maybe_rewrite_idents(&mut unresolved_item_name.0);
122    }
123
124    fn visit_data_type_mut(&mut self, data_type: &'ast mut RawDataType) {
125        // The generated `visit_data_type_mut` is a no-op because `DataType` is an
126        // associated type the generic visitor treats opaquely, so we must descend
127        // by hand. A type is referenced by a schema-qualified name precisely in
128        // these data type positions (a cast, a column type, or a nested element
129        // type), so without this a schema rename leaves stale references in any
130        // dependent that uses one of the schema's types.
131        match data_type {
132            RawDataType::Array(element_type) | RawDataType::List(element_type) => {
133                self.visit_data_type_mut(element_type)
134            }
135            RawDataType::Map {
136                key_type,
137                value_type,
138            } => {
139                self.visit_data_type_mut(key_type);
140                self.visit_data_type_mut(value_type);
141            }
142            RawDataType::Other { name, .. } => self.visit_item_name_mut(name),
143        }
144    }
145
146    fn visit_item_name_mut(
147        &mut self,
148        item_name: &'ast mut <mz_sql_parser::ast::Raw as AstInfo>::ItemName,
149    ) {
150        match item_name {
151            RawItemName::Name(n) | RawItemName::Id(_, n, _) => self.maybe_rewrite_idents(&mut n.0),
152        }
153    }
154}
155
156/// Changes the `name` used in an item's `CREATE` statement. To complete a
157/// rename operation, you must also call `create_stmt_rename_refs` on all dependent
158/// items.
159pub fn create_stmt_rename(create_stmt: &mut Statement<Raw>, to_item_name: String) {
160    // TODO(sploiselle): Support renaming schemas and databases.
161    match create_stmt {
162        Statement::CreateIndex(CreateIndexStatement { name, .. }) => {
163            *name = Some(Ident::new_unchecked(to_item_name));
164        }
165        Statement::CreateSink(CreateSinkStatement {
166            name: Some(name), ..
167        })
168        | Statement::CreateSource(CreateSourceStatement { name, .. })
169        | Statement::CreateSubsource(CreateSubsourceStatement { name, .. })
170        | Statement::CreateView(CreateViewStatement {
171            definition: ViewDefinition { name, .. },
172            ..
173        })
174        | Statement::CreateMaterializedView(CreateMaterializedViewStatement { name, .. })
175        | Statement::CreateTable(CreateTableStatement { name, .. })
176        | Statement::CreateTableFromSource(CreateTableFromSourceStatement { name, .. })
177        | Statement::CreateSecret(CreateSecretStatement { name, .. })
178        | Statement::CreateConnection(CreateConnectionStatement { name, .. })
179        | Statement::CreateWebhookSource(CreateWebhookSourceStatement { name, .. }) => {
180            // The last name in an ItemName is the item name. The item name
181            // does not have a fixed index.
182            // TODO: https://github.com/MaterializeInc/database-issues/issues/1721
183            let item_name_len = name.0.len() - 1;
184            name.0[item_name_len] = Ident::new_unchecked(to_item_name);
185        }
186        item => unreachable!("Internal error: only catalog items can be renamed {item:?}"),
187    }
188}
189
190/// Updates all references of `from_name` in `create_stmt` to `to_name` or
191/// errors if request is ambiguous.
192///
193/// Requests are considered ambiguous if `create_stmt` is a
194/// `Statement::CreateView`, and any of the following apply to its `query`:
195/// - `to_name.item` is used as an [`Ident`] in `query`.
196/// - `from_name.item` does not unambiguously refer to an item in the query,
197///   e.g. it is also used as a schema, or not all references to the item are
198///   sufficiently qualified.
199/// - `to_name.item` does not unambiguously refer to an item in the query after
200///   the rename. Right now, given the first condition, this is just a coherence
201///   check, but will be more meaningful once the first restriction is lifted.
202pub fn create_stmt_rename_refs(
203    create_stmt: &mut Statement<Raw>,
204    from_name: FullItemName,
205    to_item_name: String,
206) -> Result<(), String> {
207    let from_item = UnresolvedItemName::from(from_name.clone());
208    let maybe_update_item_name = |item_name: &mut UnresolvedItemName| {
209        if item_name.0 == from_item.0 {
210            // The last name in an ItemName is the item name. The item name
211            // does not have a fixed index.
212            // TODO: https://github.com/MaterializeInc/database-issues/issues/1721
213            let item_name_len = item_name.0.len() - 1;
214            item_name.0[item_name_len] = Ident::new_unchecked(to_item_name.clone());
215        }
216    };
217
218    // TODO(sploiselle): Support renaming schemas and databases.
219    match create_stmt {
220        Statement::CreateIndex(CreateIndexStatement { on_name, .. }) => {
221            maybe_update_item_name(on_name.name_mut());
222        }
223        Statement::CreateSink(CreateSinkStatement { from, .. }) => {
224            maybe_update_item_name(from.name_mut());
225        }
226        Statement::CreateMetricSink(CreateMetricSinkStatement { from, .. }) => {
227            maybe_update_item_name(from.name_mut());
228        }
229        Statement::CreateTableFromSource(CreateTableFromSourceStatement { source, .. }) => {
230            maybe_update_item_name(source.name_mut());
231        }
232        Statement::CreateView(CreateViewStatement {
233            definition: ViewDefinition { query, .. },
234            ..
235        }) => {
236            rewrite_query(from_name, to_item_name, query)?;
237        }
238        Statement::CreateMaterializedView(CreateMaterializedViewStatement {
239            replacement_for,
240            query,
241            ..
242        }) => {
243            if let Some(target) = replacement_for {
244                maybe_update_item_name(target.name_mut());
245            }
246            rewrite_query(from_name, to_item_name, query)?;
247        }
248        Statement::CreateSource(_)
249        | Statement::CreateSubsource(_)
250        | Statement::CreateTable(_)
251        | Statement::CreateSecret(_)
252        | Statement::CreateConnection(_)
253        | Statement::CreateWebhookSource(_) => {}
254        item => {
255            unreachable!("Internal error: only catalog items need to update item refs {item:?}")
256        }
257    }
258
259    Ok(())
260}
261
262/// Rewrites `query`'s references of `from` to `to` or errors if too ambiguous.
263fn rewrite_query(from: FullItemName, to: String, query: &mut Query<Raw>) -> Result<(), String> {
264    let from_ident = Ident::new_unchecked(from.item.clone());
265    let to_ident = Ident::new_unchecked(to);
266    let qual_depth =
267        QueryIdentAgg::determine_qual_depth(&from_ident, Some(to_ident.clone()), query)?;
268    CreateSqlRewriter::rewrite_query_with_qual_depth(from, to_ident.clone(), qual_depth, query);
269    // Ensure that our rewrite didn't didn't introduce ambiguous
270    // references to `to_name`.
271    match QueryIdentAgg::determine_qual_depth(&to_ident, None, query) {
272        Ok(_) => Ok(()),
273        Err(e) => Err(e),
274    }
275}
276
277fn ambiguous_err(n: &Ident, t: &str) -> String {
278    format!(
279        "{} potentially used ambiguously as item and {}",
280        n.as_str().quoted(),
281        t
282    )
283}
284
285/// Visits a [`Query`], assessing catalog item [`Ident`]s' use of a specified `Ident`.
286struct QueryIdentAgg<'a> {
287    /// The name whose usage you want to assess.
288    name: &'a Ident,
289    /// Tracks all second-level qualifiers used on `name` in a `BTreeMap`, as
290    /// well as any third-level qualifiers used on those second-level qualifiers
291    /// in a `BTreeSet`.
292    qualifiers: BTreeMap<Ident, BTreeSet<Ident>>,
293    /// Tracks the least qualified instance of `name` seen.
294    min_qual_depth: usize,
295    /// Provides an option to fail the visit if encounters a specified `Ident`.
296    fail_on: Option<Ident>,
297    err: Option<String>,
298}
299
300impl<'a> QueryIdentAgg<'a> {
301    /// Determines the depth of qualification needed to unambiguously reference
302    /// catalog items in a [`Query`].
303    ///
304    /// Includes an option to fail if a given `Ident` is encountered.
305    ///
306    /// `Result`s of `Ok(usize)` indicate that `name` can be unambiguously
307    /// referred to with `usize` parts, e.g. 2 requires schema and item name
308    /// qualification.
309    ///
310    /// `Result`s of `Err` indicate that we cannot unambiguously reference
311    /// `name` or encountered `fail_on`, if it's provided.
312    fn determine_qual_depth(
313        name: &Ident,
314        fail_on: Option<Ident>,
315        query: &Query<Raw>,
316    ) -> Result<usize, String> {
317        let mut v = QueryIdentAgg {
318            qualifiers: BTreeMap::new(),
319            min_qual_depth: usize::MAX,
320            err: None,
321            name,
322            fail_on,
323        };
324
325        // Aggregate identities in `v`.
326        v.visit_query(query);
327        // Not possible to have a qualification depth of 0;
328        assert!(v.min_qual_depth > 0);
329
330        if let Some(e) = v.err {
331            return Err(e);
332        }
333
334        // Check if there was more than one 3rd-level (e.g.
335        // database) qualification used for any reference to `name`.
336        let req_depth = if v.qualifiers.values().any(|v| v.len() > 1) {
337            3
338        // Check if there was more than one 2nd-level (e.g. schema)
339        // qualification used for any reference to `name`.
340        } else if v.qualifiers.len() > 1 {
341            2
342        } else {
343            1
344        };
345
346        if v.min_qual_depth < req_depth {
347            Err(format!(
348                "{} is not sufficiently qualified to support renaming",
349                name.as_str().quoted()
350            ))
351        } else {
352            Ok(req_depth)
353        }
354    }
355
356    // Assesses `v` for uses of `self.name` and `self.fail_on`.
357    fn check_failure(&mut self, v: &[Ident]) {
358        // Fail if we encounter `self.fail_on`.
359        if let Some(f) = &self.fail_on {
360            if v.iter().any(|i| i == f) {
361                self.err = Some(format!(
362                    "found reference to {}; cannot rename {} to any identity \
363                    used in any existing view definitions",
364                    f.as_str().quoted(),
365                    self.name.as_str().quoted()
366                ));
367            }
368        }
369    }
370}
371
372impl<'a, 'ast> Visit<'ast, Raw> for QueryIdentAgg<'a> {
373    fn visit_expr(&mut self, e: &'ast Expr<Raw>) {
374        match e {
375            Expr::Identifier(i) => {
376                self.check_failure(i);
377                if let Some(p) = i.iter().rposition(|e| e == self.name) {
378                    if p == i.len() - 1 {
379                        // `self.name` used as a column if it's in the final
380                        // position here, e.g. `SELECT view.col FROM ...`
381                        self.err = Some(ambiguous_err(self.name, "column"));
382                        return;
383                    }
384                    self.min_qual_depth = std::cmp::min(p + 1, self.min_qual_depth);
385                }
386            }
387            Expr::QualifiedWildcard(i) => {
388                self.check_failure(i);
389                if let Some(p) = i.iter().rposition(|e| e == self.name) {
390                    self.min_qual_depth = std::cmp::min(p + 1, self.min_qual_depth);
391                }
392            }
393            _ => visit::visit_expr(self, e),
394        }
395    }
396
397    fn visit_ident(&mut self, ident: &'ast Ident) {
398        self.check_failure(std::slice::from_ref(ident));
399        // This is an unqualified item using `self.name`, e.g. an alias, which
400        // we cannot unambiguously resolve.
401        if ident == self.name {
402            self.err = Some(ambiguous_err(self.name, "alias or column"));
403        }
404    }
405
406    fn visit_unresolved_item_name(&mut self, unresolved_item_name: &'ast UnresolvedItemName) {
407        let names = &unresolved_item_name.0;
408        self.check_failure(names);
409        // Every item is used as an `ItemName` at least once, which
410        // lets use track all items named `self.name`.
411        if let Some(p) = names.iter().rposition(|e| e == self.name) {
412            // Name used as last element of `<db>.<schema>.<item>`
413            if p == names.len() - 1 && names.len() == 3 {
414                self.qualifiers
415                    .entry(names[1].clone())
416                    .or_default()
417                    .insert(names[0].clone());
418                self.min_qual_depth = std::cmp::min(3, self.min_qual_depth);
419            } else {
420                // Any other use is a database or schema
421                self.err = Some(ambiguous_err(self.name, "database, schema, or function"))
422            }
423        }
424    }
425
426    fn visit_item_name(&mut self, item_name: &'ast <Raw as AstInfo>::ItemName) {
427        match item_name {
428            RawItemName::Name(n) | RawItemName::Id(_, n, _) => self.visit_unresolved_item_name(n),
429        }
430    }
431}
432
433struct CreateSqlRewriter {
434    from: Vec<Ident>,
435    to: Ident,
436}
437
438impl CreateSqlRewriter {
439    fn rewrite_query_with_qual_depth(
440        from_name: FullItemName,
441        to_name: Ident,
442        qual_depth: usize,
443        query: &mut Query<Raw>,
444    ) {
445        let from = match qual_depth {
446            1 => vec![Ident::new_unchecked(from_name.item)],
447            2 => vec![
448                Ident::new_unchecked(from_name.schema),
449                Ident::new_unchecked(from_name.item),
450            ],
451            3 => vec![
452                Ident::new_unchecked(from_name.database.to_string()),
453                Ident::new_unchecked(from_name.schema),
454                Ident::new_unchecked(from_name.item),
455            ],
456            _ => unreachable!(),
457        };
458        let mut v = CreateSqlRewriter { from, to: to_name };
459        v.visit_query_mut(query);
460    }
461
462    fn maybe_rewrite_idents(&self, name: &mut [Ident]) {
463        if name.len() > 0 && name.ends_with(&self.from) {
464            name[name.len() - 1] = self.to.clone();
465        }
466    }
467}
468
469impl<'ast> VisitMut<'ast, Raw> for CreateSqlRewriter {
470    fn visit_expr_mut(&mut self, e: &'ast mut Expr<Raw>) {
471        match e {
472            Expr::Identifier(id) => {
473                // The last ID component is a column name that should not be
474                // considered in the rewrite.
475                let i = id.len() - 1;
476                self.maybe_rewrite_idents(&mut id[..i]);
477            }
478            Expr::QualifiedWildcard(id) => {
479                self.maybe_rewrite_idents(id);
480            }
481            _ => visit_mut::visit_expr_mut(self, e),
482        }
483    }
484    fn visit_unresolved_item_name_mut(
485        &mut self,
486        unresolved_item_name: &'ast mut UnresolvedItemName,
487    ) {
488        self.maybe_rewrite_idents(&mut unresolved_item_name.0);
489    }
490    fn visit_item_name_mut(
491        &mut self,
492        item_name: &'ast mut <mz_sql_parser::ast::Raw as AstInfo>::ItemName,
493    ) {
494        match item_name {
495            RawItemName::Name(n) | RawItemName::Id(_, n, _) => self.maybe_rewrite_idents(&mut n.0),
496        }
497    }
498}
499
500/// Updates all `CatalogItemId`s from the keys of `ids` to the values of `ids` within `create_stmt`.
501pub fn create_stmt_replace_ids(
502    create_stmt: &mut Statement<Raw>,
503    ids: &BTreeMap<CatalogItemId, CatalogItemId>,
504) {
505    let mut id_replacer = CreateSqlIdReplacer { ids };
506    id_replacer.visit_statement_mut(create_stmt);
507}
508
509struct CreateSqlIdReplacer<'a> {
510    ids: &'a BTreeMap<CatalogItemId, CatalogItemId>,
511}
512
513impl<'ast> VisitMut<'ast, Raw> for CreateSqlIdReplacer<'_> {
514    fn visit_item_name_mut(
515        &mut self,
516        item_name: &'ast mut <mz_sql_parser::ast::Raw as AstInfo>::ItemName,
517    ) {
518        match item_name {
519            RawItemName::Id(id, _, _) => {
520                let old_id = match id.parse() {
521                    Ok(old_id) => old_id,
522                    Err(e) => panic!("invalid persisted global id {id}: {e}"),
523                };
524                if let Some(new_id) = self.ids.get(&old_id) {
525                    *id = new_id.to_string();
526                }
527            }
528            RawItemName::Name(_) => {}
529        }
530    }
531}