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