1use std::collections::{BTreeMap, BTreeSet};
32use std::sync::{Arc, LazyLock};
33
34use anyhow::bail;
35use futures::FutureExt;
36use futures::future::BoxFuture;
37use mz_build_info::{BuildInfo, DUMMY_BUILD_INFO};
38use mz_catalog::builtin::{
39 BUILTIN_LOOKUP, Builtin, Fingerprint, MZ_CATALOG_RAW, MZ_CATALOG_RAW_DESCRIPTION,
40 MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, MZ_STORAGE_USAGE_BY_SHARD,
41 MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
42};
43use mz_catalog::config::BuiltinItemMigrationConfig;
44use mz_catalog::durable::objects::SystemObjectUniqueIdentifier;
45use mz_catalog::durable::{SystemObjectDescription, SystemObjectMapping, Transaction};
46use mz_catalog::memory::error::{Error, ErrorKind};
47use mz_ore::soft_assert_or_log;
48use mz_persist_client::cfg::USE_CRITICAL_SINCE_CATALOG;
49use mz_persist_client::critical::{Opaque, SinceHandle};
50use mz_persist_client::read::ReadHandle;
51use mz_persist_client::schema::CaESchema;
52use mz_persist_client::write::WriteHandle;
53use mz_persist_client::{Diagnostics, PersistClient};
54use mz_persist_types::ShardId;
55use mz_persist_types::codec_impls::{ShardIdSchema, UnitSchema};
56use mz_persist_types::schema::backward_compatible;
57use mz_repr::namespaces::{MZ_CATALOG_SCHEMA, MZ_INTERNAL_SCHEMA};
58use mz_repr::{CatalogItemId, GlobalId, Timestamp};
59use mz_sql::catalog::{CatalogItemType, NameReference};
60use mz_storage_client::controller::StorageTxn;
61use mz_storage_types::StorageDiff;
62use mz_storage_types::sources::SourceData;
63use semver::Version;
64use timely::progress::Antichain;
65use tracing::{debug, info};
66
67use crate::catalog::migrate::get_migration_version;
68
69static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
85 vec![
86 MigrationStep::replacement(
87 "0.149.0",
88 CatalogItemType::Source,
89 MZ_INTERNAL_SCHEMA,
90 "mz_sink_statistics_raw",
91 ),
92 MigrationStep::replacement(
93 "0.149.0",
94 CatalogItemType::Source,
95 MZ_INTERNAL_SCHEMA,
96 "mz_source_statistics_raw",
97 ),
98 MigrationStep::evolution(
99 "0.159.0",
100 CatalogItemType::Source,
101 MZ_INTERNAL_SCHEMA,
102 "mz_cluster_replica_metrics_history",
103 ),
104 MigrationStep::replacement(
105 "26.18.0-dev.0",
106 CatalogItemType::MaterializedView,
107 MZ_CATALOG_SCHEMA,
108 "mz_databases",
109 ),
110 MigrationStep::replacement(
111 "26.19.0-dev.0",
112 CatalogItemType::MaterializedView,
113 MZ_CATALOG_SCHEMA,
114 "mz_schemas",
115 ),
116 MigrationStep::replacement(
117 "26.19.0-dev.0",
118 CatalogItemType::MaterializedView,
119 MZ_CATALOG_SCHEMA,
120 "mz_role_members",
121 ),
122 MigrationStep::replacement(
123 "26.19.0-dev.0",
124 CatalogItemType::MaterializedView,
125 MZ_INTERNAL_SCHEMA,
126 "mz_network_policies",
127 ),
128 MigrationStep::replacement(
129 "26.19.0-dev.0",
130 CatalogItemType::MaterializedView,
131 MZ_INTERNAL_SCHEMA,
132 "mz_network_policy_rules",
133 ),
134 MigrationStep::replacement(
135 "26.19.0-dev.0",
136 CatalogItemType::MaterializedView,
137 MZ_INTERNAL_SCHEMA,
138 "mz_cluster_workload_classes",
139 ),
140 MigrationStep::replacement(
141 "26.19.0-dev.0",
142 CatalogItemType::MaterializedView,
143 MZ_INTERNAL_SCHEMA,
144 "mz_internal_cluster_replicas",
145 ),
146 MigrationStep::replacement(
147 "26.19.0-dev.0",
148 CatalogItemType::MaterializedView,
149 MZ_INTERNAL_SCHEMA,
150 "mz_pending_cluster_replicas",
151 ),
152 MigrationStep::replacement(
153 "26.20.0-dev.0",
154 CatalogItemType::MaterializedView,
155 MZ_CATALOG_SCHEMA,
156 "mz_materialized_views",
157 ),
158 MigrationStep::replacement(
159 "26.22.0-dev.0",
160 CatalogItemType::MaterializedView,
161 MZ_CATALOG_SCHEMA,
162 "mz_connections",
163 ),
164 MigrationStep::replacement(
165 "26.22.0-dev.0",
166 CatalogItemType::MaterializedView,
167 MZ_CATALOG_SCHEMA,
168 "mz_secrets",
169 ),
170 MigrationStep::replacement(
171 "26.27.0-dev.0",
172 CatalogItemType::MaterializedView,
173 MZ_CATALOG_SCHEMA,
174 "mz_sources",
175 ),
176 MigrationStep::replacement(
177 "26.29.0-dev.0",
178 CatalogItemType::MaterializedView,
179 MZ_CATALOG_SCHEMA,
180 "mz_indexes",
181 ),
182 MigrationStep::replacement(
183 "26.29.0-dev.0",
184 CatalogItemType::MaterializedView,
185 MZ_CATALOG_SCHEMA,
186 "mz_roles",
187 ),
188 MigrationStep::replacement(
189 "26.29.0-dev.0",
190 CatalogItemType::MaterializedView,
191 MZ_CATALOG_SCHEMA,
192 "mz_role_parameters",
193 ),
194 MigrationStep::replacement(
199 "26.30.0-dev.0",
200 CatalogItemType::MaterializedView,
201 MZ_CATALOG_SCHEMA,
202 "mz_indexes",
203 ),
204 MigrationStep::replacement(
205 "26.30.0-dev.0",
206 CatalogItemType::MaterializedView,
207 MZ_CATALOG_SCHEMA,
208 "mz_clusters",
209 ),
210 MigrationStep::replacement(
211 "26.30.0-dev.0",
212 CatalogItemType::MaterializedView,
213 MZ_CATALOG_SCHEMA,
214 "mz_cluster_replicas",
215 ),
216 MigrationStep::replacement(
217 "26.30.0-dev.0",
218 CatalogItemType::MaterializedView,
219 MZ_INTERNAL_SCHEMA,
220 "mz_cluster_schedules",
221 ),
222 MigrationStep::replacement(
223 "26.30.0-dev.0",
224 CatalogItemType::MaterializedView,
225 MZ_CATALOG_SCHEMA,
226 "mz_default_privileges",
227 ),
228 MigrationStep::replacement(
229 "26.30.0-dev.0",
230 CatalogItemType::MaterializedView,
231 MZ_CATALOG_SCHEMA,
232 "mz_system_privileges",
233 ),
234 MigrationStep::replacement(
238 "26.31.0-dev.0",
239 CatalogItemType::MaterializedView,
240 MZ_CATALOG_SCHEMA,
241 "mz_cluster_replicas",
242 ),
243 MigrationStep::replacement(
244 "26.32.0-dev.0",
245 CatalogItemType::MaterializedView,
246 MZ_INTERNAL_SCHEMA,
247 "mz_comments",
248 ),
249 MigrationStep::replacement(
254 "26.32.0-dev.0",
255 CatalogItemType::MaterializedView,
256 MZ_CATALOG_SCHEMA,
257 "mz_indexes",
258 ),
259 MigrationStep::replacement(
260 "26.33.0-dev.0",
261 CatalogItemType::MaterializedView,
262 MZ_CATALOG_SCHEMA,
263 "mz_audit_events",
264 ),
265 MigrationStep::replacement(
277 "26.34.0-dev.0",
278 CatalogItemType::MaterializedView,
279 MZ_CATALOG_SCHEMA,
280 "mz_indexes",
281 ),
282 MigrationStep::replacement(
287 "26.34.0-dev.0",
288 CatalogItemType::MaterializedView,
289 MZ_INTERNAL_SCHEMA,
290 "mz_postgres_sources",
291 ),
292 MigrationStep::replacement(
293 "26.34.0-dev.0",
294 CatalogItemType::MaterializedView,
295 MZ_CATALOG_SCHEMA,
296 "mz_kafka_sources",
297 ),
298 MigrationStep::replacement(
302 "26.37.0-dev.0",
303 CatalogItemType::MaterializedView,
304 MZ_INTERNAL_SCHEMA,
305 "mz_postgres_source_tables",
306 ),
307 MigrationStep::replacement(
308 "26.37.0-dev.0",
309 CatalogItemType::MaterializedView,
310 MZ_INTERNAL_SCHEMA,
311 "mz_mysql_source_tables",
312 ),
313 MigrationStep::replacement(
314 "26.37.0-dev.0",
315 CatalogItemType::MaterializedView,
316 MZ_INTERNAL_SCHEMA,
317 "mz_sql_server_source_tables",
318 ),
319 MigrationStep::replacement(
320 "26.37.0-dev.0",
321 CatalogItemType::MaterializedView,
322 MZ_INTERNAL_SCHEMA,
323 "mz_kafka_source_tables",
324 ),
325 MigrationStep::replacement(
329 "26.37.0-dev.0",
330 CatalogItemType::MaterializedView,
331 MZ_CATALOG_SCHEMA,
332 "mz_kafka_connections",
333 ),
334 MigrationStep::replacement(
335 "26.37.0-dev.0",
336 CatalogItemType::MaterializedView,
337 MZ_CATALOG_SCHEMA,
338 "mz_ssh_tunnel_connections",
339 ),
340 MigrationStep::replacement(
341 "26.37.0-dev.0",
342 CatalogItemType::MaterializedView,
343 MZ_INTERNAL_SCHEMA,
344 "mz_aws_connections",
345 ),
346 MigrationStep::replacement(
347 "26.37.0-dev.0",
348 CatalogItemType::MaterializedView,
349 MZ_CATALOG_SCHEMA,
350 "mz_aws_privatelink_connections",
351 ),
352 MigrationStep::replacement(
358 "26.38.0-dev.0",
359 CatalogItemType::MaterializedView,
360 MZ_CATALOG_SCHEMA,
361 "mz_sinks",
362 ),
363 MigrationStep::replacement(
364 "26.38.0-dev.0",
365 CatalogItemType::MaterializedView,
366 MZ_CATALOG_SCHEMA,
367 "mz_kafka_sinks",
368 ),
369 MigrationStep::replacement(
370 "26.38.0-dev.0",
371 CatalogItemType::MaterializedView,
372 MZ_CATALOG_SCHEMA,
373 "mz_iceberg_sinks",
374 ),
375 MigrationStep::replacement(
378 "26.38.0-rc.2",
379 CatalogItemType::MaterializedView,
380 MZ_INTERNAL_SCHEMA,
381 "mz_cluster_reconfigurations",
382 ),
383 MigrationStep::replacement(
388 "26.39.0-dev.0",
389 CatalogItemType::MaterializedView,
390 MZ_CATALOG_SCHEMA,
391 "mz_audit_events",
392 ),
393 MigrationStep::replacement(
397 "26.39.0-dev.0",
398 CatalogItemType::MaterializedView,
399 MZ_CATALOG_SCHEMA,
400 "mz_indexes",
401 ),
402 MigrationStep::replacement(
408 "26.39.0-dev.0",
409 CatalogItemType::MaterializedView,
410 MZ_CATALOG_SCHEMA,
411 "mz_tables",
412 ),
413 MigrationStep::replacement(
414 "26.39.0-dev.0",
415 CatalogItemType::MaterializedView,
416 MZ_CATALOG_SCHEMA,
417 "mz_views",
418 ),
419 ]
420});
421
422#[derive(Clone, Debug)]
424struct MigrationStep {
425 version: Version,
427 object: SystemObjectDescription,
429 mechanism: Mechanism,
431}
432
433impl MigrationStep {
434 fn evolution(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
436 Self {
437 version: Version::parse(version).expect("valid"),
438 object: SystemObjectDescription {
439 schema_name: schema.into(),
440 object_type: type_,
441 object_name: name.into(),
442 },
443 mechanism: Mechanism::Evolution,
444 }
445 }
446
447 fn replacement(version: &str, type_: CatalogItemType, schema: &str, name: &str) -> Self {
449 Self {
450 version: Version::parse(version).expect("valid"),
451 object: SystemObjectDescription {
452 schema_name: schema.into(),
453 object_type: type_,
454 object_name: name.into(),
455 },
456 mechanism: Mechanism::Replacement,
457 }
458 }
459}
460
461#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
463#[allow(dead_code)]
464enum Mechanism {
465 Evolution,
470 Replacement,
474}
475
476pub(super) struct MigrationResult {
478 pub replaced_items: BTreeSet<CatalogItemId>,
480 pub cleanup_action: BoxFuture<'static, ()>,
482}
483
484impl Default for MigrationResult {
485 fn default() -> Self {
486 Self {
487 replaced_items: Default::default(),
488 cleanup_action: async {}.boxed(),
489 }
490 }
491}
492
493pub(super) async fn run(
499 build_info: &BuildInfo,
500 deploy_generation: u64,
501 txn: &mut Transaction<'_>,
502 config: BuiltinItemMigrationConfig,
503) -> Result<MigrationResult, Error> {
504 assert_eq!(config.read_only, txn.is_savepoint());
506
507 if *build_info == DUMMY_BUILD_INFO {
510 return Ok(MigrationResult::default());
511 }
512
513 let Some(durable_version) = get_migration_version(txn) else {
514 return Ok(MigrationResult::default());
516 };
517 let build_version = build_info.semver_version();
518
519 let collection_metadata = txn.get_collection_metadata();
520 let system_objects = txn
521 .get_system_object_mappings()
522 .map(|m| {
523 let object = m.description;
524 let global_id = m.unique_identifier.global_id;
525 let shard_id = collection_metadata.get(&global_id).copied();
526 let Some((_, builtin)) = BUILTIN_LOOKUP.get(&object) else {
527 panic!("missing builtin {object:?}");
528 };
529 let info = ObjectInfo {
530 global_id,
531 shard_id,
532 builtin,
533 fingerprint: m.unique_identifier.fingerprint,
534 };
535 (object, info)
536 })
537 .collect();
538
539 let migration_shard = txn.get_builtin_migration_shard().expect("must exist");
540
541 let migration = Migration {
542 source_version: durable_version.clone(),
543 target_version: build_version.clone(),
544 deploy_generation,
545 system_objects,
546 migration_shard,
547 config,
548 };
549
550 let result = migration.run(&MIGRATIONS).await.map_err(|e| {
551 Error::new(ErrorKind::FailedBuiltinSchemaMigration {
552 last_seen_version: durable_version.to_string(),
553 this_version: build_version.to_string(),
554 cause: e.to_string(),
555 })
556 })?;
557
558 result.apply(txn);
559
560 let replaced_items = txn
561 .get_system_object_mappings()
562 .map(|m| m.unique_identifier)
563 .filter(|ids| result.new_shards.contains_key(&ids.global_id))
564 .map(|ids| ids.catalog_id)
565 .collect();
566
567 Ok(MigrationResult {
568 replaced_items,
569 cleanup_action: result.cleanup_action,
570 })
571}
572
573struct MigrationRunResult {
575 new_shards: BTreeMap<GlobalId, ShardId>,
576 new_fingerprints: BTreeMap<SystemObjectDescription, String>,
577 shards_to_finalize: BTreeSet<ShardId>,
578 cleanup_action: BoxFuture<'static, ()>,
579}
580
581impl Default for MigrationRunResult {
582 fn default() -> Self {
583 Self {
584 new_shards: BTreeMap::new(),
585 new_fingerprints: BTreeMap::new(),
586 shards_to_finalize: BTreeSet::new(),
587 cleanup_action: async {}.boxed(),
588 }
589 }
590}
591
592impl MigrationRunResult {
593 fn apply(&self, txn: &mut Transaction<'_>) {
595 let replaced_ids = self.new_shards.keys().copied().collect();
597 let old_metadata = txn.delete_collection_metadata(replaced_ids);
598 txn.insert_collection_metadata(self.new_shards.clone())
599 .expect("inserting unique shards IDs after deleting existing entries");
600
601 let mut unfinalized_shards: BTreeSet<_> =
603 old_metadata.into_iter().map(|(_, sid)| sid).collect();
604 unfinalized_shards.extend(self.shards_to_finalize.iter().copied());
605 txn.insert_unfinalized_shards(unfinalized_shards)
606 .expect("cannot fail");
607
608 let mappings = txn
610 .get_system_object_mappings()
611 .filter_map(|m| {
612 let fingerprint = self.new_fingerprints.get(&m.description)?;
613 Some(SystemObjectMapping {
614 description: m.description,
615 unique_identifier: SystemObjectUniqueIdentifier {
616 catalog_id: m.unique_identifier.catalog_id,
617 global_id: m.unique_identifier.global_id,
618 fingerprint: fingerprint.clone(),
619 },
620 })
621 })
622 .collect();
623 txn.set_system_object_mappings(mappings)
624 .expect("filtered existing mappings remain unique");
625 }
626}
627
628#[derive(Clone, Debug)]
630struct ObjectInfo {
631 global_id: GlobalId,
632 shard_id: Option<ShardId>,
633 builtin: &'static Builtin<NameReference>,
634 fingerprint: String,
635}
636
637struct Migration {
639 source_version: Version,
644 target_version: Version,
648 deploy_generation: u64,
650 system_objects: BTreeMap<SystemObjectDescription, ObjectInfo>,
652 migration_shard: ShardId,
654 config: BuiltinItemMigrationConfig,
656}
657
658impl Migration {
659 async fn run(self, steps: &[MigrationStep]) -> anyhow::Result<MigrationRunResult> {
660 info!(
661 deploy_generation = %self.deploy_generation,
662 "running builtin schema migration: {} -> {}",
663 self.source_version, self.target_version
664 );
665
666 self.validate_migration_steps(steps);
667
668 let force_migration = if self.source_version != self.target_version
671 && self.source_version.pre.as_str().starts_with("dev")
672 && self.config.force_migration.is_none()
673 {
674 Some("evolution".to_string())
675 } else {
676 self.config.force_migration.clone()
677 };
678
679 let (force, plan) = match force_migration.as_deref() {
680 None => (false, self.plan_migration(steps)),
681 Some("evolution") => (true, self.plan_forced_migration(Mechanism::Evolution)),
682 Some("replacement") => (true, self.plan_forced_migration(Mechanism::Replacement)),
683 Some(other) => panic!("unknown force migration mechanism: {other}"),
684 };
685
686 if self.source_version == self.target_version && !force {
687 info!("skipping migration: already at target version");
688 return Ok(MigrationRunResult::default());
689 } else if self.source_version > self.target_version {
690 bail!("downgrade not supported");
691 }
692
693 if !self.config.read_only {
696 self.upgrade_migration_shard_version().await;
697 }
698
699 info!("executing migration plan: {plan:?}");
700
701 self.migrate_evolve(&plan.evolve).await?;
702 let new_shards = self.migrate_replace(&plan.replace).await?;
703
704 let mut migrated_objects = BTreeSet::new();
705 migrated_objects.extend(plan.evolve);
706 migrated_objects.extend(plan.replace);
707
708 let new_fingerprints = self.update_fingerprints(&migrated_objects)?;
709
710 let (shards_to_finalize, cleanup_action) = self.cleanup().await?;
711
712 Ok(MigrationRunResult {
713 new_shards,
714 new_fingerprints,
715 shards_to_finalize,
716 cleanup_action,
717 })
718 }
719
720 fn validate_migration_steps(&self, steps: &[MigrationStep]) {
724 for step in steps {
725 assert!(
726 step.version <= self.target_version,
727 "migration step version greater than target version: {} > {}",
728 step.version,
729 self.target_version,
730 );
731
732 let object = &step.object;
733
734 assert_ne!(
742 &*MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, object,
743 "mz_storage_usage_by_shard cannot be migrated or else the table will be truncated"
744 );
745
746 assert_ne!(
750 &*MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, object,
751 "mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
752 );
753
754 assert_ne!(
757 &*MZ_CATALOG_RAW_DESCRIPTION, object,
758 "mz_catalog_raw cannot be migrated"
759 );
760
761 let Some(object_info) = self.system_objects.get(object) else {
762 panic!("migration step for non-existent builtin: {object:?}");
763 };
764
765 let builtin = object_info.builtin;
766 use Builtin::*;
767 assert!(
768 matches!(builtin, Table(..) | Source(..) | MaterializedView(..)),
769 "schema migration not supported for builtin: {builtin:?}",
770 );
771 }
772 }
773
774 fn plan_migration(&self, steps: &[MigrationStep]) -> Plan {
776 let steps = steps.iter().filter(|s| s.version > self.source_version);
778
779 let mut by_object = BTreeMap::new();
783 for step in steps {
784 if let Some(entry) = by_object.get_mut(&step.object) {
785 *entry = match (step.mechanism, *entry) {
786 (Mechanism::Evolution, Mechanism::Evolution) => Mechanism::Evolution,
787 (Mechanism::Replacement, _) | (_, Mechanism::Replacement) => {
788 Mechanism::Replacement
789 }
790 };
791 } else {
792 by_object.insert(step.object.clone(), step.mechanism);
793 }
794 }
795
796 let mut plan = Plan::default();
797 for (object, mechanism) in by_object {
798 match mechanism {
799 Mechanism::Evolution => plan.evolve.push(object),
800 Mechanism::Replacement => plan.replace.push(object),
801 }
802 }
803
804 plan
805 }
806
807 fn plan_forced_migration(&self, mechanism: Mechanism) -> Plan {
809 let objects = self
810 .system_objects
811 .iter()
812 .filter(|(_, info)| info.shard_id.is_some())
816 .filter(|(_, info)| {
817 use Builtin::*;
818 match info.builtin {
819 Table(table) => **table != *MZ_STORAGE_USAGE_BY_SHARD,
822 MaterializedView(..) => true,
823 Source(source) => **source != *MZ_CATALOG_RAW,
824 Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,
825 }
826 })
827 .map(|(object, _)| object.clone())
828 .collect();
829
830 let mut plan = Plan::default();
831 match mechanism {
832 Mechanism::Evolution => plan.evolve = objects,
833 Mechanism::Replacement => plan.replace = objects,
834 }
835
836 plan
837 }
838
839 async fn upgrade_migration_shard_version(&self) {
841 let persist = &self.config.persist_client;
842 let diagnostics = Diagnostics {
843 shard_name: "builtin_migration".to_string(),
844 handle_purpose: format!("migration shard upgrade @ {}", self.target_version),
845 };
846
847 persist
848 .upgrade_version::<migration_shard::Key, ShardId, Timestamp, StorageDiff>(
849 self.migration_shard,
850 diagnostics,
851 )
852 .await
853 .expect("valid usage");
854 }
855
856 async fn migrate_evolve(&self, objects: &[SystemObjectDescription]) -> anyhow::Result<()> {
858 for object in objects {
859 self.migrate_evolve_one(object).await?;
860 }
861 Ok(())
862 }
863
864 async fn migrate_evolve_one(&self, object: &SystemObjectDescription) -> anyhow::Result<()> {
865 let persist = &self.config.persist_client;
866
867 let Some(object_info) = self.system_objects.get(object) else {
868 bail!("missing builtin {object:?}");
869 };
870 let id = object_info.global_id;
871
872 let Some(shard_id) = object_info.shard_id else {
873 if self.config.read_only {
878 bail!("missing shard ID for builtin {object:?} ({id})");
879 } else {
880 return Ok(());
881 }
882 };
883
884 let target_desc = match object_info.builtin {
885 Builtin::Table(table) => &table.desc,
886 Builtin::Source(source) => &source.desc,
887 Builtin::MaterializedView(mv) => &mv.desc,
888 _ => bail!("not a storage collection: {object:?}"),
889 };
890
891 let diagnostics = Diagnostics {
892 shard_name: id.to_string(),
893 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
894 };
895 let source_schema = persist
896 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(shard_id, diagnostics.clone())
897 .await
898 .expect("valid usage");
899
900 info!(?object, %id, %shard_id, ?source_schema, ?target_desc, "migrating by evolution");
901
902 if self.config.read_only {
903 if let Some((_, source_desc, _)) = &source_schema {
906 let old = mz_persist_types::columnar::data_type::<SourceData>(source_desc)?;
907 let new = mz_persist_types::columnar::data_type::<SourceData>(target_desc)?;
908 if backward_compatible(&old, &new).is_none() {
909 bail!(
910 "incompatible schema evolution for {object:?}: \
911 {source_desc:?} -> {target_desc:?}"
912 );
913 }
914 }
915
916 return Ok(());
917 }
918
919 let (mut schema_id, mut source_desc) = match source_schema {
920 Some((schema_id, source_desc, _)) => (schema_id, source_desc),
921 None => {
922 debug!(%id, %shard_id, "no previous schema found; registering initial one");
927 let schema_id = persist
928 .register_schema::<SourceData, (), Timestamp, StorageDiff>(
929 shard_id,
930 target_desc,
931 &UnitSchema,
932 diagnostics.clone(),
933 )
934 .await
935 .expect("valid usage");
936 if schema_id.is_some() {
937 return Ok(());
938 }
939
940 debug!(%id, %shard_id, "schema registration failed; falling back to CaES");
941 let (schema_id, source_desc, _) = persist
942 .latest_schema::<SourceData, (), Timestamp, StorageDiff>(
943 shard_id,
944 diagnostics.clone(),
945 )
946 .await
947 .expect("valid usage")
948 .expect("known to exist");
949
950 (schema_id, source_desc)
951 }
952 };
953
954 loop {
955 debug!(%id, %shard_id, %schema_id, ?source_desc, ?target_desc, "attempting CaES");
960 let result = persist
961 .compare_and_evolve_schema::<SourceData, (), Timestamp, StorageDiff>(
962 shard_id,
963 schema_id,
964 target_desc,
965 &UnitSchema,
966 diagnostics.clone(),
967 )
968 .await
969 .expect("valid usage");
970
971 match result {
972 CaESchema::Ok(schema_id) => {
973 debug!(%id, %shard_id, %schema_id, "schema evolved successfully");
974 break;
975 }
976 CaESchema::Incompatible => bail!(
977 "incompatible schema evolution for {object:?}: \
978 {source_desc:?} -> {target_desc:?}"
979 ),
980 CaESchema::ExpectedMismatch {
981 schema_id: new_id,
982 key,
983 val: UnitSchema,
984 } => {
985 schema_id = new_id;
986 source_desc = key;
987 }
988 }
989 }
990
991 Ok(())
992 }
993
994 async fn migrate_replace(
996 &self,
997 objects: &[SystemObjectDescription],
998 ) -> anyhow::Result<BTreeMap<GlobalId, ShardId>> {
999 if objects.is_empty() {
1000 return Ok(Default::default());
1001 }
1002
1003 let diagnostics = Diagnostics {
1004 shard_name: "builtin_migration".to_string(),
1005 handle_purpose: format!("builtin schema migration @ {}", self.target_version),
1006 };
1007 let (mut persist_write, mut persist_read) =
1008 self.open_migration_shard(diagnostics.clone()).await;
1009
1010 let mut ids_to_replace = BTreeSet::new();
1011 for object in objects {
1012 if let Some(info) = self.system_objects.get(object) {
1013 ids_to_replace.insert(info.global_id);
1014 } else {
1015 bail!("missing id for builtin {object:?}");
1016 }
1017 }
1018
1019 info!(?objects, ?ids_to_replace, "migrating by replacement");
1020
1021 let replaced_shards = loop {
1024 if let Some(shards) = self
1025 .try_get_or_insert_replacement_shards(
1026 &ids_to_replace,
1027 &mut persist_write,
1028 &mut persist_read,
1029 )
1030 .await?
1031 {
1032 break shards;
1033 }
1034 };
1035
1036 Ok(replaced_shards)
1037 }
1038
1039 async fn try_get_or_insert_replacement_shards(
1049 &self,
1050 ids_to_replace: &BTreeSet<GlobalId>,
1051 persist_write: &mut WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1052 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1053 ) -> anyhow::Result<Option<BTreeMap<GlobalId, ShardId>>> {
1054 let upper = persist_write.fetch_recent_upper().await;
1055 let write_ts = *upper.as_option().expect("migration shard not sealed");
1056
1057 let mut ids_to_replace = ids_to_replace.clone();
1058 let mut replaced_shards = BTreeMap::new();
1059
1060 if let Some(read_ts) = write_ts.step_back() {
1068 let pred = |key: &migration_shard::Key| {
1069 key.build_version == self.target_version
1070 && key.deploy_generation == Some(self.deploy_generation)
1071 };
1072 if let Some(entries) = read_migration_shard(persist_read, read_ts, pred).await {
1073 for (key, shard_id) in entries {
1074 let id = GlobalId::System(key.global_id);
1075 if ids_to_replace.remove(&id) {
1076 replaced_shards.insert(id, shard_id);
1077 }
1078 }
1079
1080 debug!(
1081 %read_ts, ?replaced_shards, ?ids_to_replace,
1082 "found existing entries in migration shard",
1083 );
1084 }
1085
1086 if ids_to_replace.is_empty() {
1087 return Ok(Some(replaced_shards));
1088 }
1089 }
1090
1091 let mut updates = Vec::new();
1095 for id in ids_to_replace {
1096 let shard_id = ShardId::new();
1097 replaced_shards.insert(id, shard_id);
1098
1099 let GlobalId::System(global_id) = id else {
1100 bail!("attempt to migrate a non-system collection: {id}");
1101 };
1102 let key = migration_shard::Key {
1103 global_id,
1104 build_version: self.target_version.clone(),
1105 deploy_generation: Some(self.deploy_generation),
1106 };
1107 updates.push(((key, shard_id), write_ts, 1));
1108 }
1109
1110 let upper = Antichain::from_elem(write_ts);
1111 let new_upper = Antichain::from_elem(write_ts.step_forward());
1112 debug!(%write_ts, "attempting insert into migration shard");
1113 let result = persist_write
1114 .compare_and_append(updates, upper, new_upper)
1115 .await
1116 .expect("valid usage");
1117
1118 match result {
1119 Ok(()) => {
1120 debug!(
1121 %write_ts, ?replaced_shards,
1122 "successfully inserted into migration shard"
1123 );
1124 Ok(Some(replaced_shards))
1125 }
1126 Err(_mismatch) => Ok(None),
1127 }
1128 }
1129
1130 async fn open_migration_shard(
1132 &self,
1133 diagnostics: Diagnostics,
1134 ) -> (
1135 WriteHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1136 ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1137 ) {
1138 let persist = &self.config.persist_client;
1139
1140 persist
1141 .open(
1142 self.migration_shard,
1143 Arc::new(migration_shard::KeySchema),
1144 Arc::new(ShardIdSchema),
1145 diagnostics,
1146 USE_CRITICAL_SINCE_CATALOG.get(persist.dyncfgs()),
1147 )
1148 .await
1149 .expect("valid usage")
1150 }
1151
1152 async fn open_migration_shard_since(
1154 &self,
1155 diagnostics: Diagnostics,
1156 ) -> SinceHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff> {
1157 self.config
1158 .persist_client
1159 .open_critical_since(
1160 self.migration_shard,
1161 PersistClient::CONTROLLER_CRITICAL_SINCE,
1164 Opaque::encode(&i64::MIN),
1165 diagnostics.clone(),
1166 )
1167 .await
1168 .expect("valid usage")
1169 }
1170
1171 fn update_fingerprints(
1176 &self,
1177 migrated_items: &BTreeSet<SystemObjectDescription>,
1178 ) -> anyhow::Result<BTreeMap<SystemObjectDescription, String>> {
1179 let mut new_fingerprints = BTreeMap::new();
1180 for (object, object_info) in &self.system_objects {
1181 let id = object_info.global_id;
1182 let builtin = object_info.builtin;
1183
1184 let fingerprint = builtin.fingerprint();
1185 if fingerprint == object_info.fingerprint {
1186 continue; }
1188
1189 let migrated = migrated_items.contains(object);
1191 let ephemeral = matches!(
1193 builtin,
1194 Builtin::Log(_) | Builtin::View(_) | Builtin::Index(_),
1195 );
1196
1197 if migrated || ephemeral {
1198 new_fingerprints.insert(object.clone(), fingerprint);
1199 } else if builtin.runtime_alterable() {
1200 assert_eq!(
1203 object_info.fingerprint, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
1204 "fingerprint mismatch for runtime-alterable builtin {object:?} ({id})",
1205 );
1206 } else {
1207 panic!(
1208 "fingerprint mismatch for builtin {builtin:?} ({id}): {} != {}",
1209 fingerprint, object_info.fingerprint,
1210 );
1211 }
1212 }
1213
1214 Ok(new_fingerprints)
1215 }
1216
1217 async fn cleanup(&self) -> anyhow::Result<(BTreeSet<ShardId>, BoxFuture<'static, ()>)> {
1234 let noop_action = async {}.boxed();
1235 let noop_result = (BTreeSet::new(), noop_action);
1236
1237 if self.config.read_only {
1238 return Ok(noop_result);
1239 }
1240
1241 let diagnostics = Diagnostics {
1242 shard_name: "builtin_migration".to_string(),
1243 handle_purpose: "builtin schema migration cleanup".into(),
1244 };
1245 let (mut persist_write, mut persist_read) =
1246 self.open_migration_shard(diagnostics.clone()).await;
1247 let mut persist_since = self.open_migration_shard_since(diagnostics.clone()).await;
1248
1249 let upper = persist_write.fetch_recent_upper().await.clone();
1250 let write_ts = *upper.as_option().expect("migration shard not sealed");
1251 let Some(read_ts) = write_ts.step_back() else {
1252 return Ok(noop_result);
1253 };
1254
1255 let pred = |key: &migration_shard::Key| key.build_version < self.target_version;
1257 let Some(stale_entries) = read_migration_shard(&mut persist_read, read_ts, pred).await
1258 else {
1259 return Ok(noop_result);
1260 };
1261
1262 debug!(
1263 ?stale_entries,
1264 "cleaning migration shard up to version {}", self.target_version,
1265 );
1266
1267 let current_shards: BTreeMap<_, _> = self
1268 .system_objects
1269 .values()
1270 .filter_map(|o| o.shard_id.map(|shard_id| (o.global_id, shard_id)))
1271 .collect();
1272
1273 let mut shards_to_finalize = BTreeSet::new();
1274 let mut retractions = Vec::new();
1275 for (key, shard_id) in stale_entries {
1276 let gid = GlobalId::System(key.global_id);
1280 if current_shards.get(&gid) != Some(&shard_id) {
1281 shards_to_finalize.insert(shard_id);
1282 }
1283
1284 retractions.push(((key, shard_id), write_ts, -1));
1285 }
1286
1287 let cleanup_action = async move {
1288 if !retractions.is_empty() {
1289 let new_upper = Antichain::from_elem(write_ts.step_forward());
1290 let result = persist_write
1291 .compare_and_append(retractions, upper, new_upper)
1292 .await
1293 .expect("valid usage");
1294 match result {
1295 Ok(()) => debug!("cleaned up migration shard"),
1296 Err(mismatch) => debug!(?mismatch, "migration shard cleanup failed"),
1297 }
1298 }
1299 }
1300 .boxed();
1301
1302 let o = persist_since.opaque().clone();
1304 let new_since = Antichain::from_elem(read_ts);
1305 let result = persist_since
1306 .maybe_compare_and_downgrade_since(&o, (&o, &new_since))
1307 .await;
1308 soft_assert_or_log!(result.is_none_or(|r| r.is_ok()), "opaque mismatch");
1309
1310 Ok((shards_to_finalize, cleanup_action))
1311 }
1312}
1313
1314async fn read_migration_shard<P>(
1320 persist_read: &mut ReadHandle<migration_shard::Key, ShardId, Timestamp, StorageDiff>,
1321 read_ts: Timestamp,
1322 predicate: P,
1323) -> Option<Vec<(migration_shard::Key, ShardId)>>
1324where
1325 P: for<'a> Fn(&migration_shard::Key) -> bool,
1326{
1327 let as_of = Antichain::from_elem(read_ts);
1328 let updates = persist_read.snapshot_and_fetch(as_of).await.ok()?;
1329
1330 assert!(
1331 updates.iter().all(|(_, _, diff)| *diff == 1),
1332 "migration shard contains invalid diffs: {updates:?}",
1333 );
1334
1335 let entries: Vec<_> = updates
1336 .into_iter()
1337 .map(|(data, _, _)| data)
1338 .filter(move |(key, _)| predicate(key))
1339 .collect();
1340
1341 (!entries.is_empty()).then_some(entries)
1342}
1343
1344#[derive(Debug, Default)]
1346struct Plan {
1347 evolve: Vec<SystemObjectDescription>,
1349 replace: Vec<SystemObjectDescription>,
1351}
1352
1353mod migration_shard {
1355 use std::fmt;
1356 use std::str::FromStr;
1357
1358 use arrow::array::{StringArray, StringBuilder};
1359 use bytes::{BufMut, Bytes};
1360 use mz_persist_types::Codec;
1361 use mz_persist_types::codec_impls::{
1362 SimpleColumnarData, SimpleColumnarDecoder, SimpleColumnarEncoder,
1363 };
1364 use mz_persist_types::columnar::Schema;
1365 use mz_persist_types::stats::NoneStats;
1366 use semver::Version;
1367 use serde::{Deserialize, Serialize};
1368
1369 #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
1370 pub(super) struct Key {
1371 pub(super) global_id: u64,
1372 pub(super) build_version: Version,
1373 pub(super) deploy_generation: Option<u64>,
1377 }
1378
1379 impl fmt::Display for Key {
1380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1381 if self.deploy_generation.is_some() {
1382 let s = serde_json::to_string(self).expect("JSON serializable");
1384 f.write_str(&s)
1385 } else {
1386 write!(f, "{}-{}", self.global_id, self.build_version)
1388 }
1389 }
1390 }
1391
1392 impl FromStr for Key {
1393 type Err = String;
1394
1395 fn from_str(s: &str) -> Result<Self, String> {
1396 if let Ok(key) = serde_json::from_str(s) {
1398 return Ok(key);
1399 };
1400
1401 let parts: Vec<_> = s.splitn(2, '-').collect();
1403 let &[global_id, build_version] = parts.as_slice() else {
1404 return Err(format!("invalid Key '{s}'"));
1405 };
1406 let global_id = global_id.parse::<u64>().map_err(|e| e.to_string())?;
1407 let build_version = build_version
1408 .parse::<Version>()
1409 .map_err(|e| e.to_string())?;
1410 Ok(Key {
1411 global_id,
1412 build_version,
1413 deploy_generation: None,
1414 })
1415 }
1416 }
1417
1418 impl Default for Key {
1419 fn default() -> Self {
1420 Self {
1421 global_id: Default::default(),
1422 build_version: Version::new(0, 0, 0),
1423 deploy_generation: Some(0),
1424 }
1425 }
1426 }
1427
1428 impl Codec for Key {
1429 type Schema = KeySchema;
1430 type Storage = ();
1431
1432 fn codec_name() -> String {
1433 "TableKey".into()
1434 }
1435
1436 fn encode<B: BufMut>(&self, buf: &mut B) {
1437 buf.put(self.to_string().as_bytes())
1438 }
1439
1440 fn decode<'a>(buf: &'a [u8], _schema: &KeySchema) -> Result<Self, String> {
1441 let s = str::from_utf8(buf).map_err(|e| e.to_string())?;
1442 s.parse()
1443 }
1444
1445 fn encode_schema(_schema: &KeySchema) -> Bytes {
1446 Bytes::new()
1447 }
1448
1449 fn decode_schema(buf: &Bytes) -> Self::Schema {
1450 assert_eq!(*buf, Bytes::new());
1451 KeySchema
1452 }
1453 }
1454
1455 impl SimpleColumnarData for Key {
1456 type ArrowBuilder = StringBuilder;
1457 type ArrowColumn = StringArray;
1458
1459 fn goodbytes(builder: &Self::ArrowBuilder) -> usize {
1460 builder.values_slice().len()
1461 }
1462
1463 fn push(&self, builder: &mut Self::ArrowBuilder) {
1464 builder.append_value(&self.to_string());
1465 }
1466
1467 fn push_null(builder: &mut Self::ArrowBuilder) {
1468 builder.append_null();
1469 }
1470
1471 fn read(&mut self, idx: usize, column: &Self::ArrowColumn) {
1472 *self = column.value(idx).parse().expect("valid Key");
1473 }
1474 }
1475
1476 #[derive(Debug, PartialEq)]
1477 pub(super) struct KeySchema;
1478
1479 impl Schema<Key> for KeySchema {
1480 type ArrowColumn = StringArray;
1481 type Statistics = NoneStats;
1482 type Decoder = SimpleColumnarDecoder<Key>;
1483 type Encoder = SimpleColumnarEncoder<Key>;
1484
1485 fn encoder(&self) -> anyhow::Result<SimpleColumnarEncoder<Key>> {
1486 Ok(SimpleColumnarEncoder::default())
1487 }
1488
1489 fn decoder(&self, col: StringArray) -> anyhow::Result<SimpleColumnarDecoder<Key>> {
1490 Ok(SimpleColumnarDecoder::new(col))
1491 }
1492 }
1493}
1494
1495#[cfg(test)]
1496#[path = "builtin_schema_migration_tests.rs"]
1497mod tests;