Skip to main content

mz_deploy/project/compiler/object_validation/
references.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//! Reference validation for supporting statements.
11//!
12//! Validates that indexes, grants, and comments reference the main object
13//! defined in the same file, ensuring each file is self-contained.
14
15use crate::project::ast::DatabaseIdent;
16use crate::project::error::{ValidationError, ValidationErrorKind};
17use crate::project::ir::compiled::FullyQualifiedName;
18use mz_sql_parser::ast::*;
19
20/// Validates that all CREATE INDEX statements reference the main object.
21///
22/// Ensures that every index defined in the file is created on the object
23/// defined in the same file. This maintains the principle that each file
24/// is self-contained.
25///
26/// # Example
27///
28/// Valid:
29/// ```sql
30/// CREATE TABLE users (id INT, name TEXT);
31/// CREATE INDEX users_id_idx ON users (id);
32/// ```
33///
34/// Invalid:
35/// ```sql
36/// CREATE TABLE users (id INT, name TEXT);
37/// CREATE INDEX orders_id_idx ON orders (id);  -- wrong object
38/// ```
39pub(super) fn validate_index_references(
40    fqn: &FullyQualifiedName,
41    indexes: &[CreateIndexStatement<Raw>],
42    offsets: &[usize],
43    main_ident: &DatabaseIdent,
44    errors: &mut Vec<ValidationError>,
45) {
46    for (i, index) in indexes.iter().enumerate() {
47        let on: DatabaseIdent = index.on_name.name().clone().into();
48        if !on.matches(main_ident) {
49            let index_sql = format!("{};", index);
50            errors.push(ValidationError::with_file_sql_and_offset(
51                ValidationErrorKind::IndexReferenceMismatch {
52                    referenced: on.object.to_string(),
53                    expected: main_ident.object.to_string(),
54                },
55                fqn.path.clone(),
56                index_sql,
57                offsets[i],
58            ));
59        }
60    }
61}
62
63/// Validates that all GRANT statements reference the main object with the correct type.
64///
65/// Ensures that:
66/// 1. Every grant targets the object defined in the same file
67/// 2. The object type in the GRANT matches the actual object type
68/// 3. Only supported grant types are used (no SYSTEM grants, no ALL TABLES IN SCHEMA)
69///
70/// # Object Type Handling
71///
72/// Materialize's GRANT syntax has specific requirements:
73/// - Tables, views, materialized views, and sources all use `GRANT ... ON TABLE`
74/// - Other objects (connections, secrets, sinks) use their specific type
75///
76/// # Supported Grants
77///
78/// - `GRANT ... ON TABLE` - for tables, views, materialized views, sources
79/// - `GRANT ... ON CONNECTION` - for connections
80/// - `GRANT ... ON SECRET` - for secrets
81/// - `GRANT ... ON SINK` - for sinks
82///
83/// # Example
84///
85/// Valid:
86/// ```sql
87/// CREATE TABLE users (...);
88/// GRANT SELECT ON TABLE users TO analyst_role;
89/// ```
90///
91/// Invalid:
92/// ```sql
93/// CREATE TABLE users (...);
94/// GRANT SELECT ON orders TO analyst_role;  -- wrong object
95/// ```
96pub(super) fn validate_grant_references(
97    fqn: &FullyQualifiedName,
98    grants: &[GrantPrivilegesStatement<Raw>],
99    offsets: &[usize],
100    main_ident: &DatabaseIdent,
101    main_object_type: ObjectType,
102    errors: &mut Vec<ValidationError>,
103) {
104    for (i, grant) in grants.iter().enumerate() {
105        let offset = offsets[i];
106        let grant_sql = format!("{};", grant);
107
108        match &grant.target {
109            GrantTargetSpecification::Object {
110                object_type,
111                object_spec_inner,
112                ..
113            } => match object_spec_inner {
114                GrantTargetSpecificationInner::Objects { names } => {
115                    check_grant_object_type(
116                        fqn,
117                        main_object_type,
118                        *object_type,
119                        &grant_sql,
120                        offset,
121                        errors,
122                    );
123
124                    for obj in names {
125                        match obj {
126                            UnresolvedObjectName::Item(item_name) => {
127                                let grant_target: DatabaseIdent = item_name.clone().into();
128                                if !grant_target.matches(main_ident) {
129                                    errors.push(ValidationError::with_file_sql_and_offset(
130                                        ValidationErrorKind::GrantReferenceMismatch {
131                                            referenced: grant_target.object.to_string(),
132                                            expected: main_ident.object.to_string(),
133                                        },
134                                        fqn.path.clone(),
135                                        grant_sql.clone(),
136                                        offset,
137                                    ));
138                                }
139                            }
140                            _ => {
141                                // skip
142                            }
143                        }
144                    }
145                }
146                GrantTargetSpecificationInner::All(_) => {
147                    errors.push(ValidationError::with_file_sql_and_offset(
148                        ValidationErrorKind::GrantMustTargetObject,
149                        fqn.path.clone(),
150                        grant_sql.clone(),
151                        offset,
152                    ));
153                }
154            },
155            GrantTargetSpecification::System => {
156                errors.push(ValidationError::with_file_sql_and_offset(
157                    ValidationErrorKind::SystemGrantUnsupported,
158                    fqn.path.clone(),
159                    grant_sql,
160                    offset,
161                ));
162            }
163        }
164    }
165}
166
167/// Validates that the GRANT statement uses the correct object type for the target object.
168///
169/// Materialize has specific rules about which object types can be used in GRANT statements:
170///
171/// # Type Mapping Rules
172///
173/// - **Tables, Views, Materialized Views, Sources**: Must use `GRANT ... ON TABLE`
174///   - This is because Materialize treats these objects similarly for privilege management
175/// - **Connections, Secrets, Sinks**: Must use their specific type
176///   - e.g., `GRANT ... ON CONNECTION`, `GRANT ... ON SECRET`
177///
178/// # Examples
179///
180/// ```sql
181/// -- For a table
182/// GRANT SELECT ON TABLE users TO role;
183///
184/// -- For a materialized view
185/// GRANT SELECT ON TABLE my_mv TO role;
186/// GRANT SELECT ON MATERIALIZED VIEW my_mv TO role;  -- invalid
187///
188/// -- For a connection
189/// GRANT USAGE ON CONNECTION kafka_conn TO role;
190/// ```
191fn check_grant_object_type(
192    fqn: &FullyQualifiedName,
193    main_object_type: ObjectType,
194    grant_object_type: ObjectType,
195    grant_sql: &str,
196    offset: usize,
197    errors: &mut Vec<ValidationError>,
198) {
199    if matches!(
200        main_object_type,
201        ObjectType::Table | ObjectType::Source | ObjectType::View | ObjectType::MaterializedView
202    ) {
203        if grant_object_type != ObjectType::Table {
204            errors.push(ValidationError::with_file_sql_and_offset(
205                ValidationErrorKind::GrantTypeMismatch {
206                    grant_type: format!("{}", grant_object_type),
207                    expected_type: "TABLE".to_string(),
208                },
209                fqn.path.clone(),
210                grant_sql.to_string(),
211                offset,
212            ));
213        }
214    } else if grant_object_type != main_object_type {
215        errors.push(ValidationError::with_file_sql_and_offset(
216            ValidationErrorKind::GrantTypeMismatch {
217                grant_type: format!("{}", grant_object_type),
218                expected_type: format!("{}", main_object_type),
219            },
220            fqn.path.clone(),
221            grant_sql.to_string(),
222            offset,
223        ));
224    }
225}
226
227/// Validates that a COMMENT statement targets the correct object with the correct type.
228///
229/// Ensures that:
230/// 1. The comment references the main object defined in the file
231/// 2. The object type specified in the COMMENT matches the actual object type
232fn validate_comment_target(
233    comment_name: &RawItemName,
234    main_ident: &DatabaseIdent,
235    main_obj_type: &ObjectType,
236    comment_obj_type: ObjectType,
237    fqn: &FullyQualifiedName,
238    comment_sql: &str,
239    offset: usize,
240    errors: &mut Vec<ValidationError>,
241) {
242    let comment_target: DatabaseIdent = comment_name.name().clone().into();
243
244    // Check that the comment references the main object
245    if !comment_target.matches(main_ident) {
246        errors.push(ValidationError::with_file_sql_and_offset(
247            ValidationErrorKind::CommentReferenceMismatch {
248                referenced: comment_target.object.to_string(),
249                expected: main_ident.object.to_string(),
250            },
251            fqn.path.clone(),
252            comment_sql.to_string(),
253            offset,
254        ));
255    }
256
257    // Check that the comment type matches the object type
258    if *main_obj_type != comment_obj_type {
259        errors.push(ValidationError::with_file_sql_and_offset(
260            ValidationErrorKind::CommentTypeMismatch {
261                comment_type: format!("{:?}", comment_obj_type),
262                object_type: format!("{:?}", main_obj_type),
263            },
264            fqn.path.clone(),
265            comment_sql.to_string(),
266            offset,
267        ));
268    }
269}
270
271/// Extract the target name and ObjectType from a CommentObjectType.
272///
273/// Returns Some((name, object_type)) for supported comment types, None for unsupported types.
274/// Column comments are handled separately since they reference the parent table.
275fn comment_object_to_target(obj: &CommentObjectType<Raw>) -> Option<(&RawItemName, ObjectType)> {
276    match obj {
277        CommentObjectType::Table { name } => Some((name, ObjectType::Table)),
278        CommentObjectType::View { name } => Some((name, ObjectType::View)),
279        CommentObjectType::MaterializedView { name } => Some((name, ObjectType::MaterializedView)),
280        CommentObjectType::Source { name } => Some((name, ObjectType::Source)),
281        CommentObjectType::Sink { name } => Some((name, ObjectType::Sink)),
282        CommentObjectType::Connection { name } => Some((name, ObjectType::Connection)),
283        CommentObjectType::Secret { name } => Some((name, ObjectType::Secret)),
284        // Column, Index, Func, Type, Role, Database, Schema, Cluster, etc. are not supported
285        // or handled separately
286        _ => None,
287    }
288}
289
290/// Validates that all COMMENT statements in a file reference the main object.
291///
292/// Processes all COMMENT statements and ensures they target either:
293/// - The main object defined in the file, OR
294/// - A column of the main object
295///
296/// This validation ensures that each object file is self-contained and doesn't
297/// reference other objects.
298///
299/// # Supported Comment Types
300///
301/// - `COMMENT ON TABLE` - for tables and materialized views
302/// - `COMMENT ON VIEW` - for views
303/// - `COMMENT ON MATERIALIZED VIEW` - for materialized views
304/// - `COMMENT ON SOURCE` - for sources and subsources
305/// - `COMMENT ON SINK` - for sinks
306/// - `COMMENT ON CONNECTION` - for connections
307/// - `COMMENT ON SECRET` - for secrets
308/// - `COMMENT ON COLUMN` - for columns of the main object
309///
310/// # Errors
311///
312/// Returns an error if:
313/// - A comment references a different object
314/// - The comment type doesn't match the object type
315/// - An unsupported comment type is used
316pub(super) fn validate_comment_references(
317    fqn: &FullyQualifiedName,
318    comments: &[CommentStatement<Raw>],
319    offsets: &[usize],
320    main_ident: &DatabaseIdent,
321    obj_type: &ObjectType,
322    errors: &mut Vec<ValidationError>,
323) {
324    for (i, comment) in comments.iter().enumerate() {
325        let offset = offsets[i];
326        let comment_sql = format!("{};", comment);
327
328        // Handle column comments specially (they reference the parent table)
329        if let CommentObjectType::Column { name } = &comment.object {
330            let column_parent: DatabaseIdent = name.relation.name().clone().into();
331            if !column_parent.matches(main_ident) {
332                errors.push(ValidationError::with_file_sql_and_offset(
333                    ValidationErrorKind::ColumnCommentReferenceMismatch {
334                        referenced: column_parent.object.to_string(),
335                        expected: main_ident.object.to_string(),
336                    },
337                    fqn.path.clone(),
338                    comment_sql,
339                    offset,
340                ));
341            }
342            continue;
343        }
344
345        // Handle supported object types
346        if let Some((name, comment_obj_type)) = comment_object_to_target(&comment.object) {
347            validate_comment_target(
348                name,
349                main_ident,
350                obj_type,
351                comment_obj_type,
352                fqn,
353                &comment_sql,
354                offset,
355                errors,
356            );
357            continue;
358        }
359
360        // Unsupported comment type
361        errors.push(ValidationError::with_file_sql_and_offset(
362            ValidationErrorKind::UnsupportedCommentType,
363            fqn.path.clone(),
364            comment_sql,
365            offset,
366        ));
367    }
368}