Skip to main content

mz_sql/
catalog.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#![warn(missing_docs)]
11
12//! Catalog abstraction layer.
13
14use std::borrow::Cow;
15use std::collections::{BTreeMap, BTreeSet};
16use std::error::Error;
17use std::fmt;
18use std::fmt::{Debug, Display, Formatter};
19use std::num::NonZeroU32;
20use std::str::FromStr;
21use std::sync::LazyLock;
22use std::time::Instant;
23
24use chrono::{DateTime, Utc};
25use mz_auth::password::Password;
26use mz_build_info::BuildInfo;
27use mz_cloud_provider::{CloudProvider, InvalidCloudProviderError};
28use mz_controller_types::{ClusterId, ReplicaId};
29use mz_expr::MirScalarExpr;
30use mz_ore::now::{EpochMillis, NowFn};
31use mz_ore::str::StrExt;
32use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem, PrivilegeMap};
33use mz_repr::explain::ExprHumanizer;
34use mz_repr::network_policy_id::NetworkPolicyId;
35use mz_repr::role_id::RoleId;
36use mz_repr::{
37    CatalogItemId, ColumnName, GlobalId, RelationDesc, RelationVersion, RelationVersionSelector,
38};
39use mz_sql_parser::ast::{Expr, QualifiedReplica, UnresolvedItemName};
40use mz_storage_types::connections::inline::{ConnectionResolver, ReferencedConnection};
41use mz_storage_types::connections::{Connection, ConnectionContext};
42use mz_storage_types::sources::{SourceDesc, SourceExportDataConfig, SourceExportDetails};
43use proptest_derive::Arbitrary;
44use regex::Regex;
45use serde::{Deserialize, Serialize};
46use uuid::Uuid;
47
48use crate::func::Func;
49use crate::names::{
50    Aug, CommentObjectId, DatabaseId, FullItemName, FullSchemaName, ObjectId, PartialItemName,
51    QualifiedItemName, QualifiedSchemaName, ResolvedDatabaseSpecifier, ResolvedIds, SchemaId,
52    SchemaSpecifier, SystemObjectId,
53};
54use crate::plan::statement::StatementDesc;
55use crate::plan::statement::ddl::PlannedRoleAttributes;
56use crate::plan::{
57    AutoScalingStrategy, ClusterSchedule, CreateClusterPlan, PlanError, PlanNotice, query,
58};
59use crate::session::vars::{OwnedVarInput, SystemVars};
60
61/// A catalog keeps track of SQL objects and session state available to the
62/// planner.
63///
64/// The `sql` crate is agnostic to any particular catalog implementation. This
65/// trait describes the required interface.
66///
67/// The SQL standard mandates a catalog hierarchy of exactly three layers. A
68/// catalog contains databases, databases contain schemas, and schemas contain
69/// catalog items, like sources, sinks, view, and indexes.
70///
71/// There are two classes of operations provided by a catalog:
72///
73///   * Resolution operations, like [`resolve_item`]. These fill in missing name
74///     components based upon connection defaults, e.g., resolving the partial
75///     name `view42` to the fully-specified name `materialize.public.view42`.
76///
77///   * Lookup operations, like [`SessionCatalog::get_item`]. These retrieve
78///     metadata about a catalog entity based on a fully-specified name that is
79///     known to be valid (i.e., because the name was successfully resolved, or
80///     was constructed based on the output of a prior lookup operation). These
81///     functions panic if called with invalid input.
82///
83///   * Session management, such as managing variables' states and adding
84///     notices to the session.
85///
86/// [`get_databases`]: SessionCatalog::get_databases
87/// [`get_item`]: SessionCatalog::get_item
88/// [`resolve_item`]: SessionCatalog::resolve_item
89pub trait SessionCatalog: fmt::Debug + ExprHumanizer + Send + Sync + ConnectionResolver {
90    /// Returns the id of the role that is issuing the query.
91    fn active_role_id(&self) -> &RoleId;
92
93    /// Returns the database to use if one is not explicitly specified.
94    fn active_database_name(&self) -> Option<&str> {
95        self.active_database()
96            .map(|id| self.get_database(id))
97            .map(|db| db.name())
98    }
99
100    /// Returns the database to use if one is not explicitly specified.
101    fn active_database(&self) -> Option<&DatabaseId>;
102
103    /// Returns the cluster to use if one is not explicitly specified.
104    fn active_cluster(&self) -> &str;
105
106    /// Returns the resolved search paths for the current user. (Invalid search paths are skipped.)
107    fn search_path(&self) -> &[(ResolvedDatabaseSpecifier, SchemaSpecifier)];
108
109    /// Returns the descriptor of the named prepared statement on the session, or
110    /// None if the prepared statement does not exist.
111    fn get_prepared_statement_desc(&self, name: &str) -> Option<&StatementDesc>;
112
113    /// Retrieves a reference to the specified portal's descriptor.
114    ///
115    /// If there is no such portal, returns `None`.
116    fn get_portal_desc_unverified(&self, portal_name: &str) -> Option<&StatementDesc>;
117
118    /// Resolves the named database.
119    ///
120    /// If `database_name` exists in the catalog, it returns a reference to the
121    /// resolved database; otherwise it returns an error.
122    fn resolve_database(&self, database_name: &str) -> Result<&dyn CatalogDatabase, CatalogError>;
123
124    /// Gets a database by its ID.
125    ///
126    /// Panics if `id` does not specify a valid database.
127    fn get_database(&self, id: &DatabaseId) -> &dyn CatalogDatabase;
128
129    /// Gets all databases.
130    fn get_databases(&self) -> Vec<&dyn CatalogDatabase>;
131
132    /// Resolves a partially-specified schema name.
133    ///
134    /// If the schema exists in the catalog, it returns a reference to the
135    /// resolved schema; otherwise it returns an error.
136    fn resolve_schema(
137        &self,
138        database_name: Option<&str>,
139        schema_name: &str,
140    ) -> Result<&dyn CatalogSchema, CatalogError>;
141
142    /// Resolves a schema name within a specified database.
143    ///
144    /// If the schema exists in the database, it returns a reference to the
145    /// resolved schema; otherwise it returns an error.
146    fn resolve_schema_in_database(
147        &self,
148        database_spec: &ResolvedDatabaseSpecifier,
149        schema_name: &str,
150    ) -> Result<&dyn CatalogSchema, CatalogError>;
151
152    /// Gets a schema by its ID.
153    ///
154    /// Panics if `id` does not specify a valid schema.
155    fn get_schema(
156        &self,
157        database_spec: &ResolvedDatabaseSpecifier,
158        schema_spec: &SchemaSpecifier,
159    ) -> &dyn CatalogSchema;
160
161    /// Gets all schemas.
162    fn get_schemas(&self) -> Vec<&dyn CatalogSchema>;
163
164    /// Gets the mz_internal schema id.
165    fn get_mz_internal_schema_id(&self) -> SchemaId;
166
167    /// Gets the mz_unsafe schema id.
168    fn get_mz_unsafe_schema_id(&self) -> SchemaId;
169
170    /// Returns true if `schema` is an internal system schema, false otherwise
171    fn is_system_schema_specifier(&self, schema: SchemaSpecifier) -> bool;
172
173    /// Resolves the named role.
174    fn resolve_role(&self, role_name: &str) -> Result<&dyn CatalogRole, CatalogError>;
175
176    /// Resolves the named network policy.
177    fn resolve_network_policy(
178        &self,
179        network_policy_name: &str,
180    ) -> Result<&dyn CatalogNetworkPolicy, CatalogError>;
181
182    /// Gets a role by its ID.
183    fn try_get_role(&self, id: &RoleId) -> Option<&dyn CatalogRole>;
184
185    /// Gets a role by its ID.
186    ///
187    /// Panics if `id` does not specify a valid role.
188    fn get_role(&self, id: &RoleId) -> &dyn CatalogRole;
189
190    /// Gets all roles.
191    fn get_roles(&self) -> Vec<&dyn CatalogRole>;
192
193    /// Gets the id of the `mz_system` role.
194    fn mz_system_role_id(&self) -> RoleId;
195
196    /// Collects all role IDs that `id` is transitively a member of.
197    fn collect_role_membership(&self, id: &RoleId) -> BTreeSet<RoleId>;
198
199    /// Resolves the named cluster.
200    /// Gets a network_policy by its ID.
201    ///
202    /// Panics if `id` does not specify a valid role.
203    fn get_network_policy(&self, id: &NetworkPolicyId) -> &dyn CatalogNetworkPolicy;
204
205    /// Gets all roles.
206    fn get_network_policies(&self) -> Vec<&dyn CatalogNetworkPolicy>;
207
208    ///
209    /// If the provided name is `None`, resolves the currently active cluster.
210    fn resolve_cluster<'a, 'b>(
211        &'a self,
212        cluster_name: Option<&'b str>,
213    ) -> Result<&'a dyn CatalogCluster<'a>, CatalogError>;
214
215    /// Resolves the named cluster replica.
216    fn resolve_cluster_replica<'a, 'b>(
217        &'a self,
218        cluster_replica_name: &'b QualifiedReplica,
219    ) -> Result<&'a dyn CatalogClusterReplica<'a>, CatalogError>;
220
221    /// Resolves a partially-specified item name, that is NOT a function or
222    /// type. (For resolving functions or types, please use
223    /// [SessionCatalog::resolve_function] or [SessionCatalog::resolve_type].)
224    ///
225    /// If the partial name has a database component, it searches only the
226    /// specified database; otherwise, it searches the active database. If the
227    /// partial name has a schema component, it searches only the specified
228    /// schema; otherwise, it searches a default set of schemas within the
229    /// selected database. It returns an error if none of the searched schemas
230    /// contain an item whose name matches the item component of the partial
231    /// name.
232    ///
233    /// Note that it is not an error if the named item appears in more than one
234    /// of the search schemas. The catalog implementation must choose one.
235    fn resolve_item(&self, item_name: &PartialItemName) -> Result<&dyn CatalogItem, CatalogError>;
236
237    /// Performs the same operation as [`SessionCatalog::resolve_item`] but for
238    /// functions within the catalog.
239    fn resolve_function(
240        &self,
241        item_name: &PartialItemName,
242    ) -> Result<&dyn CatalogItem, CatalogError>;
243
244    /// Performs the same operation as [`SessionCatalog::resolve_item`] but for
245    /// types within the catalog.
246    fn resolve_type(&self, item_name: &PartialItemName) -> Result<&dyn CatalogItem, CatalogError>;
247
248    /// Resolves `name` to a type or item, preferring the type if both exist.
249    fn resolve_item_or_type(
250        &self,
251        name: &PartialItemName,
252    ) -> Result<&dyn CatalogItem, CatalogError> {
253        if let Ok(ty) = self.resolve_type(name) {
254            return Ok(ty);
255        }
256        self.resolve_item(name)
257    }
258
259    /// Gets a type named `name` from exactly one of the system schemas.
260    ///
261    /// # Panics
262    /// - If `name` is not an entry in any system schema
263    /// - If more than one system schema has an entry named `name`.
264    fn get_system_type(&self, name: &str) -> &dyn CatalogItem;
265
266    /// Gets an item by its ID.
267    fn try_get_item(&self, id: &CatalogItemId) -> Option<&dyn CatalogItem>;
268
269    /// Tries to get an item by a [`GlobalId`], returning `None` if the [`GlobalId`] does not
270    /// exist.
271    ///
272    /// Note: A single Catalog Item can have multiple [`GlobalId`]s associated with it.
273    fn try_get_item_by_global_id<'a>(
274        &'a self,
275        id: &GlobalId,
276    ) -> Option<Box<dyn CatalogCollectionItem + 'a>>;
277
278    /// Gets an item by its ID.
279    ///
280    /// Panics if `id` does not specify a valid item.
281    fn get_item(&self, id: &CatalogItemId) -> &dyn CatalogItem;
282
283    /// Gets an item by a [`GlobalId`].
284    ///
285    /// Panics if `id` does not specify a valid item.
286    ///
287    /// Note: A single Catalog Item can have multiple [`GlobalId`]s associated with it.
288    fn get_item_by_global_id<'a>(&'a self, id: &GlobalId) -> Box<dyn CatalogCollectionItem + 'a>;
289
290    /// Gets all items.
291    fn get_items(&self) -> Vec<&dyn CatalogItem>;
292
293    /// Looks up an item by its name.
294    fn get_item_by_name(&self, name: &QualifiedItemName) -> Option<&dyn CatalogItem>;
295
296    /// Looks up a type by its name.
297    fn get_type_by_name(&self, name: &QualifiedItemName) -> Option<&dyn CatalogItem>;
298
299    /// Gets a cluster by ID.
300    fn get_cluster(&self, id: ClusterId) -> &dyn CatalogCluster<'_>;
301
302    /// Gets all clusters.
303    fn get_clusters(&self) -> Vec<&dyn CatalogCluster<'_>>;
304
305    /// Gets a cluster replica by ID.
306    fn get_cluster_replica(
307        &self,
308        cluster_id: ClusterId,
309        replica_id: ReplicaId,
310    ) -> &dyn CatalogClusterReplica<'_>;
311
312    /// Gets all cluster replicas.
313    fn get_cluster_replicas(&self) -> Vec<&dyn CatalogClusterReplica<'_>>;
314
315    /// Gets all system privileges.
316    fn get_system_privileges(&self) -> &PrivilegeMap;
317
318    /// Gets all default privileges.
319    fn get_default_privileges(
320        &self,
321    ) -> Vec<(&DefaultPrivilegeObject, Vec<&DefaultPrivilegeAclItem>)>;
322
323    /// Finds a name like `name` that is not already in use.
324    ///
325    /// If `name` itself is available, it is returned unchanged.
326    fn find_available_name(&self, name: QualifiedItemName) -> QualifiedItemName;
327
328    /// Returns a fully qualified human readable name from fully qualified non-human readable name
329    fn resolve_full_name(&self, name: &QualifiedItemName) -> FullItemName;
330
331    /// Returns a fully qualified human readable schema name from fully qualified non-human
332    /// readable schema name
333    fn resolve_full_schema_name(&self, name: &QualifiedSchemaName) -> FullSchemaName;
334
335    /// Returns the [`CatalogItemId`] for from a [`GlobalId`].
336    fn resolve_item_id(&self, global_id: &GlobalId) -> CatalogItemId;
337
338    /// Returns the [`GlobalId`] for the specificed Catalog Item, at the specified version.
339    fn resolve_global_id(
340        &self,
341        item_id: &CatalogItemId,
342        version: RelationVersionSelector,
343    ) -> GlobalId;
344
345    /// Returns the configuration of the catalog.
346    fn config(&self) -> &CatalogConfig;
347
348    /// Returns the number of milliseconds since the system epoch. For normal use
349    /// this means the Unix epoch. This can safely be mocked in tests and start
350    /// at 0.
351    fn now(&self) -> EpochMillis;
352
353    /// Returns the set of supported AWS PrivateLink availability zone ids.
354    fn aws_privatelink_availability_zones(&self) -> Option<BTreeSet<String>>;
355
356    /// Returns true if the session has `restrict_to_user_objects` active.
357    ///
358    /// Defaults to false so that non-session catalog implementations (e.g. those used during
359    /// catalog rehydration) are unaffected.
360    fn restrict_to_user_objects(&self) -> bool {
361        false
362    }
363
364    /// Returns system vars
365    fn system_vars(&self) -> &SystemVars;
366
367    /// Returns mutable system vars
368    ///
369    /// Clients should use this this method carefully, as changes to the backing
370    /// state here are not guarateed to be persisted. The motivating use case
371    /// for this method was ensuring that features are temporary turned on so
372    /// catalog rehydration does not break due to unsupported SQL syntax.
373    fn system_vars_mut(&mut self) -> &mut SystemVars;
374
375    /// Returns the [`RoleId`] of the owner of an object by its ID.
376    fn get_owner_id(&self, id: &ObjectId) -> Option<RoleId>;
377
378    /// Returns the [`PrivilegeMap`] of the object.
379    fn get_privileges(&self, id: &SystemObjectId) -> Option<&PrivilegeMap>;
380
381    /// Returns all the IDs of all objects that depend on `ids`, including `ids` themselves.
382    ///
383    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
384    /// earlier in the list than the roots. This is particularly userful for the order to drop
385    /// objects.
386    fn object_dependents(&self, ids: &Vec<ObjectId>) -> Vec<ObjectId>;
387
388    /// Returns all the IDs of all objects that depend on `id`, including `id` themselves.
389    ///
390    /// The order is guaranteed to be in reverse dependency order, i.e. the leafs will appear
391    /// earlier in the list than `id`. This is particularly userful for the order to drop
392    /// objects.
393    fn item_dependents(&self, id: CatalogItemId) -> Vec<ObjectId>;
394
395    /// Returns all possible privileges associated with an object type.
396    fn all_object_privileges(&self, object_type: SystemObjectType) -> AclMode;
397
398    /// Returns the object type of `object_id`.
399    fn get_object_type(&self, object_id: &ObjectId) -> ObjectType;
400
401    /// Returns the system object type of `id`.
402    fn get_system_object_type(&self, id: &SystemObjectId) -> SystemObjectType;
403
404    /// Returns the minimal qualification required to unambiguously specify
405    /// `qualified_name`.
406    fn minimal_qualification(&self, qualified_name: &QualifiedItemName) -> PartialItemName;
407
408    /// Adds a [`PlanNotice`] that will be displayed to the user if the plan
409    /// successfully executes.
410    fn add_notice(&self, notice: PlanNotice);
411
412    /// Returns the associated comments for the given `id`
413    fn get_item_comments(&self, id: &CatalogItemId) -> Option<&BTreeMap<Option<usize>, String>>;
414
415    /// Reports whether the specified cluster size is a modern "cc" size rather
416    /// than a legacy T-shirt size.
417    fn is_cluster_size_cc(&self, size: &str) -> bool;
418}
419
420/// Configuration associated with a catalog.
421#[derive(Debug, Clone)]
422pub struct CatalogConfig {
423    /// Returns the time at which the catalog booted.
424    pub start_time: DateTime<Utc>,
425    /// Returns the instant at which the catalog booted.
426    pub start_instant: Instant,
427    /// A random integer associated with this instance of the catalog.
428    ///
429    /// NOTE(benesch): this is only necessary for producing unique Kafka sink
430    /// topics. Perhaps we can remove this when database-issues#977 is complete.
431    pub nonce: u64,
432    /// A persistent ID associated with the environment.
433    pub environment_id: EnvironmentId,
434    /// A transient UUID associated with this process.
435    pub session_id: Uuid,
436    /// Information about this build of Materialize.
437    pub build_info: &'static BuildInfo,
438    /// Function that returns a wall clock now time; can safely be mocked to return
439    /// 0.
440    pub now: NowFn,
441    /// Context for source and sink connections.
442    pub connection_context: ConnectionContext,
443    /// Helm chart version
444    pub helm_chart_version: Option<String>,
445}
446
447/// A database in a [`SessionCatalog`].
448pub trait CatalogDatabase {
449    /// Returns a fully-specified name of the database.
450    fn name(&self) -> &str;
451
452    /// Returns a stable ID for the database.
453    fn id(&self) -> DatabaseId;
454
455    /// Returns whether the database contains schemas.
456    fn has_schemas(&self) -> bool;
457
458    /// Returns the schemas of the database as a map from schema name to
459    /// schema ID.
460    fn schema_ids(&self) -> &BTreeMap<String, SchemaId>;
461
462    /// Returns the schemas of the database.
463    fn schemas(&self) -> Vec<&dyn CatalogSchema>;
464
465    /// Returns the ID of the owning role.
466    fn owner_id(&self) -> RoleId;
467
468    /// Returns the privileges associated with the database.
469    fn privileges(&self) -> &PrivilegeMap;
470}
471
472/// A schema in a [`SessionCatalog`].
473pub trait CatalogSchema {
474    /// Returns a fully-specified id of the database
475    fn database(&self) -> &ResolvedDatabaseSpecifier;
476
477    /// Returns a fully-specified name of the schema.
478    fn name(&self) -> &QualifiedSchemaName;
479
480    /// Returns a stable ID for the schema.
481    fn id(&self) -> &SchemaSpecifier;
482
483    /// Lists the `CatalogItem`s for the schema.
484    fn has_items(&self) -> bool;
485
486    /// Returns the IDs of the items in the schema.
487    fn item_ids(&self) -> Box<dyn Iterator<Item = CatalogItemId> + '_>;
488
489    /// Returns the ID of the owning role.
490    fn owner_id(&self) -> RoleId;
491
492    /// Returns the privileges associated with the schema.
493    fn privileges(&self) -> &PrivilegeMap;
494}
495
496/// Parameters used to modify password
497#[derive(Debug, Clone, Eq, PartialEq, Arbitrary)]
498pub struct PasswordConfig {
499    /// The Password.
500    pub password: Password,
501    /// a non default iteration count for hashing the password.
502    pub scram_iterations: NonZeroU32,
503}
504
505/// A modification of a role password in the catalog
506#[derive(Debug, Clone, Eq, PartialEq, Arbitrary)]
507pub enum PasswordAction {
508    /// Set a new password.
509    Set(PasswordConfig),
510    /// Remove the existing password.
511    Clear,
512    /// Leave the existing password unchanged.
513    NoChange,
514}
515
516/// The authenticator that auto-provisioned a role on first login.
517#[derive(
518    Debug,
519    Copy,
520    Clone,
521    Eq,
522    PartialEq,
523    Ord,
524    PartialOrd,
525    Serialize,
526    Deserialize,
527    Arbitrary
528)]
529pub enum AutoProvisionSource {
530    /// Role was auto-provisioned by [`mz_auth::AuthenticatorKind::Oidc`].
531    Oidc,
532    /// Role was auto-provisioned by [`mz_auth::AuthenticatorKind::Frontegg`].
533    Frontegg,
534    /// Role was auto-provisioned by [`mz_auth::AuthenticatorKind::None`]
535    None,
536}
537
538/// A raw representation of attributes belonging to a [`CatalogRole`] that we might
539/// get as input from the user. This includes the password.
540/// This struct explicitly does not implement `Serialize` or `Deserialize` to avoid
541/// accidentally serializing passwords.
542#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Arbitrary)]
543pub struct RoleAttributesRaw {
544    /// Indicates whether the role has inheritance of privileges.
545    pub inherit: bool,
546    /// The raw password of the role. This is for self managed auth, not cloud.
547    pub password: Option<Password>,
548    /// Hash iterations used to securely store passwords. This is for self-managed auth
549    pub scram_iterations: Option<NonZeroU32>,
550    /// Whether or not this user is a superuser.
551    pub superuser: Option<bool>,
552    /// Whether this role is login
553    pub login: Option<bool>,
554    /// The authenticator that auto-provisioned this role, if any.
555    pub auto_provision_source: Option<AutoProvisionSource>,
556    // Force use of constructor.
557    _private: (),
558}
559
560/// Attributes belonging to a [`CatalogRole`].
561#[derive(
562    Debug,
563    Clone,
564    Eq,
565    Serialize,
566    Deserialize,
567    PartialEq,
568    Ord,
569    PartialOrd,
570    Arbitrary
571)]
572pub struct RoleAttributes {
573    /// Indicates whether the role has inheritance of privileges.
574    pub inherit: bool,
575    /// Whether or not this user is a superuser.
576    pub superuser: Option<bool>,
577    /// Whether this role is login
578    pub login: Option<bool>,
579    /// The authenticator that auto-provisioned this role, if any.
580    pub auto_provision_source: Option<AutoProvisionSource>,
581    // Force use of constructor.
582    _private: (),
583}
584
585impl RoleAttributesRaw {
586    /// Creates a new [`RoleAttributesRaw`] with default attributes.
587    pub const fn new() -> RoleAttributesRaw {
588        RoleAttributesRaw {
589            inherit: true,
590            password: None,
591            scram_iterations: None,
592            superuser: None,
593            login: None,
594            auto_provision_source: None,
595            _private: (),
596        }
597    }
598
599    /// Adds all attributes excluding password.
600    pub const fn with_all(mut self) -> RoleAttributesRaw {
601        self.inherit = true;
602        self.superuser = Some(true);
603        self.login = Some(true);
604        self
605    }
606}
607
608impl RoleAttributes {
609    /// Creates a new [`RoleAttributes`] with default attributes.
610    pub const fn new() -> RoleAttributes {
611        RoleAttributes {
612            inherit: true,
613            superuser: None,
614            login: None,
615            auto_provision_source: None,
616            _private: (),
617        }
618    }
619
620    /// Adds all attributes except password and auto_provision_source.
621    pub const fn with_all(mut self) -> RoleAttributes {
622        self.inherit = true;
623        self.superuser = Some(true);
624        self.login = Some(true);
625        self
626    }
627
628    /// Returns whether or not the role has inheritence of privileges.
629    pub const fn is_inherit(&self) -> bool {
630        self.inherit
631    }
632}
633
634impl From<RoleAttributesRaw> for RoleAttributes {
635    fn from(
636        RoleAttributesRaw {
637            inherit,
638            superuser,
639            login,
640            auto_provision_source,
641            ..
642        }: RoleAttributesRaw,
643    ) -> RoleAttributes {
644        RoleAttributes {
645            inherit,
646            superuser,
647            login,
648            auto_provision_source,
649            _private: (),
650        }
651    }
652}
653
654impl From<RoleAttributes> for RoleAttributesRaw {
655    fn from(
656        RoleAttributes {
657            inherit,
658            superuser,
659            login,
660            auto_provision_source,
661            ..
662        }: RoleAttributes,
663    ) -> RoleAttributesRaw {
664        RoleAttributesRaw {
665            inherit,
666            password: None,
667            scram_iterations: None,
668            superuser,
669            login,
670            auto_provision_source,
671            _private: (),
672        }
673    }
674}
675
676impl From<PlannedRoleAttributes> for RoleAttributesRaw {
677    fn from(
678        PlannedRoleAttributes {
679            inherit,
680            password,
681            scram_iterations,
682            superuser,
683            login,
684            ..
685        }: PlannedRoleAttributes,
686    ) -> RoleAttributesRaw {
687        let default_attributes = RoleAttributesRaw::new();
688        RoleAttributesRaw {
689            inherit: inherit.unwrap_or(default_attributes.inherit),
690            password,
691            scram_iterations,
692            superuser,
693            login,
694            auto_provision_source: None,
695            _private: (),
696        }
697    }
698}
699
700/// Default variable values for a [`CatalogRole`].
701#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
702pub struct RoleVars {
703    /// Map of variable names to their value.
704    pub map: BTreeMap<String, OwnedVarInput>,
705}
706
707/// A role in a [`SessionCatalog`].
708pub trait CatalogRole {
709    /// Returns a fully-specified name of the role.
710    fn name(&self) -> &str;
711
712    /// Returns a stable ID for the role.
713    fn id(&self) -> RoleId;
714
715    /// Returns all role IDs that this role is an immediate a member of, and the grantor of that
716    /// membership.
717    ///
718    /// Key is the role that some role is a member of, value is the grantor role ID.
719    fn membership(&self) -> &BTreeMap<RoleId, RoleId>;
720
721    /// Returns the attributes associated with this role.
722    fn attributes(&self) -> &RoleAttributes;
723
724    /// Returns all variables that this role has a default value stored for.
725    fn vars(&self) -> &BTreeMap<String, OwnedVarInput>;
726}
727
728/// A network policy in a [`SessionCatalog`].
729pub trait CatalogNetworkPolicy {
730    /// Returns a fully-specified name of the NetworkPolicy.
731    fn name(&self) -> &str;
732
733    /// Returns a stable ID for the NetworkPolicy.
734    fn id(&self) -> NetworkPolicyId;
735
736    /// Returns the ID of the owning NetworkPolicy.
737    fn owner_id(&self) -> RoleId;
738
739    /// Returns the privileges associated with the NetworkPolicy.
740    fn privileges(&self) -> &PrivilegeMap;
741}
742
743/// A cluster in a [`SessionCatalog`].
744pub trait CatalogCluster<'a> {
745    /// Returns a fully-specified name of the cluster.
746    fn name(&self) -> &str;
747
748    /// Returns a stable ID for the cluster.
749    fn id(&self) -> ClusterId;
750
751    /// Returns the objects that are bound to this cluster.
752    fn bound_objects(&self) -> &BTreeSet<CatalogItemId>;
753
754    /// Returns the replicas of the cluster as a map from replica name to
755    /// replica ID.
756    fn replica_ids(&self) -> &BTreeMap<String, ReplicaId>;
757
758    /// Returns the replicas of the cluster.
759    fn replicas(&self) -> Vec<&dyn CatalogClusterReplica<'_>>;
760
761    /// Returns the replica belonging to the cluster with replica ID `id`.
762    fn replica(&self, id: ReplicaId) -> &dyn CatalogClusterReplica<'_>;
763
764    /// Returns the ID of the owning role.
765    fn owner_id(&self) -> RoleId;
766
767    /// Returns the privileges associated with the cluster.
768    fn privileges(&self) -> &PrivilegeMap;
769
770    /// Returns true if this cluster is a managed cluster.
771    fn is_managed(&self) -> bool;
772
773    /// Returns the size of the cluster, if the cluster is a managed cluster.
774    fn managed_size(&self) -> Option<&str>;
775
776    /// Returns the schedule of the cluster, if the cluster is a managed cluster.
777    fn schedule(&self) -> Option<&ClusterSchedule>;
778
779    /// Returns the replication factor of the cluster, if the cluster is a managed cluster.
780    fn replication_factor(&self) -> Option<u32>;
781
782    /// Returns the user-configured autoscaling strategy of the cluster, if the
783    /// cluster is managed and has one set.
784    fn auto_scaling_strategy(&self) -> Option<&AutoScalingStrategy>;
785    /// Try to convert this cluster into a [`CreateClusterPlan`].
786    // TODO(jkosh44) Make this infallible and convert to `to_plan`.
787    fn try_to_plan(&self) -> Result<CreateClusterPlan, PlanError>;
788}
789
790/// A cluster replica in a [`SessionCatalog`]
791pub trait CatalogClusterReplica<'a>: Debug {
792    /// Returns the name of the cluster replica.
793    fn name(&self) -> &str;
794
795    /// Returns a stable ID for the cluster that the replica belongs to.
796    fn cluster_id(&self) -> ClusterId;
797
798    /// Returns a stable ID for the replica.
799    fn replica_id(&self) -> ReplicaId;
800
801    /// Returns the ID of the owning role.
802    fn owner_id(&self) -> RoleId;
803
804    /// Returns whether or not the replica is internal
805    fn internal(&self) -> bool;
806}
807
808/// An item in a [`SessionCatalog`].
809///
810/// Note that "item" has a very specific meaning in the context of a SQL
811/// catalog, and refers to the various entities that belong to a schema.
812pub trait CatalogItem {
813    /// Returns the fully qualified name of the catalog item.
814    fn name(&self) -> &QualifiedItemName;
815
816    /// Returns the [`CatalogItemId`] for the item.
817    fn id(&self) -> CatalogItemId;
818
819    /// Returns the [`GlobalId`]s associated with this item.
820    fn global_ids(&self) -> Box<dyn Iterator<Item = GlobalId> + '_>;
821
822    /// Returns the catalog item's OID.
823    fn oid(&self) -> u32;
824
825    /// Returns the resolved function.
826    ///
827    /// If the catalog item is not of a type that produces functions (i.e.,
828    /// anything other than a function), it returns an error.
829    fn func(&self) -> Result<&'static Func, CatalogError>;
830
831    /// Returns the resolved source connection.
832    ///
833    /// If the catalog item is not of a type that contains a `SourceDesc`
834    /// (i.e., anything other than sources), it returns an error.
835    fn source_desc(&self) -> Result<Option<&SourceDesc<ReferencedConnection>>, CatalogError>;
836
837    /// Returns the resolved connection.
838    ///
839    /// If the catalog item is not a connection, it returns an error.
840    fn connection(&self) -> Result<Connection<ReferencedConnection>, CatalogError>;
841
842    /// Returns the type of the catalog item.
843    fn item_type(&self) -> CatalogItemType;
844
845    /// A normalized SQL statement that describes how to create the catalog
846    /// item.
847    fn create_sql(&self) -> &str;
848
849    /// Returns the IDs of the catalog items upon which this catalog item
850    /// directly references.
851    fn references(&self) -> &ResolvedIds;
852
853    /// Returns the IDs of the catalog items upon which this catalog item
854    /// depends.
855    fn uses(&self) -> BTreeSet<CatalogItemId>;
856
857    /// Returns the IDs of the catalog items that directly reference this catalog item.
858    fn referenced_by(&self) -> &[CatalogItemId];
859
860    /// Returns the IDs of the catalog items that depend upon this catalog item.
861    fn used_by(&self) -> &[CatalogItemId];
862
863    /// Reports whether this catalog entry is a subsource and, if it is, the
864    /// ingestion it is an export of, as well as the item it exports.
865    fn subsource_details(
866        &self,
867    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)>;
868
869    /// Reports whether this catalog entry is a source export and, if it is, the
870    /// ingestion it is an export of, as well as the item it exports.
871    fn source_export_details(
872        &self,
873    ) -> Option<(
874        CatalogItemId,
875        &UnresolvedItemName,
876        &SourceExportDetails,
877        &SourceExportDataConfig<ReferencedConnection>,
878    )>;
879
880    /// Reports whether this catalog item is a progress source.
881    fn is_progress_source(&self) -> bool;
882
883    /// If this catalog item is a source, it return the IDs of its progress collection.
884    fn progress_id(&self) -> Option<CatalogItemId>;
885
886    /// Returns the index details associated with the catalog item, if the
887    /// catalog item is an index.
888    fn index_details(&self) -> Option<(&[MirScalarExpr], GlobalId)>;
889
890    /// Returns the column defaults associated with the catalog item, if the
891    /// catalog item is a table that accepts writes.
892    fn writable_table_details(&self) -> Option<&[Expr<Aug>]>;
893
894    /// The item this catalog item replaces, if any.
895    fn replacement_target(&self) -> Option<CatalogItemId>;
896
897    /// Returns the type information associated with the catalog item, if the
898    /// catalog item is a type.
899    fn type_details(&self) -> Option<&CatalogTypeDetails<IdReference>>;
900
901    /// Returns the ID of the owning role.
902    fn owner_id(&self) -> RoleId;
903
904    /// Returns the privileges associated with the item.
905    fn privileges(&self) -> &PrivilegeMap;
906
907    /// Returns the cluster the item belongs to.
908    fn cluster_id(&self) -> Option<ClusterId>;
909
910    /// Returns the [`CatalogCollectionItem`] for a specific version of this
911    /// [`CatalogItem`].
912    fn at_version(&self, version: RelationVersionSelector) -> Box<dyn CatalogCollectionItem>;
913
914    /// The latest version of this item, if it's version-able.
915    fn latest_version(&self) -> Option<RelationVersion>;
916}
917
918/// An item in a [`SessionCatalog`] and the specific "collection"/pTVC that it
919/// refers to.
920pub trait CatalogCollectionItem: CatalogItem + Send + Sync {
921    /// Returns a description of the result set produced by the catalog item.
922    ///
923    /// If the catalog item is not of a type that produces data (e.g., a sink or
924    /// an index), it returns `None`.
925    fn relation_desc(&self) -> Option<Cow<'_, RelationDesc>>;
926
927    /// The [`GlobalId`] for this item.
928    fn global_id(&self) -> GlobalId;
929}
930
931/// The type of a [`CatalogItem`].
932#[derive(
933    Debug,
934    Deserialize,
935    Clone,
936    Copy,
937    Eq,
938    Hash,
939    Ord,
940    PartialEq,
941    PartialOrd,
942    Serialize
943)]
944pub enum CatalogItemType {
945    /// A table.
946    Table,
947    /// A source.
948    Source,
949    /// A sink.
950    Sink,
951    /// A view.
952    View,
953    /// A materialized view.
954    MaterializedView,
955    /// An index.
956    Index,
957    /// A type.
958    Type,
959    /// A func.
960    Func,
961    /// A secret.
962    Secret,
963    /// A connection.
964    Connection,
965}
966
967impl CatalogItemType {
968    /// Reports whether the given type of item conflicts with items of type
969    /// `CatalogItemType::Type`.
970    ///
971    /// In PostgreSQL, even though types live in a separate namespace from other
972    /// schema objects, creating a table, view, or materialized view creates a
973    /// type named after that relation. This prevents creating a type with the
974    /// same name as a relational object, even though types and relational
975    /// objects live in separate namespaces. (Indexes are even weirder; while
976    /// they don't get a type with the same name, they get an entry in
977    /// `pg_class` that prevents *record* types of the same name as the index,
978    /// but not other types of types, like enums.)
979    ///
980    /// We don't presently construct types that mirror relational objects,
981    /// though we likely will need to in the future for full PostgreSQL
982    /// compatibility (see database-issues#7142). For now, we use this method to
983    /// prevent creating types and relational objects that have the same name, so
984    /// that it is a backwards compatible change in the future to introduce a
985    /// type named after each relational object in the system.
986    pub fn conflicts_with_type(&self) -> bool {
987        match self {
988            CatalogItemType::Table => true,
989            CatalogItemType::Source => true,
990            CatalogItemType::View => true,
991            CatalogItemType::MaterializedView => true,
992            CatalogItemType::Index => true,
993            CatalogItemType::Type => true,
994            CatalogItemType::Sink => false,
995            CatalogItemType::Func => false,
996            CatalogItemType::Secret => false,
997            CatalogItemType::Connection => false,
998        }
999    }
1000}
1001
1002impl fmt::Display for CatalogItemType {
1003    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1004        match self {
1005            CatalogItemType::Table => f.write_str("table"),
1006            CatalogItemType::Source => f.write_str("source"),
1007            CatalogItemType::Sink => f.write_str("sink"),
1008            CatalogItemType::View => f.write_str("view"),
1009            CatalogItemType::MaterializedView => f.write_str("materialized view"),
1010            CatalogItemType::Index => f.write_str("index"),
1011            CatalogItemType::Type => f.write_str("type"),
1012            CatalogItemType::Func => f.write_str("func"),
1013            CatalogItemType::Secret => f.write_str("secret"),
1014            CatalogItemType::Connection => f.write_str("connection"),
1015        }
1016    }
1017}
1018
1019impl From<CatalogItemType> for ObjectType {
1020    fn from(value: CatalogItemType) -> Self {
1021        match value {
1022            CatalogItemType::Table => ObjectType::Table,
1023            CatalogItemType::Source => ObjectType::Source,
1024            CatalogItemType::Sink => ObjectType::Sink,
1025            CatalogItemType::View => ObjectType::View,
1026            CatalogItemType::MaterializedView => ObjectType::MaterializedView,
1027            CatalogItemType::Index => ObjectType::Index,
1028            CatalogItemType::Type => ObjectType::Type,
1029            CatalogItemType::Func => ObjectType::Func,
1030            CatalogItemType::Secret => ObjectType::Secret,
1031            CatalogItemType::Connection => ObjectType::Connection,
1032        }
1033    }
1034}
1035
1036impl From<CatalogItemType> for mz_audit_log::ObjectType {
1037    fn from(value: CatalogItemType) -> Self {
1038        match value {
1039            CatalogItemType::Table => mz_audit_log::ObjectType::Table,
1040            CatalogItemType::Source => mz_audit_log::ObjectType::Source,
1041            CatalogItemType::View => mz_audit_log::ObjectType::View,
1042            CatalogItemType::MaterializedView => mz_audit_log::ObjectType::MaterializedView,
1043            CatalogItemType::Index => mz_audit_log::ObjectType::Index,
1044            CatalogItemType::Type => mz_audit_log::ObjectType::Type,
1045            CatalogItemType::Sink => mz_audit_log::ObjectType::Sink,
1046            CatalogItemType::Func => mz_audit_log::ObjectType::Func,
1047            CatalogItemType::Secret => mz_audit_log::ObjectType::Secret,
1048            CatalogItemType::Connection => mz_audit_log::ObjectType::Connection,
1049        }
1050    }
1051}
1052
1053/// Details about a type in the catalog.
1054#[derive(Clone, Debug, Eq, PartialEq)]
1055pub struct CatalogTypeDetails<T: TypeReference> {
1056    /// The ID of the type with this type as the array element, if available.
1057    pub array_id: Option<CatalogItemId>,
1058    /// The description of this type.
1059    pub typ: CatalogType<T>,
1060    /// Additional metadata about the type in PostgreSQL, if relevant.
1061    pub pg_metadata: Option<CatalogTypePgMetadata>,
1062}
1063
1064/// Additional PostgreSQL metadata about a type.
1065#[derive(Clone, Debug, Eq, PartialEq)]
1066pub struct CatalogTypePgMetadata {
1067    /// The OID of the `typinput` function in PostgreSQL.
1068    pub typinput_oid: u32,
1069    /// The OID of the `typreceive` function in PostgreSQL.
1070    pub typreceive_oid: u32,
1071}
1072
1073/// Represents a reference to type in the catalog
1074pub trait TypeReference {
1075    /// The actual type used to reference a `CatalogType`
1076    type Reference: Clone + Debug + Eq + PartialEq;
1077}
1078
1079/// Reference to a type by it's name
1080#[derive(Clone, Debug, Eq, PartialEq)]
1081pub struct NameReference;
1082
1083impl TypeReference for NameReference {
1084    type Reference = &'static str;
1085}
1086
1087/// Reference to a type by it's global ID
1088#[derive(Clone, Debug, Eq, PartialEq)]
1089pub struct IdReference;
1090
1091impl TypeReference for IdReference {
1092    type Reference = CatalogItemId;
1093}
1094
1095/// A type stored in the catalog.
1096///
1097/// The variants correspond one-to-one with [`mz_repr::SqlScalarType`], but with type
1098/// modifiers removed and with embedded types replaced with references to other
1099/// types in the catalog.
1100#[allow(missing_docs)]
1101#[derive(Clone, Debug, Eq, PartialEq)]
1102pub enum CatalogType<T: TypeReference> {
1103    AclItem,
1104    Array {
1105        element_reference: T::Reference,
1106    },
1107    Bool,
1108    Bytes,
1109    Char,
1110    Date,
1111    Float32,
1112    Float64,
1113    Int16,
1114    Int32,
1115    Int64,
1116    UInt16,
1117    UInt32,
1118    UInt64,
1119    MzTimestamp,
1120    Interval,
1121    Jsonb,
1122    List {
1123        element_reference: T::Reference,
1124        element_modifiers: Vec<i64>,
1125    },
1126    Map {
1127        key_reference: T::Reference,
1128        key_modifiers: Vec<i64>,
1129        value_reference: T::Reference,
1130        value_modifiers: Vec<i64>,
1131    },
1132    Numeric,
1133    Oid,
1134    PgLegacyChar,
1135    PgLegacyName,
1136    Pseudo,
1137    Range {
1138        element_reference: T::Reference,
1139    },
1140    Record {
1141        fields: Vec<CatalogRecordField<T>>,
1142    },
1143    RegClass,
1144    RegProc,
1145    RegType,
1146    String,
1147    Time,
1148    Timestamp,
1149    TimestampTz,
1150    Uuid,
1151    VarChar,
1152    Int2Vector,
1153    MzAclItem,
1154}
1155
1156impl CatalogType<IdReference> {
1157    /// Returns the relation description for the type, if the type is a record
1158    /// type.
1159    pub fn desc(&self, catalog: &dyn SessionCatalog) -> Result<Option<RelationDesc>, PlanError> {
1160        match &self {
1161            CatalogType::Record { fields } => {
1162                let mut desc = RelationDesc::builder();
1163                // Share one budget across every field. Resolving each field with
1164                // a fresh budget would let a wide record materialize an
1165                // unbounded type tree here even though each field is individually
1166                // within the bound.
1167                let mut budget = query::TypeResolutionBudget::for_root(catalog);
1168                for f in fields {
1169                    let name = f.name.clone();
1170                    let ty = budget.resolve_child(catalog, f.type_reference, &f.type_modifiers)?;
1171                    // TODO: support plumbing `NOT NULL` constraints through
1172                    // `CREATE TYPE`.
1173                    let ty = ty.nullable(true);
1174                    desc = desc.with_column(name, ty);
1175                }
1176                Ok(Some(desc.finish()))
1177            }
1178            _ => Ok(None),
1179        }
1180    }
1181}
1182
1183/// A description of a field in a [`CatalogType::Record`].
1184#[derive(Clone, Debug, Eq, PartialEq)]
1185pub struct CatalogRecordField<T: TypeReference> {
1186    /// The name of the field.
1187    pub name: ColumnName,
1188    /// The ID of the type of the field.
1189    pub type_reference: T::Reference,
1190    /// Modifiers to apply to the type.
1191    pub type_modifiers: Vec<i64>,
1192}
1193
1194#[derive(Clone, Debug, Eq, PartialEq)]
1195/// Mirrored from [PostgreSQL's `typcategory`][typcategory].
1196///
1197/// Note that Materialize also uses a number of pseudotypes when planning, but
1198/// we have yet to need to integrate them with `TypeCategory`.
1199///
1200/// [typcategory]:
1201/// https://www.postgresql.org/docs/9.6/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE
1202pub enum TypeCategory {
1203    /// Array type.
1204    Array,
1205    /// Bit string type.
1206    BitString,
1207    /// Boolean type.
1208    Boolean,
1209    /// Composite type.
1210    Composite,
1211    /// Date/time type.
1212    DateTime,
1213    /// Enum type.
1214    Enum,
1215    /// Geometric type.
1216    Geometric,
1217    /// List type. Materialize specific.
1218    List,
1219    /// Network address type.
1220    NetworkAddress,
1221    /// Numeric type.
1222    Numeric,
1223    /// Pseudo type.
1224    Pseudo,
1225    /// Range type.
1226    Range,
1227    /// String type.
1228    String,
1229    /// Timestamp type.
1230    Timespan,
1231    /// User-defined type.
1232    UserDefined,
1233    /// Unknown type.
1234    Unknown,
1235}
1236
1237impl fmt::Display for TypeCategory {
1238    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1239        f.write_str(match self {
1240            TypeCategory::Array => "array",
1241            TypeCategory::BitString => "bit-string",
1242            TypeCategory::Boolean => "boolean",
1243            TypeCategory::Composite => "composite",
1244            TypeCategory::DateTime => "date-time",
1245            TypeCategory::Enum => "enum",
1246            TypeCategory::Geometric => "geometric",
1247            TypeCategory::List => "list",
1248            TypeCategory::NetworkAddress => "network-address",
1249            TypeCategory::Numeric => "numeric",
1250            TypeCategory::Pseudo => "pseudo",
1251            TypeCategory::Range => "range",
1252            TypeCategory::String => "string",
1253            TypeCategory::Timespan => "timespan",
1254            TypeCategory::UserDefined => "user-defined",
1255            TypeCategory::Unknown => "unknown",
1256        })
1257    }
1258}
1259
1260/// Identifies an environment.
1261///
1262/// Outside of tests, an environment ID can be constructed only from a string of
1263/// the following form:
1264///
1265/// ```text
1266/// <CLOUD PROVIDER>-<CLOUD PROVIDER REGION>-<ORGANIZATION ID>-<ORDINAL>
1267/// ```
1268///
1269/// The fields have the following formats:
1270///
1271/// * The cloud provider consists of one or more alphanumeric characters.
1272/// * The cloud provider region consists of one or more alphanumeric or hyphen
1273///   characters.
1274/// * The organization ID is a UUID in its canonical text format.
1275/// * The ordinal is a decimal number with between one and eight digits.
1276///
1277/// There is no way to construct an environment ID from parts, to ensure that
1278/// the `Display` representation is parseable according to the above rules.
1279// NOTE(benesch): ideally we'd have accepted the components of the environment
1280// ID using separate command-line arguments, or at least a string format that
1281// used a field separator that did not appear in the fields. Alas. We can't
1282// easily change it now, as it's used as the e.g. default sink progress topic.
1283#[derive(Debug, Clone, PartialEq)]
1284pub struct EnvironmentId {
1285    cloud_provider: CloudProvider,
1286    cloud_provider_region: String,
1287    organization_id: Uuid,
1288    ordinal: u64,
1289}
1290
1291impl EnvironmentId {
1292    /// Creates a dummy `EnvironmentId` for use in tests.
1293    pub fn for_tests() -> EnvironmentId {
1294        EnvironmentId {
1295            cloud_provider: CloudProvider::Local,
1296            cloud_provider_region: "az1".into(),
1297            organization_id: Uuid::new_v4(),
1298            ordinal: 0,
1299        }
1300    }
1301
1302    /// Returns the cloud provider associated with this environment ID.
1303    pub fn cloud_provider(&self) -> &CloudProvider {
1304        &self.cloud_provider
1305    }
1306
1307    /// Returns the cloud provider region associated with this environment ID.
1308    pub fn cloud_provider_region(&self) -> &str {
1309        &self.cloud_provider_region
1310    }
1311
1312    /// Returns the name of the region associted with this environment ID.
1313    ///
1314    /// A region is a combination of [`EnvironmentId::cloud_provider`] and
1315    /// [`EnvironmentId::cloud_provider_region`].
1316    pub fn region(&self) -> String {
1317        format!("{}/{}", self.cloud_provider, self.cloud_provider_region)
1318    }
1319
1320    /// Returns the organization ID associated with this environment ID.
1321    pub fn organization_id(&self) -> Uuid {
1322        self.organization_id
1323    }
1324
1325    /// Returns the ordinal associated with this environment ID.
1326    pub fn ordinal(&self) -> u64 {
1327        self.ordinal
1328    }
1329}
1330
1331// *Warning*: once the LaunchDarkly integration is live, our contexts will be
1332// populated using this key. Consequently, any changes to that trait
1333// implementation will also have to be reflected in the existing feature
1334// targeting config in LaunchDarkly, otherwise environments might receive
1335// different configs upon restart.
1336impl fmt::Display for EnvironmentId {
1337    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1338        write!(
1339            f,
1340            "{}-{}-{}-{}",
1341            self.cloud_provider, self.cloud_provider_region, self.organization_id, self.ordinal
1342        )
1343    }
1344}
1345
1346impl FromStr for EnvironmentId {
1347    type Err = InvalidEnvironmentIdError;
1348
1349    fn from_str(s: &str) -> Result<EnvironmentId, InvalidEnvironmentIdError> {
1350        static MATCHER: LazyLock<Regex> = LazyLock::new(|| {
1351            Regex::new(
1352                "^(?P<cloud_provider>[[:alnum:]]+)-\
1353                  (?P<cloud_provider_region>[[:alnum:]\\-]+)-\
1354                  (?P<organization_id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\
1355                  (?P<ordinal>\\d{1,8})$"
1356            ).unwrap()
1357        });
1358        let captures = MATCHER.captures(s).ok_or(InvalidEnvironmentIdError)?;
1359        Ok(EnvironmentId {
1360            cloud_provider: CloudProvider::from_str(&captures["cloud_provider"])?,
1361            cloud_provider_region: captures["cloud_provider_region"].into(),
1362            organization_id: captures["organization_id"]
1363                .parse()
1364                .map_err(|_| InvalidEnvironmentIdError)?,
1365            ordinal: captures["ordinal"]
1366                .parse()
1367                .map_err(|_| InvalidEnvironmentIdError)?,
1368        })
1369    }
1370}
1371
1372/// The error type for [`EnvironmentId::from_str`].
1373#[derive(Debug, Clone, PartialEq)]
1374pub struct InvalidEnvironmentIdError;
1375
1376impl fmt::Display for InvalidEnvironmentIdError {
1377    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1378        f.write_str("invalid environment ID")
1379    }
1380}
1381
1382impl Error for InvalidEnvironmentIdError {}
1383
1384impl From<InvalidCloudProviderError> for InvalidEnvironmentIdError {
1385    fn from(_: InvalidCloudProviderError) -> Self {
1386        InvalidEnvironmentIdError
1387    }
1388}
1389
1390/// An error returned by the catalog.
1391#[derive(Clone, Debug, Eq, PartialEq)]
1392pub enum CatalogError {
1393    /// Unknown database.
1394    UnknownDatabase(String),
1395    /// Database already exists.
1396    DatabaseAlreadyExists(String),
1397    /// Unknown schema.
1398    UnknownSchema(String),
1399    /// Schema already exists.
1400    SchemaAlreadyExists(String),
1401    /// Unknown role.
1402    UnknownRole(String),
1403    /// Role already exists.
1404    RoleAlreadyExists(String),
1405    /// Network Policy already exists.
1406    NetworkPolicyAlreadyExists(String),
1407    /// Unknown cluster.
1408    UnknownCluster(String),
1409    /// Unexpected builtin cluster.
1410    UnexpectedBuiltinCluster(String),
1411    /// Unexpected builtin cluster.
1412    UnexpectedBuiltinClusterType(String),
1413    /// Cluster already exists.
1414    ClusterAlreadyExists(String),
1415    /// Unknown cluster replica.
1416    UnknownClusterReplica(String),
1417    /// Unknown cluster replica size.
1418    UnknownClusterReplicaSize(String),
1419    /// Duplicate Replica. #[error("cannot create multiple replicas named '{0}' on cluster '{1}'")]
1420    DuplicateReplica(String, String),
1421    /// Unknown item.
1422    UnknownItem(String),
1423    /// Item already exists.
1424    ItemAlreadyExists(CatalogItemId, String),
1425    /// Unknown function.
1426    UnknownFunction {
1427        /// The identifier of the function we couldn't find
1428        name: String,
1429        /// A suggested alternative to the named function.
1430        alternative: Option<String>,
1431    },
1432    /// Unknown type.
1433    UnknownType {
1434        /// The identifier of the type we couldn't find.
1435        name: String,
1436    },
1437    /// Unknown connection.
1438    UnknownConnection(String),
1439    /// Unknown network policy.
1440    UnknownNetworkPolicy(String),
1441    /// Expected the catalog item to have the given type, but it did not.
1442    UnexpectedType {
1443        /// The item's name.
1444        name: String,
1445        /// The actual type of the item.
1446        actual_type: CatalogItemType,
1447        /// The expected type of the item.
1448        expected_type: CatalogItemType,
1449    },
1450    /// Ran out of unique IDs.
1451    IdExhaustion,
1452    /// Ran out of unique OIDs.
1453    OidExhaustion,
1454    /// Timeline already exists.
1455    TimelineAlreadyExists(String),
1456    /// Id Allocator already exists.
1457    IdAllocatorAlreadyExists(String),
1458    /// Config already exists.
1459    ConfigAlreadyExists(String),
1460    /// Builtin migrations failed.
1461    FailedBuiltinSchemaMigration(String),
1462    /// StorageCollectionMetadata already exists.
1463    StorageCollectionMetadataAlreadyExists(GlobalId),
1464}
1465
1466impl fmt::Display for CatalogError {
1467    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1468        match self {
1469            Self::UnknownDatabase(name) => write!(f, "unknown database '{}'", name),
1470            Self::DatabaseAlreadyExists(name) => write!(f, "database '{name}' already exists"),
1471            Self::UnknownFunction { name, .. } => write!(f, "function \"{}\" does not exist", name),
1472            Self::UnknownType { name, .. } => write!(f, "type \"{}\" does not exist", name),
1473            Self::UnknownConnection(name) => write!(f, "connection \"{}\" does not exist", name),
1474            Self::UnknownSchema(name) => write!(f, "unknown schema '{}'", name),
1475            Self::SchemaAlreadyExists(name) => write!(f, "schema '{name}' already exists"),
1476            Self::UnknownRole(name) => write!(f, "unknown role '{}'", name),
1477            Self::RoleAlreadyExists(name) => write!(f, "role '{name}' already exists"),
1478            Self::NetworkPolicyAlreadyExists(name) => {
1479                write!(f, "network policy '{name}' already exists")
1480            }
1481            Self::UnknownCluster(name) => write!(f, "unknown cluster '{}'", name),
1482            Self::UnknownNetworkPolicy(name) => write!(f, "unknown network policy '{}'", name),
1483            Self::UnexpectedBuiltinCluster(name) => {
1484                write!(f, "Unexpected builtin cluster '{}'", name)
1485            }
1486            Self::UnexpectedBuiltinClusterType(name) => {
1487                write!(f, "Unexpected builtin cluster type'{}'", name)
1488            }
1489            Self::ClusterAlreadyExists(name) => write!(f, "cluster '{name}' already exists"),
1490            Self::UnknownClusterReplica(name) => {
1491                write!(f, "unknown cluster replica '{}'", name)
1492            }
1493            Self::UnknownClusterReplicaSize(name) => {
1494                write!(f, "unknown cluster replica size '{}'", name)
1495            }
1496            Self::DuplicateReplica(replica_name, cluster_name) => write!(
1497                f,
1498                "cannot create multiple replicas named '{replica_name}' on cluster '{cluster_name}'"
1499            ),
1500            Self::UnknownItem(name) => write!(f, "unknown catalog item '{}'", name),
1501            Self::ItemAlreadyExists(_gid, name) => {
1502                write!(f, "catalog item '{name}' already exists")
1503            }
1504            Self::UnexpectedType {
1505                name,
1506                actual_type,
1507                expected_type,
1508            } => {
1509                write!(f, "\"{name}\" is a {actual_type} not a {expected_type}")
1510            }
1511            Self::IdExhaustion => write!(f, "id counter overflows i64"),
1512            Self::OidExhaustion => write!(f, "oid counter overflows u32"),
1513            Self::TimelineAlreadyExists(name) => write!(f, "timeline '{name}' already exists"),
1514            Self::IdAllocatorAlreadyExists(name) => {
1515                write!(f, "ID allocator '{name}' already exists")
1516            }
1517            Self::ConfigAlreadyExists(key) => write!(f, "config '{key}' already exists"),
1518            Self::FailedBuiltinSchemaMigration(objects) => {
1519                write!(f, "failed to migrate schema of builtin objects: {objects}")
1520            }
1521            Self::StorageCollectionMetadataAlreadyExists(key) => {
1522                write!(f, "storage metadata for '{key}' already exists")
1523            }
1524        }
1525    }
1526}
1527
1528impl CatalogError {
1529    /// Returns any applicable hints for [`CatalogError`].
1530    pub fn hint(&self) -> Option<String> {
1531        match self {
1532            CatalogError::UnknownFunction { alternative, .. } => {
1533                match alternative {
1534                    None => Some("No function matches the given name and argument types. You might need to add explicit type casts.".into()),
1535                    Some(alt) => Some(format!("Try using {alt}")),
1536                }
1537            }
1538            _ => None,
1539        }
1540    }
1541}
1542
1543impl Error for CatalogError {}
1544
1545// Enum variant docs would be useless here.
1546#[allow(missing_docs)]
1547#[derive(
1548    Debug,
1549    Clone,
1550    PartialOrd,
1551    Ord,
1552    PartialEq,
1553    Eq,
1554    Hash,
1555    Copy,
1556    Deserialize,
1557    Serialize
1558)]
1559/// The types of objects stored in the catalog.
1560pub enum ObjectType {
1561    Table,
1562    View,
1563    MaterializedView,
1564    Source,
1565    Sink,
1566    Index,
1567    Type,
1568    Role,
1569    Cluster,
1570    ClusterReplica,
1571    Secret,
1572    Connection,
1573    Database,
1574    Schema,
1575    Func,
1576    NetworkPolicy,
1577}
1578
1579impl ObjectType {
1580    /// Reports if the object type can be treated as a relation.
1581    pub fn is_relation(&self) -> bool {
1582        match self {
1583            ObjectType::Table
1584            | ObjectType::View
1585            | ObjectType::MaterializedView
1586            | ObjectType::Source => true,
1587            ObjectType::Sink
1588            | ObjectType::Index
1589            | ObjectType::Type
1590            | ObjectType::Secret
1591            | ObjectType::Connection
1592            | ObjectType::Func
1593            | ObjectType::Database
1594            | ObjectType::Schema
1595            | ObjectType::Cluster
1596            | ObjectType::ClusterReplica
1597            | ObjectType::Role
1598            | ObjectType::NetworkPolicy => false,
1599        }
1600    }
1601}
1602
1603impl From<mz_sql_parser::ast::ObjectType> for ObjectType {
1604    fn from(value: mz_sql_parser::ast::ObjectType) -> Self {
1605        match value {
1606            mz_sql_parser::ast::ObjectType::Table => ObjectType::Table,
1607            mz_sql_parser::ast::ObjectType::View => ObjectType::View,
1608            mz_sql_parser::ast::ObjectType::MaterializedView => ObjectType::MaterializedView,
1609            mz_sql_parser::ast::ObjectType::Source => ObjectType::Source,
1610            mz_sql_parser::ast::ObjectType::Subsource => ObjectType::Source,
1611            mz_sql_parser::ast::ObjectType::Sink => ObjectType::Sink,
1612            mz_sql_parser::ast::ObjectType::Index => ObjectType::Index,
1613            mz_sql_parser::ast::ObjectType::Type => ObjectType::Type,
1614            mz_sql_parser::ast::ObjectType::Role => ObjectType::Role,
1615            mz_sql_parser::ast::ObjectType::Cluster => ObjectType::Cluster,
1616            mz_sql_parser::ast::ObjectType::ClusterReplica => ObjectType::ClusterReplica,
1617            mz_sql_parser::ast::ObjectType::Secret => ObjectType::Secret,
1618            mz_sql_parser::ast::ObjectType::Connection => ObjectType::Connection,
1619            mz_sql_parser::ast::ObjectType::Database => ObjectType::Database,
1620            mz_sql_parser::ast::ObjectType::Schema => ObjectType::Schema,
1621            mz_sql_parser::ast::ObjectType::Func => ObjectType::Func,
1622            mz_sql_parser::ast::ObjectType::NetworkPolicy => ObjectType::NetworkPolicy,
1623        }
1624    }
1625}
1626
1627impl From<CommentObjectId> for ObjectType {
1628    fn from(value: CommentObjectId) -> ObjectType {
1629        match value {
1630            CommentObjectId::Table(_) => ObjectType::Table,
1631            CommentObjectId::View(_) => ObjectType::View,
1632            CommentObjectId::MaterializedView(_) => ObjectType::MaterializedView,
1633            CommentObjectId::Source(_) => ObjectType::Source,
1634            CommentObjectId::Sink(_) => ObjectType::Sink,
1635            CommentObjectId::Index(_) => ObjectType::Index,
1636            CommentObjectId::Func(_) => ObjectType::Func,
1637            CommentObjectId::Connection(_) => ObjectType::Connection,
1638            CommentObjectId::Type(_) => ObjectType::Type,
1639            CommentObjectId::Secret(_) => ObjectType::Secret,
1640            CommentObjectId::Role(_) => ObjectType::Role,
1641            CommentObjectId::Database(_) => ObjectType::Database,
1642            CommentObjectId::Schema(_) => ObjectType::Schema,
1643            CommentObjectId::Cluster(_) => ObjectType::Cluster,
1644            CommentObjectId::ClusterReplica(_) => ObjectType::ClusterReplica,
1645            CommentObjectId::NetworkPolicy(_) => ObjectType::NetworkPolicy,
1646        }
1647    }
1648}
1649
1650impl Display for ObjectType {
1651    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1652        f.write_str(match self {
1653            ObjectType::Table => "TABLE",
1654            ObjectType::View => "VIEW",
1655            ObjectType::MaterializedView => "MATERIALIZED VIEW",
1656            ObjectType::Source => "SOURCE",
1657            ObjectType::Sink => "SINK",
1658            ObjectType::Index => "INDEX",
1659            ObjectType::Type => "TYPE",
1660            ObjectType::Role => "ROLE",
1661            ObjectType::Cluster => "CLUSTER",
1662            ObjectType::ClusterReplica => "CLUSTER REPLICA",
1663            ObjectType::Secret => "SECRET",
1664            ObjectType::Connection => "CONNECTION",
1665            ObjectType::Database => "DATABASE",
1666            ObjectType::Schema => "SCHEMA",
1667            ObjectType::Func => "FUNCTION",
1668            ObjectType::NetworkPolicy => "NETWORK POLICY",
1669        })
1670    }
1671}
1672
1673#[derive(
1674    Debug,
1675    Clone,
1676    PartialOrd,
1677    Ord,
1678    PartialEq,
1679    Eq,
1680    Hash,
1681    Copy,
1682    Deserialize,
1683    Serialize
1684)]
1685/// The types of objects in the system.
1686pub enum SystemObjectType {
1687    /// Catalog object type.
1688    Object(ObjectType),
1689    /// Entire system.
1690    System,
1691}
1692
1693impl SystemObjectType {
1694    /// Reports if the object type can be treated as a relation.
1695    pub fn is_relation(&self) -> bool {
1696        match self {
1697            SystemObjectType::Object(object_type) => object_type.is_relation(),
1698            SystemObjectType::System => false,
1699        }
1700    }
1701}
1702
1703impl Display for SystemObjectType {
1704    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1705        match self {
1706            SystemObjectType::Object(object_type) => std::fmt::Display::fmt(&object_type, f),
1707            SystemObjectType::System => f.write_str("SYSTEM"),
1708        }
1709    }
1710}
1711
1712/// Enum used to format object names in error messages.
1713#[derive(Debug, Clone, PartialEq, Eq)]
1714pub enum ErrorMessageObjectDescription {
1715    /// The name of a specific object.
1716    Object {
1717        /// Type of object.
1718        object_type: ObjectType,
1719        /// Name of object.
1720        object_name: Option<String>,
1721    },
1722    /// The name of the entire system.
1723    System,
1724}
1725
1726impl ErrorMessageObjectDescription {
1727    /// Generate a new [`ErrorMessageObjectDescription`] from an [`ObjectId`].
1728    pub fn from_id(
1729        object_id: &ObjectId,
1730        catalog: &dyn SessionCatalog,
1731    ) -> ErrorMessageObjectDescription {
1732        let object_name = match object_id {
1733            ObjectId::Cluster(cluster_id) => catalog.get_cluster(*cluster_id).name().to_string(),
1734            ObjectId::ClusterReplica((cluster_id, replica_id)) => catalog
1735                .get_cluster_replica(*cluster_id, *replica_id)
1736                .name()
1737                .to_string(),
1738            ObjectId::Database(database_id) => catalog.get_database(database_id).name().to_string(),
1739            ObjectId::Schema((database_spec, schema_spec)) => {
1740                let name = catalog.get_schema(database_spec, schema_spec).name();
1741                catalog.resolve_full_schema_name(name).to_string()
1742            }
1743            ObjectId::Role(role_id) => catalog.get_role(role_id).name().to_string(),
1744            ObjectId::Item(id) => {
1745                let name = catalog.get_item(id).name();
1746                catalog.resolve_full_name(name).to_string()
1747            }
1748            ObjectId::NetworkPolicy(network_policy_id) => catalog
1749                .get_network_policy(network_policy_id)
1750                .name()
1751                .to_string(),
1752        };
1753        ErrorMessageObjectDescription::Object {
1754            object_type: catalog.get_object_type(object_id),
1755            object_name: Some(object_name),
1756        }
1757    }
1758
1759    /// Generate a new [`ErrorMessageObjectDescription`] from a [`SystemObjectId`].
1760    pub fn from_sys_id(
1761        object_id: &SystemObjectId,
1762        catalog: &dyn SessionCatalog,
1763    ) -> ErrorMessageObjectDescription {
1764        match object_id {
1765            SystemObjectId::Object(object_id) => {
1766                ErrorMessageObjectDescription::from_id(object_id, catalog)
1767            }
1768            SystemObjectId::System => ErrorMessageObjectDescription::System,
1769        }
1770    }
1771
1772    /// Generate a new [`ErrorMessageObjectDescription`] from a [`SystemObjectType`].
1773    pub fn from_object_type(object_type: SystemObjectType) -> ErrorMessageObjectDescription {
1774        match object_type {
1775            SystemObjectType::Object(object_type) => ErrorMessageObjectDescription::Object {
1776                object_type,
1777                object_name: None,
1778            },
1779            SystemObjectType::System => ErrorMessageObjectDescription::System,
1780        }
1781    }
1782}
1783
1784impl Display for ErrorMessageObjectDescription {
1785    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1786        match self {
1787            ErrorMessageObjectDescription::Object {
1788                object_type,
1789                object_name,
1790            } => {
1791                let object_name = object_name
1792                    .as_ref()
1793                    .map(|object_name| format!(" {}", object_name.quoted()))
1794                    .unwrap_or_else(|| "".to_string());
1795                write!(f, "{object_type}{object_name}")
1796            }
1797            ErrorMessageObjectDescription::System => f.write_str("SYSTEM"),
1798        }
1799    }
1800}
1801
1802#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
1803// These attributes are needed because the key of a map must be a string. We also
1804// get the added benefit of flattening this struct in it's serialized form.
1805#[serde(into = "BTreeMap<String, RoleId>")]
1806#[serde(try_from = "BTreeMap<String, RoleId>")]
1807/// Represents the grantee and a grantor of a role membership.
1808pub struct RoleMembership {
1809    /// Key is the role that some role is a member of, value is the grantor role ID.
1810    // TODO(jkosh44) This structure does not allow a role to have multiple of the same membership
1811    // from different grantors. This isn't a problem now since we don't implement ADMIN OPTION, but
1812    // we should figure this out before implementing ADMIN OPTION. It will likely require a messy
1813    // migration.
1814    pub map: BTreeMap<RoleId, RoleId>,
1815}
1816
1817impl RoleMembership {
1818    /// Creates a new [`RoleMembership`].
1819    pub fn new() -> RoleMembership {
1820        RoleMembership {
1821            map: BTreeMap::new(),
1822        }
1823    }
1824}
1825
1826impl From<RoleMembership> for BTreeMap<String, RoleId> {
1827    fn from(value: RoleMembership) -> Self {
1828        value
1829            .map
1830            .into_iter()
1831            .map(|(k, v)| (k.to_string(), v))
1832            .collect()
1833    }
1834}
1835
1836impl TryFrom<BTreeMap<String, RoleId>> for RoleMembership {
1837    type Error = anyhow::Error;
1838
1839    fn try_from(value: BTreeMap<String, RoleId>) -> Result<Self, Self::Error> {
1840        Ok(RoleMembership {
1841            map: value
1842                .into_iter()
1843                .map(|(k, v)| Ok((RoleId::from_str(&k)?, v)))
1844                .collect::<Result<_, anyhow::Error>>()?,
1845        })
1846    }
1847}
1848
1849/// Specification for objects that will be affected by a default privilege.
1850#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
1851pub struct DefaultPrivilegeObject {
1852    /// The role id that created the object.
1853    pub role_id: RoleId,
1854    /// The database that the object is created in if Some, otherwise all databases.
1855    pub database_id: Option<DatabaseId>,
1856    /// The schema that the object is created in if Some, otherwise all databases.
1857    pub schema_id: Option<SchemaId>,
1858    /// The type of object.
1859    pub object_type: ObjectType,
1860}
1861
1862impl DefaultPrivilegeObject {
1863    /// Creates a new [`DefaultPrivilegeObject`].
1864    pub fn new(
1865        role_id: RoleId,
1866        database_id: Option<DatabaseId>,
1867        schema_id: Option<SchemaId>,
1868        object_type: ObjectType,
1869    ) -> DefaultPrivilegeObject {
1870        DefaultPrivilegeObject {
1871            role_id,
1872            database_id,
1873            schema_id,
1874            object_type,
1875        }
1876    }
1877}
1878
1879impl std::fmt::Display for DefaultPrivilegeObject {
1880    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1881        // TODO: Don't just wrap Debug.
1882        write!(f, "{self:?}")
1883    }
1884}
1885
1886/// Specification for the privileges that will be granted from default privileges.
1887#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
1888pub struct DefaultPrivilegeAclItem {
1889    /// The role that will receive the privileges.
1890    pub grantee: RoleId,
1891    /// The specific privileges granted.
1892    pub acl_mode: AclMode,
1893}
1894
1895impl DefaultPrivilegeAclItem {
1896    /// Creates a new [`DefaultPrivilegeAclItem`].
1897    pub fn new(grantee: RoleId, acl_mode: AclMode) -> DefaultPrivilegeAclItem {
1898        DefaultPrivilegeAclItem { grantee, acl_mode }
1899    }
1900
1901    /// Converts this [`DefaultPrivilegeAclItem`] into an [`MzAclItem`].
1902    pub fn mz_acl_item(self, grantor: RoleId) -> MzAclItem {
1903        MzAclItem {
1904            grantee: self.grantee,
1905            grantor,
1906            acl_mode: self.acl_mode,
1907        }
1908    }
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913    use super::{CloudProvider, EnvironmentId, InvalidEnvironmentIdError};
1914
1915    #[mz_ore::test]
1916    fn test_environment_id() {
1917        for (input, expected) in [
1918            (
1919                "local-az1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-452",
1920                Ok(EnvironmentId {
1921                    cloud_provider: CloudProvider::Local,
1922                    cloud_provider_region: "az1".into(),
1923                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1924                    ordinal: 452,
1925                }),
1926            ),
1927            (
1928                "aws-us-east-1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1929                Ok(EnvironmentId {
1930                    cloud_provider: CloudProvider::Aws,
1931                    cloud_provider_region: "us-east-1".into(),
1932                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1933                    ordinal: 0,
1934                }),
1935            ),
1936            (
1937                "gcp-us-central1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1938                Ok(EnvironmentId {
1939                    cloud_provider: CloudProvider::Gcp,
1940                    cloud_provider_region: "us-central1".into(),
1941                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1942                    ordinal: 0,
1943                }),
1944            ),
1945            (
1946                "azure-australiaeast-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1947                Ok(EnvironmentId {
1948                    cloud_provider: CloudProvider::Azure,
1949                    cloud_provider_region: "australiaeast".into(),
1950                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1951                    ordinal: 0,
1952                }),
1953            ),
1954            (
1955                "generic-moon-station-11-darkside-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1956                Ok(EnvironmentId {
1957                    cloud_provider: CloudProvider::Generic,
1958                    cloud_provider_region: "moon-station-11-darkside".into(),
1959                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1960                    ordinal: 0,
1961                }),
1962            ),
1963            ("", Err(InvalidEnvironmentIdError)),
1964            (
1965                "local-az1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-123456789",
1966                Err(InvalidEnvironmentIdError),
1967            ),
1968            (
1969                "local-1497a3b7-a455-4fc4-8752-b44a94b5f90a-452",
1970                Err(InvalidEnvironmentIdError),
1971            ),
1972            (
1973                "local-az1-1497a3b7-a455-4fc48752-b44a94b5f90a-452",
1974                Err(InvalidEnvironmentIdError),
1975            ),
1976        ] {
1977            let actual = input.parse();
1978            assert_eq!(expected, actual, "input = {}", input);
1979            if let Ok(actual) = actual {
1980                assert_eq!(input, actual.to_string(), "input = {}", input);
1981            }
1982        }
1983    }
1984}