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 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::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 let mut catalog_implications: BTreeMap<CatalogItemId, CatalogImplication> = BTreeMap::new();
92 let mut cluster_commands: BTreeMap<ClusterId, CatalogImplication> = BTreeMap::new();
93 let mut cluster_replica_commands: BTreeMap<(ClusterId, ReplicaId), CatalogImplication> =
94 BTreeMap::new();
95 let mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>> =
100 BTreeMap::new();
101 let mut replica_scoped_config_changed = false;
105
106 let should_reconcile_now = Self::should_reconcile_now(&catalog_updates);
109
110 for update in catalog_updates {
111 tracing::trace!(?update, "got parsed state update");
112 match &update.kind {
113 ParsedStateUpdateKind::Item {
114 durable_item,
115 parsed_item: _,
116 connection: _,
117 parsed_full_name: _,
118 } => {
119 let entry = catalog_implications
120 .entry(durable_item.id.clone())
121 .or_insert_with(|| CatalogImplication::None);
122 entry.absorb(update);
123 }
124 ParsedStateUpdateKind::TemporaryItem {
125 durable_item,
126 parsed_item: _,
127 connection: _,
128 parsed_full_name: _,
129 } => {
130 let entry = catalog_implications
131 .entry(durable_item.id.clone())
132 .or_insert_with(|| CatalogImplication::None);
133 entry.absorb(update);
134 }
135 ParsedStateUpdateKind::Cluster {
136 durable_cluster,
137 parsed_cluster: _,
138 } => {
139 let entry = cluster_commands
140 .entry(durable_cluster.id)
141 .or_insert_with(|| CatalogImplication::None);
142 entry.absorb(update.clone());
143 }
144 ParsedStateUpdateKind::ClusterReplica {
145 durable_cluster_replica,
146 parsed_cluster_replica: _,
147 } => {
148 let entry = cluster_replica_commands
149 .entry((
150 durable_cluster_replica.cluster_id,
151 durable_cluster_replica.replica_id,
152 ))
153 .or_insert_with(|| CatalogImplication::None);
154 entry.absorb(update.clone());
155 }
156 ParsedStateUpdateKind::IntrospectionSourceIndex {
157 cluster_id,
158 log,
159 index_id,
160 } => {
161 if update.diff == StateDiff::Addition {
162 introspection_source_indexes
163 .entry(*cluster_id)
164 .or_default()
165 .insert(log.clone(), *index_id);
166 }
167 }
170 ParsedStateUpdateKind::ReplicaSystemConfiguration { durable: _ } => {
171 replica_scoped_config_changed = true;
175 }
176 }
177 }
178
179 self.apply_catalog_implications_inner(
180 ctx,
181 catalog_implications.into_iter().collect_vec(),
182 cluster_commands.into_iter().collect_vec(),
183 cluster_replica_commands.into_iter().collect_vec(),
184 introspection_source_indexes,
185 replica_scoped_config_changed,
186 )
187 .await?;
188
189 if should_reconcile_now {
190 self.reconcile_now.notify_one();
195 }
196
197 self.metrics
198 .apply_catalog_implications_seconds
199 .observe(start.elapsed().as_secs_f64());
200
201 Ok(())
202 }
203
204 fn should_reconcile_now(updates: &[ParsedStateUpdate]) -> bool {
216 updates.iter().any(|update| {
217 matches!(
218 update.kind,
219 ParsedStateUpdateKind::Cluster { .. }
220 | ParsedStateUpdateKind::ClusterReplica { .. }
221 )
222 })
223 }
224
225 #[instrument(level = "debug")]
226 async fn apply_catalog_implications_inner(
227 &mut self,
228 ctx: Option<&mut ExecuteContext>,
229 implications: Vec<(CatalogItemId, CatalogImplication)>,
230 cluster_commands: Vec<(ClusterId, CatalogImplication)>,
231 cluster_replica_commands: Vec<((ClusterId, ReplicaId), CatalogImplication)>,
232 mut introspection_source_indexes: BTreeMap<ClusterId, BTreeMap<LogVariant, GlobalId>>,
233 replica_scoped_config_changed: bool,
234 ) -> Result<(), AdapterError> {
235 let mut tables_to_drop = BTreeSet::new();
236 let mut sources_to_drop = vec![];
237 let mut replication_slots_to_drop: Vec<(PostgresConnection, String)> = vec![];
238 let mut storage_sink_gids_to_drop = vec![];
239 let mut indexes_to_drop = vec![];
240 let mut compute_sinks_to_drop = vec![];
241 let mut view_gids_to_drop = vec![];
242 let mut secrets_to_drop = vec![];
243 let mut vpc_endpoints_to_drop = vec![];
244 let mut clusters_to_drop = vec![];
245 let mut cluster_replicas_to_drop = vec![];
246 let mut active_compute_sinks_to_drop = BTreeMap::new();
247 let mut peeks_to_drop = vec![];
248 let mut copies_to_drop = vec![];
249
250 let mut dropped_item_names: BTreeMap<GlobalId, String> = BTreeMap::new();
252 let mut dropped_cluster_names: BTreeMap<ClusterId, String> = BTreeMap::new();
253
254 let mut table_collections_to_create = BTreeMap::new();
257 let mut source_collections_to_create = BTreeMap::new();
258 let mut storage_policies_to_initialize = BTreeMap::new();
259 let mut execution_timestamps_to_set = BTreeSet::new();
260 let mut vpc_endpoints_to_create: Vec<(CatalogItemId, VpcEndpointConfig)> = vec![];
261
262 let mut source_gids_to_keep = BTreeSet::new();
265
266 let mut source_connections_to_alter: BTreeMap<
268 GlobalId,
269 GenericSourceConnection<InlinedConnection>,
270 > = BTreeMap::new();
271 let mut sink_connections_to_alter: BTreeMap<GlobalId, StorageSinkConnection> =
272 BTreeMap::new();
273 let mut source_export_data_configs_to_alter: BTreeMap<GlobalId, SourceExportDataConfig> =
274 BTreeMap::new();
275 let mut source_descs_to_alter: BTreeMap<GlobalId, SourceDesc> = BTreeMap::new();
276
277 for (catalog_id, implication) in implications {
284 tracing::trace!(?implication, "have to apply catalog implication");
285
286 match implication {
287 CatalogImplication::Table(CatalogImplicationKind::Added(table)) => {
288 self.handle_create_table(
289 &ctx,
290 &mut table_collections_to_create,
291 &mut storage_policies_to_initialize,
292 &mut execution_timestamps_to_set,
293 catalog_id,
294 table.clone(),
295 )
296 .await?
297 }
298 CatalogImplication::Table(CatalogImplicationKind::Altered {
299 prev: prev_table,
300 new: new_table,
301 }) => {
302 self.handle_alter_table(catalog_id, prev_table, new_table)
303 .await?
304 }
305
306 CatalogImplication::Table(CatalogImplicationKind::Dropped(table, full_name)) => {
307 let global_ids = table.global_ids();
308 for global_id in global_ids {
309 tables_to_drop.insert((catalog_id, global_id));
310 dropped_item_names.insert(global_id, full_name.clone());
311 }
312 }
313 CatalogImplication::Source(CatalogImplicationKind::Added((
314 source,
315 _connection,
316 ))) => {
317 let compaction_windows = self
322 .catalog()
323 .state()
324 .source_compaction_windows(vec![catalog_id]);
325
326 self.handle_create_source(
327 &mut source_collections_to_create,
328 &mut storage_policies_to_initialize,
329 catalog_id,
330 source,
331 compaction_windows,
332 )
333 .await?
334 }
335 CatalogImplication::Source(CatalogImplicationKind::Altered {
336 prev: (prev_source, _prev_connection),
337 new: (new_source, new_connection),
338 }) => {
339 if prev_source.custom_logical_compaction_window
340 != new_source.custom_logical_compaction_window
341 {
342 let new_window = new_source
343 .custom_logical_compaction_window
344 .unwrap_or(CompactionWindow::Default);
345 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
346 }
347 match (&prev_source.data_source, &new_source.data_source) {
348 (
349 DataSourceDesc::Ingestion {
350 desc: prev_desc, ..
351 }
352 | DataSourceDesc::OldSyntaxIngestion {
353 desc: prev_desc, ..
354 },
355 DataSourceDesc::Ingestion { desc: new_desc, .. }
356 | DataSourceDesc::OldSyntaxIngestion { desc: new_desc, .. },
357 ) => {
358 if prev_desc != new_desc {
359 let inlined_connection = new_connection
360 .expect("ingestion source should have inlined connection");
361 let inlined_desc = SourceDesc {
362 connection: inlined_connection,
363 timestamp_interval: new_desc.timestamp_interval,
364 };
365 source_descs_to_alter.insert(new_source.global_id, inlined_desc);
366 }
367 }
368 _ => {}
369 }
370 }
371 CatalogImplication::Source(CatalogImplicationKind::Dropped(
372 (source, connection),
373 full_name,
374 )) => {
375 let global_id = source.global_id();
376 sources_to_drop.push((catalog_id, global_id));
377 dropped_item_names.insert(global_id, full_name);
378
379 if let DataSourceDesc::Ingestion { desc, .. }
380 | DataSourceDesc::OldSyntaxIngestion { desc, .. } = &source.data_source
381 {
382 match &desc.connection {
383 GenericSourceConnection::Postgres(_referenced_conn) => {
384 let inline_conn = connection.expect("missing inlined connection");
385
386 let pg_conn = match inline_conn {
387 GenericSourceConnection::Postgres(pg_conn) => pg_conn,
388 other => {
389 panic!("expected postgres connection, got: {:?}", other)
390 }
391 };
392 let pending_drop = (
393 pg_conn.connection.clone(),
394 pg_conn.publication_details.slot.clone(),
395 );
396 replication_slots_to_drop.push(pending_drop);
397 }
398 _ => {}
399 }
400 }
401 }
402 CatalogImplication::Sink(CatalogImplicationKind::Added(sink)) => {
403 tracing::debug!(?sink, "not handling AddSink in here yet");
404 }
405 CatalogImplication::Sink(CatalogImplicationKind::Altered {
406 prev: prev_sink,
407 new: new_sink,
408 }) => {
409 tracing::debug!(?prev_sink, ?new_sink, "not handling AlterSink in here yet");
410 }
411 CatalogImplication::Sink(CatalogImplicationKind::Dropped(sink, full_name)) => {
412 storage_sink_gids_to_drop.push(sink.global_id());
413 dropped_item_names.insert(sink.global_id(), full_name);
414 }
415 CatalogImplication::Index(CatalogImplicationKind::Added(index)) => {
416 tracing::debug!(?index, "not handling AddIndex in here yet");
417 }
418 CatalogImplication::Index(CatalogImplicationKind::Altered {
419 prev: prev_index,
420 new: new_index,
421 }) => {
422 if prev_index.custom_logical_compaction_window
423 != new_index.custom_logical_compaction_window
424 {
425 let new_window = new_index
426 .custom_logical_compaction_window
427 .unwrap_or(CompactionWindow::Default);
428 self.update_compute_read_policy(
429 new_index.cluster_id,
430 catalog_id,
431 new_window.into(),
432 );
433 }
434 }
435 CatalogImplication::Index(CatalogImplicationKind::Dropped(index, full_name)) => {
436 indexes_to_drop.push((index.cluster_id, index.global_id()));
437 dropped_item_names.insert(index.global_id(), full_name);
438 }
439 CatalogImplication::MaterializedView(CatalogImplicationKind::Added(mv)) => {
440 tracing::debug!(?mv, "not handling AddMaterializedView in here yet");
441 }
442 CatalogImplication::MaterializedView(CatalogImplicationKind::Altered {
443 prev: prev_mv,
444 new: new_mv,
445 }) => {
446 if prev_mv.collections != new_mv.collections {
465 assert_eq!(
468 prev_mv.global_id_writes(),
469 new_mv.global_id_writes(),
470 "unexpected MV Altered implication: prev={prev_mv:?}, new={new_mv:?}",
471 );
472
473 let gid = new_mv.global_id_writes();
474 self.allow_writes(new_mv.cluster_id, gid);
475
476 source_gids_to_keep.extend(new_mv.global_ids());
481 } else if prev_mv.custom_logical_compaction_window
482 != new_mv.custom_logical_compaction_window
483 {
484 let new_window = new_mv
485 .custom_logical_compaction_window
486 .unwrap_or(CompactionWindow::Default);
487 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
488 }
489 }
490 CatalogImplication::MaterializedView(CatalogImplicationKind::Dropped(
491 mv,
492 full_name,
493 )) => {
494 compute_sinks_to_drop.push((mv.cluster_id, mv.global_id_writes()));
495 for gid in mv.global_ids() {
496 sources_to_drop.push((catalog_id, gid));
497 dropped_item_names.insert(gid, full_name.clone());
498 }
499 }
500 CatalogImplication::View(CatalogImplicationKind::Added(_view)) => {
501 }
504 CatalogImplication::View(CatalogImplicationKind::Altered {
505 prev: _prev_view,
506 new: _new_view,
507 }) => {
508 }
511 CatalogImplication::View(CatalogImplicationKind::Dropped(view, full_name)) => {
512 view_gids_to_drop.push(view.global_id());
513 dropped_item_names.insert(view.global_id(), full_name);
514 }
515 CatalogImplication::Secret(CatalogImplicationKind::Added(_secret)) => {
516 }
520 CatalogImplication::Secret(CatalogImplicationKind::Altered {
521 prev: _prev_secret,
522 new: _new_secret,
523 }) => {
524 }
527 CatalogImplication::Secret(CatalogImplicationKind::Dropped(
528 _secret,
529 _full_name,
530 )) => {
531 secrets_to_drop.push(catalog_id);
532 }
533 CatalogImplication::Connection(CatalogImplicationKind::Added(connection)) => {
534 match &connection.details {
535 ConnectionDetails::Ssh { .. } => {}
538 ConnectionDetails::AwsPrivatelink(privatelink) => {
540 let spec = VpcEndpointConfig {
541 aws_service_name: privatelink.service_name.to_owned(),
542 availability_zone_ids: privatelink.availability_zones.to_owned(),
543 };
544 vpc_endpoints_to_create.push((catalog_id, spec));
545 }
546 _ => {}
548 }
549 }
550 CatalogImplication::Connection(CatalogImplicationKind::Altered {
551 prev: _prev_connection,
552 new: new_connection,
553 }) => {
554 self.handle_alter_connection(
555 catalog_id,
556 new_connection,
557 &mut vpc_endpoints_to_create,
558 &mut source_connections_to_alter,
559 &mut sink_connections_to_alter,
560 &mut source_export_data_configs_to_alter,
561 );
562 }
563 CatalogImplication::Connection(CatalogImplicationKind::Dropped(
564 connection,
565 _full_name,
566 )) => {
567 match &connection.details {
568 ConnectionDetails::Ssh { .. } => {
570 secrets_to_drop.push(catalog_id);
571 }
572 ConnectionDetails::AwsPrivatelink(_) => {
575 vpc_endpoints_to_drop.push(catalog_id);
576 }
577 _ => (),
578 }
579 }
580 CatalogImplication::None => {
581 }
583 CatalogImplication::Cluster(_) | CatalogImplication::ClusterReplica(_) => {
584 unreachable!("clusters and cluster replicas are handled below")
585 }
586 CatalogImplication::Table(CatalogImplicationKind::None)
587 | CatalogImplication::Source(CatalogImplicationKind::None)
588 | CatalogImplication::Sink(CatalogImplicationKind::None)
589 | CatalogImplication::Index(CatalogImplicationKind::None)
590 | CatalogImplication::MaterializedView(CatalogImplicationKind::None)
591 | CatalogImplication::View(CatalogImplicationKind::None)
592 | CatalogImplication::Secret(CatalogImplicationKind::None)
593 | CatalogImplication::Connection(CatalogImplicationKind::None) => {
594 unreachable!("will never leave None in place");
595 }
596 }
597 }
598
599 for (cluster_id, command) in cluster_commands {
600 tracing::trace!(?command, "have cluster command to apply!");
601
602 match command {
603 CatalogImplication::Cluster(CatalogImplicationKind::Added(cluster)) => {
604 let arranged_logs = introspection_source_indexes
609 .remove(&cluster_id)
610 .unwrap_or_default();
611 let introspection_source_ids: Vec<_> =
612 arranged_logs.values().copied().collect();
613
614 self.controller
615 .create_cluster(
616 cluster_id,
617 mz_controller::clusters::ClusterConfig {
618 arranged_logs,
619 workload_class: cluster.config.workload_class.clone(),
620 },
621 )
622 .expect("creating cluster must not fail");
623
624 if !introspection_source_ids.is_empty() {
625 self.initialize_compute_read_policies(
626 introspection_source_ids,
627 cluster_id,
628 CompactionWindow::Default,
629 )
630 .await;
631 }
632 }
633 CatalogImplication::Cluster(CatalogImplicationKind::Altered {
634 prev: prev_cluster,
635 new: new_cluster,
636 }) => {
637 if prev_cluster.config.workload_class != new_cluster.config.workload_class {
643 self.controller.update_cluster_workload_class(
644 cluster_id,
645 new_cluster.config.workload_class.clone(),
646 );
647 }
648 }
649 CatalogImplication::Cluster(CatalogImplicationKind::Dropped(
650 cluster,
651 _full_name,
652 )) => {
653 clusters_to_drop.push(cluster_id);
654 dropped_cluster_names.insert(cluster_id, cluster.name);
655 }
656 CatalogImplication::Cluster(CatalogImplicationKind::None) => {
657 unreachable!("will never leave None in place");
658 }
659 command => {
660 unreachable!(
661 "we only handle cluster commands in this map, got: {:?}",
662 command
663 );
664 }
665 }
666 }
667
668 if replica_scoped_config_changed {
675 self.push_replica_dyncfg_overrides();
676 }
677
678 for ((cluster_id, replica_id), command) in cluster_replica_commands {
679 tracing::trace!(?command, "have cluster replica command to apply!");
680
681 match command {
682 CatalogImplication::ClusterReplica(CatalogImplicationKind::Added(replica)) => {
683 let cluster = self.catalog().get_cluster(cluster_id);
691 let cluster_name = cluster.name.clone();
692 let cluster_role = cluster.role();
693 self.handle_create_cluster_replica(
694 cluster_id,
695 replica_id,
696 cluster_role,
697 cluster_name,
698 replica.name.clone(),
699 replica.config.clone(),
700 )
701 .await;
702 }
703 CatalogImplication::ClusterReplica(CatalogImplicationKind::Altered {
704 prev: _prev_replica,
705 new: _new_replica,
706 }) => {
707 }
711 CatalogImplication::ClusterReplica(CatalogImplicationKind::Dropped(
712 _replica,
713 _full_name,
714 )) => {
715 cluster_replicas_to_drop.push((cluster_id, replica_id));
716 }
717 CatalogImplication::ClusterReplica(CatalogImplicationKind::None) => {
718 unreachable!("will never leave None in place");
719 }
720 command => {
721 unreachable!(
722 "we only handle cluster replica commands in this map, got: {:?}",
723 command
724 );
725 }
726 }
727 }
728
729 if !source_collections_to_create.is_empty() {
730 self.create_source_collections(source_collections_to_create)
731 .await?;
732 }
733
734 if !table_collections_to_create.is_empty() {
737 self.create_table_collections(table_collections_to_create, execution_timestamps_to_set)
738 .await?;
739 }
740 self.initialize_storage_collections(storage_policies_to_initialize)
746 .await?;
747
748 if !vpc_endpoints_to_create.is_empty() {
750 if let Some(cloud_resource_controller) = self.cloud_resource_controller.as_ref() {
751 for (connection_id, spec) in vpc_endpoints_to_create {
752 if let Err(err) = cloud_resource_controller
753 .ensure_vpc_endpoint(connection_id, spec)
754 .await
755 {
756 tracing::error!(?err, "failed to ensure vpc endpoint!");
757 }
758 }
759 } else {
760 tracing::error!(
761 "AWS PrivateLink connections unsupported without cloud_resource_controller"
762 );
763 }
764 }
765
766 if !source_connections_to_alter.is_empty() {
768 self.controller
769 .storage
770 .alter_ingestion_connections(source_connections_to_alter)
771 .await
772 .unwrap_or_terminate("cannot fail to alter ingestion connections");
773 }
774
775 if !sink_connections_to_alter.is_empty() {
776 self.controller
777 .storage
778 .alter_export_connections(sink_connections_to_alter)
779 .await
780 .unwrap_or_terminate("altering export connections after txn must succeed");
781 }
782
783 if !source_export_data_configs_to_alter.is_empty() {
784 self.controller
785 .storage
786 .alter_ingestion_export_data_configs(source_export_data_configs_to_alter)
787 .await
788 .unwrap_or_terminate("altering source export data configs after txn must succeed");
789 }
790
791 if !source_descs_to_alter.is_empty() {
792 self.controller
793 .storage
794 .alter_ingestion_source_desc(source_descs_to_alter)
795 .await
796 .unwrap_or_terminate("cannot fail to alter ingestion source desc");
797 }
798
799 sources_to_drop.retain(|(_, gid)| !source_gids_to_keep.contains(gid));
801
802 let readable_collections_to_drop: BTreeSet<_> = sources_to_drop
803 .iter()
804 .map(|(_, gid)| *gid)
805 .chain(tables_to_drop.iter().map(|(_, gid)| *gid))
806 .chain(indexes_to_drop.iter().map(|(_, gid)| *gid))
807 .chain(view_gids_to_drop.iter().copied())
808 .collect();
809
810 for (sink_id, sink) in &self.active_compute_sinks {
813 let cluster_id = sink.cluster_id();
814 if let Some(id) = sink
815 .depends_on()
816 .iter()
817 .find(|id| readable_collections_to_drop.contains(id))
818 {
819 let name = dropped_item_names
820 .get(id)
821 .cloned()
822 .expect("missing relation name");
823 active_compute_sinks_to_drop.insert(
824 *sink_id,
825 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Relation {
826 name,
827 }),
828 );
829 } else if clusters_to_drop.contains(&cluster_id) {
830 let name = dropped_cluster_names
831 .get(&cluster_id)
832 .cloned()
833 .expect("missing cluster name");
834 active_compute_sinks_to_drop.insert(
835 *sink_id,
836 ActiveComputeSinkRetireReason::DependencyDropped(DroppedDependency::Cluster {
837 name,
838 }),
839 );
840 }
841 }
842
843 for (uuid, pending_peek) in &self.pending_peeks {
845 if let Some(id) = pending_peek
846 .depends_on
847 .iter()
848 .find(|id| readable_collections_to_drop.contains(id))
849 {
850 let name = dropped_item_names
851 .get(id)
852 .cloned()
853 .expect("missing relation name");
854 peeks_to_drop.push((DroppedDependency::Relation { name }, uuid.clone()));
855 } else if clusters_to_drop.contains(&pending_peek.cluster_id) {
856 let name = dropped_cluster_names
857 .get(&pending_peek.cluster_id)
858 .cloned()
859 .expect("missing cluster name");
860 peeks_to_drop.push((DroppedDependency::Cluster { name }, uuid.clone()));
861 }
862 }
863
864 for (conn_id, pending_copy) in &self.active_copies {
866 let dropping_table = tables_to_drop
867 .iter()
868 .any(|(item_id, _gid)| pending_copy.table_id == *item_id);
869 let dropping_cluster = clusters_to_drop.contains(&pending_copy.cluster_id);
870
871 if dropping_table || dropping_cluster {
872 copies_to_drop.push(conn_id.clone());
873 }
874 }
875
876 let storage_gids_to_drop: BTreeSet<_> = sources_to_drop
877 .iter()
878 .map(|(_id, gid)| gid)
879 .chain(storage_sink_gids_to_drop.iter())
880 .chain(tables_to_drop.iter().map(|(_id, gid)| gid))
881 .copied()
882 .collect();
883 let compute_gids_to_drop: Vec<_> = indexes_to_drop
884 .iter()
885 .chain(compute_sinks_to_drop.iter())
886 .copied()
887 .collect();
888
889 let mut timeline_id_bundles = BTreeMap::new();
895
896 for (timeline, TimelineState { read_holds, .. }) in &self.global_timelines {
897 let mut id_bundle = CollectionIdBundle::default();
898
899 for storage_id in read_holds.storage_ids() {
900 if storage_gids_to_drop.contains(&storage_id) {
901 id_bundle.storage_ids.insert(storage_id);
902 }
903 }
904
905 for (instance_id, id) in read_holds.compute_ids() {
906 if compute_gids_to_drop.contains(&(instance_id, id))
907 || clusters_to_drop.contains(&instance_id)
908 {
909 id_bundle
910 .compute_ids
911 .entry(instance_id)
912 .or_default()
913 .insert(id);
914 }
915 }
916
917 timeline_id_bundles.insert(timeline.clone(), id_bundle);
918 }
919
920 let mut timeline_associations = BTreeMap::new();
921 for (timeline, id_bundle) in timeline_id_bundles.into_iter() {
922 let TimelineState { read_holds, .. } = self
923 .global_timelines
924 .get(&timeline)
925 .expect("all timelines have a timestamp oracle");
926
927 let empty = read_holds.id_bundle().difference(&id_bundle).is_empty();
928 timeline_associations.insert(timeline, (empty, id_bundle));
929 }
930
931 let _: () = async {
934 if !timeline_associations.is_empty() {
935 for (timeline, (should_be_empty, id_bundle)) in timeline_associations {
936 let became_empty =
937 self.remove_resources_associated_with_timeline(timeline, id_bundle);
938 assert_eq!(should_be_empty, became_empty, "emptiness did not match!");
939 }
940 }
941
942 if !tables_to_drop.is_empty() {
949 let ts = self.get_local_write_ts().await;
950 self.drop_tables(tables_to_drop.into_iter().collect_vec(), ts.timestamp);
951 }
952
953 if !sources_to_drop.is_empty() {
954 self.drop_sources(sources_to_drop);
955 }
956
957 if !storage_sink_gids_to_drop.is_empty() {
958 self.drop_storage_sinks(storage_sink_gids_to_drop);
959 }
960
961 if !active_compute_sinks_to_drop.is_empty() {
962 let retire_notify = self
963 .retire_compute_sinks(active_compute_sinks_to_drop)
964 .await;
965 if let Some(ctx) = ctx {
966 ctx.delay_response_until(retire_notify);
967 }
968 }
969
970 if !peeks_to_drop.is_empty() {
971 for (dep, uuid) in peeks_to_drop {
972 if let Some(pending_peek) = self.remove_pending_peek(&uuid) {
973 let cancel_reason = PeekResponse::Error(dep.query_terminated_error());
974 self.controller
975 .compute
976 .cancel_peek(pending_peek.cluster_id, uuid, cancel_reason)
977 .unwrap_or_terminate("unable to cancel peek");
978 self.retire_execution(
979 StatementEndedExecutionReason::Canceled,
980 pending_peek.ctx_extra.defuse(),
981 );
982 }
983 }
984 }
985
986 if !copies_to_drop.is_empty() {
987 for conn_id in copies_to_drop {
988 self.cancel_pending_copy(&conn_id);
989 }
990 }
991
992 if !compute_gids_to_drop.is_empty() {
993 self.drop_compute_collections(compute_gids_to_drop);
994 }
995
996 if !vpc_endpoints_to_drop.is_empty() {
997 self.drop_vpc_endpoints_in_background(vpc_endpoints_to_drop)
998 }
999
1000 if !cluster_replicas_to_drop.is_empty() {
1001 fail::fail_point!("after_catalog_drop_replica");
1002
1003 for (cluster_id, replica_id) in cluster_replicas_to_drop {
1004 self.drop_replica(cluster_id, replica_id);
1005 }
1006 }
1007 if !clusters_to_drop.is_empty() {
1008 for cluster_id in clusters_to_drop {
1009 self.controller.drop_cluster(cluster_id);
1010 }
1011 }
1012
1013 task::spawn(|| "drop_replication_slots_and_secrets", {
1021 let ssh_tunnel_manager = self.connection_context().ssh_tunnel_manager.clone();
1022 let caching_secrets_reader = self.caching_secrets_reader.clone();
1023 let secrets_controller = Arc::clone(&self.secrets_controller);
1024 let secrets_reader = Arc::clone(self.secrets_reader());
1025 let storage_config = self.controller.storage.config().clone();
1026
1027 async move {
1028 for (connection, replication_slot_name) in replication_slots_to_drop {
1029 tracing::info!(?replication_slot_name, "dropping replication slot");
1030
1031 let result: Result<(), anyhow::Error> = Retry::default()
1037 .max_duration(Duration::from_secs(60))
1038 .retry_async(|_state| async {
1039 let config = connection
1040 .config(&secrets_reader, &storage_config, InTask::No)
1041 .await
1042 .map_err(|e| {
1043 anyhow::anyhow!(
1044 "error creating Postgres client for \
1045 dropping acquired slots: {}",
1046 e.display_with_causes()
1047 )
1048 })?;
1049
1050 mz_postgres_util::drop_replication_slots(
1051 &ssh_tunnel_manager,
1052 config.clone(),
1053 &[(&replication_slot_name, true)],
1054 )
1055 .await?;
1056
1057 Ok(())
1058 })
1059 .await;
1060
1061 if let Err(err) = result {
1062 tracing::warn!(
1063 ?replication_slot_name,
1064 ?err,
1065 "failed to drop replication slot"
1066 );
1067 }
1068 }
1069
1070 fail_point!("drop_secrets");
1078 for secret in secrets_to_drop {
1079 if let Err(e) = secrets_controller.delete(secret).await {
1080 warn!("Dropping secrets has encountered an error: {}", e);
1081 } else {
1082 caching_secrets_reader.invalidate(secret);
1083 }
1084 }
1085 }
1086 });
1087 }
1088 .instrument(info_span!(
1089 "coord::apply_catalog_implications_inner::finalize"
1090 ))
1091 .await;
1092
1093 Ok(())
1094 }
1095
1096 #[instrument(level = "debug")]
1097 async fn create_table_collections(
1098 &mut self,
1099 table_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1100 execution_timestamps_to_set: BTreeSet<StatementLoggingId>,
1101 ) -> Result<(), AdapterError> {
1102 let write_ts = self.get_local_write_ts().await;
1104 let register_ts = write_ts.timestamp;
1105
1106 self.catalog
1116 .advance_upper(write_ts.advance_to)
1117 .await
1118 .unwrap_or_terminate("unable to advance catalog upper");
1119
1120 for id in execution_timestamps_to_set {
1121 self.set_statement_execution_timestamp(id, register_ts);
1122 }
1123
1124 let storage_metadata = self.catalog.state().storage_metadata();
1125
1126 self.controller
1127 .storage
1128 .create_collections(
1129 storage_metadata,
1130 Some(register_ts),
1131 table_collections_to_create.into_iter().collect_vec(),
1132 )
1133 .await
1134 .unwrap_or_terminate("cannot fail to create collections");
1135
1136 self.apply_local_write(register_ts).await;
1137
1138 Ok(())
1139 }
1140
1141 #[instrument(level = "debug")]
1142 async fn create_source_collections(
1143 &mut self,
1144 source_collections_to_create: BTreeMap<GlobalId, CollectionDescription>,
1145 ) -> Result<(), AdapterError> {
1146 let storage_metadata = self.catalog.state().storage_metadata();
1147
1148 self.controller
1149 .storage
1150 .create_collections(
1151 storage_metadata,
1152 None, source_collections_to_create.into_iter().collect_vec(),
1154 )
1155 .await
1156 .unwrap_or_terminate("cannot fail to create collections");
1157
1158 Ok(())
1159 }
1160
1161 #[instrument(level = "debug")]
1162 async fn initialize_storage_collections(
1163 &mut self,
1164 storage_policies_to_initialize: BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1165 ) -> Result<(), AdapterError> {
1166 for (compaction_window, global_ids) in storage_policies_to_initialize {
1167 self.initialize_read_policies(
1168 &CollectionIdBundle {
1169 storage_ids: global_ids,
1170 compute_ids: BTreeMap::new(),
1171 },
1172 compaction_window,
1173 )
1174 .await;
1175 }
1176
1177 Ok(())
1178 }
1179
1180 #[instrument(level = "debug")]
1181 async fn handle_create_table(
1182 &self,
1183 ctx: &Option<&mut ExecuteContext>,
1184 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1185 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1186 execution_timestamps_to_set: &mut BTreeSet<StatementLoggingId>,
1187 table_id: CatalogItemId,
1188 table: Table,
1189 ) -> Result<(), AdapterError> {
1190 match &table.data_source {
1194 TableDataSource::TableWrites { defaults: _ } => {
1195 let versions: BTreeMap<_, _> = table
1196 .collection_descs()
1197 .map(|(gid, version, desc)| (version, (gid, desc)))
1198 .collect();
1199 let collection_descs = versions.iter().map(|(_version, (gid, desc))| {
1200 let collection_desc = CollectionDescription::for_table(desc.clone());
1201
1202 (*gid, collection_desc)
1203 });
1204
1205 let compaction_window = table
1206 .custom_logical_compaction_window
1207 .unwrap_or(CompactionWindow::Default);
1208 let ids_to_initialize = storage_policies_to_initialize
1209 .entry(compaction_window)
1210 .or_default();
1211
1212 for (gid, collection_desc) in collection_descs {
1213 storage_collections_to_create.insert(gid, collection_desc);
1214 ids_to_initialize.insert(gid);
1215 }
1216
1217 if let Some(id) = ctx.as_ref().and_then(|ctx| ctx.extra().contents()) {
1218 execution_timestamps_to_set.insert(id);
1219 }
1220 }
1221 TableDataSource::DataSource {
1222 desc: data_source_desc,
1223 timeline,
1224 } => {
1225 match data_source_desc {
1226 DataSourceDesc::IngestionExport {
1227 ingestion_id,
1228 external_reference: _,
1229 details,
1230 data_config,
1231 } => {
1232 let global_ingestion_id =
1233 self.catalog().get_entry(ingestion_id).latest_global_id();
1234
1235 let collection_desc = CollectionDescription {
1236 desc: table.desc.latest(),
1237 data_source: DataSource::IngestionExport {
1238 ingestion_id: global_ingestion_id,
1239 details: details.clone(),
1240 data_config: data_config
1241 .clone()
1242 .into_inline_connection(self.catalog.state()),
1243 },
1244 since: None,
1245 timeline: Some(timeline.clone()),
1246 primary: None,
1247 };
1248
1249 let global_id = table
1250 .global_ids()
1251 .expect_element(|| "subsources cannot have multiple versions");
1252
1253 storage_collections_to_create.insert(global_id, collection_desc);
1254
1255 let read_policies = self
1256 .catalog()
1257 .state()
1258 .source_compaction_windows(vec![table_id]);
1259 for (compaction_window, catalog_ids) in read_policies {
1260 let compaction_ids = storage_policies_to_initialize
1261 .entry(compaction_window)
1262 .or_default();
1263
1264 let gids = catalog_ids
1265 .into_iter()
1266 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1267 .flatten();
1268 compaction_ids.extend(gids);
1269 }
1270 }
1271 DataSourceDesc::Webhook {
1272 validate_using: _,
1273 body_format: _,
1274 headers: _,
1275 cluster_id: _,
1276 } => {
1277 assert_eq!(
1279 table.desc.latest_version(),
1280 RelationVersion::root(),
1281 "found webhook with more than 1 relation version, {:?}",
1282 table.desc
1283 );
1284 let desc = table.desc.latest();
1285
1286 let collection_desc = CollectionDescription {
1287 desc,
1288 data_source: DataSource::Webhook,
1289 since: None,
1290 timeline: Some(timeline.clone()),
1291 primary: None,
1292 };
1293
1294 let global_id = table
1295 .global_ids()
1296 .expect_element(|| "webhooks cannot have multiple versions");
1297
1298 storage_collections_to_create.insert(global_id, collection_desc);
1299
1300 let read_policies = self
1301 .catalog()
1302 .state()
1303 .source_compaction_windows(vec![table_id]);
1304
1305 for (compaction_window, catalog_ids) in read_policies {
1306 let compaction_ids = storage_policies_to_initialize
1307 .entry(compaction_window)
1308 .or_default();
1309
1310 let gids = catalog_ids
1311 .into_iter()
1312 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1313 .flatten();
1314 compaction_ids.extend(gids);
1315 }
1316 }
1317 _ => unreachable!("CREATE TABLE data source got {:?}", data_source_desc),
1318 }
1319 }
1320 }
1321
1322 Ok(())
1323 }
1324
1325 #[instrument(level = "debug")]
1326 async fn handle_alter_table(
1327 &mut self,
1328 catalog_id: CatalogItemId,
1329 prev_table: Table,
1330 new_table: Table,
1331 ) -> Result<(), AdapterError> {
1332 let existing_gid = prev_table.global_id_writes();
1333 let new_gid = new_table.global_id_writes();
1334
1335 if existing_gid == new_gid {
1336 if prev_table.custom_logical_compaction_window
1339 != new_table.custom_logical_compaction_window
1340 {
1341 let new_window = new_table
1342 .custom_logical_compaction_window
1343 .unwrap_or(CompactionWindow::Default);
1344 self.update_storage_read_policies(vec![(catalog_id, new_window.into())]);
1345 }
1346 return Ok(());
1347 }
1348
1349 let existing_table = crate::CollectionIdBundle {
1353 storage_ids: BTreeSet::from([existing_gid]),
1354 compute_ids: BTreeMap::new(),
1355 };
1356 let existing_table_read_hold = self.acquire_read_holds(&existing_table);
1357
1358 let expected_version = prev_table.desc.latest_version();
1359 let new_version = new_table.desc.latest_version();
1360 let new_desc = new_table
1361 .desc
1362 .at_version(RelationVersionSelector::Specific(new_version));
1363
1364 let write_ts = self.get_local_write_ts().await;
1365 let register_ts = write_ts.timestamp;
1366
1367 self.catalog
1370 .advance_upper(write_ts.advance_to)
1371 .await
1372 .unwrap_or_terminate("unable to advance catalog upper");
1373
1374 self.controller
1376 .storage
1377 .alter_table_desc(
1378 existing_gid,
1379 new_gid,
1380 new_desc,
1381 expected_version,
1382 register_ts,
1383 )
1384 .await
1385 .expect("failed to alter desc of table");
1386
1387 let compaction_window = new_table
1389 .custom_logical_compaction_window
1390 .unwrap_or(CompactionWindow::Default);
1391 self.initialize_read_policies(
1392 &crate::CollectionIdBundle {
1393 storage_ids: BTreeSet::from([new_gid]),
1394 compute_ids: BTreeMap::new(),
1395 },
1396 compaction_window,
1397 )
1398 .await;
1399
1400 self.apply_local_write(register_ts).await;
1401
1402 drop(existing_table_read_hold);
1404
1405 Ok(())
1406 }
1407
1408 #[instrument(level = "debug")]
1409 async fn handle_create_source(
1410 &self,
1411 storage_collections_to_create: &mut BTreeMap<GlobalId, CollectionDescription>,
1412 storage_policies_to_initialize: &mut BTreeMap<CompactionWindow, BTreeSet<GlobalId>>,
1413 item_id: CatalogItemId,
1414 source: Source,
1415 compaction_windows: BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>>,
1416 ) -> Result<(), AdapterError> {
1417 let data_source = match source.data_source {
1418 DataSourceDesc::Ingestion { desc, cluster_id } => {
1419 let desc = desc.into_inline_connection(self.catalog().state());
1420 let item_global_id = self.catalog().get_entry(&item_id).latest_global_id();
1421
1422 let ingestion = mz_storage_types::sources::IngestionDescription::new(
1423 desc,
1424 cluster_id,
1425 item_global_id,
1426 );
1427
1428 DataSource::Ingestion(ingestion)
1429 }
1430 DataSourceDesc::OldSyntaxIngestion {
1431 desc,
1432 progress_subsource,
1433 data_config,
1434 details,
1435 cluster_id,
1436 } => {
1437 let desc = desc.into_inline_connection(self.catalog().state());
1438 let data_config = data_config.into_inline_connection(self.catalog().state());
1439
1440 let progress_subsource = self
1443 .catalog()
1444 .get_entry(&progress_subsource)
1445 .latest_global_id();
1446
1447 let mut ingestion = mz_storage_types::sources::IngestionDescription::new(
1448 desc,
1449 cluster_id,
1450 progress_subsource,
1451 );
1452
1453 let legacy_export = SourceExport {
1454 storage_metadata: (),
1455 data_config,
1456 details,
1457 };
1458
1459 ingestion
1460 .source_exports
1461 .insert(source.global_id, legacy_export);
1462
1463 DataSource::Ingestion(ingestion)
1464 }
1465 DataSourceDesc::IngestionExport {
1466 ingestion_id,
1467 external_reference: _,
1468 details,
1469 data_config,
1470 } => {
1471 let ingestion_id = self.catalog().get_entry(&ingestion_id).latest_global_id();
1474
1475 DataSource::IngestionExport {
1476 ingestion_id,
1477 details,
1478 data_config: data_config.into_inline_connection(self.catalog().state()),
1479 }
1480 }
1481 DataSourceDesc::Progress => DataSource::Progress,
1482 DataSourceDesc::Webhook { .. } => DataSource::Webhook,
1483 DataSourceDesc::Introspection(_) | DataSourceDesc::Catalog => {
1484 unreachable!("cannot create sources with internal data sources")
1485 }
1486 };
1487
1488 storage_collections_to_create.insert(
1489 source.global_id,
1490 CollectionDescription {
1491 desc: source.desc.clone(),
1492 data_source,
1493 timeline: Some(source.timeline),
1494 since: None,
1495 primary: None,
1496 },
1497 );
1498
1499 for (compaction_window, catalog_ids) in compaction_windows {
1501 let compaction_ids = storage_policies_to_initialize
1502 .entry(compaction_window)
1503 .or_default();
1504
1505 let gids = catalog_ids
1506 .into_iter()
1507 .map(|item_id| self.catalog().get_entry(&item_id).global_ids())
1508 .flatten();
1509 compaction_ids.extend(gids);
1510 }
1511
1512 Ok(())
1513 }
1514
1515 #[instrument(level = "debug")]
1522 fn handle_alter_connection(
1523 &self,
1524 connection_id: CatalogItemId,
1525 connection: Connection,
1526 vpc_endpoints_to_create: &mut Vec<(CatalogItemId, VpcEndpointConfig)>,
1527 source_connections_to_alter: &mut BTreeMap<
1528 GlobalId,
1529 GenericSourceConnection<InlinedConnection>,
1530 >,
1531 sink_connections_to_alter: &mut BTreeMap<GlobalId, StorageSinkConnection>,
1532 source_export_data_configs_to_alter: &mut BTreeMap<GlobalId, SourceExportDataConfig>,
1533 ) {
1534 use std::collections::VecDeque;
1535
1536 if let ConnectionDetails::AwsPrivatelink(ref privatelink) = connection.details {
1538 let spec = VpcEndpointConfig {
1539 aws_service_name: privatelink.service_name.to_owned(),
1540 availability_zone_ids: privatelink.availability_zones.to_owned(),
1541 };
1542 vpc_endpoints_to_create.push((connection_id, spec));
1543 }
1544
1545 let mut connections_to_process = VecDeque::new();
1549 connections_to_process.push_front(connection_id.clone());
1550
1551 while let Some(id) = connections_to_process.pop_front() {
1552 for dependent_id in self.catalog().get_entry(&id).used_by() {
1553 let dependent_entry = self.catalog().get_entry(dependent_id);
1554 match dependent_entry.item() {
1555 CatalogItem::Connection(_) => {
1556 connections_to_process.push_back(*dependent_id);
1560 }
1561 CatalogItem::Source(source) => {
1562 let desc = match &dependent_entry
1563 .source()
1564 .expect("known to be source")
1565 .data_source
1566 {
1567 DataSourceDesc::Ingestion { desc, .. }
1568 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1569 desc.clone().into_inline_connection(self.catalog().state())
1570 }
1571 DataSourceDesc::IngestionExport { .. }
1572 | DataSourceDesc::Introspection(_)
1573 | DataSourceDesc::Progress
1574 | DataSourceDesc::Webhook { .. }
1575 | DataSourceDesc::Catalog => {
1576 continue;
1578 }
1579 };
1580
1581 source_connections_to_alter.insert(source.global_id, desc.connection);
1582 }
1583 CatalogItem::Sink(sink) => {
1584 let export = dependent_entry.sink().expect("known to be sink");
1585 sink_connections_to_alter.insert(
1586 sink.global_id,
1587 export
1588 .connection
1589 .clone()
1590 .into_inline_connection(self.catalog().state()),
1591 );
1592 }
1593 CatalogItem::Table(table) => {
1594 if let Some((_, _, _, export_data_config)) =
1598 dependent_entry.source_export_details()
1599 {
1600 let data_config = export_data_config.clone();
1601 source_export_data_configs_to_alter.insert(
1602 table.global_id_writes(),
1603 data_config.into_inline_connection(self.catalog().state()),
1604 );
1605 }
1606 }
1607 CatalogItem::Log(_)
1608 | CatalogItem::View(_)
1609 | CatalogItem::MaterializedView(_)
1610 | CatalogItem::Index(_)
1611 | CatalogItem::Type(_)
1612 | CatalogItem::Func(_)
1613 | CatalogItem::Secret(_) => {
1614 }
1617 }
1618 }
1619 }
1620 }
1621
1622 async fn handle_create_cluster_replica(
1623 &mut self,
1624 cluster_id: ClusterId,
1625 replica_id: ReplicaId,
1626 role: ClusterRole,
1627 cluster_name: String,
1628 replica_name: String,
1629 replica_config: ReplicaConfig,
1630 ) {
1631 let enable_worker_core_affinity =
1632 self.catalog().system_config().enable_worker_core_affinity();
1633 let enable_storage_introspection_logs = self
1634 .catalog()
1635 .system_config()
1636 .enable_storage_introspection_logs();
1637
1638 self.controller
1645 .create_replica(
1646 cluster_id,
1647 replica_id,
1648 cluster_name,
1649 replica_name,
1650 role,
1651 replica_config,
1652 enable_worker_core_affinity,
1653 enable_storage_introspection_logs,
1654 )
1655 .expect("creating replicas must not fail");
1656
1657 self.install_introspection_subscribes(cluster_id, replica_id)
1658 .await;
1659 }
1660}
1661
1662#[derive(Debug, Clone)]
1668enum CatalogImplication {
1669 None,
1670 Table(CatalogImplicationKind<Table>),
1671 Source(CatalogImplicationKind<(Source, Option<GenericSourceConnection>)>),
1672 Sink(CatalogImplicationKind<Sink>),
1673 Index(CatalogImplicationKind<Index>),
1674 MaterializedView(CatalogImplicationKind<MaterializedView>),
1675 View(CatalogImplicationKind<View>),
1676 Secret(CatalogImplicationKind<Secret>),
1677 Connection(CatalogImplicationKind<Connection>),
1678 Cluster(CatalogImplicationKind<Cluster>),
1679 ClusterReplica(CatalogImplicationKind<ClusterReplica>),
1680}
1681
1682#[derive(Debug, Clone)]
1683enum CatalogImplicationKind<T> {
1684 None,
1686 Added(T),
1688 Dropped(T, String),
1690 Altered { prev: T, new: T },
1692}
1693
1694impl<T: Clone> CatalogImplicationKind<T> {
1695 fn transition(&mut self, item: T, name: Option<String>, diff: StateDiff) -> Result<(), String> {
1698 use CatalogImplicationKind::*;
1699 use StateDiff::*;
1700
1701 let new_state = match (&*self, diff) {
1702 (None, Addition) => Added(item),
1704 (None, Retraction) => Dropped(item, name.unwrap_or_else(|| "<unknown>".to_string())),
1705
1706 (Added(existing), Retraction) => {
1708 Altered {
1710 prev: item,
1711 new: existing.clone(),
1712 }
1713 }
1714 (Added(_), Addition) => {
1715 return Err("Cannot add an already added object".to_string());
1716 }
1717
1718 (Dropped(existing, _), Addition) => {
1720 Altered {
1722 prev: existing.clone(),
1723 new: item,
1724 }
1725 }
1726 (Dropped(_, _), Retraction) => {
1727 return Err("Cannot drop an already dropped object".to_string());
1728 }
1729
1730 (Altered { .. }, _) => {
1732 return Err(format!(
1733 "Cannot apply {:?} to an object in Altered state",
1734 diff
1735 ));
1736 }
1737 };
1738
1739 *self = new_state;
1740 Ok(())
1741 }
1742}
1743
1744macro_rules! impl_absorb_method {
1746 (
1747 $method_name:ident,
1748 $variant:ident,
1749 $item_type:ty
1750 ) => {
1751 fn $method_name(
1752 &mut self,
1753 item: $item_type,
1754 parsed_full_name: Option<String>,
1755 diff: StateDiff,
1756 ) {
1757 let state = match self {
1758 CatalogImplication::$variant(state) => state,
1759 CatalogImplication::None => {
1760 *self = CatalogImplication::$variant(CatalogImplicationKind::None);
1761 match self {
1762 CatalogImplication::$variant(state) => state,
1763 _ => unreachable!(),
1764 }
1765 }
1766 _ => {
1767 panic!(
1768 "Unexpected command type for {:?}: {} {:?}",
1769 self,
1770 stringify!($variant),
1771 diff,
1772 );
1773 }
1774 };
1775
1776 if let Err(e) = state.transition(item, parsed_full_name, diff) {
1777 panic!(
1778 "Invalid state transition for {}: {}",
1779 stringify!($variant),
1780 e
1781 );
1782 }
1783 }
1784 };
1785}
1786
1787impl CatalogImplication {
1788 fn absorb(&mut self, catalog_update: ParsedStateUpdate) {
1791 match catalog_update.kind {
1792 ParsedStateUpdateKind::Item {
1793 durable_item: _,
1794 parsed_item,
1795 connection,
1796 parsed_full_name,
1797 } => match parsed_item {
1798 CatalogItem::Table(table) => {
1799 self.absorb_table(table, Some(parsed_full_name), catalog_update.diff)
1800 }
1801 CatalogItem::Source(source) => {
1802 self.absorb_source(
1803 (source, connection),
1804 Some(parsed_full_name),
1805 catalog_update.diff,
1806 );
1807 }
1808 CatalogItem::Sink(sink) => {
1809 self.absorb_sink(sink, Some(parsed_full_name), catalog_update.diff);
1810 }
1811 CatalogItem::Index(index) => {
1812 self.absorb_index(index, Some(parsed_full_name), catalog_update.diff);
1813 }
1814 CatalogItem::MaterializedView(mv) => {
1815 self.absorb_materialized_view(mv, Some(parsed_full_name), catalog_update.diff);
1816 }
1817 CatalogItem::View(view) => {
1818 self.absorb_view(view, Some(parsed_full_name), catalog_update.diff);
1819 }
1820
1821 CatalogItem::Secret(secret) => {
1822 self.absorb_secret(secret, None, catalog_update.diff);
1823 }
1824 CatalogItem::Connection(connection) => {
1825 self.absorb_connection(connection, None, catalog_update.diff);
1826 }
1827 CatalogItem::Log(_) => {}
1828 CatalogItem::Type(_) => {}
1829 CatalogItem::Func(_) => {}
1830 },
1831 ParsedStateUpdateKind::TemporaryItem {
1832 durable_item: _,
1833 parsed_item,
1834 connection,
1835 parsed_full_name,
1836 } => match parsed_item {
1837 CatalogItem::Table(table) => {
1838 self.absorb_table(table, Some(parsed_full_name), catalog_update.diff)
1839 }
1840 CatalogItem::Source(source) => {
1841 self.absorb_source(
1842 (source, connection),
1843 Some(parsed_full_name),
1844 catalog_update.diff,
1845 );
1846 }
1847 CatalogItem::Sink(sink) => {
1848 self.absorb_sink(sink, Some(parsed_full_name), catalog_update.diff);
1849 }
1850 CatalogItem::Index(index) => {
1851 self.absorb_index(index, Some(parsed_full_name), catalog_update.diff);
1852 }
1853 CatalogItem::MaterializedView(mv) => {
1854 self.absorb_materialized_view(mv, Some(parsed_full_name), catalog_update.diff);
1855 }
1856 CatalogItem::View(view) => {
1857 self.absorb_view(view, Some(parsed_full_name), catalog_update.diff);
1858 }
1859
1860 CatalogItem::Secret(secret) => {
1861 self.absorb_secret(secret, None, catalog_update.diff);
1862 }
1863 CatalogItem::Connection(connection) => {
1864 self.absorb_connection(connection, None, catalog_update.diff);
1865 }
1866 CatalogItem::Log(_) => {}
1867 CatalogItem::Type(_) => {}
1868 CatalogItem::Func(_) => {}
1869 },
1870 ParsedStateUpdateKind::Cluster {
1871 durable_cluster: _,
1872 parsed_cluster,
1873 } => {
1874 let name = parsed_cluster.name.clone();
1875 self.absorb_cluster(parsed_cluster, Some(name), catalog_update.diff);
1876 }
1877 ParsedStateUpdateKind::ClusterReplica {
1878 durable_cluster_replica: _,
1879 parsed_cluster_replica,
1880 } => {
1881 let name = parsed_cluster_replica.name.clone();
1882 self.absorb_cluster_replica(
1883 parsed_cluster_replica,
1884 Some(name),
1885 catalog_update.diff,
1886 );
1887 }
1888 ParsedStateUpdateKind::IntrospectionSourceIndex { .. } => {
1889 unreachable!("IntrospectionSourceIndex should not be passed to absorb");
1893 }
1894 ParsedStateUpdateKind::ReplicaSystemConfiguration { .. } => {
1895 unreachable!("ReplicaSystemConfiguration should not be passed to absorb");
1898 }
1899 }
1900 }
1901
1902 impl_absorb_method!(absorb_table, Table, Table);
1903 impl_absorb_method!(
1904 absorb_source,
1905 Source,
1906 (Source, Option<GenericSourceConnection>)
1907 );
1908 impl_absorb_method!(absorb_sink, Sink, Sink);
1909 impl_absorb_method!(absorb_index, Index, Index);
1910 impl_absorb_method!(absorb_materialized_view, MaterializedView, MaterializedView);
1911 impl_absorb_method!(absorb_view, View, View);
1912
1913 impl_absorb_method!(absorb_secret, Secret, Secret);
1914 impl_absorb_method!(absorb_connection, Connection, Connection);
1915
1916 impl_absorb_method!(absorb_cluster, Cluster, Cluster);
1917 impl_absorb_method!(absorb_cluster_replica, ClusterReplica, ClusterReplica);
1918}
1919
1920#[cfg(test)]
1921mod tests {
1922 use super::*;
1923 use mz_repr::{GlobalId, RelationDesc, RelationVersion, VersionedRelationDesc};
1924 use mz_sql::names::ResolvedIds;
1925 use std::collections::BTreeMap;
1926
1927 fn create_test_table(name: &str) -> Table {
1928 Table {
1929 desc: VersionedRelationDesc::new(
1930 RelationDesc::builder()
1931 .with_column(name, mz_repr::SqlScalarType::String.nullable(false))
1932 .finish(),
1933 ),
1934 create_sql: None,
1935 collections: BTreeMap::from([(RelationVersion::root(), GlobalId::System(1))]),
1936 conn_id: None,
1937 resolved_ids: ResolvedIds::empty(),
1938 custom_logical_compaction_window: None,
1939 is_retained_metrics_object: false,
1940 data_source: TableDataSource::TableWrites { defaults: vec![] },
1941 }
1942 }
1943
1944 #[mz_ore::test]
1945 fn test_item_state_transitions() {
1946 let mut state = CatalogImplicationKind::None;
1948 assert!(
1949 state
1950 .transition("item1".to_string(), None, StateDiff::Addition)
1951 .is_ok()
1952 );
1953 assert!(matches!(state, CatalogImplicationKind::Added(_)));
1954
1955 let mut state = CatalogImplicationKind::Added("new_item".to_string());
1957 assert!(
1958 state
1959 .transition("old_item".to_string(), None, StateDiff::Retraction)
1960 .is_ok()
1961 );
1962 match &state {
1963 CatalogImplicationKind::Altered { prev, new } => {
1964 assert_eq!(prev, "old_item");
1966 assert_eq!(new, "new_item");
1968 }
1969 _ => panic!("Expected Altered state"),
1970 }
1971
1972 let mut state = CatalogImplicationKind::None;
1974 assert!(
1975 state
1976 .transition(
1977 "item1".to_string(),
1978 Some("test_name".to_string()),
1979 StateDiff::Retraction
1980 )
1981 .is_ok()
1982 );
1983 assert!(matches!(state, CatalogImplicationKind::Dropped(_, _)));
1984
1985 let mut state = CatalogImplicationKind::Dropped("old_item".to_string(), "name".to_string());
1987 assert!(
1988 state
1989 .transition("new_item".to_string(), None, StateDiff::Addition)
1990 .is_ok()
1991 );
1992 match &state {
1993 CatalogImplicationKind::Altered { prev, new } => {
1994 assert_eq!(prev, "old_item");
1996 assert_eq!(new, "new_item");
1998 }
1999 _ => panic!("Expected Altered state"),
2000 }
2001
2002 let mut state = CatalogImplicationKind::Added("item".to_string());
2004 assert!(
2005 state
2006 .transition("item2".to_string(), None, StateDiff::Addition)
2007 .is_err()
2008 );
2009
2010 let mut state = CatalogImplicationKind::Dropped("item".to_string(), "name".to_string());
2011 assert!(
2012 state
2013 .transition("item2".to_string(), None, StateDiff::Retraction)
2014 .is_err()
2015 );
2016 }
2017
2018 #[mz_ore::test]
2019 fn test_table_absorb_state_machine() {
2020 let table1 = create_test_table("table1");
2021 let table2 = create_test_table("table2");
2022
2023 let mut cmd = CatalogImplication::None;
2025 cmd.absorb_table(
2026 table1.clone(),
2027 Some("schema.table1".to_string()),
2028 StateDiff::Addition,
2029 );
2030 match &cmd {
2032 CatalogImplication::Table(state) => match state {
2033 CatalogImplicationKind::Added(t) => {
2034 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity())
2035 }
2036 _ => panic!("Expected Added state"),
2037 },
2038 _ => panic!("Expected Table command"),
2039 }
2040
2041 cmd.absorb_table(
2045 table2.clone(),
2046 Some("schema.table2".to_string()),
2047 StateDiff::Retraction,
2048 );
2049 match &cmd {
2050 CatalogImplication::Table(state) => match state {
2051 CatalogImplicationKind::Altered { prev, new } => {
2052 assert_eq!(prev.desc.latest().arity(), table2.desc.latest().arity());
2054 assert_eq!(new.desc.latest().arity(), table1.desc.latest().arity());
2055 }
2056 _ => panic!("Expected Altered state"),
2057 },
2058 _ => panic!("Expected Table command"),
2059 }
2060
2061 let mut cmd = CatalogImplication::None;
2063 cmd.absorb_table(
2064 table1.clone(),
2065 Some("schema.table1".to_string()),
2066 StateDiff::Retraction,
2067 );
2068 match &cmd {
2069 CatalogImplication::Table(state) => match state {
2070 CatalogImplicationKind::Dropped(t, name) => {
2071 assert_eq!(t.desc.latest().arity(), table1.desc.latest().arity());
2072 assert_eq!(name, "schema.table1");
2073 }
2074 _ => panic!("Expected Dropped state"),
2075 },
2076 _ => panic!("Expected Table command"),
2077 }
2078
2079 cmd.absorb_table(
2081 table2.clone(),
2082 Some("schema.table2".to_string()),
2083 StateDiff::Addition,
2084 );
2085 match &cmd {
2086 CatalogImplication::Table(state) => match state {
2087 CatalogImplicationKind::Altered { prev, new } => {
2088 assert_eq!(prev.desc.latest().arity(), table1.desc.latest().arity());
2090 assert_eq!(new.desc.latest().arity(), table2.desc.latest().arity());
2091 }
2092 _ => panic!("Expected Altered state"),
2093 },
2094 _ => panic!("Expected Table command"),
2095 }
2096 }
2097
2098 #[mz_ore::test]
2099 #[should_panic(expected = "Cannot add an already added object")]
2100 fn test_invalid_double_add() {
2101 let table = create_test_table("table");
2102 let mut cmd = CatalogImplication::None;
2103
2104 cmd.absorb_table(
2106 table.clone(),
2107 Some("schema.table".to_string()),
2108 StateDiff::Addition,
2109 );
2110
2111 cmd.absorb_table(
2113 table.clone(),
2114 Some("schema.table".to_string()),
2115 StateDiff::Addition,
2116 );
2117 }
2118
2119 #[mz_ore::test]
2120 #[should_panic(expected = "Cannot drop an already dropped object")]
2121 fn test_invalid_double_drop() {
2122 let table = create_test_table("table");
2123 let mut cmd = CatalogImplication::None;
2124
2125 cmd.absorb_table(
2127 table.clone(),
2128 Some("schema.table".to_string()),
2129 StateDiff::Retraction,
2130 );
2131
2132 cmd.absorb_table(
2134 table.clone(),
2135 Some("schema.table".to_string()),
2136 StateDiff::Retraction,
2137 );
2138 }
2139}