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::{ClusterReplicaSizeMap, StateConfig};
33use mz_catalog::durable::objects::{
34    SystemObjectDescription, SystemObjectMapping, SystemObjectUniqueIdentifier,
35};
36use mz_catalog::durable::{
37    ClusterReplica, ClusterReplicaSize, ClusterVariant, ClusterVariantManaged, ReplicaConfig,
38    ReplicaLocation, 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            // Populated from the durable catalog, which `sync_builtin_cluster_replica_sizes`
192            // reconciles with `config.cluster_replica_sizes`.
193            cluster_replica_sizes: ClusterReplicaSizeMap(BTreeMap::new()),
194            availability_zones: config.availability_zones,
195            egress_addresses: config.egress_addresses,
196            aws_principal_context: config.aws_principal_context,
197            aws_privatelink_availability_zones: config.aws_privatelink_availability_zones,
198            http_host_name: config.http_host_name,
199            license_key: config.license_key,
200        };
201
202        let deploy_generation = storage.get_deployment_generation().await?;
203
204        let mut updates: Vec<_> = storage.sync_to_current_updates().await?;
205        assert!(!updates.is_empty(), "initial catalog snapshot is missing");
206        let mut txn = storage.transaction().await?;
207
208        // Migrate/update durable data before we start loading the in-memory catalog.
209        let new_builtin_collections = {
210            migrate::durable_migrate(
211                &mut txn,
212                state.config.environment_id.organization_id(),
213                config.boot_ts,
214            )?;
215            // Overwrite and persist selected parameter values in `remote_system_parameters` that
216            // was pulled from a remote frontend (e.g. LaunchDarkly) if present.
217            if let Some(remote_system_parameters) = config.remote_system_parameters {
218                for (name, value) in remote_system_parameters {
219                    txn.upsert_system_config(&name, value)?;
220                }
221                txn.set_system_config_synced_once()?;
222            }
223            // Add any new builtin objects and remove old ones.
224            let new_builtin_collections = add_new_remove_old_builtin_items_migration(&mut txn)?;
225            let builtin_bootstrap_cluster_config_map = BuiltinBootstrapClusterConfigMap {
226                system_cluster: config.builtin_system_cluster_config,
227                catalog_server_cluster: config.builtin_catalog_server_cluster_config,
228                probe_cluster: config.builtin_probe_cluster_config,
229                support_cluster: config.builtin_support_cluster_config,
230                analytics_cluster: config.builtin_analytics_cluster_config,
231            };
232            add_new_remove_old_builtin_clusters_migration(
233                &mut txn,
234                &builtin_bootstrap_cluster_config_map,
235                config.boot_ts,
236            )?;
237            add_new_remove_old_builtin_introspection_source_migration(&mut txn)?;
238            reconcile_builtin_cluster_replicas(
239                &mut txn,
240                &builtin_bootstrap_cluster_config_map,
241                config.boot_ts,
242            )?;
243            add_new_remove_old_builtin_roles_migration(&mut txn)?;
244            // A read-only catalog can't commit, so it uses the durable sizes.
245            if !txn.is_read_only() {
246                sync_builtin_cluster_replica_sizes(&mut txn, &config.cluster_replica_sizes)?;
247            }
248            remove_invalid_config_param_role_defaults_migration(&mut txn)?;
249            remove_pending_cluster_replicas_migration(&mut txn, config.boot_ts)?;
250
251            new_builtin_collections
252        };
253
254        let op_updates = txn.get_and_commit_op_updates();
255        updates.extend(op_updates);
256
257        let mut builtin_table_updates = Vec::new();
258
259        // Seed the in-memory catalog with values that don't come from the durable catalog.
260        {
261            // Set defaults from configuration passed in the provided `system_parameter_defaults`
262            // map.
263            for (name, value) in config.system_parameter_defaults {
264                match state.set_system_configuration_default(&name, VarInput::Flat(&value)) {
265                    Ok(_) => (),
266                    Err(Error {
267                        kind: ErrorKind::VarError(VarError::UnknownParameter(name)),
268                    }) => {
269                        warn!(%name, "cannot load unknown system parameter from catalog storage to set default parameter");
270                    }
271                    Err(e) => return Err(e.into()),
272                };
273            }
274        }
275
276        // Make life easier by consolidating all updates, so that we end up with only positive
277        // diffs.
278        let mut updates = into_consolidatable_updates_startup(updates, config.boot_ts);
279        differential_dataflow::consolidation::consolidate_updates(&mut updates);
280        soft_assert_no_log!(
281            updates.iter().all(|(_, _, diff)| *diff == Diff::ONE),
282            "consolidated updates should be positive during startup: {updates:?}"
283        );
284
285        let mut pre_item_updates = Vec::new();
286        let mut system_item_updates = Vec::new();
287        let mut item_updates = Vec::new();
288        let mut post_item_updates = Vec::new();
289        let mut audit_log_updates = Vec::new();
290        for (kind, ts, diff) in updates {
291            match kind {
292                StateUpdateKind::Role(_)
293                | StateUpdateKind::RoleAuth(_)
294                | StateUpdateKind::Database(_)
295                | StateUpdateKind::Schema(_)
296                | StateUpdateKind::DefaultPrivilege(_)
297                | StateUpdateKind::SystemPrivilege(_)
298                | StateUpdateKind::SystemConfiguration(_)
299                | StateUpdateKind::ClusterSystemConfiguration(_)
300                | StateUpdateKind::ReplicaSystemConfiguration(_)
301                | StateUpdateKind::Cluster(_)
302                | StateUpdateKind::NetworkPolicy(_)
303                | StateUpdateKind::ClusterReplicaSize(_)
304                | StateUpdateKind::ClusterReplica(_) => pre_item_updates.push(StateUpdate {
305                    kind,
306                    ts,
307                    diff: diff.try_into().expect("valid diff"),
308                }),
309                StateUpdateKind::IntrospectionSourceIndex(_)
310                | StateUpdateKind::SystemObjectMapping(_) => {
311                    system_item_updates.push(StateUpdate {
312                        kind,
313                        ts,
314                        diff: diff.try_into().expect("valid diff"),
315                    })
316                }
317                StateUpdateKind::Item(_) => item_updates.push(StateUpdate {
318                    kind,
319                    ts,
320                    diff: diff.try_into().expect("valid diff"),
321                }),
322                StateUpdateKind::Comment(_)
323                | StateUpdateKind::StorageCollectionMetadata(_)
324                | StateUpdateKind::SourceReferences(_)
325                | StateUpdateKind::UnfinalizedShard(_) => {
326                    post_item_updates.push((kind, ts, diff));
327                }
328                StateUpdateKind::AuditLog(_) => {
329                    audit_log_updates.push(StateUpdate {
330                        kind,
331                        ts,
332                        diff: diff.try_into().expect("valid diff"),
333                    });
334                }
335            }
336        }
337
338        let (builtin_table_update, _catalog_updates) = state
339            .apply_updates(pre_item_updates, &mut LocalExpressionCache::Closed)
340            .await;
341        builtin_table_updates.extend(builtin_table_update);
342
343        // Both the `system_parameter_defaults` set above and the durable
344        // `SystemConfiguration` values applied via `pre_item_updates` live in
345        // the `SystemVars` value map by now. Mirror their effective values into
346        // the dyncfg `ConfigSet`, so that startup-only reads observe configured
347        // values rather than compile-time defaults, as with
348        // `ENABLE_EXPRESSION_CACHE` below. `apply_updates` only
349        // syncs the `ConfigSet` when a `SystemConfiguration` update is present,
350        // and a deployment configured purely via `system_parameter_default` has
351        // none, so sync explicitly here.
352        state.system_config().sync_dyncfgs();
353
354        // Ensure mz_system has a password if configured to have one.
355        // It's important we do this after the `pre_item_updates` so that
356        // the mz_system role exists in the catalog.
357        {
358            if let Some(password) = config.external_login_password_mz_system {
359                let role_auth = RoleAuth {
360                    role_id: MZ_SYSTEM_ROLE_ID,
361                    // builtin roles should always use a secure scram iteration
362                    // <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>
363                    password_hash: Some(
364                        scram256_hash(&password, &NonZeroU32::new(600_000).expect("known valid"))
365                            .map_err(|_| {
366                            AdapterError::Internal("Failed to hash mz_system password.".to_owned())
367                        })?,
368                    ),
369                    updated_at: SYSTEM_TIME(),
370                };
371                state
372                    .role_auth_by_id
373                    .insert(MZ_SYSTEM_ROLE_ID, role_auth.clone());
374                let builtin_table_update = state.generate_builtin_table_update(
375                    mz_catalog::memory::objects::StateUpdateKind::RoleAuth(role_auth.into()),
376                    mz_catalog::memory::objects::StateDiff::Addition,
377                );
378                builtin_table_updates.extend(builtin_table_update);
379            }
380        }
381
382        let expr_cache_start = Instant::now();
383        info!("startup: coordinator init: catalog open: expr cache open beginning");
384        // We wait until after the `pre_item_updates` to open the cache so we can get accurate
385        // dyncfgs because the `pre_item_updates` contains `SystemConfiguration` updates.
386        let enable_expr_cache_dyncfg = ENABLE_EXPRESSION_CACHE.get(state.system_config().dyncfgs());
387        let expr_cache_enabled = config
388            .enable_expression_cache_override
389            .unwrap_or(enable_expr_cache_dyncfg);
390        let (expr_cache_handle, cached_local_exprs, cached_global_exprs) = if expr_cache_enabled {
391            info!(
392                ?config.enable_expression_cache_override,
393                ?enable_expr_cache_dyncfg,
394                "using expression cache for startup"
395            );
396            let current_items = txn
397                .get_items()
398                .flat_map(|item| {
399                    let item_version = latest_item_version(&item.extra_versions);
400                    std::iter::once(item.global_id)
401                        .chain(item.extra_versions.into_values())
402                        .map(move |gid| (gid, item_version))
403                })
404                .chain(
405                    txn.get_system_object_mappings()
406                        .map(|som| (som.unique_identifier.global_id, RelationVersion::root())),
407                )
408                .collect();
409            let dyncfgs = config.persist_client.dyncfgs().clone();
410            let build_version = if config.build_info.is_dev() {
411                // A single dev version can be used for many different builds, so we need to use
412                // the build version that is also enriched with build metadata.
413                config
414                    .build_info
415                    .semver_version_build()
416                    .expect("build ID is not available on your platform!")
417            } else {
418                config.build_info.semver_version()
419            };
420            let expr_cache_config = ExpressionCacheConfig {
421                build_version,
422                shard_id: txn
423                    .get_expression_cache_shard()
424                    .expect("expression cache shard should exist for opened catalogs"),
425                persist: config.persist_client,
426                current_items,
427                remove_prior_versions: !config.read_only,
428                compact_shard: config.read_only,
429                dyncfgs,
430            };
431            let (expr_cache_handle, cached_local_exprs, cached_global_exprs) =
432                ExpressionCacheHandle::spawn_expression_cache(expr_cache_config).await;
433            (
434                Some(expr_cache_handle),
435                cached_local_exprs,
436                cached_global_exprs,
437            )
438        } else {
439            (None, BTreeMap::new(), BTreeMap::new())
440        };
441        let mut local_expr_cache = LocalExpressionCache::new(cached_local_exprs);
442        info!(
443            "startup: coordinator init: catalog open: expr cache open complete in {:?}",
444            expr_cache_start.elapsed()
445        );
446
447        // When initializing/bootstrapping, we don't use the catalog updates but
448        // instead load the catalog fully and then go ahead and apply commands
449        // to the controller(s). Maybe we _should_ instead use the same logic
450        // and return and use the updates from here. But that's at the very
451        // least future work.
452        let (builtin_table_update, _catalog_updates) = state
453            .apply_updates(system_item_updates, &mut local_expr_cache)
454            .await;
455        builtin_table_updates.extend(builtin_table_update);
456
457        let last_seen_version = get_migration_version(&txn);
458
459        let mz_authentication_mock_nonce =
460            txn.get_authentication_mock_nonce().ok_or_else(|| {
461                Error::new(ErrorKind::SettingError("authentication nonce".to_string()))
462            })?;
463
464        state.mock_authentication_nonce = Some(mz_authentication_mock_nonce);
465
466        // Migrate item ASTs.
467        let (builtin_table_update, _catalog_updates) = if !config.skip_migrations {
468            let migrate_result = migrate::migrate(
469                &mut state,
470                &mut txn,
471                &mut local_expr_cache,
472                item_updates,
473                config.now,
474                config.boot_ts,
475            )
476            .await
477            .map_err(|e| {
478                Error::new(ErrorKind::FailedCatalogMigration {
479                    last_seen_version: last_seen_version
480                        .as_ref()
481                        .map_or_else(|| "new".to_string(), |v| v.to_string()),
482                    this_version: config.build_info.version,
483                    cause: e.to_string(),
484                })
485            })?;
486            if !migrate_result.post_item_updates.is_empty() {
487                // Include any post-item-updates generated by migrations, and then consolidate
488                // them to ensure diffs are all positive.
489                post_item_updates.extend(migrate_result.post_item_updates);
490                // Push everything to the same timestamp so it consolidates cleanly.
491                if let Some(max_ts) = post_item_updates.iter().map(|(_, ts, _)| ts).max().cloned() {
492                    for (_, ts, _) in &mut post_item_updates {
493                        *ts = max_ts;
494                    }
495                }
496                differential_dataflow::consolidation::consolidate_updates(&mut post_item_updates);
497            }
498
499            (
500                migrate_result.builtin_table_updates,
501                migrate_result.catalog_updates,
502            )
503        } else {
504            state
505                .apply_updates(item_updates, &mut local_expr_cache)
506                .await
507        };
508        builtin_table_updates.extend(builtin_table_update);
509
510        let post_item_updates = post_item_updates
511            .into_iter()
512            .map(|(kind, ts, diff)| StateUpdate {
513                kind,
514                ts,
515                diff: diff.try_into().expect("valid diff"),
516            })
517            .collect();
518        let (builtin_table_update, _catalog_updates) = state
519            .apply_updates(post_item_updates, &mut local_expr_cache)
520            .await;
521        builtin_table_updates.extend(builtin_table_update);
522
523        // We don't need to apply the audit logs in memory, yet apply can be expensive when the
524        // audit log grows large. Therefore, we skip the apply step and just generate the builtin
525        // updates.
526        for audit_log_update in audit_log_updates {
527            builtin_table_updates.extend(
528                state.generate_builtin_table_update(audit_log_update.kind, audit_log_update.diff),
529            );
530        }
531
532        // Migrate builtin items.
533        let schema_migration_result = builtin_schema_migration::run(
534            config.build_info,
535            deploy_generation,
536            &mut txn,
537            config.builtin_item_migration_config,
538        )
539        .await?;
540
541        let state_updates = txn.get_and_commit_op_updates();
542
543        // When initializing/bootstrapping, we don't use the catalog updates but
544        // instead load the catalog fully and then go ahead and apply commands
545        // to the controller(s). Maybe we _should_ instead use the same logic
546        // and return and use the updates from here. But that's at the very
547        // least future work.
548        let (table_updates, _catalog_updates) = state
549            .apply_updates(state_updates, &mut local_expr_cache)
550            .await;
551        builtin_table_updates.extend(table_updates);
552        let builtin_table_updates = state.resolve_builtin_table_updates(builtin_table_updates);
553
554        // Every item is now planned, including those the migrations rewrote,
555        // so record what this build's planner produced for each. A read-only
556        // catalog cannot commit the rows and nothing in memory reads them, so
557        // it is left as it is; a savepoint catalog commits them in memory.
558        if !txn.is_read_only() {
559            state.refresh_item_metadata(&mut txn)?;
560        }
561
562        // Bump the migration version immediately before committing.
563        set_migration_version(&mut txn, config.build_info.semver_version())?;
564
565        txn.commit(config.boot_ts).await?;
566
567        // Now that the migration is durable, run any requested deferred cleanup.
568        schema_migration_result.cleanup_action.await;
569
570        Ok(InitializeStateResult {
571            state,
572            migrated_storage_collections_0dt: schema_migration_result.replaced_items,
573            new_builtin_collections: new_builtin_collections.into_iter().collect(),
574            builtin_table_updates,
575            last_seen_version,
576            expr_cache_handle,
577            cached_global_exprs,
578            uncached_local_exprs: local_expr_cache.into_uncached_exprs(),
579        })
580    }
581
582    /// Opens or creates a catalog that stores data at `path`.
583    ///
584    /// Returns the catalog, metadata about builtin objects that have changed
585    /// schemas since last restart, a list of updates to builtin tables that
586    /// describe the initial state of the catalog, and the version of the
587    /// catalog before any migrations were performed.
588    ///
589    /// BOXED FUTURE: As of Nov 2023 the returned Future from this function was 17KB. This would
590    /// get stored on the stack which is bad for runtime performance, and blow up our stack usage.
591    /// Because of that we purposefully move this Future onto the heap (i.e. Box it).
592    #[instrument(name = "catalog::open")]
593    pub fn open(config: Config<'_>) -> BoxFuture<'static, Result<OpenCatalogResult, AdapterError>> {
594        async move {
595            let mut storage = config.storage;
596
597            let InitializeStateResult {
598                state,
599                migrated_storage_collections_0dt,
600                new_builtin_collections,
601                mut builtin_table_updates,
602                last_seen_version,
603                expr_cache_handle,
604                cached_global_exprs,
605                uncached_local_exprs,
606            } =
607                // BOXED FUTURE: As of Nov 2023 the returned Future from this function was 7.5KB. This would
608                // get stored on the stack which is bad for runtime performance, and blow up our stack usage.
609                // Because of that we purposefully move this Future onto the heap (i.e. Box it).
610                Self::initialize_state(config.state, &mut storage)
611                    .instrument(tracing::info_span!("catalog::initialize_state"))
612                    .boxed()
613                    .await?;
614
615            let catalog = Catalog {
616                state,
617                expr_cache_handle,
618                transient_revision: 1,
619                shared_transient_revision: Arc::new(AtomicU64::new(1)),
620                storage: Arc::new(tokio::sync::Mutex::new(storage)),
621            };
622
623            // Operators aren't stored in the catalog, but we would like them in
624            // introspection views.
625            for (op, func) in OP_IMPLS.iter() {
626                match func {
627                    mz_sql::func::Func::Scalar(impls) => {
628                        for imp in impls {
629                            builtin_table_updates.push(catalog.state.resolve_builtin_table_update(
630                                catalog.state.pack_op_update(op, imp.details(), Diff::ONE),
631                            ));
632                        }
633                    }
634                    _ => unreachable!("all operators must be scalar functions"),
635                }
636            }
637
638            for ip in &catalog.state.egress_addresses {
639                builtin_table_updates.push(
640                    catalog
641                        .state
642                        .resolve_builtin_table_update(catalog.state.pack_egress_ip_update(ip)?),
643                );
644            }
645
646            if !catalog.state.license_key.id.is_empty() {
647                builtin_table_updates.push(
648                    catalog.state.resolve_builtin_table_update(
649                        catalog
650                            .state
651                            .pack_license_key_update(&catalog.state.license_key)?,
652                    ),
653                );
654            }
655
656            catalog.storage().await.mark_bootstrap_complete().await;
657
658            Ok(OpenCatalogResult {
659                catalog,
660                last_seen_version,
661                migrated_storage_collections_0dt,
662                new_builtin_collections,
663                builtin_table_updates,
664                cached_global_exprs,
665                uncached_local_exprs,
666            })
667        }
668        .instrument(tracing::info_span!("catalog::open"))
669        .boxed()
670    }
671
672    /// Initializes STORAGE to understand all shards that `self` expects to
673    /// exist.
674    ///
675    /// Note that this must be done before creating/rendering collections
676    /// because the storage controller might not be aware of new system
677    /// collections created between versions.
678    async fn initialize_storage_state(
679        &mut self,
680        storage_collections: &Arc<dyn StorageCollections + Send + Sync>,
681    ) -> Result<(), mz_catalog::durable::CatalogError> {
682        let collections = self
683            .entries()
684            .filter(|entry| entry.item().is_storage_collection())
685            .flat_map(|entry| entry.global_ids())
686            .collect();
687
688        // Clone the state so that any errors that occur do not leak any
689        // transformations on error.
690        let mut state = self.state.clone();
691
692        let mut storage = self.storage().await;
693        let shard_id = storage.shard_id();
694        let mut txn = storage.transaction().await?;
695
696        // Ensure the storage controller knows about the catalog shard and associates it with the
697        // `MZ_CATALOG_RAW` builtin source.
698        let item_id = self.resolve_builtin_storage_collection(&MZ_CATALOG_RAW);
699        let global_id = self.get_entry(&item_id).latest_global_id();
700        match txn.get_collection_metadata().get(&global_id) {
701            None => {
702                txn.insert_collection_metadata([(global_id, shard_id)].into())
703                    .map_err(mz_catalog::durable::DurableCatalogError::from)?;
704            }
705            Some(id) => assert_eq!(*id, shard_id),
706        }
707
708        storage_collections
709            .initialize_state(&mut txn, collections)
710            .await
711            .map_err(mz_catalog::durable::DurableCatalogError::from)?;
712
713        let updates = txn.get_and_commit_op_updates();
714        let (builtin_updates, catalog_updates) = state
715            .apply_updates(updates, &mut LocalExpressionCache::Closed)
716            .await;
717        assert!(
718            builtin_updates.is_empty(),
719            "storage is not allowed to generate catalog changes that would cause changes to builtin tables"
720        );
721        assert!(
722            catalog_updates.is_empty(),
723            "storage is not allowed to generate catalog changes that would change the catalog or controller state"
724        );
725        let commit_ts = txn.upper();
726        txn.commit(commit_ts).await?;
727        drop(storage);
728
729        // Save updated state.
730        self.state = state;
731        Ok(())
732    }
733
734    /// [`mz_controller::Controller`] depends on durable catalog state to boot,
735    /// so make it available and initialize the controller.
736    pub async fn initialize_controller(
737        &mut self,
738        config: mz_controller::ControllerConfig,
739        envd_epoch: core::num::NonZeroI64,
740        read_only: bool,
741    ) -> Result<mz_controller::Controller, mz_catalog::durable::CatalogError> {
742        let controller_start = Instant::now();
743        info!("startup: controller init: beginning");
744
745        let controller = {
746            let mut storage = self.storage().await;
747            let mut tx = storage.transaction().await?;
748            mz_controller::prepare_initialization(&mut tx)
749                .map_err(mz_catalog::durable::DurableCatalogError::from)?;
750            let updates = tx.get_and_commit_op_updates();
751            assert!(
752                updates.is_empty(),
753                "initializing controller should not produce updates: {updates:?}"
754            );
755            let commit_ts = tx.upper();
756            tx.commit(commit_ts).await?;
757
758            let read_only_tx = storage.transaction().await?;
759
760            mz_controller::Controller::new(config, envd_epoch, read_only, &read_only_tx).await
761        };
762
763        self.initialize_storage_state(&controller.storage_collections)
764            .await?;
765
766        info!(
767            "startup: controller init: complete in {:?}",
768            controller_start.elapsed()
769        );
770
771        Ok(controller)
772    }
773
774    /// Politely releases all external resources that can only be released in an async context.
775    pub async fn expire(self) {
776        // If no one else holds a reference to storage, then clean up the storage resources.
777        // Otherwise, hopefully the other reference cleans up the resources when it's dropped.
778        if let Some(storage) = Arc::into_inner(self.storage) {
779            let storage = storage.into_inner();
780            storage.expire().await;
781        }
782    }
783}
784
785impl CatalogState {
786    /// Set the default value for `name`, which is the value it will be reset to.
787    fn set_system_configuration_default(
788        &mut self,
789        name: &str,
790        value: VarInput,
791    ) -> Result<(), Error> {
792        Ok(Arc::make_mut(&mut self.system_configuration).set_default(name, value)?)
793    }
794}
795
796/// Updates the catalog with new and removed builtin items.
797///
798/// Returns the list of new builtin [`GlobalId`]s.
799fn add_new_remove_old_builtin_items_migration(
800    txn: &mut mz_catalog::durable::Transaction<'_>,
801) -> Result<Vec<GlobalId>, mz_catalog::durable::CatalogError> {
802    let mut new_builtin_mappings = Vec::new();
803    // Used to validate unique descriptions.
804    let mut builtin_descs = HashSet::new();
805
806    // We compare the builtin items that are compiled into the binary with the builtin items that
807    // are persisted in the catalog to discover new and deleted builtin items.
808    let mut builtins = Vec::new();
809    for builtin in BUILTINS::iter() {
810        let desc = SystemObjectDescription {
811            schema_name: builtin.schema().to_string(),
812            object_type: builtin.catalog_item_type(),
813            object_name: builtin.name().to_string(),
814        };
815        // Validate that the description is unique.
816        if !builtin_descs.insert(desc.clone()) {
817            panic!(
818                "duplicate builtin description: {:?}, {:?}",
819                SystemObjectDescription {
820                    schema_name: builtin.schema().to_string(),
821                    object_type: builtin.catalog_item_type(),
822                    object_name: builtin.name().to_string(),
823                },
824                builtin
825            );
826        }
827        builtins.push((desc, builtin));
828    }
829
830    let mut system_object_mappings: BTreeMap<_, _> = txn
831        .get_system_object_mappings()
832        .map(|system_object_mapping| {
833            (
834                system_object_mapping.description.clone(),
835                system_object_mapping,
836            )
837        })
838        .collect();
839
840    let (existing_builtins, new_builtins): (Vec<_>, Vec<_>) =
841        builtins.into_iter().partition_map(|(desc, builtin)| {
842            let fingerprint = match builtin.runtime_alterable() {
843                false => builtin.fingerprint(),
844                true => RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL.into(),
845            };
846            match system_object_mappings.remove(&desc) {
847                Some(system_object_mapping) => {
848                    Either::Left((builtin, system_object_mapping, fingerprint))
849                }
850                None => Either::Right((builtin, fingerprint)),
851            }
852        });
853    let new_builtin_ids = txn.allocate_system_item_ids(usize_to_u64(new_builtins.len()))?;
854    let new_builtins: Vec<_> = new_builtins
855        .into_iter()
856        .zip_eq(new_builtin_ids.clone())
857        .collect();
858
859    // Add new builtin items to catalog.
860    for ((builtin, fingerprint), (catalog_id, global_id)) in new_builtins.iter().cloned() {
861        new_builtin_mappings.push(SystemObjectMapping {
862            description: SystemObjectDescription {
863                schema_name: builtin.schema().to_string(),
864                object_type: builtin.catalog_item_type(),
865                object_name: builtin.name().to_string(),
866            },
867            unique_identifier: SystemObjectUniqueIdentifier {
868                catalog_id,
869                global_id,
870                fingerprint,
871            },
872        });
873
874        // Runtime-alterable system objects are durably recorded to the
875        // usual items collection, so that they can be later altered at
876        // runtime by their owner (i.e., outside of the usual builtin
877        // migration framework that requires changes to the binary
878        // itself).
879        let handled_runtime_alterable = match builtin {
880            Builtin::Connection(c) if c.runtime_alterable => {
881                let mut acl_items = vec![rbac::owner_privilege(
882                    mz_sql::catalog::ObjectType::Connection,
883                    c.owner_id.clone(),
884                )];
885                acl_items.extend_from_slice(c.access);
886                // Builtin Connections cannot be versioned.
887                let versions = BTreeMap::new();
888
889                txn.insert_item(
890                    catalog_id,
891                    c.oid,
892                    global_id,
893                    mz_catalog::durable::initialize::resolve_system_schema(c.schema).id,
894                    c.name,
895                    c.sql.into(),
896                    *c.owner_id,
897                    acl_items,
898                    versions,
899                    None,
900                )?;
901                true
902            }
903            _ => false,
904        };
905        assert_eq!(
906            builtin.runtime_alterable(),
907            handled_runtime_alterable,
908            "runtime alterable object was not handled by migration",
909        );
910    }
911    txn.set_system_object_mappings(new_builtin_mappings)?;
912
913    // Update comments of all builtin objects
914    let builtins_with_catalog_ids = existing_builtins
915        .iter()
916        .map(|(b, m, _)| (*b, m.unique_identifier.catalog_id))
917        .chain(
918            new_builtins
919                .into_iter()
920                .map(|((b, _), (catalog_id, _))| (b, catalog_id)),
921        );
922
923    for (builtin, id) in builtins_with_catalog_ids {
924        let (comment_id, desc, comments) = match builtin {
925            Builtin::Source(s) => (CommentObjectId::Source(id), &s.desc, &s.column_comments),
926            Builtin::View(v) => (CommentObjectId::View(id), &v.desc, &v.column_comments),
927            Builtin::Table(t) => (CommentObjectId::Table(id), &t.desc, &t.column_comments),
928            Builtin::MaterializedView(mv) => (
929                CommentObjectId::MaterializedView(id),
930                &mv.desc,
931                &mv.column_comments,
932            ),
933            Builtin::Log(_)
934            | Builtin::Type(_)
935            | Builtin::Func(_)
936            | Builtin::Index(_)
937            | Builtin::Connection(_) => continue,
938        };
939        // Drop comments under every relation-style `CommentObjectId` variant
940        // for this id, not just the current one. When a builtin's type changes
941        // (e.g. Table -> MaterializedView) but its catalog id is preserved, the
942        // prior type's comment rows would otherwise linger forever.
943        txn.drop_comments(&BTreeSet::from_iter([
944            CommentObjectId::Table(id),
945            CommentObjectId::View(id),
946            CommentObjectId::MaterializedView(id),
947            CommentObjectId::Source(id),
948        ]))?;
949
950        let mut comments = comments.clone();
951        for (col_idx, name) in desc.iter_names().enumerate() {
952            if let Some(comment) = comments.remove(name.as_str()) {
953                // Comment column indices are 1 based
954                txn.update_comment(comment_id, Some(col_idx + 1), Some(comment.to_owned()))?;
955            }
956        }
957        assert!(
958            comments.is_empty(),
959            "builtin object contains dangling comments that don't correspond to columns {comments:?}"
960        );
961    }
962
963    // Anything left in `system_object_mappings` must have been deleted and should be removed from
964    // the catalog.
965    let mut deleted_system_objects = BTreeSet::new();
966    let mut deleted_runtime_alterable_system_ids = BTreeSet::new();
967    let mut deleted_comments = BTreeSet::new();
968    for (desc, mapping) in system_object_mappings {
969        deleted_system_objects.insert(mapping.description);
970        if mapping.unique_identifier.fingerprint == RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL {
971            deleted_runtime_alterable_system_ids.insert(mapping.unique_identifier.catalog_id);
972        }
973
974        let id = mapping.unique_identifier.catalog_id;
975        let comment_id = match desc.object_type {
976            CatalogItemType::Table => CommentObjectId::Table(id),
977            CatalogItemType::Source => CommentObjectId::Source(id),
978            CatalogItemType::View => CommentObjectId::View(id),
979            CatalogItemType::MaterializedView => CommentObjectId::MaterializedView(id),
980            CatalogItemType::Sink
981            | CatalogItemType::MetricSink
982            | CatalogItemType::Index
983            | CatalogItemType::Type
984            | CatalogItemType::Func
985            | CatalogItemType::Secret
986            | CatalogItemType::Connection => continue,
987        };
988        deleted_comments.insert(comment_id);
989    }
990    // If you are 100% positive that it is safe to delete a system object outside any of the
991    // unstable schemas, then add it to this set. Make sure that no prod environments are
992    // using this object and that the upgrade checker does not show any issues.
993    //
994    // Objects can be removed from this set after one release.
995    let delete_exceptions: HashSet<SystemObjectDescription> = [].into();
996    // TODO(jkosh44) Technically we could support changing the type of a builtin object outside
997    // of unstable schemas (i.e. from a table to a view). However, builtin migrations don't currently
998    // handle that scenario correctly.
999    assert!(
1000        deleted_system_objects
1001            .iter()
1002            // It's okay if Indexes change because they're inherently ephemeral.
1003            .filter(|object| object.object_type != CatalogItemType::Index)
1004            .all(
1005                |deleted_object| is_unstable_schema(&deleted_object.schema_name)
1006                    || delete_exceptions.contains(deleted_object)
1007            ),
1008        "only objects in unstable schemas can be deleted, deleted objects: {:?}",
1009        deleted_system_objects
1010    );
1011    txn.drop_comments(&deleted_comments)?;
1012    txn.remove_items(&deleted_runtime_alterable_system_ids)?;
1013    txn.remove_system_object_mappings(deleted_system_objects)?;
1014
1015    // Filter down to just the GlobalIds which are used to track the underlying collections.
1016    let new_builtin_collections = new_builtin_ids
1017        .into_iter()
1018        .map(|(_catalog_id, global_id)| global_id)
1019        .collect();
1020
1021    Ok(new_builtin_collections)
1022}
1023
1024fn add_new_remove_old_builtin_clusters_migration(
1025    txn: &mut mz_catalog::durable::Transaction<'_>,
1026    builtin_cluster_config_map: &BuiltinBootstrapClusterConfigMap,
1027    boot_ts: Timestamp,
1028) -> Result<(), mz_catalog::durable::CatalogError> {
1029    let mut durable_clusters: BTreeMap<_, _> = txn
1030        .get_clusters()
1031        .filter(|cluster| cluster.id.is_system())
1032        .map(|cluster| (cluster.name.to_string(), cluster))
1033        .collect();
1034
1035    // Add new clusters.
1036    for builtin_cluster in BUILTIN_CLUSTERS {
1037        if durable_clusters.remove(builtin_cluster.name).is_none() {
1038            let cluster_config = builtin_cluster_config_map.get_config(builtin_cluster.name)?;
1039
1040            let cluster_id = txn.insert_system_cluster(
1041                builtin_cluster.name,
1042                vec![],
1043                builtin_cluster.privileges.to_vec(),
1044                builtin_cluster.owner_id.to_owned(),
1045                mz_catalog::durable::ClusterConfig {
1046                    variant: mz_catalog::durable::ClusterVariant::Managed(ClusterVariantManaged {
1047                        size: cluster_config.size,
1048                        availability_zones: vec![],
1049                        replication_factor: cluster_config.replication_factor,
1050                        logging: default_logging_config(),
1051                        arrangement_compression: false,
1052                        optimizer_feature_overrides: Default::default(),
1053                        schedule: Default::default(),
1054                        auto_scaling_strategy: None,
1055                        reconfiguration: None,
1056                        burst: None,
1057                    }),
1058                    workload_class: None,
1059                },
1060                &HashSet::new(),
1061            )?;
1062
1063            let audit_id = txn.allocate_audit_log_id()?;
1064            txn.insert_audit_log_event(VersionedEvent::new(
1065                audit_id,
1066                EventType::Create,
1067                ObjectType::Cluster,
1068                EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1069                    id: cluster_id.to_string(),
1070                    name: builtin_cluster.name.to_string(),
1071                }),
1072                None,
1073                boot_ts.into(),
1074            ));
1075        }
1076    }
1077
1078    // Remove old clusters.
1079    let old_clusters = durable_clusters
1080        .values()
1081        .map(|cluster| cluster.id)
1082        .collect();
1083    txn.remove_clusters(&old_clusters)?;
1084
1085    for (_name, cluster) in &durable_clusters {
1086        let audit_id = txn.allocate_audit_log_id()?;
1087        txn.insert_audit_log_event(VersionedEvent::new(
1088            audit_id,
1089            EventType::Drop,
1090            ObjectType::Cluster,
1091            EventDetails::IdNameV1(mz_audit_log::IdNameV1 {
1092                id: cluster.id.to_string(),
1093                name: cluster.name.clone(),
1094            }),
1095            None,
1096            boot_ts.into(),
1097        ));
1098    }
1099
1100    Ok(())
1101}
1102
1103fn add_new_remove_old_builtin_introspection_source_migration(
1104    txn: &mut mz_catalog::durable::Transaction<'_>,
1105) -> Result<(), AdapterError> {
1106    let mut new_indexes = Vec::new();
1107    let mut removed_indexes = BTreeSet::new();
1108    for cluster in txn.get_clusters() {
1109        let mut introspection_source_index_ids = txn.get_introspection_source_indexes(cluster.id);
1110
1111        let mut new_logs = Vec::new();
1112
1113        for log in BUILTINS::logs() {
1114            if introspection_source_index_ids.remove(log.name).is_none() {
1115                new_logs.push(log);
1116            }
1117        }
1118
1119        for log in new_logs {
1120            let (item_id, gid) =
1121                Transaction::allocate_introspection_source_index_id(&cluster.id, log.variant);
1122            new_indexes.push((cluster.id, log.name.to_string(), item_id, gid));
1123        }
1124
1125        // Anything left in `introspection_source_index_ids` must have been deleted and should be
1126        // removed from the catalog.
1127        removed_indexes.extend(
1128            introspection_source_index_ids
1129                .into_keys()
1130                .map(|name| (cluster.id, name.to_string())),
1131        );
1132    }
1133    txn.insert_introspection_source_indexes(new_indexes, &HashSet::new())?;
1134    txn.remove_introspection_source_indexes(removed_indexes)?;
1135    Ok(())
1136}
1137
1138fn add_new_remove_old_builtin_roles_migration(
1139    txn: &mut mz_catalog::durable::Transaction<'_>,
1140) -> Result<(), mz_catalog::durable::CatalogError> {
1141    let mut durable_roles: BTreeMap<_, _> = txn
1142        .get_roles()
1143        .filter(|role| role.id.is_system() || role.id.is_predefined())
1144        .map(|role| (role.name.to_string(), role))
1145        .collect();
1146
1147    // Add new roles.
1148    for builtin_role in BUILTIN_ROLES {
1149        if durable_roles.remove(builtin_role.name).is_none() {
1150            txn.insert_builtin_role(
1151                builtin_role.id,
1152                builtin_role.name.to_string(),
1153                builtin_role.attributes.clone(),
1154                RoleMembership::new(),
1155                RoleVars::default(),
1156                builtin_role.oid,
1157            )?;
1158        }
1159    }
1160
1161    // Remove old roles.
1162    let old_roles = durable_roles.values().map(|role| role.id).collect();
1163    txn.remove_roles(&old_roles)?;
1164
1165    Ok(())
1166}
1167
1168/// Reconciles the system cluster replica sizes in the durable catalog with the
1169/// sizes from `--cluster-replica-sizes`.
1170///
1171/// A size removed from the flag is deleted, unless a replica still uses it. Then
1172/// it is kept but disabled, so the replica can still be scheduled while no new
1173/// replica can pick the size.
1174fn sync_builtin_cluster_replica_sizes(
1175    txn: &mut Transaction<'_>,
1176    config_sizes: &ClusterReplicaSizeMap,
1177) -> Result<(), AdapterError> {
1178    let mut durable_sizes: BTreeMap<_, _> = txn
1179        .get_cluster_replica_sizes()
1180        .filter(|size| size.id.is_system())
1181        .map(|size| (size.name.clone(), size))
1182        .collect();
1183
1184    for (name, allocation) in &config_sizes.0 {
1185        match durable_sizes.remove(name) {
1186            None => {
1187                txn.insert_system_cluster_replica_size(name.clone(), allocation.clone())?;
1188            }
1189            Some(size) if size.allocation != *allocation => {
1190                txn.update_cluster_replica_size(ClusterReplicaSize {
1191                    allocation: allocation.clone(),
1192                    ..size
1193                })?;
1194            }
1195            Some(_) => {}
1196        }
1197    }
1198
1199    // Managed clusters can reference a size without having a replica on it, e.g.
1200    // between refreshes of a scheduled cluster or at replication factor 0.
1201    let clusters: Vec<_> = txn.get_clusters().collect();
1202    let replicas: Vec<_> = txn.get_cluster_replicas().collect();
1203    let in_use: BTreeSet<&str> = clusters
1204        .iter()
1205        .flat_map(|cluster| cluster.config.variant.sizes())
1206        .chain(replicas.iter().filter_map(|r| r.config.location.size()))
1207        .collect();
1208    for (name, mut size) in durable_sizes {
1209        if !in_use.contains(name.as_str()) {
1210            txn.remove_cluster_replica_size(size.id)?;
1211        } else if !size.allocation.disabled {
1212            warn!(%name, "cluster replica size removed from config but still in use, disabling it");
1213            size.allocation.disabled = true;
1214            txn.update_cluster_replica_size(size)?;
1215        }
1216    }
1217
1218    Ok(())
1219}
1220
1221/// Converges each builtin cluster's replica set on the cluster's own managed
1222/// config.
1223///
1224/// A builtin cluster is normally managed, and a managed cluster's replicas are
1225/// derived state: exactly `replication_factor` replicas, named by
1226/// [`managed_cluster_replica_name`]. One can be altered to unmanaged, which leaves
1227/// no factor to derive from, and then its replica set is the operator's.
1228///
1229/// Replicas this creates are shaped from the cluster's config. An existing replica
1230/// is matched by name alone and left untouched, so this converges cardinality and
1231/// names rather than shape. An internal replica is never derived state and is left
1232/// alone entirely.
1233///
1234/// The bootstrap flags seed `replication_factor` and `size` when a cluster is
1235/// first created (see [`add_new_remove_old_builtin_clusters_migration`]) and are
1236/// deliberately not consulted here, so an `ALTER CLUSTER` against a builtin
1237/// cluster survives a restart.
1238///
1239/// This runs at catalog open so the replicas a cluster's config calls for exist as
1240/// early as possible. The coordinator's bootstrap brings up only replicas already
1241/// recorded durably and runs before the cluster controller is spawned, and the
1242/// controller does not run at all while a deployment is read-only.
1243///
1244/// The cluster controller owns these replica sets at runtime and derives its
1245/// target from the same cluster config, so the two converge on the same set
1246/// rather than competing for it. The controller matches replicas by shape and
1247/// count, never by name, so the `r1..rN` this creates satisfy it. This converges
1248/// by name, so a boot after the controller reshaped a cluster renames or
1249/// re-creates replicas it had materialized under generator names. That is
1250/// harmless: every replica is a cold process at boot anyway, so the cost is
1251/// replica-id and audit-log noise.
1252fn reconcile_builtin_cluster_replicas(
1253    txn: &mut Transaction<'_>,
1254    builtin_cluster_config_map: &BuiltinBootstrapClusterConfigMap,
1255    boot_ts: Timestamp,
1256) -> Result<(), AdapterError> {
1257    let builtin_cluster_names: BTreeSet<&str> = BUILTIN_CLUSTERS
1258        .iter()
1259        .map(|cluster| cluster.name)
1260        .collect();
1261
1262    // Replicas of a cluster that is no longer a builtin need no handling here:
1263    // `Transaction::remove_clusters` cascades to a cluster's replicas, and the
1264    // clusters migration has already run against this transaction.
1265    let clusters: Vec<_> = txn
1266        .get_clusters()
1267        .filter(|cluster| {
1268            cluster.id.is_system() && builtin_cluster_names.contains(cluster.name.as_str())
1269        })
1270        .collect();
1271
1272    let builtin_cluster_ids: BTreeSet<ClusterId> =
1273        clusters.iter().map(|cluster| cluster.id).collect();
1274
1275    // Only the builtin clusters' replicas. An environment can have thousands of
1276    // user replicas and none of them are in scope here.
1277    //
1278    // An internal replica is deliberately out of scope. `CREATE CLUSTER REPLICA ...
1279    // INTERNAL` is allowed on a managed cluster, and its name is barred from
1280    // matching the derived `r1..rN` pattern so it cannot collide with one. It is a
1281    // break-glass replica an operator added rather than derived state, so reaping it
1282    // would undo that. The `ALTER CLUSTER` path skips it for the same reason.
1283    let mut replicas_by_cluster: BTreeMap<ClusterId, BTreeMap<String, ClusterReplica>> =
1284        BTreeMap::new();
1285    for replica in txn.get_cluster_replicas().filter(|replica| {
1286        builtin_cluster_ids.contains(&replica.cluster_id)
1287            && !matches!(
1288                replica.config.location,
1289                ReplicaLocation::Managed { internal: true, .. }
1290            )
1291    }) {
1292        replicas_by_cluster
1293            .entry(replica.cluster_id)
1294            .or_default()
1295            .insert(replica.name.clone(), replica);
1296    }
1297
1298    let mut to_drop: Vec<(String, ClusterReplica)> = Vec::new();
1299
1300    for cluster in clusters {
1301        // An unmanaged cluster has no replication factor, so there is no target to
1302        // converge on. Its replica set is whatever an operator made it, and it is
1303        // not ours to reshape.
1304        let ClusterVariant::Managed(managed) = &cluster.config.variant else {
1305            continue;
1306        };
1307
1308        // The bootstrap flags seed a cluster at creation and are never re-applied,
1309        // which is what keeps an `ALTER CLUSTER` from being reverted on every
1310        // restart. That leaves an operator who changed a flag on an existing
1311        // deployment with no feedback, so say so.
1312        let bootstrap_config = builtin_cluster_config_map.get_config(&cluster.name)?;
1313        if bootstrap_config.replication_factor != managed.replication_factor {
1314            warn!(
1315                cluster = %cluster.name,
1316                configured_replication_factor = managed.replication_factor,
1317                bootstrap_replication_factor = bootstrap_config.replication_factor,
1318                "bootstrap replication factor is not applied to an already-existing \
1319                 builtin cluster. Use ALTER CLUSTER ... SET (REPLICATION FACTOR ...) \
1320                 to change it",
1321            );
1322        }
1323
1324        // Reading the cluster's factor is what makes this compose with the other
1325        // writers of a replica set. The controller's on-refresh strategy parks a
1326        // scheduled cluster by writing its factor to 0, so converging on the
1327        // factor honors that instead of resurrecting a replica it just dropped.
1328        let mut surplus = replicas_by_cluster.remove(&cluster.id).unwrap_or_default();
1329        for index in 0..managed.replication_factor {
1330            let replica_name = managed_cluster_replica_name(index);
1331            if surplus.remove(&replica_name).is_some() {
1332                continue;
1333            }
1334
1335            // Builtin replicas live on system clusters. This runs inside the
1336            // catalog-open transaction with no coordinator, so allocating from the
1337            // same transaction is single-source and safe.
1338            let replica_id = txn.allocate_system_replica_id()?;
1339            txn.insert_cluster_replica_with_id(
1340                cluster.id,
1341                replica_id,
1342                &replica_name,
1343                managed_replica_config(managed),
1344                // The cluster's owner, not `mz_system`. `mz_support` and
1345                // `mz_analytics` are owned by their own roles, and replica
1346                // ownership is checked against the replica's own `owner_id`, so
1347                // stamping `mz_system` here would stop those roles from altering
1348                // a replica of a cluster they own.
1349                cluster.owner_id,
1350            )?;
1351            info!(
1352                cluster = %cluster.name, replica = %replica_name, %replica_id,
1353                "creating builtin cluster replica to match the cluster's replication factor"
1354            );
1355
1356            let audit_id = txn.allocate_audit_log_id()?;
1357            txn.insert_audit_log_event(VersionedEvent::new(
1358                audit_id,
1359                EventType::Create,
1360                ObjectType::ClusterReplica,
1361                EventDetails::CreateClusterReplicaV4(mz_audit_log::CreateClusterReplicaV4 {
1362                    cluster_id: cluster.id.to_string(),
1363                    cluster_name: cluster.name.clone(),
1364                    replica_id: Some(replica_id.to_string()),
1365                    replica_name,
1366                    logical_size: managed.size.clone(),
1367                    billed_as: None,
1368                    internal: false,
1369                    reason: CreateOrDropClusterReplicaReasonV1::System,
1370                    scheduling_policies: None,
1371                }),
1372                None,
1373                boot_ts.into(),
1374            ));
1375        }
1376
1377        // Whatever the config does not call for is surplus: a replica left over from
1378        // a higher replication factor, or a `-pending` replica belonging to a
1379        // graceful reconfiguration that a restart interrupted. Dropping the latter
1380        // here means `remove_pending_cluster_replicas_migration` never sees it,
1381        // which matches the abort semantics it would have applied anyway.
1382        to_drop.extend(
1383            surplus
1384                .into_values()
1385                .map(|replica| (cluster.name.clone(), replica)),
1386        );
1387    }
1388
1389    // Batched: `Transaction::remove_cluster_replica` is linear in the total replica
1390    // count, so it must not be called in a loop.
1391    let drop_ids = to_drop
1392        .iter()
1393        .map(|(_cluster_name, replica)| replica.replica_id)
1394        .collect();
1395    txn.remove_cluster_replicas(&drop_ids)?;
1396
1397    for (cluster_name, replica) in to_drop {
1398        info!(
1399            cluster = %cluster_name, replica = %replica.name, replica_id = %replica.replica_id,
1400            "dropping builtin cluster replica not called for by the cluster's replication factor"
1401        );
1402
1403        let audit_id = txn.allocate_audit_log_id()?;
1404        txn.insert_audit_log_event(VersionedEvent::new(
1405            audit_id,
1406            EventType::Drop,
1407            ObjectType::ClusterReplica,
1408            EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
1409                cluster_id: replica.cluster_id.to_string(),
1410                cluster_name,
1411                replica_id: Some(replica.replica_id.to_string()),
1412                replica_name: replica.name,
1413                reason: CreateOrDropClusterReplicaReasonV1::System,
1414                scheduling_policies: None,
1415            }),
1416            None,
1417            boot_ts.into(),
1418        ));
1419    }
1420
1421    Ok(())
1422}
1423
1424/// The replica config implied by a managed cluster's config.
1425///
1426/// Every field a replica shares with its cluster is taken from the cluster.
1427/// `availability_zones` is the cluster's pool, which is what the provisioning
1428/// paths stamp onto a managed replica.
1429///
1430/// Two things the `ALTER CLUSTER` path does that this does not. It normalizes
1431/// logging through the plan, which discards `log_logging` when no interval is
1432/// set, so for the odd cluster config that has debugging on and logging off the
1433/// two produce different records. And it validates the size against the replica
1434/// size map and the role's allowed sizes, which cannot be done from here because
1435/// the size map is not part of the durable catalog. A builtin cluster's size only
1436/// ever comes from a bootstrap flag or an `ALTER` that already validated it.
1437fn managed_replica_config(managed: &ClusterVariantManaged) -> ReplicaConfig {
1438    // Exhaustive destructure (no `..`): a field added to the managed config is a
1439    // compile error here until we decide whether a replica has to carry it.
1440    let ClusterVariantManaged {
1441        size,
1442        availability_zones,
1443        logging,
1444        arrangement_compression,
1445        replication_factor: _,
1446        optimizer_feature_overrides: _,
1447        schedule: _,
1448        auto_scaling_strategy: _,
1449        reconfiguration: _,
1450        burst: _,
1451    } = managed;
1452    ReplicaConfig {
1453        location: ReplicaLocation::Managed {
1454            size: size.clone(),
1455            availability_zones: availability_zones.clone(),
1456            billed_as: None,
1457            internal: false,
1458            pending: false,
1459        },
1460        logging: logging.clone(),
1461        arrangement_compression: *arrangement_compression,
1462    }
1463}
1464
1465/// Roles can have default values for configuration parameters, e.g. you can set a Role default for
1466/// the 'cluster' parameter.
1467///
1468/// This migration exists to remove the Role default for a configuration parameter, if the persisted
1469/// input is no longer valid. For example if we remove a configuration parameter or change the
1470/// accepted set of values.
1471fn remove_invalid_config_param_role_defaults_migration(
1472    txn: &mut Transaction<'_>,
1473) -> Result<(), AdapterError> {
1474    static BUILD_INFO: mz_build_info::BuildInfo = mz_build_info::build_info!();
1475
1476    let roles_to_migrate: BTreeMap<_, _> = txn
1477        .get_roles()
1478        .filter_map(|mut role| {
1479            // Create an empty SessionVars just so we can check if a var is valid.
1480            //
1481            // TODO(parkmycar): This is a bit hacky, instead we should have a static list of all
1482            // session variables.
1483            let session_vars = SessionVars::new_unchecked(&BUILD_INFO, SYSTEM_USER.clone(), None);
1484
1485            // Iterate over all of the variable defaults for this role.
1486            let mut invalid_roles_vars = BTreeMap::new();
1487            for (name, value) in &role.vars.map {
1488                // If one does not exist or its value is invalid, then mark it for removal.
1489                let Ok(session_var) = session_vars.inspect(name) else {
1490                    invalid_roles_vars.insert(name.clone(), value.clone());
1491                    continue;
1492                };
1493                if session_var.check(value.borrow()).is_err() {
1494                    invalid_roles_vars.insert(name.clone(), value.clone());
1495                }
1496            }
1497
1498            // If the role has no invalid values, nothing to do!
1499            if invalid_roles_vars.is_empty() {
1500                return None;
1501            }
1502
1503            tracing::warn!(?role, ?invalid_roles_vars, "removing invalid role vars");
1504
1505            // Otherwise, remove the variables from the role and return it to be updated.
1506            for (name, _value) in invalid_roles_vars {
1507                role.vars.map.remove(&name);
1508            }
1509            Some(role)
1510        })
1511        .map(|role| (role.id, role))
1512        .collect();
1513
1514    txn.update_roles_without_auth(roles_to_migrate)?;
1515
1516    Ok(())
1517}
1518
1519/// Drops replicas left durably marked `pending`.
1520///
1521/// No runtime path creates one anymore. An upgrade can still come from a version
1522/// whose staged reconfiguration machine crashed between the pending-create commit
1523/// and the finalize, and those replicas are excluded from the cluster
1524/// controller's ownership test, so this catalog-open sweep is their only
1525/// remaining cleaner. It goes away together with the durable `pending` field,
1526/// once no supported upgrade source can still write one.
1527fn remove_pending_cluster_replicas_migration(
1528    tx: &mut Transaction,
1529    boot_ts: mz_repr::Timestamp,
1530) -> Result<(), anyhow::Error> {
1531    // Build a map of cluster_id -> cluster_name for audit events.
1532    let cluster_names: BTreeMap<_, _> = tx.get_clusters().map(|c| (c.id, c.name)).collect();
1533
1534    let occurred_at = boot_ts.into();
1535
1536    for replica in tx.get_cluster_replicas().collect::<Vec<_>>() {
1537        if let mz_catalog::durable::ReplicaLocation::Managed { pending: true, .. } =
1538            replica.config.location
1539        {
1540            let cluster_name = cluster_names
1541                .get(&replica.cluster_id)
1542                .cloned()
1543                .unwrap_or_else(|| "<unknown>".to_string());
1544
1545            info!(
1546                "removing pending cluster replica '{}' from cluster '{}'",
1547                replica.name, cluster_name,
1548            );
1549
1550            tx.remove_cluster_replica(replica.replica_id)?;
1551
1552            // Emit an audit log event so that the drop is visible in
1553            // mz_audit_events, matching the create event that was
1554            // recorded when the pending replica was first created.
1555            let audit_id = tx.allocate_audit_log_id()?;
1556            tx.insert_audit_log_event(VersionedEvent::new(
1557                audit_id,
1558                EventType::Drop,
1559                ObjectType::ClusterReplica,
1560                EventDetails::DropClusterReplicaV3(mz_audit_log::DropClusterReplicaV3 {
1561                    cluster_id: replica.cluster_id.to_string(),
1562                    cluster_name,
1563                    replica_id: Some(replica.replica_id.to_string()),
1564                    replica_name: replica.name,
1565                    reason: CreateOrDropClusterReplicaReasonV1::System,
1566                    scheduling_policies: None,
1567                }),
1568                None,
1569                occurred_at,
1570            ));
1571        }
1572    }
1573    Ok(())
1574}
1575
1576fn default_logging_config() -> ReplicaLogging {
1577    ReplicaLogging {
1578        log_logging: false,
1579        interval: Some(Duration::from_secs(1)),
1580    }
1581}
1582
1583#[derive(Debug)]
1584pub struct BuiltinBootstrapClusterConfigMap {
1585    /// Size and replication factor to default system_cluster on bootstrap
1586    pub system_cluster: BootstrapBuiltinClusterConfig,
1587    /// Size and replication factor to default catalog_server_cluster on bootstrap
1588    pub catalog_server_cluster: BootstrapBuiltinClusterConfig,
1589    /// Size and replication factor to default probe_cluster on bootstrap
1590    pub probe_cluster: BootstrapBuiltinClusterConfig,
1591    /// Size and replication factor to default support_cluster on bootstrap
1592    pub support_cluster: BootstrapBuiltinClusterConfig,
1593    /// Size to default analytics_cluster on bootstrap
1594    pub analytics_cluster: BootstrapBuiltinClusterConfig,
1595}
1596
1597impl BuiltinBootstrapClusterConfigMap {
1598    /// Gets the size of the builtin cluster based on the provided name
1599    fn get_config(
1600        &self,
1601        cluster_name: &str,
1602    ) -> Result<BootstrapBuiltinClusterConfig, mz_catalog::durable::CatalogError> {
1603        let cluster_config = if cluster_name == mz_catalog::builtin::MZ_SYSTEM_CLUSTER.name {
1604            &self.system_cluster
1605        } else if cluster_name == mz_catalog::builtin::MZ_CATALOG_SERVER_CLUSTER.name {
1606            &self.catalog_server_cluster
1607        } else if cluster_name == mz_catalog::builtin::MZ_PROBE_CLUSTER.name {
1608            &self.probe_cluster
1609        } else if cluster_name == mz_catalog::builtin::MZ_SUPPORT_CLUSTER.name {
1610            &self.support_cluster
1611        } else if cluster_name == mz_catalog::builtin::MZ_ANALYTICS_CLUSTER.name {
1612            &self.analytics_cluster
1613        } else {
1614            return Err(mz_catalog::durable::CatalogError::Catalog(
1615                SqlCatalogError::UnexpectedBuiltinCluster(cluster_name.to_owned()),
1616            ));
1617        };
1618        Ok(cluster_config.clone())
1619    }
1620}
1621
1622/// Convert `updates` into a `Vec` that can be consolidated by doing the following:
1623///
1624///   - Update the timestamp of each update to the same value.
1625///   - Convert the diff of each update to a type that implements
1626///     [`differential_dataflow::difference::Semigroup`].
1627///
1628/// WARNING: Do not call outside of startup.
1629pub(crate) fn into_consolidatable_updates_startup(
1630    updates: Vec<StateUpdate>,
1631    ts: Timestamp,
1632) -> Vec<(StateUpdateKind, Timestamp, Diff)> {
1633    updates
1634        .into_iter()
1635        .map(|StateUpdate { kind, ts: _, diff }| (kind, ts, Diff::from(diff)))
1636        .collect()
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641    use mz_catalog::durable::debug::{IndexColumnsCollection, ItemColumnsCollection};
1642    use mz_catalog::durable::{
1643        CatalogError, ClusterVariantManaged, DurableCatalogError, Snapshot,
1644        TestCatalogStateBuilder, test_bootstrap_args,
1645    };
1646    use mz_persist_client::PersistClient;
1647    use mz_repr::GlobalId;
1648    use mz_sql::names::{
1649        DatabaseId, ItemQualifiers, QualifiedItemName, ResolvedDatabaseSpecifier, SchemaId,
1650        SchemaSpecifier,
1651    };
1652    use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
1653
1654    use super::*;
1655    use crate::catalog::{Catalog, Op};
1656
1657    /// A replica the reconciler creates takes every shared field from its
1658    /// cluster. Nothing here may fall back to a default, or a builtin cluster
1659    /// ends up running a replica that disagrees with its own config.
1660    #[mz_ore::test]
1661    fn test_managed_replica_config_derives_every_shared_field() {
1662        let managed = ClusterVariantManaged {
1663            size: "somesize".into(),
1664            availability_zones: vec!["az1".into(), "az2".into()],
1665            logging: ReplicaLogging {
1666                log_logging: true,
1667                interval: Some(Duration::from_millis(10)),
1668            },
1669            arrangement_compression: true,
1670            replication_factor: 3,
1671            optimizer_feature_overrides: Default::default(),
1672            schedule: Default::default(),
1673            auto_scaling_strategy: None,
1674            reconfiguration: None,
1675            burst: None,
1676        };
1677
1678        let config = managed_replica_config(&managed);
1679
1680        // Exhaustive destructures throughout, so a field added to either type is a
1681        // compile error here rather than a silently unasserted one.
1682        let ReplicaConfig {
1683            location,
1684            logging,
1685            arrangement_compression,
1686        } = config;
1687
1688        assert_eq!(logging, managed.logging);
1689        assert_eq!(arrangement_compression, managed.arrangement_compression);
1690        match location {
1691            ReplicaLocation::Managed {
1692                size,
1693                availability_zones,
1694                billed_as,
1695                internal,
1696                pending,
1697            } => {
1698                assert_eq!(size, managed.size);
1699                assert_eq!(availability_zones, managed.availability_zones);
1700                // A builtin replica is neither manually managed nor part of an
1701                // in-flight reconfiguration, and those three traits are what
1702                // exclude a replica from controller ownership.
1703                assert_eq!(billed_as, None);
1704                assert!(!internal);
1705                assert!(!pending);
1706            }
1707            ReplicaLocation::Unmanaged { .. } => panic!("expected a managed location"),
1708        }
1709    }
1710
1711    /// Writes `create_sql` into `catalog` as item `id`, as a transaction would.
1712    async fn create_item(
1713        catalog: &mut Catalog,
1714        id: CatalogItemId,
1715        gid: GlobalId,
1716        name: &str,
1717        create_sql: &str,
1718    ) {
1719        let item = catalog
1720            .state()
1721            .deserialize_item(
1722                gid,
1723                create_sql,
1724                &BTreeMap::new(),
1725                &mut LocalExpressionCache::Closed,
1726                None,
1727            )
1728            .expect("valid create_sql");
1729        let commit_ts = catalog.current_upper().await;
1730        catalog
1731            .transact(
1732                None,
1733                commit_ts,
1734                None,
1735                vec![Op::CreateItem {
1736                    item,
1737                    name: QualifiedItemName {
1738                        qualifiers: ItemQualifiers {
1739                            database_spec: ResolvedDatabaseSpecifier::Id(DatabaseId::User(1)),
1740                            schema_spec: SchemaSpecifier::Id(SchemaId::User(3)),
1741                        },
1742                        item: name.to_string(),
1743                    },
1744                    id,
1745                    owner_id: MZ_SYSTEM_ROLE_ID,
1746                }],
1747            )
1748            .await
1749            .expect("failed to transact");
1750    }
1751
1752    /// Stages a refresh of `catalog` and commits it.
1753    async fn refresh(catalog: &Catalog) -> Result<(), CatalogError> {
1754        let mut storage = catalog.storage().await;
1755        let mut tx = storage.transaction().await.expect("transaction");
1756        catalog
1757            .state()
1758            .refresh_item_metadata(&mut tx)
1759            .expect("refresh");
1760        let commit_ts = tx.upper();
1761        tx.commit(commit_ts).await
1762    }
1763
1764    /// The durable snapshot of `catalog`.
1765    async fn recorded(catalog: &Catalog) -> Snapshot {
1766        catalog.storage().await.snapshot().await.expect("snapshot")
1767    }
1768
1769    /// The bootstrap refresh writes back rows that are missing, which is how
1770    /// items that predate the collections get theirs, and nothing else: a
1771    /// steady-state boot stages no writes. A read-only catalog commits a
1772    /// transaction only when nothing was staged, which tells the two apart,
1773    /// and its open skips the refresh for the same reason, so the refresh is
1774    /// invoked by hand on it. A writable open runs it itself.
1775    #[mz_ore::test(tokio::test)]
1776    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
1777    async fn refresh_writes_only_missing_item_metadata() {
1778        let persist_client = PersistClient::new_for_tests().await;
1779        let organization_id = Uuid::new_v4();
1780        let bootstrap_args = test_bootstrap_args();
1781
1782        let view_id = CatalogItemId::User(1);
1783        let index_id = CatalogItemId::User(2);
1784        let written = {
1785            let mut catalog = Catalog::open_debug_catalog(
1786                persist_client.clone(),
1787                organization_id,
1788                &bootstrap_args,
1789            )
1790            .await
1791            .expect("unable to open debug catalog");
1792            create_item(
1793                &mut catalog,
1794                view_id,
1795                GlobalId::User(1),
1796                "v",
1797                r#"CREATE VIEW "materialize"."public"."v" AS SELECT 1 AS a, NULL::text AS b"#,
1798            )
1799            .await;
1800            create_item(
1801                &mut catalog,
1802                index_id,
1803                GlobalId::User(2),
1804                "v_idx",
1805                r#"CREATE INDEX "v_idx" IN CLUSTER [u1] ON [u1 AS "materialize"."public"."v"] ("a")"#,
1806            )
1807            .await;
1808            let written = recorded(&catalog).await;
1809            catalog.expire().await;
1810            written
1811        };
1812        // Each item recorded its row when it was written.
1813        assert_eq!(written.item_columns.len(), 1, "{:?}", written.item_columns);
1814        assert_eq!(
1815            written.index_columns.len(),
1816            1,
1817            "{:?}",
1818            written.index_columns
1819        );
1820
1821        // We record two columns for the view and one column for the index.
1822        let item_columns = written.item_columns.iter().next();
1823        assert_eq!(
1824            item_columns
1825                .expect("item columns should exist")
1826                .1
1827                .columns
1828                .len(),
1829            2
1830        );
1831
1832        let index_columns = written.index_columns.iter().next();
1833        assert_eq!(
1834            index_columns
1835                .expect("index columns should exist")
1836                .1
1837                .columns
1838                .len(),
1839            1
1840        );
1841
1842        // A fresh boot plans the items again and finds its output recorded.
1843        {
1844            let catalog = Catalog::open_debug_read_only_catalog(
1845                persist_client.clone(),
1846                organization_id,
1847                &bootstrap_args,
1848            )
1849            .await
1850            .expect("unable to open read-only debug catalog");
1851            refresh(&catalog)
1852                .await
1853                .expect("a steady-state refresh stages nothing");
1854            catalog.expire().await;
1855        }
1856
1857        // Drop the view's columns and the index's keys behind the catalog's
1858        // back.
1859        {
1860            let column_key = written.item_columns.keys().next().expect("one row");
1861            let index_key = written.index_columns.keys().next().expect("one row");
1862            let mut debug_state = TestCatalogStateBuilder::new(persist_client.clone())
1863                .with_organization_id(organization_id)
1864                .with_default_deploy_generation()
1865                .unwrap_build()
1866                .await
1867                .open_debug()
1868                .await
1869                .expect("unable to open debug state");
1870            debug_state
1871                .delete::<ItemColumnsCollection>(column_key.clone())
1872                .await
1873                .expect("delete columns");
1874            debug_state
1875                .delete::<IndexColumnsCollection>(index_key.clone())
1876                .await
1877                .expect("delete index keys");
1878        }
1879
1880        // The next refresh stages exactly the two missing rows.
1881        {
1882            let catalog = Catalog::open_debug_read_only_catalog(
1883                persist_client.clone(),
1884                organization_id,
1885                &bootstrap_args,
1886            )
1887            .await
1888            .expect("unable to open read-only debug catalog");
1889            let error = refresh(&catalog)
1890                .await
1891                .expect_err("a refresh with rows missing stages them");
1892            let CatalogError::Durable(DurableCatalogError::NotWritable(message)) = error else {
1893                panic!("unexpected error {error:?}");
1894            };
1895            assert!(
1896                message.contains("2 updates")
1897                    && message.contains("ItemColumns")
1898                    && message.contains("IndexColumns"),
1899                "{message}"
1900            );
1901            catalog.expire().await;
1902        }
1903        {
1904            let catalog = Catalog::open_debug_catalog(
1905                persist_client.clone(),
1906                organization_id,
1907                &bootstrap_args,
1908            )
1909            .await
1910            .expect("unable to open debug catalog");
1911            // A writable open refills the rows itself.
1912            let refilled = recorded(&catalog).await;
1913            assert_eq!(refilled.item_columns, written.item_columns);
1914            assert_eq!(refilled.index_columns, written.index_columns);
1915            catalog.expire().await;
1916        }
1917    }
1918
1919    /// The bootstrap refresh rewrites rows that disagree with what the
1920    /// running build's planner produces, surplus entries included.
1921    #[mz_ore::test(tokio::test)]
1922    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method` on OS `linux`
1923    async fn refresh_rewrites_stale_item_metadata() {
1924        let persist_client = PersistClient::new_for_tests().await;
1925        let organization_id = Uuid::new_v4();
1926        let bootstrap_args = test_bootstrap_args();
1927
1928        let view_id = CatalogItemId::User(1);
1929        let index_id = CatalogItemId::User(2);
1930        let written = {
1931            let mut catalog = Catalog::open_debug_catalog(
1932                persist_client.clone(),
1933                organization_id,
1934                &bootstrap_args,
1935            )
1936            .await
1937            .expect("unable to open debug catalog");
1938            create_item(
1939                &mut catalog,
1940                view_id,
1941                GlobalId::User(1),
1942                "v",
1943                r#"CREATE VIEW "materialize"."public"."v" AS SELECT 1 AS a, NULL::text AS b"#,
1944            )
1945            .await;
1946            create_item(
1947                &mut catalog,
1948                index_id,
1949                GlobalId::User(2),
1950                "v_idx",
1951                r#"CREATE INDEX "v_idx" IN CLUSTER [u1] ON [u1 AS "materialize"."public"."v"] ("a")"#,
1952            )
1953            .await;
1954            let written = recorded(&catalog).await;
1955            catalog.expire().await;
1956            written
1957        };
1958        assert_eq!(written.item_columns.len(), 1, "{:?}", written.item_columns);
1959        assert_eq!(
1960            written.index_columns.len(),
1961            1,
1962            "{:?}",
1963            written.index_columns
1964        );
1965
1966        // Behind the catalog's back, swap the view's two recorded columns,
1967        // change the index key's column and nullability, and add a column and
1968        // a key beyond the planner's output.
1969        {
1970            let (columns_key, columns_value) = written.item_columns.iter().next().expect("one row");
1971            let (index_key, index_value) = written.index_columns.iter().next().expect("one row");
1972            let mut stale_columns = columns_value.clone();
1973            stale_columns.columns.swap(0, 1);
1974            let surplus_column = stale_columns.columns[0].clone();
1975            stale_columns.columns.push(surplus_column);
1976            let mut stale_index = index_value.clone();
1977            let changed_key = &mut stale_index.columns[0];
1978            changed_key.column = changed_key.column.map(|column| column + 1);
1979            changed_key.nullable = !changed_key.nullable;
1980            let surplus_key = index_value.columns[0];
1981            stale_index.columns.push(surplus_key);
1982
1983            let mut debug_state = TestCatalogStateBuilder::new(persist_client.clone())
1984                .with_organization_id(organization_id)
1985                .with_default_deploy_generation()
1986                .unwrap_build()
1987                .await
1988                .open_debug()
1989                .await
1990                .expect("unable to open debug state");
1991            debug_state
1992                .edit::<ItemColumnsCollection>(columns_key.clone(), stale_columns)
1993                .await
1994                .expect("edit columns");
1995            debug_state
1996                .edit::<IndexColumnsCollection>(index_key.clone(), stale_index)
1997                .await
1998                .expect("edit index keys");
1999        }
2000
2001        // The next refresh stages a rewrite of each disagreeing row, a
2002        // retraction and an addition each.
2003        {
2004            let catalog = Catalog::open_debug_read_only_catalog(
2005                persist_client.clone(),
2006                organization_id,
2007                &bootstrap_args,
2008            )
2009            .await
2010            .expect("unable to open read-only debug catalog");
2011            let error = refresh(&catalog)
2012                .await
2013                .expect_err("a refresh with stale rows rewrites them");
2014            let CatalogError::Durable(DurableCatalogError::NotWritable(message)) = error else {
2015                panic!("unexpected error {error:?}");
2016            };
2017            assert!(
2018                message.contains("4 updates")
2019                    && message.contains("ItemColumns")
2020                    && message.contains("IndexColumns"),
2021                "{message}"
2022            );
2023            catalog.expire().await;
2024        }
2025        // A writable open puts the planner's rows back.
2026        {
2027            let catalog = Catalog::open_debug_catalog(
2028                persist_client.clone(),
2029                organization_id,
2030                &bootstrap_args,
2031            )
2032            .await
2033            .expect("unable to open debug catalog");
2034            let rewritten = recorded(&catalog).await;
2035            assert_eq!(rewritten.item_columns, written.item_columns);
2036            assert_eq!(rewritten.index_columns, written.index_columns);
2037            catalog.expire().await;
2038        }
2039    }
2040}