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