1use std::borrow::Cow;
13use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15use std::sync::atomic;
16use std::time::Duration;
17
18use itertools::Itertools;
19use mz_adapter_types::cluster_state::{
20 BurstAudit, BurstFinishCause, ExpectedClusterState, ReconfigurationAudit,
21};
22use mz_adapter_types::compaction::CompactionWindow;
23use mz_adapter_types::connection::ConnectionId;
24use mz_adapter_types::dyncfgs::{
25 ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT, WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL,
26 WITH_0DT_DEPLOYMENT_MAX_WAIT,
27};
28use mz_audit_log::{
29 AlterClusterReconfigurationV1, BurstFinishCauseV1, ClusterHydrationBurstV1,
30 ClusterReplicaLoggingV1, CreateOrDropClusterReplicaReasonV1, EventDetails, EventType,
31 HydrationBurstLifecycleV1, IdFullNameV1, IdNameV1, ObjectType, ReconfigurationLifecycleV1,
32 RefreshDecisionWithReasonV2, SchedulingDecisionV1, SchedulingDecisionsWithReasonsV2,
33 VersionedEvent,
34};
35use mz_catalog::SYSTEM_CONN_ID;
36use mz_catalog::builtin::BuiltinLog;
37use mz_catalog::durable::{DryRunTransaction, NetworkPolicy, Snapshot, Transaction};
38use mz_catalog::expr_cache::LocalExpressions;
39use mz_catalog::memory::error::{AmbiguousRename, Error, ErrorKind};
40use mz_catalog::memory::objects::{
41 CatalogEntry, CatalogItem, ClusterConfig, ClusterVariant, DataSourceDesc, DefaultPrivileges,
42 ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget, SourceReferences,
43};
44use mz_cluster_controller::ctx::RefreshWindowDecision;
45use mz_controller::clusters::{ManagedReplicaLocation, ReplicaConfig, ReplicaLocation};
46use mz_controller_types::{ClusterId, ReplicaId};
47use mz_ore::collections::HashSet;
48use mz_ore::{instrument, soft_assert_or_log};
49use mz_persist_types::ShardId;
50use mz_repr::adt::interval::Interval;
51use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap, merge_mz_acl_items};
52use mz_repr::network_policy_id::NetworkPolicyId;
53use mz_repr::optimize::OptimizerFeatures;
54use mz_repr::role_id::RoleId;
55use mz_repr::{CatalogItemId, ColumnName, GlobalId, SqlColumnType, strconv};
56use mz_sql::ast::RawDataType;
57use mz_sql::catalog::{
58 AutoProvisionSource, CatalogDatabase, CatalogError as SqlCatalogError,
59 CatalogItem as SqlCatalogItem, CatalogRole, CatalogSchema, DefaultPrivilegeAclItem,
60 DefaultPrivilegeObject, PasswordAction, PasswordConfig, RoleAttributesRaw, RoleMembership,
61 RoleVars,
62};
63use mz_sql::names::{
64 CommentObjectId, DatabaseId, FullItemName, ObjectId, QualifiedItemName,
65 ResolvedDatabaseSpecifier, SchemaId, SchemaSpecifier, SystemObjectId,
66};
67use mz_sql::plan::{NetworkPolicyRule, PlanError};
68use mz_sql::session::user::{MZ_SUPPORT_ROLE_ID, MZ_SYSTEM_ROLE_ID};
69use mz_sql::session::vars::OwnedVarInput;
70use mz_sql::session::vars::{Value as VarValue, VarInput};
71use mz_sql::{DEFAULT_SCHEMA, rbac};
72use mz_sql_parser::ast::{QualifiedReplica, Value};
73use mz_storage_client::storage_collections::StorageCollections;
74use serde::{Deserialize, Serialize};
75use tracing::{info, trace};
76use uuid::Uuid;
77
78use crate::AdapterError;
79use crate::catalog::state::LocalExpressionCache;
80use crate::catalog::{
81 BuiltinTableUpdate, Catalog, CatalogState, UpdatePrivilegeVariant,
82 catalog_type_to_audit_object_type, comment_id_to_audit_object_type, is_reserved_name,
83 is_reserved_role_name, object_type_to_audit_object_type,
84 system_object_type_to_audit_object_type,
85};
86use crate::config::{ScopedParameters, ScopedParametersScope};
87use crate::coord::ConnMeta;
88use crate::coord::catalog_implications::parsed_state_updates::ParsedStateUpdate;
89use crate::util::ResultExt;
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct InjectedAuditEvent {
97 pub event_type: EventType,
98 pub object_type: ObjectType,
99 pub details: EventDetails,
100 pub user: Option<String>,
101}
102
103#[derive(Debug, Clone)]
104pub enum Op {
105 AlterRetainHistory {
106 id: CatalogItemId,
107 value: Option<Value>,
108 window: CompactionWindow,
109 },
110 AlterSourceTimestampInterval {
111 id: CatalogItemId,
112 value: Option<Value>,
113 interval: Duration,
114 },
115 AlterRole {
116 id: RoleId,
117 name: String,
118 attributes: RoleAttributesRaw,
119 nopassword: bool,
120 vars: RoleVars,
121 },
122 AlterNetworkPolicy {
123 id: NetworkPolicyId,
124 rules: Vec<NetworkPolicyRule>,
125 name: String,
126 owner_id: RoleId,
127 },
128 AlterAddColumn {
129 id: CatalogItemId,
130 new_global_id: GlobalId,
131 name: ColumnName,
132 typ: SqlColumnType,
133 sql: RawDataType,
134 },
135 AlterMaterializedViewApplyReplacement {
136 id: CatalogItemId,
137 replacement_id: CatalogItemId,
138 },
139 CreateDatabase {
140 name: String,
141 owner_id: RoleId,
142 },
143 CreateSchema {
144 database_id: ResolvedDatabaseSpecifier,
145 schema_name: String,
146 owner_id: RoleId,
147 },
148 CreateRole {
149 name: String,
150 attributes: RoleAttributesRaw,
151 },
152 CreateCluster {
153 id: ClusterId,
154 name: String,
155 introspection_sources: Vec<&'static BuiltinLog>,
156 owner_id: RoleId,
157 config: ClusterConfig,
158 },
159 CreateClusterReplica {
160 cluster_id: ClusterId,
161 replica_id: ReplicaId,
162 name: String,
163 config: ReplicaConfig,
164 owner_id: RoleId,
165 reason: ReplicaCreateDropReason,
166 },
167 CreateItem {
168 id: CatalogItemId,
169 name: QualifiedItemName,
170 item: CatalogItem,
171 owner_id: RoleId,
172 },
173 CreateNetworkPolicy {
174 rules: Vec<NetworkPolicyRule>,
175 name: String,
176 owner_id: RoleId,
177 },
178 Comment {
179 object_id: CommentObjectId,
180 sub_component: Option<usize>,
181 comment: Option<String>,
182 },
183 DropObjects(Vec<DropObjectInfo>),
184 GrantRole {
185 role_id: RoleId,
186 member_id: RoleId,
187 grantor_id: RoleId,
188 },
189 RenameCluster {
190 id: ClusterId,
191 name: String,
192 to_name: String,
193 check_reserved_names: bool,
194 },
195 RenameClusterReplica {
196 cluster_id: ClusterId,
197 replica_id: ReplicaId,
198 name: QualifiedReplica,
199 to_name: String,
200 },
201 RenameItem {
202 id: CatalogItemId,
203 current_full_name: FullItemName,
204 to_name: String,
205 },
206 RenameSchema {
207 database_spec: ResolvedDatabaseSpecifier,
208 schema_spec: SchemaSpecifier,
209 new_name: String,
210 check_reserved_names: bool,
211 },
212 UpdateOwner {
213 id: ObjectId,
214 new_owner: RoleId,
215 },
216 UpdatePrivilege {
217 target_id: SystemObjectId,
218 privileges: Vec<MzAclItem>,
222 variant: UpdatePrivilegeVariant,
223 },
224 UpdateDefaultPrivilege {
225 privilege_object: DefaultPrivilegeObject,
226 privilege_acl_item: DefaultPrivilegeAclItem,
227 variant: UpdatePrivilegeVariant,
228 },
229 RevokeRole {
230 role_id: RoleId,
231 member_id: RoleId,
232 grantor_id: RoleId,
233 },
234 UpdateClusterConfig {
235 id: ClusterId,
236 name: String,
237 config: ClusterConfig,
238 reconfiguration_audit: Option<ReconfigurationAudit>,
241 burst_audit: Option<BurstAudit>,
244 },
245 UpdateClusterReplicaConfig {
246 cluster_id: ClusterId,
247 replica_id: ReplicaId,
248 config: ReplicaConfig,
249 },
250 UpdateItem {
251 id: CatalogItemId,
252 name: QualifiedItemName,
253 to_item: CatalogItem,
254 },
255 UpdateSourceReferences {
256 source_id: CatalogItemId,
257 references: SourceReferences,
258 },
259 UpdateSystemConfiguration {
260 name: String,
261 value: OwnedVarInput,
262 },
263 ResetSystemConfiguration {
264 name: String,
265 },
266 ResetAllSystemConfiguration,
267 UpdateScopedSystemParameters {
278 scoped: ScopedParameters,
279 prune_scope: ScopedParametersScope,
280 },
281 InjectAuditEvents {
287 events: Vec<InjectedAuditEvent>,
288 },
289 CheckClusterState {
296 cluster_id: ClusterId,
297 expected: ExpectedClusterState,
298 },
299}
300
301#[derive(Debug, Clone)]
305pub enum DropObjectInfo {
306 Cluster(ClusterId),
307 ClusterReplica((ClusterId, ReplicaId, ReplicaCreateDropReason)),
308 Database(DatabaseId),
309 Schema((ResolvedDatabaseSpecifier, SchemaSpecifier)),
310 Role(RoleId),
311 Item(CatalogItemId),
312 NetworkPolicy(NetworkPolicyId),
313}
314
315impl DropObjectInfo {
316 pub(crate) fn manual_drop_from_object_id(id: ObjectId) -> Self {
319 match id {
320 ObjectId::Cluster(cluster_id) => DropObjectInfo::Cluster(cluster_id),
321 ObjectId::ClusterReplica((cluster_id, replica_id)) => DropObjectInfo::ClusterReplica((
322 cluster_id,
323 replica_id,
324 ReplicaCreateDropReason::Manual,
325 )),
326 ObjectId::Database(database_id) => DropObjectInfo::Database(database_id),
327 ObjectId::Schema(schema) => DropObjectInfo::Schema(schema),
328 ObjectId::Role(role_id) => DropObjectInfo::Role(role_id),
329 ObjectId::Item(item_id) => DropObjectInfo::Item(item_id),
330 ObjectId::NetworkPolicy(policy_id) => DropObjectInfo::NetworkPolicy(policy_id),
331 }
332 }
333
334 fn to_object_id(&self) -> ObjectId {
337 match &self {
338 DropObjectInfo::Cluster(cluster_id) => ObjectId::Cluster(cluster_id.clone()),
339 DropObjectInfo::ClusterReplica((cluster_id, replica_id, _reason)) => {
340 ObjectId::ClusterReplica((cluster_id.clone(), replica_id.clone()))
341 }
342 DropObjectInfo::Database(database_id) => ObjectId::Database(database_id.clone()),
343 DropObjectInfo::Schema(schema) => ObjectId::Schema(schema.clone()),
344 DropObjectInfo::Role(role_id) => ObjectId::Role(role_id.clone()),
345 DropObjectInfo::Item(item_id) => ObjectId::Item(item_id.clone()),
346 DropObjectInfo::NetworkPolicy(network_policy_id) => {
347 ObjectId::NetworkPolicy(network_policy_id.clone())
348 }
349 }
350 }
351}
352
353#[derive(Debug, Clone)]
355pub enum ReplicaCreateDropReason {
356 Manual,
361 GracefulReconfiguration,
365 HydrationBurst,
368 OnRefresh(RefreshWindowDecision),
373 Retired,
377}
378
379impl ReplicaCreateDropReason {
380 pub fn into_audit_log(
381 self,
382 ) -> (
383 CreateOrDropClusterReplicaReasonV1,
384 Option<SchedulingDecisionsWithReasonsV2>,
385 ) {
386 match self {
387 ReplicaCreateDropReason::Manual => (CreateOrDropClusterReplicaReasonV1::Manual, None),
388 ReplicaCreateDropReason::GracefulReconfiguration => {
389 (CreateOrDropClusterReplicaReasonV1::Reconfiguration, None)
390 }
391 ReplicaCreateDropReason::HydrationBurst => {
392 (CreateOrDropClusterReplicaReasonV1::HydrationBurst, None)
393 }
394 ReplicaCreateDropReason::OnRefresh(decision) => (
395 CreateOrDropClusterReplicaReasonV1::Schedule,
396 Some(refresh_window_decision_to_audit_log(decision)),
397 ),
398 ReplicaCreateDropReason::Retired => (CreateOrDropClusterReplicaReasonV1::Retired, None),
399 }
400 }
401}
402
403fn refresh_window_decision_to_audit_log(
407 decision: RefreshWindowDecision,
408) -> SchedulingDecisionsWithReasonsV2 {
409 let mut hydration_time_estimate_str = String::new();
410 strconv::format_interval(
411 &mut hydration_time_estimate_str,
412 Interval::from_duration(&decision.hydration_time_estimate)
413 .expect("the estimate originated as a planned Interval"),
414 );
415 SchedulingDecisionsWithReasonsV2 {
416 on_refresh: RefreshDecisionWithReasonV2 {
417 decision: SchedulingDecisionV1::On,
421 objects_needing_refresh: decision
422 .objects_needing_refresh
423 .iter()
424 .map(|id| id.to_string())
425 .collect(),
426 objects_needing_compaction: decision
427 .objects_needing_compaction
428 .iter()
429 .map(|id| id.to_string())
430 .collect(),
431 hydration_time_estimate: hydration_time_estimate_str,
432 },
433 }
434}
435
436pub struct TransactionResult {
437 pub builtin_table_updates: Vec<BuiltinTableUpdate>,
438 pub catalog_updates: Vec<ParsedStateUpdate>,
440 pub audit_events: Vec<VersionedEvent>,
441}
442
443#[derive(Debug, Clone, Copy)]
444enum TransactInnerMode {
445 Commit,
449 DryRun,
456}
457
458impl Catalog {
459 fn should_audit_log_item(item: &CatalogItem) -> bool {
460 !item.is_temporary()
461 }
462
463 fn reconfiguration_record_of(
465 config: &ClusterConfig,
466 ) -> Option<&mz_catalog::memory::objects::ReconfigurationState> {
467 match &config.variant {
468 ClusterVariant::Managed(managed) => managed.reconfiguration.as_ref(),
469 ClusterVariant::Unmanaged => None,
470 }
471 }
472
473 fn burst_record_of(config: &ClusterConfig) -> Option<&mz_catalog::memory::objects::BurstState> {
475 match &config.variant {
476 ClusterVariant::Managed(managed) => managed.burst.as_ref(),
477 ClusterVariant::Unmanaged => None,
478 }
479 }
480
481 fn burst_lifecycle_moved(old_config: &ClusterConfig, new_config: &ClusterConfig) -> bool {
489 matches!(
490 (
491 Self::burst_record_of(old_config),
492 Self::burst_record_of(new_config),
493 ),
494 (None, Some(_)) | (Some(_), None)
495 )
496 }
497
498 fn reconfiguration_lifecycle_moved(
506 old_config: &ClusterConfig,
507 new_config: &ClusterConfig,
508 ) -> bool {
509 match (
510 Self::reconfiguration_record_of(old_config),
511 Self::reconfiguration_record_of(new_config),
512 ) {
513 (None, Some(_)) => true,
514 (Some(old), Some(new)) => old.status != new.status,
515 (Some(old), None) => old.is_in_progress(),
516 (None, None) => false,
517 }
518 }
519
520 fn reconfiguration_audit_details(
529 config: &ClusterConfig,
530 cluster_id: ClusterId,
531 cluster_name: &str,
532 audit: ReconfigurationAudit,
533 ) -> Result<AlterClusterReconfigurationV1, AdapterError> {
534 let record = Self::reconfiguration_record_of(config);
535 let Some(record) = record else {
536 return Err(AdapterError::Internal(format!(
537 "reconfiguration audit transition {audit:?} for cluster {cluster_name} \
538 without a reconfiguration record"
539 )));
540 };
541 let (transition, forced) = match audit {
545 ReconfigurationAudit::Started => (ReconfigurationLifecycleV1::Started, None),
546 ReconfigurationAudit::Cancelled => (ReconfigurationLifecycleV1::Cancelled, None),
547 ReconfigurationAudit::Finalized { forced } => {
548 (ReconfigurationLifecycleV1::Finalized, Some(forced))
549 }
550 ReconfigurationAudit::TimedOut => (ReconfigurationLifecycleV1::TimedOut, None),
551 ReconfigurationAudit::ResourceExhausted => {
552 (ReconfigurationLifecycleV1::ResourceExhausted, None)
553 }
554 };
555 let coherent = matches!(
556 (audit, record.status),
557 (
558 ReconfigurationAudit::Started,
559 ReconfigurationStatus::InProgress
560 ) | (
561 ReconfigurationAudit::Cancelled,
562 ReconfigurationStatus::Cancelled
563 ) | (
564 ReconfigurationAudit::Finalized { .. },
565 ReconfigurationStatus::Finalized
566 ) | (
567 ReconfigurationAudit::TimedOut,
568 ReconfigurationStatus::TimedOut
569 ) | (
570 ReconfigurationAudit::ResourceExhausted,
571 ReconfigurationStatus::ResourceExhausted
572 )
573 );
574 if !coherent {
575 return Err(AdapterError::Internal(format!(
576 "reconfiguration audit transition {audit:?} for cluster {cluster_name} \
577 contradicts the written record status {:?}",
578 record.status
579 )));
580 }
581 let ReconfigurationState {
582 target,
583 deadline,
584 on_timeout: _,
585 status: _,
586 } = record;
587 let ReconfigurationTarget {
588 size,
589 replication_factor,
590 availability_zones,
591 logging,
592 arrangement_compression: _,
596 } = target;
597 Ok(AlterClusterReconfigurationV1 {
598 cluster_id: cluster_id.to_string(),
599 cluster_name: cluster_name.to_string(),
600 transition,
601 forced,
602 target_size: size.clone(),
603 target_replication_factor: *replication_factor,
604 target_availability_zones: availability_zones.clone(),
605 target_logging: ClusterReplicaLoggingV1 {
606 log_logging: logging.log_logging,
607 interval: logging.interval,
608 },
609 deadline: Some((*deadline).into()),
610 })
611 }
612
613 fn burst_audit_details(
622 old_config: &ClusterConfig,
623 new_config: &ClusterConfig,
624 cluster_id: ClusterId,
625 cluster_name: &str,
626 audit: BurstAudit,
627 ) -> Result<ClusterHydrationBurstV1, AdapterError> {
628 let (transition, finish_cause, record) = match audit {
629 BurstAudit::Started => (
630 HydrationBurstLifecycleV1::Started,
631 None,
632 Self::burst_record_of(new_config),
633 ),
634 BurstAudit::Finished { cause } => {
635 let cause = match cause {
636 BurstFinishCause::LingerElapsed => BurstFinishCauseV1::LingerElapsed,
637 BurstFinishCause::NoLongerWarranted => BurstFinishCauseV1::NoLongerWarranted,
638 };
639 (
640 HydrationBurstLifecycleV1::Finished,
641 Some(cause),
642 Self::burst_record_of(old_config),
643 )
644 }
645 };
646 let Some(record) = record else {
647 return Err(AdapterError::Internal(format!(
648 "burst audit transition {audit:?} for cluster {cluster_name} \
649 without a burst record on the corresponding side of the write"
650 )));
651 };
652 Ok(ClusterHydrationBurstV1 {
653 cluster_id: cluster_id.to_string(),
654 cluster_name: cluster_name.to_string(),
655 transition,
656 finish_cause,
657 burst_size: record.burst_size.clone(),
658 })
659 }
660
661 fn temporary_ids(
664 &self,
665 ops: &[Op],
666 temporary_drops: BTreeSet<(&ConnectionId, String)>,
667 ) -> Result<BTreeSet<CatalogItemId>, Error> {
668 let mut creating = BTreeSet::new();
669 let mut temporary_ids = BTreeSet::new();
670 for op in ops.iter() {
671 if let Op::CreateItem {
672 id,
673 name,
674 item,
675 owner_id: _,
676 } = op
677 {
678 if let Some(conn_id) = item.conn_id() {
679 if self.item_exists_in_temp_schemas(conn_id, &name.item)
680 && !temporary_drops.contains(&(conn_id, name.item.clone()))
681 || creating.contains(&(conn_id, &name.item))
682 {
683 return Err(
684 SqlCatalogError::ItemAlreadyExists(*id, name.item.clone()).into()
685 );
686 } else {
687 creating.insert((conn_id, &name.item));
688 temporary_ids.insert(id.clone());
689 }
690 }
691 }
692 }
693 Ok(temporary_ids)
694 }
695
696 #[instrument(name = "catalog::transact")]
697 pub async fn transact(
698 &mut self,
699 storage_collections: Option<&mut Arc<dyn StorageCollections + Send + Sync>>,
702 oracle_write_ts: mz_repr::Timestamp,
703 session: Option<&ConnMeta>,
704 ops: Vec<Op>,
705 ) -> Result<TransactionResult, AdapterError> {
706 trace!("transact: {:?}", ops);
707 fail::fail_point!("catalog_transact", |arg| {
708 Err(AdapterError::Unstructured(anyhow::anyhow!(
709 "failpoint: {arg:?}"
710 )))
711 });
712
713 let drop_ids: BTreeSet<CatalogItemId> = ops
714 .iter()
715 .filter_map(|op| match op {
716 Op::DropObjects(drop_object_infos) => {
717 let ids = drop_object_infos.iter().map(|info| info.to_object_id());
718 let item_ids = ids.filter_map(|id| match id {
719 ObjectId::Item(id) => Some(id),
720 _ => None,
721 });
722 Some(item_ids)
723 }
724 _ => None,
725 })
726 .flatten()
727 .collect();
728 let temporary_drops = drop_ids
729 .iter()
730 .filter_map(|id| {
731 let entry = self.get_entry(id);
732 match entry.item().conn_id() {
733 Some(conn_id) => Some((conn_id, entry.name().item.clone())),
734 None => None,
735 }
736 })
737 .collect();
738
739 let temporary_ids = self.temporary_ids(&ops, temporary_drops)?;
740 let mut builtin_table_updates = vec![];
741 let mut catalog_updates = vec![];
742 let mut audit_events = vec![];
743 let mut storage = self.storage().await;
744 let mut tx = storage
745 .transaction()
746 .await
747 .unwrap_or_terminate("starting catalog transaction");
748 let commit_ts = std::cmp::max(oracle_write_ts, tx.upper());
750
751 let new_state = Self::transact_inner(
752 TransactInnerMode::Commit,
753 storage_collections,
754 commit_ts,
755 session,
756 ops,
757 temporary_ids,
758 &mut builtin_table_updates,
759 &mut catalog_updates,
760 &mut audit_events,
761 &mut tx,
762 &self.state,
763 )
764 .await?;
765
766 tx.commit(commit_ts)
771 .await
772 .unwrap_or_terminate("catalog storage transaction commit must succeed");
773
774 drop(storage);
777 if let Some(new_state) = new_state {
778 self.transient_revision += 1;
779 self.shared_transient_revision
785 .store(self.transient_revision, atomic::Ordering::SeqCst);
786 self.state = new_state;
787 }
788
789 Ok(TransactionResult {
790 builtin_table_updates,
791 catalog_updates,
792 audit_events,
793 })
794 }
795
796 pub async fn transact_incremental_dry_run(
811 &self,
812 base_state: &CatalogState,
813 ops: Vec<Op>,
814 session: Option<&ConnMeta>,
815 prev_snapshot: Option<Snapshot>,
816 oracle_write_ts: mz_repr::Timestamp,
817 ) -> Result<(CatalogState, Snapshot), AdapterError> {
818 let temporary_ids = self.temporary_ids(&ops, BTreeSet::new())?;
821
822 let mut builtin_table_updates = vec![];
823 let mut catalog_updates = vec![];
824 let mut audit_events = vec![];
825 let mut storage = self.storage().await;
826 let mut tx = if let Some(snapshot) = prev_snapshot {
827 storage
830 .transaction_from_snapshot(snapshot)
831 .unwrap_or_terminate("starting catalog transaction from snapshot")
832 } else {
833 let tx = storage
836 .transaction()
837 .await
838 .unwrap_or_terminate("starting catalog transaction");
839 DryRunTransaction::new(tx)
840 };
841
842 let new_state = Self::transact_inner(
844 TransactInnerMode::DryRun,
845 None,
846 oracle_write_ts,
847 session,
848 ops,
849 temporary_ids,
850 &mut builtin_table_updates,
851 &mut catalog_updates,
852 &mut audit_events,
853 tx.transaction_mut(),
854 base_state,
855 )
856 .await?;
857
858 let new_snapshot = tx.current_snapshot();
861
862 drop(storage);
864
865 let state = new_state.unwrap_or_else(|| base_state.clone());
867 Ok((state, new_snapshot))
868 }
869
870 fn extract_expressions_from_ops(
874 ops: &[Op],
875 optimizer_features: &OptimizerFeatures,
876 ) -> BTreeMap<GlobalId, LocalExpressions> {
877 let mut exprs = BTreeMap::new();
878
879 for op in ops {
880 if let Op::CreateItem { item, .. } = op {
881 match item {
882 CatalogItem::View(view) => {
883 exprs.insert(
884 view.global_id,
885 LocalExpressions {
886 local_mir: (*view.locally_optimized_expr).clone(),
887 optimizer_features: optimizer_features.clone(),
888 },
889 );
890 }
891 CatalogItem::MaterializedView(mv) => {
892 exprs.insert(
893 mv.global_id_writes(),
894 LocalExpressions {
895 local_mir: (*mv.locally_optimized_expr).clone(),
896 optimizer_features: optimizer_features.clone(),
897 },
898 );
899 }
900 CatalogItem::Table(_)
901 | CatalogItem::Source(_)
902 | CatalogItem::Log(_)
903 | CatalogItem::Sink(_)
904 | CatalogItem::Index(_)
905 | CatalogItem::Type(_)
906 | CatalogItem::Func(_)
907 | CatalogItem::Secret(_)
908 | CatalogItem::Connection(_)
909 | CatalogItem::MetricSink(_) => {}
910 }
911 }
912 }
913
914 exprs
915 }
916
917 #[instrument(name = "catalog::transact_inner")]
925 async fn transact_inner(
926 mode: TransactInnerMode,
927 storage_collections: Option<&mut Arc<dyn StorageCollections + Send + Sync>>,
928 oracle_write_ts: mz_repr::Timestamp,
929 session: Option<&ConnMeta>,
930 ops: Vec<Op>,
931 temporary_ids: BTreeSet<CatalogItemId>,
932 builtin_table_updates: &mut Vec<BuiltinTableUpdate>,
933 parsed_catalog_updates: &mut Vec<ParsedStateUpdate>,
934 audit_events: &mut Vec<VersionedEvent>,
935 tx: &mut Transaction<'_>,
936 state: &CatalogState,
937 ) -> Result<Option<CatalogState>, AdapterError> {
938 let mut preliminary_state = Cow::Borrowed(state);
965
966 let mut state = Cow::Borrowed(state);
968
969 if ops.is_empty() {
970 return Ok(None);
971 }
972
973 let optimizer_features = OptimizerFeatures::from(state.system_config());
976 let cached_exprs = Self::extract_expressions_from_ops(&ops, &optimizer_features);
977
978 let mut storage_collections_to_create = BTreeSet::new();
979 let mut storage_collections_to_drop = BTreeSet::new();
980 let mut storage_collections_to_register = BTreeMap::new();
981
982 let mut updates = Vec::new();
983
984 for op in ops {
985 Self::transact_op(
986 oracle_write_ts,
987 session,
988 op,
989 &temporary_ids,
990 audit_events,
991 tx,
992 &*preliminary_state,
993 &mut storage_collections_to_create,
994 &mut storage_collections_to_drop,
995 &mut storage_collections_to_register,
996 )
997 .await?;
998
999 let mut op_updates: Vec<_> = tx.get_and_commit_op_updates();
1000 if !op_updates.is_empty() {
1001 let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
1004 let (_op_builtin_table_updates, _op_catalog_updates) = preliminary_state
1005 .to_mut()
1006 .apply_updates(op_updates.clone(), &mut local_expr_cache)
1007 .await;
1008 }
1009 updates.append(&mut op_updates);
1010 }
1011
1012 if !updates.is_empty() {
1013 let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
1014 let (op_builtin_table_updates, op_catalog_updates) = state
1015 .to_mut()
1016 .apply_updates(updates.clone(), &mut local_expr_cache)
1017 .await;
1018 let op_builtin_table_updates = state
1019 .to_mut()
1020 .resolve_builtin_table_updates(op_builtin_table_updates);
1021 builtin_table_updates.extend(op_builtin_table_updates);
1022 parsed_catalog_updates.extend(op_catalog_updates);
1023 }
1024
1025 match mode {
1026 TransactInnerMode::Commit => {
1027 if let Some(c) = storage_collections {
1029 c.prepare_state(
1030 tx,
1031 storage_collections_to_create,
1032 storage_collections_to_drop,
1033 storage_collections_to_register,
1034 )
1035 .await?;
1036 }
1037 }
1038 TransactInnerMode::DryRun => {
1039 mz_ore::soft_assert_no_log!(
1040 storage_collections.is_none(),
1041 "dry-run mode must not prepare storage state"
1042 );
1043 }
1044 }
1045
1046 let updates = tx.get_and_commit_op_updates();
1047 if !updates.is_empty() {
1048 let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
1049 let (op_builtin_table_updates, op_catalog_updates) = state
1050 .to_mut()
1051 .apply_updates(updates.clone(), &mut local_expr_cache)
1052 .await;
1053 let op_builtin_table_updates = state
1054 .to_mut()
1055 .resolve_builtin_table_updates(op_builtin_table_updates);
1056 builtin_table_updates.extend(op_builtin_table_updates);
1057 parsed_catalog_updates.extend(op_catalog_updates);
1058 }
1059
1060 match state {
1061 Cow::Owned(state) => Ok(Some(state)),
1062 Cow::Borrowed(_) => Ok(None),
1063 }
1064 }
1065
1066 #[instrument]
1070 async fn transact_op(
1071 oracle_write_ts: mz_repr::Timestamp,
1072 session: Option<&ConnMeta>,
1073 op: Op,
1074 temporary_ids: &BTreeSet<CatalogItemId>,
1075 audit_events: &mut Vec<VersionedEvent>,
1076 tx: &mut Transaction<'_>,
1077 state: &CatalogState,
1078 storage_collections_to_create: &mut BTreeSet<GlobalId>,
1079 storage_collections_to_drop: &mut BTreeSet<GlobalId>,
1080 storage_collections_to_register: &mut BTreeMap<GlobalId, ShardId>,
1081 ) -> Result<(), AdapterError> {
1082 match op {
1083 Op::CheckClusterState {
1084 cluster_id,
1085 expected,
1086 } => {
1087 if !crate::catalog::cluster_state::cluster_matches_expected(
1091 state, cluster_id, &expected,
1092 ) {
1093 return Err(AdapterError::ClusterStateChanged { cluster_id });
1094 }
1095 }
1096 Op::AlterRetainHistory { id, value, window } => {
1097 let entry = state.get_entry(&id);
1098 if id.is_system() {
1099 let name = entry.name();
1100 let full_name =
1101 state.resolve_full_name(name, session.map(|session| session.conn_id()));
1102 return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
1103 full_name.to_string(),
1104 ))));
1105 }
1106
1107 let mut new_entry = entry.clone();
1108 let previous = new_entry
1109 .item
1110 .update_retain_history(value.clone(), window)
1111 .map_err(|_| {
1112 AdapterError::Catalog(Error::new(ErrorKind::Internal(
1113 "planner should have rejected invalid alter retain history item type"
1114 .to_string(),
1115 )))
1116 })?;
1117
1118 if Self::should_audit_log_item(new_entry.item()) {
1119 let details =
1120 EventDetails::AlterRetainHistoryV1(mz_audit_log::AlterRetainHistoryV1 {
1121 id: id.to_string(),
1122 old_history: previous.map(|previous| previous.to_string()),
1123 new_history: value.map(|v| v.to_string()),
1124 });
1125 CatalogState::add_to_audit_log(
1126 &state.system_configuration,
1127 oracle_write_ts,
1128 session,
1129 tx,
1130 audit_events,
1131 EventType::Alter,
1132 catalog_type_to_audit_object_type(new_entry.item().typ()),
1133 details,
1134 )?;
1135 }
1136
1137 tx.update_item(id, state.durable_item(new_entry)?)?;
1138
1139 Self::log_update(state, &id);
1140 }
1141 Op::AlterSourceTimestampInterval {
1142 id,
1143 value,
1144 interval,
1145 } => {
1146 let entry = state.get_entry(&id);
1147 if id.is_system() {
1148 let name = entry.name();
1149 let full_name =
1150 state.resolve_full_name(name, session.map(|session| session.conn_id()));
1151 return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
1152 full_name.to_string(),
1153 ))));
1154 }
1155
1156 let mut new_entry = entry.clone();
1157 let previous = new_entry
1158 .item
1159 .update_timestamp_interval(value.clone(), interval)
1160 .map_err(|_| {
1161 AdapterError::Catalog(Error::new(ErrorKind::Internal(
1162 "planner should have rejected invalid alter timestamp interval item type"
1163 .to_string(),
1164 )))
1165 })?;
1166
1167 if Self::should_audit_log_item(new_entry.item()) {
1168 let details = EventDetails::AlterSourceTimestampIntervalV1(
1169 mz_audit_log::AlterSourceTimestampIntervalV1 {
1170 id: id.to_string(),
1171 old_interval: previous.map(|previous| previous.to_string()),
1172 new_interval: value.map(|v| v.to_string()),
1173 },
1174 );
1175 CatalogState::add_to_audit_log(
1176 &state.system_configuration,
1177 oracle_write_ts,
1178 session,
1179 tx,
1180 audit_events,
1181 EventType::Alter,
1182 catalog_type_to_audit_object_type(new_entry.item().typ()),
1183 details,
1184 )?;
1185 }
1186
1187 tx.update_item(id, state.durable_item(new_entry)?)?;
1188
1189 Self::log_update(state, &id);
1190 }
1191 Op::AlterRole {
1192 id,
1193 name,
1194 attributes,
1195 nopassword,
1196 vars,
1197 } => {
1198 state.ensure_not_reserved_role(&id)?;
1199
1200 let mut existing_role = state.get_role(&id).clone();
1201 let password = attributes.password.clone();
1202 let scram_iterations = attributes
1203 .scram_iterations
1204 .unwrap_or_else(|| state.system_config().scram_iterations());
1205 existing_role.attributes = attributes.into();
1206 existing_role.vars = vars;
1207 let password_action = if nopassword {
1208 PasswordAction::Clear
1209 } else if let Some(password) = password {
1210 PasswordAction::Set(PasswordConfig {
1211 password,
1212 scram_iterations,
1213 })
1214 } else {
1215 PasswordAction::NoChange
1216 };
1217 tx.update_role(id, existing_role.into(), password_action)?;
1218
1219 CatalogState::add_to_audit_log(
1220 &state.system_configuration,
1221 oracle_write_ts,
1222 session,
1223 tx,
1224 audit_events,
1225 EventType::Alter,
1226 ObjectType::Role,
1227 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1228 id: id.to_string(),
1229 name: name.clone(),
1230 }),
1231 )?;
1232
1233 info!("update role {name} ({id})");
1234 }
1235 Op::AlterNetworkPolicy {
1236 id,
1237 rules,
1238 name,
1239 owner_id: _owner_id,
1240 } => {
1241 let existing_policy = state.get_network_policy(&id).clone();
1242 let mut policy: NetworkPolicy = existing_policy.into();
1243 policy.rules = rules;
1244 if is_reserved_name(&name) {
1245 return Err(AdapterError::Catalog(Error::new(
1246 ErrorKind::ReservedNetworkPolicyName(name),
1247 )));
1248 }
1249 tx.update_network_policy(id, policy.clone())?;
1250
1251 CatalogState::add_to_audit_log(
1252 &state.system_configuration,
1253 oracle_write_ts,
1254 session,
1255 tx,
1256 audit_events,
1257 EventType::Alter,
1258 ObjectType::NetworkPolicy,
1259 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1260 id: id.to_string(),
1261 name: name.clone(),
1262 }),
1263 )?;
1264
1265 info!("update network policy {name} ({id})");
1266 }
1267 Op::AlterAddColumn {
1268 id,
1269 new_global_id,
1270 name,
1271 typ,
1272 sql,
1273 } => {
1274 let column_name = name.to_string();
1275 let column_type = typ.to_string();
1276 let nullable = typ.nullable;
1277 let mut new_entry = state.get_entry(&id).clone();
1278 let version = new_entry.item.add_column(name, typ, sql)?;
1279 let shard_id = state
1282 .storage_metadata()
1283 .get_collection_shard(new_entry.latest_global_id())?;
1284
1285 let CatalogItem::Table(table) = &mut new_entry.item else {
1287 return Err(AdapterError::Unsupported("adding columns to non-Table"));
1288 };
1289 table.collections.insert(version, new_global_id);
1290
1291 if Self::should_audit_log_item(new_entry.item()) {
1292 let details = EventDetails::AlterAddColumnV1(mz_audit_log::AlterAddColumnV1 {
1293 id: id.to_string(),
1294 column: column_name,
1295 column_type,
1296 nullable,
1297 });
1298 CatalogState::add_to_audit_log(
1299 &state.system_configuration,
1300 oracle_write_ts,
1301 session,
1302 tx,
1303 audit_events,
1304 EventType::Alter,
1305 catalog_type_to_audit_object_type(new_entry.item().typ()),
1306 details,
1307 )?;
1308 }
1309
1310 tx.update_item(id, state.durable_item(new_entry)?)?;
1311 storage_collections_to_register.insert(new_global_id, shard_id);
1312 }
1313 Op::AlterMaterializedViewApplyReplacement { id, replacement_id } => {
1314 let mut new_entry = state.get_entry(&id).clone();
1315 let replacement = state.get_entry(&replacement_id);
1316
1317 let new_audit_events =
1318 apply_replacement_audit_events(state, &new_entry, replacement);
1319
1320 let CatalogItem::MaterializedView(mv) = &mut new_entry.item else {
1321 return Err(AdapterError::internal(
1322 "ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT",
1323 "id must refer to a materialized view",
1324 ));
1325 };
1326 let CatalogItem::MaterializedView(replacement_mv) = &replacement.item else {
1327 return Err(AdapterError::internal(
1328 "ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT",
1329 "replacement_id must refer to a materialized view",
1330 ));
1331 };
1332
1333 mv.apply_replacement(replacement_mv.clone());
1334
1335 tx.remove_item(replacement_id)?;
1336
1337 new_entry.id = replacement_id;
1338 tx_replace_item(tx, state, id, new_entry)?;
1339
1340 let comment_id = CommentObjectId::MaterializedView(replacement_id);
1341 tx.drop_comments(&[comment_id].into())?;
1342
1343 for (event_type, details) in new_audit_events {
1344 CatalogState::add_to_audit_log(
1345 &state.system_configuration,
1346 oracle_write_ts,
1347 session,
1348 tx,
1349 audit_events,
1350 event_type,
1351 ObjectType::MaterializedView,
1352 details,
1353 )?;
1354 }
1355 }
1356 Op::CreateDatabase { name, owner_id } => {
1357 let database_owner_privileges = vec![rbac::owner_privilege(
1358 mz_sql::catalog::ObjectType::Database,
1359 owner_id,
1360 )];
1361 let database_default_privileges = state
1362 .default_privileges
1363 .get_applicable_privileges(
1364 owner_id,
1365 None,
1366 None,
1367 mz_sql::catalog::ObjectType::Database,
1368 )
1369 .map(|item| item.mz_acl_item(owner_id));
1370 let database_privileges: Vec<_> = merge_mz_acl_items(
1371 database_owner_privileges
1372 .into_iter()
1373 .chain(database_default_privileges),
1374 )
1375 .collect();
1376
1377 let schema_owner_privileges = vec![rbac::owner_privilege(
1378 mz_sql::catalog::ObjectType::Schema,
1379 owner_id,
1380 )];
1381 let schema_default_privileges = state
1382 .default_privileges
1383 .get_applicable_privileges(
1384 owner_id,
1385 None,
1386 None,
1387 mz_sql::catalog::ObjectType::Schema,
1388 )
1389 .map(|item| item.mz_acl_item(owner_id))
1390 .chain(std::iter::once(MzAclItem {
1392 grantee: RoleId::Public,
1393 grantor: owner_id,
1394 acl_mode: AclMode::USAGE,
1395 }));
1396 let schema_privileges: Vec<_> = merge_mz_acl_items(
1397 schema_owner_privileges
1398 .into_iter()
1399 .chain(schema_default_privileges),
1400 )
1401 .collect();
1402
1403 let temporary_oids: HashSet<_> = state.get_temporary_oids().collect();
1404 let (database_id, _) = tx.insert_user_database(
1405 &name,
1406 owner_id,
1407 database_privileges.clone(),
1408 &temporary_oids,
1409 )?;
1410 let (schema_id, _) = tx.insert_user_schema(
1411 database_id,
1412 DEFAULT_SCHEMA,
1413 owner_id,
1414 schema_privileges.clone(),
1415 &temporary_oids,
1416 )?;
1417 CatalogState::add_to_audit_log(
1418 &state.system_configuration,
1419 oracle_write_ts,
1420 session,
1421 tx,
1422 audit_events,
1423 EventType::Create,
1424 ObjectType::Database,
1425 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1426 id: database_id.to_string(),
1427 name: name.clone(),
1428 }),
1429 )?;
1430 info!("create database {}", name);
1431
1432 CatalogState::add_to_audit_log(
1433 &state.system_configuration,
1434 oracle_write_ts,
1435 session,
1436 tx,
1437 audit_events,
1438 EventType::Create,
1439 ObjectType::Schema,
1440 EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
1441 id: schema_id.to_string(),
1442 name: DEFAULT_SCHEMA.to_string(),
1443 database_name: Some(name),
1444 }),
1445 )?;
1446 }
1447 Op::CreateSchema {
1448 database_id,
1449 schema_name,
1450 owner_id,
1451 } => {
1452 if is_reserved_name(&schema_name) {
1453 return Err(AdapterError::Catalog(Error::new(
1454 ErrorKind::ReservedSchemaName(schema_name),
1455 )));
1456 }
1457 let database_id = match database_id {
1458 ResolvedDatabaseSpecifier::Id(id) => id,
1459 ResolvedDatabaseSpecifier::Ambient => {
1460 return Err(AdapterError::Catalog(Error::new(
1461 ErrorKind::ReadOnlySystemSchema(schema_name),
1462 )));
1463 }
1464 };
1465 let owner_privileges = vec![rbac::owner_privilege(
1466 mz_sql::catalog::ObjectType::Schema,
1467 owner_id,
1468 )];
1469 let default_privileges = state
1470 .default_privileges
1471 .get_applicable_privileges(
1472 owner_id,
1473 Some(database_id),
1474 None,
1475 mz_sql::catalog::ObjectType::Schema,
1476 )
1477 .map(|item| item.mz_acl_item(owner_id));
1478 let privileges: Vec<_> =
1479 merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1480 .collect();
1481 let (schema_id, _) = tx.insert_user_schema(
1482 database_id,
1483 &schema_name,
1484 owner_id,
1485 privileges.clone(),
1486 &state.get_temporary_oids().collect(),
1487 )?;
1488 CatalogState::add_to_audit_log(
1489 &state.system_configuration,
1490 oracle_write_ts,
1491 session,
1492 tx,
1493 audit_events,
1494 EventType::Create,
1495 ObjectType::Schema,
1496 EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
1497 id: schema_id.to_string(),
1498 name: schema_name.clone(),
1499 database_name: Some(state.database_by_id[&database_id].name.clone()),
1500 }),
1501 )?;
1502 }
1503 Op::CreateRole { name, attributes } => {
1504 if is_reserved_role_name(&name) {
1505 return Err(AdapterError::Catalog(Error::new(
1506 ErrorKind::ReservedRoleName(name),
1507 )));
1508 }
1509 let membership = RoleMembership::new();
1510 let vars = RoleVars::default();
1511 let (id, _) = tx.insert_user_role(
1512 name.clone(),
1513 attributes.clone(),
1514 membership.clone(),
1515 vars.clone(),
1516 &state.get_temporary_oids().collect(),
1517 )?;
1518 CatalogState::add_to_audit_log(
1519 &state.system_configuration,
1520 oracle_write_ts,
1521 session,
1522 tx,
1523 audit_events,
1524 EventType::Create,
1525 ObjectType::Role,
1526 EventDetails::CreateRoleV1(mz_audit_log::CreateRoleV1 {
1527 id: id.to_string(),
1528 name: name.clone(),
1529 auto_provision_source: attributes.auto_provision_source.map(|s| match s {
1530 AutoProvisionSource::Oidc => "oidc".to_string(),
1531 AutoProvisionSource::Frontegg => "frontegg".to_string(),
1532 AutoProvisionSource::None => "none".to_string(),
1533 }),
1534 }),
1535 )?;
1536 info!("create role {}", name);
1537 }
1538 Op::CreateCluster {
1539 id,
1540 name,
1541 introspection_sources,
1542 owner_id,
1543 config,
1544 } => {
1545 if is_reserved_name(&name) {
1546 return Err(AdapterError::Catalog(Error::new(
1547 ErrorKind::ReservedClusterName(name),
1548 )));
1549 }
1550 let owner_privileges = vec![rbac::owner_privilege(
1551 mz_sql::catalog::ObjectType::Cluster,
1552 owner_id,
1553 )];
1554 let default_privileges = state
1555 .default_privileges
1556 .get_applicable_privileges(
1557 owner_id,
1558 None,
1559 None,
1560 mz_sql::catalog::ObjectType::Cluster,
1561 )
1562 .map(|item| item.mz_acl_item(owner_id));
1563 let privileges: Vec<_> =
1564 merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1565 .collect();
1566 let introspection_source_ids: Vec<_> = introspection_sources
1567 .iter()
1568 .map(|introspection_source| {
1569 Transaction::allocate_introspection_source_index_id(
1570 &id,
1571 introspection_source.variant,
1572 )
1573 })
1574 .collect();
1575
1576 let introspection_sources = introspection_sources
1577 .into_iter()
1578 .zip_eq(introspection_source_ids)
1579 .map(|(log, (item_id, gid))| (log, item_id, gid))
1580 .collect();
1581
1582 tx.insert_user_cluster(
1583 id,
1584 &name,
1585 introspection_sources,
1586 owner_id,
1587 privileges.clone(),
1588 config.clone().into(),
1589 &state.get_temporary_oids().collect(),
1590 )?;
1591 CatalogState::add_to_audit_log(
1592 &state.system_configuration,
1593 oracle_write_ts,
1594 session,
1595 tx,
1596 audit_events,
1597 EventType::Create,
1598 ObjectType::Cluster,
1599 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1600 id: id.to_string(),
1601 name: name.clone(),
1602 }),
1603 )?;
1604 info!("create cluster {}", name);
1605 }
1606 Op::CreateClusterReplica {
1607 cluster_id,
1608 replica_id,
1609 name,
1610 config,
1611 owner_id,
1612 reason,
1613 } => {
1614 if is_reserved_name(&name) {
1615 return Err(AdapterError::Catalog(Error::new(
1616 ErrorKind::ReservedReplicaName(name),
1617 )));
1618 }
1619 let cluster = state.get_cluster(cluster_id);
1620 tx.insert_cluster_replica_with_id(
1624 cluster_id,
1625 replica_id,
1626 &name,
1627 config.clone().into(),
1628 owner_id,
1629 )?;
1630 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1631 size,
1632 billed_as,
1633 internal,
1634 ..
1635 }) = &config.location
1636 {
1637 let (reason, scheduling_policies) = reason.into_audit_log();
1638 let details = EventDetails::CreateClusterReplicaV4(
1639 mz_audit_log::CreateClusterReplicaV4 {
1640 cluster_id: cluster_id.to_string(),
1641 cluster_name: cluster.name.clone(),
1642 replica_id: Some(replica_id.to_string()),
1643 replica_name: name.clone(),
1644 logical_size: size.clone(),
1645 billed_as: billed_as.clone(),
1646 internal: *internal,
1647 reason,
1648 scheduling_policies,
1649 },
1650 );
1651 CatalogState::add_to_audit_log(
1652 &state.system_configuration,
1653 oracle_write_ts,
1654 session,
1655 tx,
1656 audit_events,
1657 EventType::Create,
1658 ObjectType::ClusterReplica,
1659 details,
1660 )?;
1661 }
1662 }
1663 Op::CreateItem {
1664 id,
1665 name,
1666 item,
1667 owner_id,
1668 } => {
1669 state.check_unstable_dependencies(&item)?;
1670
1671 match &item {
1672 CatalogItem::Table(table) => {
1673 let gids: Vec<_> = table.global_ids().collect();
1674 assert_eq!(gids.len(), 1);
1675 storage_collections_to_create.extend(gids);
1676 }
1677 CatalogItem::Source(source) => {
1678 storage_collections_to_create.insert(source.global_id());
1679 }
1680 CatalogItem::MaterializedView(mv) => {
1681 let mv_gid = mv.global_id_writes();
1682 if let Some(target_id) = mv.replacement_target {
1683 let target_gid = state.get_entry(&target_id).latest_global_id();
1684 let shard_id =
1685 state.storage_metadata().get_collection_shard(target_gid)?;
1686 storage_collections_to_register.insert(mv_gid, shard_id);
1687 } else {
1688 storage_collections_to_create.insert(mv_gid);
1689 }
1690 }
1691 CatalogItem::Sink(sink) => {
1692 storage_collections_to_create.insert(sink.global_id());
1693 }
1694 CatalogItem::Log(_)
1695 | CatalogItem::View(_)
1696 | CatalogItem::Index(_)
1697 | CatalogItem::Type(_)
1698 | CatalogItem::Func(_)
1699 | CatalogItem::Secret(_)
1700 | CatalogItem::Connection(_)
1701 | CatalogItem::MetricSink(_) => (),
1704 }
1705
1706 let system_user = session.map_or(false, |s| s.user().is_system_user());
1707 if !system_user {
1708 if let Some(id @ ClusterId::System(_)) = item.cluster_id() {
1709 let cluster_name = state.clusters_by_id[&id].name.clone();
1710 return Err(AdapterError::Catalog(Error::new(
1711 ErrorKind::ReadOnlyCluster(cluster_name),
1712 )));
1713 }
1714 }
1715
1716 let owner_privileges = vec![rbac::owner_privilege(item.typ().into(), owner_id)];
1717 let default_privileges = state
1718 .default_privileges
1719 .get_applicable_privileges(
1720 owner_id,
1721 name.qualifiers.database_spec.id(),
1722 Some(name.qualifiers.schema_spec.into()),
1723 item.typ().into(),
1724 )
1725 .map(|item| item.mz_acl_item(owner_id));
1726 let progress_source_privilege = if item.is_progress_source() {
1728 Some(MzAclItem {
1729 grantee: MZ_SUPPORT_ROLE_ID,
1730 grantor: owner_id,
1731 acl_mode: AclMode::SELECT,
1732 })
1733 } else {
1734 None
1735 };
1736 let privileges: Vec<_> = merge_mz_acl_items(
1737 owner_privileges
1738 .into_iter()
1739 .chain(default_privileges)
1740 .chain(progress_source_privilege),
1741 )
1742 .collect();
1743
1744 let temporary_oids = state.get_temporary_oids().collect();
1745
1746 if item.is_temporary() {
1747 if name.qualifiers.database_spec != ResolvedDatabaseSpecifier::Ambient
1748 || name.qualifiers.schema_spec != SchemaSpecifier::Temporary
1749 {
1750 return Err(AdapterError::Catalog(Error::new(
1751 ErrorKind::InvalidTemporarySchema,
1752 )));
1753 }
1754 let owner_session =
1755 temporary_item_owner_session(state, session, &item, &name.item)?;
1756
1757 let schema_id = name.qualifiers.schema_spec.clone().into();
1758 let item_type = item.typ();
1759 let (create_sql, global_id, versions) = item.to_serialized();
1760 tx.insert_user_item(
1761 id,
1762 global_id,
1763 schema_id,
1764 &name.item,
1765 create_sql,
1766 owner_id,
1767 privileges.clone(),
1768 &temporary_oids,
1769 versions,
1770 Some(owner_session),
1771 )?;
1772
1773 info!(
1774 "create temporary {} {} ({})",
1775 item_type,
1776 state.resolve_full_name(&name, None),
1777 id
1778 );
1779 } else {
1780 if let Some(temp_id) =
1781 item.uses()
1782 .iter()
1783 .find(|id| match state.try_get_entry(*id) {
1784 Some(entry) => entry.item().is_temporary(),
1785 None => temporary_ids.contains(id),
1786 })
1787 {
1788 let temp_item = state.get_entry(temp_id);
1789 return Err(AdapterError::Catalog(Error::new(
1790 ErrorKind::InvalidTemporaryDependency(temp_item.name().item.clone()),
1791 )));
1792 }
1793 if name.qualifiers.database_spec == ResolvedDatabaseSpecifier::Ambient
1794 && !system_user
1795 {
1796 let schema_name = state
1797 .resolve_full_name(&name, session.map(|session| session.conn_id()))
1798 .schema;
1799 return Err(AdapterError::Catalog(Error::new(
1800 ErrorKind::ReadOnlySystemSchema(schema_name),
1801 )));
1802 }
1803 let schema_id = name.qualifiers.schema_spec.clone().into();
1804 let item_type = item.typ();
1805 let (create_sql, global_id, versions) = item.to_serialized();
1806 tx.insert_user_item(
1807 id,
1808 global_id,
1809 schema_id,
1810 &name.item,
1811 create_sql,
1812 owner_id,
1813 privileges.clone(),
1814 &temporary_oids,
1815 versions,
1816 None,
1817 )?;
1818 info!(
1819 "create {} {} ({})",
1820 item_type,
1821 state.resolve_full_name(&name, None),
1822 id
1823 );
1824 }
1825
1826 if Self::should_audit_log_item(&item) {
1827 let name = Self::full_name_detail(
1828 &state.resolve_full_name(&name, session.map(|session| session.conn_id())),
1829 );
1830 let details = match &item {
1831 CatalogItem::Source(s) => {
1832 let cluster_id = match s.data_source {
1833 DataSourceDesc::IngestionExport { ingestion_id, .. } => {
1836 match state.get_entry(&ingestion_id).cluster_id() {
1837 Some(cluster_id) => Some(cluster_id.to_string()),
1838 None => None,
1839 }
1840 }
1841 _ => match item.cluster_id() {
1842 Some(cluster_id) => Some(cluster_id.to_string()),
1843 None => None,
1844 },
1845 };
1846
1847 EventDetails::CreateSourceSinkV4(mz_audit_log::CreateSourceSinkV4 {
1848 id: id.to_string(),
1849 cluster_id,
1850 name,
1851 external_type: s.source_type().to_string(),
1852 })
1853 }
1854 CatalogItem::Sink(s) => {
1855 EventDetails::CreateSourceSinkV4(mz_audit_log::CreateSourceSinkV4 {
1856 id: id.to_string(),
1857 cluster_id: Some(s.cluster_id.to_string()),
1858 name,
1859 external_type: s.sink_type().to_string(),
1860 })
1861 }
1862 CatalogItem::Index(i) => {
1863 EventDetails::CreateIndexV1(mz_audit_log::CreateIndexV1 {
1864 id: id.to_string(),
1865 name,
1866 cluster_id: i.cluster_id.to_string(),
1867 })
1868 }
1869 CatalogItem::MaterializedView(mv) => {
1870 EventDetails::CreateMaterializedViewV1(
1871 mz_audit_log::CreateMaterializedViewV1 {
1872 id: id.to_string(),
1873 name,
1874 cluster_id: mv.cluster_id.to_string(),
1875 replacement_target_id: mv
1876 .replacement_target
1877 .map(|id| id.to_string()),
1878 },
1879 )
1880 }
1881 CatalogItem::Table(_)
1882 | CatalogItem::Log(_)
1883 | CatalogItem::View(_)
1884 | CatalogItem::Type(_)
1885 | CatalogItem::Func(_)
1886 | CatalogItem::Secret(_)
1887 | CatalogItem::Connection(_)
1888 | CatalogItem::MetricSink(_) => EventDetails::IdFullNameV1(IdFullNameV1 {
1889 id: id.to_string(),
1890 name,
1891 }),
1892 };
1893 CatalogState::add_to_audit_log(
1894 &state.system_configuration,
1895 oracle_write_ts,
1896 session,
1897 tx,
1898 audit_events,
1899 EventType::Create,
1900 catalog_type_to_audit_object_type(item.typ()),
1901 details,
1902 )?;
1903 }
1904 }
1905 Op::CreateNetworkPolicy {
1906 rules,
1907 name,
1908 owner_id,
1909 } => {
1910 if state.network_policies_by_name.contains_key(&name) {
1911 return Err(AdapterError::PlanError(PlanError::Catalog(
1912 SqlCatalogError::NetworkPolicyAlreadyExists(name),
1913 )));
1914 }
1915 if is_reserved_name(&name) {
1916 return Err(AdapterError::Catalog(Error::new(
1917 ErrorKind::ReservedNetworkPolicyName(name),
1918 )));
1919 }
1920
1921 let owner_privileges = vec![rbac::owner_privilege(
1922 mz_sql::catalog::ObjectType::NetworkPolicy,
1923 owner_id,
1924 )];
1925 let default_privileges = state
1926 .default_privileges
1927 .get_applicable_privileges(
1928 owner_id,
1929 None,
1930 None,
1931 mz_sql::catalog::ObjectType::NetworkPolicy,
1932 )
1933 .map(|item| item.mz_acl_item(owner_id));
1934 let privileges: Vec<_> =
1935 merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1936 .collect();
1937
1938 let temporary_oids: HashSet<_> = state.get_temporary_oids().collect();
1939 let id = tx.insert_user_network_policy(
1940 name.clone(),
1941 rules,
1942 privileges,
1943 owner_id,
1944 &temporary_oids,
1945 )?;
1946
1947 CatalogState::add_to_audit_log(
1948 &state.system_configuration,
1949 oracle_write_ts,
1950 session,
1951 tx,
1952 audit_events,
1953 EventType::Create,
1954 ObjectType::NetworkPolicy,
1955 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1956 id: id.to_string(),
1957 name: name.clone(),
1958 }),
1959 )?;
1960
1961 info!("created network policy {name} ({id})");
1962 }
1963 Op::Comment {
1964 object_id,
1965 sub_component,
1966 comment,
1967 } => {
1968 tx.update_comment(object_id, sub_component, comment)?;
1969 let entry = state.get_comment_id_entry(&object_id);
1970 let should_log = entry
1971 .map(|entry| Self::should_audit_log_item(entry.item()))
1972 .unwrap_or(true);
1974 if let (Some(conn_id), true) =
1977 (session.map(|session| session.conn_id()), should_log)
1978 {
1979 CatalogState::add_to_audit_log(
1980 &state.system_configuration,
1981 oracle_write_ts,
1982 session,
1983 tx,
1984 audit_events,
1985 EventType::Comment,
1986 comment_id_to_audit_object_type(object_id),
1987 EventDetails::IdNameV1(IdNameV1 {
1988 id: format!("{object_id:?}"),
1990 name: state.comment_id_to_audit_log_name(object_id, conn_id),
1991 }),
1992 )?;
1993 }
1994 }
1995 Op::UpdateSourceReferences {
1996 source_id,
1997 references,
1998 } => {
1999 tx.update_source_references(
2000 source_id,
2001 references
2002 .references
2003 .into_iter()
2004 .map(|reference| reference.into())
2005 .collect(),
2006 references.updated_at,
2007 )?;
2008 }
2009 Op::DropObjects(drop_object_infos) => {
2010 let delta = ObjectsToDrop::generate(drop_object_infos, state, session)?;
2012
2013 tx.drop_comments(&delta.comments)?;
2015
2016 let items_to_drop: BTreeSet<_> = delta.items.iter().copied().collect();
2018 tx.remove_items(&items_to_drop)?;
2019
2020 for item_id in delta.items {
2021 let entry = state.get_entry(&item_id);
2022
2023 if entry.item().is_storage_collection() {
2024 storage_collections_to_drop.extend(entry.global_ids());
2025 }
2026
2027 if state.source_references.contains_key(&item_id) {
2028 tx.remove_source_references(item_id)?;
2029 }
2030
2031 if Self::should_audit_log_item(entry.item()) {
2032 CatalogState::add_to_audit_log(
2033 &state.system_configuration,
2034 oracle_write_ts,
2035 session,
2036 tx,
2037 audit_events,
2038 EventType::Drop,
2039 catalog_type_to_audit_object_type(entry.item().typ()),
2040 EventDetails::IdFullNameV1(IdFullNameV1 {
2041 id: item_id.to_string(),
2042 name: Self::full_name_detail(&state.resolve_full_name(
2043 entry.name(),
2044 session.map(|session| session.conn_id()),
2045 )),
2046 }),
2047 )?;
2048 }
2049 info!(
2050 "drop {} {} ({})",
2051 entry.item_type(),
2052 state.resolve_full_name(entry.name(), entry.conn_id()),
2053 item_id
2054 );
2055 }
2056
2057 let schemas = delta
2059 .schemas
2060 .iter()
2061 .map(|(schema_spec, database_spec)| {
2062 (SchemaId::from(schema_spec), *database_spec)
2063 })
2064 .collect();
2065 tx.remove_schemas(&schemas)?;
2066
2067 for (schema_spec, database_spec) in delta.schemas {
2068 let schema = state.get_schema(
2069 &database_spec,
2070 &schema_spec,
2071 session
2072 .map(|session| session.conn_id())
2073 .unwrap_or(&SYSTEM_CONN_ID),
2074 );
2075
2076 let schema_id = SchemaId::from(schema_spec);
2077 let database_id = match database_spec {
2078 ResolvedDatabaseSpecifier::Ambient => None,
2079 ResolvedDatabaseSpecifier::Id(database_id) => Some(database_id),
2080 };
2081
2082 CatalogState::add_to_audit_log(
2083 &state.system_configuration,
2084 oracle_write_ts,
2085 session,
2086 tx,
2087 audit_events,
2088 EventType::Drop,
2089 ObjectType::Schema,
2090 EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
2091 id: schema_id.to_string(),
2092 name: schema.name.schema.to_string(),
2093 database_name: database_id
2094 .map(|database_id| state.database_by_id[&database_id].name.clone()),
2095 }),
2096 )?;
2097 }
2098
2099 tx.remove_databases(&delta.databases)?;
2101
2102 for database_id in delta.databases {
2103 let database = state.get_database(&database_id).clone();
2104
2105 CatalogState::add_to_audit_log(
2106 &state.system_configuration,
2107 oracle_write_ts,
2108 session,
2109 tx,
2110 audit_events,
2111 EventType::Drop,
2112 ObjectType::Database,
2113 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2114 id: database_id.to_string(),
2115 name: database.name.clone(),
2116 }),
2117 )?;
2118 }
2119
2120 tx.remove_user_roles(&delta.roles)?;
2122
2123 for role_id in delta.roles {
2124 let role = state
2125 .roles_by_id
2126 .get(&role_id)
2127 .expect("catalog out of sync");
2128
2129 CatalogState::add_to_audit_log(
2130 &state.system_configuration,
2131 oracle_write_ts,
2132 session,
2133 tx,
2134 audit_events,
2135 EventType::Drop,
2136 ObjectType::Role,
2137 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2138 id: role.id.to_string(),
2139 name: role.name.clone(),
2140 }),
2141 )?;
2142 info!("drop role {}", role.name());
2143 }
2144
2145 tx.remove_network_policies(&delta.network_policies)?;
2147
2148 for network_policy_id in delta.network_policies {
2149 let policy = state
2150 .network_policies_by_id
2151 .get(&network_policy_id)
2152 .expect("catalog out of sync");
2153
2154 CatalogState::add_to_audit_log(
2155 &state.system_configuration,
2156 oracle_write_ts,
2157 session,
2158 tx,
2159 audit_events,
2160 EventType::Drop,
2161 ObjectType::NetworkPolicy,
2162 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2163 id: policy.id.to_string(),
2164 name: policy.name.clone(),
2165 }),
2166 )?;
2167 info!("drop network policy {}", policy.name.clone());
2168 }
2169
2170 let replicas = delta.replicas.keys().copied().collect();
2172 tx.remove_cluster_replicas(&replicas)?;
2173
2174 for (replica_id, (cluster_id, reason)) in delta.replicas {
2175 let cluster = state.get_cluster(cluster_id);
2176 let replica = cluster.replica(replica_id).expect("Must exist");
2177
2178 let (reason, scheduling_policies) = reason.into_audit_log();
2179 let details =
2180 EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
2181 cluster_id: cluster_id.to_string(),
2182 cluster_name: cluster.name.clone(),
2183 replica_id: Some(replica_id.to_string()),
2184 replica_name: replica.name.clone(),
2185 reason,
2186 scheduling_policies,
2187 });
2188 CatalogState::add_to_audit_log(
2189 &state.system_configuration,
2190 oracle_write_ts,
2191 session,
2192 tx,
2193 audit_events,
2194 EventType::Drop,
2195 ObjectType::ClusterReplica,
2196 details,
2197 )?;
2198 }
2199
2200 tx.remove_clusters(&delta.clusters)?;
2202
2203 for cluster_id in delta.clusters {
2204 let cluster = state.get_cluster(cluster_id);
2205
2206 CatalogState::add_to_audit_log(
2207 &state.system_configuration,
2208 oracle_write_ts,
2209 session,
2210 tx,
2211 audit_events,
2212 EventType::Drop,
2213 ObjectType::Cluster,
2214 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2215 id: cluster.id.to_string(),
2216 name: cluster.name.clone(),
2217 }),
2218 )?;
2219 }
2220 }
2221 Op::GrantRole {
2222 role_id,
2223 member_id,
2224 grantor_id,
2225 } => {
2226 state.ensure_not_reserved_role(&member_id)?;
2227 state.ensure_grantable_role(&role_id)?;
2228 if state.collect_role_membership(&role_id).contains(&member_id) {
2229 let group_role = state.get_role(&role_id);
2230 let member_role = state.get_role(&member_id);
2231 return Err(AdapterError::Catalog(Error::new(
2232 ErrorKind::CircularRoleMembership {
2233 role_name: group_role.name().to_string(),
2234 member_name: member_role.name().to_string(),
2235 },
2236 )));
2237 }
2238 let mut member_role = state.get_role(&member_id).clone();
2239 member_role.membership.map.insert(role_id, grantor_id);
2240 tx.update_role(member_id, member_role.into(), PasswordAction::NoChange)?;
2241
2242 CatalogState::add_to_audit_log(
2243 &state.system_configuration,
2244 oracle_write_ts,
2245 session,
2246 tx,
2247 audit_events,
2248 EventType::Grant,
2249 ObjectType::Role,
2250 EventDetails::GrantRoleV2(mz_audit_log::GrantRoleV2 {
2251 role_id: role_id.to_string(),
2252 member_id: member_id.to_string(),
2253 grantor_id: grantor_id.to_string(),
2254 executed_by: session
2255 .map(|session| session.authenticated_role_id())
2256 .unwrap_or(&MZ_SYSTEM_ROLE_ID)
2257 .to_string(),
2258 }),
2259 )?;
2260 }
2261 Op::RevokeRole {
2262 role_id,
2263 member_id,
2264 grantor_id,
2265 } => {
2266 state.ensure_not_reserved_role(&member_id)?;
2267 state.ensure_grantable_role(&role_id)?;
2268 let mut member_role = state.get_role(&member_id).clone();
2269 member_role.membership.map.remove(&role_id);
2270 tx.update_role(member_id, member_role.into(), PasswordAction::NoChange)?;
2271
2272 CatalogState::add_to_audit_log(
2273 &state.system_configuration,
2274 oracle_write_ts,
2275 session,
2276 tx,
2277 audit_events,
2278 EventType::Revoke,
2279 ObjectType::Role,
2280 EventDetails::RevokeRoleV2(mz_audit_log::RevokeRoleV2 {
2281 role_id: role_id.to_string(),
2282 member_id: member_id.to_string(),
2283 grantor_id: grantor_id.to_string(),
2284 executed_by: session
2285 .map(|session| session.authenticated_role_id())
2286 .unwrap_or(&MZ_SYSTEM_ROLE_ID)
2287 .to_string(),
2288 }),
2289 )?;
2290 }
2291 Op::UpdatePrivilege {
2292 target_id,
2293 privileges,
2294 variant,
2295 } => {
2296 let update_privilege_fn = |target_privileges: &mut PrivilegeMap| {
2297 for privilege in &privileges {
2298 match variant {
2299 UpdatePrivilegeVariant::Grant => {
2300 target_privileges.grant(privilege.clone());
2301 }
2302 UpdatePrivilegeVariant::Revoke => {
2303 target_privileges.revoke(privilege);
2304 }
2305 }
2306 }
2307 };
2308 match &target_id {
2309 SystemObjectId::Object(object_id) => match object_id {
2310 ObjectId::Cluster(id) => {
2311 let mut cluster = state.get_cluster(*id).clone();
2312 update_privilege_fn(&mut cluster.privileges);
2313 tx.update_cluster(*id, cluster.into())?;
2314 }
2315 ObjectId::Database(id) => {
2316 let mut database = state.get_database(id).clone();
2317 update_privilege_fn(&mut database.privileges);
2318 tx.update_database(*id, database.into())?;
2319 }
2320 ObjectId::NetworkPolicy(id) => {
2321 let mut policy = state.get_network_policy(id).clone();
2322 update_privilege_fn(&mut policy.privileges);
2323 tx.update_network_policy(*id, policy.into())?;
2324 }
2325 ObjectId::Schema((database_spec, schema_spec)) => {
2326 let schema_id = schema_spec.clone().into();
2327 let mut schema = state
2328 .get_schema(
2329 database_spec,
2330 schema_spec,
2331 session
2332 .map(|session| session.conn_id())
2333 .unwrap_or(&SYSTEM_CONN_ID),
2334 )
2335 .clone();
2336 update_privilege_fn(&mut schema.privileges);
2337 tx.update_schema(schema_id, schema.into())?;
2338 }
2339 ObjectId::Item(id) => {
2340 let entry = state.get_entry(id);
2341 let mut new_entry = entry.clone();
2342 update_privilege_fn(&mut new_entry.privileges);
2343 tx.update_item(*id, state.durable_item(new_entry)?)?;
2344 }
2345 ObjectId::Role(_) | ObjectId::ClusterReplica(_) => {}
2346 },
2347 SystemObjectId::System => {
2348 let mut system_privileges = PrivilegeMap::clone(&state.system_privileges);
2349 update_privilege_fn(&mut system_privileges);
2350 for privilege in &privileges {
2351 let new_privilege = system_privileges
2352 .get_acl_item(&privilege.grantee, &privilege.grantor);
2353 tx.set_system_privilege(
2354 privilege.grantee,
2355 privilege.grantor,
2356 new_privilege.map(|new_privilege| new_privilege.acl_mode),
2357 )?;
2358 }
2359 }
2360 }
2361 let object_type = state.get_system_object_type(&target_id);
2362 let object_id_str = match &target_id {
2363 SystemObjectId::System => "SYSTEM".to_string(),
2364 SystemObjectId::Object(id) => id.to_string(),
2365 };
2366 for privilege in &privileges {
2368 CatalogState::add_to_audit_log(
2369 &state.system_configuration,
2370 oracle_write_ts,
2371 session,
2372 tx,
2373 audit_events,
2374 variant.into(),
2375 system_object_type_to_audit_object_type(&object_type),
2376 EventDetails::UpdatePrivilegeV1(mz_audit_log::UpdatePrivilegeV1 {
2377 object_id: object_id_str.clone(),
2378 grantee_id: privilege.grantee.to_string(),
2379 grantor_id: privilege.grantor.to_string(),
2380 privileges: privilege.acl_mode.to_string(),
2381 }),
2382 )?;
2383 }
2384 }
2385 Op::UpdateDefaultPrivilege {
2386 privilege_object,
2387 privilege_acl_item,
2388 variant,
2389 } => {
2390 let mut default_privileges = DefaultPrivileges::clone(&state.default_privileges);
2391 match variant {
2392 UpdatePrivilegeVariant::Grant => default_privileges
2393 .grant(privilege_object.clone(), privilege_acl_item.clone()),
2394 UpdatePrivilegeVariant::Revoke => {
2395 default_privileges.revoke(&privilege_object, &privilege_acl_item)
2396 }
2397 }
2398 let new_acl_mode = default_privileges
2399 .get_privileges_for_grantee(&privilege_object, &privilege_acl_item.grantee);
2400 tx.set_default_privilege(
2401 privilege_object.role_id,
2402 privilege_object.database_id,
2403 privilege_object.schema_id,
2404 privilege_object.object_type,
2405 privilege_acl_item.grantee,
2406 new_acl_mode.cloned(),
2407 )?;
2408 CatalogState::add_to_audit_log(
2409 &state.system_configuration,
2410 oracle_write_ts,
2411 session,
2412 tx,
2413 audit_events,
2414 variant.into(),
2415 object_type_to_audit_object_type(privilege_object.object_type),
2416 EventDetails::AlterDefaultPrivilegeV1(mz_audit_log::AlterDefaultPrivilegeV1 {
2417 role_id: privilege_object.role_id.to_string(),
2418 database_id: privilege_object.database_id.map(|id| id.to_string()),
2419 schema_id: privilege_object.schema_id.map(|id| id.to_string()),
2420 grantee_id: privilege_acl_item.grantee.to_string(),
2421 privileges: privilege_acl_item.acl_mode.to_string(),
2422 }),
2423 )?;
2424 }
2425 Op::RenameCluster {
2426 id,
2427 name,
2428 to_name,
2429 check_reserved_names,
2430 } => {
2431 if id.is_system() {
2432 return Err(AdapterError::Catalog(Error::new(
2433 ErrorKind::ReadOnlyCluster(name.clone()),
2434 )));
2435 }
2436 if check_reserved_names && is_reserved_name(&to_name) {
2437 return Err(AdapterError::Catalog(Error::new(
2438 ErrorKind::ReservedClusterName(to_name),
2439 )));
2440 }
2441 tx.rename_cluster(id, &name, &to_name)?;
2442 CatalogState::add_to_audit_log(
2443 &state.system_configuration,
2444 oracle_write_ts,
2445 session,
2446 tx,
2447 audit_events,
2448 EventType::Alter,
2449 ObjectType::Cluster,
2450 EventDetails::RenameClusterV1(mz_audit_log::RenameClusterV1 {
2451 id: id.to_string(),
2452 old_name: name.clone(),
2453 new_name: to_name.clone(),
2454 }),
2455 )?;
2456 info!("rename cluster {name} to {to_name}");
2457 }
2458 Op::RenameClusterReplica {
2459 cluster_id,
2460 replica_id,
2461 name,
2462 to_name,
2463 } => {
2464 if is_reserved_name(&to_name) {
2465 return Err(AdapterError::Catalog(Error::new(
2466 ErrorKind::ReservedReplicaName(to_name),
2467 )));
2468 }
2469 tx.rename_cluster_replica(replica_id, &name, &to_name)?;
2470 CatalogState::add_to_audit_log(
2471 &state.system_configuration,
2472 oracle_write_ts,
2473 session,
2474 tx,
2475 audit_events,
2476 EventType::Alter,
2477 ObjectType::ClusterReplica,
2478 EventDetails::RenameClusterReplicaV1(mz_audit_log::RenameClusterReplicaV1 {
2479 cluster_id: cluster_id.to_string(),
2480 replica_id: replica_id.to_string(),
2481 old_name: name.replica.as_str().to_string(),
2482 new_name: to_name.clone(),
2483 }),
2484 )?;
2485 info!("rename cluster replica {name} to {to_name}");
2486 }
2487 Op::RenameItem {
2488 id,
2489 to_name,
2490 current_full_name,
2491 } => {
2492 let mut updates = Vec::new();
2493
2494 let entry = state.get_entry(&id);
2495 if let CatalogItem::Type(_) = entry.item() {
2496 return Err(AdapterError::Catalog(Error::new(ErrorKind::TypeRename(
2497 current_full_name.to_string(),
2498 ))));
2499 }
2500
2501 if entry.id().is_system() {
2502 let name = state
2503 .resolve_full_name(entry.name(), session.map(|session| session.conn_id()));
2504 return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
2505 name.to_string(),
2506 ))));
2507 }
2508
2509 let mut to_full_name = current_full_name.clone();
2510 to_full_name.item.clone_from(&to_name);
2511
2512 let mut to_qualified_name = entry.name().clone();
2513 to_qualified_name.item.clone_from(&to_name);
2514
2515 let details = EventDetails::RenameItemV1(mz_audit_log::RenameItemV1 {
2516 id: id.to_string(),
2517 old_name: Self::full_name_detail(¤t_full_name),
2518 new_name: Self::full_name_detail(&to_full_name),
2519 });
2520 if Self::should_audit_log_item(entry.item()) {
2521 CatalogState::add_to_audit_log(
2522 &state.system_configuration,
2523 oracle_write_ts,
2524 session,
2525 tx,
2526 audit_events,
2527 EventType::Alter,
2528 catalog_type_to_audit_object_type(entry.item().typ()),
2529 details,
2530 )?;
2531 }
2532
2533 let mut new_entry = entry.clone();
2535 new_entry.name.item.clone_from(&to_name);
2536 new_entry.item = entry
2537 .item()
2538 .rename_item_refs(current_full_name.clone(), to_full_name.item.clone(), true)
2539 .map_err(|e| {
2540 Error::new(ErrorKind::from(AmbiguousRename {
2541 depender: state
2542 .resolve_full_name(entry.name(), entry.conn_id())
2543 .to_string(),
2544 dependee: state
2545 .resolve_full_name(entry.name(), entry.conn_id())
2546 .to_string(),
2547 message: e,
2548 }))
2549 })?;
2550
2551 for id in entry.referenced_by() {
2552 let dependent_item = state.get_entry(id);
2553 let mut to_entry = dependent_item.clone();
2554 to_entry.item = dependent_item
2555 .item()
2556 .rename_item_refs(
2557 current_full_name.clone(),
2558 to_full_name.item.clone(),
2559 false,
2560 )
2561 .map_err(|e| {
2562 Error::new(ErrorKind::from(AmbiguousRename {
2563 depender: state
2564 .resolve_full_name(
2565 dependent_item.name(),
2566 dependent_item.conn_id(),
2567 )
2568 .to_string(),
2569 dependee: state
2570 .resolve_full_name(entry.name(), entry.conn_id())
2571 .to_string(),
2572 message: e,
2573 }))
2574 })?;
2575
2576 tx.update_item(*id, state.durable_item(to_entry)?)?;
2577 updates.push(*id);
2578 }
2579 tx.update_item(id, state.durable_item(new_entry)?)?;
2580
2581 updates.push(id);
2582 for id in updates {
2583 Self::log_update(state, &id);
2584 }
2585 }
2586 Op::RenameSchema {
2587 database_spec,
2588 schema_spec,
2589 new_name,
2590 check_reserved_names,
2591 } => {
2592 if check_reserved_names && is_reserved_name(&new_name) {
2593 return Err(AdapterError::Catalog(Error::new(
2594 ErrorKind::ReservedSchemaName(new_name),
2595 )));
2596 }
2597
2598 let conn_id = session
2599 .map(|session| session.conn_id())
2600 .unwrap_or(&SYSTEM_CONN_ID);
2601
2602 let schema = state.get_schema(&database_spec, &schema_spec, conn_id);
2603 let cur_name = schema.name().schema.clone();
2604
2605 let ResolvedDatabaseSpecifier::Id(database_id) = database_spec else {
2606 return Err(AdapterError::Catalog(Error::new(
2607 ErrorKind::AmbientSchemaRename(cur_name),
2608 )));
2609 };
2610 let database = state.get_database(&database_id);
2611 let database_name = &database.name;
2612
2613 let mut updates: Vec<CatalogItemId> = Vec::new();
2614 let mut items_to_update = BTreeMap::new();
2615 let mut seen: BTreeSet<CatalogItemId> = BTreeSet::new();
2619
2620 let mut update_item = |id: &CatalogItemId| {
2621 if !seen.insert(*id) {
2622 return Ok(());
2623 }
2624
2625 let entry = state.get_entry(id);
2626
2627 let mut new_entry = entry.clone();
2629 new_entry.item = entry
2630 .item
2631 .rename_schema_refs(database_name, &cur_name, &new_name)
2632 .map_err(|(s, _i)| {
2633 Error::new(ErrorKind::from(AmbiguousRename {
2634 depender: state
2635 .resolve_full_name(entry.name(), entry.conn_id())
2636 .to_string(),
2637 dependee: format!("{database_name}.{cur_name}"),
2638 message: format!("ambiguous reference to schema named {s}"),
2639 }))
2640 })?;
2641
2642 items_to_update.insert(*id, state.durable_item(new_entry)?);
2644 updates.push(*id);
2645
2646 Ok::<_, AdapterError>(())
2647 };
2648
2649 for (_name, item_id) in schema
2654 .items
2655 .iter()
2656 .chain(schema.types.iter())
2657 .chain(schema.functions.iter())
2658 {
2659 update_item(item_id)?;
2661
2662 for id in state.get_entry(item_id).referenced_by() {
2664 update_item(id)?;
2665 }
2666 }
2667 tx.update_items(items_to_update)?;
2670
2671 let SchemaSpecifier::Id(schema_id) = *schema.id() else {
2673 let schema_name = schema.name().schema.clone();
2674 return Err(AdapterError::Catalog(crate::catalog::Error::new(
2675 crate::catalog::ErrorKind::ReadOnlySystemSchema(schema_name),
2676 )));
2677 };
2678
2679 let database_name = database_spec
2681 .id()
2682 .map(|id| state.get_database(&id).name.clone());
2683 let details = EventDetails::RenameSchemaV1(mz_audit_log::RenameSchemaV1 {
2684 id: schema_id.to_string(),
2685 old_name: schema.name().schema.clone(),
2686 new_name: new_name.clone(),
2687 database_name,
2688 });
2689 CatalogState::add_to_audit_log(
2690 &state.system_configuration,
2691 oracle_write_ts,
2692 session,
2693 tx,
2694 audit_events,
2695 EventType::Alter,
2696 mz_audit_log::ObjectType::Schema,
2697 details,
2698 )?;
2699
2700 let mut new_schema = schema.clone();
2702 new_schema.name.schema.clone_from(&new_name);
2703 tx.update_schema(schema_id, new_schema.into())?;
2704
2705 for id in updates {
2706 Self::log_update(state, &id);
2707 }
2708 }
2709 Op::UpdateOwner { id, new_owner } => {
2710 let conn_id = session
2711 .map(|session| session.conn_id())
2712 .unwrap_or(&SYSTEM_CONN_ID);
2713 let old_owner = state
2714 .get_owner_id(&id, conn_id)
2715 .expect("cannot update the owner of an object without an owner");
2716 match &id {
2717 ObjectId::Cluster(id) => {
2718 let mut cluster = state.get_cluster(*id).clone();
2719 if id.is_system() {
2720 return Err(AdapterError::Catalog(Error::new(
2721 ErrorKind::ReadOnlyCluster(cluster.name),
2722 )));
2723 }
2724 Self::update_privilege_owners(
2725 &mut cluster.privileges,
2726 cluster.owner_id,
2727 new_owner,
2728 );
2729 cluster.owner_id = new_owner;
2730 tx.update_cluster(*id, cluster.into())?;
2731 }
2732 ObjectId::ClusterReplica((cluster_id, replica_id)) => {
2733 let cluster = state.get_cluster(*cluster_id);
2734 let mut replica = cluster
2735 .replica(*replica_id)
2736 .expect("catalog out of sync")
2737 .clone();
2738 if replica_id.is_system() {
2739 return Err(AdapterError::Catalog(Error::new(
2740 ErrorKind::ReadOnlyClusterReplica(replica.name),
2741 )));
2742 }
2743 replica.owner_id = new_owner;
2744 tx.update_cluster_replica(*replica_id, replica.into())?;
2745 }
2746 ObjectId::Database(id) => {
2747 let mut database = state.get_database(id).clone();
2748 if id.is_system() {
2749 return Err(AdapterError::Catalog(Error::new(
2750 ErrorKind::ReadOnlyDatabase(database.name),
2751 )));
2752 }
2753 Self::update_privilege_owners(
2754 &mut database.privileges,
2755 database.owner_id,
2756 new_owner,
2757 );
2758 database.owner_id = new_owner;
2759 tx.update_database(*id, database.clone().into())?;
2760 }
2761 ObjectId::Schema((database_spec, schema_spec)) => {
2762 let schema_id: SchemaId = schema_spec.clone().into();
2763 let mut schema = state
2764 .get_schema(database_spec, schema_spec, conn_id)
2765 .clone();
2766 if schema_id.is_system() {
2767 let name = schema.name();
2768 let full_name = state.resolve_full_schema_name(name);
2769 return Err(AdapterError::Catalog(Error::new(
2770 ErrorKind::ReadOnlySystemSchema(full_name.to_string()),
2771 )));
2772 }
2773 Self::update_privilege_owners(
2774 &mut schema.privileges,
2775 schema.owner_id,
2776 new_owner,
2777 );
2778 schema.owner_id = new_owner;
2779 tx.update_schema(schema_id, schema.into())?;
2780 }
2781 ObjectId::Item(id) => {
2782 let entry = state.get_entry(id);
2783 let mut new_entry = entry.clone();
2784 if id.is_system() {
2785 let full_name = state.resolve_full_name(
2786 new_entry.name(),
2787 session.map(|session| session.conn_id()),
2788 );
2789 return Err(AdapterError::Catalog(Error::new(
2790 ErrorKind::ReadOnlyItem(full_name.to_string()),
2791 )));
2792 }
2793 Self::update_privilege_owners(
2794 &mut new_entry.privileges,
2795 new_entry.owner_id,
2796 new_owner,
2797 );
2798 new_entry.owner_id = new_owner;
2799 tx.update_item(*id, state.durable_item(new_entry)?)?;
2800 }
2801 ObjectId::NetworkPolicy(id) => {
2802 let mut policy = state.get_network_policy(id).clone();
2803 if id.is_system() {
2804 return Err(AdapterError::Catalog(Error::new(
2805 ErrorKind::ReadOnlyNetworkPolicy(policy.name),
2806 )));
2807 }
2808 Self::update_privilege_owners(
2809 &mut policy.privileges,
2810 policy.owner_id,
2811 new_owner,
2812 );
2813 policy.owner_id = new_owner;
2814 tx.update_network_policy(*id, policy.into())?;
2815 }
2816 ObjectId::Role(_) => unreachable!("roles have no owner"),
2817 }
2818 let object_type = state.get_object_type(&id);
2819 CatalogState::add_to_audit_log(
2820 &state.system_configuration,
2821 oracle_write_ts,
2822 session,
2823 tx,
2824 audit_events,
2825 EventType::Alter,
2826 object_type_to_audit_object_type(object_type),
2827 EventDetails::UpdateOwnerV1(mz_audit_log::UpdateOwnerV1 {
2828 object_id: id.to_string(),
2829 old_owner_id: old_owner.to_string(),
2830 new_owner_id: new_owner.to_string(),
2831 }),
2832 )?;
2833 }
2834 Op::UpdateClusterConfig {
2835 id,
2836 name,
2837 mut config,
2838 reconfiguration_audit,
2839 mut burst_audit,
2840 } => {
2841 let mut cluster = state.get_cluster(id).clone();
2842 if burst_audit.is_none() {
2850 if let ClusterVariant::Managed(managed) = &mut config.variant {
2851 if managed.has_unwarranted_burst_record() {
2852 managed.burst = None;
2853 burst_audit = Some(BurstAudit::Finished {
2854 cause: BurstFinishCause::NoLongerWarranted,
2855 });
2856 }
2857 }
2858 }
2859 if reconfiguration_audit.is_none()
2864 && Self::reconfiguration_lifecycle_moved(&cluster.config, &config)
2865 {
2866 return Err(AdapterError::Internal(format!(
2867 "cluster {name} reconfiguration record moved without a declared \
2868 audit intent"
2869 )));
2870 }
2871 if burst_audit.is_none() && Self::burst_lifecycle_moved(&cluster.config, &config) {
2875 return Err(AdapterError::Internal(format!(
2876 "cluster {name} burst record moved without a declared audit intent"
2877 )));
2878 }
2879 let reconfiguration_event = reconfiguration_audit
2880 .map(|audit| Self::reconfiguration_audit_details(&config, id, &name, audit))
2881 .transpose()?;
2882 let burst_event = burst_audit
2883 .map(|audit| {
2884 Self::burst_audit_details(&cluster.config, &config, id, &name, audit)
2885 })
2886 .transpose()?;
2887 cluster.config = config;
2888 tx.update_cluster(id, cluster.into())?;
2889 info!("update cluster {}", name);
2890
2891 CatalogState::add_to_audit_log(
2892 &state.system_configuration,
2893 oracle_write_ts,
2894 session,
2895 tx,
2896 audit_events,
2897 EventType::Alter,
2898 ObjectType::Cluster,
2899 EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2900 id: id.to_string(),
2901 name,
2902 }),
2903 )?;
2904
2905 if let Some(details) = reconfiguration_event {
2906 CatalogState::add_to_audit_log(
2907 &state.system_configuration,
2908 oracle_write_ts,
2909 session,
2910 tx,
2911 audit_events,
2912 EventType::Alter,
2913 ObjectType::Cluster,
2914 EventDetails::AlterClusterReconfigurationV1(details),
2915 )?;
2916 }
2917
2918 if let Some(details) = burst_event {
2919 CatalogState::add_to_audit_log(
2920 &state.system_configuration,
2921 oracle_write_ts,
2922 session,
2923 tx,
2924 audit_events,
2925 EventType::Alter,
2926 ObjectType::Cluster,
2927 EventDetails::ClusterHydrationBurstV1(details),
2928 )?;
2929 }
2930 }
2931 Op::UpdateClusterReplicaConfig {
2932 replica_id,
2933 cluster_id,
2934 config,
2935 } => {
2936 let replica = state.get_cluster_replica(cluster_id, replica_id).to_owned();
2937 info!("update replica {}", replica.name);
2938 tx.update_cluster_replica(
2939 replica_id,
2940 mz_catalog::durable::ClusterReplica {
2941 cluster_id,
2942 replica_id,
2943 name: replica.name.clone(),
2944 config: config.clone().into(),
2945 owner_id: replica.owner_id,
2946 },
2947 )?;
2948 }
2949 Op::UpdateItem { id, name, to_item } => {
2950 if !to_item.is_temporary() {
2957 if let Some(temp_id) =
2958 to_item
2959 .uses()
2960 .iter()
2961 .find(|id| match state.try_get_entry(*id) {
2962 Some(entry) => entry.item().is_temporary(),
2963 None => temporary_ids.contains(id),
2964 })
2965 {
2966 let temp_item = state.get_entry(temp_id);
2967 return Err(AdapterError::Catalog(Error::new(
2968 ErrorKind::InvalidTemporaryDependency(temp_item.name().item.clone()),
2969 )));
2970 }
2971 }
2972
2973 let mut entry = state.get_entry(&id).clone();
2974 entry.name = name.clone();
2975 entry.item = to_item.clone();
2976 tx.update_item(id, state.durable_item(entry)?)?;
2977
2978 if Self::should_audit_log_item(&to_item) {
2979 let mut full_name = Self::full_name_detail(
2980 &state.resolve_full_name(&name, session.map(|session| session.conn_id())),
2981 );
2982 full_name.item = name.item;
2983
2984 CatalogState::add_to_audit_log(
2985 &state.system_configuration,
2986 oracle_write_ts,
2987 session,
2988 tx,
2989 audit_events,
2990 EventType::Alter,
2991 catalog_type_to_audit_object_type(to_item.typ()),
2992 EventDetails::UpdateItemV1(mz_audit_log::UpdateItemV1 {
2993 id: id.to_string(),
2994 name: full_name,
2995 }),
2996 )?;
2997 }
2998
2999 Self::log_update(state, &id);
3000 }
3001 Op::UpdateSystemConfiguration { name, value } => {
3002 let parsed_value = state.parse_system_configuration(&name, value.borrow())?;
3003 tx.upsert_system_config(&name, parsed_value.clone())?;
3004 if name == WITH_0DT_DEPLOYMENT_MAX_WAIT.name() {
3009 let with_0dt_deployment_max_wait =
3010 Duration::parse(VarInput::Flat(&parsed_value))
3011 .expect("parsing succeeded above");
3012 tx.set_0dt_deployment_max_wait(with_0dt_deployment_max_wait)?;
3013 } else if name == WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name() {
3014 let with_0dt_deployment_ddl_check_interval =
3015 Duration::parse(VarInput::Flat(&parsed_value))
3016 .expect("parsing succeeded above");
3017 tx.set_0dt_deployment_ddl_check_interval(
3018 with_0dt_deployment_ddl_check_interval,
3019 )?;
3020 } else if name == ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name() {
3021 let panic_after_timeout =
3022 strconv::parse_bool(&parsed_value).expect("parsing succeeded above");
3023 tx.set_enable_0dt_deployment_panic_after_timeout(panic_after_timeout)?;
3024 }
3025
3026 CatalogState::add_to_audit_log(
3027 &state.system_configuration,
3028 oracle_write_ts,
3029 session,
3030 tx,
3031 audit_events,
3032 EventType::Alter,
3033 ObjectType::System,
3034 EventDetails::SetV1(mz_audit_log::SetV1 {
3035 name,
3036 value: Some(value.borrow().to_vec().join(", ")),
3037 }),
3038 )?;
3039 }
3040 Op::ResetSystemConfiguration { name } => {
3041 tx.remove_system_config(&name);
3042 if name == WITH_0DT_DEPLOYMENT_MAX_WAIT.name() {
3047 tx.reset_0dt_deployment_max_wait()?;
3048 } else if name == WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name() {
3049 tx.reset_0dt_deployment_ddl_check_interval()?;
3050 } else if name == ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name() {
3051 tx.reset_enable_0dt_deployment_panic_after_timeout()?;
3052 }
3053
3054 CatalogState::add_to_audit_log(
3055 &state.system_configuration,
3056 oracle_write_ts,
3057 session,
3058 tx,
3059 audit_events,
3060 EventType::Alter,
3061 ObjectType::System,
3062 EventDetails::SetV1(mz_audit_log::SetV1 { name, value: None }),
3063 )?;
3064 }
3065 Op::ResetAllSystemConfiguration => {
3066 tx.clear_system_configs();
3067 tx.reset_0dt_deployment_max_wait()?;
3068 tx.reset_0dt_deployment_ddl_check_interval()?;
3069 tx.reset_enable_0dt_deployment_panic_after_timeout()?;
3070
3071 CatalogState::add_to_audit_log(
3072 &state.system_configuration,
3073 oracle_write_ts,
3074 session,
3075 tx,
3076 audit_events,
3077 EventType::Alter,
3078 ObjectType::System,
3079 EventDetails::ResetAllV1,
3080 )?;
3081 }
3082 Op::UpdateScopedSystemParameters {
3083 scoped,
3084 prune_scope,
3085 } => {
3086 let live_clusters: BTreeSet<ClusterId> =
3097 tx.get_clusters().map(|cluster| cluster.id).collect();
3098 let live_replicas: BTreeSet<ReplicaId> = tx
3099 .get_cluster_replicas()
3100 .map(|replica| replica.replica_id)
3101 .collect();
3102
3103 let existing_cluster: BTreeMap<(ClusterId, String), String> = tx
3105 .get_cluster_system_configurations()
3106 .map(|c| ((c.cluster_id, c.name), c.value))
3107 .collect();
3108 let mut desired_cluster: BTreeSet<(ClusterId, String)> = BTreeSet::new();
3109 for (cluster_id, values) in &scoped.cluster {
3110 if !live_clusters.contains(cluster_id) {
3111 continue;
3112 }
3113 for (name, value) in values {
3114 desired_cluster.insert((*cluster_id, name.clone()));
3115 if existing_cluster.get(&(*cluster_id, name.clone())) != Some(value) {
3116 tx.upsert_cluster_system_config(*cluster_id, name, value.clone())?;
3117 }
3118 }
3119 }
3120 for (cluster_id, name) in existing_cluster.into_keys() {
3121 if (!live_clusters.contains(&cluster_id)
3122 || prune_scope.clusters.contains(&cluster_id))
3123 && !desired_cluster.contains(&(cluster_id, name.clone()))
3124 {
3125 tx.remove_cluster_system_config(cluster_id, &name);
3126 }
3127 }
3128
3129 let existing_replica: BTreeMap<(ReplicaId, String), String> = tx
3131 .get_replica_system_configurations()
3132 .map(|r| ((r.replica_id, r.name), r.value))
3133 .collect();
3134 let mut desired_replica: BTreeSet<(ReplicaId, String)> = BTreeSet::new();
3135 for (replica_id, values) in &scoped.replica {
3136 if !live_replicas.contains(replica_id) {
3137 continue;
3138 }
3139 for (name, value) in values {
3140 desired_replica.insert((*replica_id, name.clone()));
3141 if existing_replica.get(&(*replica_id, name.clone())) != Some(value) {
3142 tx.upsert_replica_system_config(*replica_id, name, value.clone())?;
3143 }
3144 }
3145 }
3146 for (replica_id, name) in existing_replica.into_keys() {
3147 if (!live_replicas.contains(&replica_id)
3148 || prune_scope.replicas.contains(&replica_id))
3149 && !desired_replica.contains(&(replica_id, name.clone()))
3150 {
3151 tx.remove_replica_system_config(replica_id, &name);
3152 }
3153 }
3154 }
3155 Op::InjectAuditEvents { events } => {
3156 for event in events {
3157 let id = tx.allocate_audit_log_id()?;
3158 let ev = VersionedEvent::new(
3159 id,
3160 event.event_type,
3161 event.object_type,
3162 event.details,
3163 event.user,
3164 oracle_write_ts.into(),
3165 );
3166 audit_events.push(ev.clone());
3167 tx.insert_audit_log_event(ev);
3168 }
3169 }
3170 };
3171 Ok(())
3172 }
3173
3174 fn log_update(state: &CatalogState, id: &CatalogItemId) {
3175 let entry = state.get_entry(id);
3176 info!(
3177 "update {} {} ({})",
3178 entry.item_type(),
3179 state.resolve_full_name(entry.name(), entry.conn_id()),
3180 id
3181 );
3182 }
3183
3184 fn update_privilege_owners(
3188 privileges: &mut PrivilegeMap,
3189 old_owner: RoleId,
3190 new_owner: RoleId,
3191 ) {
3192 let mut flat_privileges: Vec<_> = privileges.all_values_owned().collect();
3194
3195 let mut new_present = false;
3196 for privilege in flat_privileges.iter_mut() {
3197 if privilege.grantor == old_owner {
3200 privilege.grantor = new_owner;
3201 } else if privilege.grantor == new_owner {
3202 new_present = true;
3203 }
3204 if privilege.grantee == old_owner {
3206 privilege.grantee = new_owner;
3207 } else if privilege.grantee == new_owner {
3208 new_present = true;
3209 }
3210 }
3211
3212 if new_present {
3216 let privilege_map: BTreeMap<_, Vec<_>> =
3218 flat_privileges
3219 .into_iter()
3220 .fold(BTreeMap::new(), |mut accum, privilege| {
3221 accum
3222 .entry((privilege.grantee, privilege.grantor))
3223 .or_default()
3224 .push(privilege);
3225 accum
3226 });
3227
3228 flat_privileges = privilege_map
3230 .into_iter()
3231 .map(|((grantee, grantor), values)|
3232 values.into_iter().fold(
3234 MzAclItem::empty(grantee, grantor),
3235 |mut accum, mz_aclitem| {
3236 accum.acl_mode =
3237 accum.acl_mode.union(mz_aclitem.acl_mode);
3238 accum
3239 },
3240 ))
3241 .collect();
3242 }
3243
3244 *privileges = PrivilegeMap::from_mz_acl_items(flat_privileges);
3245 }
3246}
3247
3248fn temporary_item_owner_session(
3255 state: &CatalogState,
3256 session: Option<&ConnMeta>,
3257 item: &CatalogItem,
3258 item_name: &str,
3259) -> Result<Uuid, AdapterError> {
3260 let session = session.ok_or_else(|| {
3261 AdapterError::Internal("temporary items must have an owner session".to_string())
3262 })?;
3263 let owner_session = session.uuid();
3264 soft_assert_or_log!(
3265 Some(session.conn_id()) == item.conn_id(),
3266 "temporary item connection must match the creating session"
3267 );
3268 if state.temporary_namespaces.uuid_for_conn(session.conn_id()) != Some(owner_session) {
3269 return Err(AdapterError::Internal(format!(
3270 "connection {} has no temporary namespace while creating temporary item {}",
3271 session.conn_id(),
3272 item_name,
3273 )));
3274 }
3275 Ok(owner_session)
3276}
3277
3278fn tx_replace_item(
3287 tx: &mut Transaction<'_>,
3288 state: &CatalogState,
3289 id: CatalogItemId,
3290 new_entry: CatalogEntry,
3291) -> Result<(), AdapterError> {
3292 let new_id = new_entry.id;
3293
3294 for use_id in new_entry.referenced_by() {
3296 let dependent = state.get_entry(use_id);
3297
3298 if tx.get_item(use_id).is_none() {
3300 continue;
3301 }
3302
3303 let mut dependent = dependent.clone();
3304 dependent.item = dependent.item.replace_item_refs(id, new_id);
3305 tx.update_item(*use_id, state.durable_item(dependent)?)?;
3306 }
3307
3308 let old_comment_id = state.get_comment_id(ObjectId::Item(id));
3310 let new_comment_id = new_entry.comment_object_id();
3311 if let Some(comments) = state.comments.get_object_comments(old_comment_id) {
3312 tx.drop_comments(&[old_comment_id].into())?;
3313 for (sub, comment) in comments {
3314 tx.update_comment(new_comment_id, *sub, Some(comment.clone()))?;
3315 }
3316 }
3317
3318 let mz_catalog::durable::Item {
3319 id: _,
3320 oid,
3321 global_id,
3322 schema_id,
3323 name,
3324 create_sql,
3325 owner_id,
3326 privileges,
3327 extra_versions,
3328 ephemeral_owner_session,
3329 } = state.durable_item(new_entry)?;
3330
3331 tx.remove_item(id)?;
3332 tx.insert_item(
3333 new_id,
3334 oid,
3335 global_id,
3336 schema_id,
3337 &name,
3338 create_sql,
3339 owner_id,
3340 privileges,
3341 extra_versions,
3342 ephemeral_owner_session,
3343 )?;
3344
3345 Ok(())
3346}
3347
3348fn apply_replacement_audit_events(
3350 state: &CatalogState,
3351 target: &CatalogEntry,
3352 replacement: &CatalogEntry,
3353) -> Vec<(EventType, EventDetails)> {
3354 let mut events = Vec::new();
3355
3356 let target_name = state.resolve_full_name(target.name(), target.conn_id());
3357 let target_id_name = IdFullNameV1 {
3358 id: target.id().to_string(),
3359 name: Catalog::full_name_detail(&target_name),
3360 };
3361 let replacement_name = state.resolve_full_name(replacement.name(), replacement.conn_id());
3362 let replacement_id_name = IdFullNameV1 {
3363 id: replacement.id().to_string(),
3364 name: Catalog::full_name_detail(&replacement_name),
3365 };
3366
3367 if Catalog::should_audit_log_item(&replacement.item) {
3368 events.push((
3369 EventType::Drop,
3370 EventDetails::IdFullNameV1(replacement_id_name.clone()),
3371 ));
3372 }
3373
3374 if Catalog::should_audit_log_item(&target.item) {
3375 events.push((
3376 EventType::Alter,
3377 EventDetails::AlterApplyReplacementV1(mz_audit_log::AlterApplyReplacementV1 {
3378 target: target_id_name.clone(),
3379 replacement: replacement_id_name,
3380 }),
3381 ));
3382
3383 if let Some(old_cluster_id) = target.cluster_id()
3384 && let Some(new_cluster_id) = replacement.cluster_id()
3385 && old_cluster_id != new_cluster_id
3386 {
3387 events.push((
3390 EventType::Alter,
3391 EventDetails::AlterSetClusterV1(mz_audit_log::AlterSetClusterV1 {
3392 id: replacement.id().to_string(),
3393 name: target_id_name.name,
3394 old_cluster_id: old_cluster_id.to_string(),
3395 new_cluster_id: new_cluster_id.to_string(),
3396 }),
3397 ));
3398 }
3399 }
3400
3401 events
3402}
3403
3404#[derive(Debug, Default)]
3412pub(crate) struct ObjectsToDrop {
3413 pub comments: BTreeSet<CommentObjectId>,
3414 pub databases: BTreeSet<DatabaseId>,
3415 pub schemas: BTreeMap<SchemaSpecifier, ResolvedDatabaseSpecifier>,
3416 pub clusters: BTreeSet<ClusterId>,
3417 pub replicas: BTreeMap<ReplicaId, (ClusterId, ReplicaCreateDropReason)>,
3418 pub roles: BTreeSet<RoleId>,
3419 pub items: Vec<CatalogItemId>,
3420 pub network_policies: BTreeSet<NetworkPolicyId>,
3421}
3422
3423impl ObjectsToDrop {
3424 pub fn generate(
3425 drop_object_infos: impl IntoIterator<Item = DropObjectInfo>,
3426 state: &CatalogState,
3427 session: Option<&ConnMeta>,
3428 ) -> Result<Self, AdapterError> {
3429 let mut delta = ObjectsToDrop::default();
3430
3431 for drop_object_info in drop_object_infos {
3432 delta.add_item(drop_object_info, state, session)?;
3433 }
3434
3435 Ok(delta)
3436 }
3437
3438 fn add_item(
3439 &mut self,
3440 drop_object_info: DropObjectInfo,
3441 state: &CatalogState,
3442 session: Option<&ConnMeta>,
3443 ) -> Result<(), AdapterError> {
3444 self.comments
3445 .insert(state.get_comment_id(drop_object_info.to_object_id()));
3446
3447 match drop_object_info {
3448 DropObjectInfo::Database(database_id) => {
3449 let database = &state.database_by_id[&database_id];
3450 if database_id.is_system() {
3451 return Err(AdapterError::Catalog(Error::new(
3452 ErrorKind::ReadOnlyDatabase(database.name().to_string()),
3453 )));
3454 }
3455
3456 self.databases.insert(database_id);
3457 }
3458 DropObjectInfo::Schema((database_spec, schema_spec)) => {
3459 let schema = state.get_schema(
3460 &database_spec,
3461 &schema_spec,
3462 session
3463 .map(|session| session.conn_id())
3464 .unwrap_or(&SYSTEM_CONN_ID),
3465 );
3466 let schema_id: SchemaId = schema_spec.into();
3467 if schema_id.is_system() {
3468 let name = schema.name();
3469 let full_name = state.resolve_full_schema_name(name);
3470 return Err(AdapterError::Catalog(Error::new(
3471 ErrorKind::ReadOnlySystemSchema(full_name.to_string()),
3472 )));
3473 }
3474
3475 self.schemas.insert(schema_spec, database_spec);
3476 }
3477 DropObjectInfo::Role(role_id) => {
3478 let name = state.get_role(&role_id).name().to_string();
3479 if role_id.is_system() || role_id.is_predefined() {
3480 return Err(AdapterError::Catalog(Error::new(
3481 ErrorKind::ReservedRoleName(name.clone()),
3482 )));
3483 }
3484 state.ensure_not_reserved_role(&role_id)?;
3485
3486 self.roles.insert(role_id);
3487 }
3488 DropObjectInfo::Cluster(cluster_id) => {
3489 let cluster = state.get_cluster(cluster_id);
3490 let name = &cluster.name;
3491 if cluster_id.is_system() {
3492 return Err(AdapterError::Catalog(Error::new(
3493 ErrorKind::ReadOnlyCluster(name.clone()),
3494 )));
3495 }
3496
3497 self.clusters.insert(cluster_id);
3498 }
3499 DropObjectInfo::ClusterReplica((cluster_id, replica_id, reason)) => {
3500 let cluster = state.get_cluster(cluster_id);
3501 let replica = cluster.replica(replica_id).expect("Must exist");
3502
3503 self.replicas
3504 .insert(replica.replica_id, (cluster.id, reason));
3505
3506 let mut seen: BTreeSet<ObjectId> =
3525 self.items.iter().copied().map(ObjectId::Item).collect();
3526 for dep in state.cluster_replica_dependents(cluster_id, replica_id, &mut seen) {
3527 if let ObjectId::Item(dep_id) = dep {
3528 info!(
3529 "implicitly dropping {} because target replica was dropped",
3530 state.get_entry(&dep_id).name().item,
3531 );
3532 self.comments
3533 .insert(state.get_comment_id(ObjectId::Item(dep_id)));
3534 self.items.push(dep_id);
3535 }
3536 }
3537 }
3538 DropObjectInfo::Item(item_id) => {
3539 let entry = state.get_entry(&item_id);
3540 if item_id.is_system() {
3541 let name = entry.name();
3542 let full_name =
3543 state.resolve_full_name(name, session.map(|session| session.conn_id()));
3544 return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
3545 full_name.to_string(),
3546 ))));
3547 }
3548
3549 self.items.push(item_id);
3550 }
3551 DropObjectInfo::NetworkPolicy(network_policy_id) => {
3552 let policy = state.get_network_policy(&network_policy_id);
3553 let name = &policy.name;
3554 if network_policy_id.is_system() {
3555 return Err(AdapterError::Catalog(Error::new(
3556 ErrorKind::ReadOnlyNetworkPolicy(name.clone()),
3557 )));
3558 }
3559
3560 self.network_policies.insert(network_policy_id);
3561 }
3562 }
3563
3564 Ok(())
3565 }
3566}
3567
3568#[cfg(test)]
3569mod tests {
3570 use std::sync::{Arc, Mutex};
3571
3572 use mz_catalog::SYSTEM_CONN_ID;
3573 use mz_catalog::memory::objects::{CatalogItem, Table, TableDataSource};
3574 use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap};
3575 use mz_repr::role_id::RoleId;
3576 use mz_repr::{RelationDesc, RelationVersion, VersionedRelationDesc};
3577 use mz_sql::DEFAULT_SCHEMA;
3578 use mz_sql::catalog::CatalogDatabase;
3579 use mz_sql::names::{
3580 ItemQualifiers, QualifiedItemName, ResolvedDatabaseSpecifier, ResolvedIds,
3581 };
3582 use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
3583 use mz_sql::session::vars::{MAX_CONNECTIONS, OwnedVarInput, SystemVars};
3584
3585 use crate::catalog::{Catalog, Op};
3586 use crate::session::DEFAULT_DATABASE_NAME;
3587
3588 #[mz_ore::test]
3589 fn test_reconfiguration_audit_details() {
3590 use std::time::Duration;
3591
3592 use mz_adapter_types::cluster_state::ReconfigurationAudit;
3593 use mz_audit_log::ReconfigurationLifecycleV1;
3594 use mz_catalog::memory::objects::{
3595 ClusterConfig, ClusterVariant, ClusterVariantManaged, ReconfigurationState,
3596 ReconfigurationStatus, ReconfigurationTarget,
3597 };
3598 use mz_controller::clusters::ReplicaLogging;
3599 use mz_controller_types::ClusterId;
3600 use mz_repr::Timestamp;
3601 use mz_repr::optimize::OptimizerFeatureOverrides;
3602
3603 let cluster_id = ClusterId::user(1).expect("valid id");
3604 let logging = ReplicaLogging {
3605 log_logging: false,
3606 interval: None,
3607 };
3608 let managed = |reconfiguration: Option<ReconfigurationState>| ClusterConfig {
3609 variant: ClusterVariant::Managed(ClusterVariantManaged {
3610 size: "small".into(),
3611 availability_zones: Vec::new(),
3612 logging: logging.clone(),
3613 arrangement_compression: false,
3614 replication_factor: 1,
3615 optimizer_feature_overrides: OptimizerFeatureOverrides::default(),
3616 schedule: Default::default(),
3617 auto_scaling_strategy: None,
3618 reconfiguration,
3619 burst: None,
3620 }),
3621 workload_class: None,
3622 };
3623 let record = |status| ReconfigurationState {
3624 target: ReconfigurationTarget {
3625 size: "large".into(),
3626 replication_factor: 2,
3627 availability_zones: vec!["az1".into(), "az2".into()],
3628 logging: ReplicaLogging {
3629 log_logging: true,
3630 interval: Some(Duration::from_secs(5)),
3631 },
3632 arrangement_compression: false,
3633 },
3634 deadline: Timestamp::from(400u64),
3635 on_timeout: mz_sql::plan::OnTimeoutAction::Rollback,
3636 status,
3637 };
3638
3639 let details = Catalog::reconfiguration_audit_details(
3640 &managed(Some(record(ReconfigurationStatus::InProgress))),
3641 cluster_id,
3642 "c",
3643 ReconfigurationAudit::Started,
3644 )
3645 .expect("record supplies audit details");
3646 assert_eq!(details.cluster_id, cluster_id.to_string());
3647 assert_eq!(details.cluster_name, "c");
3648 assert_eq!(details.transition, ReconfigurationLifecycleV1::Started);
3649 assert_eq!(details.forced, None);
3650 assert_eq!(details.target_size, "large");
3651 assert_eq!(details.target_replication_factor, 2);
3652 assert_eq!(
3653 details.target_availability_zones,
3654 vec!["az1".to_string(), "az2".to_string()]
3655 );
3656 assert_eq!(details.target_logging.log_logging, true);
3657 assert_eq!(
3658 details.target_logging.interval,
3659 Some(Duration::from_secs(5))
3660 );
3661 assert_eq!(details.deadline, Some(400));
3662
3663 let forced = Catalog::reconfiguration_audit_details(
3667 &managed(Some(record(ReconfigurationStatus::Finalized))),
3668 cluster_id,
3669 "c",
3670 ReconfigurationAudit::Finalized { forced: true },
3671 )
3672 .expect("a coherent finalize supplies audit details");
3673 assert_eq!(forced.transition, ReconfigurationLifecycleV1::Finalized);
3674 assert_eq!(forced.forced, Some(true));
3675
3676 let incoherent = Catalog::reconfiguration_audit_details(
3679 &managed(Some(record(ReconfigurationStatus::TimedOut))),
3680 cluster_id,
3681 "c",
3682 ReconfigurationAudit::ResourceExhausted,
3683 );
3684 assert!(incoherent.is_err());
3685
3686 let missing = Catalog::reconfiguration_audit_details(
3687 &managed(None),
3688 cluster_id,
3689 "c",
3690 ReconfigurationAudit::Started,
3691 );
3692 assert!(missing.is_err());
3693 }
3694
3695 #[mz_ore::test]
3696 fn test_reconfiguration_lifecycle_moved() {
3697 use std::time::Duration;
3698
3699 use mz_catalog::memory::objects::{
3700 ClusterConfig, ClusterVariant, ClusterVariantManaged, ReconfigurationState,
3701 ReconfigurationStatus, ReconfigurationTarget,
3702 };
3703 use mz_controller::clusters::ReplicaLogging;
3704 use mz_repr::Timestamp;
3705 use mz_repr::optimize::OptimizerFeatureOverrides;
3706
3707 let managed = |reconfiguration: Option<ReconfigurationState>| ClusterConfig {
3708 variant: ClusterVariant::Managed(ClusterVariantManaged {
3709 size: "small".into(),
3710 availability_zones: Vec::new(),
3711 logging: ReplicaLogging {
3712 log_logging: false,
3713 interval: None,
3714 },
3715 arrangement_compression: false,
3716 replication_factor: 1,
3717 optimizer_feature_overrides: OptimizerFeatureOverrides::default(),
3718 schedule: Default::default(),
3719 auto_scaling_strategy: None,
3720 reconfiguration,
3721 burst: None,
3722 }),
3723 workload_class: None,
3724 };
3725 let unmanaged = ClusterConfig {
3726 variant: ClusterVariant::Unmanaged,
3727 workload_class: None,
3728 };
3729 let record = |status| ReconfigurationState {
3730 target: ReconfigurationTarget {
3731 size: "large".into(),
3732 replication_factor: 2,
3733 availability_zones: Vec::new(),
3734 logging: ReplicaLogging {
3735 log_logging: false,
3736 interval: Some(Duration::from_secs(1)),
3737 },
3738 arrangement_compression: false,
3739 },
3740 deadline: Timestamp::from(400u64),
3741 on_timeout: mz_sql::plan::OnTimeoutAction::Rollback,
3742 status,
3743 };
3744
3745 assert!(Catalog::reconfiguration_lifecycle_moved(
3748 &managed(None),
3749 &managed(Some(record(ReconfigurationStatus::InProgress))),
3750 ));
3751 assert!(Catalog::reconfiguration_lifecycle_moved(
3753 &managed(Some(record(ReconfigurationStatus::InProgress))),
3754 &managed(Some(record(ReconfigurationStatus::Finalized))),
3755 ));
3756 assert!(Catalog::reconfiguration_lifecycle_moved(
3759 &managed(Some(record(ReconfigurationStatus::InProgress))),
3760 &unmanaged,
3761 ));
3762
3763 assert!(!Catalog::reconfiguration_lifecycle_moved(
3766 &managed(None),
3767 &managed(None),
3768 ));
3769 assert!(!Catalog::reconfiguration_lifecycle_moved(
3770 &managed(Some(record(ReconfigurationStatus::InProgress))),
3771 &managed(Some(record(ReconfigurationStatus::InProgress))),
3772 ));
3773 assert!(!Catalog::reconfiguration_lifecycle_moved(
3774 &managed(Some(record(ReconfigurationStatus::Cancelled))),
3775 &unmanaged,
3776 ));
3777 }
3778
3779 #[mz_ore::test]
3780 fn test_burst_lifecycle_moved() {
3781 use std::time::Duration;
3782
3783 use mz_catalog::memory::objects::{
3784 BurstState, ClusterConfig, ClusterVariant, ClusterVariantManaged,
3785 };
3786 use mz_controller::clusters::ReplicaLogging;
3787 use mz_repr::Timestamp;
3788 use mz_repr::optimize::OptimizerFeatureOverrides;
3789
3790 let managed = |burst: Option<BurstState>| ClusterConfig {
3791 variant: ClusterVariant::Managed(ClusterVariantManaged {
3792 size: "small".into(),
3793 availability_zones: Vec::new(),
3794 logging: ReplicaLogging {
3795 log_logging: false,
3796 interval: None,
3797 },
3798 arrangement_compression: false,
3799 replication_factor: 1,
3800 optimizer_feature_overrides: OptimizerFeatureOverrides::default(),
3801 schedule: Default::default(),
3802 auto_scaling_strategy: None,
3803 reconfiguration: None,
3804 burst,
3805 }),
3806 workload_class: None,
3807 };
3808 let unmanaged = ClusterConfig {
3809 variant: ClusterVariant::Unmanaged,
3810 workload_class: None,
3811 };
3812 let record = |steady_hydrated_at| BurstState {
3813 burst_size: "large".into(),
3814 linger_duration: Duration::from_secs(60),
3815 steady_hydrated_at,
3816 };
3817
3818 assert!(Catalog::burst_lifecycle_moved(
3821 &managed(None),
3822 &managed(Some(record(None))),
3823 ));
3824 assert!(Catalog::burst_lifecycle_moved(
3826 &managed(Some(record(None))),
3827 &managed(None),
3828 ));
3829 assert!(Catalog::burst_lifecycle_moved(
3832 &managed(Some(record(None))),
3833 &unmanaged,
3834 ));
3835
3836 assert!(!Catalog::burst_lifecycle_moved(
3839 &managed(None),
3840 &managed(None)
3841 ));
3842 assert!(!Catalog::burst_lifecycle_moved(
3843 &managed(Some(record(None))),
3844 &managed(Some(record(Some(Timestamp::from(100u64))))),
3845 ));
3846 assert!(!Catalog::burst_lifecycle_moved(
3847 &managed(Some(record(Some(Timestamp::from(100u64))))),
3848 &managed(Some(record(None))),
3849 ));
3850 }
3851
3852 #[mz_ore::test]
3853 fn test_has_unwarranted_burst_record() {
3854 use std::time::Duration;
3855
3856 use mz_catalog::memory::objects::{BurstState, ClusterVariantManaged};
3857 use mz_controller::clusters::ReplicaLogging;
3858 use mz_repr::optimize::OptimizerFeatureOverrides;
3859 use mz_sql::plan::{AutoScalingStrategy, OnHydration};
3860
3861 let managed = |replication_factor: u32,
3862 strategy: Option<AutoScalingStrategy>,
3863 burst: Option<BurstState>| ClusterVariantManaged {
3864 size: "small".into(),
3865 availability_zones: Vec::new(),
3866 logging: ReplicaLogging {
3867 log_logging: false,
3868 interval: None,
3869 },
3870 arrangement_compression: false,
3871 replication_factor,
3872 optimizer_feature_overrides: OptimizerFeatureOverrides::default(),
3873 schedule: Default::default(),
3874 auto_scaling_strategy: strategy,
3875 reconfiguration: None,
3876 burst,
3877 };
3878 let policy = |hydration_size: &str| AutoScalingStrategy {
3879 on_hydration: Some(OnHydration {
3880 hydration_size: hydration_size.into(),
3881 linger_duration: None,
3882 }),
3883 };
3884 let record = || BurstState {
3885 burst_size: "large".into(),
3886 linger_duration: Duration::from_secs(60),
3887 steady_hydrated_at: None,
3888 };
3889
3890 assert!(!managed(1, None, None).has_unwarranted_burst_record());
3892 assert!(!managed(1, Some(policy("large")), None).has_unwarranted_burst_record());
3893
3894 assert!(!managed(1, Some(policy("large")), Some(record())).has_unwarranted_burst_record());
3896
3897 assert!(managed(1, None, Some(record())).has_unwarranted_burst_record());
3899 assert!(
3900 managed(
3901 1,
3902 Some(AutoScalingStrategy { on_hydration: None }),
3903 Some(record())
3904 )
3905 .has_unwarranted_burst_record()
3906 );
3907 assert!(managed(1, Some(policy("xlarge")), Some(record())).has_unwarranted_burst_record());
3908 assert!(managed(0, Some(policy("large")), Some(record())).has_unwarranted_burst_record());
3909 }
3910
3911 #[mz_ore::test]
3912 fn test_replica_create_drop_reason_into_audit_log() {
3913 use std::time::Duration;
3914
3915 use mz_audit_log::{CreateOrDropClusterReplicaReasonV1, SchedulingDecisionV1};
3916 use mz_cluster_controller::ctx::RefreshWindowDecision;
3917 use mz_repr::GlobalId;
3918
3919 use crate::catalog::ReplicaCreateDropReason;
3920
3921 let (reason, scheduling_policies) =
3927 ReplicaCreateDropReason::OnRefresh(RefreshWindowDecision {
3928 objects_needing_refresh: vec![GlobalId::User(1)],
3929 objects_needing_compaction: vec![GlobalId::User(2), GlobalId::User(3)],
3930 hydration_time_estimate: Duration::from_secs(995),
3931 })
3932 .into_audit_log();
3933 assert_eq!(reason, CreateOrDropClusterReplicaReasonV1::Schedule);
3934 let blob = scheduling_policies.expect("on-refresh create carries the detail");
3935 assert_eq!(blob.on_refresh.decision, SchedulingDecisionV1::On);
3936 assert_eq!(blob.on_refresh.objects_needing_refresh, vec!["u1"]);
3937 assert_eq!(blob.on_refresh.objects_needing_compaction, vec!["u2", "u3"]);
3938 assert_eq!(blob.on_refresh.hydration_time_estimate, "00:16:35");
3939
3940 let (reason, scheduling_policies) = ReplicaCreateDropReason::Retired.into_audit_log();
3943 assert_eq!(reason, CreateOrDropClusterReplicaReasonV1::Retired);
3944 assert!(scheduling_policies.is_none());
3945 }
3946
3947 #[mz_ore::test]
3948 fn test_burst_audit_details() {
3949 use std::time::Duration;
3950
3951 use mz_adapter_types::cluster_state::{BurstAudit, BurstFinishCause};
3952 use mz_audit_log::{BurstFinishCauseV1, HydrationBurstLifecycleV1};
3953 use mz_catalog::memory::objects::{
3954 BurstState, ClusterConfig, ClusterVariant, ClusterVariantManaged,
3955 };
3956 use mz_controller::clusters::ReplicaLogging;
3957 use mz_controller_types::ClusterId;
3958 use mz_repr::optimize::OptimizerFeatureOverrides;
3959
3960 let cluster_id = ClusterId::user(1).expect("valid id");
3961 let managed = |burst: Option<BurstState>| ClusterConfig {
3962 variant: ClusterVariant::Managed(ClusterVariantManaged {
3963 size: "small".into(),
3964 availability_zones: Vec::new(),
3965 logging: ReplicaLogging {
3966 log_logging: false,
3967 interval: None,
3968 },
3969 arrangement_compression: false,
3970 replication_factor: 1,
3971 optimizer_feature_overrides: OptimizerFeatureOverrides::default(),
3972 schedule: Default::default(),
3973 auto_scaling_strategy: None,
3974 reconfiguration: None,
3975 burst,
3976 }),
3977 workload_class: None,
3978 };
3979 let record = || BurstState {
3980 burst_size: "large".into(),
3981 linger_duration: Duration::from_secs(60),
3982 steady_hydrated_at: None,
3983 };
3984
3985 let started = Catalog::burst_audit_details(
3987 &managed(None),
3988 &managed(Some(record())),
3989 cluster_id,
3990 "c",
3991 BurstAudit::Started,
3992 )
3993 .expect("armed record supplies audit details");
3994 assert_eq!(started.transition, HydrationBurstLifecycleV1::Started);
3995 assert_eq!(started.finish_cause, None);
3996 assert_eq!(started.burst_size, "large");
3997
3998 let finished = Catalog::burst_audit_details(
4001 &managed(Some(record())),
4002 &managed(None),
4003 cluster_id,
4004 "c",
4005 BurstAudit::Finished {
4006 cause: BurstFinishCause::LingerElapsed,
4007 },
4008 )
4009 .expect("cleared record supplies audit details");
4010 assert_eq!(finished.transition, HydrationBurstLifecycleV1::Finished);
4011 assert_eq!(
4012 finished.finish_cause,
4013 Some(BurstFinishCauseV1::LingerElapsed)
4014 );
4015 assert_eq!(finished.burst_size, "large");
4016
4017 let incoherent = Catalog::burst_audit_details(
4020 &managed(None),
4021 &managed(None),
4022 cluster_id,
4023 "c",
4024 BurstAudit::Started,
4025 );
4026 assert!(incoherent.is_err());
4027 }
4028
4029 #[mz_ore::test]
4030 fn test_update_privilege_owners() {
4031 let old_owner = RoleId::User(1);
4032 let new_owner = RoleId::User(2);
4033 let other_role = RoleId::User(3);
4034
4035 let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
4037 MzAclItem {
4038 grantee: other_role,
4039 grantor: old_owner,
4040 acl_mode: AclMode::UPDATE,
4041 },
4042 MzAclItem {
4043 grantee: other_role,
4044 grantor: new_owner,
4045 acl_mode: AclMode::SELECT,
4046 },
4047 ]);
4048 Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
4049 assert_eq!(1, privileges.all_values().count());
4050 assert_eq!(
4051 vec![MzAclItem {
4052 grantee: other_role,
4053 grantor: new_owner,
4054 acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
4055 }],
4056 privileges.all_values_owned().collect::<Vec<_>>()
4057 );
4058
4059 let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
4061 MzAclItem {
4062 grantee: old_owner,
4063 grantor: other_role,
4064 acl_mode: AclMode::UPDATE,
4065 },
4066 MzAclItem {
4067 grantee: new_owner,
4068 grantor: other_role,
4069 acl_mode: AclMode::SELECT,
4070 },
4071 ]);
4072 Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
4073 assert_eq!(1, privileges.all_values().count());
4074 assert_eq!(
4075 vec![MzAclItem {
4076 grantee: new_owner,
4077 grantor: other_role,
4078 acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
4079 }],
4080 privileges.all_values_owned().collect::<Vec<_>>()
4081 );
4082
4083 let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
4085 MzAclItem {
4086 grantee: old_owner,
4087 grantor: old_owner,
4088 acl_mode: AclMode::UPDATE,
4089 },
4090 MzAclItem {
4091 grantee: new_owner,
4092 grantor: new_owner,
4093 acl_mode: AclMode::SELECT,
4094 },
4095 ]);
4096 Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
4097 assert_eq!(1, privileges.all_values().count());
4098 assert_eq!(
4099 vec![MzAclItem {
4100 grantee: new_owner,
4101 grantor: new_owner,
4102 acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
4103 }],
4104 privileges.all_values_owned().collect::<Vec<_>>()
4105 );
4106 }
4107
4108 #[mz_ore::test(tokio::test)]
4115 #[cfg_attr(miri, ignore)] async fn test_transact_incremental_dry_run_processes_only_new_ops() {
4117 Catalog::with_debug(|catalog| async move {
4118 let database = catalog
4120 .resolve_database(DEFAULT_DATABASE_NAME)
4121 .expect("default database");
4122 let database_name = database.name.clone();
4123 let database_spec = ResolvedDatabaseSpecifier::Id(database.id());
4124 let schema = catalog
4125 .resolve_schema_in_database(&database_spec, DEFAULT_SCHEMA, &SYSTEM_CONN_ID)
4126 .expect("default schema");
4127 let schema_name = schema.name.schema.clone();
4128 let schema_spec = schema.id.clone();
4129
4130 let (id_t1, global_id_t1) = catalog
4132 .allocate_user_id_for_test()
4133 .await
4134 .expect("allocate id for t1");
4135 let (id_t2, global_id_t2) = catalog
4136 .allocate_user_id_for_test()
4137 .await
4138 .expect("allocate id for t2");
4139
4140 let oracle_write_ts = catalog.current_upper().await;
4141
4142 let make_table_op = |id, global_id, name: &str| Op::CreateItem {
4144 id,
4145 name: QualifiedItemName {
4146 qualifiers: ItemQualifiers {
4147 database_spec: database_spec.clone(),
4148 schema_spec: schema_spec.clone(),
4149 },
4150 item: name.to_string(),
4151 },
4152 item: CatalogItem::Table(Table {
4153 create_sql: Some(format!(
4154 "CREATE TABLE {database_name}.{schema_name}.{name} ()"
4155 )),
4156 desc: VersionedRelationDesc::new(RelationDesc::empty()),
4157 collections: [(RelationVersion::root(), global_id)].into_iter().collect(),
4158 conn_id: None,
4159 resolved_ids: ResolvedIds::empty(),
4160 custom_logical_compaction_window: None,
4161 is_retained_metrics_object: false,
4162 data_source: TableDataSource::TableWrites { defaults: vec![] },
4163 }),
4164 owner_id: MZ_SYSTEM_ROLE_ID,
4165 };
4166
4167 let op_t1 = make_table_op(id_t1, global_id_t1, "t1");
4168 let op_t2 = make_table_op(id_t2, global_id_t2, "t2");
4169
4170 let base_state = catalog.state().clone();
4171
4172 let (state_after_t1, snapshot_after_t1) = catalog
4176 .transact_incremental_dry_run(
4177 &base_state,
4178 vec![op_t1.clone()],
4179 None,
4180 None,
4181 oracle_write_ts,
4182 )
4183 .await
4184 .expect("first dry run");
4185
4186 assert!(
4188 state_after_t1.try_get_entry(&id_t1).is_some(),
4189 "t1 should exist after first dry run"
4190 );
4191 assert_eq!(
4192 state_after_t1
4193 .try_get_entry(&id_t1)
4194 .expect("t1 entry")
4195 .name()
4196 .item,
4197 "t1"
4198 );
4199 assert!(
4200 state_after_t1.try_get_entry(&id_t2).is_none(),
4201 "t2 should NOT exist after first dry run"
4202 );
4203
4204 let (state_incremental, _) = catalog
4206 .transact_incremental_dry_run(
4207 &state_after_t1,
4208 vec![op_t2.clone()],
4209 None,
4210 Some(snapshot_after_t1),
4211 oracle_write_ts,
4212 )
4213 .await
4214 .expect("second dry run");
4215
4216 assert!(
4218 state_incremental.try_get_entry(&id_t1).is_some(),
4219 "t1 should exist in incremental result"
4220 );
4221 assert!(
4222 state_incremental.try_get_entry(&id_t2).is_some(),
4223 "t2 should exist in incremental result"
4224 );
4225
4226 let (state_all_at_once, _) = catalog
4229 .transact_incremental_dry_run(
4230 &base_state,
4231 vec![op_t1.clone(), op_t2.clone()],
4232 None,
4233 None,
4234 oracle_write_ts,
4235 )
4236 .await
4237 .expect("all-at-once dry run");
4238
4239 assert!(
4240 state_all_at_once.try_get_entry(&id_t1).is_some(),
4241 "t1 should exist in all-at-once result"
4242 );
4243 assert!(
4244 state_all_at_once.try_get_entry(&id_t2).is_some(),
4245 "t2 should exist in all-at-once result"
4246 );
4247
4248 let inc_t1 = state_incremental.try_get_entry(&id_t1).expect("inc t1");
4251 let all_t1 = state_all_at_once.try_get_entry(&id_t1).expect("all t1");
4252 assert_eq!(inc_t1.name(), all_t1.name());
4253 assert_eq!(inc_t1.owner_id, all_t1.owner_id);
4254
4255 let inc_t2 = state_incremental.try_get_entry(&id_t2).expect("inc t2");
4256 let all_t2 = state_all_at_once.try_get_entry(&id_t2).expect("all t2");
4257 assert_eq!(inc_t2.name(), all_t2.name());
4258 assert_eq!(inc_t2.owner_id, all_t2.owner_id);
4259
4260 catalog.expire().await;
4261 })
4262 .await
4263 }
4264
4265 #[mz_ore::test(tokio::test)]
4272 #[cfg_attr(miri, ignore)] async fn test_transact_update_scoped_system_parameters_prune() {
4274 use std::collections::{BTreeMap, BTreeSet};
4275
4276 use mz_catalog::memory::objects::{ClusterConfig, ClusterVariant};
4277 use mz_controller_types::ClusterId;
4278
4279 use crate::catalog::DropObjectInfo;
4280 use crate::config::{ScopedParameters, ScopedParametersScope};
4281
4282 Catalog::with_debug(|mut catalog| async move {
4283 let param = "max_query_result_size".to_string();
4284 let value = "1MB".to_string();
4285
4286 let commit_ts = catalog.current_upper().await;
4289 let cluster_a = catalog
4290 .allocate_user_cluster_id(commit_ts)
4291 .await
4292 .expect("allocate cluster A");
4293 let commit_ts = catalog.current_upper().await;
4294 let cluster_b = catalog
4295 .allocate_user_cluster_id(commit_ts)
4296 .await
4297 .expect("allocate cluster B");
4298
4299 let make_cluster_op = |id: ClusterId, name: &str| Op::CreateCluster {
4300 id,
4301 name: name.to_string(),
4302 introspection_sources: Vec::new(),
4303 owner_id: MZ_SYSTEM_ROLE_ID,
4304 config: ClusterConfig {
4305 variant: ClusterVariant::Unmanaged,
4306 workload_class: None,
4307 },
4308 };
4309
4310 let oracle_write_ts = catalog.current_upper().await;
4311 catalog
4312 .transact(
4313 None,
4314 oracle_write_ts,
4315 None,
4316 vec![
4317 make_cluster_op(cluster_a, "test_cluster_a"),
4318 make_cluster_op(cluster_b, "test_cluster_b"),
4319 ],
4320 )
4321 .await
4322 .expect("create clusters");
4323
4324 async fn rows(catalog: &Catalog) -> BTreeSet<(ClusterId, String, String)> {
4326 let mut storage = catalog.storage().await;
4327 let tx = storage.transaction().await.expect("open transaction");
4328 tx.get_cluster_system_configurations()
4329 .map(|c| (c.cluster_id, c.name, c.value))
4330 .collect()
4331 }
4332
4333 let scope_with = |ids: &[ClusterId]| ScopedParametersScope {
4334 clusters: ids.iter().copied().collect(),
4335 replicas: BTreeSet::new(),
4336 };
4337 let cluster_scoped = |id: ClusterId| {
4338 let mut cluster = BTreeMap::new();
4339 let mut overrides = BTreeMap::new();
4340 overrides.insert(param.clone(), value.clone());
4341 cluster.insert(id, overrides);
4342 ScopedParameters {
4343 cluster,
4344 replica: BTreeMap::new(),
4345 }
4346 };
4347 let empty_scoped = || ScopedParameters {
4348 cluster: BTreeMap::new(),
4349 replica: BTreeMap::new(),
4350 };
4351
4352 let oracle_write_ts = catalog.current_upper().await;
4354 catalog
4355 .transact(
4356 None,
4357 oracle_write_ts,
4358 None,
4359 vec![Op::UpdateScopedSystemParameters {
4360 scoped: cluster_scoped(cluster_a),
4361 prune_scope: scope_with(&[cluster_a]),
4362 }],
4363 )
4364 .await
4365 .expect("upsert A");
4366 assert!(
4367 rows(&catalog)
4368 .await
4369 .contains(&(cluster_a, param.clone(), value.clone())),
4370 "upsert must create the row for A"
4371 );
4372
4373 let oracle_write_ts = catalog.current_upper().await;
4376 catalog
4377 .transact(
4378 None,
4379 oracle_write_ts,
4380 None,
4381 vec![Op::UpdateScopedSystemParameters {
4382 scoped: cluster_scoped(cluster_b),
4383 prune_scope: scope_with(&[cluster_b]),
4384 }],
4385 )
4386 .await
4387 .expect("upsert B");
4388 assert!(
4389 rows(&catalog)
4390 .await
4391 .contains(&(cluster_b, param.clone(), value.clone())),
4392 "setup must create the row for B"
4393 );
4394
4395 let oracle_write_ts = catalog.current_upper().await;
4399 catalog
4400 .transact(
4401 None,
4402 oracle_write_ts,
4403 None,
4404 vec![Op::UpdateScopedSystemParameters {
4405 scoped: empty_scoped(),
4406 prune_scope: scope_with(&[cluster_a]),
4407 }],
4408 )
4409 .await
4410 .expect("bounded prune");
4411 assert!(
4412 rows(&catalog)
4413 .await
4414 .contains(&(cluster_b, param.clone(), value.clone())),
4415 "a live cluster outside prune_scope must keep its row"
4416 );
4417
4418 let oracle_write_ts = catalog.current_upper().await;
4421 catalog
4422 .transact(
4423 None,
4424 oracle_write_ts,
4425 None,
4426 vec![Op::UpdateScopedSystemParameters {
4427 scoped: empty_scoped(),
4428 prune_scope: scope_with(&[cluster_a]),
4429 }],
4430 )
4431 .await
4432 .expect("stale in-scope prune");
4433 assert!(
4434 !rows(&catalog)
4435 .await
4436 .iter()
4437 .any(|(id, _, _)| *id == cluster_a),
4438 "a live in-scope undesired row must be removed"
4439 );
4440
4441 let oracle_write_ts = catalog.current_upper().await;
4446 catalog
4447 .transact(
4448 None,
4449 oracle_write_ts,
4450 None,
4451 vec![Op::UpdateScopedSystemParameters {
4452 scoped: cluster_scoped(cluster_a),
4453 prune_scope: scope_with(&[cluster_a]),
4454 }],
4455 )
4456 .await
4457 .expect("re-create A row");
4458 assert!(
4459 rows(&catalog)
4460 .await
4461 .contains(&(cluster_a, param.clone(), value.clone())),
4462 "re-created row for A must exist"
4463 );
4464
4465 let oracle_write_ts = catalog.current_upper().await;
4466 catalog
4467 .transact(
4468 None,
4469 oracle_write_ts,
4470 None,
4471 vec![Op::DropObjects(vec![DropObjectInfo::Cluster(cluster_a)])],
4472 )
4473 .await
4474 .expect("drop cluster A");
4475
4476 let oracle_write_ts = catalog.current_upper().await;
4479 catalog
4480 .transact(
4481 None,
4482 oracle_write_ts,
4483 None,
4484 vec![Op::UpdateScopedSystemParameters {
4485 scoped: cluster_scoped(cluster_b),
4486 prune_scope: scope_with(&[cluster_b]),
4487 }],
4488 )
4489 .await
4490 .expect("orphan prune");
4491 assert!(
4492 !rows(&catalog)
4493 .await
4494 .iter()
4495 .any(|(id, _, _)| *id == cluster_a),
4496 "orphan row for a dropped cluster must be removed even when absent from prune_scope"
4497 );
4498
4499 catalog.expire().await;
4500 })
4501 .await
4502 }
4503
4504 fn record_max_connections(catalog: &mut Catalog) -> Arc<Mutex<Vec<u32>>> {
4508 let observed = Arc::new(Mutex::new(Vec::new()));
4509 let recorder = Arc::clone(&observed);
4510 catalog.system_config_mut().register_callback(
4511 &MAX_CONNECTIONS,
4512 Arc::new(move |vars: &SystemVars| {
4513 recorder
4514 .lock()
4515 .expect("recorder lock")
4516 .push(vars.max_connections())
4517 }),
4518 );
4519 observed.lock().expect("recorder lock").clear();
4520 observed
4521 }
4522
4523 fn set_max_connections_op(value: u32) -> Op {
4524 Op::UpdateSystemConfiguration {
4525 name: MAX_CONNECTIONS.name.to_string(),
4526 value: OwnedVarInput::Flat(value.to_string()),
4527 }
4528 }
4529
4530 #[mz_ore::test(tokio::test)]
4537 #[cfg_attr(miri, ignore)] async fn test_system_config_callback_not_fired_on_dry_run() {
4539 Catalog::with_debug(|mut catalog| async move {
4540 let observed = record_max_connections(&mut catalog);
4541 let before = catalog.system_config().max_connections();
4542
4543 let base_state = catalog.state().clone();
4547 let oracle_write_ts = catalog.current_upper().await;
4548 let (dry_run_state, _snapshot) = catalog
4549 .transact_incremental_dry_run(
4550 &base_state,
4551 vec![set_max_connections_op(before + 1)],
4552 None,
4553 None,
4554 oracle_write_ts,
4555 )
4556 .await
4557 .expect("dry run");
4558
4559 assert_eq!(dry_run_state.system_config().max_connections(), before + 1);
4560 assert_eq!(catalog.system_config().max_connections(), before);
4561 assert!(
4562 observed.lock().expect("recorder lock").is_empty(),
4563 "a dry run must not notify callbacks"
4564 );
4565
4566 catalog.expire().await;
4567 })
4568 .await
4569 }
4570
4571 #[mz_ore::test(tokio::test)]
4574 #[cfg_attr(miri, ignore)] async fn test_system_config_callback_not_fired_on_rollback() {
4576 Catalog::with_debug(|mut catalog| async move {
4577 let observed = record_max_connections(&mut catalog);
4578 let before = catalog.system_config().max_connections();
4579
4580 let oracle_write_ts = catalog.current_upper().await;
4581 let result = catalog
4582 .transact(
4583 None,
4584 oracle_write_ts,
4585 None,
4586 vec![
4587 set_max_connections_op(before + 1),
4588 Op::UpdateSystemConfiguration {
4591 name: MAX_CONNECTIONS.name.to_string(),
4592 value: OwnedVarInput::Flat("not a number".to_string()),
4593 },
4594 ],
4595 )
4596 .await;
4597
4598 assert!(result.is_err(), "the second op must abort the transaction");
4599 assert_eq!(catalog.system_config().max_connections(), before);
4600 assert!(
4601 observed.lock().expect("recorder lock").is_empty(),
4602 "an aborted transaction must not notify callbacks"
4603 );
4604
4605 catalog.expire().await;
4606 })
4607 .await
4608 }
4609}