@@ -561,6 +561,7 @@ impl Coordinator {
561561 mut create_sql,
562562 expr : raw_expr,
563563 dependencies,
564+ replacement_target,
564565 cluster_id,
565566 non_null_assertions,
566567 compaction_window,
@@ -577,6 +578,23 @@ impl Coordinator {
577578 global_lir_plan,
578579 ..
579580 } = stage;
581+
582+ // Validate the replacement target, if one is given.
583+ // TODO(alter-mv): Could we do this already in planning?
584+ if let Some ( target_id) = replacement_target {
585+ let Some ( target) = self . catalog ( ) . get_entry ( & target_id) . materialized_view ( ) else {
586+ return Err ( AdapterError :: internal (
587+ "create materialized view" ,
588+ "replacement target not a materialized view" ,
589+ ) ) ;
590+ } ;
591+
592+ // For now, we don't support schema evolution for materialized views.
593+ if & target. desc . latest ( ) != global_lir_plan. desc ( ) {
594+ return Err ( AdapterError :: Unstructured ( anyhow ! ( "incompatible schemas" ) ) ) ;
595+ }
596+ }
597+
580598 // Timestamp selection
581599 let id_bundle = dataflow_import_id_bundle ( global_lir_plan. df_desc ( ) , cluster_id) ;
582600
@@ -596,6 +614,10 @@ impl Coordinator {
596614 let ( dataflow_as_of, storage_as_of, until) =
597615 self . select_timestamps ( id_bundle, refresh_schedule. as_ref ( ) , read_holds) ?;
598616
617+ // TODO(alter-mv): If this is a replacement MV, ensure that `storage_as_of` >= the since of
618+ // the target storage collection. Otherwise, we risk that the storage controller panics
619+ // when we try to create a new storage collection backed by the same shard.
620+
599621 tracing:: info!(
600622 dataflow_as_of = ?dataflow_as_of,
601623 storage_as_of = ?storage_as_of,
@@ -647,6 +669,7 @@ impl Coordinator {
647669 collections,
648670 resolved_ids,
649671 dependencies,
672+ replacement_target,
650673 cluster_id,
651674 non_null_assertions,
652675 custom_logical_compaction_window: compaction_window,
@@ -687,17 +710,26 @@ impl Coordinator {
687710
688711 let storage_metadata = coord. catalog . state ( ) . storage_metadata ( ) ;
689712
713+ let mut collection_desc =
714+ CollectionDescription :: for_other ( output_desc, Some ( storage_as_of) ) ;
715+ let mut allow_writes = true ;
716+
717+ // If this MV is intended to replace another one, we need to start it in
718+ // read-only mode, targeting the shard of the replacement target.
719+ if let Some ( target_id) = replacement_target {
720+ let target_gid = coord. catalog . get_entry ( & target_id) . latest_global_id ( ) ;
721+ collection_desc. primary = Some ( target_gid) ;
722+ allow_writes = false ;
723+ }
724+
690725 // Announce the creation of the materialized view source.
691726 coord
692727 . controller
693728 . storage
694729 . create_collections (
695730 storage_metadata,
696731 None ,
697- vec ! [ (
698- global_id,
699- CollectionDescription :: for_other( output_desc, Some ( storage_as_of) ) ,
700- ) ] ,
732+ vec ! [ ( global_id, collection_desc) ] ,
701733 )
702734 . await
703735 . unwrap_or_terminate ( "cannot fail to append" ) ;
@@ -716,7 +748,10 @@ impl Coordinator {
716748 notice_builtin_updates_fut,
717749 )
718750 . await ;
719- coord. allow_writes ( cluster_id, global_id) ;
751+
752+ if allow_writes {
753+ coord. allow_writes ( cluster_id, global_id) ;
754+ }
720755 } )
721756 } )
722757 . await ;
0 commit comments