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