1use std::sync::Arc;
11
12use mz_ccsr::ListError;
13use mz_repr::adt::system::Oid;
14use mz_sql_parser::ast::display::AstDisplay;
15use mz_sql_parser::ast::{ExternalReferences, UnresolvedItemName};
16use mz_storage_types::connections::{
17 MySqlConnectionValidationError, PostgresConnectionValidationError,
18};
19use mz_storage_types::errors::{ContextCreationError, CsrConnectError};
20
21use crate::names::{FullItemName, PartialItemName};
22
23#[derive(Debug, Clone, thiserror::Error)]
25pub enum PgSourcePurificationError {
26 #[error("CREATE SOURCE specifies DETAILS option")]
27 UserSpecifiedDetails,
28 #[error("{0} option is unnecessary when no tables are added")]
29 UnnecessaryOptionsWithoutReferences(String),
30 #[error("PUBLICATION {0} is empty")]
31 EmptyPublication(String),
32 #[error("database {database} missing referenced schemas")]
33 DatabaseMissingFilteredSchemas {
34 database: String,
35 schemas: Vec<String>,
36 },
37 #[error("missing TABLES specification")]
38 RequiresExternalReferences,
39 #[error("insufficient privileges")]
40 UserLacksUsageOnSchemas { schemas: Vec<String> },
41 #[error("insufficient privileges")]
42 UserLacksSelectOnTables { tables: Vec<String> },
43 #[error("one or more tables requires BYPASSRLS")]
44 BypassRLSRequired { tables: Vec<String> },
45 #[error("referenced items not tables with REPLICA IDENTITY FULL")]
46 NotTablesWReplicaIdentityFull { items: Vec<String> },
47 #[error("TEXT COLUMNS refers to table not currently being added")]
48 DanglingTextColumns { items: Vec<PartialItemName> },
49 #[error("EXCLUDE COLUMNS refers to table not currently being added")]
50 DanglingExcludeColumns { items: Vec<PartialItemName> },
51 #[error("EXCLUDE CONSTRAINTS refers to constraints that do not exist on table {table}")]
52 ConstraintsNotFound {
53 table: PartialItemName,
54 constraints: Vec<String>,
55 },
56 #[error("duplicated column name references: {0:?}")]
57 DuplicatedColumnNames(Vec<String>),
58 #[error("referenced tables use unsupported types")]
59 UnrecognizedTypes { cols: Vec<(String, Oid)> },
60 #[error("{0} is not a POSTGRES CONNECTION")]
61 NotPgConnection(FullItemName),
62 #[error("CONNECTION must specify PUBLICATION")]
63 ConnectionMissingPublication,
64 #[error(transparent)]
65 InvalidConnection(PostgresConnectionValidationError),
66}
67
68impl PgSourcePurificationError {
69 pub fn detail(&self) -> Option<String> {
70 match self {
71 Self::DanglingTextColumns { items } => Some(format!(
72 "the following tables are referenced but not added: {}",
73 itertools::join(items, ", ")
74 )),
75 Self::ConstraintsNotFound {
76 table: _,
77 constraints,
78 } => Some(format!(
79 "the following constraints were not found: {}",
80 constraints.join(", ")
81 )),
82 Self::DatabaseMissingFilteredSchemas {
83 database: _,
84 schemas,
85 } => Some(format!(
86 "missing schemas: {}",
87 itertools::join(schemas.iter(), ", ")
88 )),
89 Self::UserLacksUsageOnSchemas { schemas } => Some(format!(
90 "user lacks USAGE privileges for schemas {}",
91 schemas.join(", ")
92 )),
93 Self::UserLacksSelectOnTables { tables } => Some(format!(
94 "user lacks SELECT privileges for tables {}",
95 tables.join(", ")
96 )),
97 Self::BypassRLSRequired { tables } => Some(format!(
98 "user must have BYPASSRLS attribute to read tables {}",
99 tables.join(", "),
100 )),
101 Self::NotTablesWReplicaIdentityFull { items } => {
102 Some(format!("referenced items: {}", items.join(", ")))
103 }
104 Self::UnrecognizedTypes { cols } => Some(format!(
105 "the following columns contain unsupported types:\n{}",
106 itertools::join(
107 cols.into_iter()
108 .map(|(col, Oid(oid))| format!("{} (OID {})", col, oid)),
109 "\n"
110 )
111 )),
112 Self::InvalidConnection(e) => e.detail(),
113 _ => None,
114 }
115 }
116
117 pub fn hint(&self) -> Option<String> {
118 match self {
119 Self::UserSpecifiedDetails => Some(
120 "If trying to use the output of SHOW CREATE SOURCE, remove the DETAILS option."
121 .into(),
122 ),
123 Self::RequiresExternalReferences => {
124 Some("provide a FOR TABLES (..), FOR SCHEMAS (..), or FOR ALL TABLES clause".into())
125 }
126 Self::UnrecognizedTypes {
127 cols: _,
128 } => Some(
129 "Use the TEXT COLUMNS option naming the listed columns, and Materialize can ingest their values \
130 as text."
131 .into(),
132 ),
133 Self::UnnecessaryOptionsWithoutReferences(option) => Some(format!(
134 "Remove the {} option, as no tables are being added.",
135 option
136 )),
137 Self::ConstraintsNotFound { .. } => Some(
138 "Constraint names are matched exactly, including case, against the upstream \
139 PRIMARY KEY and UNIQUE constraint names."
140 .into(),
141 ),
142 Self::BypassRLSRequired { .. } => Some("Add the BYPASSRLS attribute to the Materialize user".into()),
143 Self::InvalidConnection(e) => e.hint(),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Debug, Clone, thiserror::Error)]
151pub enum KafkaSourcePurificationError {
152 #[error("{} is only valid for multi-output sources", .0.to_ast_string_simple())]
153 ReferencedSubsources(ExternalReferences),
154 #[error("KAFKA CONNECTION without TOPIC")]
155 ConnectionMissingTopic,
156 #[error("{0} is not a KAFKA CONNECTION")]
157 NotKafkaConnection(FullItemName),
158 #[error("failed to create and connect Kafka consumer")]
159 KafkaConsumerError(String),
160 #[error("Referenced kafka connection uses a different topic '{0}' than specified: '{1}'")]
161 WrongKafkaTopic(String, UnresolvedItemName),
162}
163
164impl KafkaSourcePurificationError {
165 pub fn detail(&self) -> Option<String> {
166 match self {
167 Self::KafkaConsumerError(e) => Some(e.clone()),
168 _ => None,
169 }
170 }
171
172 pub fn hint(&self) -> Option<String> {
173 None
174 }
175}
176
177#[derive(Debug, Clone, thiserror::Error)]
179pub enum LoadGeneratorSourcePurificationError {
180 #[error("FOR ALL TABLES is only valid for multi-output sources")]
181 ForAllTables,
182 #[error("FOR SCHEMAS (..) unsupported")]
183 ForSchemas,
184 #[error("FOR TABLES (..) unsupported")]
185 ForTables,
186 #[error("multi-output sources require a FOR TABLES (..) or FOR ALL TABLES statement")]
187 MultiOutputRequiresForAllTables,
188 #[error("multi-output sources require an external reference")]
189 MultiOutputRequiresExternalReference,
190 #[error("Referenced load generator is different '{0}' than specified: '{1}'")]
191 WrongLoadGenerator(String, UnresolvedItemName),
192}
193
194impl LoadGeneratorSourcePurificationError {
195 pub fn detail(&self) -> Option<String> {
196 match self {
197 _ => None,
198 }
199 }
200
201 pub fn hint(&self) -> Option<String> {
202 match self {
203 _ => None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, thiserror::Error)]
210pub enum KafkaSinkPurificationError {
211 #[error("{0} is not a KAFKA CONNECTION")]
212 NotKafkaConnection(FullItemName),
213 #[error("admin client errored")]
214 AdminClientError(Arc<ContextCreationError>),
215 #[error("zero brokers discovered in metadata request")]
216 ZeroBrokers,
217}
218
219impl KafkaSinkPurificationError {
220 pub fn detail(&self) -> Option<String> {
221 match self {
222 Self::AdminClientError(e) => Some(e.to_string_with_causes()),
223 _ => None,
224 }
225 }
226
227 pub fn hint(&self) -> Option<String> {
228 None
229 }
230}
231
232#[derive(Debug, Clone, thiserror::Error)]
233pub enum IcebergSinkPurificationError {
234 #[error("catalog connection errored")]
235 CatalogError(Arc<anyhow::Error>),
236 #[error("error loading aws sdk context")]
237 AwsSdkContextError(Arc<anyhow::Error>),
238 #[error("S3 Tables connection region mismatch")]
239 S3TablesRegionMismatch {
240 s3_tables_region: String,
241 environment_region: String,
242 },
243}
244
245impl IcebergSinkPurificationError {
246 pub fn detail(&self) -> Option<String> {
247 match self {
248 Self::CatalogError(e) => Some(e.to_string_with_causes()),
249 Self::AwsSdkContextError(e) => Some(e.to_string_with_causes()),
250 Self::S3TablesRegionMismatch {
251 s3_tables_region,
252 environment_region,
253 } => Some(format!(
254 "S3 Tables connection is configured for region '{}' but this Materialize environment is running in region '{}'",
255 s3_tables_region, environment_region
256 )),
257 }
258 }
259
260 pub fn hint(&self) -> Option<String> {
261 match self {
262 Self::S3TablesRegionMismatch {
263 environment_region, ..
264 } => Some(format!(
265 "Create a new AWS connection with REGION = '{}' to use with S3 Tables in this environment.",
266 environment_region
267 )),
268 _ => None,
269 }
270 }
271}
272
273use mz_ore::error::ErrorExt;
274
275#[derive(Debug, Clone, thiserror::Error)]
277pub enum CsrPurificationError {
278 #[error("{0} is not a CONFLUENT SCHEMA REGISTRY CONNECTION")]
279 NotCsrConnection(FullItemName),
280 #[error("client errored")]
281 ClientError(Arc<CsrConnectError>),
282 #[error("list subjects failed")]
283 ListSubjectsError(Arc<ListError>),
284}
285
286impl CsrPurificationError {
287 pub fn detail(&self) -> Option<String> {
288 match self {
289 Self::ClientError(e) => Some(e.to_string_with_causes()),
290 Self::ListSubjectsError(e) => Some(e.to_string_with_causes()),
291 Self::NotCsrConnection(_) => None,
292 }
293 }
294
295 pub fn hint(&self) -> Option<String> {
296 None
297 }
298}
299
300#[derive(Debug, Clone, thiserror::Error)]
302pub enum GluePurificationError {
303 #[error("{0} is not an AWS GLUE SCHEMA REGISTRY CONNECTION")]
304 NotGlueConnection(FullItemName),
305 #[error("SCHEMA NAME option is required")]
306 MissingSchemaName,
307 #[error("loading AWS SDK configuration failed")]
308 LoadSdkConfigError(Arc<anyhow::Error>),
309 #[error("Glue schema lookup failed (registry {registry:?}, schema {schema:?})")]
310 SchemaLookupError {
311 registry: String,
312 schema: String,
313 #[source]
314 cause: Arc<mz_aws_glue_schema_registry::GetSchemaVersionError>,
315 },
316 #[error("Glue schema {schema:?} in registry {registry:?} has no definition")]
317 EmptyDefinition { registry: String, schema: String },
318 #[error(
319 "Glue schema {schema:?} in registry {registry:?} uses unsupported data format {format}; only AVRO is supported"
320 )]
321 UnsupportedDataFormat {
322 registry: String,
323 schema: String,
324 format: String,
325 },
326}
327
328impl GluePurificationError {
329 pub fn detail(&self) -> Option<String> {
330 match self {
331 Self::LoadSdkConfigError(e) => Some(e.to_string_with_causes()),
332 Self::SchemaLookupError { cause, .. } => Some(cause.to_string_with_causes()),
333 Self::NotGlueConnection(_)
334 | Self::MissingSchemaName
335 | Self::EmptyDefinition { .. }
336 | Self::UnsupportedDataFormat { .. } => None,
337 }
338 }
339
340 pub fn hint(&self) -> Option<String> {
341 None
342 }
343}
344
345#[derive(Debug, thiserror::Error)]
347pub enum MySqlSourcePurificationError {
348 #[error("User lacks required MySQL privileges")]
349 UserLacksPrivileges(Vec<(String, String)>),
350 #[error("CREATE SOURCE specifies DETAILS option")]
351 UserSpecifiedDetails,
352 #[error("{0} option is unnecessary when no tables are added")]
353 UnnecessaryOptionsWithoutReferences(String),
354 #[error("{0} is not a MYSQL CONNECTION")]
355 NotMySqlConnection(FullItemName),
356 #[error("referenced tables use unsupported types")]
357 UnrecognizedTypes { cols: Vec<(String, String, String)> },
358 #[error("duplicated column name references in table {0}: {1:?}")]
359 DuplicatedColumnNames(String, Vec<String>),
360 #[error("{option_name} refers to table not currently being added")]
361 DanglingColumns {
362 option_name: String,
363 items: Vec<UnresolvedItemName>,
364 },
365 #[error("Invalid MySQL table reference: {0}")]
366 InvalidTableReference(String),
367 #[error("No tables found for provided reference")]
368 EmptyDatabase,
369 #[error("missing TABLES specification")]
370 RequiresExternalReferences,
371 #[error("No tables found in referenced schemas")]
372 NoTablesFoundForSchemas(Vec<String>),
373 #[error(transparent)]
374 InvalidConnection(#[from] MySqlConnectionValidationError),
375 #[error(
376 "The MySQL system variable 'binlog_row_metadata' is set to an unsupported value: {setting}. Materialize requires this variable to be set to 'FULL' to use the \"CREATE TABLE FROM SOURCE\" syntax for MySQL sources."
377 )]
378 UnsupportedBinlogMetadataSetting { setting: String },
379 #[error(
380 "You are using MySQL version {version}. Materialize requires MySQL 8.0.1 or later to use the \"CREATE TABLE FROM SOURCE\" syntax for MySQL sources."
381 )]
382 UnsupportedMySqlVersion { version: String },
383}
384
385impl MySqlSourcePurificationError {
386 pub fn detail(&self) -> Option<String> {
387 match self {
388 Self::UserLacksPrivileges(missing) => Some(format!(
389 "Missing MySQL privileges: {}",
390 itertools::join(
391 missing
392 .iter()
393 .map(|(privilege, table)| format!("'{}' on '{}'", privilege, table)),
394 ", "
395 )
396 )),
397 Self::DanglingColumns {
398 option_name: _,
399 items,
400 } => Some(format!(
401 "the following columns are referenced but not added: {}",
402 itertools::join(items, ", ")
403 )),
404 Self::UnrecognizedTypes { cols } => Some(format!(
405 "the following columns contain unsupported types:\n{}",
406 itertools::join(
407 cols.into_iter().map(|(table, column, data_type)| format!(
408 "'{}' for {}.{}",
409 data_type, column, table
410 )),
411 "\n"
412 )
413 )),
414 Self::NoTablesFoundForSchemas(schemas) => Some(format!(
415 "missing schemas: {}",
416 itertools::join(schemas.iter(), ", ")
417 )),
418 Self::InvalidConnection(e) => e.detail(),
419 _ => None,
420 }
421 }
422
423 pub fn hint(&self) -> Option<String> {
424 match self {
425 Self::UserSpecifiedDetails => Some(
426 "If trying to use the output of SHOW CREATE SOURCE, remove the DETAILS option."
427 .into(),
428 ),
429 Self::RequiresExternalReferences => {
430 Some("provide a FOR TABLES (..), FOR SCHEMAS (..), or FOR ALL TABLES clause".into())
431 }
432 Self::InvalidTableReference(_) => Some(
433 "Specify tables names as SCHEMA_NAME.TABLE_NAME in a FOR TABLES (..) clause".into(),
434 ),
435 Self::UnrecognizedTypes { cols: _ } => Some(
436 "Check the docs -- some types can be supported using the TEXT COLUMNS option to \
437 ingest their values as text, or ignored using EXCLUDE COLUMNS."
438 .into(),
439 ),
440 Self::EmptyDatabase => Some(
441 "No tables were found to replicate. This could be because \
442 the user does not have privileges on the intended tables."
443 .into(),
444 ),
445 Self::UnnecessaryOptionsWithoutReferences(option) => Some(format!(
446 "Remove the {} option, as no tables are being added.",
447 option
448 )),
449 Self::InvalidConnection(e) => e.hint(),
450 _ => None,
451 }
452 }
453}
454
455#[derive(Debug, Clone, thiserror::Error)]
457pub enum SqlServerSourcePurificationError {
458 #[error("{0} is not a SQL SERVER CONNECTION")]
459 NotSqlServerConnection(FullItemName),
460 #[error("CREATE SOURCE specifies DETAILS option")]
461 UserSpecifiedDetails,
462 #[error("{0} option is unnecessary when no tables are added")]
463 UnnecessaryOptionsWithoutReferences(String),
464 #[error("missing TABLES specification")]
465 RequiresExternalReferences,
466 #[error("{option_name} refers to table not currently being added")]
467 DanglingColumns {
468 option_name: String,
469 items: Vec<UnresolvedItemName>,
470 },
471 #[error("found multiple primary keys for a table. constraints {constraint_names:?}")]
472 MultiplePrimaryKeys { constraint_names: Vec<Arc<str>> },
473 #[error("column {schema_name}.{tbl_name}.{col_name} of type {col_type} is not supported")]
474 UnsupportedColumn {
475 schema_name: Arc<str>,
476 tbl_name: Arc<str>,
477 col_name: Arc<str>,
478 col_type: Arc<str>,
479 context: String,
480 },
481 #[error("Table {tbl_name} had all columns excluded")]
482 AllColumnsExcluded { tbl_name: Arc<str> },
483 #[error("No tables found for provided reference")]
484 NoTables,
485 #[error("programming error: {0}")]
486 ProgrammingError(String),
487 #[error("No start_lsn found for capture instance {0}")]
488 NoStartLsn(String),
489 #[error("Capture instance {capture_instance} has missing columns: {col_names:?}")]
490 CdcMissingColumns {
491 capture_instance: Arc<str>,
492 col_names: Vec<Arc<str>>,
493 },
494}
495
496impl SqlServerSourcePurificationError {
497 pub fn detail(&self) -> Option<String> {
498 match self {
499 Self::DanglingColumns {
500 option_name: _,
501 items,
502 } => Some(format!(
503 "the following columns are referenced but not added: {}",
504 itertools::join(items, ", ")
505 )),
506 Self::UnsupportedColumn { context, .. } => Some(context.clone()),
507 _ => None,
508 }
509 }
510
511 pub fn hint(&self) -> Option<String> {
512 match self {
513 Self::RequiresExternalReferences => {
514 Some("provide a FOR TABLES (..), FOR SCHEMAS (..), or FOR ALL TABLES clause".into())
515 }
516 Self::UnnecessaryOptionsWithoutReferences(option) => Some(format!(
517 "Remove the {} option, as no tables are being added.",
518 option
519 )),
520 Self::NoTables => Some(
521 "No tables were found to replicate. This could be because \
522 the user does not have privileges on the intended tables."
523 .into(),
524 ),
525 Self::UnsupportedColumn { .. } => {
526 Some("Use EXCLUDE COLUMNS (...) to exclude a column from this source".into())
527 }
528 _ => None,
529 }
530 }
531}