You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by am...@apache.org on 2026/04/01 16:19:11 UTC

(iceberg) branch main updated: Spark 4.1: Control merge schema evolution by table property (#15825)

This is an automated email from the ASF dual-hosted git repository.

amoghj 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 05d7ece42a Spark 4.1: Control merge schema evolution by table property (#15825)
05d7ece42a is described below

commit 05d7ece42acccc041f711dcac0dbb03a3c127396
Author: Szehon Ho <sz...@gmail.com>
AuthorDate: Wed Apr 1 09:19:04 2026 -0700

    Spark 4.1: Control merge schema evolution by table property (#15825)
    
    * Spark: Control merge schema evolution by table property
    
    Add a new table property write.spark.auto-schema-evolution (default true)
    that controls whether the AUTOMATIC_SCHEMA_EVOLUTION capability is
    reported to Spark. When set to false, Spark's MERGE WITH SCHEMA
    EVOLUTION no longer evolves the target table schema.
    
    Also add a guard in SparkWriteBuilder to reject mergeSchema write option
    when the property is disabled.
    
    * Remove unnecessary validation from SparkWriteBuilder
    
    The capability removal in SparkTable is sufficient to control schema
    evolution. The mergeSchema write option path already requires
    accept-any-schema, making a second gate redundant.
    
    * Address review comments
    
    - Rename property to write.spark.auto-schema-evolution.enabled
    - Rename caps to tableCapabilities in computeCapabilities
    - Add explicit = in ALTER TABLE SET TBLPROPERTIES test SQL
---
 .../java/org/apache/iceberg/TableProperties.java   |  4 ++
 .../spark/extensions/TestMergeSchemaEvolution.java | 45 ++++++++++++++++++++++
 .../apache/iceberg/spark/source/SparkTable.java    | 32 +++++++++++----
 3 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/TableProperties.java b/core/src/main/java/org/apache/iceberg/TableProperties.java
index 1f778984af..71991f633d 100644
--- a/core/src/main/java/org/apache/iceberg/TableProperties.java
+++ b/core/src/main/java/org/apache/iceberg/TableProperties.java
@@ -346,6 +346,10 @@ public class TableProperties {
   public static final String SPARK_WRITE_ACCEPT_ANY_SCHEMA = "write.spark.accept-any-schema";
   public static final boolean SPARK_WRITE_ACCEPT_ANY_SCHEMA_DEFAULT = false;
 
+  public static final String SPARK_WRITE_AUTO_SCHEMA_EVOLUTION =
+      "write.spark.auto-schema-evolution.enabled";
+  public static final boolean SPARK_WRITE_AUTO_SCHEMA_EVOLUTION_DEFAULT = true;
+
   public static final String SPARK_WRITE_ADVISORY_PARTITION_SIZE_BYTES =
       "write.spark.advisory-partition-size-bytes";
 
diff --git a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMergeSchemaEvolution.java b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMergeSchemaEvolution.java
index d760d56b7a..782321b588 100644
--- a/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMergeSchemaEvolution.java
+++ b/spark/v4.1/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestMergeSchemaEvolution.java
@@ -19,10 +19,12 @@
 package org.apache.iceberg.spark.extensions;
 
 import static org.apache.spark.sql.functions.col;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assumptions.assumeThat;
 
 import java.util.Map;
 import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.junit.jupiter.api.AfterEach;
@@ -260,6 +262,49 @@ public class TestMergeSchemaEvolution extends SparkRowLevelOperationsTestBase {
         sql("SELECT id, value FROM %s ORDER BY id", selectTarget()));
   }
 
+  @TestTemplate
+  public void testMergeWithSchemaEvolutionDisabledByTableProperty() {
+    assumeThat(branch).as("Schema evolution does not work for branches currently").isNull();
+
+    createAndInitTable(
+        "id INT, dep STRING",
+        "{ \"id\": 1, \"dep\": \"hr\" }\n" + "{ \"id\": 2, \"dep\": \"software\" }");
+
+    sql(
+        "ALTER TABLE %s SET TBLPROPERTIES ('%s' = 'false')",
+        tableName, TableProperties.SPARK_WRITE_AUTO_SCHEMA_EVOLUTION);
+
+    createOrReplaceView(
+        "source",
+        "id INT, dep STRING, salary INT",
+        "{ \"id\": 1, \"dep\": \"hr\", \"salary\": 100 }\n"
+            + "{ \"id\": 3, \"dep\": \"finance\", \"salary\": 300 }");
+
+    sql(
+        "MERGE WITH SCHEMA EVOLUTION INTO %s AS t USING source AS s "
+            + "ON t.id == s.id "
+            + "WHEN MATCHED THEN "
+            + "  UPDATE SET * "
+            + "WHEN NOT MATCHED THEN "
+            + "  INSERT *",
+        commitTarget());
+
+    // Schema should NOT be evolved - 'salary' column should not be added
+    assertThat(sql("SELECT * FROM %s", selectTarget()).get(0).length)
+        .as("Table should still have only 2 columns (id, dep)")
+        .isEqualTo(2);
+
+    ImmutableList<Object[]> expectedRows =
+        ImmutableList.of(
+            row(1, "hr"), // updated without salary
+            row(2, "software"), // kept
+            row(3, "finance")); // new without salary
+    assertEquals(
+        "Should have expected rows without schema evolution",
+        expectedRows,
+        sql("SELECT id, dep FROM %s ORDER BY id", selectTarget()));
+  }
+
   @Override
   protected Map<String, String> extraTableProperties() {
     return Map.of();
diff --git a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkTable.java b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkTable.java
index ae3c0ce0c8..07db8c4ed3 100644
--- a/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkTable.java
+++ b/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/source/SparkTable.java
@@ -83,20 +83,14 @@ public class SparkTable extends BaseSparkTable
 
   private static final Logger LOG = LoggerFactory.getLogger(SparkTable.class);
 
-  private static final Set<TableCapability> CAPABILITIES =
+  private static final Set<TableCapability> BASE_CAPABILITIES =
       ImmutableSet.of(
-          TableCapability.AUTOMATIC_SCHEMA_EVOLUTION,
           TableCapability.BATCH_READ,
           TableCapability.BATCH_WRITE,
           TableCapability.MICRO_BATCH_READ,
           TableCapability.STREAMING_WRITE,
           TableCapability.OVERWRITE_BY_FILTER,
           TableCapability.OVERWRITE_DYNAMIC);
-  private static final Set<TableCapability> CAPABILITIES_WITH_ACCEPT_ANY_SCHEMA =
-      ImmutableSet.<TableCapability>builder()
-          .addAll(CAPABILITIES)
-          .add(TableCapability.ACCEPT_ANY_SCHEMA)
-          .build();
 
   private final Schema schema; // effective schema (not necessarily current table schema)
   private final Snapshot snapshot; // always set unless table is empty
@@ -133,7 +127,7 @@ public class SparkTable extends BaseSparkTable
     this.snapshot = snapshot;
     this.branch = branch;
     this.timeTravel = timeTravel;
-    this.capabilities = acceptAnySchema(table) ? CAPABILITIES_WITH_ACCEPT_ANY_SCHEMA : CAPABILITIES;
+    this.capabilities = computeCapabilities(table);
   }
 
   public SparkTable copyWithBranch(String newBranch) {
@@ -353,6 +347,21 @@ public class SparkTable extends BaseSparkTable
     return new SparkTable(table, snapshotId, timeTravel);
   }
 
+  private static Set<TableCapability> computeCapabilities(Table table) {
+    ImmutableSet.Builder<TableCapability> tableCapabilities = ImmutableSet.builder();
+    tableCapabilities.addAll(BASE_CAPABILITIES);
+
+    if (autoSchemaEvolution(table)) {
+      tableCapabilities.add(TableCapability.AUTOMATIC_SCHEMA_EVOLUTION);
+    }
+
+    if (acceptAnySchema(table)) {
+      tableCapabilities.add(TableCapability.ACCEPT_ANY_SCHEMA);
+    }
+
+    return tableCapabilities.build();
+  }
+
   private static boolean acceptAnySchema(Table table) {
     return PropertyUtil.propertyAsBoolean(
         table.properties(),
@@ -360,6 +369,13 @@ public class SparkTable extends BaseSparkTable
         TableProperties.SPARK_WRITE_ACCEPT_ANY_SCHEMA_DEFAULT);
   }
 
+  private static boolean autoSchemaEvolution(Table table) {
+    return PropertyUtil.propertyAsBoolean(
+        table.properties(),
+        TableProperties.SPARK_WRITE_AUTO_SCHEMA_EVOLUTION,
+        TableProperties.SPARK_WRITE_AUTO_SCHEMA_EVOLUTION_DEFAULT);
+  }
+
   // returns latest snapshot for branch or current snapshot if branch is yet to be created
   private static Snapshot determineLatestSnapshot(Table table, String branch) {
     if (branch != null && table.refs().containsKey(branch)) {