Skip to main content

mz_adapter/catalog/
state.rs

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