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