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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 11a7c03266 [core] Reject map key type changes in schema merging 
(#10116)
11a7c03266 is described below

commit 11a7c032660856e7464e1b2bf84f632654a09dd5
Author: YangJie <[email protected]>
AuthorDate: Thu Sep 24 01:09:16 2026 -0400

    [core] Reject map key type changes in schema merging (#10116)
---
 .../apache/paimon/schema/SchemaMergingUtils.java   |  16 ++++
 .../paimon/schema/SchemaMergingUtilsTest.java      | 103 +++++++++++++++++++++
 2 files changed, 119 insertions(+)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaMergingUtils.java 
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaMergingUtils.java
index d444d89ca4..bb9f42fc79 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaMergingUtils.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaMergingUtils.java
@@ -158,6 +158,22 @@ public class SchemaMergingUtils {
 
             return new RowType(base0.isNullable(), updatedFields);
         } else if (base instanceof MapType && update instanceof MapType) {
+            // The read layer cannot cast map keys (createMapCastExecutor 
requires equal key
+            // types), so widening a key here would make every pre-change file 
unreadable.
+            // Fail the schema change up front with a clear reason instead. 
Nullability is
+            // ignored, matching merge()'s contract and the read layer, so 
that a key that
+            // only changes nullability (Spark forces map keys to NOT NULL) 
still merges; only
+            // a genuine key type change is rejected.
+            if (!((MapType) base)
+                    .getKeyType()
+                    .equalsIgnoreNullable(((MapType) update).getKeyType())) {
+                throw new UnsupportedOperationException(
+                        String.format(
+                                "Failed to merge map types with different key 
types: %s and %s. "
+                                        + "Map key type cannot be changed; 
cast the keys manually "
+                                        + "and recreate the column if needed.",
+                                base, update));
+            }
             return new MapType(
                     base0.isNullable(),
                     merge(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaMergingUtilsTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaMergingUtilsTest.java
index 47fc100032..b5befc4158 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaMergingUtilsTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaMergingUtilsTest.java
@@ -477,6 +477,109 @@ public class SchemaMergingUtilsTest {
         assertThat(r3.getValueType() instanceof SmallIntType).isTrue();
     }
 
+    @Test
+    public void testMergeMapTypesWithDifferentKeyTypes() {
+        AtomicInteger highestFieldId = new AtomicInteger(1);
+
+        // widening a map key must be rejected at merge time: the read layer 
cannot cast map
+        // keys (SchemaEvolutionUtil.createMapCastExecutor requires equal key 
types), so a
+        // widened key would make every pre-change file unreadable
+        DataType source = new MapType(new IntType(), new 
VarCharType(VarCharType.MAX_LENGTH));
+        DataType widenedKey =
+                new MapType(new BigIntType(), new 
VarCharType(VarCharType.MAX_LENGTH));
+        assertThatThrownBy(
+                        () ->
+                                SchemaMergingUtils.merge(
+                                        source, widenedKey, highestFieldId, 
true, false, true))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("different key types");
+
+        // same rejection when explicit casts are allowed: no cast can make 
old keys readable
+        assertThatThrownBy(
+                        () ->
+                                SchemaMergingUtils.merge(
+                                        source, widenedKey, highestFieldId, 
true, true, true))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("different key types");
+
+        // without type widening the key change is rejected too: the old 
behavior silently
+        // kept the base key type, deferring the same crash to the 
write-alignment/read layer
+        assertThatThrownBy(
+                        () ->
+                                SchemaMergingUtils.merge(
+                                        source, widenedKey, highestFieldId, 
false, false, true))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("different key types");
+
+        // a nullable map with equal keys still merges normally: nullability 
flows, the value
+        // widens, and the key check does not reject same-key merges
+        MapType nonNullableSource = new MapType(false, new IntType(), new 
IntType());
+        MapType nullableSameKey = new MapType(true, new IntType(), new 
BigIntType());
+        MapType merged =
+                (MapType)
+                        SchemaMergingUtils.merge(
+                                nonNullableSource,
+                                nullableSameKey,
+                                highestFieldId,
+                                true,
+                                false,
+                                true);
+        assertThat(merged.isNullable()).isFalse();
+        assertThat(merged.getKeyType() instanceof IntType).isTrue();
+        assertThat(merged.getValueType() instanceof BigIntType).isTrue();
+    }
+
+    @Test
+    public void testMergeMapKeysDifferingOnlyInNullabilityStillMerges() {
+        AtomicInteger highestFieldId = new AtomicInteger(1);
+
+        // a key that changes only nullability is not a key type change: merge 
ignores
+        // nullability and the read layer sees identical keys. Spark forces 
map keys to
+        // NOT NULL while core/Flink default to nullable, so this must stay a 
benign merge.
+        MapType nullableKey = new MapType(new IntType(), new IntType());
+        MapType nonNullKey = new MapType(new IntType(false), new BigIntType());
+        MapType merged =
+                (MapType)
+                        SchemaMergingUtils.merge(
+                                nullableKey, nonNullKey, highestFieldId, true, 
false, true);
+        // the base key's nullability flows to the result, so pre-change files 
stay readable
+        assertThat(merged.getKeyType() instanceof IntType).isTrue();
+        assertThat(merged.getKeyType().isNullable()).isTrue();
+        assertThat(merged.getValueType() instanceof BigIntType).isTrue();
+    }
+
+    @Test
+    public void testMergeMapKeyChangeNestedInRowIsRejected() {
+        AtomicInteger highestFieldId = new AtomicInteger(1);
+
+        // the guard must fire on the recursive path too: a map key change 
nested inside a
+        // row (how a real column evolves) is rejected the same as a top-level 
map
+        RowType base =
+                new RowType(
+                        Lists.newArrayList(
+                                new DataField(
+                                        0,
+                                        "m",
+                                        new MapType(
+                                                new IntType(),
+                                                new 
VarCharType(VarCharType.MAX_LENGTH)))));
+        RowType widenedKey =
+                new RowType(
+                        Lists.newArrayList(
+                                new DataField(
+                                        0,
+                                        "m",
+                                        new MapType(
+                                                new BigIntType(),
+                                                new 
VarCharType(VarCharType.MAX_LENGTH)))));
+        assertThatThrownBy(
+                        () ->
+                                SchemaMergingUtils.merge(
+                                        base, widenedKey, highestFieldId, 
true, false, true))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("different key types");
+    }
+
     @Test
     public void testMergeMultisetTypes() {
         AtomicInteger highestFieldId = new AtomicInteger(1);

Reply via email to