Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/mz-deploy/src/project/resolve/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,38 @@ 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<Raw>],
staging_suffix: &str,
) {
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"));
}
}
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");
}
}
}

Expand Down
68 changes: 68 additions & 0 deletions src/mz-deploy/src/project/resolve/normalize/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2509,3 +2509,71 @@ 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");
}

#[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"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Loading