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_CLUSTER_REPLICA_FRONTIERS_DESCRIPTION, MZ_COMPUTE_HYDRATION_TIMES_DESCRIPTION,
41 MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, MZ_OBJECT_HYDRATION_HISTORY,
42 MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, MZ_REPLICA_HYDRATION_HISTORY,
43 MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION, MZ_STORAGE_USAGE_BY_SHARD,
44 MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
45};
46use mz_catalog::config::BuiltinItemMigrationConfig;
47use mz_catalog::durable::objects::SystemObjectUniqueIdentifier;
48use mz_catalog::durable::{SystemObjectDescription, SystemObjectMapping, Transaction};
49use mz_catalog::memory::error::{Error, ErrorKind};
50use mz_ore::soft_assert_or_log;
51use mz_persist_client::cfg::USE_CRITICAL_SINCE_CATALOG;
52use mz_persist_client::critical::{Opaque, SinceHandle};
53use mz_persist_client::read::ReadHandle;
54use mz_persist_client::schema::CaESchema;
55use mz_persist_client::write::WriteHandle;
56use mz_persist_client::{Diagnostics, PersistClient};
57use mz_persist_types::ShardId;
58use mz_persist_types::codec_impls::{ShardIdSchema, UnitSchema};
59use mz_persist_types::schema::backward_compatible;
60use mz_repr::namespaces::{MZ_CATALOG_SCHEMA, MZ_INTERNAL_SCHEMA};
61use mz_repr::{CatalogItemId, GlobalId, Timestamp};
62use mz_sql::catalog::{CatalogItemType, NameReference};
63use mz_storage_client::controller::StorageTxn;
64use mz_storage_types::StorageDiff;
65use mz_storage_types::sources::SourceData;
66use semver::Version;
67use timely::progress::Antichain;
68use tracing::{debug, info};
69
70use crate::catalog::migrate::get_migration_version;
71
72static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
88 vec![
89 MigrationStep::replacement(
90 "0.149.0",
91 CatalogItemType::Source,
92 MZ_INTERNAL_SCHEMA,
93 "mz_sink_statistics_raw",
94 ),
95 MigrationStep::replacement(
96 "0.149.0",
97 CatalogItemType::Source,
98 MZ_INTERNAL_SCHEMA,
99 "mz_source_statistics_raw",
100 ),
101 MigrationStep::evolution(
102 "0.159.0",
103 CatalogItemType::Source,
104 MZ_INTERNAL_SCHEMA,
105 "mz_cluster_replica_metrics_history",
106 ),
107 MigrationStep::replacement(
108 "26.18.0-dev.0",
109 CatalogItemType::MaterializedView,
110 MZ_CATALOG_SCHEMA,
111 "mz_databases",
112 ),
113 MigrationStep::replacement(
114 "26.19.0-dev.0",
115 CatalogItemType::MaterializedView,
116 MZ_CATALOG_SCHEMA,
117 "mz_schemas",
118 ),
119 MigrationStep::replacement(
120 "26.19.0-dev.0",
121 CatalogItemType::MaterializedView,
122 MZ_CATALOG_SCHEMA,
123 "mz_role_members",
124 ),
125 MigrationStep::replacement(
126 "26.19.0-dev.0",
127 CatalogItemType::MaterializedView,
128 MZ_INTERNAL_SCHEMA,
129 "mz_network_policies",
130 ),
131 MigrationStep::replacement(
132 "26.19.0-dev.0",
133 CatalogItemType::MaterializedView,
134 MZ_INTERNAL_SCHEMA,
135 "mz_network_policy_rules",
136 ),
137 MigrationStep::replacement(
138 "26.19.0-dev.0",
139 CatalogItemType::MaterializedView,
140 MZ_INTERNAL_SCHEMA,
141 "mz_cluster_workload_classes",
142 ),
143 MigrationStep::replacement(
144 "26.19.0-dev.0",
145 CatalogItemType::MaterializedView,
146 MZ_INTERNAL_SCHEMA,
147 "mz_internal_cluster_replicas",
148 ),
149 MigrationStep::replacement(
150 "26.19.0-dev.0",
151 CatalogItemType::MaterializedView,
152 MZ_INTERNAL_SCHEMA,
153 "mz_pending_cluster_replicas",
154 ),
155 MigrationStep::replacement(
156 "26.20.0-dev.0",
157 CatalogItemType::MaterializedView,
158 MZ_CATALOG_SCHEMA,
159 "mz_materialized_views",
160 ),
161 MigrationStep::replacement(
162 "26.22.0-dev.0",
163 CatalogItemType::MaterializedView,
164 MZ_CATALOG_SCHEMA,
165 "mz_connections",
166 ),
167 MigrationStep::replacement(
168 "26.22.0-dev.0",
169 CatalogItemType::MaterializedView,
170 MZ_CATALOG_SCHEMA,
171 "mz_secrets",
172 ),
173 MigrationStep::replacement(
174 "26.27.0-dev.0",
175 CatalogItemType::MaterializedView,
176 MZ_CATALOG_SCHEMA,
177 "mz_sources",
178 ),
179 MigrationStep::replacement(
180 "26.29.0-dev.0",
181 CatalogItemType::MaterializedView,
182 MZ_CATALOG_SCHEMA,
183 "mz_indexes",
184 ),
185 MigrationStep::replacement(
186 "26.29.0-dev.0",
187 CatalogItemType::MaterializedView,
188 MZ_CATALOG_SCHEMA,
189 "mz_roles",
190 ),
191 MigrationStep::replacement(
192 "26.29.0-dev.0",
193 CatalogItemType::MaterializedView,
194 MZ_CATALOG_SCHEMA,
195 "mz_role_parameters",
196 ),
197 MigrationStep::replacement(
202 "26.30.0-dev.0",
203 CatalogItemType::MaterializedView,
204 MZ_CATALOG_SCHEMA,
205 "mz_indexes",
206 ),
207 MigrationStep::replacement(
208 "26.30.0-dev.0",
209 CatalogItemType::MaterializedView,
210 MZ_CATALOG_SCHEMA,
211 "mz_clusters",
212 ),
213 MigrationStep::replacement(
214 "26.30.0-dev.0",
215 CatalogItemType::MaterializedView,
216 MZ_CATALOG_SCHEMA,
217 "mz_cluster_replicas",
218 ),
219 MigrationStep::replacement(
220 "26.30.0-dev.0",
221 CatalogItemType::MaterializedView,
222 MZ_INTERNAL_SCHEMA,
223 "mz_cluster_schedules",
224 ),
225 MigrationStep::replacement(
226 "26.30.0-dev.0",
227 CatalogItemType::MaterializedView,
228 MZ_CATALOG_SCHEMA,
229 "mz_default_privileges",
230 ),
231 MigrationStep::replacement(
232 "26.30.0-dev.0",
233 CatalogItemType::MaterializedView,
234 MZ_CATALOG_SCHEMA,
235 "mz_system_privileges",
236 ),
237 MigrationStep::replacement(
241 "26.31.0-dev.0",
242 CatalogItemType::MaterializedView,
243 MZ_CATALOG_SCHEMA,
244 "mz_cluster_replicas",
245 ),
246 MigrationStep::replacement(
247 "26.32.0-dev.0",
248 CatalogItemType::MaterializedView,
249 MZ_INTERNAL_SCHEMA,
250 "mz_comments",
251 ),
252 MigrationStep::replacement(
257 "26.32.0-dev.0",
258 CatalogItemType::MaterializedView,
259 MZ_CATALOG_SCHEMA,
260 "mz_indexes",
261 ),
262 MigrationStep::replacement(
263 "26.33.0-dev.0",
264 CatalogItemType::MaterializedView,
265 MZ_CATALOG_SCHEMA,
266 "mz_audit_events",
267 ),
268 MigrationStep::replacement(
280 "26.34.0-dev.0",
281 CatalogItemType::MaterializedView,
282 MZ_CATALOG_SCHEMA,
283 "mz_indexes",
284 ),
285 MigrationStep::replacement(
290 "26.34.0-dev.0",
291 CatalogItemType::MaterializedView,
292 MZ_INTERNAL_SCHEMA,
293 "mz_postgres_sources",
294 ),
295 MigrationStep::replacement(
296 "26.34.0-dev.0",
297 CatalogItemType::MaterializedView,
298 MZ_CATALOG_SCHEMA,
299 "mz_kafka_sources",
300 ),
301 MigrationStep::replacement(
305 "26.37.0-dev.0",
306 CatalogItemType::MaterializedView,
307 MZ_INTERNAL_SCHEMA,
308 "mz_postgres_source_tables",
309 ),
310 MigrationStep::replacement(
311 "26.37.0-dev.0",
312 CatalogItemType::MaterializedView,
313 MZ_INTERNAL_SCHEMA,
314 "mz_mysql_source_tables",
315 ),
316 MigrationStep::replacement(
317 "26.37.0-dev.0",
318 CatalogItemType::MaterializedView,
319 MZ_INTERNAL_SCHEMA,
320 "mz_sql_server_source_tables",
321 ),
322 MigrationStep::replacement(
323 "26.37.0-dev.0",
324 CatalogItemType::MaterializedView,
325 MZ_INTERNAL_SCHEMA,
326 "mz_kafka_source_tables",
327 ),
328 MigrationStep::replacement(
332 "26.40.0-dev.0",
333 CatalogItemType::Table,
334 MZ_INTERNAL_SCHEMA,
335 "mz_type_pg_metadata",
336 ),
337 MigrationStep::replacement(
341 "26.47.0-dev.0",
342 CatalogItemType::Table,
343 MZ_INTERNAL_SCHEMA,
344 "mz_type_pg_metadata",
345 ),
346 MigrationStep::replacement(
350 "26.37.0-dev.0",
351 CatalogItemType::MaterializedView,
352 MZ_CATALOG_SCHEMA,
353 "mz_kafka_connections",
354 ),
355 MigrationStep::replacement(
356 "26.37.0-dev.0",
357 CatalogItemType::MaterializedView,
358 MZ_CATALOG_SCHEMA,
359 "mz_ssh_tunnel_connections",
360 ),
361 MigrationStep::replacement(
362 "26.37.0-dev.0",
363 CatalogItemType::MaterializedView,
364 MZ_INTERNAL_SCHEMA,
365 "mz_aws_connections",
366 ),
367 MigrationStep::replacement(
368 "26.37.0-dev.0",
369 CatalogItemType::MaterializedView,
370 MZ_CATALOG_SCHEMA,
371 "mz_aws_privatelink_connections",
372 ),
373 MigrationStep::replacement(
379 "26.38.0-dev.0",
380 CatalogItemType::MaterializedView,
381 MZ_CATALOG_SCHEMA,
382 "mz_sinks",
383 ),
384 MigrationStep::replacement(
385 "26.38.0-dev.0",
386 CatalogItemType::MaterializedView,
387 MZ_CATALOG_SCHEMA,
388 "mz_kafka_sinks",
389 ),
390 MigrationStep::replacement(
391 "26.38.0-dev.0",
392 CatalogItemType::MaterializedView,
393 MZ_CATALOG_SCHEMA,
394 "mz_iceberg_sinks",
395 ),
396 MigrationStep::replacement(
399 "26.38.0-rc.2",
400 CatalogItemType::MaterializedView,
401 MZ_INTERNAL_SCHEMA,
402 "mz_cluster_reconfigurations",
403 ),
404 MigrationStep::replacement(
409 "26.39.0-dev.0",
410 CatalogItemType::MaterializedView,
411 MZ_CATALOG_SCHEMA,
412 "mz_audit_events",
413 ),
414 MigrationStep::replacement(
424 "26.40.0-dev.0",
425 CatalogItemType::MaterializedView,
426 MZ_CATALOG_SCHEMA,
427 "mz_indexes",
428 ),
429 MigrationStep::replacement(
435 "26.39.0-dev.0",
436 CatalogItemType::MaterializedView,
437 MZ_CATALOG_SCHEMA,
438 "mz_tables",
439 ),
440 MigrationStep::replacement(
441 "26.39.0-dev.0",
442 CatalogItemType::MaterializedView,
443 MZ_CATALOG_SCHEMA,
444 "mz_views",
445 ),
446 MigrationStep::replacement(
452 "26.40.0-dev.0",
453 CatalogItemType::MaterializedView,
454 MZ_CATALOG_SCHEMA,
455 "mz_indexes",
456 ),
457 MigrationStep::replacement(
458 "26.40.0-dev.0",
459 CatalogItemType::MaterializedView,
460 MZ_CATALOG_SCHEMA,
461 "mz_sources",
462 ),
463 MigrationStep::replacement(
469 "26.41.0-dev.0",
470 CatalogItemType::MaterializedView,
471 MZ_INTERNAL_SCHEMA,
472 "mz_object_dependencies",
473 ),
474 MigrationStep::evolution(
479 "26.43.0-dev.0",
480 CatalogItemType::Source,
481 MZ_INTERNAL_SCHEMA,
482 "mz_cluster_replica_metrics_history",
483 ),
484 MigrationStep::evolution(
485 "26.43.0-dev.0",
486 CatalogItemType::Table,
487 MZ_INTERNAL_SCHEMA,
488 "mz_replica_hydration_history",
489 ),
490 MigrationStep::replacement(
494 "26.44.0-dev.0",
495 CatalogItemType::MaterializedView,
496 MZ_INTERNAL_SCHEMA,
497 "mz_object_global_ids",
498 ),
499 MigrationStep::replacement(
503 "26.45.0-dev.0",
504 CatalogItemType::MaterializedView,
505 MZ_INTERNAL_SCHEMA,
506 "mz_source_references",
507 ),
508 MigrationStep::replacement(
512 "26.46.0-dev.0",
513 CatalogItemType::MaterializedView,
514 MZ_INTERNAL_SCHEMA,
515 "mz_replacements",
516 ),
517 MigrationStep::replacement(
521 "26.47.0-dev.0",
522 CatalogItemType::MaterializedView,
523 MZ_CATALOG_SCHEMA,
524 "mz_indexes",
525 ),
526 MigrationStep::replacement(
530 "26.47.0-dev.0",
531 CatalogItemType::MaterializedView,
532 MZ_CATALOG_SCHEMA,
533 "mz_sources",
534 ),
535 MigrationStep::replacement(
539 "26.47.0-dev.0",
540 CatalogItemType::MaterializedView,
541 MZ_INTERNAL_SCHEMA,
542 "mz_history_retention_strategies",
543 ),
544 MigrationStep::replacement(
550 "26.47.0-dev.0",
551 CatalogItemType::MaterializedView,
552 MZ_INTERNAL_SCHEMA,
553 "mz_materialized_view_refresh_strategies",
554 ),
555 MigrationStep::replacement(
559 "26.49.0-dev.0",
560 CatalogItemType::MaterializedView,
561 MZ_CATALOG_SCHEMA,
562 "mz_columns",
563 ),
564 MigrationStep::replacement(
568 "26.49.0-dev.0",
569 CatalogItemType::MaterializedView,
570 MZ_CATALOG_SCHEMA,
571 "mz_index_columns",
572 ),
573 MigrationStep::replacement(
576 "26.51.0-dev.0",
577 CatalogItemType::Table,
578 MZ_CATALOG_SCHEMA,
579 "mz_cluster_replica_sizes",
580 ),
581 ]
582});
583
584#[derive(Clone, Debug)]
586struct MigrationStep {
587 version: Version,
589 object: SystemObjectDescription,
591 mechanism: Mechanism,
593}
594
595impl MigrationStep {
596 fn evolution(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
598 Self {
599 version: Version::parse(version).expect("valid"),
600 object: SystemObjectDescription {
601 schema_name: schema.into(),
602 object_type: type_,
603 object_name: name.into(),
604 },
605 mechanism: Mechanism::Evolution,
606 }
607 }
608
609 fn replacement(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
611 Self {
612 version: Version::parse(version).expect("valid"),
613 object: SystemObjectDescription {
614 schema_name: schema.into(),
615 object_type: type_,
616 object_name: name.into(),
617 },
618 mechanism: Mechanism::Replacement,
619 }
620 }
621}
622
623#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
625#[allow(dead_code)]
626enum Mechanism {
627 Evolution,
632 Replacement,
636}
637
638pub(super) struct MigrationResult {
640 pub replaced_items: BTreeSet<CatalogItemId>,
642 pub cleanup_action: BoxFuture<'static, ()>,
644}
645
646impl Default for MigrationResult {
647 fn default() -> Self {
648 Self {
649 replaced_items: Default::default(),
650 cleanup_action: async {}.boxed(),
651 }
652 }
653}
654
655pub(super) async fn run(
661 build_info: &BuildInfo,
662 deploy_generation: u64,
663 txn: &mut Transaction<'_>,
664 config: BuiltinItemMigrationConfig,
665) -> Result<MigrationResult, Error> {
666 assert_eq!(config.read_only, txn.is_savepoint());
668
669 if *build_info == DUMMY_BUILD_INFO {
672 return Ok(MigrationResult::default());
673 }
674
675 let Some(durable_version) = get_migration_version(txn) else {
676 return Ok(MigrationResult::default());
678 };
679 let build_version = build_info.semver_version();
680
681 let collection_metadata = txn.get_collection_metadata();
682 let system_objects = txn
683 .get_system_object_mappings()
684 .map(|m| {
685 let object = m.description;
686 let global_id = m.unique_identifier.global_id;
687 let shard_id = collection_metadata.get(&global_id).copied();
688 let Some((_, builtin)) = BUILTIN_LOOKUP.get(&object) else {
689 panic!("missing builtin {object:?}");
690 };
691 let info = ObjectInfo {
692 global_id,
693 shard_id,
694 builtin,
695 fingerprint: m.unique_identifier.fingerprint,
696 };
697 (object, info)
698 })
699 .collect();
700
701 let migration_shard = txn.get_builtin_migration_shard().expect("must exist");
702
703 let migration = Migration {
704 source_version: durable_version.clone(),
705 target_version: build_version.clone(),
706 deploy_generation,
707 system_objects,
708 migration_shard,
709 config,
710 };
711
712 let result = migration.run(&MIGRATIONS).await.map_err(|e| {
713 Error::new(ErrorKind::FailedBuiltinSchemaMigration {
714 last_seen_version: durable_version.to_string(),
715 this_version: build_version.to_string(),
716 cause: e.to_string(),
717 })
718 })?;
719
720 result.apply(txn);
721
722 let replaced_items = txn
723 .get_system_object_mappings()
724 .map(|m| m.unique_identifier)
725 .filter(|ids| result.new_shards.contains_key(&ids.global_id))
726 .map(|ids| ids.catalog_id)
727 .collect();
728
729 Ok(MigrationResult {
730 replaced_items,
731 cleanup_action: result.cleanup_action,
732 })
733}
734
735struct MigrationRunResult {
737 new_shards: BTreeMap<GlobalId, ShardId>,
738 new_fingerprints: BTreeMap<SystemObjectDescription, String>,
739 shards_to_finalize: BTreeSet<ShardId>,
740 cleanup_action: BoxFuture<'static, ()>,
741}
742
743impl Default for MigrationRunResult {
744 fn default() -> Self {
745 Self {
746 new_shards: BTreeMap::new(),
747 new_fingerprints: BTreeMap::new(),
748 shards_to_finalize: BTreeSet::new(),
749 cleanup_action: async {}.boxed(),
750 }
751 }
752}
753
754impl MigrationRunResult {
755 fn apply(&self, txn: &mut Transaction<'_>) {
757 let replaced_ids = self.new_shards.keys().copied().collect();
759 let old_metadata = txn.delete_collection_metadata(replaced_ids);
760 txn.insert_collection_metadata(self.new_shards.clone())
761 .expect("inserting unique shards IDs after deleting existing entries");
762
763 let mut unfinalized_shards: BTreeSet<_> =
765 old_metadata.into_iter().map(|(_, sid)| sid).collect();
766 unfinalized_shards.extend(self.shards_to_finalize.iter().copied());
767 txn.insert_unfinalized_shards(unfinalized_shards)
768 .expect("cannot fail");
769
770 let mappings = txn
772 .get_system_object_mappings()
773 .filter_map(|m| {
774 let fingerprint = self.new_fingerprints.get(&m.description)?;
775 Some(SystemObjectMapping {
776 description: m.description,
777 unique_identifier: SystemObjectUniqueIdentifier {
778 catalog_id: m.unique_identifier.catalog_id,
779 global_id: m.unique_identifier.global_id,
780 fingerprint: fingerprint.clone(),
781 },
782 })
783 })
784 .collect();
785 txn.set_system_object_mappings(mappings)
786 .expect("filtered existing mappings remain unique");
787 }
788}
789
790#[derive(Clone, Debug)]
792struct ObjectInfo {
793 global_id: GlobalId,
794 shard_id: Option<ShardId>,
795 builtin: &'static Builtin<NameReference>,
796 fingerprint: String,
797}
798
799struct Migration {
801 source_version: Version,
806 target_version: Version,
810 deploy_generation: u64,
812 system_objects: BTreeMap<SystemObjectDescription, ObjectInfo>,
814 migration_shard: ShardId,
816 config: BuiltinItemMigrationConfig,
818}
819
820fn participates_in_forced_migration(
822 builtin: &Builtin<NameReference>,
823 mechanism: Mechanism,
824) -> bool {
825 use Builtin::*;
826 match builtin {
827 Table(table) => {
839 **table != *MZ_STORAGE_USAGE_BY_SHARD
840 && (mechanism != Mechanism::Replacement
841 || (**table != *MZ_OBJECT_HYDRATION_HISTORY
842 && **table != *MZ_REPLICA_HYDRATION_HISTORY))
843 }
844 MaterializedView(..) => true,
845 Source(source) => **source != *MZ_CATALOG_RAW,
846 Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,
847 }
848}
849
850impl Migration {
851 async fn run(self, steps: &[MigrationStep]) -> anyhow::Result<MigrationRunResult> {
852 info!(
853 deploy_generation = %self.deploy_generation,
854 "running builtin schema migration: {} -> {}",
855 self.source_version, self.target_version
856 );
857
858 self.validate_migration_steps(steps);
859
860 let force_migration = if self.source_version != self.target_version
863 && self.source_version.pre.as_str().starts_with("dev")
864 && self.config.force_migration.is_none()
865 {
866 Some("evolution".to_string())
867 } else {
868 self.config.force_migration.clone()
869 };
870
871 let (force, plan) = match force_migration.as_deref() {
872 None => (false, self.plan_migration(steps)),
873 Some("evolution") => (true, self.plan_forced_migration(Mechanism::Evolution)),
874 Some("replacement") => (true, self.plan_forced_migration(Mechanism::Replacement)),
875 Some(other) => panic!("unknown force migration mechanism: {other}"),
876 };
877 let plan = self.drop_shardless(plan);
878
879 if self.source_version == self.target_version && !force {
880 info!("skipping migration: already at target version");
881 return Ok(MigrationRunResult::default());
882 } else if self.source_version > self.target_version {
883 bail!("downgrade not supported");
884 }
885
886 if !self.config.read_only {
889 self.upgrade_migration_shard_version().await;
890 }
891
892 info!("executing migration plan: {plan:?}");
893
894 self.migrate_evolve(&plan.evolve).await?;
895 let new_shards = self.migrate_replace(&plan.replace).await?;
896
897 let mut migrated_objects = BTreeSet::new();
898 migrated_objects.extend(plan.evolve);
899 migrated_objects.extend(plan.replace);
900
901 let new_fingerprints = self.update_fingerprints(&migrated_objects)?;
902
903 let (shards_to_finalize, cleanup_action) = self.cleanup().await?;
904
905 Ok(MigrationRunResult {
906 new_shards,
907 new_fingerprints,
908 shards_to_finalize,
909 cleanup_action,
910 })
911 }
912
913 fn validate_migration_steps(&self, steps: &[MigrationStep]) {
917 for step in steps {
918 assert!(
919 step.version <= self.target_version,
920 "migration step version greater than target version: {} > {}",
921 step.version,
922 self.target_version,
923 );
924
925 let object = &step.object;
926
927 assert_ne!(
935 &*MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, object,
936 "mz_storage_usage_by_shard cannot be migrated or else the table will be truncated"
937 );
938
939 assert_ne!(
943 &*MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, object,
944 "mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
945 );
946
947 if step.mechanism == Mechanism::Replacement {
958 assert_ne!(
959 &*MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, object,
960 "replacing mz_object_hydration_history clears it, see the comment above"
961 );
962 assert_ne!(
963 &*MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION, object,
964 "replacing mz_replica_hydration_history clears it, see the comment above"
965 );
966 }
967
968 assert_ne!(
971 &*MZ_CATALOG_RAW_DESCRIPTION, object,
972 "mz_catalog_raw cannot be migrated"
973 );
974
975 assert_ne!(
980 &*MZ_CLUSTER_REPLICA_FRONTIERS_DESCRIPTION, object,
981 "mz_cluster_replica_frontiers cannot be migrated or else the 0dt caught-up gate loses its live-frontier reference"
982 );
983 assert_ne!(
984 &*MZ_COMPUTE_HYDRATION_TIMES_DESCRIPTION, object,
985 "mz_compute_hydration_times cannot be migrated or else the 0dt caught-up gate loses its leader-hydration reference"
986 );
987
988 let Some(object_info) = self.system_objects.get(object) else {
989 panic!("migration step for non-existent builtin: {object:?}");
990 };
991
992 let builtin = object_info.builtin;
993 use Builtin::*;
994 assert!(
995 matches!(builtin, Table(..) | Source(..) | MaterializedView(..)),
996 "schema migration not supported for builtin: {builtin:?}",
997 );
998 }
999 }
1000
1001 fn plan_migration(&self, steps: &[MigrationStep]) -> Plan {
1003 let steps = steps.iter().filter(|s| s.version > self.source_version);
1005
1006 let mut by_object = BTreeMap::new();
1010 for step in steps {
1011 if let Some(entry) = by_object.get_mut(&step.object) {
1012 *entry = match (step.mechanism, *entry) {
1013 (Mechanism::Evolution, Mechanism::Evolution) => Mechanism::Evolution,
1014 (Mechanism::Replacement, _) | (_, Mechanism::Replacement) => {
1015 Mechanism::Replacement
1016 }
1017 };
1018 } else {
1019 by_object.insert(step.object.clone(), step.mechanism);
1020 }
1021 }
1022
1023 let mut plan = Plan::default();
1024 for (object, mechanism) in by_object {
1025 match mechanism {
1026 Mechanism::Evolution => plan.evolve.push(object),
1027 Mechanism::Replacement => plan.replace.push(object),
1028 }
1029 }
1030
1031 plan
1032 }
1033
1034 fn plan_forced_migration(&self, mechanism: Mechanism) -> Plan {
1036 let objects = self
1037 .system_objects
1038 .iter()
1039 .filter(|(_, info)| participates_in_forced_migration(info.builtin, mechanism))
1040 .map(|(object, _)| object.clone())
1041 .collect();
1042
1043 let mut plan = Plan::default();
1044 match mechanism {
1045 Mechanism::Evolution => plan.evolve = objects,
1046 Mechanism::Replacement => plan.replace = objects,
1047 }
1048
1049 plan
1050 }
1051
1052 fn drop_shardless(&self, mut plan: Plan) -> Plan {
1062 let has_shard = |object: &SystemObjectDescription| {
1063 self.system_objects
1064 .get(object)
1065 .is_none_or(|info| info.shard_id.is_some())
1066 };
1067 plan.evolve.retain(has_shard);
1068 plan.replace.retain(has_shard);
1069 plan
1070 }
1071
1072 async fn upgrade_migration_shard_version(&self) {
1074 let persist = &self.config.persist_client;
1075 let diagnostics = Diagnostics {
1076 shard_name: "builtin_migration".to_string(),
1077 handle_purpose: format!("migration shard upgrade @ {}", self.target_version),
1078 };
1079
1080 persist
1081 .upgrade_version::<migration_shard::Key, ShardId, Timestamp, StorageDiff>(
1082 self.migration_shard,
1083 diagnostics,
1084 )
1085 .await
1086 .expect("valid usage");
1087 }
1088
1089 async fn migrate_evolve(&self, objects: &[SystemObjectDescription]) -> anyhow::Result<()> {
1091 for object in objects {
1092 self.migrate_evolve_one(object).await?;
1093 }
1094 Ok(())
1095 }
1096
1097 async fn migrate_evolve_one(&self, object: &SystemObjectDescription) -> anyhow::Result<()> {
1098 let persist = &self.config.persist_client;
1099
1100 let Some(object_info) = self.system_objects.get(object) else {
1101 bail!("missing builtin {object:?}");
1102 };
1103 let id = object_info.global_id;
1104
1105 let Some(shard_id) = object_info.shard_id else {
1106 if self.config.read_only {
1111 bail!("missing shard ID for builtin {object:?} ({id})");
1112 } else {
1113 return Ok(());
1114 }
1115 };
1116
1117 let target_desc = match object_info.builtin {
1118 Builtin::Table(table) => &table.desc,
1119 Builtin::Source(source) => &source.desc,
1120 Builtin::MaterializedView(mv) => &mv.desc,
1121 _ => bail!("not a storage collection: {object:?}"),
1122 };
1123
1124 let diagnostics = Diagnostics {
1125 shard_name: id.to_string(),
1126 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1127 };
1128 let source_schema = persist
1129 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics.clone())
1130 .await
1131 .expect("valid usage");
1132
1133 info!(?object, %id, %shard_id, ?source_schema, ?target_desc, "migrating by evolution");
1134
1135 if self.config.read_only {
1136 if let Some((_, source_desc, _)) = &source_schema {
1139 let old = mz_persist_types::columnar::data_type::<SourceData>(source_desc)?;
1140 let new = mz_persist_types::columnar::data_type::<SourceData>(target_desc)?;
1141 if backward_compatible(&old, &new).is_none() {
1142 bail!(
1143 "incompatible schema evolution for {object:?}: \
1144 {source_desc:?} -> {target_desc:?}"
1145 );
1146 }
1147 }
1148
1149 return Ok(());
1150 }
1151
1152 let (mut schema_id, mut source_desc) = match source_schema {
1153 Some((schema_id, source_desc, _)) => (schema_id, source_desc),
1154 None => {
1155 debug!(%id, %shard_id, "no previous schema found; registering initial one");
1160 let schema_id = persist
1161 .register_schema::<SourceData, (), Timestamp, StorageDiff>(
1162 shard_id,
1163 target_desc,
1164 &UnitSchema,
1165 diagnostics.clone(),
1166 )
1167 .await
1168 .expect("valid usage");
1169 if schema_id.is_some() {
1170 return Ok(());
1171 }
1172
1173 debug!(%id, %shard_id, "schema registration failed; falling back to CaES");
1174 let (schema_id, source_desc, _) = persist
1175 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(
1176 shard_id,
1177 diagnostics.clone(),
1178 )
1179 .await
1180 .expect("valid usage")
1181 .expect("known to exist");
1182
1183 (schema_id, source_desc)
1184 }
1185 };
1186
1187 loop {
1188 debug!(%id, %shard_id, %schema_id, ?source_desc, ?target_desc, "attempting CaES");
1193 let result = persist
1194 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
1195 shard_id,
1196 schema_id,
1197 target_desc,
1198 &UnitSchema,
1199 diagnostics.clone(),
1200 )
1201 .await
1202 .expect("valid usage");
1203
1204 match result {
1205 CaESchema::Ok(schema_id) => {
1206 debug!(%id, %shard_id, %schema_id, "schema evolved successfully");
1207 break;
1208 }
1209 CaESchema::Incompatible => bail!(
1210 "incompatible schema evolution for {object:?}: \
1211 {source_desc:?} -> {target_desc:?}"
1212 ),
1213 CaESchema::ExpectedMismatch {
1214 schema_id: new_id,
1215 key,
1216 val: UnitSchema,
1217 } => {
1218 schema_id = new_id;
1219 source_desc = key;
1220 }
1221 }
1222 }
1223
1224 Ok(())
1225 }
1226
1227 async fn migrate_replace(
1229 &self,
1230 objects: &[SystemObjectDescription],
1231 ) -> anyhow::Result<BTreeMap<GlobalId, ShardId>> {
1232 if objects.is_empty() {
1233 return Ok(Default::default());
1234 }
1235
1236 let diagnostics = Diagnostics {
1237 shard_name: "builtin_migration".to_string(),
1238 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1239 };
1240 let (mut persist_write, mut persist_read) =
1241 self.open_migration_shard(diagnostics.clone()).await;
1242
1243 let mut ids_to_replace = BTreeSet::new();
1244 for object in objects {
1245 if let Some(info) = self.system_objects.get(object) {
1246 ids_to_replace.insert(info.global_id);
1247 } else {
1248 bail!("missing id for builtin {object:?}");
1249 }
1250 }
1251
1252 info!(?objects, ?ids_to_replace, "migrating by replacement");
1253
1254 let replaced_shards = loop {
1257 if let Some(shards) = self
1258 .try_get_or_insert_replacement_shards(
1259 &ids_to_replace,
1260 &mut persist_write,
1261 &mut persist_read,
1262 )
1263 .await?
1264 {
1265 break shards;
1266 }
1267 };
1268
1269 Ok(replaced_shards)
1270 }
1271
1272 async fn try_get_or_insert_replacement_shards(
1282 &self,
1283 ids_to_replace: &BTreeSet<GlobalId>,
1284 persist_write: &mut WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1285 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1286 ) -> anyhow::Result<Option<BTreeMap<GlobalId, ShardId>>> {
1287 let upper = persist_write.fetch_recent_upper().await;
1288 let write_ts = *upper.as_option().expect("migration shard not sealed");
1289
1290 let mut ids_to_replace = ids_to_replace.clone();
1291 let mut replaced_shards = BTreeMap::new();
1292
1293 if let Some(read_ts) = write_ts.step_back() {
1301 let pred = |key: &migration_shard::Key| {
1302 key.build_version == self.target_version
1303 && key.deploy_generation == Some(self.deploy_generation)
1304 };
1305 if let Some(entries) = read_migration_shard(persist_read, read_ts, pred).await {
1306 for (key, shard_id) in entries {
1307 let id = GlobalId::System(key.global_id);
1308 if ids_to_replace.remove(&id) {
1309 replaced_shards.insert(id, shard_id);
1310 }
1311 }
1312
1313 debug!(
1314 %read_ts, ?replaced_shards, ?ids_to_replace,
1315 "found existing entries in migration shard",
1316 );
1317 }
1318
1319 if ids_to_replace.is_empty() {
1320 return Ok(Some(replaced_shards));
1321 }
1322 }
1323
1324 let mut updates = Vec::new();
1328 for id in ids_to_replace {
1329 let shard_id = ShardId::new();
1330 replaced_shards.insert(id, shard_id);
1331
1332 let GlobalId::System(global_id) = id else {
1333 bail!("attempt to migrate a non-system collection: {id}");
1334 };
1335 let key = migration_shard::Key {
1336 global_id,
1337 build_version: self.target_version.clone(),
1338 deploy_generation: Some(self.deploy_generation),
1339 };
1340 updates.push(((key, shard_id), write_ts, 1));
1341 }
1342
1343 let upper = Antichain::from_elem(write_ts);
1344 let new_upper = Antichain::from_elem(write_ts.step_forward());
1345 debug!(%write_ts, "attempting insert into migration shard");
1346 let result = persist_write
1347 .compare_and_append(updates, upper, new_upper)
1348 .await
1349 .expect("valid usage");
1350
1351 match result {
1352 Ok(()) => {
1353 debug!(
1354 %write_ts, ?replaced_shards,
1355 "successfully inserted into migration shard"
1356 );
1357 Ok(Some(replaced_shards))
1358 }
1359 Err(_mismatch) => Ok(None),
1360 }
1361 }
1362
1363 async fn open_migration_shard(
1365 &self,
1366 diagnostics: Diagnostics,
1367 ) -> (
1368 WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1369 ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1370 ) {
1371 let persist = &self.config.persist_client;
1372
1373 persist
1374 .open(
1375 self.migration_shard,
1376 Arc::new(migration_shard::KeySchema),
1377 Arc::new(ShardIdSchema),
1378 diagnostics,
1379 USE_CRITICAL_SINCE_CATALOG.get(persist.dyncfgs()),
1380 )
1381 .await
1382 .expect("valid usage")
1383 }
1384
1385 async fn open_migration_shard_since(
1387 &self,
1388 diagnostics: Diagnostics,
1389 ) -> SinceHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff> {
1390 self.config
1391 .persist_client
1392 .open_critical_since(
1393 self.migration_shard,
1394 PersistClient::CONTROLLER_CRITICAL_SINCE,
1397 Opaque::encode(&i64::MIN),
1398 diagnostics.clone(),
1399 )
1400 .await
1401 .expect("valid usage")
1402 }
1403
1404 fn update_fingerprints(
1409 &self,
1410 migrated_items: &BTreeSet<SystemObjectDescription>,
1411 ) -> anyhow::Result<BTreeMap<SystemObjectDescription, String>> {
1412 let mut new_fingerprints = BTreeMap::new();
1413 for (object, object_info) in &self.system_objects {
1414 let id = object_info.global_id;
1415 let builtin = object_info.builtin;
1416
1417 let fingerprint = builtin.fingerprint();
1418 if fingerprint == object_info.fingerprint {
1419 continue; }
1421
1422 let migrated = migrated_items.contains(object);
1424 let ephemeral = matches!(
1426 builtin,
1427 Builtin::Log(_) | Builtin::View(_) | Builtin::Index(_),
1428 );
1429
1430 if migrated || ephemeral {
1431 new_fingerprints.insert(object.clone(), fingerprint);
1432 } else if builtin.runtime_alterable() {
1433 assert_eq!(
1436 object_info.fingerprint, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
1437 "fingerprint mismatch for runtime-alterable builtin {object:?} ({id})",
1438 );
1439 } else {
1440 panic!(
1441 "fingerprint mismatch for builtin {builtin:?} ({id}): {} != {}",
1442 fingerprint, object_info.fingerprint,
1443 );
1444 }
1445 }
1446
1447 Ok(new_fingerprints)
1448 }
1449
1450 async fn cleanup(&self) -> anyhow::Result<(BTreeSet<ShardId>, BoxFuture<'static, ()>)> {
1467 let noop_action = async {}.boxed();
1468 let noop_result = (BTreeSet::new(), noop_action);
1469
1470 if self.config.read_only {
1471 return Ok(noop_result);
1472 }
1473
1474 let diagnostics = Diagnostics {
1475 shard_name: "builtin_migration".to_string(),
1476 handle_purpose: "builtin schema migration cleanup".into(),
1477 };
1478 let (mut persist_write, mut persist_read) =
1479 self.open_migration_shard(diagnostics.clone()).await;
1480 let mut persist_since = self.open_migration_shard_since(diagnostics.clone()).await;
1481
1482 let upper = persist_write.fetch_recent_upper().await.clone();
1483 let write_ts = *upper.as_option().expect("migration shard not sealed");
1484 let Some(read_ts) = write_ts.step_back() else {
1485 return Ok(noop_result);
1486 };
1487
1488 let pred = |key: &migration_shard::Key| key.build_version < self.target_version;
1490 let Some(stale_entries) = read_migration_shard(&mut persist_read, read_ts, pred).await
1491 else {
1492 return Ok(noop_result);
1493 };
1494
1495 debug!(
1496 ?stale_entries,
1497 "cleaning migration shard up to version {}", self.target_version,
1498 );
1499
1500 let current_shards: BTreeMap<_, _> = self
1501 .system_objects
1502 .values()
1503 .filter_map(|o| o.shard_id.map(|shard_id| (o.global_id, shard_id)))
1504 .collect();
1505
1506 let mut shards_to_finalize = BTreeSet::new();
1507 let mut retractions = Vec::new();
1508 for (key, shard_id) in stale_entries {
1509 let gid = GlobalId::System(key.global_id);
1513 if current_shards.get(&gid) != Some(&shard_id) {
1514 shards_to_finalize.insert(shard_id);
1515 }
1516
1517 retractions.push(((key, shard_id), write_ts, -1));
1518 }
1519
1520 let cleanup_action = async move {
1521 if !retractions.is_empty() {
1522 let new_upper = Antichain::from_elem(write_ts.step_forward());
1523 let result = persist_write
1524 .compare_and_append(retractions, upper, new_upper)
1525 .await
1526 .expect("valid usage");
1527 match result {
1528 Ok(()) => debug!("cleaned up migration shard"),
1529 Err(mismatch) => debug!(?mismatch, "migration shard cleanup failed"),
1530 }
1531 }
1532 }
1533 .boxed();
1534
1535 let o = persist_since.opaque().clone();
1537 let new_since = Antichain::from_elem(read_ts);
1538 let result = persist_since
1539 .maybe_compare_and_downgrade_since(&o, (&o, &new_since))
1540 .await;
1541 soft_assert_or_log!(result.is_none_or(|r| r.is_ok()), "opaque mismatch");
1542
1543 Ok((shards_to_finalize, cleanup_action))
1544 }
1545}
1546
1547async fn read_migration_shard<P>(
1553 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1554 read_ts: Timestamp,
1555 predicate: P,
1556) -> Option<Vec<(migration_shard::Key, ShardId)>>
1557where
1558 P: Fn(&migration_shard::Key) -> bool,
1559{
1560 let as_of = Antichain::from_elem(read_ts);
1561 let updates = persist_read.snapshot_and_fetch(as_of).await.ok()?;
1562
1563 assert!(
1564 updates.iter().all(|(_, _, diff)| *diff == 1),
1565 "migration shard contains invalid diffs: {updates:?}",
1566 );
1567
1568 let entries: Vec<_> = updates
1569 .into_iter()
1570 .map(|(data, _, _)| data)
1571 .filter(move |(key, _)| predicate(key))
1572 .collect();
1573
1574 (!entries.is_empty()).then_some(entries)
1575}
1576
1577#[derive(Debug, Default)]
1579struct Plan {
1580 evolve: Vec<SystemObjectDescription>,
1582 replace: Vec<SystemObjectDescription>,
1584}
1585
1586mod migration_shard {
1588 use std::fmt;
1589 use std::str::FromStr;
1590
1591 use arrow::array::{StringArray, StringBuilder};
1592 use bytes::{BufMut, Bytes};
1593 use mz_persist_types::Codec;
1594 use mz_persist_types::codec_impls::{
1595 SimpleColumnarData, SimpleColumnarDecoder, SimpleColumnarEncoder,
1596 };
1597 use mz_persist_types::columnar::Schema;
1598 use mz_persist_types::stats::NoneStats;
1599 use semver::Version;
1600 use serde::{Deserialize, Serialize};
1601
1602 #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1603 pub(super) struct Key {
1604 pub(super) global_id: u64,
1605 pub(super) build_version: Version,
1606 pub(super) deploy_generation: Option<u64>,
1610 }
1611
1612 impl fmt::Display for Key {
1613 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1614 if self.deploy_generation.is_some() {
1615 let s = serde_json::to_string(self).expect("JSON serializable");
1617 f.write_str(&s)
1618 } else {
1619 write!(f, "{}-{}", self.global_id, self.build_version)
1621 }
1622 }
1623 }
1624
1625 impl FromStr for Key {
1626 type Err = String;
1627
1628 fn from_str(s: &str) -> Result<Self, String> {
1629 if let Ok(key) = serde_json::from_str(s) {
1631 return Ok(key);
1632 };
1633
1634 let parts: Vec<_> = s.splitn(2, '-').collect();
1636 let &[global_id, build_version] = parts.as_slice() else {
1637 return Err(format!("invalid Key '{s}'"));
1638 };
1639 let global_id = global_id.parse::<u64>().map_err(|e| e.to_string())?;
1640 let build_version = build_version
1641 .parse::<Version>()
1642 .map_err(|e| e.to_string())?;
1643 Ok(Key {
1644 global_id,
1645 build_version,
1646 deploy_generation: None,
1647 })
1648 }
1649 }
1650
1651 impl Default for Key {
1652 fn default() -> Self {
1653 Self {
1654 global_id: Default::default(),
1655 build_version: Version::new(0, 0, 0),
1656 deploy_generation: Some(0),
1657 }
1658 }
1659 }
1660
1661 impl Codec for Key {
1662 type Schema = KeySchema;
1663 type Storage = ();
1664
1665 fn codec_name() -> String {
1666 "TableKey".into()
1667 }
1668
1669 fn encode<B: BufMut>(&self, buf: &mut B) {
1670 buf.put(self.to_string().as_bytes())
1671 }
1672
1673 fn decode<'a>(buf: &'a [u8], _schema: &KeySchema) -> Result<Self, String> {
1674 let s = str::from_utf8(buf).map_err(|e| e.to_string())?;
1675 s.parse()
1676 }
1677
1678 fn encode_schema(_schema: &KeySchema) -> Bytes {
1679 Bytes::new()
1680 }
1681
1682 fn decode_schema(buf: &Bytes) -> Self::Schema {
1683 assert_eq!(*buf, Bytes::new());
1684 KeySchema
1685 }
1686 }
1687
1688 impl SimpleColumnarData for Key {
1689 type ArrowBuilder = StringBuilder;
1690 type ArrowColumn = StringArray;
1691
1692 fn goodbytes(builder: &Self::ArrowBuilder) -> usize {
1693 builder.values_slice().len()
1694 }
1695
1696 fn push(&self, builder: &mut Self::ArrowBuilder) {
1697 builder.append_value(&self.to_string());
1698 }
1699
1700 fn push_null(builder: &mut Self::ArrowBuilder) {
1701 builder.append_null();
1702 }
1703
1704 fn read(&mut self, idx: usize, column: &Self::ArrowColumn) {
1705 *self = column.value(idx).parse().expect("valid Key");
1706 }
1707 }
1708
1709 #[derive(Debug, PartialEq)]
1710 pub(super) struct KeySchema;
1711
1712 impl Schema<Key> for KeySchema {
1713 type ArrowColumn = StringArray;
1714 type Statistics = NoneStats;
1715 type Decoder = SimpleColumnarDecoder<Key>;
1716 type Encoder = SimpleColumnarEncoder<Key>;
1717
1718 fn encoder(&self) -> anyhow::Result<SimpleColumnarEncoder<Key>> {
1719 Ok(SimpleColumnarEncoder::default())
1720 }
1721
1722 fn decoder(&self, col: StringArray) -> anyhow::Result<SimpleColumnarDecoder<Key>> {
1723 Ok(SimpleColumnarDecoder::new(col))
1724 }
1725 }
1726}
1727
1728#[cfg(test)]
1729#[path = "builtin_schema_migration_tests.rs"]
1730mod tests;