Skip to main content

mz_catalog/memory/
objects.rs

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