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}