You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by sz...@apache.org on 2026/10/19 23:10:52 UTC
(iceberg) branch main updated: Spark 3.5, 4.0, 4.1: Fix view rename target namespace (#18432)
This is an automated email from the ASF dual-hosted git repository.
szehon-ho pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new ddbd99073c Spark 3.5, 4.0, 4.1: Fix view rename target namespace (#18432)
ddbd99073c is described below
commit ddbd99073c9d2d1d8d4fe32e1cfccee7b2dd2151
Author: Manu Zhang <Ow...@gmail.com>
AuthorDate: Tue Oct 20 07:10:45 2026 +0800
Spark 3.5, 4.0, 4.1: Fix view rename target namespace (#18432)
---
.../v2/ExtendedDataSourceV2Strategy.scala | 25 ++++++++++++++-----
.../datasources/v2/RenameV2ViewExec.scala | 8 ++++++-
.../apache/iceberg/spark/extensions/TestViews.java | 28 ++++++++++++++++++++++
.../java/org/apache/iceberg/spark/Spark3Util.java | 3 +++
.../v2/ExtendedDataSourceV2Strategy.scala | 25 ++++++++++++++-----
.../datasources/v2/RenameV2ViewExec.scala | 8 ++++++-
.../apache/iceberg/spark/extensions/TestViews.java | 28 ++++++++++++++++++++++
.../java/org/apache/iceberg/spark/Spark3Util.java | 3 +++
.../v2/ExtendedDataSourceV2Strategy.scala | 25 ++++++++++++++-----
.../datasources/v2/RenameV2ViewExec.scala | 8 ++++++-
.../apache/iceberg/spark/extensions/TestViews.java | 28 ++++++++++++++++++++++
.../java/org/apache/iceberg/spark/Spark3Util.java | 3 +++
12 files changed, 171 insertions(+), 21 deletions(-)
diff --git a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
index 6b340b7249..9c07b3abee 100644
--- a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
+++ b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
@@ -127,12 +127,8 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
OrderAwareCoalesceExec(numPartitions, coalescer, planLater(child)) :: Nil
case RenameTable(ResolvedV2View(oldCatalog: ViewCatalog, oldIdent), newName, isView @ true) =>
- val newIdent = Spark3Util.catalogAndIdentifier(spark, newName.toList.asJava)
- if (oldCatalog.name != newIdent.catalog().name()) {
- throw new AnalysisException(
- s"Cannot move view between catalogs: from=${oldCatalog.name} and to=${newIdent.catalog().name()}")
- }
- RenameV2ViewExec(oldCatalog, oldIdent, newIdent.identifier()) :: Nil
+ val targetIdent = resolveViewRenameTarget(oldCatalog, newName)
+ RenameV2ViewExec(oldCatalog, oldIdent, targetIdent) :: Nil
case DropIcebergView(ResolvedIdentifier(viewCatalog: ViewCatalog, ident), ifExists) =>
DropV2ViewExec(viewCatalog, ident, ifExists) :: Nil
@@ -192,6 +188,23 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
new GenericInternalRow(values)
}
+ private def resolveViewRenameTarget(
+ sourceCatalog: ViewCatalog,
+ targetName: Seq[String]): Identifier = {
+ if (targetName.length == 1) {
+ // An unqualified target keeps the source view's namespace
+ Identifier.of(Array.empty[String], targetName.head)
+ } else {
+ val target = Spark3Util.catalogAndIdentifier(spark, targetName.toList.asJava, sourceCatalog)
+ if (sourceCatalog.name != target.catalog().name()) {
+ throw new AnalysisException(
+ s"Cannot move view between catalogs: from=${sourceCatalog.name} and to=${target.catalog().name()}")
+ }
+
+ target.identifier()
+ }
+ }
+
private object IcebergCatalogAndIdentifier {
def unapply(identifier: Seq[String]): Option[(TableCatalog, Identifier)] = {
val catalogAndIdentifier = Spark3Util.catalogAndIdentifier(spark, identifier.asJava)
diff --git a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
index 5dada1cab0..bb2c62c387 100644
--- a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
+++ b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
@@ -29,7 +29,13 @@ case class RenameV2ViewExec(catalog: ViewCatalog, oldIdent: Identifier, newIdent
override lazy val output: Seq[Attribute] = Nil
override protected def run(): Seq[InternalRow] = {
- catalog.renameView(oldIdent, newIdent)
+ val qualifiedNewIdent = if (newIdent.namespace.isEmpty) {
+ Identifier.of(oldIdent.namespace, newIdent.name)
+ } else {
+ newIdent
+ }
+
+ catalog.renameView(oldIdent, qualifiedNewIdent)
Seq.empty
}
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
index 9ea90bed1f..59eb4b712a 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
@@ -777,6 +777,34 @@ public class TestViews extends ExtensionsTestBase {
.containsExactlyInAnyOrderElementsOf(expected);
}
+ @TestTemplate
+ public void renameQualifiedViewToUnqualifiedTargetKeepsSourceNamespace() {
+ Namespace sourceNamespace = Namespace.of(viewName("rename_source_ns"));
+ Namespace currentNamespace = Namespace.of(viewName("rename_current_ns"));
+ String viewName = viewName("originalView");
+ String renamedView = viewName("renamedView");
+ String sql = String.format("SELECT id FROM %s.%s.%s", catalogName, NAMESPACE, tableName);
+
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, sourceNamespace);
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, currentNamespace);
+
+ ViewCatalog viewCatalog = viewCatalog();
+ viewCatalog
+ .buildView(TableIdentifier.of(sourceNamespace, viewName))
+ .withQuery("spark", sql)
+ .withDefaultNamespace(sourceNamespace)
+ .withDefaultCatalog(catalogName)
+ .withSchema(schema(sql))
+ .create();
+
+ sql("USE %s.%s", catalogName, currentNamespace);
+ sql("ALTER VIEW %s.%s RENAME TO %s", sourceNamespace, viewName, renamedView);
+
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, viewName))).isFalse();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, renamedView))).isTrue();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(currentNamespace, renamedView))).isFalse();
+ }
+
@TestTemplate
public void renameViewHiddenByTempView() throws NoSuchTableException {
insertRows(10);
diff --git a/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java b/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
index 2fd75e6a57..00189f45ce 100644
--- a/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
+++ b/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
@@ -787,6 +787,9 @@ public class Spark3Util {
* A modified version of Spark's LookupCatalog.CatalogAndIdentifier.unapply Attempts to find the
* catalog and identifier a multipart identifier represents
*
+ * <p>A single-part name is resolved in the session's current namespace, or in the default
+ * namespace of {@code defaultCatalog} if it is not the current catalog.
+ *
* @param spark Spark session to use for resolution
* @param nameParts Multipart identifier representing a table
* @param defaultCatalog Catalog to use if none is specified
diff --git a/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala b/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
index da540f5891..4d216bd97a 100644
--- a/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
+++ b/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
@@ -119,12 +119,8 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
OrderAwareCoalesceExec(numPartitions, coalescer, planLater(child)) :: Nil
case RenameTable(ResolvedV2View(oldCatalog: ViewCatalog, oldIdent), newName, isView @ true) =>
- val newIdent = Spark3Util.catalogAndIdentifier(spark, newName.toList.asJava)
- if (oldCatalog.name != newIdent.catalog().name()) {
- throw new IcebergAnalysisException(
- s"Cannot move view between catalogs: from=${oldCatalog.name} and to=${newIdent.catalog().name()}")
- }
- RenameV2ViewExec(oldCatalog, oldIdent, newIdent.identifier()) :: Nil
+ val targetIdent = resolveViewRenameTarget(oldCatalog, newName)
+ RenameV2ViewExec(oldCatalog, oldIdent, targetIdent) :: Nil
case DropIcebergView(ResolvedIdentifier(viewCatalog: ViewCatalog, ident), ifExists) =>
DropV2ViewExec(viewCatalog, ident, ifExists) :: Nil
@@ -176,6 +172,23 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
case _ => Nil
}
+ private def resolveViewRenameTarget(
+ sourceCatalog: ViewCatalog,
+ targetName: Seq[String]): Identifier = {
+ if (targetName.length == 1) {
+ // An unqualified target keeps the source view's namespace
+ Identifier.of(Array.empty[String], targetName.head)
+ } else {
+ val target = Spark3Util.catalogAndIdentifier(spark, targetName.toList.asJava, sourceCatalog)
+ if (sourceCatalog.name != target.catalog().name()) {
+ throw new IcebergAnalysisException(
+ s"Cannot move view between catalogs: from=${sourceCatalog.name} and to=${target.catalog().name()}")
+ }
+
+ target.identifier()
+ }
+ }
+
private object IcebergCatalogAndIdentifier {
def unapply(identifier: Seq[String]): Option[(TableCatalog, Identifier)] = {
val catalogAndIdentifier = Spark3Util.catalogAndIdentifier(spark, identifier.asJava)
diff --git a/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala b/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
index 5dada1cab0..bb2c62c387 100644
--- a/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
+++ b/spark/v4.0/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
@@ -29,7 +29,13 @@ case class RenameV2ViewExec(catalog: ViewCatalog, oldIdent: Identifier, newIdent
override lazy val output: Seq[Attribute] = Nil
override protected def run(): Seq[InternalRow] = {
- catalog.renameView(oldIdent, newIdent)
+ val qualifiedNewIdent = if (newIdent.namespace.isEmpty) {
+ Identifier.of(oldIdent.namespace, newIdent.name)
+ } else {
+ newIdent
+ }
+
+ catalog.renameView(oldIdent, qualifiedNewIdent)
Seq.empty
}
diff --git a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
index 640ea424b3..c72ef29b45 100644
--- a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
+++ b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
@@ -776,6 +776,34 @@ public class TestViews extends ExtensionsTestBase {
.containsExactlyInAnyOrderElementsOf(expected);
}
+ @TestTemplate
+ public void renameQualifiedViewToUnqualifiedTargetKeepsSourceNamespace() {
+ Namespace sourceNamespace = Namespace.of(viewName("rename_source_ns"));
+ Namespace currentNamespace = Namespace.of(viewName("rename_current_ns"));
+ String viewName = viewName("originalView");
+ String renamedView = viewName("renamedView");
+ String sql = String.format("SELECT id FROM %s.%s.%s", catalogName, NAMESPACE, tableName);
+
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, sourceNamespace);
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, currentNamespace);
+
+ ViewCatalog viewCatalog = viewCatalog();
+ viewCatalog
+ .buildView(TableIdentifier.of(sourceNamespace, viewName))
+ .withQuery("spark", sql)
+ .withDefaultNamespace(sourceNamespace)
+ .withDefaultCatalog(catalogName)
+ .withSchema(schema(sql))
+ .create();
+
+ sql("USE %s.%s", catalogName, currentNamespace);
+ sql("ALTER VIEW %s.%s RENAME TO %s", sourceNamespace, viewName, renamedView);
+
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, viewName))).isFalse();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, renamedView))).isTrue();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(currentNamespace, renamedView))).isFalse();
+ }
+
@TestTemplate
public void renameViewHiddenByTempView() throws NoSuchTableException {
insertRows(10);
diff --git a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
index df42175c34..37abc3c099 100644
--- a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
+++ b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
@@ -791,6 +791,9 @@ public class Spark3Util {
* A modified version of Spark's LookupCatalog.CatalogAndIdentifier.unapply Attempts to find the
* catalog and identifier a multipart identifier represents
*
+ * <p>A single-part name is resolved in the session's current namespace, or in the default
+ * namespace of {@code defaultCatalog} if it is not the current catalog.
+ *
* @param spark Spark session to use for resolution
* @param nameParts Multipart identifier representing a table
* @param defaultCatalog Catalog to use if none is specified
diff --git a/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala b/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
index da540f5891..4d216bd97a 100644
--- a/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
+++ b/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ExtendedDataSourceV2Strategy.scala
@@ -119,12 +119,8 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
OrderAwareCoalesceExec(numPartitions, coalescer, planLater(child)) :: Nil
case RenameTable(ResolvedV2View(oldCatalog: ViewCatalog, oldIdent), newName, isView @ true) =>
- val newIdent = Spark3Util.catalogAndIdentifier(spark, newName.toList.asJava)
- if (oldCatalog.name != newIdent.catalog().name()) {
- throw new IcebergAnalysisException(
- s"Cannot move view between catalogs: from=${oldCatalog.name} and to=${newIdent.catalog().name()}")
- }
- RenameV2ViewExec(oldCatalog, oldIdent, newIdent.identifier()) :: Nil
+ val targetIdent = resolveViewRenameTarget(oldCatalog, newName)
+ RenameV2ViewExec(oldCatalog, oldIdent, targetIdent) :: Nil
case DropIcebergView(ResolvedIdentifier(viewCatalog: ViewCatalog, ident), ifExists) =>
DropV2ViewExec(viewCatalog, ident, ifExists) :: Nil
@@ -176,6 +172,23 @@ case class ExtendedDataSourceV2Strategy(spark: SparkSession) extends Strategy wi
case _ => Nil
}
+ private def resolveViewRenameTarget(
+ sourceCatalog: ViewCatalog,
+ targetName: Seq[String]): Identifier = {
+ if (targetName.length == 1) {
+ // An unqualified target keeps the source view's namespace
+ Identifier.of(Array.empty[String], targetName.head)
+ } else {
+ val target = Spark3Util.catalogAndIdentifier(spark, targetName.toList.asJava, sourceCatalog)
+ if (sourceCatalog.name != target.catalog().name()) {
+ throw new IcebergAnalysisException(
+ s"Cannot move view between catalogs: from=${sourceCatalog.name} and to=${target.catalog().name()}")
+ }
+
+ target.identifier()
+ }
+ }
+
private object IcebergCatalogAndIdentifier {
def unapply(identifier: Seq[String]): Option[(TableCatalog, Identifier)] = {
val catalogAndIdentifier = Spark3Util.catalogAndIdentifier(spark, identifier.asJava)
diff --git a/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala b/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
index 5dada1cab0..bb2c62c387 100644
--- a/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
+++ b/spark/v4.1/spark-extensions/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameV2ViewExec.scala
@@ -29,7 +29,13 @@ case class RenameV2ViewExec(catalog: ViewCatalog, oldIdent: Identifier, newIdent
override lazy val output: Seq[Attribute] = Nil
override protected def run(): Seq[InternalRow] = {
- catalog.renameView(oldIdent, newIdent)
+ val qualifiedNewIdent = if (newIdent.namespace.isEmpty) {
+ Identifier.of(oldIdent.namespace, newIdent.name)
+ } else {
+ newIdent
+ }
+
+ catalog.renameView(oldIdent, qualifiedNewIdent)
Seq.empty
}
diff --git a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
index 640ea424b3..c72ef29b45 100644
--- a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
+++ b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestViews.java
@@ -776,6 +776,34 @@ public class TestViews extends ExtensionsTestBase {
.containsExactlyInAnyOrderElementsOf(expected);
}
+ @TestTemplate
+ public void renameQualifiedViewToUnqualifiedTargetKeepsSourceNamespace() {
+ Namespace sourceNamespace = Namespace.of(viewName("rename_source_ns"));
+ Namespace currentNamespace = Namespace.of(viewName("rename_current_ns"));
+ String viewName = viewName("originalView");
+ String renamedView = viewName("renamedView");
+ String sql = String.format("SELECT id FROM %s.%s.%s", catalogName, NAMESPACE, tableName);
+
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, sourceNamespace);
+ sql("CREATE NAMESPACE IF NOT EXISTS %s.%s", catalogName, currentNamespace);
+
+ ViewCatalog viewCatalog = viewCatalog();
+ viewCatalog
+ .buildView(TableIdentifier.of(sourceNamespace, viewName))
+ .withQuery("spark", sql)
+ .withDefaultNamespace(sourceNamespace)
+ .withDefaultCatalog(catalogName)
+ .withSchema(schema(sql))
+ .create();
+
+ sql("USE %s.%s", catalogName, currentNamespace);
+ sql("ALTER VIEW %s.%s RENAME TO %s", sourceNamespace, viewName, renamedView);
+
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, viewName))).isFalse();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(sourceNamespace, renamedView))).isTrue();
+ assertThat(viewCatalog.viewExists(TableIdentifier.of(currentNamespace, renamedView))).isFalse();
+ }
+
@TestTemplate
public void renameViewHiddenByTempView() throws NoSuchTableException {
insertRows(10);
diff --git a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
index 102e8d7691..aaa1f67067 100644
--- a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
+++ b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java
@@ -838,6 +838,9 @@ public class Spark3Util {
* A modified version of Spark's LookupCatalog.CatalogAndIdentifier.unapply Attempts to find the
* catalog and identifier a multipart identifier represents
*
+ * <p>A single-part name is resolved in the session's current namespace, or in the default
+ * namespace of {@code defaultCatalog} if it is not the current catalog.
+ *
* @param spark Spark session to use for resolution
* @param nameParts Multipart identifier representing a table
* @param defaultCatalog Catalog to use if none is specified