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