1use crate::types::ObjectKind;
18use std::fmt;
19use std::path::PathBuf;
20
21#[derive(Debug, Clone)]
26pub struct ErrorContext {
27 pub file: PathBuf,
29 pub sql_statement: Option<String>,
31 pub byte_offset: Option<usize>,
37}
38
39#[derive(Debug)]
44pub struct ValidationError {
45 pub kind: ValidationErrorKind,
47 pub context: ErrorContext,
49}
50
51impl fmt::Display for ValidationError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.kind.message())
54 }
55}
56
57impl std::error::Error for ValidationError {
58 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59 None
60 }
61}
62
63impl ValidationError {
64 pub fn with_context(kind: ValidationErrorKind, context: ErrorContext) -> Self {
66 Self { kind, context }
67 }
68
69 pub fn with_file(kind: ValidationErrorKind, file: PathBuf) -> Self {
71 Self {
72 kind,
73 context: ErrorContext {
74 file,
75 sql_statement: None,
76 byte_offset: None,
77 },
78 }
79 }
80
81 pub fn with_file_and_sql(kind: ValidationErrorKind, file: PathBuf, sql: String) -> Self {
83 Self {
84 kind,
85 context: ErrorContext {
86 file,
87 sql_statement: Some(sql),
88 byte_offset: None,
89 },
90 }
91 }
92
93 pub fn with_file_and_offset(
98 kind: ValidationErrorKind,
99 file: PathBuf,
100 byte_offset: usize,
101 ) -> Self {
102 Self {
103 kind,
104 context: ErrorContext {
105 file,
106 sql_statement: None,
107 byte_offset: Some(byte_offset),
108 },
109 }
110 }
111
112 pub fn with_file_sql_and_offset(
117 kind: ValidationErrorKind,
118 file: PathBuf,
119 sql: String,
120 byte_offset: usize,
121 ) -> Self {
122 Self {
123 kind,
124 context: ErrorContext {
125 file,
126 sql_statement: Some(sql),
127 byte_offset: Some(byte_offset),
128 },
129 }
130 }
131}
132
133#[derive(Debug)]
138pub enum ValidationErrorKind {
139 MultipleMainStatements { object_name: String },
141 NoMainStatement { object_name: String },
143 ObjectNameMismatch { declared: String, expected: String },
145 SchemaMismatch { declared: String, expected: String },
147 DatabaseMismatch { declared: String, expected: String },
149 IndexReferenceMismatch {
151 referenced: String,
152 expected: String,
153 },
154 GrantReferenceMismatch {
156 referenced: String,
157 expected: String,
158 },
159 CommentReferenceMismatch {
161 referenced: String,
162 expected: String,
163 },
164 ColumnCommentReferenceMismatch {
166 referenced: String,
167 expected: String,
168 },
169 CommentTypeMismatch {
171 comment_type: String,
172 object_type: String,
173 },
174 GrantTypeMismatch {
176 grant_type: String,
177 expected_type: String,
178 },
179 UnsupportedStatement {
181 object_name: String,
182 statement_type: String,
183 },
184 ClusterGrantUnsupported,
186 GrantMustTargetObject,
188 SystemGrantUnsupported,
190 UnsupportedCommentType,
192 NoObjectType,
194 SchemaExtractionFailed,
196 DatabaseExtractionFailed,
198 InvalidIdentifier { name: String, reason: String },
200 IndexMissingCluster { index_name: String },
202 IndexOnStorageObject {
204 object_type: String,
205 object_name: String,
206 index_name: String,
207 },
208 MaterializedViewMissingCluster { view_name: String },
210 SinkMissingCluster { sink_name: String },
212 SourceMissingCluster { source_name: String },
214 SourceExternalReferences { source_name: String },
216 InvalidDatabaseModStatement {
218 statement_type: String,
219 database_name: String,
220 },
221 DatabaseModCommentTargetMismatch {
223 target: String,
224 database_name: String,
225 },
226 DatabaseModGrantTargetMismatch {
228 target: String,
229 database_name: String,
230 },
231 InvalidSchemaModStatement {
233 statement_type: String,
234 schema_name: String,
235 },
236 SchemaModCommentTargetMismatch { target: String, schema_name: String },
238 SchemaModGrantTargetMismatch { target: String, schema_name: String },
240 InvalidSetVariable { variable: String, value: String },
242 AlterDefaultPrivilegesRequiresDatabaseScope { database_name: String },
244 AlterDefaultPrivilegesRequiresSchemaScope { schema_name: String },
246 AlterDefaultPrivilegesDatabaseMismatch {
248 referenced: String,
249 expected: String,
250 },
251 AlterDefaultPrivilegesSchemaNotAllowed { database_name: String },
253 AlterDefaultPrivilegesDatabaseNotAllowed { schema_name: String },
255 AlterDefaultPrivilegesSchemaMismatch {
257 referenced: String,
258 expected: String,
259 },
260 StorageAndComputationObjectsInSameSchema {
262 schema_name: String,
263 storage_objects: Vec<String>,
264 computation_objects: Vec<String>,
265 },
266 ReplacementSchemaNonMvObject {
268 database: String,
269 schema: String,
270 object_name: String,
271 object_type: ObjectKind,
272 },
273 InvalidClusterStatement {
275 statement_type: String,
276 cluster_name: String,
277 },
278 ClusterNameMismatch { declared: String, expected: String },
280 ClusterMissingCreateStatement { cluster_name: String },
282 ClusterMultipleCreateStatements { cluster_name: String },
284 ClusterGrantTargetMismatch {
286 target: String,
287 cluster_name: String,
288 },
289 ClusterCommentTargetMismatch {
291 target: String,
292 cluster_name: String,
293 },
294 InvalidRoleStatement {
296 statement_type: String,
297 role_name: String,
298 },
299 RoleNameMismatch { declared: String, expected: String },
301 RoleMissingCreateStatement { role_name: String },
303 RoleMultipleCreateStatements { role_name: String },
305 RoleAlterTargetMismatch { target: String, role_name: String },
307 RoleGrantTargetMismatch { target: String, role_name: String },
309 RoleCommentTargetMismatch { target: String, role_name: String },
311 InvalidNetworkPolicyStatement {
313 statement_type: String,
314 policy_name: String,
315 },
316 NetworkPolicyNameMismatch { declared: String, expected: String },
318 NetworkPolicyMissingCreateStatement { policy_name: String },
320 NetworkPolicyMultipleCreateStatements { policy_name: String },
322 NetworkPolicyGrantTargetMismatch { target: String, policy_name: String },
324 NetworkPolicyCommentTargetMismatch { target: String, policy_name: String },
326 ProfileObjectTypeMismatch {
328 object_name: String,
329 default_type: String,
330 override_profile: String,
331 override_type: String,
332 default_path: PathBuf,
333 override_path: PathBuf,
334 },
335 ProfileOverrideNotAllowed {
337 object_name: String,
338 object_type: String,
339 override_profile: String,
340 override_path: PathBuf,
341 },
342}
343
344impl ValidationErrorKind {
345 pub(crate) fn message(&self) -> String {
347 match self {
348 Self::MultipleMainStatements { object_name } => {
349 format!(
350 "multiple main CREATE statements found for object '{}'",
351 object_name
352 )
353 }
354 Self::NoMainStatement { object_name } => {
355 format!(
356 "no main CREATE statement found for object '{}'",
357 object_name
358 )
359 }
360 Self::ObjectNameMismatch { declared, expected } => {
361 format!(
362 "object name mismatch: declared '{}', expected '{}'",
363 declared, expected
364 )
365 }
366 Self::SchemaMismatch { declared, expected } => {
367 format!(
368 "schema qualifier mismatch: declared '{}', expected '{}'",
369 declared, expected
370 )
371 }
372 Self::DatabaseMismatch { declared, expected } => {
373 format!(
374 "database qualifier mismatch: declared '{}', expected '{}'",
375 declared, expected
376 )
377 }
378 Self::IndexReferenceMismatch {
379 referenced,
380 expected,
381 } => {
382 format!(
383 "INDEX references wrong object: '{}' instead of '{}'",
384 referenced, expected
385 )
386 }
387 Self::GrantReferenceMismatch {
388 referenced,
389 expected,
390 } => {
391 format!(
392 "GRANT references wrong object: '{}' instead of '{}'",
393 referenced, expected
394 )
395 }
396 Self::CommentReferenceMismatch {
397 referenced,
398 expected,
399 } => {
400 format!(
401 "COMMENT references wrong object: '{}' instead of '{}'",
402 referenced, expected
403 )
404 }
405 Self::ColumnCommentReferenceMismatch {
406 referenced,
407 expected,
408 } => {
409 format!(
410 "column COMMENT references wrong table: '{}' instead of '{}'",
411 referenced, expected
412 )
413 }
414 Self::CommentTypeMismatch {
415 comment_type,
416 object_type,
417 } => {
418 format!(
419 "COMMENT uses wrong object type: {} instead of {}",
420 comment_type, object_type
421 )
422 }
423 Self::GrantTypeMismatch {
424 grant_type,
425 expected_type,
426 } => {
427 format!(
428 "GRANT uses incorrect object type: GRANT ON {} instead of GRANT ON {}",
429 grant_type, expected_type
430 )
431 }
432 Self::UnsupportedStatement {
433 object_name,
434 statement_type,
435 } => {
436 format!(
437 "unsupported statement type in object '{}': {}",
438 object_name, statement_type
439 )
440 }
441 Self::ClusterGrantUnsupported => "CLUSTER grants are not supported".to_string(),
442 Self::GrantMustTargetObject => "GRANT must target a specific object".to_string(),
443 Self::SystemGrantUnsupported => "SYSTEM grants are not supported".to_string(),
444 Self::UnsupportedCommentType => "unsupported COMMENT object type".to_string(),
445 Self::NoObjectType => "could not determine object type".to_string(),
446 Self::SchemaExtractionFailed => {
447 "failed to extract schema name from file path".to_string()
448 }
449 Self::DatabaseExtractionFailed => {
450 "failed to extract database name from file path".to_string()
451 }
452 Self::InvalidIdentifier { name, reason } => {
453 format!("invalid identifier '{}': {}", name, reason)
454 }
455 Self::IndexMissingCluster { index_name } => {
456 format!(
457 "index '{}' is missing required IN CLUSTER clause",
458 index_name
459 )
460 }
461 Self::IndexOnStorageObject {
462 object_type,
463 object_name,
464 index_name,
465 } => {
466 format!(
467 "index '{}' is not supported on {} '{}'",
468 index_name, object_type, object_name
469 )
470 }
471 Self::MaterializedViewMissingCluster { view_name } => {
472 format!(
473 "materialized view '{}' is missing required IN CLUSTER clause",
474 view_name
475 )
476 }
477 Self::SinkMissingCluster { sink_name } => {
478 format!("sink '{}' is missing required IN CLUSTER clause", sink_name)
479 }
480 Self::SourceMissingCluster { source_name } => {
481 format!(
482 "source '{}' is missing required IN CLUSTER clause",
483 source_name
484 )
485 }
486 Self::SourceExternalReferences { source_name } => {
487 format!(
488 "source '{}' uses FOR TABLES, FOR SCHEMAS, or FOR ALL TABLES which is not supported",
489 source_name
490 )
491 }
492 Self::InvalidDatabaseModStatement {
493 statement_type,
494 database_name,
495 } => {
496 format!(
497 "invalid statement type in database mod file '{}': {}. Only COMMENT ON DATABASE, GRANT ON DATABASE, and ALTER DEFAULT PRIVILEGES are allowed",
498 database_name, statement_type
499 )
500 }
501 Self::DatabaseModCommentTargetMismatch {
502 target,
503 database_name,
504 } => {
505 format!(
506 "comment in database mod file must target the database itself. Expected COMMENT ON DATABASE '{}', but found COMMENT ON {}",
507 database_name, target
508 )
509 }
510 Self::DatabaseModGrantTargetMismatch {
511 target,
512 database_name,
513 } => {
514 format!(
515 "grant in database mod file must target the database itself. Expected GRANT ON DATABASE '{}', but found GRANT ON {}",
516 database_name, target
517 )
518 }
519 Self::InvalidSchemaModStatement {
520 statement_type,
521 schema_name,
522 } => {
523 format!(
524 "invalid statement type in schema mod file '{}': {}. Only COMMENT ON SCHEMA, GRANT ON SCHEMA, ALTER DEFAULT PRIVILEGES, and SET api = stable are allowed",
525 schema_name, statement_type
526 )
527 }
528 Self::SchemaModCommentTargetMismatch {
529 target,
530 schema_name,
531 } => {
532 format!(
533 "comment in schema mod file must target the schema itself. Expected COMMENT ON SCHEMA '{}', but found COMMENT ON {}",
534 schema_name, target
535 )
536 }
537 Self::SchemaModGrantTargetMismatch {
538 target,
539 schema_name,
540 } => {
541 format!(
542 "grant in schema mod file must target the schema itself. Expected GRANT ON SCHEMA '{}', but found GRANT ON {}",
543 schema_name, target
544 )
545 }
546 Self::InvalidSetVariable { variable, value } => {
547 format!("invalid value for SET {}: {}", variable, value)
548 }
549 Self::AlterDefaultPrivilegesRequiresDatabaseScope { database_name } => {
550 format!(
551 "ALTER DEFAULT PRIVILEGES in database mod file '{}' must specify IN DATABASE",
552 database_name
553 )
554 }
555 Self::AlterDefaultPrivilegesRequiresSchemaScope { schema_name } => {
556 format!(
557 "ALTER DEFAULT PRIVILEGES in schema mod file '{}' must specify IN SCHEMA",
558 schema_name
559 )
560 }
561 Self::AlterDefaultPrivilegesDatabaseMismatch {
562 referenced,
563 expected,
564 } => {
565 format!(
566 "ALTER DEFAULT PRIVILEGES IN DATABASE references wrong database: '{}' instead of '{}'",
567 referenced, expected
568 )
569 }
570 Self::AlterDefaultPrivilegesSchemaNotAllowed { database_name } => {
571 format!(
572 "ALTER DEFAULT PRIVILEGES in database mod file '{}' cannot use IN SCHEMA",
573 database_name
574 )
575 }
576 Self::AlterDefaultPrivilegesDatabaseNotAllowed { schema_name } => {
577 format!(
578 "ALTER DEFAULT PRIVILEGES in schema mod file '{}' cannot use IN DATABASE",
579 schema_name
580 )
581 }
582 Self::AlterDefaultPrivilegesSchemaMismatch {
583 referenced,
584 expected,
585 } => {
586 format!(
587 "ALTER DEFAULT PRIVILEGES IN SCHEMA references wrong schema: '{}' instead of '{}'",
588 referenced, expected
589 )
590 }
591 Self::StorageAndComputationObjectsInSameSchema {
592 schema_name,
593 storage_objects,
594 computation_objects,
595 } => {
596 format!(
597 "schema '{}' contains both storage objects (tables/sinks) and computation objects (views/materialized views)\n \
598 Storage objects (tables/sinks): [{}]\n \
599 Computation objects (views/MVs): [{}]",
600 schema_name,
601 storage_objects.join(", "),
602 computation_objects.join(", ")
603 )
604 }
605 Self::ReplacementSchemaNonMvObject {
606 database,
607 schema,
608 object_name,
609 object_type: kind,
610 } => {
611 format!(
612 "replacement schema '{}.{}' contains non-materialized-view object '{}' (kind: {})",
613 database, schema, object_name, kind
614 )
615 }
616 Self::InvalidClusterStatement {
617 statement_type,
618 cluster_name,
619 } => {
620 format!(
621 "invalid statement type in cluster file '{}': {}. Only CREATE CLUSTER, GRANT ON CLUSTER, and COMMENT ON CLUSTER are allowed",
622 cluster_name, statement_type
623 )
624 }
625 Self::ClusterNameMismatch { declared, expected } => {
626 format!(
627 "cluster name mismatch: declared '{}', expected '{}'",
628 declared, expected
629 )
630 }
631 Self::ClusterMissingCreateStatement { cluster_name } => {
632 format!(
633 "no CREATE CLUSTER statement found in cluster file '{}'",
634 cluster_name
635 )
636 }
637 Self::ClusterMultipleCreateStatements { cluster_name } => {
638 format!(
639 "multiple CREATE CLUSTER statements found in cluster file '{}'",
640 cluster_name
641 )
642 }
643 Self::ClusterGrantTargetMismatch {
644 target,
645 cluster_name,
646 } => {
647 format!(
648 "GRANT in cluster file '{}' targets wrong cluster: '{}'",
649 cluster_name, target
650 )
651 }
652 Self::ClusterCommentTargetMismatch {
653 target,
654 cluster_name,
655 } => {
656 format!(
657 "COMMENT in cluster file '{}' targets wrong cluster: '{}'",
658 cluster_name, target
659 )
660 }
661 Self::InvalidRoleStatement {
662 statement_type,
663 role_name,
664 } => {
665 format!(
666 "invalid statement type in role file '{}': {}. Only CREATE ROLE, ALTER ROLE, GRANT ROLE, and COMMENT ON ROLE are allowed",
667 role_name, statement_type
668 )
669 }
670 Self::RoleNameMismatch { declared, expected } => {
671 format!(
672 "role name mismatch: declared '{}', expected '{}'",
673 declared, expected
674 )
675 }
676 Self::RoleMissingCreateStatement { role_name } => {
677 format!(
678 "no CREATE ROLE statement found in role file '{}'",
679 role_name
680 )
681 }
682 Self::RoleMultipleCreateStatements { role_name } => {
683 format!(
684 "multiple CREATE ROLE statements found in role file '{}'",
685 role_name
686 )
687 }
688 Self::RoleAlterTargetMismatch { target, role_name } => {
689 format!(
690 "ALTER ROLE in role file '{}' targets wrong role: '{}'",
691 role_name, target
692 )
693 }
694 Self::RoleGrantTargetMismatch { target, role_name } => {
695 format!(
696 "GRANT ROLE in role file '{}' targets wrong role: '{}'",
697 role_name, target
698 )
699 }
700 Self::RoleCommentTargetMismatch { target, role_name } => {
701 format!(
702 "COMMENT in role file '{}' targets wrong role: '{}'",
703 role_name, target
704 )
705 }
706 Self::InvalidNetworkPolicyStatement {
707 statement_type,
708 policy_name,
709 } => {
710 format!(
711 "invalid statement type in network policy file '{}': {}. Only CREATE NETWORK POLICY, GRANT ON NETWORK POLICY, and COMMENT ON NETWORK POLICY are allowed",
712 policy_name, statement_type
713 )
714 }
715 Self::NetworkPolicyNameMismatch { declared, expected } => {
716 format!(
717 "network policy name mismatch: declared '{}', expected '{}'",
718 declared, expected
719 )
720 }
721 Self::NetworkPolicyMissingCreateStatement { policy_name } => {
722 format!(
723 "no CREATE NETWORK POLICY statement found in network policy file '{}'",
724 policy_name
725 )
726 }
727 Self::NetworkPolicyMultipleCreateStatements { policy_name } => {
728 format!(
729 "multiple CREATE NETWORK POLICY statements found in network policy file '{}'",
730 policy_name
731 )
732 }
733 Self::NetworkPolicyGrantTargetMismatch {
734 target,
735 policy_name,
736 } => {
737 format!(
738 "GRANT in network policy file '{}' targets wrong policy: '{}'",
739 policy_name, target
740 )
741 }
742 Self::NetworkPolicyCommentTargetMismatch {
743 target,
744 policy_name,
745 } => {
746 format!(
747 "COMMENT in network policy file '{}' targets wrong policy: '{}'",
748 policy_name, target
749 )
750 }
751 Self::ProfileObjectTypeMismatch {
752 object_name,
753 default_type,
754 override_profile,
755 override_type,
756 ..
757 } => {
758 format!(
759 "profile variant type mismatch for object '{}': default is '{}' but '{}' override is '{}'",
760 object_name, default_type, override_profile, override_type
761 )
762 }
763 Self::ProfileOverrideNotAllowed {
764 object_name,
765 object_type,
766 override_profile,
767 ..
768 } => {
769 format!(
770 "{} '{}' cannot have profile-specific overrides (found '{}' override)",
771 object_type, object_name, override_profile
772 )
773 }
774 }
775 }
776
777 pub(crate) fn help(&self) -> Option<String> {
779 match self {
780 Self::MultipleMainStatements { .. } => {
781 Some("each file must contain exactly one primary CREATE statement (TABLE, VIEW, SOURCE, etc.)".to_string())
782 }
783 Self::NoMainStatement { .. } => {
784 Some("each file must contain exactly one primary CREATE statement (CREATE TABLE, CREATE VIEW, etc.)".to_string())
785 }
786 Self::ObjectNameMismatch { .. } => {
787 Some("the object name in your CREATE statement must match the .sql file name".to_string())
788 }
789 Self::SchemaMismatch { .. } => {
790 Some("the schema in your qualified object name must match the directory name".to_string())
791 }
792 Self::DatabaseMismatch { .. } => {
793 Some("the database in your qualified object name must match the directory name".to_string())
794 }
795 Self::IndexReferenceMismatch { .. } => {
796 Some("indexes must be defined in the same file as the object they're created on".to_string())
797 }
798 Self::GrantReferenceMismatch { .. } => {
799 Some("grants must be defined in the same file as the object they apply to".to_string())
800 }
801 Self::CommentReferenceMismatch { .. } => {
802 Some("comments must be defined in the same file as the object they describe".to_string())
803 }
804 Self::ColumnCommentReferenceMismatch { .. } => {
805 Some("column comments must reference columns in the object defined in the file".to_string())
806 }
807 Self::CommentTypeMismatch { .. } => {
808 Some("the COMMENT statement must use the correct object type (TABLE, VIEW, etc.)".to_string())
809 }
810 Self::GrantTypeMismatch { .. } => {
811 Some("the GRANT statement must use the correct object type that matches the object defined in the file".to_string())
812 }
813 Self::UnsupportedStatement { .. } => {
814 Some("only CREATE, INDEX, GRANT, and COMMENT statements are supported in object files".to_string())
815 }
816 Self::ClusterGrantUnsupported => {
817 Some("use GRANT ON specific objects instead of CLUSTER".to_string())
818 }
819 Self::GrantMustTargetObject => {
820 Some("use GRANT ON objectname instead of GRANT ON ALL TABLES or similar".to_string())
821 }
822 Self::SystemGrantUnsupported => {
823 Some("use GRANT ON specific objects instead of SYSTEM".to_string())
824 }
825 Self::UnsupportedCommentType => {
826 Some("only comments on tables, views, sources, sinks, connections, secrets, and columns are supported".to_string())
827 }
828 Self::NoObjectType | Self::SchemaExtractionFailed | Self::DatabaseExtractionFailed => {
829 Some("this is an internal error, please report this issue".to_string())
830 }
831 Self::InvalidIdentifier { .. } => {
832 Some("identifiers must follow SQL naming rules (alphanumeric and underscores, must not start with a digit)".to_string())
833 }
834 Self::IndexMissingCluster { .. } => {
835 Some("add 'IN CLUSTER <cluster_name>' to your CREATE INDEX statement (e.g., CREATE INDEX idx ON table (col) IN CLUSTER quickstart)".to_string())
836 }
837 Self::IndexOnStorageObject { .. } => {
838 Some("indexes are only supported on views and materialized views; to index data from a source or table, create a view that selects from it and index the view".to_string())
839 }
840 Self::MaterializedViewMissingCluster { .. } => {
841 Some("add 'IN CLUSTER <cluster_name>' to your CREATE MATERIALIZED VIEW statement (e.g., CREATE MATERIALIZED VIEW mv IN CLUSTER quickstart AS SELECT ...)".to_string())
842 }
843 Self::SinkMissingCluster { .. } => {
844 Some("add 'IN CLUSTER <cluster_name>' to your CREATE SINK statement (e.g., CREATE SINK sink IN CLUSTER quickstart FROM ...)".to_string())
845 }
846 Self::SourceMissingCluster { .. } => {
847 Some("add 'IN CLUSTER <cluster_name>' to your CREATE SOURCE statement (e.g., CREATE SOURCE src IN CLUSTER quickstart FROM ...)".to_string())
848 }
849 Self::SourceExternalReferences { .. } => {
850 Some("use CREATE TABLE FROM SOURCE to define tables individually so mz-deploy can manage their lifecycle, privileges, and comments".to_string())
851 }
852 Self::InvalidDatabaseModStatement { .. } => {
853 Some("database mod files (e.g., materialize.sql) can only contain COMMENT ON DATABASE, GRANT ON DATABASE, and ALTER DEFAULT PRIVILEGES statements".to_string())
854 }
855 Self::DatabaseModCommentTargetMismatch { .. } => {
856 Some("comments in database mod files must target the database itself using COMMENT ON DATABASE".to_string())
857 }
858 Self::DatabaseModGrantTargetMismatch { .. } => {
859 Some("grants in database mod files must target the database itself using GRANT ON DATABASE".to_string())
860 }
861 Self::InvalidSchemaModStatement { .. } => {
862 Some("schema mod files (e.g., materialize/public.sql) can only contain COMMENT ON SCHEMA, GRANT ON SCHEMA, ALTER DEFAULT PRIVILEGES, and SET api = stable statements".to_string())
863 }
864 Self::SchemaModCommentTargetMismatch { .. } => {
865 Some("comments in schema mod files must target the schema itself using COMMENT ON SCHEMA".to_string())
866 }
867 Self::SchemaModGrantTargetMismatch { .. } => {
868 Some("grants in schema mod files must target the schema itself using GRANT ON SCHEMA".to_string())
869 }
870 Self::InvalidSetVariable { .. } => {
871 Some("only 'SET api = stable' is supported in schema mod files".to_string())
872 }
873 Self::AlterDefaultPrivilegesRequiresDatabaseScope { .. } => {
874 Some("add 'IN DATABASE <database_name>' to your ALTER DEFAULT PRIVILEGES statement".to_string())
875 }
876 Self::AlterDefaultPrivilegesRequiresSchemaScope { .. } => {
877 Some("add 'IN SCHEMA <schema_name>' to your ALTER DEFAULT PRIVILEGES statement".to_string())
878 }
879 Self::AlterDefaultPrivilegesDatabaseMismatch { .. } => {
880 Some("ALTER DEFAULT PRIVILEGES in database mod files must target the database itself".to_string())
881 }
882 Self::AlterDefaultPrivilegesSchemaNotAllowed { .. } => {
883 Some("use IN DATABASE instead of IN SCHEMA in database mod files".to_string())
884 }
885 Self::AlterDefaultPrivilegesDatabaseNotAllowed { .. } => {
886 Some("use IN SCHEMA instead of IN DATABASE in schema mod files".to_string())
887 }
888 Self::AlterDefaultPrivilegesSchemaMismatch { .. } => {
889 Some("ALTER DEFAULT PRIVILEGES in schema mod files must target the schema itself".to_string())
890 }
891 Self::StorageAndComputationObjectsInSameSchema { .. } => {
892 Some("storage objects (tables, sinks) cannot share a schema with computation objects (views, materialized views) to prevent accidentally recreating tables or sinks when recreating views. Organize your schemas: use one schema for storage objects (e.g., 'tables') and another for computation objects (e.g., 'views' or 'public')".to_string())
893 }
894 Self::ReplacementSchemaNonMvObject { .. } => {
895 Some("schemas with SET api = stable can only contain CREATE MATERIALIZED VIEW statements".to_string())
896 }
897 Self::InvalidClusterStatement { .. } => {
898 Some("cluster files can only contain CREATE CLUSTER, GRANT ON CLUSTER, and COMMENT ON CLUSTER statements".to_string())
899 }
900 Self::ClusterNameMismatch { .. } => {
901 Some("the cluster name in your CREATE CLUSTER statement must match the .sql file name".to_string())
902 }
903 Self::ClusterMissingCreateStatement { .. } => {
904 Some("each cluster file must contain exactly one CREATE CLUSTER statement".to_string())
905 }
906 Self::ClusterMultipleCreateStatements { .. } => {
907 Some("each cluster file must contain exactly one CREATE CLUSTER statement".to_string())
908 }
909 Self::ClusterGrantTargetMismatch { .. } => {
910 Some("GRANT statements in a cluster file must target the cluster defined in that file".to_string())
911 }
912 Self::ClusterCommentTargetMismatch { .. } => {
913 Some("COMMENT statements in a cluster file must target the cluster defined in that file".to_string())
914 }
915 Self::InvalidRoleStatement { .. } => {
916 Some("role files can only contain CREATE ROLE, ALTER ROLE, GRANT ROLE, and COMMENT ON ROLE statements".to_string())
917 }
918 Self::RoleNameMismatch { .. } => {
919 Some("the role name in your CREATE ROLE statement must match the .sql file name".to_string())
920 }
921 Self::RoleMissingCreateStatement { .. } => {
922 Some("each role file must contain exactly one CREATE ROLE statement".to_string())
923 }
924 Self::RoleMultipleCreateStatements { .. } => {
925 Some("each role file must contain exactly one CREATE ROLE statement".to_string())
926 }
927 Self::RoleAlterTargetMismatch { .. } => {
928 Some("ALTER ROLE statements in a role file must target the role defined in that file".to_string())
929 }
930 Self::RoleGrantTargetMismatch { .. } => {
931 Some("GRANT ROLE statements in a role file must grant the role defined in that file".to_string())
932 }
933 Self::RoleCommentTargetMismatch { .. } => {
934 Some("COMMENT statements in a role file must target the role defined in that file".to_string())
935 }
936 Self::InvalidNetworkPolicyStatement { .. } => {
937 Some("network policy files can only contain CREATE NETWORK POLICY, GRANT ON NETWORK POLICY, and COMMENT ON NETWORK POLICY statements".to_string())
938 }
939 Self::NetworkPolicyNameMismatch { .. } => {
940 Some("the network policy name in your CREATE NETWORK POLICY statement must match the .sql file name".to_string())
941 }
942 Self::NetworkPolicyMissingCreateStatement { .. } => {
943 Some("each network policy file must contain exactly one CREATE NETWORK POLICY statement".to_string())
944 }
945 Self::NetworkPolicyMultipleCreateStatements { .. } => {
946 Some("each network policy file must contain exactly one CREATE NETWORK POLICY statement".to_string())
947 }
948 Self::NetworkPolicyGrantTargetMismatch { .. } => {
949 Some("GRANT statements in a network policy file must target the policy defined in that file".to_string())
950 }
951 Self::NetworkPolicyCommentTargetMismatch { .. } => {
952 Some("COMMENT statements in a network policy file must target the policy defined in that file".to_string())
953 }
954 Self::ProfileObjectTypeMismatch { .. } => {
955 Some("all profile variants of an object must have the same primary statement type (e.g., all CREATE SECRET or all CREATE TABLE)".to_string())
956 }
957 Self::ProfileOverrideNotAllowed { .. } => {
958 Some("views and materialized views cannot have profile-specific overrides because their definitions should be consistent across environments".to_string())
959 }
960 }
961 }
962}
963
964#[derive(Debug)]
970pub struct ValidationErrors {
971 pub errors: Vec<ValidationError>,
972}
973
974impl ValidationErrors {
975 pub fn new(errors: Vec<ValidationError>) -> Self {
977 Self { errors }
978 }
979
980 pub fn is_empty(&self) -> bool {
982 self.errors.is_empty()
983 }
984
985 pub fn len(&self) -> usize {
987 self.errors.len()
988 }
989
990 pub fn into_result(self) -> Result<(), Self> {
992 if self.is_empty() { Ok(()) } else { Err(self) }
993 }
994}
995
996impl fmt::Display for ValidationErrors {
997 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
998 for error in &self.errors {
999 writeln!(f, "{}", error)?;
1000 }
1001 if !self.errors.is_empty() {
1002 write!(
1003 f,
1004 "could not compile due to {} previous error{}",
1005 self.errors.len(),
1006 if self.errors.len() == 1 { "" } else { "s" }
1007 )?;
1008 }
1009 Ok(())
1010 }
1011}
1012
1013impl std::error::Error for ValidationErrors {
1014 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1015 None
1016 }
1017}