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_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION,
41 MZ_OBJECT_HYDRATION_HISTORY, MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION,
42 MZ_REPLICA_HYDRATION_HISTORY, MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION,
43 MZ_STORAGE_USAGE_BY_SHARD, MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION,
44 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.37.0-dev.0",
342 CatalogItemType::MaterializedView,
343 MZ_CATALOG_SCHEMA,
344 "mz_kafka_connections",
345 ),
346 MigrationStep::replacement(
347 "26.37.0-dev.0",
348 CatalogItemType::MaterializedView,
349 MZ_CATALOG_SCHEMA,
350 "mz_ssh_tunnel_connections",
351 ),
352 MigrationStep::replacement(
353 "26.37.0-dev.0",
354 CatalogItemType::MaterializedView,
355 MZ_INTERNAL_SCHEMA,
356 "mz_aws_connections",
357 ),
358 MigrationStep::replacement(
359 "26.37.0-dev.0",
360 CatalogItemType::MaterializedView,
361 MZ_CATALOG_SCHEMA,
362 "mz_aws_privatelink_connections",
363 ),
364 MigrationStep::replacement(
370 "26.38.0-dev.0",
371 CatalogItemType::MaterializedView,
372 MZ_CATALOG_SCHEMA,
373 "mz_sinks",
374 ),
375 MigrationStep::replacement(
376 "26.38.0-dev.0",
377 CatalogItemType::MaterializedView,
378 MZ_CATALOG_SCHEMA,
379 "mz_kafka_sinks",
380 ),
381 MigrationStep::replacement(
382 "26.38.0-dev.0",
383 CatalogItemType::MaterializedView,
384 MZ_CATALOG_SCHEMA,
385 "mz_iceberg_sinks",
386 ),
387 MigrationStep::replacement(
390 "26.38.0-rc.2",
391 CatalogItemType::MaterializedView,
392 MZ_INTERNAL_SCHEMA,
393 "mz_cluster_reconfigurations",
394 ),
395 MigrationStep::replacement(
400 "26.39.0-dev.0",
401 CatalogItemType::MaterializedView,
402 MZ_CATALOG_SCHEMA,
403 "mz_audit_events",
404 ),
405 MigrationStep::replacement(
415 "26.40.0-dev.0",
416 CatalogItemType::MaterializedView,
417 MZ_CATALOG_SCHEMA,
418 "mz_indexes",
419 ),
420 MigrationStep::replacement(
426 "26.39.0-dev.0",
427 CatalogItemType::MaterializedView,
428 MZ_CATALOG_SCHEMA,
429 "mz_tables",
430 ),
431 MigrationStep::replacement(
432 "26.39.0-dev.0",
433 CatalogItemType::MaterializedView,
434 MZ_CATALOG_SCHEMA,
435 "mz_views",
436 ),
437 MigrationStep::replacement(
443 "26.40.0-dev.0",
444 CatalogItemType::MaterializedView,
445 MZ_CATALOG_SCHEMA,
446 "mz_indexes",
447 ),
448 MigrationStep::replacement(
449 "26.40.0-dev.0",
450 CatalogItemType::MaterializedView,
451 MZ_CATALOG_SCHEMA,
452 "mz_sources",
453 ),
454 MigrationStep::replacement(
460 "26.41.0-dev.0",
461 CatalogItemType::MaterializedView,
462 MZ_INTERNAL_SCHEMA,
463 "mz_object_dependencies",
464 ),
465 MigrationStep::evolution(
470 "26.43.0-dev.0",
471 CatalogItemType::Source,
472 MZ_INTERNAL_SCHEMA,
473 "mz_cluster_replica_metrics_history",
474 ),
475 ]
476});
477
478#[derive(Clone, Debug)]
480struct MigrationStep {
481 version: Version,
483 object: SystemObjectDescription,
485 mechanism: Mechanism,
487}
488
489impl MigrationStep {
490 fn evolution(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
492 Self {
493 version: Version::parse(version).expect("valid"),
494 object: SystemObjectDescription {
495 schema_name: schema.into(),
496 object_type: type_,
497 object_name: name.into(),
498 },
499 mechanism: Mechanism::Evolution,
500 }
501 }
502
503 fn replacement(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
505 Self {
506 version: Version::parse(version).expect("valid"),
507 object: SystemObjectDescription {
508 schema_name: schema.into(),
509 object_type: type_,
510 object_name: name.into(),
511 },
512 mechanism: Mechanism::Replacement,
513 }
514 }
515}
516
517#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
519#[allow(dead_code)]
520enum Mechanism {
521 Evolution,
526 Replacement,
530}
531
532pub(super) struct MigrationResult {
534 pub replaced_items: BTreeSet<CatalogItemId>,
536 pub cleanup_action: BoxFuture<'static, ()>,
538}
539
540impl Default for MigrationResult {
541 fn default() -> Self {
542 Self {
543 replaced_items: Default::default(),
544 cleanup_action: async {}.boxed(),
545 }
546 }
547}
548
549pub(super) async fn run(
555 build_info: &BuildInfo,
556 deploy_generation: u64,
557 txn: &mut Transaction<'_>,
558 config: BuiltinItemMigrationConfig,
559) -> Result<MigrationResult, Error> {
560 assert_eq!(config.read_only, txn.is_savepoint());
562
563 if *build_info == DUMMY_BUILD_INFO {
566 return Ok(MigrationResult::default());
567 }
568
569 let Some(durable_version) = get_migration_version(txn) else {
570 return Ok(MigrationResult::default());
572 };
573 let build_version = build_info.semver_version();
574
575 let collection_metadata = txn.get_collection_metadata();
576 let system_objects = txn
577 .get_system_object_mappings()
578 .map(|m| {
579 let object = m.description;
580 let global_id = m.unique_identifier.global_id;
581 let shard_id = collection_metadata.get(&global_id).copied();
582 let Some((_, builtin)) = BUILTIN_LOOKUP.get(&object) else {
583 panic!("missing builtin {object:?}");
584 };
585 let info = ObjectInfo {
586 global_id,
587 shard_id,
588 builtin,
589 fingerprint: m.unique_identifier.fingerprint,
590 };
591 (object, info)
592 })
593 .collect();
594
595 let migration_shard = txn.get_builtin_migration_shard().expect("must exist");
596
597 let migration = Migration {
598 source_version: durable_version.clone(),
599 target_version: build_version.clone(),
600 deploy_generation,
601 system_objects,
602 migration_shard,
603 config,
604 };
605
606 let result = migration.run(&MIGRATIONS).await.map_err(|e| {
607 Error::new(ErrorKind::FailedBuiltinSchemaMigration {
608 last_seen_version: durable_version.to_string(),
609 this_version: build_version.to_string(),
610 cause: e.to_string(),
611 })
612 })?;
613
614 result.apply(txn);
615
616 let replaced_items = txn
617 .get_system_object_mappings()
618 .map(|m| m.unique_identifier)
619 .filter(|ids| result.new_shards.contains_key(&ids.global_id))
620 .map(|ids| ids.catalog_id)
621 .collect();
622
623 Ok(MigrationResult {
624 replaced_items,
625 cleanup_action: result.cleanup_action,
626 })
627}
628
629struct MigrationRunResult {
631 new_shards: BTreeMap<GlobalId, ShardId>,
632 new_fingerprints: BTreeMap<SystemObjectDescription, String>,
633 shards_to_finalize: BTreeSet<ShardId>,
634 cleanup_action: BoxFuture<'static, ()>,
635}
636
637impl Default for MigrationRunResult {
638 fn default() -> Self {
639 Self {
640 new_shards: BTreeMap::new(),
641 new_fingerprints: BTreeMap::new(),
642 shards_to_finalize: BTreeSet::new(),
643 cleanup_action: async {}.boxed(),
644 }
645 }
646}
647
648impl MigrationRunResult {
649 fn apply(&self, txn: &mut Transaction<'_>) {
651 let replaced_ids = self.new_shards.keys().copied().collect();
653 let old_metadata = txn.delete_collection_metadata(replaced_ids);
654 txn.insert_collection_metadata(self.new_shards.clone())
655 .expect("inserting unique shards IDs after deleting existing entries");
656
657 let mut unfinalized_shards: BTreeSet<_> =
659 old_metadata.into_iter().map(|(_, sid)| sid).collect();
660 unfinalized_shards.extend(self.shards_to_finalize.iter().copied());
661 txn.insert_unfinalized_shards(unfinalized_shards)
662 .expect("cannot fail");
663
664 let mappings = txn
666 .get_system_object_mappings()
667 .filter_map(|m| {
668 let fingerprint = self.new_fingerprints.get(&m.description)?;
669 Some(SystemObjectMapping {
670 description: m.description,
671 unique_identifier: SystemObjectUniqueIdentifier {
672 catalog_id: m.unique_identifier.catalog_id,
673 global_id: m.unique_identifier.global_id,
674 fingerprint: fingerprint.clone(),
675 },
676 })
677 })
678 .collect();
679 txn.set_system_object_mappings(mappings)
680 .expect("filtered existing mappings remain unique");
681 }
682}
683
684#[derive(Clone, Debug)]
686struct ObjectInfo {
687 global_id: GlobalId,
688 shard_id: Option<ShardId>,
689 builtin: &'static Builtin<NameReference>,
690 fingerprint: String,
691}
692
693struct Migration {
695 source_version: Version,
700 target_version: Version,
704 deploy_generation: u64,
706 system_objects: BTreeMap<SystemObjectDescription, ObjectInfo>,
708 migration_shard: ShardId,
710 config: BuiltinItemMigrationConfig,
712}
713
714fn participates_in_forced_migration(
716 builtin: &Builtin<NameReference>,
717 mechanism: Mechanism,
718) -> bool {
719 use Builtin::*;
720 match builtin {
721 Table(table) => {
733 **table != *MZ_STORAGE_USAGE_BY_SHARD
734 && (mechanism != Mechanism::Replacement
735 || (**table != *MZ_OBJECT_HYDRATION_HISTORY
736 && **table != *MZ_REPLICA_HYDRATION_HISTORY))
737 }
738 MaterializedView(..) => true,
739 Source(source) => **source != *MZ_CATALOG_RAW,
740 Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,
741 }
742}
743
744impl Migration {
745 async fn run(self, steps: &[MigrationStep]) -> anyhow::Result<MigrationRunResult> {
746 info!(
747 deploy_generation = %self.deploy_generation,
748 "running builtin schema migration: {} -> {}",
749 self.source_version, self.target_version
750 );
751
752 self.validate_migration_steps(steps);
753
754 let force_migration = if self.source_version != self.target_version
757 && self.source_version.pre.as_str().starts_with("dev")
758 && self.config.force_migration.is_none()
759 {
760 Some("evolution".to_string())
761 } else {
762 self.config.force_migration.clone()
763 };
764
765 let (force, plan) = match force_migration.as_deref() {
766 None => (false, self.plan_migration(steps)),
767 Some("evolution") => (true, self.plan_forced_migration(Mechanism::Evolution)),
768 Some("replacement") => (true, self.plan_forced_migration(Mechanism::Replacement)),
769 Some(other) => panic!("unknown force migration mechanism: {other}"),
770 };
771
772 if self.source_version == self.target_version && !force {
773 info!("skipping migration: already at target version");
774 return Ok(MigrationRunResult::default());
775 } else if self.source_version > self.target_version {
776 bail!("downgrade not supported");
777 }
778
779 if !self.config.read_only {
782 self.upgrade_migration_shard_version().await;
783 }
784
785 info!("executing migration plan: {plan:?}");
786
787 self.migrate_evolve(&plan.evolve).await?;
788 let new_shards = self.migrate_replace(&plan.replace).await?;
789
790 let mut migrated_objects = BTreeSet::new();
791 migrated_objects.extend(plan.evolve);
792 migrated_objects.extend(plan.replace);
793
794 let new_fingerprints = self.update_fingerprints(&migrated_objects)?;
795
796 let (shards_to_finalize, cleanup_action) = self.cleanup().await?;
797
798 Ok(MigrationRunResult {
799 new_shards,
800 new_fingerprints,
801 shards_to_finalize,
802 cleanup_action,
803 })
804 }
805
806 fn validate_migration_steps(&self, steps: &[MigrationStep]) {
810 for step in steps {
811 assert!(
812 step.version <= self.target_version,
813 "migration step version greater than target version: {} > {}",
814 step.version,
815 self.target_version,
816 );
817
818 let object = &step.object;
819
820 assert_ne!(
828 &*MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, object,
829 "mz_storage_usage_by_shard cannot be migrated or else the table will be truncated"
830 );
831
832 assert_ne!(
836 &*MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, object,
837 "mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
838 );
839
840 if step.mechanism == Mechanism::Replacement {
851 assert_ne!(
852 &*MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, object,
853 "replacing mz_object_hydration_history clears it, see the comment above"
854 );
855 assert_ne!(
856 &*MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION, object,
857 "replacing mz_replica_hydration_history clears it, see the comment above"
858 );
859 }
860
861 assert_ne!(
864 &*MZ_CATALOG_RAW_DESCRIPTION, object,
865 "mz_catalog_raw cannot be migrated"
866 );
867
868 assert_ne!(
873 &*MZ_CLUSTER_REPLICA_FRONTIERS_DESCRIPTION, object,
874 "mz_cluster_replica_frontiers cannot be migrated or else the 0dt caught-up gate loses its live-frontier reference"
875 );
876
877 let Some(object_info) = self.system_objects.get(object) else {
878 panic!("migration step for non-existent builtin: {object:?}");
879 };
880
881 let builtin = object_info.builtin;
882 use Builtin::*;
883 assert!(
884 matches!(builtin, Table(..) | Source(..) | MaterializedView(..)),
885 "schema migration not supported for builtin: {builtin:?}",
886 );
887 }
888 }
889
890 fn plan_migration(&self, steps: &[MigrationStep]) -> Plan {
892 let steps = steps.iter().filter(|s| s.version > self.source_version);
894
895 let mut by_object = BTreeMap::new();
899 for step in steps {
900 if let Some(entry) = by_object.get_mut(&step.object) {
901 *entry = match (step.mechanism, *entry) {
902 (Mechanism::Evolution, Mechanism::Evolution) => Mechanism::Evolution,
903 (Mechanism::Replacement, _) | (_, Mechanism::Replacement) => {
904 Mechanism::Replacement
905 }
906 };
907 } else {
908 by_object.insert(step.object.clone(), step.mechanism);
909 }
910 }
911
912 let mut plan = Plan::default();
913 for (object, mechanism) in by_object {
914 match mechanism {
915 Mechanism::Evolution => plan.evolve.push(object),
916 Mechanism::Replacement => plan.replace.push(object),
917 }
918 }
919
920 plan
921 }
922
923 fn plan_forced_migration(&self, mechanism: Mechanism) -> Plan {
925 let objects = self
926 .system_objects
927 .iter()
928 .filter(|(_, info)| info.shard_id.is_some())
932 .filter(|(_, info)| participates_in_forced_migration(info.builtin, mechanism))
933 .map(|(object, _)| object.clone())
934 .collect();
935
936 let mut plan = Plan::default();
937 match mechanism {
938 Mechanism::Evolution => plan.evolve = objects,
939 Mechanism::Replacement => plan.replace = objects,
940 }
941
942 plan
943 }
944
945 async fn upgrade_migration_shard_version(&self) {
947 let persist = &self.config.persist_client;
948 let diagnostics = Diagnostics {
949 shard_name: "builtin_migration".to_string(),
950 handle_purpose: format!("migration shard upgrade @ {}", self.target_version),
951 };
952
953 persist
954 .upgrade_version::<migration_shard::Key, ShardId, Timestamp, StorageDiff>(
955 self.migration_shard,
956 diagnostics,
957 )
958 .await
959 .expect("valid usage");
960 }
961
962 async fn migrate_evolve(&self, objects: &[SystemObjectDescription]) -> anyhow::Result<()> {
964 for object in objects {
965 self.migrate_evolve_one(object).await?;
966 }
967 Ok(())
968 }
969
970 async fn migrate_evolve_one(&self, object: &SystemObjectDescription) -> anyhow::Result<()> {
971 let persist = &self.config.persist_client;
972
973 let Some(object_info) = self.system_objects.get(object) else {
974 bail!("missing builtin {object:?}");
975 };
976 let id = object_info.global_id;
977
978 let Some(shard_id) = object_info.shard_id else {
979 if self.config.read_only {
984 bail!("missing shard ID for builtin {object:?} ({id})");
985 } else {
986 return Ok(());
987 }
988 };
989
990 let target_desc = match object_info.builtin {
991 Builtin::Table(table) => &table.desc,
992 Builtin::Source(source) => &source.desc,
993 Builtin::MaterializedView(mv) => &mv.desc,
994 _ => bail!("not a storage collection: {object:?}"),
995 };
996
997 let diagnostics = Diagnostics {
998 shard_name: id.to_string(),
999 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1000 };
1001 let source_schema = persist
1002 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics.clone())
1003 .await
1004 .expect("valid usage");
1005
1006 info!(?object, %id, %shard_id, ?source_schema, ?target_desc, "migrating by evolution");
1007
1008 if self.config.read_only {
1009 if let Some((_, source_desc, _)) = &source_schema {
1012 let old = mz_persist_types::columnar::data_type::<SourceData>(source_desc)?;
1013 let new = mz_persist_types::columnar::data_type::<SourceData>(target_desc)?;
1014 if backward_compatible(&old, &new).is_none() {
1015 bail!(
1016 "incompatible schema evolution for {object:?}: \
1017 {source_desc:?} -> {target_desc:?}"
1018 );
1019 }
1020 }
1021
1022 return Ok(());
1023 }
1024
1025 let (mut schema_id, mut source_desc) = match source_schema {
1026 Some((schema_id, source_desc, _)) => (schema_id, source_desc),
1027 None => {
1028 debug!(%id, %shard_id, "no previous schema found; registering initial one");
1033 let schema_id = persist
1034 .register_schema::<SourceData, (), Timestamp, StorageDiff>(
1035 shard_id,
1036 target_desc,
1037 &UnitSchema,
1038 diagnostics.clone(),
1039 )
1040 .await
1041 .expect("valid usage");
1042 if schema_id.is_some() {
1043 return Ok(());
1044 }
1045
1046 debug!(%id, %shard_id, "schema registration failed; falling back to CaES");
1047 let (schema_id, source_desc, _) = persist
1048 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(
1049 shard_id,
1050 diagnostics.clone(),
1051 )
1052 .await
1053 .expect("valid usage")
1054 .expect("known to exist");
1055
1056 (schema_id, source_desc)
1057 }
1058 };
1059
1060 loop {
1061 debug!(%id, %shard_id, %schema_id, ?source_desc, ?target_desc, "attempting CaES");
1066 let result = persist
1067 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
1068 shard_id,
1069 schema_id,
1070 target_desc,
1071 &UnitSchema,
1072 diagnostics.clone(),
1073 )
1074 .await
1075 .expect("valid usage");
1076
1077 match result {
1078 CaESchema::Ok(schema_id) => {
1079 debug!(%id, %shard_id, %schema_id, "schema evolved successfully");
1080 break;
1081 }
1082 CaESchema::Incompatible => bail!(
1083 "incompatible schema evolution for {object:?}: \
1084 {source_desc:?} -> {target_desc:?}"
1085 ),
1086 CaESchema::ExpectedMismatch {
1087 schema_id: new_id,
1088 key,
1089 val: UnitSchema,
1090 } => {
1091 schema_id = new_id;
1092 source_desc = key;
1093 }
1094 }
1095 }
1096
1097 Ok(())
1098 }
1099
1100 async fn migrate_replace(
1102 &self,
1103 objects: &[SystemObjectDescription],
1104 ) -> anyhow::Result<BTreeMap<GlobalId, ShardId>> {
1105 if objects.is_empty() {
1106 return Ok(Default::default());
1107 }
1108
1109 let diagnostics = Diagnostics {
1110 shard_name: "builtin_migration".to_string(),
1111 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1112 };
1113 let (mut persist_write, mut persist_read) =
1114 self.open_migration_shard(diagnostics.clone()).await;
1115
1116 let mut ids_to_replace = BTreeSet::new();
1117 for object in objects {
1118 if let Some(info) = self.system_objects.get(object) {
1119 ids_to_replace.insert(info.global_id);
1120 } else {
1121 bail!("missing id for builtin {object:?}");
1122 }
1123 }
1124
1125 info!(?objects, ?ids_to_replace, "migrating by replacement");
1126
1127 let replaced_shards = loop {
1130 if let Some(shards) = self
1131 .try_get_or_insert_replacement_shards(
1132 &ids_to_replace,
1133 &mut persist_write,
1134 &mut persist_read,
1135 )
1136 .await?
1137 {
1138 break shards;
1139 }
1140 };
1141
1142 Ok(replaced_shards)
1143 }
1144
1145 async fn try_get_or_insert_replacement_shards(
1155 &self,
1156 ids_to_replace: &BTreeSet<GlobalId>,
1157 persist_write: &mut WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1158 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1159 ) -> anyhow::Result<Option<BTreeMap<GlobalId, ShardId>>> {
1160 let upper = persist_write.fetch_recent_upper().await;
1161 let write_ts = *upper.as_option().expect("migration shard not sealed");
1162
1163 let mut ids_to_replace = ids_to_replace.clone();
1164 let mut replaced_shards = BTreeMap::new();
1165
1166 if let Some(read_ts) = write_ts.step_back() {
1174 let pred = |key: &migration_shard::Key| {
1175 key.build_version == self.target_version
1176 && key.deploy_generation == Some(self.deploy_generation)
1177 };
1178 if let Some(entries) = read_migration_shard(persist_read, read_ts, pred).await {
1179 for (key, shard_id) in entries {
1180 let id = GlobalId::System(key.global_id);
1181 if ids_to_replace.remove(&id) {
1182 replaced_shards.insert(id, shard_id);
1183 }
1184 }
1185
1186 debug!(
1187 %read_ts, ?replaced_shards, ?ids_to_replace,
1188 "found existing entries in migration shard",
1189 );
1190 }
1191
1192 if ids_to_replace.is_empty() {
1193 return Ok(Some(replaced_shards));
1194 }
1195 }
1196
1197 let mut updates = Vec::new();
1201 for id in ids_to_replace {
1202 let shard_id = ShardId::new();
1203 replaced_shards.insert(id, shard_id);
1204
1205 let GlobalId::System(global_id) = id else {
1206 bail!("attempt to migrate a non-system collection: {id}");
1207 };
1208 let key = migration_shard::Key {
1209 global_id,
1210 build_version: self.target_version.clone(),
1211 deploy_generation: Some(self.deploy_generation),
1212 };
1213 updates.push(((key, shard_id), write_ts, 1));
1214 }
1215
1216 let upper = Antichain::from_elem(write_ts);
1217 let new_upper = Antichain::from_elem(write_ts.step_forward());
1218 debug!(%write_ts, "attempting insert into migration shard");
1219 let result = persist_write
1220 .compare_and_append(updates, upper, new_upper)
1221 .await
1222 .expect("valid usage");
1223
1224 match result {
1225 Ok(()) => {
1226 debug!(
1227 %write_ts, ?replaced_shards,
1228 "successfully inserted into migration shard"
1229 );
1230 Ok(Some(replaced_shards))
1231 }
1232 Err(_mismatch) => Ok(None),
1233 }
1234 }
1235
1236 async fn open_migration_shard(
1238 &self,
1239 diagnostics: Diagnostics,
1240 ) -> (
1241 WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1242 ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1243 ) {
1244 let persist = &self.config.persist_client;
1245
1246 persist
1247 .open(
1248 self.migration_shard,
1249 Arc::new(migration_shard::KeySchema),
1250 Arc::new(ShardIdSchema),
1251 diagnostics,
1252 USE_CRITICAL_SINCE_CATALOG.get(persist.dyncfgs()),
1253 )
1254 .await
1255 .expect("valid usage")
1256 }
1257
1258 async fn open_migration_shard_since(
1260 &self,
1261 diagnostics: Diagnostics,
1262 ) -> SinceHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff> {
1263 self.config
1264 .persist_client
1265 .open_critical_since(
1266 self.migration_shard,
1267 PersistClient::CONTROLLER_CRITICAL_SINCE,
1270 Opaque::encode(&i64::MIN),
1271 diagnostics.clone(),
1272 )
1273 .await
1274 .expect("valid usage")
1275 }
1276
1277 fn update_fingerprints(
1282 &self,
1283 migrated_items: &BTreeSet<SystemObjectDescription>,
1284 ) -> anyhow::Result<BTreeMap<SystemObjectDescription, String>> {
1285 let mut new_fingerprints = BTreeMap::new();
1286 for (object, object_info) in &self.system_objects {
1287 let id = object_info.global_id;
1288 let builtin = object_info.builtin;
1289
1290 let fingerprint = builtin.fingerprint();
1291 if fingerprint == object_info.fingerprint {
1292 continue; }
1294
1295 let migrated = migrated_items.contains(object);
1297 let ephemeral = matches!(
1299 builtin,
1300 Builtin::Log(_) | Builtin::View(_) | Builtin::Index(_),
1301 );
1302
1303 if migrated || ephemeral {
1304 new_fingerprints.insert(object.clone(), fingerprint);
1305 } else if builtin.runtime_alterable() {
1306 assert_eq!(
1309 object_info.fingerprint, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
1310 "fingerprint mismatch for runtime-alterable builtin {object:?} ({id})",
1311 );
1312 } else {
1313 panic!(
1314 "fingerprint mismatch for builtin {builtin:?} ({id}): {} != {}",
1315 fingerprint, object_info.fingerprint,
1316 );
1317 }
1318 }
1319
1320 Ok(new_fingerprints)
1321 }
1322
1323 async fn cleanup(&self) -> anyhow::Result<(BTreeSet<ShardId>, BoxFuture<'static, ()>)> {
1340 let noop_action = async {}.boxed();
1341 let noop_result = (BTreeSet::new(), noop_action);
1342
1343 if self.config.read_only {
1344 return Ok(noop_result);
1345 }
1346
1347 let diagnostics = Diagnostics {
1348 shard_name: "builtin_migration".to_string(),
1349 handle_purpose: "builtin schema migration cleanup".into(),
1350 };
1351 let (mut persist_write, mut persist_read) =
1352 self.open_migration_shard(diagnostics.clone()).await;
1353 let mut persist_since = self.open_migration_shard_since(diagnostics.clone()).await;
1354
1355 let upper = persist_write.fetch_recent_upper().await.clone();
1356 let write_ts = *upper.as_option().expect("migration shard not sealed");
1357 let Some(read_ts) = write_ts.step_back() else {
1358 return Ok(noop_result);
1359 };
1360
1361 let pred = |key: &migration_shard::Key| key.build_version < self.target_version;
1363 let Some(stale_entries) = read_migration_shard(&mut persist_read, read_ts, pred).await
1364 else {
1365 return Ok(noop_result);
1366 };
1367
1368 debug!(
1369 ?stale_entries,
1370 "cleaning migration shard up to version {}", self.target_version,
1371 );
1372
1373 let current_shards: BTreeMap<_, _> = self
1374 .system_objects
1375 .values()
1376 .filter_map(|o| o.shard_id.map(|shard_id| (o.global_id, shard_id)))
1377 .collect();
1378
1379 let mut shards_to_finalize = BTreeSet::new();
1380 let mut retractions = Vec::new();
1381 for (key, shard_id) in stale_entries {
1382 let gid = GlobalId::System(key.global_id);
1386 if current_shards.get(&gid) != Some(&shard_id) {
1387 shards_to_finalize.insert(shard_id);
1388 }
1389
1390 retractions.push(((key, shard_id), write_ts, -1));
1391 }
1392
1393 let cleanup_action = async move {
1394 if !retractions.is_empty() {
1395 let new_upper = Antichain::from_elem(write_ts.step_forward());
1396 let result = persist_write
1397 .compare_and_append(retractions, upper, new_upper)
1398 .await
1399 .expect("valid usage");
1400 match result {
1401 Ok(()) => debug!("cleaned up migration shard"),
1402 Err(mismatch) => debug!(?mismatch, "migration shard cleanup failed"),
1403 }
1404 }
1405 }
1406 .boxed();
1407
1408 let o = persist_since.opaque().clone();
1410 let new_since = Antichain::from_elem(read_ts);
1411 let result = persist_since
1412 .maybe_compare_and_downgrade_since(&o, (&o, &new_since))
1413 .await;
1414 soft_assert_or_log!(result.is_none_or(|r| r.is_ok()), "opaque mismatch");
1415
1416 Ok((shards_to_finalize, cleanup_action))
1417 }
1418}
1419
1420async fn read_migration_shard<P>(
1426 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1427 read_ts: Timestamp,
1428 predicate: P,
1429) -> Option<Vec<(migration_shard::Key, ShardId)>>
1430where
1431 P: Fn(&migration_shard::Key) -> bool,
1432{
1433 let as_of = Antichain::from_elem(read_ts);
1434 let updates = persist_read.snapshot_and_fetch(as_of).await.ok()?;
1435
1436 assert!(
1437 updates.iter().all(|(_, _, diff)| *diff == 1),
1438 "migration shard contains invalid diffs: {updates:?}",
1439 );
1440
1441 let entries: Vec<_> = updates
1442 .into_iter()
1443 .map(|(data, _, _)| data)
1444 .filter(move |(key, _)| predicate(key))
1445 .collect();
1446
1447 (!entries.is_empty()).then_some(entries)
1448}
1449
1450#[derive(Debug, Default)]
1452struct Plan {
1453 evolve: Vec<SystemObjectDescription>,
1455 replace: Vec<SystemObjectDescription>,
1457}
1458
1459mod migration_shard {
1461 use std::fmt;
1462 use std::str::FromStr;
1463
1464 use arrow::array::{StringArray, StringBuilder};
1465 use bytes::{BufMut, Bytes};
1466 use mz_persist_types::Codec;
1467 use mz_persist_types::codec_impls::{
1468 SimpleColumnarData, SimpleColumnarDecoder, SimpleColumnarEncoder,
1469 };
1470 use mz_persist_types::columnar::Schema;
1471 use mz_persist_types::stats::NoneStats;
1472 use semver::Version;
1473 use serde::{Deserialize, Serialize};
1474
1475 #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1476 pub(super) struct Key {
1477 pub(super) global_id: u64,
1478 pub(super) build_version: Version,
1479 pub(super) deploy_generation: Option<u64>,
1483 }
1484
1485 impl fmt::Display for Key {
1486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1487 if self.deploy_generation.is_some() {
1488 let s = serde_json::to_string(self).expect("JSON serializable");
1490 f.write_str(&s)
1491 } else {
1492 write!(f, "{}-{}", self.global_id, self.build_version)
1494 }
1495 }
1496 }
1497
1498 impl FromStr for Key {
1499 type Err = String;
1500
1501 fn from_str(s: &str) -> Result<Self, String> {
1502 if let Ok(key) = serde_json::from_str(s) {
1504 return Ok(key);
1505 };
1506
1507 let parts: Vec<_> = s.splitn(2, '-').collect();
1509 let &[global_id, build_version] = parts.as_slice() else {
1510 return Err(format!("invalid Key '{s}'"));
1511 };
1512 let global_id = global_id.parse::<u64>().map_err(|e| e.to_string())?;
1513 let build_version = build_version
1514 .parse::<Version>()
1515 .map_err(|e| e.to_string())?;
1516 Ok(Key {
1517 global_id,
1518 build_version,
1519 deploy_generation: None,
1520 })
1521 }
1522 }
1523
1524 impl Default for Key {
1525 fn default() -> Self {
1526 Self {
1527 global_id: Default::default(),
1528 build_version: Version::new(0, 0, 0),
1529 deploy_generation: Some(0),
1530 }
1531 }
1532 }
1533
1534 impl Codec for Key {
1535 type Schema = KeySchema;
1536 type Storage = ();
1537
1538 fn codec_name() -> String {
1539 "TableKey".into()
1540 }
1541
1542 fn encode<B: BufMut>(&self, buf: &mut B) {
1543 buf.put(self.to_string().as_bytes())
1544 }
1545
1546 fn decode<'a>(buf: &'a [u8], _schema: &KeySchema) -> Result<Self, String> {
1547 let s = str::from_utf8(buf).map_err(|e| e.to_string())?;
1548 s.parse()
1549 }
1550
1551 fn encode_schema(_schema: &KeySchema) -> Bytes {
1552 Bytes::new()
1553 }
1554
1555 fn decode_schema(buf: &Bytes) -> Self::Schema {
1556 assert_eq!(*buf, Bytes::new());
1557 KeySchema
1558 }
1559 }
1560
1561 impl SimpleColumnarData for Key {
1562 type ArrowBuilder = StringBuilder;
1563 type ArrowColumn = StringArray;
1564
1565 fn goodbytes(builder: &Self::ArrowBuilder) -> usize {
1566 builder.values_slice().len()
1567 }
1568
1569 fn push(&self, builder: &mut Self::ArrowBuilder) {
1570 builder.append_value(&self.to_string());
1571 }
1572
1573 fn push_null(builder: &mut Self::ArrowBuilder) {
1574 builder.append_null();
1575 }
1576
1577 fn read(&mut self, idx: usize, column: &Self::ArrowColumn) {
1578 *self = column.value(idx).parse().expect("valid Key");
1579 }
1580 }
1581
1582 #[derive(Debug, PartialEq)]
1583 pub(super) struct KeySchema;
1584
1585 impl Schema<Key> for KeySchema {
1586 type ArrowColumn = StringArray;
1587 type Statistics = NoneStats;
1588 type Decoder = SimpleColumnarDecoder<Key>;
1589 type Encoder = SimpleColumnarEncoder<Key>;
1590
1591 fn encoder(&self) -> anyhow::Result<SimpleColumnarEncoder<Key>> {
1592 Ok(SimpleColumnarEncoder::default())
1593 }
1594
1595 fn decoder(&self, col: StringArray) -> anyhow::Result<SimpleColumnarDecoder<Key>> {
1596 Ok(SimpleColumnarDecoder::new(col))
1597 }
1598 }
1599}
1600
1601#[cfg(test)]
1602#[path = "builtin_schema_migration_tests.rs"]
1603mod tests;