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