1use std::collections::{BTreeMap, BTreeSet};
15use std::fmt;
16use std::iter;
17use std::path::Path;
18use std::sync::Arc;
19
20use anyhow::anyhow;
21use itertools::Itertools;
22use mz_adapter_types::dyncfgs::ENABLE_S3_TABLES_REGION_CHECK;
23use mz_ccsr::{Client, GetBySubjectError};
24use mz_cloud_provider::CloudProvider;
25use mz_controller_types::ClusterId;
26use mz_kafka_util::client::MzClientContext;
27use mz_mysql_util::MySqlTableDesc;
28use mz_ore::collections::CollectionExt;
29use mz_ore::error::ErrorExt;
30use mz_ore::future::InTask;
31use mz_ore::iter::IteratorExt;
32use mz_ore::str::StrExt;
33use mz_postgres_util::desc::PostgresTableDesc;
34use mz_proto::RustType;
35use mz_repr::{CatalogItemId, RelationDesc, RelationVersionSelector, Timestamp, strconv};
36use mz_sql_parser::ast::display::AstDisplay;
37use mz_sql_parser::ast::visit::{Visit, visit_function};
38use mz_sql_parser::ast::visit_mut::{VisitMut, visit_expr_mut};
39use mz_sql_parser::ast::{
40 AlterSourceAction, AlterSourceAddSubsourceOptionName, AlterSourceStatement, AvroDocOn,
41 CastFailureMode, ColumnName, CreateMaterializedViewStatement, CreateSinkConnection,
42 CreateSinkOptionName, CreateSinkStatement, CreateSourceOptionName, CreateSubsourceOption,
43 CreateSubsourceOptionName, CreateTableFromSourceStatement, CsrConfigOption,
44 CsrConfigOptionName, CsrConnection, CsrSeedAvro, CsrSeedProtobuf, CsrSeedProtobufSchema,
45 DeferredItemName, DocOnIdentifier, DocOnSchema, Expr, Function, FunctionArgs, GlueAvroOption,
46 GlueAvroSeed, Ident, KafkaSourceConfigOption, KafkaSourceConfigOptionName, LoadGenerator,
47 LoadGeneratorOption, LoadGeneratorOptionName, MaterializedViewOption,
48 MaterializedViewOptionName, MySqlConfigOption, MySqlConfigOptionName, PgConfigOption,
49 PgConfigOptionName, RawItemName, ReaderSchemaSelectionStrategy, RefreshAtOptionValue,
50 RefreshEveryOptionValue, RefreshOptionValue, SourceEnvelope, SqlServerConfigOption,
51 SqlServerConfigOptionName, Statement, TableFromSourceColumns, TableFromSourceOption,
52 TableFromSourceOptionName, UnresolvedItemName,
53};
54use mz_sql_server_util::desc::SqlServerTableDesc;
55use mz_storage_types::configuration::StorageConfiguration;
56use mz_storage_types::connections::Connection;
57use mz_storage_types::connections::inline::IntoInlineConnection;
58use mz_storage_types::errors::ContextCreationError;
59use mz_storage_types::sources::load_generator::LoadGeneratorOutput;
60use mz_storage_types::sources::mysql::MySqlSourceDetails;
61use mz_storage_types::sources::postgres::PostgresSourcePublicationDetails;
62use mz_storage_types::sources::{
63 GenericSourceConnection, MzOffset, SourceConnection, SourceDesc, SourceExportStatementDetails,
64 SqlServerSourceExtras,
65};
66use prost::Message;
67use protobuf_native::MessageLite;
68use protobuf_native::compiler::{SourceTreeDescriptorDatabase, VirtualSourceTree};
69use rdkafka::admin::AdminClient;
70use references::{RetrievedSourceReferences, SourceReferenceClient};
71use uuid::Uuid;
72
73use crate::ast::{
74 AlterSourceAddSubsourceOption, AvroSchema, CreateSourceConnection, CreateSourceStatement,
75 CreateSubsourceStatement, CsrConnectionAvro, CsrConnectionProtobuf, ExternalReferenceExport,
76 ExternalReferences, Format, FormatSpecifier, ProtobufSchema, Value, WithOptionValue,
77};
78use crate::catalog::{CatalogItemType, SessionCatalog};
79use crate::kafka_util::{KafkaSinkConfigOptionExtracted, KafkaSourceConfigOptionExtracted};
80use crate::names::{
81 Aug, FullItemName, PartialItemName, ResolvedColumnReference, ResolvedDataType, ResolvedIds,
82 ResolvedItemName,
83};
84use crate::plan::error::PlanError;
85use crate::plan::statement::ddl::load_generator_ast_to_generator;
86use crate::plan::{SourceReferences, StatementContext};
87use crate::pure::error::{IcebergSinkPurificationError, SqlServerSourcePurificationError};
88use crate::pure::mysql::{ensure_binlog_full_metadata, is_binlog_full_metadata};
89use crate::{kafka_util, normalize};
90
91use self::error::{
92 CsrPurificationError, KafkaSinkPurificationError, KafkaSourcePurificationError,
93 LoadGeneratorSourcePurificationError, MySqlSourcePurificationError, PgSourcePurificationError,
94};
95
96pub(crate) mod error;
97mod references;
98
99pub mod mysql;
100pub mod postgres;
101pub mod sql_server;
102
103pub(crate) struct RequestedSourceExport<T> {
104 external_reference: UnresolvedItemName,
105 name: UnresolvedItemName,
106 meta: T,
107}
108
109impl<T: fmt::Debug> fmt::Debug for RequestedSourceExport<T> {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.debug_struct("RequestedSourceExport")
112 .field("external_reference", &self.external_reference)
113 .field("name", &self.name)
114 .field("meta", &self.meta)
115 .finish()
116 }
117}
118
119impl<T> RequestedSourceExport<T> {
120 fn change_meta<F>(self, new_meta: F) -> RequestedSourceExport<F> {
121 RequestedSourceExport {
122 external_reference: self.external_reference,
123 name: self.name,
124 meta: new_meta,
125 }
126 }
127}
128
129fn source_export_name_gen(
134 source_name: &UnresolvedItemName,
135 subsource_name: &str,
136) -> Result<UnresolvedItemName, PlanError> {
137 let mut partial = normalize::unresolved_item_name(source_name.clone())?;
138 partial.item = subsource_name.to_string();
139 Ok(UnresolvedItemName::from(partial))
140}
141
142fn validate_source_export_names<T>(
146 requested_source_exports: &[RequestedSourceExport<T>],
147) -> Result<(), PlanError> {
148 if let Some(name) = requested_source_exports
152 .iter()
153 .map(|subsource| &subsource.name)
154 .duplicates()
155 .next()
156 .cloned()
157 {
158 let mut upstream_references: Vec<_> = requested_source_exports
159 .into_iter()
160 .filter_map(|subsource| {
161 if &subsource.name == &name {
162 Some(subsource.external_reference.clone())
163 } else {
164 None
165 }
166 })
167 .collect();
168
169 upstream_references.sort();
170
171 Err(PlanError::SubsourceNameConflict {
172 name,
173 upstream_references,
174 })?;
175 }
176
177 if let Some(name) = requested_source_exports
183 .iter()
184 .map(|export| &export.external_reference)
185 .duplicates()
186 .next()
187 .cloned()
188 {
189 let mut target_names: Vec<_> = requested_source_exports
190 .into_iter()
191 .filter_map(|export| {
192 if &export.external_reference == &name {
193 Some(export.name.clone())
194 } else {
195 None
196 }
197 })
198 .collect();
199
200 target_names.sort();
201
202 Err(PlanError::SubsourceDuplicateReference { name, target_names })?;
203 }
204
205 Ok(())
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub enum PurifiedStatement {
210 PurifiedCreateSource {
211 create_progress_subsource_stmt: Option<CreateSubsourceStatement<Aug>>,
213 create_source_stmt: CreateSourceStatement<Aug>,
214 subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
216 available_source_references: SourceReferences,
219 },
220 PurifiedAlterSource {
221 alter_source_stmt: AlterSourceStatement<Aug>,
222 },
223 PurifiedAlterSourceAddSubsources {
224 source_name: ResolvedItemName,
226 options: Vec<AlterSourceAddSubsourceOption<Aug>>,
229 subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
231 },
232 PurifiedAlterSourceRefreshReferences {
233 source_name: ResolvedItemName,
234 available_source_references: SourceReferences,
236 },
237 PurifiedCreateSink(CreateSinkStatement<Aug>),
238 PurifiedCreateTableFromSource {
239 stmt: CreateTableFromSourceStatement<Aug>,
240 },
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct PurifiedSourceExport {
245 pub external_reference: UnresolvedItemName,
246 pub details: PurifiedExportDetails,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum PurifiedExportDetails {
251 MySql {
252 table: MySqlTableDesc,
253 text_columns: Option<Vec<Ident>>,
254 exclude_columns: Option<Vec<Ident>>,
255 initial_gtid_set: String,
256 binlog_full_metadata: bool,
257 },
258 Postgres {
259 table: PostgresTableDesc,
260 text_columns: Option<Vec<Ident>>,
261 exclude_columns: Option<Vec<Ident>>,
262 initial_lsn: MzOffset,
263 },
264 SqlServer {
265 table: SqlServerTableDesc,
266 text_columns: Option<Vec<Ident>>,
267 excl_columns: Option<Vec<Ident>>,
268 capture_instance: Arc<str>,
269 initial_lsn: mz_sql_server_util::cdc::Lsn,
270 },
271 Kafka {},
272 LoadGenerator {
273 table: Option<RelationDesc>,
274 output: LoadGeneratorOutput,
275 },
276}
277
278#[derive(Debug, Clone, Copy)]
284pub enum StatementSource {
285 Altered(CatalogItemId),
286 Read(CatalogItemId),
287}
288
289impl StatementSource {
290 pub fn id(&self) -> CatalogItemId {
291 match self {
292 StatementSource::Altered(id) | StatementSource::Read(id) => *id,
293 }
294 }
295}
296
297pub fn statement_source(
307 catalog: &impl SessionCatalog,
308 stmt: &Statement<Aug>,
309) -> Option<StatementSource> {
310 let scx = StatementContext::new(None, catalog);
311 match stmt {
312 Statement::AlterSource(stmt) => {
313 let item = scx
314 .resolve_item(RawItemName::Name(stmt.source_name.clone()))
315 .ok()?;
316 (item.item_type() == CatalogItemType::Source)
317 .then(|| StatementSource::Altered(item.id()))
318 }
319 Statement::CreateTableFromSource(stmt) => {
320 let item = scx.get_item_by_resolved_name(&stmt.source).ok()?;
321 (item.item_type() == CatalogItemType::Source).then(|| StatementSource::Read(item.id()))
322 }
323 _ => None,
324 }
325}
326
327pub async fn purify_statement(
337 catalog: impl SessionCatalog,
338 now: u64,
339 stmt: Statement<Aug>,
340 storage_configuration: &StorageConfiguration,
341) -> (Result<PurifiedStatement, PlanError>, Option<ClusterId>) {
342 match stmt {
343 Statement::CreateSource(stmt) => {
344 let cluster_id = stmt.in_cluster.as_ref().map(|cluster| cluster.id.clone());
345 (
346 purify_create_source(catalog, now, stmt, storage_configuration).await,
347 cluster_id,
348 )
349 }
350 Statement::AlterSource(stmt) => (
351 purify_alter_source(catalog, stmt, storage_configuration).await,
352 None,
353 ),
354 Statement::CreateSink(stmt) => {
355 let cluster_id = stmt.in_cluster.as_ref().map(|cluster| cluster.id.clone());
356 (
357 purify_create_sink(catalog, stmt, storage_configuration).await,
358 cluster_id,
359 )
360 }
361 Statement::CreateTableFromSource(stmt) => (
362 purify_create_table_from_source(catalog, stmt, storage_configuration).await,
363 None,
364 ),
365 o => (
366 Err(internal_err!(
367 "unexpected statement type in purification: {:?}",
368 o
369 )),
370 None,
371 ),
372 }
373}
374
375pub(crate) fn purify_create_sink_avro_doc_on_options(
380 catalog: &dyn SessionCatalog,
381 from_id: CatalogItemId,
382 format: &mut Option<FormatSpecifier<Aug>>,
383) -> Result<(), PlanError> {
384 let from = catalog.get_item(&from_id);
386 let object_ids = from
387 .references()
388 .items()
389 .copied()
390 .chain_one(from.id())
391 .collect::<Vec<_>>();
392
393 let mut avro_format_options = vec![];
396 for_each_format(format, |doc_on_schema, fmt| match fmt {
397 Format::Avro(AvroSchema::InlineSchema { .. })
398 | Format::Avro(AvroSchema::Glue { .. })
399 | Format::Bytes
400 | Format::Csv { .. }
401 | Format::Json { .. }
402 | Format::Protobuf(..)
403 | Format::Regex(..)
404 | Format::Text => (),
405 Format::Avro(AvroSchema::Csr {
406 csr_connection: CsrConnectionAvro { connection, .. },
407 }) => {
408 avro_format_options.push((doc_on_schema, &mut connection.options));
409 }
410 });
411
412 for (for_schema, options) in avro_format_options {
415 let user_provided_comments = options
416 .iter()
417 .filter_map(|CsrConfigOption { name, .. }| match name {
418 CsrConfigOptionName::AvroDocOn(doc_on) => Some(doc_on.clone()),
419 _ => None,
420 })
421 .collect::<BTreeSet<_>>();
422
423 for object_id in &object_ids {
425 let item = catalog
427 .get_item(object_id)
428 .at_version(RelationVersionSelector::Latest);
429 let full_resolved_name = ResolvedItemName::Item {
430 id: *object_id,
431 qualifiers: item.name().qualifiers.clone(),
432 full_name: catalog.resolve_full_name(item.name()),
433 print_id: !matches!(item.item_type(), CatalogItemType::Func),
434 version: RelationVersionSelector::Latest,
435 };
436
437 if let Some(comments_map) = catalog.get_item_comments(object_id) {
438 let doc_on_item_key = AvroDocOn {
441 identifier: DocOnIdentifier::Type(full_resolved_name.clone()),
442 for_schema,
443 };
444 if !user_provided_comments.contains(&doc_on_item_key) {
445 if let Some(root_comment) = comments_map.get(&None) {
446 options.push(CsrConfigOption {
447 name: CsrConfigOptionName::AvroDocOn(doc_on_item_key),
448 value: Some(mz_sql_parser::ast::WithOptionValue::Value(Value::String(
449 root_comment.clone(),
450 ))),
451 });
452 }
453 }
454
455 let column_descs = match item.type_details() {
459 Some(details) => details.typ.desc(catalog).unwrap_or_default(),
460 None => item.relation_desc().map(|d| d.into_owned()),
461 };
462
463 if let Some(desc) = column_descs {
464 for (pos, column_name) in desc.iter_names().enumerate() {
465 if let Some(comment_str) = comments_map.get(&Some(pos + 1)) {
466 let doc_on_column_key = AvroDocOn {
467 identifier: DocOnIdentifier::Column(ColumnName {
468 relation: full_resolved_name.clone(),
469 column: ResolvedColumnReference::Column {
470 name: column_name.to_owned(),
471 index: pos,
472 },
473 }),
474 for_schema,
475 };
476 if !user_provided_comments.contains(&doc_on_column_key) {
477 options.push(CsrConfigOption {
478 name: CsrConfigOptionName::AvroDocOn(doc_on_column_key),
479 value: Some(mz_sql_parser::ast::WithOptionValue::Value(
480 Value::String(comment_str.clone()),
481 )),
482 });
483 }
484 }
485 }
486 }
487 }
488 }
489 }
490
491 Ok(())
492}
493
494async fn purify_create_sink(
497 catalog: impl SessionCatalog,
498 mut create_sink_stmt: CreateSinkStatement<Aug>,
499 storage_configuration: &StorageConfiguration,
500) -> Result<PurifiedStatement, PlanError> {
501 let CreateSinkStatement {
503 connection,
504 format,
505 with_options,
506 name: _,
507 in_cluster: _,
508 if_not_exists: _,
509 from,
510 envelope: _,
511 mode: _,
512 } = &mut create_sink_stmt;
513
514 const USER_ALLOWED_WITH_OPTIONS: &[CreateSinkOptionName] = &[
516 CreateSinkOptionName::Snapshot,
517 CreateSinkOptionName::CommitInterval,
518 ];
519
520 if let Some(op) = with_options
521 .iter()
522 .find(|op| !USER_ALLOWED_WITH_OPTIONS.contains(&op.name))
523 {
524 sql_bail!(
525 "CREATE SINK...WITH ({}..) is not allowed",
526 op.name.to_ast_string_simple(),
527 )
528 }
529
530 match &connection {
531 CreateSinkConnection::Kafka {
532 connection,
533 options,
534 key: _,
535 headers: _,
536 } => {
537 let scx = StatementContext::new(None, &catalog);
542 let connection = {
543 let item = scx.get_item_by_resolved_name(connection)?;
544 match item.connection()? {
546 Connection::Kafka(connection) => {
547 connection.clone().into_inline_connection(scx.catalog)
548 }
549 _ => sql_bail!(
550 "{} is not a kafka connection",
551 scx.catalog.resolve_full_name(item.name())
552 ),
553 }
554 };
555
556 let extracted_options: KafkaSinkConfigOptionExtracted = options.clone().try_into()?;
557
558 if extracted_options.legacy_ids == Some(true) {
559 sql_bail!("LEGACY IDs option is not supported");
560 }
561
562 let client: AdminClient<_> = connection
563 .create_with_context(
564 storage_configuration,
565 MzClientContext::default(),
566 &BTreeMap::new(),
567 InTask::No,
568 )
569 .await
570 .map_err(|e| {
571 KafkaSinkPurificationError::AdminClientError(Arc::new(e))
573 })?;
574
575 let metadata = client
576 .inner()
577 .fetch_metadata(
578 None,
579 storage_configuration
580 .parameters
581 .kafka_timeout_config
582 .fetch_metadata_timeout,
583 )
584 .map_err(|e| {
585 KafkaSinkPurificationError::AdminClientError(Arc::new(
586 ContextCreationError::KafkaError(e),
587 ))
588 })?;
589
590 if metadata.brokers().len() == 0 {
591 Err(KafkaSinkPurificationError::ZeroBrokers)?;
592 }
593 }
594 CreateSinkConnection::Iceberg {
595 catalog_connection,
596 aws_connection,
597 ..
598 } => {
599 let scx = StatementContext::new(None, &catalog);
600 let connection = {
601 let item = scx.get_item_by_resolved_name(catalog_connection)?;
602 match item.connection()? {
604 Connection::IcebergCatalog(connection) => {
605 connection.clone().into_inline_connection(scx.catalog)
606 }
607 _ => sql_bail!(
608 "{} is not an iceberg connection",
609 scx.catalog.resolve_full_name(item.name())
610 ),
611 }
612 };
613
614 if let Some(s3tables) = connection.s3tables_catalog() {
618 let enable_region_check =
619 ENABLE_S3_TABLES_REGION_CHECK.get(scx.catalog.system_vars().dyncfgs());
620 if enable_region_check {
621 let env_id = &catalog.config().environment_id;
622 if matches!(env_id.cloud_provider(), CloudProvider::Aws) {
623 let env_region = env_id.cloud_provider_region();
624 let s3_tables_region = s3tables
627 .aws_connection
628 .connection
629 .region
630 .clone()
631 .unwrap_or_else(|| "us-east-1".to_string());
632 if s3_tables_region != env_region {
633 Err(IcebergSinkPurificationError::S3TablesRegionMismatch {
634 s3_tables_region,
635 environment_region: env_region.to_string(),
636 })?;
637 }
638 }
639 }
640 }
641
642 if let Some(aws_connection) = aws_connection {
648 let aws_conn_id = aws_connection.item_id();
649 let aws_connection = {
650 let item = scx.get_item_by_resolved_name(aws_connection)?;
651 match item.connection()? {
653 Connection::Aws(aws_connection) => aws_connection.clone(),
654 _ => sql_bail!(
655 "{} is not an aws connection",
656 scx.catalog.resolve_full_name(item.name())
657 ),
658 }
659 };
660
661 let _sdk_config = aws_connection
662 .load_sdk_config(
663 &storage_configuration.connection_context,
664 aws_conn_id.clone(),
665 InTask::No,
666 mz_storage_types::dyncfgs::ENFORCE_EXTERNAL_ADDRESSES
667 .get(storage_configuration.config_set()),
668 )
669 .await
670 .map_err(|e| IcebergSinkPurificationError::AwsSdkContextError(Arc::new(e)))?;
671 }
672
673 let _catalog = connection
679 .connect(storage_configuration, InTask::No, None)
680 .await
681 .map_err(|e| IcebergSinkPurificationError::CatalogError(Arc::new(e)))?;
682 }
683 }
684
685 let mut csr_connection_ids = BTreeSet::new();
686 for_each_format(format, |_, fmt| match fmt {
687 Format::Avro(AvroSchema::InlineSchema { .. })
688 | Format::Avro(AvroSchema::Glue { .. })
689 | Format::Bytes
690 | Format::Csv { .. }
691 | Format::Json { .. }
692 | Format::Protobuf(ProtobufSchema::InlineSchema { .. })
693 | Format::Regex(..)
694 | Format::Text => (),
695 Format::Avro(AvroSchema::Csr {
696 csr_connection: CsrConnectionAvro { connection, .. },
697 })
698 | Format::Protobuf(ProtobufSchema::Csr {
699 csr_connection: CsrConnectionProtobuf { connection, .. },
700 }) => {
701 csr_connection_ids.insert(*connection.connection.item_id());
702 }
703 });
704
705 let scx = StatementContext::new(None, &catalog);
706 for csr_connection_id in csr_connection_ids {
707 let connection = {
708 let item = scx.get_item(&csr_connection_id);
709 match item.connection()? {
711 Connection::Csr(connection) => connection.clone().into_inline_connection(&catalog),
712 _ => Err(CsrPurificationError::NotCsrConnection(
713 scx.catalog.resolve_full_name(item.name()),
714 ))?,
715 }
716 };
717
718 let client = connection
719 .connect(storage_configuration, InTask::No)
720 .await
721 .map_err(|e| CsrPurificationError::ClientError(Arc::new(e)))?;
722
723 client
724 .list_subjects()
725 .await
726 .map_err(|e| CsrPurificationError::ListSubjectsError(Arc::new(e)))?;
727 }
728
729 purify_create_sink_avro_doc_on_options(&catalog, *from.item_id(), format)?;
730
731 Ok(PurifiedStatement::PurifiedCreateSink(create_sink_stmt))
732}
733
734fn for_each_format<'a, F>(format: &'a mut Option<FormatSpecifier<Aug>>, mut f: F)
741where
742 F: FnMut(DocOnSchema, &'a mut Format<Aug>),
743{
744 match format {
745 None => (),
746 Some(FormatSpecifier::Bare(fmt)) => f(DocOnSchema::All, fmt),
747 Some(FormatSpecifier::KeyValue { key, value }) => {
748 f(DocOnSchema::KeyOnly, key);
749 f(DocOnSchema::ValueOnly, value);
750 }
751 }
752}
753
754#[derive(Debug, Copy, Clone, PartialEq, Eq)]
757pub(crate) enum SourceReferencePolicy {
758 NotAllowed,
761 Optional,
764 Required,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
770pub(crate) enum FilterConstraints {
771 ExcludeAll,
772 Exclude(BTreeSet<String>),
773}
774
775impl FilterConstraints {
776 pub(crate) fn none() -> Self {
777 FilterConstraints::Exclude(BTreeSet::new())
778 }
779
780 fn from_options(
781 exclude_constraints: Vec<String>,
782 exclude_all_constraints: bool,
783 ) -> Result<Self, PlanError> {
784 if !exclude_all_constraints {
785 return Ok(FilterConstraints::Exclude(
786 exclude_constraints.into_iter().collect(),
787 ));
788 }
789 if !exclude_constraints.is_empty() {
790 sql_bail!("EXCLUDE ALL CONSTRAINTS cannot be combined with EXCLUDE CONSTRAINTS");
791 }
792 Ok(FilterConstraints::ExcludeAll)
793 }
794
795 pub(crate) fn excludes_any(&self) -> bool {
796 match self {
797 FilterConstraints::ExcludeAll => true,
798 FilterConstraints::Exclude(names) => !names.is_empty(),
799 }
800 }
801}
802
803async fn purify_create_source(
804 catalog: impl SessionCatalog,
805 now: u64,
806 mut create_source_stmt: CreateSourceStatement<Aug>,
807 storage_configuration: &StorageConfiguration,
808) -> Result<PurifiedStatement, PlanError> {
809 let CreateSourceStatement {
810 name: source_name,
811 col_names,
812 key_constraint,
813 connection: source_connection,
814 format,
815 envelope,
816 include_metadata,
817 external_references,
818 progress_subsource,
819 with_options,
820 ..
821 } = &mut create_source_stmt;
822
823 let uses_old_syntax = !col_names.is_empty()
824 || key_constraint.is_some()
825 || format.is_some()
826 || envelope.is_some()
827 || !include_metadata.is_empty()
828 || external_references.is_some()
829 || progress_subsource.is_some();
830
831 if let Some(DeferredItemName::Named(_)) = progress_subsource {
832 sql_bail!("Cannot manually ID qualify progress subsource")
833 }
834
835 let mut requested_subsource_map = BTreeMap::new();
836
837 let progress_desc = match &source_connection {
838 CreateSourceConnection::Kafka { .. } => {
839 &mz_storage_types::sources::kafka::KAFKA_PROGRESS_DESC
840 }
841 CreateSourceConnection::Postgres { .. } => {
842 &mz_storage_types::sources::postgres::PG_PROGRESS_DESC
843 }
844 CreateSourceConnection::SqlServer { .. } => {
845 &mz_storage_types::sources::sql_server::SQL_SERVER_PROGRESS_DESC
846 }
847 CreateSourceConnection::MySql { .. } => {
848 &mz_storage_types::sources::mysql::MYSQL_PROGRESS_DESC
849 }
850 CreateSourceConnection::LoadGenerator { .. } => {
851 &mz_storage_types::sources::load_generator::LOAD_GEN_PROGRESS_DESC
852 }
853 };
854 let scx = StatementContext::new(None, &catalog);
855
856 let reference_policy = if scx.catalog.system_vars().enable_create_table_from_source()
860 && scx.catalog.system_vars().force_source_table_syntax()
861 {
862 SourceReferencePolicy::NotAllowed
863 } else if scx.catalog.system_vars().enable_create_table_from_source() {
864 SourceReferencePolicy::Optional
865 } else {
866 SourceReferencePolicy::Required
867 };
868
869 let mut format_options = SourceFormatOptions::Default;
870
871 let retrieved_source_references: RetrievedSourceReferences;
872
873 match source_connection {
874 CreateSourceConnection::Kafka {
875 connection,
876 options: base_with_options,
877 ..
878 } => {
879 if let Some(external_references) = external_references {
880 Err(KafkaSourcePurificationError::ReferencedSubsources(
881 external_references.clone(),
882 ))?;
883 }
884
885 let connection = {
886 let item = scx.get_item_by_resolved_name(connection)?;
887 match item.connection()? {
889 Connection::Kafka(connection) => {
890 connection.clone().into_inline_connection(&catalog)
891 }
892 _ => Err(KafkaSourcePurificationError::NotKafkaConnection(
893 scx.catalog.resolve_full_name(item.name()),
894 ))?,
895 }
896 };
897
898 let extracted_options: KafkaSourceConfigOptionExtracted =
899 base_with_options.clone().try_into()?;
900
901 let topic = extracted_options
902 .topic
903 .ok_or(KafkaSourcePurificationError::ConnectionMissingTopic)?;
904
905 let consumer = connection
906 .create_with_context(
907 storage_configuration,
908 MzClientContext::default(),
909 &BTreeMap::new(),
910 InTask::No,
911 )
912 .await
913 .map_err(|e| {
914 KafkaSourcePurificationError::KafkaConsumerError(
916 e.display_with_causes().to_string(),
917 )
918 })?;
919 let consumer = Arc::new(consumer);
920
921 match (
922 extracted_options.start_offset,
923 extracted_options.start_timestamp,
924 ) {
925 (None, None) => {
926 kafka_util::ensure_topic_exists(
928 Arc::clone(&consumer),
929 &topic,
930 storage_configuration
931 .parameters
932 .kafka_timeout_config
933 .fetch_metadata_timeout,
934 )
935 .await?;
936 }
937 (Some(_), Some(_)) => {
938 sql_bail!("cannot specify START TIMESTAMP and START OFFSET at same time")
939 }
940 (Some(start_offsets), None) => {
941 kafka_util::validate_start_offsets(
943 Arc::clone(&consumer),
944 &topic,
945 start_offsets,
946 storage_configuration
947 .parameters
948 .kafka_timeout_config
949 .fetch_metadata_timeout,
950 )
951 .await?;
952 }
953 (None, Some(time_offset)) => {
954 let start_offsets = kafka_util::lookup_start_offsets(
956 Arc::clone(&consumer),
957 &topic,
958 time_offset,
959 now,
960 storage_configuration
961 .parameters
962 .kafka_timeout_config
963 .fetch_metadata_timeout,
964 )
965 .await?;
966
967 base_with_options.retain(|val| {
968 !matches!(val.name, KafkaSourceConfigOptionName::StartTimestamp)
969 });
970 base_with_options.push(KafkaSourceConfigOption {
971 name: KafkaSourceConfigOptionName::StartOffset,
972 value: Some(WithOptionValue::Sequence(
973 start_offsets
974 .iter()
975 .map(|offset| {
976 WithOptionValue::Value(Value::Number(offset.to_string()))
977 })
978 .collect(),
979 )),
980 });
981 }
982 }
983
984 let reference_client = SourceReferenceClient::Kafka { topic: &topic };
985 retrieved_source_references = reference_client.get_source_references().await?;
986
987 format_options = SourceFormatOptions::Kafka { topic };
988 }
989 CreateSourceConnection::Postgres {
990 connection,
991 options,
992 } => {
993 let connection_item = scx.get_item_by_resolved_name(connection)?;
994 let connection = match connection_item.connection().map_err(PlanError::from)? {
995 Connection::Postgres(connection) => {
996 connection.clone().into_inline_connection(&catalog)
997 }
998 _ => Err(PgSourcePurificationError::NotPgConnection(
999 scx.catalog.resolve_full_name(connection_item.name()),
1000 ))?,
1001 };
1002 let crate::plan::statement::PgConfigOptionExtracted {
1003 publication,
1004 text_columns,
1005 exclude_columns,
1006 details,
1007 ..
1008 } = options.clone().try_into()?;
1009 let publication =
1010 publication.ok_or(PgSourcePurificationError::ConnectionMissingPublication)?;
1011
1012 if details.is_some() {
1013 Err(PgSourcePurificationError::UserSpecifiedDetails)?;
1014 }
1015
1016 let client = connection
1017 .validate(connection_item.id(), storage_configuration)
1018 .await?;
1019
1020 let reference_client = SourceReferenceClient::Postgres {
1021 client: &client,
1022 publication: &publication,
1023 database: &connection.database,
1024 };
1025 retrieved_source_references = reference_client.get_source_references().await?;
1026
1027 let is_physical_replica = mz_postgres_util::get_is_in_recovery(&client).await?;
1030
1031 let initial_lsn = MzOffset::from(
1033 mz_postgres_util::fetch_max_lsn(&client, is_physical_replica).await?,
1034 );
1035
1036 let postgres::PurifiedSourceExports {
1037 source_exports: subsources,
1038 normalized_text_columns,
1039 } = postgres::purify_source_exports(
1040 &client,
1041 &retrieved_source_references,
1042 external_references,
1043 text_columns,
1044 exclude_columns,
1045 &FilterConstraints::none(),
1046 false,
1047 source_name,
1048 &reference_policy,
1049 initial_lsn,
1050 )
1051 .await?;
1052
1053 if let Some(text_cols_option) = options
1054 .iter_mut()
1055 .find(|option| option.name == PgConfigOptionName::TextColumns)
1056 {
1057 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1058 }
1059
1060 requested_subsource_map.extend(subsources);
1061
1062 let timeline_id = mz_postgres_util::get_timeline_id(&client).await?;
1065
1066 options.retain(|PgConfigOption { name, .. }| name != &PgConfigOptionName::Details);
1068 let details = PostgresSourcePublicationDetails {
1069 slot: format!(
1070 "materialize_{}",
1071 Uuid::new_v4().to_string().replace('-', "")
1072 ),
1073 timeline_id: Some(timeline_id),
1074 database: connection.database,
1075 is_physical_replica: Some(is_physical_replica),
1076 };
1077 options.push(PgConfigOption {
1078 name: PgConfigOptionName::Details,
1079 value: Some(WithOptionValue::Value(Value::String(hex::encode(
1080 details.into_proto().encode_to_vec(),
1081 )))),
1082 })
1083 }
1084 CreateSourceConnection::SqlServer {
1085 connection,
1086 options,
1087 } => {
1088 let connection_item = scx.get_item_by_resolved_name(connection)?;
1091 let connection = match connection_item.connection()? {
1092 Connection::SqlServer(connection) => {
1093 connection.clone().into_inline_connection(&catalog)
1094 }
1095 _ => Err(SqlServerSourcePurificationError::NotSqlServerConnection(
1096 scx.catalog.resolve_full_name(connection_item.name()),
1097 ))?,
1098 };
1099 let crate::plan::statement::ddl::SqlServerConfigOptionExtracted {
1100 details,
1101 text_columns,
1102 exclude_columns,
1103 seen: _,
1104 } = options.clone().try_into()?;
1105
1106 if details.is_some() {
1107 Err(SqlServerSourcePurificationError::UserSpecifiedDetails)?;
1108 }
1109
1110 let mut client = connection
1111 .validate(connection_item.id(), storage_configuration)
1112 .await?;
1113
1114 let database: Arc<str> = connection.database.into();
1115 let reference_client = SourceReferenceClient::SqlServer {
1116 client: &mut client,
1117 database: Arc::clone(&database),
1118 };
1119 retrieved_source_references = reference_client.get_source_references().await?;
1120 tracing::debug!(?retrieved_source_references, "got source references");
1121
1122 let timeout = mz_storage_types::sources::sql_server::MAX_LSN_WAIT
1123 .get(storage_configuration.config_set());
1124
1125 let purified_source_exports = sql_server::purify_source_exports(
1126 &*database,
1127 &mut client,
1128 &retrieved_source_references,
1129 external_references,
1130 &text_columns,
1131 &exclude_columns,
1132 source_name,
1133 timeout,
1134 &reference_policy,
1135 )
1136 .await?;
1137
1138 let sql_server::PurifiedSourceExports {
1139 source_exports,
1140 normalized_text_columns,
1141 normalized_excl_columns,
1142 } = purified_source_exports;
1143
1144 requested_subsource_map.extend(source_exports);
1146
1147 let restore_history_id =
1151 mz_sql_server_util::inspect::get_latest_restore_history_id(&mut client).await?;
1152 let details = SqlServerSourceExtras { restore_history_id };
1153
1154 options.retain(|SqlServerConfigOption { name, .. }| {
1155 name != &SqlServerConfigOptionName::Details
1156 });
1157 options.push(SqlServerConfigOption {
1158 name: SqlServerConfigOptionName::Details,
1159 value: Some(WithOptionValue::Value(Value::String(hex::encode(
1160 details.into_proto().encode_to_vec(),
1161 )))),
1162 });
1163
1164 if let Some(text_cols_option) = options
1166 .iter_mut()
1167 .find(|option| option.name == SqlServerConfigOptionName::TextColumns)
1168 {
1169 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1170 }
1171 if let Some(excl_cols_option) = options
1172 .iter_mut()
1173 .find(|option| option.name == SqlServerConfigOptionName::ExcludeColumns)
1174 {
1175 excl_cols_option.value = Some(WithOptionValue::Sequence(normalized_excl_columns));
1176 }
1177 }
1178 CreateSourceConnection::MySql {
1179 connection,
1180 options,
1181 } => {
1182 let connection_item = scx.get_item_by_resolved_name(connection)?;
1183 let connection = match connection_item.connection()? {
1184 Connection::MySql(connection) => {
1185 connection.clone().into_inline_connection(&catalog)
1186 }
1187 _ => Err(MySqlSourcePurificationError::NotMySqlConnection(
1188 scx.catalog.resolve_full_name(connection_item.name()),
1189 ))?,
1190 };
1191 let crate::plan::statement::ddl::MySqlConfigOptionExtracted {
1192 details,
1193 text_columns,
1194 exclude_columns,
1195 seen: _,
1196 } = options.clone().try_into()?;
1197
1198 if details.is_some() {
1199 Err(MySqlSourcePurificationError::UserSpecifiedDetails)?;
1200 }
1201
1202 let mut conn = connection
1203 .validate(connection_item.id(), storage_configuration)
1204 .await
1205 .map_err(MySqlSourcePurificationError::InvalidConnection)?;
1206
1207 let initial_gtid_set =
1211 mz_mysql_util::query_sys_var(&mut conn, "global.gtid_executed").await?;
1212
1213 let binlog_full_metadata = is_binlog_full_metadata(&mut conn).await?;
1214
1215 let reference_client = SourceReferenceClient::MySql {
1216 conn: &mut conn,
1217 include_system_schemas: mysql::references_system_schemas(external_references),
1218 };
1219 retrieved_source_references = reference_client.get_source_references().await?;
1220
1221 let mysql::PurifiedSourceExports {
1222 source_exports: subsources,
1223 normalized_text_columns,
1224 normalized_exclude_columns,
1225 } = mysql::purify_source_exports(
1226 &mut conn,
1227 &retrieved_source_references,
1228 external_references,
1229 text_columns,
1230 exclude_columns,
1231 &FilterConstraints::none(),
1232 false,
1233 source_name,
1234 initial_gtid_set.clone(),
1235 &reference_policy,
1236 binlog_full_metadata,
1237 )
1238 .await?;
1239 requested_subsource_map.extend(subsources);
1240
1241 let details = MySqlSourceDetails {};
1244 options
1246 .retain(|MySqlConfigOption { name, .. }| name != &MySqlConfigOptionName::Details);
1247 options.push(MySqlConfigOption {
1248 name: MySqlConfigOptionName::Details,
1249 value: Some(WithOptionValue::Value(Value::String(hex::encode(
1250 details.into_proto().encode_to_vec(),
1251 )))),
1252 });
1253
1254 if let Some(text_cols_option) = options
1255 .iter_mut()
1256 .find(|option| option.name == MySqlConfigOptionName::TextColumns)
1257 {
1258 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1259 }
1260 if let Some(exclude_cols_option) = options
1261 .iter_mut()
1262 .find(|option| option.name == MySqlConfigOptionName::ExcludeColumns)
1263 {
1264 exclude_cols_option.value =
1265 Some(WithOptionValue::Sequence(normalized_exclude_columns));
1266 }
1267 }
1268 CreateSourceConnection::LoadGenerator { generator, options } => {
1269 let load_generator =
1270 load_generator_ast_to_generator(&scx, generator, options, include_metadata)?;
1271
1272 let reference_client = SourceReferenceClient::LoadGenerator {
1273 generator: &load_generator,
1274 };
1275 retrieved_source_references = reference_client.get_source_references().await?;
1276 let multi_output_sources =
1280 retrieved_source_references
1281 .all_references()
1282 .iter()
1283 .any(|r| {
1284 matches!(
1285 r.load_generator_output(),
1286 Some(output) if output != &LoadGeneratorOutput::Default
1287 )
1288 });
1289
1290 match external_references {
1291 Some(requested)
1292 if matches!(reference_policy, SourceReferencePolicy::NotAllowed) =>
1293 {
1294 Err(PlanError::UseTablesForSources(requested.to_string()))?
1295 }
1296 Some(requested) if !multi_output_sources => match requested {
1297 ExternalReferences::SubsetTables(_) => {
1298 Err(LoadGeneratorSourcePurificationError::ForTables)?
1299 }
1300 ExternalReferences::SubsetSchemas(_) => {
1301 Err(LoadGeneratorSourcePurificationError::ForSchemas)?
1302 }
1303 ExternalReferences::All => {
1304 Err(LoadGeneratorSourcePurificationError::ForAllTables)?
1305 }
1306 },
1307 Some(requested) => {
1308 let requested_exports = retrieved_source_references
1309 .requested_source_exports(Some(requested), source_name)?;
1310 for export in requested_exports {
1311 requested_subsource_map.insert(
1312 export.name,
1313 PurifiedSourceExport {
1314 external_reference: export.external_reference,
1315 details: PurifiedExportDetails::LoadGenerator {
1316 table: export
1317 .meta
1318 .load_generator_desc()
1319 .ok_or_else(|| {
1320 internal_err!(
1321 "expected load generator source reference"
1322 )
1323 })?
1324 .clone(),
1325 output: export
1326 .meta
1327 .load_generator_output()
1328 .ok_or_else(|| {
1329 internal_err!(
1330 "expected load generator source reference"
1331 )
1332 })?
1333 .clone(),
1334 },
1335 },
1336 );
1337 }
1338 }
1339 None => {
1340 if multi_output_sources
1341 && matches!(reference_policy, SourceReferencePolicy::Required)
1342 {
1343 Err(LoadGeneratorSourcePurificationError::MultiOutputRequiresForAllTables)?
1344 }
1345 }
1346 }
1347
1348 if let LoadGenerator::Clock = generator {
1349 if !options
1350 .iter()
1351 .any(|p| p.name == LoadGeneratorOptionName::AsOf)
1352 {
1353 let now = catalog.now();
1354 options.push(LoadGeneratorOption {
1355 name: LoadGeneratorOptionName::AsOf,
1356 value: Some(WithOptionValue::Value(Value::Number(now.to_string()))),
1357 });
1358 }
1359 }
1360 }
1361 }
1362
1363 *external_references = None;
1367
1368 let create_progress_subsource_stmt = if uses_old_syntax {
1370 let name = match progress_subsource {
1372 Some(name) => match name {
1373 DeferredItemName::Deferred(name) => name.clone(),
1374 DeferredItemName::Named(_) => {
1376 sql_bail!("progress subsource name cannot be a resolved name")
1377 }
1378 },
1379 None => {
1380 let (item, prefix) = source_name
1381 .0
1382 .split_last()
1383 .ok_or_else(|| sql_err!("source name must have at least one component"))?;
1384 let item_name =
1385 Ident::try_generate_name(item.to_string(), "_progress", |candidate| {
1386 let mut suggested_name = prefix.to_vec();
1387 suggested_name.push(candidate.clone());
1388
1389 let partial =
1390 normalize::unresolved_item_name(UnresolvedItemName(suggested_name))?;
1391 let qualified = scx.allocate_qualified_name(partial)?;
1392 let item_exists = scx.catalog.get_item_by_name(&qualified).is_some();
1393 let type_exists = scx.catalog.get_type_by_name(&qualified).is_some();
1394 Ok::<_, PlanError>(!item_exists && !type_exists)
1395 })?;
1396
1397 let mut full_name = prefix.to_vec();
1398 full_name.push(item_name);
1399 let full_name = normalize::unresolved_item_name(UnresolvedItemName(full_name))?;
1400 let qualified_name = scx.allocate_qualified_name(full_name)?;
1401 let full_name = scx.catalog.resolve_full_name(&qualified_name);
1402
1403 UnresolvedItemName::from(full_name.clone())
1404 }
1405 };
1406
1407 let (columns, constraints) = scx.relation_desc_into_table_defs(progress_desc)?;
1408
1409 let mut progress_with_options: Vec<_> = with_options
1411 .iter()
1412 .filter_map(|opt| match opt.name {
1413 CreateSourceOptionName::TimestampInterval => None,
1414 CreateSourceOptionName::RetainHistory => Some(CreateSubsourceOption {
1415 name: CreateSubsourceOptionName::RetainHistory,
1416 value: opt.value.clone(),
1417 }),
1418 })
1419 .collect();
1420 progress_with_options.push(CreateSubsourceOption {
1421 name: CreateSubsourceOptionName::Progress,
1422 value: Some(WithOptionValue::Value(Value::Boolean(true))),
1423 });
1424
1425 Some(CreateSubsourceStatement {
1426 name,
1427 columns,
1428 of_source: None,
1432 constraints,
1433 if_not_exists: false,
1434 with_options: progress_with_options,
1435 })
1436 } else {
1437 None
1438 };
1439
1440 purify_source_format(
1441 &catalog,
1442 format,
1443 &format_options,
1444 envelope,
1445 storage_configuration,
1446 )
1447 .await?;
1448
1449 Ok(PurifiedStatement::PurifiedCreateSource {
1450 create_progress_subsource_stmt,
1451 create_source_stmt,
1452 subsources: requested_subsource_map,
1453 available_source_references: retrieved_source_references.available_source_references(),
1454 })
1455}
1456
1457async fn purify_alter_source(
1460 catalog: impl SessionCatalog,
1461 stmt: AlterSourceStatement<Aug>,
1462 storage_configuration: &StorageConfiguration,
1463) -> Result<PurifiedStatement, PlanError> {
1464 let scx = StatementContext::new(None, &catalog);
1465 let AlterSourceStatement {
1466 source_name: unresolved_source_name,
1467 action,
1468 if_exists,
1469 } = stmt;
1470
1471 let item = match scx.resolve_item(RawItemName::Name(unresolved_source_name.clone())) {
1473 Ok(item) => item,
1474 Err(_) if if_exists => {
1475 return Ok(PurifiedStatement::PurifiedAlterSource {
1476 alter_source_stmt: AlterSourceStatement {
1477 source_name: unresolved_source_name,
1478 action,
1479 if_exists,
1480 },
1481 });
1482 }
1483 Err(e) => return Err(e),
1484 };
1485
1486 let desc = match item.source_desc()? {
1488 Some(desc) => desc.clone().into_inline_connection(scx.catalog),
1489 None => {
1490 sql_bail!("cannot ALTER this type of source")
1491 }
1492 };
1493
1494 let source_name = item.name();
1495
1496 let resolved_source_name = ResolvedItemName::Item {
1497 id: item.id(),
1498 qualifiers: item.name().qualifiers.clone(),
1499 full_name: scx.catalog.resolve_full_name(source_name),
1500 print_id: true,
1501 version: RelationVersionSelector::Latest,
1502 };
1503
1504 let partial_name = scx.catalog.minimal_qualification(source_name);
1505
1506 match action {
1507 AlterSourceAction::AddSubsources {
1508 external_references,
1509 options,
1510 } => {
1511 if scx.catalog.system_vars().enable_create_table_from_source()
1512 && scx.catalog.system_vars().force_source_table_syntax()
1513 {
1514 Err(PlanError::UseTablesForSources(
1515 "ALTER SOURCE .. ADD SUBSOURCES ..".to_string(),
1516 ))?;
1517 }
1518
1519 purify_alter_source_add_subsources(
1520 external_references,
1521 options,
1522 desc,
1523 partial_name,
1524 unresolved_source_name,
1525 resolved_source_name,
1526 storage_configuration,
1527 )
1528 .await
1529 }
1530 AlterSourceAction::RefreshReferences => {
1531 purify_alter_source_refresh_references(
1532 desc,
1533 resolved_source_name,
1534 storage_configuration,
1535 )
1536 .await
1537 }
1538 _ => Ok(PurifiedStatement::PurifiedAlterSource {
1539 alter_source_stmt: AlterSourceStatement {
1540 source_name: unresolved_source_name,
1541 action,
1542 if_exists,
1543 },
1544 }),
1545 }
1546}
1547
1548async fn purify_alter_source_add_subsources(
1551 external_references: Vec<ExternalReferenceExport>,
1552 mut options: Vec<AlterSourceAddSubsourceOption<Aug>>,
1553 desc: SourceDesc,
1554 partial_source_name: PartialItemName,
1555 unresolved_source_name: UnresolvedItemName,
1556 resolved_source_name: ResolvedItemName,
1557 storage_configuration: &StorageConfiguration,
1558) -> Result<PurifiedStatement, PlanError> {
1559 let connection_id = match &desc.connection {
1561 GenericSourceConnection::Postgres(c) => c.connection_id,
1562 GenericSourceConnection::MySql(c) => c.connection_id,
1563 GenericSourceConnection::SqlServer(c) => c.connection_id,
1564 _ => sql_bail!(
1565 "source {} does not support ALTER SOURCE.",
1566 partial_source_name
1567 ),
1568 };
1569
1570 let crate::plan::statement::ddl::AlterSourceAddSubsourceOptionExtracted {
1571 text_columns,
1572 exclude_columns,
1573 details,
1574 seen: _,
1575 } = options.clone().try_into()?;
1576 if details.is_some() {
1577 sql_bail!("DETAILS option cannot be explicitly set");
1578 }
1579
1580 let mut requested_subsource_map = BTreeMap::new();
1581
1582 match desc.connection {
1583 GenericSourceConnection::Postgres(pg_source_connection) => {
1584 let pg_connection = &pg_source_connection.connection;
1586
1587 let client = pg_connection
1588 .validate(connection_id, storage_configuration)
1589 .await?;
1590
1591 let reference_client = SourceReferenceClient::Postgres {
1592 client: &client,
1593 publication: &pg_source_connection.publication,
1594 database: &pg_connection.database,
1595 };
1596 let retrieved_source_references = reference_client.get_source_references().await?;
1597
1598 let initial_lsn = MzOffset::from(
1600 mz_postgres_util::fetch_max_lsn(
1601 &client,
1602 pg_source_connection
1603 .publication_details
1604 .get_is_physical_replica(),
1605 )
1606 .await?,
1607 );
1608
1609 let postgres::PurifiedSourceExports {
1610 source_exports: subsources,
1611 normalized_text_columns,
1612 } = postgres::purify_source_exports(
1613 &client,
1614 &retrieved_source_references,
1615 &Some(ExternalReferences::SubsetTables(external_references)),
1616 text_columns,
1617 exclude_columns,
1618 &FilterConstraints::none(),
1619 false,
1620 &unresolved_source_name,
1621 &SourceReferencePolicy::Required,
1622 initial_lsn,
1623 )
1624 .await?;
1625
1626 if let Some(text_cols_option) = options
1627 .iter_mut()
1628 .find(|option| option.name == AlterSourceAddSubsourceOptionName::TextColumns)
1629 {
1630 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1631 }
1632
1633 requested_subsource_map.extend(subsources);
1634 }
1635 GenericSourceConnection::MySql(mysql_source_connection) => {
1636 let mysql_connection = &mysql_source_connection.connection;
1637 let config = mysql_connection
1638 .config(
1639 &storage_configuration.connection_context.secrets_reader,
1640 storage_configuration,
1641 InTask::No,
1642 )
1643 .await?;
1644
1645 let mut conn = config
1646 .connect(
1647 "mysql purification",
1648 &storage_configuration.connection_context.ssh_tunnel_manager,
1649 )
1650 .await?;
1651
1652 let initial_gtid_set =
1655 mz_mysql_util::query_sys_var(&mut conn, "global.gtid_executed").await?;
1656
1657 let binlog_full_metadata = is_binlog_full_metadata(&mut conn).await?;
1658
1659 let requested_references = Some(ExternalReferences::SubsetTables(external_references));
1660
1661 let reference_client = SourceReferenceClient::MySql {
1662 conn: &mut conn,
1663 include_system_schemas: mysql::references_system_schemas(&requested_references),
1664 };
1665 let retrieved_source_references = reference_client.get_source_references().await?;
1666
1667 let mysql::PurifiedSourceExports {
1668 source_exports: subsources,
1669 normalized_text_columns,
1670 normalized_exclude_columns,
1671 } = mysql::purify_source_exports(
1672 &mut conn,
1673 &retrieved_source_references,
1674 &requested_references,
1675 text_columns,
1676 exclude_columns,
1677 &FilterConstraints::none(),
1678 false,
1679 &unresolved_source_name,
1680 initial_gtid_set,
1681 &SourceReferencePolicy::Required,
1682 binlog_full_metadata,
1683 )
1684 .await?;
1685 requested_subsource_map.extend(subsources);
1686
1687 if let Some(text_cols_option) = options
1689 .iter_mut()
1690 .find(|option| option.name == AlterSourceAddSubsourceOptionName::TextColumns)
1691 {
1692 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1693 }
1694 if let Some(exclude_cols_option) = options
1695 .iter_mut()
1696 .find(|option| option.name == AlterSourceAddSubsourceOptionName::ExcludeColumns)
1697 {
1698 exclude_cols_option.value =
1699 Some(WithOptionValue::Sequence(normalized_exclude_columns));
1700 }
1701 }
1702 GenericSourceConnection::SqlServer(sql_server_source) => {
1703 let sql_server_connection = &sql_server_source.connection;
1705 let config = sql_server_connection
1706 .resolve_config(
1707 &storage_configuration.connection_context.secrets_reader,
1708 storage_configuration,
1709 InTask::No,
1710 )
1711 .await?;
1712 let mut client = mz_sql_server_util::Client::connect(config).await?;
1713
1714 let database = sql_server_connection.database.clone().into();
1716 let source_references = SourceReferenceClient::SqlServer {
1717 client: &mut client,
1718 database: Arc::clone(&database),
1719 }
1720 .get_source_references()
1721 .await?;
1722 let requested_references = Some(ExternalReferences::SubsetTables(external_references));
1723
1724 let timeout = mz_storage_types::sources::sql_server::MAX_LSN_WAIT
1725 .get(storage_configuration.config_set());
1726
1727 let result = sql_server::purify_source_exports(
1728 &*database,
1729 &mut client,
1730 &source_references,
1731 &requested_references,
1732 &text_columns,
1733 &exclude_columns,
1734 &unresolved_source_name,
1735 timeout,
1736 &SourceReferencePolicy::Required,
1737 )
1738 .await;
1739 let sql_server::PurifiedSourceExports {
1740 source_exports,
1741 normalized_text_columns,
1742 normalized_excl_columns,
1743 } = result?;
1744
1745 requested_subsource_map.extend(source_exports);
1747
1748 if let Some(text_cols_option) = options
1750 .iter_mut()
1751 .find(|option| option.name == AlterSourceAddSubsourceOptionName::TextColumns)
1752 {
1753 text_cols_option.value = Some(WithOptionValue::Sequence(normalized_text_columns));
1754 }
1755 if let Some(exclude_cols_option) = options
1756 .iter_mut()
1757 .find(|option| option.name == AlterSourceAddSubsourceOptionName::ExcludeColumns)
1758 {
1759 exclude_cols_option.value =
1760 Some(WithOptionValue::Sequence(normalized_excl_columns));
1761 }
1762 }
1763 _ => bail_internal!("source does not support ALTER SOURCE...ADD SUBSOURCE"),
1766 };
1767
1768 Ok(PurifiedStatement::PurifiedAlterSourceAddSubsources {
1769 source_name: resolved_source_name,
1770 options,
1771 subsources: requested_subsource_map,
1772 })
1773}
1774
1775async fn purify_alter_source_refresh_references(
1776 desc: SourceDesc,
1777 resolved_source_name: ResolvedItemName,
1778 storage_configuration: &StorageConfiguration,
1779) -> Result<PurifiedStatement, PlanError> {
1780 let retrieved_source_references = match desc.connection {
1781 GenericSourceConnection::Postgres(pg_source_connection) => {
1782 let pg_connection = &pg_source_connection.connection;
1784
1785 let config = pg_connection
1786 .config(
1787 &storage_configuration.connection_context.secrets_reader,
1788 storage_configuration,
1789 InTask::No,
1790 )
1791 .await?;
1792
1793 let client = config
1794 .connect(
1795 "postgres_purification",
1796 &storage_configuration.connection_context.ssh_tunnel_manager,
1797 )
1798 .await?;
1799 let reference_client = SourceReferenceClient::Postgres {
1800 client: &client,
1801 publication: &pg_source_connection.publication,
1802 database: &pg_connection.database,
1803 };
1804 reference_client.get_source_references().await?
1805 }
1806 GenericSourceConnection::MySql(mysql_source_connection) => {
1807 let mysql_connection = &mysql_source_connection.connection;
1808 let config = mysql_connection
1809 .config(
1810 &storage_configuration.connection_context.secrets_reader,
1811 storage_configuration,
1812 InTask::No,
1813 )
1814 .await?;
1815
1816 let mut conn = config
1817 .connect(
1818 "mysql purification",
1819 &storage_configuration.connection_context.ssh_tunnel_manager,
1820 )
1821 .await?;
1822
1823 let reference_client = SourceReferenceClient::MySql {
1824 conn: &mut conn,
1825 include_system_schemas: false,
1826 };
1827 reference_client.get_source_references().await?
1828 }
1829 GenericSourceConnection::SqlServer(sql_server_source) => {
1830 let sql_server_connection = &sql_server_source.connection;
1832 let config = sql_server_connection
1833 .resolve_config(
1834 &storage_configuration.connection_context.secrets_reader,
1835 storage_configuration,
1836 InTask::No,
1837 )
1838 .await?;
1839 let mut client = mz_sql_server_util::Client::connect(config).await?;
1840
1841 let source_references = SourceReferenceClient::SqlServer {
1843 client: &mut client,
1844 database: sql_server_connection.database.clone().into(),
1845 }
1846 .get_source_references()
1847 .await?;
1848 source_references
1849 }
1850 GenericSourceConnection::LoadGenerator(load_gen_connection) => {
1851 let reference_client = SourceReferenceClient::LoadGenerator {
1852 generator: &load_gen_connection.load_generator,
1853 };
1854 reference_client.get_source_references().await?
1855 }
1856 GenericSourceConnection::Kafka(kafka_conn) => {
1857 let reference_client = SourceReferenceClient::Kafka {
1858 topic: &kafka_conn.topic,
1859 };
1860 reference_client.get_source_references().await?
1861 }
1862 };
1863 Ok(PurifiedStatement::PurifiedAlterSourceRefreshReferences {
1864 source_name: resolved_source_name,
1865 available_source_references: retrieved_source_references.available_source_references(),
1866 })
1867}
1868
1869async fn purify_create_table_from_source(
1870 catalog: impl SessionCatalog,
1871 mut stmt: CreateTableFromSourceStatement<Aug>,
1872 storage_configuration: &StorageConfiguration,
1873) -> Result<PurifiedStatement, PlanError> {
1874 let scx = StatementContext::new(None, &catalog);
1875 let CreateTableFromSourceStatement {
1876 name: _,
1877 columns,
1878 constraints,
1879 source: source_name,
1880 if_not_exists: _,
1881 external_reference,
1882 format,
1883 envelope,
1884 include_metadata: _,
1885 with_options,
1886 } = &mut stmt;
1887
1888 if matches!(columns, TableFromSourceColumns::Defined(_)) {
1890 sql_bail!("CREATE TABLE .. FROM SOURCE column definitions cannot be specified directly");
1891 }
1892 if !constraints.is_empty() {
1893 sql_bail!(
1894 "CREATE TABLE .. FROM SOURCE constraint definitions cannot be specified directly"
1895 );
1896 }
1897
1898 let item = match scx.get_item_by_resolved_name(source_name) {
1900 Ok(item) => item,
1901 Err(e) => return Err(e),
1902 };
1903
1904 let desc = match item.source_desc()? {
1906 Some(desc) => desc.clone().into_inline_connection(scx.catalog),
1907 None => {
1908 sql_bail!("cannot ALTER this type of source")
1909 }
1910 };
1911 let unresolved_source_name: UnresolvedItemName = source_name.full_item_name().clone().into();
1912
1913 let crate::plan::statement::ddl::TableFromSourceOptionExtracted {
1914 text_columns,
1915 exclude_columns,
1916 exclude_constraints,
1917 exclude_all_constraints,
1918 exclude_all_not_null_constraints,
1919 retain_history: _,
1920 details,
1921 partition_by: _,
1922 seen: _,
1923 } = with_options.clone().try_into()?;
1924 if details.is_some() {
1925 sql_bail!("DETAILS option cannot be explicitly set");
1926 }
1927
1928 let filter_constraints =
1929 FilterConstraints::from_options(exclude_constraints, exclude_all_constraints)?;
1930 if filter_constraints.excludes_any() || exclude_all_not_null_constraints {
1931 scx.require_feature_flag(&crate::session::vars::ENABLE_EXCLUDE_CONSTRAINTS_OPTION)?;
1932 }
1933
1934 let qualified_text_columns = text_columns
1938 .iter()
1939 .map(|col| {
1940 UnresolvedItemName(
1941 external_reference
1942 .as_ref()
1943 .map(|er| er.0.iter().chain_one(col).map(|i| i.clone()).collect())
1944 .unwrap_or_else(|| vec![col.clone()]),
1945 )
1946 })
1947 .collect_vec();
1948 let qualified_exclude_columns = exclude_columns
1949 .iter()
1950 .map(|col| {
1951 UnresolvedItemName(
1952 external_reference
1953 .as_ref()
1954 .map(|er| er.0.iter().chain_one(col).map(|i| i.clone()).collect())
1955 .unwrap_or_else(|| vec![col.clone()]),
1956 )
1957 })
1958 .collect_vec();
1959
1960 let mut format_options = SourceFormatOptions::Default;
1962
1963 let retrieved_source_references: RetrievedSourceReferences;
1964
1965 let requested_references = external_reference.as_ref().map(|ref_name| {
1966 ExternalReferences::SubsetTables(vec![ExternalReferenceExport {
1967 reference: ref_name.clone(),
1968 alias: None,
1969 }])
1970 });
1971
1972 if (filter_constraints.excludes_any() || exclude_all_not_null_constraints)
1973 && !matches!(
1974 desc.connection,
1975 GenericSourceConnection::Postgres(_) | GenericSourceConnection::MySql(_)
1976 )
1977 {
1978 sql_bail!(
1979 "EXCLUDE CONSTRAINTS is not supported for {} sources",
1980 desc.connection.name()
1981 );
1982 }
1983
1984 let purified_export = match desc.connection {
1987 GenericSourceConnection::Postgres(pg_source_connection) => {
1988 let pg_connection = &pg_source_connection.connection;
1990
1991 let client = pg_connection
1992 .validate(pg_source_connection.connection_id, storage_configuration)
1993 .await?;
1994
1995 let reference_client = SourceReferenceClient::Postgres {
1996 client: &client,
1997 publication: &pg_source_connection.publication,
1998 database: &pg_connection.database,
1999 };
2000 retrieved_source_references = reference_client.get_source_references().await?;
2001
2002 let initial_lsn = MzOffset::from(
2004 mz_postgres_util::fetch_max_lsn(
2005 &client,
2006 pg_source_connection
2007 .publication_details
2008 .get_is_physical_replica(),
2009 )
2010 .await?,
2011 );
2012
2013 let postgres::PurifiedSourceExports {
2014 source_exports,
2015 normalized_text_columns: _,
2019 } = postgres::purify_source_exports(
2020 &client,
2021 &retrieved_source_references,
2022 &requested_references,
2023 qualified_text_columns,
2024 qualified_exclude_columns,
2025 &filter_constraints,
2026 exclude_all_not_null_constraints,
2027 &unresolved_source_name,
2028 &SourceReferencePolicy::Required,
2029 initial_lsn,
2030 )
2031 .await?;
2032 let (_, purified_export) = source_exports.into_element();
2034 purified_export
2035 }
2036 GenericSourceConnection::MySql(mysql_source_connection) => {
2037 let mysql_connection = &mysql_source_connection.connection;
2038 let config = mysql_connection
2039 .config(
2040 &storage_configuration.connection_context.secrets_reader,
2041 storage_configuration,
2042 InTask::No,
2043 )
2044 .await?;
2045
2046 let mut conn = config
2047 .connect(
2048 "mysql purification",
2049 &storage_configuration.connection_context.ssh_tunnel_manager,
2050 )
2051 .await?;
2052
2053 ensure_binlog_full_metadata(&mut conn).await?;
2054 let binlog_full_metadata = true;
2055
2056 let initial_gtid_set =
2059 mz_mysql_util::query_sys_var(&mut conn, "global.gtid_executed").await?;
2060
2061 let reference_client = SourceReferenceClient::MySql {
2062 conn: &mut conn,
2063 include_system_schemas: mysql::references_system_schemas(&requested_references),
2064 };
2065 retrieved_source_references = reference_client.get_source_references().await?;
2066
2067 let mysql::PurifiedSourceExports {
2068 source_exports,
2069 normalized_text_columns: _,
2073 normalized_exclude_columns: _,
2074 } = mysql::purify_source_exports(
2075 &mut conn,
2076 &retrieved_source_references,
2077 &requested_references,
2078 qualified_text_columns,
2079 qualified_exclude_columns,
2080 &filter_constraints,
2081 exclude_all_not_null_constraints,
2082 &unresolved_source_name,
2083 initial_gtid_set,
2084 &SourceReferencePolicy::Required,
2085 binlog_full_metadata,
2086 )
2087 .await?;
2088 let (_, purified_export) = source_exports.into_element();
2090 purified_export
2091 }
2092 GenericSourceConnection::SqlServer(sql_server_source) => {
2093 let connection = sql_server_source.connection;
2094 let config = connection
2095 .resolve_config(
2096 &storage_configuration.connection_context.secrets_reader,
2097 storage_configuration,
2098 InTask::No,
2099 )
2100 .await?;
2101 let mut client = mz_sql_server_util::Client::connect(config).await?;
2102
2103 let database: Arc<str> = connection.database.into();
2104 let reference_client = SourceReferenceClient::SqlServer {
2105 client: &mut client,
2106 database: Arc::clone(&database),
2107 };
2108 retrieved_source_references = reference_client.get_source_references().await?;
2109 tracing::debug!(?retrieved_source_references, "got source references");
2110
2111 let timeout = mz_storage_types::sources::sql_server::MAX_LSN_WAIT
2112 .get(storage_configuration.config_set());
2113
2114 let purified_source_exports = sql_server::purify_source_exports(
2115 &*database,
2116 &mut client,
2117 &retrieved_source_references,
2118 &requested_references,
2119 &qualified_text_columns,
2120 &qualified_exclude_columns,
2121 &unresolved_source_name,
2122 timeout,
2123 &SourceReferencePolicy::Required,
2124 )
2125 .await?;
2126
2127 let (_, purified_export) = purified_source_exports.source_exports.into_element();
2129 purified_export
2130 }
2131 GenericSourceConnection::LoadGenerator(load_gen_connection) => {
2132 let reference_client = SourceReferenceClient::LoadGenerator {
2133 generator: &load_gen_connection.load_generator,
2134 };
2135 retrieved_source_references = reference_client.get_source_references().await?;
2136
2137 let requested_exports = retrieved_source_references
2138 .requested_source_exports(requested_references.as_ref(), &unresolved_source_name)?;
2139 let export = requested_exports.into_element();
2141 PurifiedSourceExport {
2142 external_reference: export.external_reference,
2143 details: PurifiedExportDetails::LoadGenerator {
2144 table: export
2145 .meta
2146 .load_generator_desc()
2147 .ok_or_else(|| internal_err!("expected load generator source reference"))?
2148 .clone(),
2149 output: export
2150 .meta
2151 .load_generator_output()
2152 .ok_or_else(|| internal_err!("expected load generator source reference"))?
2153 .clone(),
2154 },
2155 }
2156 }
2157 GenericSourceConnection::Kafka(kafka_conn) => {
2158 let reference_client = SourceReferenceClient::Kafka {
2159 topic: &kafka_conn.topic,
2160 };
2161 retrieved_source_references = reference_client.get_source_references().await?;
2162 let requested_exports = retrieved_source_references
2163 .requested_source_exports(requested_references.as_ref(), &unresolved_source_name)?;
2164 let export = requested_exports.into_element();
2166
2167 format_options = SourceFormatOptions::Kafka {
2168 topic: kafka_conn.topic.clone(),
2169 };
2170 PurifiedSourceExport {
2171 external_reference: export.external_reference,
2172 details: PurifiedExportDetails::Kafka {},
2173 }
2174 }
2175 };
2176
2177 purify_source_format(
2178 &catalog,
2179 format,
2180 &format_options,
2181 envelope,
2182 storage_configuration,
2183 )
2184 .await?;
2185
2186 *external_reference = Some(purified_export.external_reference.clone());
2189
2190 match &purified_export.details {
2192 PurifiedExportDetails::Postgres { .. } => {
2193 let mut unsupported_cols = vec![];
2194 let postgres::PostgresExportStatementValues {
2195 columns: gen_columns,
2196 constraints: gen_constraints,
2197 text_columns: gen_text_columns,
2198 exclude_columns: gen_exclude_columns,
2199 details: gen_details,
2200 external_reference: _,
2201 } = postgres::generate_source_export_statement_values(
2202 &scx,
2203 purified_export,
2204 &mut unsupported_cols,
2205 )?;
2206 if !unsupported_cols.is_empty() {
2207 unsupported_cols.sort();
2208 Err(PgSourcePurificationError::UnrecognizedTypes {
2209 cols: unsupported_cols,
2210 })?;
2211 }
2212
2213 if let Some(text_cols_option) = with_options
2214 .iter_mut()
2215 .find(|option| option.name == TableFromSourceOptionName::TextColumns)
2216 {
2217 if let Some(gen_text_columns) = gen_text_columns {
2218 text_cols_option.value = Some(WithOptionValue::Sequence(gen_text_columns));
2219 }
2220 }
2221 if let Some(exclude_cols_option) = with_options
2222 .iter_mut()
2223 .find(|option| option.name == TableFromSourceOptionName::ExcludeColumns)
2224 {
2225 if let Some(gen_exclude_columns) = gen_exclude_columns {
2226 exclude_cols_option.value =
2227 Some(WithOptionValue::Sequence(gen_exclude_columns));
2228 }
2229 }
2230 match columns {
2231 TableFromSourceColumns::Defined(_) => {
2232 bail_internal!(
2235 "column definitions cannot be explicitly set for this source type"
2236 )
2237 }
2238 TableFromSourceColumns::NotSpecified => {
2239 *columns = TableFromSourceColumns::Defined(gen_columns);
2240 *constraints = gen_constraints;
2241 }
2242 TableFromSourceColumns::Named(_) => {
2243 sql_bail!("columns cannot be named for Postgres sources")
2244 }
2245 }
2246 with_options.push(TableFromSourceOption {
2247 name: TableFromSourceOptionName::Details,
2248 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2249 gen_details.into_proto().encode_to_vec(),
2250 )))),
2251 })
2252 }
2253 PurifiedExportDetails::MySql { .. } => {
2254 let mysql::MySqlExportStatementValues {
2255 columns: gen_columns,
2256 constraints: gen_constraints,
2257 text_columns: gen_text_columns,
2258 exclude_columns: gen_exclude_columns,
2259 details: gen_details,
2260 external_reference: _,
2261 } = mysql::generate_source_export_statement_values(&scx, purified_export)?;
2262
2263 if let Some(text_cols_option) = with_options
2264 .iter_mut()
2265 .find(|option| option.name == TableFromSourceOptionName::TextColumns)
2266 {
2267 if let Some(gen_text_columns) = gen_text_columns {
2268 text_cols_option.value = Some(WithOptionValue::Sequence(gen_text_columns));
2269 }
2270 }
2271 if let Some(exclude_cols_option) = with_options
2272 .iter_mut()
2273 .find(|option| option.name == TableFromSourceOptionName::ExcludeColumns)
2274 {
2275 if let Some(gen_exclude_columns) = gen_exclude_columns {
2276 exclude_cols_option.value =
2277 Some(WithOptionValue::Sequence(gen_exclude_columns));
2278 }
2279 }
2280 match columns {
2281 TableFromSourceColumns::Defined(_) => {
2282 bail_internal!(
2285 "column definitions cannot be explicitly set for this source type"
2286 )
2287 }
2288 TableFromSourceColumns::NotSpecified => {
2289 *columns = TableFromSourceColumns::Defined(gen_columns);
2290 *constraints = gen_constraints;
2291 }
2292 TableFromSourceColumns::Named(_) => {
2293 sql_bail!("columns cannot be named for MySQL sources")
2294 }
2295 }
2296 with_options.push(TableFromSourceOption {
2297 name: TableFromSourceOptionName::Details,
2298 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2299 gen_details.into_proto().encode_to_vec(),
2300 )))),
2301 })
2302 }
2303 PurifiedExportDetails::SqlServer { .. } => {
2304 let sql_server::SqlServerExportStatementValues {
2305 columns: gen_columns,
2306 constraints: gen_constraints,
2307 text_columns: gen_text_columns,
2308 excl_columns: gen_excl_columns,
2309 details: gen_details,
2310 external_reference: _,
2311 } = sql_server::generate_source_export_statement_values(&scx, purified_export)?;
2312
2313 if let Some(text_cols_option) = with_options
2314 .iter_mut()
2315 .find(|opt| opt.name == TableFromSourceOptionName::TextColumns)
2316 {
2317 if let Some(gen_text_columns) = gen_text_columns {
2318 text_cols_option.value = Some(WithOptionValue::Sequence(gen_text_columns));
2319 }
2320 }
2321 if let Some(exclude_cols_option) = with_options
2322 .iter_mut()
2323 .find(|opt| opt.name == TableFromSourceOptionName::ExcludeColumns)
2324 {
2325 if let Some(gen_excl_columns) = gen_excl_columns {
2326 exclude_cols_option.value = Some(WithOptionValue::Sequence(gen_excl_columns));
2327 }
2328 }
2329
2330 match columns {
2331 TableFromSourceColumns::NotSpecified => {
2332 *columns = TableFromSourceColumns::Defined(gen_columns);
2333 *constraints = gen_constraints;
2334 }
2335 TableFromSourceColumns::Named(_) => {
2336 sql_bail!("columns cannot be named for SQL Server sources")
2337 }
2338 TableFromSourceColumns::Defined(_) => {
2339 bail_internal!(
2342 "column definitions cannot be explicitly set for this source type"
2343 )
2344 }
2345 }
2346
2347 with_options.push(TableFromSourceOption {
2348 name: TableFromSourceOptionName::Details,
2349 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2350 gen_details.into_proto().encode_to_vec(),
2351 )))),
2352 })
2353 }
2354 PurifiedExportDetails::LoadGenerator { .. } => {
2355 let (desc, output) = match purified_export.details {
2356 PurifiedExportDetails::LoadGenerator { table, output } => (table, output),
2357 _ => bail_internal!("purified export details must be load generator"),
2358 };
2359 if let Some(desc) = desc {
2364 let (gen_columns, gen_constraints) = scx.relation_desc_into_table_defs(&desc)?;
2365 match columns {
2366 TableFromSourceColumns::Defined(_) => bail_internal!(
2369 "column definitions cannot be explicitly set for this source type"
2370 ),
2371 TableFromSourceColumns::NotSpecified => {
2372 *columns = TableFromSourceColumns::Defined(gen_columns);
2373 *constraints = gen_constraints;
2374 }
2375 TableFromSourceColumns::Named(_) => {
2376 sql_bail!("columns cannot be named for multi-output load generator sources")
2377 }
2378 }
2379 }
2380 let details = SourceExportStatementDetails::LoadGenerator { output };
2381 with_options.push(TableFromSourceOption {
2382 name: TableFromSourceOptionName::Details,
2383 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2384 details.into_proto().encode_to_vec(),
2385 )))),
2386 })
2387 }
2388 PurifiedExportDetails::Kafka {} => {
2389 let details = SourceExportStatementDetails::Kafka {};
2393 with_options.push(TableFromSourceOption {
2394 name: TableFromSourceOptionName::Details,
2395 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2396 details.into_proto().encode_to_vec(),
2397 )))),
2398 })
2399 }
2400 };
2401
2402 Ok(PurifiedStatement::PurifiedCreateTableFromSource { stmt })
2406}
2407
2408enum SourceFormatOptions {
2409 Default,
2410 Kafka { topic: String },
2411}
2412
2413async fn purify_source_format(
2414 catalog: &dyn SessionCatalog,
2415 format: &mut Option<FormatSpecifier<Aug>>,
2416 options: &SourceFormatOptions,
2417 envelope: &Option<SourceEnvelope>,
2418 storage_configuration: &StorageConfiguration,
2419) -> Result<(), PlanError> {
2420 if matches!(format, Some(FormatSpecifier::KeyValue { .. }))
2421 && !matches!(options, SourceFormatOptions::Kafka { .. })
2422 {
2423 sql_bail!("Kafka sources are the only source type that can provide KEY/VALUE formats")
2424 }
2425
2426 match format.as_mut() {
2427 None => {}
2428 Some(FormatSpecifier::Bare(format)) => {
2429 purify_source_format_single(catalog, format, options, envelope, storage_configuration)
2430 .await?;
2431 }
2432
2433 Some(FormatSpecifier::KeyValue { key, value: val }) => {
2434 purify_source_format_single(catalog, key, options, envelope, storage_configuration)
2435 .await?;
2436 purify_source_format_single(catalog, val, options, envelope, storage_configuration)
2437 .await?;
2438 }
2439 }
2440 Ok(())
2441}
2442
2443async fn purify_source_format_single(
2444 catalog: &dyn SessionCatalog,
2445 format: &mut Format<Aug>,
2446 options: &SourceFormatOptions,
2447 envelope: &Option<SourceEnvelope>,
2448 storage_configuration: &StorageConfiguration,
2449) -> Result<(), PlanError> {
2450 match format {
2451 Format::Avro(schema) => match schema {
2452 AvroSchema::Csr { csr_connection } => {
2453 purify_csr_connection_avro(
2454 catalog,
2455 options,
2456 csr_connection,
2457 envelope,
2458 storage_configuration,
2459 )
2460 .await?
2461 }
2462 AvroSchema::InlineSchema { .. } => {}
2463 AvroSchema::Glue {
2464 connection,
2465 with_options,
2466 seed,
2467 } => {
2468 purify_glue_connection_avro(
2469 catalog,
2470 options,
2471 connection,
2472 with_options,
2473 seed,
2474 storage_configuration,
2475 )
2476 .await?
2477 }
2478 },
2479 Format::Protobuf(schema) => match schema {
2480 ProtobufSchema::Csr { csr_connection } => {
2481 purify_csr_connection_proto(
2482 catalog,
2483 options,
2484 csr_connection,
2485 envelope,
2486 storage_configuration,
2487 )
2488 .await?;
2489 }
2490 ProtobufSchema::InlineSchema { .. } => {}
2491 },
2492 Format::Bytes
2493 | Format::Regex(_)
2494 | Format::Json { .. }
2495 | Format::Text
2496 | Format::Csv { .. } => (),
2497 }
2498 Ok(())
2499}
2500
2501pub fn generate_subsource_statements(
2502 scx: &StatementContext,
2503 source_name: ResolvedItemName,
2504 subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
2505) -> Result<Vec<CreateSubsourceStatement<Aug>>, PlanError> {
2506 if subsources.is_empty() {
2508 return Ok(vec![]);
2509 }
2510 let (_, purified_export) = subsources
2511 .iter()
2512 .next()
2513 .ok_or_else(|| internal_err!("expected at least one subsource"))?;
2514
2515 let statements = match &purified_export.details {
2516 PurifiedExportDetails::Postgres { .. } => {
2517 crate::pure::postgres::generate_create_subsource_statements(
2518 scx,
2519 source_name,
2520 subsources,
2521 )?
2522 }
2523 PurifiedExportDetails::MySql { .. } => {
2524 crate::pure::mysql::generate_create_subsource_statements(scx, source_name, subsources)?
2525 }
2526 PurifiedExportDetails::SqlServer { .. } => {
2527 crate::pure::sql_server::generate_create_subsource_statements(
2528 scx,
2529 source_name,
2530 subsources,
2531 )?
2532 }
2533 PurifiedExportDetails::LoadGenerator { .. } => {
2534 let mut subsource_stmts = Vec::with_capacity(subsources.len());
2535 for (subsource_name, purified_export) in subsources {
2536 let (desc, output) = match purified_export.details {
2537 PurifiedExportDetails::LoadGenerator { table, output } => (table, output),
2538 _ => {
2539 bail_internal!("purified export details must be load generator")
2540 }
2541 };
2542 let desc = desc.ok_or_else(|| {
2543 internal_err!(
2544 "subsources cannot be generated for single-output load generators"
2545 )
2546 })?;
2547
2548 let (columns, table_constraints) = scx.relation_desc_into_table_defs(&desc)?;
2549 let details = SourceExportStatementDetails::LoadGenerator { output };
2550 let subsource = CreateSubsourceStatement {
2552 name: subsource_name,
2553 columns,
2554 of_source: Some(source_name.clone()),
2555 constraints: table_constraints,
2560 if_not_exists: false,
2561 with_options: vec![
2562 CreateSubsourceOption {
2563 name: CreateSubsourceOptionName::ExternalReference,
2564 value: Some(WithOptionValue::UnresolvedItemName(
2565 purified_export.external_reference,
2566 )),
2567 },
2568 CreateSubsourceOption {
2569 name: CreateSubsourceOptionName::Details,
2570 value: Some(WithOptionValue::Value(Value::String(hex::encode(
2571 details.into_proto().encode_to_vec(),
2572 )))),
2573 },
2574 ],
2575 };
2576 subsource_stmts.push(subsource);
2577 }
2578
2579 subsource_stmts
2580 }
2581 PurifiedExportDetails::Kafka { .. } => {
2582 if !subsources.is_empty() {
2586 bail_internal!("Kafka sources do not produce data-bearing subsources");
2587 }
2588 vec![]
2589 }
2590 };
2591 Ok(statements)
2592}
2593
2594async fn purify_csr_connection_proto(
2595 catalog: &dyn SessionCatalog,
2596 options: &SourceFormatOptions,
2597 csr_connection: &mut CsrConnectionProtobuf<Aug>,
2598 envelope: &Option<SourceEnvelope>,
2599 storage_configuration: &StorageConfiguration,
2600) -> Result<(), PlanError> {
2601 let SourceFormatOptions::Kafka { topic } = options else {
2602 sql_bail!("Confluent Schema Registry is only supported with Kafka sources")
2603 };
2604
2605 let CsrConnectionProtobuf {
2606 seed,
2607 connection: CsrConnection {
2608 connection,
2609 options: _,
2610 },
2611 } = csr_connection;
2612 match seed {
2613 None => {
2614 let scx = StatementContext::new(None, &*catalog);
2615
2616 let ccsr_connection = match scx.get_item_by_resolved_name(connection)?.connection()? {
2617 Connection::Csr(connection) => connection.clone().into_inline_connection(catalog),
2618 _ => sql_bail!("{} is not a schema registry connection", connection),
2619 };
2620
2621 let ccsr_client = ccsr_connection
2622 .connect(storage_configuration, InTask::No)
2623 .await
2624 .map_err(|e| CsrPurificationError::ClientError(Arc::new(e)))?;
2625
2626 let value = compile_proto(&format!("{}-value", topic), &ccsr_client).await?;
2627 let key = compile_proto(&format!("{}-key", topic), &ccsr_client)
2628 .await
2629 .ok();
2630
2631 if matches!(envelope, Some(SourceEnvelope::Debezium)) && key.is_none() {
2632 sql_bail!("Key schema is required for ENVELOPE DEBEZIUM");
2633 }
2634
2635 *seed = Some(CsrSeedProtobuf { value, key });
2636 }
2637 Some(_) => (),
2638 }
2639
2640 Ok(())
2641}
2642
2643async fn purify_csr_connection_avro(
2644 catalog: &dyn SessionCatalog,
2645 options: &SourceFormatOptions,
2646 csr_connection: &mut CsrConnectionAvro<Aug>,
2647 envelope: &Option<SourceEnvelope>,
2648 storage_configuration: &StorageConfiguration,
2649) -> Result<(), PlanError> {
2650 let SourceFormatOptions::Kafka { topic } = options else {
2651 sql_bail!("Confluent Schema Registry is only supported with Kafka sources")
2652 };
2653
2654 let CsrConnectionAvro {
2655 connection: CsrConnection { connection, .. },
2656 seed,
2657 key_strategy,
2658 value_strategy,
2659 } = csr_connection;
2660 if seed.is_none() {
2661 let scx = StatementContext::new(None, &*catalog);
2662 let csr_connection = match scx.get_item_by_resolved_name(connection)?.connection()? {
2663 Connection::Csr(connection) => connection.clone().into_inline_connection(catalog),
2664 _ => sql_bail!("{} is not a schema registry connection", connection),
2665 };
2666 let ccsr_client = csr_connection
2667 .connect(storage_configuration, InTask::No)
2668 .await
2669 .map_err(|e| CsrPurificationError::ClientError(Arc::new(e)))?;
2670
2671 let Schema {
2672 key_schema,
2673 value_schema,
2674 key_reference_schemas,
2675 value_reference_schemas,
2676 } = get_remote_csr_schema(
2677 &ccsr_client,
2678 key_strategy.clone().unwrap_or_default(),
2679 value_strategy.clone().unwrap_or_default(),
2680 topic,
2681 )
2682 .await?;
2683 if matches!(envelope, Some(SourceEnvelope::Debezium)) && key_schema.is_none() {
2684 sql_bail!("Key schema is required for ENVELOPE DEBEZIUM");
2685 }
2686
2687 *seed = Some(CsrSeedAvro {
2688 key_schema,
2689 value_schema,
2690 key_reference_schemas,
2691 value_reference_schemas,
2692 })
2693 }
2694
2695 Ok(())
2696}
2697
2698async fn purify_glue_connection_avro(
2699 catalog: &dyn SessionCatalog,
2700 options: &SourceFormatOptions,
2701 connection: &ResolvedItemName,
2702 with_options: &[GlueAvroOption<Aug>],
2703 seed: &mut Option<GlueAvroSeed>,
2704 storage_configuration: &StorageConfiguration,
2705) -> Result<(), PlanError> {
2706 use crate::pure::error::GluePurificationError;
2707 let SourceFormatOptions::Kafka { .. } = options else {
2708 sql_bail!("AWS Glue Schema Registry is only supported with Kafka sources")
2709 };
2710
2711 let scx = StatementContext::new(None, &*catalog);
2712 let item = scx.get_item_by_resolved_name(connection)?;
2713 let full_name = scx.catalog.resolve_full_name(item.name());
2714 let Connection::GlueSchemaRegistry(gsr_connection) = item.connection()? else {
2721 return Err(GluePurificationError::NotGlueConnection(full_name).into());
2722 };
2723
2724 let crate::plan::statement::ddl::GlueAvroOptionExtracted {
2728 schema_name,
2729 key_schema_name,
2730 value_schema_name,
2731 key_compatibility_level,
2732 value_compatibility_level,
2733 seen: _,
2734 } = with_options.to_vec().try_into()?;
2735 if key_schema_name.is_some()
2738 || value_schema_name.is_some()
2739 || key_compatibility_level.is_some()
2740 || value_compatibility_level.is_some()
2741 {
2742 sql_bail!(
2743 "KEY SCHEMA NAME, VALUE SCHEMA NAME, KEY COMPATIBILITY LEVEL, and VALUE \
2744 COMPATIBILITY LEVEL are not supported for AWS Glue Schema Registry sources, \
2745 use SCHEMA NAME instead"
2746 );
2747 }
2748 let schema_name = schema_name.ok_or(GluePurificationError::MissingSchemaName)?;
2749
2750 if seed.is_some() {
2751 return Ok(());
2756 }
2757 let gsr_connection = gsr_connection.into_inline_connection(catalog);
2758
2759 let enforce_external_addresses = mz_storage_types::dyncfgs::ENFORCE_EXTERNAL_ADDRESSES
2762 .get(storage_configuration.config_set());
2763 let sdk_config = gsr_connection
2764 .aws_connection
2765 .connection
2766 .load_sdk_config(
2767 &storage_configuration.connection_context,
2768 gsr_connection.aws_connection.connection_id,
2769 InTask::No,
2771 enforce_external_addresses,
2772 )
2773 .await
2774 .map_err(|e| GluePurificationError::LoadSdkConfigError(Arc::new(e)))?;
2775 let glue_client = mz_aws_glue_schema_registry::ClientConfig::new(sdk_config).build();
2776
2777 let version = glue_client
2778 .get_schema_version_latest_by_name(&gsr_connection.registry_name, &schema_name)
2779 .await
2780 .map_err(|e| GluePurificationError::SchemaLookupError {
2781 registry: gsr_connection.registry_name.clone(),
2782 schema: schema_name.clone(),
2783 cause: Arc::new(e),
2784 })?;
2785 match &version.data_format {
2789 Some(mz_aws_glue_schema_registry::DataFormat::Avro) => {}
2790 other => {
2791 return Err(GluePurificationError::UnsupportedDataFormat {
2792 registry: gsr_connection.registry_name.clone(),
2793 schema: schema_name.clone(),
2794 format: other
2795 .as_ref()
2796 .map(|f| f.as_str().to_string())
2797 .unwrap_or_else(|| "<unspecified>".to_string()),
2798 }
2799 .into());
2800 }
2801 }
2802 let value_schema =
2803 version
2804 .definition
2805 .ok_or_else(|| GluePurificationError::EmptyDefinition {
2806 registry: gsr_connection.registry_name.clone(),
2807 schema: schema_name.clone(),
2808 })?;
2809
2810 *seed = Some(GlueAvroSeed { value_schema });
2811 Ok(())
2812}
2813
2814#[derive(Debug)]
2815pub struct Schema {
2816 pub key_schema: Option<String>,
2817 pub value_schema: String,
2818 pub key_reference_schemas: Vec<String>,
2820 pub value_reference_schemas: Vec<String>,
2822}
2823
2824struct SchemaWithReferences {
2826 schema: String,
2828 references: Vec<String>,
2830}
2831
2832async fn get_schema_with_strategy(
2833 client: &Client,
2834 strategy: ReaderSchemaSelectionStrategy,
2835 subject: &str,
2836) -> Result<Option<SchemaWithReferences>, PlanError> {
2837 match strategy {
2838 ReaderSchemaSelectionStrategy::Latest => {
2839 match client.get_subject_and_references(subject).await {
2841 Ok((primary, dependencies)) => Ok(Some(SchemaWithReferences {
2842 schema: primary.schema.raw,
2843 references: dependencies.into_iter().map(|s| s.schema.raw).collect(),
2844 })),
2845 Err(GetBySubjectError::SubjectNotFound)
2846 | Err(GetBySubjectError::VersionNotFound(_)) => Ok(None),
2847 Err(e) => Err(PlanError::FetchingCsrSchemaFailed {
2848 schema_lookup: format!("subject {}", subject.quoted()),
2849 cause: Arc::new(e),
2850 }),
2851 }
2852 }
2853 ReaderSchemaSelectionStrategy::Inline(raw) => Ok(Some(SchemaWithReferences {
2857 schema: raw,
2858 references: vec![],
2859 })),
2860 ReaderSchemaSelectionStrategy::ById(id) => {
2861 match client.get_subject_and_references_by_id(id).await {
2862 Ok((primary, dependencies)) => Ok(Some(SchemaWithReferences {
2863 schema: primary.schema.raw,
2864 references: dependencies.into_iter().map(|s| s.schema.raw).collect(),
2865 })),
2866 Err(GetBySubjectError::SubjectNotFound)
2867 | Err(GetBySubjectError::VersionNotFound(_)) => Ok(None),
2868 Err(e) => Err(PlanError::FetchingCsrSchemaFailed {
2869 schema_lookup: format!("subject {}", subject.quoted()),
2870 cause: Arc::new(e),
2871 }),
2872 }
2873 }
2874 }
2875}
2876
2877async fn get_remote_csr_schema(
2878 ccsr_client: &mz_ccsr::Client,
2879 key_strategy: ReaderSchemaSelectionStrategy,
2880 value_strategy: ReaderSchemaSelectionStrategy,
2881 topic: &str,
2882) -> Result<Schema, PlanError> {
2883 let value_schema_name = format!("{}-value", topic);
2884 let value_result =
2885 get_schema_with_strategy(ccsr_client, value_strategy, &value_schema_name).await?;
2886 let value_result = value_result.ok_or_else(|| anyhow!("No value schema found"))?;
2887
2888 let key_subject = format!("{}-key", topic);
2889 let key_result = get_schema_with_strategy(ccsr_client, key_strategy, &key_subject).await?;
2890 Ok(Schema {
2891 key_schema: key_result.as_ref().map(|r| r.schema.clone()),
2892 value_schema: value_result.schema,
2893 key_reference_schemas: key_result.map(|r| r.references).unwrap_or_default(),
2894 value_reference_schemas: value_result.references,
2895 })
2896}
2897
2898async fn compile_proto(
2900 subject_name: &String,
2901 ccsr_client: &Client,
2902) -> Result<CsrSeedProtobufSchema, PlanError> {
2903 let (primary_subject, dependency_subjects) = ccsr_client
2904 .get_subject_and_references(subject_name)
2905 .await
2906 .map_err(|e| PlanError::FetchingCsrSchemaFailed {
2907 schema_lookup: format!("subject {}", subject_name.quoted()),
2908 cause: Arc::new(e),
2909 })?;
2910
2911 let mut source_tree = VirtualSourceTree::new();
2913
2914 source_tree.as_mut().map_well_known_types();
2918
2919 for subject in iter::once(&primary_subject).chain(dependency_subjects.iter()) {
2920 source_tree.as_mut().add_file(
2921 Path::new(&subject.name),
2922 subject.schema.raw.as_bytes().to_vec(),
2923 );
2924 }
2925 let mut db = SourceTreeDescriptorDatabase::new(source_tree.as_mut());
2926 let fds = db
2927 .as_mut()
2928 .build_file_descriptor_set(&[Path::new(&primary_subject.name)])
2929 .map_err(|cause| PlanError::InvalidProtobufSchema { cause })?;
2930
2931 let primary_fd = fds.file(0);
2933 let message_name = match primary_fd.message_type_size() {
2934 1 => String::from_utf8_lossy(primary_fd.message_type(0).name()).into_owned(),
2935 0 => bail_unsupported!(29603, "Protobuf schemas with no messages"),
2936 _ => bail_unsupported!(29603, "Protobuf schemas with multiple messages"),
2937 };
2938
2939 let bytes = &fds
2941 .serialize()
2942 .map_err(|cause| PlanError::InvalidProtobufSchema { cause })?;
2943 let mut schema = String::new();
2944 strconv::format_bytes(&mut schema, bytes);
2945
2946 Ok(CsrSeedProtobufSchema {
2947 schema,
2948 message_name,
2949 })
2950}
2951
2952const MZ_NOW_NAME: &str = "mz_now";
2953const MZ_NOW_SCHEMA: &str = "mz_catalog";
2954
2955pub fn purify_create_materialized_view_options(
2961 catalog: impl SessionCatalog,
2962 mz_now: Option<Timestamp>,
2963 cmvs: &mut CreateMaterializedViewStatement<Aug>,
2964 resolved_ids: &mut ResolvedIds,
2965) {
2966 let (mz_now_id, mz_now_expr) = {
2969 let item = catalog
2970 .resolve_function(&PartialItemName {
2971 database: None,
2972 schema: Some(MZ_NOW_SCHEMA.to_string()),
2973 item: MZ_NOW_NAME.to_string(),
2974 })
2975 .expect("we should be able to resolve mz_now");
2976 (
2977 item.id(),
2978 Expr::Function(Function {
2979 name: ResolvedItemName::Item {
2980 id: item.id(),
2981 qualifiers: item.name().qualifiers.clone(),
2982 full_name: catalog.resolve_full_name(item.name()),
2983 print_id: false,
2984 version: RelationVersionSelector::Latest,
2985 },
2986 args: FunctionArgs::Args {
2987 args: Vec::new(),
2988 order_by: Vec::new(),
2989 },
2990 filter: None,
2991 over: None,
2992 distinct: false,
2993 }),
2994 )
2995 };
2996 let (mz_timestamp_id, mz_timestamp_type) = {
2998 let item = catalog.get_system_type("mz_timestamp");
2999 let full_name = catalog.resolve_full_name(item.name());
3000 (
3001 item.id(),
3002 ResolvedDataType::Named {
3003 id: item.id(),
3004 qualifiers: item.name().qualifiers.clone(),
3005 full_name,
3006 modifiers: vec![],
3007 print_id: true,
3008 },
3009 )
3010 };
3011
3012 let mut introduced_mz_timestamp = false;
3013
3014 for option in cmvs.with_options.iter_mut() {
3015 if matches!(
3017 option.value,
3018 Some(WithOptionValue::Refresh(RefreshOptionValue::AtCreation))
3019 ) {
3020 option.value = Some(WithOptionValue::Refresh(RefreshOptionValue::At(
3021 RefreshAtOptionValue {
3022 time: mz_now_expr.clone(),
3023 },
3024 )));
3025 }
3026
3027 if let Some(WithOptionValue::Refresh(RefreshOptionValue::Every(
3029 RefreshEveryOptionValue { aligned_to, .. },
3030 ))) = &mut option.value
3031 {
3032 if aligned_to.is_none() {
3033 *aligned_to = Some(mz_now_expr.clone());
3034 }
3035 }
3036
3037 match &mut option.value {
3040 Some(WithOptionValue::Refresh(RefreshOptionValue::At(RefreshAtOptionValue {
3041 time,
3042 }))) => {
3043 let mut visitor = MzNowPurifierVisitor::new(mz_now, mz_timestamp_type.clone());
3044 visitor.visit_expr_mut(time);
3045 introduced_mz_timestamp |= visitor.introduced_mz_timestamp;
3046 }
3047 Some(WithOptionValue::Refresh(RefreshOptionValue::Every(
3048 RefreshEveryOptionValue {
3049 interval: _,
3050 aligned_to: Some(aligned_to),
3051 },
3052 ))) => {
3053 let mut visitor = MzNowPurifierVisitor::new(mz_now, mz_timestamp_type.clone());
3054 visitor.visit_expr_mut(aligned_to);
3055 introduced_mz_timestamp |= visitor.introduced_mz_timestamp;
3056 }
3057 _ => {}
3058 }
3059 }
3060
3061 if !cmvs.with_options.iter().any(|o| {
3063 matches!(
3064 o,
3065 MaterializedViewOption {
3066 value: Some(WithOptionValue::Refresh(..)),
3067 ..
3068 }
3069 )
3070 }) {
3071 cmvs.with_options.push(MaterializedViewOption {
3072 name: MaterializedViewOptionName::Refresh,
3073 value: Some(WithOptionValue::Refresh(RefreshOptionValue::OnCommit)),
3074 })
3075 }
3076
3077 if introduced_mz_timestamp {
3081 resolved_ids.add_item(mz_timestamp_id);
3082 }
3083 let mut visitor = ExprContainsTemporalVisitor::new();
3087 visitor.visit_create_materialized_view_statement(cmvs);
3088 if !visitor.contains_temporal {
3089 resolved_ids.remove_item(&mz_now_id);
3090 }
3091}
3092
3093pub fn materialized_view_option_contains_temporal(mvo: &MaterializedViewOption<Aug>) -> bool {
3096 match &mvo.value {
3097 Some(WithOptionValue::Refresh(RefreshOptionValue::At(RefreshAtOptionValue { time }))) => {
3098 let mut visitor = ExprContainsTemporalVisitor::new();
3099 visitor.visit_expr(time);
3100 visitor.contains_temporal
3101 }
3102 Some(WithOptionValue::Refresh(RefreshOptionValue::Every(RefreshEveryOptionValue {
3103 interval: _,
3104 aligned_to: Some(aligned_to),
3105 }))) => {
3106 let mut visitor = ExprContainsTemporalVisitor::new();
3107 visitor.visit_expr(aligned_to);
3108 visitor.contains_temporal
3109 }
3110 Some(WithOptionValue::Refresh(RefreshOptionValue::Every(RefreshEveryOptionValue {
3111 interval: _,
3112 aligned_to: None,
3113 }))) => {
3114 true
3117 }
3118 Some(WithOptionValue::Refresh(RefreshOptionValue::AtCreation)) => {
3119 true
3121 }
3122 _ => false,
3123 }
3124}
3125
3126struct ExprContainsTemporalVisitor {
3128 pub contains_temporal: bool,
3129}
3130
3131impl ExprContainsTemporalVisitor {
3132 pub fn new() -> ExprContainsTemporalVisitor {
3133 ExprContainsTemporalVisitor {
3134 contains_temporal: false,
3135 }
3136 }
3137}
3138
3139impl Visit<'_, Aug> for ExprContainsTemporalVisitor {
3140 fn visit_function(&mut self, func: &Function<Aug>) {
3141 self.contains_temporal |= func.name.full_item_name().item == MZ_NOW_NAME;
3142 visit_function(self, func);
3143 }
3144}
3145
3146struct MzNowPurifierVisitor {
3147 pub mz_now: Option<Timestamp>,
3148 pub mz_timestamp_type: ResolvedDataType,
3149 pub introduced_mz_timestamp: bool,
3150}
3151
3152impl MzNowPurifierVisitor {
3153 pub fn new(
3154 mz_now: Option<Timestamp>,
3155 mz_timestamp_type: ResolvedDataType,
3156 ) -> MzNowPurifierVisitor {
3157 MzNowPurifierVisitor {
3158 mz_now,
3159 mz_timestamp_type,
3160 introduced_mz_timestamp: false,
3161 }
3162 }
3163}
3164
3165impl VisitMut<'_, Aug> for MzNowPurifierVisitor {
3166 fn visit_expr_mut(&mut self, expr: &'_ mut Expr<Aug>) {
3167 match expr {
3168 Expr::Function(Function {
3169 name:
3170 ResolvedItemName::Item {
3171 full_name: FullItemName { item, .. },
3172 ..
3173 },
3174 ..
3175 }) if item == &MZ_NOW_NAME.to_string() => {
3176 let mz_now = self.mz_now.expect(
3177 "we should have chosen a timestamp if the expression contains mz_now()",
3178 );
3179 *expr = Expr::Cast {
3182 expr: Box::new(Expr::Value(Value::Number(mz_now.to_string()))),
3183 data_type: self.mz_timestamp_type.clone(),
3184 failure_mode: CastFailureMode::Error,
3185 };
3186 self.introduced_mz_timestamp = true;
3187 }
3188 _ => visit_expr_mut(self, expr),
3189 }
3190 }
3191}