From 2b7dee622a75e4612d06dc738e8d33583f8117ff Mon Sep 17 00:00:00 2001 From: Jan Teske Date: Tue, 16 Dec 2025 11:29:20 +0100 Subject: [PATCH] sql: disallow replacement MV depending on its target This commit adds an additional check in materialzied view planning, to reject replacement MVs whose queries depend on the replacement target. We can't allow those because after applying the replacement we would end up with an object that depends on itself. --- src/sql/src/plan/statement/ddl.rs | 57 ++++++++++--------- .../replacement-materialized-views.slt | 3 + 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/sql/src/plan/statement/ddl.rs b/src/sql/src/plan/statement/ddl.rs index 8be8f0599028f..114257c5de5fe 100644 --- a/src/sql/src/plan/statement/ddl.rs +++ b/src/sql/src/plan/statement/ddl.rs @@ -2733,30 +2733,6 @@ pub fn plan_create_materialized_view( let partial_name = normalize::unresolved_item_name(stmt.name)?; let name = scx.allocate_qualified_name(partial_name.clone())?; - // Validate the replacement target, if one is given. - let replacement_target = if let Some(target_name) = &stmt.replacing { - scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?; - - let target = scx.get_item_by_resolved_name(target_name)?; - if target.item_type() != CatalogItemType::MaterializedView { - return Err(PlanError::InvalidReplacement { - item_type: target.item_type(), - item_name: scx.catalog.minimal_qualification(target.name()), - replacement_type: CatalogItemType::MaterializedView, - replacement_name: partial_name, - }); - } - if target.id().is_system() { - sql_bail!( - "cannot replace {} because it is required by the database system", - scx.catalog.minimal_qualification(target.name()), - ); - } - Some(target.id()) - } else { - None - }; - let query::PlannedRootQuery { expr, mut desc, @@ -2953,7 +2929,7 @@ pub fn plan_create_materialized_view( scx, ObjectType::MaterializedView, if_exists, - partial_name.into(), + partial_name.clone().into(), cascade, )?; @@ -2992,8 +2968,35 @@ pub fn plan_create_materialized_view( .map(|gid| scx.catalog.resolve_item_id(&gid)) .collect(); - if let Some(id) = replacement_target { - dependencies.insert(id); + // Validate the replacement target, if one is given. + let mut replacement_target = None; + if let Some(target_name) = &stmt.replacing { + scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?; + + let target = scx.get_item_by_resolved_name(target_name)?; + if target.item_type() != CatalogItemType::MaterializedView { + return Err(PlanError::InvalidReplacement { + item_type: target.item_type(), + item_name: scx.catalog.minimal_qualification(target.name()), + replacement_type: CatalogItemType::MaterializedView, + replacement_name: partial_name, + }); + } + if target.id().is_system() { + sql_bail!( + "cannot replace {} because it is required by the database system", + scx.catalog.minimal_qualification(target.name()), + ); + } + + if !dependencies.insert(target.id()) { + sql_bail!( + "cannot replace {} because it is also a dependency", + scx.catalog.minimal_qualification(target.name()), + ); + } + + replacement_target = Some(target.id()); } // Check for an object in the catalog with this same name diff --git a/test/sqllogictest/replacement-materialized-views.slt b/test/sqllogictest/replacement-materialized-views.slt index 00689b65501b6..05aacff7f33c5 100644 --- a/test/sqllogictest/replacement-materialized-views.slt +++ b/test/sqllogictest/replacement-materialized-views.slt @@ -158,6 +158,9 @@ CREATE MATERIALIZED VIEW rp REPLACING t AS SELECT a, b FROM t statement error db error: ERROR: t is a table not a materialized view ALTER MATERIALIZED VIEW mv APPLY REPLACEMENT t +statement error db error: ERROR: cannot replace mv because it is also a dependency +CREATE MATERIALIZED VIEW rp REPLACING mv AS SELECT * FROM mv + statement ok CREATE MATERIALIZED VIEW mv2 AS SELECT * FROM mv