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