Skip to main content

mz_adapter/
catalog.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// TODO(jkosh44) Move to mz_catalog crate.
11
12//! Persistent metadata storage for the coordinator.
13
14use std::borrow::Cow;
15use std::collections::{BTreeMap, BTreeSet};
16use std::convert;
17use std::sync::Arc;
18use std::sync::atomic::AtomicU64;
19
20use futures::future::BoxFuture;
21use futures::{Future, FutureExt};
22use itertools::Itertools;
23use mz_adapter_types::bootstrap_builtin_cluster_config::{
24    ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
25    CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
26    SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR, SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
27};
28use mz_adapter_types::connection::ConnectionId;
29use mz_audit_log::{EventType, FullNameV1, ObjectType, VersionedStorageUsage};
30use mz_build_info::{BuildInfo, DUMMY_BUILD_INFO};
31use mz_catalog::builtin::{
32    BUILTIN_PREFIXES, BuiltinCluster, BuiltinLog, BuiltinSource, BuiltinTable,
33    MZ_CATALOG_SERVER_CLUSTER,
34};
35use mz_catalog::config::{BuiltinItemMigrationConfig, ClusterReplicaSizeMap, Config, StateConfig};
36#[cfg(test)]
37use mz_catalog::durable::CatalogError;
38use mz_catalog::durable::{
39    BootstrapArgs, DurableCatalogState, STORAGE_USAGE_ID_ALLOC_KEY, TestCatalogStateBuilder,
40    test_bootstrap_args,
41};
42use mz_catalog::expr_cache::{ExpressionCacheHandle, GlobalExpressions, LocalExpressions};
43use mz_catalog::memory::error::{Error, ErrorKind};
44use mz_catalog::memory::objects::{
45    CatalogCollectionEntry, CatalogEntry, CatalogItem, Cluster, ClusterReplica, Database,
46    NetworkPolicy, Role, RoleAuth, Schema,
47};
48use mz_compute_types::dataflows::DataflowDescription;
49use mz_controller::clusters::ReplicaLocation;
50use mz_controller_types::{ClusterId, ReplicaId};
51use mz_expr::OptimizedMirRelationExpr;
52use mz_license_keys::ValidatedLicenseKey;
53use mz_ore::metrics::MetricsRegistry;
54use mz_ore::now::{EpochMillis, NowFn, SYSTEM_TIME};
55use mz_ore::result::ResultExt as _;
56use mz_persist_client::PersistClient;
57use mz_repr::adt::mz_acl_item::{AclMode, PrivilegeMap};
58use mz_repr::explain::ExprHumanizer;
59use mz_repr::namespaces::MZ_TEMP_SCHEMA;
60use mz_repr::network_policy_id::NetworkPolicyId;
61use mz_repr::optimize::OptimizerFeatures;
62use mz_repr::role_id::RoleId;
63use mz_repr::{CatalogItemId, Diff, GlobalId, RelationVersionSelector, SqlScalarType};
64use mz_secrets::InMemorySecretsController;
65use mz_sql::catalog::{
66    CatalogCluster, CatalogClusterReplica, CatalogDatabase, CatalogError as SqlCatalogError,
67    CatalogItem as SqlCatalogItem, CatalogItemType as SqlCatalogItemType, CatalogNetworkPolicy,
68    CatalogRole, CatalogSchema, DefaultPrivilegeAclItem, DefaultPrivilegeObject, EnvironmentId,
69    SessionCatalog, SystemObjectType,
70};
71use mz_sql::names::{
72    CommentObjectId, DatabaseId, FullItemName, FullSchemaName, ItemQualifiers, ObjectId,
73    PUBLIC_ROLE_NAME, PartialItemName, QualifiedItemName, QualifiedSchemaName,
74    ResolvedDatabaseSpecifier, ResolvedIds, SchemaId, SchemaSpecifier, SystemObjectId,
75};
76use mz_sql::plan::{Plan, PlanNotice, StatementDesc};
77use mz_sql::rbac;
78use mz_sql::session::metadata::SessionMetadata;
79use mz_sql::session::user::{MZ_SYSTEM_ROLE_ID, SUPPORT_USER, SYSTEM_USER};
80use mz_sql::session::vars::SystemVars;
81use mz_sql_parser::ast::QualifiedReplica;
82use mz_storage_types::connections::ConnectionContext;
83use mz_storage_types::connections::inline::{ConnectionResolver, InlinedConnection};
84use mz_transform::dataflow::DataflowMetainfo;
85use mz_transform::notice::OptimizerNotice;
86use tokio::sync::MutexGuard;
87use tokio::sync::mpsc::UnboundedSender;
88use uuid::Uuid;
89
90// DO NOT add any more imports from `crate` outside of `crate::catalog`.
91pub use crate::catalog::builtin_table_updates::BuiltinTableUpdate;
92pub use crate::catalog::open::{InitializeStateResult, OpenCatalogResult};
93pub use crate::catalog::state::CatalogState;
94pub use crate::catalog::transact::{
95    DropObjectInfo, InjectedAuditEvent, Op, ReplicaCreateDropReason, TransactionResult,
96};
97use crate::command::CatalogDump;
98use crate::coord::TargetCluster;
99#[cfg(test)]
100use crate::coord::catalog_implications::parsed_state_updates::ParsedStateUpdate;
101use crate::session::{Portal, PreparedStatement, Session};
102use crate::util::ResultExt;
103use crate::{AdapterError, AdapterNotice, ExecuteResponse};
104
105mod builtin_table_updates;
106pub(crate) mod consistency;
107mod migrate;
108
109mod apply;
110pub(crate) mod cluster_state;
111mod open;
112mod state;
113mod timeline;
114mod transact;
115
116/// A `Catalog` keeps track of the SQL objects known to the planner.
117///
118/// For each object, it keeps track of both forward and reverse dependencies:
119/// i.e., which objects are depended upon by the object, and which objects
120/// depend upon the object. It enforces the SQL rules around dropping: an object
121/// cannot be dropped until all of the objects that depend upon it are dropped.
122/// It also enforces uniqueness of names.
123///
124/// SQL mandates a hierarchy of exactly three layers. A catalog contains
125/// databases, databases contain schemas, and schemas contain catalog items,
126/// like sources, sinks, view, and indexes.
127///
128/// To the outside world, databases, schemas, and items are all identified by
129/// name. Items can be referred to by their [`FullItemName`], which fully and
130/// unambiguously specifies the item, or a [`PartialItemName`], which can omit the
131/// database name and/or the schema name. Partial names can be converted into
132/// full names via a complicated resolution process documented by the
133/// [`CatalogState::resolve`] method.
134///
135/// The catalog also maintains special "ambient schemas": virtual schemas,
136/// implicitly present in all databases, that house various system views.
137/// The big examples of ambient schemas are `pg_catalog` and `mz_catalog`.
138#[derive(Debug)]
139pub struct Catalog {
140    state: CatalogState,
141    expr_cache_handle: Option<ExpressionCacheHandle>,
142    storage: Arc<tokio::sync::Mutex<Box<dyn mz_catalog::durable::DurableCatalogState>>>,
143    transient_revision: u64,
144    /// The latest `transient_revision`, shared by all clones of this catalog.
145    /// While `transient_revision` is this clone's own revision, frozen when
146    /// the snapshot was taken, this field always tracks the latest revision
147    /// across all clones. Comparing the two lets a snapshot holder detect
148    /// from off-thread whether its snapshot is still current, via
149    /// [`Catalog::transient_revision_is_current`], without a Coordinator
150    /// round-trip (see `PeekClient::catalog_snapshot`).
151    ///
152    /// The store happens in `transact`, before the transaction's effects can
153    /// be observed anywhere (responses, notices, builtin table writes), so a
154    /// session that has observed any evidence of a catalog change is
155    /// guaranteed to see the corresponding bump.
156    shared_transient_revision: Arc<AtomicU64>,
157}
158
159/// A handle for advancing the durable catalog upper off the coordinator loop.
160#[derive(Debug, Clone)]
161pub struct CatalogUpperHandle {
162    storage: Arc<tokio::sync::Mutex<Box<dyn mz_catalog::durable::DurableCatalogState>>>,
163}
164
165impl CatalogUpperHandle {
166    /// Advances the durable catalog upper to at least `new_upper`.
167    pub async fn advance_upper(
168        &self,
169        new_upper: mz_repr::Timestamp,
170    ) -> Result<(), mz_catalog::durable::CatalogError> {
171        self.storage.lock().await.advance_upper(new_upper).await
172    }
173}
174
175// Implement our own Clone because derive can't unless S is Clone, which it's
176// not (hence the Arc).
177impl Clone for Catalog {
178    fn clone(&self) -> Self {
179        Self {
180            state: self.state.clone(),
181            expr_cache_handle: self.expr_cache_handle.clone(),
182            storage: Arc::clone(&self.storage),
183            transient_revision: self.transient_revision,
184            shared_transient_revision: Arc::clone(&self.shared_transient_revision),
185        }
186    }
187}
188
189impl Catalog {
190    /// Set the optimized plan for the item identified by `id`.
191    ///
192    /// # Panics
193    /// If the item is not an `Index`, `MaterializedView`, or
194    /// `ContinualTask`.
195    #[mz_ore::instrument(level = "trace")]
196    pub fn set_optimized_plan(
197        &mut self,
198        id: GlobalId,
199        plan: DataflowDescription<OptimizedMirRelationExpr>,
200    ) {
201        self.state.set_optimized_plan(id, plan);
202    }
203
204    /// Set the physical plan for the item identified by `id`.
205    ///
206    /// # Panics
207    /// If the item is not an `Index`, `MaterializedView`, or
208    /// `ContinualTask`.
209    #[mz_ore::instrument(level = "trace")]
210    pub fn set_physical_plan(
211        &mut self,
212        id: GlobalId,
213        plan: DataflowDescription<mz_compute_types::plan::LirRelationExpr>,
214    ) {
215        self.state.set_physical_plan(id, plan);
216    }
217
218    /// Try to get the optimized plan for the item identified by `id`.
219    #[mz_ore::instrument(level = "trace")]
220    pub fn try_get_optimized_plan(
221        &self,
222        id: &GlobalId,
223    ) -> Option<&DataflowDescription<OptimizedMirRelationExpr>> {
224        let entry = self.state.try_get_entry_by_global_id(id)?;
225        entry.item().optimized_plan().map(AsRef::as_ref)
226    }
227
228    /// Try to get the physical plan for the item identified by `id`.
229    #[mz_ore::instrument(level = "trace")]
230    pub fn try_get_physical_plan(
231        &self,
232        id: &GlobalId,
233    ) -> Option<&DataflowDescription<mz_compute_types::plan::LirRelationExpr>> {
234        let entry = self.state.try_get_entry_by_global_id(id)?;
235        entry.item().physical_plan().map(AsRef::as_ref)
236    }
237
238    /// Set the `DataflowMetainfo` for the item identified by `id`.
239    ///
240    /// # Panics
241    /// If the item is not an `Index`, `MaterializedView`, or
242    /// `ContinualTask`.
243    #[mz_ore::instrument(level = "trace")]
244    pub fn set_dataflow_metainfo(
245        &mut self,
246        id: GlobalId,
247        metainfo: DataflowMetainfo<Arc<OptimizerNotice>>,
248    ) {
249        self.state.set_dataflow_metainfo(id, metainfo);
250    }
251
252    /// Try to get the `DataflowMetainfo` for the item identified by `id`.
253    #[mz_ore::instrument(level = "trace")]
254    pub fn try_get_dataflow_metainfo(
255        &self,
256        id: &GlobalId,
257    ) -> Option<&DataflowMetainfo<Arc<OptimizerNotice>>> {
258        let entry = self.state.try_get_entry_by_global_id(id)?;
259        entry.item().dataflow_metainfo()
260    }
261}
262
263#[derive(Debug)]
264pub struct ConnCatalog<'a> {
265    state: Cow<'a, CatalogState>,
266    /// Because we don't have any way of removing items from the catalog
267    /// temporarily, we allow the ConnCatalog to pretend that a set of items
268    /// don't exist during resolution.
269    ///
270    /// This feature is necessary to allow re-planning of statements, which is
271    /// either incredibly useful or required when altering item definitions.
272    ///
273    /// Note that uses of this field should be used by short-lived
274    /// catalogs.
275    unresolvable_ids: BTreeSet<CatalogItemId>,
276    conn_id: ConnectionId,
277    cluster: String,
278    database: Option<DatabaseId>,
279    search_path: Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
280    role_id: RoleId,
281    prepared_statements: Option<&'a BTreeMap<String, PreparedStatement>>,
282    portals: Option<&'a BTreeMap<String, Portal>>,
283    notices_tx: UnboundedSender<AdapterNotice>,
284    restrict_to_user_objects: bool,
285}
286
287impl ConnCatalog<'_> {
288    pub fn conn_id(&self) -> &ConnectionId {
289        &self.conn_id
290    }
291
292    pub fn state(&self) -> &CatalogState {
293        &*self.state
294    }
295
296    /// Prevent planning from resolving item with the provided ID. Instead,
297    /// return an error as if the item did not exist.
298    ///
299    /// This feature is meant exclusively to permit re-planning statements
300    /// during update operations and should not be used otherwise given its
301    /// extremely "powerful" semantics.
302    ///
303    /// # Panics
304    /// If the catalog's role ID is not [`MZ_SYSTEM_ROLE_ID`].
305    pub fn mark_id_unresolvable_for_replanning(&mut self, id: CatalogItemId) {
306        assert_eq!(
307            self.role_id, MZ_SYSTEM_ROLE_ID,
308            "only the system role can mark IDs unresolvable",
309        );
310        self.unresolvable_ids.insert(id);
311    }
312
313    /// Returns the schemas:
314    /// - mz_catalog
315    /// - pg_catalog
316    /// - temp (if requested)
317    /// - all schemas from the session's search_path var that exist
318    pub fn effective_search_path(
319        &self,
320        include_temp_schema: bool,
321    ) -> Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)> {
322        self.state
323            .effective_search_path(&self.search_path, include_temp_schema)
324    }
325}
326
327impl ConnectionResolver for ConnCatalog<'_> {
328    fn resolve_connection(
329        &self,
330        id: CatalogItemId,
331    ) -> mz_storage_types::connections::Connection<InlinedConnection> {
332        self.state().resolve_connection(id)
333    }
334}
335
336impl Catalog {
337    /// Returns the catalog's transient revision, which starts at 1 and is
338    /// incremented on every change. This is not persisted to disk, and will
339    /// restart on every load.
340    pub fn transient_revision(&self) -> u64 {
341        self.transient_revision
342    }
343
344    /// Reports whether this catalog's transient revision is still the latest,
345    /// i.e., whether no catalog transaction has committed since this snapshot
346    /// was taken. Can be called on a snapshot from off-thread, without a
347    /// Coordinator round-trip. See the field documentation on
348    /// `shared_transient_revision`.
349    pub fn transient_revision_is_current(&self) -> bool {
350        self.transient_revision
351            == self
352                .shared_transient_revision
353                .load(std::sync::atomic::Ordering::SeqCst)
354    }
355
356    /// Creates a debug catalog from the current
357    /// `METADATA_BACKEND_URL` with parameters set appropriately for debug contexts,
358    /// like in tests.
359    ///
360    /// WARNING! This function can arbitrarily fail because it does not make any
361    /// effort to adjust the catalog's contents' structure or semantics to the
362    /// currently running version, i.e. it does not apply any migrations.
363    ///
364    /// This function must not be called in production contexts. Use
365    /// [`Catalog::open`] with appropriately set configuration parameters
366    /// instead.
367    pub async fn with_debug<F, Fut, T>(f: F) -> T
368    where
369        F: FnOnce(Catalog) -> Fut,
370        Fut: Future<Output = T>,
371    {
372        let persist_client = PersistClient::new_for_tests().await;
373        let organization_id = Uuid::new_v4();
374        let bootstrap_args = test_bootstrap_args();
375        let catalog = Self::open_debug_catalog(persist_client, organization_id, &bootstrap_args)
376            .await
377            .expect("can open debug catalog");
378        f(catalog).await
379    }
380
381    /// Like [`Catalog::with_debug`], but the catalog created believes that bootstrap is still
382    /// in progress.
383    pub async fn with_debug_in_bootstrap<F, Fut, T>(f: F) -> T
384    where
385        F: FnOnce(Catalog) -> Fut,
386        Fut: Future<Output = T>,
387    {
388        let persist_client = PersistClient::new_for_tests().await;
389        let organization_id = Uuid::new_v4();
390        let bootstrap_args = test_bootstrap_args();
391        let mut catalog =
392            Self::open_debug_catalog(persist_client.clone(), organization_id, &bootstrap_args)
393                .await
394                .expect("can open debug catalog");
395
396        // Replace `storage` in `catalog` with one that doesn't think bootstrap is over.
397        let now = SYSTEM_TIME.clone();
398        let openable_storage = TestCatalogStateBuilder::new(persist_client)
399            .with_organization_id(organization_id)
400            .with_default_deploy_generation()
401            .build()
402            .await
403            .expect("can create durable catalog");
404        let mut storage = openable_storage
405            .open(now().into(), &bootstrap_args)
406            .await
407            .expect("can open durable catalog");
408        // Drain updates.
409        let _ = storage
410            .sync_to_current_updates()
411            .await
412            .expect("can sync to current updates");
413        catalog.storage = Arc::new(tokio::sync::Mutex::new(storage));
414
415        f(catalog).await
416    }
417
418    /// Opens a debug catalog.
419    ///
420    /// See [`Catalog::with_debug`].
421    pub async fn open_debug_catalog(
422        persist_client: PersistClient,
423        organization_id: Uuid,
424        bootstrap_args: &BootstrapArgs,
425    ) -> Result<Catalog, anyhow::Error> {
426        let now = SYSTEM_TIME.clone();
427        let environment_id = None;
428        let openable_storage = TestCatalogStateBuilder::new(persist_client.clone())
429            .with_organization_id(organization_id)
430            .with_default_deploy_generation()
431            .build()
432            .await?;
433        let storage = openable_storage.open(now().into(), bootstrap_args).await?;
434        let system_parameter_defaults = BTreeMap::default();
435        Self::open_debug_catalog_inner(
436            persist_client,
437            storage,
438            now,
439            environment_id,
440            &DUMMY_BUILD_INFO,
441            system_parameter_defaults,
442            bootstrap_args,
443            None,
444        )
445        .await
446    }
447
448    /// Opens a read only debug persist backed catalog defined by `persist_client` and
449    /// `organization_id`.
450    ///
451    /// See [`Catalog::with_debug`].
452    pub async fn open_debug_read_only_catalog(
453        persist_client: PersistClient,
454        organization_id: Uuid,
455        bootstrap_args: &BootstrapArgs,
456    ) -> Result<Catalog, anyhow::Error> {
457        let now = SYSTEM_TIME.clone();
458        let environment_id = None;
459        let openable_storage = TestCatalogStateBuilder::new(persist_client.clone())
460            .with_organization_id(organization_id)
461            .build()
462            .await?;
463        let storage = openable_storage
464            .open_read_only(&test_bootstrap_args())
465            .await?;
466        let system_parameter_defaults = BTreeMap::default();
467        Self::open_debug_catalog_inner(
468            persist_client,
469            storage,
470            now,
471            environment_id,
472            &DUMMY_BUILD_INFO,
473            system_parameter_defaults,
474            bootstrap_args,
475            None,
476        )
477        .await
478    }
479
480    /// Opens a read only debug persist backed catalog defined by `persist_client` and
481    /// `organization_id`.
482    ///
483    /// See [`Catalog::with_debug`].
484    pub async fn open_debug_read_only_persist_catalog_config(
485        persist_client: PersistClient,
486        now: NowFn,
487        environment_id: EnvironmentId,
488        system_parameter_defaults: BTreeMap<String, String>,
489        build_info: &'static BuildInfo,
490        bootstrap_args: &BootstrapArgs,
491        enable_expression_cache_override: Option<bool>,
492    ) -> Result<Catalog, anyhow::Error> {
493        let openable_storage = TestCatalogStateBuilder::new(persist_client.clone())
494            .with_organization_id(environment_id.organization_id())
495            .with_version(
496                build_info
497                    .version
498                    .parse()
499                    .expect("build version is parseable"),
500            )
501            .build()
502            .await?;
503        let storage = openable_storage.open_read_only(bootstrap_args).await?;
504        Self::open_debug_catalog_inner(
505            persist_client,
506            storage,
507            now,
508            Some(environment_id),
509            build_info,
510            system_parameter_defaults,
511            bootstrap_args,
512            enable_expression_cache_override,
513        )
514        .await
515    }
516
517    async fn open_debug_catalog_inner(
518        persist_client: PersistClient,
519        storage: Box<dyn DurableCatalogState>,
520        now: NowFn,
521        environment_id: Option<EnvironmentId>,
522        build_info: &'static BuildInfo,
523        system_parameter_defaults: BTreeMap<String, String>,
524        bootstrap_args: &BootstrapArgs,
525        enable_expression_cache_override: Option<bool>,
526    ) -> Result<Catalog, anyhow::Error> {
527        let metrics_registry = &MetricsRegistry::new();
528        let secrets_reader = Arc::new(InMemorySecretsController::new());
529        // Used as a lower boundary of the boot_ts, but it's ok to use now() for
530        // debugging/testing.
531        let previous_ts = now().into();
532        let replica_size = &bootstrap_args.default_cluster_replica_size;
533        let read_only = false;
534
535        let OpenCatalogResult {
536            catalog,
537            migrated_storage_collections_0dt: _,
538            new_builtin_collections: _,
539            builtin_table_updates: _,
540            cached_global_exprs: _,
541            uncached_local_exprs: _,
542        } = Catalog::open(Config {
543            storage,
544            metrics_registry,
545            state: StateConfig {
546                unsafe_mode: true,
547                all_features: false,
548                build_info,
549                environment_id: environment_id.unwrap_or_else(EnvironmentId::for_tests),
550                read_only,
551                now,
552                boot_ts: previous_ts,
553                skip_migrations: true,
554                cluster_replica_sizes: bootstrap_args.cluster_replica_size_map.clone(),
555                builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
556                    size: replica_size.clone(),
557                    replication_factor: SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
558                },
559                builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
560                    size: replica_size.clone(),
561                    replication_factor: CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR,
562                },
563                builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
564                    size: replica_size.clone(),
565                    replication_factor: PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
566                },
567                builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
568                    size: replica_size.clone(),
569                    replication_factor: SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
570                },
571                builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
572                    size: replica_size.clone(),
573                    replication_factor: ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR,
574                },
575                system_parameter_defaults,
576                remote_system_parameters: None,
577                availability_zones: vec![],
578                egress_addresses: vec![],
579                aws_principal_context: None,
580                aws_privatelink_availability_zones: None,
581                http_host_name: None,
582                connection_context: ConnectionContext::for_tests(secrets_reader),
583                builtin_item_migration_config: BuiltinItemMigrationConfig {
584                    persist_client: persist_client.clone(),
585                    read_only,
586                    force_migration: None,
587                },
588                persist_client,
589                enable_expression_cache_override,
590                helm_chart_version: None,
591                external_login_password_mz_system: None,
592                license_key: ValidatedLicenseKey::for_tests(),
593            },
594        })
595        .await?;
596        Ok(catalog)
597    }
598
599    pub fn for_session<'a>(&'a self, session: &'a Session) -> ConnCatalog<'a> {
600        self.state.for_session(session)
601    }
602
603    pub fn for_sessionless_user(&self, role_id: RoleId) -> ConnCatalog<'_> {
604        self.state.for_sessionless_user(role_id)
605    }
606
607    pub fn for_system_session(&self) -> ConnCatalog<'_> {
608        self.state.for_system_session()
609    }
610
611    async fn storage<'a>(
612        &'a self,
613    ) -> MutexGuard<'a, Box<dyn mz_catalog::durable::DurableCatalogState>> {
614        self.storage.lock().await
615    }
616
617    pub async fn current_upper(&self) -> mz_repr::Timestamp {
618        self.storage().await.current_upper().await
619    }
620
621    /// Allocates and returns both a user [`CatalogItemId`] and [`GlobalId`], delegating to
622    /// [`DurableCatalogState::allocate_user_id`].
623    pub async fn allocate_user_id(
624        &self,
625        commit_ts: mz_repr::Timestamp,
626    ) -> Result<(CatalogItemId, GlobalId), Error> {
627        Ok(self
628            .storage()
629            .await
630            .allocate_user_id(commit_ts)
631            .await
632            .maybe_terminate("allocating user ids")?)
633    }
634
635    /// Allocate `amount` many user IDs. See [`DurableCatalogState::allocate_user_ids`].
636    pub async fn allocate_user_ids(
637        &self,
638        amount: u64,
639        commit_ts: mz_repr::Timestamp,
640    ) -> Result<Vec<(CatalogItemId, GlobalId)>, Error> {
641        Ok(self
642            .storage()
643            .await
644            .allocate_user_ids(amount, commit_ts)
645            .await
646            .maybe_terminate("allocating user ids")?)
647    }
648
649    pub async fn allocate_user_id_for_test(&self) -> Result<(CatalogItemId, GlobalId), Error> {
650        let commit_ts = self.storage().await.current_upper().await;
651        self.allocate_user_id(commit_ts).await
652    }
653
654    /// Allocates a single durable id for a storage usage collection batch.
655    ///
656    /// Bumps the durable `STORAGE_USAGE_ID_ALLOC_KEY` allocator by one and
657    /// returns the previous value. The bump is committed at `commit_ts`.
658    /// One id is shared by every row produced by a collection cycle (see
659    /// `Coordinator::storage_usage_update`), so the durable cost is one
660    /// allocator round-trip per cycle, not per shard.
661    pub async fn allocate_storage_usage_id(
662        &self,
663        commit_ts: mz_repr::Timestamp,
664    ) -> Result<u64, Error> {
665        use mz_ore::collections::CollectionExt;
666
667        self.storage()
668            .await
669            .allocate_id(STORAGE_USAGE_ID_ALLOC_KEY, 1, commit_ts)
670            .await
671            .maybe_terminate("allocating storage usage id")
672            .map(|ids| ids.into_element())
673            .err_into()
674    }
675
676    /// Get the next user item ID without allocating it.
677    pub async fn get_next_user_item_id(&self) -> Result<u64, Error> {
678        self.storage()
679            .await
680            .get_next_user_item_id()
681            .await
682            .err_into()
683    }
684
685    #[cfg(test)]
686    pub async fn allocate_system_id(
687        &self,
688        commit_ts: mz_repr::Timestamp,
689    ) -> Result<(CatalogItemId, GlobalId), Error> {
690        use mz_ore::collections::CollectionExt;
691
692        let mut storage = self.storage().await;
693        let mut txn = storage.transaction().await?;
694        let id = txn
695            .allocate_system_item_ids(1)
696            .maybe_terminate("allocating system ids")?
697            .into_element();
698        // Drain transaction.
699        let _ = txn.get_and_commit_op_updates();
700        txn.commit(commit_ts).await?;
701        Ok(id)
702    }
703
704    /// Get the next system item ID without allocating it.
705    pub async fn get_next_system_item_id(&self) -> Result<u64, Error> {
706        self.storage()
707            .await
708            .get_next_system_item_id()
709            .await
710            .err_into()
711    }
712
713    /// Allocates and returns a user [`ClusterId`], delegating to
714    /// [`DurableCatalogState::allocate_user_cluster_id`].
715    pub async fn allocate_user_cluster_id(
716        &self,
717        commit_ts: mz_repr::Timestamp,
718    ) -> Result<ClusterId, Error> {
719        Ok(self
720            .storage()
721            .await
722            .allocate_user_cluster_id(commit_ts)
723            .await
724            .maybe_terminate("allocating user cluster ids")?)
725    }
726
727    /// Allocate `amount` many user replica IDs. See
728    /// [`DurableCatalogState::allocate_user_replica_ids`].
729    pub async fn allocate_user_replica_ids(
730        &self,
731        amount: u64,
732        commit_ts: mz_repr::Timestamp,
733    ) -> Result<Vec<ReplicaId>, Error> {
734        Ok(self
735            .storage()
736            .await
737            .allocate_user_replica_ids(amount, commit_ts)
738            .await
739            .maybe_terminate("allocating user replica ids")?)
740    }
741
742    /// Allocate `amount` many system replica IDs. See
743    /// [`DurableCatalogState::allocate_system_replica_ids`].
744    pub async fn allocate_system_replica_ids(
745        &self,
746        amount: u64,
747        commit_ts: mz_repr::Timestamp,
748    ) -> Result<Vec<ReplicaId>, Error> {
749        Ok(self
750            .storage()
751            .await
752            .allocate_system_replica_ids(amount, commit_ts)
753            .await
754            .maybe_terminate("allocating system replica ids")?)
755    }
756
757    /// Allocate `amount` many replica IDs for `cluster_id`, picking user or
758    /// system IDs based on the cluster's ID type.
759    pub async fn allocate_replica_ids(
760        &self,
761        cluster_id: ClusterId,
762        amount: u64,
763        commit_ts: mz_repr::Timestamp,
764    ) -> Result<Vec<ReplicaId>, Error> {
765        if cluster_id.is_system() {
766            self.allocate_system_replica_ids(amount, commit_ts).await
767        } else {
768            self.allocate_user_replica_ids(amount, commit_ts).await
769        }
770    }
771
772    /// Get the next system replica id without allocating it.
773    pub async fn get_next_system_replica_id(&self) -> Result<u64, Error> {
774        self.storage()
775            .await
776            .get_next_system_replica_id()
777            .await
778            .err_into()
779    }
780
781    /// Get the next user replica id without allocating it.
782    pub async fn get_next_user_replica_id(&self) -> Result<u64, Error> {
783        self.storage()
784            .await
785            .get_next_user_replica_id()
786            .await
787            .err_into()
788    }
789
790    pub fn resolve_database(&self, database_name: &str) -> Result<&Database, SqlCatalogError> {
791        self.state.resolve_database(database_name)
792    }
793
794    pub fn resolve_schema(
795        &self,
796        current_database: Option<&DatabaseId>,
797        database_name: Option<&str>,
798        schema_name: &str,
799        conn_id: &ConnectionId,
800    ) -> Result<&Schema, SqlCatalogError> {
801        self.state
802            .resolve_schema(current_database, database_name, schema_name, conn_id)
803    }
804
805    pub fn resolve_schema_in_database(
806        &self,
807        database_spec: &ResolvedDatabaseSpecifier,
808        schema_name: &str,
809        conn_id: &ConnectionId,
810    ) -> Result<&Schema, SqlCatalogError> {
811        self.state
812            .resolve_schema_in_database(database_spec, schema_name, conn_id)
813    }
814
815    pub fn resolve_replica_in_cluster(
816        &self,
817        cluster_id: &ClusterId,
818        replica_name: &str,
819    ) -> Result<&ClusterReplica, SqlCatalogError> {
820        self.state
821            .resolve_replica_in_cluster(cluster_id, replica_name)
822    }
823
824    pub fn resolve_system_schema(&self, name: &'static str) -> SchemaId {
825        self.state.resolve_system_schema(name)
826    }
827
828    pub fn resolve_search_path(
829        &self,
830        session: &Session,
831    ) -> Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)> {
832        self.state.resolve_search_path(session)
833    }
834
835    /// Resolves `name` to a non-function [`CatalogEntry`].
836    pub fn resolve_entry(
837        &self,
838        current_database: Option<&DatabaseId>,
839        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
840        name: &PartialItemName,
841        conn_id: &ConnectionId,
842    ) -> Result<&CatalogEntry, SqlCatalogError> {
843        self.state
844            .resolve_entry(current_database, search_path, name, conn_id)
845    }
846
847    /// Resolves a `BuiltinTable`.
848    pub fn resolve_builtin_table(&self, builtin: &'static BuiltinTable) -> CatalogItemId {
849        self.state.resolve_builtin_table(builtin)
850    }
851
852    /// Resolves a `BuiltinLog`.
853    pub fn resolve_builtin_log(&self, builtin: &'static BuiltinLog) -> CatalogItemId {
854        self.state.resolve_builtin_log(builtin).0
855    }
856
857    /// Resolves a `BuiltinSource`.
858    pub fn resolve_builtin_storage_collection(
859        &self,
860        builtin: &'static BuiltinSource,
861    ) -> CatalogItemId {
862        self.state.resolve_builtin_source(builtin)
863    }
864
865    /// Resolves `name` to a function [`CatalogEntry`].
866    pub fn resolve_function(
867        &self,
868        current_database: Option<&DatabaseId>,
869        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
870        name: &PartialItemName,
871        conn_id: &ConnectionId,
872    ) -> Result<&CatalogEntry, SqlCatalogError> {
873        self.state
874            .resolve_function(current_database, search_path, name, conn_id)
875    }
876
877    /// Resolves `name` to a type [`CatalogEntry`].
878    pub fn resolve_type(
879        &self,
880        current_database: Option<&DatabaseId>,
881        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
882        name: &PartialItemName,
883        conn_id: &ConnectionId,
884    ) -> Result<&CatalogEntry, SqlCatalogError> {
885        self.state
886            .resolve_type(current_database, search_path, name, conn_id)
887    }
888
889    pub fn resolve_cluster(&self, name: &str) -> Result<&Cluster, SqlCatalogError> {
890        self.state.resolve_cluster(name)
891    }
892
893    /// Resolves a [`Cluster`] for a [`BuiltinCluster`].
894    ///
895    /// # Panics
896    /// * If the [`BuiltinCluster`] doesn't exist.
897    ///
898    pub fn resolve_builtin_cluster(&self, cluster: &BuiltinCluster) -> &Cluster {
899        self.state.resolve_builtin_cluster(cluster)
900    }
901
902    pub fn get_mz_catalog_server_cluster_id(&self) -> &ClusterId {
903        &self.resolve_builtin_cluster(&MZ_CATALOG_SERVER_CLUSTER).id
904    }
905
906    /// Resolves a [`Cluster`] for a TargetCluster.
907    pub fn resolve_target_cluster(
908        &self,
909        target_cluster: TargetCluster,
910        session: &Session,
911    ) -> Result<&Cluster, AdapterError> {
912        match target_cluster {
913            TargetCluster::CatalogServer => {
914                Ok(self.resolve_builtin_cluster(&MZ_CATALOG_SERVER_CLUSTER))
915            }
916            TargetCluster::Active => self.active_cluster(session),
917            TargetCluster::Transaction(cluster_id) => self
918                .try_get_cluster(cluster_id)
919                .ok_or(AdapterError::ConcurrentClusterDrop),
920        }
921    }
922
923    pub fn active_cluster(&self, session: &Session) -> Result<&Cluster, AdapterError> {
924        // TODO(benesch): this check here is not sufficiently protective. It'd
925        // be very easy for a code path to accidentally avoid this check by
926        // calling `resolve_cluster(session.vars().cluster())`.
927        if session.user().name != SYSTEM_USER.name
928            && session.user().name != SUPPORT_USER.name
929            && session.vars().cluster() == SYSTEM_USER.name
930        {
931            coord_bail!(
932                "system cluster '{}' cannot execute user queries",
933                SYSTEM_USER.name
934            );
935        }
936        let cluster = self.resolve_cluster(session.vars().cluster())?;
937        Ok(cluster)
938    }
939
940    pub fn state(&self) -> &CatalogState {
941        &self.state
942    }
943
944    pub fn resolve_full_name(
945        &self,
946        name: &QualifiedItemName,
947        conn_id: Option<&ConnectionId>,
948    ) -> FullItemName {
949        self.state.resolve_full_name(name, conn_id)
950    }
951
952    pub fn try_get_entry(&self, id: &CatalogItemId) -> Option<&CatalogEntry> {
953        self.state.try_get_entry(id)
954    }
955
956    pub fn try_get_entry_by_global_id(&self, id: &GlobalId) -> Option<&CatalogEntry> {
957        self.state.try_get_entry_by_global_id(id)
958    }
959
960    pub fn get_entry(&self, id: &CatalogItemId) -> &CatalogEntry {
961        self.state.get_entry(id)
962    }
963
964    pub fn get_entry_by_global_id(&self, id: &GlobalId) -> CatalogCollectionEntry {
965        self.state.get_entry_by_global_id(id)
966    }
967
968    pub fn get_global_ids<'a>(
969        &'a self,
970        id: &CatalogItemId,
971    ) -> impl Iterator<Item = GlobalId> + use<'a> {
972        self.get_entry(id).global_ids()
973    }
974
975    pub fn resolve_item_id(&self, id: &GlobalId) -> CatalogItemId {
976        self.get_entry_by_global_id(id).id()
977    }
978
979    pub fn try_resolve_item_id(&self, id: &GlobalId) -> Option<CatalogItemId> {
980        let item = self.try_get_entry_by_global_id(id)?;
981        Some(item.id())
982    }
983
984    pub fn get_schema(
985        &self,
986        database_spec: &ResolvedDatabaseSpecifier,
987        schema_spec: &SchemaSpecifier,
988        conn_id: &ConnectionId,
989    ) -> &Schema {
990        self.state.get_schema(database_spec, schema_spec, conn_id)
991    }
992
993    pub fn try_get_schema(
994        &self,
995        database_spec: &ResolvedDatabaseSpecifier,
996        schema_spec: &SchemaSpecifier,
997        conn_id: &ConnectionId,
998    ) -> Option<&Schema> {
999        self.state
1000            .try_get_schema(database_spec, schema_spec, conn_id)
1001    }
1002
1003    pub fn get_mz_catalog_schema_id(&self) -> SchemaId {
1004        self.state.get_mz_catalog_schema_id()
1005    }
1006
1007    pub fn get_pg_catalog_schema_id(&self) -> SchemaId {
1008        self.state.get_pg_catalog_schema_id()
1009    }
1010
1011    pub fn get_information_schema_id(&self) -> SchemaId {
1012        self.state.get_information_schema_id()
1013    }
1014
1015    pub fn get_mz_internal_schema_id(&self) -> SchemaId {
1016        self.state.get_mz_internal_schema_id()
1017    }
1018
1019    pub fn get_mz_introspection_schema_id(&self) -> SchemaId {
1020        self.state.get_mz_introspection_schema_id()
1021    }
1022
1023    pub fn get_mz_unsafe_schema_id(&self) -> SchemaId {
1024        self.state.get_mz_unsafe_schema_id()
1025    }
1026
1027    pub fn system_schema_ids(&self) -> impl Iterator<Item = SchemaId> + '_ {
1028        self.state.system_schema_ids()
1029    }
1030
1031    pub fn get_database(&self, id: &DatabaseId) -> &Database {
1032        self.state.get_database(id)
1033    }
1034
1035    pub fn try_get_role(&self, id: &RoleId) -> Option<&Role> {
1036        self.state.try_get_role(id)
1037    }
1038
1039    pub fn get_role(&self, id: &RoleId) -> &Role {
1040        self.state.get_role(id)
1041    }
1042
1043    pub fn try_get_role_by_name(&self, role_name: &str) -> Option<&Role> {
1044        self.state.try_get_role_by_name(role_name)
1045    }
1046
1047    pub fn try_get_role_auth_by_id(&self, id: &RoleId) -> Option<&RoleAuth> {
1048        self.state.try_get_role_auth_by_id(id)
1049    }
1050
1051    /// Creates a new schema in the `Catalog` for temporary items
1052    /// indicated by the TEMPORARY or TEMP keywords.
1053    pub fn create_temporary_schema(
1054        &mut self,
1055        conn_id: &ConnectionId,
1056        owner_id: RoleId,
1057    ) -> Result<(), Error> {
1058        self.state.create_temporary_schema(conn_id, owner_id)
1059    }
1060
1061    fn item_exists_in_temp_schemas(&self, conn_id: &ConnectionId, item_name: &str) -> bool {
1062        // Temporary schemas are created lazily, so it's valid for one to not exist yet.
1063        self.state
1064            .temporary_schemas
1065            .get(conn_id)
1066            .map(|schema| schema.items.contains_key(item_name))
1067            .unwrap_or(false)
1068    }
1069
1070    /// Drops schema for connection if it exists. Returns an error if it exists and has items.
1071    /// Returns Ok if conn_id's temp schema does not exist.
1072    pub fn drop_temporary_schema(&mut self, conn_id: &ConnectionId) -> Result<(), Error> {
1073        let Some(schema) = self.state.temporary_schemas.remove(conn_id) else {
1074            return Ok(());
1075        };
1076        if !schema.items.is_empty() {
1077            return Err(Error::new(ErrorKind::SchemaNotEmpty(MZ_TEMP_SCHEMA.into())));
1078        }
1079        Ok(())
1080    }
1081
1082    pub(crate) fn object_dependents(
1083        &self,
1084        object_ids: &Vec<ObjectId>,
1085        conn_id: &ConnectionId,
1086    ) -> Vec<ObjectId> {
1087        let mut seen = BTreeSet::new();
1088        self.state.object_dependents(object_ids, conn_id, &mut seen)
1089    }
1090
1091    fn full_name_detail(name: &FullItemName) -> FullNameV1 {
1092        FullNameV1 {
1093            database: name.database.to_string(),
1094            schema: name.schema.clone(),
1095            item: name.item.clone(),
1096        }
1097    }
1098
1099    pub fn find_available_cluster_name(&self, name: &str) -> String {
1100        let mut i = 0;
1101        let mut candidate = name.to_string();
1102        while self.state.clusters_by_name.contains_key(&candidate) {
1103            i += 1;
1104            candidate = format!("{}{}", name, i);
1105        }
1106        candidate
1107    }
1108
1109    pub fn get_role_allowed_cluster_sizes(&self, role_id: &Option<RoleId>) -> Vec<String> {
1110        if role_id == &Some(MZ_SYSTEM_ROLE_ID) {
1111            self.cluster_replica_sizes()
1112                .enabled_allocations()
1113                .map(|a| a.0.to_owned())
1114                .collect::<Vec<_>>()
1115        } else {
1116            self.system_config().allowed_cluster_replica_sizes()
1117        }
1118    }
1119
1120    pub fn concretize_replica_location(
1121        &self,
1122        location: mz_catalog::durable::ReplicaLocation,
1123        allowed_sizes: &Vec<String>,
1124        allowed_availability_zones: Option<&[String]>,
1125        allow_disabled: bool,
1126    ) -> Result<ReplicaLocation, Error> {
1127        self.state.concretize_replica_location(
1128            location,
1129            allowed_sizes,
1130            allowed_availability_zones,
1131            allow_disabled,
1132        )
1133    }
1134
1135    pub(crate) fn ensure_valid_replica_size(
1136        &self,
1137        allowed_sizes: &[String],
1138        size: &String,
1139        allow_disabled: bool,
1140    ) -> Result<(), Error> {
1141        self.state
1142            .ensure_valid_replica_size(allowed_sizes, size, allow_disabled)
1143    }
1144
1145    pub fn cluster_replica_sizes(&self) -> &ClusterReplicaSizeMap {
1146        &self.state.cluster_replica_sizes
1147    }
1148
1149    /// Returns the privileges of an object by its ID.
1150    pub fn get_privileges(
1151        &self,
1152        id: &SystemObjectId,
1153        conn_id: &ConnectionId,
1154    ) -> Option<&PrivilegeMap> {
1155        match id {
1156            SystemObjectId::Object(id) => match id {
1157                ObjectId::Cluster(id) => Some(self.get_cluster(*id).privileges()),
1158                ObjectId::Database(id) => Some(self.get_database(id).privileges()),
1159                ObjectId::Schema((database_spec, schema_spec)) => Some(
1160                    self.get_schema(database_spec, schema_spec, conn_id)
1161                        .privileges(),
1162                ),
1163                ObjectId::Item(id) => Some(self.get_entry(id).privileges()),
1164                ObjectId::ClusterReplica(_) | ObjectId::Role(_) => None,
1165                ObjectId::NetworkPolicy(id) => Some(self.get_network_policy(*id).privileges()),
1166            },
1167            SystemObjectId::System => Some(&self.state.system_privileges),
1168        }
1169    }
1170
1171    /// Advances the catalog upper to at least `new_upper`.
1172    ///
1173    /// Empty progress can overtake `new_upper`. A durable upper mismatch with content returns
1174    /// `CatalogOutOfSync`. See [`mz_catalog::durable::DurableCatalogState::advance_upper`] for
1175    /// fencing semantics.
1176    #[mz_ore::instrument(level = "debug")]
1177    pub async fn advance_upper(&self, new_upper: mz_repr::Timestamp) -> Result<(), AdapterError> {
1178        Ok(self.storage().await.advance_upper(new_upper).await?)
1179    }
1180
1181    /// Returns a durable-upper handle that shares the catalog storage mutex.
1182    pub fn upper_handle(&self) -> CatalogUpperHandle {
1183        CatalogUpperHandle {
1184            storage: Arc::clone(&self.storage),
1185        }
1186    }
1187
1188    /// Return the ids of all log sources the given object depends on.
1189    pub fn introspection_dependencies(&self, id: CatalogItemId) -> Vec<CatalogItemId> {
1190        self.state.introspection_dependencies(id)
1191    }
1192
1193    /// Serializes the catalog's in-memory state.
1194    ///
1195    /// There are no guarantees about the format of the serialized state, except
1196    /// that the serialized state for two identical catalogs will compare
1197    /// identically.
1198    pub fn dump(&self) -> Result<CatalogDump, Error> {
1199        Ok(CatalogDump::new(self.state.dump(None)?))
1200    }
1201
1202    /// Checks the [`Catalog`]s internal consistency.
1203    ///
1204    /// Returns a JSON object describing the inconsistencies, if there are any.
1205    pub fn check_consistency(&self) -> Result<(), serde_json::Value> {
1206        self.state.check_consistency().map_err(|inconsistencies| {
1207            serde_json::to_value(inconsistencies).unwrap_or_else(|_| {
1208                serde_json::Value::String("failed to serialize inconsistencies".to_string())
1209            })
1210        })
1211    }
1212
1213    pub fn config(&self) -> &mz_sql::catalog::CatalogConfig {
1214        self.state.config()
1215    }
1216
1217    pub fn entries(&self) -> impl Iterator<Item = &CatalogEntry> {
1218        self.state.entry_by_id.values()
1219    }
1220
1221    pub fn user_connections(&self) -> impl Iterator<Item = &CatalogEntry> {
1222        self.entries()
1223            .filter(|entry| entry.is_connection() && entry.id().is_user())
1224    }
1225
1226    pub fn user_tables(&self) -> impl Iterator<Item = &CatalogEntry> {
1227        self.entries()
1228            .filter(|entry| entry.is_table() && entry.id().is_user())
1229    }
1230
1231    pub fn user_sources(&self) -> impl Iterator<Item = &CatalogEntry> {
1232        self.entries()
1233            .filter(|entry| entry.is_source() && entry.id().is_user())
1234    }
1235
1236    pub fn user_sinks(&self) -> impl Iterator<Item = &CatalogEntry> {
1237        self.entries()
1238            .filter(|entry| entry.is_sink() && entry.id().is_user())
1239    }
1240
1241    pub fn user_materialized_views(&self) -> impl Iterator<Item = &CatalogEntry> {
1242        self.entries()
1243            .filter(|entry| entry.is_materialized_view() && entry.id().is_user())
1244    }
1245
1246    pub fn user_secrets(&self) -> impl Iterator<Item = &CatalogEntry> {
1247        self.entries()
1248            .filter(|entry| entry.is_secret() && entry.id().is_user())
1249    }
1250
1251    pub fn get_network_policy(&self, network_policy_id: NetworkPolicyId) -> &NetworkPolicy {
1252        self.state.get_network_policy(&network_policy_id)
1253    }
1254
1255    pub fn get_network_policy_by_name(&self, name: &str) -> Option<&NetworkPolicy> {
1256        self.state.try_get_network_policy_by_name(name)
1257    }
1258
1259    pub fn clusters(&self) -> impl Iterator<Item = &Cluster> {
1260        self.state.clusters_by_id.values()
1261    }
1262
1263    pub fn get_cluster(&self, cluster_id: ClusterId) -> &Cluster {
1264        self.state.get_cluster(cluster_id)
1265    }
1266
1267    pub fn try_get_cluster(&self, cluster_id: ClusterId) -> Option<&Cluster> {
1268        self.state.try_get_cluster(cluster_id)
1269    }
1270
1271    pub fn user_clusters(&self) -> impl Iterator<Item = &Cluster> {
1272        self.clusters().filter(|cluster| cluster.id.is_user())
1273    }
1274
1275    pub fn get_cluster_replica(
1276        &self,
1277        cluster_id: ClusterId,
1278        replica_id: ReplicaId,
1279    ) -> &ClusterReplica {
1280        self.state.get_cluster_replica(cluster_id, replica_id)
1281    }
1282
1283    pub fn try_get_cluster_replica(
1284        &self,
1285        cluster_id: ClusterId,
1286        replica_id: ReplicaId,
1287    ) -> Option<&ClusterReplica> {
1288        self.state.try_get_cluster_replica(cluster_id, replica_id)
1289    }
1290
1291    pub fn user_cluster_replicas(&self) -> impl Iterator<Item = &ClusterReplica> {
1292        self.user_clusters()
1293            .flat_map(|cluster| cluster.user_replicas())
1294    }
1295
1296    pub fn databases(&self) -> impl Iterator<Item = &Database> {
1297        self.state.database_by_id.values()
1298    }
1299
1300    pub fn user_roles(&self) -> impl Iterator<Item = &Role> {
1301        self.state
1302            .roles_by_id
1303            .values()
1304            .filter(|role| role.is_user())
1305    }
1306
1307    pub fn user_network_policies(&self) -> impl Iterator<Item = &NetworkPolicy> {
1308        self.state
1309            .network_policies_by_id
1310            .iter()
1311            .filter(|(id, _)| id.is_user())
1312            .map(|(_, policy)| policy)
1313    }
1314
1315    pub fn system_privileges(&self) -> &PrivilegeMap {
1316        &self.state.system_privileges
1317    }
1318
1319    pub fn default_privileges(
1320        &self,
1321    ) -> impl Iterator<
1322        Item = (
1323            &DefaultPrivilegeObject,
1324            impl Iterator<Item = &DefaultPrivilegeAclItem>,
1325        ),
1326    > {
1327        self.state.default_privileges.iter()
1328    }
1329
1330    pub fn pack_item_update(&self, id: CatalogItemId, diff: Diff) -> Vec<BuiltinTableUpdate> {
1331        self.state
1332            .resolve_builtin_table_updates(self.state.pack_item_update(id, diff))
1333    }
1334
1335    pub fn pack_storage_usage_update(
1336        &self,
1337        event: VersionedStorageUsage,
1338        diff: Diff,
1339    ) -> BuiltinTableUpdate {
1340        self.state
1341            .resolve_builtin_table_update(self.state.pack_storage_usage_update(event, diff))
1342    }
1343
1344    pub fn system_config(&self) -> &SystemVars {
1345        self.state.system_config()
1346    }
1347
1348    pub fn system_config_mut(&mut self) -> &mut SystemVars {
1349        self.state.system_config_mut()
1350    }
1351
1352    pub fn ensure_not_reserved_role(&self, role_id: &RoleId) -> Result<(), Error> {
1353        self.state.ensure_not_reserved_role(role_id)
1354    }
1355
1356    pub fn ensure_grantable_role(&self, role_id: &RoleId) -> Result<(), Error> {
1357        self.state.ensure_grantable_role(role_id)
1358    }
1359
1360    pub fn ensure_not_system_role(&self, role_id: &RoleId) -> Result<(), Error> {
1361        self.state.ensure_not_system_role(role_id)
1362    }
1363
1364    pub fn ensure_not_predefined_role(&self, role_id: &RoleId) -> Result<(), Error> {
1365        self.state.ensure_not_predefined_role(role_id)
1366    }
1367
1368    pub fn ensure_not_reserved_network_policy(
1369        &self,
1370        network_policy_id: &NetworkPolicyId,
1371    ) -> Result<(), Error> {
1372        self.state
1373            .ensure_not_reserved_network_policy(network_policy_id)
1374    }
1375
1376    pub fn ensure_not_reserved_object(
1377        &self,
1378        object_id: &ObjectId,
1379        conn_id: &ConnectionId,
1380    ) -> Result<(), Error> {
1381        match object_id {
1382            ObjectId::Cluster(cluster_id) => {
1383                if cluster_id.is_system() {
1384                    let cluster = self.get_cluster(*cluster_id);
1385                    Err(Error::new(ErrorKind::ReadOnlyCluster(
1386                        cluster.name().to_string(),
1387                    )))
1388                } else {
1389                    Ok(())
1390                }
1391            }
1392            ObjectId::ClusterReplica((cluster_id, replica_id)) => {
1393                if replica_id.is_system() {
1394                    let replica = self.get_cluster_replica(*cluster_id, *replica_id);
1395                    Err(Error::new(ErrorKind::ReadOnlyClusterReplica(
1396                        replica.name().to_string(),
1397                    )))
1398                } else {
1399                    Ok(())
1400                }
1401            }
1402            ObjectId::Database(database_id) => {
1403                if database_id.is_system() {
1404                    let database = self.get_database(database_id);
1405                    Err(Error::new(ErrorKind::ReadOnlyDatabase(
1406                        database.name().to_string(),
1407                    )))
1408                } else {
1409                    Ok(())
1410                }
1411            }
1412            ObjectId::Schema((database_spec, schema_spec)) => {
1413                if schema_spec.is_system() {
1414                    let schema = self.get_schema(database_spec, schema_spec, conn_id);
1415                    Err(Error::new(ErrorKind::ReadOnlySystemSchema(
1416                        schema.name().schema.clone(),
1417                    )))
1418                } else {
1419                    Ok(())
1420                }
1421            }
1422            ObjectId::Role(role_id) => self.ensure_not_reserved_role(role_id),
1423            ObjectId::Item(item_id) => {
1424                if item_id.is_system() {
1425                    let item = self.get_entry(item_id);
1426                    let name = self.resolve_full_name(item.name(), Some(conn_id));
1427                    Err(Error::new(ErrorKind::ReadOnlyItem(name.to_string())))
1428                } else {
1429                    Ok(())
1430                }
1431            }
1432            ObjectId::NetworkPolicy(network_policy_id) => {
1433                self.ensure_not_reserved_network_policy(network_policy_id)
1434            }
1435        }
1436    }
1437
1438    /// See [`CatalogState::deserialize_plan_with_enable_for_item_parsing`].
1439    pub(crate) fn deserialize_plan_with_enable_for_item_parsing(
1440        &mut self,
1441        create_sql: &str,
1442        force_if_exists_skip: bool,
1443    ) -> Result<(Plan, ResolvedIds), AdapterError> {
1444        self.state
1445            .deserialize_plan_with_enable_for_item_parsing(create_sql, force_if_exists_skip)
1446    }
1447
1448    /// Cache global and, optionally, local expressions for the given
1449    /// `GlobalId`.
1450    ///
1451    /// Takes the plans and metainfo directly as parameters (rather than
1452    /// fishing them out of catalog state), so this can be called **before**
1453    /// the catalog transaction that creates the item. Returns the future
1454    /// returned by [`Catalog::update_expression_cache`]; callers should
1455    /// `.await` it before the catalog transaction commits, so the durable
1456    /// expression cache is observed to contain the entries by the time any
1457    /// other process (or a subsequent bootstrap on this process) reads them.
1458    pub(crate) fn cache_expressions(
1459        &self,
1460        id: GlobalId,
1461        local_mir: Option<OptimizedMirRelationExpr>,
1462        mut global_mir: DataflowDescription<OptimizedMirRelationExpr>,
1463        mut physical_plan: DataflowDescription<mz_compute_types::plan::LirRelationExpr>,
1464        dataflow_metainfos: DataflowMetainfo<Arc<OptimizerNotice>>,
1465        optimizer_features: OptimizerFeatures,
1466    ) -> BoxFuture<'static, ()> {
1467        // Make sure we're not caching the result of timestamp selection, as
1468        // it will almost certainly be wrong if we re-install the dataflow at
1469        // a later time.
1470        global_mir.as_of = None;
1471        global_mir.until = Default::default();
1472        physical_plan.as_of = None;
1473        physical_plan.until = Default::default();
1474
1475        let mut local_exprs = Vec::new();
1476        if let Some(local_mir) = local_mir {
1477            local_exprs.push((
1478                id,
1479                LocalExpressions {
1480                    local_mir,
1481                    optimizer_features: optimizer_features.clone(),
1482                },
1483            ));
1484        }
1485        let global_exprs = vec![(
1486            id,
1487            GlobalExpressions {
1488                global_mir,
1489                physical_plan,
1490                dataflow_metainfos,
1491                optimizer_features,
1492            },
1493        )];
1494        self.update_expression_cache(local_exprs, global_exprs, Default::default())
1495    }
1496
1497    pub(crate) fn update_expression_cache<'a, 'b>(
1498        &'a self,
1499        new_local_expressions: Vec<(GlobalId, LocalExpressions)>,
1500        new_global_expressions: Vec<(GlobalId, GlobalExpressions)>,
1501        invalidate_ids: BTreeSet<GlobalId>,
1502    ) -> BoxFuture<'b, ()> {
1503        if let Some(expr_cache) = &self.expr_cache_handle {
1504            expr_cache
1505                .update(
1506                    new_local_expressions,
1507                    new_global_expressions,
1508                    invalidate_ids,
1509                )
1510                .boxed()
1511        } else {
1512            async {}.boxed()
1513        }
1514    }
1515
1516    /// Listen for and apply all unconsumed updates to the durable catalog state.
1517    // TODO(jkosh44) When this method is actually used outside of a test we can remove the
1518    // `#[cfg(test)]` annotation.
1519    #[cfg(test)]
1520    async fn sync_to_current_updates(
1521        &mut self,
1522    ) -> Result<
1523        (
1524            Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
1525            Vec<ParsedStateUpdate>,
1526        ),
1527        CatalogError,
1528    > {
1529        let updates = self.storage().await.sync_to_current_updates().await?;
1530        let (builtin_table_updates, catalog_updates) = self
1531            .state
1532            .apply_updates(updates, &mut state::LocalExpressionCache::Closed)
1533            .await;
1534        Ok((builtin_table_updates, catalog_updates))
1535    }
1536}
1537
1538pub fn is_reserved_name(name: &str) -> bool {
1539    BUILTIN_PREFIXES
1540        .iter()
1541        .any(|prefix| name.starts_with(prefix))
1542}
1543
1544/// Role names that PostgreSQL reserves for role specifications in statements
1545/// like `GRANT ... TO CURRENT_USER` and `SET ROLE NONE`. A role with such a
1546/// name would be ambiguous there, so creating one is not allowed.
1547///
1548/// PostgreSQL rejects most of these at parse time, only when they appear as
1549/// unquoted keywords. Our parser does not track whether an identifier was
1550/// quoted, so we instead reject the names themselves. Only the lowercase
1551/// spellings are reserved, which is what the unquoted forms normalize to, so
1552/// quoted names like `"CURRENT_USER"` remain valid, as in PostgreSQL.
1553const RESERVED_ROLE_SPECIFICATION_NAMES: [&str; 5] = [
1554    "current_user",
1555    "current_role",
1556    "session_user",
1557    "user",
1558    "none",
1559];
1560
1561pub fn is_reserved_role_name(name: &str) -> bool {
1562    is_reserved_name(name)
1563        || is_public_role(name)
1564        || RESERVED_ROLE_SPECIFICATION_NAMES.contains(&name)
1565}
1566
1567pub fn is_public_role(name: &str) -> bool {
1568    name == &*PUBLIC_ROLE_NAME
1569}
1570
1571pub(crate) fn catalog_type_to_audit_object_type(sql_type: SqlCatalogItemType) -> ObjectType {
1572    object_type_to_audit_object_type(sql_type.into())
1573}
1574
1575pub(crate) fn comment_id_to_audit_object_type(id: CommentObjectId) -> ObjectType {
1576    match id {
1577        CommentObjectId::Table(_) => ObjectType::Table,
1578        CommentObjectId::View(_) => ObjectType::View,
1579        CommentObjectId::MaterializedView(_) => ObjectType::MaterializedView,
1580        CommentObjectId::Source(_) => ObjectType::Source,
1581        CommentObjectId::Sink(_) => ObjectType::Sink,
1582        CommentObjectId::Index(_) => ObjectType::Index,
1583        CommentObjectId::Func(_) => ObjectType::Func,
1584        CommentObjectId::Connection(_) => ObjectType::Connection,
1585        CommentObjectId::Type(_) => ObjectType::Type,
1586        CommentObjectId::Secret(_) => ObjectType::Secret,
1587        CommentObjectId::Role(_) => ObjectType::Role,
1588        CommentObjectId::Database(_) => ObjectType::Database,
1589        CommentObjectId::Schema(_) => ObjectType::Schema,
1590        CommentObjectId::Cluster(_) => ObjectType::Cluster,
1591        CommentObjectId::ClusterReplica(_) => ObjectType::ClusterReplica,
1592        CommentObjectId::NetworkPolicy(_) => ObjectType::NetworkPolicy,
1593    }
1594}
1595
1596pub(crate) fn object_type_to_audit_object_type(
1597    object_type: mz_sql::catalog::ObjectType,
1598) -> ObjectType {
1599    system_object_type_to_audit_object_type(&SystemObjectType::Object(object_type))
1600}
1601
1602pub(crate) fn system_object_type_to_audit_object_type(
1603    system_type: &SystemObjectType,
1604) -> ObjectType {
1605    match system_type {
1606        SystemObjectType::Object(object_type) => match object_type {
1607            mz_sql::catalog::ObjectType::Table => ObjectType::Table,
1608            mz_sql::catalog::ObjectType::View => ObjectType::View,
1609            mz_sql::catalog::ObjectType::MaterializedView => ObjectType::MaterializedView,
1610            mz_sql::catalog::ObjectType::Source => ObjectType::Source,
1611            mz_sql::catalog::ObjectType::Sink => ObjectType::Sink,
1612            mz_sql::catalog::ObjectType::Index => ObjectType::Index,
1613            mz_sql::catalog::ObjectType::Type => ObjectType::Type,
1614            mz_sql::catalog::ObjectType::Role => ObjectType::Role,
1615            mz_sql::catalog::ObjectType::Cluster => ObjectType::Cluster,
1616            mz_sql::catalog::ObjectType::ClusterReplica => ObjectType::ClusterReplica,
1617            mz_sql::catalog::ObjectType::Secret => ObjectType::Secret,
1618            mz_sql::catalog::ObjectType::Connection => ObjectType::Connection,
1619            mz_sql::catalog::ObjectType::Database => ObjectType::Database,
1620            mz_sql::catalog::ObjectType::Schema => ObjectType::Schema,
1621            mz_sql::catalog::ObjectType::Func => ObjectType::Func,
1622            mz_sql::catalog::ObjectType::NetworkPolicy => ObjectType::NetworkPolicy,
1623        },
1624        SystemObjectType::System => ObjectType::System,
1625    }
1626}
1627
1628#[derive(Debug, Copy, Clone)]
1629pub enum UpdatePrivilegeVariant {
1630    Grant,
1631    Revoke,
1632}
1633
1634impl From<UpdatePrivilegeVariant> for ExecuteResponse {
1635    fn from(variant: UpdatePrivilegeVariant) -> Self {
1636        match variant {
1637            UpdatePrivilegeVariant::Grant => ExecuteResponse::GrantedPrivilege,
1638            UpdatePrivilegeVariant::Revoke => ExecuteResponse::RevokedPrivilege,
1639        }
1640    }
1641}
1642
1643impl From<UpdatePrivilegeVariant> for EventType {
1644    fn from(variant: UpdatePrivilegeVariant) -> Self {
1645        match variant {
1646            UpdatePrivilegeVariant::Grant => EventType::Grant,
1647            UpdatePrivilegeVariant::Revoke => EventType::Revoke,
1648        }
1649    }
1650}
1651
1652impl ConnCatalog<'_> {
1653    fn resolve_item_name(
1654        &self,
1655        name: &PartialItemName,
1656    ) -> Result<&QualifiedItemName, SqlCatalogError> {
1657        self.resolve_item(name).map(|entry| entry.name())
1658    }
1659
1660    fn resolve_function_name(
1661        &self,
1662        name: &PartialItemName,
1663    ) -> Result<&QualifiedItemName, SqlCatalogError> {
1664        self.resolve_function(name).map(|entry| entry.name())
1665    }
1666
1667    fn resolve_type_name(
1668        &self,
1669        name: &PartialItemName,
1670    ) -> Result<&QualifiedItemName, SqlCatalogError> {
1671        self.resolve_type(name).map(|entry| entry.name())
1672    }
1673}
1674
1675impl ExprHumanizer for ConnCatalog<'_> {
1676    fn humanize_id(&self, id: GlobalId) -> Option<String> {
1677        let entry = self.state.try_get_entry_by_global_id(&id)?;
1678        Some(self.resolve_full_name(entry.name()).to_string())
1679    }
1680
1681    fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String> {
1682        let entry = self.state.try_get_entry_by_global_id(&id)?;
1683        Some(entry.name().item.clone())
1684    }
1685
1686    fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>> {
1687        let entry = self.state.try_get_entry_by_global_id(&id)?;
1688        Some(self.resolve_full_name(entry.name()).into_parts())
1689    }
1690
1691    fn humanize_sql_scalar_type(&self, typ: &SqlScalarType, postgres_compat: bool) -> String {
1692        use SqlScalarType::*;
1693
1694        match typ {
1695            Array(t) => format!("{}[]", self.humanize_sql_scalar_type(t, postgres_compat)),
1696            List {
1697                custom_id: Some(item_id),
1698                ..
1699            }
1700            | Map {
1701                custom_id: Some(item_id),
1702                ..
1703            } => {
1704                let item = self.get_item(item_id);
1705                self.minimal_qualification(item.name()).to_string()
1706            }
1707            List { element_type, .. } => {
1708                format!(
1709                    "{} list",
1710                    self.humanize_sql_scalar_type(element_type, postgres_compat)
1711                )
1712            }
1713            Map { value_type, .. } => format!(
1714                "map[{}=>{}]",
1715                self.humanize_sql_scalar_type(&SqlScalarType::String, postgres_compat),
1716                self.humanize_sql_scalar_type(value_type, postgres_compat)
1717            ),
1718            Record {
1719                custom_id: Some(item_id),
1720                ..
1721            } => {
1722                let item = self.get_item(item_id);
1723                self.minimal_qualification(item.name()).to_string()
1724            }
1725            Record { fields, .. } => format!(
1726                "record({})",
1727                fields
1728                    .iter()
1729                    .map(|f| format!(
1730                        "{}: {}",
1731                        f.0,
1732                        self.humanize_sql_column_type(&f.1, postgres_compat)
1733                    ))
1734                    .join(",")
1735            ),
1736            PgLegacyChar => "\"char\"".into(),
1737            Char { length } if !postgres_compat => match length {
1738                None => "char".into(),
1739                Some(length) => format!("char({})", length.into_u32()),
1740            },
1741            VarChar { max_length } if !postgres_compat => match max_length {
1742                None => "varchar".into(),
1743                Some(length) => format!("varchar({})", length.into_u32()),
1744            },
1745            UInt16 => "uint2".into(),
1746            UInt32 => "uint4".into(),
1747            UInt64 => "uint8".into(),
1748            ty => {
1749                let pgrepr_type = mz_pgrepr::Type::from(ty);
1750                let pg_catalog_schema = SchemaSpecifier::Id(self.state.get_pg_catalog_schema_id());
1751
1752                let res = if self
1753                    .effective_search_path(true)
1754                    .iter()
1755                    .any(|(_, schema)| schema == &pg_catalog_schema)
1756                {
1757                    pgrepr_type.name().to_string()
1758                } else {
1759                    // If PG_CATALOG_SCHEMA is not in search path, you need
1760                    // qualified object name to refer to type.
1761                    let name = QualifiedItemName {
1762                        qualifiers: ItemQualifiers {
1763                            database_spec: ResolvedDatabaseSpecifier::Ambient,
1764                            schema_spec: pg_catalog_schema,
1765                        },
1766                        item: pgrepr_type.name().to_string(),
1767                    };
1768                    self.resolve_full_name(&name).to_string()
1769                };
1770                res
1771            }
1772        }
1773    }
1774
1775    fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>> {
1776        let entry = self.state.try_get_entry_by_global_id(&id)?;
1777
1778        match entry.index() {
1779            Some(index) => {
1780                let on_desc = self.state.try_get_desc_by_global_id(&index.on)?;
1781                let mut on_names = on_desc
1782                    .iter_names()
1783                    .map(|col_name| col_name.to_string())
1784                    .collect::<Vec<_>>();
1785
1786                let (p, _) = mz_expr::permutation_for_arrangement(&index.keys, on_desc.arity());
1787
1788                // Init ix_names with unknown column names. Unknown columns are
1789                // represented as an empty String and rendered as `#c` by the
1790                // Display::fmt implementation for HumanizedExpr<'a, usize, M>.
1791                let ix_arity = p.iter().map(|x| *x + 1).max().unwrap_or(0);
1792                let mut ix_names = vec![String::new(); ix_arity];
1793
1794                // Apply the permutation by swapping on_names with ix_names.
1795                for (on_pos, ix_pos) in p.into_iter().enumerate() {
1796                    let on_name = on_names.get_mut(on_pos).expect("on_name");
1797                    let ix_name = ix_names.get_mut(ix_pos).expect("ix_name");
1798                    std::mem::swap(on_name, ix_name);
1799                }
1800
1801                Some(ix_names) // Return the updated ix_names vector.
1802            }
1803            None => {
1804                let desc = self.state.try_get_desc_by_global_id(&id)?;
1805                let column_names = desc
1806                    .iter_names()
1807                    .map(|col_name| col_name.to_string())
1808                    .collect();
1809
1810                Some(column_names)
1811            }
1812        }
1813    }
1814
1815    fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String> {
1816        let desc = self.state.try_get_desc_by_global_id(&id)?;
1817        Some(desc.get_name(column).to_string())
1818    }
1819
1820    fn id_exists(&self, id: GlobalId) -> bool {
1821        self.state.entry_by_global_id.contains_key(&id)
1822    }
1823}
1824
1825impl SessionCatalog for ConnCatalog<'_> {
1826    fn active_role_id(&self) -> &RoleId {
1827        &self.role_id
1828    }
1829
1830    fn restrict_to_user_objects(&self) -> bool {
1831        self.restrict_to_user_objects
1832    }
1833
1834    fn get_prepared_statement_desc(&self, name: &str) -> Option<&StatementDesc> {
1835        self.prepared_statements
1836            .as_ref()
1837            .map(|ps| ps.get(name).map(|ps| ps.desc()))
1838            .flatten()
1839    }
1840
1841    fn get_portal_desc_unverified(&self, portal_name: &str) -> Option<&StatementDesc> {
1842        self.portals
1843            .and_then(|portals| portals.get(portal_name).map(|portal| &portal.desc))
1844    }
1845
1846    fn active_database(&self) -> Option<&DatabaseId> {
1847        self.database.as_ref()
1848    }
1849
1850    fn active_cluster(&self) -> &str {
1851        &self.cluster
1852    }
1853
1854    fn search_path(&self) -> &[(ResolvedDatabaseSpecifier, SchemaSpecifier)] {
1855        &self.search_path
1856    }
1857
1858    fn resolve_database(
1859        &self,
1860        database_name: &str,
1861    ) -> Result<&dyn mz_sql::catalog::CatalogDatabase, SqlCatalogError> {
1862        Ok(self.state.resolve_database(database_name)?)
1863    }
1864
1865    fn get_database(&self, id: &DatabaseId) -> &dyn mz_sql::catalog::CatalogDatabase {
1866        self.state
1867            .database_by_id
1868            .get(id)
1869            .expect("database doesn't exist")
1870    }
1871
1872    // `as` is ok to use to cast to a trait object.
1873    #[allow(clippy::as_conversions)]
1874    fn get_databases(&self) -> Vec<&dyn CatalogDatabase> {
1875        self.state
1876            .database_by_id
1877            .values()
1878            .map(|database| database as &dyn CatalogDatabase)
1879            .collect()
1880    }
1881
1882    fn resolve_schema(
1883        &self,
1884        database_name: Option<&str>,
1885        schema_name: &str,
1886    ) -> Result<&dyn mz_sql::catalog::CatalogSchema, SqlCatalogError> {
1887        Ok(self.state.resolve_schema(
1888            self.database.as_ref(),
1889            database_name,
1890            schema_name,
1891            &self.conn_id,
1892        )?)
1893    }
1894
1895    fn resolve_schema_in_database(
1896        &self,
1897        database_spec: &ResolvedDatabaseSpecifier,
1898        schema_name: &str,
1899    ) -> Result<&dyn mz_sql::catalog::CatalogSchema, SqlCatalogError> {
1900        Ok(self
1901            .state
1902            .resolve_schema_in_database(database_spec, schema_name, &self.conn_id)?)
1903    }
1904
1905    fn get_schema(
1906        &self,
1907        database_spec: &ResolvedDatabaseSpecifier,
1908        schema_spec: &SchemaSpecifier,
1909    ) -> &dyn CatalogSchema {
1910        self.state
1911            .get_schema(database_spec, schema_spec, &self.conn_id)
1912    }
1913
1914    // `as` is ok to use to cast to a trait object.
1915    #[allow(clippy::as_conversions)]
1916    fn get_schemas(&self) -> Vec<&dyn CatalogSchema> {
1917        self.get_databases()
1918            .into_iter()
1919            .flat_map(|database| database.schemas().into_iter())
1920            .chain(
1921                self.state
1922                    .ambient_schemas_by_id
1923                    .values()
1924                    .chain(self.state.temporary_schemas.values())
1925                    .map(|schema| schema as &dyn CatalogSchema),
1926            )
1927            .collect()
1928    }
1929
1930    fn get_mz_internal_schema_id(&self) -> SchemaId {
1931        self.state().get_mz_internal_schema_id()
1932    }
1933
1934    fn get_mz_unsafe_schema_id(&self) -> SchemaId {
1935        self.state().get_mz_unsafe_schema_id()
1936    }
1937
1938    fn is_system_schema_specifier(&self, schema: SchemaSpecifier) -> bool {
1939        self.state.is_system_schema_specifier(schema)
1940    }
1941
1942    fn resolve_role(
1943        &self,
1944        role_name: &str,
1945    ) -> Result<&dyn mz_sql::catalog::CatalogRole, SqlCatalogError> {
1946        match self.state.try_get_role_by_name(role_name) {
1947            Some(role) => Ok(role),
1948            None => Err(SqlCatalogError::UnknownRole(role_name.into())),
1949        }
1950    }
1951
1952    fn resolve_network_policy(
1953        &self,
1954        policy_name: &str,
1955    ) -> Result<&dyn mz_sql::catalog::CatalogNetworkPolicy, SqlCatalogError> {
1956        match self.state.try_get_network_policy_by_name(policy_name) {
1957            Some(policy) => Ok(policy),
1958            None => Err(SqlCatalogError::UnknownNetworkPolicy(policy_name.into())),
1959        }
1960    }
1961
1962    fn try_get_role(&self, id: &RoleId) -> Option<&dyn CatalogRole> {
1963        Some(self.state.roles_by_id.get(id)?)
1964    }
1965
1966    fn get_role(&self, id: &RoleId) -> &dyn mz_sql::catalog::CatalogRole {
1967        self.state.get_role(id)
1968    }
1969
1970    fn get_roles(&self) -> Vec<&dyn CatalogRole> {
1971        // `as` is ok to use to cast to a trait object.
1972        #[allow(clippy::as_conversions)]
1973        self.state
1974            .roles_by_id
1975            .values()
1976            .map(|role| role as &dyn CatalogRole)
1977            .collect()
1978    }
1979
1980    fn mz_system_role_id(&self) -> RoleId {
1981        MZ_SYSTEM_ROLE_ID
1982    }
1983
1984    fn collect_role_membership(&self, id: &RoleId) -> BTreeSet<RoleId> {
1985        self.state.collect_role_membership(id)
1986    }
1987
1988    fn get_network_policy(
1989        &self,
1990        id: &NetworkPolicyId,
1991    ) -> &dyn mz_sql::catalog::CatalogNetworkPolicy {
1992        self.state.get_network_policy(id)
1993    }
1994
1995    fn get_network_policies(&self) -> Vec<&dyn mz_sql::catalog::CatalogNetworkPolicy> {
1996        // `as` is ok to use to cast to a trait object.
1997        #[allow(clippy::as_conversions)]
1998        self.state
1999            .network_policies_by_id
2000            .values()
2001            .map(|policy| policy as &dyn CatalogNetworkPolicy)
2002            .collect()
2003    }
2004
2005    fn resolve_cluster(
2006        &self,
2007        cluster_name: Option<&str>,
2008    ) -> Result<&dyn mz_sql::catalog::CatalogCluster<'_>, SqlCatalogError> {
2009        Ok(self
2010            .state
2011            .resolve_cluster(cluster_name.unwrap_or_else(|| self.active_cluster()))?)
2012    }
2013
2014    fn resolve_cluster_replica(
2015        &self,
2016        cluster_replica_name: &QualifiedReplica,
2017    ) -> Result<&dyn CatalogClusterReplica<'_>, SqlCatalogError> {
2018        Ok(self.state.resolve_cluster_replica(cluster_replica_name)?)
2019    }
2020
2021    fn resolve_item(
2022        &self,
2023        name: &PartialItemName,
2024    ) -> Result<&dyn mz_sql::catalog::CatalogItem, SqlCatalogError> {
2025        let r = self.state.resolve_entry(
2026            self.database.as_ref(),
2027            &self.effective_search_path(true),
2028            name,
2029            &self.conn_id,
2030        )?;
2031        if self.unresolvable_ids.contains(&r.id()) {
2032            Err(SqlCatalogError::UnknownItem(name.to_string()))
2033        } else {
2034            Ok(r)
2035        }
2036    }
2037
2038    fn resolve_function(
2039        &self,
2040        name: &PartialItemName,
2041    ) -> Result<&dyn mz_sql::catalog::CatalogItem, SqlCatalogError> {
2042        let r = self.state.resolve_function(
2043            self.database.as_ref(),
2044            &self.effective_search_path(false),
2045            name,
2046            &self.conn_id,
2047        )?;
2048
2049        if self.unresolvable_ids.contains(&r.id()) {
2050            Err(SqlCatalogError::UnknownFunction {
2051                name: name.to_string(),
2052                alternative: None,
2053            })
2054        } else {
2055            Ok(r)
2056        }
2057    }
2058
2059    fn resolve_type(
2060        &self,
2061        name: &PartialItemName,
2062    ) -> Result<&dyn mz_sql::catalog::CatalogItem, SqlCatalogError> {
2063        let r = self.state.resolve_type(
2064            self.database.as_ref(),
2065            &self.effective_search_path(false),
2066            name,
2067            &self.conn_id,
2068        )?;
2069
2070        if self.unresolvable_ids.contains(&r.id()) {
2071            Err(SqlCatalogError::UnknownType {
2072                name: name.to_string(),
2073            })
2074        } else {
2075            Ok(r)
2076        }
2077    }
2078
2079    fn get_system_type(&self, name: &str) -> &dyn mz_sql::catalog::CatalogItem {
2080        self.state.get_system_type(name)
2081    }
2082
2083    fn try_get_item(&self, id: &CatalogItemId) -> Option<&dyn mz_sql::catalog::CatalogItem> {
2084        Some(self.state.try_get_entry(id)?)
2085    }
2086
2087    fn try_get_item_by_global_id(
2088        &self,
2089        id: &GlobalId,
2090    ) -> Option<Box<dyn mz_sql::catalog::CatalogCollectionItem>> {
2091        let entry = self.state.try_get_entry_by_global_id(id)?;
2092        let entry = match &entry.item {
2093            CatalogItem::Table(table) => {
2094                let (version, _gid) = table
2095                    .collections
2096                    .iter()
2097                    .find(|(_version, gid)| *gid == id)
2098                    .expect("catalog out of sync, mismatched GlobalId");
2099                entry.at_version(RelationVersionSelector::Specific(*version))
2100            }
2101            _ => entry.at_version(RelationVersionSelector::Latest),
2102        };
2103        Some(entry)
2104    }
2105
2106    fn get_item(&self, id: &CatalogItemId) -> &dyn mz_sql::catalog::CatalogItem {
2107        self.state.get_entry(id)
2108    }
2109
2110    fn get_item_by_global_id(
2111        &self,
2112        id: &GlobalId,
2113    ) -> Box<dyn mz_sql::catalog::CatalogCollectionItem> {
2114        let entry = self.state.get_entry_by_global_id(id);
2115        let entry = match &entry.item {
2116            CatalogItem::Table(table) => {
2117                let (version, _gid) = table
2118                    .collections
2119                    .iter()
2120                    .find(|(_version, gid)| *gid == id)
2121                    .expect("catalog out of sync, mismatched GlobalId");
2122                entry.at_version(RelationVersionSelector::Specific(*version))
2123            }
2124            _ => entry.at_version(RelationVersionSelector::Latest),
2125        };
2126        entry
2127    }
2128
2129    fn get_items(&self) -> Vec<&dyn mz_sql::catalog::CatalogItem> {
2130        self.get_schemas()
2131            .into_iter()
2132            .flat_map(|schema| schema.item_ids())
2133            .map(|id| self.get_item(&id))
2134            .collect()
2135    }
2136
2137    fn get_item_by_name(&self, name: &QualifiedItemName) -> Option<&dyn SqlCatalogItem> {
2138        self.state
2139            .get_item_by_name(name, &self.conn_id)
2140            .map(|item| convert::identity::<&dyn SqlCatalogItem>(item))
2141    }
2142
2143    fn get_type_by_name(&self, name: &QualifiedItemName) -> Option<&dyn SqlCatalogItem> {
2144        self.state
2145            .get_type_by_name(name, &self.conn_id)
2146            .map(|item| convert::identity::<&dyn SqlCatalogItem>(item))
2147    }
2148
2149    fn get_cluster(&self, id: ClusterId) -> &dyn mz_sql::catalog::CatalogCluster<'_> {
2150        &self.state.clusters_by_id[&id]
2151    }
2152
2153    fn get_clusters(&self) -> Vec<&dyn mz_sql::catalog::CatalogCluster<'_>> {
2154        self.state
2155            .clusters_by_id
2156            .values()
2157            .map(|cluster| convert::identity::<&dyn mz_sql::catalog::CatalogCluster>(cluster))
2158            .collect()
2159    }
2160
2161    fn get_cluster_replica(
2162        &self,
2163        cluster_id: ClusterId,
2164        replica_id: ReplicaId,
2165    ) -> &dyn mz_sql::catalog::CatalogClusterReplica<'_> {
2166        let cluster = self.get_cluster(cluster_id);
2167        cluster.replica(replica_id)
2168    }
2169
2170    fn get_cluster_replicas(&self) -> Vec<&dyn mz_sql::catalog::CatalogClusterReplica<'_>> {
2171        self.get_clusters()
2172            .into_iter()
2173            .flat_map(|cluster| cluster.replicas().into_iter())
2174            .collect()
2175    }
2176
2177    fn get_system_privileges(&self) -> &PrivilegeMap {
2178        &self.state.system_privileges
2179    }
2180
2181    fn get_default_privileges(
2182        &self,
2183    ) -> Vec<(&DefaultPrivilegeObject, Vec<&DefaultPrivilegeAclItem>)> {
2184        self.state
2185            .default_privileges
2186            .iter()
2187            .map(|(object, acl_items)| (object, acl_items.collect()))
2188            .collect()
2189    }
2190
2191    fn find_available_name(&self, name: QualifiedItemName) -> QualifiedItemName {
2192        self.state.find_available_name(name, &self.conn_id)
2193    }
2194
2195    fn resolve_full_name(&self, name: &QualifiedItemName) -> FullItemName {
2196        self.state.resolve_full_name(name, Some(&self.conn_id))
2197    }
2198
2199    fn resolve_full_schema_name(&self, name: &QualifiedSchemaName) -> FullSchemaName {
2200        self.state.resolve_full_schema_name(name)
2201    }
2202
2203    fn resolve_item_id(&self, global_id: &GlobalId) -> CatalogItemId {
2204        self.state.get_entry_by_global_id(global_id).id()
2205    }
2206
2207    fn resolve_global_id(
2208        &self,
2209        item_id: &CatalogItemId,
2210        version: RelationVersionSelector,
2211    ) -> GlobalId {
2212        self.state
2213            .get_entry(item_id)
2214            .at_version(version)
2215            .global_id()
2216    }
2217
2218    fn config(&self) -> &mz_sql::catalog::CatalogConfig {
2219        self.state.config()
2220    }
2221
2222    fn now(&self) -> EpochMillis {
2223        (self.state.config().now)()
2224    }
2225
2226    fn aws_privatelink_availability_zones(&self) -> Option<BTreeSet<String>> {
2227        self.state.aws_privatelink_availability_zones.clone()
2228    }
2229
2230    fn system_vars(&self) -> &SystemVars {
2231        &self.state.system_configuration
2232    }
2233
2234    fn system_vars_mut(&mut self) -> &mut SystemVars {
2235        Arc::make_mut(&mut self.state.to_mut().system_configuration)
2236    }
2237
2238    fn get_owner_id(&self, id: &ObjectId) -> Option<RoleId> {
2239        self.state().get_owner_id(id, self.conn_id())
2240    }
2241
2242    fn get_privileges(&self, id: &SystemObjectId) -> Option<&PrivilegeMap> {
2243        match id {
2244            SystemObjectId::System => Some(&self.state.system_privileges),
2245            SystemObjectId::Object(ObjectId::Cluster(id)) => {
2246                Some(self.get_cluster(*id).privileges())
2247            }
2248            SystemObjectId::Object(ObjectId::Database(id)) => {
2249                Some(self.get_database(id).privileges())
2250            }
2251            SystemObjectId::Object(ObjectId::Schema((database_spec, schema_spec))) => {
2252                // For temporary schemas that haven't been created yet (lazy creation),
2253                // we return None - the RBAC check will need to handle this case.
2254                self.state
2255                    .try_get_schema(database_spec, schema_spec, &self.conn_id)
2256                    .map(|schema| schema.privileges())
2257            }
2258            SystemObjectId::Object(ObjectId::Item(id)) => Some(self.get_item(id).privileges()),
2259            SystemObjectId::Object(ObjectId::NetworkPolicy(id)) => {
2260                Some(self.get_network_policy(id).privileges())
2261            }
2262            SystemObjectId::Object(ObjectId::ClusterReplica(_))
2263            | SystemObjectId::Object(ObjectId::Role(_)) => None,
2264        }
2265    }
2266
2267    fn object_dependents(&self, ids: &Vec<ObjectId>) -> Vec<ObjectId> {
2268        let mut seen = BTreeSet::new();
2269        self.state.object_dependents(ids, &self.conn_id, &mut seen)
2270    }
2271
2272    fn item_dependents(&self, id: CatalogItemId) -> Vec<ObjectId> {
2273        let mut seen = BTreeSet::new();
2274        self.state.item_dependents(id, &mut seen)
2275    }
2276
2277    fn all_object_privileges(&self, object_type: mz_sql::catalog::SystemObjectType) -> AclMode {
2278        rbac::all_object_privileges(object_type)
2279    }
2280
2281    fn get_object_type(&self, object_id: &ObjectId) -> mz_sql::catalog::ObjectType {
2282        self.state.get_object_type(object_id)
2283    }
2284
2285    fn get_system_object_type(&self, id: &SystemObjectId) -> mz_sql::catalog::SystemObjectType {
2286        self.state.get_system_object_type(id)
2287    }
2288
2289    /// Returns a [`PartialItemName`] with the minimum amount of qualifiers to unambiguously resolve
2290    /// the object.
2291    ///
2292    /// Warning: This is broken for temporary objects. Don't use this function for serious stuff,
2293    /// i.e., don't expect that what you get back is a thing you can resolve. Current usages are
2294    /// only for error msgs and other humanizations.
2295    fn minimal_qualification(&self, qualified_name: &QualifiedItemName) -> PartialItemName {
2296        if qualified_name.qualifiers.schema_spec.is_temporary() {
2297            // All bets are off. Just give up and return the qualified name as is.
2298            // TODO: Figure out what's going on with temporary objects.
2299
2300            // See e.g. `temporary_objects.slt` fail if you comment this out, which has the repro
2301            // from https://github.com/MaterializeInc/database-issues/issues/9973#issuecomment-3646382143
2302            // There is also https://github.com/MaterializeInc/database-issues/issues/9974, for
2303            // which we don't have a simple repro.
2304            return qualified_name.item.clone().into();
2305        }
2306
2307        let database_id = match &qualified_name.qualifiers.database_spec {
2308            ResolvedDatabaseSpecifier::Ambient => None,
2309            ResolvedDatabaseSpecifier::Id(id)
2310                if self.database.is_some() && self.database == Some(*id) =>
2311            {
2312                None
2313            }
2314            ResolvedDatabaseSpecifier::Id(id) => Some(id.clone()),
2315        };
2316
2317        let schema_spec = if database_id.is_none()
2318            && self.resolve_item_name(&PartialItemName {
2319                database: None,
2320                schema: None,
2321                item: qualified_name.item.clone(),
2322            }) == Ok(qualified_name)
2323            || self.resolve_function_name(&PartialItemName {
2324                database: None,
2325                schema: None,
2326                item: qualified_name.item.clone(),
2327            }) == Ok(qualified_name)
2328            || self.resolve_type_name(&PartialItemName {
2329                database: None,
2330                schema: None,
2331                item: qualified_name.item.clone(),
2332            }) == Ok(qualified_name)
2333        {
2334            None
2335        } else {
2336            // If `search_path` does not contain `full_name.schema`, the
2337            // `PartialName` must contain it.
2338            Some(qualified_name.qualifiers.schema_spec.clone())
2339        };
2340
2341        let res = PartialItemName {
2342            database: database_id.map(|id| self.get_database(&id).name().to_string()),
2343            schema: schema_spec.map(|spec| {
2344                self.get_schema(&qualified_name.qualifiers.database_spec, &spec)
2345                    .name()
2346                    .schema
2347                    .clone()
2348            }),
2349            item: qualified_name.item.clone(),
2350        };
2351        assert!(
2352            self.resolve_item_name(&res) == Ok(qualified_name)
2353                || self.resolve_function_name(&res) == Ok(qualified_name)
2354                || self.resolve_type_name(&res) == Ok(qualified_name)
2355        );
2356        res
2357    }
2358
2359    fn add_notice(&self, notice: PlanNotice) {
2360        let _ = self.notices_tx.send(notice.into());
2361    }
2362
2363    fn get_item_comments(&self, id: &CatalogItemId) -> Option<&BTreeMap<Option<usize>, String>> {
2364        let comment_id = self.state.get_comment_id(ObjectId::Item(*id));
2365        self.state.comments.get_object_comments(comment_id)
2366    }
2367
2368    fn is_cluster_size_cc(&self, size: &str) -> bool {
2369        self.state
2370            .cluster_replica_sizes
2371            .0
2372            .get(size)
2373            .map_or(false, |a| a.is_cc)
2374    }
2375}
2376
2377#[cfg(test)]
2378mod tests {
2379    use std::collections::{BTreeMap, BTreeSet};
2380    use std::sync::Arc;
2381    use std::{env, iter};
2382
2383    use itertools::Itertools;
2384    use mz_catalog::memory::objects::CatalogItem;
2385    use mz_postgres_util::{query, sql};
2386    use tokio_postgres::NoTls;
2387    use tokio_postgres::types::Type;
2388    use uuid::Uuid;
2389
2390    use mz_catalog::SYSTEM_CONN_ID;
2391    use mz_catalog::builtin::{BUILTINS, Builtin, BuiltinType};
2392    use mz_catalog::durable::{CatalogError, DurableCatalogError, FenceError, test_bootstrap_args};
2393    use mz_controller_types::{ClusterId, ReplicaId};
2394    use mz_expr::{Eval, MirScalarExpr};
2395    use mz_ore::now::to_datetime;
2396    use mz_ore::{assert_err, assert_ok, soft_assert_eq_or_log, task};
2397    use mz_persist_client::PersistClient;
2398    use mz_pgrepr::oid::{FIRST_MATERIALIZE_OID, FIRST_UNPINNED_OID, FIRST_USER_OID};
2399    use mz_repr::namespaces::{INFORMATION_SCHEMA, PG_CATALOG_SCHEMA};
2400    use mz_repr::role_id::RoleId;
2401    use mz_repr::{
2402        CatalogItemId, Datum, GlobalId, RelationVersionSelector, Row, RowArena, SqlRelationType,
2403        SqlScalarType, Timestamp,
2404    };
2405    use mz_sql::catalog::{CatalogSchema, CatalogType, SessionCatalog};
2406    use mz_sql::func::{Func, FuncImpl, OP_IMPLS, Operation};
2407    use mz_sql::names::{
2408        self, DatabaseId, ItemQualifiers, ObjectId, PartialItemName, QualifiedItemName,
2409        ResolvedDatabaseSpecifier, SchemaId, SchemaSpecifier, SystemObjectId,
2410    };
2411    use mz_sql::plan::{
2412        CoercibleScalarExpr, ExprContext, HirScalarExpr, HirToMirConfig, PlanContext, QueryContext,
2413        QueryLifetime, Scope, StatementContext,
2414    };
2415    use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
2416    use mz_sql::session::vars::{SystemVars, VarInput};
2417
2418    use crate::catalog::state::LocalExpressionCache;
2419    use crate::catalog::{Catalog, Op};
2420    use crate::optimize::dataflows::{EvalTime, ExprPrep, ExprPrepOneShot};
2421    use crate::session::Session;
2422
2423    /// System sessions have an empty `search_path` so it's necessary to
2424    /// schema-qualify all referenced items.
2425    ///
2426    /// Dummy (and ostensibly client) sessions contain system schemas in their
2427    /// search paths, so do not require schema qualification on system objects such
2428    /// as types.
2429    #[mz_ore::test(tokio::test)]
2430    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2431    async fn test_minimal_qualification() {
2432        Catalog::with_debug(|catalog| async move {
2433            struct TestCase {
2434                input: QualifiedItemName,
2435                system_output: PartialItemName,
2436                normal_output: PartialItemName,
2437            }
2438
2439            let test_cases = vec![
2440                TestCase {
2441                    input: QualifiedItemName {
2442                        qualifiers: ItemQualifiers {
2443                            database_spec: ResolvedDatabaseSpecifier::Ambient,
2444                            schema_spec: SchemaSpecifier::Id(catalog.get_pg_catalog_schema_id()),
2445                        },
2446                        item: "numeric".to_string(),
2447                    },
2448                    system_output: PartialItemName {
2449                        database: None,
2450                        schema: None,
2451                        item: "numeric".to_string(),
2452                    },
2453                    normal_output: PartialItemName {
2454                        database: None,
2455                        schema: None,
2456                        item: "numeric".to_string(),
2457                    },
2458                },
2459                TestCase {
2460                    input: QualifiedItemName {
2461                        qualifiers: ItemQualifiers {
2462                            database_spec: ResolvedDatabaseSpecifier::Ambient,
2463                            schema_spec: SchemaSpecifier::Id(catalog.get_mz_catalog_schema_id()),
2464                        },
2465                        item: "mz_array_types".to_string(),
2466                    },
2467                    system_output: PartialItemName {
2468                        database: None,
2469                        schema: None,
2470                        item: "mz_array_types".to_string(),
2471                    },
2472                    normal_output: PartialItemName {
2473                        database: None,
2474                        schema: None,
2475                        item: "mz_array_types".to_string(),
2476                    },
2477                },
2478            ];
2479
2480            for tc in test_cases {
2481                assert_eq!(
2482                    catalog
2483                        .for_system_session()
2484                        .minimal_qualification(&tc.input),
2485                    tc.system_output
2486                );
2487                assert_eq!(
2488                    catalog
2489                        .for_session(&Session::dummy())
2490                        .minimal_qualification(&tc.input),
2491                    tc.normal_output
2492                );
2493            }
2494            catalog.expire().await;
2495        })
2496        .await
2497    }
2498
2499    #[mz_ore::test(tokio::test)]
2500    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2501    async fn test_catalog_revision() {
2502        let persist_client = PersistClient::new_for_tests().await;
2503        let organization_id = Uuid::new_v4();
2504        let bootstrap_args = test_bootstrap_args();
2505        {
2506            let mut catalog = Catalog::open_debug_catalog(
2507                persist_client.clone(),
2508                organization_id.clone(),
2509                &bootstrap_args,
2510            )
2511            .await
2512            .expect("unable to open debug catalog");
2513            assert_eq!(catalog.transient_revision(), 1);
2514            assert!(catalog.transient_revision_is_current());
2515            let snapshot = catalog.clone();
2516            let commit_ts = catalog.current_upper().await;
2517            catalog
2518                .transact(
2519                    None,
2520                    commit_ts,
2521                    None,
2522                    vec![Op::CreateDatabase {
2523                        name: "test".to_string(),
2524                        owner_id: MZ_SYSTEM_ROLE_ID,
2525                    }],
2526                )
2527                .await
2528                .expect("failed to transact");
2529            assert_eq!(catalog.transient_revision(), 2);
2530            assert!(catalog.transient_revision_is_current());
2531            // The pre-transaction snapshot detects its own staleness through
2532            // the shared latest revision.
2533            assert!(!snapshot.transient_revision_is_current());
2534            catalog.expire().await;
2535        }
2536        {
2537            let catalog =
2538                Catalog::open_debug_catalog(persist_client, organization_id, &bootstrap_args)
2539                    .await
2540                    .expect("unable to open debug catalog");
2541            // Re-opening the same catalog resets the transient_revision to 1.
2542            assert_eq!(catalog.transient_revision(), 1);
2543            catalog.expire().await;
2544        }
2545    }
2546
2547    #[mz_ore::test(tokio::test)]
2548    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2549    async fn test_effective_search_path() {
2550        Catalog::with_debug(|catalog| async move {
2551            let mz_catalog_schema = (
2552                ResolvedDatabaseSpecifier::Ambient,
2553                SchemaSpecifier::Id(catalog.state().get_mz_catalog_schema_id()),
2554            );
2555            let pg_catalog_schema = (
2556                ResolvedDatabaseSpecifier::Ambient,
2557                SchemaSpecifier::Id(catalog.state().get_pg_catalog_schema_id()),
2558            );
2559            let mz_temp_schema = (
2560                ResolvedDatabaseSpecifier::Ambient,
2561                SchemaSpecifier::Temporary,
2562            );
2563
2564            // Behavior with the default search_schema (public)
2565            let session = Session::dummy();
2566            let conn_catalog = catalog.for_session(&session);
2567            assert_ne!(
2568                conn_catalog.effective_search_path(false),
2569                conn_catalog.search_path
2570            );
2571            assert_ne!(
2572                conn_catalog.effective_search_path(true),
2573                conn_catalog.search_path
2574            );
2575            assert_eq!(
2576                conn_catalog.effective_search_path(false),
2577                vec![
2578                    mz_catalog_schema.clone(),
2579                    pg_catalog_schema.clone(),
2580                    conn_catalog.search_path[0].clone()
2581                ]
2582            );
2583            assert_eq!(
2584                conn_catalog.effective_search_path(true),
2585                vec![
2586                    mz_temp_schema.clone(),
2587                    mz_catalog_schema.clone(),
2588                    pg_catalog_schema.clone(),
2589                    conn_catalog.search_path[0].clone()
2590                ]
2591            );
2592
2593            // missing schemas are added when missing
2594            let mut session = Session::dummy();
2595            session
2596                .vars_mut()
2597                .set(
2598                    &SystemVars::new(),
2599                    "search_path",
2600                    VarInput::Flat(mz_repr::namespaces::PG_CATALOG_SCHEMA),
2601                    false,
2602                )
2603                .expect("failed to set search_path");
2604            let conn_catalog = catalog.for_session(&session);
2605            assert_ne!(
2606                conn_catalog.effective_search_path(false),
2607                conn_catalog.search_path
2608            );
2609            assert_ne!(
2610                conn_catalog.effective_search_path(true),
2611                conn_catalog.search_path
2612            );
2613            assert_eq!(
2614                conn_catalog.effective_search_path(false),
2615                vec![mz_catalog_schema.clone(), pg_catalog_schema.clone()]
2616            );
2617            assert_eq!(
2618                conn_catalog.effective_search_path(true),
2619                vec![
2620                    mz_temp_schema.clone(),
2621                    mz_catalog_schema.clone(),
2622                    pg_catalog_schema.clone()
2623                ]
2624            );
2625
2626            let mut session = Session::dummy();
2627            session
2628                .vars_mut()
2629                .set(
2630                    &SystemVars::new(),
2631                    "search_path",
2632                    VarInput::Flat(mz_repr::namespaces::MZ_CATALOG_SCHEMA),
2633                    false,
2634                )
2635                .expect("failed to set search_path");
2636            let conn_catalog = catalog.for_session(&session);
2637            assert_ne!(
2638                conn_catalog.effective_search_path(false),
2639                conn_catalog.search_path
2640            );
2641            assert_ne!(
2642                conn_catalog.effective_search_path(true),
2643                conn_catalog.search_path
2644            );
2645            assert_eq!(
2646                conn_catalog.effective_search_path(false),
2647                vec![pg_catalog_schema.clone(), mz_catalog_schema.clone()]
2648            );
2649            assert_eq!(
2650                conn_catalog.effective_search_path(true),
2651                vec![
2652                    mz_temp_schema.clone(),
2653                    pg_catalog_schema.clone(),
2654                    mz_catalog_schema.clone()
2655                ]
2656            );
2657
2658            let mut session = Session::dummy();
2659            session
2660                .vars_mut()
2661                .set(
2662                    &SystemVars::new(),
2663                    "search_path",
2664                    VarInput::Flat(mz_repr::namespaces::MZ_TEMP_SCHEMA),
2665                    false,
2666                )
2667                .expect("failed to set search_path");
2668            let conn_catalog = catalog.for_session(&session);
2669            assert_ne!(
2670                conn_catalog.effective_search_path(false),
2671                conn_catalog.search_path
2672            );
2673            assert_ne!(
2674                conn_catalog.effective_search_path(true),
2675                conn_catalog.search_path
2676            );
2677            assert_eq!(
2678                conn_catalog.effective_search_path(false),
2679                vec![
2680                    mz_catalog_schema.clone(),
2681                    pg_catalog_schema.clone(),
2682                    mz_temp_schema.clone()
2683                ]
2684            );
2685            assert_eq!(
2686                conn_catalog.effective_search_path(true),
2687                vec![mz_catalog_schema, pg_catalog_schema, mz_temp_schema]
2688            );
2689            catalog.expire().await;
2690        })
2691        .await
2692    }
2693
2694    #[mz_ore::test(tokio::test)]
2695    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2696    async fn test_normalized_create() {
2697        use mz_ore::collections::CollectionExt;
2698        Catalog::with_debug(|catalog| async move {
2699            let conn_catalog = catalog.for_system_session();
2700            let scx = &mut StatementContext::new(None, &conn_catalog);
2701
2702            let parsed = mz_sql_parser::parser::parse_statements(
2703                "create view public.foo as select 1 as bar",
2704            )
2705            .expect("")
2706            .into_element()
2707            .ast;
2708
2709            let (stmt, _) = names::resolve(scx.catalog, parsed).expect("");
2710
2711            // Ensure that all identifiers are quoted.
2712            assert_eq!(
2713                r#"CREATE VIEW "materialize"."public"."foo" AS SELECT 1 AS "bar""#,
2714                mz_sql::normalize::create_statement(scx, stmt).expect(""),
2715            );
2716            catalog.expire().await;
2717        })
2718        .await;
2719    }
2720
2721    // Test that if a large catalog item is somehow committed, then we can still load the catalog.
2722    #[mz_ore::test(tokio::test)]
2723    #[cfg_attr(miri, ignore)] // slow
2724    async fn test_large_catalog_item() {
2725        let view_def = "CREATE VIEW \"materialize\".\"public\".\"v\" AS SELECT 1 FROM (SELECT 1";
2726        let column = ", 1";
2727        let view_def_size = view_def.bytes().count();
2728        let column_size = column.bytes().count();
2729        let column_count =
2730            (mz_sql_parser::parser::MAX_STATEMENT_BATCH_SIZE - view_def_size) / column_size + 1;
2731        let columns = iter::repeat(column).take(column_count).join("");
2732        let create_sql = format!("{view_def}{columns})");
2733        let create_sql_check = create_sql.clone();
2734        assert_ok!(mz_sql_parser::parser::parse_statements(&create_sql));
2735        assert_err!(mz_sql_parser::parser::parse_statements_with_limit(
2736            &create_sql
2737        ));
2738
2739        let persist_client = PersistClient::new_for_tests().await;
2740        let organization_id = Uuid::new_v4();
2741        let id = CatalogItemId::User(1);
2742        let gid = GlobalId::User(1);
2743        let bootstrap_args = test_bootstrap_args();
2744        {
2745            let mut catalog = Catalog::open_debug_catalog(
2746                persist_client.clone(),
2747                organization_id.clone(),
2748                &bootstrap_args,
2749            )
2750            .await
2751            .expect("unable to open debug catalog");
2752            let item = catalog
2753                .state()
2754                .deserialize_item(
2755                    gid,
2756                    &create_sql,
2757                    &BTreeMap::new(),
2758                    &mut LocalExpressionCache::Closed,
2759                    None,
2760                )
2761                .expect("unable to parse view");
2762            let commit_ts = catalog.current_upper().await;
2763            catalog
2764                .transact(
2765                    None,
2766                    commit_ts,
2767                    None,
2768                    vec![Op::CreateItem {
2769                        item,
2770                        name: QualifiedItemName {
2771                            qualifiers: ItemQualifiers {
2772                                database_spec: ResolvedDatabaseSpecifier::Id(DatabaseId::User(1)),
2773                                schema_spec: SchemaSpecifier::Id(SchemaId::User(3)),
2774                            },
2775                            item: "v".to_string(),
2776                        },
2777                        id,
2778                        owner_id: MZ_SYSTEM_ROLE_ID,
2779                    }],
2780                )
2781                .await
2782                .expect("failed to transact");
2783            catalog.expire().await;
2784        }
2785        {
2786            let catalog =
2787                Catalog::open_debug_catalog(persist_client, organization_id, &bootstrap_args)
2788                    .await
2789                    .expect("unable to open debug catalog");
2790            let view = catalog.get_entry(&id);
2791            assert_eq!("v", view.name.item);
2792            match &view.item {
2793                CatalogItem::View(view) => assert_eq!(create_sql_check, view.create_sql),
2794                item => panic!("expected view, got {}", item.typ()),
2795            }
2796            catalog.expire().await;
2797        }
2798    }
2799
2800    #[mz_ore::test(tokio::test)]
2801    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2802    async fn test_object_type() {
2803        Catalog::with_debug(|catalog| async move {
2804            let conn_catalog = catalog.for_system_session();
2805
2806            assert_eq!(
2807                mz_sql::catalog::ObjectType::ClusterReplica,
2808                conn_catalog.get_object_type(&ObjectId::ClusterReplica((
2809                    ClusterId::user(1).expect("1 is a valid ID"),
2810                    ReplicaId::User(1)
2811                )))
2812            );
2813            assert_eq!(
2814                mz_sql::catalog::ObjectType::Role,
2815                conn_catalog.get_object_type(&ObjectId::Role(RoleId::User(1)))
2816            );
2817            catalog.expire().await;
2818        })
2819        .await;
2820    }
2821
2822    #[mz_ore::test(tokio::test)]
2823    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2824    async fn test_get_privileges() {
2825        Catalog::with_debug(|catalog| async move {
2826            let conn_catalog = catalog.for_system_session();
2827
2828            assert_eq!(
2829                None,
2830                conn_catalog.get_privileges(&SystemObjectId::Object(ObjectId::ClusterReplica((
2831                    ClusterId::user(1).expect("1 is a valid ID"),
2832                    ReplicaId::User(1),
2833                ))))
2834            );
2835            assert_eq!(
2836                None,
2837                conn_catalog
2838                    .get_privileges(&SystemObjectId::Object(ObjectId::Role(RoleId::User(1))))
2839            );
2840            catalog.expire().await;
2841        })
2842        .await;
2843    }
2844
2845    #[mz_ore::test(tokio::test)]
2846    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2847    async fn verify_builtin_descs() {
2848        Catalog::with_debug(|catalog| async move {
2849            let conn_catalog = catalog.for_system_session();
2850
2851            for builtin in BUILTINS::iter() {
2852                let (schema, name, expected_desc) = match builtin {
2853                    Builtin::Table(t) => (&t.schema, &t.name, &t.desc),
2854                    Builtin::View(v) => (&v.schema, &v.name, &v.desc),
2855                    Builtin::MaterializedView(mv) => (&mv.schema, &mv.name, &mv.desc),
2856                    Builtin::Source(s) => (&s.schema, &s.name, &s.desc),
2857                    Builtin::Log(_)
2858                    | Builtin::Type(_)
2859                    | Builtin::Func(_)
2860                    | Builtin::Index(_)
2861                    | Builtin::Connection(_) => continue,
2862                };
2863                let item = conn_catalog
2864                    .resolve_item(&PartialItemName {
2865                        database: None,
2866                        schema: Some(schema.to_string()),
2867                        item: name.to_string(),
2868                    })
2869                    .expect("unable to resolve item")
2870                    .at_version(RelationVersionSelector::Latest);
2871
2872                let actual_desc = item.relation_desc().expect("invalid item type");
2873                for (index, ((actual_name, actual_typ), (expected_name, expected_typ))) in
2874                    actual_desc.iter().zip_eq(expected_desc.iter()).enumerate()
2875                {
2876                    assert_eq!(
2877                        actual_name, expected_name,
2878                        "item {schema}.{name} column {index} name did not match its expected name"
2879                    );
2880                    assert_eq!(
2881                        actual_typ, expected_typ,
2882                        "item {schema}.{name} column {index} ('{actual_name}') type did not match its expected type"
2883                    );
2884                }
2885                assert_eq!(
2886                    &*actual_desc, expected_desc,
2887                    "item {schema}.{name} did not match its expected RelationDesc"
2888                );
2889            }
2890            catalog.expire().await;
2891        })
2892        .await
2893    }
2894
2895    // Connect to a running Postgres server and verify that our builtin
2896    // types and functions match it, in addition to some other things.
2897    #[mz_ore::test(tokio::test)]
2898    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
2899    async fn test_compare_builtins_postgres() {
2900        async fn inner(catalog: Catalog) {
2901            // Verify that all builtin functions:
2902            // - have a unique OID
2903            // - if they have a postgres counterpart (same oid) then they have matching name
2904            let (client, connection) = tokio_postgres::connect(
2905                &env::var("POSTGRES_URL").unwrap_or_else(|_| "host=localhost user=postgres".into()),
2906                NoTls,
2907            )
2908            .await
2909            .expect("failed to connect to Postgres");
2910
2911            task::spawn(|| "compare_builtin_postgres", async move {
2912                if let Err(e) = connection.await {
2913                    panic!("connection error: {}", e);
2914                }
2915            });
2916
2917            struct PgProc {
2918                name: String,
2919                arg_oids: Vec<u32>,
2920                ret_oid: Option<u32>,
2921                ret_set: bool,
2922            }
2923
2924            struct PgType {
2925                name: String,
2926                ty: String,
2927                elem: u32,
2928                array: u32,
2929                input: u32,
2930                receive: u32,
2931            }
2932
2933            struct PgOper {
2934                oprresult: u32,
2935                name: String,
2936            }
2937
2938            let pg_proc: BTreeMap<_, _> = query(
2939                &client,
2940                sql!(
2941                    "SELECT
2942                    p.oid,
2943                    proname,
2944                    proargtypes,
2945                    prorettype,
2946                    proretset
2947                FROM pg_proc p
2948                JOIN pg_namespace n ON p.pronamespace = n.oid"
2949                ),
2950                &[],
2951            )
2952            .await
2953            .expect("pg query failed")
2954            .into_iter()
2955            .map(|row| {
2956                let oid: u32 = row.get("oid");
2957                let pg_proc = PgProc {
2958                    name: row.get("proname"),
2959                    arg_oids: row.get("proargtypes"),
2960                    ret_oid: row.get("prorettype"),
2961                    ret_set: row.get("proretset"),
2962                };
2963                (oid, pg_proc)
2964            })
2965            .collect();
2966
2967            let pg_type: BTreeMap<_, _> = query(
2968                &client,
2969                sql!(
2970                    "SELECT oid, typname, typtype::text, typelem, typarray, typinput::oid, typreceive::oid as typreceive FROM pg_type"
2971                ),
2972                &[],
2973            )
2974            .await
2975                .expect("pg query failed")
2976                .into_iter()
2977                .map(|row| {
2978                    let oid: u32 = row.get("oid");
2979                    let pg_type = PgType {
2980                        name: row.get("typname"),
2981                        ty: row.get("typtype"),
2982                        elem: row.get("typelem"),
2983                        array: row.get("typarray"),
2984                        input: row.get("typinput"),
2985                        receive: row.get("typreceive"),
2986                    };
2987                    (oid, pg_type)
2988                })
2989                .collect();
2990
2991            let pg_oper: BTreeMap<_, _> = query(
2992                &client,
2993                sql!("SELECT oid, oprname, oprresult FROM pg_operator"),
2994                &[],
2995            )
2996            .await
2997            .expect("pg query failed")
2998            .into_iter()
2999            .map(|row| {
3000                let oid: u32 = row.get("oid");
3001                let pg_oper = PgOper {
3002                    name: row.get("oprname"),
3003                    oprresult: row.get("oprresult"),
3004                };
3005                (oid, pg_oper)
3006            })
3007            .collect();
3008
3009            let conn_catalog = catalog.for_system_session();
3010            let resolve_type_oid = |item: &str| {
3011                conn_catalog
3012                    .resolve_type(&PartialItemName {
3013                        database: None,
3014                        // All functions we check exist in PG, so the types must, as
3015                        // well
3016                        schema: Some(PG_CATALOG_SCHEMA.into()),
3017                        item: item.to_string(),
3018                    })
3019                    .expect("unable to resolve type")
3020                    .oid()
3021            };
3022
3023            let func_oids: BTreeSet<_> = BUILTINS::funcs()
3024                .flat_map(|f| f.inner.func_impls().into_iter().map(|f| f.oid))
3025                .collect();
3026
3027            let mut all_oids = BTreeSet::new();
3028
3029            // A function to determine if two oids are equivalent enough for these tests. We don't
3030            // support some types, so map exceptions here.
3031            let equivalent_types: BTreeSet<(Option<u32>, Option<u32>)> = BTreeSet::from_iter(
3032                [
3033                    // We don't support NAME.
3034                    (Type::NAME, Type::TEXT),
3035                    (Type::NAME_ARRAY, Type::TEXT_ARRAY),
3036                    // We don't support time with time zone.
3037                    (Type::TIME, Type::TIMETZ),
3038                    (Type::TIME_ARRAY, Type::TIMETZ_ARRAY),
3039                ]
3040                .map(|(a, b)| (Some(a.oid()), Some(b.oid()))),
3041            );
3042            let ignore_return_types: BTreeSet<u32> = BTreeSet::from([
3043                1619, // pg_typeof: TODO: We now have regtype and can correctly implement this.
3044            ]);
3045            let is_same_type = |fn_oid: u32, a: Option<u32>, b: Option<u32>| -> bool {
3046                if ignore_return_types.contains(&fn_oid) {
3047                    return true;
3048                }
3049                if equivalent_types.contains(&(a, b)) || equivalent_types.contains(&(b, a)) {
3050                    return true;
3051                }
3052                a == b
3053            };
3054
3055            for builtin in BUILTINS::iter() {
3056                match builtin {
3057                    Builtin::Type(ty) => {
3058                        assert!(all_oids.insert(ty.oid), "{} reused oid {}", ty.name, ty.oid);
3059
3060                        if ty.oid >= FIRST_MATERIALIZE_OID {
3061                            // High OIDs are reserved in Materialize and don't have
3062                            // PostgreSQL counterparts.
3063                            continue;
3064                        }
3065
3066                        // For types that have a PostgreSQL counterpart, verify that
3067                        // the name and oid match.
3068                        let pg_ty = pg_type.get(&ty.oid).unwrap_or_else(|| {
3069                            panic!("pg_proc missing type {}: oid {}", ty.name, ty.oid)
3070                        });
3071                        assert_eq!(
3072                            ty.name, pg_ty.name,
3073                            "oid {} has name {} in postgres; expected {}",
3074                            ty.oid, pg_ty.name, ty.name,
3075                        );
3076
3077                        let (typinput_oid, typreceive_oid) = match &ty.details.pg_metadata {
3078                            None => (0, 0),
3079                            Some(pgmeta) => (pgmeta.typinput_oid, pgmeta.typreceive_oid),
3080                        };
3081                        assert_eq!(
3082                            typinput_oid, pg_ty.input,
3083                            "type {} has typinput OID {:?} in mz but {:?} in pg",
3084                            ty.name, typinput_oid, pg_ty.input,
3085                        );
3086                        assert_eq!(
3087                            typreceive_oid, pg_ty.receive,
3088                            "type {} has typreceive OID {:?} in mz but {:?} in pg",
3089                            ty.name, typreceive_oid, pg_ty.receive,
3090                        );
3091                        if typinput_oid != 0 {
3092                            assert!(
3093                                func_oids.contains(&typinput_oid),
3094                                "type {} has typinput OID {} that does not exist in pg_proc",
3095                                ty.name,
3096                                typinput_oid,
3097                            );
3098                        }
3099                        if typreceive_oid != 0 {
3100                            assert!(
3101                                func_oids.contains(&typreceive_oid),
3102                                "type {} has typreceive OID {} that does not exist in pg_proc",
3103                                ty.name,
3104                                typreceive_oid,
3105                            );
3106                        }
3107
3108                        // Ensure the type matches.
3109                        match &ty.details.typ {
3110                            CatalogType::Array { element_reference } => {
3111                                let elem_ty = BUILTINS::iter()
3112                                    .filter_map(|builtin| match builtin {
3113                                        Builtin::Type(ty @ BuiltinType { name, .. })
3114                                            if element_reference == name =>
3115                                        {
3116                                            Some(ty)
3117                                        }
3118                                        _ => None,
3119                                    })
3120                                    .next();
3121                                let elem_ty = match elem_ty {
3122                                    Some(ty) => ty,
3123                                    None => {
3124                                        panic!("{} is unexpectedly not a type", element_reference)
3125                                    }
3126                                };
3127                                assert_eq!(
3128                                    pg_ty.elem, elem_ty.oid,
3129                                    "type {} has mismatched element OIDs",
3130                                    ty.name
3131                                )
3132                            }
3133                            CatalogType::Pseudo => {
3134                                assert_eq!(
3135                                    pg_ty.ty, "p",
3136                                    "type {} is not a pseudo type as expected",
3137                                    ty.name
3138                                )
3139                            }
3140                            CatalogType::Range { .. } => {
3141                                assert_eq!(
3142                                    pg_ty.ty, "r",
3143                                    "type {} is not a range type as expected",
3144                                    ty.name
3145                                );
3146                            }
3147                            _ => {
3148                                assert_eq!(
3149                                    pg_ty.ty, "b",
3150                                    "type {} is not a base type as expected",
3151                                    ty.name
3152                                )
3153                            }
3154                        }
3155
3156                        // Ensure the array type reference is correct.
3157                        let schema = catalog
3158                            .resolve_schema_in_database(
3159                                &ResolvedDatabaseSpecifier::Ambient,
3160                                ty.schema,
3161                                &SYSTEM_CONN_ID,
3162                            )
3163                            .expect("unable to resolve schema");
3164                        let allocated_type = catalog
3165                            .resolve_type(
3166                                None,
3167                                &vec![(ResolvedDatabaseSpecifier::Ambient, schema.id().clone())],
3168                                &PartialItemName {
3169                                    database: None,
3170                                    schema: Some(schema.name().schema.clone()),
3171                                    item: ty.name.to_string(),
3172                                },
3173                                &SYSTEM_CONN_ID,
3174                            )
3175                            .expect("unable to resolve type");
3176                        let ty = if let CatalogItem::Type(ty) = &allocated_type.item {
3177                            ty
3178                        } else {
3179                            panic!("unexpectedly not a type")
3180                        };
3181                        match ty.details.array_id {
3182                            Some(array_id) => {
3183                                let array_ty = catalog.get_entry(&array_id);
3184                                assert_eq!(
3185                                    pg_ty.array, array_ty.oid,
3186                                    "type {} has mismatched array OIDs",
3187                                    allocated_type.name.item,
3188                                );
3189                            }
3190                            None => assert_eq!(
3191                                pg_ty.array, 0,
3192                                "type {} does not have an array type in mz but does in pg",
3193                                allocated_type.name.item,
3194                            ),
3195                        }
3196                    }
3197                    Builtin::Func(func) => {
3198                        for imp in func.inner.func_impls() {
3199                            assert!(
3200                                all_oids.insert(imp.oid),
3201                                "{} reused oid {}",
3202                                func.name,
3203                                imp.oid
3204                            );
3205
3206                            assert!(
3207                                imp.oid < FIRST_USER_OID,
3208                                "built-in function {} erroneously has OID in user space ({})",
3209                                func.name,
3210                                imp.oid,
3211                            );
3212
3213                            // For functions that have a postgres counterpart, verify that the name and
3214                            // oid match.
3215                            let pg_fn = if imp.oid >= FIRST_UNPINNED_OID {
3216                                continue;
3217                            } else {
3218                                pg_proc.get(&imp.oid).unwrap_or_else(|| {
3219                                    panic!(
3220                                        "pg_proc missing function {}: oid {}",
3221                                        func.name, imp.oid
3222                                    )
3223                                })
3224                            };
3225                            assert_eq!(
3226                                func.name, pg_fn.name,
3227                                "funcs with oid {} don't match names: {} in mz, {} in pg",
3228                                imp.oid, func.name, pg_fn.name
3229                            );
3230
3231                            // Complain, but don't fail, if argument oids don't match.
3232                            // TODO: make these match.
3233                            let imp_arg_oids = imp
3234                                .arg_typs
3235                                .iter()
3236                                .map(|item| resolve_type_oid(item))
3237                                .collect::<Vec<_>>();
3238
3239                            if imp_arg_oids != pg_fn.arg_oids {
3240                                println!(
3241                                    "funcs with oid {} ({}) don't match arguments: {:?} in mz, {:?} in pg",
3242                                    imp.oid, func.name, imp_arg_oids, pg_fn.arg_oids
3243                                );
3244                            }
3245
3246                            let imp_return_oid = imp.return_typ.map(resolve_type_oid);
3247
3248                            assert!(
3249                                is_same_type(imp.oid, imp_return_oid, pg_fn.ret_oid),
3250                                "funcs with oid {} ({}) don't match return types: {:?} in mz, {:?} in pg",
3251                                imp.oid,
3252                                func.name,
3253                                imp_return_oid,
3254                                pg_fn.ret_oid
3255                            );
3256
3257                            assert_eq!(
3258                                imp.return_is_set, pg_fn.ret_set,
3259                                "funcs with oid {} ({}) don't match set-returning value: {:?} in mz, {:?} in pg",
3260                                imp.oid, func.name, imp.return_is_set, pg_fn.ret_set
3261                            );
3262                        }
3263                    }
3264                    _ => (),
3265                }
3266            }
3267
3268            for (op, func) in OP_IMPLS.iter() {
3269                for imp in func.func_impls() {
3270                    assert!(all_oids.insert(imp.oid), "{} reused oid {}", op, imp.oid);
3271
3272                    // For operators that have a postgres counterpart, verify that the name and oid match.
3273                    let pg_op = if imp.oid >= FIRST_UNPINNED_OID {
3274                        continue;
3275                    } else {
3276                        pg_oper.get(&imp.oid).unwrap_or_else(|| {
3277                            panic!("pg_operator missing operator {}: oid {}", op, imp.oid)
3278                        })
3279                    };
3280
3281                    assert_eq!(*op, pg_op.name);
3282
3283                    let imp_return_oid =
3284                        imp.return_typ.map(resolve_type_oid).expect("must have oid");
3285                    if imp_return_oid != pg_op.oprresult {
3286                        panic!(
3287                            "operators with oid {} ({}) don't match return typs: {} in mz, {} in pg",
3288                            imp.oid, op, imp_return_oid, pg_op.oprresult
3289                        );
3290                    }
3291                }
3292            }
3293            catalog.expire().await;
3294        }
3295
3296        Catalog::with_debug(inner).await
3297    }
3298
3299    // Execute all builtin functions with all combinations of arguments from interesting datums.
3300    #[mz_ore::test(tokio::test)]
3301    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
3302    async fn test_smoketest_all_builtins() {
3303        fn inner(catalog: Catalog) -> Vec<mz_ore::task::JoinHandle<()>> {
3304            let catalog = Arc::new(catalog);
3305            let conn_catalog = catalog.for_system_session();
3306
3307            let resolve_type_oid = |item: &str| conn_catalog.state().get_system_type(item).oid();
3308            let mut handles = Vec::new();
3309
3310            // Extracted during planning; always panics when executed.
3311            let ignore_names = BTreeSet::from([
3312                "avg",
3313                "avg_internal_v1",
3314                "bool_and",
3315                "bool_or",
3316                "has_table_privilege", // > 3 s each
3317                "has_type_privilege",  // > 3 s each
3318                "mod",
3319                "mz_panic",
3320                "mz_sleep",
3321                "pow",
3322                "stddev_pop",
3323                "stddev_samp",
3324                "stddev",
3325                "var_pop",
3326                "var_samp",
3327                "variance",
3328            ]);
3329
3330            let fns = BUILTINS::funcs()
3331                .map(|func| (&func.name, func.inner))
3332                .chain(OP_IMPLS.iter());
3333
3334            for (name, func) in fns {
3335                if ignore_names.contains(name) {
3336                    continue;
3337                }
3338                let Func::Scalar(impls) = func else {
3339                    continue;
3340                };
3341
3342                'outer: for imp in impls {
3343                    let details = imp.details();
3344                    let mut styps = Vec::new();
3345                    for item in details.arg_typs.iter() {
3346                        let oid = resolve_type_oid(item);
3347                        let Ok(pgtyp) = mz_pgrepr::Type::from_oid(oid) else {
3348                            continue 'outer;
3349                        };
3350                        styps.push(SqlScalarType::try_from(&pgtyp).expect("must exist"));
3351                    }
3352                    let datums = styps
3353                        .iter()
3354                        .map(|styp| {
3355                            let mut datums = vec![Datum::Null];
3356                            datums.extend(styp.interesting_datums());
3357                            datums
3358                        })
3359                        .collect::<Vec<_>>();
3360                    // Skip nullary fns.
3361                    if datums.is_empty() {
3362                        continue;
3363                    }
3364
3365                    let return_oid = details
3366                        .return_typ
3367                        .map(resolve_type_oid)
3368                        .expect("must exist");
3369                    let return_styp = mz_pgrepr::Type::from_oid(return_oid)
3370                        .ok()
3371                        .map(|typ| SqlScalarType::try_from(&typ).expect("must exist"));
3372
3373                    let mut idxs = vec![0; datums.len()];
3374                    while idxs[0] < datums[0].len() {
3375                        let mut args = Vec::with_capacity(idxs.len());
3376                        for i in 0..(datums.len()) {
3377                            args.push(datums[i][idxs[i]]);
3378                        }
3379
3380                        let op = &imp.op;
3381                        let scalars = args
3382                            .iter()
3383                            .enumerate()
3384                            .map(|(i, datum)| {
3385                                CoercibleScalarExpr::Coerced(HirScalarExpr::literal(
3386                                    datum.clone(),
3387                                    styps[i].clone(),
3388                                ))
3389                            })
3390                            .collect();
3391
3392                        let call_name = format!(
3393                            "{name}({}) (oid: {})",
3394                            args.iter()
3395                                .map(|d| d.to_string())
3396                                .collect::<Vec<_>>()
3397                                .join(", "),
3398                            imp.oid
3399                        );
3400                        let catalog = Arc::clone(&catalog);
3401                        let call_name_fn = call_name.clone();
3402                        let return_styp = return_styp.clone();
3403                        let handle = task::spawn_blocking(
3404                            || call_name,
3405                            move || {
3406                                smoketest_fn(
3407                                    name,
3408                                    call_name_fn,
3409                                    op,
3410                                    imp,
3411                                    args,
3412                                    catalog,
3413                                    scalars,
3414                                    return_styp,
3415                                )
3416                            },
3417                        );
3418                        handles.push(handle);
3419
3420                        // Advance to the next datum combination.
3421                        for i in (0..datums.len()).rev() {
3422                            idxs[i] += 1;
3423                            if idxs[i] >= datums[i].len() {
3424                                if i == 0 {
3425                                    break;
3426                                }
3427                                idxs[i] = 0;
3428                                continue;
3429                            } else {
3430                                break;
3431                            }
3432                        }
3433                    }
3434                }
3435            }
3436            handles
3437        }
3438
3439        let handles = Catalog::with_debug(|catalog| async { inner(catalog) }).await;
3440        for handle in handles {
3441            handle.await;
3442        }
3443    }
3444
3445    fn smoketest_fn(
3446        name: &&str,
3447        call_name: String,
3448        op: &Operation<HirScalarExpr>,
3449        imp: &FuncImpl<HirScalarExpr>,
3450        args: Vec<Datum<'_>>,
3451        catalog: Arc<Catalog>,
3452        scalars: Vec<CoercibleScalarExpr>,
3453        return_styp: Option<SqlScalarType>,
3454    ) {
3455        let conn_catalog = catalog.for_system_session();
3456        let pcx = PlanContext::zero();
3457        let scx = StatementContext::new(Some(&pcx), &conn_catalog);
3458        let qcx = QueryContext::root(&scx, QueryLifetime::OneShot);
3459        let ecx = ExprContext {
3460            qcx: &qcx,
3461            name: "smoketest",
3462            scope: &Scope::empty(),
3463            relation_type: &SqlRelationType::empty(),
3464            allow_aggregates: false,
3465            allow_subqueries: false,
3466            allow_parameters: false,
3467            allow_windows: false,
3468        };
3469        let arena = RowArena::new();
3470        let mut session = Session::dummy();
3471        session
3472            .start_transaction(to_datetime(0), None, None)
3473            .expect("must succeed");
3474        let prep_style = ExprPrepOneShot {
3475            logical_time: EvalTime::Time(Timestamp::MIN),
3476            session: &session,
3477            catalog_state: &catalog.state,
3478        };
3479
3480        // Execute the function as much as possible, ensuring no panics occur, but
3481        // otherwise ignoring eval errors. We also do various other checks.
3482        let res = (op.0)(&ecx, scalars, &imp.params, vec![]);
3483        if let Ok(hir) = res {
3484            let uneliminated_result_row = {
3485                if let HirScalarExpr::CallUnary { func, .. } = &hir
3486                    && func.is_eliminable_cast()
3487                {
3488                    let mut uneliminated_mir = hir
3489                        .clone()
3490                        .lower_uncorrelated(HirToMirConfig {
3491                            enable_cast_elimination: false,
3492                            ..catalog.system_config().into()
3493                        })
3494                        .expect("lowering eliminable cast should always succeed");
3495                    prep_style
3496                        .prep_scalar_expr(&mut uneliminated_mir)
3497                        .expect("must succeed");
3498
3499                    // Pack the row, to avoid lifetime issues with the MIR we lowered here
3500                    uneliminated_mir
3501                        .eval(&[], &arena)
3502                        .ok()
3503                        .map(|datum| Row::pack([datum]))
3504                } else {
3505                    None
3506                }
3507            };
3508
3509            if let Ok(mut mir) = hir.lower_uncorrelated(catalog.system_config()) {
3510                // Populate unmaterialized functions.
3511                prep_style.prep_scalar_expr(&mut mir).expect("must succeed");
3512
3513                if let Ok(eval_result_datum) = mir.eval(&[], &arena) {
3514                    if let Some(return_styp) = return_styp {
3515                        let mir_typ = mir.typ(&[]);
3516                        // MIR type inference should be consistent with the type
3517                        // we get from the catalog.
3518                        soft_assert_eq_or_log!(
3519                            mir_typ.scalar_type,
3520                            (&return_styp).into(),
3521                            "MIR type did not match the catalog type (cast elimination/repr type error)"
3522                        );
3523                        // The following will check not just that the scalar type
3524                        // is ok, but also catches if the function returned a null
3525                        // but the MIR type inference said "non-nullable".
3526                        if !eval_result_datum.is_instance_of(&mir_typ) {
3527                            panic!(
3528                                "{call_name}: expected return type of {return_styp:?}, got {eval_result_datum}"
3529                            );
3530                        }
3531                        // Check the consistency of `is_eliminable_cast`---we should get the same datum either way.
3532                        if let Some(row) = uneliminated_result_row {
3533                            let uneliminated_result_datum = row.unpack_first();
3534                            assert_eq!(
3535                                uneliminated_result_datum, eval_result_datum,
3536                                "datums should not change if cast is eliminable"
3537                            );
3538                        }
3539                        // Check the consistency of `introduces_nulls` and
3540                        // `propagates_nulls` with `MirScalarExpr::typ`.
3541                        if let Some((introduces_nulls, propagates_nulls)) =
3542                            call_introduces_propagates_nulls(&mir)
3543                        {
3544                            if introduces_nulls {
3545                                // If the function introduces_nulls, then the return
3546                                // type should always be nullable, regardless of
3547                                // the nullability of the input types.
3548                                assert!(
3549                                    mir_typ.nullable,
3550                                    "fn named `{}` called on args `{:?}` (lowered to `{}`) yielded mir_typ.nullable: {}",
3551                                    name, args, mir, mir_typ.nullable
3552                                );
3553                            } else {
3554                                let any_input_null = args.iter().any(|arg| arg.is_null());
3555                                if !any_input_null {
3556                                    assert!(
3557                                        !mir_typ.nullable,
3558                                        "fn named `{}` called on args `{:?}` (lowered to `{}`) yielded mir_typ.nullable: {}",
3559                                        name, args, mir, mir_typ.nullable
3560                                    );
3561                                } else if propagates_nulls {
3562                                    // propagates_nulls means the optimizer short-circuits
3563                                    // all-null inputs, so the output must be nullable.
3564                                    assert!(
3565                                        mir_typ.nullable,
3566                                        "fn named `{}` called on args `{:?}` (lowered to `{}`) yielded mir_typ.nullable: {}",
3567                                        name, args, mir, mir_typ.nullable
3568                                    );
3569                                }
3570                                // When propagates_nulls is false, the output may still
3571                                // be nullable if a non-nullable parameter received a null
3572                                // input (per-position null rejection). The is_instance_of
3573                                // check above ensures type consistency.
3574                            }
3575                        }
3576                        // Check that `MirScalarExpr::reduce` yields the same result
3577                        // as the real evaluation.
3578                        let mut reduced = mir.clone();
3579                        reduced.reduce(&[]);
3580                        match reduced {
3581                            MirScalarExpr::Literal(reduce_result, ctyp) => {
3582                                match reduce_result {
3583                                    Ok(reduce_result_row) => {
3584                                        let reduce_result_datum = reduce_result_row.unpack_first();
3585                                        assert_eq!(
3586                                            reduce_result_datum,
3587                                            eval_result_datum,
3588                                            "eval/reduce datum mismatch: fn named `{}` called on args `{:?}` (lowered to `{}`) evaluated to `{}` with typ `{:?}`, but reduced to `{}` with typ `{:?}`",
3589                                            name,
3590                                            args,
3591                                            mir,
3592                                            eval_result_datum,
3593                                            mir_typ.scalar_type,
3594                                            reduce_result_datum,
3595                                            ctyp.scalar_type
3596                                        );
3597                                        // Let's check that the types also match.
3598                                        // (We are not checking nullability here,
3599                                        // because it's ok when we know a more
3600                                        // precise nullability after actually
3601                                        // evaluating a function than before.)
3602                                        assert_eq!(
3603                                            ctyp.scalar_type,
3604                                            mir_typ.scalar_type,
3605                                            "eval/reduce type mismatch: fn named `{}` called on args `{:?}` (lowered to `{}`) evaluated to `{}` with typ `{:?}`, but reduced to `{}` with typ `{:?}`",
3606                                            name,
3607                                            args,
3608                                            mir,
3609                                            eval_result_datum,
3610                                            mir_typ.scalar_type,
3611                                            reduce_result_datum,
3612                                            ctyp.scalar_type
3613                                        );
3614                                    }
3615                                    Err(..) => {} // It's ok, we might have given invalid args to the function
3616                                }
3617                            }
3618                            _ => unreachable!(
3619                                "all args are literals, so should have reduced to a literal"
3620                            ),
3621                        }
3622                    }
3623                }
3624            }
3625        }
3626    }
3627
3628    /// If the given MirScalarExpr
3629    ///  - is a function call, and
3630    ///  - all arguments are literals
3631    /// then it returns whether the called function (introduces_nulls, propagates_nulls).
3632    fn call_introduces_propagates_nulls(mir_func_call: &MirScalarExpr) -> Option<(bool, bool)> {
3633        match mir_func_call {
3634            MirScalarExpr::CallUnary { func, expr } => {
3635                if expr.is_literal() {
3636                    Some((func.introduces_nulls(), func.propagates_nulls()))
3637                } else {
3638                    None
3639                }
3640            }
3641            MirScalarExpr::CallBinary { func, expr1, expr2 } => {
3642                if expr1.is_literal() && expr2.is_literal() {
3643                    Some((func.introduces_nulls(), func.propagates_nulls()))
3644                } else {
3645                    None
3646                }
3647            }
3648            MirScalarExpr::CallVariadic { func, exprs } => {
3649                if exprs.iter().all(|arg| arg.is_literal()) {
3650                    Some((func.introduces_nulls(), func.propagates_nulls()))
3651                } else {
3652                    None
3653                }
3654            }
3655            _ => None,
3656        }
3657    }
3658
3659    // Make sure pg views don't use types that only exist in Materialize.
3660    #[mz_ore::test(tokio::test)]
3661    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
3662    async fn test_pg_views_forbidden_types() {
3663        Catalog::with_debug(|catalog| async move {
3664            let conn_catalog = catalog.for_system_session();
3665
3666            for view in BUILTINS::views().filter(|view| {
3667                view.schema == PG_CATALOG_SCHEMA || view.schema == INFORMATION_SCHEMA
3668            }) {
3669                let item = conn_catalog
3670                    .resolve_item(&PartialItemName {
3671                        database: None,
3672                        schema: Some(view.schema.to_string()),
3673                        item: view.name.to_string(),
3674                    })
3675                    .expect("unable to resolve view")
3676                    // TODO(alter_table)
3677                    .at_version(RelationVersionSelector::Latest);
3678                let full_name = conn_catalog.resolve_full_name(item.name());
3679                let desc = item.relation_desc().expect("invalid item type");
3680                for col_type in desc.iter_types() {
3681                    match &col_type.scalar_type {
3682                        typ @ SqlScalarType::UInt16
3683                        | typ @ SqlScalarType::UInt32
3684                        | typ @ SqlScalarType::UInt64
3685                        | typ @ SqlScalarType::MzTimestamp
3686                        | typ @ SqlScalarType::List { .. }
3687                        | typ @ SqlScalarType::Map { .. }
3688                        | typ @ SqlScalarType::MzAclItem => {
3689                            panic!("{typ:?} type found in {full_name}");
3690                        }
3691                        SqlScalarType::AclItem
3692                        | SqlScalarType::Bool
3693                        | SqlScalarType::Int16
3694                        | SqlScalarType::Int32
3695                        | SqlScalarType::Int64
3696                        | SqlScalarType::Float32
3697                        | SqlScalarType::Float64
3698                        | SqlScalarType::Numeric { .. }
3699                        | SqlScalarType::Date
3700                        | SqlScalarType::Time
3701                        | SqlScalarType::Timestamp { .. }
3702                        | SqlScalarType::TimestampTz { .. }
3703                        | SqlScalarType::Interval
3704                        | SqlScalarType::PgLegacyChar
3705                        | SqlScalarType::Bytes
3706                        | SqlScalarType::String
3707                        | SqlScalarType::Char { .. }
3708                        | SqlScalarType::VarChar { .. }
3709                        | SqlScalarType::Jsonb
3710                        | SqlScalarType::Uuid
3711                        | SqlScalarType::Array(_)
3712                        | SqlScalarType::Record { .. }
3713                        | SqlScalarType::Oid
3714                        | SqlScalarType::RegProc
3715                        | SqlScalarType::RegType
3716                        | SqlScalarType::RegClass
3717                        | SqlScalarType::Int2Vector
3718                        | SqlScalarType::Range { .. }
3719                        | SqlScalarType::PgLegacyName => {}
3720                    }
3721                }
3722            }
3723            catalog.expire().await;
3724        })
3725        .await
3726    }
3727
3728    // Make sure objects reside in the `mz_introspection` schema iff they depend on per-replica
3729    // introspection relations.
3730    #[mz_ore::test(tokio::test)]
3731    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
3732    async fn test_mz_introspection_builtins() {
3733        Catalog::with_debug(|catalog| async move {
3734            let conn_catalog = catalog.for_system_session();
3735
3736            let introspection_schema_id = catalog.get_mz_introspection_schema_id();
3737            let introspection_schema_spec = SchemaSpecifier::Id(introspection_schema_id);
3738
3739            for entry in catalog.entries() {
3740                let schema_spec = entry.name().qualifiers.schema_spec;
3741                let introspection_deps = catalog.introspection_dependencies(entry.id);
3742                if introspection_deps.is_empty() {
3743                    assert!(
3744                        schema_spec != introspection_schema_spec,
3745                        "entry does not depend on introspection sources but is in \
3746                         `mz_introspection`: {}",
3747                        conn_catalog.resolve_full_name(entry.name()),
3748                    );
3749                } else {
3750                    assert!(
3751                        schema_spec == introspection_schema_spec,
3752                        "entry depends on introspection sources but is not in \
3753                         `mz_introspection`: {}",
3754                        conn_catalog.resolve_full_name(entry.name()),
3755                    );
3756                }
3757            }
3758        })
3759        .await
3760    }
3761
3762    #[mz_ore::test(tokio::test)]
3763    #[cfg_attr(miri, ignore)] //  unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
3764    async fn test_multi_subscriber_catalog() {
3765        let persist_client = PersistClient::new_for_tests().await;
3766        let bootstrap_args = test_bootstrap_args();
3767        let organization_id = Uuid::new_v4();
3768        let db_name = "DB";
3769
3770        let mut writer_catalog = Catalog::open_debug_catalog(
3771            persist_client.clone(),
3772            organization_id.clone(),
3773            &bootstrap_args,
3774        )
3775        .await
3776        .expect("open_debug_catalog");
3777        let mut read_only_catalog = Catalog::open_debug_read_only_catalog(
3778            persist_client.clone(),
3779            organization_id.clone(),
3780            &bootstrap_args,
3781        )
3782        .await
3783        .expect("open_debug_read_only_catalog");
3784        assert_err!(writer_catalog.resolve_database(db_name));
3785        assert_err!(read_only_catalog.resolve_database(db_name));
3786
3787        let commit_ts = writer_catalog.current_upper().await;
3788        writer_catalog
3789            .transact(
3790                None,
3791                commit_ts,
3792                None,
3793                vec![Op::CreateDatabase {
3794                    name: db_name.to_string(),
3795                    owner_id: MZ_SYSTEM_ROLE_ID,
3796                }],
3797            )
3798            .await
3799            .expect("failed to transact");
3800
3801        let write_db = writer_catalog
3802            .resolve_database(db_name)
3803            .expect("resolve_database");
3804        read_only_catalog
3805            .sync_to_current_updates()
3806            .await
3807            .expect("sync_to_current_updates");
3808        let read_db = read_only_catalog
3809            .resolve_database(db_name)
3810            .expect("resolve_database");
3811
3812        assert_eq!(write_db, read_db);
3813
3814        let writer_catalog_fencer =
3815            Catalog::open_debug_catalog(persist_client, organization_id, &bootstrap_args)
3816                .await
3817                .expect("open_debug_catalog for fencer");
3818        let fencer_db = writer_catalog_fencer
3819            .resolve_database(db_name)
3820            .expect("resolve_database for fencer");
3821        assert_eq!(fencer_db, read_db);
3822
3823        let write_fence_err = writer_catalog
3824            .sync_to_current_updates()
3825            .await
3826            .expect_err("sync_to_current_updates for fencer");
3827        assert!(matches!(
3828            write_fence_err,
3829            CatalogError::Durable(DurableCatalogError::Fence(FenceError::Epoch { .. }))
3830        ));
3831        let read_fence_err = read_only_catalog
3832            .sync_to_current_updates()
3833            .await
3834            .expect_err("sync_to_current_updates after fencer");
3835        assert!(matches!(
3836            read_fence_err,
3837            CatalogError::Durable(DurableCatalogError::Fence(FenceError::Epoch { .. }))
3838        ));
3839
3840        writer_catalog.expire().await;
3841        read_only_catalog.expire().await;
3842        writer_catalog_fencer.expire().await;
3843    }
3844}