Skip to main content

mz_catalog/memory/
objects.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//! The current types used by the in-memory Catalog. Many of the objects in this module are
11//! extremely similar to the objects found in [`crate::durable::objects`] but in a format that is
12//! easier consumed by higher layers.
13
14use std::borrow::Cow;
15use std::collections::{BTreeMap, BTreeSet};
16use std::ops::{Deref, DerefMut};
17use std::sync::{Arc, LazyLock};
18use std::time::Duration;
19
20use chrono::{DateTime, Utc};
21use mz_adapter_types::compaction::CompactionWindow;
22use mz_adapter_types::connection::ConnectionId;
23use mz_compute_client::logging::LogVariant;
24use mz_compute_types::dataflows::DataflowDescription;
25use mz_compute_types::plan::LirRelationExpr as ComputePlan;
26use mz_controller::clusters::{ClusterRole, ClusterStatus, ReplicaConfig, ReplicaLogging};
27use mz_controller_types::{ClusterId, ReplicaId};
28use mz_expr::{MirScalarExpr, OptimizedMirRelationExpr};
29use mz_ore::collections::CollectionExt;
30use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap};
31use mz_repr::network_policy_id::NetworkPolicyId;
32use mz_repr::optimize::OptimizerFeatureOverrides;
33use mz_repr::refresh_schedule::RefreshSchedule;
34use mz_repr::role_id::RoleId;
35use mz_repr::{
36    CatalogItemId, ColumnName, Diff, GlobalId, RelationDesc, RelationVersion,
37    RelationVersionSelector, SqlColumnType, Timestamp, VersionedRelationDesc,
38};
39use mz_sql::ast::display::AstDisplay;
40use mz_sql::ast::{
41    ColumnDef, ColumnOption, ColumnOptionDef, ColumnVersioned, Expr, Raw, RawDataType, Statement,
42    UnresolvedItemName, Value, WithOptionValue,
43};
44use mz_sql::catalog::{
45    CatalogClusterReplica, CatalogError as SqlCatalogError, CatalogItem as SqlCatalogItem,
46    CatalogItemType as SqlCatalogItemType, CatalogItemType, CatalogSchema, CatalogType,
47    CatalogTypeDetails, DefaultPrivilegeAclItem, DefaultPrivilegeObject, IdReference,
48    RoleAttributes, RoleMembership, RoleVars, SystemObjectType,
49};
50use mz_sql::names::{
51    Aug, CommentObjectId, DatabaseId, DependencyIds, FullItemName, QualifiedItemName,
52    QualifiedSchemaName, ResolvedDatabaseSpecifier, ResolvedIds, SchemaId, SchemaSpecifier,
53};
54use mz_sql::plan::{
55    AutoScalingStrategy, ClusterSchedule, ComputeReplicaConfig, ComputeReplicaIntrospectionConfig,
56    ConnectionDetails, CreateClusterManagedPlan, CreateClusterPlan, CreateClusterVariant,
57    CreateSourcePlan, HirRelationExpr, NetworkPolicyRule, OnTimeoutAction, PlanError,
58    WebhookBodyFormat, WebhookHeaders, WebhookValidation,
59};
60use mz_sql::rbac;
61use mz_sql::session::vars::OwnedVarInput;
62use mz_storage_client::controller::IntrospectionType;
63use mz_storage_types::connections::inline::ReferencedConnection;
64use mz_storage_types::sinks::{SinkEnvelope, StorageSinkConnection};
65use mz_storage_types::sources::load_generator::LoadGenerator;
66use mz_storage_types::sources::{
67    GenericSourceConnection, SourceConnection, SourceDesc, SourceEnvelope, SourceExportDataConfig,
68    SourceExportDetails, Timeline,
69};
70use mz_transform::dataflow::DataflowMetainfo;
71use mz_transform::notice::OptimizerNotice;
72use serde::ser::SerializeSeq;
73use serde::{Deserialize, Serialize};
74use timely::progress::Antichain;
75use tracing::debug;
76
77use crate::builtin::{MZ_CATALOG_SERVER_CLUSTER, MZ_SYSTEM_CLUSTER};
78use crate::durable;
79
80/// Used to update `self` from the input value while consuming the input value.
81pub trait UpdateFrom<T>: From<T> {
82    fn update_from(&mut self, from: T);
83}
84
85#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
86pub struct Database {
87    pub name: String,
88    pub id: DatabaseId,
89    pub oid: u32,
90    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
91    pub schemas_by_id: BTreeMap<SchemaId, Schema>,
92    pub schemas_by_name: BTreeMap<String, SchemaId>,
93    pub owner_id: RoleId,
94    pub privileges: PrivilegeMap,
95}
96
97impl From<Database> for durable::Database {
98    fn from(database: Database) -> durable::Database {
99        durable::Database {
100            id: database.id,
101            oid: database.oid,
102            name: database.name,
103            owner_id: database.owner_id,
104            privileges: database.privileges.into_all_values().collect(),
105        }
106    }
107}
108
109impl From<durable::Database> for Database {
110    fn from(
111        durable::Database {
112            id,
113            oid,
114            name,
115            owner_id,
116            privileges,
117        }: durable::Database,
118    ) -> Database {
119        Database {
120            id,
121            oid,
122            schemas_by_id: BTreeMap::new(),
123            schemas_by_name: BTreeMap::new(),
124            name,
125            owner_id,
126            privileges: PrivilegeMap::from_mz_acl_items(privileges),
127        }
128    }
129}
130
131impl UpdateFrom<durable::Database> for Database {
132    fn update_from(
133        &mut self,
134        durable::Database {
135            id,
136            oid,
137            name,
138            owner_id,
139            privileges,
140        }: durable::Database,
141    ) {
142        self.id = id;
143        self.oid = oid;
144        self.name = name;
145        self.owner_id = owner_id;
146        self.privileges = PrivilegeMap::from_mz_acl_items(privileges);
147    }
148}
149
150#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
151pub struct Schema {
152    pub name: QualifiedSchemaName,
153    pub id: SchemaSpecifier,
154    pub oid: u32,
155    pub items: BTreeMap<String, CatalogItemId>,
156    pub functions: BTreeMap<String, CatalogItemId>,
157    pub types: BTreeMap<String, CatalogItemId>,
158    pub owner_id: RoleId,
159    pub privileges: PrivilegeMap,
160}
161
162impl From<Schema> for durable::Schema {
163    fn from(schema: Schema) -> durable::Schema {
164        durable::Schema {
165            id: schema.id.into(),
166            oid: schema.oid,
167            name: schema.name.schema,
168            database_id: schema.name.database.id(),
169            owner_id: schema.owner_id,
170            privileges: schema.privileges.into_all_values().collect(),
171        }
172    }
173}
174
175impl From<durable::Schema> for Schema {
176    fn from(
177        durable::Schema {
178            id,
179            oid,
180            name,
181            database_id,
182            owner_id,
183            privileges,
184        }: durable::Schema,
185    ) -> Schema {
186        Schema {
187            name: QualifiedSchemaName {
188                database: database_id.into(),
189                schema: name,
190            },
191            id: id.into(),
192            oid,
193            items: BTreeMap::new(),
194            functions: BTreeMap::new(),
195            types: BTreeMap::new(),
196            owner_id,
197            privileges: PrivilegeMap::from_mz_acl_items(privileges),
198        }
199    }
200}
201
202impl UpdateFrom<durable::Schema> for Schema {
203    fn update_from(
204        &mut self,
205        durable::Schema {
206            id,
207            oid,
208            name,
209            database_id,
210            owner_id,
211            privileges,
212        }: durable::Schema,
213    ) {
214        self.name = QualifiedSchemaName {
215            database: database_id.into(),
216            schema: name,
217        };
218        self.id = id.into();
219        self.oid = oid;
220        self.owner_id = owner_id;
221        self.privileges = PrivilegeMap::from_mz_acl_items(privileges);
222    }
223}
224
225#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
226pub struct Role {
227    pub name: String,
228    pub id: RoleId,
229    pub oid: u32,
230    pub attributes: RoleAttributes,
231    pub membership: RoleMembership,
232    pub vars: RoleVars,
233}
234
235impl Role {
236    pub fn is_user(&self) -> bool {
237        self.id.is_user()
238    }
239
240    pub fn vars<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a OwnedVarInput)> {
241        self.vars.map.iter().map(|(name, val)| (name.as_str(), val))
242    }
243}
244
245impl From<Role> for durable::Role {
246    fn from(role: Role) -> durable::Role {
247        durable::Role {
248            id: role.id,
249            oid: role.oid,
250            name: role.name,
251            attributes: role.attributes,
252            membership: role.membership,
253            vars: role.vars,
254        }
255    }
256}
257
258impl From<durable::Role> for Role {
259    fn from(
260        durable::Role {
261            id,
262            oid,
263            name,
264            attributes,
265            membership,
266            vars,
267        }: durable::Role,
268    ) -> Self {
269        Role {
270            name,
271            id,
272            oid,
273            attributes,
274            membership,
275            vars,
276        }
277    }
278}
279
280impl UpdateFrom<durable::Role> for Role {
281    fn update_from(
282        &mut self,
283        durable::Role {
284            id,
285            oid,
286            name,
287            attributes,
288            membership,
289            vars,
290        }: durable::Role,
291    ) {
292        self.id = id;
293        self.oid = oid;
294        self.name = name;
295        self.attributes = attributes;
296        self.membership = membership;
297        self.vars = vars;
298    }
299}
300
301#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
302pub struct RoleAuth {
303    pub role_id: RoleId,
304    pub password_hash: Option<String>,
305    pub updated_at: u64,
306}
307
308impl From<RoleAuth> for durable::RoleAuth {
309    fn from(role_auth: RoleAuth) -> durable::RoleAuth {
310        durable::RoleAuth {
311            role_id: role_auth.role_id,
312            password_hash: role_auth.password_hash,
313            updated_at: role_auth.updated_at,
314        }
315    }
316}
317
318impl From<durable::RoleAuth> for RoleAuth {
319    fn from(
320        durable::RoleAuth {
321            role_id,
322            password_hash,
323            updated_at,
324        }: durable::RoleAuth,
325    ) -> RoleAuth {
326        RoleAuth {
327            role_id,
328            password_hash,
329            updated_at,
330        }
331    }
332}
333
334impl UpdateFrom<durable::RoleAuth> for RoleAuth {
335    fn update_from(&mut self, from: durable::RoleAuth) {
336        self.role_id = from.role_id;
337        self.password_hash = from.password_hash;
338        self.updated_at = from.updated_at;
339    }
340}
341
342#[derive(Debug, Serialize, Clone, PartialEq)]
343pub struct Cluster {
344    pub name: String,
345    pub id: ClusterId,
346    pub config: ClusterConfig,
347    #[serde(skip)]
348    pub log_indexes: BTreeMap<LogVariant, GlobalId>,
349    /// Objects bound to this cluster. Does not include introspection source
350    /// indexes.
351    pub bound_objects: BTreeSet<CatalogItemId>,
352    pub replica_id_by_name_: BTreeMap<String, ReplicaId>,
353    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
354    pub replicas_by_id_: BTreeMap<ReplicaId, ClusterReplica>,
355    pub owner_id: RoleId,
356    pub privileges: PrivilegeMap,
357}
358
359impl Cluster {
360    /// The role of the cluster. Currently used to set alert severity.
361    pub fn role(&self) -> ClusterRole {
362        // NOTE - These roles power monitoring systems. Do not change
363        // them without talking to the cloud or observability groups.
364        if self.name == MZ_SYSTEM_CLUSTER.name {
365            ClusterRole::SystemCritical
366        } else if self.name == MZ_CATALOG_SERVER_CLUSTER.name {
367            ClusterRole::System
368        } else {
369            ClusterRole::User
370        }
371    }
372
373    /// Returns `true` if the cluster is a managed cluster.
374    pub fn is_managed(&self) -> bool {
375        matches!(self.config.variant, ClusterVariant::Managed { .. })
376    }
377
378    /// Lists the user replicas, which are those that do not have the internal flag set.
379    pub fn user_replicas(&self) -> impl Iterator<Item = &ClusterReplica> {
380        self.replicas().filter(|r| !r.config.location.internal())
381    }
382
383    /// Lists all replicas in the cluster
384    pub fn replicas(&self) -> impl Iterator<Item = &ClusterReplica> {
385        self.replicas_by_id_.values()
386    }
387
388    /// Lookup a replica by ID.
389    pub fn replica(&self, replica_id: ReplicaId) -> Option<&ClusterReplica> {
390        self.replicas_by_id_.get(&replica_id)
391    }
392
393    /// Lookup a replica ID by name.
394    pub fn replica_id(&self, name: &str) -> Option<ReplicaId> {
395        self.replica_id_by_name_.get(name).copied()
396    }
397
398    /// Returns the availability zones of this cluster, if they exist.
399    pub fn availability_zones(&self) -> Option<&[String]> {
400        match &self.config.variant {
401            ClusterVariant::Managed(managed) => Some(&managed.availability_zones),
402            ClusterVariant::Unmanaged => None,
403        }
404    }
405
406    /// Builds the plan that a `CREATE CLUSTER` statement for this cluster would
407    /// have produced.
408    ///
409    /// Clusters are stored as a structured config, not as SQL text, so `SHOW
410    /// CREATE CLUSTER` cannot just print a stored statement. It rebuilds one,
411    /// and this is the first half of that: config to plan. `unplan_create_cluster`
412    /// then turns the plan back into a statement.
413    pub fn try_to_plan(&self) -> Result<CreateClusterPlan, PlanError> {
414        let name = self.name.clone();
415        let variant = match &self.config.variant {
416            ClusterVariant::Managed(ClusterVariantManaged {
417                size,
418                availability_zones,
419                logging,
420                arrangement_compression,
421                replication_factor,
422                optimizer_feature_overrides,
423                schedule,
424                auto_scaling_strategy,
425                // In-flight runtime records, controller-managed and not part of
426                // the create statement.
427                reconfiguration: _,
428                burst: _,
429            }) => {
430                let introspection = match logging {
431                    ReplicaLogging {
432                        log_logging,
433                        interval: Some(interval),
434                    } => Some(ComputeReplicaIntrospectionConfig {
435                        debugging: *log_logging,
436                        interval: interval.clone(),
437                    }),
438                    ReplicaLogging {
439                        log_logging: _,
440                        interval: None,
441                    } => None,
442                };
443                let compute = ComputeReplicaConfig {
444                    introspection,
445                    arrangement_compression: *arrangement_compression,
446                };
447                CreateClusterVariant::Managed(CreateClusterManagedPlan {
448                    replication_factor: replication_factor.clone(),
449                    size: size.clone(),
450                    availability_zones: availability_zones.clone(),
451                    compute,
452                    optimizer_feature_overrides: optimizer_feature_overrides.clone(),
453                    schedule: schedule.clone(),
454                    auto_scaling_strategy: auto_scaling_strategy.clone(),
455                })
456            }
457            ClusterVariant::Unmanaged => {
458                // Unmanaged clusters are deprecated, so hopefully we can remove
459                // them before we have to implement this.
460                return Err(PlanError::Unsupported {
461                    feature: "SHOW CREATE for unmanaged clusters".to_string(),
462                    discussion_no: None,
463                });
464            }
465        };
466        let workload_class = self.config.workload_class.clone();
467        Ok(CreateClusterPlan {
468            name,
469            variant,
470            workload_class,
471            // Nothing about `IF NOT EXISTS` is stored. It only ever affected how
472            // the original statement handled a name collision, so the rebuilt
473            // statement never shows it.
474            if_not_exists: false,
475        })
476    }
477}
478
479impl From<Cluster> for durable::Cluster {
480    fn from(cluster: Cluster) -> durable::Cluster {
481        durable::Cluster {
482            id: cluster.id,
483            name: cluster.name,
484            owner_id: cluster.owner_id,
485            privileges: cluster.privileges.into_all_values().collect(),
486            config: cluster.config.into(),
487        }
488    }
489}
490
491impl From<durable::Cluster> for Cluster {
492    fn from(
493        durable::Cluster {
494            id,
495            name,
496            owner_id,
497            privileges,
498            config,
499        }: durable::Cluster,
500    ) -> Self {
501        Cluster {
502            name: name.clone(),
503            id,
504            bound_objects: BTreeSet::new(),
505            log_indexes: BTreeMap::new(),
506            replica_id_by_name_: BTreeMap::new(),
507            replicas_by_id_: BTreeMap::new(),
508            owner_id,
509            privileges: PrivilegeMap::from_mz_acl_items(privileges),
510            config: config.into(),
511        }
512    }
513}
514
515impl UpdateFrom<durable::Cluster> for Cluster {
516    fn update_from(
517        &mut self,
518        durable::Cluster {
519            id,
520            name,
521            owner_id,
522            privileges,
523            config,
524        }: durable::Cluster,
525    ) {
526        self.id = id;
527        self.name = name;
528        self.owner_id = owner_id;
529        self.privileges = PrivilegeMap::from_mz_acl_items(privileges);
530        self.config = config.into();
531    }
532}
533
534#[derive(Debug, Serialize, Clone, PartialEq)]
535pub struct ClusterReplica {
536    pub name: String,
537    pub cluster_id: ClusterId,
538    pub replica_id: ReplicaId,
539    pub config: ReplicaConfig,
540    pub owner_id: RoleId,
541}
542
543impl From<ClusterReplica> for durable::ClusterReplica {
544    fn from(replica: ClusterReplica) -> durable::ClusterReplica {
545        durable::ClusterReplica {
546            cluster_id: replica.cluster_id,
547            replica_id: replica.replica_id,
548            name: replica.name,
549            config: replica.config.into(),
550            owner_id: replica.owner_id,
551        }
552    }
553}
554
555#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
556pub struct ClusterReplicaProcessStatus {
557    pub status: ClusterStatus,
558    /// Cumulative restart count of the process, mirrored from the orchestrator.
559    /// See [`mz_orchestrator::ServiceEvent::restart_count`].
560    pub restart_count: u64,
561    /// Time of the most recent change to `status` or `restart_count`.
562    pub time: DateTime<Utc>,
563}
564
565#[derive(Debug, Serialize, Clone, PartialEq)]
566pub struct SourceReferences {
567    pub updated_at: u64,
568    pub references: Vec<SourceReference>,
569}
570
571#[derive(Debug, Serialize, Clone, PartialEq)]
572pub struct SourceReference {
573    pub name: String,
574    pub namespace: Option<String>,
575    pub columns: Vec<String>,
576}
577
578impl From<SourceReference> for durable::SourceReference {
579    fn from(source_reference: SourceReference) -> durable::SourceReference {
580        durable::SourceReference {
581            name: source_reference.name,
582            namespace: source_reference.namespace,
583            columns: source_reference.columns,
584        }
585    }
586}
587
588impl SourceReferences {
589    pub fn to_durable(self, source_id: CatalogItemId) -> durable::SourceReferences {
590        durable::SourceReferences {
591            source_id,
592            updated_at: self.updated_at,
593            references: self.references.into_iter().map(Into::into).collect(),
594        }
595    }
596}
597
598impl From<durable::SourceReference> for SourceReference {
599    fn from(source_reference: durable::SourceReference) -> SourceReference {
600        SourceReference {
601            name: source_reference.name,
602            namespace: source_reference.namespace,
603            columns: source_reference.columns,
604        }
605    }
606}
607
608impl From<durable::SourceReferences> for SourceReferences {
609    fn from(source_references: durable::SourceReferences) -> SourceReferences {
610        SourceReferences {
611            updated_at: source_references.updated_at,
612            references: source_references
613                .references
614                .into_iter()
615                .map(|source_reference| source_reference.into())
616                .collect(),
617        }
618    }
619}
620
621impl From<mz_sql::plan::SourceReference> for SourceReference {
622    fn from(source_reference: mz_sql::plan::SourceReference) -> SourceReference {
623        SourceReference {
624            name: source_reference.name,
625            namespace: source_reference.namespace,
626            columns: source_reference.columns,
627        }
628    }
629}
630
631impl From<mz_sql::plan::SourceReferences> for SourceReferences {
632    fn from(source_references: mz_sql::plan::SourceReferences) -> SourceReferences {
633        SourceReferences {
634            updated_at: source_references.updated_at,
635            references: source_references
636                .references
637                .into_iter()
638                .map(|source_reference| source_reference.into())
639                .collect(),
640        }
641    }
642}
643
644impl From<SourceReferences> for mz_sql::plan::SourceReferences {
645    fn from(source_references: SourceReferences) -> mz_sql::plan::SourceReferences {
646        mz_sql::plan::SourceReferences {
647            updated_at: source_references.updated_at,
648            references: source_references
649                .references
650                .into_iter()
651                .map(|source_reference| source_reference.into())
652                .collect(),
653        }
654    }
655}
656
657impl From<SourceReference> for mz_sql::plan::SourceReference {
658    fn from(source_reference: SourceReference) -> mz_sql::plan::SourceReference {
659        mz_sql::plan::SourceReference {
660            name: source_reference.name,
661            namespace: source_reference.namespace,
662            columns: source_reference.columns,
663        }
664    }
665}
666
667#[derive(Clone, Debug, Serialize)]
668pub struct CatalogEntry {
669    pub item: CatalogItem,
670    #[serde(skip)]
671    pub referenced_by: Vec<CatalogItemId>,
672    // TODO(database-issues#7922)––this should have an invariant tied to it that all
673    // dependents (i.e. entries in this field) have IDs greater than this
674    // entry's ID.
675    #[serde(skip)]
676    pub used_by: Vec<CatalogItemId>,
677    pub id: CatalogItemId,
678    pub oid: u32,
679    pub name: QualifiedItemName,
680    pub owner_id: RoleId,
681    pub privileges: PrivilegeMap,
682}
683
684/// A [`CatalogEntry`] that is associated with a specific "collection" of data.
685/// A single item in the catalog may be associated with multiple "collections".
686///
687/// Here "collection" generally means a pTVC, e.g. a Persist Shard, an Index, a
688/// currently running dataflow, etc.
689///
690/// Items in the Catalog have a stable name -> ID mapping, in other words for
691/// the entire lifetime of an object its [`CatalogItemId`] will _never_ change.
692/// Similarly, we need to maintain a stable mapping from [`GlobalId`] to pTVC.
693/// This presents a challenge when `ALTER`-ing an object, e.g. adding columns
694/// to a table. We can't just change the schema of the underlying Persist Shard
695/// because that would be rebinding the [`GlobalId`] of the pTVC. Instead we
696/// allocate a new [`GlobalId`] to refer to the new version of the table, and
697/// then the [`CatalogEntry`] tracks the [`GlobalId`] for each version.
698#[derive(Clone, Debug)]
699pub struct CatalogCollectionEntry {
700    pub entry: CatalogEntry,
701    pub version: RelationVersionSelector,
702}
703
704impl CatalogCollectionEntry {
705    pub fn relation_desc(&self) -> Option<Cow<'_, RelationDesc>> {
706        self.item().relation_desc(self.version)
707    }
708}
709
710impl mz_sql::catalog::CatalogCollectionItem for CatalogCollectionEntry {
711    fn relation_desc(&self) -> Option<Cow<'_, RelationDesc>> {
712        self.item().relation_desc(self.version)
713    }
714
715    fn global_id(&self) -> GlobalId {
716        self.entry
717            .item()
718            .global_id_for_version(self.version)
719            .expect("catalog corruption, missing version!")
720    }
721}
722
723impl Deref for CatalogCollectionEntry {
724    type Target = CatalogEntry;
725
726    fn deref(&self) -> &CatalogEntry {
727        &self.entry
728    }
729}
730
731impl mz_sql::catalog::CatalogItem for CatalogCollectionEntry {
732    fn name(&self) -> &QualifiedItemName {
733        self.entry.name()
734    }
735
736    fn id(&self) -> CatalogItemId {
737        self.entry.id()
738    }
739
740    fn global_ids(&self) -> Box<dyn Iterator<Item = GlobalId> + '_> {
741        Box::new(self.entry.global_ids())
742    }
743
744    fn oid(&self) -> u32 {
745        self.entry.oid()
746    }
747
748    fn func(&self) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
749        self.entry.func()
750    }
751
752    fn source_desc(&self) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
753        self.entry.source_desc()
754    }
755
756    fn connection(
757        &self,
758    ) -> Result<mz_storage_types::connections::Connection<ReferencedConnection>, SqlCatalogError>
759    {
760        mz_sql::catalog::CatalogItem::connection(&self.entry)
761    }
762
763    fn create_sql(&self) -> &str {
764        self.entry.create_sql()
765    }
766
767    fn item_type(&self) -> SqlCatalogItemType {
768        self.entry.item_type()
769    }
770
771    fn index_details(&self) -> Option<(&[MirScalarExpr], GlobalId)> {
772        self.entry.index_details()
773    }
774
775    fn writable_table_details(&self) -> Option<&[Expr<Aug>]> {
776        self.entry.writable_table_details()
777    }
778
779    fn replacement_target(&self) -> Option<CatalogItemId> {
780        self.entry.replacement_target()
781    }
782
783    fn type_details(&self) -> Option<&CatalogTypeDetails<IdReference>> {
784        self.entry.type_details()
785    }
786
787    fn references(&self) -> &ResolvedIds {
788        self.entry.references()
789    }
790
791    fn uses(&self) -> BTreeSet<CatalogItemId> {
792        self.entry.uses()
793    }
794
795    fn referenced_by(&self) -> &[CatalogItemId] {
796        self.entry.referenced_by()
797    }
798
799    fn used_by(&self) -> &[CatalogItemId] {
800        self.entry.used_by()
801    }
802
803    fn subsource_details(
804        &self,
805    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)> {
806        self.entry.subsource_details()
807    }
808
809    fn source_export_details(
810        &self,
811    ) -> Option<(
812        CatalogItemId,
813        &UnresolvedItemName,
814        &SourceExportDetails,
815        &SourceExportDataConfig<ReferencedConnection>,
816    )> {
817        self.entry.source_export_details()
818    }
819
820    fn is_progress_source(&self) -> bool {
821        self.entry.is_progress_source()
822    }
823
824    fn progress_id(&self) -> Option<CatalogItemId> {
825        self.entry.progress_id()
826    }
827
828    fn owner_id(&self) -> RoleId {
829        *self.entry.owner_id()
830    }
831
832    fn privileges(&self) -> &PrivilegeMap {
833        self.entry.privileges()
834    }
835
836    fn cluster_id(&self) -> Option<ClusterId> {
837        self.entry.item().cluster_id()
838    }
839
840    fn at_version(
841        &self,
842        version: RelationVersionSelector,
843    ) -> Box<dyn mz_sql::catalog::CatalogCollectionItem> {
844        Box::new(CatalogCollectionEntry {
845            entry: self.entry.clone(),
846            version,
847        })
848    }
849
850    fn latest_version(&self) -> Option<RelationVersion> {
851        self.entry.latest_version()
852    }
853}
854
855#[derive(Debug, Clone, Serialize)]
856pub enum CatalogItem {
857    Table(Table),
858    Source(Source),
859    Log(Log),
860    View(View),
861    MaterializedView(MaterializedView),
862    Sink(Sink),
863    Index(Index),
864    Type(Type),
865    Func(Func),
866    Secret(Secret),
867    Connection(Connection),
868    MetricSink(MetricSink),
869}
870
871#[derive(Debug, Clone, Serialize)]
872pub struct Table {
873    /// Parse-able SQL that defines this table.
874    pub create_sql: Option<String>,
875    /// [`VersionedRelationDesc`] of this table, derived from the `create_sql`.
876    pub desc: VersionedRelationDesc,
877    /// Versions of this table, and the [`GlobalId`]s that refer to them.
878    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
879    pub collections: BTreeMap<RelationVersion, GlobalId>,
880    /// If created in the `TEMPORARY` schema, the [`ConnectionId`] for that session.
881    #[serde(skip)]
882    pub conn_id: Option<ConnectionId>,
883    /// Other catalog objects referenced by this table, e.g. custom types.
884    pub resolved_ids: ResolvedIds,
885    /// Custom compaction window, e.g. set via `ALTER RETAIN HISTORY`.
886    pub custom_logical_compaction_window: Option<CompactionWindow>,
887    /// Whether the table's logical compaction window is controlled by the ['metrics_retention']
888    /// session variable.
889    ///
890    /// ['metrics_retention']: mz_sql::session::vars::METRICS_RETENTION
891    pub is_retained_metrics_object: bool,
892    /// Where data for this table comes from, e.g. `INSERT` statements or an upstream source.
893    pub data_source: TableDataSource,
894}
895
896impl Table {
897    pub fn timeline(&self) -> Timeline {
898        match &self.data_source {
899            // The Coordinator controls insertions for writable tables
900            // (including system tables), so they are realtime.
901            TableDataSource::TableWrites { .. } => Timeline::EpochMilliseconds,
902            TableDataSource::DataSource { timeline, .. } => timeline.clone(),
903        }
904    }
905
906    /// Returns all of the [`GlobalId`]s that this [`Table`] can be referenced by.
907    pub fn global_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
908        self.collections.values().copied()
909    }
910
911    /// Returns the latest [`GlobalId`] for this [`Table`] which should be used for writes.
912    pub fn global_id_writes(&self) -> GlobalId {
913        *self
914            .collections
915            .last_key_value()
916            .expect("at least one version of a table")
917            .1
918    }
919
920    /// Returns all of the collections and their [`RelationDesc`]s associated with this [`Table`].
921    pub fn collection_descs(
922        &self,
923    ) -> impl Iterator<Item = (GlobalId, RelationVersion, RelationDesc)> + '_ {
924        self.collections.iter().map(|(version, gid)| {
925            let desc = self
926                .desc
927                .at_version(RelationVersionSelector::Specific(*version));
928            (*gid, *version, desc)
929        })
930    }
931
932    /// Returns the [`RelationDesc`] for a specific [`GlobalId`].
933    pub fn desc_for(&self, id: &GlobalId) -> RelationDesc {
934        let (version, _gid) = self
935            .collections
936            .iter()
937            .find(|(_version, gid)| *gid == id)
938            .expect("GlobalId to exist");
939        self.desc
940            .at_version(RelationVersionSelector::Specific(*version))
941    }
942}
943
944#[derive(Clone, Debug, Serialize)]
945pub enum TableDataSource {
946    /// The table owns data created via INSERT/UPDATE/DELETE statements.
947    TableWrites {
948        #[serde(skip)]
949        defaults: Vec<Expr<Aug>>,
950    },
951
952    /// The table receives its data from the identified `DataSourceDesc`.
953    /// This table type does not support INSERT/UPDATE/DELETE statements.
954    DataSource {
955        desc: DataSourceDesc,
956        timeline: Timeline,
957    },
958}
959
960#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
961pub enum DataSourceDesc {
962    /// Receives data from an external system
963    Ingestion {
964        desc: SourceDesc<ReferencedConnection>,
965        cluster_id: ClusterId,
966    },
967    /// Receives data from an external system
968    OldSyntaxIngestion {
969        desc: SourceDesc<ReferencedConnection>,
970        cluster_id: ClusterId,
971        // If we're dealing with an old syntax ingestion the progress id will be some other collection
972        // and the ingestion itself will have the data from an external reference
973        progress_subsource: CatalogItemId,
974        data_config: SourceExportDataConfig<ReferencedConnection>,
975        details: SourceExportDetails,
976    },
977    /// This source receives its data from the identified ingestion,
978    /// specifically the output identified by `external_reference`.
979    /// N.B. that `external_reference` should not be used to identify
980    /// anything downstream of purification, as the purification process
981    /// encodes source-specific identifiers into the `details` struct.
982    /// The `external_reference` field is only used here for displaying
983    /// human-readable names in system tables.
984    IngestionExport {
985        ingestion_id: CatalogItemId,
986        external_reference: UnresolvedItemName,
987        details: SourceExportDetails,
988        data_config: SourceExportDataConfig<ReferencedConnection>,
989    },
990    /// Receives introspection data from an internal system
991    Introspection(IntrospectionType),
992    /// Receives data from the source's reclocking/remapping operations.
993    Progress,
994    /// Receives data from HTTP requests.
995    Webhook {
996        /// Optional components used to validation a webhook request.
997        validate_using: Option<WebhookValidation>,
998        /// Describes how we deserialize the body of a webhook request.
999        body_format: WebhookBodyFormat,
1000        /// Describes whether or not to include headers and how to map them.
1001        headers: WebhookHeaders,
1002        /// The cluster which this source is associated with.
1003        cluster_id: ClusterId,
1004    },
1005    /// Exposes the contents of the catalog shard.
1006    Catalog,
1007}
1008
1009impl From<IntrospectionType> for DataSourceDesc {
1010    fn from(typ: IntrospectionType) -> Self {
1011        Self::Introspection(typ)
1012    }
1013}
1014
1015impl DataSourceDesc {
1016    /// The key and value formats of the data source.
1017    pub fn formats(&self) -> (Option<&str>, Option<&str>) {
1018        match &self {
1019            DataSourceDesc::Ingestion { .. } => (None, None),
1020            DataSourceDesc::OldSyntaxIngestion { data_config, .. } => {
1021                match &data_config.encoding.as_ref() {
1022                    Some(encoding) => match &encoding.key {
1023                        Some(key) => (Some(key.type_()), Some(encoding.value.type_())),
1024                        None => (None, Some(encoding.value.type_())),
1025                    },
1026                    None => (None, None),
1027                }
1028            }
1029            DataSourceDesc::IngestionExport { data_config, .. } => match &data_config.encoding {
1030                Some(encoding) => match &encoding.key {
1031                    Some(key) => (Some(key.type_()), Some(encoding.value.type_())),
1032                    None => (None, Some(encoding.value.type_())),
1033                },
1034                None => (None, None),
1035            },
1036            DataSourceDesc::Introspection(_)
1037            | DataSourceDesc::Webhook { .. }
1038            | DataSourceDesc::Progress
1039            | DataSourceDesc::Catalog => (None, None),
1040        }
1041    }
1042
1043    /// Envelope of the data source.
1044    pub fn envelope(&self) -> Option<&str> {
1045        // Note how "none"/"append-only" is different from `None`. Source
1046        // sources don't have an envelope (internal logs, for example), while
1047        // other sources have an envelope that we call the "NONE"-envelope.
1048
1049        fn envelope_string(envelope: &SourceEnvelope) -> &str {
1050            match envelope {
1051                SourceEnvelope::None(_) => "none",
1052                SourceEnvelope::Upsert(upsert_envelope) => match upsert_envelope.style {
1053                    mz_storage_types::sources::envelope::UpsertStyle::Default(_) => "upsert",
1054                    mz_storage_types::sources::envelope::UpsertStyle::Debezium { .. } => {
1055                        // NOTE(aljoscha): Should we somehow mark that this is
1056                        // using upsert internally? See note above about
1057                        // DEBEZIUM.
1058                        "debezium"
1059                    }
1060                    mz_storage_types::sources::envelope::UpsertStyle::ValueErrInline { .. } => {
1061                        "upsert-value-err-inline"
1062                    }
1063                },
1064                SourceEnvelope::CdcV2 => {
1065                    // TODO(aljoscha): Should we even report this? It's
1066                    // currently not exposed.
1067                    "materialize"
1068                }
1069            }
1070        }
1071
1072        match self {
1073            // NOTE(aljoscha): We could move the block for ingestions into
1074            // `SourceEnvelope` itself, but that one feels more like an internal
1075            // thing and adapter should own how we represent envelopes as a
1076            // string? It would not be hard to convince me otherwise, though.
1077            DataSourceDesc::Ingestion { .. } => None,
1078            DataSourceDesc::OldSyntaxIngestion { data_config, .. } => {
1079                Some(envelope_string(&data_config.envelope))
1080            }
1081            DataSourceDesc::IngestionExport { data_config, .. } => {
1082                Some(envelope_string(&data_config.envelope))
1083            }
1084            DataSourceDesc::Introspection(_)
1085            | DataSourceDesc::Webhook { .. }
1086            | DataSourceDesc::Progress
1087            | DataSourceDesc::Catalog => None,
1088        }
1089    }
1090}
1091
1092#[derive(Debug, Clone, Serialize)]
1093pub struct Source {
1094    /// Parse-able SQL that defines this table.
1095    pub create_sql: Option<String>,
1096    /// [`GlobalId`] used to reference this source from outside the catalog.
1097    pub global_id: GlobalId,
1098    // TODO: Unskip: currently blocked on some inner BTreeMap<X, _> problems.
1099    #[serde(skip)]
1100    pub data_source: DataSourceDesc,
1101    /// [`RelationDesc`] of this source, derived from the `create_sql`.
1102    pub desc: RelationDesc,
1103    /// The timeline this source exists on.
1104    pub timeline: Timeline,
1105    /// Other catalog objects referenced by this table, e.g. custom types.
1106    pub resolved_ids: ResolvedIds,
1107    /// This value is ignored for subsources, i.e. for
1108    /// [`DataSourceDesc::IngestionExport`]. Instead, it uses the primary
1109    /// sources logical compaction window.
1110    pub custom_logical_compaction_window: Option<CompactionWindow>,
1111    /// Whether the source's logical compaction window is controlled by
1112    /// METRICS_RETENTION
1113    pub is_retained_metrics_object: bool,
1114}
1115
1116impl Source {
1117    /// Creates a new `Source`.
1118    ///
1119    /// # Panics
1120    /// - If an ingestion-based plan is not given a cluster_id.
1121    /// - If a non-ingestion-based source has a defined cluster config in its plan.
1122    /// - If a non-ingestion-based source is given a cluster_id.
1123    pub fn new(
1124        plan: CreateSourcePlan,
1125        global_id: GlobalId,
1126        resolved_ids: ResolvedIds,
1127        custom_logical_compaction_window: Option<CompactionWindow>,
1128        is_retained_metrics_object: bool,
1129    ) -> Source {
1130        Source {
1131            create_sql: Some(plan.source.create_sql),
1132            data_source: match plan.source.data_source {
1133                mz_sql::plan::DataSourceDesc::Ingestion(desc) => DataSourceDesc::Ingestion {
1134                    desc,
1135                    cluster_id: plan
1136                        .in_cluster
1137                        .expect("ingestion-based sources must be given a cluster ID"),
1138                },
1139                mz_sql::plan::DataSourceDesc::OldSyntaxIngestion {
1140                    desc,
1141                    progress_subsource,
1142                    data_config,
1143                    details,
1144                } => DataSourceDesc::OldSyntaxIngestion {
1145                    desc,
1146                    cluster_id: plan
1147                        .in_cluster
1148                        .expect("ingestion-based sources must be given a cluster ID"),
1149                    progress_subsource,
1150                    data_config,
1151                    details,
1152                },
1153                mz_sql::plan::DataSourceDesc::Progress => {
1154                    assert!(
1155                        plan.in_cluster.is_none(),
1156                        "subsources must not have a host config or cluster_id defined"
1157                    );
1158                    DataSourceDesc::Progress
1159                }
1160                mz_sql::plan::DataSourceDesc::IngestionExport {
1161                    ingestion_id,
1162                    external_reference,
1163                    details,
1164                    data_config,
1165                } => {
1166                    assert!(
1167                        plan.in_cluster.is_none(),
1168                        "subsources must not have a host config or cluster_id defined"
1169                    );
1170                    DataSourceDesc::IngestionExport {
1171                        ingestion_id,
1172                        external_reference,
1173                        details,
1174                        data_config,
1175                    }
1176                }
1177                mz_sql::plan::DataSourceDesc::Webhook {
1178                    validate_using,
1179                    body_format,
1180                    headers,
1181                    cluster_id,
1182                } => {
1183                    mz_ore::soft_assert_or_log!(
1184                        cluster_id.is_none(),
1185                        "cluster_id set at Source level for Webhooks"
1186                    );
1187                    DataSourceDesc::Webhook {
1188                        validate_using,
1189                        body_format,
1190                        headers,
1191                        cluster_id: plan
1192                            .in_cluster
1193                            .expect("webhook sources must be given a cluster ID"),
1194                    }
1195                }
1196            },
1197            desc: plan.source.desc,
1198            global_id,
1199            timeline: plan.timeline,
1200            resolved_ids,
1201            custom_logical_compaction_window: plan
1202                .source
1203                .compaction_window
1204                .or(custom_logical_compaction_window),
1205            is_retained_metrics_object,
1206        }
1207    }
1208
1209    /// Type of the source.
1210    pub fn source_type(&self) -> &str {
1211        match &self.data_source {
1212            DataSourceDesc::Ingestion { desc, .. }
1213            | DataSourceDesc::OldSyntaxIngestion { desc, .. } => desc.connection.name(),
1214            DataSourceDesc::Progress => "progress",
1215            DataSourceDesc::IngestionExport { .. } => "subsource",
1216            DataSourceDesc::Introspection(_) | DataSourceDesc::Catalog => "source",
1217            DataSourceDesc::Webhook { .. } => "webhook",
1218        }
1219    }
1220
1221    /// Connection ID of the source, if one exists.
1222    pub fn connection_id(&self) -> Option<CatalogItemId> {
1223        match &self.data_source {
1224            DataSourceDesc::Ingestion { desc, .. }
1225            | DataSourceDesc::OldSyntaxIngestion { desc, .. } => desc.connection.connection_id(),
1226            DataSourceDesc::IngestionExport { .. }
1227            | DataSourceDesc::Introspection(_)
1228            | DataSourceDesc::Webhook { .. }
1229            | DataSourceDesc::Progress
1230            | DataSourceDesc::Catalog => None,
1231        }
1232    }
1233
1234    /// The single [`GlobalId`] that refers to this Source.
1235    pub fn global_id(&self) -> GlobalId {
1236        self.global_id
1237    }
1238
1239    /// The expensive resource that each source consumes is persist shards. To
1240    /// prevent abuse, we want to prevent users from creating sources that use an
1241    /// unbounded number of persist shards. But we also don't want to count
1242    /// persist shards that are mandated by the system (e.g., the progress
1243    /// shard) so that future versions of Materialize can introduce additional
1244    /// per-source shards (e.g., a per-source status shard) without impacting
1245    /// the limit calculation.
1246    pub fn user_controllable_persist_shard_count(&self) -> i64 {
1247        match &self.data_source {
1248            DataSourceDesc::Ingestion { .. } => 0,
1249            DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1250                match &desc.connection {
1251                    // These multi-output sources do not use their primary
1252                    // source's data shard, so we don't include it in accounting
1253                    // for users.
1254                    GenericSourceConnection::Postgres(_)
1255                    | GenericSourceConnection::MySql(_)
1256                    | GenericSourceConnection::SqlServer(_) => 0,
1257                    GenericSourceConnection::LoadGenerator(lg) => match lg.load_generator {
1258                        // Load generators that output data in their primary shard
1259                        LoadGenerator::Clock
1260                        | LoadGenerator::Counter { .. }
1261                        | LoadGenerator::Datums
1262                        | LoadGenerator::KeyValue(_) => 1,
1263                        LoadGenerator::Auction
1264                        | LoadGenerator::Marketing
1265                        | LoadGenerator::Tpch { .. } => 0,
1266                    },
1267                    GenericSourceConnection::Kafka(_) => 1,
1268                }
1269            }
1270            //  DataSourceDesc::IngestionExport represents a subsource, which
1271            //  use a data shard.
1272            DataSourceDesc::IngestionExport { .. } => 1,
1273            DataSourceDesc::Webhook { .. } => 1,
1274            // Introspection, catalog, and progress subsources are not under the user's control, so
1275            // shouldn't count toward their quota.
1276            DataSourceDesc::Introspection(_)
1277            | DataSourceDesc::Progress
1278            | DataSourceDesc::Catalog => 0,
1279        }
1280    }
1281}
1282
1283#[derive(Debug, Clone, Serialize)]
1284pub struct Log {
1285    /// The category of data this log stores.
1286    pub variant: LogVariant,
1287    /// [`GlobalId`] used to reference this log from outside the catalog.
1288    pub global_id: GlobalId,
1289}
1290
1291impl Log {
1292    /// The single [`GlobalId`] that refers to this Log.
1293    pub fn global_id(&self) -> GlobalId {
1294        self.global_id
1295    }
1296}
1297
1298#[derive(Debug, Clone, Serialize)]
1299pub struct Sink {
1300    /// Parse-able SQL that defines this sink.
1301    pub create_sql: String,
1302    /// [`GlobalId`] used to reference this sink from outside the catalog, e.g storage.
1303    pub global_id: GlobalId,
1304    /// Collection we read into this sink.
1305    pub from: GlobalId,
1306    /// Connection to the external service we're sinking into, e.g. Kafka.
1307    pub connection: StorageSinkConnection<ReferencedConnection>,
1308    /// Envelope we use to sink into the external system.
1309    ///
1310    /// TODO(guswynn): this probably should just be in the `connection`.
1311    pub envelope: SinkEnvelope,
1312    /// Emit an initial snapshot into the sink.
1313    pub with_snapshot: bool,
1314    /// Used to fence other writes into this sink as we evolve the upstream materialized view.
1315    pub version: u64,
1316    /// Other catalog objects this sink references.
1317    pub resolved_ids: ResolvedIds,
1318    /// Cluster this sink runs on.
1319    pub cluster_id: ClusterId,
1320    /// Commit interval for the sink.
1321    pub commit_interval: Option<Duration>,
1322}
1323
1324impl Sink {
1325    pub fn sink_type(&self) -> &str {
1326        self.connection.name()
1327    }
1328
1329    /// Envelope of the sink.
1330    ///
1331    /// NOTE: the `mz_sinks` view works this out from `create_sql` instead, in
1332    /// the `CreateSink` arm of `parse_catalog_create_sql`. Kafka gets it from
1333    /// `ENVELOPE` there, iceberg from `MODE`. Change both or they drift.
1334    pub fn envelope(&self) -> Option<&str> {
1335        match &self.envelope {
1336            SinkEnvelope::Debezium => Some("debezium"),
1337            SinkEnvelope::Upsert => Some("upsert"),
1338            SinkEnvelope::Append => Some("append"),
1339        }
1340    }
1341
1342    /// Output a combined format string of the sink. For legacy reasons
1343    /// if the key-format is none or the key & value formats are
1344    /// both the same (either avro or json), we return the value format name,
1345    /// otherwise we return a composite name.
1346    ///
1347    /// NOTE: `parse_catalog_create_sql` redoes this collapse for the `mz_sinks`
1348    /// `format` column. Change both or they drift.
1349    pub fn combined_format(&self) -> Option<Cow<'_, str>> {
1350        match &self.connection {
1351            StorageSinkConnection::Kafka(connection) => Some(connection.format.get_format_name()),
1352            StorageSinkConnection::Iceberg(_) => None,
1353        }
1354    }
1355
1356    /// Output distinct key_format and value_format of the sink.
1357    ///
1358    /// NOTE: also derived from `create_sql` for `mz_sinks`. Watch out that a key
1359    /// format exists only when the sink has a `KEY`, so the SQL side has to work
1360    /// that out from the statement for itself.
1361    pub fn formats(&self) -> Option<(Option<&str>, &str)> {
1362        match &self.connection {
1363            StorageSinkConnection::Kafka(connection) => {
1364                let key_format = connection
1365                    .format
1366                    .key_format
1367                    .as_ref()
1368                    .map(|f| f.get_format_name());
1369                let value_format = connection.format.value_format.get_format_name();
1370                Some((key_format, value_format))
1371            }
1372            StorageSinkConnection::Iceberg(_) => None,
1373        }
1374    }
1375
1376    pub fn connection_id(&self) -> Option<CatalogItemId> {
1377        self.connection.connection_id()
1378    }
1379
1380    /// The single [`GlobalId`] that this Sink can be referenced by.
1381    pub fn global_id(&self) -> GlobalId {
1382        self.global_id
1383    }
1384}
1385
1386#[derive(Debug, Clone, Serialize)]
1387pub struct View {
1388    /// Parse-able SQL that defines this view.
1389    pub create_sql: String,
1390    /// [`GlobalId`] used to reference this view from outside the catalog, e.g. compute.
1391    pub global_id: GlobalId,
1392    /// Unoptimized high-level expression from parsing the `create_sql`.
1393    pub raw_expr: Arc<HirRelationExpr>,
1394    /// Optimized mid-level expression from (locally) optimizing the `raw_expr`.
1395    pub locally_optimized_expr: Arc<OptimizedMirRelationExpr>,
1396    /// Columns of this view.
1397    pub desc: RelationDesc,
1398    /// If created in the `TEMPORARY` schema, the [`ConnectionId`] for that session.
1399    pub conn_id: Option<ConnectionId>,
1400    /// Other catalog objects that are referenced by this view, determined at name resolution.
1401    pub resolved_ids: ResolvedIds,
1402    /// All of the catalog objects that are referenced by this view.
1403    pub dependencies: DependencyIds,
1404}
1405
1406impl View {
1407    /// The single [`GlobalId`] this [`View`] can be referenced by.
1408    pub fn global_id(&self) -> GlobalId {
1409        self.global_id
1410    }
1411}
1412
1413#[derive(Debug, Clone, Serialize)]
1414pub struct MaterializedView {
1415    /// Parse-able SQL that defines this materialized view.
1416    pub create_sql: String,
1417    /// Versions of this materialized view, and the [`GlobalId`]s that refer to them.
1418    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
1419    pub collections: BTreeMap<RelationVersion, GlobalId>,
1420    /// Raw high-level expression from planning, derived from the `create_sql`.
1421    pub raw_expr: Arc<HirRelationExpr>,
1422    /// Optimized mid-level expression, derived from the `raw_expr`.
1423    pub locally_optimized_expr: Arc<OptimizedMirRelationExpr>,
1424    /// [`VersionedRelationDesc`] of this materialized view, derived from the `create_sql`.
1425    pub desc: VersionedRelationDesc,
1426    /// Other catalog items that this materialized view references, determined at name resolution.
1427    pub resolved_ids: ResolvedIds,
1428    /// All of the catalog objects that are referenced by this view.
1429    pub dependencies: DependencyIds,
1430    /// ID of the materialized view this materialized view is intended to replace.
1431    pub replacement_target: Option<CatalogItemId>,
1432    /// Cluster that this materialized view runs on.
1433    pub cluster_id: ClusterId,
1434    /// If set, only install this materialized view's dataflow on the specified replica.
1435    pub target_replica: Option<ReplicaId>,
1436    /// Column indexes that we assert are not `NULL`.
1437    ///
1438    /// TODO(parkmycar): Switch this to use the `ColumnIdx` type.
1439    pub non_null_assertions: Vec<usize>,
1440    /// Custom compaction window, e.g. set via `ALTER RETAIN HISTORY`.
1441    pub custom_logical_compaction_window: Option<CompactionWindow>,
1442    /// Schedule to refresh this materialized view, e.g. set via `REFRESH EVERY` option.
1443    pub refresh_schedule: Option<RefreshSchedule>,
1444    /// The initial `as_of` of the storage collection associated with the materialized view.
1445    ///
1446    /// Note: This doesn't change upon restarts.
1447    /// (The dataflow's initial `as_of` can be different.)
1448    pub initial_as_of: Option<Antichain<mz_repr::Timestamp>>,
1449    // The catalog `dump` method uses serde to serialize catalog state, e.g., Testdrive catalog
1450    // consistency checks do two dumps and compare them. One of these states comes from the durable
1451    // catalog, but the following fields are not restored when the consistency check loads the
1452    // durable catalog, hence we need `#[serde(skip)]`.
1453    /// Optimized global MIR plan, set after global optimization.
1454    #[serde(skip)]
1455    pub optimized_plan: Option<Arc<DataflowDescription<OptimizedMirRelationExpr>>>,
1456    /// Physical (LIR) plan, set after physical optimization.
1457    #[serde(skip)]
1458    pub physical_plan: Option<Arc<DataflowDescription<ComputePlan>>>,
1459    /// Dataflow metainfo (optimizer notices, etc.), set after optimization.
1460    #[serde(skip)]
1461    pub dataflow_metainfo: Option<DataflowMetainfo<Arc<OptimizerNotice>>>,
1462}
1463
1464impl MaterializedView {
1465    /// Returns all [`GlobalId`]s that this [`MaterializedView`] can be referenced by.
1466    pub fn global_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
1467        self.collections.values().copied()
1468    }
1469
1470    /// The latest [`GlobalId`] for this [`MaterializedView`] which represents the writing
1471    /// version.
1472    pub fn global_id_writes(&self) -> GlobalId {
1473        *self
1474            .collections
1475            .last_key_value()
1476            .expect("at least one version of a materialized view")
1477            .1
1478    }
1479
1480    /// Returns all collections and their [`RelationDesc`]s associated with this [`MaterializedView`].
1481    pub fn collection_descs(
1482        &self,
1483    ) -> impl Iterator<Item = (GlobalId, RelationVersion, RelationDesc)> + '_ {
1484        self.collections.iter().map(|(version, gid)| {
1485            let desc = self
1486                .desc
1487                .at_version(RelationVersionSelector::Specific(*version));
1488            (*gid, *version, desc)
1489        })
1490    }
1491
1492    /// Returns the [`RelationDesc`] for a specific [`GlobalId`].
1493    pub fn desc_for(&self, id: &GlobalId) -> RelationDesc {
1494        let (version, _gid) = self
1495            .collections
1496            .iter()
1497            .find(|(_version, gid)| *gid == id)
1498            .expect("GlobalId to exist");
1499        self.desc
1500            .at_version(RelationVersionSelector::Specific(*version))
1501    }
1502
1503    /// Apply the given replacement materialized view to this [`MaterializedView`].
1504    pub fn apply_replacement(&mut self, replacement: Self) {
1505        let target_id = replacement
1506            .replacement_target
1507            .expect("replacement has target");
1508
1509        fn parse(create_sql: &str) -> mz_sql::ast::CreateMaterializedViewStatement<Raw> {
1510            let res = mz_sql::parse::parse(create_sql).unwrap_or_else(|e| {
1511                panic!("invalid create_sql persisted in catalog: {e}\n{create_sql}");
1512            });
1513            if let Statement::CreateMaterializedView(cmvs) = res.into_element().ast {
1514                cmvs
1515            } else {
1516                panic!("invalid MV create_sql persisted in catalog\n{create_sql}");
1517            }
1518        }
1519
1520        let old_stmt = parse(&self.create_sql);
1521        let rpl_stmt = parse(&replacement.create_sql);
1522        let new_stmt = mz_sql::ast::CreateMaterializedViewStatement {
1523            if_exists: old_stmt.if_exists,
1524            name: old_stmt.name,
1525            columns: rpl_stmt.columns,
1526            replacement_for: None,
1527            in_cluster: rpl_stmt.in_cluster,
1528            in_cluster_replica: rpl_stmt.in_cluster_replica,
1529            query: rpl_stmt.query,
1530            as_of: rpl_stmt.as_of,
1531            with_options: rpl_stmt.with_options,
1532        };
1533        let create_sql = new_stmt.to_ast_string_stable();
1534
1535        let mut collections = std::mem::take(&mut self.collections);
1536        // Note: We can't use `self.desc.latest_version` here because a replacement doesn't
1537        // necessary evolve the relation schema, so that version might be lower than the actual
1538        // latest version.
1539        let latest_version = collections.keys().max().expect("at least one version");
1540        let new_version = latest_version.bump();
1541        collections.insert(new_version, replacement.global_id_writes());
1542
1543        let mut resolved_ids = replacement.resolved_ids;
1544        resolved_ids.remove_item(&target_id);
1545        let mut dependencies = replacement.dependencies;
1546        dependencies.0.remove(&target_id);
1547
1548        *self = Self {
1549            create_sql,
1550            collections,
1551            raw_expr: replacement.raw_expr,
1552            locally_optimized_expr: replacement.locally_optimized_expr,
1553            desc: replacement.desc,
1554            resolved_ids,
1555            dependencies,
1556            replacement_target: None,
1557            cluster_id: replacement.cluster_id,
1558            target_replica: replacement.target_replica,
1559            non_null_assertions: replacement.non_null_assertions,
1560            custom_logical_compaction_window: replacement.custom_logical_compaction_window,
1561            refresh_schedule: replacement.refresh_schedule,
1562            initial_as_of: replacement.initial_as_of,
1563            optimized_plan: replacement.optimized_plan,
1564            physical_plan: replacement.physical_plan,
1565            dataflow_metainfo: replacement.dataflow_metainfo,
1566        };
1567    }
1568}
1569
1570#[derive(Debug, Clone, Serialize)]
1571pub struct Index {
1572    /// Parse-able SQL that defines this table.
1573    pub create_sql: String,
1574    /// [`GlobalId`] used to reference this index from outside the catalog, e.g. compute.
1575    pub global_id: GlobalId,
1576    /// The [`GlobalId`] this Index is on.
1577    pub on: GlobalId,
1578    /// Keys of the index.
1579    pub keys: Arc<[MirScalarExpr]>,
1580    /// If created in the `TEMPORARY` schema, the [`ConnectionId`] for that session.
1581    pub conn_id: Option<ConnectionId>,
1582    /// Other catalog objects referenced by this index, e.g. the object we're indexing.
1583    pub resolved_ids: ResolvedIds,
1584    /// Cluster this index is installed on.
1585    pub cluster_id: ClusterId,
1586    /// Custom compaction window, e.g. set via `ALTER RETAIN HISTORY`.
1587    pub custom_logical_compaction_window: Option<CompactionWindow>,
1588    /// Whether the table's logical compaction window is controlled by the ['metrics_retention']
1589    /// session variable.
1590    ///
1591    /// ['metrics_retention']: mz_sql::session::vars::METRICS_RETENTION
1592    pub is_retained_metrics_object: bool,
1593    // The catalog `dump` method uses serde to serialize catalog state, e.g., Testdrive catalog
1594    // consistency checks do two dumps and compare them. One of these states comes from the durable
1595    // catalog, but the following fields are not restored when the consistency check loads the
1596    // durable catalog, hence we need `#[serde(skip)]`.
1597    /// Optimized global MIR plan, set after global optimization.
1598    #[serde(skip)]
1599    pub optimized_plan: Option<Arc<DataflowDescription<OptimizedMirRelationExpr>>>,
1600    /// Physical (LIR) plan, set after physical optimization.
1601    #[serde(skip)]
1602    pub physical_plan: Option<Arc<DataflowDescription<ComputePlan>>>,
1603    /// Dataflow metainfo (optimizer notices, etc.), set after optimization.
1604    #[serde(skip)]
1605    pub dataflow_metainfo: Option<DataflowMetainfo<Arc<OptimizerNotice>>>,
1606}
1607
1608impl Index {
1609    /// The [`GlobalId`] that refers to this Index.
1610    pub fn global_id(&self) -> GlobalId {
1611        self.global_id
1612    }
1613}
1614
1615/// A sink that exports a relation's rows as Prometheus metrics.
1616#[derive(Debug, Clone, Serialize)]
1617pub struct MetricSink {
1618    /// Parse-able SQL that defines this metric sink.
1619    pub create_sql: String,
1620    /// [`GlobalId`] used to reference this metric sink from outside the catalog, e.g. compute.
1621    pub global_id: GlobalId,
1622    /// Collection we read into this metric sink.
1623    pub from: GlobalId,
1624    /// Other catalog objects referenced by this metric sink.
1625    pub resolved_ids: ResolvedIds,
1626    /// Cluster this metric sink runs on.
1627    pub cluster_id: ClusterId,
1628    /// Prepended to every metric name this sink publishes. Not durable on its own: like `from`
1629    /// and `cluster_id`, it rides in `create_sql` and is recovered by re-parsing that on boot.
1630    pub prefix: String,
1631    /// Optimized global MIR plan, set after global optimization.
1632    #[serde(skip)]
1633    pub optimized_plan: Option<Arc<DataflowDescription<OptimizedMirRelationExpr>>>,
1634    /// Physical (LIR) plan, set after physical optimization.
1635    #[serde(skip)]
1636    pub physical_plan: Option<Arc<DataflowDescription<ComputePlan>>>,
1637    /// Dataflow metainfo (optimizer notices, etc.), set after optimization.
1638    #[serde(skip)]
1639    pub dataflow_metainfo: Option<DataflowMetainfo<Arc<OptimizerNotice>>>,
1640}
1641
1642impl MetricSink {
1643    /// The [`GlobalId`] that refers to this metric sink.
1644    pub fn global_id(&self) -> GlobalId {
1645        self.global_id
1646    }
1647}
1648
1649#[derive(Debug, Clone, Serialize)]
1650pub struct Type {
1651    /// Parse-able SQL that defines this type.
1652    pub create_sql: Option<String>,
1653    /// [`GlobalId`] used to reference this type from outside the catalog.
1654    pub global_id: GlobalId,
1655    #[serde(skip)]
1656    pub details: CatalogTypeDetails<IdReference>,
1657    /// Other catalog objects referenced by this type.
1658    pub resolved_ids: ResolvedIds,
1659}
1660
1661#[derive(Debug, Clone, Serialize)]
1662pub struct Func {
1663    /// Static definition of the function.
1664    #[serde(skip)]
1665    pub inner: &'static mz_sql::func::Func,
1666    /// [`GlobalId`] used to reference this function from outside the catalog.
1667    pub global_id: GlobalId,
1668}
1669
1670#[derive(Debug, Clone, Serialize)]
1671pub struct Secret {
1672    /// Parse-able SQL that defines this secret.
1673    pub create_sql: String,
1674    /// [`GlobalId`] used to reference this secret from outside the catalog.
1675    pub global_id: GlobalId,
1676}
1677
1678#[derive(Debug, Clone, Serialize)]
1679pub struct Connection {
1680    /// Parse-able SQL that defines this connection.
1681    pub create_sql: String,
1682    /// [`GlobalId`] used to reference this connection from the storage layer.
1683    pub global_id: GlobalId,
1684    /// The kind of connection.
1685    pub details: ConnectionDetails,
1686    /// Other objects this connection depends on.
1687    pub resolved_ids: ResolvedIds,
1688}
1689
1690impl Connection {
1691    /// The single [`GlobalId`] used to reference this connection.
1692    pub fn global_id(&self) -> GlobalId {
1693        self.global_id
1694    }
1695}
1696
1697#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1698pub struct NetworkPolicy {
1699    pub name: String,
1700    pub id: NetworkPolicyId,
1701    pub oid: u32,
1702    pub rules: Vec<NetworkPolicyRule>,
1703    pub owner_id: RoleId,
1704    pub privileges: PrivilegeMap,
1705}
1706
1707impl From<NetworkPolicy> for durable::NetworkPolicy {
1708    fn from(policy: NetworkPolicy) -> durable::NetworkPolicy {
1709        durable::NetworkPolicy {
1710            id: policy.id,
1711            oid: policy.oid,
1712            name: policy.name,
1713            rules: policy.rules,
1714            owner_id: policy.owner_id,
1715            privileges: policy.privileges.into_all_values().collect(),
1716        }
1717    }
1718}
1719
1720impl From<durable::NetworkPolicy> for NetworkPolicy {
1721    fn from(
1722        durable::NetworkPolicy {
1723            id,
1724            oid,
1725            name,
1726            rules,
1727            owner_id,
1728            privileges,
1729        }: durable::NetworkPolicy,
1730    ) -> Self {
1731        NetworkPolicy {
1732            id,
1733            oid,
1734            name,
1735            rules,
1736            owner_id,
1737            privileges: PrivilegeMap::from_mz_acl_items(privileges),
1738        }
1739    }
1740}
1741
1742impl UpdateFrom<durable::NetworkPolicy> for NetworkPolicy {
1743    fn update_from(
1744        &mut self,
1745        durable::NetworkPolicy {
1746            id,
1747            oid,
1748            name,
1749            rules,
1750            owner_id,
1751            privileges,
1752        }: durable::NetworkPolicy,
1753    ) {
1754        self.id = id;
1755        self.oid = oid;
1756        self.name = name;
1757        self.rules = rules;
1758        self.owner_id = owner_id;
1759        self.privileges = PrivilegeMap::from_mz_acl_items(privileges);
1760    }
1761}
1762
1763impl CatalogItem {
1764    /// Returns a string indicating the type of this catalog entry.
1765    pub fn typ(&self) -> mz_sql::catalog::CatalogItemType {
1766        match self {
1767            CatalogItem::Table(_) => CatalogItemType::Table,
1768            CatalogItem::Source(_) => CatalogItemType::Source,
1769            CatalogItem::Log(_) => CatalogItemType::Source,
1770            CatalogItem::Sink(_) => CatalogItemType::Sink,
1771            CatalogItem::View(_) => CatalogItemType::View,
1772            CatalogItem::MaterializedView(_) => CatalogItemType::MaterializedView,
1773            CatalogItem::Index(_) => CatalogItemType::Index,
1774            CatalogItem::Type(_) => CatalogItemType::Type,
1775            CatalogItem::Func(_) => CatalogItemType::Func,
1776            CatalogItem::Secret(_) => CatalogItemType::Secret,
1777            CatalogItem::Connection(_) => CatalogItemType::Connection,
1778            CatalogItem::MetricSink(_) => CatalogItemType::MetricSink,
1779        }
1780    }
1781
1782    /// Returns the [`GlobalId`]s that reference this item, if any.
1783    pub fn global_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
1784        let gid = match self {
1785            CatalogItem::Source(source) => source.global_id,
1786            CatalogItem::Log(log) => log.global_id,
1787            CatalogItem::Sink(sink) => sink.global_id,
1788            CatalogItem::View(view) => view.global_id,
1789            CatalogItem::MaterializedView(mv) => {
1790                return itertools::Either::Left(mv.collections.values().copied());
1791            }
1792            CatalogItem::Index(index) => index.global_id,
1793            CatalogItem::Func(func) => func.global_id,
1794            CatalogItem::Type(ty) => ty.global_id,
1795            CatalogItem::Secret(secret) => secret.global_id,
1796            CatalogItem::Connection(conn) => conn.global_id,
1797            CatalogItem::MetricSink(metric_sink) => metric_sink.global_id,
1798            CatalogItem::Table(table) => {
1799                return itertools::Either::Left(table.collections.values().copied());
1800            }
1801        };
1802        itertools::Either::Right(std::iter::once(gid))
1803    }
1804
1805    /// Returns the most up-to-date [`GlobalId`] for this item.
1806    ///
1807    /// Note: The only type of object that can have multiple [`GlobalId`]s are tables.
1808    pub fn latest_global_id(&self) -> GlobalId {
1809        match self {
1810            CatalogItem::Source(source) => source.global_id,
1811            CatalogItem::Log(log) => log.global_id,
1812            CatalogItem::Sink(sink) => sink.global_id,
1813            CatalogItem::View(view) => view.global_id,
1814            CatalogItem::MaterializedView(mv) => mv.global_id_writes(),
1815            CatalogItem::Index(index) => index.global_id,
1816            CatalogItem::Func(func) => func.global_id,
1817            CatalogItem::Type(ty) => ty.global_id,
1818            CatalogItem::Secret(secret) => secret.global_id,
1819            CatalogItem::Connection(conn) => conn.global_id,
1820            CatalogItem::MetricSink(metric_sink) => metric_sink.global_id,
1821            CatalogItem::Table(table) => table.global_id_writes(),
1822        }
1823    }
1824
1825    /// Returns the optimized global MIR plan, if this item has one.
1826    pub fn optimized_plan(&self) -> Option<&Arc<DataflowDescription<OptimizedMirRelationExpr>>> {
1827        match self {
1828            CatalogItem::Index(idx) => idx.optimized_plan.as_ref(),
1829            CatalogItem::MaterializedView(mv) => mv.optimized_plan.as_ref(),
1830            CatalogItem::MetricSink(ms) => ms.optimized_plan.as_ref(),
1831            _ => None,
1832        }
1833    }
1834
1835    /// Returns the physical (LIR) plan, if this item has one.
1836    pub fn physical_plan(&self) -> Option<&Arc<DataflowDescription<ComputePlan>>> {
1837        match self {
1838            CatalogItem::Index(idx) => idx.physical_plan.as_ref(),
1839            CatalogItem::MaterializedView(mv) => mv.physical_plan.as_ref(),
1840            CatalogItem::MetricSink(ms) => ms.physical_plan.as_ref(),
1841            _ => None,
1842        }
1843    }
1844
1845    /// Returns the dataflow metainfo, if this item has one.
1846    pub fn dataflow_metainfo(&self) -> Option<&DataflowMetainfo<Arc<OptimizerNotice>>> {
1847        match self {
1848            CatalogItem::Index(idx) => idx.dataflow_metainfo.as_ref(),
1849            CatalogItem::MaterializedView(mv) => mv.dataflow_metainfo.as_ref(),
1850            CatalogItem::MetricSink(ms) => ms.dataflow_metainfo.as_ref(),
1851            _ => None,
1852        }
1853    }
1854
1855    /// Returns a mutable reference to the dataflow metainfo, if this item has one.
1856    pub fn dataflow_metainfo_mut(&mut self) -> Option<&mut DataflowMetainfo<Arc<OptimizerNotice>>> {
1857        match self {
1858            CatalogItem::Index(idx) => idx.dataflow_metainfo.as_mut(),
1859            CatalogItem::MaterializedView(mv) => mv.dataflow_metainfo.as_mut(),
1860            CatalogItem::MetricSink(ms) => ms.dataflow_metainfo.as_mut(),
1861            _ => None,
1862        }
1863    }
1864
1865    /// Returns mutable references to the plan fields (`optimized_plan`,
1866    /// `physical_plan`, `dataflow_metainfo`) on plan-bearing items
1867    /// (`Index`, `MaterializedView`), or `None` for
1868    /// other item kinds.
1869    pub fn plan_fields_mut(
1870        &mut self,
1871    ) -> Option<(
1872        &mut Option<Arc<DataflowDescription<OptimizedMirRelationExpr>>>,
1873        &mut Option<Arc<DataflowDescription<ComputePlan>>>,
1874        &mut Option<DataflowMetainfo<Arc<OptimizerNotice>>>,
1875    )> {
1876        match self {
1877            CatalogItem::Index(idx) => Some((
1878                &mut idx.optimized_plan,
1879                &mut idx.physical_plan,
1880                &mut idx.dataflow_metainfo,
1881            )),
1882            CatalogItem::MaterializedView(mv) => Some((
1883                &mut mv.optimized_plan,
1884                &mut mv.physical_plan,
1885                &mut mv.dataflow_metainfo,
1886            )),
1887            CatalogItem::MetricSink(ms) => Some((
1888                &mut ms.optimized_plan,
1889                &mut ms.physical_plan,
1890                &mut ms.dataflow_metainfo,
1891            )),
1892            _ => None,
1893        }
1894    }
1895
1896    /// Whether this item represents a storage collection.
1897    pub fn is_storage_collection(&self) -> bool {
1898        match self {
1899            CatalogItem::Table(_)
1900            | CatalogItem::Source(_)
1901            | CatalogItem::MaterializedView(_)
1902            | CatalogItem::Sink(_) => true,
1903            CatalogItem::Log(_)
1904            | CatalogItem::View(_)
1905            | CatalogItem::Index(_)
1906            | CatalogItem::Type(_)
1907            | CatalogItem::Func(_)
1908            | CatalogItem::Secret(_)
1909            | CatalogItem::Connection(_)
1910            | CatalogItem::MetricSink(_) => false,
1911        }
1912    }
1913
1914    /// Returns the [`RelationDesc`] for items that yield rows, at the requested
1915    /// version.
1916    ///
1917    /// Some item types honor `version` so callers can ask for the schema that
1918    /// matches a specific [`GlobalId`] or historical definition. Other relation
1919    /// types ignore `version` because they have a single shape. Non-relational
1920    /// items ( for example functions, indexes, sinks, secrets, and connections)
1921    /// return `None`.
1922    pub fn relation_desc(&self, version: RelationVersionSelector) -> Option<Cow<'_, RelationDesc>> {
1923        match &self {
1924            CatalogItem::Source(src) => Some(Cow::Borrowed(&src.desc)),
1925            CatalogItem::Log(log) => Some(Cow::Owned(log.variant.desc())),
1926            CatalogItem::Table(tbl) => Some(Cow::Owned(tbl.desc.at_version(version))),
1927            CatalogItem::View(view) => Some(Cow::Borrowed(&view.desc)),
1928            CatalogItem::MaterializedView(mview) => {
1929                Some(Cow::Owned(mview.desc.at_version(version)))
1930            }
1931            CatalogItem::Func(_)
1932            | CatalogItem::Index(_)
1933            | CatalogItem::Sink(_)
1934            | CatalogItem::Secret(_)
1935            | CatalogItem::Connection(_)
1936            | CatalogItem::Type(_)
1937            | CatalogItem::MetricSink(_) => None,
1938        }
1939    }
1940
1941    pub fn func(
1942        &self,
1943        entry: &CatalogEntry,
1944    ) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
1945        match &self {
1946            CatalogItem::Func(func) => Ok(func.inner),
1947            _ => Err(SqlCatalogError::UnexpectedType {
1948                name: entry.name().item.to_string(),
1949                actual_type: entry.item_type(),
1950                expected_type: CatalogItemType::Func,
1951            }),
1952        }
1953    }
1954
1955    pub fn source_desc(
1956        &self,
1957        entry: &CatalogEntry,
1958    ) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
1959        match &self {
1960            CatalogItem::Source(source) => match &source.data_source {
1961                DataSourceDesc::Ingestion { desc, .. }
1962                | DataSourceDesc::OldSyntaxIngestion { desc, .. } => Ok(Some(desc)),
1963                DataSourceDesc::IngestionExport { .. }
1964                | DataSourceDesc::Introspection(_)
1965                | DataSourceDesc::Webhook { .. }
1966                | DataSourceDesc::Progress
1967                | DataSourceDesc::Catalog => Ok(None),
1968            },
1969            _ => Err(SqlCatalogError::UnexpectedType {
1970                name: entry.name().item.to_string(),
1971                actual_type: entry.item_type(),
1972                expected_type: CatalogItemType::Source,
1973            }),
1974        }
1975    }
1976
1977    /// Reports whether this catalog entry is a progress source.
1978    pub fn is_progress_source(&self) -> bool {
1979        matches!(
1980            self,
1981            CatalogItem::Source(Source {
1982                data_source: DataSourceDesc::Progress,
1983                ..
1984            })
1985        )
1986    }
1987
1988    /// Collects the identifiers of the objects that were encountered when resolving names in the
1989    /// item's DDL statement.
1990    pub fn references(&self) -> &ResolvedIds {
1991        static EMPTY: LazyLock<ResolvedIds> = LazyLock::new(ResolvedIds::empty);
1992        match self {
1993            CatalogItem::Func(_) => &*EMPTY,
1994            CatalogItem::Index(idx) => &idx.resolved_ids,
1995            CatalogItem::Sink(sink) => &sink.resolved_ids,
1996            CatalogItem::Source(source) => &source.resolved_ids,
1997            CatalogItem::Log(_) => &*EMPTY,
1998            CatalogItem::Table(table) => &table.resolved_ids,
1999            CatalogItem::Type(typ) => &typ.resolved_ids,
2000            CatalogItem::View(view) => &view.resolved_ids,
2001            CatalogItem::MaterializedView(mview) => &mview.resolved_ids,
2002            CatalogItem::Secret(_) => &*EMPTY,
2003            CatalogItem::Connection(connection) => &connection.resolved_ids,
2004            CatalogItem::MetricSink(metric_sink) => &metric_sink.resolved_ids,
2005        }
2006    }
2007
2008    /// Collects the identifiers of the objects used by this [`CatalogItem`].
2009    ///
2010    /// Like [`CatalogItem::references()`] but also includes objects that are not directly
2011    /// referenced. For example this will include any catalog objects used to implement functions
2012    /// and casts in the item.
2013    pub fn uses(&self) -> BTreeSet<CatalogItemId> {
2014        let mut uses: BTreeSet<_> = self.references().items().copied().collect();
2015        match self {
2016            // TODO(jkosh44) This isn't really correct for functions. They may use other objects in
2017            // their implementation. However, currently there's no way to get that information.
2018            CatalogItem::Func(_) => {}
2019            CatalogItem::Index(_) => {}
2020            CatalogItem::Sink(_) => {}
2021            CatalogItem::Source(_) => {}
2022            CatalogItem::Log(_) => {}
2023            CatalogItem::Table(_) => {}
2024            CatalogItem::Type(_) => {}
2025            CatalogItem::View(view) => uses.extend(view.dependencies.0.iter().copied()),
2026            CatalogItem::MaterializedView(mview) => {
2027                uses.extend(mview.dependencies.0.iter().copied())
2028            }
2029            CatalogItem::Secret(_) => {}
2030            CatalogItem::Connection(_) => {}
2031            CatalogItem::MetricSink(_) => {}
2032        }
2033        uses
2034    }
2035
2036    /// Returns direct dependencies to traverse when finding this item's query leaves.
2037    ///
2038    /// Functions and views return all of their uses. Materialized views do the
2039    /// same except for the replacement target, which is lifecycle metadata. All
2040    /// other items are query leaves or cannot be selected from.
2041    pub fn query_dependencies(&self) -> BTreeSet<CatalogItemId> {
2042        match self {
2043            CatalogItem::Func(_) | CatalogItem::View(_) => self.uses(),
2044            CatalogItem::MaterializedView(mv) => {
2045                let mut dependencies = self.uses();
2046                if let Some(target) = mv.replacement_target {
2047                    dependencies.remove(&target);
2048                }
2049                dependencies
2050            }
2051            CatalogItem::Index(_)
2052            | CatalogItem::Sink(_)
2053            | CatalogItem::Source(_)
2054            | CatalogItem::Log(_)
2055            | CatalogItem::Table(_)
2056            | CatalogItem::Type(_)
2057            | CatalogItem::Secret(_)
2058            | CatalogItem::Connection(_)
2059            | CatalogItem::MetricSink(_) => BTreeSet::new(),
2060        }
2061    }
2062
2063    /// Returns the connection ID that this item belongs to, if this item is
2064    /// temporary.
2065    pub fn conn_id(&self) -> Option<&ConnectionId> {
2066        match self {
2067            CatalogItem::View(view) => view.conn_id.as_ref(),
2068            CatalogItem::Index(index) => index.conn_id.as_ref(),
2069            CatalogItem::Table(table) => table.conn_id.as_ref(),
2070            CatalogItem::Log(_)
2071            | CatalogItem::Source(_)
2072            | CatalogItem::Sink(_)
2073            | CatalogItem::MaterializedView(_)
2074            | CatalogItem::Secret(_)
2075            | CatalogItem::Type(_)
2076            | CatalogItem::Func(_)
2077            | CatalogItem::Connection(_)
2078            | CatalogItem::MetricSink(_) => None,
2079        }
2080    }
2081
2082    /// Sets the connection ID that this item belongs to, which makes it a
2083    /// temporary item.
2084    pub fn set_conn_id(&mut self, conn_id: Option<ConnectionId>) {
2085        match self {
2086            CatalogItem::View(view) => view.conn_id = conn_id,
2087            CatalogItem::Index(index) => index.conn_id = conn_id,
2088            CatalogItem::Table(table) => table.conn_id = conn_id,
2089            CatalogItem::Log(_)
2090            | CatalogItem::Source(_)
2091            | CatalogItem::Sink(_)
2092            | CatalogItem::MaterializedView(_)
2093            | CatalogItem::Secret(_)
2094            | CatalogItem::Type(_)
2095            | CatalogItem::Func(_)
2096            | CatalogItem::Connection(_)
2097            | CatalogItem::MetricSink(_) => (),
2098        }
2099    }
2100
2101    /// Overwrites the `create_sql` of this item, without replanning.
2102    ///
2103    /// Only used when applying temporary item updates. A temporary item's
2104    /// in-memory `create_sql` must stay byte-identical to the update that
2105    /// created it, so that re-serializing the item, for example when a later
2106    /// op in the same transaction retracts it, yields a value that
2107    /// consolidates away against that update. Persistent items get this
2108    /// guarantee from the durable catalog, which stores the exact bytes.
2109    pub fn set_create_sql(&mut self, create_sql: String) {
2110        match self {
2111            CatalogItem::View(view) => view.create_sql = create_sql,
2112            CatalogItem::Index(index) => index.create_sql = create_sql,
2113            CatalogItem::Table(table) => table.create_sql = Some(create_sql),
2114            CatalogItem::Log(_)
2115            | CatalogItem::Source(_)
2116            | CatalogItem::Sink(_)
2117            | CatalogItem::MaterializedView(_)
2118            | CatalogItem::Secret(_)
2119            | CatalogItem::Type(_)
2120            | CatalogItem::Func(_)
2121            | CatalogItem::Connection(_)
2122            | CatalogItem::MetricSink(_) => {
2123                unreachable!("only views, indexes, and tables can be temporary")
2124            }
2125        }
2126    }
2127
2128    /// Indicates whether this item is temporary or not.
2129    pub fn is_temporary(&self) -> bool {
2130        self.conn_id().is_some()
2131    }
2132
2133    pub fn rename_schema_refs(
2134        &self,
2135        database_name: &str,
2136        cur_schema_name: &str,
2137        new_schema_name: &str,
2138    ) -> Result<CatalogItem, (String, String)> {
2139        let do_rewrite = |create_sql: String| -> Result<String, (String, String)> {
2140            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2141                .expect("invalid create sql persisted to catalog")
2142                .into_element()
2143                .ast;
2144
2145            // Rename all references to cur_schema_name.
2146            mz_sql::ast::transform::create_stmt_rename_schema_refs(
2147                &mut create_stmt,
2148                database_name,
2149                cur_schema_name,
2150                new_schema_name,
2151            )?;
2152
2153            Ok(create_stmt.to_ast_string_stable())
2154        };
2155
2156        match self {
2157            CatalogItem::Table(i) => {
2158                let mut i = i.clone();
2159                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2160                Ok(CatalogItem::Table(i))
2161            }
2162            CatalogItem::Log(i) => Ok(CatalogItem::Log(i.clone())),
2163            CatalogItem::Source(i) => {
2164                let mut i = i.clone();
2165                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2166                Ok(CatalogItem::Source(i))
2167            }
2168            CatalogItem::Sink(i) => {
2169                let mut i = i.clone();
2170                i.create_sql = do_rewrite(i.create_sql)?;
2171                Ok(CatalogItem::Sink(i))
2172            }
2173            CatalogItem::View(i) => {
2174                let mut i = i.clone();
2175                i.create_sql = do_rewrite(i.create_sql)?;
2176                Ok(CatalogItem::View(i))
2177            }
2178            CatalogItem::MaterializedView(i) => {
2179                let mut i = i.clone();
2180                i.create_sql = do_rewrite(i.create_sql)?;
2181                Ok(CatalogItem::MaterializedView(i))
2182            }
2183            CatalogItem::Index(i) => {
2184                let mut i = i.clone();
2185                i.create_sql = do_rewrite(i.create_sql)?;
2186                Ok(CatalogItem::Index(i))
2187            }
2188            CatalogItem::Secret(i) => {
2189                let mut i = i.clone();
2190                i.create_sql = do_rewrite(i.create_sql)?;
2191                Ok(CatalogItem::Secret(i))
2192            }
2193            CatalogItem::Connection(i) => {
2194                let mut i = i.clone();
2195                i.create_sql = do_rewrite(i.create_sql)?;
2196                Ok(CatalogItem::Connection(i))
2197            }
2198            CatalogItem::Type(i) => {
2199                let mut i = i.clone();
2200                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2201                Ok(CatalogItem::Type(i))
2202            }
2203            CatalogItem::Func(i) => Ok(CatalogItem::Func(i.clone())),
2204            CatalogItem::MetricSink(i) => {
2205                let mut i = i.clone();
2206                i.create_sql = do_rewrite(i.create_sql)?;
2207                Ok(CatalogItem::MetricSink(i))
2208            }
2209        }
2210    }
2211
2212    /// Returns a clone of `self` with all instances of `from` renamed to `to`
2213    /// (with the option of including the item's own name) or errors if request
2214    /// is ambiguous.
2215    pub fn rename_item_refs(
2216        &self,
2217        from: FullItemName,
2218        to_item_name: String,
2219        rename_self: bool,
2220    ) -> Result<CatalogItem, String> {
2221        let do_rewrite = |create_sql: String| -> Result<String, String> {
2222            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2223                .expect("invalid create sql persisted to catalog")
2224                .into_element()
2225                .ast;
2226            if rename_self {
2227                mz_sql::ast::transform::create_stmt_rename(&mut create_stmt, to_item_name.clone());
2228            }
2229            // Determination of what constitutes an ambiguous request is done here.
2230            mz_sql::ast::transform::create_stmt_rename_refs(&mut create_stmt, from, to_item_name)?;
2231            Ok(create_stmt.to_ast_string_stable())
2232        };
2233
2234        match self {
2235            CatalogItem::Table(i) => {
2236                let mut i = i.clone();
2237                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2238                Ok(CatalogItem::Table(i))
2239            }
2240            CatalogItem::Log(i) => Ok(CatalogItem::Log(i.clone())),
2241            CatalogItem::Source(i) => {
2242                let mut i = i.clone();
2243                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2244                Ok(CatalogItem::Source(i))
2245            }
2246            CatalogItem::Sink(i) => {
2247                let mut i = i.clone();
2248                i.create_sql = do_rewrite(i.create_sql)?;
2249                Ok(CatalogItem::Sink(i))
2250            }
2251            CatalogItem::View(i) => {
2252                let mut i = i.clone();
2253                i.create_sql = do_rewrite(i.create_sql)?;
2254                Ok(CatalogItem::View(i))
2255            }
2256            CatalogItem::MaterializedView(i) => {
2257                let mut i = i.clone();
2258                i.create_sql = do_rewrite(i.create_sql)?;
2259                Ok(CatalogItem::MaterializedView(i))
2260            }
2261            CatalogItem::Index(i) => {
2262                let mut i = i.clone();
2263                i.create_sql = do_rewrite(i.create_sql)?;
2264                Ok(CatalogItem::Index(i))
2265            }
2266            CatalogItem::Secret(i) => {
2267                let mut i = i.clone();
2268                i.create_sql = do_rewrite(i.create_sql)?;
2269                Ok(CatalogItem::Secret(i))
2270            }
2271            CatalogItem::Func(_) | CatalogItem::Type(_) => {
2272                unreachable!("{}s cannot be renamed", self.typ())
2273            }
2274            CatalogItem::Connection(i) => {
2275                let mut i = i.clone();
2276                i.create_sql = do_rewrite(i.create_sql)?;
2277                Ok(CatalogItem::Connection(i))
2278            }
2279            CatalogItem::MetricSink(i) => {
2280                let mut i = i.clone();
2281                i.create_sql = do_rewrite(i.create_sql)?;
2282                Ok(CatalogItem::MetricSink(i))
2283            }
2284        }
2285    }
2286
2287    /// Returns a clone of `self` with all instances of `old_id` replaced with `new_id`.
2288    pub fn replace_item_refs(&self, old_id: CatalogItemId, new_id: CatalogItemId) -> CatalogItem {
2289        let do_rewrite = |create_sql: String| -> String {
2290            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2291                .expect("invalid create sql persisted to catalog")
2292                .into_element()
2293                .ast;
2294            mz_sql::ast::transform::create_stmt_replace_ids(
2295                &mut create_stmt,
2296                &[(old_id, new_id)].into(),
2297            );
2298            create_stmt.to_ast_string_stable()
2299        };
2300
2301        match self {
2302            CatalogItem::Table(i) => {
2303                let mut i = i.clone();
2304                i.create_sql = i.create_sql.map(do_rewrite);
2305                CatalogItem::Table(i)
2306            }
2307            CatalogItem::Log(i) => CatalogItem::Log(i.clone()),
2308            CatalogItem::Source(i) => {
2309                let mut i = i.clone();
2310                i.create_sql = i.create_sql.map(do_rewrite);
2311                CatalogItem::Source(i)
2312            }
2313            CatalogItem::Sink(i) => {
2314                let mut i = i.clone();
2315                i.create_sql = do_rewrite(i.create_sql);
2316                CatalogItem::Sink(i)
2317            }
2318            CatalogItem::View(i) => {
2319                let mut i = i.clone();
2320                i.create_sql = do_rewrite(i.create_sql);
2321                CatalogItem::View(i)
2322            }
2323            CatalogItem::MaterializedView(i) => {
2324                let mut i = i.clone();
2325                i.create_sql = do_rewrite(i.create_sql);
2326                CatalogItem::MaterializedView(i)
2327            }
2328            CatalogItem::Index(i) => {
2329                let mut i = i.clone();
2330                i.create_sql = do_rewrite(i.create_sql);
2331                CatalogItem::Index(i)
2332            }
2333            CatalogItem::Secret(i) => {
2334                let mut i = i.clone();
2335                i.create_sql = do_rewrite(i.create_sql);
2336                CatalogItem::Secret(i)
2337            }
2338            CatalogItem::Func(_) | CatalogItem::Type(_) => {
2339                unreachable!("references of {}s cannot be replaced", self.typ())
2340            }
2341            CatalogItem::Connection(i) => {
2342                let mut i = i.clone();
2343                i.create_sql = do_rewrite(i.create_sql);
2344                CatalogItem::Connection(i)
2345            }
2346            CatalogItem::MetricSink(i) => {
2347                let mut i = i.clone();
2348                i.create_sql = do_rewrite(i.create_sql);
2349                CatalogItem::MetricSink(i)
2350            }
2351        }
2352    }
2353    /// Updates the retain history for an item. Returns the previous retain history value. Returns
2354    /// an error if this item does not support retain history.
2355    pub fn update_retain_history(
2356        &mut self,
2357        value: Option<Value>,
2358        window: CompactionWindow,
2359    ) -> Result<Option<WithOptionValue<Raw>>, ()> {
2360        let update = |mut ast: &mut Statement<Raw>| {
2361            // Each statement type has unique option types. This macro handles them commonly.
2362            macro_rules! update_retain_history {
2363                ( $stmt:ident, $opt:ident, $name:ident ) => {{
2364                    // Replace or add the option.
2365                    let pos = $stmt
2366                        .with_options
2367                        .iter()
2368                        // In case there are ever multiple, look for the last one.
2369                        .rposition(|o| o.name == mz_sql_parser::ast::$name::RetainHistory);
2370                    if let Some(value) = value {
2371                        let next = mz_sql_parser::ast::$opt {
2372                            name: mz_sql_parser::ast::$name::RetainHistory,
2373                            value: Some(WithOptionValue::RetainHistoryFor(value)),
2374                        };
2375                        if let Some(idx) = pos {
2376                            let previous = $stmt.with_options[idx].clone();
2377                            $stmt.with_options[idx] = next;
2378                            previous.value
2379                        } else {
2380                            $stmt.with_options.push(next);
2381                            None
2382                        }
2383                    } else {
2384                        if let Some(idx) = pos {
2385                            $stmt.with_options.swap_remove(idx).value
2386                        } else {
2387                            None
2388                        }
2389                    }
2390                }};
2391            }
2392            let previous = match &mut ast {
2393                Statement::CreateTable(stmt) => {
2394                    update_retain_history!(stmt, TableOption, TableOptionName)
2395                }
2396                Statement::CreateIndex(stmt) => {
2397                    update_retain_history!(stmt, IndexOption, IndexOptionName)
2398                }
2399                Statement::CreateSource(stmt) => {
2400                    update_retain_history!(stmt, CreateSourceOption, CreateSourceOptionName)
2401                }
2402                Statement::CreateMaterializedView(stmt) => {
2403                    update_retain_history!(stmt, MaterializedViewOption, MaterializedViewOptionName)
2404                }
2405                _ => {
2406                    return Err(());
2407                }
2408            };
2409            Ok(previous)
2410        };
2411
2412        let res = self.update_sql(update)?;
2413        let cw = self
2414            .custom_logical_compaction_window_mut()
2415            .expect("item must have compaction window");
2416        *cw = Some(window);
2417        Ok(res)
2418    }
2419
2420    /// Updates the timestamp interval for a source. Returns the previous timestamp interval
2421    /// value, if any. Returns an error if this item is not a source.
2422    pub fn update_timestamp_interval(
2423        &mut self,
2424        value: Option<Value>,
2425        interval: Duration,
2426    ) -> Result<Option<WithOptionValue<Raw>>, ()> {
2427        let update = |ast: &mut Statement<Raw>| match ast {
2428            Statement::CreateSource(stmt) => {
2429                let pos = stmt.with_options.iter().rposition(|o| {
2430                    o.name == mz_sql_parser::ast::CreateSourceOptionName::TimestampInterval
2431                });
2432                let previous = if let Some(value) = value {
2433                    let next = mz_sql_parser::ast::CreateSourceOption {
2434                        name: mz_sql_parser::ast::CreateSourceOptionName::TimestampInterval,
2435                        value: Some(WithOptionValue::Value(value)),
2436                    };
2437                    if let Some(idx) = pos {
2438                        let previous = stmt.with_options[idx].clone();
2439                        stmt.with_options[idx] = next;
2440                        previous.value
2441                    } else {
2442                        stmt.with_options.push(next);
2443                        None
2444                    }
2445                } else if let Some(idx) = pos {
2446                    stmt.with_options.swap_remove(idx).value
2447                } else {
2448                    None
2449                };
2450                Ok(previous)
2451            }
2452            _ => Err(()),
2453        };
2454
2455        let previous = self.update_sql(update)?;
2456
2457        // Update the in-memory SourceDesc timestamp_interval.
2458        match self {
2459            CatalogItem::Source(source) => {
2460                match &mut source.data_source {
2461                    DataSourceDesc::Ingestion { desc, .. }
2462                    | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
2463                        desc.timestamp_interval = interval;
2464                    }
2465                    _ => return Err(()),
2466                }
2467                Ok(previous)
2468            }
2469            _ => Err(()),
2470        }
2471    }
2472
2473    pub fn add_column(
2474        &mut self,
2475        name: ColumnName,
2476        typ: SqlColumnType,
2477        sql: RawDataType,
2478    ) -> Result<RelationVersion, PlanError> {
2479        let CatalogItem::Table(table) = self else {
2480            return Err(PlanError::Unsupported {
2481                feature: "adding columns to a non-Table".to_string(),
2482                discussion_no: None,
2483            });
2484        };
2485        let next_version = table.desc.add_column(name.clone(), typ);
2486
2487        let update = |mut ast: &mut Statement<Raw>| match &mut ast {
2488            Statement::CreateTable(stmt) => {
2489                let version = ColumnOptionDef {
2490                    name: None,
2491                    option: ColumnOption::Versioned {
2492                        action: ColumnVersioned::Added,
2493                        version: next_version.into(),
2494                    },
2495                };
2496                let column = ColumnDef {
2497                    name: name.into(),
2498                    data_type: sql,
2499                    collation: None,
2500                    options: vec![version],
2501                };
2502                stmt.columns.push(column);
2503                Ok(())
2504            }
2505            _ => Err(()),
2506        };
2507
2508        self.update_sql(update)
2509            .map_err(|()| PlanError::Unstructured("expected CREATE TABLE statement".to_string()))?;
2510        Ok(next_version)
2511    }
2512
2513    /// Updates the create_sql field of this item. Returns an error if this is a builtin item,
2514    /// otherwise returns f's result.
2515    pub fn update_sql<F, T>(&mut self, f: F) -> Result<T, ()>
2516    where
2517        F: FnOnce(&mut Statement<Raw>) -> Result<T, ()>,
2518    {
2519        let create_sql = match self {
2520            CatalogItem::Table(Table { create_sql, .. })
2521            | CatalogItem::Type(Type { create_sql, .. })
2522            | CatalogItem::Source(Source { create_sql, .. }) => create_sql.as_mut(),
2523            CatalogItem::Sink(Sink { create_sql, .. })
2524            | CatalogItem::View(View { create_sql, .. })
2525            | CatalogItem::MaterializedView(MaterializedView { create_sql, .. })
2526            | CatalogItem::Index(Index { create_sql, .. })
2527            | CatalogItem::Secret(Secret { create_sql, .. })
2528            | CatalogItem::Connection(Connection { create_sql, .. })
2529            | CatalogItem::MetricSink(MetricSink { create_sql, .. }) => Some(create_sql),
2530            CatalogItem::Func(_) | CatalogItem::Log(_) => None,
2531        };
2532        let Some(create_sql) = create_sql else {
2533            return Err(());
2534        };
2535        let mut ast = mz_sql_parser::parser::parse_statements(create_sql)
2536            .expect("non-system items must be parseable")
2537            .into_element()
2538            .ast;
2539        debug!("rewrite: {}", ast.to_ast_string_redacted());
2540        let t = f(&mut ast)?;
2541        *create_sql = ast.to_ast_string_stable();
2542        debug!("rewrote: {}", ast.to_ast_string_redacted());
2543        Ok(t)
2544    }
2545
2546    /// If the object is considered a "compute object"
2547    /// (i.e., it is managed by the compute controller),
2548    /// this function returns its cluster ID. Otherwise, it returns nothing.
2549    ///
2550    /// This function differs from `cluster_id` because while all
2551    /// compute objects run on a cluster, the converse is not true.
2552    pub fn is_compute_object_on_cluster(&self) -> Option<ClusterId> {
2553        match self {
2554            CatalogItem::Index(index) => Some(index.cluster_id),
2555            CatalogItem::MetricSink(metric_sink) => Some(metric_sink.cluster_id),
2556            CatalogItem::Table(_)
2557            | CatalogItem::Source(_)
2558            | CatalogItem::Log(_)
2559            | CatalogItem::View(_)
2560            | CatalogItem::MaterializedView(_)
2561            | CatalogItem::Sink(_)
2562            | CatalogItem::Type(_)
2563            | CatalogItem::Func(_)
2564            | CatalogItem::Secret(_)
2565            | CatalogItem::Connection(_) => None,
2566        }
2567    }
2568
2569    /// Whether this item runs a dataflow on its cluster's replicas and so has
2570    /// hydration state: an index, materialized view, sink, or ingestion source.
2571    /// Non-ingestion sources (webhooks, ingestion exports) are bound to a
2572    /// cluster but run no dataflow on any replica.
2573    ///
2574    /// NOTE: `Coordinator::cluster_has_hydratable_objects` routes a cluster onto
2575    /// the hydration-aware scheduling and reconfiguration path off this answer,
2576    /// so an item that ships no dataflow must answer `false` even where the
2577    /// per-replica hydration check would resolve vacuously.
2578    pub fn is_hydratable(&self) -> bool {
2579        match self {
2580            CatalogItem::Index(_)
2581            | CatalogItem::MaterializedView(_)
2582            | CatalogItem::Sink(_)
2583            | CatalogItem::MetricSink(_) => true,
2584            CatalogItem::Source(source) => matches!(
2585                source.data_source,
2586                DataSourceDesc::Ingestion { .. } | DataSourceDesc::OldSyntaxIngestion { .. }
2587            ),
2588            CatalogItem::Table(_)
2589            | CatalogItem::Log(_)
2590            | CatalogItem::View(_)
2591            | CatalogItem::Type(_)
2592            | CatalogItem::Func(_)
2593            | CatalogItem::Secret(_)
2594            | CatalogItem::Connection(_) => false,
2595        }
2596    }
2597
2598    pub fn cluster_id(&self) -> Option<ClusterId> {
2599        match self {
2600            CatalogItem::MaterializedView(mv) => Some(mv.cluster_id),
2601            CatalogItem::Index(index) => Some(index.cluster_id),
2602            CatalogItem::MetricSink(metric_sink) => Some(metric_sink.cluster_id),
2603            CatalogItem::Source(source) => match &source.data_source {
2604                DataSourceDesc::Ingestion { cluster_id, .. }
2605                | DataSourceDesc::OldSyntaxIngestion { cluster_id, .. } => Some(*cluster_id),
2606                // This is somewhat of a lie because the export runs on the same
2607                // cluster as its ingestion but we don't yet have a way of
2608                // cross-referencing the items
2609                DataSourceDesc::IngestionExport { .. } => None,
2610                DataSourceDesc::Webhook { cluster_id, .. } => Some(*cluster_id),
2611                DataSourceDesc::Introspection(_)
2612                | DataSourceDesc::Progress
2613                | DataSourceDesc::Catalog => None,
2614            },
2615            CatalogItem::Sink(sink) => Some(sink.cluster_id),
2616            CatalogItem::Table(_)
2617            | CatalogItem::Log(_)
2618            | CatalogItem::View(_)
2619            | CatalogItem::Type(_)
2620            | CatalogItem::Func(_)
2621            | CatalogItem::Secret(_)
2622            | CatalogItem::Connection(_) => None,
2623        }
2624    }
2625
2626    /// The custom compaction window, if any has been set. This does not reflect any propagated
2627    /// compaction window (i.e., source -> subsource).
2628    pub fn custom_logical_compaction_window(&self) -> Option<CompactionWindow> {
2629        match self {
2630            CatalogItem::Table(table) => table.custom_logical_compaction_window,
2631            CatalogItem::Source(source) => source.custom_logical_compaction_window,
2632            CatalogItem::Index(index) => index.custom_logical_compaction_window,
2633            CatalogItem::MaterializedView(mview) => mview.custom_logical_compaction_window,
2634            CatalogItem::Log(_)
2635            | CatalogItem::View(_)
2636            | CatalogItem::Sink(_)
2637            | CatalogItem::Type(_)
2638            | CatalogItem::Func(_)
2639            | CatalogItem::Secret(_)
2640            | CatalogItem::Connection(_)
2641            | CatalogItem::MetricSink(_) => None,
2642        }
2643    }
2644
2645    /// Mutable access to the custom compaction window, or None if this type does not support custom
2646    /// compaction windows. This does not reflect any propagated compaction window (i.e., source ->
2647    /// subsource).
2648    pub fn custom_logical_compaction_window_mut(
2649        &mut self,
2650    ) -> Option<&mut Option<CompactionWindow>> {
2651        let cw = match self {
2652            CatalogItem::Table(table) => &mut table.custom_logical_compaction_window,
2653            CatalogItem::Source(source) => &mut source.custom_logical_compaction_window,
2654            CatalogItem::Index(index) => &mut index.custom_logical_compaction_window,
2655            CatalogItem::MaterializedView(mview) => &mut mview.custom_logical_compaction_window,
2656            CatalogItem::Log(_)
2657            | CatalogItem::View(_)
2658            | CatalogItem::Sink(_)
2659            | CatalogItem::Type(_)
2660            | CatalogItem::Func(_)
2661            | CatalogItem::Secret(_)
2662            | CatalogItem::Connection(_)
2663            | CatalogItem::MetricSink(_) => return None,
2664        };
2665        Some(cw)
2666    }
2667
2668    /// The initial compaction window, for objects that have one; that is, tables, sources, indexes,
2669    /// and MVs. This does not reflect any propagated compaction window (i.e., source -> subsource).
2670    ///
2671    /// If `custom_logical_compaction_window()` returns something, use that.  Otherwise, use a
2672    /// sensible default (currently 1s).
2673    ///
2674    /// For objects that do not have the concept of compaction window, return None.
2675    pub fn initial_logical_compaction_window(&self) -> Option<CompactionWindow> {
2676        let custom_logical_compaction_window = match self {
2677            CatalogItem::Table(_)
2678            | CatalogItem::Source(_)
2679            | CatalogItem::Index(_)
2680            | CatalogItem::MaterializedView(_) => self.custom_logical_compaction_window(),
2681            CatalogItem::Log(_)
2682            | CatalogItem::View(_)
2683            | CatalogItem::Sink(_)
2684            | CatalogItem::Type(_)
2685            | CatalogItem::Func(_)
2686            | CatalogItem::Secret(_)
2687            | CatalogItem::Connection(_)
2688            | CatalogItem::MetricSink(_) => return None,
2689        };
2690        Some(custom_logical_compaction_window.unwrap_or(CompactionWindow::Default))
2691    }
2692
2693    /// Whether the item's logical compaction window
2694    /// is controlled by the METRICS_RETENTION
2695    /// system var.
2696    pub fn is_retained_metrics_object(&self) -> bool {
2697        match self {
2698            CatalogItem::Table(table) => table.is_retained_metrics_object,
2699            CatalogItem::Source(source) => source.is_retained_metrics_object,
2700            CatalogItem::Index(index) => index.is_retained_metrics_object,
2701            CatalogItem::Log(_)
2702            | CatalogItem::View(_)
2703            | CatalogItem::MaterializedView(_)
2704            | CatalogItem::Sink(_)
2705            | CatalogItem::Type(_)
2706            | CatalogItem::Func(_)
2707            | CatalogItem::Secret(_)
2708            | CatalogItem::Connection(_)
2709            | CatalogItem::MetricSink(_) => false,
2710        }
2711    }
2712
2713    pub fn to_serialized(&self) -> (String, GlobalId, BTreeMap<RelationVersion, GlobalId>) {
2714        match self {
2715            CatalogItem::Table(table) => {
2716                let create_sql = table
2717                    .create_sql
2718                    .clone()
2719                    .expect("builtin tables cannot be serialized");
2720                let mut collections = table.collections.clone();
2721                let global_id = collections
2722                    .remove(&RelationVersion::root())
2723                    .expect("at least one version");
2724                (create_sql, global_id, collections)
2725            }
2726            CatalogItem::Log(_) => unreachable!("builtin logs cannot be serialized"),
2727            CatalogItem::Source(source) => {
2728                assert!(
2729                    !matches!(source.data_source, DataSourceDesc::Introspection(_)),
2730                    "cannot serialize introspection/builtin sources",
2731                );
2732                let create_sql = source
2733                    .create_sql
2734                    .clone()
2735                    .expect("builtin sources cannot be serialized");
2736                (create_sql, source.global_id, BTreeMap::new())
2737            }
2738            CatalogItem::View(view) => (view.create_sql.clone(), view.global_id, BTreeMap::new()),
2739            CatalogItem::MaterializedView(mview) => {
2740                let mut collections = mview.collections.clone();
2741                let global_id = collections
2742                    .remove(&RelationVersion::root())
2743                    .expect("at least one version");
2744                (mview.create_sql.clone(), global_id, collections)
2745            }
2746            CatalogItem::Index(index) => {
2747                (index.create_sql.clone(), index.global_id, BTreeMap::new())
2748            }
2749            CatalogItem::Sink(sink) => (sink.create_sql.clone(), sink.global_id, BTreeMap::new()),
2750            CatalogItem::Type(typ) => {
2751                let create_sql = typ
2752                    .create_sql
2753                    .clone()
2754                    .expect("builtin types cannot be serialized");
2755                (create_sql, typ.global_id, BTreeMap::new())
2756            }
2757            CatalogItem::Secret(secret) => {
2758                (secret.create_sql.clone(), secret.global_id, BTreeMap::new())
2759            }
2760            CatalogItem::Connection(connection) => (
2761                connection.create_sql.clone(),
2762                connection.global_id,
2763                BTreeMap::new(),
2764            ),
2765            CatalogItem::Func(_) => unreachable!("cannot serialize functions yet"),
2766            CatalogItem::MetricSink(ms) => (ms.create_sql.clone(), ms.global_id, BTreeMap::new()),
2767        }
2768    }
2769
2770    pub fn into_serialized(self) -> (String, GlobalId, BTreeMap<RelationVersion, GlobalId>) {
2771        match self {
2772            CatalogItem::Table(mut table) => {
2773                let create_sql = table
2774                    .create_sql
2775                    .expect("builtin tables cannot be serialized");
2776                let global_id = table
2777                    .collections
2778                    .remove(&RelationVersion::root())
2779                    .expect("at least one version");
2780                (create_sql, global_id, table.collections)
2781            }
2782            CatalogItem::Log(_) => unreachable!("builtin logs cannot be serialized"),
2783            CatalogItem::Source(source) => {
2784                assert!(
2785                    !matches!(source.data_source, DataSourceDesc::Introspection(_)),
2786                    "cannot serialize introspection/builtin sources",
2787                );
2788                let create_sql = source
2789                    .create_sql
2790                    .expect("builtin sources cannot be serialized");
2791                (create_sql, source.global_id, BTreeMap::new())
2792            }
2793            CatalogItem::View(view) => (view.create_sql, view.global_id, BTreeMap::new()),
2794            CatalogItem::MaterializedView(mut mview) => {
2795                let global_id = mview
2796                    .collections
2797                    .remove(&RelationVersion::root())
2798                    .expect("at least one version");
2799                (mview.create_sql, global_id, mview.collections)
2800            }
2801            CatalogItem::Index(index) => (index.create_sql, index.global_id, BTreeMap::new()),
2802            CatalogItem::Sink(sink) => (sink.create_sql, sink.global_id, BTreeMap::new()),
2803            CatalogItem::Type(typ) => {
2804                let create_sql = typ.create_sql.expect("builtin types cannot be serialized");
2805                (create_sql, typ.global_id, BTreeMap::new())
2806            }
2807            CatalogItem::Secret(secret) => (secret.create_sql, secret.global_id, BTreeMap::new()),
2808            CatalogItem::Connection(connection) => {
2809                (connection.create_sql, connection.global_id, BTreeMap::new())
2810            }
2811            CatalogItem::Func(_) => unreachable!("cannot serialize functions yet"),
2812            CatalogItem::MetricSink(ms) => (ms.create_sql, ms.global_id, BTreeMap::new()),
2813        }
2814    }
2815
2816    /// Returns a global ID for a specific version selector. Returns `None` if the item does
2817    /// not have versions or if the version does not exist.
2818    pub fn global_id_for_version(&self, version: RelationVersionSelector) -> Option<GlobalId> {
2819        let collections = match self {
2820            CatalogItem::MaterializedView(mv) => &mv.collections,
2821            CatalogItem::Table(table) => &table.collections,
2822            CatalogItem::Source(source) => return Some(source.global_id),
2823            CatalogItem::Log(log) => return Some(log.global_id),
2824            CatalogItem::View(view) => return Some(view.global_id),
2825            CatalogItem::Sink(sink) => return Some(sink.global_id),
2826            CatalogItem::Index(index) => return Some(index.global_id),
2827            CatalogItem::Type(ty) => return Some(ty.global_id),
2828            CatalogItem::Func(func) => return Some(func.global_id),
2829            CatalogItem::Secret(secret) => return Some(secret.global_id),
2830            CatalogItem::Connection(conn) => return Some(conn.global_id),
2831            CatalogItem::MetricSink(metric_sink) => return Some(metric_sink.global_id),
2832        };
2833        match version {
2834            RelationVersionSelector::Latest => collections.values().last().copied(),
2835            RelationVersionSelector::Specific(version) => collections.get(&version).copied(),
2836        }
2837    }
2838}
2839
2840impl CatalogEntry {
2841    /// Reports the latest [`RelationDesc`] of the rows produced by this [`CatalogEntry`], if it
2842    /// produces rows.
2843    pub fn relation_desc_latest(&self) -> Option<Cow<'_, RelationDesc>> {
2844        self.item.relation_desc(RelationVersionSelector::Latest)
2845    }
2846
2847    /// Reports if the item has columns.
2848    pub fn has_columns(&self) -> bool {
2849        match self.item() {
2850            CatalogItem::Type(Type { details, .. }) => {
2851                matches!(details.typ, CatalogType::Record { .. })
2852            }
2853            _ => self.relation_desc_latest().is_some(),
2854        }
2855    }
2856
2857    /// Returns the [`mz_sql::func::Func`] associated with this `CatalogEntry`.
2858    pub fn func(&self) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
2859        self.item.func(self)
2860    }
2861
2862    /// Returns the inner [`Index`] if this entry is an index, else `None`.
2863    pub fn index(&self) -> Option<&Index> {
2864        match self.item() {
2865            CatalogItem::Index(idx) => Some(idx),
2866            _ => None,
2867        }
2868    }
2869
2870    /// Returns the inner [`MaterializedView`] if this entry is a materialized view, else `None`.
2871    pub fn materialized_view(&self) -> Option<&MaterializedView> {
2872        match self.item() {
2873            CatalogItem::MaterializedView(mv) => Some(mv),
2874            _ => None,
2875        }
2876    }
2877
2878    /// Returns the inner [`Table`] if this entry is a table, else `None`.
2879    pub fn table(&self) -> Option<&Table> {
2880        match self.item() {
2881            CatalogItem::Table(tbl) => Some(tbl),
2882            _ => None,
2883        }
2884    }
2885
2886    /// Returns the inner [`Source`] if this entry is a source, else `None`.
2887    pub fn source(&self) -> Option<&Source> {
2888        match self.item() {
2889            CatalogItem::Source(src) => Some(src),
2890            _ => None,
2891        }
2892    }
2893
2894    /// Returns the inner [`Sink`] if this entry is a sink, else `None`.
2895    pub fn sink(&self) -> Option<&Sink> {
2896        match self.item() {
2897            CatalogItem::Sink(sink) => Some(sink),
2898            _ => None,
2899        }
2900    }
2901
2902    /// Returns the inner [`Secret`] if this entry is a secret, else `None`.
2903    pub fn secret(&self) -> Option<&Secret> {
2904        match self.item() {
2905            CatalogItem::Secret(secret) => Some(secret),
2906            _ => None,
2907        }
2908    }
2909
2910    pub fn connection(&self) -> Result<&Connection, SqlCatalogError> {
2911        match self.item() {
2912            CatalogItem::Connection(connection) => Ok(connection),
2913            _ => {
2914                let db_name = match self.name().qualifiers.database_spec {
2915                    ResolvedDatabaseSpecifier::Ambient => "".to_string(),
2916                    ResolvedDatabaseSpecifier::Id(id) => format!("{id}."),
2917                };
2918                Err(SqlCatalogError::UnknownConnection(format!(
2919                    "{}{}.{}",
2920                    db_name,
2921                    self.name().qualifiers.schema_spec,
2922                    self.name().item
2923                )))
2924            }
2925        }
2926    }
2927
2928    /// Returns the [`mz_storage_types::sources::SourceDesc`] associated with
2929    /// this `CatalogEntry`, if any.
2930    pub fn source_desc(
2931        &self,
2932    ) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
2933        self.item.source_desc(self)
2934    }
2935
2936    /// Reports whether this catalog entry is a connection.
2937    pub fn is_connection(&self) -> bool {
2938        matches!(self.item(), CatalogItem::Connection(_))
2939    }
2940
2941    /// Reports whether this catalog entry is a table.
2942    pub fn is_table(&self) -> bool {
2943        matches!(self.item(), CatalogItem::Table(_))
2944    }
2945
2946    /// Reports whether this catalog entry is a source. Note that this includes
2947    /// subsources.
2948    pub fn is_source(&self) -> bool {
2949        matches!(self.item(), CatalogItem::Source(_))
2950    }
2951
2952    /// Reports whether this catalog entry is a subsource and, if it is, the
2953    /// ingestion it is an export of, as well as the item it exports.
2954    pub fn subsource_details(
2955        &self,
2956    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)> {
2957        match &self.item() {
2958            CatalogItem::Source(source) => match &source.data_source {
2959                DataSourceDesc::IngestionExport {
2960                    ingestion_id,
2961                    external_reference,
2962                    details,
2963                    data_config: _,
2964                } => Some((*ingestion_id, external_reference, details)),
2965                _ => None,
2966            },
2967            _ => None,
2968        }
2969    }
2970
2971    /// Reports whether this catalog entry is a source export and, if it is, the
2972    /// ingestion it is an export of, as well as the item it exports.
2973    pub fn source_export_details(
2974        &self,
2975    ) -> Option<(
2976        CatalogItemId,
2977        &UnresolvedItemName,
2978        &SourceExportDetails,
2979        &SourceExportDataConfig<ReferencedConnection>,
2980    )> {
2981        match &self.item() {
2982            CatalogItem::Source(source) => match &source.data_source {
2983                DataSourceDesc::IngestionExport {
2984                    ingestion_id,
2985                    external_reference,
2986                    details,
2987                    data_config,
2988                } => Some((*ingestion_id, external_reference, details, data_config)),
2989                _ => None,
2990            },
2991            CatalogItem::Table(table) => match &table.data_source {
2992                TableDataSource::DataSource {
2993                    desc:
2994                        DataSourceDesc::IngestionExport {
2995                            ingestion_id,
2996                            external_reference,
2997                            details,
2998                            data_config,
2999                        },
3000                    timeline: _,
3001                } => Some((*ingestion_id, external_reference, details, data_config)),
3002                _ => None,
3003            },
3004            _ => None,
3005        }
3006    }
3007
3008    /// Reports whether this catalog entry is a progress source.
3009    pub fn is_progress_source(&self) -> bool {
3010        self.item().is_progress_source()
3011    }
3012
3013    /// Returns the `GlobalId` of all of this entry's progress ID.
3014    pub fn progress_id(&self) -> Option<CatalogItemId> {
3015        match &self.item() {
3016            CatalogItem::Source(source) => match &source.data_source {
3017                DataSourceDesc::Ingestion { .. } => Some(self.id),
3018                DataSourceDesc::OldSyntaxIngestion {
3019                    progress_subsource, ..
3020                } => Some(*progress_subsource),
3021                DataSourceDesc::IngestionExport { .. }
3022                | DataSourceDesc::Introspection(_)
3023                | DataSourceDesc::Progress
3024                | DataSourceDesc::Webhook { .. }
3025                | DataSourceDesc::Catalog => None,
3026            },
3027            CatalogItem::Table(_)
3028            | CatalogItem::Log(_)
3029            | CatalogItem::View(_)
3030            | CatalogItem::MaterializedView(_)
3031            | CatalogItem::Sink(_)
3032            | CatalogItem::Index(_)
3033            | CatalogItem::Type(_)
3034            | CatalogItem::Func(_)
3035            | CatalogItem::Secret(_)
3036            | CatalogItem::Connection(_)
3037            | CatalogItem::MetricSink(_) => None,
3038        }
3039    }
3040
3041    /// Reports whether this catalog entry is a sink.
3042    pub fn is_sink(&self) -> bool {
3043        matches!(self.item(), CatalogItem::Sink(_))
3044    }
3045
3046    /// Reports whether this catalog entry is a materialized view.
3047    pub fn is_materialized_view(&self) -> bool {
3048        matches!(self.item(), CatalogItem::MaterializedView(_))
3049    }
3050
3051    /// Reports whether this catalog entry is a view.
3052    pub fn is_view(&self) -> bool {
3053        matches!(self.item(), CatalogItem::View(_))
3054    }
3055
3056    /// Reports whether this catalog entry is a secret.
3057    pub fn is_secret(&self) -> bool {
3058        matches!(self.item(), CatalogItem::Secret(_))
3059    }
3060
3061    /// Reports whether this catalog entry is an introspection source.
3062    pub fn is_introspection_source(&self) -> bool {
3063        matches!(self.item(), CatalogItem::Log(_))
3064    }
3065
3066    /// Reports whether this catalog entry is an index.
3067    pub fn is_index(&self) -> bool {
3068        matches!(self.item(), CatalogItem::Index(_))
3069    }
3070
3071    /// Reports whether this catalog entry is a metric sink.
3072    pub fn is_metric_sink(&self) -> bool {
3073        matches!(self.item(), CatalogItem::MetricSink(_))
3074    }
3075
3076    /// Reports whether this catalog entry can be treated as a relation, it can produce rows.
3077    pub fn is_relation(&self) -> bool {
3078        mz_sql::catalog::ObjectType::from(self.item_type()).is_relation()
3079    }
3080
3081    /// Collects the identifiers of the objects that were encountered when
3082    /// resolving names in the item's DDL statement.
3083    pub fn references(&self) -> &ResolvedIds {
3084        self.item.references()
3085    }
3086
3087    /// Collects the identifiers of the objects used by this [`CatalogEntry`].
3088    ///
3089    /// Like [`CatalogEntry::references()`] but also includes objects that are not directly
3090    /// referenced. For example this will include any catalog objects used to implement functions
3091    /// and casts in the item.
3092    pub fn uses(&self) -> BTreeSet<CatalogItemId> {
3093        self.item.uses()
3094    }
3095
3096    /// Returns the `CatalogItem` associated with this catalog entry.
3097    pub fn item(&self) -> &CatalogItem {
3098        &self.item
3099    }
3100
3101    /// Returns a mutable reference to the `CatalogItem` associated with this
3102    /// catalog entry.
3103    pub fn item_mut(&mut self) -> &mut CatalogItem {
3104        &mut self.item
3105    }
3106
3107    /// Returns the [`CatalogItemId`] of this catalog entry.
3108    pub fn id(&self) -> CatalogItemId {
3109        self.id
3110    }
3111
3112    /// Returns all of the [`GlobalId`]s associated with this item.
3113    pub fn global_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
3114        self.item().global_ids()
3115    }
3116
3117    pub fn latest_global_id(&self) -> GlobalId {
3118        self.item().latest_global_id()
3119    }
3120
3121    /// Returns the OID of this catalog entry.
3122    pub fn oid(&self) -> u32 {
3123        self.oid
3124    }
3125
3126    /// Returns the fully qualified name of this catalog entry.
3127    pub fn name(&self) -> &QualifiedItemName {
3128        &self.name
3129    }
3130
3131    /// Returns the identifiers of the dataflows that are directly referenced by this dataflow.
3132    pub fn referenced_by(&self) -> &[CatalogItemId] {
3133        &self.referenced_by
3134    }
3135
3136    /// Returns the identifiers of the dataflows that depend upon this dataflow.
3137    pub fn used_by(&self) -> &[CatalogItemId] {
3138        &self.used_by
3139    }
3140
3141    /// Returns the connection ID that this item belongs to, if this item is
3142    /// temporary.
3143    pub fn conn_id(&self) -> Option<&ConnectionId> {
3144        self.item.conn_id()
3145    }
3146
3147    /// Returns the role ID of the entry owner.
3148    pub fn owner_id(&self) -> &RoleId {
3149        &self.owner_id
3150    }
3151
3152    /// Returns the privileges of the entry.
3153    pub fn privileges(&self) -> &PrivilegeMap {
3154        &self.privileges
3155    }
3156
3157    /// Returns the comment object ID for this entry.
3158    pub fn comment_object_id(&self) -> CommentObjectId {
3159        use CatalogItemType::*;
3160        match self.item_type() {
3161            Table => CommentObjectId::Table(self.id),
3162            Source => CommentObjectId::Source(self.id),
3163            Sink => CommentObjectId::Sink(self.id),
3164            View => CommentObjectId::View(self.id),
3165            MaterializedView => CommentObjectId::MaterializedView(self.id),
3166            Index => CommentObjectId::Index(self.id),
3167            Func => CommentObjectId::Func(self.id),
3168            Connection => CommentObjectId::Connection(self.id),
3169            Type => CommentObjectId::Type(self.id),
3170            Secret => CommentObjectId::Secret(self.id),
3171            MetricSink => CommentObjectId::MetricSink(self.id),
3172        }
3173    }
3174}
3175
3176#[derive(Debug, Clone, Default)]
3177pub struct CommentsMap {
3178    map: BTreeMap<CommentObjectId, BTreeMap<Option<usize>, String>>,
3179}
3180
3181impl CommentsMap {
3182    pub fn update_comment(
3183        &mut self,
3184        object_id: CommentObjectId,
3185        sub_component: Option<usize>,
3186        comment: Option<String>,
3187    ) -> Option<String> {
3188        let object_comments = self.map.entry(object_id).or_default();
3189
3190        // Either replace the existing comment, or remove it if comment is None/NULL.
3191        let (empty, prev) = if let Some(comment) = comment {
3192            let prev = object_comments.insert(sub_component, comment);
3193            (false, prev)
3194        } else {
3195            let prev = object_comments.remove(&sub_component);
3196            (object_comments.is_empty(), prev)
3197        };
3198
3199        // Cleanup entries that are now empty.
3200        if empty {
3201            self.map.remove(&object_id);
3202        }
3203
3204        // Return the previous comment, if there was one, for easy removal.
3205        prev
3206    }
3207
3208    /// Remove all comments for `object_id` from the map.
3209    ///
3210    /// Generally there is one comment for a given [`CommentObjectId`], but in the case of
3211    /// relations you can also have comments on the individual columns. Dropping the comments for a
3212    /// relation will also drop all of the comments on any columns.
3213    pub fn drop_comments(
3214        &mut self,
3215        object_ids: &BTreeSet<CommentObjectId>,
3216    ) -> Vec<(CommentObjectId, Option<usize>, String)> {
3217        let mut removed_comments = Vec::new();
3218
3219        for object_id in object_ids {
3220            if let Some(comments) = self.map.remove(object_id) {
3221                let removed = comments
3222                    .into_iter()
3223                    .map(|(sub_comp, comment)| (object_id.clone(), sub_comp, comment));
3224                removed_comments.extend(removed);
3225            }
3226        }
3227
3228        removed_comments
3229    }
3230
3231    pub fn iter(&self) -> impl Iterator<Item = (CommentObjectId, Option<usize>, &str)> {
3232        self.map
3233            .iter()
3234            .map(|(id, comments)| {
3235                comments
3236                    .iter()
3237                    .map(|(pos, comment)| (*id, *pos, comment.as_str()))
3238            })
3239            .flatten()
3240    }
3241
3242    pub fn get_object_comments(
3243        &self,
3244        object_id: CommentObjectId,
3245    ) -> Option<&BTreeMap<Option<usize>, String>> {
3246        self.map.get(&object_id)
3247    }
3248}
3249
3250impl Serialize for CommentsMap {
3251    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3252    where
3253        S: serde::Serializer,
3254    {
3255        let comment_count = self
3256            .map
3257            .iter()
3258            .map(|(_object_id, comments)| comments.len())
3259            .sum();
3260
3261        let mut seq = serializer.serialize_seq(Some(comment_count))?;
3262        for (object_id, sub) in &self.map {
3263            for (sub_component, comment) in sub {
3264                seq.serialize_element(&(
3265                    format!("{object_id:?}"),
3266                    format!("{sub_component:?}"),
3267                    comment,
3268                ))?;
3269            }
3270        }
3271        seq.end()
3272    }
3273}
3274
3275#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Default)]
3276pub struct DefaultPrivileges {
3277    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
3278    privileges: BTreeMap<DefaultPrivilegeObject, RoleDefaultPrivileges>,
3279}
3280
3281// Use a new type here because otherwise we have two levels of BTreeMap, both needing
3282// map_key_to_string.
3283#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Default)]
3284struct RoleDefaultPrivileges(
3285    /// Denormalized, the key is the grantee Role.
3286    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
3287    BTreeMap<RoleId, DefaultPrivilegeAclItem>,
3288);
3289
3290impl Deref for RoleDefaultPrivileges {
3291    type Target = BTreeMap<RoleId, DefaultPrivilegeAclItem>;
3292
3293    fn deref(&self) -> &Self::Target {
3294        &self.0
3295    }
3296}
3297
3298impl DerefMut for RoleDefaultPrivileges {
3299    fn deref_mut(&mut self) -> &mut Self::Target {
3300        &mut self.0
3301    }
3302}
3303
3304impl DefaultPrivileges {
3305    /// Add a new default privilege into the set of all default privileges.
3306    pub fn grant(&mut self, object: DefaultPrivilegeObject, privilege: DefaultPrivilegeAclItem) {
3307        if privilege.acl_mode.is_empty() {
3308            return;
3309        }
3310
3311        let privileges = self.privileges.entry(object).or_default();
3312        if let Some(default_privilege) = privileges.get_mut(&privilege.grantee) {
3313            default_privilege.acl_mode |= privilege.acl_mode;
3314        } else {
3315            privileges.insert(privilege.grantee, privilege);
3316        }
3317    }
3318
3319    /// Revoke a default privilege from the set of all default privileges.
3320    pub fn revoke(&mut self, object: &DefaultPrivilegeObject, privilege: &DefaultPrivilegeAclItem) {
3321        if let Some(privileges) = self.privileges.get_mut(object) {
3322            if let Some(default_privilege) = privileges.get_mut(&privilege.grantee) {
3323                default_privilege.acl_mode =
3324                    default_privilege.acl_mode.difference(privilege.acl_mode);
3325                if default_privilege.acl_mode.is_empty() {
3326                    privileges.remove(&privilege.grantee);
3327                }
3328            }
3329            if privileges.is_empty() {
3330                self.privileges.remove(object);
3331            }
3332        }
3333    }
3334
3335    /// Get the privileges that will be granted on all objects matching `object` to `grantee`, if
3336    /// any exist.
3337    pub fn get_privileges_for_grantee(
3338        &self,
3339        object: &DefaultPrivilegeObject,
3340        grantee: &RoleId,
3341    ) -> Option<&AclMode> {
3342        self.privileges
3343            .get(object)
3344            .and_then(|privileges| privileges.get(grantee))
3345            .map(|privilege| &privilege.acl_mode)
3346    }
3347
3348    /// Get all default privileges that apply to the provided object details.
3349    pub fn get_applicable_privileges(
3350        &self,
3351        role_id: RoleId,
3352        database_id: Option<DatabaseId>,
3353        schema_id: Option<SchemaId>,
3354        object_type: mz_sql::catalog::ObjectType,
3355    ) -> impl Iterator<Item = DefaultPrivilegeAclItem> + '_ {
3356        // Privileges consider all relations to be of type table due to PostgreSQL compatibility. We
3357        // don't require the caller to worry about that and we will map their `object_type` to the
3358        // correct type for privileges.
3359        let privilege_object_type = if object_type.is_relation() {
3360            mz_sql::catalog::ObjectType::Table
3361        } else {
3362            object_type
3363        };
3364        let valid_acl_mode = rbac::all_object_privileges(SystemObjectType::Object(object_type));
3365
3366        // Collect all entries that apply to the provided object details.
3367        // If either `database_id` or `schema_id` are `None`, then we might end up with duplicate
3368        // entries in the vec below. That's OK because we consolidate the results after.
3369        [
3370            DefaultPrivilegeObject {
3371                role_id,
3372                database_id,
3373                schema_id,
3374                object_type: privilege_object_type,
3375            },
3376            DefaultPrivilegeObject {
3377                role_id,
3378                database_id,
3379                schema_id: None,
3380                object_type: privilege_object_type,
3381            },
3382            DefaultPrivilegeObject {
3383                role_id,
3384                database_id: None,
3385                schema_id: None,
3386                object_type: privilege_object_type,
3387            },
3388            DefaultPrivilegeObject {
3389                role_id: RoleId::Public,
3390                database_id,
3391                schema_id,
3392                object_type: privilege_object_type,
3393            },
3394            DefaultPrivilegeObject {
3395                role_id: RoleId::Public,
3396                database_id,
3397                schema_id: None,
3398                object_type: privilege_object_type,
3399            },
3400            DefaultPrivilegeObject {
3401                role_id: RoleId::Public,
3402                database_id: None,
3403                schema_id: None,
3404                object_type: privilege_object_type,
3405            },
3406        ]
3407        .into_iter()
3408        .filter_map(|object| self.privileges.get(&object))
3409        .flat_map(|acl_map| acl_map.values())
3410        // Consolidate privileges with a common grantee.
3411        .fold(
3412            BTreeMap::new(),
3413            |mut accum, DefaultPrivilegeAclItem { grantee, acl_mode }| {
3414                let accum_acl_mode = accum.entry(grantee).or_insert_with(AclMode::empty);
3415                *accum_acl_mode |= *acl_mode;
3416                accum
3417            },
3418        )
3419        .into_iter()
3420        // Restrict the acl_mode to only privileges valid for the provided object type. If the
3421        // default privilege has an object type of Table, then it may contain privileges valid for
3422        // tables but not other relations. If the passed in object type is another relation, then
3423        // we need to remove any privilege that is not valid for the specified relation.
3424        .map(move |(grantee, acl_mode)| (grantee, acl_mode & valid_acl_mode))
3425        // Filter out empty privileges.
3426        .filter(|(_, acl_mode)| !acl_mode.is_empty())
3427        .map(|(grantee, acl_mode)| DefaultPrivilegeAclItem {
3428            grantee: *grantee,
3429            acl_mode,
3430        })
3431    }
3432
3433    pub fn iter(
3434        &self,
3435    ) -> impl Iterator<
3436        Item = (
3437            &DefaultPrivilegeObject,
3438            impl Iterator<Item = &DefaultPrivilegeAclItem>,
3439        ),
3440    > {
3441        self.privileges
3442            .iter()
3443            .map(|(object, acl_map)| (object, acl_map.values()))
3444    }
3445}
3446
3447#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3448pub struct ClusterConfig {
3449    pub variant: ClusterVariant,
3450    pub workload_class: Option<String>,
3451}
3452
3453impl ClusterConfig {
3454    pub fn features(&self) -> Option<&OptimizerFeatureOverrides> {
3455        match &self.variant {
3456            ClusterVariant::Managed(managed) => Some(&managed.optimizer_feature_overrides),
3457            ClusterVariant::Unmanaged => None,
3458        }
3459    }
3460}
3461
3462impl From<ClusterConfig> for durable::ClusterConfig {
3463    fn from(config: ClusterConfig) -> Self {
3464        Self {
3465            variant: config.variant.into(),
3466            workload_class: config.workload_class,
3467        }
3468    }
3469}
3470
3471impl From<durable::ClusterConfig> for ClusterConfig {
3472    fn from(config: durable::ClusterConfig) -> Self {
3473        Self {
3474            variant: config.variant.into(),
3475            workload_class: config.workload_class,
3476        }
3477    }
3478}
3479
3480#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3481pub struct ClusterVariantManaged {
3482    pub size: String,
3483    pub availability_zones: Vec<String>,
3484    pub logging: ReplicaLogging,
3485    /// Whether arrangements on this cluster's replicas request dictionary compression.
3486    pub arrangement_compression: bool,
3487    pub replication_factor: u32,
3488    pub optimizer_feature_overrides: OptimizerFeatureOverrides,
3489    pub schedule: ClusterSchedule,
3490    /// User-configured autoscaling policy, distinct from the in-flight runtime
3491    /// records below. Shared with the durable layer, like [`ClusterSchedule`].
3492    pub auto_scaling_strategy: Option<AutoScalingStrategy>,
3493    /// Latest graceful reconfiguration record, if one has been written.
3494    pub reconfiguration: Option<ReconfigurationState>,
3495    /// In-flight hydration burst the controller is running.
3496    pub burst: Option<BurstState>,
3497}
3498
3499/// Per-replica config shape of a managed cluster.
3500#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3501pub struct ManagedReplicaConfigShape<'a> {
3502    pub size: &'a str,
3503    pub availability_zones: &'a [String],
3504    pub logging: &'a ReplicaLogging,
3505    pub arrangement_compression: bool,
3506}
3507
3508impl<'a> ManagedReplicaConfigShape<'a> {
3509    /// Returns a per-replica config shape from its component fields.
3510    pub fn new(
3511        size: &'a str,
3512        availability_zones: &'a [String],
3513        logging: &'a ReplicaLogging,
3514        arrangement_compression: bool,
3515    ) -> Self {
3516        Self {
3517            size,
3518            availability_zones,
3519            logging,
3520            arrangement_compression,
3521        }
3522    }
3523}
3524
3525impl ClusterVariantManaged {
3526    /// Returns the per-replica config shape of this managed cluster.
3527    pub fn replica_config_shape(&self) -> ManagedReplicaConfigShape<'_> {
3528        let ClusterVariantManaged {
3529            size,
3530            availability_zones,
3531            logging,
3532            arrangement_compression,
3533            replication_factor: _,
3534            optimizer_feature_overrides: _,
3535            schedule: _,
3536            auto_scaling_strategy: _,
3537            reconfiguration: _,
3538            burst: _,
3539        } = self;
3540        ManagedReplicaConfigShape::new(size, availability_zones, logging, *arrangement_compression)
3541    }
3542
3543    /// Returns this managed cluster's realized shape as a reconfiguration target.
3544    pub fn realized_reconfiguration_target(&self) -> ReconfigurationTarget {
3545        let ClusterVariantManaged {
3546            size,
3547            availability_zones,
3548            logging,
3549            arrangement_compression,
3550            replication_factor,
3551            optimizer_feature_overrides: _,
3552            schedule: _,
3553            auto_scaling_strategy: _,
3554            reconfiguration: _,
3555            burst: _,
3556        } = self;
3557        ReconfigurationTarget {
3558            size: size.clone(),
3559            replication_factor: *replication_factor,
3560            availability_zones: availability_zones.clone(),
3561            logging: logging.clone(),
3562            arrangement_compression: *arrangement_compression,
3563        }
3564    }
3565
3566    /// Advances this config's realized shape to `target`, the write a
3567    /// reconfiguration cut-over performs. The inverse of
3568    /// [`ClusterVariantManaged::realized_reconfiguration_target`].
3569    pub fn apply_reconfiguration_target(&mut self, target: ReconfigurationTarget) {
3570        // Destructured so a new target dimension fails to compile until it is
3571        // applied here too.
3572        let ReconfigurationTarget {
3573            size,
3574            replication_factor,
3575            availability_zones,
3576            logging,
3577            arrangement_compression,
3578        } = target;
3579        self.size = size;
3580        self.replication_factor = replication_factor;
3581        self.availability_zones = availability_zones;
3582        self.logging = logging;
3583        self.arrangement_compression = arrangement_compression;
3584    }
3585
3586    /// Whether the in-flight `burst` record is no longer warranted by this
3587    /// config: the `ON HYDRATION` policy was removed or re-sized away from the
3588    /// record's size, or the cluster was turned off (`replication_factor` 0).
3589    /// `false` when there is no record.
3590    pub fn has_unwarranted_burst_record(&self) -> bool {
3591        let Some(record) = &self.burst else {
3592            return false;
3593        };
3594        let hydration_size = self
3595            .auto_scaling_strategy
3596            .as_ref()
3597            .and_then(|strategy| strategy.on_hydration.as_ref())
3598            .map(|policy| policy.hydration_size.as_str());
3599        !mz_adapter_types::cluster_state::burst_record_warranted(
3600            &record.burst_size,
3601            self.replication_factor,
3602            hydration_size,
3603        )
3604    }
3605}
3606
3607impl From<ClusterVariantManaged> for durable::ClusterVariantManaged {
3608    fn from(managed: ClusterVariantManaged) -> Self {
3609        // Destructure the source (no `..`): a field added to either side is a
3610        // compile error here until it's carried across the boundary.
3611        let ClusterVariantManaged {
3612            size,
3613            availability_zones,
3614            logging,
3615            arrangement_compression,
3616            replication_factor,
3617            optimizer_feature_overrides,
3618            schedule,
3619            auto_scaling_strategy,
3620            reconfiguration,
3621            burst,
3622        } = managed;
3623        Self {
3624            size,
3625            availability_zones,
3626            logging,
3627            arrangement_compression,
3628            replication_factor,
3629            optimizer_feature_overrides: optimizer_feature_overrides.into(),
3630            schedule,
3631            auto_scaling_strategy,
3632            reconfiguration: reconfiguration.map(Into::into),
3633            burst: burst.map(Into::into),
3634        }
3635    }
3636}
3637
3638impl From<durable::ClusterVariantManaged> for ClusterVariantManaged {
3639    fn from(managed: durable::ClusterVariantManaged) -> Self {
3640        // Destructure the source (no `..`): a field added to either side is a
3641        // compile error here until it's carried across the boundary.
3642        let durable::ClusterVariantManaged {
3643            size,
3644            availability_zones,
3645            logging,
3646            arrangement_compression,
3647            replication_factor,
3648            optimizer_feature_overrides,
3649            schedule,
3650            auto_scaling_strategy,
3651            reconfiguration,
3652            burst,
3653        } = managed;
3654        Self {
3655            size,
3656            availability_zones,
3657            logging,
3658            arrangement_compression,
3659            replication_factor,
3660            optimizer_feature_overrides: optimizer_feature_overrides.into(),
3661            schedule,
3662            auto_scaling_strategy,
3663            reconfiguration: reconfiguration.map(Into::into),
3664            burst: burst.map(Into::into),
3665        }
3666    }
3667}
3668
3669/// In-memory mirror of [`durable::ReconfigurationState`].
3670///
3671/// This runtime state lives only in the durable layer, so the memory layer
3672/// carries its own `Serialize`/`Deserialize` mirror (to back the catalog
3673/// `dump()`) and converts across the boundary, rather than embedding the
3674/// durable-only type. The semantic contract lives on the durable type.
3675#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3676pub struct ReconfigurationState {
3677    pub target: ReconfigurationTarget,
3678    pub deadline: Timestamp,
3679    pub on_timeout: OnTimeoutAction,
3680    pub status: ReconfigurationStatus,
3681}
3682
3683/// In-memory mirror of [`durable::ReconfigurationStatus`].
3684///
3685/// The lifecycle of a cluster's `reconfiguration` record. Writers only ever
3686/// move the record along these transitions:
3687///
3688/// | from          | to                                                    | audited as          |
3689/// |---------------|-------------------------------------------------------|---------------------|
3690/// | no record     | `InProgress`                                          | `started`           |
3691/// | `InProgress`  | `InProgress` (re-target)                              | `started`           |
3692/// | `InProgress`  | `Finalized`                                           | `finalized`         |
3693/// | `InProgress`  | `TimedOut`                                            | `timed-out`         |
3694/// | `InProgress`  | `Cancelled`                                           | `cancelled`         |
3695/// | `InProgress`  | `ResourceExhausted`                                   | `resource-exhausted`|
3696/// | any terminal  | no record (drop), or `InProgress` (fresh record)      | none / `started`    |
3697///
3698/// The terminal statuses never transition into one another and a record is
3699/// never revived in place: a new reconfiguration overwrites the settled record
3700/// with a fresh `InProgress` one.
3701#[derive(
3702    Clone,
3703    Copy,
3704    Debug,
3705    Deserialize,
3706    Serialize,
3707    PartialOrd,
3708    PartialEq,
3709    Eq,
3710    Ord
3711)]
3712pub enum ReconfigurationStatus {
3713    InProgress,
3714    Finalized,
3715    TimedOut,
3716    Cancelled,
3717    ResourceExhausted,
3718}
3719
3720impl From<ReconfigurationStatus> for durable::ReconfigurationStatus {
3721    fn from(status: ReconfigurationStatus) -> Self {
3722        match status {
3723            ReconfigurationStatus::InProgress => durable::ReconfigurationStatus::InProgress,
3724            ReconfigurationStatus::Finalized => durable::ReconfigurationStatus::Finalized,
3725            ReconfigurationStatus::TimedOut => durable::ReconfigurationStatus::TimedOut,
3726            ReconfigurationStatus::Cancelled => durable::ReconfigurationStatus::Cancelled,
3727            ReconfigurationStatus::ResourceExhausted => {
3728                durable::ReconfigurationStatus::ResourceExhausted
3729            }
3730        }
3731    }
3732}
3733
3734impl From<durable::ReconfigurationStatus> for ReconfigurationStatus {
3735    fn from(status: durable::ReconfigurationStatus) -> Self {
3736        match status {
3737            durable::ReconfigurationStatus::InProgress => ReconfigurationStatus::InProgress,
3738            durable::ReconfigurationStatus::Finalized => ReconfigurationStatus::Finalized,
3739            durable::ReconfigurationStatus::TimedOut => ReconfigurationStatus::TimedOut,
3740            durable::ReconfigurationStatus::Cancelled => ReconfigurationStatus::Cancelled,
3741            durable::ReconfigurationStatus::ResourceExhausted => {
3742                ReconfigurationStatus::ResourceExhausted
3743            }
3744        }
3745    }
3746}
3747
3748impl ReconfigurationState {
3749    pub fn is_in_progress(&self) -> bool {
3750        matches!(self.status, ReconfigurationStatus::InProgress)
3751    }
3752}
3753
3754impl From<ReconfigurationState> for durable::ReconfigurationState {
3755    fn from(state: ReconfigurationState) -> Self {
3756        // Destructure the source (no `..`): a field added to either side is a
3757        // compile error here until it's carried across the boundary.
3758        let ReconfigurationState {
3759            target,
3760            deadline,
3761            on_timeout,
3762            status,
3763        } = state;
3764        Self {
3765            target: target.into(),
3766            deadline,
3767            on_timeout,
3768            status: status.into(),
3769        }
3770    }
3771}
3772
3773impl From<durable::ReconfigurationState> for ReconfigurationState {
3774    fn from(state: durable::ReconfigurationState) -> Self {
3775        // Destructure the source (no `..`): a field added to either side is a
3776        // compile error here until it's carried across the boundary.
3777        let durable::ReconfigurationState {
3778            target,
3779            deadline,
3780            on_timeout,
3781            status,
3782        } = state;
3783        Self {
3784            target: target.into(),
3785            deadline,
3786            on_timeout,
3787            status: status.into(),
3788        }
3789    }
3790}
3791
3792/// In-memory mirror of [`durable::ReconfigurationTarget`].
3793#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3794pub struct ReconfigurationTarget {
3795    pub size: String,
3796    pub replication_factor: u32,
3797    pub availability_zones: Vec<String>,
3798    pub logging: ReplicaLogging,
3799    pub arrangement_compression: bool,
3800}
3801
3802impl ReconfigurationTarget {
3803    /// Whether this target matches the realized config shape of `managed`.
3804    pub fn matches_realized_config(&self, managed: &ClusterVariantManaged) -> bool {
3805        self == &managed.realized_reconfiguration_target()
3806    }
3807}
3808
3809impl From<ReconfigurationTarget> for durable::ReconfigurationTarget {
3810    fn from(target: ReconfigurationTarget) -> Self {
3811        // Destructure the source (no `..`): a field added to either side is a
3812        // compile error here until it's carried across the boundary.
3813        let ReconfigurationTarget {
3814            size,
3815            replication_factor,
3816            availability_zones,
3817            logging,
3818            arrangement_compression,
3819        } = target;
3820        Self {
3821            size,
3822            replication_factor,
3823            availability_zones,
3824            logging,
3825            arrangement_compression,
3826        }
3827    }
3828}
3829
3830impl From<durable::ReconfigurationTarget> for ReconfigurationTarget {
3831    fn from(target: durable::ReconfigurationTarget) -> Self {
3832        // Destructure the source (no `..`): a field added to either side is a
3833        // compile error here until it's carried across the boundary.
3834        let durable::ReconfigurationTarget {
3835            size,
3836            replication_factor,
3837            availability_zones,
3838            logging,
3839            arrangement_compression,
3840        } = target;
3841        Self {
3842            size,
3843            replication_factor,
3844            availability_zones,
3845            logging,
3846            arrangement_compression,
3847        }
3848    }
3849}
3850
3851/// In-memory mirror of [`durable::BurstState`].
3852#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3853pub struct BurstState {
3854    pub burst_size: String,
3855    pub linger_duration: Duration,
3856    pub steady_hydrated_at: Option<Timestamp>,
3857}
3858
3859impl From<BurstState> for durable::BurstState {
3860    fn from(burst: BurstState) -> Self {
3861        // Destructure the source (no `..`): a field added to either side is a
3862        // compile error here until it's carried across the boundary.
3863        let BurstState {
3864            burst_size,
3865            linger_duration,
3866            steady_hydrated_at,
3867        } = burst;
3868        Self {
3869            burst_size,
3870            linger_duration,
3871            steady_hydrated_at,
3872        }
3873    }
3874}
3875
3876impl From<durable::BurstState> for BurstState {
3877    fn from(burst: durable::BurstState) -> Self {
3878        // Destructure the source (no `..`): a field added to either side is a
3879        // compile error here until it's carried across the boundary.
3880        let durable::BurstState {
3881            burst_size,
3882            linger_duration,
3883            steady_hydrated_at,
3884        } = burst;
3885        Self {
3886            burst_size,
3887            linger_duration,
3888            steady_hydrated_at,
3889        }
3890    }
3891}
3892
3893#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3894pub enum ClusterVariant {
3895    Managed(ClusterVariantManaged),
3896    Unmanaged,
3897}
3898
3899impl From<ClusterVariant> for durable::ClusterVariant {
3900    fn from(variant: ClusterVariant) -> Self {
3901        match variant {
3902            ClusterVariant::Managed(managed) => Self::Managed(managed.into()),
3903            ClusterVariant::Unmanaged => Self::Unmanaged,
3904        }
3905    }
3906}
3907
3908impl From<durable::ClusterVariant> for ClusterVariant {
3909    fn from(variant: durable::ClusterVariant) -> Self {
3910        match variant {
3911            durable::ClusterVariant::Managed(managed) => Self::Managed(managed.into()),
3912            durable::ClusterVariant::Unmanaged => Self::Unmanaged,
3913        }
3914    }
3915}
3916
3917impl mz_sql::catalog::CatalogDatabase for Database {
3918    fn name(&self) -> &str {
3919        &self.name
3920    }
3921
3922    fn id(&self) -> DatabaseId {
3923        self.id
3924    }
3925
3926    fn has_schemas(&self) -> bool {
3927        !self.schemas_by_name.is_empty()
3928    }
3929
3930    fn schema_ids(&self) -> &BTreeMap<String, SchemaId> {
3931        &self.schemas_by_name
3932    }
3933
3934    // `as` is ok to use to cast to a trait object.
3935    #[allow(clippy::as_conversions)]
3936    fn schemas(&self) -> Vec<&dyn CatalogSchema> {
3937        self.schemas_by_id
3938            .values()
3939            .map(|schema| schema as &dyn CatalogSchema)
3940            .collect()
3941    }
3942
3943    fn owner_id(&self) -> RoleId {
3944        self.owner_id
3945    }
3946
3947    fn privileges(&self) -> &PrivilegeMap {
3948        &self.privileges
3949    }
3950}
3951
3952impl mz_sql::catalog::CatalogSchema for Schema {
3953    fn database(&self) -> &ResolvedDatabaseSpecifier {
3954        &self.name.database
3955    }
3956
3957    fn name(&self) -> &QualifiedSchemaName {
3958        &self.name
3959    }
3960
3961    fn id(&self) -> &SchemaSpecifier {
3962        &self.id
3963    }
3964
3965    fn has_items(&self) -> bool {
3966        // A schema holds items, types, and functions in separate maps (see
3967        // `item_ids`). All three keep the schema non-empty, so e.g. DROP SCHEMA
3968        // without CASCADE must be rejected when only a type or function remains.
3969        !self.items.is_empty() || !self.types.is_empty() || !self.functions.is_empty()
3970    }
3971
3972    fn item_ids(&self) -> Box<dyn Iterator<Item = CatalogItemId> + '_> {
3973        Box::new(
3974            self.items
3975                .values()
3976                .chain(self.functions.values())
3977                .chain(self.types.values())
3978                .copied(),
3979        )
3980    }
3981
3982    fn owner_id(&self) -> RoleId {
3983        self.owner_id
3984    }
3985
3986    fn privileges(&self) -> &PrivilegeMap {
3987        &self.privileges
3988    }
3989}
3990
3991impl mz_sql::catalog::CatalogRole for Role {
3992    fn name(&self) -> &str {
3993        &self.name
3994    }
3995
3996    fn id(&self) -> RoleId {
3997        self.id
3998    }
3999
4000    fn membership(&self) -> &BTreeMap<RoleId, RoleId> {
4001        &self.membership.map
4002    }
4003
4004    fn attributes(&self) -> &RoleAttributes {
4005        &self.attributes
4006    }
4007
4008    fn vars(&self) -> &BTreeMap<String, OwnedVarInput> {
4009        &self.vars.map
4010    }
4011}
4012
4013impl mz_sql::catalog::CatalogNetworkPolicy for NetworkPolicy {
4014    fn name(&self) -> &str {
4015        &self.name
4016    }
4017
4018    fn id(&self) -> NetworkPolicyId {
4019        self.id
4020    }
4021
4022    fn owner_id(&self) -> RoleId {
4023        self.owner_id
4024    }
4025
4026    fn privileges(&self) -> &PrivilegeMap {
4027        &self.privileges
4028    }
4029}
4030
4031impl mz_sql::catalog::CatalogCluster<'_> for Cluster {
4032    fn name(&self) -> &str {
4033        &self.name
4034    }
4035
4036    fn id(&self) -> ClusterId {
4037        self.id
4038    }
4039
4040    fn bound_objects(&self) -> &BTreeSet<CatalogItemId> {
4041        &self.bound_objects
4042    }
4043
4044    fn replica_ids(&self) -> &BTreeMap<String, ReplicaId> {
4045        &self.replica_id_by_name_
4046    }
4047
4048    // `as` is ok to use to cast to a trait object.
4049    #[allow(clippy::as_conversions)]
4050    fn replicas(&self) -> Vec<&dyn CatalogClusterReplica<'_>> {
4051        self.replicas()
4052            .map(|replica| replica as &dyn CatalogClusterReplica)
4053            .collect()
4054    }
4055
4056    fn replica(&self, id: ReplicaId) -> &dyn CatalogClusterReplica<'_> {
4057        self.replica(id).expect("catalog out of sync")
4058    }
4059
4060    fn owner_id(&self) -> RoleId {
4061        self.owner_id
4062    }
4063
4064    fn privileges(&self) -> &PrivilegeMap {
4065        &self.privileges
4066    }
4067
4068    fn is_managed(&self) -> bool {
4069        self.is_managed()
4070    }
4071
4072    fn managed_size(&self) -> Option<&str> {
4073        match &self.config.variant {
4074            ClusterVariant::Managed(ClusterVariantManaged { size, .. }) => Some(size),
4075            ClusterVariant::Unmanaged => None,
4076        }
4077    }
4078
4079    fn schedule(&self) -> Option<&ClusterSchedule> {
4080        match &self.config.variant {
4081            ClusterVariant::Managed(ClusterVariantManaged { schedule, .. }) => Some(schedule),
4082            ClusterVariant::Unmanaged => None,
4083        }
4084    }
4085
4086    fn replication_factor(&self) -> Option<u32> {
4087        match &self.config.variant {
4088            ClusterVariant::Managed(ClusterVariantManaged {
4089                replication_factor, ..
4090            }) => Some(*replication_factor),
4091            ClusterVariant::Unmanaged => None,
4092        }
4093    }
4094
4095    fn auto_scaling_strategy(&self) -> Option<&AutoScalingStrategy> {
4096        match &self.config.variant {
4097            ClusterVariant::Managed(ClusterVariantManaged {
4098                auto_scaling_strategy,
4099                ..
4100            }) => auto_scaling_strategy.as_ref(),
4101            ClusterVariant::Unmanaged => None,
4102        }
4103    }
4104    fn try_to_plan(&self) -> Result<CreateClusterPlan, PlanError> {
4105        self.try_to_plan()
4106    }
4107}
4108
4109impl mz_sql::catalog::CatalogClusterReplica<'_> for ClusterReplica {
4110    fn name(&self) -> &str {
4111        &self.name
4112    }
4113
4114    fn cluster_id(&self) -> ClusterId {
4115        self.cluster_id
4116    }
4117
4118    fn replica_id(&self) -> ReplicaId {
4119        self.replica_id
4120    }
4121
4122    fn owner_id(&self) -> RoleId {
4123        self.owner_id
4124    }
4125
4126    fn internal(&self) -> bool {
4127        self.config.location.internal()
4128    }
4129}
4130
4131impl mz_sql::catalog::CatalogItem for CatalogEntry {
4132    fn name(&self) -> &QualifiedItemName {
4133        self.name()
4134    }
4135
4136    fn id(&self) -> CatalogItemId {
4137        self.id()
4138    }
4139
4140    fn global_ids(&self) -> Box<dyn Iterator<Item = GlobalId> + '_> {
4141        Box::new(self.global_ids())
4142    }
4143
4144    fn oid(&self) -> u32 {
4145        self.oid()
4146    }
4147
4148    fn func(&self) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
4149        self.func()
4150    }
4151
4152    fn source_desc(&self) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
4153        self.source_desc()
4154    }
4155
4156    fn connection(
4157        &self,
4158    ) -> Result<mz_storage_types::connections::Connection<ReferencedConnection>, SqlCatalogError>
4159    {
4160        Ok(self.connection()?.details.to_connection())
4161    }
4162
4163    fn create_sql(&self) -> &str {
4164        match self.item() {
4165            CatalogItem::Table(Table { create_sql, .. }) => {
4166                create_sql.as_deref().unwrap_or("<builtin>")
4167            }
4168            CatalogItem::Source(Source { create_sql, .. }) => {
4169                create_sql.as_deref().unwrap_or("<builtin>")
4170            }
4171            CatalogItem::Sink(Sink { create_sql, .. }) => create_sql,
4172            CatalogItem::View(View { create_sql, .. }) => create_sql,
4173            CatalogItem::MaterializedView(MaterializedView { create_sql, .. }) => create_sql,
4174            CatalogItem::Index(Index { create_sql, .. }) => create_sql,
4175            CatalogItem::Type(Type { create_sql, .. }) => {
4176                create_sql.as_deref().unwrap_or("<builtin>")
4177            }
4178            CatalogItem::Secret(Secret { create_sql, .. }) => create_sql,
4179            CatalogItem::Connection(Connection { create_sql, .. }) => create_sql,
4180            CatalogItem::MetricSink(MetricSink { create_sql, .. }) => create_sql,
4181            CatalogItem::Func(_) => "<builtin>",
4182            CatalogItem::Log(_) => "<builtin>",
4183        }
4184    }
4185
4186    fn item_type(&self) -> SqlCatalogItemType {
4187        self.item().typ()
4188    }
4189
4190    fn index_details(&self) -> Option<(&[MirScalarExpr], GlobalId)> {
4191        if let CatalogItem::Index(Index { keys, on, .. }) = self.item() {
4192            Some((keys, *on))
4193        } else {
4194            None
4195        }
4196    }
4197
4198    fn writable_table_details(&self) -> Option<&[Expr<Aug>]> {
4199        if let CatalogItem::Table(Table {
4200            data_source: TableDataSource::TableWrites { defaults },
4201            ..
4202        }) = self.item()
4203        {
4204            Some(defaults.as_slice())
4205        } else {
4206            None
4207        }
4208    }
4209
4210    fn replacement_target(&self) -> Option<CatalogItemId> {
4211        if let CatalogItem::MaterializedView(mv) = self.item() {
4212            mv.replacement_target
4213        } else {
4214            None
4215        }
4216    }
4217
4218    fn type_details(&self) -> Option<&CatalogTypeDetails<IdReference>> {
4219        if let CatalogItem::Type(Type { details, .. }) = self.item() {
4220            Some(details)
4221        } else {
4222            None
4223        }
4224    }
4225
4226    fn references(&self) -> &ResolvedIds {
4227        self.references()
4228    }
4229
4230    fn uses(&self) -> BTreeSet<CatalogItemId> {
4231        self.uses()
4232    }
4233
4234    fn referenced_by(&self) -> &[CatalogItemId] {
4235        self.referenced_by()
4236    }
4237
4238    fn used_by(&self) -> &[CatalogItemId] {
4239        self.used_by()
4240    }
4241
4242    fn subsource_details(
4243        &self,
4244    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)> {
4245        self.subsource_details()
4246    }
4247
4248    fn source_export_details(
4249        &self,
4250    ) -> Option<(
4251        CatalogItemId,
4252        &UnresolvedItemName,
4253        &SourceExportDetails,
4254        &SourceExportDataConfig<ReferencedConnection>,
4255    )> {
4256        self.source_export_details()
4257    }
4258
4259    fn is_progress_source(&self) -> bool {
4260        self.is_progress_source()
4261    }
4262
4263    fn progress_id(&self) -> Option<CatalogItemId> {
4264        self.progress_id()
4265    }
4266
4267    fn owner_id(&self) -> RoleId {
4268        self.owner_id
4269    }
4270
4271    fn privileges(&self) -> &PrivilegeMap {
4272        &self.privileges
4273    }
4274
4275    fn cluster_id(&self) -> Option<ClusterId> {
4276        self.item().cluster_id()
4277    }
4278
4279    fn at_version(
4280        &self,
4281        version: RelationVersionSelector,
4282    ) -> Box<dyn mz_sql::catalog::CatalogCollectionItem> {
4283        Box::new(CatalogCollectionEntry {
4284            entry: self.clone(),
4285            version,
4286        })
4287    }
4288
4289    fn latest_version(&self) -> Option<RelationVersion> {
4290        self.table().map(|t| t.desc.latest_version())
4291    }
4292}
4293
4294/// A single update to the catalog state.
4295#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4296pub struct StateUpdate {
4297    pub kind: StateUpdateKind,
4298    pub ts: Timestamp,
4299    pub diff: StateDiff,
4300}
4301
4302/// The contents of a single state update.
4303///
4304/// Variants are listed in dependency order.
4305#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4306pub enum StateUpdateKind {
4307    Role(durable::objects::Role),
4308    RoleAuth(durable::objects::RoleAuth),
4309    Database(durable::objects::Database),
4310    Schema(durable::objects::Schema),
4311    DefaultPrivilege(durable::objects::DefaultPrivilege),
4312    SystemPrivilege(MzAclItem),
4313    SystemConfiguration(durable::objects::SystemConfiguration),
4314    Cluster(durable::objects::Cluster),
4315    ClusterSystemConfiguration(durable::objects::ClusterSystemConfiguration),
4316    NetworkPolicy(durable::objects::NetworkPolicy),
4317    IntrospectionSourceIndex(durable::objects::IntrospectionSourceIndex),
4318    ClusterReplica(durable::objects::ClusterReplica),
4319    ReplicaSystemConfiguration(durable::objects::ReplicaSystemConfiguration),
4320    SourceReferences(durable::objects::SourceReferences),
4321    SystemObjectMapping(durable::objects::SystemObjectMapping),
4322    Item(durable::objects::Item),
4323    Comment(durable::objects::Comment),
4324    AuditLog(durable::objects::AuditLog),
4325    // Storage updates.
4326    StorageCollectionMetadata(durable::objects::StorageCollectionMetadata),
4327    UnfinalizedShard(durable::objects::UnfinalizedShard),
4328}
4329
4330/// Valid diffs for catalog state updates.
4331#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
4332pub enum StateDiff {
4333    Retraction,
4334    Addition,
4335}
4336
4337impl From<StateDiff> for Diff {
4338    fn from(diff: StateDiff) -> Self {
4339        match diff {
4340            StateDiff::Retraction => Diff::MINUS_ONE,
4341            StateDiff::Addition => Diff::ONE,
4342        }
4343    }
4344}
4345impl TryFrom<Diff> for StateDiff {
4346    type Error = String;
4347
4348    fn try_from(diff: Diff) -> Result<Self, Self::Error> {
4349        match diff {
4350            Diff::MINUS_ONE => Ok(Self::Retraction),
4351            Diff::ONE => Ok(Self::Addition),
4352            diff => Err(format!("invalid diff {diff}")),
4353        }
4354    }
4355}