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