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