1use std::collections::{BTreeMap, BTreeSet};
32use std::sync::{Arc, LazyLock};
33
34use anyhow::bail;
35use futures::FutureExt;
36use futures::future::BoxFuture;
37use mz_build_info::{BuildInfo, DUMMY_BUILD_INFO};
38use mz_catalog::builtin::{
39 BUILTIN_LOOKUP, Builtin, Fingerprint, MZ_CATALOG_RAW, MZ_CATALOG_RAW_DESCRIPTION,
40 MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, MZ_STORAGE_USAGE_BY_SHARD,
41 MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
42};
43use mz_catalog::config::BuiltinItemMigrationConfig;
44use mz_catalog::durable::objects::SystemObjectUniqueIdentifier;
45use mz_catalog::durable::{SystemObjectDescription, SystemObjectMapping, Transaction};
46use mz_catalog::memory::error::{Error, ErrorKind};
47use mz_ore::soft_assert_or_log;
48use mz_persist_client::cfg::USE_CRITICAL_SINCE_CATALOG;
49use mz_persist_client::critical::{Opaque, SinceHandle};
50use mz_persist_client::read::ReadHandle;
51use mz_persist_client::schema::CaESchema;
52use mz_persist_client::write::WriteHandle;
53use mz_persist_client::{Diagnostics, PersistClient};
54use mz_persist_types::ShardId;
55use mz_persist_types::codec_impls::{ShardIdSchema, UnitSchema};
56use mz_persist_types::schema::backward_compatible;
57use mz_repr::namespaces::{MZ_CATALOG_SCHEMA, MZ_INTERNAL_SCHEMA};
58use mz_repr::{CatalogItemId, GlobalId, Timestamp};
59use mz_sql::catalog::{CatalogItemType, NameReference};
60use mz_storage_client::controller::StorageTxn;
61use mz_storage_types::StorageDiff;
62use mz_storage_types::sources::SourceData;
63use semver::Version;
64use timely::progress::Antichain;
65use tracing::{debug, info};
66
67use crate::catalog::migrate::get_migration_version;
68
69static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
85 vec![
86 MigrationStep::replacement(
87 "0.149.0",
88 CatalogItemType::Source,
89 MZ_INTERNAL_SCHEMA,
90 "mz_sink_statistics_raw",
91 ),
92 MigrationStep::replacement(
93 "0.149.0",
94 CatalogItemType::Source,
95 MZ_INTERNAL_SCHEMA,
96 "mz_source_statistics_raw",
97 ),
98 MigrationStep::evolution(
99 "0.159.0",
100 CatalogItemType::Source,
101 MZ_INTERNAL_SCHEMA,
102 "mz_cluster_replica_metrics_history",
103 ),
104 MigrationStep::replacement(
105 "0.160.0",
106 CatalogItemType::Table,
107 MZ_CATALOG_SCHEMA,
108 "mz_sinks",
109 ),
110 MigrationStep::replacement(
111 "26.18.0-dev.0",
112 CatalogItemType::MaterializedView,
113 MZ_CATALOG_SCHEMA,
114 "mz_databases",
115 ),
116 MigrationStep::replacement(
117 "26.19.0-dev.0",
118 CatalogItemType::MaterializedView,
119 MZ_CATALOG_SCHEMA,
120 "mz_schemas",
121 ),
122 MigrationStep::replacement(
123 "26.19.0-dev.0",
124 CatalogItemType::MaterializedView,
125 MZ_CATALOG_SCHEMA,
126 "mz_role_members",
127 ),
128 MigrationStep::replacement(
129 "26.19.0-dev.0",
130 CatalogItemType::MaterializedView,
131 MZ_INTERNAL_SCHEMA,
132 "mz_network_policies",
133 ),
134 MigrationStep::replacement(
135 "26.19.0-dev.0",
136 CatalogItemType::MaterializedView,
137 MZ_INTERNAL_SCHEMA,
138 "mz_network_policy_rules",
139 ),
140 MigrationStep::replacement(
141 "26.19.0-dev.0",
142 CatalogItemType::MaterializedView,
143 MZ_INTERNAL_SCHEMA,
144 "mz_cluster_workload_classes",
145 ),
146 MigrationStep::replacement(
147 "26.19.0-dev.0",
148 CatalogItemType::MaterializedView,
149 MZ_INTERNAL_SCHEMA,
150 "mz_internal_cluster_replicas",
151 ),
152 MigrationStep::replacement(
153 "26.19.0-dev.0",
154 CatalogItemType::MaterializedView,
155 MZ_INTERNAL_SCHEMA,
156 "mz_pending_cluster_replicas",
157 ),
158 MigrationStep::replacement(
159 "26.20.0-dev.0",
160 CatalogItemType::MaterializedView,
161 MZ_CATALOG_SCHEMA,
162 "mz_materialized_views",
163 ),
164 MigrationStep::replacement(
165 "26.22.0-dev.0",
166 CatalogItemType::MaterializedView,
167 MZ_CATALOG_SCHEMA,
168 "mz_connections",
169 ),
170 MigrationStep::replacement(
171 "26.22.0-dev.0",
172 CatalogItemType::MaterializedView,
173 MZ_CATALOG_SCHEMA,
174 "mz_secrets",
175 ),
176 MigrationStep::replacement(
177 "26.27.0-dev.0",
178 CatalogItemType::MaterializedView,
179 MZ_CATALOG_SCHEMA,
180 "mz_sources",
181 ),
182 MigrationStep::replacement(
183 "26.29.0-dev.0",
184 CatalogItemType::MaterializedView,
185 MZ_CATALOG_SCHEMA,
186 "mz_indexes",
187 ),
188 MigrationStep::replacement(
189 "26.29.0-dev.0",
190 CatalogItemType::MaterializedView,
191 MZ_CATALOG_SCHEMA,
192 "mz_roles",
193 ),
194 MigrationStep::replacement(
195 "26.29.0-dev.0",
196 CatalogItemType::MaterializedView,
197 MZ_CATALOG_SCHEMA,
198 "mz_role_parameters",
199 ),
200 MigrationStep::replacement(
205 "26.30.0-dev.0",
206 CatalogItemType::MaterializedView,
207 MZ_CATALOG_SCHEMA,
208 "mz_indexes",
209 ),
210 MigrationStep::replacement(
211 "26.30.0-dev.0",
212 CatalogItemType::MaterializedView,
213 MZ_CATALOG_SCHEMA,
214 "mz_clusters",
215 ),
216 MigrationStep::replacement(
217 "26.30.0-dev.0",
218 CatalogItemType::MaterializedView,
219 MZ_CATALOG_SCHEMA,
220 "mz_cluster_replicas",
221 ),
222 MigrationStep::replacement(
223 "26.30.0-dev.0",
224 CatalogItemType::MaterializedView,
225 MZ_INTERNAL_SCHEMA,
226 "mz_cluster_schedules",
227 ),
228 MigrationStep::replacement(
229 "26.30.0-dev.0",
230 CatalogItemType::MaterializedView,
231 MZ_CATALOG_SCHEMA,
232 "mz_default_privileges",
233 ),
234 MigrationStep::replacement(
235 "26.30.0-dev.0",
236 CatalogItemType::MaterializedView,
237 MZ_CATALOG_SCHEMA,
238 "mz_system_privileges",
239 ),
240 MigrationStep::replacement(
244 "26.31.0-dev.0",
245 CatalogItemType::MaterializedView,
246 MZ_CATALOG_SCHEMA,
247 "mz_cluster_replicas",
248 ),
249 MigrationStep::replacement(
250 "26.32.0-dev.0",
251 CatalogItemType::MaterializedView,
252 MZ_INTERNAL_SCHEMA,
253 "mz_comments",
254 ),
255 MigrationStep::replacement(
260 "26.32.0-dev.0",
261 CatalogItemType::MaterializedView,
262 MZ_CATALOG_SCHEMA,
263 "mz_indexes",
264 ),
265 MigrationStep::replacement(
266 "26.33.0-dev.0",
267 CatalogItemType::MaterializedView,
268 MZ_CATALOG_SCHEMA,
269 "mz_audit_events",
270 ),
271 ]
272});
273
274#[derive(Clone, Debug)]
276struct MigrationStep {
277 version: Version,
279 object: SystemObjectDescription,
281 mechanism: Mechanism,
283}
284
285impl MigrationStep {
286 fn evolution(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
288 Self {
289 version: Version::parse(version).expect("valid"),
290 object: SystemObjectDescription {
291 schema_name: schema.into(),
292 object_type: type_,
293 object_name: name.into(),
294 },
295 mechanism: Mechanism::Evolution,
296 }
297 }
298
299 fn replacement(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
301 Self {
302 version: Version::parse(version).expect("valid"),
303 object: SystemObjectDescription {
304 schema_name: schema.into(),
305 object_type: type_,
306 object_name: name.into(),
307 },
308 mechanism: Mechanism::Replacement,
309 }
310 }
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
315#[allow(dead_code)]
316enum Mechanism {
317 Evolution,
322 Replacement,
326}
327
328pub(super) struct MigrationResult {
330 pub replaced_items: BTreeSet<CatalogItemId>,
332 pub cleanup_action: BoxFuture<'static, ()>,
334}
335
336impl Default for MigrationResult {
337 fn default() -> Self {
338 Self {
339 replaced_items: Default::default(),
340 cleanup_action: async {}.boxed(),
341 }
342 }
343}
344
345pub(super) async fn run(
351 build_info: &BuildInfo,
352 deploy_generation: u64,
353 txn: &mut Transaction<'_>,
354 config: BuiltinItemMigrationConfig,
355) -> Result<MigrationResult, Error> {
356 assert_eq!(config.read_only, txn.is_savepoint());
358
359 if *build_info == DUMMY_BUILD_INFO {
362 return Ok(MigrationResult::default());
363 }
364
365 let Some(durable_version) = get_migration_version(txn) else {
366 return Ok(MigrationResult::default());
368 };
369 let build_version = build_info.semver_version();
370
371 let collection_metadata = txn.get_collection_metadata();
372 let system_objects = txn
373 .get_system_object_mappings()
374 .map(|m| {
375 let object = m.description;
376 let global_id = m.unique_identifier.global_id;
377 let shard_id = collection_metadata.get(&global_id).copied();
378 let Some((_, builtin)) = BUILTIN_LOOKUP.get(&object) else {
379 panic!("missing builtin {object:?}");
380 };
381 let info = ObjectInfo {
382 global_id,
383 shard_id,
384 builtin,
385 fingerprint: m.unique_identifier.fingerprint,
386 };
387 (object, info)
388 })
389 .collect();
390
391 let migration_shard = txn.get_builtin_migration_shard().expect("must exist");
392
393 let migration = Migration {
394 source_version: durable_version.clone(),
395 target_version: build_version.clone(),
396 deploy_generation,
397 system_objects,
398 migration_shard,
399 config,
400 };
401
402 let result = migration.run(&MIGRATIONS).await.map_err(|e| {
403 Error::new(ErrorKind::FailedBuiltinSchemaMigration {
404 last_seen_version: durable_version.to_string(),
405 this_version: build_version.to_string(),
406 cause: e.to_string(),
407 })
408 })?;
409
410 result.apply(txn);
411
412 let replaced_items = txn
413 .get_system_object_mappings()
414 .map(|m| m.unique_identifier)
415 .filter(|ids| result.new_shards.contains_key(&ids.global_id))
416 .map(|ids| ids.catalog_id)
417 .collect();
418
419 Ok(MigrationResult {
420 replaced_items,
421 cleanup_action: result.cleanup_action,
422 })
423}
424
425struct MigrationRunResult {
427 new_shards: BTreeMap<GlobalId, ShardId>,
428 new_fingerprints: BTreeMap<SystemObjectDescription, String>,
429 shards_to_finalize: BTreeSet<ShardId>,
430 cleanup_action: BoxFuture<'static, ()>,
431}
432
433impl Default for MigrationRunResult {
434 fn default() -> Self {
435 Self {
436 new_shards: BTreeMap::new(),
437 new_fingerprints: BTreeMap::new(),
438 shards_to_finalize: BTreeSet::new(),
439 cleanup_action: async {}.boxed(),
440 }
441 }
442}
443
444impl MigrationRunResult {
445 fn apply(&self, txn: &mut Transaction<'_>) {
447 let replaced_ids = self.new_shards.keys().copied().collect();
449 let old_metadata = txn.delete_collection_metadata(replaced_ids);
450 txn.insert_collection_metadata(self.new_shards.clone())
451 .expect("inserting unique shards IDs after deleting existing entries");
452
453 let mut unfinalized_shards: BTreeSet<_> =
455 old_metadata.into_iter().map(|(_, sid)| sid).collect();
456 unfinalized_shards.extend(self.shards_to_finalize.iter().copied());
457 txn.insert_unfinalized_shards(unfinalized_shards)
458 .expect("cannot fail");
459
460 let mappings = txn
462 .get_system_object_mappings()
463 .filter_map(|m| {
464 let fingerprint = self.new_fingerprints.get(&m.description)?;
465 Some(SystemObjectMapping {
466 description: m.description,
467 unique_identifier: SystemObjectUniqueIdentifier {
468 catalog_id: m.unique_identifier.catalog_id,
469 global_id: m.unique_identifier.global_id,
470 fingerprint: fingerprint.clone(),
471 },
472 })
473 })
474 .collect();
475 txn.set_system_object_mappings(mappings)
476 .expect("filtered existing mappings remain unique");
477 }
478}
479
480#[derive(Clone, Debug)]
482struct ObjectInfo {
483 global_id: GlobalId,
484 shard_id: Option<ShardId>,
485 builtin: &'static Builtin<NameReference>,
486 fingerprint: String,
487}
488
489struct Migration {
491 source_version: Version,
496 target_version: Version,
500 deploy_generation: u64,
502 system_objects: BTreeMap<SystemObjectDescription, ObjectInfo>,
504 migration_shard: ShardId,
506 config: BuiltinItemMigrationConfig,
508}
509
510impl Migration {
511 async fn run(self, steps: &[MigrationStep]) -> anyhow::Result<MigrationRunResult> {
512 info!(
513 deploy_generation = %self.deploy_generation,
514 "running builtin schema migration: {} -> {}",
515 self.source_version, self.target_version
516 );
517
518 self.validate_migration_steps(steps);
519
520 let force_migration = if self.source_version != self.target_version
523 && self.source_version.pre.as_str().starts_with("dev")
524 && self.config.force_migration.is_none()
525 {
526 Some("evolution".to_string())
527 } else {
528 self.config.force_migration.clone()
529 };
530
531 let (force, plan) = match force_migration.as_deref() {
532 None => (false, self.plan_migration(steps)),
533 Some("evolution") => (true, self.plan_forced_migration(Mechanism::Evolution)),
534 Some("replacement") => (true, self.plan_forced_migration(Mechanism::Replacement)),
535 Some(other) => panic!("unknown force migration mechanism: {other}"),
536 };
537
538 if self.source_version == self.target_version && !force {
539 info!("skipping migration: already at target version");
540 return Ok(MigrationRunResult::default());
541 } else if self.source_version > self.target_version {
542 bail!("downgrade not supported");
543 }
544
545 if !self.config.read_only {
548 self.upgrade_migration_shard_version().await;
549 }
550
551 info!("executing migration plan: {plan:?}");
552
553 self.migrate_evolve(&plan.evolve).await?;
554 let new_shards = self.migrate_replace(&plan.replace).await?;
555
556 let mut migrated_objects = BTreeSet::new();
557 migrated_objects.extend(plan.evolve);
558 migrated_objects.extend(plan.replace);
559
560 let new_fingerprints = self.update_fingerprints(&migrated_objects)?;
561
562 let (shards_to_finalize, cleanup_action) = self.cleanup().await?;
563
564 Ok(MigrationRunResult {
565 new_shards,
566 new_fingerprints,
567 shards_to_finalize,
568 cleanup_action,
569 })
570 }
571
572 fn validate_migration_steps(&self, steps: &[MigrationStep]) {
576 for step in steps {
577 assert!(
578 step.version <= self.target_version,
579 "migration step version greater than target version: {} > {}",
580 step.version,
581 self.target_version,
582 );
583
584 let object = &step.object;
585
586 assert_ne!(
594 &*MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, object,
595 "mz_storage_usage_by_shard cannot be migrated or else the table will be truncated"
596 );
597
598 assert_ne!(
602 &*MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, object,
603 "mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
604 );
605
606 assert_ne!(
609 &*MZ_CATALOG_RAW_DESCRIPTION, object,
610 "mz_catalog_raw cannot be migrated"
611 );
612
613 let Some(object_info) = self.system_objects.get(object) else {
614 panic!("migration step for non-existent builtin: {object:?}");
615 };
616
617 let builtin = object_info.builtin;
618 use Builtin::*;
619 assert!(
620 matches!(builtin, Table(..) | Source(..) | MaterializedView(..)),
621 "schema migration not supported for builtin: {builtin:?}",
622 );
623 }
624 }
625
626 fn plan_migration(&self, steps: &[MigrationStep]) -> Plan {
628 let steps = steps.iter().filter(|s| s.version > self.source_version);
630
631 let mut by_object = BTreeMap::new();
635 for step in steps {
636 if let Some(entry) = by_object.get_mut(&step.object) {
637 *entry = match (step.mechanism, *entry) {
638 (Mechanism::Evolution, Mechanism::Evolution) => Mechanism::Evolution,
639 (Mechanism::Replacement, _) | (_, Mechanism::Replacement) => {
640 Mechanism::Replacement
641 }
642 };
643 } else {
644 by_object.insert(step.object.clone(), step.mechanism);
645 }
646 }
647
648 let mut plan = Plan::default();
649 for (object, mechanism) in by_object {
650 match mechanism {
651 Mechanism::Evolution => plan.evolve.push(object),
652 Mechanism::Replacement => plan.replace.push(object),
653 }
654 }
655
656 plan
657 }
658
659 fn plan_forced_migration(&self, mechanism: Mechanism) -> Plan {
661 let objects = self
662 .system_objects
663 .iter()
664 .filter(|(_, info)| info.shard_id.is_some())
668 .filter(|(_, info)| {
669 use Builtin::*;
670 match info.builtin {
671 Table(table) => **table != *MZ_STORAGE_USAGE_BY_SHARD,
674 MaterializedView(..) => true,
675 Source(source) => **source != *MZ_CATALOG_RAW,
676 Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,
677 }
678 })
679 .map(|(object, _)| object.clone())
680 .collect();
681
682 let mut plan = Plan::default();
683 match mechanism {
684 Mechanism::Evolution => plan.evolve = objects,
685 Mechanism::Replacement => plan.replace = objects,
686 }
687
688 plan
689 }
690
691 async fn upgrade_migration_shard_version(&self) {
693 let persist = &self.config.persist_client;
694 let diagnostics = Diagnostics {
695 shard_name: "builtin_migration".to_string(),
696 handle_purpose: format!("migration shard upgrade @ {}", self.target_version),
697 };
698
699 persist
700 .upgrade_version::<migration_shard::Key, ShardId, Timestamp, StorageDiff>(
701 self.migration_shard,
702 diagnostics,
703 )
704 .await
705 .expect("valid usage");
706 }
707
708 async fn migrate_evolve(&self, objects: &[SystemObjectDescription]) -> anyhow::Result<()> {
710 for object in objects {
711 self.migrate_evolve_one(object).await?;
712 }
713 Ok(())
714 }
715
716 async fn migrate_evolve_one(&self, object: &SystemObjectDescription) -> anyhow::Result<()> {
717 let persist = &self.config.persist_client;
718
719 let Some(object_info) = self.system_objects.get(object) else {
720 bail!("missing builtin {object:?}");
721 };
722 let id = object_info.global_id;
723
724 let Some(shard_id) = object_info.shard_id else {
725 if self.config.read_only {
730 bail!("missing shard ID for builtin {object:?} ({id})");
731 } else {
732 return Ok(());
733 }
734 };
735
736 let target_desc = match object_info.builtin {
737 Builtin::Table(table) => &table.desc,
738 Builtin::Source(source) => &source.desc,
739 Builtin::MaterializedView(mv) => &mv.desc,
740 _ => bail!("not a storage collection: {object:?}"),
741 };
742
743 let diagnostics = Diagnostics {
744 shard_name: id.to_string(),
745 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
746 };
747 let source_schema = persist
748 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics.clone())
749 .await
750 .expect("valid usage");
751
752 info!(?object, %id, %shard_id, ?source_schema, ?target_desc, "migrating by evolution");
753
754 if self.config.read_only {
755 if let Some((_, source_desc, _)) = &source_schema {
758 let old = mz_persist_types::columnar::data_type::<SourceData>(source_desc)?;
759 let new = mz_persist_types::columnar::data_type::<SourceData>(target_desc)?;
760 if backward_compatible(&old, &new).is_none() {
761 bail!(
762 "incompatible schema evolution for {object:?}: \
763 {source_desc:?} -> {target_desc:?}"
764 );
765 }
766 }
767
768 return Ok(());
769 }
770
771 let (mut schema_id, mut source_desc) = match source_schema {
772 Some((schema_id, source_desc, _)) => (schema_id, source_desc),
773 None => {
774 debug!(%id, %shard_id, "no previous schema found; registering initial one");
779 let schema_id = persist
780 .register_schema::<SourceData, (), Timestamp, StorageDiff>(
781 shard_id,
782 target_desc,
783 &UnitSchema,
784 diagnostics.clone(),
785 )
786 .await
787 .expect("valid usage");
788 if schema_id.is_some() {
789 return Ok(());
790 }
791
792 debug!(%id, %shard_id, "schema registration failed; falling back to CaES");
793 let (schema_id, source_desc, _) = persist
794 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(
795 shard_id,
796 diagnostics.clone(),
797 )
798 .await
799 .expect("valid usage")
800 .expect("known to exist");
801
802 (schema_id, source_desc)
803 }
804 };
805
806 loop {
807 debug!(%id, %shard_id, %schema_id, ?source_desc, ?target_desc, "attempting CaES");
812 let result = persist
813 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
814 shard_id,
815 schema_id,
816 target_desc,
817 &UnitSchema,
818 diagnostics.clone(),
819 )
820 .await
821 .expect("valid usage");
822
823 match result {
824 CaESchema::Ok(schema_id) => {
825 debug!(%id, %shard_id, %schema_id, "schema evolved successfully");
826 break;
827 }
828 CaESchema::Incompatible => bail!(
829 "incompatible schema evolution for {object:?}: \
830 {source_desc:?} -> {target_desc:?}"
831 ),
832 CaESchema::ExpectedMismatch {
833 schema_id: new_id,
834 key,
835 val: UnitSchema,
836 } => {
837 schema_id = new_id;
838 source_desc = key;
839 }
840 }
841 }
842
843 Ok(())
844 }
845
846 async fn migrate_replace(
848 &self,
849 objects: &[SystemObjectDescription],
850 ) -> anyhow::Result<BTreeMap<GlobalId, ShardId>> {
851 if objects.is_empty() {
852 return Ok(Default::default());
853 }
854
855 let diagnostics = Diagnostics {
856 shard_name: "builtin_migration".to_string(),
857 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
858 };
859 let (mut persist_write, mut persist_read) =
860 self.open_migration_shard(diagnostics.clone()).await;
861
862 let mut ids_to_replace = BTreeSet::new();
863 for object in objects {
864 if let Some(info) = self.system_objects.get(object) {
865 ids_to_replace.insert(info.global_id);
866 } else {
867 bail!("missing id for builtin {object:?}");
868 }
869 }
870
871 info!(?objects, ?ids_to_replace, "migrating by replacement");
872
873 let replaced_shards = loop {
876 if let Some(shards) = self
877 .try_get_or_insert_replacement_shards(
878 &ids_to_replace,
879 &mut persist_write,
880 &mut persist_read,
881 )
882 .await?
883 {
884 break shards;
885 }
886 };
887
888 Ok(replaced_shards)
889 }
890
891 async fn try_get_or_insert_replacement_shards(
901 &self,
902 ids_to_replace: &BTreeSet<GlobalId>,
903 persist_write: &mut WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
904 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
905 ) -> anyhow::Result<Option<BTreeMap<GlobalId, ShardId>>> {
906 let upper = persist_write.fetch_recent_upper().await;
907 let write_ts = *upper.as_option().expect("migration shard not sealed");
908
909 let mut ids_to_replace = ids_to_replace.clone();
910 let mut replaced_shards = BTreeMap::new();
911
912 if let Some(read_ts) = write_ts.step_back() {
920 let pred = |key: &migration_shard::Key| {
921 key.build_version == self.target_version
922 && key.deploy_generation == Some(self.deploy_generation)
923 };
924 if let Some(entries) = read_migration_shard(persist_read, read_ts, pred).await {
925 for (key, shard_id) in entries {
926 let id = GlobalId::System(key.global_id);
927 if ids_to_replace.remove(&id) {
928 replaced_shards.insert(id, shard_id);
929 }
930 }
931
932 debug!(
933 %read_ts, ?replaced_shards, ?ids_to_replace,
934 "found existing entries in migration shard",
935 );
936 }
937
938 if ids_to_replace.is_empty() {
939 return Ok(Some(replaced_shards));
940 }
941 }
942
943 let mut updates = Vec::new();
947 for id in ids_to_replace {
948 let shard_id = ShardId::new();
949 replaced_shards.insert(id, shard_id);
950
951 let GlobalId::System(global_id) = id else {
952 bail!("attempt to migrate a non-system collection: {id}");
953 };
954 let key = migration_shard::Key {
955 global_id,
956 build_version: self.target_version.clone(),
957 deploy_generation: Some(self.deploy_generation),
958 };
959 updates.push(((key, shard_id), write_ts, 1));
960 }
961
962 let upper = Antichain::from_elem(write_ts);
963 let new_upper = Antichain::from_elem(write_ts.step_forward());
964 debug!(%write_ts, "attempting insert into migration shard");
965 let result = persist_write
966 .compare_and_append(updates, upper, new_upper)
967 .await
968 .expect("valid usage");
969
970 match result {
971 Ok(()) => {
972 debug!(
973 %write_ts, ?replaced_shards,
974 "successfully inserted into migration shard"
975 );
976 Ok(Some(replaced_shards))
977 }
978 Err(_mismatch) => Ok(None),
979 }
980 }
981
982 async fn open_migration_shard(
984 &self,
985 diagnostics: Diagnostics,
986 ) -> (
987 WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
988 ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
989 ) {
990 let persist = &self.config.persist_client;
991
992 persist
993 .open(
994 self.migration_shard,
995 Arc::new(migration_shard::KeySchema),
996 Arc::new(ShardIdSchema),
997 diagnostics,
998 USE_CRITICAL_SINCE_CATALOG.get(persist.dyncfgs()),
999 )
1000 .await
1001 .expect("valid usage")
1002 }
1003
1004 async fn open_migration_shard_since(
1006 &self,
1007 diagnostics: Diagnostics,
1008 ) -> SinceHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff> {
1009 self.config
1010 .persist_client
1011 .open_critical_since(
1012 self.migration_shard,
1013 PersistClient::CONTROLLER_CRITICAL_SINCE,
1016 Opaque::encode(&i64::MIN),
1017 diagnostics.clone(),
1018 )
1019 .await
1020 .expect("valid usage")
1021 }
1022
1023 fn update_fingerprints(
1028 &self,
1029 migrated_items: &BTreeSet<SystemObjectDescription>,
1030 ) -> anyhow::Result<BTreeMap<SystemObjectDescription, String>> {
1031 let mut new_fingerprints = BTreeMap::new();
1032 for (object, object_info) in &self.system_objects {
1033 let id = object_info.global_id;
1034 let builtin = object_info.builtin;
1035
1036 let fingerprint = builtin.fingerprint();
1037 if fingerprint == object_info.fingerprint {
1038 continue; }
1040
1041 let migrated = migrated_items.contains(object);
1043 let ephemeral = matches!(
1045 builtin,
1046 Builtin::Log(_) | Builtin::View(_) | Builtin::Index(_),
1047 );
1048
1049 if migrated || ephemeral {
1050 new_fingerprints.insert(object.clone(), fingerprint);
1051 } else if builtin.runtime_alterable() {
1052 assert_eq!(
1055 object_info.fingerprint, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
1056 "fingerprint mismatch for runtime-alterable builtin {object:?} ({id})",
1057 );
1058 } else {
1059 panic!(
1060 "fingerprint mismatch for builtin {builtin:?} ({id}): {} != {}",
1061 fingerprint, object_info.fingerprint,
1062 );
1063 }
1064 }
1065
1066 Ok(new_fingerprints)
1067 }
1068
1069 async fn cleanup(&self) -> anyhow::Result<(BTreeSet<ShardId>, BoxFuture<'static, ()>)> {
1086 let noop_action = async {}.boxed();
1087 let noop_result = (BTreeSet::new(), noop_action);
1088
1089 if self.config.read_only {
1090 return Ok(noop_result);
1091 }
1092
1093 let diagnostics = Diagnostics {
1094 shard_name: "builtin_migration".to_string(),
1095 handle_purpose: "builtin schema migration cleanup".into(),
1096 };
1097 let (mut persist_write, mut persist_read) =
1098 self.open_migration_shard(diagnostics.clone()).await;
1099 let mut persist_since = self.open_migration_shard_since(diagnostics.clone()).await;
1100
1101 let upper = persist_write.fetch_recent_upper().await.clone();
1102 let write_ts = *upper.as_option().expect("migration shard not sealed");
1103 let Some(read_ts) = write_ts.step_back() else {
1104 return Ok(noop_result);
1105 };
1106
1107 let pred = |key: &migration_shard::Key| key.build_version < self.target_version;
1109 let Some(stale_entries) = read_migration_shard(&mut persist_read, read_ts, pred).await
1110 else {
1111 return Ok(noop_result);
1112 };
1113
1114 debug!(
1115 ?stale_entries,
1116 "cleaning migration shard up to version {}", self.target_version,
1117 );
1118
1119 let current_shards: BTreeMap<_, _> = self
1120 .system_objects
1121 .values()
1122 .filter_map(|o| o.shard_id.map(|shard_id| (o.global_id, shard_id)))
1123 .collect();
1124
1125 let mut shards_to_finalize = BTreeSet::new();
1126 let mut retractions = Vec::new();
1127 for (key, shard_id) in stale_entries {
1128 let gid = GlobalId::System(key.global_id);
1132 if current_shards.get(&gid) != Some(&shard_id) {
1133 shards_to_finalize.insert(shard_id);
1134 }
1135
1136 retractions.push(((key, shard_id), write_ts, -1));
1137 }
1138
1139 let cleanup_action = async move {
1140 if !retractions.is_empty() {
1141 let new_upper = Antichain::from_elem(write_ts.step_forward());
1142 let result = persist_write
1143 .compare_and_append(retractions, upper, new_upper)
1144 .await
1145 .expect("valid usage");
1146 match result {
1147 Ok(()) => debug!("cleaned up migration shard"),
1148 Err(mismatch) => debug!(?mismatch, "migration shard cleanup failed"),
1149 }
1150 }
1151 }
1152 .boxed();
1153
1154 let o = persist_since.opaque().clone();
1156 let new_since = Antichain::from_elem(read_ts);
1157 let result = persist_since
1158 .maybe_compare_and_downgrade_since(&o, (&o, &new_since))
1159 .await;
1160 soft_assert_or_log!(result.is_none_or(|r| r.is_ok()), "opaque mismatch");
1161
1162 Ok((shards_to_finalize, cleanup_action))
1163 }
1164}
1165
1166async fn read_migration_shard<P>(
1172 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1173 read_ts: Timestamp,
1174 predicate: P,
1175) -> Option<Vec<(migration_shard::Key, ShardId)>>
1176where
1177 P: for<'a> Fn(&migration_shard::Key) -> bool,
1178{
1179 let as_of = Antichain::from_elem(read_ts);
1180 let updates = persist_read.snapshot_and_fetch(as_of).await.ok()?;
1181
1182 assert!(
1183 updates.iter().all(|(_, _, diff)| *diff == 1),
1184 "migration shard contains invalid diffs: {updates:?}",
1185 );
1186
1187 let entries: Vec<_> = updates
1188 .into_iter()
1189 .map(|(data, _, _)| data)
1190 .filter(move |(key, _)| predicate(key))
1191 .collect();
1192
1193 (!entries.is_empty()).then_some(entries)
1194}
1195
1196#[derive(Debug, Default)]
1198struct Plan {
1199 evolve: Vec<SystemObjectDescription>,
1201 replace: Vec<SystemObjectDescription>,
1203}
1204
1205mod migration_shard {
1207 use std::fmt;
1208 use std::str::FromStr;
1209
1210 use arrow::array::{StringArray, StringBuilder};
1211 use bytes::{BufMut, Bytes};
1212 use mz_persist_types::Codec;
1213 use mz_persist_types::codec_impls::{
1214 SimpleColumnarData, SimpleColumnarDecoder, SimpleColumnarEncoder,
1215 };
1216 use mz_persist_types::columnar::Schema;
1217 use mz_persist_types::stats::NoneStats;
1218 use semver::Version;
1219 use serde::{Deserialize, Serialize};
1220
1221 #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1222 pub(super) struct Key {
1223 pub(super) global_id: u64,
1224 pub(super) build_version: Version,
1225 pub(super) deploy_generation: Option<u64>,
1229 }
1230
1231 impl fmt::Display for Key {
1232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1233 if self.deploy_generation.is_some() {
1234 let s = serde_json::to_string(self).expect("JSON serializable");
1236 f.write_str(&s)
1237 } else {
1238 write!(f, "{}-{}", self.global_id, self.build_version)
1240 }
1241 }
1242 }
1243
1244 impl FromStr for Key {
1245 type Err = String;
1246
1247 fn from_str(s: &str) -> Result<Self, String> {
1248 if let Ok(key) = serde_json::from_str(s) {
1250 return Ok(key);
1251 };
1252
1253 let parts: Vec<_> = s.splitn(2, '-').collect();
1255 let &[global_id, build_version] = parts.as_slice() else {
1256 return Err(format!("invalid Key '{s}'"));
1257 };
1258 let global_id = global_id.parse::<u64>().map_err(|e| e.to_string())?;
1259 let build_version = build_version
1260 .parse::<Version>()
1261 .map_err(|e| e.to_string())?;
1262 Ok(Key {
1263 global_id,
1264 build_version,
1265 deploy_generation: None,
1266 })
1267 }
1268 }
1269
1270 impl Default for Key {
1271 fn default() -> Self {
1272 Self {
1273 global_id: Default::default(),
1274 build_version: Version::new(0, 0, 0),
1275 deploy_generation: Some(0),
1276 }
1277 }
1278 }
1279
1280 impl Codec for Key {
1281 type Schema = KeySchema;
1282 type Storage = ();
1283
1284 fn codec_name() -> String {
1285 "TableKey".into()
1286 }
1287
1288 fn encode<B: BufMut>(&self, buf: &mut B) {
1289 buf.put(self.to_string().as_bytes())
1290 }
1291
1292 fn decode<'a>(buf: &'a [u8], _schema: &KeySchema) -> Result<Self, String> {
1293 let s = str::from_utf8(buf).map_err(|e| e.to_string())?;
1294 s.parse()
1295 }
1296
1297 fn encode_schema(_schema: &KeySchema) -> Bytes {
1298 Bytes::new()
1299 }
1300
1301 fn decode_schema(buf: &Bytes) -> Self::Schema {
1302 assert_eq!(*buf, Bytes::new());
1303 KeySchema
1304 }
1305 }
1306
1307 impl SimpleColumnarData for Key {
1308 type ArrowBuilder = StringBuilder;
1309 type ArrowColumn = StringArray;
1310
1311 fn goodbytes(builder: &Self::ArrowBuilder) -> usize {
1312 builder.values_slice().len()
1313 }
1314
1315 fn push(&self, builder: &mut Self::ArrowBuilder) {
1316 builder.append_value(&self.to_string());
1317 }
1318
1319 fn push_null(builder: &mut Self::ArrowBuilder) {
1320 builder.append_null();
1321 }
1322
1323 fn read(&mut self, idx: usize, column: &Self::ArrowColumn) {
1324 *self = column.value(idx).parse().expect("valid Key");
1325 }
1326 }
1327
1328 #[derive(Debug, PartialEq)]
1329 pub(super) struct KeySchema;
1330
1331 impl Schema<Key> for KeySchema {
1332 type ArrowColumn = StringArray;
1333 type Statistics = NoneStats;
1334 type Decoder = SimpleColumnarDecoder<Key>;
1335 type Encoder = SimpleColumnarEncoder<Key>;
1336
1337 fn encoder(&self) -> anyhow::Result<SimpleColumnarEncoder<Key>> {
1338 Ok(SimpleColumnarEncoder::default())
1339 }
1340
1341 fn decoder(&self, col: StringArray) -> anyhow::Result<SimpleColumnarDecoder<Key>> {
1342 Ok(SimpleColumnarDecoder::new(col))
1343 }
1344 }
1345}
1346
1347#[cfg(test)]
1348#[path = "builtin_schema_migration_tests.rs"]
1349mod tests;