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