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