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