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