Skip to main content

mz_adapter/catalog/
open.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//! Logic related to opening a [`Catalog`].
11
12mod builtin_schema_migration;
13
14use std::collections::{BTreeMap, BTreeSet};
15use std::num::NonZeroU32;
16use std::sync::Arc;
17use std::sync::atomic::AtomicU64;
18use std::time::{Duration, Instant};
19
20use futures::future::{BoxFuture, FutureExt};
21use itertools::{Either, Itertools};
22use mz_adapter_types::bootstrap_builtin_cluster_config::BootstrapBuiltinClusterConfig;
23use mz_adapter_types::dyncfgs::ENABLE_EXPRESSION_CACHE;
24use mz_audit_log::{
25    CreateOrDropClusterReplicaReasonV1, EventDetails, EventType, ObjectType, VersionedEvent,
26};
27use mz_auth::hash::scram256_hash;
28use mz_catalog::SYSTEM_CONN_ID;
29use mz_catalog::builtin::{
30    BUILTIN_CLUSTER_REPLICAS, BUILTIN_CLUSTERS, BUILTIN_PREFIXES, BUILTIN_ROLES, BUILTINS, Builtin,
31    Fingerprint, MZ_CATALOG_RAW, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
32};
33use mz_catalog::config::StateConfig;
34use mz_catalog::durable::objects::{
35    SystemObjectDescription, SystemObjectMapping, SystemObjectUniqueIdentifier,
36};
37use mz_catalog::durable::{ClusterReplica, ClusterVariant, ClusterVariantManaged, Transaction};
38use mz_catalog::expr_cache::{
39    ExpressionCacheConfig, ExpressionCacheHandle, GlobalExpressions, LocalExpressions,
40};
41use mz_catalog::memory::error::{Error, ErrorKind};
42use mz_catalog::memory::objects::{
43    BootstrapStateUpdateKind, CommentsMap, DefaultPrivileges, RoleAuth, StateUpdate,
44};
45use mz_controller::clusters::ReplicaLogging;
46use mz_controller_types::ClusterId;
47use mz_ore::cast::usize_to_u64;
48use mz_ore::collections::HashSet;
49use mz_ore::now::{SYSTEM_TIME, to_datetime};
50use mz_ore::{instrument, soft_assert_no_log};
51use mz_repr::adt::mz_acl_item::PrivilegeMap;
52use mz_repr::namespaces::is_unstable_schema;
53use mz_repr::{CatalogItemId, Diff, GlobalId, Timestamp};
54use mz_sql::catalog::{CatalogError as SqlCatalogError, CatalogItemType, RoleMembership, RoleVars};
55use mz_sql::func::OP_IMPLS;
56use mz_sql::names::CommentObjectId;
57use mz_sql::rbac;
58use mz_sql::session::user::{MZ_SYSTEM_ROLE_ID, SYSTEM_USER};
59use mz_sql::session::vars::{SessionVars, SystemVars, VarError, VarInput};
60use mz_storage_client::controller::{StorageMetadata, StorageTxn};
61use mz_storage_client::storage_collections::StorageCollections;
62use tracing::{Instrument, info, warn};
63use uuid::Uuid;
64
65// DO NOT add any more imports from `crate` outside of `crate::catalog`.
66use crate::AdapterError;
67use crate::catalog::migrate::{self, get_migration_version, set_migration_version};
68use crate::catalog::state::LocalExpressionCache;
69use crate::catalog::{BuiltinTableUpdate, Catalog, CatalogState, Config, is_reserved_name};
70
71pub struct InitializeStateResult {
72    /// An initialized [`CatalogState`].
73    pub state: CatalogState,
74    /// A set of new shards that may need to be initialized (only used by 0dt migration).
75    pub migrated_storage_collections_0dt: BTreeSet<CatalogItemId>,
76    /// A set of new builtin items.
77    pub new_builtin_collections: BTreeSet<GlobalId>,
78    /// A list of builtin table updates corresponding to the initialized state.
79    pub builtin_table_updates: Vec<BuiltinTableUpdate>,
80    /// The version of the catalog that existed before initializing the catalog.
81    pub last_seen_version: String,
82    /// A handle to the expression cache if it's enabled.
83    pub expr_cache_handle: Option<ExpressionCacheHandle>,
84    /// The global expressions that were cached in `expr_cache_handle`.
85    pub cached_global_exprs: BTreeMap<GlobalId, GlobalExpressions>,
86    /// The local expressions that were NOT cached in `expr_cache_handle`.
87    pub uncached_local_exprs: BTreeMap<GlobalId, LocalExpressions>,
88}
89
90pub struct OpenCatalogResult {
91    /// An opened [`Catalog`].
92    pub catalog: Catalog,
93    /// A set of new shards that may need to be initialized.
94    pub migrated_storage_collections_0dt: BTreeSet<CatalogItemId>,
95    /// A set of new builtin items.
96    pub new_builtin_collections: BTreeSet<GlobalId>,
97    /// A list of builtin table updates corresponding to the initialized state.
98    pub builtin_table_updates: Vec<BuiltinTableUpdate>,
99    /// The global expressions that were cached in the expression cache.
100    pub cached_global_exprs: BTreeMap<GlobalId, GlobalExpressions>,
101    /// The local expressions that were NOT cached in the expression cache.
102    pub uncached_local_exprs: BTreeMap<GlobalId, LocalExpressions>,
103}
104
105impl Catalog {
106    /// Initializes a CatalogState. Separate from [`Catalog::open`] to avoid depending on state
107    /// external to a [mz_catalog::durable::DurableCatalogState]
108    /// (for example: no [mz_secrets::SecretsReader]).
109    pub async fn initialize_state<'a>(
110        config: StateConfig,
111        storage: &'a mut Box<dyn mz_catalog::durable::DurableCatalogState>,
112    ) -> Result<InitializeStateResult, AdapterError> {
113        for builtin_role in BUILTIN_ROLES {
114            assert!(
115                is_reserved_name(builtin_role.name),
116                "builtin role {builtin_role:?} must start with one of the following prefixes {}",
117                BUILTIN_PREFIXES.join(", ")
118            );
119        }
120        for builtin_cluster in BUILTIN_CLUSTERS {
121            assert!(
122                is_reserved_name(builtin_cluster.name),
123                "builtin cluster {builtin_cluster:?} must start with one of the following prefixes {}",
124                BUILTIN_PREFIXES.join(", ")
125            );
126        }
127
128        let mut system_configuration = SystemVars::new().set_unsafe(config.unsafe_mode);
129        if config.all_features {
130            system_configuration.enable_all_feature_flags_by_default();
131        }
132
133        let mut state = CatalogState {
134            database_by_name: imbl::OrdMap::new(),
135            database_by_id: imbl::OrdMap::new(),
136            entry_by_id: imbl::OrdMap::new(),
137            entry_by_global_id: imbl::OrdMap::new(),
138            notices_by_dep_id: imbl::OrdMap::new(),
139            ambient_schemas_by_name: imbl::OrdMap::new(),
140            ambient_schemas_by_id: imbl::OrdMap::new(),
141            clusters_by_name: imbl::OrdMap::new(),
142            clusters_by_id: imbl::OrdMap::new(),
143            roles_by_name: imbl::OrdMap::new(),
144            roles_by_id: imbl::OrdMap::new(),
145            network_policies_by_id: imbl::OrdMap::new(),
146            role_auth_by_id: imbl::OrdMap::new(),
147            network_policies_by_name: imbl::OrdMap::new(),
148            system_configuration: Arc::new(system_configuration),
149            scoped_system_parameters: Default::default(),
150            default_privileges: Arc::new(DefaultPrivileges::default()),
151            system_privileges: Arc::new(PrivilegeMap::default()),
152            comments: Arc::new(CommentsMap::default()),
153            source_references: imbl::OrdMap::new(),
154            storage_metadata: Arc::new(StorageMetadata::default()),
155            temporary_schemas: imbl::OrdMap::new(),
156            mock_authentication_nonce: Default::default(),
157            config: mz_sql::catalog::CatalogConfig {
158                start_time: to_datetime((config.now)()),
159                start_instant: Instant::now(),
160                nonce: rand::random(),
161                environment_id: config.environment_id,
162                session_id: Uuid::new_v4(),
163                build_info: config.build_info,
164                now: config.now.clone(),
165                connection_context: config.connection_context,
166                helm_chart_version: config.helm_chart_version,
167            },
168            cluster_replica_sizes: config.cluster_replica_sizes,
169            availability_zones: config.availability_zones,
170            egress_addresses: config.egress_addresses,
171            aws_principal_context: config.aws_principal_context,
172            aws_privatelink_availability_zones: config.aws_privatelink_availability_zones,
173            http_host_name: config.http_host_name,
174            license_key: config.license_key,
175        };
176
177        let deploy_generation = storage.get_deployment_generation().await?;
178
179        let mut updates: Vec<_> = storage.sync_to_current_updates().await?;
180        assert!(!updates.is_empty(), "initial catalog snapshot is missing");
181        let mut txn = storage.transaction().await?;
182
183        // Migrate/update durable data before we start loading the in-memory catalog.
184        let new_builtin_collections = {
185            migrate::durable_migrate(
186                &mut txn,
187                state.config.environment_id.organization_id(),
188                config.boot_ts,
189            )?;
190            // Overwrite and persist selected parameter values in `remote_system_parameters` that
191            // was pulled from a remote frontend (e.g. LaunchDarkly) if present.
192            if let Some(remote_system_parameters) = config.remote_system_parameters {
193                for (name, value) in remote_system_parameters {
194                    txn.upsert_system_config(&name, value)?;
195                }
196                txn.set_system_config_synced_once()?;
197            }
198            // Add any new builtin objects and remove old ones.
199            let new_builtin_collections = add_new_remove_old_builtin_items_migration(&mut txn)?;
200            let builtin_bootstrap_cluster_config_map = BuiltinBootstrapClusterConfigMap {
201                system_cluster: config.builtin_system_cluster_config,
202                catalog_server_cluster: config.builtin_catalog_server_cluster_config,
203                probe_cluster: config.builtin_probe_cluster_config,
204                support_cluster: config.builtin_support_cluster_config,
205                analytics_cluster: config.builtin_analytics_cluster_config,
206            };
207            add_new_remove_old_builtin_clusters_migration(
208                &mut txn,
209                &builtin_bootstrap_cluster_config_map,
210                config.boot_ts,
211            )?;
212            add_new_remove_old_builtin_introspection_source_migration(&mut txn)?;
213            add_new_remove_old_builtin_cluster_replicas_migration(
214                &mut txn,
215                &builtin_bootstrap_cluster_config_map,
216                config.boot_ts,
217            )?;
218            add_new_remove_old_builtin_roles_migration(&mut txn)?;
219            remove_invalid_config_param_role_defaults_migration(&mut txn)?;
220            remove_pending_cluster_replicas_migration(&mut txn, config.boot_ts)?;
221
222            new_builtin_collections
223        };
224
225        let op_updates = txn.get_and_commit_op_updates();
226        updates.extend(op_updates);
227
228        let mut builtin_table_updates = Vec::new();
229
230        // Seed the in-memory catalog with values that don't come from the durable catalog.
231        {
232            // Set defaults from configuration passed in the provided `system_parameter_defaults`
233            // map.
234            for (name, value) in config.system_parameter_defaults {
235                match state.set_system_configuration_default(&name, VarInput::Flat(&value)) {
236                    Ok(_) => (),
237                    Err(Error {
238                        kind: ErrorKind::VarError(VarError::UnknownParameter(name)),
239                    }) => {
240                        warn!(%name, "cannot load unknown system parameter from catalog storage to set default parameter");
241                    }
242                    Err(e) => return Err(e.into()),
243                };
244            }
245            state.create_temporary_schema(&SYSTEM_CONN_ID, MZ_SYSTEM_ROLE_ID)?;
246        }
247
248        // Make life easier by consolidating all updates, so that we end up with only positive
249        // diffs.
250        let mut updates = into_consolidatable_updates_startup(updates, config.boot_ts);
251        differential_dataflow::consolidation::consolidate_updates(&mut updates);
252        soft_assert_no_log!(
253            updates.iter().all(|(_, _, diff)| *diff == Diff::ONE),
254            "consolidated updates should be positive during startup: {updates:?}"
255        );
256
257        let mut pre_item_updates = Vec::new();
258        let mut system_item_updates = Vec::new();
259        let mut item_updates = Vec::new();
260        let mut post_item_updates = Vec::new();
261        let mut audit_log_updates = Vec::new();
262        for (kind, ts, diff) in updates {
263            match kind {
264                BootstrapStateUpdateKind::Role(_)
265                | BootstrapStateUpdateKind::RoleAuth(_)
266                | BootstrapStateUpdateKind::Database(_)
267                | BootstrapStateUpdateKind::Schema(_)
268                | BootstrapStateUpdateKind::DefaultPrivilege(_)
269                | BootstrapStateUpdateKind::SystemPrivilege(_)
270                | BootstrapStateUpdateKind::SystemConfiguration(_)
271                | BootstrapStateUpdateKind::ClusterSystemConfiguration(_)
272                | BootstrapStateUpdateKind::ReplicaSystemConfiguration(_)
273                | BootstrapStateUpdateKind::Cluster(_)
274                | BootstrapStateUpdateKind::NetworkPolicy(_)
275                | BootstrapStateUpdateKind::ClusterReplica(_) => {
276                    pre_item_updates.push(StateUpdate {
277                        kind: kind.into(),
278                        ts,
279                        diff: diff.try_into().expect("valid diff"),
280                    })
281                }
282                BootstrapStateUpdateKind::IntrospectionSourceIndex(_)
283                | BootstrapStateUpdateKind::SystemObjectMapping(_) => {
284                    system_item_updates.push(StateUpdate {
285                        kind: kind.into(),
286                        ts,
287                        diff: diff.try_into().expect("valid diff"),
288                    })
289                }
290                BootstrapStateUpdateKind::Item(_) => item_updates.push(StateUpdate {
291                    kind: kind.into(),
292                    ts,
293                    diff: diff.try_into().expect("valid diff"),
294                }),
295                BootstrapStateUpdateKind::Comment(_)
296                | BootstrapStateUpdateKind::StorageCollectionMetadata(_)
297                | BootstrapStateUpdateKind::SourceReferences(_)
298                | BootstrapStateUpdateKind::UnfinalizedShard(_) => {
299                    post_item_updates.push((kind, ts, diff));
300                }
301                BootstrapStateUpdateKind::AuditLog(_) => {
302                    audit_log_updates.push(StateUpdate {
303                        kind: kind.into(),
304                        ts,
305                        diff: diff.try_into().expect("valid diff"),
306                    });
307                }
308            }
309        }
310
311        let (builtin_table_update, _catalog_updates) = state
312            .apply_updates(pre_item_updates, &mut LocalExpressionCache::Closed)
313            .await;
314        builtin_table_updates.extend(builtin_table_update);
315
316        // Ensure mz_system has a password if configured to have one.
317        // It's important we do this after the `pre_item_updates` so that
318        // the mz_system role exists in the catalog.
319        {
320            if let Some(password) = config.external_login_password_mz_system {
321                let role_auth = RoleAuth {
322                    role_id: MZ_SYSTEM_ROLE_ID,
323                    // builtin roles should always use a secure scram iteration
324                    // <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>
325                    password_hash: Some(
326                        scram256_hash(&password, &NonZeroU32::new(600_000).expect("known valid"))
327                            .map_err(|_| {
328                            AdapterError::Internal("Failed to hash mz_system password.".to_owned())
329                        })?,
330                    ),
331                    updated_at: SYSTEM_TIME(),
332                };
333                state
334                    .role_auth_by_id
335                    .insert(MZ_SYSTEM_ROLE_ID, role_auth.clone());
336                let builtin_table_update = state.generate_builtin_table_update(
337                    mz_catalog::memory::objects::StateUpdateKind::RoleAuth(role_auth.into()),
338                    mz_catalog::memory::objects::StateDiff::Addition,
339                );
340                builtin_table_updates.extend(builtin_table_update);
341            }
342        }
343
344        let expr_cache_start = Instant::now();
345        info!("startup: coordinator init: catalog open: expr cache open beginning");
346        // We wait until after the `pre_item_updates` to open the cache so we can get accurate
347        // dyncfgs because the `pre_item_updates` contains `SystemConfiguration` updates.
348        let enable_expr_cache_dyncfg = ENABLE_EXPRESSION_CACHE.get(state.system_config().dyncfgs());
349        let expr_cache_enabled = config
350            .enable_expression_cache_override
351            .unwrap_or(enable_expr_cache_dyncfg);
352        let (expr_cache_handle, cached_local_exprs, cached_global_exprs) = if expr_cache_enabled {
353            info!(
354                ?config.enable_expression_cache_override,
355                ?enable_expr_cache_dyncfg,
356                "using expression cache for startup"
357            );
358            let current_ids = txn
359                .get_items()
360                .flat_map(|item| {
361                    let gid = item.global_id.clone();
362                    let gids: Vec<_> = item.extra_versions.values().cloned().collect();
363                    std::iter::once(gid).chain(gids)
364                })
365                .chain(
366                    txn.get_system_object_mappings()
367                        .map(|som| som.unique_identifier.global_id),
368                )
369                .collect();
370            let dyncfgs = config.persist_client.dyncfgs().clone();
371            let build_version = if config.build_info.is_dev() {
372                // A single dev version can be used for many different builds, so we need to use
373                // the build version that is also enriched with build metadata.
374                config
375                    .build_info
376                    .semver_version_build()
377                    .expect("build ID is not available on your platform!")
378            } else {
379                config.build_info.semver_version()
380            };
381            let expr_cache_config = ExpressionCacheConfig {
382                build_version,
383                shard_id: txn
384                    .get_expression_cache_shard()
385                    .expect("expression cache shard should exist for opened catalogs"),
386                persist: config.persist_client,
387                current_ids,
388                remove_prior_versions: !config.read_only,
389                compact_shard: config.read_only,
390                dyncfgs,
391            };
392            let (expr_cache_handle, cached_local_exprs, cached_global_exprs) =
393                ExpressionCacheHandle::spawn_expression_cache(expr_cache_config).await;
394            (
395                Some(expr_cache_handle),
396                cached_local_exprs,
397                cached_global_exprs,
398            )
399        } else {
400            (None, BTreeMap::new(), BTreeMap::new())
401        };
402        let mut local_expr_cache = LocalExpressionCache::new(cached_local_exprs);
403        info!(
404            "startup: coordinator init: catalog open: expr cache open complete in {:?}",
405            expr_cache_start.elapsed()
406        );
407
408        // When initializing/bootstrapping, we don't use the catalog updates but
409        // instead load the catalog fully and then go ahead and apply commands
410        // to the controller(s). Maybe we _should_ instead use the same logic
411        // and return and use the updates from here. But that's at the very
412        // least future work.
413        let (builtin_table_update, _catalog_updates) = state
414            .apply_updates(system_item_updates, &mut local_expr_cache)
415            .await;
416        builtin_table_updates.extend(builtin_table_update);
417
418        let last_seen_version =
419            get_migration_version(&txn).map_or_else(|| "new".into(), |v| v.to_string());
420
421        let mz_authentication_mock_nonce =
422            txn.get_authentication_mock_nonce().ok_or_else(|| {
423                Error::new(ErrorKind::SettingError("authentication nonce".to_string()))
424            })?;
425
426        state.mock_authentication_nonce = Some(mz_authentication_mock_nonce);
427
428        // Migrate item ASTs.
429        let (builtin_table_update, _catalog_updates) = if !config.skip_migrations {
430            let migrate_result = migrate::migrate(
431                &mut state,
432                &mut txn,
433                &mut local_expr_cache,
434                item_updates,
435                config.now,
436                config.boot_ts,
437            )
438            .await
439            .map_err(|e| {
440                Error::new(ErrorKind::FailedCatalogMigration {
441                    last_seen_version: last_seen_version.clone(),
442                    this_version: config.build_info.version,
443                    cause: e.to_string(),
444                })
445            })?;
446            if !migrate_result.post_item_updates.is_empty() {
447                // Include any post-item-updates generated by migrations, and then consolidate
448                // them to ensure diffs are all positive.
449                post_item_updates.extend(migrate_result.post_item_updates);
450                // Push everything to the same timestamp so it consolidates cleanly.
451                if let Some(max_ts) = post_item_updates.iter().map(|(_, ts, _)| ts).max().cloned() {
452                    for (_, ts, _) in &mut post_item_updates {
453                        *ts = max_ts;
454                    }
455                }
456                differential_dataflow::consolidation::consolidate_updates(&mut post_item_updates);
457            }
458
459            (
460                migrate_result.builtin_table_updates,
461                migrate_result.catalog_updates,
462            )
463        } else {
464            state
465                .apply_updates(item_updates, &mut local_expr_cache)
466                .await
467        };
468        builtin_table_updates.extend(builtin_table_update);
469
470        let post_item_updates = post_item_updates
471            .into_iter()
472            .map(|(kind, ts, diff)| StateUpdate {
473                kind: kind.into(),
474                ts,
475                diff: diff.try_into().expect("valid diff"),
476            })
477            .collect();
478        let (builtin_table_update, _catalog_updates) = state
479            .apply_updates(post_item_updates, &mut local_expr_cache)
480            .await;
481        builtin_table_updates.extend(builtin_table_update);
482
483        // We don't need to apply the audit logs in memory, yet apply can be expensive when the
484        // audit log grows large. Therefore, we skip the apply step and just generate the builtin
485        // updates.
486        for audit_log_update in audit_log_updates {
487            builtin_table_updates.extend(
488                state.generate_builtin_table_update(audit_log_update.kind, audit_log_update.diff),
489            );
490        }
491
492        // Migrate builtin items.
493        let schema_migration_result = builtin_schema_migration::run(
494            config.build_info,
495            deploy_generation,
496            &mut txn,
497            config.builtin_item_migration_config,
498        )
499        .await?;
500
501        let state_updates = txn.get_and_commit_op_updates();
502
503        // When initializing/bootstrapping, we don't use the catalog updates but
504        // instead load the catalog fully and then go ahead and apply commands
505        // to the controller(s). Maybe we _should_ instead use the same logic
506        // and return and use the updates from here. But that's at the very
507        // least future work.
508        let (table_updates, _catalog_updates) = state
509            .apply_updates(state_updates, &mut local_expr_cache)
510            .await;
511        builtin_table_updates.extend(table_updates);
512        let builtin_table_updates = state.resolve_builtin_table_updates(builtin_table_updates);
513
514        // Bump the migration version immediately before committing.
515        set_migration_version(&mut txn, config.build_info.semver_version())?;
516
517        txn.commit(config.boot_ts).await?;
518
519        // Now that the migration is durable, run any requested deferred cleanup.
520        schema_migration_result.cleanup_action.await;
521
522        Ok(InitializeStateResult {
523            state,
524            migrated_storage_collections_0dt: schema_migration_result.replaced_items,
525            new_builtin_collections: new_builtin_collections.into_iter().collect(),
526            builtin_table_updates,
527            last_seen_version,
528            expr_cache_handle,
529            cached_global_exprs,
530            uncached_local_exprs: local_expr_cache.into_uncached_exprs(),
531        })
532    }
533
534    /// Opens or creates a catalog that stores data at `path`.
535    ///
536    /// Returns the catalog, metadata about builtin objects that have changed
537    /// schemas since last restart, a list of updates to builtin tables that
538    /// describe the initial state of the catalog, and the version of the
539    /// catalog before any migrations were performed.
540    ///
541    /// BOXED FUTURE: As of Nov 2023 the returned Future from this function was 17KB. This would
542    /// get stored on the stack which is bad for runtime performance, and blow up our stack usage.
543    /// Because of that we purposefully move this Future onto the heap (i.e. Box it).
544    #[instrument(name = "catalog::open")]
545    pub fn open(config: Config<'_>) -> BoxFuture<'static, Result<OpenCatalogResult, AdapterError>> {
546        async move {
547            let mut storage = config.storage;
548
549            let InitializeStateResult {
550                state,
551                migrated_storage_collections_0dt,
552                new_builtin_collections,
553                mut builtin_table_updates,
554                last_seen_version: _,
555                expr_cache_handle,
556                cached_global_exprs,
557                uncached_local_exprs,
558            } =
559                // BOXED FUTURE: As of Nov 2023 the returned Future from this function was 7.5KB. This would
560                // get stored on the stack which is bad for runtime performance, and blow up our stack usage.
561                // Because of that we purposefully move this Future onto the heap (i.e. Box it).
562                Self::initialize_state(config.state, &mut storage)
563                    .instrument(tracing::info_span!("catalog::initialize_state"))
564                    .boxed()
565                    .await?;
566
567            let catalog = Catalog {
568                state,
569                expr_cache_handle,
570                transient_revision: 1,
571                shared_transient_revision: Arc::new(AtomicU64::new(1)),
572                storage: Arc::new(tokio::sync::Mutex::new(storage)),
573            };
574
575            // Operators aren't stored in the catalog, but we would like them in
576            // introspection views.
577            for (op, func) in OP_IMPLS.iter() {
578                match func {
579                    mz_sql::func::Func::Scalar(impls) => {
580                        for imp in impls {
581                            builtin_table_updates.push(catalog.state.resolve_builtin_table_update(
582                                catalog.state.pack_op_update(op, imp.details(), Diff::ONE),
583                            ));
584                        }
585                    }
586                    _ => unreachable!("all operators must be scalar functions"),
587                }
588            }
589
590            for ip in &catalog.state.egress_addresses {
591                builtin_table_updates.push(
592                    catalog
593                        .state
594                        .resolve_builtin_table_update(catalog.state.pack_egress_ip_update(ip)?),
595                );
596            }
597
598            if !catalog.state.license_key.id.is_empty() {
599                builtin_table_updates.push(
600                    catalog.state.resolve_builtin_table_update(
601                        catalog
602                            .state
603                            .pack_license_key_update(&catalog.state.license_key)?,
604                    ),
605                );
606            }
607
608            catalog.storage().await.mark_bootstrap_complete().await;
609
610            Ok(OpenCatalogResult {
611                catalog,
612                migrated_storage_collections_0dt,
613                new_builtin_collections,
614                builtin_table_updates,
615                cached_global_exprs,
616                uncached_local_exprs,
617            })
618        }
619        .instrument(tracing::info_span!("catalog::open"))
620        .boxed()
621    }
622
623    /// Initializes STORAGE to understand all shards that `self` expects to
624    /// exist.
625    ///
626    /// Note that this must be done before creating/rendering collections
627    /// because the storage controller might not be aware of new system
628    /// collections created between versions.
629    async fn initialize_storage_state(
630        &mut self,
631        storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
632    ) -> Result<(), mz_catalog::durable::CatalogError> {
633        let collections = self
634            .entries()
635            .filter(|entry| entry.item().is_storage_collection())
636            .flat_map(|entry| entry.global_ids())
637            .collect();
638
639        // Clone the state so that any errors that occur do not leak any
640        // transformations on error.
641        let mut state = self.state.clone();
642
643        let mut storage = self.storage().await;
644        let shard_id = storage.shard_id();
645        let mut txn = storage.transaction().await?;
646
647        // Ensure the storage controller knows about the catalog shard and associates it with the
648        // `MZ_CATALOG_RAW` builtin source.
649        let item_id = self.resolve_builtin_storage_collection(&MZ_CATALOG_RAW);
650        let global_id = self.get_entry(&item_id).latest_global_id();
651        match txn.get_collection_metadata().get(&global_id) {
652            None => {
653                txn.insert_collection_metadata([(global_id, shard_id)].into())
654                    .map_err(mz_catalog::durable::DurableCatalogError::from)?;
655            }
656            Some(id) => assert_eq!(*id, shard_id),
657        }
658
659        storage_collections
660            .initialize_state(&mut txn, collections)
661            .await
662            .map_err(mz_catalog::durable::DurableCatalogError::from)?;
663
664        let updates = txn.get_and_commit_op_updates();
665        let (builtin_updates, catalog_updates) = state
666            .apply_updates(updates, &mut LocalExpressionCache::Closed)
667            .await;
668        assert!(
669            builtin_updates.is_empty(),
670            "storage is not allowed to generate catalog changes that would cause changes to builtin tables"
671        );
672        assert!(
673            catalog_updates.is_empty(),
674            "storage is not allowed to generate catalog changes that would change the catalog or controller state"
675        );
676        let commit_ts = txn.upper();
677        txn.commit(commit_ts).await?;
678        drop(storage);
679
680        // Save updated state.
681        self.state = state;
682        Ok(())
683    }
684
685    /// [`mz_controller::Controller`] depends on durable catalog state to boot,
686    /// so make it available and initialize the controller.
687    pub async fn initialize_controller(
688        &mut self,
689        config: mz_controller::ControllerConfig,
690        envd_epoch: core::num::NonZeroI64,
691        read_only: bool,
692    ) -> Result<mz_controller::Controller, mz_catalog::durable::CatalogError> {
693        let controller_start = Instant::now();
694        info!("startup: controller init: beginning");
695
696        let controller = {
697            let mut storage = self.storage().await;
698            let mut tx = storage.transaction().await?;
699            mz_controller::prepare_initialization(&mut tx)
700                .map_err(mz_catalog::durable::DurableCatalogError::from)?;
701            let updates = tx.get_and_commit_op_updates();
702            assert!(
703                updates.is_empty(),
704                "initializing controller should not produce updates: {updates:?}"
705            );
706            let commit_ts = tx.upper();
707            tx.commit(commit_ts).await?;
708
709            let read_only_tx = storage.transaction().await?;
710
711            mz_controller::Controller::new(config, envd_epoch, read_only, &read_only_tx).await
712        };
713
714        self.initialize_storage_state(&controller.storage_collections)
715            .await?;
716
717        info!(
718            "startup: controller init: complete in {:?}",
719            controller_start.elapsed()
720        );
721
722        Ok(controller)
723    }
724
725    /// Politely releases all external resources that can only be released in an async context.
726    pub async fn expire(self) {
727        // If no one else holds a reference to storage, then clean up the storage resources.
728        // Otherwise, hopefully the other reference cleans up the resources when it's dropped.
729        if let Some(storage) = Arc::into_inner(self.storage) {
730            let storage = storage.into_inner();
731            storage.expire().await;
732        }
733    }
734}
735
736impl CatalogState {
737    /// Set the default value for `name`, which is the value it will be reset to.
738    fn set_system_configuration_default(
739        &mut self,
740        name: &str,
741        value: VarInput,
742    ) -> Result<(), Error> {
743        Ok(Arc::make_mut(&mut self.system_configuration).set_default(name, value)?)
744    }
745}
746
747/// Updates the catalog with new and removed builtin items.
748///
749/// Returns the list of new builtin [`GlobalId`]s.
750fn add_new_remove_old_builtin_items_migration(
751    txn: &mut mz_catalog::durable::Transaction<'_>,
752) -> Result<Vec<GlobalId>, mz_catalog::durable::CatalogError> {
753    let mut new_builtin_mappings = Vec::new();
754    // Used to validate unique descriptions.
755    let mut builtin_descs = HashSet::new();
756
757    // We compare the builtin items that are compiled into the binary with the builtin items that
758    // are persisted in the catalog to discover new and deleted builtin items.
759    let mut builtins = Vec::new();
760    for builtin in BUILTINS::iter() {
761        let desc = SystemObjectDescription {
762            schema_name: builtin.schema().to_string(),
763            object_type: builtin.catalog_item_type(),
764            object_name: builtin.name().to_string(),
765        };
766        // Validate that the description is unique.
767        if !builtin_descs.insert(desc.clone()) {
768            panic!(
769                "duplicate builtin description: {:?}, {:?}",
770                SystemObjectDescription {
771                    schema_name: builtin.schema().to_string(),
772                    object_type: builtin.catalog_item_type(),
773                    object_name: builtin.name().to_string(),
774                },
775                builtin
776            );
777        }
778        builtins.push((desc, builtin));
779    }
780
781    let mut system_object_mappings: BTreeMap<_, _> = txn
782        .get_system_object_mappings()
783        .map(|system_object_mapping| {
784            (
785                system_object_mapping.description.clone(),
786                system_object_mapping,
787            )
788        })
789        .collect();
790
791    let (existing_builtins, new_builtins): (Vec<_>, Vec<_>) =
792        builtins.into_iter().partition_map(|(desc, builtin)| {
793            let fingerprint = match builtin.runtime_alterable() {
794                false => builtin.fingerprint(),
795                true => RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL.into(),
796            };
797            match system_object_mappings.remove(&desc) {
798                Some(system_object_mapping) => {
799                    Either::Left((builtin, system_object_mapping, fingerprint))
800                }
801                None => Either::Right((builtin, fingerprint)),
802            }
803        });
804    let new_builtin_ids = txn.allocate_system_item_ids(usize_to_u64(new_builtins.len()))?;
805    let new_builtins: Vec<_> = new_builtins
806        .into_iter()
807        .zip_eq(new_builtin_ids.clone())
808        .collect();
809
810    // Add new builtin items to catalog.
811    for ((builtin, fingerprint), (catalog_id, global_id)) in new_builtins.iter().cloned() {
812        new_builtin_mappings.push(SystemObjectMapping {
813            description: SystemObjectDescription {
814                schema_name: builtin.schema().to_string(),
815                object_type: builtin.catalog_item_type(),
816                object_name: builtin.name().to_string(),
817            },
818            unique_identifier: SystemObjectUniqueIdentifier {
819                catalog_id,
820                global_id,
821                fingerprint,
822            },
823        });
824
825        // Runtime-alterable system objects are durably recorded to the
826        // usual items collection, so that they can be later altered at
827        // runtime by their owner (i.e., outside of the usual builtin
828        // migration framework that requires changes to the binary
829        // itself).
830        let handled_runtime_alterable = match builtin {
831            Builtin::Connection(c) if c.runtime_alterable => {
832                let mut acl_items = vec![rbac::owner_privilege(
833                    mz_sql::catalog::ObjectType::Connection,
834                    c.owner_id.clone(),
835                )];
836                acl_items.extend_from_slice(c.access);
837                // Builtin Connections cannot be versioned.
838                let versions = BTreeMap::new();
839
840                txn.insert_item(
841                    catalog_id,
842                    c.oid,
843                    global_id,
844                    mz_catalog::durable::initialize::resolve_system_schema(c.schema).id,
845                    c.name,
846                    c.sql.into(),
847                    *c.owner_id,
848                    acl_items,
849                    versions,
850                )?;
851                true
852            }
853            _ => false,
854        };
855        assert_eq!(
856            builtin.runtime_alterable(),
857            handled_runtime_alterable,
858            "runtime alterable object was not handled by migration",
859        );
860    }
861    txn.set_system_object_mappings(new_builtin_mappings)?;
862
863    // Update comments of all builtin objects
864    let builtins_with_catalog_ids = existing_builtins
865        .iter()
866        .map(|(b, m, _)| (*b, m.unique_identifier.catalog_id))
867        .chain(
868            new_builtins
869                .into_iter()
870                .map(|((b, _), (catalog_id, _))| (b, catalog_id)),
871        );
872
873    for (builtin, id) in builtins_with_catalog_ids {
874        let (comment_id, desc, comments) = match builtin {
875            Builtin::Source(s) => (CommentObjectId::Source(id), &s.desc, &s.column_comments),
876            Builtin::View(v) => (CommentObjectId::View(id), &v.desc, &v.column_comments),
877            Builtin::Table(t) => (CommentObjectId::Table(id), &t.desc, &t.column_comments),
878            Builtin::MaterializedView(mv) => (
879                CommentObjectId::MaterializedView(id),
880                &mv.desc,
881                &mv.column_comments,
882            ),
883            Builtin::Log(_)
884            | Builtin::Type(_)
885            | Builtin::Func(_)
886            | Builtin::Index(_)
887            | Builtin::Connection(_) => continue,
888        };
889        // Drop comments under every relation-style `CommentObjectId` variant
890        // for this id, not just the current one. When a builtin's type changes
891        // (e.g. Table -> MaterializedView) but its catalog id is preserved, the
892        // prior type's comment rows would otherwise linger forever.
893        txn.drop_comments(&BTreeSet::from_iter([
894            CommentObjectId::Table(id),
895            CommentObjectId::View(id),
896            CommentObjectId::MaterializedView(id),
897            CommentObjectId::Source(id),
898        ]))?;
899
900        let mut comments = comments.clone();
901        for (col_idx, name) in desc.iter_names().enumerate() {
902            if let Some(comment) = comments.remove(name.as_str()) {
903                // Comment column indices are 1 based
904                txn.update_comment(comment_id, Some(col_idx + 1), Some(comment.to_owned()))?;
905            }
906        }
907        assert!(
908            comments.is_empty(),
909            "builtin object contains dangling comments that don't correspond to columns {comments:?}"
910        );
911    }
912
913    // Anything left in `system_object_mappings` must have been deleted and should be removed from
914    // the catalog.
915    let mut deleted_system_objects = BTreeSet::new();
916    let mut deleted_runtime_alterable_system_ids = BTreeSet::new();
917    let mut deleted_comments = BTreeSet::new();
918    for (desc, mapping) in system_object_mappings {
919        deleted_system_objects.insert(mapping.description);
920        if mapping.unique_identifier.fingerprint == RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL {
921            deleted_runtime_alterable_system_ids.insert(mapping.unique_identifier.catalog_id);
922        }
923
924        let id = mapping.unique_identifier.catalog_id;
925        let comment_id = match desc.object_type {
926            CatalogItemType::Table => CommentObjectId::Table(id),
927            CatalogItemType::Source => CommentObjectId::Source(id),
928            CatalogItemType::View => CommentObjectId::View(id),
929            CatalogItemType::MaterializedView => CommentObjectId::MaterializedView(id),
930            CatalogItemType::Sink
931            | CatalogItemType::Index
932            | CatalogItemType::Type
933            | CatalogItemType::Func
934            | CatalogItemType::Secret
935            | CatalogItemType::Connection => continue,
936        };
937        deleted_comments.insert(comment_id);
938    }
939    // If you are 100% positive that it is safe to delete a system object outside any of the
940    // unstable schemas, then add it to this set. Make sure that no prod environments are
941    // using this object and that the upgrade checker does not show any issues.
942    //
943    // Objects can be removed from this set after one release.
944    let delete_exceptions: HashSet<SystemObjectDescription> = [].into();
945    // TODO(jkosh44) Technically we could support changing the type of a builtin object outside
946    // of unstable schemas (i.e. from a table to a view). However, builtin migrations don't currently
947    // handle that scenario correctly.
948    assert!(
949        deleted_system_objects
950            .iter()
951            // It's okay if Indexes change because they're inherently ephemeral.
952            .filter(|object| object.object_type != CatalogItemType::Index)
953            .all(
954                |deleted_object| is_unstable_schema(&deleted_object.schema_name)
955                    || delete_exceptions.contains(deleted_object)
956            ),
957        "only objects in unstable schemas can be deleted, deleted objects: {:?}",
958        deleted_system_objects
959    );
960    txn.drop_comments(&deleted_comments)?;
961    txn.remove_items(&deleted_runtime_alterable_system_ids)?;
962    txn.remove_system_object_mappings(deleted_system_objects)?;
963
964    // Filter down to just the GlobalIds which are used to track the underlying collections.
965    let new_builtin_collections = new_builtin_ids
966        .into_iter()
967        .map(|(_catalog_id, global_id)| global_id)
968        .collect();
969
970    Ok(new_builtin_collections)
971}
972
973fn add_new_remove_old_builtin_clusters_migration(
974    txn: &mut mz_catalog::durable::Transaction<'_>,
975    builtin_cluster_config_map: &BuiltinBootstrapClusterConfigMap,
976    boot_ts: Timestamp,
977) -> Result<(), mz_catalog::durable::CatalogError> {
978    let mut durable_clusters: BTreeMap<_, _> = txn
979        .get_clusters()
980        .filter(|cluster| cluster.id.is_system())
981        .map(|cluster| (cluster.name.to_string(), cluster))
982        .collect();
983
984    // Add new clusters.
985    for builtin_cluster in BUILTIN_CLUSTERS {
986        if durable_clusters.remove(builtin_cluster.name).is_none() {
987            let cluster_config = builtin_cluster_config_map.get_config(builtin_cluster.name)?;
988
989            let cluster_id = txn.insert_system_cluster(
990                builtin_cluster.name,
991                vec![],
992                builtin_cluster.privileges.to_vec(),
993                builtin_cluster.owner_id.to_owned(),
994                mz_catalog::durable::ClusterConfig {
995                    variant: mz_catalog::durable::ClusterVariant::Managed(ClusterVariantManaged {
996                        size: cluster_config.size,
997                        availability_zones: vec![],
998                        replication_factor: cluster_config.replication_factor,
999                        logging: default_logging_config(),
1000                        optimizer_feature_overrides: Default::default(),
1001                        schedule: Default::default(),
1002                        auto_scaling_strategy: None,
1003                        reconfiguration: None,
1004                        burst: None,
1005                    }),
1006                    workload_class: None,
1007                },
1008                &HashSet::new(),
1009            )?;
1010
1011            let audit_id = txn.allocate_audit_log_id()?;
1012            txn.insert_audit_log_event(VersionedEvent::new(
1013                audit_id,
1014                EventType::Create,
1015                ObjectType::Cluster,
1016                EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1017                    id: cluster_id.to_string(),
1018                    name: builtin_cluster.name.to_string(),
1019                }),
1020                None,
1021                boot_ts.into(),
1022            ));
1023        }
1024    }
1025
1026    // Remove old clusters.
1027    let old_clusters = durable_clusters
1028        .values()
1029        .map(|cluster| cluster.id)
1030        .collect();
1031    txn.remove_clusters(&old_clusters)?;
1032
1033    for (_name, cluster) in &durable_clusters {
1034        let audit_id = txn.allocate_audit_log_id()?;
1035        txn.insert_audit_log_event(VersionedEvent::new(
1036            audit_id,
1037            EventType::Drop,
1038            ObjectType::Cluster,
1039            EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1040                id: cluster.id.to_string(),
1041                name: cluster.name.clone(),
1042            }),
1043            None,
1044            boot_ts.into(),
1045        ));
1046    }
1047
1048    Ok(())
1049}
1050
1051fn add_new_remove_old_builtin_introspection_source_migration(
1052    txn: &mut mz_catalog::durable::Transaction<'_>,
1053) -> Result<(), AdapterError> {
1054    let mut new_indexes = Vec::new();
1055    let mut removed_indexes = BTreeSet::new();
1056    for cluster in txn.get_clusters() {
1057        let mut introspection_source_index_ids = txn.get_introspection_source_indexes(cluster.id);
1058
1059        let mut new_logs = Vec::new();
1060
1061        for log in BUILTINS::logs() {
1062            if introspection_source_index_ids.remove(log.name).is_none() {
1063                new_logs.push(log);
1064            }
1065        }
1066
1067        for log in new_logs {
1068            let (item_id, gid) =
1069                Transaction::allocate_introspection_source_index_id(&cluster.id, log.variant);
1070            new_indexes.push((cluster.id, log.name.to_string(), item_id, gid));
1071        }
1072
1073        // Anything left in `introspection_source_index_ids` must have been deleted and should be
1074        // removed from the catalog.
1075        removed_indexes.extend(
1076            introspection_source_index_ids
1077                .into_keys()
1078                .map(|name| (cluster.id, name.to_string())),
1079        );
1080    }
1081    txn.insert_introspection_source_indexes(new_indexes, &HashSet::new())?;
1082    txn.remove_introspection_source_indexes(removed_indexes)?;
1083    Ok(())
1084}
1085
1086fn add_new_remove_old_builtin_roles_migration(
1087    txn: &mut mz_catalog::durable::Transaction<'_>,
1088) -> Result<(), mz_catalog::durable::CatalogError> {
1089    let mut durable_roles: BTreeMap<_, _> = txn
1090        .get_roles()
1091        .filter(|role| role.id.is_system() || role.id.is_predefined())
1092        .map(|role| (role.name.to_string(), role))
1093        .collect();
1094
1095    // Add new roles.
1096    for builtin_role in BUILTIN_ROLES {
1097        if durable_roles.remove(builtin_role.name).is_none() {
1098            txn.insert_builtin_role(
1099                builtin_role.id,
1100                builtin_role.name.to_string(),
1101                builtin_role.attributes.clone(),
1102                RoleMembership::new(),
1103                RoleVars::default(),
1104                builtin_role.oid,
1105            )?;
1106        }
1107    }
1108
1109    // Remove old roles.
1110    let old_roles = durable_roles.values().map(|role| role.id).collect();
1111    txn.remove_roles(&old_roles)?;
1112
1113    Ok(())
1114}
1115
1116fn add_new_remove_old_builtin_cluster_replicas_migration(
1117    txn: &mut Transaction<'_>,
1118    builtin_cluster_config_map: &BuiltinBootstrapClusterConfigMap,
1119    boot_ts: Timestamp,
1120) -> Result<(), AdapterError> {
1121    let cluster_lookup: BTreeMap<_, _> = txn
1122        .get_clusters()
1123        .map(|cluster| (cluster.name.clone(), cluster.clone()))
1124        .collect();
1125
1126    let cluster_id_to_name: BTreeMap<ClusterId, String> = cluster_lookup
1127        .values()
1128        .map(|cluster| (cluster.id, cluster.name.clone()))
1129        .collect();
1130
1131    let mut durable_replicas: BTreeMap<ClusterId, BTreeMap<String, ClusterReplica>> = txn
1132        .get_cluster_replicas()
1133        .filter(|replica| replica.replica_id.is_system())
1134        .fold(BTreeMap::new(), |mut acc, replica| {
1135            acc.entry(replica.cluster_id)
1136                .or_insert_with(BTreeMap::new)
1137                .insert(replica.name.to_string(), replica);
1138            acc
1139        });
1140
1141    // Add new replicas.
1142    for builtin_replica in BUILTIN_CLUSTER_REPLICAS {
1143        let cluster = cluster_lookup
1144            .get(builtin_replica.cluster_name)
1145            .expect("builtin cluster replica references non-existent cluster");
1146        // `empty_map` is a hack to simplify the if statement below.
1147        let mut empty_map: BTreeMap<String, ClusterReplica> = BTreeMap::new();
1148        let replica_names = durable_replicas
1149            .get_mut(&cluster.id)
1150            .unwrap_or(&mut empty_map);
1151
1152        let builtin_cluster_bootstrap_config =
1153            builtin_cluster_config_map.get_config(builtin_replica.cluster_name)?;
1154        if replica_names.remove(builtin_replica.name).is_none()
1155            // NOTE(SangJunBak): We need to explicitly check the replication factor because
1156            // BUILT_IN_CLUSTER_REPLICAS is constant throughout all deployments but the replication
1157            // factor is configurable on bootstrap.
1158            && builtin_cluster_bootstrap_config.replication_factor > 0
1159        {
1160            let replica_size = match cluster.config.variant {
1161                ClusterVariant::Managed(ClusterVariantManaged { ref size, .. }) => size.clone(),
1162                ClusterVariant::Unmanaged => builtin_cluster_bootstrap_config.size.clone(),
1163            };
1164
1165            let config = builtin_cluster_replica_config(replica_size.clone());
1166            // Builtin replicas live on system clusters. This runs inside the
1167            // catalog-open transaction with no coordinator, so allocating from
1168            // the same transaction is single-source and safe.
1169            let replica_id = txn.allocate_system_replica_id()?;
1170            txn.insert_cluster_replica_with_id(
1171                cluster.id,
1172                replica_id,
1173                builtin_replica.name,
1174                config,
1175                MZ_SYSTEM_ROLE_ID,
1176            )?;
1177
1178            let audit_id = txn.allocate_audit_log_id()?;
1179            txn.insert_audit_log_event(VersionedEvent::new(
1180                audit_id,
1181                EventType::Create,
1182                ObjectType::ClusterReplica,
1183                EventDetails::CreateClusterReplicaV4(mz_audit_log::CreateClusterReplicaV4 {
1184                    cluster_id: cluster.id.to_string(),
1185                    cluster_name: cluster.name.clone(),
1186                    replica_id: Some(replica_id.to_string()),
1187                    replica_name: builtin_replica.name.to_string(),
1188                    logical_size: replica_size,
1189                    billed_as: None,
1190                    internal: false,
1191                    reason: CreateOrDropClusterReplicaReasonV1::System,
1192                    scheduling_policies: None,
1193                }),
1194                None,
1195                boot_ts.into(),
1196            ));
1197        }
1198    }
1199
1200    // Remove old replicas.
1201    let old_replicas: Vec<_> = durable_replicas
1202        .values()
1203        .flat_map(|replicas| replicas.values())
1204        .collect();
1205    let old_replica_ids = old_replicas.iter().map(|r| r.replica_id).collect();
1206    txn.remove_cluster_replicas(&old_replica_ids)?;
1207
1208    for replica in &old_replicas {
1209        let cluster_name = cluster_id_to_name
1210            .get(&replica.cluster_id)
1211            .cloned()
1212            .unwrap_or_else(|| "<unknown>".to_string());
1213
1214        let audit_id = txn.allocate_audit_log_id()?;
1215        txn.insert_audit_log_event(VersionedEvent::new(
1216            audit_id,
1217            EventType::Drop,
1218            ObjectType::ClusterReplica,
1219            EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
1220                cluster_id: replica.cluster_id.to_string(),
1221                cluster_name,
1222                replica_id: Some(replica.replica_id.to_string()),
1223                replica_name: replica.name.clone(),
1224                reason: CreateOrDropClusterReplicaReasonV1::System,
1225                scheduling_policies: None,
1226            }),
1227            None,
1228            boot_ts.into(),
1229        ));
1230    }
1231
1232    Ok(())
1233}
1234
1235/// Roles can have default values for configuration parameters, e.g. you can set a Role default for
1236/// the 'cluster' parameter.
1237///
1238/// This migration exists to remove the Role default for a configuration parameter, if the persisted
1239/// input is no longer valid. For example if we remove a configuration parameter or change the
1240/// accepted set of values.
1241fn remove_invalid_config_param_role_defaults_migration(
1242    txn: &mut Transaction<'_>,
1243) -> Result<(), AdapterError> {
1244    static BUILD_INFO: mz_build_info::BuildInfo = mz_build_info::build_info!();
1245
1246    let roles_to_migrate: BTreeMap<_, _> = txn
1247        .get_roles()
1248        .filter_map(|mut role| {
1249            // Create an empty SessionVars just so we can check if a var is valid.
1250            //
1251            // TODO(parkmycar): This is a bit hacky, instead we should have a static list of all
1252            // session variables.
1253            let session_vars = SessionVars::new_unchecked(&BUILD_INFO, SYSTEM_USER.clone(), None);
1254
1255            // Iterate over all of the variable defaults for this role.
1256            let mut invalid_roles_vars = BTreeMap::new();
1257            for (name, value) in &role.vars.map {
1258                // If one does not exist or its value is invalid, then mark it for removal.
1259                let Ok(session_var) = session_vars.inspect(name) else {
1260                    invalid_roles_vars.insert(name.clone(), value.clone());
1261                    continue;
1262                };
1263                if session_var.check(value.borrow()).is_err() {
1264                    invalid_roles_vars.insert(name.clone(), value.clone());
1265                }
1266            }
1267
1268            // If the role has no invalid values, nothing to do!
1269            if invalid_roles_vars.is_empty() {
1270                return None;
1271            }
1272
1273            tracing::warn!(?role, ?invalid_roles_vars, "removing invalid role vars");
1274
1275            // Otherwise, remove the variables from the role and return it to be updated.
1276            for (name, _value) in invalid_roles_vars {
1277                role.vars.map.remove(&name);
1278            }
1279            Some(role)
1280        })
1281        .map(|role| (role.id, role))
1282        .collect();
1283
1284    txn.update_roles_without_auth(roles_to_migrate)?;
1285
1286    Ok(())
1287}
1288
1289/// Cluster Replicas may be created ephemerally during an alter statement, these replicas
1290/// are marked as pending and should be cleaned up on catalog open.
1291fn remove_pending_cluster_replicas_migration(
1292    tx: &mut Transaction,
1293    boot_ts: mz_repr::Timestamp,
1294) -> Result<(), anyhow::Error> {
1295    // Build a map of cluster_id -> cluster_name for audit events.
1296    let cluster_names: BTreeMap<_, _> = tx.get_clusters().map(|c| (c.id, c.name)).collect();
1297
1298    let occurred_at = boot_ts.into();
1299
1300    for replica in tx.get_cluster_replicas().collect::<Vec<_>>() {
1301        if let mz_catalog::durable::ReplicaLocation::Managed { pending: true, .. } =
1302            replica.config.location
1303        {
1304            let cluster_name = cluster_names
1305                .get(&replica.cluster_id)
1306                .cloned()
1307                .unwrap_or_else(|| "<unknown>".to_string());
1308
1309            info!(
1310                "removing pending cluster replica '{}' from cluster '{}'",
1311                replica.name, cluster_name,
1312            );
1313
1314            tx.remove_cluster_replica(replica.replica_id)?;
1315
1316            // Emit an audit log event so that the drop is visible in
1317            // mz_audit_events, matching the create event that was
1318            // recorded when the pending replica was first created.
1319            let audit_id = tx.allocate_audit_log_id()?;
1320            tx.insert_audit_log_event(VersionedEvent::new(
1321                audit_id,
1322                EventType::Drop,
1323                ObjectType::ClusterReplica,
1324                EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
1325                    cluster_id: replica.cluster_id.to_string(),
1326                    cluster_name,
1327                    replica_id: Some(replica.replica_id.to_string()),
1328                    replica_name: replica.name,
1329                    reason: CreateOrDropClusterReplicaReasonV1::System,
1330                    scheduling_policies: None,
1331                }),
1332                None,
1333                occurred_at,
1334            ));
1335        }
1336    }
1337    Ok(())
1338}
1339
1340pub(crate) fn builtin_cluster_replica_config(
1341    replica_size: String,
1342) -> mz_catalog::durable::ReplicaConfig {
1343    mz_catalog::durable::ReplicaConfig {
1344        location: mz_catalog::durable::ReplicaLocation::Managed {
1345            availability_zones: Vec::new(),
1346            billed_as: None,
1347            pending: false,
1348            internal: false,
1349            size: replica_size,
1350        },
1351        logging: default_logging_config(),
1352    }
1353}
1354
1355fn default_logging_config() -> ReplicaLogging {
1356    ReplicaLogging {
1357        log_logging: false,
1358        interval: Some(Duration::from_secs(1)),
1359    }
1360}
1361
1362#[derive(Debug)]
1363pub struct BuiltinBootstrapClusterConfigMap {
1364    /// Size and replication factor to default system_cluster on bootstrap
1365    pub system_cluster: BootstrapBuiltinClusterConfig,
1366    /// Size and replication factor to default catalog_server_cluster on bootstrap
1367    pub catalog_server_cluster: BootstrapBuiltinClusterConfig,
1368    /// Size and replication factor to default probe_cluster on bootstrap
1369    pub probe_cluster: BootstrapBuiltinClusterConfig,
1370    /// Size and replication factor to default support_cluster on bootstrap
1371    pub support_cluster: BootstrapBuiltinClusterConfig,
1372    /// Size to default analytics_cluster on bootstrap
1373    pub analytics_cluster: BootstrapBuiltinClusterConfig,
1374}
1375
1376impl BuiltinBootstrapClusterConfigMap {
1377    /// Gets the size of the builtin cluster based on the provided name
1378    fn get_config(
1379        &self,
1380        cluster_name: &str,
1381    ) -> Result<BootstrapBuiltinClusterConfig, mz_catalog::durable::CatalogError> {
1382        let cluster_config = if cluster_name == mz_catalog::builtin::MZ_SYSTEM_CLUSTER.name {
1383            &self.system_cluster
1384        } else if cluster_name == mz_catalog::builtin::MZ_CATALOG_SERVER_CLUSTER.name {
1385            &self.catalog_server_cluster
1386        } else if cluster_name == mz_catalog::builtin::MZ_PROBE_CLUSTER.name {
1387            &self.probe_cluster
1388        } else if cluster_name == mz_catalog::builtin::MZ_SUPPORT_CLUSTER.name {
1389            &self.support_cluster
1390        } else if cluster_name == mz_catalog::builtin::MZ_ANALYTICS_CLUSTER.name {
1391            &self.analytics_cluster
1392        } else {
1393            return Err(mz_catalog::durable::CatalogError::Catalog(
1394                SqlCatalogError::UnexpectedBuiltinCluster(cluster_name.to_owned()),
1395            ));
1396        };
1397        Ok(cluster_config.clone())
1398    }
1399}
1400
1401/// Convert `updates` into a `Vec` that can be consolidated by doing the following:
1402///
1403///   - Convert each update into a type that implements [`std::cmp::Ord`].
1404///   - Update the timestamp of each update to the same value.
1405///   - Convert the diff of each update to a type that implements
1406///     [`differential_dataflow::difference::Semigroup`].
1407///
1408/// [`mz_catalog::memory::objects::StateUpdateKind`] does not implement [`std::cmp::Ord`] only
1409/// because it contains a variant for temporary items, which do not implement [`std::cmp::Ord`].
1410/// However, we know that during bootstrap no temporary items exist, because they are not persisted
1411/// and are only created after bootstrap is complete. So we forcibly convert each
1412/// [`mz_catalog::memory::objects::StateUpdateKind`] into an [`BootstrapStateUpdateKind`], which is
1413/// identical to [`mz_catalog::memory::objects::StateUpdateKind`] except it doesn't have a
1414/// temporary item variant and does implement [`std::cmp::Ord`].
1415///
1416/// WARNING: Do not call outside of startup.
1417pub(crate) fn into_consolidatable_updates_startup(
1418    updates: Vec<StateUpdate>,
1419    ts: Timestamp,
1420) -> Vec<(BootstrapStateUpdateKind, Timestamp, Diff)> {
1421    updates
1422        .into_iter()
1423        .map(|StateUpdate { kind, ts: _, diff }| {
1424            let kind: BootstrapStateUpdateKind = kind
1425                .try_into()
1426                .unwrap_or_else(|e| panic!("temporary items do not exist during bootstrap: {e:?}"));
1427            (kind, ts, Diff::from(diff))
1428        })
1429        .collect()
1430}