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 mutable references to the plan fields (`optimized_plan`,
1856    /// `physical_plan`, `dataflow_metainfo`) on plan-bearing items
1857    /// (`Index`, `MaterializedView`), or `None` for
1858    /// other item kinds.
1859    pub fn plan_fields_mut(
1860        &mut self,
1861    ) -> Option<(
1862        &mut Option<Arc<DataflowDescription<OptimizedMirRelationExpr>>>,
1863        &mut Option<Arc<DataflowDescription<ComputePlan>>>,
1864        &mut Option<DataflowMetainfo<Arc<OptimizerNotice>>>,
1865    )> {
1866        match self {
1867            CatalogItem::Index(idx) => Some((
1868                &mut idx.optimized_plan,
1869                &mut idx.physical_plan,
1870                &mut idx.dataflow_metainfo,
1871            )),
1872            CatalogItem::MaterializedView(mv) => Some((
1873                &mut mv.optimized_plan,
1874                &mut mv.physical_plan,
1875                &mut mv.dataflow_metainfo,
1876            )),
1877            CatalogItem::MetricSink(ms) => Some((
1878                &mut ms.optimized_plan,
1879                &mut ms.physical_plan,
1880                &mut ms.dataflow_metainfo,
1881            )),
1882            _ => None,
1883        }
1884    }
1885
1886    /// Whether this item represents a storage collection.
1887    pub fn is_storage_collection(&self) -> bool {
1888        match self {
1889            CatalogItem::Table(_)
1890            | CatalogItem::Source(_)
1891            | CatalogItem::MaterializedView(_)
1892            | CatalogItem::Sink(_) => true,
1893            CatalogItem::Log(_)
1894            | CatalogItem::View(_)
1895            | CatalogItem::Index(_)
1896            | CatalogItem::Type(_)
1897            | CatalogItem::Func(_)
1898            | CatalogItem::Secret(_)
1899            | CatalogItem::Connection(_)
1900            | CatalogItem::MetricSink(_) => false,
1901        }
1902    }
1903
1904    /// Returns the [`RelationDesc`] for items that yield rows, at the requested
1905    /// version.
1906    ///
1907    /// Some item types honor `version` so callers can ask for the schema that
1908    /// matches a specific [`GlobalId`] or historical definition. Other relation
1909    /// types ignore `version` because they have a single shape. Non-relational
1910    /// items ( for example functions, indexes, sinks, secrets, and connections)
1911    /// return `None`.
1912    pub fn relation_desc(&self, version: RelationVersionSelector) -> Option<Cow<'_, RelationDesc>> {
1913        match &self {
1914            CatalogItem::Source(src) => Some(Cow::Borrowed(&src.desc)),
1915            CatalogItem::Log(log) => Some(Cow::Owned(log.variant.desc())),
1916            CatalogItem::Table(tbl) => Some(Cow::Owned(tbl.desc.at_version(version))),
1917            CatalogItem::View(view) => Some(Cow::Borrowed(&view.desc)),
1918            CatalogItem::MaterializedView(mview) => {
1919                Some(Cow::Owned(mview.desc.at_version(version)))
1920            }
1921            CatalogItem::Func(_)
1922            | CatalogItem::Index(_)
1923            | CatalogItem::Sink(_)
1924            | CatalogItem::Secret(_)
1925            | CatalogItem::Connection(_)
1926            | CatalogItem::Type(_)
1927            | CatalogItem::MetricSink(_) => None,
1928        }
1929    }
1930
1931    pub fn func(
1932        &self,
1933        entry: &CatalogEntry,
1934    ) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
1935        match &self {
1936            CatalogItem::Func(func) => Ok(func.inner),
1937            _ => Err(SqlCatalogError::UnexpectedType {
1938                name: entry.name().item.to_string(),
1939                actual_type: entry.item_type(),
1940                expected_type: CatalogItemType::Func,
1941            }),
1942        }
1943    }
1944
1945    pub fn source_desc(
1946        &self,
1947        entry: &CatalogEntry,
1948    ) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
1949        match &self {
1950            CatalogItem::Source(source) => match &source.data_source {
1951                DataSourceDesc::Ingestion { desc, .. }
1952                | DataSourceDesc::OldSyntaxIngestion { desc, .. } => Ok(Some(desc)),
1953                DataSourceDesc::IngestionExport { .. }
1954                | DataSourceDesc::Introspection(_)
1955                | DataSourceDesc::Webhook { .. }
1956                | DataSourceDesc::Progress
1957                | DataSourceDesc::Catalog => Ok(None),
1958            },
1959            _ => Err(SqlCatalogError::UnexpectedType {
1960                name: entry.name().item.to_string(),
1961                actual_type: entry.item_type(),
1962                expected_type: CatalogItemType::Source,
1963            }),
1964        }
1965    }
1966
1967    /// Reports whether this catalog entry is a progress source.
1968    pub fn is_progress_source(&self) -> bool {
1969        matches!(
1970            self,
1971            CatalogItem::Source(Source {
1972                data_source: DataSourceDesc::Progress,
1973                ..
1974            })
1975        )
1976    }
1977
1978    /// Collects the identifiers of the objects that were encountered when resolving names in the
1979    /// item's DDL statement.
1980    pub fn references(&self) -> &ResolvedIds {
1981        static EMPTY: LazyLock<ResolvedIds> = LazyLock::new(ResolvedIds::empty);
1982        match self {
1983            CatalogItem::Func(_) => &*EMPTY,
1984            CatalogItem::Index(idx) => &idx.resolved_ids,
1985            CatalogItem::Sink(sink) => &sink.resolved_ids,
1986            CatalogItem::Source(source) => &source.resolved_ids,
1987            CatalogItem::Log(_) => &*EMPTY,
1988            CatalogItem::Table(table) => &table.resolved_ids,
1989            CatalogItem::Type(typ) => &typ.resolved_ids,
1990            CatalogItem::View(view) => &view.resolved_ids,
1991            CatalogItem::MaterializedView(mview) => &mview.resolved_ids,
1992            CatalogItem::Secret(_) => &*EMPTY,
1993            CatalogItem::Connection(connection) => &connection.resolved_ids,
1994            CatalogItem::MetricSink(metric_sink) => &metric_sink.resolved_ids,
1995        }
1996    }
1997
1998    /// Collects the identifiers of the objects used by this [`CatalogItem`].
1999    ///
2000    /// Like [`CatalogItem::references()`] but also includes objects that are not directly
2001    /// referenced. For example this will include any catalog objects used to implement functions
2002    /// and casts in the item.
2003    pub fn uses(&self) -> BTreeSet<CatalogItemId> {
2004        let mut uses: BTreeSet<_> = self.references().items().copied().collect();
2005        match self {
2006            // TODO(jkosh44) This isn't really correct for functions. They may use other objects in
2007            // their implementation. However, currently there's no way to get that information.
2008            CatalogItem::Func(_) => {}
2009            CatalogItem::Index(_) => {}
2010            CatalogItem::Sink(_) => {}
2011            CatalogItem::Source(_) => {}
2012            CatalogItem::Log(_) => {}
2013            CatalogItem::Table(_) => {}
2014            CatalogItem::Type(_) => {}
2015            CatalogItem::View(view) => uses.extend(view.dependencies.0.iter().copied()),
2016            CatalogItem::MaterializedView(mview) => {
2017                uses.extend(mview.dependencies.0.iter().copied())
2018            }
2019            CatalogItem::Secret(_) => {}
2020            CatalogItem::Connection(_) => {}
2021            CatalogItem::MetricSink(_) => {}
2022        }
2023        uses
2024    }
2025
2026    /// Returns the connection ID that this item belongs to, if this item is
2027    /// temporary.
2028    pub fn conn_id(&self) -> Option<&ConnectionId> {
2029        match self {
2030            CatalogItem::View(view) => view.conn_id.as_ref(),
2031            CatalogItem::Index(index) => index.conn_id.as_ref(),
2032            CatalogItem::Table(table) => table.conn_id.as_ref(),
2033            CatalogItem::Log(_)
2034            | CatalogItem::Source(_)
2035            | CatalogItem::Sink(_)
2036            | CatalogItem::MaterializedView(_)
2037            | CatalogItem::Secret(_)
2038            | CatalogItem::Type(_)
2039            | CatalogItem::Func(_)
2040            | CatalogItem::Connection(_)
2041            | CatalogItem::MetricSink(_) => None,
2042        }
2043    }
2044
2045    /// Sets the connection ID that this item belongs to, which makes it a
2046    /// temporary item.
2047    pub fn set_conn_id(&mut self, conn_id: Option<ConnectionId>) {
2048        match self {
2049            CatalogItem::View(view) => view.conn_id = conn_id,
2050            CatalogItem::Index(index) => index.conn_id = conn_id,
2051            CatalogItem::Table(table) => table.conn_id = conn_id,
2052            CatalogItem::Log(_)
2053            | CatalogItem::Source(_)
2054            | CatalogItem::Sink(_)
2055            | CatalogItem::MaterializedView(_)
2056            | CatalogItem::Secret(_)
2057            | CatalogItem::Type(_)
2058            | CatalogItem::Func(_)
2059            | CatalogItem::Connection(_)
2060            | CatalogItem::MetricSink(_) => (),
2061        }
2062    }
2063
2064    /// Overwrites the `create_sql` of this item, without replanning.
2065    ///
2066    /// Only used when applying temporary item updates. A temporary item's
2067    /// in-memory `create_sql` must stay byte-identical to the update that
2068    /// created it, so that re-serializing the item, for example when a later
2069    /// op in the same transaction retracts it, yields a value that
2070    /// consolidates away against that update. Persistent items get this
2071    /// guarantee from the durable catalog, which stores the exact bytes.
2072    pub fn set_create_sql(&mut self, create_sql: String) {
2073        match self {
2074            CatalogItem::View(view) => view.create_sql = create_sql,
2075            CatalogItem::Index(index) => index.create_sql = create_sql,
2076            CatalogItem::Table(table) => table.create_sql = Some(create_sql),
2077            CatalogItem::Log(_)
2078            | CatalogItem::Source(_)
2079            | CatalogItem::Sink(_)
2080            | CatalogItem::MaterializedView(_)
2081            | CatalogItem::Secret(_)
2082            | CatalogItem::Type(_)
2083            | CatalogItem::Func(_)
2084            | CatalogItem::Connection(_)
2085            | CatalogItem::MetricSink(_) => {
2086                unreachable!("only views, indexes, and tables can be temporary")
2087            }
2088        }
2089    }
2090
2091    /// Indicates whether this item is temporary or not.
2092    pub fn is_temporary(&self) -> bool {
2093        self.conn_id().is_some()
2094    }
2095
2096    pub fn rename_schema_refs(
2097        &self,
2098        database_name: &str,
2099        cur_schema_name: &str,
2100        new_schema_name: &str,
2101    ) -> Result<CatalogItem, (String, String)> {
2102        let do_rewrite = |create_sql: String| -> Result<String, (String, String)> {
2103            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2104                .expect("invalid create sql persisted to catalog")
2105                .into_element()
2106                .ast;
2107
2108            // Rename all references to cur_schema_name.
2109            mz_sql::ast::transform::create_stmt_rename_schema_refs(
2110                &mut create_stmt,
2111                database_name,
2112                cur_schema_name,
2113                new_schema_name,
2114            )?;
2115
2116            Ok(create_stmt.to_ast_string_stable())
2117        };
2118
2119        match self {
2120            CatalogItem::Table(i) => {
2121                let mut i = i.clone();
2122                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2123                Ok(CatalogItem::Table(i))
2124            }
2125            CatalogItem::Log(i) => Ok(CatalogItem::Log(i.clone())),
2126            CatalogItem::Source(i) => {
2127                let mut i = i.clone();
2128                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2129                Ok(CatalogItem::Source(i))
2130            }
2131            CatalogItem::Sink(i) => {
2132                let mut i = i.clone();
2133                i.create_sql = do_rewrite(i.create_sql)?;
2134                Ok(CatalogItem::Sink(i))
2135            }
2136            CatalogItem::View(i) => {
2137                let mut i = i.clone();
2138                i.create_sql = do_rewrite(i.create_sql)?;
2139                Ok(CatalogItem::View(i))
2140            }
2141            CatalogItem::MaterializedView(i) => {
2142                let mut i = i.clone();
2143                i.create_sql = do_rewrite(i.create_sql)?;
2144                Ok(CatalogItem::MaterializedView(i))
2145            }
2146            CatalogItem::Index(i) => {
2147                let mut i = i.clone();
2148                i.create_sql = do_rewrite(i.create_sql)?;
2149                Ok(CatalogItem::Index(i))
2150            }
2151            CatalogItem::Secret(i) => {
2152                let mut i = i.clone();
2153                i.create_sql = do_rewrite(i.create_sql)?;
2154                Ok(CatalogItem::Secret(i))
2155            }
2156            CatalogItem::Connection(i) => {
2157                let mut i = i.clone();
2158                i.create_sql = do_rewrite(i.create_sql)?;
2159                Ok(CatalogItem::Connection(i))
2160            }
2161            CatalogItem::Type(i) => {
2162                let mut i = i.clone();
2163                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2164                Ok(CatalogItem::Type(i))
2165            }
2166            CatalogItem::Func(i) => Ok(CatalogItem::Func(i.clone())),
2167            CatalogItem::MetricSink(i) => {
2168                let mut i = i.clone();
2169                i.create_sql = do_rewrite(i.create_sql)?;
2170                Ok(CatalogItem::MetricSink(i))
2171            }
2172        }
2173    }
2174
2175    /// Returns a clone of `self` with all instances of `from` renamed to `to`
2176    /// (with the option of including the item's own name) or errors if request
2177    /// is ambiguous.
2178    pub fn rename_item_refs(
2179        &self,
2180        from: FullItemName,
2181        to_item_name: String,
2182        rename_self: bool,
2183    ) -> Result<CatalogItem, String> {
2184        let do_rewrite = |create_sql: String| -> Result<String, String> {
2185            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2186                .expect("invalid create sql persisted to catalog")
2187                .into_element()
2188                .ast;
2189            if rename_self {
2190                mz_sql::ast::transform::create_stmt_rename(&mut create_stmt, to_item_name.clone());
2191            }
2192            // Determination of what constitutes an ambiguous request is done here.
2193            mz_sql::ast::transform::create_stmt_rename_refs(&mut create_stmt, from, to_item_name)?;
2194            Ok(create_stmt.to_ast_string_stable())
2195        };
2196
2197        match self {
2198            CatalogItem::Table(i) => {
2199                let mut i = i.clone();
2200                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2201                Ok(CatalogItem::Table(i))
2202            }
2203            CatalogItem::Log(i) => Ok(CatalogItem::Log(i.clone())),
2204            CatalogItem::Source(i) => {
2205                let mut i = i.clone();
2206                i.create_sql = i.create_sql.map(do_rewrite).transpose()?;
2207                Ok(CatalogItem::Source(i))
2208            }
2209            CatalogItem::Sink(i) => {
2210                let mut i = i.clone();
2211                i.create_sql = do_rewrite(i.create_sql)?;
2212                Ok(CatalogItem::Sink(i))
2213            }
2214            CatalogItem::View(i) => {
2215                let mut i = i.clone();
2216                i.create_sql = do_rewrite(i.create_sql)?;
2217                Ok(CatalogItem::View(i))
2218            }
2219            CatalogItem::MaterializedView(i) => {
2220                let mut i = i.clone();
2221                i.create_sql = do_rewrite(i.create_sql)?;
2222                Ok(CatalogItem::MaterializedView(i))
2223            }
2224            CatalogItem::Index(i) => {
2225                let mut i = i.clone();
2226                i.create_sql = do_rewrite(i.create_sql)?;
2227                Ok(CatalogItem::Index(i))
2228            }
2229            CatalogItem::Secret(i) => {
2230                let mut i = i.clone();
2231                i.create_sql = do_rewrite(i.create_sql)?;
2232                Ok(CatalogItem::Secret(i))
2233            }
2234            CatalogItem::Func(_) | CatalogItem::Type(_) => {
2235                unreachable!("{}s cannot be renamed", self.typ())
2236            }
2237            CatalogItem::Connection(i) => {
2238                let mut i = i.clone();
2239                i.create_sql = do_rewrite(i.create_sql)?;
2240                Ok(CatalogItem::Connection(i))
2241            }
2242            CatalogItem::MetricSink(i) => {
2243                let mut i = i.clone();
2244                i.create_sql = do_rewrite(i.create_sql)?;
2245                Ok(CatalogItem::MetricSink(i))
2246            }
2247        }
2248    }
2249
2250    /// Returns a clone of `self` with all instances of `old_id` replaced with `new_id`.
2251    pub fn replace_item_refs(&self, old_id: CatalogItemId, new_id: CatalogItemId) -> CatalogItem {
2252        let do_rewrite = |create_sql: String| -> String {
2253            let mut create_stmt = mz_sql::parse::parse(&create_sql)
2254                .expect("invalid create sql persisted to catalog")
2255                .into_element()
2256                .ast;
2257            mz_sql::ast::transform::create_stmt_replace_ids(
2258                &mut create_stmt,
2259                &[(old_id, new_id)].into(),
2260            );
2261            create_stmt.to_ast_string_stable()
2262        };
2263
2264        match self {
2265            CatalogItem::Table(i) => {
2266                let mut i = i.clone();
2267                i.create_sql = i.create_sql.map(do_rewrite);
2268                CatalogItem::Table(i)
2269            }
2270            CatalogItem::Log(i) => CatalogItem::Log(i.clone()),
2271            CatalogItem::Source(i) => {
2272                let mut i = i.clone();
2273                i.create_sql = i.create_sql.map(do_rewrite);
2274                CatalogItem::Source(i)
2275            }
2276            CatalogItem::Sink(i) => {
2277                let mut i = i.clone();
2278                i.create_sql = do_rewrite(i.create_sql);
2279                CatalogItem::Sink(i)
2280            }
2281            CatalogItem::View(i) => {
2282                let mut i = i.clone();
2283                i.create_sql = do_rewrite(i.create_sql);
2284                CatalogItem::View(i)
2285            }
2286            CatalogItem::MaterializedView(i) => {
2287                let mut i = i.clone();
2288                i.create_sql = do_rewrite(i.create_sql);
2289                CatalogItem::MaterializedView(i)
2290            }
2291            CatalogItem::Index(i) => {
2292                let mut i = i.clone();
2293                i.create_sql = do_rewrite(i.create_sql);
2294                CatalogItem::Index(i)
2295            }
2296            CatalogItem::Secret(i) => {
2297                let mut i = i.clone();
2298                i.create_sql = do_rewrite(i.create_sql);
2299                CatalogItem::Secret(i)
2300            }
2301            CatalogItem::Func(_) | CatalogItem::Type(_) => {
2302                unreachable!("references of {}s cannot be replaced", self.typ())
2303            }
2304            CatalogItem::Connection(i) => {
2305                let mut i = i.clone();
2306                i.create_sql = do_rewrite(i.create_sql);
2307                CatalogItem::Connection(i)
2308            }
2309            CatalogItem::MetricSink(i) => {
2310                let mut i = i.clone();
2311                i.create_sql = do_rewrite(i.create_sql);
2312                CatalogItem::MetricSink(i)
2313            }
2314        }
2315    }
2316    /// Updates the retain history for an item. Returns the previous retain history value. Returns
2317    /// an error if this item does not support retain history.
2318    pub fn update_retain_history(
2319        &mut self,
2320        value: Option<Value>,
2321        window: CompactionWindow,
2322    ) -> Result<Option<WithOptionValue<Raw>>, ()> {
2323        let update = |mut ast: &mut Statement<Raw>| {
2324            // Each statement type has unique option types. This macro handles them commonly.
2325            macro_rules! update_retain_history {
2326                ( $stmt:ident, $opt:ident, $name:ident ) => {{
2327                    // Replace or add the option.
2328                    let pos = $stmt
2329                        .with_options
2330                        .iter()
2331                        // In case there are ever multiple, look for the last one.
2332                        .rposition(|o| o.name == mz_sql_parser::ast::$name::RetainHistory);
2333                    if let Some(value) = value {
2334                        let next = mz_sql_parser::ast::$opt {
2335                            name: mz_sql_parser::ast::$name::RetainHistory,
2336                            value: Some(WithOptionValue::RetainHistoryFor(value)),
2337                        };
2338                        if let Some(idx) = pos {
2339                            let previous = $stmt.with_options[idx].clone();
2340                            $stmt.with_options[idx] = next;
2341                            previous.value
2342                        } else {
2343                            $stmt.with_options.push(next);
2344                            None
2345                        }
2346                    } else {
2347                        if let Some(idx) = pos {
2348                            $stmt.with_options.swap_remove(idx).value
2349                        } else {
2350                            None
2351                        }
2352                    }
2353                }};
2354            }
2355            let previous = match &mut ast {
2356                Statement::CreateTable(stmt) => {
2357                    update_retain_history!(stmt, TableOption, TableOptionName)
2358                }
2359                Statement::CreateIndex(stmt) => {
2360                    update_retain_history!(stmt, IndexOption, IndexOptionName)
2361                }
2362                Statement::CreateSource(stmt) => {
2363                    update_retain_history!(stmt, CreateSourceOption, CreateSourceOptionName)
2364                }
2365                Statement::CreateMaterializedView(stmt) => {
2366                    update_retain_history!(stmt, MaterializedViewOption, MaterializedViewOptionName)
2367                }
2368                _ => {
2369                    return Err(());
2370                }
2371            };
2372            Ok(previous)
2373        };
2374
2375        let res = self.update_sql(update)?;
2376        let cw = self
2377            .custom_logical_compaction_window_mut()
2378            .expect("item must have compaction window");
2379        *cw = Some(window);
2380        Ok(res)
2381    }
2382
2383    /// Updates the timestamp interval for a source. Returns the previous timestamp interval
2384    /// value, if any. Returns an error if this item is not a source.
2385    pub fn update_timestamp_interval(
2386        &mut self,
2387        value: Option<Value>,
2388        interval: Duration,
2389    ) -> Result<Option<WithOptionValue<Raw>>, ()> {
2390        let update = |ast: &mut Statement<Raw>| match ast {
2391            Statement::CreateSource(stmt) => {
2392                let pos = stmt.with_options.iter().rposition(|o| {
2393                    o.name == mz_sql_parser::ast::CreateSourceOptionName::TimestampInterval
2394                });
2395                let previous = if let Some(value) = value {
2396                    let next = mz_sql_parser::ast::CreateSourceOption {
2397                        name: mz_sql_parser::ast::CreateSourceOptionName::TimestampInterval,
2398                        value: Some(WithOptionValue::Value(value)),
2399                    };
2400                    if let Some(idx) = pos {
2401                        let previous = stmt.with_options[idx].clone();
2402                        stmt.with_options[idx] = next;
2403                        previous.value
2404                    } else {
2405                        stmt.with_options.push(next);
2406                        None
2407                    }
2408                } else if let Some(idx) = pos {
2409                    stmt.with_options.swap_remove(idx).value
2410                } else {
2411                    None
2412                };
2413                Ok(previous)
2414            }
2415            _ => Err(()),
2416        };
2417
2418        let previous = self.update_sql(update)?;
2419
2420        // Update the in-memory SourceDesc timestamp_interval.
2421        match self {
2422            CatalogItem::Source(source) => {
2423                match &mut source.data_source {
2424                    DataSourceDesc::Ingestion { desc, .. }
2425                    | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
2426                        desc.timestamp_interval = interval;
2427                    }
2428                    _ => return Err(()),
2429                }
2430                Ok(previous)
2431            }
2432            _ => Err(()),
2433        }
2434    }
2435
2436    pub fn add_column(
2437        &mut self,
2438        name: ColumnName,
2439        typ: SqlColumnType,
2440        sql: RawDataType,
2441    ) -> Result<RelationVersion, PlanError> {
2442        let CatalogItem::Table(table) = self else {
2443            return Err(PlanError::Unsupported {
2444                feature: "adding columns to a non-Table".to_string(),
2445                discussion_no: None,
2446            });
2447        };
2448        let next_version = table.desc.add_column(name.clone(), typ);
2449
2450        let update = |mut ast: &mut Statement<Raw>| match &mut ast {
2451            Statement::CreateTable(stmt) => {
2452                let version = ColumnOptionDef {
2453                    name: None,
2454                    option: ColumnOption::Versioned {
2455                        action: ColumnVersioned::Added,
2456                        version: next_version.into(),
2457                    },
2458                };
2459                let column = ColumnDef {
2460                    name: name.into(),
2461                    data_type: sql,
2462                    collation: None,
2463                    options: vec![version],
2464                };
2465                stmt.columns.push(column);
2466                Ok(())
2467            }
2468            _ => Err(()),
2469        };
2470
2471        self.update_sql(update)
2472            .map_err(|()| PlanError::Unstructured("expected CREATE TABLE statement".to_string()))?;
2473        Ok(next_version)
2474    }
2475
2476    /// Updates the create_sql field of this item. Returns an error if this is a builtin item,
2477    /// otherwise returns f's result.
2478    pub fn update_sql<F, T>(&mut self, f: F) -> Result<T, ()>
2479    where
2480        F: FnOnce(&mut Statement<Raw>) -> Result<T, ()>,
2481    {
2482        let create_sql = match self {
2483            CatalogItem::Table(Table { create_sql, .. })
2484            | CatalogItem::Type(Type { create_sql, .. })
2485            | CatalogItem::Source(Source { create_sql, .. }) => create_sql.as_mut(),
2486            CatalogItem::Sink(Sink { create_sql, .. })
2487            | CatalogItem::View(View { create_sql, .. })
2488            | CatalogItem::MaterializedView(MaterializedView { create_sql, .. })
2489            | CatalogItem::Index(Index { create_sql, .. })
2490            | CatalogItem::Secret(Secret { create_sql, .. })
2491            | CatalogItem::Connection(Connection { create_sql, .. })
2492            | CatalogItem::MetricSink(MetricSink { create_sql, .. }) => Some(create_sql),
2493            CatalogItem::Func(_) | CatalogItem::Log(_) => None,
2494        };
2495        let Some(create_sql) = create_sql else {
2496            return Err(());
2497        };
2498        let mut ast = mz_sql_parser::parser::parse_statements(create_sql)
2499            .expect("non-system items must be parseable")
2500            .into_element()
2501            .ast;
2502        debug!("rewrite: {}", ast.to_ast_string_redacted());
2503        let t = f(&mut ast)?;
2504        *create_sql = ast.to_ast_string_stable();
2505        debug!("rewrote: {}", ast.to_ast_string_redacted());
2506        Ok(t)
2507    }
2508
2509    /// If the object is considered a "compute object"
2510    /// (i.e., it is managed by the compute controller),
2511    /// this function returns its cluster ID. Otherwise, it returns nothing.
2512    ///
2513    /// This function differs from `cluster_id` because while all
2514    /// compute objects run on a cluster, the converse is not true.
2515    pub fn is_compute_object_on_cluster(&self) -> Option<ClusterId> {
2516        match self {
2517            CatalogItem::Index(index) => Some(index.cluster_id),
2518            CatalogItem::MetricSink(metric_sink) => Some(metric_sink.cluster_id),
2519            CatalogItem::Table(_)
2520            | CatalogItem::Source(_)
2521            | CatalogItem::Log(_)
2522            | CatalogItem::View(_)
2523            | CatalogItem::MaterializedView(_)
2524            | CatalogItem::Sink(_)
2525            | CatalogItem::Type(_)
2526            | CatalogItem::Func(_)
2527            | CatalogItem::Secret(_)
2528            | CatalogItem::Connection(_) => None,
2529        }
2530    }
2531
2532    /// Whether this item runs a dataflow on its cluster's replicas and so has
2533    /// hydration state: an index, materialized view, sink, or ingestion source.
2534    /// Non-ingestion sources (webhooks, ingestion exports) are bound to a
2535    /// cluster but run no dataflow on any replica.
2536    ///
2537    /// NOTE: `Coordinator::cluster_has_hydratable_objects` routes a cluster onto
2538    /// the hydration-aware scheduling and reconfiguration path off this answer,
2539    /// so an item that ships no dataflow must answer `false` even where the
2540    /// per-replica hydration check would resolve vacuously.
2541    pub fn is_hydratable(&self) -> bool {
2542        match self {
2543            CatalogItem::Index(_)
2544            | CatalogItem::MaterializedView(_)
2545            | CatalogItem::Sink(_)
2546            | CatalogItem::MetricSink(_) => true,
2547            CatalogItem::Source(source) => matches!(
2548                source.data_source,
2549                DataSourceDesc::Ingestion { .. } | DataSourceDesc::OldSyntaxIngestion { .. }
2550            ),
2551            CatalogItem::Table(_)
2552            | CatalogItem::Log(_)
2553            | CatalogItem::View(_)
2554            | CatalogItem::Type(_)
2555            | CatalogItem::Func(_)
2556            | CatalogItem::Secret(_)
2557            | CatalogItem::Connection(_) => false,
2558        }
2559    }
2560
2561    pub fn cluster_id(&self) -> Option<ClusterId> {
2562        match self {
2563            CatalogItem::MaterializedView(mv) => Some(mv.cluster_id),
2564            CatalogItem::Index(index) => Some(index.cluster_id),
2565            CatalogItem::MetricSink(metric_sink) => Some(metric_sink.cluster_id),
2566            CatalogItem::Source(source) => match &source.data_source {
2567                DataSourceDesc::Ingestion { cluster_id, .. }
2568                | DataSourceDesc::OldSyntaxIngestion { cluster_id, .. } => Some(*cluster_id),
2569                // This is somewhat of a lie because the export runs on the same
2570                // cluster as its ingestion but we don't yet have a way of
2571                // cross-referencing the items
2572                DataSourceDesc::IngestionExport { .. } => None,
2573                DataSourceDesc::Webhook { cluster_id, .. } => Some(*cluster_id),
2574                DataSourceDesc::Introspection(_)
2575                | DataSourceDesc::Progress
2576                | DataSourceDesc::Catalog => None,
2577            },
2578            CatalogItem::Sink(sink) => Some(sink.cluster_id),
2579            CatalogItem::Table(_)
2580            | CatalogItem::Log(_)
2581            | CatalogItem::View(_)
2582            | CatalogItem::Type(_)
2583            | CatalogItem::Func(_)
2584            | CatalogItem::Secret(_)
2585            | CatalogItem::Connection(_) => None,
2586        }
2587    }
2588
2589    /// The custom compaction window, if any has been set. This does not reflect any propagated
2590    /// compaction window (i.e., source -> subsource).
2591    pub fn custom_logical_compaction_window(&self) -> Option<CompactionWindow> {
2592        match self {
2593            CatalogItem::Table(table) => table.custom_logical_compaction_window,
2594            CatalogItem::Source(source) => source.custom_logical_compaction_window,
2595            CatalogItem::Index(index) => index.custom_logical_compaction_window,
2596            CatalogItem::MaterializedView(mview) => mview.custom_logical_compaction_window,
2597            CatalogItem::Log(_)
2598            | CatalogItem::View(_)
2599            | CatalogItem::Sink(_)
2600            | CatalogItem::Type(_)
2601            | CatalogItem::Func(_)
2602            | CatalogItem::Secret(_)
2603            | CatalogItem::Connection(_)
2604            | CatalogItem::MetricSink(_) => None,
2605        }
2606    }
2607
2608    /// Mutable access to the custom compaction window, or None if this type does not support custom
2609    /// compaction windows. This does not reflect any propagated compaction window (i.e., source ->
2610    /// subsource).
2611    pub fn custom_logical_compaction_window_mut(
2612        &mut self,
2613    ) -> Option<&mut Option<CompactionWindow>> {
2614        let cw = match self {
2615            CatalogItem::Table(table) => &mut table.custom_logical_compaction_window,
2616            CatalogItem::Source(source) => &mut source.custom_logical_compaction_window,
2617            CatalogItem::Index(index) => &mut index.custom_logical_compaction_window,
2618            CatalogItem::MaterializedView(mview) => &mut mview.custom_logical_compaction_window,
2619            CatalogItem::Log(_)
2620            | CatalogItem::View(_)
2621            | CatalogItem::Sink(_)
2622            | CatalogItem::Type(_)
2623            | CatalogItem::Func(_)
2624            | CatalogItem::Secret(_)
2625            | CatalogItem::Connection(_)
2626            | CatalogItem::MetricSink(_) => return None,
2627        };
2628        Some(cw)
2629    }
2630
2631    /// The initial compaction window, for objects that have one; that is, tables, sources, indexes,
2632    /// and MVs. This does not reflect any propagated compaction window (i.e., source -> subsource).
2633    ///
2634    /// If `custom_logical_compaction_window()` returns something, use that.  Otherwise, use a
2635    /// sensible default (currently 1s).
2636    ///
2637    /// For objects that do not have the concept of compaction window, return None.
2638    pub fn initial_logical_compaction_window(&self) -> Option<CompactionWindow> {
2639        let custom_logical_compaction_window = match self {
2640            CatalogItem::Table(_)
2641            | CatalogItem::Source(_)
2642            | CatalogItem::Index(_)
2643            | CatalogItem::MaterializedView(_) => self.custom_logical_compaction_window(),
2644            CatalogItem::Log(_)
2645            | CatalogItem::View(_)
2646            | CatalogItem::Sink(_)
2647            | CatalogItem::Type(_)
2648            | CatalogItem::Func(_)
2649            | CatalogItem::Secret(_)
2650            | CatalogItem::Connection(_)
2651            | CatalogItem::MetricSink(_) => return None,
2652        };
2653        Some(custom_logical_compaction_window.unwrap_or(CompactionWindow::Default))
2654    }
2655
2656    /// Whether the item's logical compaction window
2657    /// is controlled by the METRICS_RETENTION
2658    /// system var.
2659    pub fn is_retained_metrics_object(&self) -> bool {
2660        match self {
2661            CatalogItem::Table(table) => table.is_retained_metrics_object,
2662            CatalogItem::Source(source) => source.is_retained_metrics_object,
2663            CatalogItem::Index(index) => index.is_retained_metrics_object,
2664            CatalogItem::Log(_)
2665            | CatalogItem::View(_)
2666            | CatalogItem::MaterializedView(_)
2667            | CatalogItem::Sink(_)
2668            | CatalogItem::Type(_)
2669            | CatalogItem::Func(_)
2670            | CatalogItem::Secret(_)
2671            | CatalogItem::Connection(_)
2672            | CatalogItem::MetricSink(_) => false,
2673        }
2674    }
2675
2676    pub fn to_serialized(&self) -> (String, GlobalId, BTreeMap<RelationVersion, GlobalId>) {
2677        match self {
2678            CatalogItem::Table(table) => {
2679                let create_sql = table
2680                    .create_sql
2681                    .clone()
2682                    .expect("builtin tables cannot be serialized");
2683                let mut collections = table.collections.clone();
2684                let global_id = collections
2685                    .remove(&RelationVersion::root())
2686                    .expect("at least one version");
2687                (create_sql, global_id, collections)
2688            }
2689            CatalogItem::Log(_) => unreachable!("builtin logs cannot be serialized"),
2690            CatalogItem::Source(source) => {
2691                assert!(
2692                    !matches!(source.data_source, DataSourceDesc::Introspection(_)),
2693                    "cannot serialize introspection/builtin sources",
2694                );
2695                let create_sql = source
2696                    .create_sql
2697                    .clone()
2698                    .expect("builtin sources cannot be serialized");
2699                (create_sql, source.global_id, BTreeMap::new())
2700            }
2701            CatalogItem::View(view) => (view.create_sql.clone(), view.global_id, BTreeMap::new()),
2702            CatalogItem::MaterializedView(mview) => {
2703                let mut collections = mview.collections.clone();
2704                let global_id = collections
2705                    .remove(&RelationVersion::root())
2706                    .expect("at least one version");
2707                (mview.create_sql.clone(), global_id, collections)
2708            }
2709            CatalogItem::Index(index) => {
2710                (index.create_sql.clone(), index.global_id, BTreeMap::new())
2711            }
2712            CatalogItem::Sink(sink) => (sink.create_sql.clone(), sink.global_id, BTreeMap::new()),
2713            CatalogItem::Type(typ) => {
2714                let create_sql = typ
2715                    .create_sql
2716                    .clone()
2717                    .expect("builtin types cannot be serialized");
2718                (create_sql, typ.global_id, BTreeMap::new())
2719            }
2720            CatalogItem::Secret(secret) => {
2721                (secret.create_sql.clone(), secret.global_id, BTreeMap::new())
2722            }
2723            CatalogItem::Connection(connection) => (
2724                connection.create_sql.clone(),
2725                connection.global_id,
2726                BTreeMap::new(),
2727            ),
2728            CatalogItem::Func(_) => unreachable!("cannot serialize functions yet"),
2729            CatalogItem::MetricSink(ms) => (ms.create_sql.clone(), ms.global_id, BTreeMap::new()),
2730        }
2731    }
2732
2733    pub fn into_serialized(self) -> (String, GlobalId, BTreeMap<RelationVersion, GlobalId>) {
2734        match self {
2735            CatalogItem::Table(mut table) => {
2736                let create_sql = table
2737                    .create_sql
2738                    .expect("builtin tables cannot be serialized");
2739                let global_id = table
2740                    .collections
2741                    .remove(&RelationVersion::root())
2742                    .expect("at least one version");
2743                (create_sql, global_id, table.collections)
2744            }
2745            CatalogItem::Log(_) => unreachable!("builtin logs cannot be serialized"),
2746            CatalogItem::Source(source) => {
2747                assert!(
2748                    !matches!(source.data_source, DataSourceDesc::Introspection(_)),
2749                    "cannot serialize introspection/builtin sources",
2750                );
2751                let create_sql = source
2752                    .create_sql
2753                    .expect("builtin sources cannot be serialized");
2754                (create_sql, source.global_id, BTreeMap::new())
2755            }
2756            CatalogItem::View(view) => (view.create_sql, view.global_id, BTreeMap::new()),
2757            CatalogItem::MaterializedView(mut mview) => {
2758                let global_id = mview
2759                    .collections
2760                    .remove(&RelationVersion::root())
2761                    .expect("at least one version");
2762                (mview.create_sql, global_id, mview.collections)
2763            }
2764            CatalogItem::Index(index) => (index.create_sql, index.global_id, BTreeMap::new()),
2765            CatalogItem::Sink(sink) => (sink.create_sql, sink.global_id, BTreeMap::new()),
2766            CatalogItem::Type(typ) => {
2767                let create_sql = typ.create_sql.expect("builtin types cannot be serialized");
2768                (create_sql, typ.global_id, BTreeMap::new())
2769            }
2770            CatalogItem::Secret(secret) => (secret.create_sql, secret.global_id, BTreeMap::new()),
2771            CatalogItem::Connection(connection) => {
2772                (connection.create_sql, connection.global_id, BTreeMap::new())
2773            }
2774            CatalogItem::Func(_) => unreachable!("cannot serialize functions yet"),
2775            CatalogItem::MetricSink(ms) => (ms.create_sql, ms.global_id, BTreeMap::new()),
2776        }
2777    }
2778
2779    /// Returns a global ID for a specific version selector. Returns `None` if the item does
2780    /// not have versions or if the version does not exist.
2781    pub fn global_id_for_version(&self, version: RelationVersionSelector) -> Option<GlobalId> {
2782        let collections = match self {
2783            CatalogItem::MaterializedView(mv) => &mv.collections,
2784            CatalogItem::Table(table) => &table.collections,
2785            CatalogItem::Source(source) => return Some(source.global_id),
2786            CatalogItem::Log(log) => return Some(log.global_id),
2787            CatalogItem::View(view) => return Some(view.global_id),
2788            CatalogItem::Sink(sink) => return Some(sink.global_id),
2789            CatalogItem::Index(index) => return Some(index.global_id),
2790            CatalogItem::Type(ty) => return Some(ty.global_id),
2791            CatalogItem::Func(func) => return Some(func.global_id),
2792            CatalogItem::Secret(secret) => return Some(secret.global_id),
2793            CatalogItem::Connection(conn) => return Some(conn.global_id),
2794            CatalogItem::MetricSink(metric_sink) => return Some(metric_sink.global_id),
2795        };
2796        match version {
2797            RelationVersionSelector::Latest => collections.values().last().copied(),
2798            RelationVersionSelector::Specific(version) => collections.get(&version).copied(),
2799        }
2800    }
2801}
2802
2803impl CatalogEntry {
2804    /// Reports the latest [`RelationDesc`] of the rows produced by this [`CatalogEntry`], if it
2805    /// produces rows.
2806    pub fn relation_desc_latest(&self) -> Option<Cow<'_, RelationDesc>> {
2807        self.item.relation_desc(RelationVersionSelector::Latest)
2808    }
2809
2810    /// Reports if the item has columns.
2811    pub fn has_columns(&self) -> bool {
2812        match self.item() {
2813            CatalogItem::Type(Type { details, .. }) => {
2814                matches!(details.typ, CatalogType::Record { .. })
2815            }
2816            _ => self.relation_desc_latest().is_some(),
2817        }
2818    }
2819
2820    /// Returns the [`mz_sql::func::Func`] associated with this `CatalogEntry`.
2821    pub fn func(&self) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
2822        self.item.func(self)
2823    }
2824
2825    /// Returns the inner [`Index`] if this entry is an index, else `None`.
2826    pub fn index(&self) -> Option<&Index> {
2827        match self.item() {
2828            CatalogItem::Index(idx) => Some(idx),
2829            _ => None,
2830        }
2831    }
2832
2833    /// Returns the inner [`MaterializedView`] if this entry is a materialized view, else `None`.
2834    pub fn materialized_view(&self) -> Option<&MaterializedView> {
2835        match self.item() {
2836            CatalogItem::MaterializedView(mv) => Some(mv),
2837            _ => None,
2838        }
2839    }
2840
2841    /// Returns the inner [`Table`] if this entry is a table, else `None`.
2842    pub fn table(&self) -> Option<&Table> {
2843        match self.item() {
2844            CatalogItem::Table(tbl) => Some(tbl),
2845            _ => None,
2846        }
2847    }
2848
2849    /// Returns the inner [`Source`] if this entry is a source, else `None`.
2850    pub fn source(&self) -> Option<&Source> {
2851        match self.item() {
2852            CatalogItem::Source(src) => Some(src),
2853            _ => None,
2854        }
2855    }
2856
2857    /// Returns the inner [`Sink`] if this entry is a sink, else `None`.
2858    pub fn sink(&self) -> Option<&Sink> {
2859        match self.item() {
2860            CatalogItem::Sink(sink) => Some(sink),
2861            _ => None,
2862        }
2863    }
2864
2865    /// Returns the inner [`Secret`] if this entry is a secret, else `None`.
2866    pub fn secret(&self) -> Option<&Secret> {
2867        match self.item() {
2868            CatalogItem::Secret(secret) => Some(secret),
2869            _ => None,
2870        }
2871    }
2872
2873    pub fn connection(&self) -> Result<&Connection, SqlCatalogError> {
2874        match self.item() {
2875            CatalogItem::Connection(connection) => Ok(connection),
2876            _ => {
2877                let db_name = match self.name().qualifiers.database_spec {
2878                    ResolvedDatabaseSpecifier::Ambient => "".to_string(),
2879                    ResolvedDatabaseSpecifier::Id(id) => format!("{id}."),
2880                };
2881                Err(SqlCatalogError::UnknownConnection(format!(
2882                    "{}{}.{}",
2883                    db_name,
2884                    self.name().qualifiers.schema_spec,
2885                    self.name().item
2886                )))
2887            }
2888        }
2889    }
2890
2891    /// Returns the [`mz_storage_types::sources::SourceDesc`] associated with
2892    /// this `CatalogEntry`, if any.
2893    pub fn source_desc(
2894        &self,
2895    ) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
2896        self.item.source_desc(self)
2897    }
2898
2899    /// Reports whether this catalog entry is a connection.
2900    pub fn is_connection(&self) -> bool {
2901        matches!(self.item(), CatalogItem::Connection(_))
2902    }
2903
2904    /// Reports whether this catalog entry is a table.
2905    pub fn is_table(&self) -> bool {
2906        matches!(self.item(), CatalogItem::Table(_))
2907    }
2908
2909    /// Reports whether this catalog entry is a source. Note that this includes
2910    /// subsources.
2911    pub fn is_source(&self) -> bool {
2912        matches!(self.item(), CatalogItem::Source(_))
2913    }
2914
2915    /// Reports whether this catalog entry is a subsource and, if it is, the
2916    /// ingestion it is an export of, as well as the item it exports.
2917    pub fn subsource_details(
2918        &self,
2919    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)> {
2920        match &self.item() {
2921            CatalogItem::Source(source) => match &source.data_source {
2922                DataSourceDesc::IngestionExport {
2923                    ingestion_id,
2924                    external_reference,
2925                    details,
2926                    data_config: _,
2927                } => Some((*ingestion_id, external_reference, details)),
2928                _ => None,
2929            },
2930            _ => None,
2931        }
2932    }
2933
2934    /// Reports whether this catalog entry is a source export and, if it is, the
2935    /// ingestion it is an export of, as well as the item it exports.
2936    pub fn source_export_details(
2937        &self,
2938    ) -> Option<(
2939        CatalogItemId,
2940        &UnresolvedItemName,
2941        &SourceExportDetails,
2942        &SourceExportDataConfig<ReferencedConnection>,
2943    )> {
2944        match &self.item() {
2945            CatalogItem::Source(source) => match &source.data_source {
2946                DataSourceDesc::IngestionExport {
2947                    ingestion_id,
2948                    external_reference,
2949                    details,
2950                    data_config,
2951                } => Some((*ingestion_id, external_reference, details, data_config)),
2952                _ => None,
2953            },
2954            CatalogItem::Table(table) => match &table.data_source {
2955                TableDataSource::DataSource {
2956                    desc:
2957                        DataSourceDesc::IngestionExport {
2958                            ingestion_id,
2959                            external_reference,
2960                            details,
2961                            data_config,
2962                        },
2963                    timeline: _,
2964                } => Some((*ingestion_id, external_reference, details, data_config)),
2965                _ => None,
2966            },
2967            _ => None,
2968        }
2969    }
2970
2971    /// Reports whether this catalog entry is a progress source.
2972    pub fn is_progress_source(&self) -> bool {
2973        self.item().is_progress_source()
2974    }
2975
2976    /// Returns the `GlobalId` of all of this entry's progress ID.
2977    pub fn progress_id(&self) -> Option<CatalogItemId> {
2978        match &self.item() {
2979            CatalogItem::Source(source) => match &source.data_source {
2980                DataSourceDesc::Ingestion { .. } => Some(self.id),
2981                DataSourceDesc::OldSyntaxIngestion {
2982                    progress_subsource, ..
2983                } => Some(*progress_subsource),
2984                DataSourceDesc::IngestionExport { .. }
2985                | DataSourceDesc::Introspection(_)
2986                | DataSourceDesc::Progress
2987                | DataSourceDesc::Webhook { .. }
2988                | DataSourceDesc::Catalog => None,
2989            },
2990            CatalogItem::Table(_)
2991            | CatalogItem::Log(_)
2992            | CatalogItem::View(_)
2993            | CatalogItem::MaterializedView(_)
2994            | CatalogItem::Sink(_)
2995            | CatalogItem::Index(_)
2996            | CatalogItem::Type(_)
2997            | CatalogItem::Func(_)
2998            | CatalogItem::Secret(_)
2999            | CatalogItem::Connection(_)
3000            | CatalogItem::MetricSink(_) => None,
3001        }
3002    }
3003
3004    /// Reports whether this catalog entry is a sink.
3005    pub fn is_sink(&self) -> bool {
3006        matches!(self.item(), CatalogItem::Sink(_))
3007    }
3008
3009    /// Reports whether this catalog entry is a materialized view.
3010    pub fn is_materialized_view(&self) -> bool {
3011        matches!(self.item(), CatalogItem::MaterializedView(_))
3012    }
3013
3014    /// Reports whether this catalog entry is a view.
3015    pub fn is_view(&self) -> bool {
3016        matches!(self.item(), CatalogItem::View(_))
3017    }
3018
3019    /// Reports whether this catalog entry is a secret.
3020    pub fn is_secret(&self) -> bool {
3021        matches!(self.item(), CatalogItem::Secret(_))
3022    }
3023
3024    /// Reports whether this catalog entry is an introspection source.
3025    pub fn is_introspection_source(&self) -> bool {
3026        matches!(self.item(), CatalogItem::Log(_))
3027    }
3028
3029    /// Reports whether this catalog entry is an index.
3030    pub fn is_index(&self) -> bool {
3031        matches!(self.item(), CatalogItem::Index(_))
3032    }
3033
3034    /// Reports whether this catalog entry is a metric sink.
3035    pub fn is_metric_sink(&self) -> bool {
3036        matches!(self.item(), CatalogItem::MetricSink(_))
3037    }
3038
3039    /// Reports whether this catalog entry can be treated as a relation, it can produce rows.
3040    pub fn is_relation(&self) -> bool {
3041        mz_sql::catalog::ObjectType::from(self.item_type()).is_relation()
3042    }
3043
3044    /// Collects the identifiers of the objects that were encountered when
3045    /// resolving names in the item's DDL statement.
3046    pub fn references(&self) -> &ResolvedIds {
3047        self.item.references()
3048    }
3049
3050    /// Collects the identifiers of the objects used by this [`CatalogEntry`].
3051    ///
3052    /// Like [`CatalogEntry::references()`] but also includes objects that are not directly
3053    /// referenced. For example this will include any catalog objects used to implement functions
3054    /// and casts in the item.
3055    pub fn uses(&self) -> BTreeSet<CatalogItemId> {
3056        self.item.uses()
3057    }
3058
3059    /// Returns the `CatalogItem` associated with this catalog entry.
3060    pub fn item(&self) -> &CatalogItem {
3061        &self.item
3062    }
3063
3064    /// Returns a mutable reference to the `CatalogItem` associated with this
3065    /// catalog entry.
3066    pub fn item_mut(&mut self) -> &mut CatalogItem {
3067        &mut self.item
3068    }
3069
3070    /// Returns the [`CatalogItemId`] of this catalog entry.
3071    pub fn id(&self) -> CatalogItemId {
3072        self.id
3073    }
3074
3075    /// Returns all of the [`GlobalId`]s associated with this item.
3076    pub fn global_ids(&self) -> impl Iterator<Item = GlobalId> + '_ {
3077        self.item().global_ids()
3078    }
3079
3080    pub fn latest_global_id(&self) -> GlobalId {
3081        self.item().latest_global_id()
3082    }
3083
3084    /// Returns the OID of this catalog entry.
3085    pub fn oid(&self) -> u32 {
3086        self.oid
3087    }
3088
3089    /// Returns the fully qualified name of this catalog entry.
3090    pub fn name(&self) -> &QualifiedItemName {
3091        &self.name
3092    }
3093
3094    /// Returns the identifiers of the dataflows that are directly referenced by this dataflow.
3095    pub fn referenced_by(&self) -> &[CatalogItemId] {
3096        &self.referenced_by
3097    }
3098
3099    /// Returns the identifiers of the dataflows that depend upon this dataflow.
3100    pub fn used_by(&self) -> &[CatalogItemId] {
3101        &self.used_by
3102    }
3103
3104    /// Returns the connection ID that this item belongs to, if this item is
3105    /// temporary.
3106    pub fn conn_id(&self) -> Option<&ConnectionId> {
3107        self.item.conn_id()
3108    }
3109
3110    /// Returns the role ID of the entry owner.
3111    pub fn owner_id(&self) -> &RoleId {
3112        &self.owner_id
3113    }
3114
3115    /// Returns the privileges of the entry.
3116    pub fn privileges(&self) -> &PrivilegeMap {
3117        &self.privileges
3118    }
3119
3120    /// Returns the comment object ID for this entry.
3121    pub fn comment_object_id(&self) -> CommentObjectId {
3122        use CatalogItemType::*;
3123        match self.item_type() {
3124            Table => CommentObjectId::Table(self.id),
3125            Source => CommentObjectId::Source(self.id),
3126            Sink => CommentObjectId::Sink(self.id),
3127            View => CommentObjectId::View(self.id),
3128            MaterializedView => CommentObjectId::MaterializedView(self.id),
3129            Index => CommentObjectId::Index(self.id),
3130            Func => CommentObjectId::Func(self.id),
3131            Connection => CommentObjectId::Connection(self.id),
3132            Type => CommentObjectId::Type(self.id),
3133            Secret => CommentObjectId::Secret(self.id),
3134            MetricSink => CommentObjectId::MetricSink(self.id),
3135        }
3136    }
3137}
3138
3139#[derive(Debug, Clone, Default)]
3140pub struct CommentsMap {
3141    map: BTreeMap<CommentObjectId, BTreeMap<Option<usize>, String>>,
3142}
3143
3144impl CommentsMap {
3145    pub fn update_comment(
3146        &mut self,
3147        object_id: CommentObjectId,
3148        sub_component: Option<usize>,
3149        comment: Option<String>,
3150    ) -> Option<String> {
3151        let object_comments = self.map.entry(object_id).or_default();
3152
3153        // Either replace the existing comment, or remove it if comment is None/NULL.
3154        let (empty, prev) = if let Some(comment) = comment {
3155            let prev = object_comments.insert(sub_component, comment);
3156            (false, prev)
3157        } else {
3158            let prev = object_comments.remove(&sub_component);
3159            (object_comments.is_empty(), prev)
3160        };
3161
3162        // Cleanup entries that are now empty.
3163        if empty {
3164            self.map.remove(&object_id);
3165        }
3166
3167        // Return the previous comment, if there was one, for easy removal.
3168        prev
3169    }
3170
3171    /// Remove all comments for `object_id` from the map.
3172    ///
3173    /// Generally there is one comment for a given [`CommentObjectId`], but in the case of
3174    /// relations you can also have comments on the individual columns. Dropping the comments for a
3175    /// relation will also drop all of the comments on any columns.
3176    pub fn drop_comments(
3177        &mut self,
3178        object_ids: &BTreeSet<CommentObjectId>,
3179    ) -> Vec<(CommentObjectId, Option<usize>, String)> {
3180        let mut removed_comments = Vec::new();
3181
3182        for object_id in object_ids {
3183            if let Some(comments) = self.map.remove(object_id) {
3184                let removed = comments
3185                    .into_iter()
3186                    .map(|(sub_comp, comment)| (object_id.clone(), sub_comp, comment));
3187                removed_comments.extend(removed);
3188            }
3189        }
3190
3191        removed_comments
3192    }
3193
3194    pub fn iter(&self) -> impl Iterator<Item = (CommentObjectId, Option<usize>, &str)> {
3195        self.map
3196            .iter()
3197            .map(|(id, comments)| {
3198                comments
3199                    .iter()
3200                    .map(|(pos, comment)| (*id, *pos, comment.as_str()))
3201            })
3202            .flatten()
3203    }
3204
3205    pub fn get_object_comments(
3206        &self,
3207        object_id: CommentObjectId,
3208    ) -> Option<&BTreeMap<Option<usize>, String>> {
3209        self.map.get(&object_id)
3210    }
3211}
3212
3213impl Serialize for CommentsMap {
3214    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3215    where
3216        S: serde::Serializer,
3217    {
3218        let comment_count = self
3219            .map
3220            .iter()
3221            .map(|(_object_id, comments)| comments.len())
3222            .sum();
3223
3224        let mut seq = serializer.serialize_seq(Some(comment_count))?;
3225        for (object_id, sub) in &self.map {
3226            for (sub_component, comment) in sub {
3227                seq.serialize_element(&(
3228                    format!("{object_id:?}"),
3229                    format!("{sub_component:?}"),
3230                    comment,
3231                ))?;
3232            }
3233        }
3234        seq.end()
3235    }
3236}
3237
3238#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Default)]
3239pub struct DefaultPrivileges {
3240    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
3241    privileges: BTreeMap<DefaultPrivilegeObject, RoleDefaultPrivileges>,
3242}
3243
3244// Use a new type here because otherwise we have two levels of BTreeMap, both needing
3245// map_key_to_string.
3246#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Default)]
3247struct RoleDefaultPrivileges(
3248    /// Denormalized, the key is the grantee Role.
3249    #[serde(serialize_with = "mz_ore::serde::map_key_to_string")]
3250    BTreeMap<RoleId, DefaultPrivilegeAclItem>,
3251);
3252
3253impl Deref for RoleDefaultPrivileges {
3254    type Target = BTreeMap<RoleId, DefaultPrivilegeAclItem>;
3255
3256    fn deref(&self) -> &Self::Target {
3257        &self.0
3258    }
3259}
3260
3261impl DerefMut for RoleDefaultPrivileges {
3262    fn deref_mut(&mut self) -> &mut Self::Target {
3263        &mut self.0
3264    }
3265}
3266
3267impl DefaultPrivileges {
3268    /// Add a new default privilege into the set of all default privileges.
3269    pub fn grant(&mut self, object: DefaultPrivilegeObject, privilege: DefaultPrivilegeAclItem) {
3270        if privilege.acl_mode.is_empty() {
3271            return;
3272        }
3273
3274        let privileges = self.privileges.entry(object).or_default();
3275        if let Some(default_privilege) = privileges.get_mut(&privilege.grantee) {
3276            default_privilege.acl_mode |= privilege.acl_mode;
3277        } else {
3278            privileges.insert(privilege.grantee, privilege);
3279        }
3280    }
3281
3282    /// Revoke a default privilege from the set of all default privileges.
3283    pub fn revoke(&mut self, object: &DefaultPrivilegeObject, privilege: &DefaultPrivilegeAclItem) {
3284        if let Some(privileges) = self.privileges.get_mut(object) {
3285            if let Some(default_privilege) = privileges.get_mut(&privilege.grantee) {
3286                default_privilege.acl_mode =
3287                    default_privilege.acl_mode.difference(privilege.acl_mode);
3288                if default_privilege.acl_mode.is_empty() {
3289                    privileges.remove(&privilege.grantee);
3290                }
3291            }
3292            if privileges.is_empty() {
3293                self.privileges.remove(object);
3294            }
3295        }
3296    }
3297
3298    /// Get the privileges that will be granted on all objects matching `object` to `grantee`, if
3299    /// any exist.
3300    pub fn get_privileges_for_grantee(
3301        &self,
3302        object: &DefaultPrivilegeObject,
3303        grantee: &RoleId,
3304    ) -> Option<&AclMode> {
3305        self.privileges
3306            .get(object)
3307            .and_then(|privileges| privileges.get(grantee))
3308            .map(|privilege| &privilege.acl_mode)
3309    }
3310
3311    /// Get all default privileges that apply to the provided object details.
3312    pub fn get_applicable_privileges(
3313        &self,
3314        role_id: RoleId,
3315        database_id: Option<DatabaseId>,
3316        schema_id: Option<SchemaId>,
3317        object_type: mz_sql::catalog::ObjectType,
3318    ) -> impl Iterator<Item = DefaultPrivilegeAclItem> + '_ {
3319        // Privileges consider all relations to be of type table due to PostgreSQL compatibility. We
3320        // don't require the caller to worry about that and we will map their `object_type` to the
3321        // correct type for privileges.
3322        let privilege_object_type = if object_type.is_relation() {
3323            mz_sql::catalog::ObjectType::Table
3324        } else {
3325            object_type
3326        };
3327        let valid_acl_mode = rbac::all_object_privileges(SystemObjectType::Object(object_type));
3328
3329        // Collect all entries that apply to the provided object details.
3330        // If either `database_id` or `schema_id` are `None`, then we might end up with duplicate
3331        // entries in the vec below. That's OK because we consolidate the results after.
3332        [
3333            DefaultPrivilegeObject {
3334                role_id,
3335                database_id,
3336                schema_id,
3337                object_type: privilege_object_type,
3338            },
3339            DefaultPrivilegeObject {
3340                role_id,
3341                database_id,
3342                schema_id: None,
3343                object_type: privilege_object_type,
3344            },
3345            DefaultPrivilegeObject {
3346                role_id,
3347                database_id: None,
3348                schema_id: None,
3349                object_type: privilege_object_type,
3350            },
3351            DefaultPrivilegeObject {
3352                role_id: RoleId::Public,
3353                database_id,
3354                schema_id,
3355                object_type: privilege_object_type,
3356            },
3357            DefaultPrivilegeObject {
3358                role_id: RoleId::Public,
3359                database_id,
3360                schema_id: None,
3361                object_type: privilege_object_type,
3362            },
3363            DefaultPrivilegeObject {
3364                role_id: RoleId::Public,
3365                database_id: None,
3366                schema_id: None,
3367                object_type: privilege_object_type,
3368            },
3369        ]
3370        .into_iter()
3371        .filter_map(|object| self.privileges.get(&object))
3372        .flat_map(|acl_map| acl_map.values())
3373        // Consolidate privileges with a common grantee.
3374        .fold(
3375            BTreeMap::new(),
3376            |mut accum, DefaultPrivilegeAclItem { grantee, acl_mode }| {
3377                let accum_acl_mode = accum.entry(grantee).or_insert_with(AclMode::empty);
3378                *accum_acl_mode |= *acl_mode;
3379                accum
3380            },
3381        )
3382        .into_iter()
3383        // Restrict the acl_mode to only privileges valid for the provided object type. If the
3384        // default privilege has an object type of Table, then it may contain privileges valid for
3385        // tables but not other relations. If the passed in object type is another relation, then
3386        // we need to remove any privilege that is not valid for the specified relation.
3387        .map(move |(grantee, acl_mode)| (grantee, acl_mode & valid_acl_mode))
3388        // Filter out empty privileges.
3389        .filter(|(_, acl_mode)| !acl_mode.is_empty())
3390        .map(|(grantee, acl_mode)| DefaultPrivilegeAclItem {
3391            grantee: *grantee,
3392            acl_mode,
3393        })
3394    }
3395
3396    pub fn iter(
3397        &self,
3398    ) -> impl Iterator<
3399        Item = (
3400            &DefaultPrivilegeObject,
3401            impl Iterator<Item = &DefaultPrivilegeAclItem>,
3402        ),
3403    > {
3404        self.privileges
3405            .iter()
3406            .map(|(object, acl_map)| (object, acl_map.values()))
3407    }
3408}
3409
3410#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3411pub struct ClusterConfig {
3412    pub variant: ClusterVariant,
3413    pub workload_class: Option<String>,
3414}
3415
3416impl ClusterConfig {
3417    pub fn features(&self) -> Option<&OptimizerFeatureOverrides> {
3418        match &self.variant {
3419            ClusterVariant::Managed(managed) => Some(&managed.optimizer_feature_overrides),
3420            ClusterVariant::Unmanaged => None,
3421        }
3422    }
3423}
3424
3425impl From<ClusterConfig> for durable::ClusterConfig {
3426    fn from(config: ClusterConfig) -> Self {
3427        Self {
3428            variant: config.variant.into(),
3429            workload_class: config.workload_class,
3430        }
3431    }
3432}
3433
3434impl From<durable::ClusterConfig> for ClusterConfig {
3435    fn from(config: durable::ClusterConfig) -> Self {
3436        Self {
3437            variant: config.variant.into(),
3438            workload_class: config.workload_class,
3439        }
3440    }
3441}
3442
3443#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3444pub struct ClusterVariantManaged {
3445    pub size: String,
3446    pub availability_zones: Vec<String>,
3447    pub logging: ReplicaLogging,
3448    /// Whether arrangements on this cluster's replicas request dictionary compression.
3449    pub arrangement_compression: bool,
3450    pub replication_factor: u32,
3451    pub optimizer_feature_overrides: OptimizerFeatureOverrides,
3452    pub schedule: ClusterSchedule,
3453    /// User-configured autoscaling policy, distinct from the in-flight runtime
3454    /// records below. Shared with the durable layer, like [`ClusterSchedule`].
3455    pub auto_scaling_strategy: Option<AutoScalingStrategy>,
3456    /// Latest graceful reconfiguration record, if one has been written.
3457    pub reconfiguration: Option<ReconfigurationState>,
3458    /// In-flight hydration burst the controller is running.
3459    pub burst: Option<BurstState>,
3460}
3461
3462/// Per-replica config shape of a managed cluster.
3463#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3464pub struct ManagedReplicaConfigShape<'a> {
3465    pub size: &'a str,
3466    pub availability_zones: &'a [String],
3467    pub logging: &'a ReplicaLogging,
3468    pub arrangement_compression: bool,
3469}
3470
3471impl<'a> ManagedReplicaConfigShape<'a> {
3472    /// Returns a per-replica config shape from its component fields.
3473    pub fn new(
3474        size: &'a str,
3475        availability_zones: &'a [String],
3476        logging: &'a ReplicaLogging,
3477        arrangement_compression: bool,
3478    ) -> Self {
3479        Self {
3480            size,
3481            availability_zones,
3482            logging,
3483            arrangement_compression,
3484        }
3485    }
3486}
3487
3488impl ClusterVariantManaged {
3489    /// Returns the per-replica config shape of this managed cluster.
3490    pub fn replica_config_shape(&self) -> ManagedReplicaConfigShape<'_> {
3491        let ClusterVariantManaged {
3492            size,
3493            availability_zones,
3494            logging,
3495            arrangement_compression,
3496            replication_factor: _,
3497            optimizer_feature_overrides: _,
3498            schedule: _,
3499            auto_scaling_strategy: _,
3500            reconfiguration: _,
3501            burst: _,
3502        } = self;
3503        ManagedReplicaConfigShape::new(size, availability_zones, logging, *arrangement_compression)
3504    }
3505
3506    /// Returns this managed cluster's realized shape as a reconfiguration target.
3507    pub fn realized_reconfiguration_target(&self) -> ReconfigurationTarget {
3508        let ClusterVariantManaged {
3509            size,
3510            availability_zones,
3511            logging,
3512            arrangement_compression,
3513            replication_factor,
3514            optimizer_feature_overrides: _,
3515            schedule: _,
3516            auto_scaling_strategy: _,
3517            reconfiguration: _,
3518            burst: _,
3519        } = self;
3520        ReconfigurationTarget {
3521            size: size.clone(),
3522            replication_factor: *replication_factor,
3523            availability_zones: availability_zones.clone(),
3524            logging: logging.clone(),
3525            arrangement_compression: *arrangement_compression,
3526        }
3527    }
3528
3529    /// Advances this config's realized shape to `target`, the write a
3530    /// reconfiguration cut-over performs. The inverse of
3531    /// [`ClusterVariantManaged::realized_reconfiguration_target`].
3532    pub fn apply_reconfiguration_target(&mut self, target: ReconfigurationTarget) {
3533        // Destructured so a new target dimension fails to compile until it is
3534        // applied here too.
3535        let ReconfigurationTarget {
3536            size,
3537            replication_factor,
3538            availability_zones,
3539            logging,
3540            arrangement_compression,
3541        } = target;
3542        self.size = size;
3543        self.replication_factor = replication_factor;
3544        self.availability_zones = availability_zones;
3545        self.logging = logging;
3546        self.arrangement_compression = arrangement_compression;
3547    }
3548
3549    /// Whether the in-flight `burst` record is no longer warranted by this
3550    /// config: the `ON HYDRATION` policy was removed or re-sized away from the
3551    /// record's size, or the cluster was turned off (`replication_factor` 0).
3552    /// `false` when there is no record.
3553    pub fn has_unwarranted_burst_record(&self) -> bool {
3554        let Some(record) = &self.burst else {
3555            return false;
3556        };
3557        let hydration_size = self
3558            .auto_scaling_strategy
3559            .as_ref()
3560            .and_then(|strategy| strategy.on_hydration.as_ref())
3561            .map(|policy| policy.hydration_size.as_str());
3562        !mz_adapter_types::cluster_state::burst_record_warranted(
3563            &record.burst_size,
3564            self.replication_factor,
3565            hydration_size,
3566        )
3567    }
3568}
3569
3570impl From<ClusterVariantManaged> for durable::ClusterVariantManaged {
3571    fn from(managed: ClusterVariantManaged) -> Self {
3572        // Destructure the source (no `..`): a field added to either side is a
3573        // compile error here until it's carried across the boundary.
3574        let ClusterVariantManaged {
3575            size,
3576            availability_zones,
3577            logging,
3578            arrangement_compression,
3579            replication_factor,
3580            optimizer_feature_overrides,
3581            schedule,
3582            auto_scaling_strategy,
3583            reconfiguration,
3584            burst,
3585        } = managed;
3586        Self {
3587            size,
3588            availability_zones,
3589            logging,
3590            arrangement_compression,
3591            replication_factor,
3592            optimizer_feature_overrides: optimizer_feature_overrides.into(),
3593            schedule,
3594            auto_scaling_strategy,
3595            reconfiguration: reconfiguration.map(Into::into),
3596            burst: burst.map(Into::into),
3597        }
3598    }
3599}
3600
3601impl From<durable::ClusterVariantManaged> for ClusterVariantManaged {
3602    fn from(managed: durable::ClusterVariantManaged) -> Self {
3603        // Destructure the source (no `..`): a field added to either side is a
3604        // compile error here until it's carried across the boundary.
3605        let durable::ClusterVariantManaged {
3606            size,
3607            availability_zones,
3608            logging,
3609            arrangement_compression,
3610            replication_factor,
3611            optimizer_feature_overrides,
3612            schedule,
3613            auto_scaling_strategy,
3614            reconfiguration,
3615            burst,
3616        } = managed;
3617        Self {
3618            size,
3619            availability_zones,
3620            logging,
3621            arrangement_compression,
3622            replication_factor,
3623            optimizer_feature_overrides: optimizer_feature_overrides.into(),
3624            schedule,
3625            auto_scaling_strategy,
3626            reconfiguration: reconfiguration.map(Into::into),
3627            burst: burst.map(Into::into),
3628        }
3629    }
3630}
3631
3632/// In-memory mirror of [`durable::ReconfigurationState`].
3633///
3634/// This runtime state lives only in the durable layer, so the memory layer
3635/// carries its own `Serialize`/`Deserialize` mirror (to back the catalog
3636/// `dump()`) and converts across the boundary, rather than embedding the
3637/// durable-only type. The semantic contract lives on the durable type.
3638#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3639pub struct ReconfigurationState {
3640    pub target: ReconfigurationTarget,
3641    pub deadline: Timestamp,
3642    pub on_timeout: OnTimeoutAction,
3643    pub status: ReconfigurationStatus,
3644}
3645
3646/// In-memory mirror of [`durable::ReconfigurationStatus`].
3647///
3648/// The lifecycle of a cluster's `reconfiguration` record. Writers only ever
3649/// move the record along these transitions:
3650///
3651/// | from          | to                                                    | audited as          |
3652/// |---------------|-------------------------------------------------------|---------------------|
3653/// | no record     | `InProgress`                                          | `started`           |
3654/// | `InProgress`  | `InProgress` (re-target)                              | `started`           |
3655/// | `InProgress`  | `Finalized`                                           | `finalized`         |
3656/// | `InProgress`  | `TimedOut`                                            | `timed-out`         |
3657/// | `InProgress`  | `Cancelled`                                           | `cancelled`         |
3658/// | `InProgress`  | `ResourceExhausted`                                   | `resource-exhausted`|
3659/// | any terminal  | no record (drop), or `InProgress` (fresh record)      | none / `started`    |
3660///
3661/// The terminal statuses never transition into one another and a record is
3662/// never revived in place: a new reconfiguration overwrites the settled record
3663/// with a fresh `InProgress` one.
3664#[derive(
3665    Clone,
3666    Copy,
3667    Debug,
3668    Deserialize,
3669    Serialize,
3670    PartialOrd,
3671    PartialEq,
3672    Eq,
3673    Ord
3674)]
3675pub enum ReconfigurationStatus {
3676    InProgress,
3677    Finalized,
3678    TimedOut,
3679    Cancelled,
3680    ResourceExhausted,
3681}
3682
3683impl From<ReconfigurationStatus> for durable::ReconfigurationStatus {
3684    fn from(status: ReconfigurationStatus) -> Self {
3685        match status {
3686            ReconfigurationStatus::InProgress => durable::ReconfigurationStatus::InProgress,
3687            ReconfigurationStatus::Finalized => durable::ReconfigurationStatus::Finalized,
3688            ReconfigurationStatus::TimedOut => durable::ReconfigurationStatus::TimedOut,
3689            ReconfigurationStatus::Cancelled => durable::ReconfigurationStatus::Cancelled,
3690            ReconfigurationStatus::ResourceExhausted => {
3691                durable::ReconfigurationStatus::ResourceExhausted
3692            }
3693        }
3694    }
3695}
3696
3697impl From<durable::ReconfigurationStatus> for ReconfigurationStatus {
3698    fn from(status: durable::ReconfigurationStatus) -> Self {
3699        match status {
3700            durable::ReconfigurationStatus::InProgress => ReconfigurationStatus::InProgress,
3701            durable::ReconfigurationStatus::Finalized => ReconfigurationStatus::Finalized,
3702            durable::ReconfigurationStatus::TimedOut => ReconfigurationStatus::TimedOut,
3703            durable::ReconfigurationStatus::Cancelled => ReconfigurationStatus::Cancelled,
3704            durable::ReconfigurationStatus::ResourceExhausted => {
3705                ReconfigurationStatus::ResourceExhausted
3706            }
3707        }
3708    }
3709}
3710
3711impl ReconfigurationState {
3712    pub fn is_in_progress(&self) -> bool {
3713        matches!(self.status, ReconfigurationStatus::InProgress)
3714    }
3715}
3716
3717impl From<ReconfigurationState> for durable::ReconfigurationState {
3718    fn from(state: ReconfigurationState) -> Self {
3719        // Destructure the source (no `..`): a field added to either side is a
3720        // compile error here until it's carried across the boundary.
3721        let ReconfigurationState {
3722            target,
3723            deadline,
3724            on_timeout,
3725            status,
3726        } = state;
3727        Self {
3728            target: target.into(),
3729            deadline,
3730            on_timeout,
3731            status: status.into(),
3732        }
3733    }
3734}
3735
3736impl From<durable::ReconfigurationState> for ReconfigurationState {
3737    fn from(state: durable::ReconfigurationState) -> Self {
3738        // Destructure the source (no `..`): a field added to either side is a
3739        // compile error here until it's carried across the boundary.
3740        let durable::ReconfigurationState {
3741            target,
3742            deadline,
3743            on_timeout,
3744            status,
3745        } = state;
3746        Self {
3747            target: target.into(),
3748            deadline,
3749            on_timeout,
3750            status: status.into(),
3751        }
3752    }
3753}
3754
3755/// In-memory mirror of [`durable::ReconfigurationTarget`].
3756#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3757pub struct ReconfigurationTarget {
3758    pub size: String,
3759    pub replication_factor: u32,
3760    pub availability_zones: Vec<String>,
3761    pub logging: ReplicaLogging,
3762    pub arrangement_compression: bool,
3763}
3764
3765impl ReconfigurationTarget {
3766    /// Whether this target matches the realized config shape of `managed`.
3767    pub fn matches_realized_config(&self, managed: &ClusterVariantManaged) -> bool {
3768        self == &managed.realized_reconfiguration_target()
3769    }
3770}
3771
3772impl From<ReconfigurationTarget> for durable::ReconfigurationTarget {
3773    fn from(target: ReconfigurationTarget) -> Self {
3774        // Destructure the source (no `..`): a field added to either side is a
3775        // compile error here until it's carried across the boundary.
3776        let ReconfigurationTarget {
3777            size,
3778            replication_factor,
3779            availability_zones,
3780            logging,
3781            arrangement_compression,
3782        } = target;
3783        Self {
3784            size,
3785            replication_factor,
3786            availability_zones,
3787            logging,
3788            arrangement_compression,
3789        }
3790    }
3791}
3792
3793impl From<durable::ReconfigurationTarget> for ReconfigurationTarget {
3794    fn from(target: durable::ReconfigurationTarget) -> Self {
3795        // Destructure the source (no `..`): a field added to either side is a
3796        // compile error here until it's carried across the boundary.
3797        let durable::ReconfigurationTarget {
3798            size,
3799            replication_factor,
3800            availability_zones,
3801            logging,
3802            arrangement_compression,
3803        } = target;
3804        Self {
3805            size,
3806            replication_factor,
3807            availability_zones,
3808            logging,
3809            arrangement_compression,
3810        }
3811    }
3812}
3813
3814/// In-memory mirror of [`durable::BurstState`].
3815#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3816pub struct BurstState {
3817    pub burst_size: String,
3818    pub linger_duration: Duration,
3819    pub steady_hydrated_at: Option<Timestamp>,
3820}
3821
3822impl From<BurstState> for durable::BurstState {
3823    fn from(burst: BurstState) -> Self {
3824        // Destructure the source (no `..`): a field added to either side is a
3825        // compile error here until it's carried across the boundary.
3826        let BurstState {
3827            burst_size,
3828            linger_duration,
3829            steady_hydrated_at,
3830        } = burst;
3831        Self {
3832            burst_size,
3833            linger_duration,
3834            steady_hydrated_at,
3835        }
3836    }
3837}
3838
3839impl From<durable::BurstState> for BurstState {
3840    fn from(burst: durable::BurstState) -> Self {
3841        // Destructure the source (no `..`): a field added to either side is a
3842        // compile error here until it's carried across the boundary.
3843        let durable::BurstState {
3844            burst_size,
3845            linger_duration,
3846            steady_hydrated_at,
3847        } = burst;
3848        Self {
3849            burst_size,
3850            linger_duration,
3851            steady_hydrated_at,
3852        }
3853    }
3854}
3855
3856#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Eq, Ord)]
3857pub enum ClusterVariant {
3858    Managed(ClusterVariantManaged),
3859    Unmanaged,
3860}
3861
3862impl From<ClusterVariant> for durable::ClusterVariant {
3863    fn from(variant: ClusterVariant) -> Self {
3864        match variant {
3865            ClusterVariant::Managed(managed) => Self::Managed(managed.into()),
3866            ClusterVariant::Unmanaged => Self::Unmanaged,
3867        }
3868    }
3869}
3870
3871impl From<durable::ClusterVariant> for ClusterVariant {
3872    fn from(variant: durable::ClusterVariant) -> Self {
3873        match variant {
3874            durable::ClusterVariant::Managed(managed) => Self::Managed(managed.into()),
3875            durable::ClusterVariant::Unmanaged => Self::Unmanaged,
3876        }
3877    }
3878}
3879
3880impl mz_sql::catalog::CatalogDatabase for Database {
3881    fn name(&self) -> &str {
3882        &self.name
3883    }
3884
3885    fn id(&self) -> DatabaseId {
3886        self.id
3887    }
3888
3889    fn has_schemas(&self) -> bool {
3890        !self.schemas_by_name.is_empty()
3891    }
3892
3893    fn schema_ids(&self) -> &BTreeMap<String, SchemaId> {
3894        &self.schemas_by_name
3895    }
3896
3897    // `as` is ok to use to cast to a trait object.
3898    #[allow(clippy::as_conversions)]
3899    fn schemas(&self) -> Vec<&dyn CatalogSchema> {
3900        self.schemas_by_id
3901            .values()
3902            .map(|schema| schema as &dyn CatalogSchema)
3903            .collect()
3904    }
3905
3906    fn owner_id(&self) -> RoleId {
3907        self.owner_id
3908    }
3909
3910    fn privileges(&self) -> &PrivilegeMap {
3911        &self.privileges
3912    }
3913}
3914
3915impl mz_sql::catalog::CatalogSchema for Schema {
3916    fn database(&self) -> &ResolvedDatabaseSpecifier {
3917        &self.name.database
3918    }
3919
3920    fn name(&self) -> &QualifiedSchemaName {
3921        &self.name
3922    }
3923
3924    fn id(&self) -> &SchemaSpecifier {
3925        &self.id
3926    }
3927
3928    fn has_items(&self) -> bool {
3929        // A schema holds items, types, and functions in separate maps (see
3930        // `item_ids`). All three keep the schema non-empty, so e.g. DROP SCHEMA
3931        // without CASCADE must be rejected when only a type or function remains.
3932        !self.items.is_empty() || !self.types.is_empty() || !self.functions.is_empty()
3933    }
3934
3935    fn item_ids(&self) -> Box<dyn Iterator<Item = CatalogItemId> + '_> {
3936        Box::new(
3937            self.items
3938                .values()
3939                .chain(self.functions.values())
3940                .chain(self.types.values())
3941                .copied(),
3942        )
3943    }
3944
3945    fn owner_id(&self) -> RoleId {
3946        self.owner_id
3947    }
3948
3949    fn privileges(&self) -> &PrivilegeMap {
3950        &self.privileges
3951    }
3952}
3953
3954impl mz_sql::catalog::CatalogRole for Role {
3955    fn name(&self) -> &str {
3956        &self.name
3957    }
3958
3959    fn id(&self) -> RoleId {
3960        self.id
3961    }
3962
3963    fn membership(&self) -> &BTreeMap<RoleId, RoleId> {
3964        &self.membership.map
3965    }
3966
3967    fn attributes(&self) -> &RoleAttributes {
3968        &self.attributes
3969    }
3970
3971    fn vars(&self) -> &BTreeMap<String, OwnedVarInput> {
3972        &self.vars.map
3973    }
3974}
3975
3976impl mz_sql::catalog::CatalogNetworkPolicy for NetworkPolicy {
3977    fn name(&self) -> &str {
3978        &self.name
3979    }
3980
3981    fn id(&self) -> NetworkPolicyId {
3982        self.id
3983    }
3984
3985    fn owner_id(&self) -> RoleId {
3986        self.owner_id
3987    }
3988
3989    fn privileges(&self) -> &PrivilegeMap {
3990        &self.privileges
3991    }
3992}
3993
3994impl mz_sql::catalog::CatalogCluster<'_> for Cluster {
3995    fn name(&self) -> &str {
3996        &self.name
3997    }
3998
3999    fn id(&self) -> ClusterId {
4000        self.id
4001    }
4002
4003    fn bound_objects(&self) -> &BTreeSet<CatalogItemId> {
4004        &self.bound_objects
4005    }
4006
4007    fn replica_ids(&self) -> &BTreeMap<String, ReplicaId> {
4008        &self.replica_id_by_name_
4009    }
4010
4011    // `as` is ok to use to cast to a trait object.
4012    #[allow(clippy::as_conversions)]
4013    fn replicas(&self) -> Vec<&dyn CatalogClusterReplica<'_>> {
4014        self.replicas()
4015            .map(|replica| replica as &dyn CatalogClusterReplica)
4016            .collect()
4017    }
4018
4019    fn replica(&self, id: ReplicaId) -> &dyn CatalogClusterReplica<'_> {
4020        self.replica(id).expect("catalog out of sync")
4021    }
4022
4023    fn owner_id(&self) -> RoleId {
4024        self.owner_id
4025    }
4026
4027    fn privileges(&self) -> &PrivilegeMap {
4028        &self.privileges
4029    }
4030
4031    fn is_managed(&self) -> bool {
4032        self.is_managed()
4033    }
4034
4035    fn managed_size(&self) -> Option<&str> {
4036        match &self.config.variant {
4037            ClusterVariant::Managed(ClusterVariantManaged { size, .. }) => Some(size),
4038            ClusterVariant::Unmanaged => None,
4039        }
4040    }
4041
4042    fn schedule(&self) -> Option<&ClusterSchedule> {
4043        match &self.config.variant {
4044            ClusterVariant::Managed(ClusterVariantManaged { schedule, .. }) => Some(schedule),
4045            ClusterVariant::Unmanaged => None,
4046        }
4047    }
4048
4049    fn replication_factor(&self) -> Option<u32> {
4050        match &self.config.variant {
4051            ClusterVariant::Managed(ClusterVariantManaged {
4052                replication_factor, ..
4053            }) => Some(*replication_factor),
4054            ClusterVariant::Unmanaged => None,
4055        }
4056    }
4057
4058    fn auto_scaling_strategy(&self) -> Option<&AutoScalingStrategy> {
4059        match &self.config.variant {
4060            ClusterVariant::Managed(ClusterVariantManaged {
4061                auto_scaling_strategy,
4062                ..
4063            }) => auto_scaling_strategy.as_ref(),
4064            ClusterVariant::Unmanaged => None,
4065        }
4066    }
4067    fn try_to_plan(&self) -> Result<CreateClusterPlan, PlanError> {
4068        self.try_to_plan()
4069    }
4070}
4071
4072impl mz_sql::catalog::CatalogClusterReplica<'_> for ClusterReplica {
4073    fn name(&self) -> &str {
4074        &self.name
4075    }
4076
4077    fn cluster_id(&self) -> ClusterId {
4078        self.cluster_id
4079    }
4080
4081    fn replica_id(&self) -> ReplicaId {
4082        self.replica_id
4083    }
4084
4085    fn owner_id(&self) -> RoleId {
4086        self.owner_id
4087    }
4088
4089    fn internal(&self) -> bool {
4090        self.config.location.internal()
4091    }
4092}
4093
4094impl mz_sql::catalog::CatalogItem for CatalogEntry {
4095    fn name(&self) -> &QualifiedItemName {
4096        self.name()
4097    }
4098
4099    fn id(&self) -> CatalogItemId {
4100        self.id()
4101    }
4102
4103    fn global_ids(&self) -> Box<dyn Iterator<Item = GlobalId> + '_> {
4104        Box::new(self.global_ids())
4105    }
4106
4107    fn oid(&self) -> u32 {
4108        self.oid()
4109    }
4110
4111    fn func(&self) -> Result<&'static mz_sql::func::Func, SqlCatalogError> {
4112        self.func()
4113    }
4114
4115    fn source_desc(&self) -> Result<Option<&SourceDesc<ReferencedConnection>>, SqlCatalogError> {
4116        self.source_desc()
4117    }
4118
4119    fn connection(
4120        &self,
4121    ) -> Result<mz_storage_types::connections::Connection<ReferencedConnection>, SqlCatalogError>
4122    {
4123        Ok(self.connection()?.details.to_connection())
4124    }
4125
4126    fn create_sql(&self) -> &str {
4127        match self.item() {
4128            CatalogItem::Table(Table { create_sql, .. }) => {
4129                create_sql.as_deref().unwrap_or("<builtin>")
4130            }
4131            CatalogItem::Source(Source { create_sql, .. }) => {
4132                create_sql.as_deref().unwrap_or("<builtin>")
4133            }
4134            CatalogItem::Sink(Sink { create_sql, .. }) => create_sql,
4135            CatalogItem::View(View { create_sql, .. }) => create_sql,
4136            CatalogItem::MaterializedView(MaterializedView { create_sql, .. }) => create_sql,
4137            CatalogItem::Index(Index { create_sql, .. }) => create_sql,
4138            CatalogItem::Type(Type { create_sql, .. }) => {
4139                create_sql.as_deref().unwrap_or("<builtin>")
4140            }
4141            CatalogItem::Secret(Secret { create_sql, .. }) => create_sql,
4142            CatalogItem::Connection(Connection { create_sql, .. }) => create_sql,
4143            CatalogItem::MetricSink(MetricSink { create_sql, .. }) => create_sql,
4144            CatalogItem::Func(_) => "<builtin>",
4145            CatalogItem::Log(_) => "<builtin>",
4146        }
4147    }
4148
4149    fn item_type(&self) -> SqlCatalogItemType {
4150        self.item().typ()
4151    }
4152
4153    fn index_details(&self) -> Option<(&[MirScalarExpr], GlobalId)> {
4154        if let CatalogItem::Index(Index { keys, on, .. }) = self.item() {
4155            Some((keys, *on))
4156        } else {
4157            None
4158        }
4159    }
4160
4161    fn writable_table_details(&self) -> Option<&[Expr<Aug>]> {
4162        if let CatalogItem::Table(Table {
4163            data_source: TableDataSource::TableWrites { defaults },
4164            ..
4165        }) = self.item()
4166        {
4167            Some(defaults.as_slice())
4168        } else {
4169            None
4170        }
4171    }
4172
4173    fn replacement_target(&self) -> Option<CatalogItemId> {
4174        if let CatalogItem::MaterializedView(mv) = self.item() {
4175            mv.replacement_target
4176        } else {
4177            None
4178        }
4179    }
4180
4181    fn type_details(&self) -> Option<&CatalogTypeDetails<IdReference>> {
4182        if let CatalogItem::Type(Type { details, .. }) = self.item() {
4183            Some(details)
4184        } else {
4185            None
4186        }
4187    }
4188
4189    fn references(&self) -> &ResolvedIds {
4190        self.references()
4191    }
4192
4193    fn uses(&self) -> BTreeSet<CatalogItemId> {
4194        self.uses()
4195    }
4196
4197    fn referenced_by(&self) -> &[CatalogItemId] {
4198        self.referenced_by()
4199    }
4200
4201    fn used_by(&self) -> &[CatalogItemId] {
4202        self.used_by()
4203    }
4204
4205    fn subsource_details(
4206        &self,
4207    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)> {
4208        self.subsource_details()
4209    }
4210
4211    fn source_export_details(
4212        &self,
4213    ) -> Option<(
4214        CatalogItemId,
4215        &UnresolvedItemName,
4216        &SourceExportDetails,
4217        &SourceExportDataConfig<ReferencedConnection>,
4218    )> {
4219        self.source_export_details()
4220    }
4221
4222    fn is_progress_source(&self) -> bool {
4223        self.is_progress_source()
4224    }
4225
4226    fn progress_id(&self) -> Option<CatalogItemId> {
4227        self.progress_id()
4228    }
4229
4230    fn owner_id(&self) -> RoleId {
4231        self.owner_id
4232    }
4233
4234    fn privileges(&self) -> &PrivilegeMap {
4235        &self.privileges
4236    }
4237
4238    fn cluster_id(&self) -> Option<ClusterId> {
4239        self.item().cluster_id()
4240    }
4241
4242    fn at_version(
4243        &self,
4244        version: RelationVersionSelector,
4245    ) -> Box<dyn mz_sql::catalog::CatalogCollectionItem> {
4246        Box::new(CatalogCollectionEntry {
4247            entry: self.clone(),
4248            version,
4249        })
4250    }
4251
4252    fn latest_version(&self) -> Option<RelationVersion> {
4253        self.table().map(|t| t.desc.latest_version())
4254    }
4255}
4256
4257/// A single update to the catalog state.
4258#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4259pub struct StateUpdate {
4260    pub kind: StateUpdateKind,
4261    pub ts: Timestamp,
4262    pub diff: StateDiff,
4263}
4264
4265/// The contents of a single state update.
4266///
4267/// Variants are listed in dependency order.
4268#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4269pub enum StateUpdateKind {
4270    Role(durable::objects::Role),
4271    RoleAuth(durable::objects::RoleAuth),
4272    Database(durable::objects::Database),
4273    Schema(durable::objects::Schema),
4274    DefaultPrivilege(durable::objects::DefaultPrivilege),
4275    SystemPrivilege(MzAclItem),
4276    SystemConfiguration(durable::objects::SystemConfiguration),
4277    Cluster(durable::objects::Cluster),
4278    ClusterSystemConfiguration(durable::objects::ClusterSystemConfiguration),
4279    NetworkPolicy(durable::objects::NetworkPolicy),
4280    IntrospectionSourceIndex(durable::objects::IntrospectionSourceIndex),
4281    ClusterReplica(durable::objects::ClusterReplica),
4282    ReplicaSystemConfiguration(durable::objects::ReplicaSystemConfiguration),
4283    SourceReferences(durable::objects::SourceReferences),
4284    SystemObjectMapping(durable::objects::SystemObjectMapping),
4285    Item(durable::objects::Item),
4286    Comment(durable::objects::Comment),
4287    AuditLog(durable::objects::AuditLog),
4288    // Storage updates.
4289    StorageCollectionMetadata(durable::objects::StorageCollectionMetadata),
4290    UnfinalizedShard(durable::objects::UnfinalizedShard),
4291}
4292
4293/// Valid diffs for catalog state updates.
4294#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
4295pub enum StateDiff {
4296    Retraction,
4297    Addition,
4298}
4299
4300impl From<StateDiff> for Diff {
4301    fn from(diff: StateDiff) -> Self {
4302        match diff {
4303            StateDiff::Retraction => Diff::MINUS_ONE,
4304            StateDiff::Addition => Diff::ONE,
4305        }
4306    }
4307}
4308impl TryFrom<Diff> for StateDiff {
4309    type Error = String;
4310
4311    fn try_from(diff: Diff) -> Result<Self, Self::Error> {
4312        match diff {
4313            Diff::MINUS_ONE => Ok(Self::Retraction),
4314            Diff::ONE => Ok(Self::Addition),
4315            diff => Err(format!("invalid diff {diff}")),
4316        }
4317    }
4318}