From 7433be75be5ca75b878eddce2eb12ad0ef8e52ed Mon Sep 17 00:00:00 2001 From: Seth Wiesman Date: Tue, 30 Jun 2026 11:29:09 -0500 Subject: [PATCH 1/2] mz-deploy: suffix staging cluster names with the raw identifier value Both the external-index and deployed-index staging rewrites composed the suffixed cluster name with `format!("{}{}", ident, suffix)`, which uses the identifier's quoted `Display` form. A cluster whose name is not bare (e.g. `prod-cluster`) became `"prod-cluster"_staging`, with the quotes embedded in the name, so the staged `IN CLUSTER` pointed at a cluster that was never created. Compose with the raw `as_str()` value instead. Ticket: DEX-61 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/project/resolve/normalize.rs | 2 +- .../src/project/resolve/normalize/tests.rs | 43 +++++++++++++++++++ .../project/resolve/normalize/transformers.rs | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/mz-deploy/src/project/resolve/normalize.rs b/src/mz-deploy/src/project/resolve/normalize.rs index afc321fb55690..f53ef99dc213e 100644 --- a/src/mz-deploy/src/project/resolve/normalize.rs +++ b/src/mz-deploy/src/project/resolve/normalize.rs @@ -71,7 +71,7 @@ pub(crate) fn transform_cluster_names_for_staging( for index in indexes { if let Some(ref mut cluster_name) = index.in_cluster { if let RawClusterName::Unresolved(ident) = cluster_name { - let new_name = format!("{}{}", ident, staging_suffix); + let new_name = format!("{}{}", ident.as_str(), staging_suffix); *cluster_name = RawClusterName::Unresolved(Ident::new(&new_name).expect("valid cluster name")); } diff --git a/src/mz-deploy/src/project/resolve/normalize/tests.rs b/src/mz-deploy/src/project/resolve/normalize/tests.rs index ca52b815f1c22..5317efbce09e9 100644 --- a/src/mz-deploy/src/project/resolve/normalize/tests.rs +++ b/src/mz-deploy/src/project/resolve/normalize/tests.rs @@ -2509,3 +2509,46 @@ fn test_system_schema_2part_not_qualified() { panic!("Expected CreateView statement"); } } + +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux` +#[mz_ore::test] +fn test_staging_external_index_cluster_name_uses_raw_value() { + let stmts = parse_statements(vec![ + "CREATE INDEX my_idx IN CLUSTER \"prod-cluster\" ON my_view (id)", + ]) + .expect("valid SQL"); + let mut indexes: Vec<_> = stmts + .into_iter() + .map(|s| match s { + Statement::CreateIndex(i) => i, + _ => panic!("expected CREATE INDEX"), + }) + .collect(); + + transform_cluster_names_for_staging(&mut indexes, "_staging"); + + match indexes[0].in_cluster.as_ref().expect("index has a cluster") { + RawClusterName::Unresolved(ident) => { + assert_eq!(ident.as_str(), "prod-cluster_staging"); + } + RawClusterName::Resolved(_) => panic!("expected unresolved cluster name"), + } +} + +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux` +#[mz_ore::test] +fn test_staging_deployed_index_cluster_name_uses_raw_value() { + let fqn = staging_test_fqn(); + let external_deps = BTreeSet::new(); + let replacement_objects = BTreeSet::new(); + let transformer = transformers::StagingTransformer::new( + &fqn, + "_staging".to_string(), + &external_deps, + None, + &replacement_objects, + ); + + let staged = transformer.transform_cluster(&Ident::new_unchecked("prod-cluster")); + assert_eq!(staged.as_str(), "prod-cluster_staging"); +} diff --git a/src/mz-deploy/src/project/resolve/normalize/transformers.rs b/src/mz-deploy/src/project/resolve/normalize/transformers.rs index 73ac9e7eba4cd..712ecdf74eca9 100644 --- a/src/mz-deploy/src/project/resolve/normalize/transformers.rs +++ b/src/mz-deploy/src/project/resolve/normalize/transformers.rs @@ -361,7 +361,7 @@ pub trait ClusterTransformer: NameTransformer { impl<'a> ClusterTransformer for StagingTransformer<'a> { fn transform_cluster(&self, cluster_name: &Ident) -> Ident { // Transform: quickstart → quickstart_staging - let staging_name = format!("{}{}", cluster_name, self.staging_suffix); + let staging_name = format!("{}{}", cluster_name.as_str(), self.staging_suffix); Ident::new(&staging_name).expect("valid cluster identifier") } From 02dfa7ff09fca0eb9c37793e11e60d1860ff246d Mon Sep 17 00:00:00 2001 From: Seth Wiesman Date: Tue, 30 Jun 2026 11:47:40 -0500 Subject: [PATCH 2/2] mz-deploy: rename recreated external indexes for staging When an incremental stage recreates an index belonging to an unchanged object onto a staged cluster, the index kept its production name and target. Materialize derives a named index's schema from its target relation, so the recreated index landed in the production schema and collided with the existing index of the same name, failing the stage. Suffix the index's own name so it no longer collides; the optimizer matches indexes by cluster and structure, not name. Ticket: DEX-39 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/project/resolve/normalize.rs | 28 +++++++++++-------- .../src/project/resolve/normalize/tests.rs | 25 +++++++++++++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/mz-deploy/src/project/resolve/normalize.rs b/src/mz-deploy/src/project/resolve/normalize.rs index f53ef99dc213e..6737044f98028 100644 --- a/src/mz-deploy/src/project/resolve/normalize.rs +++ b/src/mz-deploy/src/project/resolve/normalize.rs @@ -48,22 +48,22 @@ pub(crate) use visitor::NormalizingVisitor; use mz_sql_parser::ast::{CreateIndexStatement, Ident, Raw, RawClusterName}; -/// Transform cluster names in index statements for staging environments. +/// Move external indexes onto the staging cluster for staging environments. /// -/// This is a standalone function that transforms cluster references without -/// needing a full `NormalizingVisitor`. Use this when you only need to rename -/// clusters (e.g., `quickstart` -> `quickstart_staging`) without transforming -/// object names. +/// Used for indexes belonging to objects that are not being redeployed but whose +/// cluster is staged. Each index's `IN CLUSTER` is suffixed onto the staging +/// cluster, and the index's own name is suffixed too. +/// +/// The name suffix matters: the index still targets the production relation +/// (its `on_name` is unchanged), and Materialize derives a named index's schema +/// from that target. Without renaming, the recreated index would collide with +/// the production index of the same name in the same schema. The optimizer +/// selects indexes by cluster and structure, not by name, so the renamed index +/// still serves the staged objects. /// /// # Arguments /// * `indexes` - Slice of index statements to transform in place -/// * `staging_suffix` - The suffix to append to cluster names (e.g., "_staging") -/// -/// # Example -/// ```rust,ignore -/// transform_cluster_names_for_staging(&mut indexes, "_staging"); -/// // Transforms: IN CLUSTER quickstart -> IN CLUSTER quickstart_staging -/// ``` +/// * `staging_suffix` - The suffix to append (e.g., "_staging") pub(crate) fn transform_cluster_names_for_staging( indexes: &mut [CreateIndexStatement], staging_suffix: &str, @@ -76,6 +76,10 @@ pub(crate) fn transform_cluster_names_for_staging( RawClusterName::Unresolved(Ident::new(&new_name).expect("valid cluster name")); } } + if let Some(ref mut name) = index.name { + let new_name = format!("{}{}", name.as_str(), staging_suffix); + *name = Ident::new(&new_name).expect("valid index name"); + } } } diff --git a/src/mz-deploy/src/project/resolve/normalize/tests.rs b/src/mz-deploy/src/project/resolve/normalize/tests.rs index 5317efbce09e9..5d165d5000bc3 100644 --- a/src/mz-deploy/src/project/resolve/normalize/tests.rs +++ b/src/mz-deploy/src/project/resolve/normalize/tests.rs @@ -2552,3 +2552,28 @@ fn test_staging_deployed_index_cluster_name_uses_raw_value() { let staged = transformer.transform_cluster(&Ident::new_unchecked("prod-cluster")); assert_eq!(staged.as_str(), "prod-cluster_staging"); } + +#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux` +#[mz_ore::test] +fn test_staging_external_index_name_is_suffixed() { + let stmts = parse_statements(vec![ + "CREATE INDEX my_idx IN CLUSTER compute ON my_view (id)", + ]) + .expect("valid SQL"); + let mut indexes: Vec<_> = stmts + .into_iter() + .map(|s| match s { + Statement::CreateIndex(i) => i, + _ => panic!("expected CREATE INDEX"), + }) + .collect(); + + transform_cluster_names_for_staging(&mut indexes, "_staging"); + + // The index name must be suffixed so the recreated external index does not + // collide with the production index of the same name. + assert_eq!( + indexes[0].name.as_ref().expect("named index").as_str(), + "my_idx_staging" + ); +}