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