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    /// Try to convert this cluster into a [`CreateClusterPlan`].
778    // TODO(jkosh44) Make this infallible and convert to `to_plan`.
779    fn try_to_plan(&self) -> Result<CreateClusterPlan, PlanError>;
780}
781
782/// A cluster replica in a [`SessionCatalog`]
783pub trait CatalogClusterReplica<'a>: Debug {
784    /// Returns the name of the cluster replica.
785    fn name(&self) -> &str;
786
787    /// Returns a stable ID for the cluster that the replica belongs to.
788    fn cluster_id(&self) -> ClusterId;
789
790    /// Returns a stable ID for the replica.
791    fn replica_id(&self) -> ReplicaId;
792
793    /// Returns the ID of the owning role.
794    fn owner_id(&self) -> RoleId;
795
796    /// Returns whether or not the replica is internal
797    fn internal(&self) -> bool;
798}
799
800/// An item in a [`SessionCatalog`].
801///
802/// Note that "item" has a very specific meaning in the context of a SQL
803/// catalog, and refers to the various entities that belong to a schema.
804pub trait CatalogItem {
805    /// Returns the fully qualified name of the catalog item.
806    fn name(&self) -> &QualifiedItemName;
807
808    /// Returns the [`CatalogItemId`] for the item.
809    fn id(&self) -> CatalogItemId;
810
811    /// Returns the [`GlobalId`]s associated with this item.
812    fn global_ids(&self) -> Box<dyn Iterator<Item = GlobalId> + '_>;
813
814    /// Returns the catalog item's OID.
815    fn oid(&self) -> u32;
816
817    /// Returns the resolved function.
818    ///
819    /// If the catalog item is not of a type that produces functions (i.e.,
820    /// anything other than a function), it returns an error.
821    fn func(&self) -> Result<&'static Func, CatalogError>;
822
823    /// Returns the resolved source connection.
824    ///
825    /// If the catalog item is not of a type that contains a `SourceDesc`
826    /// (i.e., anything other than sources), it returns an error.
827    fn source_desc(&self) -> Result<Option<&SourceDesc<ReferencedConnection>>, CatalogError>;
828
829    /// Returns the resolved connection.
830    ///
831    /// If the catalog item is not a connection, it returns an error.
832    fn connection(&self) -> Result<Connection<ReferencedConnection>, CatalogError>;
833
834    /// Returns the type of the catalog item.
835    fn item_type(&self) -> CatalogItemType;
836
837    /// A normalized SQL statement that describes how to create the catalog
838    /// item.
839    fn create_sql(&self) -> &str;
840
841    /// Returns the IDs of the catalog items upon which this catalog item
842    /// directly references.
843    fn references(&self) -> &ResolvedIds;
844
845    /// Returns the IDs of the catalog items upon which this catalog item
846    /// depends.
847    fn uses(&self) -> BTreeSet<CatalogItemId>;
848
849    /// Returns the IDs of the catalog items that directly reference this catalog item.
850    fn referenced_by(&self) -> &[CatalogItemId];
851
852    /// Returns the IDs of the catalog items that depend upon this catalog item.
853    fn used_by(&self) -> &[CatalogItemId];
854
855    /// Reports whether this catalog entry is a subsource and, if it is, the
856    /// ingestion it is an export of, as well as the item it exports.
857    fn subsource_details(
858        &self,
859    ) -> Option<(CatalogItemId, &UnresolvedItemName, &SourceExportDetails)>;
860
861    /// Reports whether this catalog entry is a source export and, if it is, the
862    /// ingestion it is an export of, as well as the item it exports.
863    fn source_export_details(
864        &self,
865    ) -> Option<(
866        CatalogItemId,
867        &UnresolvedItemName,
868        &SourceExportDetails,
869        &SourceExportDataConfig<ReferencedConnection>,
870    )>;
871
872    /// Reports whether this catalog item is a progress source.
873    fn is_progress_source(&self) -> bool;
874
875    /// If this catalog item is a source, it return the IDs of its progress collection.
876    fn progress_id(&self) -> Option<CatalogItemId>;
877
878    /// Returns the index details associated with the catalog item, if the
879    /// catalog item is an index.
880    fn index_details(&self) -> Option<(&[MirScalarExpr], GlobalId)>;
881
882    /// Returns the column defaults associated with the catalog item, if the
883    /// catalog item is a table that accepts writes.
884    fn writable_table_details(&self) -> Option<&[Expr<Aug>]>;
885
886    /// The item this catalog item replaces, if any.
887    fn replacement_target(&self) -> Option<CatalogItemId>;
888
889    /// Returns the type information associated with the catalog item, if the
890    /// catalog item is a type.
891    fn type_details(&self) -> Option<&CatalogTypeDetails<IdReference>>;
892
893    /// Returns the ID of the owning role.
894    fn owner_id(&self) -> RoleId;
895
896    /// Returns the privileges associated with the item.
897    fn privileges(&self) -> &PrivilegeMap;
898
899    /// Returns the cluster the item belongs to.
900    fn cluster_id(&self) -> Option<ClusterId>;
901
902    /// Returns the [`CatalogCollectionItem`] for a specific version of this
903    /// [`CatalogItem`].
904    fn at_version(&self, version: RelationVersionSelector) -> Box<dyn CatalogCollectionItem>;
905
906    /// The latest version of this item, if it's version-able.
907    fn latest_version(&self) -> Option<RelationVersion>;
908}
909
910/// An item in a [`SessionCatalog`] and the specific "collection"/pTVC that it
911/// refers to.
912pub trait CatalogCollectionItem: CatalogItem + Send + Sync {
913    /// Returns a description of the result set produced by the catalog item.
914    ///
915    /// If the catalog item is not of a type that produces data (e.g., a sink or
916    /// an index), it returns `None`.
917    fn relation_desc(&self) -> Option<Cow<'_, RelationDesc>>;
918
919    /// The [`GlobalId`] for this item.
920    fn global_id(&self) -> GlobalId;
921}
922
923/// The type of a [`CatalogItem`].
924#[derive(
925    Debug,
926    Deserialize,
927    Clone,
928    Copy,
929    Eq,
930    Hash,
931    Ord,
932    PartialEq,
933    PartialOrd,
934    Serialize
935)]
936pub enum CatalogItemType {
937    /// A table.
938    Table,
939    /// A source.
940    Source,
941    /// A sink.
942    Sink,
943    /// A view.
944    View,
945    /// A materialized view.
946    MaterializedView,
947    /// An index.
948    Index,
949    /// A type.
950    Type,
951    /// A func.
952    Func,
953    /// A secret.
954    Secret,
955    /// A connection.
956    Connection,
957}
958
959impl CatalogItemType {
960    /// Reports whether the given type of item conflicts with items of type
961    /// `CatalogItemType::Type`.
962    ///
963    /// In PostgreSQL, even though types live in a separate namespace from other
964    /// schema objects, creating a table, view, or materialized view creates a
965    /// type named after that relation. This prevents creating a type with the
966    /// same name as a relational object, even though types and relational
967    /// objects live in separate namespaces. (Indexes are even weirder; while
968    /// they don't get a type with the same name, they get an entry in
969    /// `pg_class` that prevents *record* types of the same name as the index,
970    /// but not other types of types, like enums.)
971    ///
972    /// We don't presently construct types that mirror relational objects,
973    /// though we likely will need to in the future for full PostgreSQL
974    /// compatibility (see database-issues#7142). For now, we use this method to
975    /// prevent creating types and relational objects that have the same name, so
976    /// that it is a backwards compatible change in the future to introduce a
977    /// type named after each relational object in the system.
978    pub fn conflicts_with_type(&self) -> bool {
979        match self {
980            CatalogItemType::Table => true,
981            CatalogItemType::Source => true,
982            CatalogItemType::View => true,
983            CatalogItemType::MaterializedView => true,
984            CatalogItemType::Index => true,
985            CatalogItemType::Type => true,
986            CatalogItemType::Sink => false,
987            CatalogItemType::Func => false,
988            CatalogItemType::Secret => false,
989            CatalogItemType::Connection => false,
990        }
991    }
992}
993
994impl fmt::Display for CatalogItemType {
995    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
996        match self {
997            CatalogItemType::Table => f.write_str("table"),
998            CatalogItemType::Source => f.write_str("source"),
999            CatalogItemType::Sink => f.write_str("sink"),
1000            CatalogItemType::View => f.write_str("view"),
1001            CatalogItemType::MaterializedView => f.write_str("materialized view"),
1002            CatalogItemType::Index => f.write_str("index"),
1003            CatalogItemType::Type => f.write_str("type"),
1004            CatalogItemType::Func => f.write_str("func"),
1005            CatalogItemType::Secret => f.write_str("secret"),
1006            CatalogItemType::Connection => f.write_str("connection"),
1007        }
1008    }
1009}
1010
1011impl From<CatalogItemType> for ObjectType {
1012    fn from(value: CatalogItemType) -> Self {
1013        match value {
1014            CatalogItemType::Table => ObjectType::Table,
1015            CatalogItemType::Source => ObjectType::Source,
1016            CatalogItemType::Sink => ObjectType::Sink,
1017            CatalogItemType::View => ObjectType::View,
1018            CatalogItemType::MaterializedView => ObjectType::MaterializedView,
1019            CatalogItemType::Index => ObjectType::Index,
1020            CatalogItemType::Type => ObjectType::Type,
1021            CatalogItemType::Func => ObjectType::Func,
1022            CatalogItemType::Secret => ObjectType::Secret,
1023            CatalogItemType::Connection => ObjectType::Connection,
1024        }
1025    }
1026}
1027
1028impl From<CatalogItemType> for mz_audit_log::ObjectType {
1029    fn from(value: CatalogItemType) -> Self {
1030        match value {
1031            CatalogItemType::Table => mz_audit_log::ObjectType::Table,
1032            CatalogItemType::Source => mz_audit_log::ObjectType::Source,
1033            CatalogItemType::View => mz_audit_log::ObjectType::View,
1034            CatalogItemType::MaterializedView => mz_audit_log::ObjectType::MaterializedView,
1035            CatalogItemType::Index => mz_audit_log::ObjectType::Index,
1036            CatalogItemType::Type => mz_audit_log::ObjectType::Type,
1037            CatalogItemType::Sink => mz_audit_log::ObjectType::Sink,
1038            CatalogItemType::Func => mz_audit_log::ObjectType::Func,
1039            CatalogItemType::Secret => mz_audit_log::ObjectType::Secret,
1040            CatalogItemType::Connection => mz_audit_log::ObjectType::Connection,
1041        }
1042    }
1043}
1044
1045/// Details about a type in the catalog.
1046#[derive(Clone, Debug, Eq, PartialEq)]
1047pub struct CatalogTypeDetails<T: TypeReference> {
1048    /// The ID of the type with this type as the array element, if available.
1049    pub array_id: Option<CatalogItemId>,
1050    /// The description of this type.
1051    pub typ: CatalogType<T>,
1052    /// Additional metadata about the type in PostgreSQL, if relevant.
1053    pub pg_metadata: Option<CatalogTypePgMetadata>,
1054}
1055
1056/// Additional PostgreSQL metadata about a type.
1057#[derive(Clone, Debug, Eq, PartialEq)]
1058pub struct CatalogTypePgMetadata {
1059    /// The OID of the `typinput` function in PostgreSQL.
1060    pub typinput_oid: u32,
1061    /// The OID of the `typreceive` function in PostgreSQL.
1062    pub typreceive_oid: u32,
1063}
1064
1065/// Represents a reference to type in the catalog
1066pub trait TypeReference {
1067    /// The actual type used to reference a `CatalogType`
1068    type Reference: Clone + Debug + Eq + PartialEq;
1069}
1070
1071/// Reference to a type by it's name
1072#[derive(Clone, Debug, Eq, PartialEq)]
1073pub struct NameReference;
1074
1075impl TypeReference for NameReference {
1076    type Reference = &'static str;
1077}
1078
1079/// Reference to a type by it's global ID
1080#[derive(Clone, Debug, Eq, PartialEq)]
1081pub struct IdReference;
1082
1083impl TypeReference for IdReference {
1084    type Reference = CatalogItemId;
1085}
1086
1087/// A type stored in the catalog.
1088///
1089/// The variants correspond one-to-one with [`mz_repr::SqlScalarType`], but with type
1090/// modifiers removed and with embedded types replaced with references to other
1091/// types in the catalog.
1092#[allow(missing_docs)]
1093#[derive(Clone, Debug, Eq, PartialEq)]
1094pub enum CatalogType<T: TypeReference> {
1095    AclItem,
1096    Array {
1097        element_reference: T::Reference,
1098    },
1099    Bool,
1100    Bytes,
1101    Char,
1102    Date,
1103    Float32,
1104    Float64,
1105    Int16,
1106    Int32,
1107    Int64,
1108    UInt16,
1109    UInt32,
1110    UInt64,
1111    MzTimestamp,
1112    Interval,
1113    Jsonb,
1114    List {
1115        element_reference: T::Reference,
1116        element_modifiers: Vec<i64>,
1117    },
1118    Map {
1119        key_reference: T::Reference,
1120        key_modifiers: Vec<i64>,
1121        value_reference: T::Reference,
1122        value_modifiers: Vec<i64>,
1123    },
1124    Numeric,
1125    Oid,
1126    PgLegacyChar,
1127    PgLegacyName,
1128    Pseudo,
1129    Range {
1130        element_reference: T::Reference,
1131    },
1132    Record {
1133        fields: Vec<CatalogRecordField<T>>,
1134    },
1135    RegClass,
1136    RegProc,
1137    RegType,
1138    String,
1139    Time,
1140    Timestamp,
1141    TimestampTz,
1142    Uuid,
1143    VarChar,
1144    Int2Vector,
1145    MzAclItem,
1146}
1147
1148impl CatalogType<IdReference> {
1149    /// Returns the relation description for the type, if the type is a record
1150    /// type.
1151    pub fn desc(&self, catalog: &dyn SessionCatalog) -> Result<Option<RelationDesc>, PlanError> {
1152        match &self {
1153            CatalogType::Record { fields } => {
1154                let mut desc = RelationDesc::builder();
1155                for f in fields {
1156                    let name = f.name.clone();
1157                    let ty = query::scalar_type_from_catalog(
1158                        catalog,
1159                        f.type_reference,
1160                        &f.type_modifiers,
1161                    )?;
1162                    // TODO: support plumbing `NOT NULL` constraints through
1163                    // `CREATE TYPE`.
1164                    let ty = ty.nullable(true);
1165                    desc = desc.with_column(name, ty);
1166                }
1167                Ok(Some(desc.finish()))
1168            }
1169            _ => Ok(None),
1170        }
1171    }
1172}
1173
1174/// A description of a field in a [`CatalogType::Record`].
1175#[derive(Clone, Debug, Eq, PartialEq)]
1176pub struct CatalogRecordField<T: TypeReference> {
1177    /// The name of the field.
1178    pub name: ColumnName,
1179    /// The ID of the type of the field.
1180    pub type_reference: T::Reference,
1181    /// Modifiers to apply to the type.
1182    pub type_modifiers: Vec<i64>,
1183}
1184
1185#[derive(Clone, Debug, Eq, PartialEq)]
1186/// Mirrored from [PostgreSQL's `typcategory`][typcategory].
1187///
1188/// Note that Materialize also uses a number of pseudotypes when planning, but
1189/// we have yet to need to integrate them with `TypeCategory`.
1190///
1191/// [typcategory]:
1192/// https://www.postgresql.org/docs/9.6/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE
1193pub enum TypeCategory {
1194    /// Array type.
1195    Array,
1196    /// Bit string type.
1197    BitString,
1198    /// Boolean type.
1199    Boolean,
1200    /// Composite type.
1201    Composite,
1202    /// Date/time type.
1203    DateTime,
1204    /// Enum type.
1205    Enum,
1206    /// Geometric type.
1207    Geometric,
1208    /// List type. Materialize specific.
1209    List,
1210    /// Network address type.
1211    NetworkAddress,
1212    /// Numeric type.
1213    Numeric,
1214    /// Pseudo type.
1215    Pseudo,
1216    /// Range type.
1217    Range,
1218    /// String type.
1219    String,
1220    /// Timestamp type.
1221    Timespan,
1222    /// User-defined type.
1223    UserDefined,
1224    /// Unknown type.
1225    Unknown,
1226}
1227
1228impl fmt::Display for TypeCategory {
1229    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1230        f.write_str(match self {
1231            TypeCategory::Array => "array",
1232            TypeCategory::BitString => "bit-string",
1233            TypeCategory::Boolean => "boolean",
1234            TypeCategory::Composite => "composite",
1235            TypeCategory::DateTime => "date-time",
1236            TypeCategory::Enum => "enum",
1237            TypeCategory::Geometric => "geometric",
1238            TypeCategory::List => "list",
1239            TypeCategory::NetworkAddress => "network-address",
1240            TypeCategory::Numeric => "numeric",
1241            TypeCategory::Pseudo => "pseudo",
1242            TypeCategory::Range => "range",
1243            TypeCategory::String => "string",
1244            TypeCategory::Timespan => "timespan",
1245            TypeCategory::UserDefined => "user-defined",
1246            TypeCategory::Unknown => "unknown",
1247        })
1248    }
1249}
1250
1251/// Identifies an environment.
1252///
1253/// Outside of tests, an environment ID can be constructed only from a string of
1254/// the following form:
1255///
1256/// ```text
1257/// <CLOUD PROVIDER>-<CLOUD PROVIDER REGION>-<ORGANIZATION ID>-<ORDINAL>
1258/// ```
1259///
1260/// The fields have the following formats:
1261///
1262/// * The cloud provider consists of one or more alphanumeric characters.
1263/// * The cloud provider region consists of one or more alphanumeric or hyphen
1264///   characters.
1265/// * The organization ID is a UUID in its canonical text format.
1266/// * The ordinal is a decimal number with between one and eight digits.
1267///
1268/// There is no way to construct an environment ID from parts, to ensure that
1269/// the `Display` representation is parseable according to the above rules.
1270// NOTE(benesch): ideally we'd have accepted the components of the environment
1271// ID using separate command-line arguments, or at least a string format that
1272// used a field separator that did not appear in the fields. Alas. We can't
1273// easily change it now, as it's used as the e.g. default sink progress topic.
1274#[derive(Debug, Clone, PartialEq)]
1275pub struct EnvironmentId {
1276    cloud_provider: CloudProvider,
1277    cloud_provider_region: String,
1278    organization_id: Uuid,
1279    ordinal: u64,
1280}
1281
1282impl EnvironmentId {
1283    /// Creates a dummy `EnvironmentId` for use in tests.
1284    pub fn for_tests() -> EnvironmentId {
1285        EnvironmentId {
1286            cloud_provider: CloudProvider::Local,
1287            cloud_provider_region: "az1".into(),
1288            organization_id: Uuid::new_v4(),
1289            ordinal: 0,
1290        }
1291    }
1292
1293    /// Returns the cloud provider associated with this environment ID.
1294    pub fn cloud_provider(&self) -> &CloudProvider {
1295        &self.cloud_provider
1296    }
1297
1298    /// Returns the cloud provider region associated with this environment ID.
1299    pub fn cloud_provider_region(&self) -> &str {
1300        &self.cloud_provider_region
1301    }
1302
1303    /// Returns the name of the region associted with this environment ID.
1304    ///
1305    /// A region is a combination of [`EnvironmentId::cloud_provider`] and
1306    /// [`EnvironmentId::cloud_provider_region`].
1307    pub fn region(&self) -> String {
1308        format!("{}/{}", self.cloud_provider, self.cloud_provider_region)
1309    }
1310
1311    /// Returns the organization ID associated with this environment ID.
1312    pub fn organization_id(&self) -> Uuid {
1313        self.organization_id
1314    }
1315
1316    /// Returns the ordinal associated with this environment ID.
1317    pub fn ordinal(&self) -> u64 {
1318        self.ordinal
1319    }
1320}
1321
1322// *Warning*: once the LaunchDarkly integration is live, our contexts will be
1323// populated using this key. Consequently, any changes to that trait
1324// implementation will also have to be reflected in the existing feature
1325// targeting config in LaunchDarkly, otherwise environments might receive
1326// different configs upon restart.
1327impl fmt::Display for EnvironmentId {
1328    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1329        write!(
1330            f,
1331            "{}-{}-{}-{}",
1332            self.cloud_provider, self.cloud_provider_region, self.organization_id, self.ordinal
1333        )
1334    }
1335}
1336
1337impl FromStr for EnvironmentId {
1338    type Err = InvalidEnvironmentIdError;
1339
1340    fn from_str(s: &str) -> Result<EnvironmentId, InvalidEnvironmentIdError> {
1341        static MATCHER: LazyLock<Regex> = LazyLock::new(|| {
1342            Regex::new(
1343                "^(?P<cloud_provider>[[:alnum:]]+)-\
1344                  (?P<cloud_provider_region>[[:alnum:]\\-]+)-\
1345                  (?P<organization_id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-\
1346                  (?P<ordinal>\\d{1,8})$"
1347            ).unwrap()
1348        });
1349        let captures = MATCHER.captures(s).ok_or(InvalidEnvironmentIdError)?;
1350        Ok(EnvironmentId {
1351            cloud_provider: CloudProvider::from_str(&captures["cloud_provider"])?,
1352            cloud_provider_region: captures["cloud_provider_region"].into(),
1353            organization_id: captures["organization_id"]
1354                .parse()
1355                .map_err(|_| InvalidEnvironmentIdError)?,
1356            ordinal: captures["ordinal"]
1357                .parse()
1358                .map_err(|_| InvalidEnvironmentIdError)?,
1359        })
1360    }
1361}
1362
1363/// The error type for [`EnvironmentId::from_str`].
1364#[derive(Debug, Clone, PartialEq)]
1365pub struct InvalidEnvironmentIdError;
1366
1367impl fmt::Display for InvalidEnvironmentIdError {
1368    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1369        f.write_str("invalid environment ID")
1370    }
1371}
1372
1373impl Error for InvalidEnvironmentIdError {}
1374
1375impl From<InvalidCloudProviderError> for InvalidEnvironmentIdError {
1376    fn from(_: InvalidCloudProviderError) -> Self {
1377        InvalidEnvironmentIdError
1378    }
1379}
1380
1381/// An error returned by the catalog.
1382#[derive(Clone, Debug, Eq, PartialEq)]
1383pub enum CatalogError {
1384    /// Unknown database.
1385    UnknownDatabase(String),
1386    /// Database already exists.
1387    DatabaseAlreadyExists(String),
1388    /// Unknown schema.
1389    UnknownSchema(String),
1390    /// Schema already exists.
1391    SchemaAlreadyExists(String),
1392    /// Unknown role.
1393    UnknownRole(String),
1394    /// Role already exists.
1395    RoleAlreadyExists(String),
1396    /// Network Policy already exists.
1397    NetworkPolicyAlreadyExists(String),
1398    /// Unknown cluster.
1399    UnknownCluster(String),
1400    /// Unexpected builtin cluster.
1401    UnexpectedBuiltinCluster(String),
1402    /// Unexpected builtin cluster.
1403    UnexpectedBuiltinClusterType(String),
1404    /// Cluster already exists.
1405    ClusterAlreadyExists(String),
1406    /// Unknown cluster replica.
1407    UnknownClusterReplica(String),
1408    /// Unknown cluster replica size.
1409    UnknownClusterReplicaSize(String),
1410    /// Duplicate Replica. #[error("cannot create multiple replicas named '{0}' on cluster '{1}'")]
1411    DuplicateReplica(String, String),
1412    /// Unknown item.
1413    UnknownItem(String),
1414    /// Item already exists.
1415    ItemAlreadyExists(CatalogItemId, String),
1416    /// Unknown function.
1417    UnknownFunction {
1418        /// The identifier of the function we couldn't find
1419        name: String,
1420        /// A suggested alternative to the named function.
1421        alternative: Option<String>,
1422    },
1423    /// Unknown type.
1424    UnknownType {
1425        /// The identifier of the type we couldn't find.
1426        name: String,
1427    },
1428    /// Unknown connection.
1429    UnknownConnection(String),
1430    /// Unknown network policy.
1431    UnknownNetworkPolicy(String),
1432    /// Expected the catalog item to have the given type, but it did not.
1433    UnexpectedType {
1434        /// The item's name.
1435        name: String,
1436        /// The actual type of the item.
1437        actual_type: CatalogItemType,
1438        /// The expected type of the item.
1439        expected_type: CatalogItemType,
1440    },
1441    /// Ran out of unique IDs.
1442    IdExhaustion,
1443    /// Ran out of unique OIDs.
1444    OidExhaustion,
1445    /// Timeline already exists.
1446    TimelineAlreadyExists(String),
1447    /// Id Allocator already exists.
1448    IdAllocatorAlreadyExists(String),
1449    /// Config already exists.
1450    ConfigAlreadyExists(String),
1451    /// Builtin migrations failed.
1452    FailedBuiltinSchemaMigration(String),
1453    /// StorageCollectionMetadata already exists.
1454    StorageCollectionMetadataAlreadyExists(GlobalId),
1455}
1456
1457impl fmt::Display for CatalogError {
1458    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1459        match self {
1460            Self::UnknownDatabase(name) => write!(f, "unknown database '{}'", name),
1461            Self::DatabaseAlreadyExists(name) => write!(f, "database '{name}' already exists"),
1462            Self::UnknownFunction { name, .. } => write!(f, "function \"{}\" does not exist", name),
1463            Self::UnknownType { name, .. } => write!(f, "type \"{}\" does not exist", name),
1464            Self::UnknownConnection(name) => write!(f, "connection \"{}\" does not exist", name),
1465            Self::UnknownSchema(name) => write!(f, "unknown schema '{}'", name),
1466            Self::SchemaAlreadyExists(name) => write!(f, "schema '{name}' already exists"),
1467            Self::UnknownRole(name) => write!(f, "unknown role '{}'", name),
1468            Self::RoleAlreadyExists(name) => write!(f, "role '{name}' already exists"),
1469            Self::NetworkPolicyAlreadyExists(name) => {
1470                write!(f, "network policy '{name}' already exists")
1471            }
1472            Self::UnknownCluster(name) => write!(f, "unknown cluster '{}'", name),
1473            Self::UnknownNetworkPolicy(name) => write!(f, "unknown network policy '{}'", name),
1474            Self::UnexpectedBuiltinCluster(name) => {
1475                write!(f, "Unexpected builtin cluster '{}'", name)
1476            }
1477            Self::UnexpectedBuiltinClusterType(name) => {
1478                write!(f, "Unexpected builtin cluster type'{}'", name)
1479            }
1480            Self::ClusterAlreadyExists(name) => write!(f, "cluster '{name}' already exists"),
1481            Self::UnknownClusterReplica(name) => {
1482                write!(f, "unknown cluster replica '{}'", name)
1483            }
1484            Self::UnknownClusterReplicaSize(name) => {
1485                write!(f, "unknown cluster replica size '{}'", name)
1486            }
1487            Self::DuplicateReplica(replica_name, cluster_name) => write!(
1488                f,
1489                "cannot create multiple replicas named '{replica_name}' on cluster '{cluster_name}'"
1490            ),
1491            Self::UnknownItem(name) => write!(f, "unknown catalog item '{}'", name),
1492            Self::ItemAlreadyExists(_gid, name) => {
1493                write!(f, "catalog item '{name}' already exists")
1494            }
1495            Self::UnexpectedType {
1496                name,
1497                actual_type,
1498                expected_type,
1499            } => {
1500                write!(f, "\"{name}\" is a {actual_type} not a {expected_type}")
1501            }
1502            Self::IdExhaustion => write!(f, "id counter overflows i64"),
1503            Self::OidExhaustion => write!(f, "oid counter overflows u32"),
1504            Self::TimelineAlreadyExists(name) => write!(f, "timeline '{name}' already exists"),
1505            Self::IdAllocatorAlreadyExists(name) => {
1506                write!(f, "ID allocator '{name}' already exists")
1507            }
1508            Self::ConfigAlreadyExists(key) => write!(f, "config '{key}' already exists"),
1509            Self::FailedBuiltinSchemaMigration(objects) => {
1510                write!(f, "failed to migrate schema of builtin objects: {objects}")
1511            }
1512            Self::StorageCollectionMetadataAlreadyExists(key) => {
1513                write!(f, "storage metadata for '{key}' already exists")
1514            }
1515        }
1516    }
1517}
1518
1519impl CatalogError {
1520    /// Returns any applicable hints for [`CatalogError`].
1521    pub fn hint(&self) -> Option<String> {
1522        match self {
1523            CatalogError::UnknownFunction { alternative, .. } => {
1524                match alternative {
1525                    None => Some("No function matches the given name and argument types. You might need to add explicit type casts.".into()),
1526                    Some(alt) => Some(format!("Try using {alt}")),
1527                }
1528            }
1529            _ => None,
1530        }
1531    }
1532}
1533
1534impl Error for CatalogError {}
1535
1536// Enum variant docs would be useless here.
1537#[allow(missing_docs)]
1538#[derive(
1539    Debug,
1540    Clone,
1541    PartialOrd,
1542    Ord,
1543    PartialEq,
1544    Eq,
1545    Hash,
1546    Copy,
1547    Deserialize,
1548    Serialize
1549)]
1550/// The types of objects stored in the catalog.
1551pub enum ObjectType {
1552    Table,
1553    View,
1554    MaterializedView,
1555    Source,
1556    Sink,
1557    Index,
1558    Type,
1559    Role,
1560    Cluster,
1561    ClusterReplica,
1562    Secret,
1563    Connection,
1564    Database,
1565    Schema,
1566    Func,
1567    NetworkPolicy,
1568}
1569
1570impl ObjectType {
1571    /// Reports if the object type can be treated as a relation.
1572    pub fn is_relation(&self) -> bool {
1573        match self {
1574            ObjectType::Table
1575            | ObjectType::View
1576            | ObjectType::MaterializedView
1577            | ObjectType::Source => true,
1578            ObjectType::Sink
1579            | ObjectType::Index
1580            | ObjectType::Type
1581            | ObjectType::Secret
1582            | ObjectType::Connection
1583            | ObjectType::Func
1584            | ObjectType::Database
1585            | ObjectType::Schema
1586            | ObjectType::Cluster
1587            | ObjectType::ClusterReplica
1588            | ObjectType::Role
1589            | ObjectType::NetworkPolicy => false,
1590        }
1591    }
1592}
1593
1594impl From<mz_sql_parser::ast::ObjectType> for ObjectType {
1595    fn from(value: mz_sql_parser::ast::ObjectType) -> Self {
1596        match value {
1597            mz_sql_parser::ast::ObjectType::Table => ObjectType::Table,
1598            mz_sql_parser::ast::ObjectType::View => ObjectType::View,
1599            mz_sql_parser::ast::ObjectType::MaterializedView => ObjectType::MaterializedView,
1600            mz_sql_parser::ast::ObjectType::Source => ObjectType::Source,
1601            mz_sql_parser::ast::ObjectType::Subsource => ObjectType::Source,
1602            mz_sql_parser::ast::ObjectType::Sink => ObjectType::Sink,
1603            mz_sql_parser::ast::ObjectType::Index => ObjectType::Index,
1604            mz_sql_parser::ast::ObjectType::Type => ObjectType::Type,
1605            mz_sql_parser::ast::ObjectType::Role => ObjectType::Role,
1606            mz_sql_parser::ast::ObjectType::Cluster => ObjectType::Cluster,
1607            mz_sql_parser::ast::ObjectType::ClusterReplica => ObjectType::ClusterReplica,
1608            mz_sql_parser::ast::ObjectType::Secret => ObjectType::Secret,
1609            mz_sql_parser::ast::ObjectType::Connection => ObjectType::Connection,
1610            mz_sql_parser::ast::ObjectType::Database => ObjectType::Database,
1611            mz_sql_parser::ast::ObjectType::Schema => ObjectType::Schema,
1612            mz_sql_parser::ast::ObjectType::Func => ObjectType::Func,
1613            mz_sql_parser::ast::ObjectType::NetworkPolicy => ObjectType::NetworkPolicy,
1614        }
1615    }
1616}
1617
1618impl From<CommentObjectId> for ObjectType {
1619    fn from(value: CommentObjectId) -> ObjectType {
1620        match value {
1621            CommentObjectId::Table(_) => ObjectType::Table,
1622            CommentObjectId::View(_) => ObjectType::View,
1623            CommentObjectId::MaterializedView(_) => ObjectType::MaterializedView,
1624            CommentObjectId::Source(_) => ObjectType::Source,
1625            CommentObjectId::Sink(_) => ObjectType::Sink,
1626            CommentObjectId::Index(_) => ObjectType::Index,
1627            CommentObjectId::Func(_) => ObjectType::Func,
1628            CommentObjectId::Connection(_) => ObjectType::Connection,
1629            CommentObjectId::Type(_) => ObjectType::Type,
1630            CommentObjectId::Secret(_) => ObjectType::Secret,
1631            CommentObjectId::Role(_) => ObjectType::Role,
1632            CommentObjectId::Database(_) => ObjectType::Database,
1633            CommentObjectId::Schema(_) => ObjectType::Schema,
1634            CommentObjectId::Cluster(_) => ObjectType::Cluster,
1635            CommentObjectId::ClusterReplica(_) => ObjectType::ClusterReplica,
1636            CommentObjectId::NetworkPolicy(_) => ObjectType::NetworkPolicy,
1637        }
1638    }
1639}
1640
1641impl Display for ObjectType {
1642    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1643        f.write_str(match self {
1644            ObjectType::Table => "TABLE",
1645            ObjectType::View => "VIEW",
1646            ObjectType::MaterializedView => "MATERIALIZED VIEW",
1647            ObjectType::Source => "SOURCE",
1648            ObjectType::Sink => "SINK",
1649            ObjectType::Index => "INDEX",
1650            ObjectType::Type => "TYPE",
1651            ObjectType::Role => "ROLE",
1652            ObjectType::Cluster => "CLUSTER",
1653            ObjectType::ClusterReplica => "CLUSTER REPLICA",
1654            ObjectType::Secret => "SECRET",
1655            ObjectType::Connection => "CONNECTION",
1656            ObjectType::Database => "DATABASE",
1657            ObjectType::Schema => "SCHEMA",
1658            ObjectType::Func => "FUNCTION",
1659            ObjectType::NetworkPolicy => "NETWORK POLICY",
1660        })
1661    }
1662}
1663
1664#[derive(
1665    Debug,
1666    Clone,
1667    PartialOrd,
1668    Ord,
1669    PartialEq,
1670    Eq,
1671    Hash,
1672    Copy,
1673    Deserialize,
1674    Serialize
1675)]
1676/// The types of objects in the system.
1677pub enum SystemObjectType {
1678    /// Catalog object type.
1679    Object(ObjectType),
1680    /// Entire system.
1681    System,
1682}
1683
1684impl SystemObjectType {
1685    /// Reports if the object type can be treated as a relation.
1686    pub fn is_relation(&self) -> bool {
1687        match self {
1688            SystemObjectType::Object(object_type) => object_type.is_relation(),
1689            SystemObjectType::System => false,
1690        }
1691    }
1692}
1693
1694impl Display for SystemObjectType {
1695    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1696        match self {
1697            SystemObjectType::Object(object_type) => std::fmt::Display::fmt(&object_type, f),
1698            SystemObjectType::System => f.write_str("SYSTEM"),
1699        }
1700    }
1701}
1702
1703/// Enum used to format object names in error messages.
1704#[derive(Debug, Clone, PartialEq, Eq)]
1705pub enum ErrorMessageObjectDescription {
1706    /// The name of a specific object.
1707    Object {
1708        /// Type of object.
1709        object_type: ObjectType,
1710        /// Name of object.
1711        object_name: Option<String>,
1712    },
1713    /// The name of the entire system.
1714    System,
1715}
1716
1717impl ErrorMessageObjectDescription {
1718    /// Generate a new [`ErrorMessageObjectDescription`] from an [`ObjectId`].
1719    pub fn from_id(
1720        object_id: &ObjectId,
1721        catalog: &dyn SessionCatalog,
1722    ) -> ErrorMessageObjectDescription {
1723        let object_name = match object_id {
1724            ObjectId::Cluster(cluster_id) => catalog.get_cluster(*cluster_id).name().to_string(),
1725            ObjectId::ClusterReplica((cluster_id, replica_id)) => catalog
1726                .get_cluster_replica(*cluster_id, *replica_id)
1727                .name()
1728                .to_string(),
1729            ObjectId::Database(database_id) => catalog.get_database(database_id).name().to_string(),
1730            ObjectId::Schema((database_spec, schema_spec)) => {
1731                let name = catalog.get_schema(database_spec, schema_spec).name();
1732                catalog.resolve_full_schema_name(name).to_string()
1733            }
1734            ObjectId::Role(role_id) => catalog.get_role(role_id).name().to_string(),
1735            ObjectId::Item(id) => {
1736                let name = catalog.get_item(id).name();
1737                catalog.resolve_full_name(name).to_string()
1738            }
1739            ObjectId::NetworkPolicy(network_policy_id) => catalog
1740                .get_network_policy(network_policy_id)
1741                .name()
1742                .to_string(),
1743        };
1744        ErrorMessageObjectDescription::Object {
1745            object_type: catalog.get_object_type(object_id),
1746            object_name: Some(object_name),
1747        }
1748    }
1749
1750    /// Generate a new [`ErrorMessageObjectDescription`] from a [`SystemObjectId`].
1751    pub fn from_sys_id(
1752        object_id: &SystemObjectId,
1753        catalog: &dyn SessionCatalog,
1754    ) -> ErrorMessageObjectDescription {
1755        match object_id {
1756            SystemObjectId::Object(object_id) => {
1757                ErrorMessageObjectDescription::from_id(object_id, catalog)
1758            }
1759            SystemObjectId::System => ErrorMessageObjectDescription::System,
1760        }
1761    }
1762
1763    /// Generate a new [`ErrorMessageObjectDescription`] from a [`SystemObjectType`].
1764    pub fn from_object_type(object_type: SystemObjectType) -> ErrorMessageObjectDescription {
1765        match object_type {
1766            SystemObjectType::Object(object_type) => ErrorMessageObjectDescription::Object {
1767                object_type,
1768                object_name: None,
1769            },
1770            SystemObjectType::System => ErrorMessageObjectDescription::System,
1771        }
1772    }
1773}
1774
1775impl Display for ErrorMessageObjectDescription {
1776    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1777        match self {
1778            ErrorMessageObjectDescription::Object {
1779                object_type,
1780                object_name,
1781            } => {
1782                let object_name = object_name
1783                    .as_ref()
1784                    .map(|object_name| format!(" {}", object_name.quoted()))
1785                    .unwrap_or_else(|| "".to_string());
1786                write!(f, "{object_type}{object_name}")
1787            }
1788            ErrorMessageObjectDescription::System => f.write_str("SYSTEM"),
1789        }
1790    }
1791}
1792
1793#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
1794// These attributes are needed because the key of a map must be a string. We also
1795// get the added benefit of flattening this struct in it's serialized form.
1796#[serde(into = "BTreeMap<String, RoleId>")]
1797#[serde(try_from = "BTreeMap<String, RoleId>")]
1798/// Represents the grantee and a grantor of a role membership.
1799pub struct RoleMembership {
1800    /// Key is the role that some role is a member of, value is the grantor role ID.
1801    // TODO(jkosh44) This structure does not allow a role to have multiple of the same membership
1802    // from different grantors. This isn't a problem now since we don't implement ADMIN OPTION, but
1803    // we should figure this out before implementing ADMIN OPTION. It will likely require a messy
1804    // migration.
1805    pub map: BTreeMap<RoleId, RoleId>,
1806}
1807
1808impl RoleMembership {
1809    /// Creates a new [`RoleMembership`].
1810    pub fn new() -> RoleMembership {
1811        RoleMembership {
1812            map: BTreeMap::new(),
1813        }
1814    }
1815}
1816
1817impl From<RoleMembership> for BTreeMap<String, RoleId> {
1818    fn from(value: RoleMembership) -> Self {
1819        value
1820            .map
1821            .into_iter()
1822            .map(|(k, v)| (k.to_string(), v))
1823            .collect()
1824    }
1825}
1826
1827impl TryFrom<BTreeMap<String, RoleId>> for RoleMembership {
1828    type Error = anyhow::Error;
1829
1830    fn try_from(value: BTreeMap<String, RoleId>) -> Result<Self, Self::Error> {
1831        Ok(RoleMembership {
1832            map: value
1833                .into_iter()
1834                .map(|(k, v)| Ok((RoleId::from_str(&k)?, v)))
1835                .collect::<Result<_, anyhow::Error>>()?,
1836        })
1837    }
1838}
1839
1840/// Specification for objects that will be affected by a default privilege.
1841#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
1842pub struct DefaultPrivilegeObject {
1843    /// The role id that created the object.
1844    pub role_id: RoleId,
1845    /// The database that the object is created in if Some, otherwise all databases.
1846    pub database_id: Option<DatabaseId>,
1847    /// The schema that the object is created in if Some, otherwise all databases.
1848    pub schema_id: Option<SchemaId>,
1849    /// The type of object.
1850    pub object_type: ObjectType,
1851}
1852
1853impl DefaultPrivilegeObject {
1854    /// Creates a new [`DefaultPrivilegeObject`].
1855    pub fn new(
1856        role_id: RoleId,
1857        database_id: Option<DatabaseId>,
1858        schema_id: Option<SchemaId>,
1859        object_type: ObjectType,
1860    ) -> DefaultPrivilegeObject {
1861        DefaultPrivilegeObject {
1862            role_id,
1863            database_id,
1864            schema_id,
1865            object_type,
1866        }
1867    }
1868}
1869
1870impl std::fmt::Display for DefaultPrivilegeObject {
1871    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1872        // TODO: Don't just wrap Debug.
1873        write!(f, "{self:?}")
1874    }
1875}
1876
1877/// Specification for the privileges that will be granted from default privileges.
1878#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
1879pub struct DefaultPrivilegeAclItem {
1880    /// The role that will receive the privileges.
1881    pub grantee: RoleId,
1882    /// The specific privileges granted.
1883    pub acl_mode: AclMode,
1884}
1885
1886impl DefaultPrivilegeAclItem {
1887    /// Creates a new [`DefaultPrivilegeAclItem`].
1888    pub fn new(grantee: RoleId, acl_mode: AclMode) -> DefaultPrivilegeAclItem {
1889        DefaultPrivilegeAclItem { grantee, acl_mode }
1890    }
1891
1892    /// Converts this [`DefaultPrivilegeAclItem`] into an [`MzAclItem`].
1893    pub fn mz_acl_item(self, grantor: RoleId) -> MzAclItem {
1894        MzAclItem {
1895            grantee: self.grantee,
1896            grantor,
1897            acl_mode: self.acl_mode,
1898        }
1899    }
1900}
1901
1902#[cfg(test)]
1903mod tests {
1904    use super::{CloudProvider, EnvironmentId, InvalidEnvironmentIdError};
1905
1906    #[mz_ore::test]
1907    fn test_environment_id() {
1908        for (input, expected) in [
1909            (
1910                "local-az1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-452",
1911                Ok(EnvironmentId {
1912                    cloud_provider: CloudProvider::Local,
1913                    cloud_provider_region: "az1".into(),
1914                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1915                    ordinal: 452,
1916                }),
1917            ),
1918            (
1919                "aws-us-east-1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1920                Ok(EnvironmentId {
1921                    cloud_provider: CloudProvider::Aws,
1922                    cloud_provider_region: "us-east-1".into(),
1923                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1924                    ordinal: 0,
1925                }),
1926            ),
1927            (
1928                "gcp-us-central1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1929                Ok(EnvironmentId {
1930                    cloud_provider: CloudProvider::Gcp,
1931                    cloud_provider_region: "us-central1".into(),
1932                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1933                    ordinal: 0,
1934                }),
1935            ),
1936            (
1937                "azure-australiaeast-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1938                Ok(EnvironmentId {
1939                    cloud_provider: CloudProvider::Azure,
1940                    cloud_provider_region: "australiaeast".into(),
1941                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1942                    ordinal: 0,
1943                }),
1944            ),
1945            (
1946                "generic-moon-station-11-darkside-1497a3b7-a455-4fc4-8752-b44a94b5f90a-0",
1947                Ok(EnvironmentId {
1948                    cloud_provider: CloudProvider::Generic,
1949                    cloud_provider_region: "moon-station-11-darkside".into(),
1950                    organization_id: "1497a3b7-a455-4fc4-8752-b44a94b5f90a".parse().unwrap(),
1951                    ordinal: 0,
1952                }),
1953            ),
1954            ("", Err(InvalidEnvironmentIdError)),
1955            (
1956                "local-az1-1497a3b7-a455-4fc4-8752-b44a94b5f90a-123456789",
1957                Err(InvalidEnvironmentIdError),
1958            ),
1959            (
1960                "local-1497a3b7-a455-4fc4-8752-b44a94b5f90a-452",
1961                Err(InvalidEnvironmentIdError),
1962            ),
1963            (
1964                "local-az1-1497a3b7-a455-4fc48752-b44a94b5f90a-452",
1965                Err(InvalidEnvironmentIdError),
1966            ),
1967        ] {
1968            let actual = input.parse();
1969            assert_eq!(expected, actual, "input = {}", input);
1970            if let Ok(actual) = actual {
1971                assert_eq!(input, actual.to_string(), "input = {}", input);
1972            }
1973        }
1974    }
1975}