Skip to main content

mz_adapter/catalog/
transact.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Logic related to executing catalog transactions.
11
12use 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::ExpectedClusterState;
20use mz_adapter_types::compaction::CompactionWindow;
21use mz_adapter_types::connection::ConnectionId;
22use mz_adapter_types::dyncfgs::{
23    ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT, WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL,
24    WITH_0DT_DEPLOYMENT_MAX_WAIT,
25};
26use mz_audit_log::{
27    CreateOrDropClusterReplicaReasonV1, EventDetails, EventType, IdFullNameV1, IdNameV1,
28    ObjectType, SchedulingDecisionsWithReasonsV2, VersionedEvent,
29};
30use mz_catalog::SYSTEM_CONN_ID;
31use mz_catalog::builtin::BuiltinLog;
32use mz_catalog::durable::{NetworkPolicy, Snapshot, Transaction};
33use mz_catalog::expr_cache::LocalExpressions;
34use mz_catalog::memory::error::{AmbiguousRename, Error, ErrorKind};
35use mz_catalog::memory::objects::{
36    CatalogEntry, CatalogItem, ClusterConfig, DataSourceDesc, DefaultPrivileges, SourceReferences,
37    StateDiff, StateUpdate, StateUpdateKind, TemporaryItem,
38};
39use mz_controller::clusters::{ManagedReplicaLocation, ReplicaConfig, ReplicaLocation};
40use mz_controller_types::{ClusterId, ReplicaId};
41use mz_ore::collections::HashSet;
42use mz_ore::instrument;
43use mz_persist_types::ShardId;
44use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap, merge_mz_acl_items};
45use mz_repr::network_policy_id::NetworkPolicyId;
46use mz_repr::optimize::OptimizerFeatures;
47use mz_repr::role_id::RoleId;
48use mz_repr::{CatalogItemId, ColumnName, GlobalId, SqlColumnType, strconv};
49use mz_sql::ast::RawDataType;
50use mz_sql::catalog::{
51    AutoProvisionSource, CatalogDatabase, CatalogError as SqlCatalogError,
52    CatalogItem as SqlCatalogItem, CatalogRole, CatalogSchema, DefaultPrivilegeAclItem,
53    DefaultPrivilegeObject, PasswordAction, PasswordConfig, RoleAttributesRaw, RoleMembership,
54    RoleVars,
55};
56use mz_sql::names::{
57    CommentObjectId, DatabaseId, FullItemName, ObjectId, QualifiedItemName,
58    ResolvedDatabaseSpecifier, SchemaId, SchemaSpecifier, SystemObjectId,
59};
60use mz_sql::plan::{NetworkPolicyRule, PlanError};
61use mz_sql::session::user::{MZ_SUPPORT_ROLE_ID, MZ_SYSTEM_ROLE_ID};
62use mz_sql::session::vars::OwnedVarInput;
63use mz_sql::session::vars::{Value as VarValue, VarInput};
64use mz_sql::{DEFAULT_SCHEMA, rbac};
65use mz_sql_parser::ast::{QualifiedReplica, Value};
66use mz_storage_client::storage_collections::StorageCollections;
67use serde::{Deserialize, Serialize};
68use tracing::{info, trace};
69
70use crate::AdapterError;
71use crate::catalog::state::LocalExpressionCache;
72use crate::catalog::{
73    BuiltinTableUpdate, Catalog, CatalogState, UpdatePrivilegeVariant,
74    catalog_type_to_audit_object_type, comment_id_to_audit_object_type, is_reserved_name,
75    is_reserved_role_name, object_type_to_audit_object_type,
76    system_object_type_to_audit_object_type,
77};
78use crate::config::{ScopedParameters, ScopedParametersScope};
79use crate::coord::ConnMeta;
80use crate::coord::catalog_implications::parsed_state_updates::ParsedStateUpdate;
81use crate::coord::cluster_scheduling::SchedulingDecision;
82use crate::util::ResultExt;
83
84/// A manually injected audit event.
85///
86/// Matches [`mz_audit_log::EventV1`], but without the `id` and `occurred_at` fields -- both are
87/// filled in automatically.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct InjectedAuditEvent {
90    pub event_type: EventType,
91    pub object_type: ObjectType,
92    pub details: EventDetails,
93    pub user: Option<String>,
94}
95
96#[derive(Debug, Clone)]
97pub enum Op {
98    AlterRetainHistory {
99        id: CatalogItemId,
100        value: Option<Value>,
101        window: CompactionWindow,
102    },
103    AlterSourceTimestampInterval {
104        id: CatalogItemId,
105        value: Option<Value>,
106        interval: Duration,
107    },
108    AlterRole {
109        id: RoleId,
110        name: String,
111        attributes: RoleAttributesRaw,
112        nopassword: bool,
113        vars: RoleVars,
114    },
115    AlterNetworkPolicy {
116        id: NetworkPolicyId,
117        rules: Vec<NetworkPolicyRule>,
118        name: String,
119        owner_id: RoleId,
120    },
121    AlterAddColumn {
122        id: CatalogItemId,
123        new_global_id: GlobalId,
124        name: ColumnName,
125        typ: SqlColumnType,
126        sql: RawDataType,
127    },
128    AlterMaterializedViewApplyReplacement {
129        id: CatalogItemId,
130        replacement_id: CatalogItemId,
131    },
132    CreateDatabase {
133        name: String,
134        owner_id: RoleId,
135    },
136    CreateSchema {
137        database_id: ResolvedDatabaseSpecifier,
138        schema_name: String,
139        owner_id: RoleId,
140    },
141    CreateRole {
142        name: String,
143        attributes: RoleAttributesRaw,
144    },
145    CreateCluster {
146        id: ClusterId,
147        name: String,
148        introspection_sources: Vec<&'static BuiltinLog>,
149        owner_id: RoleId,
150        config: ClusterConfig,
151    },
152    CreateClusterReplica {
153        cluster_id: ClusterId,
154        replica_id: ReplicaId,
155        name: String,
156        config: ReplicaConfig,
157        owner_id: RoleId,
158        reason: ReplicaCreateDropReason,
159    },
160    CreateItem {
161        id: CatalogItemId,
162        name: QualifiedItemName,
163        item: CatalogItem,
164        owner_id: RoleId,
165    },
166    CreateNetworkPolicy {
167        rules: Vec<NetworkPolicyRule>,
168        name: String,
169        owner_id: RoleId,
170    },
171    Comment {
172        object_id: CommentObjectId,
173        sub_component: Option<usize>,
174        comment: Option<String>,
175    },
176    DropObjects(Vec<DropObjectInfo>),
177    GrantRole {
178        role_id: RoleId,
179        member_id: RoleId,
180        grantor_id: RoleId,
181    },
182    RenameCluster {
183        id: ClusterId,
184        name: String,
185        to_name: String,
186        check_reserved_names: bool,
187    },
188    RenameClusterReplica {
189        cluster_id: ClusterId,
190        replica_id: ReplicaId,
191        name: QualifiedReplica,
192        to_name: String,
193    },
194    RenameItem {
195        id: CatalogItemId,
196        current_full_name: FullItemName,
197        to_name: String,
198    },
199    RenameSchema {
200        database_spec: ResolvedDatabaseSpecifier,
201        schema_spec: SchemaSpecifier,
202        new_name: String,
203        check_reserved_names: bool,
204    },
205    UpdateOwner {
206        id: ObjectId,
207        new_owner: RoleId,
208    },
209    UpdatePrivilege {
210        target_id: SystemObjectId,
211        /// The ACL changes to apply to `target_id`, applied as a single durable write. A bulk
212        /// `GRANT`/`REVOKE` touching one object for many grantees lands here as one op rather
213        /// than one op per grantee.
214        privileges: Vec<MzAclItem>,
215        variant: UpdatePrivilegeVariant,
216    },
217    UpdateDefaultPrivilege {
218        privilege_object: DefaultPrivilegeObject,
219        privilege_acl_item: DefaultPrivilegeAclItem,
220        variant: UpdatePrivilegeVariant,
221    },
222    RevokeRole {
223        role_id: RoleId,
224        member_id: RoleId,
225        grantor_id: RoleId,
226    },
227    UpdateClusterConfig {
228        id: ClusterId,
229        name: String,
230        config: ClusterConfig,
231    },
232    UpdateClusterReplicaConfig {
233        cluster_id: ClusterId,
234        replica_id: ReplicaId,
235        config: ReplicaConfig,
236    },
237    UpdateItem {
238        id: CatalogItemId,
239        name: QualifiedItemName,
240        to_item: CatalogItem,
241    },
242    UpdateSourceReferences {
243        source_id: CatalogItemId,
244        references: SourceReferences,
245    },
246    UpdateSystemConfiguration {
247        name: String,
248        value: OwnedVarInput,
249    },
250    ResetSystemConfiguration {
251        name: String,
252    },
253    ResetAllSystemConfiguration,
254    /// Persists the durable cache of scoped (per-cluster and per-replica)
255    /// system parameters towards `scoped`. The handler diffs against the current
256    /// durable contents: it upserts changed/added entries and removes entries
257    /// the update no longer serves.
258    ///
259    /// `prune_scope` bounds removals to the objects the update was evaluated
260    /// for. `Some(scope)` removes a row only when its owning object is in
261    /// `scope`, so an object created after the update's evaluation snapshot, and
262    /// the override it folded into its own create transaction, survives a
263    /// concurrent full-state reconcile. `None` is an unscoped full replace, used
264    /// only by the disabled-feature clear path where no create-time writes race.
265    UpdateScopedSystemParameters {
266        scoped: ScopedParameters,
267        prune_scope: Option<ScopedParametersScope>,
268    },
269    /// Injects audit events into the catalog.
270    ///
271    /// This is a nonstandard path used for manually appending audit events at the current time.
272    /// Mainly useful for correcting the audit log in the face of bugs that made us forget to emit
273    /// audit events.
274    InjectAuditEvents {
275        events: Vec<InjectedAuditEvent>,
276    },
277    /// Precondition, not a mutation. Aborts the whole transaction unless
278    /// `cluster_id`'s current managed config still equals `expected`. Running
279    /// the check inside the transaction makes it inseparable from the commit it
280    /// guards, giving a compare-and-append over the cluster's config. A purely
281    /// internal op for conditional cluster-config writes, never emitted by SQL
282    /// DDL.
283    CheckClusterState {
284        cluster_id: ClusterId,
285        expected: ExpectedClusterState,
286    },
287}
288
289/// Almost the same as `ObjectId`, but the `ClusterReplica` case has an extra
290/// `ReplicaCreateDropReason` field. This is forwarded to `mz_audit_events.details` when applying
291/// the `Op::DropObjects`.
292#[derive(Debug, Clone)]
293pub enum DropObjectInfo {
294    Cluster(ClusterId),
295    ClusterReplica((ClusterId, ReplicaId, ReplicaCreateDropReason)),
296    Database(DatabaseId),
297    Schema((ResolvedDatabaseSpecifier, SchemaSpecifier)),
298    Role(RoleId),
299    Item(CatalogItemId),
300    NetworkPolicy(NetworkPolicyId),
301}
302
303impl DropObjectInfo {
304    /// Creates a `DropObjectInfo` from an `ObjectId`.
305    /// If it is a `ClusterReplica`, the reason will be set to `ReplicaCreateDropReason::Manual`.
306    pub(crate) fn manual_drop_from_object_id(id: ObjectId) -> Self {
307        match id {
308            ObjectId::Cluster(cluster_id) => DropObjectInfo::Cluster(cluster_id),
309            ObjectId::ClusterReplica((cluster_id, replica_id)) => DropObjectInfo::ClusterReplica((
310                cluster_id,
311                replica_id,
312                ReplicaCreateDropReason::Manual,
313            )),
314            ObjectId::Database(database_id) => DropObjectInfo::Database(database_id),
315            ObjectId::Schema(schema) => DropObjectInfo::Schema(schema),
316            ObjectId::Role(role_id) => DropObjectInfo::Role(role_id),
317            ObjectId::Item(item_id) => DropObjectInfo::Item(item_id),
318            ObjectId::NetworkPolicy(policy_id) => DropObjectInfo::NetworkPolicy(policy_id),
319        }
320    }
321
322    /// Creates an `ObjectId` from a `DropObjectInfo`.
323    /// Loses the `ReplicaCreateDropReason` if there is one!
324    fn to_object_id(&self) -> ObjectId {
325        match &self {
326            DropObjectInfo::Cluster(cluster_id) => ObjectId::Cluster(cluster_id.clone()),
327            DropObjectInfo::ClusterReplica((cluster_id, replica_id, _reason)) => {
328                ObjectId::ClusterReplica((cluster_id.clone(), replica_id.clone()))
329            }
330            DropObjectInfo::Database(database_id) => ObjectId::Database(database_id.clone()),
331            DropObjectInfo::Schema(schema) => ObjectId::Schema(schema.clone()),
332            DropObjectInfo::Role(role_id) => ObjectId::Role(role_id.clone()),
333            DropObjectInfo::Item(item_id) => ObjectId::Item(item_id.clone()),
334            DropObjectInfo::NetworkPolicy(network_policy_id) => {
335                ObjectId::NetworkPolicy(network_policy_id.clone())
336            }
337        }
338    }
339}
340
341/// The reason for creating or dropping a replica.
342#[derive(Debug, Clone)]
343pub enum ReplicaCreateDropReason {
344    /// The user initiated the replica create or drop, e.g., by
345    /// - creating/dropping a cluster,
346    /// - ALTERing various options on a managed cluster,
347    /// - CREATE/DROP CLUSTER REPLICA on an unmanaged cluster.
348    Manual,
349    /// The automated cluster scheduling initiated the replica create or drop, e.g., a
350    /// materialized view is needing a refresh on a SCHEDULE ON REFRESH cluster.
351    ClusterScheduling(Vec<SchedulingDecision>),
352    /// The cluster controller dropped the replica because the cluster's configuration no longer
353    /// calls for it. The uniform reason on every controller-emitted drop (e.g. a
354    /// replication-factor decrease).
355    Retired,
356}
357
358impl ReplicaCreateDropReason {
359    pub fn into_audit_log(
360        self,
361    ) -> (
362        CreateOrDropClusterReplicaReasonV1,
363        Option<SchedulingDecisionsWithReasonsV2>,
364    ) {
365        let (reason, scheduling_policies) = match self {
366            ReplicaCreateDropReason::Manual => (CreateOrDropClusterReplicaReasonV1::Manual, None),
367            ReplicaCreateDropReason::ClusterScheduling(scheduling_decisions) => (
368                CreateOrDropClusterReplicaReasonV1::Schedule,
369                Some(scheduling_decisions),
370            ),
371            ReplicaCreateDropReason::Retired => (CreateOrDropClusterReplicaReasonV1::Retired, None),
372        };
373        (
374            reason,
375            scheduling_policies
376                .as_ref()
377                .map(SchedulingDecision::reasons_to_audit_log_reasons),
378        )
379    }
380}
381
382pub struct TransactionResult {
383    pub builtin_table_updates: Vec<BuiltinTableUpdate>,
384    /// Parsed catalog updates from which we will derive catalog implications.
385    pub catalog_updates: Vec<ParsedStateUpdate>,
386    pub audit_events: Vec<VersionedEvent>,
387}
388
389#[derive(Debug, Clone, Copy)]
390enum TransactInnerMode {
391    /// Prepare storage state and return a commit-ready transaction state.
392    ///
393    /// This mode is used by durable catalog transactions.
394    Commit,
395    /// Execute a dry run against a durable transaction that will not be
396    /// committed.
397    ///
398    /// This mode still validates and applies updates to an in-memory
399    /// `CatalogState`, but it must not call `prepare_state` because dry runs
400    /// must not trigger controller side effects.
401    DryRun,
402}
403
404impl Catalog {
405    fn should_audit_log_item(item: &CatalogItem) -> bool {
406        !item.is_temporary()
407    }
408
409    /// Gets [`CatalogItemId`]s of temporary items to be created, checks for name collisions
410    /// within a connection id.
411    fn temporary_ids(
412        &self,
413        ops: &[Op],
414        temporary_drops: BTreeSet<(&ConnectionId, String)>,
415    ) -> Result<BTreeSet<CatalogItemId>, Error> {
416        let mut creating = BTreeSet::new();
417        let mut temporary_ids = BTreeSet::new();
418        for op in ops.iter() {
419            if let Op::CreateItem {
420                id,
421                name,
422                item,
423                owner_id: _,
424            } = op
425            {
426                if let Some(conn_id) = item.conn_id() {
427                    if self.item_exists_in_temp_schemas(conn_id, &name.item)
428                        && !temporary_drops.contains(&(conn_id, name.item.clone()))
429                        || creating.contains(&(conn_id, &name.item))
430                    {
431                        return Err(
432                            SqlCatalogError::ItemAlreadyExists(*id, name.item.clone()).into()
433                        );
434                    } else {
435                        creating.insert((conn_id, &name.item));
436                        temporary_ids.insert(id.clone());
437                    }
438                }
439            }
440        }
441        Ok(temporary_ids)
442    }
443
444    #[instrument(name = "catalog::transact")]
445    pub async fn transact(
446        &mut self,
447        // n.b. this is an option to prevent us from needing to build out a
448        // dummy impl of `StorageController` for tests.
449        storage_collections: Option<&mut Arc<dyn StorageCollections + Send + Sync>>,
450        oracle_write_ts: mz_repr::Timestamp,
451        session: Option<&ConnMeta>,
452        ops: Vec<Op>,
453    ) -> Result<TransactionResult, AdapterError> {
454        trace!("transact: {:?}", ops);
455        fail::fail_point!("catalog_transact", |arg| {
456            Err(AdapterError::Unstructured(anyhow::anyhow!(
457                "failpoint: {arg:?}"
458            )))
459        });
460
461        let drop_ids: BTreeSet<CatalogItemId> = ops
462            .iter()
463            .filter_map(|op| match op {
464                Op::DropObjects(drop_object_infos) => {
465                    let ids = drop_object_infos.iter().map(|info| info.to_object_id());
466                    let item_ids = ids.filter_map(|id| match id {
467                        ObjectId::Item(id) => Some(id),
468                        _ => None,
469                    });
470                    Some(item_ids)
471                }
472                _ => None,
473            })
474            .flatten()
475            .collect();
476        let temporary_drops = drop_ids
477            .iter()
478            .filter_map(|id| {
479                let entry = self.get_entry(id);
480                match entry.item().conn_id() {
481                    Some(conn_id) => Some((conn_id, entry.name().item.clone())),
482                    None => None,
483                }
484            })
485            .collect();
486
487        let temporary_ids = self.temporary_ids(&ops, temporary_drops)?;
488        let mut builtin_table_updates = vec![];
489        let mut catalog_updates = vec![];
490        let mut audit_events = vec![];
491        let mut storage = self.storage().await;
492        let mut tx = storage
493            .transaction()
494            .await
495            .unwrap_or_terminate("starting catalog transaction");
496
497        let new_state = Self::transact_inner(
498            TransactInnerMode::Commit,
499            storage_collections,
500            oracle_write_ts,
501            session,
502            ops,
503            temporary_ids,
504            &mut builtin_table_updates,
505            &mut catalog_updates,
506            &mut audit_events,
507            &mut tx,
508            &self.state,
509        )
510        .await?;
511
512        // The user closure was successful, apply the updates. Terminate the
513        // process if this fails, because we have to restart envd due to
514        // indeterminate catalog state, which we only reconcile during catalog
515        // init.
516        tx.commit(oracle_write_ts)
517            .await
518            .unwrap_or_terminate("catalog storage transaction commit must succeed");
519
520        // Dropping here keeps the mutable borrow on self, preventing us accidentally
521        // mutating anything until after f is executed.
522        drop(storage);
523        if let Some(new_state) = new_state {
524            self.transient_revision += 1;
525            // Publish the new revision before returning. Everything that can
526            // reveal this transaction's effects (responses, notices, builtin
527            // table writes) happens after `transact` returns, so any session
528            // that has observed such evidence is guaranteed to see this bump
529            // and refresh its cached catalog snapshot.
530            self.shared_transient_revision
531                .store(self.transient_revision, atomic::Ordering::SeqCst);
532            self.state = new_state;
533        }
534
535        Ok(TransactionResult {
536            builtin_table_updates,
537            catalog_updates,
538            audit_events,
539        })
540    }
541
542    /// Performs an incremental dry-run catalog transaction: processes only the
543    /// NEW ops against an accumulated `CatalogState` from previous dry runs.
544    /// This avoids the O(N^2) replay cost of replaying all accumulated ops.
545    ///
546    /// The durable transaction is intentionally never committed and no storage
547    /// controller prepare-state side effects are run.
548    ///
549    /// If `prev_snapshot` is `Some`, the transaction is initialized from that
550    /// snapshot (which represents the tx state after the previous dry run),
551    /// ensuring it starts in sync with `base_state`. If `None` (first
552    /// statement), a fresh transaction is loaded from durable storage.
553    ///
554    /// Returns the new accumulated state and a snapshot of the transaction's
555    /// state for use in subsequent incremental dry runs.
556    pub async fn transact_incremental_dry_run(
557        &self,
558        base_state: &CatalogState,
559        ops: Vec<Op>,
560        session: Option<&ConnMeta>,
561        prev_snapshot: Option<Snapshot>,
562        oracle_write_ts: mz_repr::Timestamp,
563    ) -> Result<(CatalogState, Snapshot), AdapterError> {
564        // For DDL transactions, items are not temporary (CREATE TABLE FROM SOURCE, etc.)
565        // but we still need to check for collisions.
566        let temporary_ids = self.temporary_ids(&ops, BTreeSet::new())?;
567
568        let mut builtin_table_updates = vec![];
569        let mut catalog_updates = vec![];
570        let mut audit_events = vec![];
571        let mut storage = self.storage().await;
572        let mut tx = if let Some(snapshot) = prev_snapshot {
573            // Restore transaction from saved snapshot so it starts in sync
574            // with the accumulated CatalogState from previous dry runs.
575            storage
576                .transaction_from_snapshot(snapshot)
577                .unwrap_or_terminate("starting catalog transaction from snapshot")
578        } else {
579            // First statement: fresh transaction from durable storage, which
580            // is in sync with the real catalog state.
581            storage
582                .transaction()
583                .await
584                .unwrap_or_terminate("starting catalog transaction")
585        };
586
587        // Process only the new ops against the accumulated state in dry-run mode.
588        let new_state = Self::transact_inner(
589            TransactInnerMode::DryRun,
590            None,
591            oracle_write_ts,
592            session,
593            ops,
594            temporary_ids,
595            &mut builtin_table_updates,
596            &mut catalog_updates,
597            &mut audit_events,
598            &mut tx,
599            base_state,
600        )
601        .await?;
602
603        // Save the transaction's current state as a snapshot for the next
604        // incremental dry run.
605        let new_snapshot = tx.current_snapshot();
606
607        // Transaction is NOT committed — drop it.
608        drop(storage);
609
610        // transact_inner returns Some(state) when ops produced changes.
611        let state = new_state.unwrap_or_else(|| base_state.clone());
612        Ok((state, new_snapshot))
613    }
614
615    /// Extracts optimized expressions from `Op::CreateItem` operations for views
616    /// and materialized views. These can be used to populate a `LocalExpressionCache`
617    /// to avoid re-optimization during `apply_updates`.
618    fn extract_expressions_from_ops(
619        ops: &[Op],
620        optimizer_features: &OptimizerFeatures,
621    ) -> BTreeMap<GlobalId, LocalExpressions> {
622        let mut exprs = BTreeMap::new();
623
624        for op in ops {
625            if let Op::CreateItem { item, .. } = op {
626                match item {
627                    CatalogItem::View(view) => {
628                        exprs.insert(
629                            view.global_id,
630                            LocalExpressions {
631                                local_mir: (*view.locally_optimized_expr).clone(),
632                                optimizer_features: optimizer_features.clone(),
633                            },
634                        );
635                    }
636                    CatalogItem::MaterializedView(mv) => {
637                        exprs.insert(
638                            mv.global_id_writes(),
639                            LocalExpressions {
640                                local_mir: (*mv.locally_optimized_expr).clone(),
641                                optimizer_features: optimizer_features.clone(),
642                            },
643                        );
644                    }
645                    CatalogItem::Table(_)
646                    | CatalogItem::Source(_)
647                    | CatalogItem::Log(_)
648                    | CatalogItem::Sink(_)
649                    | CatalogItem::Index(_)
650                    | CatalogItem::Type(_)
651                    | CatalogItem::Func(_)
652                    | CatalogItem::Secret(_)
653                    | CatalogItem::Connection(_) => {}
654                }
655            }
656        }
657
658        exprs
659    }
660
661    /// Performs the transaction described by `ops` and returns the new state of the catalog, if
662    /// it has changed. If `ops` don't result in a change in the state this method returns `None`.
663    ///
664    /// `mode` controls whether storage prepare-state side effects are allowed.
665    /// In `DryRun` mode, this method may update the in-memory state returned to
666    /// the caller, but it must not trigger controller side effects.
667    ///
668    #[instrument(name = "catalog::transact_inner")]
669    async fn transact_inner(
670        mode: TransactInnerMode,
671        storage_collections: Option<&mut Arc<dyn StorageCollections + Send + Sync>>,
672        oracle_write_ts: mz_repr::Timestamp,
673        session: Option<&ConnMeta>,
674        ops: Vec<Op>,
675        temporary_ids: BTreeSet<CatalogItemId>,
676        builtin_table_updates: &mut Vec<BuiltinTableUpdate>,
677        parsed_catalog_updates: &mut Vec<ParsedStateUpdate>,
678        audit_events: &mut Vec<VersionedEvent>,
679        tx: &mut Transaction<'_>,
680        state: &CatalogState,
681    ) -> Result<Option<CatalogState>, AdapterError> {
682        // We come up with new catalog state, builtin state updates, and parsed
683        // catalog updates (for deriving catalog implications) in two phases:
684        //
685        // 1. We (cow)-clone catalog state as `preliminary_state` and apply ops
686        //    one-by-one. This will give us the full list of updates to apply to
687        //    the catalog, which will allow us to apply it in one batch, which
688        //    in turn will allow the apply machinery to consolidate the updates.
689        // 2. We do one final apply call with all updates, which gives us the
690        //    final builtin table updates and parsed catalog updates.
691        //
692        // The reason is that the loop that is working off ops first does a
693        // transact_op to derive the state updates for that op, and then calls
694        // apply_updates on the catalog state. And successive ops might expect
695        // the catalog state to reflect the modified state _after_ applying
696        // previous ops.
697        //
698        // We want to, however, have one final apply_state that takes all the
699        // accumulated updates to derive the required controller updates and the
700        // builtin table updates.
701        //
702        // We won't win any DDL throughput benchmarks, but so far that's not
703        // what we're optimizing for and there would probably be other
704        // bottlenecks before we hit this one as a bottleneck.
705        //
706        // We could work around this by refactoring how the interplay of
707        // transact_op and apply_updates works, but that's a larger undertaking.
708        let mut preliminary_state = Cow::Borrowed(state);
709
710        // The final state that we will return, if modified.
711        let mut state = Cow::Borrowed(state);
712
713        if ops.is_empty() {
714            return Ok(None);
715        }
716
717        // Extract optimized expressions from CreateItem ops to avoid re-optimization
718        // during apply_updates. We extract before the loop since `ops` is moved there.
719        let optimizer_features = OptimizerFeatures::from(state.system_config());
720        let cached_exprs = Self::extract_expressions_from_ops(&ops, &optimizer_features);
721
722        let mut storage_collections_to_create = BTreeSet::new();
723        let mut storage_collections_to_drop = BTreeSet::new();
724        let mut storage_collections_to_register = BTreeMap::new();
725
726        let mut updates = Vec::new();
727
728        for op in ops {
729            let temporary_item_updates = Self::transact_op(
730                oracle_write_ts,
731                session,
732                op,
733                &temporary_ids,
734                audit_events,
735                tx,
736                &*preliminary_state,
737                &mut storage_collections_to_create,
738                &mut storage_collections_to_drop,
739                &mut storage_collections_to_register,
740            )
741            .await?;
742
743            // Temporary items are not stored in the durable catalog, so they need to be handled
744            // separately for updating state and builtin tables.
745            // TODO(jkosh44) Some more thought needs to be given as to how temporary tables work
746            // in a multi-subscriber catalog world.
747            let upper = tx.upper();
748            let temporary_item_updates =
749                temporary_item_updates
750                    .into_iter()
751                    .map(|(item, diff)| StateUpdate {
752                        kind: StateUpdateKind::TemporaryItem(item),
753                        ts: upper,
754                        diff,
755                    });
756
757            let mut op_updates: Vec<_> = tx.get_and_commit_op_updates();
758            op_updates.extend(temporary_item_updates);
759            if !op_updates.is_empty() {
760                // Clone the cache so each apply_updates call has access to cached expressions.
761                // The cache uses `remove` semantics, so we need a fresh clone for each call.
762                let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
763                let (_op_builtin_table_updates, _op_catalog_updates) = preliminary_state
764                    .to_mut()
765                    .apply_updates(op_updates.clone(), &mut local_expr_cache)
766                    .await;
767            }
768            updates.append(&mut op_updates);
769        }
770
771        if !updates.is_empty() {
772            let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
773            let (op_builtin_table_updates, op_catalog_updates) = state
774                .to_mut()
775                .apply_updates(updates.clone(), &mut local_expr_cache)
776                .await;
777            let op_builtin_table_updates = state
778                .to_mut()
779                .resolve_builtin_table_updates(op_builtin_table_updates);
780            builtin_table_updates.extend(op_builtin_table_updates);
781            parsed_catalog_updates.extend(op_catalog_updates);
782        }
783
784        match mode {
785            TransactInnerMode::Commit => {
786                // `storage_collections` can be `None` in tests.
787                if let Some(c) = storage_collections {
788                    c.prepare_state(
789                        tx,
790                        storage_collections_to_create,
791                        storage_collections_to_drop,
792                        storage_collections_to_register,
793                    )
794                    .await?;
795                }
796            }
797            TransactInnerMode::DryRun => {
798                debug_assert!(
799                    storage_collections.is_none(),
800                    "dry-run mode must not prepare storage state"
801                );
802            }
803        }
804
805        let updates = tx.get_and_commit_op_updates();
806        if !updates.is_empty() {
807            let mut local_expr_cache = LocalExpressionCache::new(cached_exprs.clone());
808            let (op_builtin_table_updates, op_catalog_updates) = state
809                .to_mut()
810                .apply_updates(updates.clone(), &mut local_expr_cache)
811                .await;
812            let op_builtin_table_updates = state
813                .to_mut()
814                .resolve_builtin_table_updates(op_builtin_table_updates);
815            builtin_table_updates.extend(op_builtin_table_updates);
816            parsed_catalog_updates.extend(op_catalog_updates);
817        }
818
819        match state {
820            Cow::Owned(state) => Ok(Some(state)),
821            Cow::Borrowed(_) => Ok(None),
822        }
823    }
824
825    /// Performs the transaction operation described by `op`. This function prepares the changes in
826    /// `tx`, but does not update `state`. `state` will be updated when applying the durable
827    /// changes.
828    ///
829    /// Optionally returns a builtin table update for any builtin table updates than cannot be
830    /// derived from the durable catalog state, and temporary item diffs. These are all very weird
831    /// scenarios and ideally in the future don't exist.
832    #[instrument]
833    async fn transact_op(
834        oracle_write_ts: mz_repr::Timestamp,
835        session: Option<&ConnMeta>,
836        op: Op,
837        temporary_ids: &BTreeSet<CatalogItemId>,
838        audit_events: &mut Vec<VersionedEvent>,
839        tx: &mut Transaction<'_>,
840        state: &CatalogState,
841        storage_collections_to_create: &mut BTreeSet<GlobalId>,
842        storage_collections_to_drop: &mut BTreeSet<GlobalId>,
843        storage_collections_to_register: &mut BTreeMap<GlobalId, ShardId>,
844    ) -> Result<Vec<(TemporaryItem, StateDiff)>, AdapterError> {
845        let mut temporary_item_updates = Vec::new();
846
847        match op {
848            Op::CheckClusterState {
849                cluster_id,
850                expected,
851            } => {
852                // Precondition only. Returning `Err` here aborts `transact_inner`
853                // before `tx.commit`, so the compare-and-append holds atomically
854                // with the write it guards.
855                if !crate::catalog::cluster_state::cluster_matches_expected(
856                    state, cluster_id, &expected,
857                ) {
858                    return Err(AdapterError::ClusterStateChanged { cluster_id });
859                }
860            }
861            Op::AlterRetainHistory { id, value, window } => {
862                let entry = state.get_entry(&id);
863                if id.is_system() {
864                    let name = entry.name();
865                    let full_name =
866                        state.resolve_full_name(name, session.map(|session| session.conn_id()));
867                    return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
868                        full_name.to_string(),
869                    ))));
870                }
871
872                let mut new_entry = entry.clone();
873                let previous = new_entry
874                    .item
875                    .update_retain_history(value.clone(), window)
876                    .map_err(|_| {
877                        AdapterError::Catalog(Error::new(ErrorKind::Internal(
878                            "planner should have rejected invalid alter retain history item type"
879                                .to_string(),
880                        )))
881                    })?;
882
883                if Self::should_audit_log_item(new_entry.item()) {
884                    let details =
885                        EventDetails::AlterRetainHistoryV1(mz_audit_log::AlterRetainHistoryV1 {
886                            id: id.to_string(),
887                            old_history: previous.map(|previous| previous.to_string()),
888                            new_history: value.map(|v| v.to_string()),
889                        });
890                    CatalogState::add_to_audit_log(
891                        &state.system_configuration,
892                        oracle_write_ts,
893                        session,
894                        tx,
895                        audit_events,
896                        EventType::Alter,
897                        catalog_type_to_audit_object_type(new_entry.item().typ()),
898                        details,
899                    )?;
900                }
901
902                tx.update_item(id, new_entry.into())?;
903
904                Self::log_update(state, &id);
905            }
906            Op::AlterSourceTimestampInterval {
907                id,
908                value,
909                interval,
910            } => {
911                let entry = state.get_entry(&id);
912                if id.is_system() {
913                    let name = entry.name();
914                    let full_name =
915                        state.resolve_full_name(name, session.map(|session| session.conn_id()));
916                    return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
917                        full_name.to_string(),
918                    ))));
919                }
920
921                let mut new_entry = entry.clone();
922                let previous = new_entry
923                    .item
924                    .update_timestamp_interval(value.clone(), interval)
925                    .map_err(|_| {
926                        AdapterError::Catalog(Error::new(ErrorKind::Internal(
927                            "planner should have rejected invalid alter timestamp interval item type"
928                                .to_string(),
929                        )))
930                    })?;
931
932                if Self::should_audit_log_item(new_entry.item()) {
933                    let details = EventDetails::AlterSourceTimestampIntervalV1(
934                        mz_audit_log::AlterSourceTimestampIntervalV1 {
935                            id: id.to_string(),
936                            old_interval: previous.map(|previous| previous.to_string()),
937                            new_interval: value.map(|v| v.to_string()),
938                        },
939                    );
940                    CatalogState::add_to_audit_log(
941                        &state.system_configuration,
942                        oracle_write_ts,
943                        session,
944                        tx,
945                        audit_events,
946                        EventType::Alter,
947                        catalog_type_to_audit_object_type(new_entry.item().typ()),
948                        details,
949                    )?;
950                }
951
952                tx.update_item(id, new_entry.into())?;
953
954                Self::log_update(state, &id);
955            }
956            Op::AlterRole {
957                id,
958                name,
959                attributes,
960                nopassword,
961                vars,
962            } => {
963                state.ensure_not_reserved_role(&id)?;
964
965                let mut existing_role = state.get_role(&id).clone();
966                let password = attributes.password.clone();
967                let scram_iterations = attributes
968                    .scram_iterations
969                    .unwrap_or_else(|| state.system_config().scram_iterations());
970                existing_role.attributes = attributes.into();
971                existing_role.vars = vars;
972                let password_action = if nopassword {
973                    PasswordAction::Clear
974                } else if let Some(password) = password {
975                    PasswordAction::Set(PasswordConfig {
976                        password,
977                        scram_iterations,
978                    })
979                } else {
980                    PasswordAction::NoChange
981                };
982                tx.update_role(id, existing_role.into(), password_action)?;
983
984                CatalogState::add_to_audit_log(
985                    &state.system_configuration,
986                    oracle_write_ts,
987                    session,
988                    tx,
989                    audit_events,
990                    EventType::Alter,
991                    ObjectType::Role,
992                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
993                        id: id.to_string(),
994                        name: name.clone(),
995                    }),
996                )?;
997
998                info!("update role {name} ({id})");
999            }
1000            Op::AlterNetworkPolicy {
1001                id,
1002                rules,
1003                name,
1004                owner_id: _owner_id,
1005            } => {
1006                let existing_policy = state.get_network_policy(&id).clone();
1007                let mut policy: NetworkPolicy = existing_policy.into();
1008                policy.rules = rules;
1009                if is_reserved_name(&name) {
1010                    return Err(AdapterError::Catalog(Error::new(
1011                        ErrorKind::ReservedNetworkPolicyName(name),
1012                    )));
1013                }
1014                tx.update_network_policy(id, policy.clone())?;
1015
1016                CatalogState::add_to_audit_log(
1017                    &state.system_configuration,
1018                    oracle_write_ts,
1019                    session,
1020                    tx,
1021                    audit_events,
1022                    EventType::Alter,
1023                    ObjectType::NetworkPolicy,
1024                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1025                        id: id.to_string(),
1026                        name: name.clone(),
1027                    }),
1028                )?;
1029
1030                info!("update network policy {name} ({id})");
1031            }
1032            Op::AlterAddColumn {
1033                id,
1034                new_global_id,
1035                name,
1036                typ,
1037                sql,
1038            } => {
1039                let column_name = name.to_string();
1040                let column_type = typ.to_string();
1041                let nullable = typ.nullable;
1042                let mut new_entry = state.get_entry(&id).clone();
1043                let version = new_entry.item.add_column(name, typ, sql)?;
1044                // All versions of a table share the same shard, so it shouldn't matter what
1045                // GlobalId we use here.
1046                let shard_id = state
1047                    .storage_metadata()
1048                    .get_collection_shard(new_entry.latest_global_id())?;
1049
1050                // TODO(alter_table): Support adding columns to sources.
1051                let CatalogItem::Table(table) = &mut new_entry.item else {
1052                    return Err(AdapterError::Unsupported("adding columns to non-Table"));
1053                };
1054                table.collections.insert(version, new_global_id);
1055
1056                if Self::should_audit_log_item(new_entry.item()) {
1057                    let details = EventDetails::AlterAddColumnV1(mz_audit_log::AlterAddColumnV1 {
1058                        id: id.to_string(),
1059                        column: column_name,
1060                        column_type,
1061                        nullable,
1062                    });
1063                    CatalogState::add_to_audit_log(
1064                        &state.system_configuration,
1065                        oracle_write_ts,
1066                        session,
1067                        tx,
1068                        audit_events,
1069                        EventType::Alter,
1070                        catalog_type_to_audit_object_type(new_entry.item().typ()),
1071                        details,
1072                    )?;
1073                }
1074
1075                tx.update_item(id, new_entry.into())?;
1076                storage_collections_to_register.insert(new_global_id, shard_id);
1077            }
1078            Op::AlterMaterializedViewApplyReplacement { id, replacement_id } => {
1079                let mut new_entry = state.get_entry(&id).clone();
1080                let replacement = state.get_entry(&replacement_id);
1081
1082                let new_audit_events =
1083                    apply_replacement_audit_events(state, &new_entry, replacement);
1084
1085                let CatalogItem::MaterializedView(mv) = &mut new_entry.item else {
1086                    return Err(AdapterError::internal(
1087                        "ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT",
1088                        "id must refer to a materialized view",
1089                    ));
1090                };
1091                let CatalogItem::MaterializedView(replacement_mv) = &replacement.item else {
1092                    return Err(AdapterError::internal(
1093                        "ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT",
1094                        "replacement_id must refer to a materialized view",
1095                    ));
1096                };
1097
1098                mv.apply_replacement(replacement_mv.clone());
1099
1100                tx.remove_item(replacement_id)?;
1101
1102                new_entry.id = replacement_id;
1103                tx_replace_item(tx, state, id, new_entry)?;
1104
1105                let comment_id = CommentObjectId::MaterializedView(replacement_id);
1106                tx.drop_comments(&[comment_id].into())?;
1107
1108                for (event_type, details) in new_audit_events {
1109                    CatalogState::add_to_audit_log(
1110                        &state.system_configuration,
1111                        oracle_write_ts,
1112                        session,
1113                        tx,
1114                        audit_events,
1115                        event_type,
1116                        ObjectType::MaterializedView,
1117                        details,
1118                    )?;
1119                }
1120            }
1121            Op::CreateDatabase { name, owner_id } => {
1122                let database_owner_privileges = vec![rbac::owner_privilege(
1123                    mz_sql::catalog::ObjectType::Database,
1124                    owner_id,
1125                )];
1126                let database_default_privileges = state
1127                    .default_privileges
1128                    .get_applicable_privileges(
1129                        owner_id,
1130                        None,
1131                        None,
1132                        mz_sql::catalog::ObjectType::Database,
1133                    )
1134                    .map(|item| item.mz_acl_item(owner_id));
1135                let database_privileges: Vec<_> = merge_mz_acl_items(
1136                    database_owner_privileges
1137                        .into_iter()
1138                        .chain(database_default_privileges),
1139                )
1140                .collect();
1141
1142                let schema_owner_privileges = vec![rbac::owner_privilege(
1143                    mz_sql::catalog::ObjectType::Schema,
1144                    owner_id,
1145                )];
1146                let schema_default_privileges = state
1147                    .default_privileges
1148                    .get_applicable_privileges(
1149                        owner_id,
1150                        None,
1151                        None,
1152                        mz_sql::catalog::ObjectType::Schema,
1153                    )
1154                    .map(|item| item.mz_acl_item(owner_id))
1155                    // Special default privilege on public schemas.
1156                    .chain(std::iter::once(MzAclItem {
1157                        grantee: RoleId::Public,
1158                        grantor: owner_id,
1159                        acl_mode: AclMode::USAGE,
1160                    }));
1161                let schema_privileges: Vec<_> = merge_mz_acl_items(
1162                    schema_owner_privileges
1163                        .into_iter()
1164                        .chain(schema_default_privileges),
1165                )
1166                .collect();
1167
1168                let temporary_oids: HashSet<_> = state.get_temporary_oids().collect();
1169                let (database_id, _) = tx.insert_user_database(
1170                    &name,
1171                    owner_id,
1172                    database_privileges.clone(),
1173                    &temporary_oids,
1174                )?;
1175                let (schema_id, _) = tx.insert_user_schema(
1176                    database_id,
1177                    DEFAULT_SCHEMA,
1178                    owner_id,
1179                    schema_privileges.clone(),
1180                    &temporary_oids,
1181                )?;
1182                CatalogState::add_to_audit_log(
1183                    &state.system_configuration,
1184                    oracle_write_ts,
1185                    session,
1186                    tx,
1187                    audit_events,
1188                    EventType::Create,
1189                    ObjectType::Database,
1190                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1191                        id: database_id.to_string(),
1192                        name: name.clone(),
1193                    }),
1194                )?;
1195                info!("create database {}", name);
1196
1197                CatalogState::add_to_audit_log(
1198                    &state.system_configuration,
1199                    oracle_write_ts,
1200                    session,
1201                    tx,
1202                    audit_events,
1203                    EventType::Create,
1204                    ObjectType::Schema,
1205                    EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
1206                        id: schema_id.to_string(),
1207                        name: DEFAULT_SCHEMA.to_string(),
1208                        database_name: Some(name),
1209                    }),
1210                )?;
1211            }
1212            Op::CreateSchema {
1213                database_id,
1214                schema_name,
1215                owner_id,
1216            } => {
1217                if is_reserved_name(&schema_name) {
1218                    return Err(AdapterError::Catalog(Error::new(
1219                        ErrorKind::ReservedSchemaName(schema_name),
1220                    )));
1221                }
1222                let database_id = match database_id {
1223                    ResolvedDatabaseSpecifier::Id(id) => id,
1224                    ResolvedDatabaseSpecifier::Ambient => {
1225                        return Err(AdapterError::Catalog(Error::new(
1226                            ErrorKind::ReadOnlySystemSchema(schema_name),
1227                        )));
1228                    }
1229                };
1230                let owner_privileges = vec![rbac::owner_privilege(
1231                    mz_sql::catalog::ObjectType::Schema,
1232                    owner_id,
1233                )];
1234                let default_privileges = state
1235                    .default_privileges
1236                    .get_applicable_privileges(
1237                        owner_id,
1238                        Some(database_id),
1239                        None,
1240                        mz_sql::catalog::ObjectType::Schema,
1241                    )
1242                    .map(|item| item.mz_acl_item(owner_id));
1243                let privileges: Vec<_> =
1244                    merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1245                        .collect();
1246                let (schema_id, _) = tx.insert_user_schema(
1247                    database_id,
1248                    &schema_name,
1249                    owner_id,
1250                    privileges.clone(),
1251                    &state.get_temporary_oids().collect(),
1252                )?;
1253                CatalogState::add_to_audit_log(
1254                    &state.system_configuration,
1255                    oracle_write_ts,
1256                    session,
1257                    tx,
1258                    audit_events,
1259                    EventType::Create,
1260                    ObjectType::Schema,
1261                    EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
1262                        id: schema_id.to_string(),
1263                        name: schema_name.clone(),
1264                        database_name: Some(state.database_by_id[&database_id].name.clone()),
1265                    }),
1266                )?;
1267            }
1268            Op::CreateRole { name, attributes } => {
1269                if is_reserved_role_name(&name) {
1270                    return Err(AdapterError::Catalog(Error::new(
1271                        ErrorKind::ReservedRoleName(name),
1272                    )));
1273                }
1274                let membership = RoleMembership::new();
1275                let vars = RoleVars::default();
1276                let (id, _) = tx.insert_user_role(
1277                    name.clone(),
1278                    attributes.clone(),
1279                    membership.clone(),
1280                    vars.clone(),
1281                    &state.get_temporary_oids().collect(),
1282                )?;
1283                CatalogState::add_to_audit_log(
1284                    &state.system_configuration,
1285                    oracle_write_ts,
1286                    session,
1287                    tx,
1288                    audit_events,
1289                    EventType::Create,
1290                    ObjectType::Role,
1291                    EventDetails::CreateRoleV1(mz_audit_log::CreateRoleV1 {
1292                        id: id.to_string(),
1293                        name: name.clone(),
1294                        auto_provision_source: attributes.auto_provision_source.map(|s| match s {
1295                            AutoProvisionSource::Oidc => "oidc".to_string(),
1296                            AutoProvisionSource::Frontegg => "frontegg".to_string(),
1297                            AutoProvisionSource::None => "none".to_string(),
1298                        }),
1299                    }),
1300                )?;
1301                info!("create role {}", name);
1302            }
1303            Op::CreateCluster {
1304                id,
1305                name,
1306                introspection_sources,
1307                owner_id,
1308                config,
1309            } => {
1310                if is_reserved_name(&name) {
1311                    return Err(AdapterError::Catalog(Error::new(
1312                        ErrorKind::ReservedClusterName(name),
1313                    )));
1314                }
1315                let owner_privileges = vec![rbac::owner_privilege(
1316                    mz_sql::catalog::ObjectType::Cluster,
1317                    owner_id,
1318                )];
1319                let default_privileges = state
1320                    .default_privileges
1321                    .get_applicable_privileges(
1322                        owner_id,
1323                        None,
1324                        None,
1325                        mz_sql::catalog::ObjectType::Cluster,
1326                    )
1327                    .map(|item| item.mz_acl_item(owner_id));
1328                let privileges: Vec<_> =
1329                    merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1330                        .collect();
1331                let introspection_source_ids: Vec<_> = introspection_sources
1332                    .iter()
1333                    .map(|introspection_source| {
1334                        Transaction::allocate_introspection_source_index_id(
1335                            &id,
1336                            introspection_source.variant,
1337                        )
1338                    })
1339                    .collect();
1340
1341                let introspection_sources = introspection_sources
1342                    .into_iter()
1343                    .zip_eq(introspection_source_ids)
1344                    .map(|(log, (item_id, gid))| (log, item_id, gid))
1345                    .collect();
1346
1347                tx.insert_user_cluster(
1348                    id,
1349                    &name,
1350                    introspection_sources,
1351                    owner_id,
1352                    privileges.clone(),
1353                    config.clone().into(),
1354                    &state.get_temporary_oids().collect(),
1355                )?;
1356                CatalogState::add_to_audit_log(
1357                    &state.system_configuration,
1358                    oracle_write_ts,
1359                    session,
1360                    tx,
1361                    audit_events,
1362                    EventType::Create,
1363                    ObjectType::Cluster,
1364                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1365                        id: id.to_string(),
1366                        name: name.clone(),
1367                    }),
1368                )?;
1369                info!("create cluster {}", name);
1370            }
1371            Op::CreateClusterReplica {
1372                cluster_id,
1373                replica_id,
1374                name,
1375                config,
1376                owner_id,
1377                reason,
1378            } => {
1379                if is_reserved_name(&name) {
1380                    return Err(AdapterError::Catalog(Error::new(
1381                        ErrorKind::ReservedReplicaName(name),
1382                    )));
1383                }
1384                let cluster = state.get_cluster(cluster_id);
1385                // The replica id is allocated out-of-band by the durable
1386                // allocator before the transaction, mirroring cluster and item
1387                // ids. Nothing allocates a replica id in-apply.
1388                tx.insert_cluster_replica_with_id(
1389                    cluster_id,
1390                    replica_id,
1391                    &name,
1392                    config.clone().into(),
1393                    owner_id,
1394                )?;
1395                if let ReplicaLocation::Managed(ManagedReplicaLocation {
1396                    size,
1397                    billed_as,
1398                    internal,
1399                    ..
1400                }) = &config.location
1401                {
1402                    let (reason, scheduling_policies) = reason.into_audit_log();
1403                    let details = EventDetails::CreateClusterReplicaV4(
1404                        mz_audit_log::CreateClusterReplicaV4 {
1405                            cluster_id: cluster_id.to_string(),
1406                            cluster_name: cluster.name.clone(),
1407                            replica_id: Some(replica_id.to_string()),
1408                            replica_name: name.clone(),
1409                            logical_size: size.clone(),
1410                            billed_as: billed_as.clone(),
1411                            internal: *internal,
1412                            reason,
1413                            scheduling_policies,
1414                        },
1415                    );
1416                    CatalogState::add_to_audit_log(
1417                        &state.system_configuration,
1418                        oracle_write_ts,
1419                        session,
1420                        tx,
1421                        audit_events,
1422                        EventType::Create,
1423                        ObjectType::ClusterReplica,
1424                        details,
1425                    )?;
1426                }
1427            }
1428            Op::CreateItem {
1429                id,
1430                name,
1431                item,
1432                owner_id,
1433            } => {
1434                state.check_unstable_dependencies(&item)?;
1435
1436                match &item {
1437                    CatalogItem::Table(table) => {
1438                        let gids: Vec<_> = table.global_ids().collect();
1439                        assert_eq!(gids.len(), 1);
1440                        storage_collections_to_create.extend(gids);
1441                    }
1442                    CatalogItem::Source(source) => {
1443                        storage_collections_to_create.insert(source.global_id());
1444                    }
1445                    CatalogItem::MaterializedView(mv) => {
1446                        let mv_gid = mv.global_id_writes();
1447                        if let Some(target_id) = mv.replacement_target {
1448                            let target_gid = state.get_entry(&target_id).latest_global_id();
1449                            let shard_id =
1450                                state.storage_metadata().get_collection_shard(target_gid)?;
1451                            storage_collections_to_register.insert(mv_gid, shard_id);
1452                        } else {
1453                            storage_collections_to_create.insert(mv_gid);
1454                        }
1455                    }
1456                    CatalogItem::Sink(sink) => {
1457                        storage_collections_to_create.insert(sink.global_id());
1458                    }
1459                    CatalogItem::Log(_)
1460                    | CatalogItem::View(_)
1461                    | CatalogItem::Index(_)
1462                    | CatalogItem::Type(_)
1463                    | CatalogItem::Func(_)
1464                    | CatalogItem::Secret(_)
1465                    | CatalogItem::Connection(_) => (),
1466                }
1467
1468                let system_user = session.map_or(false, |s| s.user().is_system_user());
1469                if !system_user {
1470                    if let Some(id @ ClusterId::System(_)) = item.cluster_id() {
1471                        let cluster_name = state.clusters_by_id[&id].name.clone();
1472                        return Err(AdapterError::Catalog(Error::new(
1473                            ErrorKind::ReadOnlyCluster(cluster_name),
1474                        )));
1475                    }
1476                }
1477
1478                let owner_privileges = vec![rbac::owner_privilege(item.typ().into(), owner_id)];
1479                let default_privileges = state
1480                    .default_privileges
1481                    .get_applicable_privileges(
1482                        owner_id,
1483                        name.qualifiers.database_spec.id(),
1484                        Some(name.qualifiers.schema_spec.into()),
1485                        item.typ().into(),
1486                    )
1487                    .map(|item| item.mz_acl_item(owner_id));
1488                // mz_support can read all progress sources.
1489                let progress_source_privilege = if item.is_progress_source() {
1490                    Some(MzAclItem {
1491                        grantee: MZ_SUPPORT_ROLE_ID,
1492                        grantor: owner_id,
1493                        acl_mode: AclMode::SELECT,
1494                    })
1495                } else {
1496                    None
1497                };
1498                let privileges: Vec<_> = merge_mz_acl_items(
1499                    owner_privileges
1500                        .into_iter()
1501                        .chain(default_privileges)
1502                        .chain(progress_source_privilege),
1503                )
1504                .collect();
1505
1506                let temporary_oids = state.get_temporary_oids().collect();
1507
1508                if item.is_temporary() {
1509                    if name.qualifiers.database_spec != ResolvedDatabaseSpecifier::Ambient
1510                        || name.qualifiers.schema_spec != SchemaSpecifier::Temporary
1511                    {
1512                        return Err(AdapterError::Catalog(Error::new(
1513                            ErrorKind::InvalidTemporarySchema,
1514                        )));
1515                    }
1516                    let oid = tx.allocate_oid(&temporary_oids)?;
1517
1518                    let schema_id = name.qualifiers.schema_spec.clone().into();
1519                    let item_type = item.typ();
1520                    let (create_sql, global_id, versions) = item.to_serialized();
1521
1522                    let item = TemporaryItem {
1523                        id,
1524                        oid,
1525                        global_id,
1526                        schema_id,
1527                        name: name.item.clone(),
1528                        create_sql,
1529                        conn_id: item.conn_id().cloned(),
1530                        owner_id,
1531                        privileges: privileges.clone(),
1532                        extra_versions: versions,
1533                    };
1534                    temporary_item_updates.push((item, StateDiff::Addition));
1535
1536                    info!(
1537                        "create temporary {} {} ({})",
1538                        item_type,
1539                        state.resolve_full_name(&name, None),
1540                        id
1541                    );
1542                } else {
1543                    if let Some(temp_id) =
1544                        item.uses()
1545                            .iter()
1546                            .find(|id| match state.try_get_entry(*id) {
1547                                Some(entry) => entry.item().is_temporary(),
1548                                None => temporary_ids.contains(id),
1549                            })
1550                    {
1551                        let temp_item = state.get_entry(temp_id);
1552                        return Err(AdapterError::Catalog(Error::new(
1553                            ErrorKind::InvalidTemporaryDependency(temp_item.name().item.clone()),
1554                        )));
1555                    }
1556                    if name.qualifiers.database_spec == ResolvedDatabaseSpecifier::Ambient
1557                        && !system_user
1558                    {
1559                        let schema_name = state
1560                            .resolve_full_name(&name, session.map(|session| session.conn_id()))
1561                            .schema;
1562                        return Err(AdapterError::Catalog(Error::new(
1563                            ErrorKind::ReadOnlySystemSchema(schema_name),
1564                        )));
1565                    }
1566                    let schema_id = name.qualifiers.schema_spec.clone().into();
1567                    let item_type = item.typ();
1568                    let (create_sql, global_id, versions) = item.to_serialized();
1569                    tx.insert_user_item(
1570                        id,
1571                        global_id,
1572                        schema_id,
1573                        &name.item,
1574                        create_sql,
1575                        owner_id,
1576                        privileges.clone(),
1577                        &temporary_oids,
1578                        versions,
1579                    )?;
1580                    info!(
1581                        "create {} {} ({})",
1582                        item_type,
1583                        state.resolve_full_name(&name, None),
1584                        id
1585                    );
1586                }
1587
1588                if Self::should_audit_log_item(&item) {
1589                    let name = Self::full_name_detail(
1590                        &state.resolve_full_name(&name, session.map(|session| session.conn_id())),
1591                    );
1592                    let details = match &item {
1593                        CatalogItem::Source(s) => {
1594                            let cluster_id = match s.data_source {
1595                                // Ingestion exports don't have their own cluster, but
1596                                // run on their ingestion's cluster.
1597                                DataSourceDesc::IngestionExport { ingestion_id, .. } => {
1598                                    match state.get_entry(&ingestion_id).cluster_id() {
1599                                        Some(cluster_id) => Some(cluster_id.to_string()),
1600                                        None => None,
1601                                    }
1602                                }
1603                                _ => match item.cluster_id() {
1604                                    Some(cluster_id) => Some(cluster_id.to_string()),
1605                                    None => None,
1606                                },
1607                            };
1608
1609                            EventDetails::CreateSourceSinkV4(mz_audit_log::CreateSourceSinkV4 {
1610                                id: id.to_string(),
1611                                cluster_id,
1612                                name,
1613                                external_type: s.source_type().to_string(),
1614                            })
1615                        }
1616                        CatalogItem::Sink(s) => {
1617                            EventDetails::CreateSourceSinkV4(mz_audit_log::CreateSourceSinkV4 {
1618                                id: id.to_string(),
1619                                cluster_id: Some(s.cluster_id.to_string()),
1620                                name,
1621                                external_type: s.sink_type().to_string(),
1622                            })
1623                        }
1624                        CatalogItem::Index(i) => {
1625                            EventDetails::CreateIndexV1(mz_audit_log::CreateIndexV1 {
1626                                id: id.to_string(),
1627                                name,
1628                                cluster_id: i.cluster_id.to_string(),
1629                            })
1630                        }
1631                        CatalogItem::MaterializedView(mv) => {
1632                            EventDetails::CreateMaterializedViewV1(
1633                                mz_audit_log::CreateMaterializedViewV1 {
1634                                    id: id.to_string(),
1635                                    name,
1636                                    cluster_id: mv.cluster_id.to_string(),
1637                                    replacement_target_id: mv
1638                                        .replacement_target
1639                                        .map(|id| id.to_string()),
1640                                },
1641                            )
1642                        }
1643                        CatalogItem::Table(_)
1644                        | CatalogItem::Log(_)
1645                        | CatalogItem::View(_)
1646                        | CatalogItem::Type(_)
1647                        | CatalogItem::Func(_)
1648                        | CatalogItem::Secret(_)
1649                        | CatalogItem::Connection(_) => EventDetails::IdFullNameV1(IdFullNameV1 {
1650                            id: id.to_string(),
1651                            name,
1652                        }),
1653                    };
1654                    CatalogState::add_to_audit_log(
1655                        &state.system_configuration,
1656                        oracle_write_ts,
1657                        session,
1658                        tx,
1659                        audit_events,
1660                        EventType::Create,
1661                        catalog_type_to_audit_object_type(item.typ()),
1662                        details,
1663                    )?;
1664                }
1665            }
1666            Op::CreateNetworkPolicy {
1667                rules,
1668                name,
1669                owner_id,
1670            } => {
1671                if state.network_policies_by_name.contains_key(&name) {
1672                    return Err(AdapterError::PlanError(PlanError::Catalog(
1673                        SqlCatalogError::NetworkPolicyAlreadyExists(name),
1674                    )));
1675                }
1676                if is_reserved_name(&name) {
1677                    return Err(AdapterError::Catalog(Error::new(
1678                        ErrorKind::ReservedNetworkPolicyName(name),
1679                    )));
1680                }
1681
1682                let owner_privileges = vec![rbac::owner_privilege(
1683                    mz_sql::catalog::ObjectType::NetworkPolicy,
1684                    owner_id,
1685                )];
1686                let default_privileges = state
1687                    .default_privileges
1688                    .get_applicable_privileges(
1689                        owner_id,
1690                        None,
1691                        None,
1692                        mz_sql::catalog::ObjectType::NetworkPolicy,
1693                    )
1694                    .map(|item| item.mz_acl_item(owner_id));
1695                let privileges: Vec<_> =
1696                    merge_mz_acl_items(owner_privileges.into_iter().chain(default_privileges))
1697                        .collect();
1698
1699                let temporary_oids: HashSet<_> = state.get_temporary_oids().collect();
1700                let id = tx.insert_user_network_policy(
1701                    name.clone(),
1702                    rules,
1703                    privileges,
1704                    owner_id,
1705                    &temporary_oids,
1706                )?;
1707
1708                CatalogState::add_to_audit_log(
1709                    &state.system_configuration,
1710                    oracle_write_ts,
1711                    session,
1712                    tx,
1713                    audit_events,
1714                    EventType::Create,
1715                    ObjectType::NetworkPolicy,
1716                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1717                        id: id.to_string(),
1718                        name: name.clone(),
1719                    }),
1720                )?;
1721
1722                info!("created network policy {name} ({id})");
1723            }
1724            Op::Comment {
1725                object_id,
1726                sub_component,
1727                comment,
1728            } => {
1729                tx.update_comment(object_id, sub_component, comment)?;
1730                let entry = state.get_comment_id_entry(&object_id);
1731                let should_log = entry
1732                    .map(|entry| Self::should_audit_log_item(entry.item()))
1733                    // Things that aren't catalog entries can't be temp, so should be logged.
1734                    .unwrap_or(true);
1735                // TODO: We need a conn_id to resolve schema names. This means that system-initiated
1736                // comments won't be logged for now.
1737                if let (Some(conn_id), true) =
1738                    (session.map(|session| session.conn_id()), should_log)
1739                {
1740                    CatalogState::add_to_audit_log(
1741                        &state.system_configuration,
1742                        oracle_write_ts,
1743                        session,
1744                        tx,
1745                        audit_events,
1746                        EventType::Comment,
1747                        comment_id_to_audit_object_type(object_id),
1748                        EventDetails::IdNameV1(IdNameV1 {
1749                            // CommentObjectIds don't have a great string representation, but debug will do for now.
1750                            id: format!("{object_id:?}"),
1751                            name: state.comment_id_to_audit_log_name(object_id, conn_id),
1752                        }),
1753                    )?;
1754                }
1755            }
1756            Op::UpdateSourceReferences {
1757                source_id,
1758                references,
1759            } => {
1760                tx.update_source_references(
1761                    source_id,
1762                    references
1763                        .references
1764                        .into_iter()
1765                        .map(|reference| reference.into())
1766                        .collect(),
1767                    references.updated_at,
1768                )?;
1769            }
1770            Op::DropObjects(drop_object_infos) => {
1771                // Generate all of the objects that need to get dropped.
1772                let delta = ObjectsToDrop::generate(drop_object_infos, state, session)?;
1773
1774                // Drop any associated comments.
1775                tx.drop_comments(&delta.comments)?;
1776
1777                // Drop any items.
1778                let (durable_items_to_drop, temporary_items_to_drop): (BTreeSet<_>, BTreeSet<_>) =
1779                    delta
1780                        .items
1781                        .iter()
1782                        .map(|id| id)
1783                        .partition(|id| !state.get_entry(*id).item().is_temporary());
1784                tx.remove_items(&durable_items_to_drop)?;
1785                temporary_item_updates.extend(temporary_items_to_drop.into_iter().map(|id| {
1786                    let entry = state.get_entry(&id);
1787                    (entry.clone().into(), StateDiff::Retraction)
1788                }));
1789
1790                for item_id in delta.items {
1791                    let entry = state.get_entry(&item_id);
1792
1793                    if entry.item().is_storage_collection() {
1794                        storage_collections_to_drop.extend(entry.global_ids());
1795                    }
1796
1797                    if state.source_references.contains_key(&item_id) {
1798                        tx.remove_source_references(item_id)?;
1799                    }
1800
1801                    if Self::should_audit_log_item(entry.item()) {
1802                        CatalogState::add_to_audit_log(
1803                            &state.system_configuration,
1804                            oracle_write_ts,
1805                            session,
1806                            tx,
1807                            audit_events,
1808                            EventType::Drop,
1809                            catalog_type_to_audit_object_type(entry.item().typ()),
1810                            EventDetails::IdFullNameV1(IdFullNameV1 {
1811                                id: item_id.to_string(),
1812                                name: Self::full_name_detail(&state.resolve_full_name(
1813                                    entry.name(),
1814                                    session.map(|session| session.conn_id()),
1815                                )),
1816                            }),
1817                        )?;
1818                    }
1819                    info!(
1820                        "drop {} {} ({})",
1821                        entry.item_type(),
1822                        state.resolve_full_name(entry.name(), entry.conn_id()),
1823                        item_id
1824                    );
1825                }
1826
1827                // Drop any schemas.
1828                let schemas = delta
1829                    .schemas
1830                    .iter()
1831                    .map(|(schema_spec, database_spec)| {
1832                        (SchemaId::from(schema_spec), *database_spec)
1833                    })
1834                    .collect();
1835                tx.remove_schemas(&schemas)?;
1836
1837                for (schema_spec, database_spec) in delta.schemas {
1838                    let schema = state.get_schema(
1839                        &database_spec,
1840                        &schema_spec,
1841                        session
1842                            .map(|session| session.conn_id())
1843                            .unwrap_or(&SYSTEM_CONN_ID),
1844                    );
1845
1846                    let schema_id = SchemaId::from(schema_spec);
1847                    let database_id = match database_spec {
1848                        ResolvedDatabaseSpecifier::Ambient => None,
1849                        ResolvedDatabaseSpecifier::Id(database_id) => Some(database_id),
1850                    };
1851
1852                    CatalogState::add_to_audit_log(
1853                        &state.system_configuration,
1854                        oracle_write_ts,
1855                        session,
1856                        tx,
1857                        audit_events,
1858                        EventType::Drop,
1859                        ObjectType::Schema,
1860                        EventDetails::SchemaV2(mz_audit_log::SchemaV2 {
1861                            id: schema_id.to_string(),
1862                            name: schema.name.schema.to_string(),
1863                            database_name: database_id
1864                                .map(|database_id| state.database_by_id[&database_id].name.clone()),
1865                        }),
1866                    )?;
1867                }
1868
1869                // Drop any databases.
1870                tx.remove_databases(&delta.databases)?;
1871
1872                for database_id in delta.databases {
1873                    let database = state.get_database(&database_id).clone();
1874
1875                    CatalogState::add_to_audit_log(
1876                        &state.system_configuration,
1877                        oracle_write_ts,
1878                        session,
1879                        tx,
1880                        audit_events,
1881                        EventType::Drop,
1882                        ObjectType::Database,
1883                        EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1884                            id: database_id.to_string(),
1885                            name: database.name.clone(),
1886                        }),
1887                    )?;
1888                }
1889
1890                // Drop any roles.
1891                tx.remove_user_roles(&delta.roles)?;
1892
1893                for role_id in delta.roles {
1894                    let role = state
1895                        .roles_by_id
1896                        .get(&role_id)
1897                        .expect("catalog out of sync");
1898
1899                    CatalogState::add_to_audit_log(
1900                        &state.system_configuration,
1901                        oracle_write_ts,
1902                        session,
1903                        tx,
1904                        audit_events,
1905                        EventType::Drop,
1906                        ObjectType::Role,
1907                        EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1908                            id: role.id.to_string(),
1909                            name: role.name.clone(),
1910                        }),
1911                    )?;
1912                    info!("drop role {}", role.name());
1913                }
1914
1915                // Drop any network policies.
1916                tx.remove_network_policies(&delta.network_policies)?;
1917
1918                for network_policy_id in delta.network_policies {
1919                    let policy = state
1920                        .network_policies_by_id
1921                        .get(&network_policy_id)
1922                        .expect("catalog out of sync");
1923
1924                    CatalogState::add_to_audit_log(
1925                        &state.system_configuration,
1926                        oracle_write_ts,
1927                        session,
1928                        tx,
1929                        audit_events,
1930                        EventType::Drop,
1931                        ObjectType::NetworkPolicy,
1932                        EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1933                            id: policy.id.to_string(),
1934                            name: policy.name.clone(),
1935                        }),
1936                    )?;
1937                    info!("drop network policy {}", policy.name.clone());
1938                }
1939
1940                // Drop any replicas.
1941                let replicas = delta.replicas.keys().copied().collect();
1942                tx.remove_cluster_replicas(&replicas)?;
1943
1944                for (replica_id, (cluster_id, reason)) in delta.replicas {
1945                    let cluster = state.get_cluster(cluster_id);
1946                    let replica = cluster.replica(replica_id).expect("Must exist");
1947
1948                    let (reason, scheduling_policies) = reason.into_audit_log();
1949                    let details =
1950                        EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
1951                            cluster_id: cluster_id.to_string(),
1952                            cluster_name: cluster.name.clone(),
1953                            replica_id: Some(replica_id.to_string()),
1954                            replica_name: replica.name.clone(),
1955                            reason,
1956                            scheduling_policies,
1957                        });
1958                    CatalogState::add_to_audit_log(
1959                        &state.system_configuration,
1960                        oracle_write_ts,
1961                        session,
1962                        tx,
1963                        audit_events,
1964                        EventType::Drop,
1965                        ObjectType::ClusterReplica,
1966                        details,
1967                    )?;
1968                }
1969
1970                // Drop any clusters.
1971                tx.remove_clusters(&delta.clusters)?;
1972
1973                for cluster_id in delta.clusters {
1974                    let cluster = state.get_cluster(cluster_id);
1975
1976                    CatalogState::add_to_audit_log(
1977                        &state.system_configuration,
1978                        oracle_write_ts,
1979                        session,
1980                        tx,
1981                        audit_events,
1982                        EventType::Drop,
1983                        ObjectType::Cluster,
1984                        EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1985                            id: cluster.id.to_string(),
1986                            name: cluster.name.clone(),
1987                        }),
1988                    )?;
1989                }
1990            }
1991            Op::GrantRole {
1992                role_id,
1993                member_id,
1994                grantor_id,
1995            } => {
1996                state.ensure_not_reserved_role(&member_id)?;
1997                state.ensure_grantable_role(&role_id)?;
1998                if state.collect_role_membership(&role_id).contains(&member_id) {
1999                    let group_role = state.get_role(&role_id);
2000                    let member_role = state.get_role(&member_id);
2001                    return Err(AdapterError::Catalog(Error::new(
2002                        ErrorKind::CircularRoleMembership {
2003                            role_name: group_role.name().to_string(),
2004                            member_name: member_role.name().to_string(),
2005                        },
2006                    )));
2007                }
2008                let mut member_role = state.get_role(&member_id).clone();
2009                member_role.membership.map.insert(role_id, grantor_id);
2010                tx.update_role(member_id, member_role.into(), PasswordAction::NoChange)?;
2011
2012                CatalogState::add_to_audit_log(
2013                    &state.system_configuration,
2014                    oracle_write_ts,
2015                    session,
2016                    tx,
2017                    audit_events,
2018                    EventType::Grant,
2019                    ObjectType::Role,
2020                    EventDetails::GrantRoleV2(mz_audit_log::GrantRoleV2 {
2021                        role_id: role_id.to_string(),
2022                        member_id: member_id.to_string(),
2023                        grantor_id: grantor_id.to_string(),
2024                        executed_by: session
2025                            .map(|session| session.authenticated_role_id())
2026                            .unwrap_or(&MZ_SYSTEM_ROLE_ID)
2027                            .to_string(),
2028                    }),
2029                )?;
2030            }
2031            Op::RevokeRole {
2032                role_id,
2033                member_id,
2034                grantor_id,
2035            } => {
2036                state.ensure_not_reserved_role(&member_id)?;
2037                state.ensure_grantable_role(&role_id)?;
2038                let mut member_role = state.get_role(&member_id).clone();
2039                member_role.membership.map.remove(&role_id);
2040                tx.update_role(member_id, member_role.into(), PasswordAction::NoChange)?;
2041
2042                CatalogState::add_to_audit_log(
2043                    &state.system_configuration,
2044                    oracle_write_ts,
2045                    session,
2046                    tx,
2047                    audit_events,
2048                    EventType::Revoke,
2049                    ObjectType::Role,
2050                    EventDetails::RevokeRoleV2(mz_audit_log::RevokeRoleV2 {
2051                        role_id: role_id.to_string(),
2052                        member_id: member_id.to_string(),
2053                        grantor_id: grantor_id.to_string(),
2054                        executed_by: session
2055                            .map(|session| session.authenticated_role_id())
2056                            .unwrap_or(&MZ_SYSTEM_ROLE_ID)
2057                            .to_string(),
2058                    }),
2059                )?;
2060            }
2061            Op::UpdatePrivilege {
2062                target_id,
2063                privileges,
2064                variant,
2065            } => {
2066                let update_privilege_fn = |target_privileges: &mut PrivilegeMap| {
2067                    for privilege in &privileges {
2068                        match variant {
2069                            UpdatePrivilegeVariant::Grant => {
2070                                target_privileges.grant(privilege.clone());
2071                            }
2072                            UpdatePrivilegeVariant::Revoke => {
2073                                target_privileges.revoke(privilege);
2074                            }
2075                        }
2076                    }
2077                };
2078                match &target_id {
2079                    SystemObjectId::Object(object_id) => match object_id {
2080                        ObjectId::Cluster(id) => {
2081                            let mut cluster = state.get_cluster(*id).clone();
2082                            update_privilege_fn(&mut cluster.privileges);
2083                            tx.update_cluster(*id, cluster.into())?;
2084                        }
2085                        ObjectId::Database(id) => {
2086                            let mut database = state.get_database(id).clone();
2087                            update_privilege_fn(&mut database.privileges);
2088                            tx.update_database(*id, database.into())?;
2089                        }
2090                        ObjectId::NetworkPolicy(id) => {
2091                            let mut policy = state.get_network_policy(id).clone();
2092                            update_privilege_fn(&mut policy.privileges);
2093                            tx.update_network_policy(*id, policy.into())?;
2094                        }
2095                        ObjectId::Schema((database_spec, schema_spec)) => {
2096                            let schema_id = schema_spec.clone().into();
2097                            let mut schema = state
2098                                .get_schema(
2099                                    database_spec,
2100                                    schema_spec,
2101                                    session
2102                                        .map(|session| session.conn_id())
2103                                        .unwrap_or(&SYSTEM_CONN_ID),
2104                                )
2105                                .clone();
2106                            update_privilege_fn(&mut schema.privileges);
2107                            tx.update_schema(schema_id, schema.into())?;
2108                        }
2109                        ObjectId::Item(id) => {
2110                            let entry = state.get_entry(id);
2111                            let mut new_entry = entry.clone();
2112                            update_privilege_fn(&mut new_entry.privileges);
2113                            if !new_entry.item().is_temporary() {
2114                                tx.update_item(*id, new_entry.into())?;
2115                            } else {
2116                                temporary_item_updates
2117                                    .push((entry.clone().into(), StateDiff::Retraction));
2118                                temporary_item_updates
2119                                    .push((new_entry.into(), StateDiff::Addition));
2120                            }
2121                        }
2122                        ObjectId::Role(_) | ObjectId::ClusterReplica(_) => {}
2123                    },
2124                    SystemObjectId::System => {
2125                        let mut system_privileges = PrivilegeMap::clone(&state.system_privileges);
2126                        update_privilege_fn(&mut system_privileges);
2127                        for privilege in &privileges {
2128                            let new_privilege = system_privileges
2129                                .get_acl_item(&privilege.grantee, &privilege.grantor);
2130                            tx.set_system_privilege(
2131                                privilege.grantee,
2132                                privilege.grantor,
2133                                new_privilege.map(|new_privilege| new_privilege.acl_mode),
2134                            )?;
2135                        }
2136                    }
2137                }
2138                let object_type = state.get_system_object_type(&target_id);
2139                let object_id_str = match &target_id {
2140                    SystemObjectId::System => "SYSTEM".to_string(),
2141                    SystemObjectId::Object(id) => id.to_string(),
2142                };
2143                // One audit event per grantee, even though the batch is a single durable write.
2144                for privilege in &privileges {
2145                    CatalogState::add_to_audit_log(
2146                        &state.system_configuration,
2147                        oracle_write_ts,
2148                        session,
2149                        tx,
2150                        audit_events,
2151                        variant.into(),
2152                        system_object_type_to_audit_object_type(&object_type),
2153                        EventDetails::UpdatePrivilegeV1(mz_audit_log::UpdatePrivilegeV1 {
2154                            object_id: object_id_str.clone(),
2155                            grantee_id: privilege.grantee.to_string(),
2156                            grantor_id: privilege.grantor.to_string(),
2157                            privileges: privilege.acl_mode.to_string(),
2158                        }),
2159                    )?;
2160                }
2161            }
2162            Op::UpdateDefaultPrivilege {
2163                privilege_object,
2164                privilege_acl_item,
2165                variant,
2166            } => {
2167                let mut default_privileges = DefaultPrivileges::clone(&state.default_privileges);
2168                match variant {
2169                    UpdatePrivilegeVariant::Grant => default_privileges
2170                        .grant(privilege_object.clone(), privilege_acl_item.clone()),
2171                    UpdatePrivilegeVariant::Revoke => {
2172                        default_privileges.revoke(&privilege_object, &privilege_acl_item)
2173                    }
2174                }
2175                let new_acl_mode = default_privileges
2176                    .get_privileges_for_grantee(&privilege_object, &privilege_acl_item.grantee);
2177                tx.set_default_privilege(
2178                    privilege_object.role_id,
2179                    privilege_object.database_id,
2180                    privilege_object.schema_id,
2181                    privilege_object.object_type,
2182                    privilege_acl_item.grantee,
2183                    new_acl_mode.cloned(),
2184                )?;
2185                CatalogState::add_to_audit_log(
2186                    &state.system_configuration,
2187                    oracle_write_ts,
2188                    session,
2189                    tx,
2190                    audit_events,
2191                    variant.into(),
2192                    object_type_to_audit_object_type(privilege_object.object_type),
2193                    EventDetails::AlterDefaultPrivilegeV1(mz_audit_log::AlterDefaultPrivilegeV1 {
2194                        role_id: privilege_object.role_id.to_string(),
2195                        database_id: privilege_object.database_id.map(|id| id.to_string()),
2196                        schema_id: privilege_object.schema_id.map(|id| id.to_string()),
2197                        grantee_id: privilege_acl_item.grantee.to_string(),
2198                        privileges: privilege_acl_item.acl_mode.to_string(),
2199                    }),
2200                )?;
2201            }
2202            Op::RenameCluster {
2203                id,
2204                name,
2205                to_name,
2206                check_reserved_names,
2207            } => {
2208                if id.is_system() {
2209                    return Err(AdapterError::Catalog(Error::new(
2210                        ErrorKind::ReadOnlyCluster(name.clone()),
2211                    )));
2212                }
2213                if check_reserved_names && is_reserved_name(&to_name) {
2214                    return Err(AdapterError::Catalog(Error::new(
2215                        ErrorKind::ReservedClusterName(to_name),
2216                    )));
2217                }
2218                tx.rename_cluster(id, &name, &to_name)?;
2219                CatalogState::add_to_audit_log(
2220                    &state.system_configuration,
2221                    oracle_write_ts,
2222                    session,
2223                    tx,
2224                    audit_events,
2225                    EventType::Alter,
2226                    ObjectType::Cluster,
2227                    EventDetails::RenameClusterV1(mz_audit_log::RenameClusterV1 {
2228                        id: id.to_string(),
2229                        old_name: name.clone(),
2230                        new_name: to_name.clone(),
2231                    }),
2232                )?;
2233                info!("rename cluster {name} to {to_name}");
2234            }
2235            Op::RenameClusterReplica {
2236                cluster_id,
2237                replica_id,
2238                name,
2239                to_name,
2240            } => {
2241                if is_reserved_name(&to_name) {
2242                    return Err(AdapterError::Catalog(Error::new(
2243                        ErrorKind::ReservedReplicaName(to_name),
2244                    )));
2245                }
2246                tx.rename_cluster_replica(replica_id, &name, &to_name)?;
2247                CatalogState::add_to_audit_log(
2248                    &state.system_configuration,
2249                    oracle_write_ts,
2250                    session,
2251                    tx,
2252                    audit_events,
2253                    EventType::Alter,
2254                    ObjectType::ClusterReplica,
2255                    EventDetails::RenameClusterReplicaV1(mz_audit_log::RenameClusterReplicaV1 {
2256                        cluster_id: cluster_id.to_string(),
2257                        replica_id: replica_id.to_string(),
2258                        old_name: name.replica.as_str().to_string(),
2259                        new_name: to_name.clone(),
2260                    }),
2261                )?;
2262                info!("rename cluster replica {name} to {to_name}");
2263            }
2264            Op::RenameItem {
2265                id,
2266                to_name,
2267                current_full_name,
2268            } => {
2269                let mut updates = Vec::new();
2270
2271                let entry = state.get_entry(&id);
2272                if let CatalogItem::Type(_) = entry.item() {
2273                    return Err(AdapterError::Catalog(Error::new(ErrorKind::TypeRename(
2274                        current_full_name.to_string(),
2275                    ))));
2276                }
2277
2278                if entry.id().is_system() {
2279                    let name = state
2280                        .resolve_full_name(entry.name(), session.map(|session| session.conn_id()));
2281                    return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
2282                        name.to_string(),
2283                    ))));
2284                }
2285
2286                let mut to_full_name = current_full_name.clone();
2287                to_full_name.item.clone_from(&to_name);
2288
2289                let mut to_qualified_name = entry.name().clone();
2290                to_qualified_name.item.clone_from(&to_name);
2291
2292                let details = EventDetails::RenameItemV1(mz_audit_log::RenameItemV1 {
2293                    id: id.to_string(),
2294                    old_name: Self::full_name_detail(&current_full_name),
2295                    new_name: Self::full_name_detail(&to_full_name),
2296                });
2297                if Self::should_audit_log_item(entry.item()) {
2298                    CatalogState::add_to_audit_log(
2299                        &state.system_configuration,
2300                        oracle_write_ts,
2301                        session,
2302                        tx,
2303                        audit_events,
2304                        EventType::Alter,
2305                        catalog_type_to_audit_object_type(entry.item().typ()),
2306                        details,
2307                    )?;
2308                }
2309
2310                // Rename item itself.
2311                let mut new_entry = entry.clone();
2312                new_entry.name.item.clone_from(&to_name);
2313                new_entry.item = entry
2314                    .item()
2315                    .rename_item_refs(current_full_name.clone(), to_full_name.item.clone(), true)
2316                    .map_err(|e| {
2317                        Error::new(ErrorKind::from(AmbiguousRename {
2318                            depender: state
2319                                .resolve_full_name(entry.name(), entry.conn_id())
2320                                .to_string(),
2321                            dependee: state
2322                                .resolve_full_name(entry.name(), entry.conn_id())
2323                                .to_string(),
2324                            message: e,
2325                        }))
2326                    })?;
2327
2328                for id in entry.referenced_by() {
2329                    let dependent_item = state.get_entry(id);
2330                    let mut to_entry = dependent_item.clone();
2331                    to_entry.item = dependent_item
2332                        .item()
2333                        .rename_item_refs(
2334                            current_full_name.clone(),
2335                            to_full_name.item.clone(),
2336                            false,
2337                        )
2338                        .map_err(|e| {
2339                            Error::new(ErrorKind::from(AmbiguousRename {
2340                                depender: state
2341                                    .resolve_full_name(
2342                                        dependent_item.name(),
2343                                        dependent_item.conn_id(),
2344                                    )
2345                                    .to_string(),
2346                                dependee: state
2347                                    .resolve_full_name(entry.name(), entry.conn_id())
2348                                    .to_string(),
2349                                message: e,
2350                            }))
2351                        })?;
2352
2353                    if !to_entry.item().is_temporary() {
2354                        tx.update_item(*id, to_entry.into())?;
2355                    } else {
2356                        temporary_item_updates
2357                            .push((dependent_item.clone().into(), StateDiff::Retraction));
2358                        temporary_item_updates.push((to_entry.into(), StateDiff::Addition));
2359                    }
2360                    updates.push(*id);
2361                }
2362                if !new_entry.item().is_temporary() {
2363                    tx.update_item(id, new_entry.into())?;
2364                } else {
2365                    temporary_item_updates.push((entry.clone().into(), StateDiff::Retraction));
2366                    temporary_item_updates.push((new_entry.into(), StateDiff::Addition));
2367                }
2368
2369                updates.push(id);
2370                for id in updates {
2371                    Self::log_update(state, &id);
2372                }
2373            }
2374            Op::RenameSchema {
2375                database_spec,
2376                schema_spec,
2377                new_name,
2378                check_reserved_names,
2379            } => {
2380                if check_reserved_names && is_reserved_name(&new_name) {
2381                    return Err(AdapterError::Catalog(Error::new(
2382                        ErrorKind::ReservedSchemaName(new_name),
2383                    )));
2384                }
2385
2386                let conn_id = session
2387                    .map(|session| session.conn_id())
2388                    .unwrap_or(&SYSTEM_CONN_ID);
2389
2390                let schema = state.get_schema(&database_spec, &schema_spec, conn_id);
2391                let cur_name = schema.name().schema.clone();
2392
2393                let ResolvedDatabaseSpecifier::Id(database_id) = database_spec else {
2394                    return Err(AdapterError::Catalog(Error::new(
2395                        ErrorKind::AmbientSchemaRename(cur_name),
2396                    )));
2397                };
2398                let database = state.get_database(&database_id);
2399                let database_name = &database.name;
2400
2401                let mut updates: Vec<CatalogItemId> = Vec::new();
2402                let mut items_to_update = BTreeMap::new();
2403                // Dedup guard covering both persistent and temporary items. An
2404                // item can be reached more than once: once as a member of the
2405                // schema and again for each object in the schema it depends on.
2406                // Persistent items are also tracked in `items_to_update`, but
2407                // temporary items only land in `temporary_item_updates` (a Vec
2408                // with no dedup), so a temp dependent referencing multiple
2409                // objects in the renamed schema would otherwise be enqueued once
2410                // per reference. That produces duplicate retraction/addition
2411                // updates that consolidate to an invalid diff (e.g. -2) and
2412                // panic catalog apply.
2413                let mut seen: BTreeSet<CatalogItemId> = BTreeSet::new();
2414
2415                let mut update_item = |id: &CatalogItemId| {
2416                    if !seen.insert(*id) {
2417                        return Ok(());
2418                    }
2419
2420                    let entry = state.get_entry(id);
2421
2422                    // Update our item.
2423                    let mut new_entry = entry.clone();
2424                    new_entry.item = entry
2425                        .item
2426                        .rename_schema_refs(database_name, &cur_name, &new_name)
2427                        .map_err(|(s, _i)| {
2428                            Error::new(ErrorKind::from(AmbiguousRename {
2429                                depender: state
2430                                    .resolve_full_name(entry.name(), entry.conn_id())
2431                                    .to_string(),
2432                                dependee: format!("{database_name}.{cur_name}"),
2433                                message: format!("ambiguous reference to schema named {s}"),
2434                            }))
2435                        })?;
2436
2437                    // Queue updates for Catalog storage and Builtin Tables.
2438                    if !new_entry.item().is_temporary() {
2439                        items_to_update.insert(*id, new_entry.into());
2440                    } else {
2441                        temporary_item_updates.push((entry.clone().into(), StateDiff::Retraction));
2442                        temporary_item_updates.push((new_entry.into(), StateDiff::Addition));
2443                    }
2444                    updates.push(*id);
2445
2446                    Ok::<_, AdapterError>(())
2447                };
2448
2449                // Update all of the items in the schema. A schema holds items,
2450                // types, and functions in separate maps, and any of them may be
2451                // referenced by another object's create_sql via a schema-qualified
2452                // name, so all three must be rewritten.
2453                for (_name, item_id) in schema
2454                    .items
2455                    .iter()
2456                    .chain(schema.types.iter())
2457                    .chain(schema.functions.iter())
2458                {
2459                    // Update the item itself.
2460                    update_item(item_id)?;
2461
2462                    // Update everything that depends on this item.
2463                    for id in state.get_entry(item_id).referenced_by() {
2464                        update_item(id)?;
2465                    }
2466                }
2467                // Note: When updating the transaction it's very important that we update the
2468                // items as a whole group, otherwise we exhibit quadratic behavior.
2469                tx.update_items(items_to_update)?;
2470
2471                // Renaming temporary schemas is not supported.
2472                let SchemaSpecifier::Id(schema_id) = *schema.id() else {
2473                    let schema_name = schema.name().schema.clone();
2474                    return Err(AdapterError::Catalog(crate::catalog::Error::new(
2475                        crate::catalog::ErrorKind::ReadOnlySystemSchema(schema_name),
2476                    )));
2477                };
2478
2479                // Add an entry to the audit log.
2480                let database_name = database_spec
2481                    .id()
2482                    .map(|id| state.get_database(&id).name.clone());
2483                let details = EventDetails::RenameSchemaV1(mz_audit_log::RenameSchemaV1 {
2484                    id: schema_id.to_string(),
2485                    old_name: schema.name().schema.clone(),
2486                    new_name: new_name.clone(),
2487                    database_name,
2488                });
2489                CatalogState::add_to_audit_log(
2490                    &state.system_configuration,
2491                    oracle_write_ts,
2492                    session,
2493                    tx,
2494                    audit_events,
2495                    EventType::Alter,
2496                    mz_audit_log::ObjectType::Schema,
2497                    details,
2498                )?;
2499
2500                // Update the schema itself.
2501                let mut new_schema = schema.clone();
2502                new_schema.name.schema.clone_from(&new_name);
2503                tx.update_schema(schema_id, new_schema.into())?;
2504
2505                for id in updates {
2506                    Self::log_update(state, &id);
2507                }
2508            }
2509            Op::UpdateOwner { id, new_owner } => {
2510                let conn_id = session
2511                    .map(|session| session.conn_id())
2512                    .unwrap_or(&SYSTEM_CONN_ID);
2513                let old_owner = state
2514                    .get_owner_id(&id, conn_id)
2515                    .expect("cannot update the owner of an object without an owner");
2516                match &id {
2517                    ObjectId::Cluster(id) => {
2518                        let mut cluster = state.get_cluster(*id).clone();
2519                        if id.is_system() {
2520                            return Err(AdapterError::Catalog(Error::new(
2521                                ErrorKind::ReadOnlyCluster(cluster.name),
2522                            )));
2523                        }
2524                        Self::update_privilege_owners(
2525                            &mut cluster.privileges,
2526                            cluster.owner_id,
2527                            new_owner,
2528                        );
2529                        cluster.owner_id = new_owner;
2530                        tx.update_cluster(*id, cluster.into())?;
2531                    }
2532                    ObjectId::ClusterReplica((cluster_id, replica_id)) => {
2533                        let cluster = state.get_cluster(*cluster_id);
2534                        let mut replica = cluster
2535                            .replica(*replica_id)
2536                            .expect("catalog out of sync")
2537                            .clone();
2538                        if replica_id.is_system() {
2539                            return Err(AdapterError::Catalog(Error::new(
2540                                ErrorKind::ReadOnlyClusterReplica(replica.name),
2541                            )));
2542                        }
2543                        replica.owner_id = new_owner;
2544                        tx.update_cluster_replica(*replica_id, replica.into())?;
2545                    }
2546                    ObjectId::Database(id) => {
2547                        let mut database = state.get_database(id).clone();
2548                        if id.is_system() {
2549                            return Err(AdapterError::Catalog(Error::new(
2550                                ErrorKind::ReadOnlyDatabase(database.name),
2551                            )));
2552                        }
2553                        Self::update_privilege_owners(
2554                            &mut database.privileges,
2555                            database.owner_id,
2556                            new_owner,
2557                        );
2558                        database.owner_id = new_owner;
2559                        tx.update_database(*id, database.clone().into())?;
2560                    }
2561                    ObjectId::Schema((database_spec, schema_spec)) => {
2562                        let schema_id: SchemaId = schema_spec.clone().into();
2563                        let mut schema = state
2564                            .get_schema(database_spec, schema_spec, conn_id)
2565                            .clone();
2566                        if schema_id.is_system() {
2567                            let name = schema.name();
2568                            let full_name = state.resolve_full_schema_name(name);
2569                            return Err(AdapterError::Catalog(Error::new(
2570                                ErrorKind::ReadOnlySystemSchema(full_name.to_string()),
2571                            )));
2572                        }
2573                        Self::update_privilege_owners(
2574                            &mut schema.privileges,
2575                            schema.owner_id,
2576                            new_owner,
2577                        );
2578                        schema.owner_id = new_owner;
2579                        tx.update_schema(schema_id, schema.into())?;
2580                    }
2581                    ObjectId::Item(id) => {
2582                        let entry = state.get_entry(id);
2583                        let mut new_entry = entry.clone();
2584                        if id.is_system() {
2585                            let full_name = state.resolve_full_name(
2586                                new_entry.name(),
2587                                session.map(|session| session.conn_id()),
2588                            );
2589                            return Err(AdapterError::Catalog(Error::new(
2590                                ErrorKind::ReadOnlyItem(full_name.to_string()),
2591                            )));
2592                        }
2593                        Self::update_privilege_owners(
2594                            &mut new_entry.privileges,
2595                            new_entry.owner_id,
2596                            new_owner,
2597                        );
2598                        new_entry.owner_id = new_owner;
2599                        if !new_entry.item().is_temporary() {
2600                            tx.update_item(*id, new_entry.into())?;
2601                        } else {
2602                            temporary_item_updates
2603                                .push((entry.clone().into(), StateDiff::Retraction));
2604                            temporary_item_updates.push((new_entry.into(), StateDiff::Addition));
2605                        }
2606                    }
2607                    ObjectId::NetworkPolicy(id) => {
2608                        let mut policy = state.get_network_policy(id).clone();
2609                        if id.is_system() {
2610                            return Err(AdapterError::Catalog(Error::new(
2611                                ErrorKind::ReadOnlyNetworkPolicy(policy.name),
2612                            )));
2613                        }
2614                        Self::update_privilege_owners(
2615                            &mut policy.privileges,
2616                            policy.owner_id,
2617                            new_owner,
2618                        );
2619                        policy.owner_id = new_owner;
2620                        tx.update_network_policy(*id, policy.into())?;
2621                    }
2622                    ObjectId::Role(_) => unreachable!("roles have no owner"),
2623                }
2624                let object_type = state.get_object_type(&id);
2625                CatalogState::add_to_audit_log(
2626                    &state.system_configuration,
2627                    oracle_write_ts,
2628                    session,
2629                    tx,
2630                    audit_events,
2631                    EventType::Alter,
2632                    object_type_to_audit_object_type(object_type),
2633                    EventDetails::UpdateOwnerV1(mz_audit_log::UpdateOwnerV1 {
2634                        object_id: id.to_string(),
2635                        old_owner_id: old_owner.to_string(),
2636                        new_owner_id: new_owner.to_string(),
2637                    }),
2638                )?;
2639            }
2640            Op::UpdateClusterConfig { id, name, config } => {
2641                let mut cluster = state.get_cluster(id).clone();
2642                cluster.config = config;
2643                tx.update_cluster(id, cluster.into())?;
2644                info!("update cluster {}", name);
2645
2646                CatalogState::add_to_audit_log(
2647                    &state.system_configuration,
2648                    oracle_write_ts,
2649                    session,
2650                    tx,
2651                    audit_events,
2652                    EventType::Alter,
2653                    ObjectType::Cluster,
2654                    EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
2655                        id: id.to_string(),
2656                        name,
2657                    }),
2658                )?;
2659            }
2660            Op::UpdateClusterReplicaConfig {
2661                replica_id,
2662                cluster_id,
2663                config,
2664            } => {
2665                let replica = state.get_cluster_replica(cluster_id, replica_id).to_owned();
2666                info!("update replica {}", replica.name);
2667                tx.update_cluster_replica(
2668                    replica_id,
2669                    mz_catalog::durable::ClusterReplica {
2670                        cluster_id,
2671                        replica_id,
2672                        name: replica.name.clone(),
2673                        config: config.clone().into(),
2674                        owner_id: replica.owner_id,
2675                    },
2676                )?;
2677            }
2678            Op::UpdateItem { id, name, to_item } => {
2679                // A non-temporary item must not depend on a temporary one.
2680                // Temporary objects are session-scoped and never persisted, so
2681                // a durable item referencing one is a dangling reference that
2682                // panics the coordinator when its create_sql is re-planned on
2683                // catalog apply (apply emits durable items before temporary
2684                // ones, relying on this invariant). `Op::CreateItem` enforces
2685                // it; mirror it here so ALTER paths (e.g. ALTER SINK ... SET
2686                // FROM) cannot repoint a persistent item at a temporary one.
2687                if !to_item.is_temporary() {
2688                    if let Some(temp_id) =
2689                        to_item
2690                            .uses()
2691                            .iter()
2692                            .find(|id| match state.try_get_entry(*id) {
2693                                Some(entry) => entry.item().is_temporary(),
2694                                None => temporary_ids.contains(id),
2695                            })
2696                    {
2697                        let temp_item = state.get_entry(temp_id);
2698                        return Err(AdapterError::Catalog(Error::new(
2699                            ErrorKind::InvalidTemporaryDependency(temp_item.name().item.clone()),
2700                        )));
2701                    }
2702                }
2703
2704                let mut entry = state.get_entry(&id).clone();
2705                entry.name = name.clone();
2706                entry.item = to_item.clone();
2707                tx.update_item(id, entry.into())?;
2708
2709                if Self::should_audit_log_item(&to_item) {
2710                    let mut full_name = Self::full_name_detail(
2711                        &state.resolve_full_name(&name, session.map(|session| session.conn_id())),
2712                    );
2713                    full_name.item = name.item;
2714
2715                    CatalogState::add_to_audit_log(
2716                        &state.system_configuration,
2717                        oracle_write_ts,
2718                        session,
2719                        tx,
2720                        audit_events,
2721                        EventType::Alter,
2722                        catalog_type_to_audit_object_type(to_item.typ()),
2723                        EventDetails::UpdateItemV1(mz_audit_log::UpdateItemV1 {
2724                            id: id.to_string(),
2725                            name: full_name,
2726                        }),
2727                    )?;
2728                }
2729
2730                Self::log_update(state, &id);
2731            }
2732            Op::UpdateSystemConfiguration { name, value } => {
2733                let parsed_value = state.parse_system_configuration(&name, value.borrow())?;
2734                tx.upsert_system_config(&name, parsed_value.clone())?;
2735                // This mirrors some "system vars" into the catalog storage
2736                // "config" collection so that we can toggle the flag with
2737                // Launch Darkly, but use it in boot before Launch Darkly is
2738                // available.
2739                if name == WITH_0DT_DEPLOYMENT_MAX_WAIT.name() {
2740                    let with_0dt_deployment_max_wait =
2741                        Duration::parse(VarInput::Flat(&parsed_value))
2742                            .expect("parsing succeeded above");
2743                    tx.set_0dt_deployment_max_wait(with_0dt_deployment_max_wait)?;
2744                } else if name == WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name() {
2745                    let with_0dt_deployment_ddl_check_interval =
2746                        Duration::parse(VarInput::Flat(&parsed_value))
2747                            .expect("parsing succeeded above");
2748                    tx.set_0dt_deployment_ddl_check_interval(
2749                        with_0dt_deployment_ddl_check_interval,
2750                    )?;
2751                } else if name == ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name() {
2752                    let panic_after_timeout =
2753                        strconv::parse_bool(&parsed_value).expect("parsing succeeded above");
2754                    tx.set_enable_0dt_deployment_panic_after_timeout(panic_after_timeout)?;
2755                }
2756
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                    ObjectType::System,
2765                    EventDetails::SetV1(mz_audit_log::SetV1 {
2766                        name,
2767                        value: Some(value.borrow().to_vec().join(", ")),
2768                    }),
2769                )?;
2770            }
2771            Op::ResetSystemConfiguration { name } => {
2772                tx.remove_system_config(&name);
2773                // This mirrors some "system vars" into the catalog storage
2774                // "config" collection so that we can toggle the flag with
2775                // Launch Darkly, but use it in boot before Launch Darkly is
2776                // available.
2777                if name == WITH_0DT_DEPLOYMENT_MAX_WAIT.name() {
2778                    tx.reset_0dt_deployment_max_wait()?;
2779                } else if name == WITH_0DT_DEPLOYMENT_DDL_CHECK_INTERVAL.name() {
2780                    tx.reset_0dt_deployment_ddl_check_interval()?;
2781                } else if name == ENABLE_0DT_DEPLOYMENT_PANIC_AFTER_TIMEOUT.name() {
2782                    tx.reset_enable_0dt_deployment_panic_after_timeout()?;
2783                }
2784
2785                CatalogState::add_to_audit_log(
2786                    &state.system_configuration,
2787                    oracle_write_ts,
2788                    session,
2789                    tx,
2790                    audit_events,
2791                    EventType::Alter,
2792                    ObjectType::System,
2793                    EventDetails::SetV1(mz_audit_log::SetV1 { name, value: None }),
2794                )?;
2795            }
2796            Op::ResetAllSystemConfiguration => {
2797                tx.clear_system_configs();
2798                tx.reset_0dt_deployment_max_wait()?;
2799                tx.reset_0dt_deployment_ddl_check_interval()?;
2800                tx.reset_enable_0dt_deployment_panic_after_timeout()?;
2801
2802                CatalogState::add_to_audit_log(
2803                    &state.system_configuration,
2804                    oracle_write_ts,
2805                    session,
2806                    tx,
2807                    audit_events,
2808                    EventType::Alter,
2809                    ObjectType::System,
2810                    EventDetails::ResetAllV1,
2811                )?;
2812            }
2813            Op::UpdateScopedSystemParameters {
2814                scoped,
2815                prune_scope,
2816            } => {
2817                // Diff `scoped` against the durable cache and persist only the
2818                // delta: upsert changed/added entries, then remove entries the
2819                // update no longer serves. A removal for a still-live object is
2820                // bounded by `prune_scope` so this update does not delete a row
2821                // for an object it was not evaluated for (e.g. one created after
2822                // the update's snapshot, whose override rode its own create
2823                // transaction). `None` prunes unconditionally (the
2824                // disabled-feature clear path). Rows whose owning object is no
2825                // longer live are always pruned regardless of `prune_scope`,
2826                // because nothing else garbage collects them and ids are never
2827                // reused, so this lazily reclaims orphans left by dropped
2828                // objects.
2829                let live_clusters: BTreeSet<ClusterId> =
2830                    tx.get_clusters().map(|cluster| cluster.id).collect();
2831                let live_replicas: BTreeSet<ReplicaId> = tx
2832                    .get_cluster_replicas()
2833                    .map(|replica| replica.replica_id)
2834                    .collect();
2835                let prune_cluster = |id: &ClusterId| {
2836                    prune_scope
2837                        .as_ref()
2838                        .map_or(true, |s| s.clusters.contains(id))
2839                };
2840                let prune_replica = |id: &ReplicaId| {
2841                    prune_scope
2842                        .as_ref()
2843                        .map_or(true, |s| s.replicas.contains(id))
2844                };
2845
2846                // Cluster-coherent scope.
2847                let existing_cluster: BTreeMap<(ClusterId, String), String> = tx
2848                    .get_cluster_system_configurations()
2849                    .map(|c| ((c.cluster_id, c.name), c.value))
2850                    .collect();
2851                let mut desired_cluster: BTreeSet<(ClusterId, String)> = BTreeSet::new();
2852                for (cluster_id, values) in &scoped.cluster {
2853                    if !live_clusters.contains(cluster_id) {
2854                        continue;
2855                    }
2856                    for (name, value) in values {
2857                        desired_cluster.insert((*cluster_id, name.clone()));
2858                        if existing_cluster.get(&(*cluster_id, name.clone())) != Some(value) {
2859                            tx.upsert_cluster_system_config(*cluster_id, name, value.clone())?;
2860                        }
2861                    }
2862                }
2863                for (cluster_id, name) in existing_cluster.into_keys() {
2864                    if (!live_clusters.contains(&cluster_id) || prune_cluster(&cluster_id))
2865                        && !desired_cluster.contains(&(cluster_id, name.clone()))
2866                    {
2867                        tx.remove_cluster_system_config(cluster_id, &name);
2868                    }
2869                }
2870
2871                // Replica-local scope.
2872                let existing_replica: BTreeMap<(ReplicaId, String), String> = tx
2873                    .get_replica_system_configurations()
2874                    .map(|r| ((r.replica_id, r.name), r.value))
2875                    .collect();
2876                let mut desired_replica: BTreeSet<(ReplicaId, String)> = BTreeSet::new();
2877                for (replica_id, values) in &scoped.replica {
2878                    if !live_replicas.contains(replica_id) {
2879                        continue;
2880                    }
2881                    for (name, value) in values {
2882                        desired_replica.insert((*replica_id, name.clone()));
2883                        if existing_replica.get(&(*replica_id, name.clone())) != Some(value) {
2884                            tx.upsert_replica_system_config(*replica_id, name, value.clone())?;
2885                        }
2886                    }
2887                }
2888                for (replica_id, name) in existing_replica.into_keys() {
2889                    if (!live_replicas.contains(&replica_id) || prune_replica(&replica_id))
2890                        && !desired_replica.contains(&(replica_id, name.clone()))
2891                    {
2892                        tx.remove_replica_system_config(replica_id, &name);
2893                    }
2894                }
2895            }
2896            Op::InjectAuditEvents { events } => {
2897                for event in events {
2898                    let id = tx.allocate_audit_log_id()?;
2899                    let ev = VersionedEvent::new(
2900                        id,
2901                        event.event_type,
2902                        event.object_type,
2903                        event.details,
2904                        event.user,
2905                        oracle_write_ts.into(),
2906                    );
2907                    audit_events.push(ev.clone());
2908                    tx.insert_audit_log_event(ev);
2909                }
2910            }
2911        };
2912        Ok(temporary_item_updates)
2913    }
2914
2915    fn log_update(state: &CatalogState, id: &CatalogItemId) {
2916        let entry = state.get_entry(id);
2917        info!(
2918            "update {} {} ({})",
2919            entry.item_type(),
2920            state.resolve_full_name(entry.name(), entry.conn_id()),
2921            id
2922        );
2923    }
2924
2925    /// Update privileges to reflect the new owner. Based off of PostgreSQL's
2926    /// implementation:
2927    /// <https://github.com/postgres/postgres/blob/43a33ef54e503b61f269d088f2623ba3b9484ad7/src/backend/utils/adt/acl.c#L1078-L1177>
2928    fn update_privilege_owners(
2929        privileges: &mut PrivilegeMap,
2930        old_owner: RoleId,
2931        new_owner: RoleId,
2932    ) {
2933        // TODO(jkosh44) Would be nice not to clone every privilege.
2934        let mut flat_privileges: Vec<_> = privileges.all_values_owned().collect();
2935
2936        let mut new_present = false;
2937        for privilege in flat_privileges.iter_mut() {
2938            // Old owner's granted privilege are updated to be granted by the new
2939            // owner.
2940            if privilege.grantor == old_owner {
2941                privilege.grantor = new_owner;
2942            } else if privilege.grantor == new_owner {
2943                new_present = true;
2944            }
2945            // Old owner's privileges is given to the new owner.
2946            if privilege.grantee == old_owner {
2947                privilege.grantee = new_owner;
2948            } else if privilege.grantee == new_owner {
2949                new_present = true;
2950            }
2951        }
2952
2953        // If the old privilege list contained references to the new owner, we may
2954        // have created duplicate entries. Here we try and consolidate them. This
2955        // is inspired by PostgreSQL's algorithm but not identical.
2956        if new_present {
2957            // Group privileges by (grantee, grantor).
2958            let privilege_map: BTreeMap<_, Vec<_>> =
2959                flat_privileges
2960                    .into_iter()
2961                    .fold(BTreeMap::new(), |mut accum, privilege| {
2962                        accum
2963                            .entry((privilege.grantee, privilege.grantor))
2964                            .or_default()
2965                            .push(privilege);
2966                        accum
2967                    });
2968
2969            // Consolidate and update all privileges.
2970            flat_privileges = privilege_map
2971                .into_iter()
2972                .map(|((grantee, grantor), values)|
2973                    // Combine the acl_mode of all mz_aclitems with the same grantee and grantor.
2974                    values.into_iter().fold(
2975                        MzAclItem::empty(grantee, grantor),
2976                        |mut accum, mz_aclitem| {
2977                            accum.acl_mode =
2978                                accum.acl_mode.union(mz_aclitem.acl_mode);
2979                            accum
2980                        },
2981                    ))
2982                .collect();
2983        }
2984
2985        *privileges = PrivilegeMap::from_mz_acl_items(flat_privileges);
2986    }
2987}
2988
2989/// Prepare the given transaction for replacing a catalog item with a new version.
2990///
2991/// The new version gets a new `CatalogItemId`, which requires rewriting the `create_sql` of all
2992/// dependent objects to refer to that new ID (at a previous version).
2993///
2994/// Note that here is where we break the assumption that the `CatalogItemId` is a stable identifier
2995/// for catalog items. We currently think that there are no use cases that require this assumption,
2996/// but no way to know for sure.
2997fn tx_replace_item(
2998    tx: &mut Transaction<'_>,
2999    state: &CatalogState,
3000    id: CatalogItemId,
3001    new_entry: CatalogEntry,
3002) -> Result<(), AdapterError> {
3003    let new_id = new_entry.id;
3004
3005    // Rewrite dependent objects to point to the new ID.
3006    for use_id in new_entry.referenced_by() {
3007        // The dependent might be dropped in the same tx, so check.
3008        if tx.get_item(use_id).is_none() {
3009            continue;
3010        }
3011
3012        let mut dependent = state.get_entry(use_id).clone();
3013        dependent.item = dependent.item.replace_item_refs(id, new_id);
3014        tx.update_item(*use_id, dependent.into())?;
3015    }
3016
3017    // Move comments to the new ID.
3018    let old_comment_id = state.get_comment_id(ObjectId::Item(id));
3019    let new_comment_id = new_entry.comment_object_id();
3020    if let Some(comments) = state.comments.get_object_comments(old_comment_id) {
3021        tx.drop_comments(&[old_comment_id].into())?;
3022        for (sub, comment) in comments {
3023            tx.update_comment(new_comment_id, *sub, Some(comment.clone()))?;
3024        }
3025    }
3026
3027    let mz_catalog::durable::Item {
3028        id: _,
3029        oid,
3030        global_id,
3031        schema_id,
3032        name,
3033        create_sql,
3034        owner_id,
3035        privileges,
3036        extra_versions,
3037    } = new_entry.into();
3038
3039    tx.remove_item(id)?;
3040    tx.insert_item(
3041        new_id,
3042        oid,
3043        global_id,
3044        schema_id,
3045        &name,
3046        create_sql,
3047        owner_id,
3048        privileges,
3049        extra_versions,
3050    )?;
3051
3052    Ok(())
3053}
3054
3055/// Generate audit events for a replacement apply operation.
3056fn apply_replacement_audit_events(
3057    state: &CatalogState,
3058    target: &CatalogEntry,
3059    replacement: &CatalogEntry,
3060) -> Vec<(EventType, EventDetails)> {
3061    let mut events = Vec::new();
3062
3063    let target_name = state.resolve_full_name(target.name(), target.conn_id());
3064    let target_id_name = IdFullNameV1 {
3065        id: target.id().to_string(),
3066        name: Catalog::full_name_detail(&target_name),
3067    };
3068    let replacement_name = state.resolve_full_name(replacement.name(), replacement.conn_id());
3069    let replacement_id_name = IdFullNameV1 {
3070        id: replacement.id().to_string(),
3071        name: Catalog::full_name_detail(&replacement_name),
3072    };
3073
3074    if Catalog::should_audit_log_item(&replacement.item) {
3075        events.push((
3076            EventType::Drop,
3077            EventDetails::IdFullNameV1(replacement_id_name.clone()),
3078        ));
3079    }
3080
3081    if Catalog::should_audit_log_item(&target.item) {
3082        events.push((
3083            EventType::Alter,
3084            EventDetails::AlterApplyReplacementV1(mz_audit_log::AlterApplyReplacementV1 {
3085                target: target_id_name.clone(),
3086                replacement: replacement_id_name,
3087            }),
3088        ));
3089
3090        if let Some(old_cluster_id) = target.cluster_id()
3091            && let Some(new_cluster_id) = replacement.cluster_id()
3092            && old_cluster_id != new_cluster_id
3093        {
3094            // When the replacement is applied, the target takes on the ID of the replacement, so
3095            // we should use that ID for subsequent events.
3096            events.push((
3097                EventType::Alter,
3098                EventDetails::AlterSetClusterV1(mz_audit_log::AlterSetClusterV1 {
3099                    id: replacement.id().to_string(),
3100                    name: target_id_name.name,
3101                    old_cluster_id: old_cluster_id.to_string(),
3102                    new_cluster_id: new_cluster_id.to_string(),
3103                }),
3104            ));
3105        }
3106    }
3107
3108    events
3109}
3110
3111/// All of the objects that need to be removed in response to an [`Op::DropObjects`].
3112///
3113/// Note: Previously we used to omit a single `Op::DropObject` for every object
3114/// we needed to drop. But removing a batch of objects from a durable Catalog
3115/// Transaction is O(n) where `n` is the number of objects that exist in the
3116/// Catalog. This resulted in an unacceptable `O(m * n)` performance for a
3117/// `DROP ... CASCADE` statement.
3118#[derive(Debug, Default)]
3119pub(crate) struct ObjectsToDrop {
3120    pub comments: BTreeSet<CommentObjectId>,
3121    pub databases: BTreeSet<DatabaseId>,
3122    pub schemas: BTreeMap<SchemaSpecifier, ResolvedDatabaseSpecifier>,
3123    pub clusters: BTreeSet<ClusterId>,
3124    pub replicas: BTreeMap<ReplicaId, (ClusterId, ReplicaCreateDropReason)>,
3125    pub roles: BTreeSet<RoleId>,
3126    pub items: Vec<CatalogItemId>,
3127    pub network_policies: BTreeSet<NetworkPolicyId>,
3128}
3129
3130impl ObjectsToDrop {
3131    pub fn generate(
3132        drop_object_infos: impl IntoIterator<Item = DropObjectInfo>,
3133        state: &CatalogState,
3134        session: Option<&ConnMeta>,
3135    ) -> Result<Self, AdapterError> {
3136        let mut delta = ObjectsToDrop::default();
3137
3138        for drop_object_info in drop_object_infos {
3139            delta.add_item(drop_object_info, state, session)?;
3140        }
3141
3142        Ok(delta)
3143    }
3144
3145    fn add_item(
3146        &mut self,
3147        drop_object_info: DropObjectInfo,
3148        state: &CatalogState,
3149        session: Option<&ConnMeta>,
3150    ) -> Result<(), AdapterError> {
3151        self.comments
3152            .insert(state.get_comment_id(drop_object_info.to_object_id()));
3153
3154        match drop_object_info {
3155            DropObjectInfo::Database(database_id) => {
3156                let database = &state.database_by_id[&database_id];
3157                if database_id.is_system() {
3158                    return Err(AdapterError::Catalog(Error::new(
3159                        ErrorKind::ReadOnlyDatabase(database.name().to_string()),
3160                    )));
3161                }
3162
3163                self.databases.insert(database_id);
3164            }
3165            DropObjectInfo::Schema((database_spec, schema_spec)) => {
3166                let schema = state.get_schema(
3167                    &database_spec,
3168                    &schema_spec,
3169                    session
3170                        .map(|session| session.conn_id())
3171                        .unwrap_or(&SYSTEM_CONN_ID),
3172                );
3173                let schema_id: SchemaId = schema_spec.into();
3174                if schema_id.is_system() {
3175                    let name = schema.name();
3176                    let full_name = state.resolve_full_schema_name(name);
3177                    return Err(AdapterError::Catalog(Error::new(
3178                        ErrorKind::ReadOnlySystemSchema(full_name.to_string()),
3179                    )));
3180                }
3181
3182                self.schemas.insert(schema_spec, database_spec);
3183            }
3184            DropObjectInfo::Role(role_id) => {
3185                let name = state.get_role(&role_id).name().to_string();
3186                if role_id.is_system() || role_id.is_predefined() {
3187                    return Err(AdapterError::Catalog(Error::new(
3188                        ErrorKind::ReservedRoleName(name.clone()),
3189                    )));
3190                }
3191                state.ensure_not_reserved_role(&role_id)?;
3192
3193                self.roles.insert(role_id);
3194            }
3195            DropObjectInfo::Cluster(cluster_id) => {
3196                let cluster = state.get_cluster(cluster_id);
3197                let name = &cluster.name;
3198                if cluster_id.is_system() {
3199                    return Err(AdapterError::Catalog(Error::new(
3200                        ErrorKind::ReadOnlyCluster(name.clone()),
3201                    )));
3202                }
3203
3204                self.clusters.insert(cluster_id);
3205            }
3206            DropObjectInfo::ClusterReplica((cluster_id, replica_id, reason)) => {
3207                let cluster = state.get_cluster(cluster_id);
3208                let replica = cluster.replica(replica_id).expect("Must exist");
3209
3210                self.replicas
3211                    .insert(replica.replica_id, (cluster.id, reason));
3212
3213                // Implicitly drop materialized views that target this replica.
3214                // When the target replica is gone, no replica advances the
3215                // persist shard's upper frontier, causing reads to hang. Cascade
3216                // to anything depending on the implicitly-dropped MV so we don't
3217                // leave dangling references in the catalog.
3218                //
3219                // Plan-driven drops already include these dependents as their
3220                // own `DropObjectInfo::Item` entries (the plan stage expands
3221                // them via `cluster_replica_dependents`), so each one's comment
3222                // is recorded by the top-of-function `self.comments` insert.
3223                // This branch handles internal callers that build
3224                // `Op::DropObjects` directly with only the replica, so we expand
3225                // the dependents and record their comments ourselves.
3226                //
3227                // `seen` is seeded from the items already collected so that the
3228                // plan-driven path (where the dependents are processed before
3229                // the replica, in reverse-dependency order) does not re-add them
3230                // here.
3231                let mut seen: BTreeSet<ObjectId> =
3232                    self.items.iter().copied().map(ObjectId::Item).collect();
3233                for dep in state.cluster_replica_dependents(cluster_id, replica_id, &mut seen) {
3234                    if let ObjectId::Item(dep_id) = dep {
3235                        info!(
3236                            "implicitly dropping {} because target replica was dropped",
3237                            state.get_entry(&dep_id).name().item,
3238                        );
3239                        self.comments
3240                            .insert(state.get_comment_id(ObjectId::Item(dep_id)));
3241                        self.items.push(dep_id);
3242                    }
3243                }
3244            }
3245            DropObjectInfo::Item(item_id) => {
3246                let entry = state.get_entry(&item_id);
3247                if item_id.is_system() {
3248                    let name = entry.name();
3249                    let full_name =
3250                        state.resolve_full_name(name, session.map(|session| session.conn_id()));
3251                    return Err(AdapterError::Catalog(Error::new(ErrorKind::ReadOnlyItem(
3252                        full_name.to_string(),
3253                    ))));
3254                }
3255
3256                self.items.push(item_id);
3257            }
3258            DropObjectInfo::NetworkPolicy(network_policy_id) => {
3259                let policy = state.get_network_policy(&network_policy_id);
3260                let name = &policy.name;
3261                if network_policy_id.is_system() {
3262                    return Err(AdapterError::Catalog(Error::new(
3263                        ErrorKind::ReadOnlyNetworkPolicy(name.clone()),
3264                    )));
3265                }
3266
3267                self.network_policies.insert(network_policy_id);
3268            }
3269        }
3270
3271        Ok(())
3272    }
3273}
3274
3275#[cfg(test)]
3276mod tests {
3277    use mz_catalog::SYSTEM_CONN_ID;
3278    use mz_catalog::memory::objects::{CatalogItem, Table, TableDataSource};
3279    use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap};
3280    use mz_repr::role_id::RoleId;
3281    use mz_repr::{RelationDesc, RelationVersion, VersionedRelationDesc};
3282    use mz_sql::DEFAULT_SCHEMA;
3283    use mz_sql::catalog::CatalogDatabase;
3284    use mz_sql::names::{
3285        ItemQualifiers, QualifiedItemName, ResolvedDatabaseSpecifier, ResolvedIds,
3286    };
3287    use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
3288
3289    use crate::catalog::{Catalog, Op};
3290    use crate::session::DEFAULT_DATABASE_NAME;
3291
3292    #[mz_ore::test]
3293    fn test_replica_create_drop_reason_into_audit_log() {
3294        use mz_audit_log::CreateOrDropClusterReplicaReasonV1;
3295
3296        use crate::catalog::ReplicaCreateDropReason;
3297
3298        // `Retired` is the uniform word for every controller drop, with no
3299        // `scheduling_policies` blob: a drop happens exactly when no strategy
3300        // desires the replica, so there is no decision to record.
3301        let (reason, scheduling_policies) = ReplicaCreateDropReason::Retired.into_audit_log();
3302        assert_eq!(reason, CreateOrDropClusterReplicaReasonV1::Retired);
3303        assert!(scheduling_policies.is_none());
3304    }
3305
3306    #[mz_ore::test]
3307    fn test_update_privilege_owners() {
3308        let old_owner = RoleId::User(1);
3309        let new_owner = RoleId::User(2);
3310        let other_role = RoleId::User(3);
3311
3312        // older owner exists as grantor.
3313        let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
3314            MzAclItem {
3315                grantee: other_role,
3316                grantor: old_owner,
3317                acl_mode: AclMode::UPDATE,
3318            },
3319            MzAclItem {
3320                grantee: other_role,
3321                grantor: new_owner,
3322                acl_mode: AclMode::SELECT,
3323            },
3324        ]);
3325        Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
3326        assert_eq!(1, privileges.all_values().count());
3327        assert_eq!(
3328            vec![MzAclItem {
3329                grantee: other_role,
3330                grantor: new_owner,
3331                acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
3332            }],
3333            privileges.all_values_owned().collect::<Vec<_>>()
3334        );
3335
3336        // older owner exists as grantee.
3337        let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
3338            MzAclItem {
3339                grantee: old_owner,
3340                grantor: other_role,
3341                acl_mode: AclMode::UPDATE,
3342            },
3343            MzAclItem {
3344                grantee: new_owner,
3345                grantor: other_role,
3346                acl_mode: AclMode::SELECT,
3347            },
3348        ]);
3349        Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
3350        assert_eq!(1, privileges.all_values().count());
3351        assert_eq!(
3352            vec![MzAclItem {
3353                grantee: new_owner,
3354                grantor: other_role,
3355                acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
3356            }],
3357            privileges.all_values_owned().collect::<Vec<_>>()
3358        );
3359
3360        // older owner exists as grantee and grantor.
3361        let mut privileges = PrivilegeMap::from_mz_acl_items(vec![
3362            MzAclItem {
3363                grantee: old_owner,
3364                grantor: old_owner,
3365                acl_mode: AclMode::UPDATE,
3366            },
3367            MzAclItem {
3368                grantee: new_owner,
3369                grantor: new_owner,
3370                acl_mode: AclMode::SELECT,
3371            },
3372        ]);
3373        Catalog::update_privilege_owners(&mut privileges, old_owner, new_owner);
3374        assert_eq!(1, privileges.all_values().count());
3375        assert_eq!(
3376            vec![MzAclItem {
3377                grantee: new_owner,
3378                grantor: new_owner,
3379                acl_mode: AclMode::SELECT.union(AclMode::UPDATE)
3380            }],
3381            privileges.all_values_owned().collect::<Vec<_>>()
3382        );
3383    }
3384
3385    /// Verifies that `transact_incremental_dry_run` processes only new ops
3386    /// against the accumulated state, not all ops from scratch. Two paths are
3387    /// compared:
3388    ///   - Incremental: two separate calls, each with one op
3389    ///   - All-at-once: one call with both ops
3390    /// Both must produce equivalent catalog state.
3391    #[mz_ore::test(tokio::test)]
3392    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
3393    async fn test_transact_incremental_dry_run_processes_only_new_ops() {
3394        Catalog::with_debug(|catalog| async move {
3395            // Resolve default database and schema.
3396            let database = catalog
3397                .resolve_database(DEFAULT_DATABASE_NAME)
3398                .expect("default database");
3399            let database_name = database.name.clone();
3400            let database_spec = ResolvedDatabaseSpecifier::Id(database.id());
3401            let schema = catalog
3402                .resolve_schema_in_database(&database_spec, DEFAULT_SCHEMA, &SYSTEM_CONN_ID)
3403                .expect("default schema");
3404            let schema_name = schema.name.schema.clone();
3405            let schema_spec = schema.id.clone();
3406
3407            // Allocate IDs for two tables.
3408            let (id_t1, global_id_t1) = catalog
3409                .allocate_user_id_for_test()
3410                .await
3411                .expect("allocate id for t1");
3412            let (id_t2, global_id_t2) = catalog
3413                .allocate_user_id_for_test()
3414                .await
3415                .expect("allocate id for t2");
3416
3417            let oracle_write_ts = catalog.current_upper().await;
3418
3419            // Build two CreateItem ops.
3420            let make_table_op = |id, global_id, name: &str| Op::CreateItem {
3421                id,
3422                name: QualifiedItemName {
3423                    qualifiers: ItemQualifiers {
3424                        database_spec: database_spec.clone(),
3425                        schema_spec: schema_spec.clone(),
3426                    },
3427                    item: name.to_string(),
3428                },
3429                item: CatalogItem::Table(Table {
3430                    create_sql: Some(format!(
3431                        "CREATE TABLE {database_name}.{schema_name}.{name} ()"
3432                    )),
3433                    desc: VersionedRelationDesc::new(RelationDesc::empty()),
3434                    collections: [(RelationVersion::root(), global_id)].into_iter().collect(),
3435                    conn_id: None,
3436                    resolved_ids: ResolvedIds::empty(),
3437                    custom_logical_compaction_window: None,
3438                    is_retained_metrics_object: false,
3439                    data_source: TableDataSource::TableWrites { defaults: vec![] },
3440                }),
3441                owner_id: MZ_SYSTEM_ROLE_ID,
3442            };
3443
3444            let op_t1 = make_table_op(id_t1, global_id_t1, "t1");
3445            let op_t2 = make_table_op(id_t2, global_id_t2, "t2");
3446
3447            let base_state = catalog.state().clone();
3448
3449            // --- Path A: Incremental (two separate dry-run calls) ---
3450
3451            // First call: only op_t1, no previous snapshot.
3452            let (state_after_t1, snapshot_after_t1) = catalog
3453                .transact_incremental_dry_run(
3454                    &base_state,
3455                    vec![op_t1.clone()],
3456                    None,
3457                    None,
3458                    oracle_write_ts,
3459                )
3460                .await
3461                .expect("first dry run");
3462
3463            // After first dry run: t1 exists, t2 does not.
3464            assert!(
3465                state_after_t1.try_get_entry(&id_t1).is_some(),
3466                "t1 should exist after first dry run"
3467            );
3468            assert_eq!(
3469                state_after_t1
3470                    .try_get_entry(&id_t1)
3471                    .expect("t1 entry")
3472                    .name()
3473                    .item,
3474                "t1"
3475            );
3476            assert!(
3477                state_after_t1.try_get_entry(&id_t2).is_none(),
3478                "t2 should NOT exist after first dry run"
3479            );
3480
3481            // Second call: only op_t2, using state/snapshot from first call.
3482            let (state_incremental, _) = catalog
3483                .transact_incremental_dry_run(
3484                    &state_after_t1,
3485                    vec![op_t2.clone()],
3486                    None,
3487                    Some(snapshot_after_t1),
3488                    oracle_write_ts,
3489                )
3490                .await
3491                .expect("second dry run");
3492
3493            // After second dry run: both t1 and t2 exist.
3494            assert!(
3495                state_incremental.try_get_entry(&id_t1).is_some(),
3496                "t1 should exist in incremental result"
3497            );
3498            assert!(
3499                state_incremental.try_get_entry(&id_t2).is_some(),
3500                "t2 should exist in incremental result"
3501            );
3502
3503            // --- Path B: All-at-once (single dry-run call with both ops) ---
3504
3505            let (state_all_at_once, _) = catalog
3506                .transact_incremental_dry_run(
3507                    &base_state,
3508                    vec![op_t1.clone(), op_t2.clone()],
3509                    None,
3510                    None,
3511                    oracle_write_ts,
3512                )
3513                .await
3514                .expect("all-at-once dry run");
3515
3516            assert!(
3517                state_all_at_once.try_get_entry(&id_t1).is_some(),
3518                "t1 should exist in all-at-once result"
3519            );
3520            assert!(
3521                state_all_at_once.try_get_entry(&id_t2).is_some(),
3522                "t2 should exist in all-at-once result"
3523            );
3524
3525            // --- Compare: both paths produce equivalent items ---
3526
3527            let inc_t1 = state_incremental.try_get_entry(&id_t1).expect("inc t1");
3528            let all_t1 = state_all_at_once.try_get_entry(&id_t1).expect("all t1");
3529            assert_eq!(inc_t1.name(), all_t1.name());
3530            assert_eq!(inc_t1.owner_id, all_t1.owner_id);
3531
3532            let inc_t2 = state_incremental.try_get_entry(&id_t2).expect("inc t2");
3533            let all_t2 = state_all_at_once.try_get_entry(&id_t2).expect("all t2");
3534            assert_eq!(inc_t2.name(), all_t2.name());
3535            assert_eq!(inc_t2.owner_id, all_t2.owner_id);
3536
3537            catalog.expire().await;
3538        })
3539        .await
3540    }
3541
3542    /// Exercises the diff-and-prune semantics of the
3543    /// `Op::UpdateScopedSystemParameters` apply handler over the cluster scope.
3544    /// Covers four behaviors: upsert of a desired row, the bounded-prune
3545    /// protection that spares a live cluster outside `prune_scope`, removal of a
3546    /// stale in-scope row, and the orphan-prune that reclaims a row whose owning
3547    /// cluster was dropped even though that id is absent from `prune_scope`.
3548    #[mz_ore::test(tokio::test)]
3549    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
3550    async fn test_transact_update_scoped_system_parameters_prune() {
3551        use std::collections::{BTreeMap, BTreeSet};
3552
3553        use mz_catalog::memory::objects::{ClusterConfig, ClusterVariant};
3554        use mz_controller_types::ClusterId;
3555
3556        use crate::catalog::DropObjectInfo;
3557        use crate::config::{ScopedParameters, ScopedParametersScope};
3558
3559        Catalog::with_debug(|mut catalog| async move {
3560            let param = "max_query_result_size".to_string();
3561            let value = "1MB".to_string();
3562
3563            // Allocate and create two live clusters, A and B, the same way
3564            // production code does.
3565            let commit_ts = catalog.current_upper().await;
3566            let cluster_a = catalog
3567                .allocate_user_cluster_id(commit_ts)
3568                .await
3569                .expect("allocate cluster A");
3570            let commit_ts = catalog.current_upper().await;
3571            let cluster_b = catalog
3572                .allocate_user_cluster_id(commit_ts)
3573                .await
3574                .expect("allocate cluster B");
3575
3576            let make_cluster_op = |id: ClusterId, name: &str| Op::CreateCluster {
3577                id,
3578                name: name.to_string(),
3579                introspection_sources: Vec::new(),
3580                owner_id: MZ_SYSTEM_ROLE_ID,
3581                config: ClusterConfig {
3582                    variant: ClusterVariant::Unmanaged,
3583                    workload_class: None,
3584                },
3585            };
3586
3587            let oracle_write_ts = catalog.current_upper().await;
3588            catalog
3589                .transact(
3590                    None,
3591                    oracle_write_ts,
3592                    None,
3593                    vec![
3594                        make_cluster_op(cluster_a, "test_cluster_a"),
3595                        make_cluster_op(cluster_b, "test_cluster_b"),
3596                    ],
3597                )
3598                .await
3599                .expect("create clusters");
3600
3601            // Reads the durable cluster system configuration rows.
3602            async fn rows(catalog: &Catalog) -> BTreeSet<(ClusterId, String, String)> {
3603                let mut storage = catalog.storage().await;
3604                let tx = storage.transaction().await.expect("open transaction");
3605                tx.get_cluster_system_configurations()
3606                    .map(|c| (c.cluster_id, c.name, c.value))
3607                    .collect()
3608            }
3609
3610            let scope_with = |ids: &[ClusterId]| ScopedParametersScope {
3611                clusters: ids.iter().copied().collect(),
3612                replicas: BTreeSet::new(),
3613            };
3614            let cluster_scoped = |id: ClusterId| {
3615                let mut cluster = BTreeMap::new();
3616                let mut overrides = BTreeMap::new();
3617                overrides.insert(param.clone(), value.clone());
3618                cluster.insert(id, overrides);
3619                ScopedParameters {
3620                    cluster,
3621                    replica: BTreeMap::new(),
3622                }
3623            };
3624            let empty_scoped = || ScopedParameters {
3625                cluster: BTreeMap::new(),
3626                replica: BTreeMap::new(),
3627            };
3628
3629            // Behavior 1: upsert. A is live and in scope, so the row is created.
3630            let oracle_write_ts = catalog.current_upper().await;
3631            catalog
3632                .transact(
3633                    None,
3634                    oracle_write_ts,
3635                    None,
3636                    vec![Op::UpdateScopedSystemParameters {
3637                        scoped: cluster_scoped(cluster_a),
3638                        prune_scope: Some(scope_with(&[cluster_a])),
3639                    }],
3640                )
3641                .await
3642                .expect("upsert A");
3643            assert!(
3644                rows(&catalog)
3645                    .await
3646                    .contains(&(cluster_a, param.clone(), value.clone())),
3647                "upsert must create the row for A"
3648            );
3649
3650            // Set up an additional row for B so the prune cases have something to
3651            // act on.
3652            let oracle_write_ts = catalog.current_upper().await;
3653            catalog
3654                .transact(
3655                    None,
3656                    oracle_write_ts,
3657                    None,
3658                    vec![Op::UpdateScopedSystemParameters {
3659                        scoped: cluster_scoped(cluster_b),
3660                        prune_scope: Some(scope_with(&[cluster_b])),
3661                    }],
3662                )
3663                .await
3664                .expect("upsert B");
3665            assert!(
3666                rows(&catalog)
3667                    .await
3668                    .contains(&(cluster_b, param.clone(), value.clone())),
3669                "setup must create the row for B"
3670            );
3671
3672            // Behavior 2: bounded prune. Running with empty `scoped` and a scope
3673            // that excludes the live cluster B must leave B's row intact, because
3674            // the update was not authoritative for B.
3675            let oracle_write_ts = catalog.current_upper().await;
3676            catalog
3677                .transact(
3678                    None,
3679                    oracle_write_ts,
3680                    None,
3681                    vec![Op::UpdateScopedSystemParameters {
3682                        scoped: empty_scoped(),
3683                        prune_scope: Some(scope_with(&[cluster_a])),
3684                    }],
3685                )
3686                .await
3687                .expect("bounded prune");
3688            assert!(
3689                rows(&catalog)
3690                    .await
3691                    .contains(&(cluster_b, param.clone(), value.clone())),
3692                "a live cluster outside prune_scope must keep its row"
3693            );
3694
3695            // Behavior 3: stale in-scope prune. A is live and in scope but not
3696            // desired, so its row is removed.
3697            let oracle_write_ts = catalog.current_upper().await;
3698            catalog
3699                .transact(
3700                    None,
3701                    oracle_write_ts,
3702                    None,
3703                    vec![Op::UpdateScopedSystemParameters {
3704                        scoped: empty_scoped(),
3705                        prune_scope: Some(scope_with(&[cluster_a])),
3706                    }],
3707                )
3708                .await
3709                .expect("stale in-scope prune");
3710            assert!(
3711                !rows(&catalog)
3712                    .await
3713                    .iter()
3714                    .any(|(id, _, _)| *id == cluster_a),
3715                "a live in-scope undesired row must be removed"
3716            );
3717
3718            // Behavior 4: orphan prune. Re-create A's row, drop cluster A, then run
3719            // the update with a scope that does NOT contain A. The sync loop only
3720            // ever places live ids in prune_scope, so the orphan must still be
3721            // reclaimed because its owning cluster is no longer live.
3722            let oracle_write_ts = catalog.current_upper().await;
3723            catalog
3724                .transact(
3725                    None,
3726                    oracle_write_ts,
3727                    None,
3728                    vec![Op::UpdateScopedSystemParameters {
3729                        scoped: cluster_scoped(cluster_a),
3730                        prune_scope: Some(scope_with(&[cluster_a])),
3731                    }],
3732                )
3733                .await
3734                .expect("re-create A row");
3735            assert!(
3736                rows(&catalog)
3737                    .await
3738                    .contains(&(cluster_a, param.clone(), value.clone())),
3739                "re-created row for A must exist"
3740            );
3741
3742            let oracle_write_ts = catalog.current_upper().await;
3743            catalog
3744                .transact(
3745                    None,
3746                    oracle_write_ts,
3747                    None,
3748                    vec![Op::DropObjects(vec![DropObjectInfo::Cluster(cluster_a)])],
3749                )
3750                .await
3751                .expect("drop cluster A");
3752
3753            // B is the only live cluster the sync loop would have evaluated, so
3754            // only B appears in prune_scope. A's id is deliberately absent.
3755            let oracle_write_ts = catalog.current_upper().await;
3756            catalog
3757                .transact(
3758                    None,
3759                    oracle_write_ts,
3760                    None,
3761                    vec![Op::UpdateScopedSystemParameters {
3762                        scoped: cluster_scoped(cluster_b),
3763                        prune_scope: Some(scope_with(&[cluster_b])),
3764                    }],
3765                )
3766                .await
3767                .expect("orphan prune");
3768            assert!(
3769                !rows(&catalog)
3770                    .await
3771                    .iter()
3772                    .any(|(id, _, _)| *id == cluster_a),
3773                "orphan row for a dropped cluster must be removed even when absent from prune_scope"
3774            );
3775
3776            catalog.expire().await;
3777        })
3778        .await
3779    }
3780}