1use crate::config::ConfigError;
21use crate::project::SchemaQualifier;
22use crate::project::ir::object_id::ObjectId;
23use owo_colors::{OwoColorize, Stream, Style};
24use std::fmt;
25use std::path::PathBuf;
26use thiserror::Error;
27
28#[derive(Debug, Error)]
30pub enum ConnectionError {
31 #[error("configuration error: {0}")]
32 Config(#[from] ConfigError),
33
34 #[error("failed to connect to {host}:{port}: {source}")]
35 Connect {
36 host: String,
37 port: u16,
38 source: tokio_postgres::Error,
39 },
40
41 #[error(
42 "TLS required by profile but server at {host}:{port} does not support TLS\n\
43 \n\
44 help: The server did not offer TLS. To connect without encryption, set\n\
45 \x20 sslmode = \"disable\" on the profile. To use TLS if available\n\
46 \x20 but fall back to plaintext otherwise, set sslmode = \"prefer\"."
47 )]
48 TlsRequiredNotSupported {
49 host: String,
50 port: u16,
51 source: tokio_postgres::Error,
52 },
53
54 #[error(
55 "TLS certificate verification failed for {host}:{port}: {source}\n\
56 \n\
57 help: The server's certificate could not be verified against the trusted\n\
58 \x20 CA bundle{hostname_suffix}. To skip verification, set\n\
59 \x20 sslmode = \"require\" or sslmode = \"prefer\". To use a custom\n\
60 \x20 CA bundle, set sslrootcert = \"/path/to/ca.pem\" on the profile."
61 )]
62 TlsVerification {
63 host: String,
64 port: u16,
65 hostname_suffix: &'static str,
66 source: tokio_postgres::Error,
67 },
68
69 #[error(
70 "no CA bundle found for TLS verification\n\
71 \n\
72 help: Set sslrootcert = \"/path/to/ca.pem\" on the profile to point at\n\
73 \x20 a specific CA bundle, or install the system CA bundle at one\n\
74 \x20 of: /etc/ssl/cert.pem, /etc/ssl/certs/ca-certificates.crt, or\n\
75 \x20 the platform-appropriate equivalent."
76 )]
77 TlsCaNotFound,
78
79 #[error("{}", format_query_error(.0))]
80 Query(tokio_postgres::Error),
81
82 #[error("dependency error: {0}")]
83 Dependency(#[from] crate::project::error::DependencyError),
84
85 #[error("failed to create database '{database}': {source}")]
86 DatabaseCreationFailed {
87 database: String,
88 source: Box<dyn std::error::Error + Send + Sync>,
89 },
90
91 #[error("failed to create schema '{database}.{schema}': {source}")]
92 SchemaCreationFailed {
93 database: String,
94 schema: String,
95 source: Box<dyn std::error::Error + Send + Sync>,
96 },
97
98 #[error("failed to create cluster '{name}': {source}")]
99 ClusterCreationFailed {
100 name: String,
101 source: Box<dyn std::error::Error + Send + Sync>,
102 },
103
104 #[error("cluster '{name}' already exists")]
105 ClusterAlreadyExists { name: String },
106
107 #[error("introspection failed for {object_type}: {source}")]
108 IntrospectionFailed {
109 object_type: String,
110 source: Box<dyn std::error::Error + Send + Sync>,
111 },
112
113 #[error("cluster '{name}' not found")]
114 ClusterNotFound { name: String },
115
116 #[error("deployment '{deploy_id}' already exists")]
117 DeploymentAlreadyExists { deploy_id: String },
118
119 #[error("deployment '{deploy_id}' not found")]
120 DeploymentNotFound { deploy_id: String },
121
122 #[error("deployment '{deploy_id}' has already been promoted to production")]
123 DeploymentAlreadyPromoted { deploy_id: String },
124
125 #[error("unsupported statement type: {0}")]
126 UnsupportedStatementType(String),
127
128 #[error("{0}")]
129 Message(String),
130}
131
132fn format_query_error(error: &tokio_postgres::Error) -> String {
133 if let Some(db_error) = error.as_db_error() {
134 let mut parts = vec![format!("database error: {}", db_error.message())];
135
136 if let Some(detail) = db_error.detail() {
137 parts.push(format!(" Detail: {}", detail));
138 }
139
140 if let Some(hint) = db_error.hint() {
141 parts.push(format!(" Hint: {}", hint));
142 }
143
144 parts.push(format!(" Code: {:?}", db_error.code()));
145 parts.join("\n")
146 } else {
147 format!("query error: {}", error)
148 }
149}
150
151impl From<tokio_postgres::Error> for ConnectionError {
152 fn from(error: tokio_postgres::Error) -> Self {
153 ConnectionError::Query(error)
154 }
155}
156
157impl From<mz_postgres_util::PostgresError> for ConnectionError {
158 fn from(error: mz_postgres_util::PostgresError) -> Self {
159 match error {
160 mz_postgres_util::PostgresError::Postgres(error) => ConnectionError::Query(error),
161 other => ConnectionError::Message(other.to_string()),
162 }
163 }
164}
165
166#[derive(Debug)]
168pub struct MissingSourceReference {
169 pub table: ObjectId,
171 pub reference: String,
173 pub suggestions: Vec<String>,
176}
177
178#[derive(Debug)]
181pub struct SourceReferenceMismatch {
182 pub source: ObjectId,
184 pub source_id: String,
187 pub tables: Vec<MissingSourceReference>,
189 pub available_count: usize,
192 pub unreadable: Option<String>,
196}
197
198#[derive(Debug)]
200pub enum DatabaseValidationError {
201 MissingDatabases(Vec<String>),
203 MissingSchemas(Vec<SchemaQualifier>),
205 MissingClusters(Vec<String>),
207 CompilationFailed {
209 file_path: PathBuf,
210 object_name: ObjectId,
211 missing_dependencies: Vec<ObjectId>,
212 },
213 Multiple {
215 databases: Vec<String>,
216 schemas: Vec<SchemaQualifier>,
217 clusters: Vec<String>,
218 compilation_errors: Vec<DatabaseValidationError>,
219 },
220 ClusterConflict {
223 cluster_name: String,
224 compute_objects: Vec<String>,
225 storage_objects: Vec<String>,
226 },
227 InsufficientPrivileges {
229 missing_database_usage: Vec<String>,
230 missing_createcluster: bool,
231 },
232 SchemaOwnershipMismatch {
234 unowned_schemas: Vec<SchemaQualifier>,
235 current_user: String,
236 },
237 ClusterOwnershipMismatch {
239 unowned_clusters: Vec<String>,
240 current_user: String,
241 },
242 MissingSources(Vec<ObjectId>),
244 MissingConnections(Vec<ObjectId>),
246 MissingSourceReferences(Vec<SourceReferenceMismatch>),
248 MissingTableDependencies {
250 objects_needing_tables: Vec<(ObjectId, Vec<ObjectId>)>,
251 },
252 QueryError(ConnectionError),
254}
255
256impl fmt::Display for DatabaseValidationError {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 DatabaseValidationError::MissingDatabases(dbs) => {
260 write!(f, "Missing databases: {}", dbs.join(", "))
261 }
262 DatabaseValidationError::MissingSchemas(schemas) => {
263 let schema_list: Vec<String> = schemas
264 .iter()
265 .map(|sq| format!("{}.{}", sq.database, sq.schema))
266 .collect();
267 write!(f, "Missing schemas: {}", schema_list.join(", "))
268 }
269 DatabaseValidationError::MissingClusters(clusters) => {
270 write!(f, "Missing clusters: {}", clusters.join(", "))
271 }
272 DatabaseValidationError::CompilationFailed {
273 file_path,
274 object_name,
275 missing_dependencies,
276 } => {
277 let relative_path = format_relative_path(file_path);
278
279 let error_style = Style::new().bright_red().bold();
280 let arrow_style = Style::new().bright_blue().bold();
281 writeln!(
282 f,
283 "{}: failed to compile '{}': missing external dependencies",
284 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
285 object_name
286 )?;
287 writeln!(
288 f,
289 " {} {}",
290 "-->".if_supports_color(Stream::Stderr, |t| arrow_style.style(t)),
291 relative_path
292 )?;
293 writeln!(f)?;
294 writeln!(f, " Missing dependencies:")?;
295 for dep in missing_dependencies {
296 writeln!(f, " - {}", dep)?;
297 }
298 Ok(())
299 }
300 DatabaseValidationError::Multiple {
301 databases,
302 schemas,
303 clusters,
304 compilation_errors,
305 } => {
306 let mut has_errors = false;
307
308 writeln!(f, "Missing dependencies")?;
309 if !databases.is_empty() {
310 writeln!(f, "Missing databases: {}", databases.join(", "))?;
311 has_errors = true;
312 }
313
314 if !schemas.is_empty() {
315 let schema_list: Vec<String> = schemas
316 .iter()
317 .map(|sq| format!("{}.{}", sq.database, sq.schema))
318 .collect();
319 writeln!(f, "Missing schemas: {}", schema_list.join(", "))?;
320 has_errors = true;
321 }
322
323 if !clusters.is_empty() {
324 writeln!(f, "Missing clusters: {}", clusters.join(", "))?;
325 has_errors = true;
326 }
327
328 if !compilation_errors.is_empty() {
329 if has_errors {
330 writeln!(f)?;
331 }
332 for (idx, err) in compilation_errors.iter().enumerate() {
333 if idx > 0 {
334 writeln!(f)?;
335 }
336 write!(f, "{}", err)?;
337 }
338 }
339
340 Ok(())
341 }
342 DatabaseValidationError::ClusterConflict {
343 cluster_name,
344 compute_objects,
345 storage_objects,
346 } => {
347 let error_style = Style::new().bright_red().bold();
348 writeln!(
349 f,
350 "{}: cluster '{}' contains both storage and computation objects",
351 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
352 cluster_name
353 )?;
354 writeln!(f)?;
355 writeln!(f, " Computation objects (indexes, materialized views):")?;
356 for obj in compute_objects {
357 writeln!(f, " - {}", obj)?;
358 }
359 writeln!(f)?;
360 writeln!(f, " Storage objects (sources, sinks):")?;
361 for obj in storage_objects {
362 writeln!(f, " - {}", obj)?;
363 }
364 writeln!(f)?;
365 let help_style = Style::new().bright_cyan().bold();
366 writeln!(
367 f,
368 " {} Move sources/sinks to a separate cluster to avoid accidental recreation",
369 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
370 )?;
371 Ok(())
372 }
373 DatabaseValidationError::InsufficientPrivileges {
374 missing_database_usage,
375 missing_createcluster,
376 } => {
377 let error_style = Style::new().bright_red().bold();
378 let help_style = Style::new().bright_cyan().bold();
379 writeln!(
380 f,
381 "{}: insufficient privileges to deploy this project",
382 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t))
383 )?;
384 writeln!(f)?;
385
386 if !missing_database_usage.is_empty() {
387 writeln!(f, " Missing USAGE privilege on databases:")?;
388 for db in missing_database_usage {
389 writeln!(f, " - {}", db)?;
390 }
391 writeln!(f)?;
392 }
393
394 if *missing_createcluster {
395 writeln!(f, " Missing CREATECLUSTER system privilege")?;
396 writeln!(f)?;
397 }
398
399 writeln!(
400 f,
401 " {} Ask your administrator to grant the required privileges:",
402 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
403 )?;
404 writeln!(f)?;
405
406 if !missing_database_usage.is_empty() {
407 for db in missing_database_usage {
408 writeln!(f, " GRANT USAGE ON DATABASE {} TO <user>;", db)?;
409 }
410 }
411
412 if *missing_createcluster {
413 writeln!(f, " GRANT CREATECLUSTER ON SYSTEM TO <user>;")?;
414 }
415
416 Ok(())
417 }
418 DatabaseValidationError::SchemaOwnershipMismatch {
419 unowned_schemas,
420 current_user,
421 } => {
422 let error_style = Style::new().bright_red().bold();
423 let help_style = Style::new().bright_cyan().bold();
424 writeln!(
425 f,
426 "{}: current role '{}' does not own the following production schemas",
427 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
428 current_user
429 )?;
430 writeln!(f)?;
431 for sq in unowned_schemas {
432 writeln!(f, " - {}.{}", sq.database, sq.schema)?;
433 }
434 writeln!(f)?;
435 writeln!(
436 f,
437 " {} Grant ownership of the schemas to the current role:",
438 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
439 )?;
440 writeln!(f)?;
441 for sq in unowned_schemas {
442 writeln!(
443 f,
444 " ALTER SCHEMA {}.{} OWNER TO {};",
445 sq.database, sq.schema, current_user
446 )?;
447 }
448 Ok(())
449 }
450 DatabaseValidationError::ClusterOwnershipMismatch {
451 unowned_clusters,
452 current_user,
453 } => {
454 let error_style = Style::new().bright_red().bold();
455 let help_style = Style::new().bright_cyan().bold();
456 writeln!(
457 f,
458 "{}: current role '{}' does not own the following production clusters",
459 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
460 current_user
461 )?;
462 writeln!(f)?;
463 for cluster in unowned_clusters {
464 writeln!(f, " - {}", cluster)?;
465 }
466 writeln!(f)?;
467 writeln!(
468 f,
469 " {} Grant ownership of the clusters to the current role:",
470 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
471 )?;
472 writeln!(f)?;
473 for cluster in unowned_clusters {
474 writeln!(
475 f,
476 " ALTER CLUSTER {} OWNER TO {};",
477 cluster, current_user
478 )?;
479 }
480 Ok(())
481 }
482 DatabaseValidationError::MissingSources(sources) => {
483 let error_style = Style::new().bright_red().bold();
484 writeln!(
485 f,
486 "{}: The following sources are referenced but do not exist:",
487 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t))
488 )?;
489 for source in sources {
490 writeln!(f, " - {}", source)?;
491 }
492 writeln!(f)?;
493 writeln!(
494 f,
495 "Please ensure all sources are created before running this command."
496 )?;
497 Ok(())
498 }
499 DatabaseValidationError::MissingConnections(connections) => {
500 let error_style = Style::new().bright_red().bold();
501 let help_style = Style::new().bright_cyan().bold();
502 writeln!(
503 f,
504 "{}: The following connections are referenced but do not exist:",
505 "error".if_supports_color(Stream::Stderr, |t| error_style.style(t))
506 )?;
507 for conn in connections {
508 writeln!(f, " - {}", conn)?;
509 }
510 writeln!(f)?;
511 writeln!(
512 f,
513 "{} Connections are not managed by mz-deploy and must be created separately.",
514 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
515 )?;
516 Ok(())
517 }
518 DatabaseValidationError::MissingSourceReferences(mismatches) => {
519 let help_style = Style::new().bright_cyan().bold();
520 writeln!(
521 f,
522 "The following tables reference upstream objects their source does not expose:"
523 )?;
524 for mismatch in mismatches {
525 writeln!(f)?;
526 writeln!(f, " from {}:", mismatch.source)?;
527 for table in &mismatch.tables {
528 writeln!(f, " - {} ({})", table.table, table.reference)?;
529 if !table.suggestions.is_empty() {
530 writeln!(f, " did you mean: {}?", table.suggestions.join(", "))?;
531 }
532 }
533 if let Some(reason) = &mismatch.unreadable {
534 writeln!(f)?;
535 writeln!(
536 f,
537 " could not read the references for {}: {}",
538 mismatch.source, reason
539 )?;
540 }
541 writeln!(f)?;
542 writeln!(
543 f,
544 " {} exposes {} references. To see them all:",
545 mismatch.source, mismatch.available_count
546 )?;
547 writeln!(
548 f,
549 " SELECT namespace, name FROM mz_internal.mz_source_references"
550 )?;
551 writeln!(f, " WHERE source_id = '{}';", mismatch.source_id)?;
552 }
553 writeln!(f)?;
554 writeln!(
555 f,
556 "{} Confirm the object exists upstream and that the source's publication,",
557 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
558 )?;
559 writeln!(f, " schema filter, and credentials include it.")?;
560 Ok(())
561 }
562 DatabaseValidationError::MissingTableDependencies {
563 objects_needing_tables,
564 } => {
565 let help_style = Style::new().bright_cyan().bold();
566 writeln!(
567 f,
568 "Objects depend on tables that don't exist in the database",
569 )?;
570 writeln!(f)?;
571 for (object, missing_tables) in objects_needing_tables {
572 writeln!(
573 f,
574 " {} {} depends on:",
575 "×".if_supports_color(Stream::Stderr, |t| t.bright_red()),
576 object
577 )?;
578 for table in missing_tables {
579 writeln!(f, " - {}", table)?;
580 }
581 }
582 writeln!(f)?;
583 writeln!(
584 f,
585 "{} Run 'mz-deploy apply' to create the required tables first",
586 "help:".if_supports_color(Stream::Stderr, |t| help_style.style(t))
587 )?;
588 Ok(())
589 }
590 DatabaseValidationError::QueryError(e) => {
591 write!(f, "Database query failed: {}", e)
592 }
593 }
594 }
595}
596
597impl std::error::Error for DatabaseValidationError {}
598
599pub fn format_relative_path(path: &std::path::Path) -> String {
604 let path_components: Vec<_> = path.components().collect();
605 let len = path_components.len();
606 if len >= 3 {
607 format!(
608 "{}/{}/{}",
609 path_components[len - 3].as_os_str().to_string_lossy(),
610 path_components[len - 2].as_os_str().to_string_lossy(),
611 path_components[len - 1].as_os_str().to_string_lossy()
612 )
613 } else {
614 path.display().to_string()
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use std::path::PathBuf;
622
623 fn object(schema: &str, object: &str) -> ObjectId {
624 ObjectId::new("app".to_string(), schema.to_string(), object.to_string())
625 }
626
627 #[mz_ore::test]
628 fn test_missing_source_references_error_display() {
629 let error =
630 DatabaseValidationError::MissingSourceReferences(vec![SourceReferenceMismatch {
631 source: object("ingest", "pg_source"),
632 source_id: "u1043".to_string(),
633 tables: vec![MissingSourceReference {
634 table: object("ingest", "widgets"),
635 reference: "public.widgest".to_string(),
636 suggestions: vec!["public.widgets".to_string()],
637 }],
638 available_count: 1284,
639 unreadable: None,
640 }]);
641 let output = error.to_string();
642
643 assert!(
644 output.contains("app.ingest.widgets (public.widgest)"),
645 "{output}"
646 );
647 assert!(output.contains("from app.ingest.pg_source:"), "{output}");
648 assert!(output.contains("did you mean: public.widgets?"), "{output}");
649 assert!(
650 output.contains("app.ingest.pg_source exposes 1284 references"),
651 "{output}"
652 );
653 assert!(output.contains("WHERE source_id = 'u1043';"), "{output}");
654 assert!(!output.contains("could not read"), "{output}");
655 }
656
657 #[mz_ore::test]
658 fn test_missing_source_references_error_display_multiple_suggestions() {
659 let error =
660 DatabaseValidationError::MissingSourceReferences(vec![SourceReferenceMismatch {
661 source: object("ingest", "pg_source"),
662 source_id: "u1043".to_string(),
663 tables: vec![MissingSourceReference {
664 table: object("ingest", "widgets"),
665 reference: "sales.widgets".to_string(),
666 suggestions: vec!["public.widgets".to_string(), "staging.widgets".to_string()],
667 }],
668 available_count: 2,
669 unreadable: None,
670 }]);
671 let output = error.to_string();
672
673 assert!(
674 output.contains("did you mean: public.widgets, staging.widgets?"),
675 "{output}"
676 );
677 }
678
679 #[mz_ore::test]
680 fn test_missing_source_references_error_display_without_suggestions() {
681 let error =
682 DatabaseValidationError::MissingSourceReferences(vec![SourceReferenceMismatch {
683 source: object("ingest", "pg_source"),
684 source_id: "u1043".to_string(),
685 tables: vec![MissingSourceReference {
686 table: object("ingest", "widgets"),
687 reference: "public.widgets".to_string(),
688 suggestions: Vec::new(),
689 }],
690 available_count: 3,
691 unreadable: None,
692 }]);
693 let output = error.to_string();
694
695 assert!(!output.contains("did you mean"), "{output}");
698 assert!(
699 output.contains("app.ingest.pg_source exposes 3 references"),
700 "{output}"
701 );
702 assert!(
703 output.contains("Confirm the object exists upstream"),
704 "{output}"
705 );
706 }
707
708 #[mz_ore::test]
709 fn test_missing_source_references_error_display_unreadable() {
710 let error =
711 DatabaseValidationError::MissingSourceReferences(vec![SourceReferenceMismatch {
712 source: object("ingest", "pg_source"),
713 source_id: "u1043".to_string(),
714 tables: vec![MissingSourceReference {
715 table: object("ingest", "widgets"),
716 reference: "public.widgets".to_string(),
717 suggestions: Vec::new(),
718 }],
719 available_count: 12,
720 unreadable: Some("permission denied".to_string()),
721 }]);
722 let output = error.to_string();
723
724 assert!(
725 output.contains(
726 "could not read the references for app.ingest.pg_source: permission denied"
727 ),
728 "{output}"
729 );
730 }
731
732 #[mz_ore::test]
733 fn test_missing_table_dependencies_error_display() {
734 let error = DatabaseValidationError::MissingTableDependencies {
735 objects_needing_tables: vec![
736 (
737 ObjectId::new(
738 "materialize".to_string(),
739 "public".to_string(),
740 "my_view".to_string(),
741 ),
742 vec![
743 ObjectId::new(
744 "materialize".to_string(),
745 "tables".to_string(),
746 "users".to_string(),
747 ),
748 ObjectId::new(
749 "materialize".to_string(),
750 "tables".to_string(),
751 "orders".to_string(),
752 ),
753 ],
754 ),
755 (
756 ObjectId::new(
757 "materialize".to_string(),
758 "public".to_string(),
759 "another_view".to_string(),
760 ),
761 vec![ObjectId::new(
762 "materialize".to_string(),
763 "tables".to_string(),
764 "products".to_string(),
765 )],
766 ),
767 ],
768 };
769
770 let error_string = format!("{}", error);
771
772 assert!(error_string.contains("Objects depend on tables that don't exist"));
774 assert!(error_string.contains("materialize.public.my_view"));
775 assert!(error_string.contains("materialize.tables.users"));
776 assert!(error_string.contains("materialize.tables.orders"));
777 assert!(error_string.contains("materialize.public.another_view"));
778 assert!(error_string.contains("materialize.tables.products"));
779 assert!(error_string.contains("help"));
780 assert!(error_string.contains("mz-deploy apply"));
781 }
782
783 #[mz_ore::test]
784 fn test_format_relative_path() {
785 let path = PathBuf::from("/home/user/project/database/schema/file.sql");
786 assert_eq!(format_relative_path(&path), "database/schema/file.sql");
787
788 let short_path = PathBuf::from("file.sql");
789 assert_eq!(format_relative_path(&short_path), "file.sql");
790 }
791
792 #[mz_ore::test]
793 fn test_format_relative_path_exactly_three_components() {
794 let path = PathBuf::from("database/schema/file.sql");
795 assert_eq!(format_relative_path(&path), "database/schema/file.sql");
796 }
797
798 #[mz_ore::test]
799 fn test_format_relative_path_two_components() {
800 let path = PathBuf::from("schema/file.sql");
801 assert_eq!(format_relative_path(&path), "schema/file.sql");
802 }
803
804 #[mz_ore::test]
805 fn test_missing_databases_error_display() {
806 let error =
807 DatabaseValidationError::MissingDatabases(vec!["db1".to_string(), "db2".to_string()]);
808 let error_string = format!("{}", error);
809 assert!(error_string.contains("Missing databases"));
810 assert!(error_string.contains("db1"));
811 assert!(error_string.contains("db2"));
812 }
813
814 #[mz_ore::test]
815 fn test_missing_schemas_error_display() {
816 let error = DatabaseValidationError::MissingSchemas(vec![
817 SchemaQualifier::new("db1".to_string(), "schema1".to_string()),
818 SchemaQualifier::new("db2".to_string(), "schema2".to_string()),
819 ]);
820 let error_string = format!("{}", error);
821 assert!(error_string.contains("Missing schemas"));
822 assert!(error_string.contains("db1.schema1"));
823 assert!(error_string.contains("db2.schema2"));
824 }
825
826 #[mz_ore::test]
827 fn test_missing_clusters_error_display() {
828 let error = DatabaseValidationError::MissingClusters(vec![
829 "cluster1".to_string(),
830 "cluster2".to_string(),
831 ]);
832 let error_string = format!("{}", error);
833 assert!(error_string.contains("Missing clusters"));
834 assert!(error_string.contains("cluster1"));
835 assert!(error_string.contains("cluster2"));
836 }
837
838 #[mz_ore::test]
839 fn test_cluster_conflict_error_display() {
840 let error = DatabaseValidationError::ClusterConflict {
841 cluster_name: "shared_cluster".to_string(),
842 compute_objects: vec!["my_index".to_string(), "my_mv".to_string()],
843 storage_objects: vec!["my_source".to_string()],
844 };
845 let error_string = format!("{}", error);
846 assert!(error_string.contains("shared_cluster"));
847 assert!(error_string.contains("storage and computation objects"));
848 assert!(error_string.contains("my_index"));
849 assert!(error_string.contains("my_mv"));
850 assert!(error_string.contains("my_source"));
851 assert!(error_string.contains("help"));
852 }
853
854 #[mz_ore::test]
855 fn test_insufficient_privileges_error_display() {
856 let error = DatabaseValidationError::InsufficientPrivileges {
857 missing_database_usage: vec!["db1".to_string(), "db2".to_string()],
858 missing_createcluster: true,
859 };
860 let error_string = format!("{}", error);
861 assert!(error_string.contains("insufficient privileges"));
862 assert!(error_string.contains("db1"));
863 assert!(error_string.contains("db2"));
864 assert!(error_string.contains("CREATECLUSTER"));
865 assert!(error_string.contains("GRANT"));
866 }
867
868 #[mz_ore::test]
869 fn test_insufficient_privileges_only_database() {
870 let error = DatabaseValidationError::InsufficientPrivileges {
871 missing_database_usage: vec!["db1".to_string()],
872 missing_createcluster: false,
873 };
874 let error_string = format!("{}", error);
875 assert!(error_string.contains("db1"));
876 assert!(!error_string.contains("CREATECLUSTER ON SYSTEM"));
877 }
878
879 #[mz_ore::test]
880 fn test_missing_sources_error_display() {
881 let error = DatabaseValidationError::MissingSources(vec![ObjectId::new(
882 "materialize".to_string(),
883 "public".to_string(),
884 "kafka_source".to_string(),
885 )]);
886 let error_string = format!("{}", error);
887 assert!(error_string.contains("sources are referenced but do not exist"));
888 assert!(error_string.contains("materialize.public.kafka_source"));
889 }
890
891 #[mz_ore::test]
892 fn test_multiple_validation_errors_display() {
893 let error = DatabaseValidationError::Multiple {
894 databases: vec!["missing_db".to_string()],
895 schemas: vec![SchemaQualifier::new(
896 "db".to_string(),
897 "missing_schema".to_string(),
898 )],
899 clusters: vec!["missing_cluster".to_string()],
900 compilation_errors: vec![],
901 };
902 let error_string = format!("{}", error);
903 assert!(error_string.contains("missing_db"));
904 assert!(error_string.contains("db.missing_schema"));
905 assert!(error_string.contains("missing_cluster"));
906 }
907
908 #[mz_ore::test]
909 fn test_connection_error_display() {
910 let error = ConnectionError::Message("test error message".to_string());
911 let error_string = format!("{}", error);
912 assert_eq!(error_string, "test error message");
913 }
914
915 #[mz_ore::test]
916 fn test_connection_error_cluster_not_found() {
917 let error = ConnectionError::ClusterNotFound {
918 name: "missing_cluster".to_string(),
919 };
920 let error_string = format!("{}", error);
921 assert!(error_string.contains("missing_cluster"));
922 assert!(error_string.contains("not found"));
923 }
924
925 #[mz_ore::test]
926 fn test_connection_error_deployment_already_exists() {
927 let error = ConnectionError::DeploymentAlreadyExists {
928 deploy_id: "staging_123".to_string(),
929 };
930 let error_string = format!("{}", error);
931 assert!(error_string.contains("staging_123"));
932 assert!(error_string.contains("already exists"));
933 }
934
935 #[mz_ore::test]
936 fn test_connection_error_deployment_not_found() {
937 let error = ConnectionError::DeploymentNotFound {
938 deploy_id: "nonexistent".to_string(),
939 };
940 let error_string = format!("{}", error);
941 assert!(error_string.contains("nonexistent"));
942 assert!(error_string.contains("not found"));
943 }
944
945 #[mz_ore::test]
946 fn test_connection_error_deployment_already_promoted() {
947 let error = ConnectionError::DeploymentAlreadyPromoted {
948 deploy_id: "prod_deploy".to_string(),
949 };
950 let error_string = format!("{}", error);
951 assert!(error_string.contains("prod_deploy"));
952 assert!(error_string.contains("already been promoted"));
953 }
954
955 #[mz_ore::test]
956 fn test_database_validation_error_is_error_trait() {
957 let error: Box<dyn std::error::Error> =
959 Box::new(DatabaseValidationError::MissingDatabases(vec![]));
960 assert!(error.to_string().contains("Missing databases"));
961 }
962}