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 ]
556});
557
558#[derive(Clone, Debug)]
560struct MigrationStep {
561 version: Version,
563 object: SystemObjectDescription,
565 mechanism: Mechanism,
567}
568
569impl MigrationStep {
570 fn evolution(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
572 Self {
573 version: Version::parse(version).expect("valid"),
574 object: SystemObjectDescription {
575 schema_name: schema.into(),
576 object_type: type_,
577 object_name: name.into(),
578 },
579 mechanism: Mechanism::Evolution,
580 }
581 }
582
583 fn replacement(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
585 Self {
586 version: Version::parse(version).expect("valid"),
587 object: SystemObjectDescription {
588 schema_name: schema.into(),
589 object_type: type_,
590 object_name: name.into(),
591 },
592 mechanism: Mechanism::Replacement,
593 }
594 }
595}
596
597#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
599#[allow(dead_code)]
600enum Mechanism {
601 Evolution,
606 Replacement,
610}
611
612pub(super) struct MigrationResult {
614 pub replaced_items: BTreeSet<CatalogItemId>,
616 pub cleanup_action: BoxFuture<'static, ()>,
618}
619
620impl Default for MigrationResult {
621 fn default() -> Self {
622 Self {
623 replaced_items: Default::default(),
624 cleanup_action: async {}.boxed(),
625 }
626 }
627}
628
629pub(super) async fn run(
635 build_info: &BuildInfo,
636 deploy_generation: u64,
637 txn: &mut Transaction<'_>,
638 config: BuiltinItemMigrationConfig,
639) -> Result<MigrationResult, Error> {
640 assert_eq!(config.read_only, txn.is_savepoint());
642
643 if *build_info == DUMMY_BUILD_INFO {
646 return Ok(MigrationResult::default());
647 }
648
649 let Some(durable_version) = get_migration_version(txn) else {
650 return Ok(MigrationResult::default());
652 };
653 let build_version = build_info.semver_version();
654
655 let collection_metadata = txn.get_collection_metadata();
656 let system_objects = txn
657 .get_system_object_mappings()
658 .map(|m| {
659 let object = m.description;
660 let global_id = m.unique_identifier.global_id;
661 let shard_id = collection_metadata.get(&global_id).copied();
662 let Some((_, builtin)) = BUILTIN_LOOKUP.get(&object) else {
663 panic!("missing builtin {object:?}");
664 };
665 let info = ObjectInfo {
666 global_id,
667 shard_id,
668 builtin,
669 fingerprint: m.unique_identifier.fingerprint,
670 };
671 (object, info)
672 })
673 .collect();
674
675 let migration_shard = txn.get_builtin_migration_shard().expect("must exist");
676
677 let migration = Migration {
678 source_version: durable_version.clone(),
679 target_version: build_version.clone(),
680 deploy_generation,
681 system_objects,
682 migration_shard,
683 config,
684 };
685
686 let result = migration.run(&MIGRATIONS).await.map_err(|e| {
687 Error::new(ErrorKind::FailedBuiltinSchemaMigration {
688 last_seen_version: durable_version.to_string(),
689 this_version: build_version.to_string(),
690 cause: e.to_string(),
691 })
692 })?;
693
694 result.apply(txn);
695
696 let replaced_items = txn
697 .get_system_object_mappings()
698 .map(|m| m.unique_identifier)
699 .filter(|ids| result.new_shards.contains_key(&ids.global_id))
700 .map(|ids| ids.catalog_id)
701 .collect();
702
703 Ok(MigrationResult {
704 replaced_items,
705 cleanup_action: result.cleanup_action,
706 })
707}
708
709struct MigrationRunResult {
711 new_shards: BTreeMap<GlobalId, ShardId>,
712 new_fingerprints: BTreeMap<SystemObjectDescription, String>,
713 shards_to_finalize: BTreeSet<ShardId>,
714 cleanup_action: BoxFuture<'static, ()>,
715}
716
717impl Default for MigrationRunResult {
718 fn default() -> Self {
719 Self {
720 new_shards: BTreeMap::new(),
721 new_fingerprints: BTreeMap::new(),
722 shards_to_finalize: BTreeSet::new(),
723 cleanup_action: async {}.boxed(),
724 }
725 }
726}
727
728impl MigrationRunResult {
729 fn apply(&self, txn: &mut Transaction<'_>) {
731 let replaced_ids = self.new_shards.keys().copied().collect();
733 let old_metadata = txn.delete_collection_metadata(replaced_ids);
734 txn.insert_collection_metadata(self.new_shards.clone())
735 .expect("inserting unique shards IDs after deleting existing entries");
736
737 let mut unfinalized_shards: BTreeSet<_> =
739 old_metadata.into_iter().map(|(_, sid)| sid).collect();
740 unfinalized_shards.extend(self.shards_to_finalize.iter().copied());
741 txn.insert_unfinalized_shards(unfinalized_shards)
742 .expect("cannot fail");
743
744 let mappings = txn
746 .get_system_object_mappings()
747 .filter_map(|m| {
748 let fingerprint = self.new_fingerprints.get(&m.description)?;
749 Some(SystemObjectMapping {
750 description: m.description,
751 unique_identifier: SystemObjectUniqueIdentifier {
752 catalog_id: m.unique_identifier.catalog_id,
753 global_id: m.unique_identifier.global_id,
754 fingerprint: fingerprint.clone(),
755 },
756 })
757 })
758 .collect();
759 txn.set_system_object_mappings(mappings)
760 .expect("filtered existing mappings remain unique");
761 }
762}
763
764#[derive(Clone, Debug)]
766struct ObjectInfo {
767 global_id: GlobalId,
768 shard_id: Option<ShardId>,
769 builtin: &'static Builtin<NameReference>,
770 fingerprint: String,
771}
772
773struct Migration {
775 source_version: Version,
780 target_version: Version,
784 deploy_generation: u64,
786 system_objects: BTreeMap<SystemObjectDescription, ObjectInfo>,
788 migration_shard: ShardId,
790 config: BuiltinItemMigrationConfig,
792}
793
794fn participates_in_forced_migration(
796 builtin: &Builtin<NameReference>,
797 mechanism: Mechanism,
798) -> bool {
799 use Builtin::*;
800 match builtin {
801 Table(table) => {
813 **table != *MZ_STORAGE_USAGE_BY_SHARD
814 && (mechanism != Mechanism::Replacement
815 || (**table != *MZ_OBJECT_HYDRATION_HISTORY
816 && **table != *MZ_REPLICA_HYDRATION_HISTORY))
817 }
818 MaterializedView(..) => true,
819 Source(source) => **source != *MZ_CATALOG_RAW,
820 Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,
821 }
822}
823
824impl Migration {
825 async fn run(self, steps: &[MigrationStep]) -> anyhow::Result<MigrationRunResult> {
826 info!(
827 deploy_generation = %self.deploy_generation,
828 "running builtin schema migration: {} -> {}",
829 self.source_version, self.target_version
830 );
831
832 self.validate_migration_steps(steps);
833
834 let force_migration = if self.source_version != self.target_version
837 && self.source_version.pre.as_str().starts_with("dev")
838 && self.config.force_migration.is_none()
839 {
840 Some("evolution".to_string())
841 } else {
842 self.config.force_migration.clone()
843 };
844
845 let (force, plan) = match force_migration.as_deref() {
846 None => (false, self.plan_migration(steps)),
847 Some("evolution") => (true, self.plan_forced_migration(Mechanism::Evolution)),
848 Some("replacement") => (true, self.plan_forced_migration(Mechanism::Replacement)),
849 Some(other) => panic!("unknown force migration mechanism: {other}"),
850 };
851 let plan = self.drop_shardless(plan);
852
853 if self.source_version == self.target_version && !force {
854 info!("skipping migration: already at target version");
855 return Ok(MigrationRunResult::default());
856 } else if self.source_version > self.target_version {
857 bail!("downgrade not supported");
858 }
859
860 if !self.config.read_only {
863 self.upgrade_migration_shard_version().await;
864 }
865
866 info!("executing migration plan: {plan:?}");
867
868 self.migrate_evolve(&plan.evolve).await?;
869 let new_shards = self.migrate_replace(&plan.replace).await?;
870
871 let mut migrated_objects = BTreeSet::new();
872 migrated_objects.extend(plan.evolve);
873 migrated_objects.extend(plan.replace);
874
875 let new_fingerprints = self.update_fingerprints(&migrated_objects)?;
876
877 let (shards_to_finalize, cleanup_action) = self.cleanup().await?;
878
879 Ok(MigrationRunResult {
880 new_shards,
881 new_fingerprints,
882 shards_to_finalize,
883 cleanup_action,
884 })
885 }
886
887 fn validate_migration_steps(&self, steps: &[MigrationStep]) {
891 for step in steps {
892 assert!(
893 step.version <= self.target_version,
894 "migration step version greater than target version: {} > {}",
895 step.version,
896 self.target_version,
897 );
898
899 let object = &step.object;
900
901 assert_ne!(
909 &*MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, object,
910 "mz_storage_usage_by_shard cannot be migrated or else the table will be truncated"
911 );
912
913 assert_ne!(
917 &*MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, object,
918 "mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
919 );
920
921 if step.mechanism == Mechanism::Replacement {
932 assert_ne!(
933 &*MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, object,
934 "replacing mz_object_hydration_history clears it, see the comment above"
935 );
936 assert_ne!(
937 &*MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION, object,
938 "replacing mz_replica_hydration_history clears it, see the comment above"
939 );
940 }
941
942 assert_ne!(
945 &*MZ_CATALOG_RAW_DESCRIPTION, object,
946 "mz_catalog_raw cannot be migrated"
947 );
948
949 assert_ne!(
954 &*MZ_CLUSTER_REPLICA_FRONTIERS_DESCRIPTION, object,
955 "mz_cluster_replica_frontiers cannot be migrated or else the 0dt caught-up gate loses its live-frontier reference"
956 );
957 assert_ne!(
958 &*MZ_COMPUTE_HYDRATION_TIMES_DESCRIPTION, object,
959 "mz_compute_hydration_times cannot be migrated or else the 0dt caught-up gate loses its leader-hydration reference"
960 );
961
962 let Some(object_info) = self.system_objects.get(object) else {
963 panic!("migration step for non-existent builtin: {object:?}");
964 };
965
966 let builtin = object_info.builtin;
967 use Builtin::*;
968 assert!(
969 matches!(builtin, Table(..) | Source(..) | MaterializedView(..)),
970 "schema migration not supported for builtin: {builtin:?}",
971 );
972 }
973 }
974
975 fn plan_migration(&self, steps: &[MigrationStep]) -> Plan {
977 let steps = steps.iter().filter(|s| s.version > self.source_version);
979
980 let mut by_object = BTreeMap::new();
984 for step in steps {
985 if let Some(entry) = by_object.get_mut(&step.object) {
986 *entry = match (step.mechanism, *entry) {
987 (Mechanism::Evolution, Mechanism::Evolution) => Mechanism::Evolution,
988 (Mechanism::Replacement, _) | (_, Mechanism::Replacement) => {
989 Mechanism::Replacement
990 }
991 };
992 } else {
993 by_object.insert(step.object.clone(), step.mechanism);
994 }
995 }
996
997 let mut plan = Plan::default();
998 for (object, mechanism) in by_object {
999 match mechanism {
1000 Mechanism::Evolution => plan.evolve.push(object),
1001 Mechanism::Replacement => plan.replace.push(object),
1002 }
1003 }
1004
1005 plan
1006 }
1007
1008 fn plan_forced_migration(&self, mechanism: Mechanism) -> Plan {
1010 let objects = self
1011 .system_objects
1012 .iter()
1013 .filter(|(_, info)| participates_in_forced_migration(info.builtin, mechanism))
1014 .map(|(object, _)| object.clone())
1015 .collect();
1016
1017 let mut plan = Plan::default();
1018 match mechanism {
1019 Mechanism::Evolution => plan.evolve = objects,
1020 Mechanism::Replacement => plan.replace = objects,
1021 }
1022
1023 plan
1024 }
1025
1026 fn drop_shardless(&self, mut plan: Plan) -> Plan {
1036 let has_shard = |object: &SystemObjectDescription| {
1037 self.system_objects
1038 .get(object)
1039 .is_none_or(|info| info.shard_id.is_some())
1040 };
1041 plan.evolve.retain(has_shard);
1042 plan.replace.retain(has_shard);
1043 plan
1044 }
1045
1046 async fn upgrade_migration_shard_version(&self) {
1048 let persist = &self.config.persist_client;
1049 let diagnostics = Diagnostics {
1050 shard_name: "builtin_migration".to_string(),
1051 handle_purpose: format!("migration shard upgrade @ {}", self.target_version),
1052 };
1053
1054 persist
1055 .upgrade_version::<migration_shard::Key, ShardId, Timestamp, StorageDiff>(
1056 self.migration_shard,
1057 diagnostics,
1058 )
1059 .await
1060 .expect("valid usage");
1061 }
1062
1063 async fn migrate_evolve(&self, objects: &[SystemObjectDescription]) -> anyhow::Result<()> {
1065 for object in objects {
1066 self.migrate_evolve_one(object).await?;
1067 }
1068 Ok(())
1069 }
1070
1071 async fn migrate_evolve_one(&self, object: &SystemObjectDescription) -> anyhow::Result<()> {
1072 let persist = &self.config.persist_client;
1073
1074 let Some(object_info) = self.system_objects.get(object) else {
1075 bail!("missing builtin {object:?}");
1076 };
1077 let id = object_info.global_id;
1078
1079 let Some(shard_id) = object_info.shard_id else {
1080 if self.config.read_only {
1085 bail!("missing shard ID for builtin {object:?} ({id})");
1086 } else {
1087 return Ok(());
1088 }
1089 };
1090
1091 let target_desc = match object_info.builtin {
1092 Builtin::Table(table) => &table.desc,
1093 Builtin::Source(source) => &source.desc,
1094 Builtin::MaterializedView(mv) => &mv.desc,
1095 _ => bail!("not a storage collection: {object:?}"),
1096 };
1097
1098 let diagnostics = Diagnostics {
1099 shard_name: id.to_string(),
1100 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1101 };
1102 let source_schema = persist
1103 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics.clone())
1104 .await
1105 .expect("valid usage");
1106
1107 info!(?object, %id, %shard_id, ?source_schema, ?target_desc, "migrating by evolution");
1108
1109 if self.config.read_only {
1110 if let Some((_, source_desc, _)) = &source_schema {
1113 let old = mz_persist_types::columnar::data_type::<SourceData>(source_desc)?;
1114 let new = mz_persist_types::columnar::data_type::<SourceData>(target_desc)?;
1115 if backward_compatible(&old, &new).is_none() {
1116 bail!(
1117 "incompatible schema evolution for {object:?}: \
1118 {source_desc:?} -> {target_desc:?}"
1119 );
1120 }
1121 }
1122
1123 return Ok(());
1124 }
1125
1126 let (mut schema_id, mut source_desc) = match source_schema {
1127 Some((schema_id, source_desc, _)) => (schema_id, source_desc),
1128 None => {
1129 debug!(%id, %shard_id, "no previous schema found; registering initial one");
1134 let schema_id = persist
1135 .register_schema::<SourceData, (), Timestamp, StorageDiff>(
1136 shard_id,
1137 target_desc,
1138 &UnitSchema,
1139 diagnostics.clone(),
1140 )
1141 .await
1142 .expect("valid usage");
1143 if schema_id.is_some() {
1144 return Ok(());
1145 }
1146
1147 debug!(%id, %shard_id, "schema registration failed; falling back to CaES");
1148 let (schema_id, source_desc, _) = persist
1149 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(
1150 shard_id,
1151 diagnostics.clone(),
1152 )
1153 .await
1154 .expect("valid usage")
1155 .expect("known to exist");
1156
1157 (schema_id, source_desc)
1158 }
1159 };
1160
1161 loop {
1162 debug!(%id, %shard_id, %schema_id, ?source_desc, ?target_desc, "attempting CaES");
1167 let result = persist
1168 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
1169 shard_id,
1170 schema_id,
1171 target_desc,
1172 &UnitSchema,
1173 diagnostics.clone(),
1174 )
1175 .await
1176 .expect("valid usage");
1177
1178 match result {
1179 CaESchema::Ok(schema_id) => {
1180 debug!(%id, %shard_id, %schema_id, "schema evolved successfully");
1181 break;
1182 }
1183 CaESchema::Incompatible => bail!(
1184 "incompatible schema evolution for {object:?}: \
1185 {source_desc:?} -> {target_desc:?}"
1186 ),
1187 CaESchema::ExpectedMismatch {
1188 schema_id: new_id,
1189 key,
1190 val: UnitSchema,
1191 } => {
1192 schema_id = new_id;
1193 source_desc = key;
1194 }
1195 }
1196 }
1197
1198 Ok(())
1199 }
1200
1201 async fn migrate_replace(
1203 &self,
1204 objects: &[SystemObjectDescription],
1205 ) -> anyhow::Result<BTreeMap<GlobalId, ShardId>> {
1206 if objects.is_empty() {
1207 return Ok(Default::default());
1208 }
1209
1210 let diagnostics = Diagnostics {
1211 shard_name: "builtin_migration".to_string(),
1212 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1213 };
1214 let (mut persist_write, mut persist_read) =
1215 self.open_migration_shard(diagnostics.clone()).await;
1216
1217 let mut ids_to_replace = BTreeSet::new();
1218 for object in objects {
1219 if let Some(info) = self.system_objects.get(object) {
1220 ids_to_replace.insert(info.global_id);
1221 } else {
1222 bail!("missing id for builtin {object:?}");
1223 }
1224 }
1225
1226 info!(?objects, ?ids_to_replace, "migrating by replacement");
1227
1228 let replaced_shards = loop {
1231 if let Some(shards) = self
1232 .try_get_or_insert_replacement_shards(
1233 &ids_to_replace,
1234 &mut persist_write,
1235 &mut persist_read,
1236 )
1237 .await?
1238 {
1239 break shards;
1240 }
1241 };
1242
1243 Ok(replaced_shards)
1244 }
1245
1246 async fn try_get_or_insert_replacement_shards(
1256 &self,
1257 ids_to_replace: &BTreeSet<GlobalId>,
1258 persist_write: &mut WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1259 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1260 ) -> anyhow::Result<Option<BTreeMap<GlobalId, ShardId>>> {
1261 let upper = persist_write.fetch_recent_upper().await;
1262 let write_ts = *upper.as_option().expect("migration shard not sealed");
1263
1264 let mut ids_to_replace = ids_to_replace.clone();
1265 let mut replaced_shards = BTreeMap::new();
1266
1267 if let Some(read_ts) = write_ts.step_back() {
1275 let pred = |key: &migration_shard::Key| {
1276 key.build_version == self.target_version
1277 && key.deploy_generation == Some(self.deploy_generation)
1278 };
1279 if let Some(entries) = read_migration_shard(persist_read, read_ts, pred).await {
1280 for (key, shard_id) in entries {
1281 let id = GlobalId::System(key.global_id);
1282 if ids_to_replace.remove(&id) {
1283 replaced_shards.insert(id, shard_id);
1284 }
1285 }
1286
1287 debug!(
1288 %read_ts, ?replaced_shards, ?ids_to_replace,
1289 "found existing entries in migration shard",
1290 );
1291 }
1292
1293 if ids_to_replace.is_empty() {
1294 return Ok(Some(replaced_shards));
1295 }
1296 }
1297
1298 let mut updates = Vec::new();
1302 for id in ids_to_replace {
1303 let shard_id = ShardId::new();
1304 replaced_shards.insert(id, shard_id);
1305
1306 let GlobalId::System(global_id) = id else {
1307 bail!("attempt to migrate a non-system collection: {id}");
1308 };
1309 let key = migration_shard::Key {
1310 global_id,
1311 build_version: self.target_version.clone(),
1312 deploy_generation: Some(self.deploy_generation),
1313 };
1314 updates.push(((key, shard_id), write_ts, 1));
1315 }
1316
1317 let upper = Antichain::from_elem(write_ts);
1318 let new_upper = Antichain::from_elem(write_ts.step_forward());
1319 debug!(%write_ts, "attempting insert into migration shard");
1320 let result = persist_write
1321 .compare_and_append(updates, upper, new_upper)
1322 .await
1323 .expect("valid usage");
1324
1325 match result {
1326 Ok(()) => {
1327 debug!(
1328 %write_ts, ?replaced_shards,
1329 "successfully inserted into migration shard"
1330 );
1331 Ok(Some(replaced_shards))
1332 }
1333 Err(_mismatch) => Ok(None),
1334 }
1335 }
1336
1337 async fn open_migration_shard(
1339 &self,
1340 diagnostics: Diagnostics,
1341 ) -> (
1342 WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1343 ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1344 ) {
1345 let persist = &self.config.persist_client;
1346
1347 persist
1348 .open(
1349 self.migration_shard,
1350 Arc::new(migration_shard::KeySchema),
1351 Arc::new(ShardIdSchema),
1352 diagnostics,
1353 USE_CRITICAL_SINCE_CATALOG.get(persist.dyncfgs()),
1354 )
1355 .await
1356 .expect("valid usage")
1357 }
1358
1359 async fn open_migration_shard_since(
1361 &self,
1362 diagnostics: Diagnostics,
1363 ) -> SinceHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff> {
1364 self.config
1365 .persist_client
1366 .open_critical_since(
1367 self.migration_shard,
1368 PersistClient::CONTROLLER_CRITICAL_SINCE,
1371 Opaque::encode(&i64::MIN),
1372 diagnostics.clone(),
1373 )
1374 .await
1375 .expect("valid usage")
1376 }
1377
1378 fn update_fingerprints(
1383 &self,
1384 migrated_items: &BTreeSet<SystemObjectDescription>,
1385 ) -> anyhow::Result<BTreeMap<SystemObjectDescription, String>> {
1386 let mut new_fingerprints = BTreeMap::new();
1387 for (object, object_info) in &self.system_objects {
1388 let id = object_info.global_id;
1389 let builtin = object_info.builtin;
1390
1391 let fingerprint = builtin.fingerprint();
1392 if fingerprint == object_info.fingerprint {
1393 continue; }
1395
1396 let migrated = migrated_items.contains(object);
1398 let ephemeral = matches!(
1400 builtin,
1401 Builtin::Log(_) | Builtin::View(_) | Builtin::Index(_),
1402 );
1403
1404 if migrated || ephemeral {
1405 new_fingerprints.insert(object.clone(), fingerprint);
1406 } else if builtin.runtime_alterable() {
1407 assert_eq!(
1410 object_info.fingerprint, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
1411 "fingerprint mismatch for runtime-alterable builtin {object:?} ({id})",
1412 );
1413 } else {
1414 panic!(
1415 "fingerprint mismatch for builtin {builtin:?} ({id}): {} != {}",
1416 fingerprint, object_info.fingerprint,
1417 );
1418 }
1419 }
1420
1421 Ok(new_fingerprints)
1422 }
1423
1424 async fn cleanup(&self) -> anyhow::Result<(BTreeSet<ShardId>, BoxFuture<'static, ()>)> {
1441 let noop_action = async {}.boxed();
1442 let noop_result = (BTreeSet::new(), noop_action);
1443
1444 if self.config.read_only {
1445 return Ok(noop_result);
1446 }
1447
1448 let diagnostics = Diagnostics {
1449 shard_name: "builtin_migration".to_string(),
1450 handle_purpose: "builtin schema migration cleanup".into(),
1451 };
1452 let (mut persist_write, mut persist_read) =
1453 self.open_migration_shard(diagnostics.clone()).await;
1454 let mut persist_since = self.open_migration_shard_since(diagnostics.clone()).await;
1455
1456 let upper = persist_write.fetch_recent_upper().await.clone();
1457 let write_ts = *upper.as_option().expect("migration shard not sealed");
1458 let Some(read_ts) = write_ts.step_back() else {
1459 return Ok(noop_result);
1460 };
1461
1462 let pred = |key: &migration_shard::Key| key.build_version < self.target_version;
1464 let Some(stale_entries) = read_migration_shard(&mut persist_read, read_ts, pred).await
1465 else {
1466 return Ok(noop_result);
1467 };
1468
1469 debug!(
1470 ?stale_entries,
1471 "cleaning migration shard up to version {}", self.target_version,
1472 );
1473
1474 let current_shards: BTreeMap<_, _> = self
1475 .system_objects
1476 .values()
1477 .filter_map(|o| o.shard_id.map(|shard_id| (o.global_id, shard_id)))
1478 .collect();
1479
1480 let mut shards_to_finalize = BTreeSet::new();
1481 let mut retractions = Vec::new();
1482 for (key, shard_id) in stale_entries {
1483 let gid = GlobalId::System(key.global_id);
1487 if current_shards.get(&gid) != Some(&shard_id) {
1488 shards_to_finalize.insert(shard_id);
1489 }
1490
1491 retractions.push(((key, shard_id), write_ts, -1));
1492 }
1493
1494 let cleanup_action = async move {
1495 if !retractions.is_empty() {
1496 let new_upper = Antichain::from_elem(write_ts.step_forward());
1497 let result = persist_write
1498 .compare_and_append(retractions, upper, new_upper)
1499 .await
1500 .expect("valid usage");
1501 match result {
1502 Ok(()) => debug!("cleaned up migration shard"),
1503 Err(mismatch) => debug!(?mismatch, "migration shard cleanup failed"),
1504 }
1505 }
1506 }
1507 .boxed();
1508
1509 let o = persist_since.opaque().clone();
1511 let new_since = Antichain::from_elem(read_ts);
1512 let result = persist_since
1513 .maybe_compare_and_downgrade_since(&o, (&o, &new_since))
1514 .await;
1515 soft_assert_or_log!(result.is_none_or(|r| r.is_ok()), "opaque mismatch");
1516
1517 Ok((shards_to_finalize, cleanup_action))
1518 }
1519}
1520
1521async fn read_migration_shard<P>(
1527 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1528 read_ts: Timestamp,
1529 predicate: P,
1530) -> Option<Vec<(migration_shard::Key, ShardId)>>
1531where
1532 P: Fn(&migration_shard::Key) -> bool,
1533{
1534 let as_of = Antichain::from_elem(read_ts);
1535 let updates = persist_read.snapshot_and_fetch(as_of).await.ok()?;
1536
1537 assert!(
1538 updates.iter().all(|(_, _, diff)| *diff == 1),
1539 "migration shard contains invalid diffs: {updates:?}",
1540 );
1541
1542 let entries: Vec<_> = updates
1543 .into_iter()
1544 .map(|(data, _, _)| data)
1545 .filter(move |(key, _)| predicate(key))
1546 .collect();
1547
1548 (!entries.is_empty()).then_some(entries)
1549}
1550
1551#[derive(Debug, Default)]
1553struct Plan {
1554 evolve: Vec<SystemObjectDescription>,
1556 replace: Vec<SystemObjectDescription>,
1558}
1559
1560mod migration_shard {
1562 use std::fmt;
1563 use std::str::FromStr;
1564
1565 use arrow::array::{StringArray, StringBuilder};
1566 use bytes::{BufMut, Bytes};
1567 use mz_persist_types::Codec;
1568 use mz_persist_types::codec_impls::{
1569 SimpleColumnarData, SimpleColumnarDecoder, SimpleColumnarEncoder,
1570 };
1571 use mz_persist_types::columnar::Schema;
1572 use mz_persist_types::stats::NoneStats;
1573 use semver::Version;
1574 use serde::{Deserialize, Serialize};
1575
1576 #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1577 pub(super) struct Key {
1578 pub(super) global_id: u64,
1579 pub(super) build_version: Version,
1580 pub(super) deploy_generation: Option<u64>,
1584 }
1585
1586 impl fmt::Display for Key {
1587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1588 if self.deploy_generation.is_some() {
1589 let s = serde_json::to_string(self).expect("JSON serializable");
1591 f.write_str(&s)
1592 } else {
1593 write!(f, "{}-{}", self.global_id, self.build_version)
1595 }
1596 }
1597 }
1598
1599 impl FromStr for Key {
1600 type Err = String;
1601
1602 fn from_str(s: &str) -> Result<Self, String> {
1603 if let Ok(key) = serde_json::from_str(s) {
1605 return Ok(key);
1606 };
1607
1608 let parts: Vec<_> = s.splitn(2, '-').collect();
1610 let &[global_id, build_version] = parts.as_slice() else {
1611 return Err(format!("invalid Key '{s}'"));
1612 };
1613 let global_id = global_id.parse::<u64>().map_err(|e| e.to_string())?;
1614 let build_version = build_version
1615 .parse::<Version>()
1616 .map_err(|e| e.to_string())?;
1617 Ok(Key {
1618 global_id,
1619 build_version,
1620 deploy_generation: None,
1621 })
1622 }
1623 }
1624
1625 impl Default for Key {
1626 fn default() -> Self {
1627 Self {
1628 global_id: Default::default(),
1629 build_version: Version::new(0, 0, 0),
1630 deploy_generation: Some(0),
1631 }
1632 }
1633 }
1634
1635 impl Codec for Key {
1636 type Schema = KeySchema;
1637 type Storage = ();
1638
1639 fn codec_name() -> String {
1640 "TableKey".into()
1641 }
1642
1643 fn encode<B: BufMut>(&self, buf: &mut B) {
1644 buf.put(self.to_string().as_bytes())
1645 }
1646
1647 fn decode<'a>(buf: &'a [u8], _schema: &KeySchema) -> Result<Self, String> {
1648 let s = str::from_utf8(buf).map_err(|e| e.to_string())?;
1649 s.parse()
1650 }
1651
1652 fn encode_schema(_schema: &KeySchema) -> Bytes {
1653 Bytes::new()
1654 }
1655
1656 fn decode_schema(buf: &Bytes) -> Self::Schema {
1657 assert_eq!(*buf, Bytes::new());
1658 KeySchema
1659 }
1660 }
1661
1662 impl SimpleColumnarData for Key {
1663 type ArrowBuilder = StringBuilder;
1664 type ArrowColumn = StringArray;
1665
1666 fn goodbytes(builder: &Self::ArrowBuilder) -> usize {
1667 builder.values_slice().len()
1668 }
1669
1670 fn push(&self, builder: &mut Self::ArrowBuilder) {
1671 builder.append_value(&self.to_string());
1672 }
1673
1674 fn push_null(builder: &mut Self::ArrowBuilder) {
1675 builder.append_null();
1676 }
1677
1678 fn read(&mut self, idx: usize, column: &Self::ArrowColumn) {
1679 *self = column.value(idx).parse().expect("valid Key");
1680 }
1681 }
1682
1683 #[derive(Debug, PartialEq)]
1684 pub(super) struct KeySchema;
1685
1686 impl Schema<Key> for KeySchema {
1687 type ArrowColumn = StringArray;
1688 type Statistics = NoneStats;
1689 type Decoder = SimpleColumnarDecoder<Key>;
1690 type Encoder = SimpleColumnarEncoder<Key>;
1691
1692 fn encoder(&self) -> anyhow::Result<SimpleColumnarEncoder<Key>> {
1693 Ok(SimpleColumnarEncoder::default())
1694 }
1695
1696 fn decoder(&self, col: StringArray) -> anyhow::Result<SimpleColumnarDecoder<Key>> {
1697 Ok(SimpleColumnarDecoder::new(col))
1698 }
1699 }
1700}
1701
1702#[cfg(test)]
1703#[path = "builtin_schema_migration_tests.rs"]
1704mod tests;