1mod 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
69use 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 pub state: CatalogState,
78 pub migrated_storage_collections_0dt: BTreeSet<CatalogItemId>,
87 pub new_builtin_collections: BTreeSet<GlobalId>,
89 pub builtin_table_updates: Vec<BuiltinTableUpdate>,
91 pub last_seen_version: Option<Version>,
98 pub expr_cache_handle: Option<ExpressionCacheHandle>,
100 pub cached_global_exprs: BTreeMap<GlobalId, GlobalExpressions>,
102 pub uncached_local_exprs: BTreeMap<GlobalId, LocalExpressions>,
104}
105
106pub struct OpenCatalogResult {
107 pub catalog: Catalog,
109 pub last_seen_version: Option<Version>,
111 pub migrated_storage_collections_0dt: BTreeSet<CatalogItemId>,
114 pub new_builtin_collections: BTreeSet<GlobalId>,
116 pub builtin_table_updates: Vec<BuiltinTableUpdate>,
118 pub cached_global_exprs: BTreeMap<GlobalId, GlobalExpressions>,
120 pub uncached_local_exprs: BTreeMap<GlobalId, LocalExpressions>,
122}
123
124impl Catalog {
125 pub async fn initialize_state<'a>(
129 config: StateConfig,
130 storage: &'a mut Box<dyn mz_catalog::durable::DurableCatalogState>,
131 ) -> Result<InitializeStateResult, AdapterError> {
132 for builtin_role in BUILTIN_ROLES {
133 assert!(
134 is_reserved_name(builtin_role.name),
135 "builtin role {builtin_role:?} must start with one of the following prefixes {}",
136 BUILTIN_PREFIXES.join(", ")
137 );
138 }
139 for builtin_cluster in BUILTIN_CLUSTERS {
140 assert!(
141 is_reserved_name(builtin_cluster.name),
142 "builtin cluster {builtin_cluster:?} must start with one of the following prefixes {}",
143 BUILTIN_PREFIXES.join(", ")
144 );
145 }
146
147 let mut system_configuration = SystemVars::new().set_unsafe(config.unsafe_mode);
148 if config.all_features {
149 system_configuration.enable_all_feature_flags_by_default();
150 }
151
152 let mut state = CatalogState {
153 database_by_name: imbl::OrdMap::new(),
154 database_by_id: imbl::OrdMap::new(),
155 entry_by_id: imbl::OrdMap::new(),
156 entry_by_global_id: imbl::OrdMap::new(),
157 notices_by_dep_id: imbl::OrdMap::new(),
158 ambient_schemas_by_name: imbl::OrdMap::new(),
159 ambient_schemas_by_id: imbl::OrdMap::new(),
160 clusters_by_name: imbl::OrdMap::new(),
161 clusters_by_id: imbl::OrdMap::new(),
162 roles_by_name: imbl::OrdMap::new(),
163 roles_by_id: imbl::OrdMap::new(),
164 network_policies_by_id: imbl::OrdMap::new(),
165 role_auth_by_id: imbl::OrdMap::new(),
166 network_policies_by_name: imbl::OrdMap::new(),
167 system_configuration: Arc::new(system_configuration),
168 scoped_system_parameters: Default::default(),
169 default_privileges: Arc::new(DefaultPrivileges::default()),
170 system_privileges: Arc::new(PrivilegeMap::default()),
171 comments: Arc::new(CommentsMap::default()),
172 source_references: imbl::OrdMap::new(),
173 storage_metadata: Arc::new(StorageMetadata::default()),
174 temporary_namespaces: Default::default(),
175 mock_authentication_nonce: Default::default(),
176 config: mz_sql::catalog::CatalogConfig {
177 start_time: to_datetime((config.now)()),
178 start_instant: Instant::now(),
179 nonce: rand::random(),
180 environment_id: config.environment_id,
181 session_id: Uuid::new_v4(),
182 build_info: config.build_info,
183 now: config.now.clone(),
184 connection_context: config.connection_context,
185 aws_account_id: config
186 .aws_principal_context
187 .as_ref()
188 .map(|c| c.aws_account_id.clone()),
189 helm_chart_version: config.helm_chart_version,
190 },
191 cluster_replica_sizes: 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 let new_builtin_collections = {
210 migrate::durable_migrate(
211 &mut txn,
212 state.config.environment_id.organization_id(),
213 config.boot_ts,
214 )?;
215 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 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 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 {
261 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 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 state.system_config().sync_dyncfgs();
353
354 {
358 if let Some(password) = config.external_login_password_mz_system {
359 let role_auth = RoleAuth {
360 role_id: MZ_SYSTEM_ROLE_ID,
361 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 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 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 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 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 post_item_updates.extend(migrate_result.post_item_updates);
490 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 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 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 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 if !txn.is_read_only() {
559 state.refresh_item_metadata(&mut txn)?;
560 }
561
562 set_migration_version(&mut txn, config.build_info.semver_version())?;
564
565 txn.commit(config.boot_ts).await?;
566
567 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 #[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 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 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 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 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 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 self.state = state;
731 Ok(())
732 }
733
734 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 pub async fn expire(self) {
776 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 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
796fn 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 let mut builtin_descs = HashSet::new();
805
806 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 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 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 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 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 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 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 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 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 let delete_exceptions: HashSet<SystemObjectDescription> = [].into();
996 assert!(
1000 deleted_system_objects
1001 .iter()
1002 .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 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 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 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 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 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 let old_roles = durable_roles.values().map(|role| role.id).collect();
1163 txn.remove_roles(&old_roles)?;
1164
1165 Ok(())
1166}
1167
1168fn 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 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
1221fn 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 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 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 let ClusterVariant::Managed(managed) = &cluster.config.variant else {
1305 continue;
1306 };
1307
1308 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 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 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 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 to_drop.extend(
1383 surplus
1384 .into_values()
1385 .map(|replica| (cluster.name.clone(), replica)),
1386 );
1387 }
1388
1389 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
1424fn managed_replica_config(managed: &ClusterVariantManaged) -> ReplicaConfig {
1438 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
1465fn 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 let session_vars = SessionVars::new_unchecked(&BUILD_INFO, SYSTEM_USER.clone(), None);
1484
1485 let mut invalid_roles_vars = BTreeMap::new();
1487 for (name, value) in &role.vars.map {
1488 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 invalid_roles_vars.is_empty() {
1500 return None;
1501 }
1502
1503 tracing::warn!(?role, ?invalid_roles_vars, "removing invalid role vars");
1504
1505 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
1519fn remove_pending_cluster_replicas_migration(
1528 tx: &mut Transaction,
1529 boot_ts: mz_repr::Timestamp,
1530) -> Result<(), anyhow::Error> {
1531 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 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 pub system_cluster: BootstrapBuiltinClusterConfig,
1587 pub catalog_server_cluster: BootstrapBuiltinClusterConfig,
1589 pub probe_cluster: BootstrapBuiltinClusterConfig,
1591 pub support_cluster: BootstrapBuiltinClusterConfig,
1593 pub analytics_cluster: BootstrapBuiltinClusterConfig,
1595}
1596
1597impl BuiltinBootstrapClusterConfigMap {
1598 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
1622pub(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 #[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 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 assert_eq!(billed_as, None);
1704 assert!(!internal);
1705 assert!(!pending);
1706 }
1707 ReplicaLocation::Unmanaged { .. } => panic!("expected a managed location"),
1708 }
1709 }
1710
1711 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 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 async fn recorded(catalog: &Catalog) -> Snapshot {
1766 catalog.storage().await.snapshot().await.expect("snapshot")
1767 }
1768
1769 #[mz_ore::test(tokio::test)]
1776 #[cfg_attr(miri, ignore)] 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 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 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 {
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 {
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 {
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 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 #[mz_ore::test(tokio::test)]
1922 #[cfg_attr(miri, ignore)] 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 {
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 {
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 {
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}