Skip to main content

mz_deploy/project/
ast.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//! Abstract syntax types shared across project compilation and graph analysis.
11//!
12//! This module contains AST-adjacent types that are used by source inputs,
13//! compiled objects, and dependency analysis without creating circular
14//! dependencies. The shared types in this module (`Statement`,
15//! `DatabaseIdent`, `Cluster`) provide common vocabulary without carrying
16//! validation status themselves.
17
18use crate::types::ObjectKind;
19use mz_sql_parser::ast::*;
20
21/// A structured identifier for database objects supporting partial qualification.
22///
23/// Represents a database object identifier that may be partially or fully qualified:
24/// - Unqualified: `object`
25/// - Schema-qualified: `schema.object`
26/// - Fully-qualified: `database.schema.object`
27///
28/// This type is used internally for matching and validating object references across
29/// SQL statements where references may have different levels of qualification.
30#[derive(Debug)]
31pub struct DatabaseIdent {
32    pub database: Option<Ident>,
33    pub schema: Option<Ident>,
34    pub object: Ident,
35}
36
37impl From<UnresolvedItemName> for DatabaseIdent {
38    fn from(value: UnresolvedItemName) -> Self {
39        match value.0.as_slice() {
40            [object] => Self {
41                database: None,
42                schema: None,
43                object: object.clone(),
44            },
45            [schema, object] => Self {
46                database: None,
47                schema: Some(schema.clone()),
48                object: object.clone(),
49            },
50            [database, schema, object] => Self {
51                database: Some(database.clone()),
52                schema: Some(schema.clone()),
53                object: object.clone(),
54            },
55            _ => unreachable!(),
56        }
57    }
58}
59
60impl DatabaseIdent {
61    /// Checks if this identifier matches another identifier with flexible qualification matching.
62    ///
63    /// This method performs a partial match where an identifier with fewer qualification
64    /// levels can match an identifier with more levels, as long as the specified parts match.
65    ///
66    /// # Matching Rules
67    ///
68    /// - Object names must always match exactly
69    /// - If this ident has a schema, it must match the other's schema (if present)
70    /// - If this ident has a database, it must match the other's database (if present)
71    /// - Missing qualifiers in either ident are treated as wildcards
72    ///
73    /// # Examples
74    ///
75    /// ```text
76    /// "table"              matches "schema.table"           ✓
77    /// "schema.table"       matches "db.schema.table"        ✓
78    /// "schema.table"       matches "table"                  ✗ (schema specified but not in other)
79    /// "schema1.table"      matches "schema2.table"          ✗ (schema mismatch)
80    /// "db.schema.table"    matches "db.schema.table"        ✓
81    /// ```
82    pub(crate) fn matches(&self, other: &DatabaseIdent) -> bool {
83        if self.object != other.object {
84            return false;
85        }
86
87        // If we have a schema specified, it must match
88        if let Some(ref our_schema) = self.schema
89            && let Some(ref their_schema) = other.schema
90            && our_schema != their_schema
91        {
92            return false;
93        }
94
95        // If we have a database specified, it must match
96        if let Some(ref our_db) = self.database
97            && let Some(ref their_db) = other.database
98            && our_db != their_db
99        {
100            return false;
101        }
102
103        true
104    }
105}
106
107/// A Materialize cluster reference.
108///
109/// Clusters in Materialize are non-namespaced objects that can be referenced
110/// by indexes and materialized views via `IN CLUSTER <name>` clauses.
111///
112/// This struct provides type safety for cluster references and allows for
113/// future extensibility (e.g., tracking cluster size, replicas, etc.).
114#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
115pub struct Cluster {
116    pub name: String,
117}
118
119impl Cluster {
120    /// Creates a new cluster reference with the given name.
121    pub fn new(name: String) -> Self {
122        Self { name }
123    }
124}
125
126impl<T: AsRef<str>> From<T> for Cluster {
127    fn from(value: T) -> Self {
128        Self::new(value.as_ref().to_string())
129    }
130}
131
132impl std::fmt::Display for Cluster {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        write!(f, "{}", self.name)
135    }
136}
137
138/// A validated SQL statement representing a database object.
139///
140/// This enum wraps all supported CREATE statements from Materialize's SQL dialect.
141/// Each variant contains the parsed AST node with the `Raw` resolution state.
142#[derive(Debug, Clone, Hash)]
143pub enum Statement {
144    /// CREATE SINK statement
145    CreateSink(CreateSinkStatement<Raw>),
146    /// CREATE VIEW statement
147    CreateView(CreateViewStatement<Raw>),
148    /// CREATE MATERIALIZED VIEW statement
149    CreateMaterializedView(CreateMaterializedViewStatement<Raw>),
150    /// CREATE TABLE statement
151    CreateTable(CreateTableStatement<Raw>),
152    /// CREATE TABLE ... FROM SOURCE statement
153    CreateTableFromSource(CreateTableFromSourceStatement<Raw>),
154    /// CREATE SOURCE statement
155    CreateSource(CreateSourceStatement<Raw>),
156    /// CREATE SECRET statement
157    CreateSecret(CreateSecretStatement<Raw>),
158    /// CREATE CONNECTION statement
159    CreateConnection(CreateConnectionStatement<Raw>),
160}
161
162impl Statement {
163    /// Returns the [`ObjectKind`] corresponding to this statement variant.
164    pub fn kind(&self) -> ObjectKind {
165        match self {
166            Statement::CreateTable(_) | Statement::CreateTableFromSource(_) => ObjectKind::Table,
167            Statement::CreateView(_) => ObjectKind::View,
168            Statement::CreateMaterializedView(_) => ObjectKind::MaterializedView,
169            Statement::CreateSource(_) => ObjectKind::Source,
170            Statement::CreateSink(_) => ObjectKind::Sink,
171            Statement::CreateSecret(_) => ObjectKind::Secret,
172            Statement::CreateConnection(_) => ObjectKind::Connection,
173        }
174    }
175
176    /// Convert into the parser's `Statement<Raw>` enum without re-parsing.
177    pub fn into_parser_statement(self) -> mz_sql_parser::ast::Statement<Raw> {
178        use mz_sql_parser::ast::Statement as Parser;
179        match self {
180            Statement::CreateSink(s) => Parser::CreateSink(s),
181            Statement::CreateView(s) => Parser::CreateView(s),
182            Statement::CreateMaterializedView(s) => Parser::CreateMaterializedView(s),
183            Statement::CreateTable(s) => Parser::CreateTable(s),
184            Statement::CreateTableFromSource(s) => Parser::CreateTableFromSource(s),
185            Statement::CreateSource(s) => Parser::CreateSource(s),
186            Statement::CreateSecret(s) => Parser::CreateSecret(s),
187            Statement::CreateConnection(s) => Parser::CreateConnection(s),
188        }
189    }
190
191    /// Extracts the database identifier from the statement.
192    ///
193    /// Returns the object name (potentially qualified with schema/database)
194    /// declared in the CREATE statement.
195    pub fn ident(&self) -> DatabaseIdent {
196        match self {
197            Statement::CreateSink(s) => s
198                .name
199                .clone()
200                .expect("CREATE SINK statement should have a name")
201                .into(),
202            Statement::CreateView(v) => v.definition.name.clone().into(),
203            Statement::CreateMaterializedView(m) => m.name.clone().into(),
204            Statement::CreateTable(t) => t.name.clone().into(),
205            Statement::CreateTableFromSource(t) => t.name.clone().into(),
206            Statement::CreateSource(s) => s.name.clone().into(),
207            Statement::CreateSecret(s) => s.name.clone().into(),
208            Statement::CreateConnection(c) => c.name.clone().into(),
209        }
210    }
211}
212
213impl std::fmt::Display for Statement {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            Statement::CreateSink(s) => write!(f, "{}", s),
217            Statement::CreateView(s) => write!(f, "{}", s),
218            Statement::CreateMaterializedView(s) => write!(f, "{}", s),
219            Statement::CreateTable(s) => write!(f, "{}", s),
220            Statement::CreateTableFromSource(s) => write!(f, "{}", s),
221            Statement::CreateSource(s) => write!(f, "{}", s),
222            Statement::CreateSecret(s) => write!(f, "{}", s),
223            Statement::CreateConnection(c) => write!(f, "{}", c),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use std::collections::BTreeSet;
232
233    #[mz_ore::test]
234    fn test_cluster_creation() {
235        let cluster = Cluster::new("quickstart".to_string());
236        assert_eq!(cluster.name, "quickstart");
237    }
238
239    #[mz_ore::test]
240    fn test_cluster_equality() {
241        let c1 = Cluster::new("quickstart".to_string());
242        let c2 = Cluster::new("quickstart".to_string());
243        let c3 = Cluster::new("prod".to_string());
244
245        assert_eq!(c1, c2);
246        assert_ne!(c1, c3);
247        assert_ne!(c2, c3);
248    }
249
250    #[mz_ore::test]
251    fn test_cluster_clone() {
252        let c1 = Cluster::new("quickstart".to_string());
253        let c2 = c1.clone();
254
255        assert_eq!(c1, c2);
256        assert_eq!(c1.name, c2.name);
257    }
258
259    #[mz_ore::test]
260    fn test_cluster_hash_consistency() {
261        use std::collections::hash_map::DefaultHasher;
262        use std::hash::{Hash, Hasher};
263
264        let c1 = Cluster::new("quickstart".to_string());
265        let c2 = Cluster::new("quickstart".to_string());
266
267        let mut hasher1 = DefaultHasher::new();
268        c1.hash(&mut hasher1);
269        let hash1 = hasher1.finish();
270
271        let mut hasher2 = DefaultHasher::new();
272        c2.hash(&mut hasher2);
273        let hash2 = hasher2.finish();
274
275        assert_eq!(hash1, hash2, "Equal clusters should have equal hashes");
276    }
277
278    #[mz_ore::test]
279    fn test_cluster_in_hashset() {
280        let mut clusters = BTreeSet::new();
281
282        assert!(clusters.insert(Cluster::new("quickstart".to_string())));
283        assert!(!clusters.insert(Cluster::new("quickstart".to_string()))); // duplicate
284        assert!(clusters.insert(Cluster::new("prod".to_string())));
285
286        assert_eq!(clusters.len(), 2);
287        assert!(clusters.contains(&Cluster::new("quickstart".to_string())));
288        assert!(clusters.contains(&Cluster::new("prod".to_string())));
289        assert!(!clusters.contains(&Cluster::new("staging".to_string())));
290    }
291
292    #[mz_ore::test]
293    fn test_database_ident_matches() {
294        // Object name only
295        let ident1 = DatabaseIdent {
296            database: None,
297            schema: None,
298            object: Ident::new_unchecked("table"),
299        };
300
301        let ident2 = DatabaseIdent {
302            database: Some(Ident::new_unchecked("db")),
303            schema: Some(Ident::new_unchecked("public")),
304            object: Ident::new_unchecked("table"),
305        };
306
307        assert!(ident1.matches(&ident2));
308
309        // Schema qualified
310        let ident3 = DatabaseIdent {
311            database: None,
312            schema: Some(Ident::new_unchecked("public")),
313            object: Ident::new_unchecked("table"),
314        };
315
316        assert!(ident3.matches(&ident2));
317
318        // Schema mismatch
319        let ident4 = DatabaseIdent {
320            database: None,
321            schema: Some(Ident::new_unchecked("private")),
322            object: Ident::new_unchecked("table"),
323        };
324
325        assert!(!ident4.matches(&ident2));
326    }
327}