1use std::collections::{BTreeMap, BTreeSet};
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33use fail::fail_point;
34use itertools::Itertools;
35use mz_adapter_types::compaction::CompactionWindow;
36use mz_catalog::memory::objects::{
37 CatalogItem, Cluster, ClusterReplica, Connection, DataSourceDesc, Index, MaterializedView,
38 MetricSink, Secret, Sink, Source, StateDiff, Table, TableDataSource, View,
39};
40use mz_cloud_resources::VpcEndpointConfig;
41use mz_compute_client::logging::LogVariant;
42use mz_compute_client::protocol::response::{PeekError, PeekResponse};
43use mz_controller::clusters::{ClusterRole, ReplicaConfig};
44use mz_controller_types::{ClusterId, ReplicaId};
45use mz_ore::collections::CollectionExt;
46use mz_ore::error::ErrorExt;
47use mz_ore::future::InTask;
48use mz_ore::instrument;
49use mz_ore::retry::Retry;
50use mz_ore::task;
51use mz_repr::{CatalogItemId, GlobalId, RelationVersion, RelationVersionSelector};
52use mz_sql::plan::ConnectionDetails;
53use mz_storage_client::controller::{CollectionDescription, DataSource};
54use mz_storage_types::connections::PostgresConnection;
55use mz_storage_types::connections::inline::{InlinedConnection, IntoInlineConnection};
56use mz_storage_types::sinks::StorageSinkConnection;
57use mz_storage_types::sources::{
58 GenericSourceConnection, SourceDesc, SourceExport, SourceExportDataConfig,
59};
60use tracing::{Instrument, info_span, warn};
61
62use crate::active_compute_sink::ActiveComputeSinkRetireReason;
63use crate::coord::Coordinator;
64use crate::coord::catalog_implications::parsed_state_updates::{
65 ParsedStateUpdate, ParsedStateUpdateKind,
66};
67use crate::coord::peek::DroppedDependency;
68use crate::coord::timeline::TimelineState;
69use crate::statement_logging::{StatementEndedExecutionReason, StatementLoggingId};
70use crate::{AdapterError, CollectionIdBundle, ExecuteContext, ResultExt};
71
72pub mod parsed_state_updates;
73
74impl Coordinator {
75 #[instrument(level = "debug")]
84 pub async fn apply_catalog_implications(
85 &mut self,
86 ctx: Option<&mut ExecuteContext>,
87 catalog_updates: Vec<ParsedStateUpdate>,
88 ) -> Result<(), AdapterError> {
89 let start = Instant::now();
90
91 for id in webhook_sources_to_restart(&catalog_updates) {
92 self.active_webhooks.remove(&id);
93 }
94
95 let mut catalog_implications: BTreeMap<CatalogItemId, CatalogImplication> = BTreeMap::new();
96 let mut cluster_commands: BTreeMap<ClusterId, CatalogImplication> = BTreeMap::new();
97 let mut cluster_replica_commands: BTreeMap<(ClusterId, ReplicaId), CatalogImplication> =
98 BTreeMap::new();
99 let mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>> =
104 BTreeMap::new();
105 let mut replica_scoped_config_changed = false;
109 let mut system_config_changed = false;
113
114 let should_reconcile_now = Self::should_reconcile_now(&catalog_updates);
117
118 for update in catalog_updates {
119 tracing::trace!(?update, "got parsed state update");
120 match &update.kind {
121 ParsedStateUpdateKind::Item {
122 durable_item,
123 parsed_item: _,
124 connection: _,
125 parsed_full_name: _,
126 } => {
127 let entry = catalog_implications
128 .entry(durable_item.id.clone())
129 .or_insert_with(|| CatalogImplication::None);
130 entry.absorb(update);
131 }
132 ParsedStateUpdateKind::Cluster {
133 durable_cluster,
134 parsed_cluster: _,
135 } => {
136 let entry = cluster_commands
137 .entry(durable_cluster.id)
138 .or_insert_with(|| CatalogImplication::None);
139 entry.absorb(update.clone());
140 }
141 ParsedStateUpdateKind::ClusterReplica {
142 durable_cluster_replica,
143 parsed_cluster_replica: _,
144 } => {
145 let entry = cluster_replica_commands
146 .entry((
147 durable_cluster_replica.cluster_id,
148 durable_cluster_replica.replica_id,
149 ))
150 .or_insert_with(|| CatalogImplication::None);
151 entry.absorb(update.clone());
152 }
153 ParsedStateUpdateKind::IntrospectionSourceIndex {
154 cluster_id,
155 log,
156 index_id,
157 } => {
158 if update.diff == StateDiff::Addition {
159 introspection_source_indexes
160 .entry(*cluster_id)
161 .or_default()
162 .insert(log.clone(), *index_id);
163 }
164 }
167 ParsedStateUpdateKind::ReplicaSystemConfiguration { durable: _ } => {
168 replica_scoped_config_changed = true;
172 }
173 ParsedStateUpdateKind::SystemConfiguration { durable: _ } => {
174 system_config_changed = true;
178 }
179 }
180 }
181
182 self.apply_catalog_implications_inner(
183 ctx,
184 catalog_implications.into_iter().collect_vec(),
185 cluster_commands.into_iter().collect_vec(),
186 cluster_replica_commands.into_iter().collect_vec(),
187 introspection_source_indexes,
188 replica_scoped_config_changed,
189 system_config_changed,
190 )
191 .await?;
192
193 if should_reconcile_now {
194 self.reconcile_now.notify_one();
199 }
200
201 self.metrics
202 .apply_catalog_implications_seconds
203 .observe(start.elapsed().as_secs_f64());
204
205 Ok(())
206 }
207
208 fn should_reconcile_now(updates: &[ParsedStateUpdate]) -> bool {
221 updates.iter().any(|update| {
222 matches!(
223 update.kind,
224 ParsedStateUpdateKind::Cluster { .. }
225 | ParsedStateUpdateKind::ClusterReplica { .. }
226 )
227 })
228 }
229
230 #[instrument(level = "debug")]
231 async fn apply_catalog_implications_inner(
232 &mut self,
233 ctx: Option<&mut ExecuteContext>,
234 implications: Vec<(CatalogItemId, CatalogImplication)>,
235 cluster_commands: Vec<(ClusterId, CatalogImplication)>,
236 cluster_replica_commands: Vec<((ClusterId, ReplicaId), CatalogImplication)>,
237 mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>>,
238 replica_scoped_config_changed: bool,
239 system_config_changed: bool,
240 ) -> Result<(), AdapterError> {
241 if system_config_changed {
247 self.catalog().system_config().notify_all_callbacks();
248 }
249
250 let mut tables_to_drop = BTreeSet::new();
251 let mut sources_to_drop = vec![];
252 let mut replication_slots_to_drop: Vec<(PostgresConnection, String)> = vec![];
253 let mut storage_sink_gids_to_drop = vec![];
254 let mut indexes_to_drop = vec![];
255 let mut compute_sinks_to_drop = vec![];
256 let mut view_gids_to_drop = vec![];
257 let mut secrets_to_drop = vec![];
258 let mut vpc_endpoints_to_drop = vec![];
259 let mut clusters_to_drop = vec![];
260 let mut cluster_replicas_to_drop = vec![];
261 let mut cluster_replicas_to_create = vec![];
262 let mut active_compute_sinks_to_drop = BTreeMap::new();
263 let mut peeks_to_drop = vec![];
264 let mut copies_to_drop = vec![];
265
266 let mut dropped_item_names: BTreeMap<GlobalId, String> = BTreeMap::new();
268 let mut dropped_cluster_names: BTreeMap<ClusterId, String> = BTreeMap::new();
269
270 let mut table_collections_to_create = BTreeMap::new();
273 let mut source_collections_to_create = BTreeMap::new();
274 let mut storage_policies_to_initialize = BTreeMap::new();
275 let mut execution_timestamps_to_set = BTreeSet::new();
276 let mut vpc_endpoints_to_create: Vec<(CatalogItemId, VpcEndpointConfig)> = vec![];
277
278 let mut source_gids_to_keep = BTreeSet::new();
281
282 let mut source_connections_to_alter: BTreeMap<
284 GlobalId,
285 GenericSourceConnection<InlinedConnection>,
286 > = BTreeMap::new();
287 let mut sink_connections_to_alter: BTreeMap<GlobalId, StorageSinkConnection> =
288 BTreeMap::new();
289 let mut source_export_data_configs_to_alter: BTreeMap<GlobalId, SourceExportDataConfig> =
290 BTreeMap::new();
291 let mut source_descs_to_alter: BTreeMap<GlobalId, SourceDesc> = BTreeMap::new();
292
293 for (catalog_id, implication) in implications {
300 tracing::trace!(?implication, "have to apply catalog implication");
301
302 match implication {
303 CatalogImplication::Table(CatalogImplicationKind::Added(table)) => {
304 self.handle_create_table(
305 &ctx,
306 &mut table_collections_to_create,
307 &mut storage_policies_to_initialize,
308 &mut execution_timestamps_to_set,
309 catalog_id,
310 table.clone(),
311 )
312 .await?
313 }
314 CatalogImplication::Table(CatalogImplicationKind::Altered {
315 prev: prev_table,
316 new: new_table,
317 }) => {
318 self.handle_alter_table(catalog_id, prev_table, new_table)
319 .await?
320 }
321
322 CatalogImplication::Table(CatalogImplicationKind::Dropped(table, full_name)) => {
323 let global_ids = table.global_ids();
324 for global_id in global_ids {
325 tables_to_drop.insert((catalog_id, global_id));
326 dropped_item_names.insert(global_id, full_name.clone());
327 }
328 }
329 CatalogImplication::Source(CatalogImplicationKind::Added((
330 source,
331 _connection,
332 ))) => {
333 let compaction_windows = self
338 .catalog()
339 .state()
340 .source_compaction_windows(vec![catalog_id]);
341
342 self.handle_create_source(
343 &mut source_collections_to_create,
344 &mut storage_policies_to_initialize,
345 catalog_id,
346 source,
347 compaction_windows,
348 )
349 .await?
350 }
351 CatalogImplication::Source(CatalogImplicationKind::Altered {
352 prev: (prev_source, _prev_connection),
353 new: (new_source, new_connection),
354 }) => {
355 if prev_source.custom_logical_compaction_window
356 != new_source.custom_logical_compaction_window
357 {
358 let new_window = new_source
359 .custom_logical_compaction_window
360 .unwrap_or(CompactionWindow::Default);
361 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
362 }
363 match (&prev_source.data_source, &new_source.data_source) {
364 (
365 DataSourceDesc::Ingestion {
366 desc: prev_desc, ..
367 }
368 | DataSourceDesc::OldSyntaxIngestion {
369 desc: prev_desc, ..
370 },
371 DataSourceDesc::Ingestion { desc: new_desc, .. }
372 | DataSourceDesc::OldSyntaxIngestion { desc: new_desc, .. },
373 ) => {
374 if prev_desc != new_desc {
375 let inlined_connection = new_connection
376 .expect("ingestion source should have inlined connection");
377 let inlined_desc = SourceDesc {
378 connection: inlined_connection,
379 timestamp_interval: new_desc.timestamp_interval,
380 };
381 source_descs_to_alter.insert(new_source.global_id, inlined_desc);
382 }
383 }
384 _ => {}
385 }
386 }
387 CatalogImplication::Source(CatalogImplicationKind::Dropped(
388 (source, connection),
389 full_name,
390 )) => {
391 let global_id = source.global_id();
392 sources_to_drop.push((catalog_id, global_id));
393 dropped_item_names.insert(global_id, full_name);
394
395 if let DataSourceDesc::Ingestion { desc, .. }
396 | DataSourceDesc::OldSyntaxIngestion { desc, .. } = &source.data_source
397 {
398 match &desc.connection {
399 GenericSourceConnection::Postgres(_referenced_conn) => {
400 let inline_conn = connection.expect("missing inlined connection");
401
402 let pg_conn = match inline_conn {
403 GenericSourceConnection::Postgres(pg_conn) => pg_conn,
404 other => {
405 panic!("expected postgres connection, got: {:?}", other)
406 }
407 };
408 let pending_drop = (
409 pg_conn.connection.clone(),
410 pg_conn.publication_details.slot.clone(),
411 );
412 replication_slots_to_drop.push(pending_drop);
413 }
414 _ => {}
415 }
416 }
417 }
418 CatalogImplication::Sink(CatalogImplicationKind::Added(sink)) => {
419 tracing::debug!(?sink, "not handling AddSink in here yet");
420 }
421 CatalogImplication::Sink(CatalogImplicationKind::Altered {
422 prev: prev_sink,
423 new: new_sink,
424 }) => {
425 tracing::debug!(?prev_sink, ?new_sink, "not handling AlterSink in here yet");
426 }
427 CatalogImplication::Sink(CatalogImplicationKind::Dropped(sink, full_name)) => {
428 storage_sink_gids_to_drop.push(sink.global_id());
429 dropped_item_names.insert(sink.global_id(), full_name);
430 }
431 CatalogImplication::Index(CatalogImplicationKind::Added(index)) => {
432 tracing::debug!(?index, "not handling AddIndex in here yet");
433 }
434 CatalogImplication::Index(CatalogImplicationKind::Altered {
435 prev: prev_index,
436 new: new_index,
437 }) => {
438 if prev_index.custom_logical_compaction_window
439 != new_index.custom_logical_compaction_window
440 {
441 let new_window = new_index
442 .custom_logical_compaction_window
443 .unwrap_or(CompactionWindow::Default);
444 self.update_compute_read_policy(
445 new_index.cluster_id,
446 catalog_id,
447 new_window.into(),
448 );
449 }
450 }
451 CatalogImplication::Index(CatalogImplicationKind::Dropped(index, full_name)) => {
452 indexes_to_drop.push((index.cluster_id, index.global_id()));
453 dropped_item_names.insert(index.global_id(), full_name);
454 }
455 CatalogImplication::MetricSink(CatalogImplicationKind::Added(_metric_sink)) => {
456 }
460 CatalogImplication::MetricSink(CatalogImplicationKind::Altered { .. }) => {
461 }
463 CatalogImplication::MetricSink(CatalogImplicationKind::Dropped(
464 metric_sink,
465 full_name,
466 )) => {
467 compute_sinks_to_drop.push((metric_sink.cluster_id, metric_sink.global_id));
470 dropped_item_names.insert(metric_sink.global_id, full_name);
471 }
472 CatalogImplication::MaterializedView(CatalogImplicationKind::Added(mv)) => {
473 tracing::debug!(?mv, "not handling AddMaterializedView in here yet");
474 }
475 CatalogImplication::MaterializedView(CatalogImplicationKind::Altered {
476 prev: prev_mv,
477 new: new_mv,
478 }) => {
479 if prev_mv.collections != new_mv.collections {
498 assert_eq!(
501 prev_mv.global_id_writes(),
502 new_mv.global_id_writes(),
503 "unexpected MV Altered implication: prev={prev_mv:?}, new={new_mv:?}",
504 );
505
506 let gid = new_mv.global_id_writes();
507 self.allow_writes(new_mv.cluster_id, gid);
508
509 source_gids_to_keep.extend(new_mv.global_ids());
514 } else if prev_mv.custom_logical_compaction_window
515 != new_mv.custom_logical_compaction_window
516 {
517 let new_window = new_mv
518 .custom_logical_compaction_window
519 .unwrap_or(CompactionWindow::Default);
520 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
521 }
522 }
523 CatalogImplication::MaterializedView(CatalogImplicationKind::Dropped(
524 mv,
525 full_name,
526 )) => {
527 compute_sinks_to_drop.push((mv.cluster_id, mv.global_id_writes()));
528 for gid in mv.global_ids() {
529 sources_to_drop.push((catalog_id, gid));
530 dropped_item_names.insert(gid, full_name.clone());
531 }
532 }
533 CatalogImplication::View(CatalogImplicationKind::Added(_view)) => {
534 }
537 CatalogImplication::View(CatalogImplicationKind::Altered {
538 prev: _prev_view,
539 new: _new_view,
540 }) => {
541 }
544 CatalogImplication::View(CatalogImplicationKind::Dropped(view, full_name)) => {
545 view_gids_to_drop.push(view.global_id());
546 dropped_item_names.insert(view.global_id(), full_name);
547 }
548 CatalogImplication::Secret(CatalogImplicationKind::Added(_secret)) => {
549 }
553 CatalogImplication::Secret(CatalogImplicationKind::Altered {
554 prev: _prev_secret,
555 new: _new_secret,
556 }) => {
557 }
560 CatalogImplication::Secret(CatalogImplicationKind::Dropped(
561 _secret,
562 _full_name,
563 )) => {
564 secrets_to_drop.push(catalog_id);
565 }
566 CatalogImplication::Connection(CatalogImplicationKind::Added(connection)) => {
567 match &connection.details {
568 ConnectionDetails::Ssh { .. } => {}
571 ConnectionDetails::AwsPrivatelink(privatelink) => {
573 let spec = VpcEndpointConfig {
574 aws_service_name: privatelink.service_name.to_owned(),
575 availability_zone_ids: privatelink.availability_zones.to_owned(),
576 };
577 vpc_endpoints_to_create.push((catalog_id, spec));
578 }
579 _ => {}
581 }
582 }
583 CatalogImplication::Connection(CatalogImplicationKind::Altered {
584 prev: _prev_connection,
585 new: new_connection,
586 }) => {
587 self.handle_alter_connection(
588 catalog_id,
589 new_connection,
590 &mut vpc_endpoints_to_create,
591 &mut source_connections_to_alter,
592 &mut sink_connections_to_alter,
593 &mut source_export_data_configs_to_alter,
594 );
595 }
596 CatalogImplication::Connection(CatalogImplicationKind::Dropped(
597 connection,
598 _full_name,
599 )) => {
600 match &connection.details {
601 ConnectionDetails::Ssh { .. } => {
603 secrets_to_drop.push(catalog_id);
604 }
605 ConnectionDetails::AwsPrivatelink(_) => {
608 vpc_endpoints_to_drop.push(catalog_id);
609 }
610 _ => (),
611 }
612 }
613 CatalogImplication::None => {
614 }
616 CatalogImplication::Cluster(_) | CatalogImplication::ClusterReplica(_) => {
617 unreachable!("clusters and cluster replicas are handled below")
618 }
619 CatalogImplication::Table(CatalogImplicationKind::None)
620 | CatalogImplication::Source(CatalogImplicationKind::None)
621 | CatalogImplication::Sink(CatalogImplicationKind::None)
622 | CatalogImplication::Index(CatalogImplicationKind::None)
623 | CatalogImplication::MetricSink(CatalogImplicationKind::None)
624 | CatalogImplication::MaterializedView(CatalogImplicationKind::None)
625 | CatalogImplication::View(CatalogImplicationKind::None)
626 | CatalogImplication::Secret(CatalogImplicationKind::None)
627 | CatalogImplication::Connection(CatalogImplicationKind::None) => {
628 unreachable!("will never leave None in place");
629 }
630 }
631 }
632
633 for (cluster_id, command) in cluster_commands {
634 tracing::trace!(?command, "have cluster command to apply!");
635
636 match command {
637 CatalogImplication::Cluster(CatalogImplicationKind::Added(cluster)) => {
638 let arranged_logs = introspection_source_indexes
643 .remove(&cluster_id)
644 .unwrap_or_default();
645 let introspection_source_ids: Vec<_> =
646 arranged_logs.values().copied().collect();
647
648 self.controller
649 .create_cluster(
650 cluster_id,
651 mz_controller::clusters::ClusterConfig {
652 arranged_logs,
653 workload_class: cluster.config.workload_class.clone(),
654 },
655 )
656 .expect("creating cluster must not fail");
657
658 if !introspection_source_ids.is_empty() {
659 self.initialize_compute_read_policies(
660 introspection_source_ids,
661 cluster_id,
662 CompactionWindow::Default,
663 )
664 .await;
665 }
666 }
667 CatalogImplication::Cluster(CatalogImplicationKind::Altered {
668 prev: prev_cluster,
669 new: new_cluster,
670 }) => {
671 if prev_cluster.config.workload_class != new_cluster.config.workload_class {
677 self.controller.update_cluster_workload_class(
678 cluster_id,
679 new_cluster.config.workload_class.clone(),
680 );
681 }
682 }
683 CatalogImplication::Cluster(CatalogImplicationKind::Dropped(
684 cluster,
685 _full_name,
686 )) => {
687 clusters_to_drop.push(cluster_id);
688 dropped_cluster_names.insert(cluster_id, cluster.name);
689 }
690 CatalogImplication::Cluster(CatalogImplicationKind::None) => {
691 unreachable!("will never leave None in place");
692 }
693 command => {
694 unreachable!(
695 "we only handle cluster commands in this map, got: {:?}",
696 command
697 );
698 }
699 }
700 }
701
702 if replica_scoped_config_changed {
711 self.push_replica_dyncfg_overrides();
712 }
713
714 for ((cluster_id, replica_id), command) in cluster_replica_commands {
715 tracing::trace!(?command, "have cluster replica command to apply!");
716
717 match command {
718 CatalogImplication::ClusterReplica(CatalogImplicationKind::Added(replica)) => {
719 let cluster = self.catalog().get_cluster(cluster_id);
727 let cluster_name = cluster.name.clone();
728 let cluster_role = cluster.role();
729 cluster_replicas_to_create.push((
730 cluster_id,
731 replica_id,
732 cluster_role,
733 cluster_name,
734 replica.name.clone(),
735 replica.config.clone(),
736 ));
737 }
738 CatalogImplication::ClusterReplica(CatalogImplicationKind::Altered {
739 prev: _prev_replica,
740 new: _new_replica,
741 }) => {
742 }
746 CatalogImplication::ClusterReplica(CatalogImplicationKind::Dropped(
747 _replica,
748 _full_name,
749 )) => {
750 cluster_replicas_to_drop.push((cluster_id, replica_id));
751 }
752 CatalogImplication::ClusterReplica(CatalogImplicationKind::None) => {
753 unreachable!("will never leave None in place");
754 }
755 command => {
756 unreachable!(
757 "we only handle cluster replica commands in this map, got: {:?}",
758 command
759 );
760 }
761 }
762 }
763
764 let clusters_with_replica_creates = cluster_replicas_to_create
765 .iter()
766 .map(|(cluster_id, ..)| *cluster_id)
767 .collect();
768 let (replacement_drops, deferred_drops) = partition_cluster_replica_drops(
769 &clusters_with_replica_creates,
770 cluster_replicas_to_drop,
771 );
772 cluster_replicas_to_drop = deferred_drops;
773
774 if !replacement_drops.is_empty() {
781 fail::fail_point!("after_catalog_drop_replica");
782 for (cluster_id, replica_id) in replacement_drops {
783 self.drop_replica(cluster_id, replica_id);
784 }
785 }
786 for (cluster_id, replica_id, role, cluster_name, replica_name, config) in
787 cluster_replicas_to_create
788 {
789 self.handle_create_cluster_replica(
790 cluster_id,
791 replica_id,
792 role,
793 cluster_name,
794 replica_name,
795 config,
796 )
797 .await;
798 }
799
800 if !source_collections_to_create.is_empty() {
801 self.create_source_collections(source_collections_to_create)
802 .await?;
803 }
804
805 if !table_collections_to_create.is_empty() {
808 self.create_table_collections(table_collections_to_create, execution_timestamps_to_set)
809 .await?;
810 }
811 self.initialize_storage_collections(storage_policies_to_initialize)
817 .await?;
818
819 if !vpc_endpoints_to_create.is_empty() {
821 if let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() {
822 for (connection_id, spec) in vpc_endpoints_to_create {
823 if let Err(err) = cloud_resource_controller
824 .ensure_vpc_endpoint(connection_id, spec)
825 .await
826 {
827 tracing::error!(?err, "failed to ensure vpc endpoint!");
828 }
829 }
830 } else {
831 tracing::error!(
832 "AWS PrivateLink connections unsupported without cloud_resource_controller"
833 );
834 }
835 }
836
837 if !source_connections_to_alter.is_empty() {
839 self.controller
840 .storage
841 .alter_ingestion_connections(source_connections_to_alter)
842 .await
843 .unwrap_or_terminate("cannot fail to alter ingestion connections");
844 }
845
846 if !sink_connections_to_alter.is_empty() {
847 self.controller
848 .storage
849 .alter_export_connections(sink_connections_to_alter)
850 .await
851 .unwrap_or_terminate("altering export connections after txn must succeed");
852 }
853
854 if !source_export_data_configs_to_alter.is_empty() {
855 self.controller
856 .storage
857 .alter_ingestion_export_data_configs(source_export_data_configs_to_alter)
858 .await
859 .unwrap_or_terminate("altering source export data configs after txn must succeed");
860 }
861
862 if !source_descs_to_alter.is_empty() {
863 self.controller
864 .storage
865 .alter_ingestion_source_desc(source_descs_to_alter)
866 .await
867 .unwrap_or_terminate("cannot fail to alter ingestion source desc");
868 }
869
870 sources_to_drop.retain(|(_, gid)| !source_gids_to_keep.contains(gid));
872
873 let readable_collections_to_drop: BTreeSet<_> = sources_to_drop
874 .iter()
875 .map(|(_, gid)| *gid)
876 .chain(tables_to_drop.iter().map(|(_, gid)| *gid))
877 .chain(indexes_to_drop.iter().map(|(_, gid)| *gid))
878 .chain(view_gids_to_drop.iter().copied())
879 .collect();
880
881 for (sink_id, sink) in &self.active_compute_sinks {
884 let cluster_id = sink.cluster_id();
885 if let Some(id) = sink
886 .depends_on()
887 .iter()
888 .find(|id| readable_collections_to_drop.contains(id))
889 {
890 let name = dropped_item_names
891 .get(id)
892 .cloned()
893 .expect("missing relation name");
894 active_compute_sinks_to_drop.insert(
895 *sink_id,
896 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Relation {
897 name,
898 }),
899 );
900 } else if clusters_to_drop.contains(&cluster_id) {
901 let name = dropped_cluster_names
902 .get(&cluster_id)
903 .cloned()
904 .expect("missing cluster name");
905 active_compute_sinks_to_drop.insert(
906 *sink_id,
907 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Cluster {
908 name,
909 }),
910 );
911 }
912 }
913
914 for (uuid, pending_peek) in &self.pending_peeks {
916 if let Some(id) = pending_peek
917 .depends_on
918 .iter()
919 .find(|id| readable_collections_to_drop.contains(id))
920 {
921 let name = dropped_item_names
922 .get(id)
923 .cloned()
924 .expect("missing relation name");
925 peeks_to_drop.push((DroppedDependency::Relation { name }, uuid.clone()));
926 } else if clusters_to_drop.contains(&pending_peek.cluster_id) {
927 let name = dropped_cluster_names
928 .get(&pending_peek.cluster_id)
929 .cloned()
930 .expect("missing cluster name");
931 peeks_to_drop.push((DroppedDependency::Cluster { name }, uuid.clone()));
932 }
933 }
934
935 for (conn_id, pending_copy) in &self.active_copies {
937 let dropping_table = tables_to_drop
938 .iter()
939 .any(|(item_id, _gid)| pending_copy.table_id == *item_id);
940 let dropping_cluster = clusters_to_drop.contains(&pending_copy.cluster_id);
941
942 if dropping_table || dropping_cluster {
943 copies_to_drop.push(conn_id.clone());
944 }
945 }
946
947 let storage_gids_to_drop: BTreeSet<_> = sources_to_drop
948 .iter()
949 .map(|(_id, gid)| gid)
950 .chain(storage_sink_gids_to_drop.iter())
951 .chain(tables_to_drop.iter().map(|(_id, gid)| gid))
952 .copied()
953 .collect();
954 let compute_gids_to_drop: Vec<_> = indexes_to_drop
955 .iter()
956 .chain(compute_sinks_to_drop.iter())
957 .copied()
958 .collect();
959
960 let mut timeline_id_bundles = BTreeMap::new();
966
967 for (timeline, TimelineState { read_holds, .. }) in &self.global_timelines {
968 let mut id_bundle = CollectionIdBundle::default();
969
970 for storage_id in read_holds.storage_ids() {
971 if storage_gids_to_drop.contains(&storage_id) {
972 id_bundle.storage_ids.insert(storage_id);
973 }
974 }
975
976 for (instance_id, id) in read_holds.compute_ids() {
977 if compute_gids_to_drop.contains(&(instance_id, id))
978 || clusters_to_drop.contains(&instance_id)
979 {
980 id_bundle
981 .compute_ids
982 .entry(instance_id)
983 .or_default()
984 .insert(id);
985 }
986 }
987
988 timeline_id_bundles.insert(timeline.clone(), id_bundle);
989 }
990
991 let mut timeline_associations = BTreeMap::new();
992 for (timeline, id_bundle) in timeline_id_bundles.into_iter() {
993 let TimelineState { read_holds, .. } = self
994 .global_timelines
995 .get(&timeline)
996 .expect("all timelines have a timestamp oracle");
997
998 let empty = read_holds.id_bundle().difference(&id_bundle).is_empty();
999 timeline_associations.insert(timeline, (empty, id_bundle));
1000 }
1001
1002 let _: () = async {
1005 if !timeline_associations.is_empty() {
1006 for (timeline, (should_be_empty, id_bundle)) in timeline_associations {
1007 let became_empty =
1008 self.remove_resources_associated_with_timeline(timeline, id_bundle);
1009 assert_eq!(should_be_empty, became_empty, "emptiness did not match!");
1010 }
1011 }
1012
1013 if !tables_to_drop.is_empty() {
1020 self.drop_tables(tables_to_drop.into_iter().collect_vec())
1021 .await;
1022 }
1023
1024 if !sources_to_drop.is_empty() {
1025 self.drop_sources(sources_to_drop);
1026 }
1027
1028 if !storage_sink_gids_to_drop.is_empty() {
1029 self.drop_storage_sinks(storage_sink_gids_to_drop);
1030 }
1031
1032 if !active_compute_sinks_to_drop.is_empty() {
1033 let retire_notify = self
1034 .retire_compute_sinks(active_compute_sinks_to_drop)
1035 .await;
1036 if let Some(ctx) = ctx {
1037 ctx.delay_response_until(retire_notify);
1038 }
1039 }
1040
1041 if !peeks_to_drop.is_empty() {
1042 for (dep, uuid) in peeks_to_drop {
1043 if let Some(pending_peek) = self.remove_pending_peek(&uuid) {
1044 let cancel_reason = PeekResponse::Error(PeekError::unstructured(
1045 dep.query_terminated_error(),
1046 ));
1047 self.controller
1048 .compute
1049 .cancel_peek(pending_peek.cluster_id, uuid, cancel_reason)
1050 .unwrap_or_terminate("unable to cancel peek");
1051 self.retire_execution(
1052 StatementEndedExecutionReason::Canceled,
1053 pending_peek.ctx_extra.defuse(),
1054 );
1055 }
1056 }
1057 }
1058
1059 if !copies_to_drop.is_empty() {
1060 for conn_id in copies_to_drop {
1061 self.cancel_pending_copy(&conn_id);
1062 }
1063 }
1064
1065 if !compute_gids_to_drop.is_empty() {
1066 self.drop_compute_collections(compute_gids_to_drop);
1067 }
1068
1069 if !vpc_endpoints_to_drop.is_empty() {
1070 self.drop_vpc_endpoints_in_background(vpc_endpoints_to_drop)
1071 }
1072
1073 let clusters_losing_replicas: BTreeSet<_> = cluster_replicas_to_drop
1074 .iter()
1075 .map(|(cluster_id, _)| *cluster_id)
1076 .collect();
1077 if !cluster_replicas_to_drop.is_empty() {
1078 fail::fail_point!("after_catalog_drop_replica");
1079
1080 for (cluster_id, replica_id) in cluster_replicas_to_drop {
1081 self.drop_replica(cluster_id, replica_id);
1082 }
1083 }
1084 if !clusters_to_drop.is_empty() {
1085 for cluster_id in &clusters_to_drop {
1086 self.controller.drop_cluster(*cluster_id);
1087 }
1088 }
1089 for cluster_id in clusters_losing_replicas.into_iter().chain(clusters_to_drop) {
1093 let has_replicas = self
1094 .catalog()
1095 .try_get_cluster(cluster_id)
1096 .is_some_and(|cluster| cluster.replicas().next().is_some());
1097 if !has_replicas {
1098 self.metrics.by_cluster.remove_cluster(cluster_id);
1099 }
1100 }
1101
1102 task::spawn(|| "drop_replication_slots_and_secrets", {
1110 let ssh_tunnel_manager = self.connection_context().ssh_tunnel_manager.clone();
1111 let caching_secrets_reader = self.caching_secrets_reader.clone();
1112 let secrets_controller = Arc::clone(&self.secrets_controller);
1113 let secrets_reader = Arc::clone(self.secrets_reader());
1114 let storage_config = self.controller.storage.config().clone();
1115
1116 async move {
1117 for (connection, replication_slot_name) in replication_slots_to_drop {
1118 tracing::info!(?replication_slot_name, "dropping replication slot");
1119
1120 let result: Result<(), anyhow::Error> = Retry::default()
1126 .max_duration(Duration::from_secs(60))
1127 .retry_async(|_state| async {
1128 let config = connection
1129 .config(&secrets_reader, &storage_config, InTask::No)
1130 .await
1131 .map_err(|e| {
1132 anyhow::anyhow!(
1133 "error creating Postgres client for \
1134 dropping acquired slots: {}",
1135 e.display_with_causes()
1136 )
1137 })?;
1138
1139 mz_postgres_util::drop_replication_slots(
1140 &ssh_tunnel_manager,
1141 config.clone(),
1142 &[(&replication_slot_name, true)],
1143 )
1144 .await?;
1145
1146 Ok(())
1147 })
1148 .await;
1149
1150 if let Err(err) = result {
1151 tracing::warn!(
1152 ?replication_slot_name,
1153 ?err,
1154 "failed to drop replication slot"
1155 );
1156 }
1157 }
1158
1159 fail_point!("drop_secrets");
1167 for secret in secrets_to_drop {
1168 if let Err(e) = secrets_controller.delete(secret).await {
1169 warn!("Dropping secrets has encountered an error: {}", e);
1170 } else {
1171 caching_secrets_reader.invalidate(secret);
1172 }
1173 }
1174 }
1175 });
1176 }
1177 .instrument(info_span!(
1178 "coord::apply_catalog_implications_inner::finalize"
1179 ))
1180 .await;
1181
1182 Ok(())
1183 }
1184
1185 #[instrument(level = "debug")]
1186 async fn create_table_collections(
1187 &mut self,
1188 table_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1189 execution_timestamps_to_set: BTreeSet<StatementLoggingId>,
1190 ) -> Result<(), AdapterError> {
1191 let table_ids: Vec<GlobalId> = table_collections_to_create.keys().copied().collect();
1193 let collections = table_collections_to_create.into_iter().collect_vec();
1194
1195 let write_ts = self.get_local_write_ts().await;
1197 let register_ts = write_ts.timestamp;
1198 self.catalog
1199 .advance_upper(write_ts.advance_to)
1200 .await
1201 .unwrap_or_terminate("unable to advance catalog upper");
1202
1203 {
1204 let storage_metadata = self.catalog.state().storage_metadata();
1205 self.controller
1206 .storage
1207 .create_collections(storage_metadata, Some(register_ts), collections)
1208 .await
1209 .unwrap_or_terminate("cannot fail to create collections");
1210 }
1211
1212 let registrations = self
1215 .controller
1216 .storage
1217 .table_registrations(table_ids)
1218 .unwrap_or_terminate("cannot fail to look up table registrations");
1219 let table_ts = if registrations.is_empty() {
1220 self.apply_local_write(register_ts).await;
1222 register_ts
1223 } else {
1224 self.register_tables_via_committer(registrations).await
1225 };
1226
1227 for id in execution_timestamps_to_set {
1228 self.set_statement_execution_timestamp(id, table_ts);
1229 }
1230
1231 Ok(())
1232 }
1233
1234 #[instrument(level = "debug")]
1235 async fn create_source_collections(
1236 &mut self,
1237 source_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1238 ) -> Result<(), AdapterError> {
1239 let storage_metadata = self.catalog.state().storage_metadata();
1240
1241 self.controller
1242 .storage
1243 .create_collections(
1244 storage_metadata,
1245 None, source_collections_to_create.into_iter().collect_vec(),
1247 )
1248 .await
1249 .unwrap_or_terminate("cannot fail to create collections");
1250
1251 Ok(())
1252 }
1253
1254 #[instrument(level = "debug")]
1255 async fn initialize_storage_collections(
1256 &mut self,
1257 storage_policies_to_initialize: BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1258 ) -> Result<(), AdapterError> {
1259 for (compaction_window, global_ids) in storage_policies_to_initialize {
1260 self.initialize_read_policies(
1261 &CollectionIdBundle {
1262 storage_ids: global_ids,
1263 compute_ids: BTreeMap::new(),
1264 },
1265 compaction_window,
1266 )
1267 .await;
1268 }
1269
1270 Ok(())
1271 }
1272
1273 #[instrument(level = "debug")]
1274 async fn handle_create_table(
1275 &self,
1276 ctx: &Option<&mut ExecuteContext>,
1277 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1278 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1279 execution_timestamps_to_set: &mut BTreeSet<StatementLoggingId>,
1280 table_id: CatalogItemId,
1281 table: Table,
1282 ) -> Result<(), AdapterError> {
1283 match &table.data_source {
1287 TableDataSource::TableWrites { defaults: _ } => {
1288 let versions: BTreeMap<_, _> = table
1289 .collection_descs()
1290 .map(|(gid, version, desc)| (version, (gid, desc)))
1291 .collect();
1292 let collection_descs = versions.iter().map(|(_version, (gid, desc))| {
1293 let collection_desc = CollectionDescription::for_table(desc.clone());
1294
1295 (*gid, collection_desc)
1296 });
1297
1298 let compaction_window = table
1299 .custom_logical_compaction_window
1300 .unwrap_or(CompactionWindow::Default);
1301 let ids_to_initialize = storage_policies_to_initialize
1302 .entry(compaction_window)
1303 .or_default();
1304
1305 for (gid, collection_desc) in collection_descs {
1306 storage_collections_to_create.insert(gid, collection_desc);
1307 ids_to_initialize.insert(gid);
1308 }
1309
1310 if let Some(id) = ctx.as_ref().and_then(|ctx| ctx.extra().contents()) {
1311 execution_timestamps_to_set.insert(id);
1312 }
1313 }
1314 TableDataSource::DataSource {
1315 desc: data_source_desc,
1316 timeline,
1317 } => {
1318 match data_source_desc {
1319 DataSourceDesc::IngestionExport {
1320 ingestion_id,
1321 external_reference: _,
1322 details,
1323 data_config,
1324 } => {
1325 let global_ingestion_id =
1326 self.catalog().get_entry(ingestion_id).latest_global_id();
1327
1328 let collection_desc = CollectionDescription {
1329 desc: table.desc.latest(),
1330 data_source: DataSource::IngestionExport {
1331 ingestion_id: global_ingestion_id,
1332 details: details.clone(),
1333 data_config: data_config
1334 .clone()
1335 .into_inline_connection(self.catalog.state()),
1336 },
1337 since: None,
1338 timeline: Some(timeline.clone()),
1339 primary: None,
1340 };
1341
1342 let global_id = table
1343 .global_ids()
1344 .expect_element(|| "subsources cannot have multiple versions");
1345
1346 storage_collections_to_create.insert(global_id, collection_desc);
1347
1348 let read_policies = self
1349 .catalog()
1350 .state()
1351 .source_compaction_windows(vec![table_id]);
1352 for (compaction_window, catalog_ids) in read_policies {
1353 let compaction_ids = storage_policies_to_initialize
1354 .entry(compaction_window)
1355 .or_default();
1356
1357 let gids = catalog_ids
1358 .into_iter()
1359 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1360 .flatten();
1361 compaction_ids.extend(gids);
1362 }
1363 }
1364 DataSourceDesc::Webhook {
1365 validate_using: _,
1366 body_format: _,
1367 headers: _,
1368 cluster_id: _,
1369 } => {
1370 assert_eq!(
1372 table.desc.latest_version(),
1373 RelationVersion::root(),
1374 "found webhook with more than 1 relation version, {:?}",
1375 table.desc
1376 );
1377 let desc = table.desc.latest();
1378
1379 let collection_desc = CollectionDescription {
1380 desc,
1381 data_source: DataSource::Webhook,
1382 since: None,
1383 timeline: Some(timeline.clone()),
1384 primary: None,
1385 };
1386
1387 let global_id = table
1388 .global_ids()
1389 .expect_element(|| "webhooks cannot have multiple versions");
1390
1391 storage_collections_to_create.insert(global_id, collection_desc);
1392
1393 let read_policies = self
1394 .catalog()
1395 .state()
1396 .source_compaction_windows(vec![table_id]);
1397
1398 for (compaction_window, catalog_ids) in read_policies {
1399 let compaction_ids = storage_policies_to_initialize
1400 .entry(compaction_window)
1401 .or_default();
1402
1403 let gids = catalog_ids
1404 .into_iter()
1405 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1406 .flatten();
1407 compaction_ids.extend(gids);
1408 }
1409 }
1410 _ => unreachable!("CREATE TABLE data source got {:?}", data_source_desc),
1411 }
1412 }
1413 }
1414
1415 Ok(())
1416 }
1417
1418 #[instrument(level = "debug")]
1419 async fn handle_alter_table(
1420 &mut self,
1421 catalog_id: CatalogItemId,
1422 prev_table: Table,
1423 new_table: Table,
1424 ) -> Result<(), AdapterError> {
1425 let existing_gid = prev_table.global_id_writes();
1426 let new_gid = new_table.global_id_writes();
1427
1428 if existing_gid == new_gid {
1429 if prev_table.custom_logical_compaction_window
1432 != new_table.custom_logical_compaction_window
1433 {
1434 let new_window = new_table
1435 .custom_logical_compaction_window
1436 .unwrap_or(CompactionWindow::Default);
1437 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
1438 }
1439 return Ok(());
1440 }
1441
1442 let existing_table = crate::CollectionIdBundle {
1446 storage_ids: BTreeSet::from([existing_gid]),
1447 compute_ids: BTreeMap::new(),
1448 };
1449 let existing_table_read_hold = self.acquire_read_holds(&existing_table);
1450
1451 let expected_version = prev_table.desc.latest_version();
1452 let new_version = new_table.desc.latest_version();
1453 let new_desc = new_table
1454 .desc
1455 .at_version(RelationVersionSelector::Specific(new_version));
1456
1457 let write_ts = self.get_local_write_ts().await;
1459 self.catalog
1460 .advance_upper(write_ts.advance_to)
1461 .await
1462 .unwrap_or_terminate("unable to advance catalog upper");
1463
1464 self.controller
1465 .storage
1466 .alter_table_desc(existing_gid, new_gid, new_desc, expected_version)
1467 .await
1468 .unwrap_or_terminate("failed to alter desc of table");
1469
1470 let registrations = self
1472 .controller
1473 .storage
1474 .table_registrations(vec![new_gid])
1475 .unwrap_or_terminate("cannot fail to look up table registrations");
1476 self.register_tables_via_committer(registrations).await;
1477
1478 let compaction_window = new_table
1480 .custom_logical_compaction_window
1481 .unwrap_or(CompactionWindow::Default);
1482 self.initialize_read_policies(
1483 &crate::CollectionIdBundle {
1484 storage_ids: BTreeSet::from([new_gid]),
1485 compute_ids: BTreeMap::new(),
1486 },
1487 compaction_window,
1488 )
1489 .await;
1490
1491 drop(existing_table_read_hold);
1493
1494 Ok(())
1495 }
1496
1497 #[instrument(level = "debug")]
1498 async fn handle_create_source(
1499 &self,
1500 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1501 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1502 item_id: CatalogItemId,
1503 source: Source,
1504 compaction_windows: BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>>,
1505 ) -> Result<(), AdapterError> {
1506 let data_source = match source.data_source {
1507 DataSourceDesc::Ingestion { desc, cluster_id } => {
1508 let desc = desc.into_inline_connection(self.catalog().state());
1509 let item_global_id = self.catalog().get_entry(&item_id).latest_global_id();
1510
1511 let ingestion = mz_storage_types::sources::IngestionDescription::new(
1512 desc,
1513 cluster_id,
1514 item_global_id,
1515 );
1516
1517 DataSource::Ingestion(ingestion)
1518 }
1519 DataSourceDesc::OldSyntaxIngestion {
1520 desc,
1521 progress_subsource,
1522 data_config,
1523 details,
1524 cluster_id,
1525 } => {
1526 let desc = desc.into_inline_connection(self.catalog().state());
1527 let data_config = data_config.into_inline_connection(self.catalog().state());
1528
1529 let progress_subsource = self
1532 .catalog()
1533 .get_entry(&progress_subsource)
1534 .latest_global_id();
1535
1536 let mut ingestion = mz_storage_types::sources::IngestionDescription::new(
1537 desc,
1538 cluster_id,
1539 progress_subsource,
1540 );
1541
1542 let legacy_export = SourceExport {
1543 storage_metadata: (),
1544 data_config,
1545 details,
1546 };
1547
1548 ingestion
1549 .source_exports
1550 .insert(source.global_id, legacy_export);
1551
1552 DataSource::Ingestion(ingestion)
1553 }
1554 DataSourceDesc::IngestionExport {
1555 ingestion_id,
1556 external_reference: _,
1557 details,
1558 data_config,
1559 } => {
1560 let ingestion_id = self.catalog().get_entry(&ingestion_id).latest_global_id();
1563
1564 DataSource::IngestionExport {
1565 ingestion_id,
1566 details,
1567 data_config: data_config.into_inline_connection(self.catalog().state()),
1568 }
1569 }
1570 DataSourceDesc::Progress => DataSource::Progress,
1571 DataSourceDesc::Webhook { .. } => DataSource::Webhook,
1572 DataSourceDesc::Introspection(_) | DataSourceDesc::Catalog => {
1573 unreachable!("cannot create sources with internal data sources")
1574 }
1575 };
1576
1577 storage_collections_to_create.insert(
1578 source.global_id,
1579 CollectionDescription {
1580 desc: source.desc.clone(),
1581 data_source,
1582 timeline: Some(source.timeline),
1583 since: None,
1584 primary: None,
1585 },
1586 );
1587
1588 for (compaction_window, catalog_ids) in compaction_windows {
1590 let compaction_ids = storage_policies_to_initialize
1591 .entry(compaction_window)
1592 .or_default();
1593
1594 let gids = catalog_ids
1595 .into_iter()
1596 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1597 .flatten();
1598 compaction_ids.extend(gids);
1599 }
1600
1601 Ok(())
1602 }
1603
1604 #[instrument(level = "debug")]
1611 fn handle_alter_connection(
1612 &self,
1613 connection_id: CatalogItemId,
1614 connection: Connection,
1615 vpc_endpoints_to_create: &mut Vec<(CatalogItemId, VpcEndpointConfig)>,
1616 source_connections_to_alter: &mut BTreeMap<
1617 GlobalId,
1618 GenericSourceConnection<InlinedConnection>,
1619 >,
1620 sink_connections_to_alter: &mut BTreeMap<GlobalId, StorageSinkConnection>,
1621 source_export_data_configs_to_alter: &mut BTreeMap<GlobalId, SourceExportDataConfig>,
1622 ) {
1623 use std::collections::VecDeque;
1624
1625 if let ConnectionDetails::AwsPrivatelink(ref privatelink) = connection.details {
1627 let spec = VpcEndpointConfig {
1628 aws_service_name: privatelink.service_name.to_owned(),
1629 availability_zone_ids: privatelink.availability_zones.to_owned(),
1630 };
1631 vpc_endpoints_to_create.push((connection_id, spec));
1632 }
1633
1634 let mut connections_to_process = VecDeque::new();
1638 connections_to_process.push_front(connection_id.clone());
1639
1640 while let Some(id) = connections_to_process.pop_front() {
1641 for dependent_id in self.catalog().get_entry(&id).used_by() {
1642 let dependent_entry = self.catalog().get_entry(dependent_id);
1643 match dependent_entry.item() {
1644 CatalogItem::Connection(_) => {
1645 connections_to_process.push_back(*dependent_id);
1649 }
1650 CatalogItem::Source(source) => {
1651 let desc = match &dependent_entry
1652 .source()
1653 .expect("known to be source")
1654 .data_source
1655 {
1656 DataSourceDesc::Ingestion { desc, .. }
1657 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1658 desc.clone().into_inline_connection(self.catalog().state())
1659 }
1660 DataSourceDesc::IngestionExport { .. }
1661 | DataSourceDesc::Introspection(_)
1662 | DataSourceDesc::Progress
1663 | DataSourceDesc::Webhook { .. }
1664 | DataSourceDesc::Catalog => {
1665 continue;
1667 }
1668 };
1669
1670 source_connections_to_alter.insert(source.global_id, desc.connection);
1671 }
1672 CatalogItem::Sink(sink) => {
1673 let export = dependent_entry.sink().expect("known to be sink");
1674 sink_connections_to_alter.insert(
1675 sink.global_id,
1676 export
1677 .connection
1678 .clone()
1679 .into_inline_connection(self.catalog().state()),
1680 );
1681 }
1682 CatalogItem::Table(table) => {
1683 if let Some((_, _, _, export_data_config)) =
1687 dependent_entry.source_export_details()
1688 {
1689 let data_config = export_data_config.clone();
1690 source_export_data_configs_to_alter.insert(
1691 table.global_id_writes(),
1692 data_config.into_inline_connection(self.catalog().state()),
1693 );
1694 }
1695 }
1696 CatalogItem::Log(_)
1697 | CatalogItem::View(_)
1698 | CatalogItem::MaterializedView(_)
1699 | CatalogItem::Index(_)
1700 | CatalogItem::Type(_)
1701 | CatalogItem::Func(_)
1702 | CatalogItem::Secret(_)
1703 | CatalogItem::MetricSink(_) => {
1704 }
1707 }
1708 }
1709 }
1710 }
1711
1712 async fn handle_create_cluster_replica(
1713 &mut self,
1714 cluster_id: ClusterId,
1715 replica_id: ReplicaId,
1716 role: ClusterRole,
1717 cluster_name: String,
1718 replica_name: String,
1719 replica_config: ReplicaConfig,
1720 ) {
1721 let enable_worker_core_affinity =
1722 self.catalog().system_config().enable_worker_core_affinity();
1723
1724 self.controller
1731 .create_replica(
1732 cluster_id,
1733 replica_id,
1734 cluster_name,
1735 replica_name,
1736 role,
1737 replica_config,
1738 enable_worker_core_affinity,
1739 )
1740 .expect("creating replicas must not fail");
1741
1742 self.install_introspection_subscribes(cluster_id, replica_id)
1743 .await;
1744 self.install_metric_sinks(cluster_id, replica_id).await;
1745 }
1746}
1747
1748#[derive(Debug, Clone)]
1754enum CatalogImplication {
1755 None,
1756 Table(CatalogImplicationKind<Table>),
1757 Source(CatalogImplicationKind<(Source, Option<GenericSourceConnection>)>),
1758 Sink(CatalogImplicationKind<Sink>),
1759 Index(CatalogImplicationKind<Index>),
1760 MetricSink(CatalogImplicationKind<MetricSink>),
1761 MaterializedView(CatalogImplicationKind<MaterializedView>),
1762 View(CatalogImplicationKind<View>),
1763 Secret(CatalogImplicationKind<Secret>),
1764 Connection(CatalogImplicationKind<Connection>),
1765 Cluster(CatalogImplicationKind<Cluster>),
1766 ClusterReplica(CatalogImplicationKind<ClusterReplica>),
1767}
1768
1769#[derive(Debug, Clone)]
1770enum CatalogImplicationKind<T> {
1771 None,
1773 Added(T),
1775 Dropped(T, String),
1777 Altered { prev: T, new: T },
1779}
1780
1781impl<T: Clone> CatalogImplicationKind<T> {
1782 fn transition(&mut self, item: T, name: Option<String>, diff: StateDiff) -> Result<(), String> {
1785 use CatalogImplicationKind::*;
1786 use StateDiff::*;
1787
1788 let new_state = match (&*self, diff) {
1789 (None, Addition) => Added(item),
1791 (None, Retraction) => Dropped(item, name.unwrap_or_else(|| "<unknown>".to_string())),
1792
1793 (Added(existing), Retraction) => {
1795 Altered {
1797 prev: item,
1798 new: existing.clone(),
1799 }
1800 }
1801 (Added(_), Addition) => {
1802 return Err("Cannot add an already added object".to_string());
1803 }
1804
1805 (Dropped(existing, _), Addition) => {
1807 Altered {
1809 prev: existing.clone(),
1810 new: item,
1811 }
1812 }
1813 (Dropped(_, _), Retraction) => {
1814 return Err("Cannot drop an already dropped object".to_string());
1815 }
1816
1817 (Altered { .. }, _) => {
1819 return Err(format!(
1820 "Cannot apply {:?} to an object in Altered state",
1821 diff
1822 ));
1823 }
1824 };
1825
1826 *self = new_state;
1827 Ok(())
1828 }
1829}
1830
1831macro_rules! impl_absorb_method {
1833 (
1834 $method_name:ident,
1835 $variant:ident,
1836 $item_type:ty
1837 ) => {
1838 fn $method_name(
1839 &mut self,
1840 item: $item_type,
1841 parsed_full_name: Option<String>,
1842 diff: StateDiff,
1843 ) {
1844 let state = match self {
1845 CatalogImplication::$variant(state) => state,
1846 CatalogImplication::None => {
1847 *self = CatalogImplication::$variant(CatalogImplicationKind::None);
1848 match self {
1849 CatalogImplication::$variant(state) => state,
1850 _ => unreachable!(),
1851 }
1852 }
1853 _ => {
1854 panic!(
1855 "Unexpected command type for {:?}: {} {:?}",
1856 self,
1857 stringify!($variant),
1858 diff,
1859 );
1860 }
1861 };
1862
1863 if let Err(e) = state.transition(item, parsed_full_name, diff) {
1864 panic!(
1865 "Invalid state transition for {}: {}",
1866 stringify!($variant),
1867 e
1868 );
1869 }
1870 }
1871 };
1872}
1873
1874impl CatalogImplication {
1875 fn absorb(&mut self, catalog_update: ParsedStateUpdate) {
1878 match catalog_update.kind {
1879 ParsedStateUpdateKind::Item {
1880 durable_item: _,
1881 parsed_item,
1882 connection,
1883 parsed_full_name,
1884 } => match parsed_item {
1885 CatalogItem::Table(table) => self.absorb_table(
1886 table,
1887 Some(parsed_full_name.to_string()),
1888 catalog_update.diff,
1889 ),
1890 CatalogItem::Source(source) => {
1891 self.absorb_source(
1892 (source, connection),
1893 Some(parsed_full_name.to_string()),
1894 catalog_update.diff,
1895 );
1896 }
1897 CatalogItem::Sink(sink) => {
1898 self.absorb_sink(
1899 sink,
1900 Some(parsed_full_name.to_string()),
1901 catalog_update.diff,
1902 );
1903 }
1904 CatalogItem::Index(index) => {
1905 self.absorb_index(
1906 index,
1907 Some(parsed_full_name.to_string()),
1908 catalog_update.diff,
1909 );
1910 }
1911 CatalogItem::MaterializedView(mv) => {
1912 self.absorb_materialized_view(
1913 mv,
1914 Some(parsed_full_name.to_string()),
1915 catalog_update.diff,
1916 );
1917 }
1918 CatalogItem::View(view) => {
1919 self.absorb_view(
1920 view,
1921 Some(parsed_full_name.to_string()),
1922 catalog_update.diff,
1923 );
1924 }
1925
1926 CatalogItem::Secret(secret) => {
1927 self.absorb_secret(secret, None, catalog_update.diff);
1928 }
1929 CatalogItem::Connection(connection) => {
1930 self.absorb_connection(connection, None, catalog_update.diff);
1931 }
1932 CatalogItem::MetricSink(metric_sink) => {
1933 self.absorb_metric_sink(
1934 metric_sink,
1935 Some(parsed_full_name.to_string()),
1936 catalog_update.diff,
1937 );
1938 }
1939 CatalogItem::Log(_) => {}
1940 CatalogItem::Type(_) => {}
1941 CatalogItem::Func(_) => {}
1942 },
1943 ParsedStateUpdateKind::Cluster {
1944 durable_cluster: _,
1945 parsed_cluster,
1946 } => {
1947 let name = parsed_cluster.name.clone();
1948 self.absorb_cluster(parsed_cluster, Some(name), catalog_update.diff);
1949 }
1950 ParsedStateUpdateKind::ClusterReplica {
1951 durable_cluster_replica: _,
1952 parsed_cluster_replica,
1953 } => {
1954 let name = parsed_cluster_replica.name.clone();
1955 self.absorb_cluster_replica(
1956 parsed_cluster_replica,
1957 Some(name),
1958 catalog_update.diff,
1959 );
1960 }
1961 ParsedStateUpdateKind::IntrospectionSourceIndex { .. } => {
1962 unreachable!("IntrospectionSourceIndex should not be passed to absorb");
1966 }
1967 ParsedStateUpdateKind::ReplicaSystemConfiguration { .. } => {
1968 unreachable!("ReplicaSystemConfiguration should not be passed to absorb");
1971 }
1972 ParsedStateUpdateKind::SystemConfiguration { .. } => {
1973 unreachable!("SystemConfiguration should not be passed to absorb");
1976 }
1977 }
1978 }
1979
1980 impl_absorb_method!(absorb_table, Table, Table);
1981 impl_absorb_method!(
1982 absorb_source,
1983 Source,
1984 (Source, Option<GenericSourceConnection>)
1985 );
1986 impl_absorb_method!(absorb_sink, Sink, Sink);
1987 impl_absorb_method!(absorb_index, Index, Index);
1988 impl_absorb_method!(absorb_metric_sink, MetricSink, MetricSink);
1989 impl_absorb_method!(absorb_materialized_view, MaterializedView, MaterializedView);
1990 impl_absorb_method!(absorb_view, View, View);
1991
1992 impl_absorb_method!(absorb_secret, Secret, Secret);
1993 impl_absorb_method!(absorb_connection, Connection, Connection);
1994
1995 impl_absorb_method!(absorb_cluster, Cluster, Cluster);
1996 impl_absorb_method!(absorb_cluster_replica, ClusterReplica, ClusterReplica);
1997}
1998
1999fn webhook_sources_to_restart(updates: &[ParsedStateUpdate]) -> BTreeSet<CatalogItemId> {
2001 let mut names = BTreeMap::new();
2002 for update in updates {
2003 let ParsedStateUpdateKind::Item {
2004 durable_item,
2005 parsed_item,
2006 parsed_full_name,
2007 ..
2008 } = &update.kind
2009 else {
2010 continue;
2011 };
2012 let is_webhook = matches!(
2013 parsed_item,
2014 CatalogItem::Source(Source {
2015 data_source: DataSourceDesc::Webhook { .. },
2016 ..
2017 }) | CatalogItem::Table(Table {
2018 data_source: TableDataSource::DataSource {
2019 desc: DataSourceDesc::Webhook { .. },
2020 ..
2021 },
2022 ..
2023 })
2024 );
2025 if is_webhook {
2026 names
2027 .entry(durable_item.id)
2028 .or_insert(CatalogImplicationKind::None)
2029 .transition(parsed_full_name, None, update.diff)
2030 .expect("webhook updates must be consolidated");
2031 }
2032 }
2033
2034 names
2037 .into_iter()
2038 .filter_map(|(id, change)| match change {
2039 CatalogImplicationKind::Altered { prev, new } if prev != new => Some(id),
2040 _ => None,
2041 })
2042 .collect()
2043}
2044
2045fn partition_cluster_replica_drops(
2046 clusters_with_creates: &BTreeSet<ClusterId>,
2047 drops: Vec<(ClusterId, ReplicaId)>,
2048) -> (Vec<(ClusterId, ReplicaId)>, Vec<(ClusterId, ReplicaId)>) {
2049 drops
2050 .into_iter()
2051 .partition(|(cluster_id, _)| clusters_with_creates.contains(cluster_id))
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056 use super::*;
2057 use mz_repr::{GlobalId, RelationDesc, RelationVersion, VersionedRelationDesc};
2058 use mz_sql::names::{FullItemName, RawDatabaseSpecifier, ResolvedIds};
2059 use std::collections::BTreeMap;
2060
2061 fn create_test_table(name: &str) -> Table {
2062 Table {
2063 desc: VersionedRelationDesc::new(
2064 RelationDesc::builder()
2065 .with_column(name, mz_repr::SqlScalarType::String.nullable(false))
2066 .finish(),
2067 ),
2068 create_sql: None,
2069 collections: BTreeMap::from([(RelationVersion::root(), GlobalId::System(1))]),
2070 conn_id: None,
2071 resolved_ids: ResolvedIds::empty(),
2072 custom_logical_compaction_window: None,
2073 is_retained_metrics_object: false,
2074 data_source: TableDataSource::TableWrites { defaults: vec![] },
2075 }
2076 }
2077
2078 fn webhook_update(
2079 id: u64,
2080 [database, schema, item]: [&str; 3],
2081 diff: StateDiff,
2082 as_table: bool,
2083 ) -> ParsedStateUpdate {
2084 let full_name = FullItemName {
2085 database: RawDatabaseSpecifier::Name(database.to_string()),
2086 schema: schema.to_string(),
2087 item: item.to_string(),
2088 };
2089 let data_source = DataSourceDesc::Webhook {
2090 validate_using: None,
2091 body_format: mz_sql::plan::WebhookBodyFormat::Text,
2092 headers: mz_sql::plan::WebhookHeaders::default(),
2093 cluster_id: ClusterId::user(1).expect("valid id"),
2094 };
2095 let mut table = create_test_table("body");
2096 let parsed_item = if as_table {
2097 table.data_source = TableDataSource::DataSource {
2098 desc: data_source,
2099 timeline: mz_storage_types::sources::Timeline::EpochMilliseconds,
2100 };
2101 CatalogItem::Table(table)
2102 } else {
2103 CatalogItem::Source(Source {
2104 create_sql: None,
2105 global_id: GlobalId::User(id),
2106 data_source,
2107 desc: table.desc.latest(),
2108 timeline: mz_storage_types::sources::Timeline::EpochMilliseconds,
2109 resolved_ids: ResolvedIds::empty(),
2110 custom_logical_compaction_window: None,
2111 is_retained_metrics_object: false,
2112 })
2113 };
2114 ParsedStateUpdate {
2115 kind: ParsedStateUpdateKind::Item {
2116 durable_item: mz_catalog::durable::objects::Item {
2117 id: CatalogItemId::User(id),
2118 oid: 1,
2119 global_id: GlobalId::User(id),
2120 schema_id: mz_sql::names::SchemaId::User(1),
2121 name: full_name.item.clone(),
2122 create_sql: format!("CREATE SOURCE {full_name} FROM WEBHOOK BODY FORMAT TEXT"),
2123 owner_id: mz_repr::role_id::RoleId::User(1),
2124 privileges: vec![],
2125 extra_versions: BTreeMap::new(),
2126 ephemeral_owner_session: None,
2127 },
2128 parsed_item,
2129 connection: None,
2130 parsed_full_name: full_name,
2131 },
2132 ts: mz_repr::Timestamp::MIN,
2133 diff,
2134 }
2135 }
2136
2137 #[mz_ore::test]
2138 fn webhook_item_and_schema_renames_invalidate_both_representations() {
2139 for as_table in [false, true] {
2140 for new_name in [["db", "schema", "renamed"], ["db", "renamed", "hook"]] {
2141 let before =
2142 webhook_update(1, ["db", "schema", "hook"], StateDiff::Retraction, as_table);
2143 let after = webhook_update(1, new_name, StateDiff::Addition, as_table);
2144 for updates in [
2145 vec![before.clone(), after.clone()],
2146 vec![after.clone(), before.clone()],
2147 ] {
2148 assert_eq!(
2149 webhook_sources_to_restart(&updates),
2150 BTreeSet::from([CatalogItemId::User(1)])
2151 );
2152 }
2153 }
2154 }
2155 }
2156
2157 #[mz_ore::test]
2158 fn webhook_invalidation_preserves_dotted_name_components() {
2159 for as_table in [false, true] {
2160 let before = webhook_update(1, ["db", "a", "b.c"], StateDiff::Retraction, as_table);
2161 let after = webhook_update(1, ["db", "a.b", "c"], StateDiff::Addition, as_table);
2162 let full_name = |update: &ParsedStateUpdate| {
2163 let ParsedStateUpdateKind::Item {
2164 parsed_full_name, ..
2165 } = &update.kind
2166 else {
2167 unreachable!();
2168 };
2169 parsed_full_name.to_string()
2170 };
2171 assert_eq!(full_name(&before), full_name(&after));
2172 for updates in [
2173 vec![before.clone(), after.clone()],
2174 vec![after.clone(), before.clone()],
2175 ] {
2176 assert_eq!(
2177 webhook_sources_to_restart(&updates),
2178 BTreeSet::from([CatalogItemId::User(1)])
2179 );
2180 }
2181 let unchanged = webhook_update(1, ["db", "a", "b.c"], StateDiff::Addition, as_table);
2182 assert!(webhook_sources_to_restart(&[before, unchanged]).is_empty());
2183 }
2184 }
2185
2186 #[mz_ore::test]
2187 fn webhook_invalidation_uses_net_names_not_intermediate_operations() {
2188 for as_table in [false, true] {
2189 let before =
2190 webhook_update(1, ["db", "schema", "hook"], StateDiff::Retraction, as_table);
2191 let after = webhook_update(1, ["db", "schema", "hook"], StateDiff::Addition, as_table);
2192 assert!(webhook_sources_to_restart(&[before.clone(), after.clone()]).is_empty());
2194 assert!(webhook_sources_to_restart(&[after]).is_empty());
2196 assert!(webhook_sources_to_restart(&[before]).is_empty());
2197 }
2198
2199 let mut before = webhook_update(1, ["db", "schema", "old"], StateDiff::Retraction, true);
2200 let mut after = webhook_update(1, ["db", "schema", "new"], StateDiff::Addition, true);
2201 for update in [&mut before, &mut after] {
2202 let ParsedStateUpdateKind::Item { parsed_item, .. } = &mut update.kind else {
2203 unreachable!();
2204 };
2205 *parsed_item = CatalogItem::Table(create_test_table("body"));
2206 }
2207 assert!(webhook_sources_to_restart(&[before, after]).is_empty());
2208 }
2209
2210 #[mz_ore::test]
2211 fn webhook_atomic_name_swap_invalidates_each_item() {
2212 let updates = [
2213 webhook_update(1, ["db", "schema", "a"], StateDiff::Retraction, true),
2214 webhook_update(2, ["db", "schema", "b"], StateDiff::Retraction, true),
2215 webhook_update(2, ["db", "schema", "a"], StateDiff::Addition, true),
2216 webhook_update(1, ["db", "schema", "b"], StateDiff::Addition, true),
2217 ];
2218 assert_eq!(
2219 webhook_sources_to_restart(&updates),
2220 BTreeSet::from([CatalogItemId::User(1), CatalogItemId::User(2)])
2221 );
2222 }
2223
2224 #[mz_ore::test]
2225 fn mixed_replica_drops_are_applied_before_creates() {
2226 let c1 = ClusterId::user(1).expect("valid id");
2227 let c2 = ClusterId::user(2).expect("valid id");
2228 let r1 = ReplicaId::User(1);
2229 let r2 = ReplicaId::User(2);
2230 let creates = BTreeSet::from([c1]);
2231
2232 let (before_creates, deferred) =
2233 partition_cluster_replica_drops(&creates, vec![(c1, r1), (c2, r2)]);
2234
2235 assert_eq!(before_creates, vec![(c1, r1)]);
2236 assert_eq!(deferred, vec![(c2, r2)]);
2237 }
2238
2239 #[mz_ore::test]
2240 fn test_item_state_transitions() {
2241 let mut state = CatalogImplicationKind::None;
2243 assert!(
2244 state
2245 .transition("item1".to_string(), None, StateDiff::Addition)
2246 .is_ok()
2247 );
2248 assert!(matches!(state, CatalogImplicationKind::Added(_)));
2249
2250 let mut state = CatalogImplicationKind::Added("new_item".to_string());
2252 assert!(
2253 state
2254 .transition("old_item".to_string(), None, StateDiff::Retraction)
2255 .is_ok()
2256 );
2257 match &state {
2258 CatalogImplicationKind::Altered { prev, new } => {
2259 assert_eq!(prev, "old_item");
2261 assert_eq!(new, "new_item");
2263 }
2264 _ => panic!("Expected Altered state"),
2265 }
2266
2267 let mut state = CatalogImplicationKind::None;
2269 assert!(
2270 state
2271 .transition(
2272 "item1".to_string(),
2273 Some("test_name".to_string()),
2274 StateDiff::Retraction
2275 )
2276 .is_ok()
2277 );
2278 assert!(matches!(state, CatalogImplicationKind::Dropped(_, _)));
2279
2280 let mut state = CatalogImplicationKind::Dropped("old_item".to_string(), "name".to_string());
2282 assert!(
2283 state
2284 .transition("new_item".to_string(), None, StateDiff::Addition)
2285 .is_ok()
2286 );
2287 match &state {
2288 CatalogImplicationKind::Altered { prev, new } => {
2289 assert_eq!(prev, "old_item");
2291 assert_eq!(new, "new_item");
2293 }
2294 _ => panic!("Expected Altered state"),
2295 }
2296
2297 let mut state = CatalogImplicationKind::Added("item".to_string());
2299 assert!(
2300 state
2301 .transition("item2".to_string(), None, StateDiff::Addition)
2302 .is_err()
2303 );
2304
2305 let mut state = CatalogImplicationKind::Dropped("item".to_string(), "name".to_string());
2306 assert!(
2307 state
2308 .transition("item2".to_string(), None, StateDiff::Retraction)
2309 .is_err()
2310 );
2311 }
2312
2313 #[mz_ore::test]
2314 fn test_table_absorb_state_machine() {
2315 let table1 = create_test_table("table1");
2316 let table2 = create_test_table("table2");
2317
2318 let mut cmd = CatalogImplication::None;
2320 cmd.absorb_table(
2321 table1.clone(),
2322 Some("schema.table1".to_string()),
2323 StateDiff::Addition,
2324 );
2325 match &cmd {
2327 CatalogImplication::Table(state) => match state {
2328 CatalogImplicationKind::Added(t) => {
2329 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity())
2330 }
2331 _ => panic!("Expected Added state"),
2332 },
2333 _ => panic!("Expected Table command"),
2334 }
2335
2336 cmd.absorb_table(
2340 table2.clone(),
2341 Some("schema.table2".to_string()),
2342 StateDiff::Retraction,
2343 );
2344 match &cmd {
2345 CatalogImplication::Table(state) => match state {
2346 CatalogImplicationKind::Altered { prev, new } => {
2347 assert_eq!(prev.desc.latest().arity(), table2.desc.latest().arity());
2349 assert_eq!(new.desc.latest().arity(), table1.desc.latest().arity());
2350 }
2351 _ => panic!("Expected Altered state"),
2352 },
2353 _ => panic!("Expected Table command"),
2354 }
2355
2356 let mut cmd = CatalogImplication::None;
2358 cmd.absorb_table(
2359 table1.clone(),
2360 Some("schema.table1".to_string()),
2361 StateDiff::Retraction,
2362 );
2363 match &cmd {
2364 CatalogImplication::Table(state) => match state {
2365 CatalogImplicationKind::Dropped(t, name) => {
2366 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity());
2367 assert_eq!(name, "schema.table1");
2368 }
2369 _ => panic!("Expected Dropped state"),
2370 },
2371 _ => panic!("Expected Table command"),
2372 }
2373
2374 cmd.absorb_table(
2376 table2.clone(),
2377 Some("schema.table2".to_string()),
2378 StateDiff::Addition,
2379 );
2380 match &cmd {
2381 CatalogImplication::Table(state) => match state {
2382 CatalogImplicationKind::Altered { prev, new } => {
2383 assert_eq!(prev.desc.latest().arity(), table1.desc.latest().arity());
2385 assert_eq!(new.desc.latest().arity(), table2.desc.latest().arity());
2386 }
2387 _ => panic!("Expected Altered state"),
2388 },
2389 _ => panic!("Expected Table command"),
2390 }
2391 }
2392
2393 #[mz_ore::test]
2394 #[should_panic(expected = "Cannot add an already added object")]
2395 fn test_invalid_double_add() {
2396 let table = create_test_table("table");
2397 let mut cmd = CatalogImplication::None;
2398
2399 cmd.absorb_table(
2401 table.clone(),
2402 Some("schema.table".to_string()),
2403 StateDiff::Addition,
2404 );
2405
2406 cmd.absorb_table(
2408 table.clone(),
2409 Some("schema.table".to_string()),
2410 StateDiff::Addition,
2411 );
2412 }
2413
2414 #[mz_ore::test]
2415 #[should_panic(expected = "Cannot drop an already dropped object")]
2416 fn test_invalid_double_drop() {
2417 let table = create_test_table("table");
2418 let mut cmd = CatalogImplication::None;
2419
2420 cmd.absorb_table(
2422 table.clone(),
2423 Some("schema.table".to_string()),
2424 StateDiff::Retraction,
2425 );
2426
2427 cmd.absorb_table(
2429 table.clone(),
2430 Some("schema.table".to_string()),
2431 StateDiff::Retraction,
2432 );
2433 }
2434}