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