Skip to main content

mz_adapter/catalog/
state.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//! In-memory metadata storage for the coordinator.
11
12use std::borrow::Cow;
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14use std::fmt::Debug;
15use std::sync::Arc;
16use std::sync::LazyLock;
17use std::time::Instant;
18
19use ipnet::IpNet;
20use itertools::Itertools;
21use mz_adapter_types::compaction::CompactionWindow;
22use mz_adapter_types::connection::ConnectionId;
23use mz_audit_log::{EventDetails, EventType, ObjectType, VersionedEvent};
24use mz_build_info::DUMMY_BUILD_INFO;
25use mz_catalog::SYSTEM_CONN_ID;
26use mz_catalog::builtin::{
27    BUILTINS, Builtin, BuiltinCluster, BuiltinLog, BuiltinSource, BuiltinTable, BuiltinType,
28};
29use mz_catalog::config::{AwsPrincipalContext, ClusterReplicaSizeMap};
30use mz_catalog::expr_cache::LocalExpressions;
31use mz_catalog::memory::error::{Error, ErrorKind};
32use mz_catalog::memory::objects::{
33    CatalogCollectionEntry, CatalogEntry, CatalogItem, Cluster, ClusterReplica, CommentsMap,
34    Connection, DataSourceDesc, Database, DefaultPrivileges, Index, MaterializedView, MetricSink,
35    NetworkPolicy, Role, RoleAuth, Schema, Secret, Sink, Source, SourceReferences, Table,
36    TableDataSource, Type, View,
37};
38use mz_controller::clusters::{
39    ManagedReplicaLocation, ReplicaAllocation, ReplicaLocation, UnmanagedReplicaLocation,
40};
41use mz_controller_types::{ClusterId, ReplicaId};
42use mz_expr::{CollectionPlan, OptimizedMirRelationExpr};
43use mz_license_keys::ValidatedLicenseKey;
44use mz_ore::collections::CollectionExt;
45use mz_ore::now::NOW_ZERO;
46use mz_ore::soft_assert_no_log;
47use mz_ore::str::StrExt;
48use mz_pgrepr::oid::INVALID_OID;
49use mz_repr::adt::mz_acl_item::PrivilegeMap;
50use mz_repr::namespaces::{
51    INFORMATION_SCHEMA, MZ_CATALOG_SCHEMA, MZ_CATALOG_UNSTABLE_SCHEMA, MZ_INTERNAL_SCHEMA,
52    MZ_INTROSPECTION_SCHEMA, MZ_TEMP_SCHEMA, MZ_UNSAFE_SCHEMA, PG_CATALOG_SCHEMA, SYSTEM_SCHEMAS,
53    UNSTABLE_SCHEMAS,
54};
55use mz_repr::network_policy_id::NetworkPolicyId;
56use mz_repr::optimize::{OptimizerFeatureOverrides, OptimizerFeatures, OverrideFrom};
57use mz_repr::role_id::RoleId;
58use mz_repr::{
59    CatalogItemId, GlobalId, RelationDesc, RelationVersion, RelationVersionSelector,
60    VersionedRelationDesc,
61};
62use mz_secrets::InMemorySecretsController;
63use mz_sql::ast::Ident;
64use mz_sql::catalog::{
65    CatalogCluster, CatalogClusterReplica, CatalogDatabase, CatalogError as SqlCatalogError,
66    CatalogItem as SqlCatalogItem, CatalogItemType, CatalogRecordField, CatalogRole, CatalogSchema,
67    CatalogType, CatalogTypeDetails, IdReference, NameReference, SessionCatalog, SystemObjectType,
68    TypeReference,
69};
70use mz_sql::catalog::{CatalogConfig, EnvironmentId};
71use mz_sql::names::{
72    CommentObjectId, DatabaseId, DependencyIds, FullItemName, FullSchemaName, ObjectId,
73    PartialItemName, QualifiedItemName, QualifiedSchemaName, RawDatabaseSpecifier,
74    ResolvedDatabaseSpecifier, ResolvedIds, SchemaId, SchemaSpecifier, SystemObjectId,
75};
76use mz_sql::plan::{
77    CreateConnectionPlan, CreateIndexPlan, CreateMaterializedViewPlan, CreateMetricSinkPlan,
78    CreateSecretPlan, CreateSinkPlan, CreateSourcePlan, CreateTablePlan, CreateTypePlan,
79    CreateViewPlan, Params, Plan, PlanContext,
80};
81use mz_sql::rbac;
82use mz_sql::session::metadata::SessionMetadata;
83use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
84use mz_sql::session::vars::{DEFAULT_DATABASE_NAME, SystemVars, Var, VarInput};
85use mz_sql_parser::ast::QualifiedReplica;
86use mz_storage_client::controller::StorageMetadata;
87use mz_storage_types::connections::ConnectionContext;
88use mz_storage_types::connections::inline::{
89    ConnectionResolver, InlinedConnection, IntoInlineConnection,
90};
91use mz_transform::notice::OptimizerNotice;
92use serde::Serialize;
93use timely::progress::Antichain;
94use tokio::sync::mpsc;
95use tracing::{debug, warn};
96use uuid::Uuid;
97
98// DO NOT add any more imports from `crate` outside of `crate::catalog`.
99use crate::AdapterError;
100use crate::catalog::{Catalog, ConnCatalog};
101use crate::config::ScopedParameters;
102use crate::coord::{ConnMeta, infer_sql_type_for_catalog};
103use crate::optimize::{self, Optimize, OptimizerCatalog};
104use crate::session::Session;
105
106/// The in-memory representation of the Catalog. This struct is not directly used to persist
107/// metadata to persistent storage. For persistent metadata see
108/// [`mz_catalog::durable::DurableCatalogState`].
109///
110/// [`Serialize`] is implemented to create human readable dumps of the in-memory state, not for
111/// storing the contents of this struct on disk.
112#[derive(Debug, Clone, Serialize)]
113pub struct CatalogState {
114    // State derived from the durable catalog. These fields should only be mutated in `open.rs` or
115    // `apply.rs`. Some of these fields are not 100% derived from the durable catalog. Those
116    // include:
117    //  - Temporary items.
118    //  - Certain objects are partially derived from read-only state.
119    pub(super) database_by_name: imbl::OrdMap<String, DatabaseId>,
120    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
121    pub(super) database_by_id: imbl::OrdMap<DatabaseId, Database>,
122    #[serde(serialize_with = "skip_temp_items")]
123    pub(super) entry_by_id: imbl::OrdMap<CatalogItemId, CatalogEntry>,
124    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
125    pub(super) entry_by_global_id: imbl::OrdMap<GlobalId, CatalogItemId>,
126    pub(super) ambient_schemas_by_name: imbl::OrdMap<String, SchemaId>,
127    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
128    pub(super) ambient_schemas_by_id: imbl::OrdMap<SchemaId, Schema>,
129    pub(super) clusters_by_name: imbl::OrdMap<String, ClusterId>,
130    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
131    pub(super) clusters_by_id: imbl::OrdMap<ClusterId, Cluster>,
132    pub(super) roles_by_name: imbl::OrdMap<String, RoleId>,
133    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
134    pub(super) roles_by_id: imbl::OrdMap<RoleId, Role>,
135    pub(super) network_policies_by_name: imbl::OrdMap<String, NetworkPolicyId>,
136    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
137    pub(super) network_policies_by_id: imbl::OrdMap<NetworkPolicyId, NetworkPolicy>,
138    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
139    pub(super) role_auth_by_id: imbl::OrdMap<RoleId, RoleAuth>,
140
141    #[serde(skip)]
142    pub(super) system_configuration: Arc<SystemVars>,
143    /// In-memory mirror of the durable scoped (per-cluster and per-replica)
144    /// system-parameter cache, maintained by `apply.rs` from the durable
145    /// collections. Resolution reads from here: the optimizer's per-cluster
146    /// feature overrides (`cluster_scoped_optimizer_overrides`) and the
147    /// coordinator's per-replica dyncfg push. See the scoped feature flags
148    /// design. Skipped in the consistency-check snapshot because it is fully
149    /// derived from the durable catalog.
150    #[serde(skip)]
151    pub(super) scoped_system_parameters: ScopedParameters,
152    pub(super) default_privileges: Arc<DefaultPrivileges>,
153    pub(super) system_privileges: Arc<PrivilegeMap>,
154    pub(super) comments: Arc<CommentsMap>,
155    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
156    pub(super) source_references: imbl::OrdMap<CatalogItemId, SourceReferences>,
157    pub(super) storage_metadata: Arc<StorageMetadata>,
158    pub(super) mock_authentication_nonce: Option<String>,
159
160    // Mutable state not derived from the durable catalog. Populated
161    // during dataflow bootstrapping (`bootstrap_dataflow_plans`), which
162    // doesn't run in Testdrive's read-only consistency check, so this
163    // must be `#[serde(skip)]`.
164    #[serde(skip)]
165    pub(super) notices_by_dep_id: imbl::OrdMap<GlobalId, Vec<Arc<OptimizerNotice>>>,
166
167    // Populated by active connections creating temporary objects. The
168    // read-only catalog opened by Testdrive's consistency check has no
169    // active connections, so this must be `#[serde(skip)]`.
170    #[serde(skip)]
171    pub(super) temporary_namespaces: TemporaryNamespaces,
172
173    // Read-only state not derived from the durable catalog.
174    #[serde(skip)]
175    pub(super) config: mz_sql::catalog::CatalogConfig,
176    pub(super) cluster_replica_sizes: ClusterReplicaSizeMap,
177    #[serde(skip)]
178    pub(crate) availability_zones: Vec<String>,
179
180    // Read-only not derived from the durable catalog.
181    #[serde(skip)]
182    pub(super) egress_addresses: Vec<IpNet>,
183    pub(super) aws_principal_context: Option<AwsPrincipalContext>,
184    pub(super) aws_privatelink_availability_zones: Option<BTreeSet<String>>,
185    pub(super) http_host_name: Option<String>,
186
187    // Read-only not derived from the durable catalog.
188    #[serde(skip)]
189    pub(super) license_key: ValidatedLicenseKey,
190}
191
192/// The temporary namespaces of the sessions connected to this process: for
193/// each session that has created a temporary item, the ephemeral-owner
194/// mapping between the session's UUID (stamped on its durable temporary
195/// items) and its connection, together with the session's `mz_temp`
196/// [`Schema`] once it has materialized.
197///
198/// The coordinator registers a namespace at a session's first
199/// temporary-item creation, and unregisters it when the session terminates.
200#[derive(Debug, Clone, Default)]
201pub(super) struct TemporaryNamespaces {
202    by_conn: imbl::OrdMap<ConnectionId, TemporaryNamespace>,
203    // For resolving a durable item's owner UUID to its connection in
204    // the apply path.
205    conns_by_uuid: imbl::OrdMap<Uuid, ConnectionId>,
206}
207
208#[derive(Debug, Clone)]
209struct TemporaryNamespace {
210    uuid: Uuid,
211    // Instantiated by `ensure_schema` at the first applied temporary item
212    schema: Option<Schema>,
213}
214
215impl TemporaryNamespaces {
216    /// Registers `conn_id` as the connection of the session `uuid`.
217    ///
218    /// Callers guard on [`CatalogState::has_temporary_namespace`], so
219    /// registering an already-registered connection is a bug.
220    pub(super) fn register(&mut self, conn_id: ConnectionId, uuid: Uuid) {
221        let prev_conn = self.conns_by_uuid.insert(uuid, conn_id.clone());
222        mz_ore::soft_assert_or_log!(
223            prev_conn.is_none(),
224            "duplicate temporary namespace registration for {uuid}"
225        );
226        let prev_ns = self
227            .by_conn
228            .insert(conn_id, TemporaryNamespace { uuid, schema: None });
229        mz_ore::soft_assert_or_log!(
230            prev_ns.is_none(),
231            "duplicate temporary namespace registration for the connection of {uuid}"
232        );
233    }
234
235    /// Returns `conn_id`'s `mz_temp` schema, creating an empty one owned by
236    /// `owner_id` if one hasn't been instantiated yet.
237    pub(super) fn ensure_schema(
238        &mut self,
239        conn_id: &ConnectionId,
240        owner_id: RoleId,
241    ) -> &mut Schema {
242        let namespace = self
243            .by_conn
244            .get_mut(conn_id)
245            .expect("temporary namespace must be registered before items are applied");
246        namespace.schema.get_or_insert_with(|| {
247            // Temporary schema OIDs are never used, and it's therefore wasteful to go to the
248            // durable catalog to allocate a new OID for every temporary schema. Instead, we give
249            // them all the same invalid OID. This matches the semantics of temporary schema
250            // `GlobalId`s which are all -1.
251            let oid = INVALID_OID;
252            Schema {
253                name: QualifiedSchemaName {
254                    database: ResolvedDatabaseSpecifier::Ambient,
255                    schema: MZ_TEMP_SCHEMA.into(),
256                },
257                id: SchemaSpecifier::Temporary,
258                oid,
259                items: BTreeMap::new(),
260                functions: BTreeMap::new(),
261                types: BTreeMap::new(),
262                owner_id,
263                privileges: PrivilegeMap::from_mz_acl_items(vec![rbac::owner_privilege(
264                    mz_sql::catalog::ObjectType::Schema,
265                    owner_id,
266                )]),
267            }
268        })
269    }
270
271    /// Removes `conn_id`'s temporary namespace.
272    pub(super) fn unregister(&mut self, conn_id: &ConnectionId) {
273        let Some(namespace) = self.by_conn.get(conn_id) else {
274            return;
275        };
276        // A non-empty schema here means temporary items (and
277        // possibly their storage shards) weren't cleaned up, so we surface the
278        // invariant violation and keep the namespace registered.
279        if namespace
280            .schema
281            .as_ref()
282            .is_some_and(|schema| !schema.items.is_empty())
283        {
284            mz_ore::soft_panic_or_log!(
285                "temporary namespace for connection {conn_id} still has items at unregistration"
286            );
287            return;
288        }
289        let uuid = namespace.uuid;
290        self.by_conn.remove(conn_id);
291        self.conns_by_uuid.remove(&uuid);
292    }
293
294    pub(super) fn schema(&self, conn_id: &ConnectionId) -> Option<&Schema> {
295        self.by_conn
296            .get(conn_id)
297            .and_then(|namespace| namespace.schema.as_ref())
298    }
299
300    pub(super) fn schema_mut(&mut self, conn_id: &ConnectionId) -> Option<&mut Schema> {
301        self.by_conn
302            .get_mut(conn_id)
303            .and_then(|namespace| namespace.schema.as_mut())
304    }
305
306    pub(super) fn schemas(&self) -> impl Iterator<Item = &Schema> {
307        self.by_conn
308            .values()
309            .filter_map(|namespace| namespace.schema.as_ref())
310    }
311
312    pub(super) fn conn_for_uuid(&self, uuid: &Uuid) -> Option<&ConnectionId> {
313        self.conns_by_uuid.get(uuid)
314    }
315
316    pub(super) fn uuid_for_conn(&self, conn_id: &ConnectionId) -> Option<Uuid> {
317        self.by_conn.get(conn_id).map(|namespace| namespace.uuid)
318    }
319
320    pub(super) fn contains_conn(&self, conn_id: &ConnectionId) -> bool {
321        self.by_conn.contains_key(conn_id)
322    }
323
324    pub(super) fn contains_uuid(&self, uuid: &Uuid) -> bool {
325        self.conns_by_uuid.contains_key(uuid)
326    }
327}
328
329/// Keeps track of what expressions are cached or not during startup.
330/// It's also used during catalog transactions to avoid re-optimizing CREATE VIEW / CREATE MAT VIEW
331/// statements when going back and forth between durable catalog operations and in-memory catalog
332/// operations.
333#[derive(Debug, Clone, Serialize)]
334pub(crate) enum LocalExpressionCache {
335    /// The cache is being used.
336    Open {
337        /// The local expressions that were cached in the expression cache.
338        cached_exprs: BTreeMap<GlobalId, LocalExpressions>,
339        /// The local expressions that were NOT cached in the expression cache.
340        uncached_exprs: BTreeMap<GlobalId, LocalExpressions>,
341    },
342    /// The cache is not being used.
343    Closed,
344}
345
346impl LocalExpressionCache {
347    pub(super) fn new(cached_exprs: BTreeMap<GlobalId, LocalExpressions>) -> Self {
348        Self::Open {
349            cached_exprs,
350            uncached_exprs: BTreeMap::new(),
351        }
352    }
353
354    pub(super) fn remove_cached_expression(&mut self, id: &GlobalId) -> Option<LocalExpressions> {
355        match self {
356            LocalExpressionCache::Open { cached_exprs, .. } => cached_exprs.remove(id),
357            LocalExpressionCache::Closed => None,
358        }
359    }
360
361    /// Insert an expression that was cached, back into the cache. This is generally needed when
362    /// parsing/planning an expression fails, but we don't want to lose the cached expression.
363    pub(super) fn insert_cached_expression(
364        &mut self,
365        id: GlobalId,
366        local_expressions: LocalExpressions,
367    ) {
368        match self {
369            LocalExpressionCache::Open { cached_exprs, .. } => {
370                cached_exprs.insert(id, local_expressions);
371            }
372            LocalExpressionCache::Closed => {}
373        }
374    }
375
376    /// Inform the cache that `id` was not found in the cache and that we should add it as
377    /// `local_mir` and `optimizer_features`.
378    pub(super) fn insert_uncached_expression(
379        &mut self,
380        id: GlobalId,
381        local_mir: OptimizedMirRelationExpr,
382        optimizer_features: OptimizerFeatures,
383    ) {
384        match self {
385            LocalExpressionCache::Open { uncached_exprs, .. } => {
386                let local_expr = LocalExpressions {
387                    local_mir,
388                    optimizer_features,
389                };
390                // If we are trying to cache the same item a second time, with a different
391                // expression, then we must be migrating the object or doing something else weird.
392                // Caching the unmigrated expression may cause us to incorrectly use the unmigrated
393                // version after a restart. Caching the migrated version may cause us to incorrectly
394                // think that the object has already been migrated. To simplify things, we cache
395                // neither.
396                let prev = uncached_exprs.remove(&id);
397                match prev {
398                    Some(prev) if prev == local_expr => {
399                        uncached_exprs.insert(id, local_expr);
400                    }
401                    None => {
402                        uncached_exprs.insert(id, local_expr);
403                    }
404                    Some(_) => {}
405                }
406            }
407            LocalExpressionCache::Closed => {}
408        }
409    }
410
411    pub(super) fn into_uncached_exprs(self) -> BTreeMap<GlobalId, LocalExpressions> {
412        match self {
413            LocalExpressionCache::Open { uncached_exprs, .. } => uncached_exprs,
414            LocalExpressionCache::Closed => BTreeMap::new(),
415        }
416    }
417}
418
419fn skip_temp_items<S>(
420    entries: &imbl::OrdMap<CatalogItemId, CatalogEntry>,
421    serializer: S,
422) -> Result<S::Ok, S::Error>
423where
424    S: serde::Serializer,
425{
426    mz_ore::serde::map_key_to_string(
427        entries.iter().filter(|(_k, v)| v.conn_id().is_none()),
428        serializer,
429    )
430}
431
432impl CatalogState {
433    /// Returns an empty [`CatalogState`] that can be used in tests.
434    // TODO: Ideally we'd mark this as `#[cfg(test)]`, but that doesn't work with the way
435    // tests are structured in this repository.
436    pub fn empty_test() -> Self {
437        CatalogState {
438            database_by_name: Default::default(),
439            database_by_id: Default::default(),
440            entry_by_id: Default::default(),
441            entry_by_global_id: Default::default(),
442            notices_by_dep_id: Default::default(),
443            ambient_schemas_by_name: Default::default(),
444            ambient_schemas_by_id: Default::default(),
445            temporary_namespaces: Default::default(),
446            clusters_by_id: Default::default(),
447            clusters_by_name: Default::default(),
448            network_policies_by_name: Default::default(),
449            roles_by_name: Default::default(),
450            roles_by_id: Default::default(),
451            network_policies_by_id: Default::default(),
452            role_auth_by_id: Default::default(),
453            config: CatalogConfig {
454                start_time: Default::default(),
455                start_instant: Instant::now(),
456                nonce: Default::default(),
457                environment_id: EnvironmentId::for_tests(),
458                session_id: Default::default(),
459                build_info: &DUMMY_BUILD_INFO,
460                now: NOW_ZERO.clone(),
461                connection_context: ConnectionContext::for_tests(Arc::new(
462                    InMemorySecretsController::new(),
463                )),
464                aws_account_id: None,
465                helm_chart_version: None,
466            },
467            cluster_replica_sizes: ClusterReplicaSizeMap::for_tests(),
468            availability_zones: Default::default(),
469            system_configuration: Arc::new(SystemVars::default()),
470            scoped_system_parameters: Default::default(),
471            egress_addresses: Default::default(),
472            aws_principal_context: Default::default(),
473            aws_privatelink_availability_zones: Default::default(),
474            http_host_name: Default::default(),
475            default_privileges: Arc::new(DefaultPrivileges::default()),
476            system_privileges: Arc::new(PrivilegeMap::default()),
477            comments: Arc::new(CommentsMap::default()),
478            source_references: Default::default(),
479            storage_metadata: Arc::new(StorageMetadata::default()),
480            license_key: ValidatedLicenseKey::for_tests(),
481            mock_authentication_nonce: Default::default(),
482        }
483    }
484
485    pub fn for_session<'a>(&'a self, session: &'a Session) -> ConnCatalog<'a> {
486        let search_path = self.resolve_search_path(session);
487        let database = self
488            .database_by_name
489            .get(session.vars().database())
490            .map(|id| id.clone());
491        let state = match session.transaction().catalog_state() {
492            Some(txn_catalog_state) => Cow::Borrowed(txn_catalog_state),
493            None => Cow::Borrowed(self),
494        };
495        ConnCatalog {
496            state,
497            unresolvable_ids: BTreeSet::new(),
498            conn_id: session.conn_id().clone(),
499            cluster: session.vars().cluster().into(),
500            database,
501            search_path,
502            role_id: session.current_role_id().clone(),
503            prepared_statements: Some(session.prepared_statements()),
504            portals: Some(session.portals()),
505            notices_tx: session.retain_notice_transmitter(),
506            restrict_to_user_objects: session.vars().restrict_to_user_objects(),
507        }
508    }
509
510    pub fn for_sessionless_user(&self, role_id: RoleId) -> ConnCatalog<'_> {
511        let (notices_tx, _notices_rx) = mpsc::unbounded_channel();
512        let cluster = self.system_configuration.default_cluster();
513
514        ConnCatalog {
515            state: Cow::Borrowed(self),
516            unresolvable_ids: BTreeSet::new(),
517            conn_id: SYSTEM_CONN_ID.clone(),
518            cluster,
519            database: self
520                .resolve_database(DEFAULT_DATABASE_NAME)
521                .ok()
522                .map(|db| db.id()),
523            // Leaving the system's search path empty allows us to catch issues
524            // where catalog object names have not been normalized correctly.
525            search_path: Vec::new(),
526            role_id,
527            prepared_statements: None,
528            portals: None,
529            notices_tx,
530            restrict_to_user_objects: false,
531        }
532    }
533
534    pub fn for_system_session(&self) -> ConnCatalog<'_> {
535        self.for_sessionless_user(MZ_SYSTEM_ROLE_ID)
536    }
537
538    /// Returns an iterator over the deduplicated identifiers of all
539    /// objects this catalog entry transitively depends on (where
540    /// "depends on" is meant in the sense of [`CatalogItem::uses`], rather than
541    /// [`CatalogItem::references`]).
542    pub fn transitive_uses(&self, id: CatalogItemId) -> impl Iterator<Item = CatalogItemId> + '_ {
543        struct I<'a> {
544            queue: VecDeque<CatalogItemId>,
545            seen: BTreeSet<CatalogItemId>,
546            this: &'a CatalogState,
547        }
548        impl<'a> Iterator for I<'a> {
549            type Item = CatalogItemId;
550            fn next(&mut self) -> Option<Self::Item> {
551                if let Some(next) = self.queue.pop_front() {
552                    for child in self.this.get_entry(&next).item().uses() {
553                        if !self.seen.contains(&child) {
554                            self.queue.push_back(child);
555                            self.seen.insert(child);
556                        }
557                    }
558                    Some(next)
559                } else {
560                    None
561                }
562            }
563        }
564
565        I {
566            queue: [id].into_iter().collect(),
567            seen: [id].into_iter().collect(),
568            this: self,
569        }
570    }
571
572    /// Computes the IDs of any log sources this catalog entry transitively
573    /// depends on.
574    pub fn introspection_dependencies(&self, id: CatalogItemId) -> Vec<CatalogItemId> {
575        let mut out = Vec::new();
576
577        // Iterative worklist traversal rather than recursion. The dependency
578        // chain is user controlled and can be arbitrarily deep.
579        let mut queue: VecDeque<CatalogItemId> = [id].into_iter().collect();
580        let mut seen: BTreeSet<CatalogItemId> = [id].into_iter().collect();
581        while let Some(id) = queue.pop_front() {
582            match self.get_entry(&id).item() {
583                CatalogItem::Log(_) => out.push(id),
584                item @ (CatalogItem::View(_)
585                | CatalogItem::MaterializedView(_)
586                | CatalogItem::Connection(_)) => {
587                    // TODO Unclear if this table wants to include all uses or only references.
588                    for item_id in item.references().items() {
589                        if seen.insert(*item_id) {
590                            queue.push_back(*item_id);
591                        }
592                    }
593                }
594                CatalogItem::Sink(sink) => {
595                    let from_item_id = self.get_entry_by_global_id(&sink.from).id();
596                    if seen.insert(from_item_id) {
597                        queue.push_back(from_item_id);
598                    }
599                }
600                CatalogItem::MetricSink(metric_sink) => {
601                    let from_item_id = self.get_entry_by_global_id(&metric_sink.from).id();
602                    if seen.insert(from_item_id) {
603                        queue.push_back(from_item_id);
604                    }
605                }
606                CatalogItem::Index(idx) => {
607                    let on_item_id = self.get_entry_by_global_id(&idx.on).id();
608                    if seen.insert(on_item_id) {
609                        queue.push_back(on_item_id);
610                    }
611                }
612                CatalogItem::Table(_)
613                | CatalogItem::Source(_)
614                | CatalogItem::Type(_)
615                | CatalogItem::Func(_)
616                | CatalogItem::Secret(_) => (),
617            }
618        }
619
620        out
621    }
622
623    /// Returns all the IDs of all objects that depend on `ids`, including `ids` themselves.
624    ///
625    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
626    /// earlier in the list than the roots. This is particularly useful for the order to drop
627    /// objects.
628    pub(super) fn object_dependents(
629        &self,
630        object_ids: &Vec<ObjectId>,
631        conn_id: &ConnectionId,
632        seen: &mut BTreeSet<ObjectId>,
633    ) -> Vec<ObjectId> {
634        let mut dependents = Vec::new();
635        for object_id in object_ids {
636            match object_id {
637                ObjectId::Cluster(id) => {
638                    dependents.extend_from_slice(&self.cluster_dependents(*id, seen));
639                }
640                ObjectId::ClusterReplica((cluster_id, replica_id)) => dependents.extend_from_slice(
641                    &self.cluster_replica_dependents(*cluster_id, *replica_id, seen),
642                ),
643                ObjectId::Database(id) => {
644                    dependents.extend_from_slice(&self.database_dependents(*id, conn_id, seen))
645                }
646                ObjectId::Schema((database_spec, schema_spec)) => {
647                    dependents.extend_from_slice(&self.schema_dependents(
648                        database_spec.clone(),
649                        schema_spec.clone(),
650                        conn_id,
651                        seen,
652                    ));
653                }
654                ObjectId::NetworkPolicy(id) => {
655                    dependents.extend_from_slice(&self.network_policy_dependents(*id, seen));
656                }
657                id @ ObjectId::Role(_) => {
658                    let unseen = seen.insert(id.clone());
659                    if unseen {
660                        dependents.push(id.clone());
661                    }
662                }
663                ObjectId::Item(id) => {
664                    dependents.extend_from_slice(&self.item_dependents(*id, seen))
665                }
666            }
667        }
668        dependents
669    }
670
671    /// Returns all the IDs of all objects that depend on `cluster_id`, including `cluster_id`
672    /// itself.
673    ///
674    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
675    /// earlier in the list than the roots. This is particularly useful for the order to drop
676    /// objects.
677    fn cluster_dependents(
678        &self,
679        cluster_id: ClusterId,
680        seen: &mut BTreeSet<ObjectId>,
681    ) -> Vec<ObjectId> {
682        let mut dependents = Vec::new();
683        let object_id = ObjectId::Cluster(cluster_id);
684        if !seen.contains(&object_id) {
685            seen.insert(object_id.clone());
686            let cluster = self.get_cluster(cluster_id);
687            for item_id in cluster.bound_objects() {
688                dependents.extend_from_slice(&self.item_dependents(*item_id, seen));
689            }
690            for replica_id in cluster.replica_ids().values() {
691                dependents.extend_from_slice(&self.cluster_replica_dependents(
692                    cluster_id,
693                    *replica_id,
694                    seen,
695                ));
696            }
697            dependents.push(object_id);
698        }
699        dependents
700    }
701
702    /// Returns all the IDs of all objects that depend on `replica_id`, including `replica_id`
703    /// itself.
704    ///
705    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
706    /// earlier in the list than the roots. This is particularly useful for the order to drop
707    /// objects.
708    pub(super) fn cluster_replica_dependents(
709        &self,
710        cluster_id: ClusterId,
711        replica_id: ReplicaId,
712        seen: &mut BTreeSet<ObjectId>,
713    ) -> Vec<ObjectId> {
714        let mut dependents = Vec::new();
715        let object_id = ObjectId::ClusterReplica((cluster_id, replica_id));
716        if !seen.contains(&object_id) {
717            seen.insert(object_id.clone());
718            // Materialized views that target this replica are implicitly
719            // dropped with it, so cascade to their dependents to avoid leaving
720            // dangling references.
721            let cluster = self.get_cluster(cluster_id);
722            for item_id in cluster.bound_objects() {
723                if let CatalogItem::MaterializedView(mv) = self.get_entry(item_id).item()
724                    && mv.target_replica == Some(replica_id)
725                {
726                    dependents.extend_from_slice(&self.item_dependents(*item_id, seen));
727                }
728            }
729            dependents.push(object_id);
730        }
731        dependents
732    }
733
734    /// Returns all the IDs of all objects that depend on `database_id`, including `database_id`
735    /// itself.
736    ///
737    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
738    /// earlier in the list than the roots. This is particularly useful for the order to drop
739    /// objects.
740    fn database_dependents(
741        &self,
742        database_id: DatabaseId,
743        conn_id: &ConnectionId,
744        seen: &mut BTreeSet<ObjectId>,
745    ) -> Vec<ObjectId> {
746        let mut dependents = Vec::new();
747        let object_id = ObjectId::Database(database_id);
748        if !seen.contains(&object_id) {
749            seen.insert(object_id.clone());
750            let database = self.get_database(&database_id);
751            for schema_id in database.schema_ids().values() {
752                dependents.extend_from_slice(&self.schema_dependents(
753                    ResolvedDatabaseSpecifier::Id(database_id),
754                    SchemaSpecifier::Id(*schema_id),
755                    conn_id,
756                    seen,
757                ));
758            }
759            dependents.push(object_id);
760        }
761        dependents
762    }
763
764    /// Returns all the IDs of all objects that depend on `schema_id`, including `schema_id`
765    /// itself.
766    ///
767    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
768    /// earlier in the list than the roots. This is particularly useful for the order to drop
769    /// objects.
770    fn schema_dependents(
771        &self,
772        database_spec: ResolvedDatabaseSpecifier,
773        schema_spec: SchemaSpecifier,
774        conn_id: &ConnectionId,
775        seen: &mut BTreeSet<ObjectId>,
776    ) -> Vec<ObjectId> {
777        let mut dependents = Vec::new();
778        let object_id = ObjectId::Schema((database_spec, schema_spec.clone()));
779        if !seen.contains(&object_id) {
780            seen.insert(object_id.clone());
781            let schema = self.get_schema(&database_spec, &schema_spec, conn_id);
782            for item_id in schema.item_ids() {
783                dependents.extend_from_slice(&self.item_dependents(item_id, seen));
784            }
785            dependents.push(object_id)
786        }
787        dependents
788    }
789
790    /// Returns all the IDs of all objects that depend on `item_id`, including `item_id`
791    /// itself.
792    ///
793    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
794    /// earlier in the list than the roots. This is particularly useful for the order to drop
795    /// objects.
796    pub(super) fn item_dependents(
797        &self,
798        item_id: CatalogItemId,
799        seen: &mut BTreeSet<ObjectId>,
800    ) -> Vec<ObjectId> {
801        let mut dependents = Vec::new();
802
803        // Iterative post-order traversal rather than recursion. Dependency
804        // chains are user controlled and can be arbitrarily deep (e.g. a long
805        // chain of stacked views), so recursing risks a stack overflow.
806        enum Work {
807            Enter(CatalogItemId),
808            Emit(ObjectId),
809        }
810        let mut stack = vec![Work::Enter(item_id)];
811        while let Some(work) = stack.pop() {
812            match work {
813                Work::Enter(item_id) => {
814                    let object_id = ObjectId::Item(item_id);
815                    if !seen.insert(object_id.clone()) {
816                        continue;
817                    }
818                    let entry = self.get_entry(&item_id);
819                    // Pushed in reverse of the desired output order: dependents
820                    // first, then self, then the progress collection. We treat
821                    // the progress collection as if it depends on the source
822                    // for dropping. We have additional code in planning to
823                    // create a kind of special-case "CASCADE" for this
824                    // dependency.
825                    if let Some(progress_id) = entry.progress_id() {
826                        stack.push(Work::Enter(progress_id));
827                    }
828                    stack.push(Work::Emit(object_id));
829                    for dependent_id in entry.used_by().iter().rev() {
830                        stack.push(Work::Enter(*dependent_id));
831                    }
832                }
833                Work::Emit(object_id) => dependents.push(object_id),
834            }
835        }
836
837        dependents
838    }
839
840    /// Returns all the IDs of all objects that depend on `network_policy_id`, including `network_policy_id`
841    /// itself.
842    ///
843    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
844    /// earlier in the list than the roots. This is particularly useful for the order to drop
845    /// objects.
846    pub(super) fn network_policy_dependents(
847        &self,
848        network_policy_id: NetworkPolicyId,
849        _seen: &mut BTreeSet<ObjectId>,
850    ) -> Vec<ObjectId> {
851        let object_id = ObjectId::NetworkPolicy(network_policy_id);
852        // Currently network policies have no dependents
853        // when we add the ability for users or sources/sinks to have policies
854        // this method will need to be updated.
855        vec![object_id]
856    }
857
858    /// Indicates whether the indicated item is considered stable or not.
859    ///
860    /// Only stable items can be used as dependencies of other catalog items.
861    fn is_stable(&self, id: CatalogItemId) -> bool {
862        let spec = self.get_entry(&id).name().qualifiers.schema_spec;
863        !self.is_unstable_schema_specifier(spec)
864    }
865
866    pub(super) fn check_unstable_dependencies(&self, item: &CatalogItem) -> Result<(), Error> {
867        if self.system_config().unsafe_enable_unstable_dependencies() {
868            return Ok(());
869        }
870
871        let unstable_dependencies: Vec<_> = item
872            .references()
873            .items()
874            .filter(|id| !self.is_stable(**id))
875            .map(|id| self.get_entry(id).name().item.clone())
876            .collect();
877
878        // It's okay to create a temporary object with unstable
879        // dependencies, since we will never need to reboot a catalog
880        // that contains it.
881        if unstable_dependencies.is_empty() || item.is_temporary() {
882            Ok(())
883        } else {
884            let object_type = item.typ().to_string();
885            Err(Error {
886                kind: ErrorKind::UnstableDependency {
887                    object_type,
888                    unstable_dependencies,
889                },
890            })
891        }
892    }
893
894    pub fn resolve_full_name(
895        &self,
896        name: &QualifiedItemName,
897        conn_id: Option<&ConnectionId>,
898    ) -> FullItemName {
899        let conn_id = conn_id.unwrap_or(&SYSTEM_CONN_ID);
900
901        let database = match &name.qualifiers.database_spec {
902            ResolvedDatabaseSpecifier::Ambient => RawDatabaseSpecifier::Ambient,
903            ResolvedDatabaseSpecifier::Id(id) => {
904                RawDatabaseSpecifier::Name(self.get_database(id).name().to_string())
905            }
906        };
907        // For temporary schemas, we know the name is always MZ_TEMP_SCHEMA,
908        // and the schema may not exist yet if no temporary items have been created.
909        let schema = match &name.qualifiers.schema_spec {
910            SchemaSpecifier::Temporary => MZ_TEMP_SCHEMA.to_string(),
911            SchemaSpecifier::Id(_) => self
912                .get_schema(
913                    &name.qualifiers.database_spec,
914                    &name.qualifiers.schema_spec,
915                    conn_id,
916                )
917                .name()
918                .schema
919                .clone(),
920        };
921        FullItemName {
922            database,
923            schema,
924            item: name.item.clone(),
925        }
926    }
927
928    pub(super) fn resolve_full_schema_name(&self, name: &QualifiedSchemaName) -> FullSchemaName {
929        let database = match &name.database {
930            ResolvedDatabaseSpecifier::Ambient => RawDatabaseSpecifier::Ambient,
931            ResolvedDatabaseSpecifier::Id(id) => {
932                RawDatabaseSpecifier::Name(self.get_database(id).name().to_string())
933            }
934        };
935        FullSchemaName {
936            database,
937            schema: name.schema.clone(),
938        }
939    }
940
941    pub fn get_entry(&self, id: &CatalogItemId) -> &CatalogEntry {
942        self.entry_by_id
943            .get(id)
944            .unwrap_or_else(|| panic!("catalog out of sync, missing id {id:?}"))
945    }
946
947    pub fn get_entry_by_global_id(&self, id: &GlobalId) -> CatalogCollectionEntry {
948        let item_id = self
949            .entry_by_global_id
950            .get(id)
951            .unwrap_or_else(|| panic!("catalog out of sync, missing id {id:?}"));
952
953        let entry = self.get_entry(item_id).clone();
954        let version = match entry.item() {
955            CatalogItem::Table(table) => {
956                let (version, _) = table
957                    .collections
958                    .iter()
959                    .find(|(_verison, gid)| *gid == id)
960                    .expect("version to exist");
961                RelationVersionSelector::Specific(*version)
962            }
963            _ => RelationVersionSelector::Latest,
964        };
965        CatalogCollectionEntry { entry, version }
966    }
967
968    pub fn get_entries(&self) -> impl Iterator<Item = (&CatalogItemId, &CatalogEntry)> + '_ {
969        self.entry_by_id.iter()
970    }
971
972    pub fn get_temp_items(&self, conn: &ConnectionId) -> impl Iterator<Item = ObjectId> + '_ {
973        // A temporary namespace is registered at the connection's first
974        // temporary-item creation, so it's valid for one to not exist yet.
975        self.temporary_namespaces
976            .schema(conn)
977            .into_iter()
978            .flat_map(|schema| schema.items.values().copied().map(ObjectId::from))
979    }
980
981    /// Returns true if a temporary namespace is registered for the given
982    /// connection, i.e. it has (or has had) temporary items.
983    ///
984    /// The namespace is registered at the connection's first temporary-item
985    /// creation, so this returns false for connections that never created
986    /// any temporary objects.
987    pub fn has_temporary_namespace(&self, conn: &ConnectionId) -> bool {
988        self.temporary_namespaces.contains_conn(conn)
989    }
990
991    /// Converts an in-memory catalog entry into its durable representation.
992    ///
993    /// The durable owner of a temporary entry is the session whose connection
994    /// currently holds it, resolved from the temporary namespace
995    /// registered at the session's first temporary-item creation.
996    pub(super) fn durable_item(
997        &self,
998        entry: CatalogEntry,
999    ) -> Result<mz_catalog::durable::Item, AdapterError> {
1000        let ephemeral_owner_session = entry
1001            .conn_id()
1002            .map(|conn_id| {
1003                self.temporary_namespaces
1004                    .uuid_for_conn(conn_id)
1005                    .ok_or_else(|| {
1006                        AdapterError::Internal(format!(
1007                            "no session record for connection {conn_id} owning temporary item"
1008                        ))
1009                    })
1010            })
1011            .transpose()?;
1012        let (create_sql, global_id, extra_versions) = entry.item.into_serialized();
1013        Ok(mz_catalog::durable::Item {
1014            id: entry.id,
1015            oid: entry.oid,
1016            global_id,
1017            schema_id: entry.name.qualifiers.schema_spec.into(),
1018            name: entry.name.item,
1019            create_sql,
1020            owner_id: entry.owner_id,
1021            privileges: entry.privileges.into_all_values().collect(),
1022            extra_versions,
1023            ephemeral_owner_session,
1024        })
1025    }
1026
1027    /// Gets a type named `name` from exactly one of the system schemas.
1028    ///
1029    /// # Panics
1030    /// - If `name` is not an entry in any system schema
1031    /// - If more than one system schema has an entry named `name`.
1032    pub(super) fn get_system_type(&self, name: &str) -> &CatalogEntry {
1033        let mut res = None;
1034        for schema_id in self.system_schema_ids() {
1035            let schema = &self.ambient_schemas_by_id[&schema_id];
1036            if let Some(global_id) = schema.types.get(name) {
1037                match res {
1038                    None => res = Some(self.get_entry(global_id)),
1039                    Some(_) => panic!(
1040                        "only call get_system_type on objects uniquely identifiable in one system schema"
1041                    ),
1042                }
1043            }
1044        }
1045
1046        res.unwrap_or_else(|| panic!("cannot find type {} in system schema", name))
1047    }
1048
1049    pub fn get_item_by_name(
1050        &self,
1051        name: &QualifiedItemName,
1052        conn_id: &ConnectionId,
1053    ) -> Option<&CatalogEntry> {
1054        self.get_schema(
1055            &name.qualifiers.database_spec,
1056            &name.qualifiers.schema_spec,
1057            conn_id,
1058        )
1059        .items
1060        .get(&name.item)
1061        .and_then(|id| self.try_get_entry(id))
1062    }
1063
1064    pub fn get_type_by_name(
1065        &self,
1066        name: &QualifiedItemName,
1067        conn_id: &ConnectionId,
1068    ) -> Option<&CatalogEntry> {
1069        self.get_schema(
1070            &name.qualifiers.database_spec,
1071            &name.qualifiers.schema_spec,
1072            conn_id,
1073        )
1074        .types
1075        .get(&name.item)
1076        .and_then(|id| self.try_get_entry(id))
1077    }
1078
1079    pub(super) fn find_available_name(
1080        &self,
1081        mut name: QualifiedItemName,
1082        conn_id: &ConnectionId,
1083    ) -> QualifiedItemName {
1084        let mut i = 0;
1085        let orig_item_name = name.item.clone();
1086        while self.get_item_by_name(&name, conn_id).is_some() {
1087            i += 1;
1088            name.item = format!("{}{}", orig_item_name, i);
1089        }
1090        name
1091    }
1092
1093    pub fn try_get_entry(&self, id: &CatalogItemId) -> Option<&CatalogEntry> {
1094        self.entry_by_id.get(id)
1095    }
1096
1097    pub fn try_get_entry_by_global_id(&self, id: &GlobalId) -> Option<&CatalogEntry> {
1098        let item_id = self.entry_by_global_id.get(id)?;
1099        self.try_get_entry(item_id)
1100    }
1101
1102    /// Returns the [`RelationDesc`] for a [`GlobalId`], if the provided [`GlobalId`] refers to an
1103    /// object that returns rows.
1104    pub fn try_get_desc_by_global_id(&self, id: &GlobalId) -> Option<Cow<'_, RelationDesc>> {
1105        let entry = self.try_get_entry_by_global_id(id)?;
1106        let desc = match entry.item() {
1107            CatalogItem::Table(table) => Cow::Owned(table.desc_for(id)),
1108            // TODO(alter_table): Support schema evolution on sources.
1109            other => other.relation_desc(RelationVersionSelector::Latest)?,
1110        };
1111        Some(desc)
1112    }
1113
1114    pub(crate) fn get_cluster(&self, cluster_id: ClusterId) -> &Cluster {
1115        self.try_get_cluster(cluster_id)
1116            .unwrap_or_else(|| panic!("unknown cluster {cluster_id}"))
1117    }
1118
1119    pub(super) fn try_get_cluster(&self, cluster_id: ClusterId) -> Option<&Cluster> {
1120        self.clusters_by_id.get(&cluster_id)
1121    }
1122
1123    pub(super) fn try_get_role(&self, id: &RoleId) -> Option<&Role> {
1124        self.roles_by_id.get(id)
1125    }
1126
1127    pub fn get_role(&self, id: &RoleId) -> &Role {
1128        self.roles_by_id.get(id).expect("catalog out of sync")
1129    }
1130
1131    pub fn get_roles(&self) -> impl Iterator<Item = &RoleId> {
1132        self.roles_by_id.keys()
1133    }
1134
1135    pub(super) fn try_get_role_by_name(&self, role_name: &str) -> Option<&Role> {
1136        self.roles_by_name
1137            .get(role_name)
1138            .map(|id| &self.roles_by_id[id])
1139    }
1140
1141    pub(super) fn get_role_auth(&self, id: &RoleId) -> &RoleAuth {
1142        self.role_auth_by_id
1143            .get(id)
1144            .unwrap_or_else(|| panic!("catalog out of sync, missing role auth for {id}"))
1145    }
1146
1147    pub(super) fn try_get_role_auth_by_id(&self, id: &RoleId) -> Option<&RoleAuth> {
1148        self.role_auth_by_id.get(id)
1149    }
1150
1151    pub(super) fn try_get_network_policy_by_name(
1152        &self,
1153        policy_name: &str,
1154    ) -> Option<&NetworkPolicy> {
1155        self.network_policies_by_name
1156            .get(policy_name)
1157            .map(|id| &self.network_policies_by_id[id])
1158    }
1159
1160    pub(crate) fn collect_role_membership(&self, id: &RoleId) -> BTreeSet<RoleId> {
1161        let mut membership = BTreeSet::new();
1162        let mut queue = VecDeque::from(vec![id]);
1163        while let Some(cur_id) = queue.pop_front() {
1164            if !membership.contains(cur_id) {
1165                membership.insert(cur_id.clone());
1166                let role = self.get_role(cur_id);
1167                soft_assert_no_log!(
1168                    !role.membership().keys().contains(id),
1169                    "circular membership exists in the catalog"
1170                );
1171                queue.extend(role.membership().keys());
1172            }
1173        }
1174        membership.insert(RoleId::Public);
1175        membership
1176    }
1177
1178    pub fn get_network_policy(&self, id: &NetworkPolicyId) -> &NetworkPolicy {
1179        self.network_policies_by_id
1180            .get(id)
1181            .expect("catalog out of sync")
1182    }
1183
1184    pub fn get_network_policies(&self) -> impl Iterator<Item = &NetworkPolicyId> {
1185        self.network_policies_by_id.keys()
1186    }
1187
1188    /// Returns the URL for POST-ing data to a webhook source, if `id` corresponds to a webhook
1189    /// source.
1190    ///
1191    /// Note: Identifiers for the source, e.g. item name, are URL encoded.
1192    pub fn try_get_webhook_url(&self, id: &CatalogItemId) -> Option<url::Url> {
1193        let entry = self.try_get_entry(id)?;
1194        // Note: Webhook sources can never be created in the temporary schema, hence passing None.
1195        let name = self.resolve_full_name(entry.name(), None);
1196        let host_name = self
1197            .http_host_name
1198            .as_ref()
1199            .map(|x| x.as_str())
1200            .unwrap_or_else(|| "HOST");
1201
1202        let RawDatabaseSpecifier::Name(database) = name.database else {
1203            return None;
1204        };
1205
1206        let mut url = url::Url::parse(&format!("https://{host_name}/api/webhook")).ok()?;
1207        url.path_segments_mut()
1208            .ok()?
1209            .push(&database)
1210            .push(&name.schema)
1211            .push(&name.item);
1212
1213        Some(url)
1214    }
1215
1216    /// Parses the given SQL string into a pair of [`Plan`] and a [`ResolvedIds`].
1217    ///
1218    /// This function will temporarily enable all "enable_for_item_parsing" feature flags. See
1219    /// [`CatalogState::with_enable_for_item_parsing`] for more details.
1220    ///
1221    /// NOTE: While this method takes a `&mut self`, all mutations are temporary and restored to
1222    /// their original state before the method returns.
1223    pub(crate) fn deserialize_plan_with_enable_for_item_parsing(
1224        // DO NOT add any additional mutations to this method. It would be fairly surprising to the
1225        // caller if this method changed the state of the catalog.
1226        &mut self,
1227        create_sql: &str,
1228        force_if_exists_skip: bool,
1229    ) -> Result<(Plan, ResolvedIds), AdapterError> {
1230        self.with_enable_for_item_parsing(|state| {
1231            let pcx = PlanContext::zero().with_ignore_if_exists_errors(force_if_exists_skip);
1232            let pcx = Some(&pcx);
1233            let session_catalog = state.for_system_session();
1234
1235            let stmt = mz_sql::parse::parse(create_sql)?.into_element().ast;
1236            let (stmt, resolved_ids) = mz_sql::names::resolve(&session_catalog, stmt)?;
1237            let (plan, _sql_impl_ids) =
1238                mz_sql::plan::plan(pcx, &session_catalog, stmt, &Params::empty(), &resolved_ids)?;
1239
1240            Ok((plan, resolved_ids))
1241        })
1242    }
1243
1244    /// Parses the given SQL string into a pair of [`Plan`] and a [`ResolvedIds`].
1245    #[mz_ore::instrument]
1246    pub(crate) fn parse_plan(
1247        create_sql: &str,
1248        pcx: Option<&PlanContext>,
1249        catalog: &ConnCatalog,
1250    ) -> Result<(Plan, ResolvedIds), AdapterError> {
1251        let stmt = mz_sql::parse::parse(create_sql)?.into_element().ast;
1252        let (stmt, resolved_ids) = mz_sql::names::resolve(catalog, stmt)?;
1253        let (plan, _sql_impl_ids) =
1254            mz_sql::plan::plan(pcx, catalog, stmt, &Params::empty(), &resolved_ids)?;
1255
1256        Ok((plan, resolved_ids))
1257    }
1258
1259    /// Parses the given SQL string into a pair of [`CatalogItem`].
1260    pub(crate) fn deserialize_item(
1261        &self,
1262        global_id: GlobalId,
1263        create_sql: &str,
1264        extra_versions: &BTreeMap<RelationVersion, GlobalId>,
1265        local_expression_cache: &mut LocalExpressionCache,
1266        previous_item: Option<CatalogItem>,
1267    ) -> Result<CatalogItem, AdapterError> {
1268        self.parse_item(
1269            global_id,
1270            create_sql,
1271            extra_versions,
1272            None,
1273            false,
1274            None,
1275            local_expression_cache,
1276            previous_item,
1277        )
1278    }
1279
1280    /// Parses the given SQL string into a `CatalogItem`.
1281    #[mz_ore::instrument]
1282    pub(crate) fn parse_item(
1283        &self,
1284        global_id: GlobalId,
1285        create_sql: &str,
1286        extra_versions: &BTreeMap<RelationVersion, GlobalId>,
1287        pcx: Option<&PlanContext>,
1288        is_retained_metrics_object: bool,
1289        custom_logical_compaction_window: Option<CompactionWindow>,
1290        local_expression_cache: &mut LocalExpressionCache,
1291        previous_item: Option<CatalogItem>,
1292    ) -> Result<CatalogItem, AdapterError> {
1293        let cached_expr = local_expression_cache.remove_cached_expression(&global_id);
1294        match self.parse_item_inner(
1295            global_id,
1296            create_sql,
1297            extra_versions,
1298            pcx,
1299            is_retained_metrics_object,
1300            custom_logical_compaction_window,
1301            cached_expr,
1302            previous_item,
1303        ) {
1304            Ok((item, uncached_expr)) => {
1305                if let Some((uncached_expr, optimizer_features)) = uncached_expr {
1306                    local_expression_cache.insert_uncached_expression(
1307                        global_id,
1308                        uncached_expr,
1309                        optimizer_features,
1310                    );
1311                }
1312                Ok(item)
1313            }
1314            Err((err, cached_expr)) => {
1315                if let Some(local_expr) = cached_expr {
1316                    local_expression_cache.insert_cached_expression(global_id, local_expr);
1317                }
1318                Err(err)
1319            }
1320        }
1321    }
1322
1323    /// Parses the given SQL string into a `CatalogItem`, using `cached_expr` if it's Some.
1324    ///
1325    /// On success returns the `CatalogItem` and an optimized expression iff the expression was
1326    /// not cached.
1327    ///
1328    /// On failure returns an error and `cached_expr` so it can be used later.
1329    #[mz_ore::instrument]
1330    pub(crate) fn parse_item_inner(
1331        &self,
1332        global_id: GlobalId,
1333        create_sql: &str,
1334        extra_versions: &BTreeMap<RelationVersion, GlobalId>,
1335        pcx: Option<&PlanContext>,
1336        is_retained_metrics_object: bool,
1337        custom_logical_compaction_window: Option<CompactionWindow>,
1338        cached_expr: Option<LocalExpressions>,
1339        previous_item: Option<CatalogItem>,
1340    ) -> Result<
1341        (
1342            CatalogItem,
1343            Option<(OptimizedMirRelationExpr, OptimizerFeatures)>,
1344        ),
1345        (AdapterError, Option<LocalExpressions>),
1346    > {
1347        let session_catalog = self.for_system_session();
1348
1349        let (plan, resolved_ids) = match Self::parse_plan(create_sql, pcx, &session_catalog) {
1350            Ok((plan, resolved_ids)) => (plan, resolved_ids),
1351            Err(err) => return Err((err, cached_expr)),
1352        };
1353
1354        let mut uncached_expr = None;
1355
1356        // Carry over the plans (`optimized_plan`, `physical_plan`,
1357        // `dataflow_metainfo`) from the previous incarnation of this item when
1358        // re-parsing an existing item (e.g. after a RENAME). These fields live
1359        // on the `CatalogItem` since #35834, but are not reconstructable from
1360        // `create_sql` alone — they are populated by the sequencer `_finish`
1361        // paths at create time, and by the expression-cache / bootstrap
1362        // rendering path on boot. If we don't preserve them here, a RENAME
1363        // silently drops the plans and dataflow metainfo for the affected
1364        // MV/Index/CT.
1365        let previous_plans = previous_item.as_ref().map(|item| {
1366            (
1367                item.optimized_plan().cloned(),
1368                item.physical_plan().cloned(),
1369                item.dataflow_metainfo().cloned(),
1370            )
1371        });
1372
1373        let mut item = match plan {
1374            Plan::CreateTable(CreateTablePlan { table, .. }) => {
1375                let collections = extra_versions
1376                    .iter()
1377                    .map(|(version, gid)| (*version, *gid))
1378                    .chain([(RelationVersion::root(), global_id)].into_iter())
1379                    .collect();
1380
1381                CatalogItem::Table(Table {
1382                    create_sql: Some(table.create_sql),
1383                    desc: table.desc,
1384                    collections,
1385                    conn_id: None,
1386                    resolved_ids,
1387                    custom_logical_compaction_window: custom_logical_compaction_window
1388                        .or(table.compaction_window),
1389                    is_retained_metrics_object,
1390                    data_source: match table.data_source {
1391                        mz_sql::plan::TableDataSource::TableWrites { defaults } => {
1392                            TableDataSource::TableWrites { defaults }
1393                        }
1394                        mz_sql::plan::TableDataSource::DataSource {
1395                            desc: data_source_desc,
1396                            timeline,
1397                        } => match data_source_desc {
1398                            mz_sql::plan::DataSourceDesc::IngestionExport {
1399                                ingestion_id,
1400                                external_reference,
1401                                details,
1402                                data_config,
1403                            } => TableDataSource::DataSource {
1404                                desc: DataSourceDesc::IngestionExport {
1405                                    ingestion_id,
1406                                    external_reference,
1407                                    details,
1408                                    data_config,
1409                                },
1410                                timeline,
1411                            },
1412                            mz_sql::plan::DataSourceDesc::Webhook {
1413                                validate_using,
1414                                body_format,
1415                                headers,
1416                                cluster_id,
1417                            } => TableDataSource::DataSource {
1418                                desc: DataSourceDesc::Webhook {
1419                                    validate_using,
1420                                    body_format,
1421                                    headers,
1422                                    cluster_id: cluster_id
1423                                        .expect("Webhook Tables must have a cluster_id set"),
1424                                },
1425                                timeline,
1426                            },
1427                            _ => {
1428                                return Err((
1429                                    AdapterError::Unstructured(anyhow::anyhow!(
1430                                        "unsupported data source for table"
1431                                    )),
1432                                    cached_expr,
1433                                ));
1434                            }
1435                        },
1436                    },
1437                })
1438            }
1439            Plan::CreateSource(CreateSourcePlan {
1440                source,
1441                timeline,
1442                in_cluster,
1443                ..
1444            }) => CatalogItem::Source(Source {
1445                create_sql: Some(source.create_sql),
1446                data_source: match source.data_source {
1447                    mz_sql::plan::DataSourceDesc::Ingestion(desc) => DataSourceDesc::Ingestion {
1448                        desc,
1449                        cluster_id: match in_cluster {
1450                            Some(id) => id,
1451                            None => {
1452                                return Err((
1453                                    AdapterError::Unstructured(anyhow::anyhow!(
1454                                        "ingestion-based sources must have cluster specified"
1455                                    )),
1456                                    cached_expr,
1457                                ));
1458                            }
1459                        },
1460                    },
1461                    mz_sql::plan::DataSourceDesc::OldSyntaxIngestion {
1462                        desc,
1463                        progress_subsource,
1464                        data_config,
1465                        details,
1466                    } => DataSourceDesc::OldSyntaxIngestion {
1467                        desc,
1468                        progress_subsource,
1469                        data_config,
1470                        details,
1471                        cluster_id: match in_cluster {
1472                            Some(id) => id,
1473                            None => {
1474                                return Err((
1475                                    AdapterError::Unstructured(anyhow::anyhow!(
1476                                        "ingestion-based sources must have cluster specified"
1477                                    )),
1478                                    cached_expr,
1479                                ));
1480                            }
1481                        },
1482                    },
1483                    mz_sql::plan::DataSourceDesc::IngestionExport {
1484                        ingestion_id,
1485                        external_reference,
1486                        details,
1487                        data_config,
1488                    } => DataSourceDesc::IngestionExport {
1489                        ingestion_id,
1490                        external_reference,
1491                        details,
1492                        data_config,
1493                    },
1494                    mz_sql::plan::DataSourceDesc::Progress => DataSourceDesc::Progress,
1495                    mz_sql::plan::DataSourceDesc::Webhook {
1496                        validate_using,
1497                        body_format,
1498                        headers,
1499                        cluster_id,
1500                    } => {
1501                        mz_ore::soft_assert_or_log!(
1502                            cluster_id.is_none(),
1503                            "cluster_id set at Source level for Webhooks"
1504                        );
1505                        DataSourceDesc::Webhook {
1506                            validate_using,
1507                            body_format,
1508                            headers,
1509                            cluster_id: in_cluster
1510                                .expect("webhook sources must use an existing cluster"),
1511                        }
1512                    }
1513                },
1514                desc: source.desc,
1515                global_id,
1516                timeline,
1517                resolved_ids,
1518                custom_logical_compaction_window: source
1519                    .compaction_window
1520                    .or(custom_logical_compaction_window),
1521                is_retained_metrics_object,
1522            }),
1523            Plan::CreateView(CreateViewPlan { view, .. }) => {
1524                // Collect optimizer parameters.
1525                let optimizer_config =
1526                    optimize::OptimizerConfig::from(session_catalog.system_vars());
1527                let previous_exprs = previous_item.map(|item| match item {
1528                    CatalogItem::View(view) => Some((view.raw_expr, view.locally_optimized_expr)),
1529                    _ => None,
1530                });
1531
1532                let (raw_expr, optimized_expr) = match (cached_expr, previous_exprs) {
1533                    (Some(local_expr), _)
1534                        if local_expr.optimizer_features == optimizer_config.features =>
1535                    {
1536                        debug!("local expression cache hit for {global_id:?}");
1537                        (Arc::new(view.expr), Arc::new(local_expr.local_mir))
1538                    }
1539                    // If the new expr is equivalent to the old expr, then we don't need to re-optimize.
1540                    (_, Some(Some((raw_expr, optimized_expr)))) if *raw_expr == view.expr => {
1541                        (Arc::clone(&raw_expr), Arc::clone(&optimized_expr))
1542                    }
1543                    (cached_expr, _) => {
1544                        let optimizer_features = optimizer_config.features.clone();
1545                        // Build an optimizer for this VIEW.
1546                        let mut optimizer = optimize::view::Optimizer::new(optimizer_config, None);
1547
1548                        // HIR ⇒ MIR lowering and MIR ⇒ MIR optimization (local)
1549                        let raw_expr = view.expr;
1550                        let optimized_expr = match optimizer.optimize(raw_expr.clone()) {
1551                            Ok(optimzed_expr) => optimzed_expr,
1552                            Err(err) => return Err((err.into(), cached_expr)),
1553                        };
1554
1555                        uncached_expr = Some((optimized_expr.clone(), optimizer_features));
1556
1557                        (Arc::new(raw_expr), Arc::new(optimized_expr))
1558                    }
1559                };
1560
1561                // Resolve all item dependencies from the HIR expression.
1562                let dependencies: BTreeSet<_> = raw_expr
1563                    .depends_on()
1564                    .into_iter()
1565                    .map(|gid| self.get_entry_by_global_id(&gid).id())
1566                    .collect();
1567
1568                let typ = infer_sql_type_for_catalog(&raw_expr, &optimized_expr);
1569                CatalogItem::View(View {
1570                    create_sql: view.create_sql,
1571                    global_id,
1572                    raw_expr,
1573                    desc: RelationDesc::new(typ, view.column_names),
1574                    locally_optimized_expr: optimized_expr,
1575                    conn_id: None,
1576                    resolved_ids,
1577                    dependencies: DependencyIds(dependencies),
1578                })
1579            }
1580            Plan::CreateMaterializedView(CreateMaterializedViewPlan {
1581                materialized_view, ..
1582            }) => {
1583                let collections = extra_versions
1584                    .iter()
1585                    .map(|(version, gid)| (*version, *gid))
1586                    .chain([(RelationVersion::root(), global_id)].into_iter())
1587                    .collect();
1588
1589                // Collect optimizer parameters.
1590                let system_vars = session_catalog.system_vars();
1591                let overrides = self
1592                    .get_cluster(materialized_view.cluster_id)
1593                    .config
1594                    .features();
1595                let optimizer_config =
1596                    optimize::OptimizerConfig::from(system_vars).override_from(&overrides);
1597                let previous_exprs = previous_item.map(|item| match item {
1598                    CatalogItem::MaterializedView(materialized_view) => (
1599                        materialized_view.raw_expr,
1600                        materialized_view.locally_optimized_expr,
1601                    ),
1602                    item => unreachable!("expected materialized view, found: {item:#?}"),
1603                });
1604
1605                let (raw_expr, optimized_expr) = match (cached_expr, previous_exprs) {
1606                    (Some(local_expr), _)
1607                        if local_expr.optimizer_features == optimizer_config.features =>
1608                    {
1609                        debug!("local expression cache hit for {global_id:?}");
1610                        (
1611                            Arc::new(materialized_view.expr),
1612                            Arc::new(local_expr.local_mir),
1613                        )
1614                    }
1615                    // If the new expr is equivalent to the old expr, then we don't need to re-optimize.
1616                    (_, Some((raw_expr, optimized_expr)))
1617                        if *raw_expr == materialized_view.expr =>
1618                    {
1619                        (Arc::clone(&raw_expr), Arc::clone(&optimized_expr))
1620                    }
1621                    (cached_expr, _) => {
1622                        let optimizer_features = optimizer_config.features.clone();
1623                        // TODO(aalexandrov): ideally this should be a materialized_view::Optimizer.
1624                        let mut optimizer = optimize::view::Optimizer::new(optimizer_config, None);
1625
1626                        let raw_expr = materialized_view.expr;
1627                        let optimized_expr = match optimizer.optimize(raw_expr.clone()) {
1628                            Ok(optimized_expr) => optimized_expr,
1629                            Err(err) => return Err((err.into(), cached_expr)),
1630                        };
1631
1632                        uncached_expr = Some((optimized_expr.clone(), optimizer_features));
1633
1634                        (Arc::new(raw_expr), Arc::new(optimized_expr))
1635                    }
1636                };
1637                let mut typ = infer_sql_type_for_catalog(&raw_expr, &optimized_expr);
1638
1639                for &i in &materialized_view.non_null_assertions {
1640                    typ.column_types[i].nullable = false;
1641                }
1642                let desc = RelationDesc::new(typ, materialized_view.column_names);
1643                let desc = VersionedRelationDesc::new(desc);
1644
1645                let initial_as_of = materialized_view.as_of.map(Antichain::from_elem);
1646
1647                // Resolve all item dependencies from the HIR expression.
1648                let dependencies = raw_expr
1649                    .depends_on()
1650                    .into_iter()
1651                    .map(|gid| self.get_entry_by_global_id(&gid).id())
1652                    .collect();
1653
1654                CatalogItem::MaterializedView(MaterializedView {
1655                    create_sql: materialized_view.create_sql,
1656                    collections,
1657                    raw_expr,
1658                    locally_optimized_expr: optimized_expr,
1659                    desc,
1660                    resolved_ids,
1661                    dependencies,
1662                    replacement_target: materialized_view.replacement_target,
1663                    cluster_id: materialized_view.cluster_id,
1664                    target_replica: materialized_view.target_replica,
1665                    non_null_assertions: materialized_view.non_null_assertions,
1666                    custom_logical_compaction_window: materialized_view.compaction_window,
1667                    refresh_schedule: materialized_view.refresh_schedule,
1668                    initial_as_of,
1669                    optimized_plan: None,
1670                    physical_plan: None,
1671                    dataflow_metainfo: None,
1672                })
1673            }
1674            Plan::CreateIndex(CreateIndexPlan { index, .. }) => CatalogItem::Index(Index {
1675                create_sql: index.create_sql,
1676                global_id,
1677                on: index.on,
1678                keys: index.keys.into(),
1679                conn_id: None,
1680                resolved_ids,
1681                cluster_id: index.cluster_id,
1682                custom_logical_compaction_window: custom_logical_compaction_window
1683                    .or(index.compaction_window),
1684                is_retained_metrics_object,
1685                optimized_plan: None,
1686                physical_plan: None,
1687                dataflow_metainfo: None,
1688            }),
1689            Plan::CreateMetricSink(CreateMetricSinkPlan { metric_sink, .. }) => {
1690                CatalogItem::MetricSink(MetricSink {
1691                    create_sql: metric_sink.create_sql,
1692                    global_id,
1693                    from: metric_sink.from,
1694                    resolved_ids,
1695                    cluster_id: metric_sink.cluster_id,
1696                    prefix: metric_sink.prefix,
1697                    optimized_plan: None,
1698                    physical_plan: None,
1699                    dataflow_metainfo: None,
1700                })
1701            }
1702            Plan::CreateSink(CreateSinkPlan {
1703                sink,
1704                with_snapshot,
1705                in_cluster,
1706                ..
1707            }) => CatalogItem::Sink(Sink {
1708                create_sql: sink.create_sql,
1709                global_id,
1710                from: sink.from,
1711                connection: sink.connection,
1712                envelope: sink.envelope,
1713                version: sink.version,
1714                with_snapshot,
1715                resolved_ids,
1716                cluster_id: in_cluster,
1717                commit_interval: sink.commit_interval,
1718            }),
1719            Plan::CreateType(CreateTypePlan { typ, .. }) => {
1720                // Even if we don't need the `RelationDesc` here, error out
1721                // early and eagerly, as a kind of soft assertion that we _can_
1722                // build the `RelationDesc` when needed.
1723                if let Err(err) = typ.inner.desc(&session_catalog) {
1724                    return Err((err.into(), cached_expr));
1725                }
1726                CatalogItem::Type(Type {
1727                    create_sql: Some(typ.create_sql),
1728                    global_id,
1729                    details: CatalogTypeDetails {
1730                        array_id: None,
1731                        typ: typ.inner,
1732                        pg_metadata: None,
1733                    },
1734                    resolved_ids,
1735                })
1736            }
1737            Plan::CreateSecret(CreateSecretPlan { secret, .. }) => CatalogItem::Secret(Secret {
1738                create_sql: secret.create_sql,
1739                global_id,
1740            }),
1741            Plan::CreateConnection(CreateConnectionPlan {
1742                connection:
1743                    mz_sql::plan::Connection {
1744                        create_sql,
1745                        details,
1746                    },
1747                ..
1748            }) => CatalogItem::Connection(Connection {
1749                create_sql,
1750                global_id,
1751                details,
1752                resolved_ids,
1753            }),
1754            _ => {
1755                return Err((
1756                    Error::new(ErrorKind::Corruption {
1757                        detail: "catalog entry generated inappropriate plan".to_string(),
1758                    })
1759                    .into(),
1760                    cached_expr,
1761                ));
1762            }
1763        };
1764
1765        // Carry over the plans (`optimized_plan`, `physical_plan`,
1766        // `dataflow_metainfo`) from the previous incarnation of this item, if
1767        // any. See the comment on `previous_plans` above.
1768        if let Some((prev_optimized, prev_physical, prev_metainfo)) = previous_plans {
1769            if let Some((optimized_plan, physical_plan, dataflow_metainfo)) = item.plan_fields_mut()
1770            {
1771                *optimized_plan = prev_optimized;
1772                *physical_plan = prev_physical;
1773                *dataflow_metainfo = prev_metainfo;
1774            }
1775        }
1776
1777        Ok((item, uncached_expr))
1778    }
1779
1780    /// Execute function `f` on `self`, with all "enable_for_item_parsing" feature flags enabled.
1781    /// Calling this method will not permanently modify any system configuration variables.
1782    ///
1783    /// WARNING:
1784    /// Any modifications made to the system configuration variables in `f`, will be lost.
1785    pub fn with_enable_for_item_parsing<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
1786        // Enable catalog features that might be required during planning existing
1787        // catalog items. Existing catalog items might have been created while
1788        // a specific feature flag was turned on, so we need to ensure that this
1789        // is also the case during catalog rehydration in order to avoid panics.
1790        //
1791        // WARNING / CONTRACT:
1792        // 1. Features used in this method that related to parsing / planning
1793        //    should be `enable_for_item_parsing` set to `true`.
1794        // 2. After this step, feature flag configuration must not be
1795        //    overridden.
1796        // 3. We don't notify `SystemVars` callbacks here, neither for the
1797        //    flags this enables nor for the `Arc` restore that undoes them
1798        //    afterwards. A callback on a `feature_flags!` var therefore won't
1799        //    observe this transient flip, only committed changes to it. See
1800        //    `SystemVars::register_callback`.
1801        let restore = Arc::clone(&self.system_configuration);
1802        Arc::make_mut(&mut self.system_configuration).enable_for_item_parsing();
1803        let res = f(self);
1804        self.system_configuration = restore;
1805        res
1806    }
1807
1808    /// Returns all indexes on the given object and cluster known in the catalog.
1809    pub fn get_indexes_on(
1810        &self,
1811        id: GlobalId,
1812        cluster: ClusterId,
1813    ) -> impl Iterator<Item = (GlobalId, &Index)> {
1814        let index_matches = move |idx: &Index| idx.on == id && idx.cluster_id == cluster;
1815
1816        self.try_get_entry_by_global_id(&id)
1817            .into_iter()
1818            .map(move |e| {
1819                e.used_by()
1820                    .iter()
1821                    .filter_map(move |uses_id| match self.get_entry(uses_id).item() {
1822                        CatalogItem::Index(index) if index_matches(index) => {
1823                            Some((index.global_id(), index))
1824                        }
1825                        _ => None,
1826                    })
1827            })
1828            .flatten()
1829    }
1830
1831    pub(super) fn get_database(&self, database_id: &DatabaseId) -> &Database {
1832        &self.database_by_id[database_id]
1833    }
1834
1835    /// Gets a reference to the specified replica of the specified cluster.
1836    ///
1837    /// Returns `None` if either the cluster or the replica does not
1838    /// exist.
1839    pub(super) fn try_get_cluster_replica(
1840        &self,
1841        id: ClusterId,
1842        replica_id: ReplicaId,
1843    ) -> Option<&ClusterReplica> {
1844        self.try_get_cluster(id)
1845            .and_then(|cluster| cluster.replica(replica_id))
1846    }
1847
1848    /// Gets a reference to the specified replica of the specified cluster.
1849    ///
1850    /// Panics if either the cluster or the replica does not exist.
1851    pub(crate) fn get_cluster_replica(
1852        &self,
1853        cluster_id: ClusterId,
1854        replica_id: ReplicaId,
1855    ) -> &ClusterReplica {
1856        self.try_get_cluster_replica(cluster_id, replica_id)
1857            .unwrap_or_else(|| panic!("unknown cluster replica: {cluster_id}.{replica_id}"))
1858    }
1859
1860    pub(super) fn resolve_replica_in_cluster(
1861        &self,
1862        cluster_id: &ClusterId,
1863        replica_name: &str,
1864    ) -> Result<&ClusterReplica, SqlCatalogError> {
1865        let cluster = self.get_cluster(*cluster_id);
1866        let replica_id = cluster
1867            .replica_id_by_name_
1868            .get(replica_name)
1869            .ok_or_else(|| SqlCatalogError::UnknownClusterReplica(replica_name.to_string()))?;
1870        Ok(&cluster.replicas_by_id_[replica_id])
1871    }
1872
1873    /// Get system configuration `name`.
1874    pub fn get_system_configuration(&self, name: &str) -> Result<&dyn Var, Error> {
1875        Ok(self.system_configuration.get(name)?)
1876    }
1877
1878    /// Parse system configuration `name` with `value` int.
1879    ///
1880    /// Returns the parsed value as a string.
1881    pub(super) fn parse_system_configuration(
1882        &self,
1883        name: &str,
1884        value: VarInput,
1885    ) -> Result<String, Error> {
1886        let value = self.system_configuration.parse(name, value)?;
1887        Ok(value.format())
1888    }
1889
1890    /// Gets the schema map for the database matching `database_spec`.
1891    pub(super) fn resolve_schema_in_database(
1892        &self,
1893        database_spec: &ResolvedDatabaseSpecifier,
1894        schema_name: &str,
1895        conn_id: &ConnectionId,
1896    ) -> Result<&Schema, SqlCatalogError> {
1897        let schema = match database_spec {
1898            ResolvedDatabaseSpecifier::Ambient if schema_name == MZ_TEMP_SCHEMA => {
1899                self.temporary_namespaces.schema(conn_id)
1900            }
1901            ResolvedDatabaseSpecifier::Ambient => self
1902                .ambient_schemas_by_name
1903                .get(schema_name)
1904                .and_then(|id| self.ambient_schemas_by_id.get(id)),
1905            ResolvedDatabaseSpecifier::Id(id) => self.database_by_id.get(id).and_then(|db| {
1906                db.schemas_by_name
1907                    .get(schema_name)
1908                    .and_then(|id| db.schemas_by_id.get(id))
1909            }),
1910        };
1911        schema.ok_or_else(|| SqlCatalogError::UnknownSchema(schema_name.into()))
1912    }
1913
1914    /// Try to get a schema, returning `None` if it doesn't exist.
1915    ///
1916    /// For temporary schemas, returns `None` if the connection's temporary
1917    /// namespace hasn't been registered yet (that happens at its first
1918    /// temporary-item creation).
1919    pub fn try_get_schema(
1920        &self,
1921        database_spec: &ResolvedDatabaseSpecifier,
1922        schema_spec: &SchemaSpecifier,
1923        conn_id: &ConnectionId,
1924    ) -> Option<&Schema> {
1925        // Keep in sync with `get_schema` and `get_schemas_mut`
1926        match (database_spec, schema_spec) {
1927            (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Temporary) => {
1928                self.temporary_namespaces.schema(conn_id)
1929            }
1930            (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Id(id)) => {
1931                self.ambient_schemas_by_id.get(id)
1932            }
1933            (ResolvedDatabaseSpecifier::Id(database_id), SchemaSpecifier::Id(schema_id)) => self
1934                .database_by_id
1935                .get(database_id)
1936                .and_then(|db| db.schemas_by_id.get(schema_id)),
1937            (ResolvedDatabaseSpecifier::Id(_), SchemaSpecifier::Temporary) => {
1938                unreachable!("temporary schemas are in the ambient database")
1939            }
1940        }
1941    }
1942
1943    pub fn get_schema(
1944        &self,
1945        database_spec: &ResolvedDatabaseSpecifier,
1946        schema_spec: &SchemaSpecifier,
1947        conn_id: &ConnectionId,
1948    ) -> &Schema {
1949        // Keep in sync with `try_get_schema` and `get_schemas_mut`
1950        self.try_get_schema(database_spec, schema_spec, conn_id)
1951            .expect("schema must exist")
1952    }
1953
1954    pub(super) fn find_non_temp_schema(&self, schema_id: &SchemaId) -> &Schema {
1955        self.database_by_id
1956            .values()
1957            .filter_map(|database| database.schemas_by_id.get(schema_id))
1958            .chain(self.ambient_schemas_by_id.values())
1959            .filter(|schema| schema.id() == &SchemaSpecifier::from(*schema_id))
1960            .into_first()
1961    }
1962
1963    pub fn get_mz_catalog_schema_id(&self) -> SchemaId {
1964        self.ambient_schemas_by_name[MZ_CATALOG_SCHEMA]
1965    }
1966
1967    pub fn get_mz_catalog_unstable_schema_id(&self) -> SchemaId {
1968        self.ambient_schemas_by_name[MZ_CATALOG_UNSTABLE_SCHEMA]
1969    }
1970
1971    pub fn get_pg_catalog_schema_id(&self) -> SchemaId {
1972        self.ambient_schemas_by_name[PG_CATALOG_SCHEMA]
1973    }
1974
1975    pub fn get_information_schema_id(&self) -> SchemaId {
1976        self.ambient_schemas_by_name[INFORMATION_SCHEMA]
1977    }
1978
1979    pub fn get_mz_internal_schema_id(&self) -> SchemaId {
1980        self.ambient_schemas_by_name[MZ_INTERNAL_SCHEMA]
1981    }
1982
1983    pub fn get_mz_introspection_schema_id(&self) -> SchemaId {
1984        self.ambient_schemas_by_name[MZ_INTROSPECTION_SCHEMA]
1985    }
1986
1987    pub fn get_mz_unsafe_schema_id(&self) -> SchemaId {
1988        self.ambient_schemas_by_name[MZ_UNSAFE_SCHEMA]
1989    }
1990
1991    pub fn system_schema_ids(&self) -> impl Iterator<Item = SchemaId> + '_ {
1992        SYSTEM_SCHEMAS
1993            .iter()
1994            .map(|name| self.ambient_schemas_by_name[*name])
1995    }
1996
1997    pub fn is_system_schema_id(&self, id: SchemaId) -> bool {
1998        self.system_schema_ids().contains(&id)
1999    }
2000
2001    pub fn is_system_schema_specifier(&self, spec: SchemaSpecifier) -> bool {
2002        match spec {
2003            SchemaSpecifier::Temporary => false,
2004            SchemaSpecifier::Id(id) => self.is_system_schema_id(id),
2005        }
2006    }
2007
2008    pub fn unstable_schema_ids(&self) -> impl Iterator<Item = SchemaId> + '_ {
2009        UNSTABLE_SCHEMAS
2010            .iter()
2011            .map(|name| self.ambient_schemas_by_name[*name])
2012    }
2013
2014    pub fn is_unstable_schema_id(&self, id: SchemaId) -> bool {
2015        self.unstable_schema_ids().contains(&id)
2016    }
2017
2018    pub fn is_unstable_schema_specifier(&self, spec: SchemaSpecifier) -> bool {
2019        match spec {
2020            SchemaSpecifier::Temporary => false,
2021            SchemaSpecifier::Id(id) => self.is_unstable_schema_id(id),
2022        }
2023    }
2024
2025    /// Return all OIDs that are allocated to temporary objects.
2026    pub(crate) fn get_temporary_oids(&self) -> impl Iterator<Item = u32> + '_ {
2027        std::iter::empty()
2028            .chain(self.ambient_schemas_by_id.values().filter_map(|schema| {
2029                if schema.id.is_temporary() {
2030                    Some(schema.oid)
2031                } else {
2032                    None
2033                }
2034            }))
2035            .chain(self.entry_by_id.values().filter_map(|entry| {
2036                if entry.item().is_temporary() {
2037                    Some(entry.oid)
2038                } else {
2039                    None
2040                }
2041            }))
2042    }
2043
2044    /// Optimized lookup for a builtin table.
2045    ///
2046    /// Panics if the builtin table doesn't exist in the catalog.
2047    pub fn resolve_builtin_table(&self, builtin: &'static BuiltinTable) -> CatalogItemId {
2048        self.resolve_builtin_object(&Builtin::<IdReference>::Table(builtin))
2049    }
2050
2051    /// Optimized lookup for a builtin log.
2052    ///
2053    /// Panics if the builtin log doesn't exist in the catalog.
2054    pub fn resolve_builtin_log(&self, builtin: &'static BuiltinLog) -> (CatalogItemId, GlobalId) {
2055        let item_id = self.resolve_builtin_object(&Builtin::<IdReference>::Log(builtin));
2056        let log = match self.get_entry(&item_id).item() {
2057            CatalogItem::Log(log) => log,
2058            other => unreachable!("programming error, expected BuiltinLog, found {other:?}"),
2059        };
2060        (item_id, log.global_id)
2061    }
2062
2063    /// Optimized lookup for a builtin storage collection.
2064    ///
2065    /// Panics if the builtin storage collection doesn't exist in the catalog.
2066    pub fn resolve_builtin_source(&self, builtin: &'static BuiltinSource) -> CatalogItemId {
2067        self.resolve_builtin_object(&Builtin::<IdReference>::Source(builtin))
2068    }
2069
2070    /// Optimized lookup for a builtin object.
2071    ///
2072    /// Panics if the builtin object doesn't exist in the catalog.
2073    pub fn resolve_builtin_object<T: TypeReference>(&self, builtin: &Builtin<T>) -> CatalogItemId {
2074        let schema_id = &self.ambient_schemas_by_name[builtin.schema()];
2075        let schema = &self.ambient_schemas_by_id[schema_id];
2076        match builtin.catalog_item_type() {
2077            CatalogItemType::Type => schema.types[builtin.name()],
2078            CatalogItemType::Func => schema.functions[builtin.name()],
2079            CatalogItemType::Table
2080            | CatalogItemType::Source
2081            | CatalogItemType::Sink
2082            | CatalogItemType::MetricSink
2083            | CatalogItemType::View
2084            | CatalogItemType::MaterializedView
2085            | CatalogItemType::Index
2086            | CatalogItemType::Secret
2087            | CatalogItemType::Connection => schema.items[builtin.name()],
2088        }
2089    }
2090
2091    /// Resolve a [`BuiltinType<NameReference>`] to a [`BuiltinType<IdReference>`].
2092    pub fn resolve_builtin_type_references(
2093        &self,
2094        builtin: &BuiltinType<NameReference>,
2095    ) -> BuiltinType<IdReference> {
2096        let typ: CatalogType<IdReference> = match &builtin.details.typ {
2097            CatalogType::AclItem => CatalogType::AclItem,
2098            CatalogType::Array { element_reference } => CatalogType::Array {
2099                element_reference: self.get_system_type(element_reference).id,
2100            },
2101            CatalogType::List {
2102                element_reference,
2103                element_modifiers,
2104            } => CatalogType::List {
2105                element_reference: self.get_system_type(element_reference).id,
2106                element_modifiers: element_modifiers.clone(),
2107            },
2108            CatalogType::Map {
2109                key_reference,
2110                value_reference,
2111                key_modifiers,
2112                value_modifiers,
2113            } => CatalogType::Map {
2114                key_reference: self.get_system_type(key_reference).id,
2115                value_reference: self.get_system_type(value_reference).id,
2116                key_modifiers: key_modifiers.clone(),
2117                value_modifiers: value_modifiers.clone(),
2118            },
2119            CatalogType::Range { element_reference } => CatalogType::Range {
2120                element_reference: self.get_system_type(element_reference).id,
2121            },
2122            CatalogType::Record { fields } => CatalogType::Record {
2123                fields: fields
2124                    .into_iter()
2125                    .map(|f| CatalogRecordField {
2126                        name: f.name.clone(),
2127                        type_reference: self.get_system_type(f.type_reference).id,
2128                        type_modifiers: f.type_modifiers.clone(),
2129                    })
2130                    .collect(),
2131            },
2132            CatalogType::Bool => CatalogType::Bool,
2133            CatalogType::Bytes => CatalogType::Bytes,
2134            CatalogType::Char => CatalogType::Char,
2135            CatalogType::Date => CatalogType::Date,
2136            CatalogType::Float32 => CatalogType::Float32,
2137            CatalogType::Float64 => CatalogType::Float64,
2138            CatalogType::Int16 => CatalogType::Int16,
2139            CatalogType::Int32 => CatalogType::Int32,
2140            CatalogType::Int64 => CatalogType::Int64,
2141            CatalogType::UInt16 => CatalogType::UInt16,
2142            CatalogType::UInt32 => CatalogType::UInt32,
2143            CatalogType::UInt64 => CatalogType::UInt64,
2144            CatalogType::MzTimestamp => CatalogType::MzTimestamp,
2145            CatalogType::Interval => CatalogType::Interval,
2146            CatalogType::Jsonb => CatalogType::Jsonb,
2147            CatalogType::Numeric => CatalogType::Numeric,
2148            CatalogType::Oid => CatalogType::Oid,
2149            CatalogType::PgLegacyChar => CatalogType::PgLegacyChar,
2150            CatalogType::PgLegacyName => CatalogType::PgLegacyName,
2151            CatalogType::Pseudo => CatalogType::Pseudo,
2152            CatalogType::RegClass => CatalogType::RegClass,
2153            CatalogType::RegProc => CatalogType::RegProc,
2154            CatalogType::RegType => CatalogType::RegType,
2155            CatalogType::String => CatalogType::String,
2156            CatalogType::Time => CatalogType::Time,
2157            CatalogType::Timestamp => CatalogType::Timestamp,
2158            CatalogType::TimestampTz => CatalogType::TimestampTz,
2159            CatalogType::Uuid => CatalogType::Uuid,
2160            CatalogType::VarChar => CatalogType::VarChar,
2161            CatalogType::Int2Vector => CatalogType::Int2Vector,
2162            CatalogType::MzAclItem => CatalogType::MzAclItem,
2163        };
2164
2165        BuiltinType {
2166            name: builtin.name,
2167            schema: builtin.schema,
2168            oid: builtin.oid,
2169            details: CatalogTypeDetails {
2170                array_id: builtin.details.array_id,
2171                typ,
2172                pg_metadata: builtin.details.pg_metadata.clone(),
2173            },
2174        }
2175    }
2176
2177    pub fn config(&self) -> &mz_sql::catalog::CatalogConfig {
2178        &self.config
2179    }
2180
2181    pub fn resolve_database(&self, database_name: &str) -> Result<&Database, SqlCatalogError> {
2182        match self.database_by_name.get(database_name) {
2183            Some(id) => Ok(&self.database_by_id[id]),
2184            None => Err(SqlCatalogError::UnknownDatabase(database_name.into())),
2185        }
2186    }
2187
2188    pub fn resolve_schema(
2189        &self,
2190        current_database: Option<&DatabaseId>,
2191        database_name: Option<&str>,
2192        schema_name: &str,
2193        conn_id: &ConnectionId,
2194    ) -> Result<&Schema, SqlCatalogError> {
2195        let database_spec = match database_name {
2196            // If a database is explicitly specified, validate it. Note that we
2197            // intentionally do not validate `current_database` to permit
2198            // querying `mz_catalog` with an invalid session database, e.g., so
2199            // that you can run `SHOW DATABASES` to *find* a valid database.
2200            Some(database) => Some(ResolvedDatabaseSpecifier::Id(
2201                self.resolve_database(database)?.id().clone(),
2202            )),
2203            None => current_database.map(|id| ResolvedDatabaseSpecifier::Id(id.clone())),
2204        };
2205
2206        // First try to find the schema in the named database.
2207        if let Some(database_spec) = database_spec {
2208            if let Ok(schema) =
2209                self.resolve_schema_in_database(&database_spec, schema_name, conn_id)
2210            {
2211                return Ok(schema);
2212            }
2213        }
2214
2215        // Then fall back to the ambient database.
2216        if let Ok(schema) = self.resolve_schema_in_database(
2217            &ResolvedDatabaseSpecifier::Ambient,
2218            schema_name,
2219            conn_id,
2220        ) {
2221            return Ok(schema);
2222        }
2223
2224        Err(SqlCatalogError::UnknownSchema(schema_name.into()))
2225    }
2226
2227    /// Optimized lookup for a system schema.
2228    ///
2229    /// Panics if the system schema doesn't exist in the catalog.
2230    pub fn resolve_system_schema(&self, name: &'static str) -> SchemaId {
2231        self.ambient_schemas_by_name[name]
2232    }
2233
2234    pub fn resolve_search_path(
2235        &self,
2236        session: &dyn SessionMetadata,
2237    ) -> Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)> {
2238        let database = self
2239            .database_by_name
2240            .get(session.database())
2241            .map(|id| id.clone());
2242
2243        // NOTE: This drops schemas that don't resolve, and consumers rely on
2244        // every returned entry existing (e.g. `allocate_full_name` and the
2245        // `current_schemas` evaluation look schemas up infallibly). In
2246        // particular, `mz_temp` is dropped until the session's temporary
2247        // namespace is registered at its first temporary-item creation.
2248        session
2249            .search_path()
2250            .iter()
2251            .map(|schema| {
2252                self.resolve_schema(database.as_ref(), None, schema.as_str(), session.conn_id())
2253            })
2254            .filter_map(|schema| schema.ok())
2255            .map(|schema| (schema.name().database.clone(), schema.id().clone()))
2256            .collect()
2257    }
2258
2259    pub fn effective_search_path(
2260        &self,
2261        search_path: &[(ResolvedDatabaseSpecifier, SchemaSpecifier)],
2262        include_temp_schema: bool,
2263    ) -> Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)> {
2264        let mut v = Vec::with_capacity(search_path.len() + 3);
2265        // Temp schema is only included for relations and data types, not for functions and operators
2266        let temp_schema = (
2267            ResolvedDatabaseSpecifier::Ambient,
2268            SchemaSpecifier::Temporary,
2269        );
2270        if include_temp_schema && !search_path.contains(&temp_schema) {
2271            v.push(temp_schema);
2272        }
2273        let default_schemas = [
2274            (
2275                ResolvedDatabaseSpecifier::Ambient,
2276                SchemaSpecifier::Id(self.get_mz_catalog_schema_id()),
2277            ),
2278            (
2279                ResolvedDatabaseSpecifier::Ambient,
2280                SchemaSpecifier::Id(self.get_pg_catalog_schema_id()),
2281            ),
2282        ];
2283        for schema in default_schemas.into_iter() {
2284            if !search_path.contains(&schema) {
2285                v.push(schema);
2286            }
2287        }
2288        v.extend_from_slice(search_path);
2289        v
2290    }
2291
2292    pub fn resolve_cluster(&self, name: &str) -> Result<&Cluster, SqlCatalogError> {
2293        let id = self
2294            .clusters_by_name
2295            .get(name)
2296            .ok_or_else(|| SqlCatalogError::UnknownCluster(name.to_string()))?;
2297        Ok(&self.clusters_by_id[id])
2298    }
2299
2300    pub fn resolve_builtin_cluster(&self, cluster: &BuiltinCluster) -> &Cluster {
2301        let id = self
2302            .clusters_by_name
2303            .get(cluster.name)
2304            .expect("failed to lookup BuiltinCluster by name");
2305        self.clusters_by_id
2306            .get(id)
2307            .expect("failed to lookup BuiltinCluster by ID")
2308    }
2309
2310    pub fn resolve_cluster_replica(
2311        &self,
2312        cluster_replica_name: &QualifiedReplica,
2313    ) -> Result<&ClusterReplica, SqlCatalogError> {
2314        let cluster = self.resolve_cluster(cluster_replica_name.cluster.as_str())?;
2315        let replica_name = cluster_replica_name.replica.as_str();
2316        let replica_id = cluster
2317            .replica_id(replica_name)
2318            .ok_or_else(|| SqlCatalogError::UnknownClusterReplica(replica_name.to_string()))?;
2319        Ok(cluster.replica(replica_id).expect("Must exist"))
2320    }
2321
2322    /// Resolves [`PartialItemName`] into a [`CatalogEntry`].
2323    ///
2324    /// If `name` does not specify a database, the `current_database` is used.
2325    /// If `name` does not specify a schema, then the schemas in `search_path`
2326    /// are searched in order.
2327    #[allow(clippy::useless_let_if_seq)]
2328    pub fn resolve(
2329        &self,
2330        get_schema_entries: fn(&Schema) -> &BTreeMap<String, CatalogItemId>,
2331        current_database: Option<&DatabaseId>,
2332        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
2333        name: &PartialItemName,
2334        conn_id: &ConnectionId,
2335        err_gen: fn(String) -> SqlCatalogError,
2336    ) -> Result<&CatalogEntry, SqlCatalogError> {
2337        // If a schema name was specified, just try to find the item in that
2338        // schema. If no schema was specified, try to find the item in the connection's
2339        // temporary schema. If the item is not found, try to find the item in every
2340        // schema in the search path.
2341        let schemas = match &name.schema {
2342            Some(schema_name) => {
2343                match self.resolve_schema(
2344                    current_database,
2345                    name.database.as_deref(),
2346                    schema_name,
2347                    conn_id,
2348                ) {
2349                    Ok(schema) => vec![(schema.name.database.clone(), schema.id.clone())],
2350                    Err(e) => return Err(e),
2351                }
2352            }
2353            None => match self
2354                .try_get_schema(
2355                    &ResolvedDatabaseSpecifier::Ambient,
2356                    &SchemaSpecifier::Temporary,
2357                    conn_id,
2358                )
2359                .and_then(|schema| schema.items.get(&name.item))
2360            {
2361                Some(id) => return Ok(self.get_entry(id)),
2362                None => search_path.to_vec(),
2363            },
2364        };
2365
2366        for (database_spec, schema_spec) in &schemas {
2367            // Use try_get_schema because the temp schema might not exist yet
2368            // (it's created lazily when the first temp object is created).
2369            let Some(schema) = self.try_get_schema(database_spec, schema_spec, conn_id) else {
2370                continue;
2371            };
2372
2373            if let Some(id) = get_schema_entries(schema).get(&name.item) {
2374                return Ok(&self.entry_by_id[id]);
2375            }
2376        }
2377
2378        // Some relations that have previously lived in the `mz_internal` schema have been moved to
2379        // `mz_catalog_unstable` or `mz_introspection`. To simplify the transition for users, we
2380        // automatically let uses of the old schema resolve to the new ones as well.
2381        // TODO(database-issues#8173) remove this after sufficient time has passed
2382        let mz_internal_schema = SchemaSpecifier::Id(self.get_mz_internal_schema_id());
2383        if schemas.iter().any(|(_, spec)| *spec == mz_internal_schema) {
2384            for schema_id in [
2385                self.get_mz_catalog_unstable_schema_id(),
2386                self.get_mz_introspection_schema_id(),
2387            ] {
2388                let schema = self.get_schema(
2389                    &ResolvedDatabaseSpecifier::Ambient,
2390                    &SchemaSpecifier::Id(schema_id),
2391                    conn_id,
2392                );
2393
2394                if let Some(id) = get_schema_entries(schema).get(&name.item) {
2395                    debug!(
2396                        github_27831 = true,
2397                        "encountered use of outdated schema `mz_internal` for relation: {name}",
2398                    );
2399                    return Ok(&self.entry_by_id[id]);
2400                }
2401            }
2402        }
2403
2404        Err(err_gen(name.to_string()))
2405    }
2406
2407    /// Resolves `name` to a non-function [`CatalogEntry`].
2408    pub fn resolve_entry(
2409        &self,
2410        current_database: Option<&DatabaseId>,
2411        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
2412        name: &PartialItemName,
2413        conn_id: &ConnectionId,
2414    ) -> Result<&CatalogEntry, SqlCatalogError> {
2415        self.resolve(
2416            |schema| &schema.items,
2417            current_database,
2418            search_path,
2419            name,
2420            conn_id,
2421            SqlCatalogError::UnknownItem,
2422        )
2423    }
2424
2425    /// Resolves `name` to a function [`CatalogEntry`].
2426    pub fn resolve_function(
2427        &self,
2428        current_database: Option<&DatabaseId>,
2429        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
2430        name: &PartialItemName,
2431        conn_id: &ConnectionId,
2432    ) -> Result<&CatalogEntry, SqlCatalogError> {
2433        self.resolve(
2434            |schema| &schema.functions,
2435            current_database,
2436            search_path,
2437            name,
2438            conn_id,
2439            |name| SqlCatalogError::UnknownFunction {
2440                name,
2441                alternative: None,
2442            },
2443        )
2444    }
2445
2446    /// Resolves `name` to a type [`CatalogEntry`].
2447    pub fn resolve_type(
2448        &self,
2449        current_database: Option<&DatabaseId>,
2450        search_path: &Vec<(ResolvedDatabaseSpecifier, SchemaSpecifier)>,
2451        name: &PartialItemName,
2452        conn_id: &ConnectionId,
2453    ) -> Result<&CatalogEntry, SqlCatalogError> {
2454        static NON_PG_CATALOG_TYPES: LazyLock<
2455            BTreeMap<&'static str, &'static BuiltinType<NameReference>>,
2456        > = LazyLock::new(|| {
2457            BUILTINS::types()
2458                .filter(|typ| typ.schema != PG_CATALOG_SCHEMA)
2459                .map(|typ| (typ.name, typ))
2460                .collect()
2461        });
2462
2463        let entry = self.resolve(
2464            |schema| &schema.types,
2465            current_database,
2466            search_path,
2467            name,
2468            conn_id,
2469            |name| SqlCatalogError::UnknownType { name },
2470        )?;
2471
2472        if conn_id != &SYSTEM_CONN_ID && name.schema.as_deref() == Some(PG_CATALOG_SCHEMA) {
2473            if let Some(typ) = NON_PG_CATALOG_TYPES.get(entry.name().item.as_str()) {
2474                warn!(
2475                    "user specified an incorrect schema of {} for the type {}, which should be in \
2476                    the {} schema. This works now due to a bug but will be fixed in a later release.",
2477                    PG_CATALOG_SCHEMA.quoted(),
2478                    typ.name.quoted(),
2479                    typ.schema.quoted(),
2480                )
2481            }
2482        }
2483
2484        Ok(entry)
2485    }
2486
2487    /// For an [`ObjectId`] gets the corresponding [`CommentObjectId`].
2488    pub(super) fn get_comment_id(&self, object_id: ObjectId) -> CommentObjectId {
2489        match object_id {
2490            ObjectId::Item(item_id) => self.get_entry(&item_id).comment_object_id(),
2491            ObjectId::Role(role_id) => CommentObjectId::Role(role_id),
2492            ObjectId::Database(database_id) => CommentObjectId::Database(database_id),
2493            ObjectId::Schema((database, schema)) => CommentObjectId::Schema((database, schema)),
2494            ObjectId::Cluster(cluster_id) => CommentObjectId::Cluster(cluster_id),
2495            ObjectId::ClusterReplica(cluster_replica_id) => {
2496                CommentObjectId::ClusterReplica(cluster_replica_id)
2497            }
2498            ObjectId::NetworkPolicy(network_policy_id) => {
2499                CommentObjectId::NetworkPolicy(network_policy_id)
2500            }
2501        }
2502    }
2503
2504    /// Return current system configuration.
2505    pub fn system_config(&self) -> &SystemVars {
2506        &self.system_configuration
2507    }
2508
2509    /// Returns the cluster-coherent scoped optimizer-feature overrides for
2510    /// `cluster_id` from the in-memory scoped-parameter working copy, or empty
2511    /// if the cluster has none.
2512    pub fn cluster_scoped_optimizer_overrides(
2513        &self,
2514        cluster_id: ClusterId,
2515    ) -> OptimizerFeatureOverrides {
2516        self.scoped_system_parameters
2517            .cluster
2518            .get(&cluster_id)
2519            .cloned()
2520            .map(OptimizerFeatureOverrides::from)
2521            .unwrap_or_default()
2522    }
2523
2524    /// Returns the entire scoped system-parameter working copy, read by the
2525    /// coordinator's scoped-parameter reconcile path.
2526    pub fn scoped_system_parameters(&self) -> &ScopedParameters {
2527        &self.scoped_system_parameters
2528    }
2529
2530    /// Return a mutable reference to the current system configuration.
2531    pub fn system_config_mut(&mut self) -> &mut SystemVars {
2532        Arc::make_mut(&mut self.system_configuration)
2533    }
2534
2535    /// Serializes the catalog's in-memory state.
2536    ///
2537    /// There are no guarantees about the format of the serialized state, except
2538    /// that the serialized state for two identical catalogs will compare
2539    /// identically.
2540    ///
2541    /// Some consumers would like the ability to overwrite the `unfinalized_shards` catalog field,
2542    /// which they can accomplish by passing in a value of `Some` for the `unfinalized_shards`
2543    /// argument.
2544    pub fn dump(&self, unfinalized_shards: Option<BTreeSet<String>>) -> Result<String, Error> {
2545        // Dump the base catalog.
2546        let mut dump = serde_json::to_value(&self).map_err(|e| {
2547            Error::new(ErrorKind::Unstructured(format!(
2548                // Don't panic here because we don't have compile-time failures for maps with
2549                // non-string keys.
2550                "internal error: could not dump catalog: {}",
2551                e
2552            )))
2553        })?;
2554
2555        let dump_obj = dump.as_object_mut().expect("state must have been dumped");
2556        // Stitch in system parameter defaults.
2557        dump_obj.insert(
2558            "system_parameter_defaults".into(),
2559            serde_json::json!(self.system_config().defaults()),
2560        );
2561        // Potentially overwrite unfinalized shards.
2562        if let Some(unfinalized_shards) = unfinalized_shards {
2563            dump_obj
2564                .get_mut("storage_metadata")
2565                .expect("known to exist")
2566                .as_object_mut()
2567                .expect("storage_metadata is an object")
2568                .insert(
2569                    "unfinalized_shards".into(),
2570                    serde_json::json!(unfinalized_shards),
2571                );
2572        }
2573        // Remove GlobalIds for temporary objects from the mapping.
2574        //
2575        // Post-test consistency checks with the durable catalog don't know about temporary items
2576        // since they're kept entirely in memory.
2577        let temporary_gids: Vec<_> = self
2578            .entry_by_global_id
2579            .iter()
2580            .filter(|(_gid, item_id)| self.get_entry(item_id).conn_id().is_some())
2581            .map(|(gid, _item_id)| *gid)
2582            .collect();
2583        if !temporary_gids.is_empty() {
2584            let gids = dump_obj
2585                .get_mut("entry_by_global_id")
2586                .expect("known_to_exist")
2587                .as_object_mut()
2588                .expect("entry_by_global_id is an object");
2589            for gid in temporary_gids {
2590                gids.remove(&gid.to_string());
2591            }
2592        }
2593        // We exclude role_auth_by_id because it contains password information
2594        // which should not be included in the dump.
2595        dump_obj.remove("role_auth_by_id");
2596        // The mock authentication nonce is a server-wide secret used to derive
2597        // deterministic mock SASL challenges for absent or password-less roles.
2598        // Leaking it would re-enable the username enumeration the mock challenge
2599        // defends against, so it must not appear in the dump either.
2600        dump_obj.remove("mock_authentication_nonce");
2601
2602        // Emit as pretty-printed JSON.
2603        Ok(serde_json::to_string_pretty(&dump).expect("cannot fail on serde_json::Value"))
2604    }
2605
2606    pub fn availability_zones(&self) -> &[String] {
2607        &self.availability_zones
2608    }
2609
2610    pub fn concretize_replica_location(
2611        &self,
2612        location: mz_catalog::durable::ReplicaLocation,
2613        allowed_sizes: &Vec<String>,
2614        allowed_availability_zones: Option<&[String]>,
2615        allow_disabled: bool,
2616    ) -> Result<ReplicaLocation, Error> {
2617        let location = match location {
2618            mz_catalog::durable::ReplicaLocation::Unmanaged {
2619                storagectl_addrs,
2620                computectl_addrs,
2621            } => {
2622                if allowed_availability_zones.is_some() {
2623                    return Err(Error {
2624                        kind: ErrorKind::Internal(
2625                            "tried concretize unmanaged replica with specific availability_zones"
2626                                .to_string(),
2627                        ),
2628                    });
2629                }
2630                ReplicaLocation::Unmanaged(UnmanagedReplicaLocation {
2631                    storagectl_addrs,
2632                    computectl_addrs,
2633                })
2634            }
2635            mz_catalog::durable::ReplicaLocation::Managed {
2636                size,
2637                // The AZ list the replica was provisioned under: provisioning
2638                // paths pass the cluster's pool as `allowed_availability_zones`
2639                // to stamp it, while rebuilds from durable state pass `None` to
2640                // keep it. For an unmanaged cluster's replica it is the single
2641                // user-pinned AZ.
2642                availability_zones,
2643                billed_as,
2644                internal,
2645                pending,
2646            } => {
2647                self.ensure_valid_replica_size(allowed_sizes, &size, allow_disabled)?;
2648                let cluster_replica_sizes = &self.cluster_replica_sizes;
2649
2650                ReplicaLocation::Managed(ManagedReplicaLocation {
2651                    allocation: cluster_replica_sizes
2652                        .0
2653                        .get(&size)
2654                        .expect("catalog out of sync")
2655                        .clone(),
2656                    availability_zones: match allowed_availability_zones {
2657                        Some(azs) => azs.to_vec(),
2658                        None => availability_zones,
2659                    },
2660                    size,
2661                    billed_as,
2662                    internal,
2663                    pending,
2664                })
2665            }
2666        };
2667        Ok(location)
2668    }
2669
2670    pub(crate) fn ensure_valid_replica_size(
2671        &self,
2672        allowed_sizes: &[String],
2673        size: &String,
2674        allow_disabled: bool,
2675    ) -> Result<(), Error> {
2676        let cluster_replica_sizes = &self.cluster_replica_sizes;
2677
2678        if !cluster_replica_sizes.0.contains_key(size)
2679            || (!allowed_sizes.is_empty() && !allowed_sizes.contains(size))
2680            || (!allow_disabled && cluster_replica_sizes.0[size].disabled)
2681        {
2682            let mut entries = cluster_replica_sizes
2683                .enabled_allocations()
2684                .collect::<Vec<_>>();
2685
2686            if !allowed_sizes.is_empty() {
2687                let allowed_sizes = BTreeSet::<&String>::from_iter(allowed_sizes.iter());
2688                entries.retain(|(name, _)| allowed_sizes.contains(name));
2689            }
2690
2691            entries.sort_by_key(
2692                |(
2693                    _name,
2694                    ReplicaAllocation {
2695                        scale, cpu_limit, ..
2696                    },
2697                )| (scale, cpu_limit),
2698            );
2699
2700            Err(Error {
2701                kind: ErrorKind::InvalidClusterReplicaSize {
2702                    size: size.to_owned(),
2703                    expected: entries.into_iter().map(|(name, _)| name.clone()).collect(),
2704                },
2705            })
2706        } else {
2707            Ok(())
2708        }
2709    }
2710
2711    pub fn ensure_not_reserved_role(&self, role_id: &RoleId) -> Result<(), Error> {
2712        if role_id.is_builtin() {
2713            let role = self.get_role(role_id);
2714            Err(Error::new(ErrorKind::ReservedRoleName(
2715                role.name().to_string(),
2716            )))
2717        } else {
2718            Ok(())
2719        }
2720    }
2721
2722    pub fn ensure_not_reserved_network_policy(
2723        &self,
2724        network_policy_id: &NetworkPolicyId,
2725    ) -> Result<(), Error> {
2726        if network_policy_id.is_builtin() {
2727            let policy = self.get_network_policy(network_policy_id);
2728            Err(Error::new(ErrorKind::ReservedNetworkPolicyName(
2729                policy.name.clone(),
2730            )))
2731        } else {
2732            Ok(())
2733        }
2734    }
2735
2736    pub fn ensure_grantable_role(&self, role_id: &RoleId) -> Result<(), Error> {
2737        let is_grantable = !role_id.is_public() && !role_id.is_system();
2738        if is_grantable {
2739            Ok(())
2740        } else {
2741            let role = self.get_role(role_id);
2742            Err(Error::new(ErrorKind::UngrantableRoleName(
2743                role.name().to_string(),
2744            )))
2745        }
2746    }
2747
2748    pub fn ensure_not_system_role(&self, role_id: &RoleId) -> Result<(), Error> {
2749        if role_id.is_system() {
2750            let role = self.get_role(role_id);
2751            Err(Error::new(ErrorKind::ReservedSystemRoleName(
2752                role.name().to_string(),
2753            )))
2754        } else {
2755            Ok(())
2756        }
2757    }
2758
2759    pub fn ensure_not_predefined_role(&self, role_id: &RoleId) -> Result<(), Error> {
2760        if role_id.is_predefined() {
2761            let role = self.get_role(role_id);
2762            Err(Error::new(ErrorKind::ReservedSystemRoleName(
2763                role.name().to_string(),
2764            )))
2765        } else {
2766            Ok(())
2767        }
2768    }
2769
2770    // TODO(mjibson): Is there a way to make this a closure to avoid explicitly
2771    // passing tx, and session?
2772    pub(crate) fn add_to_audit_log(
2773        system_configuration: &SystemVars,
2774        oracle_write_ts: mz_repr::Timestamp,
2775        session: Option<&ConnMeta>,
2776        tx: &mut mz_catalog::durable::Transaction,
2777        audit_events: &mut Vec<VersionedEvent>,
2778        event_type: EventType,
2779        object_type: ObjectType,
2780        details: EventDetails,
2781    ) -> Result<(), Error> {
2782        let user = session.map(|session| session.user().name.to_string());
2783
2784        // unsafe_mock_audit_event_timestamp can only be set to Some when running in unsafe mode.
2785
2786        let occurred_at = match system_configuration.unsafe_mock_audit_event_timestamp() {
2787            Some(ts) => ts.into(),
2788            _ => oracle_write_ts.into(),
2789        };
2790        let id = tx.allocate_audit_log_id()?;
2791        let event = VersionedEvent::new(id, event_type, object_type, details, user, occurred_at);
2792        audit_events.push(event.clone());
2793        tx.insert_audit_log_event(event);
2794        Ok(())
2795    }
2796
2797    pub(super) fn get_owner_id(&self, id: &ObjectId, conn_id: &ConnectionId) -> Option<RoleId> {
2798        match id {
2799            ObjectId::Cluster(id) => Some(self.get_cluster(*id).owner_id()),
2800            ObjectId::ClusterReplica((cluster_id, replica_id)) => Some(
2801                self.get_cluster_replica(*cluster_id, *replica_id)
2802                    .owner_id(),
2803            ),
2804            ObjectId::Database(id) => Some(self.get_database(id).owner_id()),
2805            ObjectId::Schema((database_spec, schema_spec)) => Some(
2806                self.get_schema(database_spec, schema_spec, conn_id)
2807                    .owner_id(),
2808            ),
2809            ObjectId::Item(id) => Some(*self.get_entry(id).owner_id()),
2810            ObjectId::Role(_) => None,
2811            ObjectId::NetworkPolicy(id) => Some(self.get_network_policy(id).owner_id.clone()),
2812        }
2813    }
2814
2815    pub(super) fn get_object_type(&self, object_id: &ObjectId) -> mz_sql::catalog::ObjectType {
2816        match object_id {
2817            ObjectId::Cluster(_) => mz_sql::catalog::ObjectType::Cluster,
2818            ObjectId::ClusterReplica(_) => mz_sql::catalog::ObjectType::ClusterReplica,
2819            ObjectId::Database(_) => mz_sql::catalog::ObjectType::Database,
2820            ObjectId::Schema(_) => mz_sql::catalog::ObjectType::Schema,
2821            ObjectId::Role(_) => mz_sql::catalog::ObjectType::Role,
2822            ObjectId::Item(id) => self.get_entry(id).item_type().into(),
2823            ObjectId::NetworkPolicy(_) => mz_sql::catalog::ObjectType::NetworkPolicy,
2824        }
2825    }
2826
2827    pub(super) fn get_system_object_type(
2828        &self,
2829        id: &SystemObjectId,
2830    ) -> mz_sql::catalog::SystemObjectType {
2831        match id {
2832            SystemObjectId::Object(object_id) => {
2833                SystemObjectType::Object(self.get_object_type(object_id))
2834            }
2835            SystemObjectId::System => SystemObjectType::System,
2836        }
2837    }
2838
2839    /// Returns a read-only view of the current [`StorageMetadata`].
2840    ///
2841    /// To write to this struct, you must use a catalog transaction.
2842    pub fn storage_metadata(&self) -> &StorageMetadata {
2843        &self.storage_metadata
2844    }
2845
2846    /// For the Sources ids in `ids`, return their compaction windows.
2847    pub fn source_compaction_windows(
2848        &self,
2849        ids: impl IntoIterator<Item = CatalogItemId>,
2850    ) -> BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>> {
2851        let mut cws: BTreeMap<CompactionWindow, BTreeSet<CatalogItemId>> = BTreeMap::new();
2852        let mut seen = BTreeSet::new();
2853        for item_id in ids {
2854            if !seen.insert(item_id) {
2855                continue;
2856            }
2857            let entry = self.get_entry(&item_id);
2858            match entry.item() {
2859                CatalogItem::Source(source) => {
2860                    let source_cw = source.custom_logical_compaction_window.unwrap_or_default();
2861                    cws.entry(source_cw).or_default().insert(item_id);
2862                }
2863                CatalogItem::Table(table) => {
2864                    let table_cw = table.custom_logical_compaction_window.unwrap_or_default();
2865                    match &table.data_source {
2866                        TableDataSource::DataSource {
2867                            desc:
2868                                DataSourceDesc::IngestionExport { .. }
2869                                // Also match webhook tables (source-to-table migration).
2870                                | DataSourceDesc::Webhook { .. },
2871                            timeline: _,
2872                        } => {
2873                            cws.entry(table_cw).or_default().insert(item_id);
2874                        }
2875                        // Regular tables handle compaction directly in
2876                        // catalog_implications, not through this function.
2877                        TableDataSource::TableWrites { .. } => {}
2878                        TableDataSource::DataSource {
2879                            desc:
2880                                DataSourceDesc::Ingestion { .. }
2881                                | DataSourceDesc::OldSyntaxIngestion { .. }
2882                                | DataSourceDesc::Introspection(_)
2883                                | DataSourceDesc::Progress
2884                                | DataSourceDesc::Catalog,
2885                            ..
2886                        } => {
2887                            unreachable!(
2888                                "unexpected DataSourceDesc for table {item_id}: {:?}",
2889                                table.data_source
2890                            )
2891                        }
2892                    }
2893                }
2894                _ => {
2895                    // Views could depend on sources, so ignore them if added by used_by above.
2896                    continue;
2897                }
2898            }
2899        }
2900        cws
2901    }
2902
2903    pub fn comment_id_to_item_id(id: &CommentObjectId) -> Option<CatalogItemId> {
2904        match id {
2905            CommentObjectId::Table(id)
2906            | CommentObjectId::View(id)
2907            | CommentObjectId::MaterializedView(id)
2908            | CommentObjectId::Source(id)
2909            | CommentObjectId::Sink(id)
2910            | CommentObjectId::MetricSink(id)
2911            | CommentObjectId::Index(id)
2912            | CommentObjectId::Func(id)
2913            | CommentObjectId::Connection(id)
2914            | CommentObjectId::Type(id)
2915            | CommentObjectId::Secret(id) => Some(*id),
2916            CommentObjectId::Role(_)
2917            | CommentObjectId::Database(_)
2918            | CommentObjectId::Schema(_)
2919            | CommentObjectId::Cluster(_)
2920            | CommentObjectId::ClusterReplica(_)
2921            | CommentObjectId::NetworkPolicy(_) => None,
2922        }
2923    }
2924
2925    pub fn get_comment_id_entry(&self, id: &CommentObjectId) -> Option<&CatalogEntry> {
2926        Self::comment_id_to_item_id(id).map(|id| self.get_entry(&id))
2927    }
2928
2929    pub fn comment_id_to_audit_log_name(
2930        &self,
2931        id: CommentObjectId,
2932        conn_id: &ConnectionId,
2933    ) -> String {
2934        match id {
2935            CommentObjectId::Table(id)
2936            | CommentObjectId::View(id)
2937            | CommentObjectId::MaterializedView(id)
2938            | CommentObjectId::Source(id)
2939            | CommentObjectId::Sink(id)
2940            | CommentObjectId::MetricSink(id)
2941            | CommentObjectId::Index(id)
2942            | CommentObjectId::Func(id)
2943            | CommentObjectId::Connection(id)
2944            | CommentObjectId::Type(id)
2945            | CommentObjectId::Secret(id) => {
2946                let item = self.get_entry(&id);
2947                let name = self.resolve_full_name(item.name(), Some(conn_id));
2948                name.to_string()
2949            }
2950            CommentObjectId::Role(id) => self.get_role(&id).name.clone(),
2951            CommentObjectId::Database(id) => self.get_database(&id).name.clone(),
2952            CommentObjectId::Schema((spec, schema_id)) => {
2953                let schema = self.get_schema(&spec, &schema_id, conn_id);
2954                self.resolve_full_schema_name(&schema.name).to_string()
2955            }
2956            CommentObjectId::Cluster(id) => self.get_cluster(id).name.clone(),
2957            CommentObjectId::ClusterReplica((cluster_id, replica_id)) => {
2958                let cluster = self.get_cluster(cluster_id);
2959                let replica = self.get_cluster_replica(cluster_id, replica_id);
2960                QualifiedReplica {
2961                    cluster: Ident::new_unchecked(cluster.name.clone()),
2962                    replica: Ident::new_unchecked(replica.name.clone()),
2963                }
2964                .to_string()
2965            }
2966            CommentObjectId::NetworkPolicy(id) => self.get_network_policy(&id).name.clone(),
2967        }
2968    }
2969
2970    pub fn mock_authentication_nonce(&self) -> String {
2971        self.mock_authentication_nonce.clone().unwrap_or_default()
2972    }
2973}
2974
2975impl ConnectionResolver for CatalogState {
2976    fn resolve_connection(
2977        &self,
2978        id: CatalogItemId,
2979    ) -> mz_storage_types::connections::Connection<InlinedConnection> {
2980        use mz_storage_types::connections::Connection::*;
2981        match self
2982            .get_entry(&id)
2983            .connection()
2984            .expect("catalog out of sync")
2985            .details
2986            .to_connection()
2987        {
2988            Kafka(conn) => Kafka(conn.into_inline_connection(self)),
2989            Postgres(conn) => Postgres(conn.into_inline_connection(self)),
2990            Csr(conn) => Csr(conn.into_inline_connection(self)),
2991            GlueSchemaRegistry(conn) => GlueSchemaRegistry(conn.into_inline_connection(self)),
2992            Ssh(conn) => Ssh(conn),
2993            Aws(conn) => Aws(conn),
2994            AwsPrivatelink(conn) => AwsPrivatelink(conn),
2995            Gcp(conn) => Gcp(conn),
2996            MySql(conn) => MySql(conn.into_inline_connection(self)),
2997            SqlServer(conn) => SqlServer(conn.into_inline_connection(self)),
2998            IcebergCatalog(conn) => IcebergCatalog(conn.into_inline_connection(self)),
2999        }
3000    }
3001}
3002
3003impl OptimizerCatalog for CatalogState {
3004    fn get_entry(&self, id: &GlobalId) -> CatalogCollectionEntry {
3005        CatalogState::get_entry_by_global_id(self, id)
3006    }
3007    fn get_entry_by_item_id(&self, id: &CatalogItemId) -> &CatalogEntry {
3008        CatalogState::get_entry(self, id)
3009    }
3010    fn resolve_full_name(
3011        &self,
3012        name: &QualifiedItemName,
3013        conn_id: Option<&ConnectionId>,
3014    ) -> FullItemName {
3015        CatalogState::resolve_full_name(self, name, conn_id)
3016    }
3017    fn get_indexes_on(
3018        &self,
3019        id: GlobalId,
3020        cluster: ClusterId,
3021    ) -> Box<dyn Iterator<Item = (GlobalId, &Index)> + '_> {
3022        Box::new(CatalogState::get_indexes_on(self, id, cluster))
3023    }
3024}
3025
3026impl OptimizerCatalog for Catalog {
3027    fn get_entry(&self, id: &GlobalId) -> CatalogCollectionEntry {
3028        self.state.get_entry_by_global_id(id)
3029    }
3030
3031    fn get_entry_by_item_id(&self, id: &CatalogItemId) -> &CatalogEntry {
3032        self.state.get_entry(id)
3033    }
3034
3035    fn resolve_full_name(
3036        &self,
3037        name: &QualifiedItemName,
3038        conn_id: Option<&ConnectionId>,
3039    ) -> FullItemName {
3040        self.state.resolve_full_name(name, conn_id)
3041    }
3042
3043    fn get_indexes_on(
3044        &self,
3045        id: GlobalId,
3046        cluster: ClusterId,
3047    ) -> Box<dyn Iterator<Item = (GlobalId, &Index)> + '_> {
3048        Box::new(self.state.get_indexes_on(id, cluster))
3049    }
3050}
3051
3052impl Catalog {
3053    pub fn as_optimizer_catalog(self: Arc<Self>) -> Arc<dyn OptimizerCatalog> {
3054        self
3055    }
3056}
3057
3058#[cfg(test)]
3059mod tests {
3060    use super::*;
3061
3062    /// A deep dependency chain (a long chain of stacked views) must not
3063    /// overflow the stack when computing its dependents, and the dependents
3064    /// must come out in reverse-dependency order (deepest dependent first,
3065    /// root last) so that `DROP ... CASCADE` drops them in a valid order.
3066    #[mz_ore::test(tokio::test)]
3067    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
3068    async fn item_dependents_deep_chain_no_stack_overflow() {
3069        use mz_ore::cast::CastFrom;
3070
3071        Catalog::with_debug(|mut catalog| async move {
3072            // Deep enough that the previous recursive implementation overflowed
3073            // the stack.
3074            const DEPTH: usize = 100_000;
3075            // Well above any id the debug catalog assigns, so the synthetic
3076            // chain does not collide with real entries.
3077            const BASE: u64 = 1 << 40;
3078
3079            // Clone a builtin log entry as a template so we don't have to
3080            // construct a `CatalogItem` by hand. `item_dependents` only reads
3081            // `used_by`/`progress_id`, and a log's `progress_id` is `None`.
3082            let template = catalog
3083                .state
3084                .entry_by_id
3085                .values()
3086                .find(|entry| matches!(entry.item(), CatalogItem::Log(_)))
3087                .expect("debug catalog has log sources")
3088                .clone();
3089
3090            // Build a chain where entry `BASE + i` is used by `BASE + i + 1`.
3091            for i in 0..=DEPTH {
3092                let id = CatalogItemId::User(BASE + u64::cast_from(i));
3093                let mut entry = template.clone();
3094                entry.id = id;
3095                entry.referenced_by = Vec::new();
3096                entry.used_by = if i < DEPTH {
3097                    vec![CatalogItemId::User(BASE + u64::cast_from(i + 1))]
3098                } else {
3099                    Vec::new()
3100                };
3101                catalog.state.entry_by_id.insert(id, entry);
3102            }
3103
3104            let mut seen = BTreeSet::new();
3105            let dependents = catalog
3106                .state
3107                .item_dependents(CatalogItemId::User(BASE), &mut seen);
3108
3109            // Every element of the chain appears exactly once.
3110            assert_eq!(dependents.len(), DEPTH + 1);
3111            // Reverse-dependency order: the deepest dependent is first and the
3112            // root we asked about is last.
3113            for (offset, dependent) in dependents.iter().enumerate() {
3114                let expected = CatalogItemId::User(BASE + u64::cast_from(DEPTH - offset));
3115                assert_eq!(dependent, &ObjectId::Item(expected));
3116            }
3117
3118            catalog.expire().await;
3119        })
3120        .await
3121    }
3122
3123    /// Read-then-write dependency validation walks the transitive `uses()` of
3124    /// the read set. A deep chain of stacked views (user controlled, arbitrarily
3125    /// deep) must be validated without overflowing the coordinator thread's
3126    /// stack, so the traversal must not recurse.
3127    #[mz_ore::test(tokio::test)]
3128    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
3129    async fn validate_read_then_write_deep_chain_no_stack_overflow() {
3130        use crate::coord::read_then_write::validate_read_then_write_dependencies;
3131
3132        Catalog::with_debug(|mut catalog| async move {
3133            // Deep enough that the previous recursive implementation overflowed
3134            // the stack.
3135            const DEPTH: usize = 100_000;
3136            const BASE: u64 = 1 << 40;
3137            insert_synthetic_view_chain(&mut catalog, BASE, DEPTH);
3138
3139            // A generous bound so this test isolates the no-overflow property
3140            // rather than the dependency limit.
3141            validate_read_then_write_dependencies(
3142                &catalog,
3143                [CatalogItemId::User(BASE)],
3144                usize::MAX,
3145            )
3146            .expect("deep chain of user views is valid for read-then-write");
3147
3148            catalog.expire().await;
3149        })
3150        .await
3151    }
3152
3153    /// Read-then-write dependency validation is bounded: a read set with more
3154    /// transitive dependencies than the limit is rejected with a clean error
3155    /// rather than walking an unbounded graph.
3156    #[mz_ore::test(tokio::test)]
3157    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
3158    async fn validate_read_then_write_dependency_limit() {
3159        use crate::coord::read_then_write::validate_read_then_write_dependencies;
3160        use crate::error::AdapterError;
3161
3162        Catalog::with_debug(|mut catalog| async move {
3163            const DEPTH: usize = 100;
3164            const BASE: u64 = 1 << 40;
3165            insert_synthetic_view_chain(&mut catalog, BASE, DEPTH);
3166
3167            // The chain has DEPTH + 1 distinct objects (root plus DEPTH links).
3168            const OBJECTS: usize = DEPTH + 1;
3169
3170            // Exactly at the limit is allowed.
3171            validate_read_then_write_dependencies(&catalog, [CatalogItemId::User(BASE)], OBJECTS)
3172                .expect("chain at the limit is valid");
3173
3174            // One below the limit is rejected with a clean error.
3175            let err = validate_read_then_write_dependencies(
3176                &catalog,
3177                [CatalogItemId::User(BASE)],
3178                OBJECTS - 1,
3179            )
3180            .expect_err("chain over the limit is rejected");
3181            assert!(matches!(
3182                err,
3183                AdapterError::ReadThenWriteDependencyLimitExceeded {
3184                    max_rw_dependencies
3185                } if max_rw_dependencies == OBJECTS - 1
3186            ));
3187
3188            catalog.expire().await;
3189        })
3190        .await
3191    }
3192
3193    /// Inserts a synthetic chain of `depth + 1` user views into `catalog` where
3194    /// view `base + i` reads from `base + i + 1`. Ids start well above any id
3195    /// the debug catalog assigns so they do not collide with real entries.
3196    ///
3197    /// Clones a builtin view as a template rather than constructing a `View` by
3198    /// hand. Read-then-write validation only reads the item type, `uses()`, and
3199    /// the optimized expression's temporal-ness, all of which a builtin view
3200    /// satisfies (user id + non-temporal).
3201    fn insert_synthetic_view_chain(catalog: &mut Catalog, base: u64, depth: usize) {
3202        use mz_ore::cast::CastFrom;
3203        use mz_sql::names::{DependencyIds, ResolvedIds};
3204
3205        let template = catalog
3206            .state
3207            .entry_by_id
3208            .values()
3209            .find(|entry| matches!(entry.item(), CatalogItem::View(_)))
3210            .expect("debug catalog has builtin views")
3211            .clone();
3212
3213        // `uses()` for a view unions `resolved_ids` and `dependencies`, so clear
3214        // both and point only at the next link.
3215        for i in 0..=depth {
3216            let id = CatalogItemId::User(base + u64::cast_from(i));
3217            let mut entry = template.clone();
3218            entry.id = id;
3219            entry.referenced_by = Vec::new();
3220            entry.used_by = Vec::new();
3221            let mut resolved_ids = ResolvedIds::empty();
3222            if i < depth {
3223                resolved_ids.add_item(CatalogItemId::User(base + u64::cast_from(i + 1)));
3224            }
3225            match &mut entry.item {
3226                CatalogItem::View(view) => {
3227                    view.resolved_ids = resolved_ids;
3228                    view.dependencies = DependencyIds(BTreeSet::new());
3229                }
3230                _ => unreachable!("template is a view"),
3231            }
3232            catalog.state.entry_by_id.insert(id, entry);
3233        }
3234    }
3235}