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

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


The following commit(s) were added to refs/heads/master by this push:
     new cb9bbe99892 [fix](iceberg) Fix historical scans after schema evolution 
(#67687)
cb9bbe99892 is described below

commit cb9bbe998928ddca71989c2186c31ddb59a182ef
Author: Gabriel <[email protected]>
AuthorDate: Mon Sep 14 10:25:01 2026 +0800

    [fix](iceberg) Fix historical scans after schema evolution (#67687)
    
    ### What this PR does
    
    Ports #67479 from branch-4.1 to master.
    
    - Upgrade Iceberg from 1.10.1 to 1.11.0 and sync the vendored
    `DeleteFileIndex`.
    - Preserve historical-schema field lookup for equality deletes after
    columns are renamed or dropped.
    - Adapt the fix to the connector-based master layout, including DLF and
    HiveCatalog compatibility.
    - Index evolved partition structs by their position in each spec,
    because partition field IDs may contain gaps.
    - Add connector unit coverage for historical predicates after
    rename/drop and update Iceberg regression expectations.
    
    Issue: [DORIS-28397](https://issues.apache.org/jira/browse/DORIS-28397)
    
    ### Tests
    
    - Historical-schema and Iceberg 1.11 compatibility tests: 6 passed, 0
    failed.
    - FE Checkstyle validation: passed with 0 violations.
    - Full `fe-connector-iceberg` reactor test run: 1390 passed, 5 skipped,
    1 failed. The single failure
    
(`IcebergWritePlanProviderTest.planMergePreservesExplicitlyEmptyReadAcrossConcurrentFirstAppend`)
    reproduces unchanged on the master baseline and is unrelated to this PR.
---
 .../doris/iceberg/IcebergSerializationCompat.java  |  84 ++++++
 .../doris/iceberg/IcebergSysTableJniScanner.java   |   3 +-
 .../iceberg/IcebergSerializationCompatTest.java    |  76 +++++
 fe/check/checkstyle/suppressions.xml               |   2 +
 .../doris/connector/cache/ConnectorTableKey.java   |  29 +-
 .../cache/ConnectorMetadataCacheTest.java          |  22 ++
 .../fe-connector-hms-hive-shade/pom.xml            |   7 +-
 .../iceberg/IcebergConnectorMetadata.java          |  46 ++-
 .../connector/iceberg/IcebergPartitionCache.java   |  26 +-
 .../connector/iceberg/IcebergPartitionUtils.java   |  45 ++-
 .../connector/iceberg/IcebergScanPlanProvider.java |  54 +++-
 .../iceberg/IcebergSystemTableSerialization.java   |  90 ++++++
 .../connector/iceberg/dlf/DLFTableOperations.java  |   3 +-
 .../java/org/apache/iceberg/DeleteFileIndex.java   |  65 ++++-
 .../apache/iceberg/SchemaAwareDataTableScan.java   |  57 ++++
 .../iceberg/IcebergConnectorCacheTest.java         |   2 +-
 ...ergConnectorMetadataPartitionViewCacheTest.java |  62 +++-
 .../iceberg/IcebergPartitionCacheTest.java         |   7 +-
 .../iceberg/IcebergPartitionUtilsTest.java         | 108 +++++--
 .../iceberg/IcebergScanPlanProviderTest.java       | 316 ++++++++++++++++++++-
 fe/pom.xml                                         |   4 +-
 .../iceberg_schema_change_ddl_with_branch.out      |   1 -
 .../iceberg/test_iceberg_sys_table.out             |   2 +
 .../iceberg_schema_change_ddl_with_branch.groovy   |   2 +-
 24 files changed, 972 insertions(+), 141 deletions(-)

diff --git 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSerializationCompat.java
 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSerializationCompat.java
new file mode 100644
index 00000000000..dda1c43c1b2
--- /dev/null
+++ 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSerializationCompat.java
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.iceberg;
+
+import org.apache.iceberg.Schema;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+final class IcebergSerializationCompat {
+    private static final long ICEBERG_1_10_1_SCHEMA_UID = 6812231194765760118L;
+    private static final long ICEBERG_1_11_0_SCHEMA_UID = 
-1265875184407129845L;
+    private static final ObjectStreamClass LOCAL_SCHEMA_DESCRIPTOR = 
ObjectStreamClass.lookup(Schema.class);
+
+    private IcebergSerializationCompat() {
+    }
+
+    @SuppressWarnings({"DangerousJavaDeserialization", "unchecked"})
+    static <T> T deserializeFromBase64(String base64) {
+        if (base64 == null) {
+            return null;
+        }
+        byte[] bytes = 
Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
+        try (ByteArrayInputStream input = new ByteArrayInputStream(bytes);
+                ObjectInputStream objectInput = new 
SchemaCompatibleObjectInputStream(input)) {
+            return (T) objectInput.readObject();
+        } catch (IOException e) {
+            throw new UncheckedIOException("Failed to deserialize object", e);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException("Could not read object ", e);
+        }
+    }
+
+    private static final class SchemaCompatibleObjectInputStream extends 
ObjectInputStream {
+        private SchemaCompatibleObjectInputStream(ByteArrayInputStream input) 
throws IOException {
+            super(input);
+        }
+
+        @Override
+        protected ObjectStreamClass readClassDescriptor() throws IOException, 
ClassNotFoundException {
+            ObjectStreamClass descriptor = super.readClassDescriptor();
+            // Iceberg 1.11 changed Schema's generated UID without changing 
its serialized fields. Accept only
+            // that known rolling-upgrade pair so unrelated or future 
class-layout changes still fail closed.
+            if (Schema.class.getName().equals(descriptor.getName())
+                    && descriptor.getSerialVersionUID() == 
ICEBERG_1_10_1_SCHEMA_UID
+                    && LOCAL_SCHEMA_DESCRIPTOR.getSerialVersionUID() == 
ICEBERG_1_11_0_SCHEMA_UID) {
+                return LOCAL_SCHEMA_DESCRIPTOR;
+            }
+            return descriptor;
+        }
+
+        @Override
+        protected Class<?> resolveClass(ObjectStreamClass descriptor) throws 
IOException, ClassNotFoundException {
+            String className = descriptor.getName();
+            if (className.indexOf('/') >= 0) {
+                // Replacing Schema's stream descriptor exposes JVM-style 
array signatures with slashes; resolve
+                // them through the scanner's isolated class loader after 
normalizing to Class.forName syntax.
+                return Class.forName(className.replace('/', '.'), false,
+                        IcebergSerializationCompat.class.getClassLoader());
+            }
+            return super.resolveClass(descriptor);
+        }
+    }
+}
diff --git 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
index 5b1df1ab93a..a2b8e104afb 100644
--- 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
+++ 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
@@ -28,7 +28,6 @@ import com.google.common.base.Preconditions;
 import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.io.CloseableIterator;
-import org.apache.iceberg.util.SerializationUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,7 +54,7 @@ public class IcebergSysTableJniScanner extends JniScanner {
         String serializedSplitParams = params.get("serialized_split");
         Preconditions.checkArgument(serializedSplitParams != null && 
!serializedSplitParams.isEmpty(),
                 "serialized_split should not be empty");
-        this.scanTask = 
SerializationUtil.deserializeFromBase64(serializedSplitParams);
+        this.scanTask = 
IcebergSerializationCompat.deserializeFromBase64(serializedSplitParams);
         String requiredFieldsParam = params.get("required_fields");
         Preconditions.checkArgument(requiredFieldsParam != null && 
!requiredFieldsParam.isEmpty(),
                 "required_fields should not be empty");
diff --git 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/test/java/org/apache/doris/iceberg/IcebergSerializationCompatTest.java
 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/test/java/org/apache/doris/iceberg/IcebergSerializationCompatTest.java
new file mode 100644
index 00000000000..cc5588c4b95
--- /dev/null
+++ 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/test/java/org/apache/doris/iceberg/IcebergSerializationCompatTest.java
@@ -0,0 +1,76 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.iceberg;
+
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.types.Types;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.Base64;
+
+public class IcebergSerializationCompatTest {
+    // An empty StaticDataTask serialized by Iceberg 1.10.1. It carries the 
old Schema serialVersionUID while
+    // keeping all data neutral and local, so the fixture remains stable and 
safe to commit.
+    private static final String ICEBERG_1_10_1_TASK = 
"rO0ABXNyACFvcmcuYXBhY2hlLmljZWJlcmcuU3RhdGljRGF0YVRhc2t2PvVIpr/rlAIABEwADG1ldGFkYXRh"
+            + 
"RmlsZXQAHUxvcmcvYXBhY2hlL2ljZWJlcmcvRGF0YUZpbGU7TAAPcHJvamVjdGVkU2NoZW1hdAAbTG9yZy9h"
+            + 
"cGFjaGUvaWNlYmVyZy9TY2hlbWE7WwAEcm93c3QAIFtMb3JnL2FwYWNoZS9pY2ViZXJnL1N0cnVjdExpa2U7"
+            + 
"TAALdGFibGVTY2hlbWFxAH4AAnhwcHNyABlvcmcuYXBhY2hlLmljZWJlcmcuU2NoZW1hXonoLcvZFnYCAARJ"
+            + 
"AA5oaWdoZXN0RmllbGRJZEkACHNjaGVtYUlkWwASaWRlbnRpZmllckZpZWxkSWRzdAACW0lMAAZzdHJ1Y3R0"
+            + 
"ACtMb3JnL2FwYWNoZS9pY2ViZXJnL3R5cGVzL1R5cGVzJFN0cnVjdFR5cGU7eHAAAAABAAAAAHVyAAJbSU26"
+            + 
"YCZ26rKlAgAAeHAAAAAAc3IAKW9yZy5hcGFjaGUuaWNlYmVyZy50eXBlcy5UeXBlcyRTdHJ1Y3RUeXBlY2OW"
+            + 
"YF+O53QCAAFbAAZmaWVsZHN0AC1bTG9yZy9hcGFjaGUvaWNlYmVyZy90eXBlcy9UeXBlcyROZXN0ZWRGaWVs"
+            + 
"ZDt4cgAob3JnLmFwYWNoZS5pY2ViZXJnLnR5cGVzLlR5cGUkTmVzdGVkVHlwZWpUXj112XcBAgAAeHB1cgAt"
+            + 
"W0xvcmcuYXBhY2hlLmljZWJlcmcudHlwZXMuVHlwZXMkTmVzdGVkRmllbGQ7A428r/O1h1gCAAB4cAAAAAFz"
+            + 
"cgAqb3JnLmFwYWNoZS5pY2ViZXJnLnR5cGVzLlR5cGVzJE5lc3RlZEZpZWxkRnIcmOgj/wICAAdJAAJpZFoA"
+            + 
"CmlzT3B0aW9uYWxMAANkb2N0ABJMamF2YS9sYW5nL1N0cmluZztMAA5pbml0aWFsRGVmYXVsdHQAKExvcmcv"
+            + 
"YXBhY2hlL2ljZWJlcmcvZXhwcmVzc2lvbnMvTGl0ZXJhbDtMAARuYW1lcQB+ABJMAAR0eXBldAAfTG9yZy9h"
+            + 
"cGFjaGUvaWNlYmVyZy90eXBlcy9UeXBlO0wADHdyaXRlRGVmYXVsdHEAfgATeHAAAAABAHBwdAACaWRzcgAs"
+            + 
"b3JnLmFwYWNoZS5pY2ViZXJnLnR5cGVzLlByaW1pdGl2ZUxpa2VIb2xkZXLbKTPuyM89cAIAAUwADHR5cGVB"
+            + 
"c1N0cmluZ3EAfgASeHB0AANpbnRwdXIAIFtMb3JnL2FwYWNoZS5pY2ViZXJnLlN0cnVjdExpa2U7MJKGbVap"
+            + "uFMCAAB4cAAAAABxAH4ACA==";
+
+    @Test
+    public void deserializesIceberg1101SystemTableTask() {
+        FileScanTask task = 
IcebergSerializationCompat.deserializeFromBase64(ICEBERG_1_10_1_TASK);
+
+        Assert.assertEquals("id", task.schema().columns().get(0).name());
+    }
+
+    @Test
+    public void preservesCurrentIcebergSerialization() throws IOException {
+        Schema expected = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+
+        Schema actual = IcebergSerializationCompat.deserializeFromBase64(
+                serializeToBase64(expected));
+
+        Assert.assertTrue(expected.sameSchema(actual));
+    }
+
+    private static String serializeToBase64(Object value) throws IOException {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try (ObjectOutputStream objectOutput = new ObjectOutputStream(output)) 
{
+            objectOutput.writeObject(value);
+        }
+        return Base64.getEncoder().encodeToString(output.toByteArray());
+    }
+}
diff --git a/fe/check/checkstyle/suppressions.xml 
b/fe/check/checkstyle/suppressions.xml
index 505fcfff829..8ef6fe5c631 100644
--- a/fe/check/checkstyle/suppressions.xml
+++ b/fe/check/checkstyle/suppressions.xml
@@ -81,6 +81,8 @@ under the License.
 
     <!-- ignore iceberg delete file index copied from 
iceberg/DeleteFileIndex.java -->
     <suppress files="org[\\/]apache[\\/]iceberg[\\/]DeleteFileIndex\.java" 
checks="[a-zA-Z0-9]*"/>
+    <!-- Iceberg package access is required to preserve historical schema 
binding in DataTableScan. -->
+    <suppress 
files="org[\\/]apache[\\/]iceberg[\\/]SchemaAwareDataTableScan\.java" 
checks="[a-zA-Z0-9]*"/>
 
     <!-- ignore gensrc/thrift/ExternalTableSchema.thrift -->
     <suppress files=".*thrift/schema/external/.*" checks=".*"/>
diff --git 
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ConnectorTableKey.java
 
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ConnectorTableKey.java
index 41ac71c9930..e8893406750 100644
--- 
a/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ConnectorTableKey.java
+++ 
b/fe/fe-connector/fe-connector-cache/src/main/java/org/apache/doris/connector/cache/ConnectorTableKey.java
@@ -20,16 +20,19 @@ package org.apache.doris.connector.cache;
 import java.util.Objects;
 
 /**
- * Immutable cache key for {@link ConnectorMetadataCache}: {@code (db, table, 
snapshotId, schemaId)}.
+ * Immutable cache key for {@link ConnectorMetadataCache}:
+ * {@code (db, table, snapshotId, schemaId, metadataGeneration)}.
  *
  * <p>Engine-agnostic (external-partition-derived-cache design doc §5, "cache 
A"): a table's derived partition
  * view is a pure function of its identity plus the MVCC coordinate it was 
read at, so pinning that coordinate
  * into the key is what makes the cache "always correct" — a new 
snapshot/schema yields a new key, never a stale
  * hit. Non-MVCC engines (hive) or engines without a separate schema version 
pass {@code snapshotId = -1} /
- * {@code schemaId = -1}; the key still holds them, it just means 
"unversioned" for that axis.
+ * {@code schemaId = -1}; the key still holds them, it just means 
"unversioned" for that axis. Connectors whose
+ * derived view depends on another independently evolving metadata generation 
may use
+ * {@code metadataGeneration}; other connectors leave it at {@code -1} through 
the four-argument constructor.
  *
  * <p>{@link #matches} / {@link #matchesDb} back {@link 
ConnectorMetadataCache#invalidateTable} /
- * {@link ConnectorMetadataCache#invalidateDb}, which must drop every 
snapshot/schema of a (db, table) or
+ * {@link ConnectorMetadataCache#invalidateDb}, which must drop every metadata 
generation of a (db, table) or
  * every table of a db — mirrors the {@code matches}/{@code matchesDb} helpers 
on the sibling connector caches
  * ({@code MaxComputePartitionCache.PartitionKey}, {@code 
HiveFileListingCache.FileListingKey}).
  */
@@ -38,12 +41,18 @@ public final class ConnectorTableKey {
     private final String table;
     private final long snapshotId;
     private final long schemaId;
+    private final long metadataGeneration;
 
     public ConnectorTableKey(String db, String table, long snapshotId, long 
schemaId) {
+        this(db, table, snapshotId, schemaId, -1L);
+    }
+
+    public ConnectorTableKey(String db, String table, long snapshotId, long 
schemaId, long metadataGeneration) {
         this.db = db;
         this.table = table;
         this.snapshotId = snapshotId;
         this.schemaId = schemaId;
+        this.metadataGeneration = metadataGeneration;
     }
 
     public String getDb() {
@@ -62,12 +71,16 @@ public final class ConnectorTableKey {
         return schemaId;
     }
 
-    /** Whether this key belongs to the given (db, table), regardless of 
snapshotId/schemaId. */
+    public long getMetadataGeneration() {
+        return metadataGeneration;
+    }
+
+    /** Whether this key belongs to the given (db, table), regardless of its 
metadata coordinates. */
     public boolean matches(String db, String table) {
         return Objects.equals(this.db, db) && Objects.equals(this.table, 
table);
     }
 
-    /** Whether this key belongs to the given db, regardless of 
table/snapshotId/schemaId. */
+    /** Whether this key belongs to the given db, regardless of table or 
metadata coordinates. */
     public boolean matchesDb(String db) {
         return Objects.equals(this.db, db);
     }
@@ -83,18 +96,20 @@ public final class ConnectorTableKey {
         ConnectorTableKey that = (ConnectorTableKey) o;
         return snapshotId == that.snapshotId
                 && schemaId == that.schemaId
+                && metadataGeneration == that.metadataGeneration
                 && Objects.equals(db, that.db)
                 && Objects.equals(table, that.table);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(db, table, snapshotId, schemaId);
+        return Objects.hash(db, table, snapshotId, schemaId, 
metadataGeneration);
     }
 
     @Override
     public String toString() {
         return "ConnectorTableKey{db=" + db + ", table=" + table
-                + ", snapshotId=" + snapshotId + ", schemaId=" + schemaId + 
'}';
+                + ", snapshotId=" + snapshotId + ", schemaId=" + schemaId
+                + ", metadataGeneration=" + metadataGeneration + '}';
     }
 }
diff --git 
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ConnectorMetadataCacheTest.java
 
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ConnectorMetadataCacheTest.java
index 8a6c1596fcc..e643e361d64 100644
--- 
a/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ConnectorMetadataCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-cache/src/test/java/org/apache/doris/connector/cache/ConnectorMetadataCacheTest.java
@@ -41,6 +41,11 @@ public class ConnectorMetadataCacheTest {
         return new ConnectorTableKey(db, table, snapshotId, schemaId);
     }
 
+    private static ConnectorTableKey key(
+            String db, String table, long snapshotId, long schemaId, long 
metadataGeneration) {
+        return new ConnectorTableKey(db, table, snapshotId, schemaId, 
metadataGeneration);
+    }
+
     private static ConnectorMetadataCache<String> newCache() {
         return new ConnectorMetadataCache<>(ENGINE, "partition_view", new 
HashMap<>());
     }
@@ -108,6 +113,23 @@ public class ConnectorMetadataCacheTest {
         Assertions.assertEquals(2, loads.get(), "distinct schemaId must 
trigger a distinct load");
     }
 
+    @Test
+    public void differentMetadataGenerationIsADistinctEntry() {
+        AtomicInteger loads = new AtomicInteger();
+        ConnectorMetadataCache<String> cache = newCache();
+
+        cache.get(key("db", "t", 1L, 1L, 10L), () -> "generation-10");
+        String second = cache.get(key("db", "t", 1L, 1L, 11L), () -> {
+            loads.incrementAndGet();
+            return "generation-11";
+        });
+
+        // Some table metadata evolves independently of both snapshot and 
schema; treating this axis as part of
+        // key identity prevents a connector from serving a derived view built 
for the preceding generation.
+        Assertions.assertEquals("generation-11", second);
+        Assertions.assertEquals(1, loads.get(), "a distinct metadata 
generation must trigger a distinct load");
+    }
+
     @Test
     public void invalidateTableEvictsAllSnapshotsOfThatTableOnly() {
         AtomicInteger loads = new AtomicInteger();
diff --git a/fe/fe-connector/fe-connector-hms-hive-shade/pom.xml 
b/fe/fe-connector/fe-connector-hms-hive-shade/pom.xml
index 69db7fef637..a5c39c45d17 100644
--- a/fe/fe-connector/fe-connector-hms-hive-shade/pom.xml
+++ b/fe/fe-connector/fe-connector-hms-hive-shade/pom.xml
@@ -211,9 +211,9 @@ under the License.
             </exclusions>
         </dependency>
 
-        <!-- iceberg-hive-metastore 1.10.1 supplies 
org.apache.iceberg.hive.HiveCatalog (the hms flavor);
+        <!-- iceberg-hive-metastore supplies 
org.apache.iceberg.hive.HiveCatalog (the hms flavor);
              only fe-connector-iceberg loads it. 
iceberg-core/api/common/bundled-guava + caffeine + slf4j
-             are excluded — the iceberg plugin already ships iceberg-core 
1.10.1 (which brings api/common/
+             are excluded — the iceberg plugin already ships the matching 
iceberg-core version (which brings api/common/
              guava/caffeine) as its own child-first jar, so re-bundling them 
here would create duplicate
              classes. Only the org.apache.iceberg.hive.* classes are kept, and 
the relocation below rewrites
              their org.apache.thrift refs to the shared private prefix so they 
link against the same
@@ -221,7 +221,8 @@ under the License.
         <dependency>
             <groupId>org.apache.iceberg</groupId>
             <artifactId>iceberg-hive-metastore</artifactId>
-            <version>1.10.1</version>
+            <!-- Keep HiveCatalog bytecode aligned with the Iceberg core 
loaded by the connector. -->
+            <version>${iceberg.version}</version>
             <optional>true</optional>
             <exclusions>
                 
<exclusion><groupId>org.apache.iceberg</groupId><artifactId>iceberg-core</artifactId></exclusion>
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
index 1caeae8fce7..088057890c2 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
@@ -1976,21 +1976,24 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
             ConnectorSession session, ConnectorTableHandle handle) {
         IcebergTableHandle iceHandle = (IcebergTableHandle) handle;
         try {
-            // PERF-06 cache A: memoize the BUILT derived view keyed by (db, 
table, snapshotId, schemaId) -- pure
-            // function of the pinned MVCC coordinate (a new snapshot/schema 
yields a new key, never a stale hit).
-            // The lookup sits INSIDE executeAuthenticated so a miss runs the 
loader (resolveTableForRead + the
-            // remote PARTITIONS build) under the FE-injected auth scope; a 
hit returns without any remote call. A
-            // null cache (session=user / no-cache catalog) computes directly 
every call. A resolved-empty -1
+            // PERF-06 cache A: memoize the BUILT derived view keyed by 
snapshot/schema/spec generation.
+            // The lookup sits INSIDE executeAuthenticated: every lookup 
resolves the live spec generation, while
+            // a miss additionally runs the remote PARTITIONS build under the 
FE-injected auth scope. A null cache
+            // (session=user / no-cache catalog) computes directly every call. 
A resolved-empty -1
             // bypasses cache A because its numeric key is otherwise 
indistinguishable from an unresolved latest
             // read, even though only the former is a query-begin MVCC 
boundary.
             return executeAuthenticated(() -> {
                 if (mvccPartitionViewCache == null || 
iceHandle.isResolvedEmptySnapshot()) {
                     return Optional.of(buildMvccPartitionViewUncached(session, 
iceHandle));
                 }
+                Table table = resolveTableForRead(session, iceHandle);
+                // A partition-spec commit does not create a snapshot or 
schema, so the spec id is a separate
+                // cache generation; omitting it can retain a stale derived 
view for the full cache TTL.
                 ConnectorTableKey key = new 
ConnectorTableKey(iceHandle.getDbName(),
-                        iceHandle.getTableName(), iceHandle.getSnapshotId(), 
iceHandle.getSchemaId());
+                        iceHandle.getTableName(), iceHandle.getSnapshotId(), 
iceHandle.getSchemaId(),
+                        table.spec().specId());
                 return Optional.of(mvccPartitionViewCache.get(key,
-                        () -> buildMvccPartitionViewUncached(session, 
iceHandle)));
+                        () -> buildMvccPartitionView(table, iceHandle)));
             });
         } catch (Exception e) {
             throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
@@ -2013,6 +2016,10 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
                     iceHandle.getResolvedEmptyPartitionStyle());
         }
         Table table = resolveTableForRead(session, iceHandle);
+        return buildMvccPartitionView(table, iceHandle);
+    }
+
+    private ConnectorMvccPartitionView buildMvccPartitionView(Table table, 
IcebergTableHandle iceHandle) {
         return IcebergPartitionUtils.buildMvccPartitionView(table, 
iceHandle.getSnapshotId(),
                 TableIdentifier.of(iceHandle.getDbName(), 
iceHandle.getTableName()), partitionCache);
     }
@@ -2064,18 +2071,29 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
             return Collections.emptyList();
         }
         try {
-            // PERF-06 cache A: memoize the BUILT partition-info list keyed by 
(db, table, snapshotId, schemaId).
-            // The lookup sits INSIDE executeAuthenticated (a miss runs the 
remote build under the auth scope; a hit
-            // returns without a remote call). BYPASS the cache when the 
filter is present -- that is not the
+            // PERF-06 cache A: memoize the BUILT partition-info list keyed by 
snapshot/schema/spec generation.
+            // The lookup sits INSIDE executeAuthenticated (each lookup 
resolves the current spec, and a miss runs
+            // the remote build under the auth scope). BYPASS the cache when 
the filter is present -- that is not the
             // pruning path (which always passes Optional.empty()) and is not 
keyed by (snapshot, schema) alone -- or
             // when the cache is null (session=user / no-cache catalog): 
compute directly every call.
             return executeAuthenticated(() -> {
                 if (listPartitionsViewCache == null || filter.isPresent()) {
                     return listPartitionsUncached(session, iceHandle);
                 }
+                Table table;
+                try {
+                    table = resolveTableForRead(session, iceHandle);
+                } catch (NoSuchTableException e) {
+                    LOG.warn("Iceberg table not found while listing 
partitions: {}.{}",
+                            iceHandle.getDbName(), iceHandle.getTableName(), 
e);
+                    return Collections.<ConnectorPartitionInfo>emptyList();
+                }
                 ConnectorTableKey key = new 
ConnectorTableKey(iceHandle.getDbName(),
-                        iceHandle.getTableName(), iceHandle.getSnapshotId(), 
iceHandle.getSchemaId());
-                return listPartitionsViewCache.get(key, () -> 
listPartitionsUncached(session, iceHandle));
+                        iceHandle.getTableName(), iceHandle.getSnapshotId(), 
iceHandle.getSchemaId(),
+                        table.spec().specId());
+                // A partition-spec commit does not create a snapshot or 
schema, so the spec id is a separate
+                // cache generation; omitting it can retain a stale derived 
view for the full cache TTL.
+                return listPartitionsViewCache.get(key, () -> 
listPartitions(table, iceHandle));
             });
         } catch (Exception e) {
             throw IcebergExceptionUtils.wrapTableLoadFailure(iceHandle, e,
@@ -2099,6 +2117,10 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
                     iceHandle.getDbName(), iceHandle.getTableName(), e);
             return Collections.<ConnectorPartitionInfo>emptyList();
         }
+        return listPartitions(table, iceHandle);
+    }
+
+    private List<ConnectorPartitionInfo> listPartitions(Table table, 
IcebergTableHandle iceHandle) {
         return IcebergPartitionUtils.listPartitions(table,
                 TableIdentifier.of(iceHandle.getDbName(), 
iceHandle.getTableName()), partitionCache);
     }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionCache.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionCache.java
index 105ca3ee506..b6a4144ea61 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionCache.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionCache.java
@@ -36,18 +36,17 @@ import java.util.function.Supplier;
 
 /**
  * Per-catalog cache of an iceberg table's raw partition list (PERF-02), keyed 
by {@code (TableIdentifier,
- * snapshotId)}. Restores the partition-info half of the legacy {@code 
IcebergExternalMetaCache} that the SPI
+ * snapshotId, schemaId, specId)}. Restores the partition-info half of the 
legacy
+ * {@code IcebergExternalMetaCache} that the SPI
  * cutover dropped: the analysis-phase PARTITIONS metadata-table scan
  * ({@link IcebergPartitionUtils#loadRawPartitionsUncached}, which the iceberg 
SDK materializes by reading EVERY
  * data+delete manifest of the snapshot) was re-run per query and re-run 4~6 
times per MTMV refresh, with no
  * cross-query reuse. The three consumers ({@code buildMvccPartitionView} for 
the MVCC/MTMV partition view,
  * {@code listPartitions} for {@code selectedPartitionNum}, {@code 
listPartitionNames} for SHOW PARTITIONS) all
- * funnel through it, so they share a single scan per {@code (table, 
snapshot)}.
+ * funnel through it, so they share a single scan per metadata generation.
  *
- * <p><b>Snapshot-keyed, so always correct.</b> A snapshot is immutable, so 
the derived partitions are a pure
- * function of the key; a new commit yields a new snapshot id (a new key -&gt; 
a live scan). Within the TTL the
- * snapshot id itself is held stable by {@link IcebergLatestSnapshotCache}, 
which is what makes the key stable
- * across queries and across the enumeration points of one MTMV refresh.
+ * <p><b>Metadata-generation-keyed.</b> A data commit yields a new snapshot 
id, while schema/spec IDs fence
+ * metadata-only evolution that changes Iceberg's unified partition struct 
without changing the snapshot.
  *
  * <p><b>No credential gate</b> (unlike {@link IcebergTableCache}): the cached 
value is pure metadata (partition
  * names, values, transforms, timestamps, snapshot ids) and carries no {@code 
FileIO} / credential, so it is
@@ -62,14 +61,18 @@ import java.util.function.Supplier;
  */
 final class IcebergPartitionCache {
 
-    /** Immutable composite key: a table's partition list is distinct per 
pinned snapshot id. */
+    /** Immutable composite key: partition projection depends on the snapshot 
and current schema/spec metadata. */
     static final class Key {
         final TableIdentifier id;
         final long snapshotId;
+        final int schemaId;
+        final int specId;
 
-        Key(TableIdentifier id, long snapshotId) {
+        Key(TableIdentifier id, long snapshotId, int schemaId, int specId) {
             this.id = id;
             this.snapshotId = snapshotId;
+            this.schemaId = schemaId;
+            this.specId = specId;
         }
 
         @Override
@@ -81,12 +84,15 @@ final class IcebergPartitionCache {
                 return false;
             }
             Key that = (Key) o;
-            return snapshotId == that.snapshotId && Objects.equals(id, 
that.id);
+            return snapshotId == that.snapshotId
+                    && schemaId == that.schemaId
+                    && specId == that.specId
+                    && Objects.equals(id, that.id);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(id, snapshotId);
+            return Objects.hash(id, snapshotId, schemaId, specId);
         }
     }
 
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
index ddc8cbde915..9d9738e130c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergPartitionUtils.java
@@ -33,6 +33,7 @@ import org.apache.iceberg.MetadataTableUtils;
 import org.apache.iceberg.PartitionData;
 import org.apache.iceberg.PartitionField;
 import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Partitioning;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.Table;
@@ -42,6 +43,7 @@ import org.apache.iceberg.io.CloseableIterable;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Type.TypeID;
 import org.apache.iceberg.types.Types.NestedField;
+import org.apache.iceberg.types.Types.StructType;
 import org.apache.iceberg.types.Types.TimestampType;
 import org.apache.iceberg.util.JsonUtil;
 import org.apache.iceberg.util.StructProjection;
@@ -502,8 +504,6 @@ final class IcebergPartitionUtils {
     private static final String DAY = "day";
     private static final String HOUR = "hour";
 
-    // Iceberg partition field id starts at PARTITION_DATA_ID_START 
(org.apache.iceberg.PartitionSpec).
-    private static final int PARTITION_DATA_ID_START = 1000;
     // Master IcebergUtils.UNKNOWN_SNAPSHOT_ID: an empty table / a null 
last_updated_snapshot_id row.
     private static final long UNKNOWN_SNAPSHOT_ID = -1;
 
@@ -758,30 +758,45 @@ final class IcebergPartitionUtils {
     /**
      * The cross-query PARTITIONS-scan de-duplication seam (PERF-02): when 
{@code cache} is non-null the raw
      * partition list is served from / populated into the per-catalog {@link 
IcebergPartitionCache} keyed by
-     * {@code (id, snapshotId)} — a snapshot is immutable, so the derived 
partitions are a pure function of that
-     * key and safe to reuse across queries (restoring the legacy 
IcebergExternalMetaCache partition-info cache).
+     * {@code (id, snapshotId, schemaId, specId)}. The schema/spec generation 
is required because metadata-only
+     * evolution can change the unified partition projection without creating 
a snapshot.
      * A {@code null} cache (offline unit tests / the no-cache catalog) reads 
live every call. The cached list is
-     * unmodifiable so a shared entry cannot be mutated by a concurrent 
reader; the loader's exception (e.g. the
-     * dropped-partition-source-column {@link ValidationException}) propagates 
verbatim so callers keep their own
-     * degradation, and a failed scan is not cached.
+     * unmodifiable so a shared entry cannot be mutated by a concurrent 
reader; loader exceptions propagate
+     * verbatim so callers keep their own degradation, and a failed scan is 
not cached.
      */
     private static List<IcebergRawPartition> loadRawPartitions(TableIdentifier 
id, Table table, long snapshotId,
             IcebergPartitionCache cache) {
         if (cache == null) {
             return loadRawPartitionsUncached(table, snapshotId);
         }
-        return cache.getOrLoad(new IcebergPartitionCache.Key(id, snapshotId),
+        return cache.getOrLoad(new IcebergPartitionCache.Key(
+                        id, snapshotId, table.schema().schemaId(), 
table.spec().specId()),
                 () -> 
Collections.unmodifiableList(loadRawPartitionsUncached(table, snapshotId)));
     }
 
     private static List<IcebergRawPartition> loadRawPartitionsUncached(Table 
table, long snapshotId) {
+        StructType unifiedPartitionType = Partitioning.partitionType(table);
+        Map<Integer, Integer> partitionFieldOrdinals = new HashMap<>();
+        for (int i = 0; i < unifiedPartitionType.fields().size(); i++) {
+            
partitionFieldOrdinals.put(unifiedPartitionType.fields().get(i).fieldId(), i);
+        }
         Table partitionsTable = 
MetadataTableUtils.createMetadataTableInstance(table, 
MetadataTableType.PARTITIONS);
         List<IcebergRawPartition> partitions = new ArrayList<>();
         try (CloseableIterable<FileScanTask> tasks = 
partitionsTable.newScan().useSnapshot(snapshotId).planFiles()) {
             for (FileScanTask task : tasks) {
                 CloseableIterable<StructLike> rows = task.asDataTask().rows();
                 for (StructLike row : rows) {
-                    partitions.add(generateRawPartition(table, row));
+                    PartitionSpec liveSpec = table.specs().get(row.get(1, 
Integer.class));
+                    boolean hasUnrepresentableField = 
liveSpec.fields().stream()
+                            .anyMatch(field -> 
!partitionFieldOrdinals.containsKey(field.fieldId()));
+                    if (hasUnrepresentableField) {
+                        // Only specs represented by live partition rows 
affect this snapshot. Rejecting an
+                        // orphaned but file-free historical spec would hide 
otherwise valid current partitions.
+                        LOG.warn("Cannot represent a live historical partition 
spec for iceberg table {}; "
+                                + "reporting an empty partition display.", 
table.name());
+                        return Collections.emptyList();
+                    }
+                    partitions.add(generateRawPartition(table, row, 
partitionFieldOrdinals));
                 }
             }
         } catch (IOException e) {
@@ -790,7 +805,8 @@ final class IcebergPartitionUtils {
         return partitions;
     }
 
-    private static IcebergRawPartition generateRawPartition(Table table, 
StructLike row) {
+    private static IcebergRawPartition generateRawPartition(
+            Table table, StructLike row, Map<Integer, Integer> 
partitionFieldOrdinals) {
         // PARTITIONS row layout: 0 partitionData, 1 spec_id, 2 record_count, 
3 file_count,
         // 4 total_data_file_size_in_bytes, 5..8 position/equality delete 
stats, 9 last_updated_at,
         // 10 last_updated_snapshot_id. Only 0/1/9/10 are needed by the MTMV 
partition view.
@@ -805,11 +821,10 @@ final class IcebergPartitionUtils {
         for (int i = 0; i < partitionSpec.fields().size(); ++i) {
             PartitionField partitionField = partitionSpec.fields().get(i);
             Class<?> fieldClass = partitionSpec.javaClasses()[i];
-            int fieldId = partitionField.fieldId();
-            // Iceberg partition field id starts at PARTITION_DATA_ID_START, 
so the index into partitionData is
-            // fieldId - PARTITION_DATA_ID_START.
-            int index = fieldId - PARTITION_DATA_ID_START;
-            Object o = partitionData.get(index, fieldClass);
+            // Iceberg 1.11 projects every metadata row into the table-wide 
unified partition struct; a spec-local
+            // position can therefore point at a different evolved field, so 
resolve the ordinal by field ID.
+            int ordinal = partitionFieldOrdinals.get(partitionField.fieldId());
+            Object o = partitionData.get(ordinal, fieldClass);
             String fieldValue = o == null ? null : o.toString();
             
sb.append(partitionField.name()).append("=").append(fieldValue).append("/");
             // Resolve the partition field's SOURCE column name 
(case-preserved), matching the generic
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
index b04b5ec82e0..3fb1472114a 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
@@ -66,9 +66,11 @@ import org.apache.iceberg.PartitionSpecParser;
 import org.apache.iceberg.PositionDeletesScanTask;
 import org.apache.iceberg.ScanTask;
 import org.apache.iceberg.Schema;
+import org.apache.iceberg.SchemaAwareDataTableScan;
 import org.apache.iceberg.SchemaParser;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.SplittableScanTask;
+import org.apache.iceberg.SupportsDistributedScanPlanning;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableOperations;
 import org.apache.iceberg.TableProperties;
@@ -92,7 +94,6 @@ import org.apache.iceberg.types.TypeUtil;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.types.Types.NestedField;
 import org.apache.iceberg.util.ScanTaskUtil;
-import org.apache.iceberg.util.SerializationUtil;
 import org.apache.iceberg.util.TableScanUtil;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -493,7 +494,8 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
         long threshold = sessionLong(session, NUM_FILES_IN_BATCH_MODE, 
DEFAULT_NUM_FILES_IN_BATCH_MODE);
         long fileCount = 0;
         try (CloseableIterable<ManifestFile> matching = getMatchingManifest(
-                snapshot.dataManifests(table.io()), table.specs(), 
scan.filter())) {
+                snapshot.dataManifests(table.io()),
+                SchemaAwareDataTableScan.specsFor(table, scan.schema()), 
scan.filter())) {
             for (ManifestFile manifest : matching) {
                 // Manifest metadata counts (cheap — no per-file read). Null 
guard for ancient manifests that
                 // omit the counts (legacy summed them unguarded; 0 is the 
safe under-count, never over-streams).
@@ -888,7 +890,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
      * resolve the metadata table ({@link #resolveSysTable}), apply the 
time-travel pin + predicate through the
      * shared {@link #buildScan} (legacy {@code createTableScan} honors {@code 
useSnapshot}/{@code useRef} on the
      * metadata-table scan too — iceberg system tables are legal time-travel 
targets), then serialize each
-     * metadata {@code FileScanTask} ({@code 
SerializationUtil.serializeToBase64}) into a JNI split carrying ONLY
+     * metadata {@code FileScanTask} into a rolling-upgrade-compatible JNI 
split carrying ONLY
      * {@code serialized_split} + {@code FORMAT_JNI} (see {@link 
IcebergScanRange#populateRangeParams}). COUNT(*)
      * pushdown does not apply (a metadata table has no snapshot-summary 
count). The serialized {@code
      * FileScanTask} bytes are consumed verbatim by BE's {@code 
IcebergSysTableJniScanner}
@@ -960,7 +962,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
             for (FileScanTask task : tasks) {
                 ranges.add(new IcebergScanRange.Builder()
                         .path(SYS_TABLE_DUMMY_PATH)
-                        
.serializedSplit(SerializationUtil.serializeToBase64(task))
+                        
.serializedSplit(IcebergSystemTableSerialization.serializeToBase64(task))
                         .build());
             }
         } catch (IOException e) {
@@ -1201,7 +1203,12 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
      */
     private TableScan buildScan(Table table, IcebergTableHandle handle, 
Optional<ConnectorExpression> filter,
             ConnectorSession session) {
-        TableScan scan = table.newScan();
+        Schema selectedSchema = !handle.isSystemTable() && 
handle.hasSnapshotPin()
+                ? pinnedSchema(table, handle) : table.schema();
+        // Keep the SDK's native Table.newScan implementation unless an actual 
historical schema needs the
+        // metadata-only snapshot fix; catalog-specific Table wrappers may 
provide their own scan behavior.
+        TableScan scan = !handle.isSystemTable() && 
!selectedSchema.sameSchema(table.schema())
+                ? SchemaAwareDataTableScan.newScan(table) : table.newScan();
         // MVCC / time-travel pin: a tag/branch pins by REF (so a later commit 
to the ref is honored, legacy
         // parity), else by snapshot id (legacy createTableScan: useRef when 
info.getRef()!=null else useSnapshot).
         if (handle.hasSnapshotPin() && supportsSnapshotSelection(handle)) {
@@ -1211,10 +1218,16 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
                 scan = scan.useSnapshot(handle.getSnapshotId());
             }
         }
+        // A latest MVCC pin may pair the current schema with the preceding 
snapshot after a schema-only commit.
+        // Preserve that logical schema explicitly so cache metrics use new 
field IDs and their initial defaults.
+        if (!handle.isSystemTable() && 
!scan.schema().sameSchema(selectedSchema)) {
+            scan = scan.project(selectedSchema);
+        }
         if (filter.isPresent()) {
             // Historical predicates must resolve names to the field ids of 
the generation used for binding;
             // using the current schema drops renamed predicates or can bind a 
later reused name incorrectly.
-            Schema predicateSchema = handle.hasSnapshotPin() ? 
pinnedSchema(table, handle) : table.schema();
+            Schema predicateSchema = handle.isSystemTable() && 
handle.hasSnapshotPin()
+                    ? pinnedSchema(table, handle) : selectedSchema;
             List<Expression> predicates =
                     new IcebergPredicateConverter(predicateSchema, 
resolveSessionZone(session)).convert(filter.get());
             for (Expression predicate : predicates) {
@@ -2754,16 +2767,17 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
         if (snapshot == null) {
             return CloseableIterable.withNoopClose(Collections.emptyList());
         }
-        Expression filterExpr = combineFilter(filter, table, session);
-        Map<Integer, PartitionSpec> specsById = table.specs();
+        Schema scanSchema = scan.schema();
+        Expression filterExpr = combineFilter(filter, scanSchema, session);
+        Map<Integer, PartitionSpec> specsById = 
SchemaAwareDataTableScan.specsFor(table, scanSchema);
         boolean caseSensitive = true;
 
         Map<Integer, ResidualEvaluator> residualEvaluators = new HashMap<>();
         specsById.forEach((id, spec) -> residualEvaluators.put(id,
                 ResidualEvaluator.of(spec, filterExpr, caseSensitive)));
         InclusiveMetricsEvaluator metricsEvaluator =
-                new InclusiveMetricsEvaluator(table.schema(), filterExpr, 
caseSensitive);
-        String schemaJson = SchemaParser.toJson(table.schema());
+                new InclusiveMetricsEvaluator(scanSchema, filterExpr, 
caseSensitive);
+        String schemaJson = SchemaParser.toJson(scanSchema);
 
         // Phase 1 (eager): partition-prune + cache-load delete manifests into 
the delete-file index.
         List<DeleteFile> deleteFiles = new ArrayList<>();
@@ -2781,6 +2795,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
             deleteFiles.addAll(manifestCacheGet(manifest, table, 
statsQueryId).getDeleteFiles());
         }
         DeleteFileIndex deleteIndex = DeleteFileIndex.builderFor(deleteFiles)
+                .schemasById(table.schemas())
                 .specsById(specsById)
                 .caseSensitive(caseSensitive)
                 .build();
@@ -2908,15 +2923,15 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
     /**
      * Combine the pushed predicate into one iceberg {@link Expression} for 
manifest-level pruning, mirroring
      * legacy {@code 
conjuncts.stream().map(convertToIcebergExpr).filter(nonNull).reduce(alwaysTrue, 
and)}. Reuses
-     * the T02 {@link IcebergPredicateConverter} on the table's CURRENT 
schema; an absent filter is
+     * the T02 {@link IcebergPredicateConverter} on the scan-bound schema; an 
absent filter is
      * {@code alwaysTrue()} (scan everything).
      */
-    private Expression combineFilter(Optional<ConnectorExpression> filter, 
Table table, ConnectorSession session) {
+    private Expression combineFilter(Optional<ConnectorExpression> filter, 
Schema schema, ConnectorSession session) {
         if (!filter.isPresent()) {
             return Expressions.alwaysTrue();
         }
         List<Expression> predicates =
-                new IcebergPredicateConverter(table.schema(), 
resolveSessionZone(session)).convert(filter.get());
+                new IcebergPredicateConverter(schema, 
resolveSessionZone(session)).convert(filter.get());
         Expression combined = Expressions.alwaysTrue();
         for (Expression predicate : predicates) {
             combined = Expressions.and(combined, predicate);
@@ -3092,9 +3107,21 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
                         () -> tableCache.borrow(
                                 TableIdentifier.of(handle.getDbName(), 
handle.getTableName()), directLoader),
                         directLoader);
+        rejectServerSideScanPlanning(raw, handle);
         return wrapTableForScan(raw);
     }
 
+    private static void rejectServerSideScanPlanning(Table table, 
IcebergTableHandle handle) {
+        if (table instanceof SupportsDistributedScanPlanning
+                && !((SupportsDistributedScanPlanning) 
table).allowDistributedPlanning()) {
+            // Iceberg 1.11 marks REST server-planned tables this way. Doris 
reads manifests and table.io()
+            // before planFiles(), when REST scan-scoped credentials do not 
exist, so fail before any local I/O.
+            throw new DorisConnectorException("Iceberg server-side scan 
planning is not supported for table "
+                    + handle.getDbName() + "." + handle.getTableName()
+                    + "; configure the REST catalog to use client-side scan 
planning");
+        }
+    }
+
     /**
      * Loads the RAW iceberg table for {@code handle} through the cross-query 
{@link IcebergTableCache} when
      * enabled (the connector disables it for credential-dependent catalogs), 
else a direct remote
@@ -3160,6 +3187,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
                         () -> 
tableCache.borrow(TableIdentifier.of(handle.getDbName(), handle.getTableName()),
                                 () -> ops.loadTable(handle.getDbName(), 
handle.getTableName())),
                         () -> ops.loadTable(handle.getDbName(), 
handle.getTableName()));
+        rejectServerSideScanPlanning(base, handle);
         return MetadataTableUtils.createMetadataTableInstance(
                 base,
                 MetadataTableType.from(handle.getSysTableName()));
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSystemTableSerialization.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSystemTableSerialization.java
new file mode 100644
index 00000000000..97c702a4901
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSystemTableSerialization.java
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg;
+
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.util.SerializationUtil;
+
+import java.io.ObjectStreamClass;
+import java.io.ObjectStreamConstants;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/** Keeps the Java-serialized Iceberg system-table task wire compatible with 
Iceberg 1.10.1. */
+final class IcebergSystemTableSerialization {
+    static final long ICEBERG_1_10_1_SCHEMA_UID = 6812231194765760118L;
+    private static final long ICEBERG_1_11_0_SCHEMA_UID = 
-1265875184407129845L;
+    private static final long LOCAL_SCHEMA_UID = 
ObjectStreamClass.lookup(Schema.class).getSerialVersionUID();
+    private static final byte[] SCHEMA_CLASS_NAME = 
Schema.class.getName().getBytes(StandardCharsets.UTF_8);
+
+    private IcebergSystemTableSerialization() {
+    }
+
+    static String serializeToBase64(Object value) {
+        byte[] bytes = SerializationUtil.serializeToBytes(value);
+        rewriteSchemaUid(bytes, ICEBERG_1_11_0_SCHEMA_UID, 
ICEBERG_1_10_1_SCHEMA_UID);
+        return new String(Base64.getMimeEncoder().encode(bytes), 
StandardCharsets.UTF_8);
+    }
+
+    static <T> T deserializeFromBase64(String base64) {
+        byte[] bytes = 
Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
+        rewriteSchemaUid(bytes, ICEBERG_1_10_1_SCHEMA_UID, 
ICEBERG_1_11_0_SCHEMA_UID);
+        return SerializationUtil.deserializeFromBytes(bytes);
+    }
+
+    static long schemaUid(String base64) {
+        byte[] bytes = 
Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
+        int offset = schemaUidOffset(bytes);
+        return offset < 0 ? Long.MIN_VALUE : ByteBuffer.wrap(bytes, offset, 
Long.BYTES).getLong();
+    }
+
+    private static void rewriteSchemaUid(byte[] bytes, long fromUid, long 
toUid) {
+        // The 1.10 and 1.11 Schema classes have identical serialized fields. 
Pinning only this known descriptor
+        // to the legacy UID lets old and new BE readers share one task wire 
while future layouts fail closed.
+        if (LOCAL_SCHEMA_UID != ICEBERG_1_11_0_SCHEMA_UID) {
+            throw new IllegalStateException("Unsupported Iceberg Schema 
serialVersionUID: " + LOCAL_SCHEMA_UID);
+        }
+        int offset = schemaUidOffset(bytes);
+        if (offset >= 0 && ByteBuffer.wrap(bytes, offset, 
Long.BYTES).getLong() == fromUid) {
+            ByteBuffer.wrap(bytes, offset, Long.BYTES).putLong(toUid);
+        }
+    }
+
+    private static int schemaUidOffset(byte[] bytes) {
+        int descriptorSize = 1 + Short.BYTES + SCHEMA_CLASS_NAME.length + 
Long.BYTES;
+        for (int i = 0; i <= bytes.length - descriptorSize; i++) {
+            if (bytes[i] != ObjectStreamConstants.TC_CLASSDESC
+                    || bytes[i + 1] != (byte) (SCHEMA_CLASS_NAME.length >>> 8)
+                    || bytes[i + 2] != (byte) SCHEMA_CLASS_NAME.length) {
+                continue;
+            }
+            boolean matches = true;
+            for (int j = 0; j < SCHEMA_CLASS_NAME.length; j++) {
+                if (bytes[i + 3 + j] != SCHEMA_CLASS_NAME[j]) {
+                    matches = false;
+                    break;
+                }
+            }
+            if (matches) {
+                return i + 3 + SCHEMA_CLASS_NAME.length;
+            }
+        }
+        return -1;
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/dlf/DLFTableOperations.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/dlf/DLFTableOperations.java
index d0a894430f3..17dd2242e75 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/dlf/DLFTableOperations.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/dlf/DLFTableOperations.java
@@ -33,6 +33,7 @@ public class DLFTableOperations extends HiveTableOperations {
             String catalogName,
             String database,
             String table) {
-        super(conf, metaClients, fileIO, catalogName, database, table);
+        // DLF does not configure an Iceberg KMS client; null preserves the 
existing unencrypted behavior.
+        super(conf, metaClients, fileIO, null, catalogName, database, table);
     }
 }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/DeleteFileIndex.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/DeleteFileIndex.java
index 5f997bdfadd..4c45cfc1b3e 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/DeleteFileIndex.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/DeleteFileIndex.java
@@ -32,6 +32,9 @@ import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 import org.apache.iceberg.exceptions.RuntimeIOException;
 import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.iceberg.expressions.Expression;
@@ -64,7 +67,7 @@ import org.apache.iceberg.util.Tasks;
  * DataFile)} or {@link #forEntry(ManifestEntry)} to get the delete files to 
apply to a given data
  * file.
  * 
- * Copied from 
https://github.com/apache/iceberg/blob/apache-iceberg-1.9.1/core/src/main/java/org/apache/iceberg/DeleteFileIndex.java
+ * Copied from 
https://github.com/apache/iceberg/blob/apache-iceberg-1.11.0/core/src/main/java/org/apache/iceberg/DeleteFileIndex.java
  * Change DeleteFileIndex and some methods to public.
  *
  * <p>P6.2-T08 (catalog-spi): VENDORED into the iceberg connector module 
(mirrors the identical fe-core copy)
@@ -381,6 +384,7 @@ public class DeleteFileIndex {
     private final Iterable<DeleteFile> deleteFiles;
     private long minSequenceNumber = 0L;
     private Map<Integer, PartitionSpec> specsById = null;
+    private Map<Integer, Schema> schemasById = null;
     private Expression dataFilter = Expressions.alwaysTrue();
     private Expression partitionFilter = Expressions.alwaysTrue();
     private PartitionSet partitionSet = null;
@@ -406,6 +410,13 @@ public class DeleteFileIndex {
       return this;
     }
 
+    // Doris' cache-backed planner builds this index outside 
org.apache.iceberg and must preserve equality-delete
+    // fields from every historical schema, including fields absent from the 
current table schema.
+    public Builder schemasById(Map<Integer, Schema> newSchemasById) {
+      this.schemasById = newSchemasById;
+      return this;
+    }
+
     public Builder specsById(Map<Integer, PartitionSpec> newSpecsById) {
       this.specsById = newSpecsById;
       return this;
@@ -469,8 +480,14 @@ public class DeleteFileIndex {
                 try (CloseableIterable<ManifestEntry<DeleteFile>> reader = 
deleteFile) {
                   for (ManifestEntry<DeleteFile> entry : reader) {
                     if (entry.dataSequenceNumber() > minSequenceNumber) {
+                      DeleteFile file = entry.file();
+                      // keep minimum stats to avoid memory pressure
+                      Set<Integer> columns =
+                          file.content() == FileContent.POSITION_DELETES
+                              ? 
Collections.singleton(MetadataColumns.DELETE_FILE_PATH.fieldId())
+                              : Sets.newHashSet(file.equalityFieldIds());
                       // copy with stats for better filtering against data 
file stats
-                      files.add(entry.file().copy());
+                      files.add(ContentFileUtil.copy(file, true, columns));
                     }
                   }
                 } catch (IOException e) {
@@ -480,10 +497,21 @@ public class DeleteFileIndex {
       return files;
     }
 
+    private Collection<Schema> schemas() {
+      if (schemasById != null) {
+        return schemasById.values();
+      } else {
+        return 
specsById.values().stream().map(PartitionSpec::schema).collect(Collectors.toList());
+      }
+    }
+
     public DeleteFileIndex build() {
+      // Equality deletes may reference fields from historical schemas, so 
index every known field ID.
+      Map<Integer, Types.NestedField> fieldsById = 
Schema.indexFields(schemas());
+      Function<Integer, Types.NestedField> fieldLookup = fieldsById::get;
       Iterable<DeleteFile> files = deleteFiles != null ? filterDeleteFiles() : 
loadDeleteFiles();
 
-      EqualityDeletes globalDeletes = new EqualityDeletes();
+      EqualityDeletes globalDeletes = new EqualityDeletes(fieldLookup);
       PartitionMap<EqualityDeletes> eqDeletesByPartition = 
PartitionMap.create(specsById);
       PartitionMap<PositionDeletes> posDeletesByPartition = 
PartitionMap.create(specsById);
       Map<String, PositionDeletes> posDeletesByPath = Maps.newHashMap();
@@ -499,7 +527,7 @@ public class DeleteFileIndex {
             }
             break;
           case EQUALITY_DELETES:
-            add(globalDeletes, eqDeletesByPartition, file);
+            add(globalDeletes, eqDeletesByPartition, file, fieldLookup);
             break;
           default:
             throw new UnsupportedOperationException("Unsupported content: " + 
file.content());
@@ -546,7 +574,8 @@ public class DeleteFileIndex {
     private void add(
         EqualityDeletes globalDeletes,
         PartitionMap<EqualityDeletes> deletesByPartition,
-        DeleteFile file) {
+        DeleteFile file,
+        Function<Integer, Types.NestedField> fieldLookup) {
       PartitionSpec spec = specsById.get(file.specId());
 
       EqualityDeletes deletes;
@@ -555,10 +584,11 @@ public class DeleteFileIndex {
       } else {
         int specId = spec.specId();
         StructLike partition = file.partition();
-        deletes = deletesByPartition.computeIfAbsent(specId, partition, 
EqualityDeletes::new);
+        Supplier<EqualityDeletes> initEqDeletes = () -> new 
EqualityDeletes(fieldLookup);
+        deletes = deletesByPartition.computeIfAbsent(specId, partition, 
initEqDeletes);
       }
 
-      deletes.add(spec, file);
+      deletes.add(file);
     }
 
     private Iterable<CloseableIterable<ManifestEntry<DeleteFile>>> 
deleteManifestReaders() {
@@ -735,6 +765,8 @@ public class DeleteFileIndex {
         Comparator.comparingLong(EqualityDeleteFile::applySequenceNumber);
     private static final EqualityDeleteFile[] EMPTY_EQUALITY_DELETES = new 
EqualityDeleteFile[0];
 
+    private final Function<Integer, Types.NestedField> fieldLookup;
+
     // indexed state
     private long[] seqs = null;
     private EqualityDeleteFile[] files = null;
@@ -742,9 +774,13 @@ public class DeleteFileIndex {
     // a buffer that is used to hold files before indexing
     private volatile List<EqualityDeleteFile> buffer = Lists.newArrayList();
 
-    public void add(PartitionSpec spec, DeleteFile file) {
+    EqualityDeletes(Function<Integer, Types.NestedField> fieldLookup) {
+      this.fieldLookup = fieldLookup;
+    }
+
+    public void add(DeleteFile file) {
       Preconditions.checkState(buffer != null, "Can't add files upon 
indexing");
-      buffer.add(new EqualityDeleteFile(spec, file));
+      buffer.add(new EqualityDeleteFile(fieldLookup, file));
     }
 
     public DeleteFile[] filter(long seq, DataFile dataFile) {
@@ -810,15 +846,15 @@ public class DeleteFileIndex {
   // an equality delete file wrapper that caches the converted boundaries for 
faster boundary checks
   // this class is not meant to be exposed beyond the delete file index
   private static class EqualityDeleteFile {
-    private final PartitionSpec spec;
+    private final Function<Integer, Types.NestedField> fieldLookup;
     private final DeleteFile wrapped;
     private final long applySequenceNumber;
     private volatile List<Types.NestedField> equalityFields = null;
     private volatile Map<Integer, Object> convertedLowerBounds = null;
     private volatile Map<Integer, Object> convertedUpperBounds = null;
 
-    EqualityDeleteFile(PartitionSpec spec, DeleteFile file) {
-      this.spec = spec;
+    EqualityDeleteFile(Function<Integer, Types.NestedField> fieldLookup, 
DeleteFile file) {
+      this.fieldLookup = fieldLookup;
       this.wrapped = file;
       this.applySequenceNumber = wrapped.dataSequenceNumber() - 1;
     }
@@ -837,7 +873,8 @@ public class DeleteFileIndex {
           if (equalityFields == null) {
             List<Types.NestedField> fields = Lists.newArrayList();
             for (int id : wrapped.equalityFieldIds()) {
-              Types.NestedField field = spec.schema().findField(id);
+              Types.NestedField field = fieldLookup.apply(id);
+              Preconditions.checkArgument(field != null, "Cannot find field 
for ID %s", id);
               fields.add(field);
             }
             this.equalityFields = fields;
@@ -900,7 +937,7 @@ public class DeleteFileIndex {
       if (bounds != null) {
         for (Types.NestedField field : equalityFields()) {
           int id = field.fieldId();
-          Type type = spec.schema().findField(id).type();
+          Type type = field.type();
           if (type.isPrimitiveType()) {
             ByteBuffer bound = bounds.get(id);
             if (bound != null) {
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/SchemaAwareDataTableScan.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/SchemaAwareDataTableScan.java
new file mode 100644
index 00000000000..dbef7b6c44c
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/iceberg/SchemaAwareDataTableScan.java
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.iceberg;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** A data-table scan that keeps partition specs bound to the scan's schema. */
+public final class SchemaAwareDataTableScan extends DataTableScan {
+
+    private SchemaAwareDataTableScan(Table table, Schema schema, 
TableScanContext context) {
+        super(table, schema, context);
+    }
+
+    public static TableScan newScan(Table table) {
+        return new SchemaAwareDataTableScan(table, table.schema(), 
TableScanContext.empty());
+    }
+
+    /** Returns every table spec rebound to {@code schema}. */
+    public static Map<Integer, PartitionSpec> specsFor(Table table, Schema 
schema) {
+        if (schema.sameSchema(table.schema())) {
+            return table.specs();
+        }
+
+        Map<Integer, PartitionSpec> specs = new LinkedHashMap<>();
+        table.specs().forEach((id, spec) -> specs.put(id, 
spec.toUnbound().bind(schema, true)));
+        return Collections.unmodifiableMap(specs);
+    }
+
+    @Override
+    protected Map<Integer, PartitionSpec> specs() {
+        // A metadata-only schema commit preserves the current snapshot ID, so 
schema identity—not snapshot
+        // identity—must decide whether historical partition specs need 
rebinding.
+        return specsFor(table(), tableSchema());
+    }
+
+    @Override
+    protected TableScan newRefinedScan(Table table, Schema schema, 
TableScanContext context) {
+        return new SchemaAwareDataTableScan(table, schema, context);
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorCacheTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorCacheTest.java
index 97f4054640a..1b8a92ced7c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorCacheTest.java
@@ -403,7 +403,7 @@ public class IcebergConnectorCacheTest {
     // ============ PERF-02: partition-view cache (session=user gated) + 
invalidation ============
 
     private static IcebergPartitionCache.Key partKey(String db, String tbl, 
long snapshotId) {
-        return new IcebergPartitionCache.Key(TableIdentifier.of(db, tbl), 
snapshotId);
+        return new IcebergPartitionCache.Key(TableIdentifier.of(db, tbl), 
snapshotId, 0, 0);
     }
 
     @Test
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataPartitionViewCacheTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataPartitionViewCacheTest.java
index 337a23c50ad..204266c1536 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataPartitionViewCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataPartitionViewCacheTest.java
@@ -50,11 +50,11 @@ import java.util.stream.Collectors;
  * {@link ConnectorMetadataCache}) wired into {@link 
IcebergConnectorMetadata#getMvccPartitionView} /
  * {@link IcebergConnectorMetadata#listPartitions}. Uses the real {@link 
InMemoryCatalog} +
  * {@link RecordingIcebergCatalogOps} harness (no Mockito, no docker): the 
cache sits ABOVE the per-query build,
- * whose first step is {@code resolveTableForRead -> catalogOps.loadTable} 
(logged as {@code loadTable:db1.t1}), so
- * a cache HIT skips the whole loader and the {@code loadTable} count is the 
enumeration counter — the SAME proxy
- * the sibling cache tests use ({@code 
IcebergConnectorMetadataMvccTest.beginQuerySnapshot*Cache*}). The raw
- * partition cache (PERF-02) is passed null throughout, so the {@code 
loadTable} count isolates cache A's effect.
- * The partition math/merge parity itself is covered by {@link 
IcebergPartitionUtilsTest}.
+ * whose first step is {@code resolveTableForRead -> catalogOps.loadTable} 
(logged as {@code loadTable:db1.t1}).
+ * The spec generation must be read before each lookup because an external 
spec-only commit changes neither the
+ * snapshot nor schema id; object identity therefore proves a derived-cache 
hit while load counts prove generation
+ * checks still happen. The raw partition cache (PERF-02) is passed null 
throughout, so object identity comes
+ * directly from cache A. The partition math/merge parity itself is covered by 
{@link IcebergPartitionUtilsTest}.
  */
 public class IcebergConnectorMetadataPartitionViewCacheTest {
 
@@ -191,9 +191,9 @@ public class IcebergConnectorMetadataPartitionViewCacheTest 
{
 
     @Test
     public void getMvccPartitionViewCachesDerivedViewAcrossQueries() {
-        // WHY: cache A must memoize the BUILT MVCC view keyed by (db, table, 
snapshotId, schemaId), so a repeated
-        // query on the same pin skips the derived rebuild AND the underlying 
loadTable/scan. MUTATION: not
-        // consulting the cache (compute directly every call) -> loadTable 
runs twice -> red.
+        // WHY: cache A must memoize the BUILT MVCC view keyed by 
snapshot/schema/spec generation, so a repeated
+        // query on the same generation skips the derived rebuild. Resolving 
the live table is still required to
+        // detect spec-only commits that do not change snapshot/schema ids.
         TwoSnap f = twoSnapshotTable();
         RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
         ops.table = f.table;
@@ -205,7 +205,9 @@ public class IcebergConnectorMetadataPartitionViewCacheTest 
{
 
         Assertions.assertEquals(java.util.Arrays.asList("ts_day=100", 
"ts_day=101"), mvccNames(first));
         Assertions.assertEquals(mvccNames(first), mvccNames(second), "the 
cached view is returned verbatim");
-        Assertions.assertEquals(1, loadCount(ops), "a cache hit must not 
re-enumerate (loadTable once)");
+        Assertions.assertSame(first.orElseThrow(), second.orElseThrow(),
+                "a cache hit must return the same derived view instance");
+        Assertions.assertEquals(2, loadCount(ops), "each lookup must resolve 
the live spec generation");
     }
 
     @Test
@@ -259,6 +261,28 @@ public class 
IcebergConnectorMetadataPartitionViewCacheTest {
         Assertions.assertEquals(2, loadCount(ops), "a null (disabled) cache 
must re-enumerate every call");
     }
 
+    @Test
+    public void getMvccPartitionViewSpecOnlyCommitDoesNotReuseDerivedView() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("test", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db1"));
+        PartitionSpec daySpec = 
PartitionSpec.builderFor(PARTITIONED_SCHEMA).day("ts").build();
+        Table table = catalog.createTable(TableIdentifier.of("db1", "t1"), 
PARTITIONED_SCHEMA, daySpec,
+                Collections.singletonMap("format-version", "2"));
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = table;
+        IcebergConnectorMetadata md = metadataWithMvccCache(ops, mvccCache());
+
+        ConnectorMvccPartitionView before = md.getMvccPartitionView(null, 
handle()).orElseThrow();
+        table.updateSpec().removeField("ts_day").addField("id").commit();
+        ops.table = catalog.loadTable(TableIdentifier.of("db1", "t1"));
+        ConnectorMvccPartitionView after = md.getMvccPartitionView(null, 
handle()).orElseThrow();
+
+        Assertions.assertEquals(ConnectorMvccPartitionView.Style.RANGE, 
before.getStyle());
+        
Assertions.assertEquals(ConnectorMvccPartitionView.Style.UNPARTITIONED, 
after.getStyle(),
+                "a spec-only commit must not reuse the preceding derived 
partition view");
+    }
+
     @Test
     public void resolvedEmptyPartitionViewIgnoresConcurrentFirstAppend() {
         InMemoryCatalog catalog = new InMemoryCatalog();
@@ -347,8 +371,8 @@ public class IcebergConnectorMetadataPartitionViewCacheTest 
{
 
     @Test
     public void listPartitionsCachesDerivedListAcrossQueries() {
-        // WHY: the empty-filter pruning path must memoize the built 
partition-info list. MUTATION: not consulting
-        // the cache -> loadTable twice -> red.
+        // WHY: the empty-filter pruning path must memoize the built 
partition-info list while resolving the live
+        // table on every lookup to detect spec-only commits.
         TwoSnap f = twoSnapshotTable();
         RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
         ops.table = f.table;
@@ -362,7 +386,8 @@ public class IcebergConnectorMetadataPartitionViewCacheTest 
{
         Assertions.assertEquals(java.util.Arrays.asList("ts_day=100", 
"ts_day=101"), names);
         Assertions.assertEquals(names,
                 
second.stream().map(ConnectorPartitionInfo::getPartitionName).collect(Collectors.toList()));
-        Assertions.assertEquals(1, loadCount(ops), "a cache hit must not 
re-enumerate (loadTable once)");
+        Assertions.assertSame(first, second, "a cache hit must return the same 
derived list instance");
+        Assertions.assertEquals(2, loadCount(ops), "each lookup must resolve 
the live spec generation");
     }
 
     @Test
@@ -401,4 +426,17 @@ public class 
IcebergConnectorMetadataPartitionViewCacheTest {
         md.listPartitions(null, handle(), Optional.empty());
         Assertions.assertEquals(2, loadCount(ops), "invalidateAll must force a 
re-enumeration");
     }
+
+    @Test
+    public void listPartitionsSpecOnlyCommitDoesNotReuseDerivedList() {
+        TwoSnap f = twoSnapshotTable();
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = f.table;
+        IcebergConnectorMetadata md = metadataWithListCache(ops, listCache());
+
+        Assertions.assertEquals(2, md.listPartitions(null, handle(), 
Optional.empty()).size());
+        f.table.updateSpec().removeField("ts_day").commit();
+        Assertions.assertTrue(md.listPartitions(null, handle(), 
Optional.empty()).isEmpty(),
+                "a spec-only commit must not reuse a list built for the 
preceding partition spec");
+    }
 }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionCacheTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionCacheTest.java
index 1cc83596399..315acb132b4 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionCacheTest.java
@@ -32,14 +32,13 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Unit tests for {@link IcebergPartitionCache} (PERF-02). Mirrors {@link 
IcebergTableCacheTest} but keys by
- * {@code (TableIdentifier, snapshotId)} and stores the raw partition list. 
Covers within-TTL stability, the
- * {@code ttl <= 0} disable, invalidation, and the exception-propagation 
guarantee that {@code listPartitions}'
- * dropped-partition-source-column degradation depends on.
+ * {@code (TableIdentifier, snapshotId, schemaId, specId)} and stores the raw 
partition list. Covers within-TTL
+ * stability, the {@code ttl <= 0} disable, invalidation, and exception 
propagation.
  */
 public class IcebergPartitionCacheTest {
 
     private static IcebergPartitionCache.Key key(String db, String tbl, long 
snapshotId) {
-        return new IcebergPartitionCache.Key(TableIdentifier.of(db, tbl), 
snapshotId);
+        return new IcebergPartitionCache.Key(TableIdentifier.of(db, tbl), 
snapshotId, 0, 0);
     }
 
     /** A raw partition list of the given size, distinguishable by size. */
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
index 6afffa80d48..5e6d2c744be 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergPartitionUtilsTest.java
@@ -24,18 +24,13 @@ import 
org.apache.doris.connector.spi.mvcc.ConnectorMvccPartitionView;
 
 import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.FileFormat;
-import org.apache.iceberg.FileScanTask;
-import org.apache.iceberg.MetadataTableType;
-import org.apache.iceberg.MetadataTableUtils;
 import org.apache.iceberg.PartitionData;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
-import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.iceberg.inmemory.InMemoryCatalog;
-import org.apache.iceberg.io.CloseableIterable;
 import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -1099,13 +1094,11 @@ public class IcebergPartitionUtilsTest {
     }
 
     @Test
-    public void 
listPartitionsDegradesToEmptyWhenPartitionSourceColumnDropped() {
+    public void 
listPartitionsDegradesWhenDroppedPartitionSourceCannotBeRepresented() {
         // Partition-evolution regression 
(external_table_p0/iceberg/test_iceberg_partition_evolution): a
-        // HISTORICAL spec references a source column that was later DROPPED, 
while the CURRENT spec stays
-        // partitioned on a surviving column. Building the PARTITIONS metadata 
table unifies the partition type
-        // across ALL specs, so iceberg throws ValidationException ("Cannot 
find source column for partition
-        // field: ...") for the orphaned field. listPartitions is 
display/enforcement metadata only (never the
-        // read set), so it must degrade to an empty (UNPARTITIONED) list 
instead of failing the whole query.
+        // HISTORICAL spec references a source column that is later DROPPED, 
while the CURRENT spec stays
+        // partitioned on a surviving column. Iceberg 1.11 removes the 
historical field from the unified
+        // partition type, so distinct old buckets become unrepresentable and 
must not be reported as null.
         InMemoryCatalog catalog = new InMemoryCatalog();
         catalog.initialize("test", Collections.emptyMap());
         catalog.createNamespace(Namespace.of("db1"));
@@ -1119,37 +1112,94 @@ public class IcebergPartitionUtilsTest {
                 Collections.singletonMap("format-version", "2"));
         // A data file under the original (bucket-on-region) spec so the 
PARTITIONS metadata scan has a row whose
         // spec must be unified.
-        table.newAppend().appendFile(DataFiles.builder(table.spec())
-                
.withPath("s3://b/db1/t/f0.parquet").withFileSizeInBytes(100).withRecordCount(1)
-                
.withPartitionPath("region_bucket=1").withFormat(FileFormat.PARQUET).build()).commit();
+        table.newAppend()
+                .appendFile(DataFiles.builder(table.spec())
+                        
.withPath("s3://b/db1/t/f0.parquet").withFileSizeInBytes(100).withRecordCount(1)
+                        
.withPartitionPath("region_bucket=1").withFormat(FileFormat.PARQUET).build())
+                .appendFile(DataFiles.builder(table.spec())
+                        
.withPath("s3://b/db1/t/f1.parquet").withFileSizeInBytes(100).withRecordCount(1)
+                        
.withPartitionPath("region_bucket=2").withFormat(FileFormat.PARQUET).build())
+                .commit();
         // Evolve: drop the bucket(region) partition field and add 
identity(id) — the CURRENT spec stays
         // PARTITIONED (on the surviving id column), so listPartitions passes 
the isUnpartitioned() early-return
         // and genuinely reaches the metadata scan (guards this test against a 
vacuous unpartitioned pass).
         
table.updateSpec().removeField("region_bucket").addField("id").commit();
         table.newAppend().appendFile(DataFiles.builder(table.spec())
-                
.withPath("s3://b/db1/t/f1.parquet").withFileSizeInBytes(100).withRecordCount(1)
+                
.withPath("s3://b/db1/t/f2.parquet").withFileSizeInBytes(100).withRecordCount(1)
                 
.withPartitionPath("id=5").withFormat(FileFormat.PARQUET).build()).commit();
+        IcebergPartitionCache cache = new IcebergPartitionCache(100, 1000);
+        List<String> beforeDrop = 
IcebergPartitionUtils.listPartitions(catalog.loadTable(id), id, cache).stream()
+                
.map(ConnectorPartitionInfo::getPartitionName).sorted().collect(Collectors.toList());
+        Assertions.assertEquals(Arrays.asList("id=5", "region_bucket=1", 
"region_bucket=2"), beforeDrop);
+        long snapshotBeforeDrop = table.currentSnapshot().snapshotId();
         // Drop the source column referenced only by the historical spec, 
leaving that spec dangling.
         table.updateSchema().deleteColumn("region").commit();
         Table evolved = catalog.loadTable(id);
+        Assertions.assertEquals(snapshotBeforeDrop, 
evolved.currentSnapshot().snapshotId(),
+                "schema-only evolution must keep the same snapshot and 
exercise the cache-key boundary");
         Assertions.assertTrue(evolved.spec().isPartitioned(),
                 "current spec must stay partitioned so listPartitions reaches 
the metadata scan, not the "
                         + "unpartitioned early-return (otherwise this test 
would pass vacuously)");
 
-        // Precondition — prove the raw iceberg partition-metadata scan 
genuinely throws ValidationException here
-        // (guards against a future iceberg that tolerates the dangling spec, 
which would make this test vacuous).
-        Assertions.assertThrows(ValidationException.class, () -> {
-            Table partitionsTable = 
MetadataTableUtils.createMetadataTableInstance(
-                    evolved, MetadataTableType.PARTITIONS);
-            try (CloseableIterable<FileScanTask> tasks = 
partitionsTable.newScan().planFiles()) {
-                tasks.forEach(t -> { });
-            }
-        });
-
-        // The fix: listPartitions swallows exactly that failure and reports 
UNPARTITIONED (empty), so a
-        // full-table select on such a table is not blocked by uncomputable 
display metadata. MUTATION:
-        // rethrowing (or removing the catch) -> this throws instead of 
returning empty -> red.
-        
Assertions.assertTrue(IcebergPartitionUtils.listPartitions(evolved).isEmpty());
+        Assertions.assertTrue(IcebergPartitionUtils.listPartitions(evolved, 
id, cache).isEmpty(),
+                "unrepresentable historical buckets must degrade to an empty 
partition display");
+        Assertions.assertEquals(2, cache.loadCountForTest(),
+                "a schema-only commit must not reuse partition metadata cached 
under the preceding schema");
+    }
+
+    @Test
+    public void listPartitionsIgnoresUnrepresentableSpecWithoutLiveFiles() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("test", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db1"));
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "region", 
Types.StringType.get()));
+        TableIdentifier id = TableIdentifier.of("db1", "live_specs_only");
+        Table table = catalog.createTable(id, schema,
+                PartitionSpec.builderFor(schema).bucket("region", 8).build(),
+                Collections.singletonMap("format-version", "2"));
+        
table.updateSpec().removeField("region_bucket").addField("id").commit();
+        table.newAppend().appendFile(DataFiles.builder(table.spec())
+                .withPath("s3://b/db1/live_specs_only/f.parquet")
+                .withFileSizeInBytes(100).withRecordCount(1)
+                
.withPartitionPath("id=5").withFormat(FileFormat.PARQUET).build()).commit();
+        table.updateSchema().deleteColumn("region").commit();
+
+        List<ConnectorPartitionInfo> partitions = 
IcebergPartitionUtils.listPartitions(
+                catalog.loadTable(id), id, null);
+
+        Assertions.assertEquals(1, partitions.size());
+        Assertions.assertEquals("id=5", partitions.get(0).getPartitionName(),
+                "an unrepresentable historical spec with no live files must 
not hide live partitions");
+    }
+
+    @Test
+    public void listPartitionsReadsValuesFromUnifiedPartitionStructByFieldId() 
{
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("test", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db1"));
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "region", 
Types.StringType.get()));
+        TableIdentifier id = TableIdentifier.of("db1", 
"unified_partition_values");
+        Table table = catalog.createTable(id, schema,
+                PartitionSpec.builderFor(schema).bucket("region", 8).build(),
+                Collections.singletonMap("format-version", "2"));
+        table.newAppend().appendFile(DataFiles.builder(table.spec())
+                
.withPath("s3://b/db1/t/f0.parquet").withFileSizeInBytes(100).withRecordCount(1)
+                
.withPartitionPath("region_bucket=1").withFormat(FileFormat.PARQUET).build()).commit();
+        
table.updateSpec().removeField("region_bucket").addField("id").commit();
+        table.newAppend().appendFile(DataFiles.builder(table.spec())
+                
.withPath("s3://b/db1/t/f1.parquet").withFileSizeInBytes(100).withRecordCount(1)
+                
.withPartitionPath("id=5").withFormat(FileFormat.PARQUET).build()).commit();
+
+        List<ConnectorPartitionInfo> partitions = 
IcebergPartitionUtils.listPartitions(
+                catalog.loadTable(id), id, null);
+        List<String> names = 
partitions.stream().map(ConnectorPartitionInfo::getPartitionName)
+                .sorted().collect(java.util.stream.Collectors.toList());
+
+        Assertions.assertEquals(Arrays.asList("id=5", "region_bucket=1"), 
names);
     }
 
     @Test
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
index 512c2ffa338..27b453c9c5c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergScanPlanProviderTest.java
@@ -45,6 +45,7 @@ import org.apache.doris.thrift.schema.external.TFieldPtr;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import org.apache.hadoop.conf.Configuration;
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.DeleteFile;
@@ -58,11 +59,14 @@ import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.StructLike;
+import org.apache.iceberg.SupportsDistributedScanPlanning;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TableScan;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.expressions.Literal;
+import org.apache.iceberg.hadoop.HadoopTables;
 import org.apache.iceberg.inmemory.InMemoryCatalog;
 import org.apache.iceberg.io.CloseableIterable;
 import org.apache.iceberg.io.FileIO;
@@ -73,15 +77,16 @@ import org.apache.iceberg.io.SupportsStorageCredentials;
 import org.apache.iceberg.types.Conversions;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.types.Types.NestedField;
-import org.apache.iceberg.util.SerializationUtil;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Proxy;
 import java.nio.ByteBuffer;
+import java.nio.file.Path;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
@@ -107,6 +112,9 @@ import java.util.function.UnaryOperator;
  */
 public class IcebergScanPlanProviderTest {
 
+    @TempDir
+    Path tempDir;
+
     private static final Schema SCHEMA = new Schema(
             Types.NestedField.required(1, "id", Types.IntegerType.get()),
             Types.NestedField.optional(2, "name", Types.StringType.get()));
@@ -128,6 +136,15 @@ public class IcebergScanPlanProviderTest {
         return catalog.createTable(TableIdentifier.of("db1", name), schema, 
spec, null, props);
     }
 
+    private Table createPersistedTable(String name, Schema schema, 
PartitionSpec spec, Map<String, String> props) {
+        return new HadoopTables(new Configuration()).create(
+                schema, spec, props, tempDir.resolve(name).toUri().toString());
+    }
+
+    private static Table reloadPersistedTable(Table table) {
+        return new HadoopTables(new Configuration()).load(table.location());
+    }
+
     private static DataFile dataFile(PartitionSpec spec, String path, long 
sizeBytes, List<Long> splitOffsets,
             String partitionPath) {
         return dataFile(spec, path, sizeBytes, splitOffsets, partitionPath, 
FileFormat.PARQUET);
@@ -159,6 +176,20 @@ public class IcebergScanPlanProviderTest {
                 (proxy, method, args) -> method.getName().equals("io") ? 
fileIO : invoke(method, table, args));
     }
 
+    private static Table serverPlannedTable(Table table, AtomicBoolean 
localMetadataAccessed) {
+        return (Table) Proxy.newProxyInstance(Table.class.getClassLoader(),
+                new Class<?>[] {Table.class, 
SupportsDistributedScanPlanning.class},
+                (proxy, method, args) -> {
+                    if (method.getName().equals("allowDistributedPlanning")) {
+                        return false;
+                    }
+                    if (method.getName().equals("newScan") || 
method.getName().equals("io")) {
+                        localMetadataAccessed.set(true);
+                    }
+                    return invoke(method, table, args);
+                });
+    }
+
     private static Table tableWithMissingManifestRowsAndIo(Table table, FileIO 
fileIO) {
         return (Table) Proxy.newProxyInstance(Table.class.getClassLoader(), 
new Class<?>[] {Table.class},
                 (proxy, method, args) -> {
@@ -298,6 +329,58 @@ public class IcebergScanPlanProviderTest {
         Assertions.assertEquals("t1", ops.lastLoadTable);
     }
 
+    @Test
+    public void planScanRejectsServerPlanningBeforeReplacingHistoricalScan() {
+        Table table = createTable("t1", SCHEMA, PartitionSpec.unpartitioned());
+        table.newAppend().appendFile(dataFile(
+                table.spec(), "s3://b/db1/t1/f.parquet", 100, null, 
null)).commit();
+        long historicalSnapshotId = table.currentSnapshot().snapshotId();
+        int historicalSchemaId = table.schema().schemaId();
+        table.updateSchema().renameColumn("name", "renamed_name").commit();
+        AtomicBoolean localMetadataAccessed = new AtomicBoolean();
+        IcebergScanPlanProvider provider = 
providerOver(serverPlannedTable(table, localMetadataAccessed));
+        IcebergTableHandle handle = new IcebergTableHandle("db1", "t1")
+                .withSnapshot(historicalSnapshotId, null, historicalSchemaId);
+
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> provider.planScan(emptySession(),
+                        ConnectorScanRequest.builder(handle, 
Collections.emptyList()).build()));
+
+        Assertions.assertTrue(failure.getMessage().contains("server-side scan 
planning"), failure.getMessage());
+        Assertions.assertFalse(localMetadataAccessed.get(),
+                "rejection must happen before replacing the native scan or 
opening local metadata");
+    }
+
+    @Test
+    public void 
streamingEstimateRejectsServerPlanningBeforeReadingLocalManifests() {
+        Table table = threeFileTable();
+        AtomicBoolean localMetadataAccessed = new AtomicBoolean();
+        IcebergScanPlanProvider provider = 
providerOver(serverPlannedTable(table, localMetadataAccessed));
+
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> provider.streamingSplitEstimate(batchSession(1, true),
+                        new IcebergTableHandle("db1", "t1"), Optional.empty(), 
false));
+
+        Assertions.assertTrue(failure.getMessage().contains("server-side scan 
planning"), failure.getMessage());
+        Assertions.assertFalse(localMetadataAccessed.get(),
+                "streaming dispatch must reject before table.io() reads local 
manifests");
+    }
+
+    @Test
+    public void 
scanPropertiesRejectServerPlanningBeforeReadingTableCredentials() {
+        Table table = threeFileTable();
+        AtomicBoolean localMetadataAccessed = new AtomicBoolean();
+        IcebergScanPlanProvider provider = 
providerOver(serverPlannedTable(table, localMetadataAccessed));
+
+        DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> provider.getScanNodeProperties(emptySession(), new 
IcebergTableHandle("db1", "t1"),
+                        Collections.emptyList(), Optional.empty()));
+
+        Assertions.assertTrue(failure.getMessage().contains("server-side scan 
planning"), failure.getMessage());
+        Assertions.assertFalse(localMetadataAccessed.get(),
+                "scan properties must reject before reading table.io() instead 
of planned-scan credentials");
+    }
+
     @Test
     public void planScanMissingMetadataFileHasStableTableError() {
         RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
@@ -315,11 +398,13 @@ public class IcebergScanPlanProviderTest {
 
     @Test
     public void planScanMissingManifestListHasStableTableError() {
-        Table table = createTable("missing_manifest_list", SCHEMA, 
PartitionSpec.unpartitioned());
+        Table table = createPersistedTable(
+                "missing_manifest_list", SCHEMA, 
PartitionSpec.unpartitioned(), Collections.emptyMap());
         table.newAppend().appendFile(dataFile(table.spec(),
                 "s3://b/db/missing_manifest_list/f1.parquet", 1024, null, 
null)).commit();
         table.io().deleteFile(table.currentSnapshot().manifestListLocation());
-        IcebergScanPlanProvider provider = providerOver(table);
+        // Iceberg 1.11 caches parsed manifests on Snapshot; reload from 
metadata to exercise the missing file.
+        IcebergScanPlanProvider provider = 
providerOver(reloadPersistedTable(table));
 
         DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
                 () -> provider.planScan(emptySession(), 
ConnectorScanRequest.builder(
@@ -330,11 +415,13 @@ public class IcebergScanPlanProviderTest {
 
     @Test
     public void streamingMissingManifestListHasStableTableError() {
-        Table table = createTable("missing_stream_manifest", SCHEMA, 
PartitionSpec.unpartitioned());
+        Table table = createPersistedTable(
+                "missing_stream_manifest", SCHEMA, 
PartitionSpec.unpartitioned(), Collections.emptyMap());
         table.newAppend().appendFile(dataFile(table.spec(),
                 "s3://b/db/missing_stream_manifest/f1.parquet", 1024, null, 
null)).commit();
         table.io().deleteFile(table.currentSnapshot().manifestListLocation());
-        IcebergScanPlanProvider provider = providerOver(table);
+        // Iceberg 1.11 caches parsed manifests on Snapshot; reload from 
metadata to exercise the missing file.
+        IcebergScanPlanProvider provider = 
providerOver(reloadPersistedTable(table));
         IcebergTableHandle handle = new IcebergTableHandle("db1", 
"missing_stream_manifest");
 
         DorisConnectorException estimate = 
Assertions.assertThrows(DorisConnectorException.class,
@@ -1539,7 +1626,7 @@ public class IcebergScanPlanProviderTest {
                 .build());
 
         Assertions.assertEquals(1, ranges.size(), "one data file -> one 
metadata split");
-        FileScanTask task = SerializationUtil.deserializeFromBase64(
+        FileScanTask task = 
IcebergSystemTableSerialization.deserializeFromBase64(
                 ((IcebergScanRange) ranges.get(0)).getSerializedSplit());
         Assertions.assertNotNull(task.schema().findField("file_size_in_bytes"),
                 "the requested column must survive in the projected task 
schema");
@@ -1582,7 +1669,7 @@ public class IcebergScanPlanProviderTest {
                 .build());
 
         Assertions.assertEquals(1, ranges.size(), "one commit -> one 
$snapshots metadata split");
-        FileScanTask task = SerializationUtil.deserializeFromBase64(
+        FileScanTask task = 
IcebergSystemTableSerialization.deserializeFromBase64(
                 ((IcebergScanRange) ranges.get(0)).getSerializedSplit());
         try (CloseableIterable<StructLike> rows = task.asDataTask().rows()) {
             Iterator<StructLike> it = rows.iterator();
@@ -1648,11 +1735,18 @@ public class IcebergScanPlanProviderTest {
                 .withFileSizeInBytes(100)
                 .withRecordCount(1)
                 .build();
-        Table table = tableWithPositionDelete(deleteFile);
+        Table table = createPersistedTable(
+                "missing_position_delete_manifest", SCHEMA, 
PartitionSpec.unpartitioned(), Collections.emptyMap());
+        table.newAppend()
+                .appendFile(dataFile(table.spec(), "s3://b/db/t1/f1.parquet", 
512, null, null))
+                .commit();
+        table.newRowDelta().addDeletes(deleteFile).commit();
         table.io().deleteFile(table.currentSnapshot().manifestListLocation());
+        // Iceberg 1.11 caches parsed manifests on Snapshot; reload from 
metadata to exercise the missing file.
+        Table reloaded = reloadPersistedTable(table);
 
         DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
-                () -> planPositionDeletes(table, Collections.emptyList()));
+                () -> planPositionDeletes(reloaded, Collections.emptyList()));
         Assertions.assertTrue(ex.getMessage().contains(
                 "Metadata not found in metadata location for table db1.t1"), 
ex.getMessage());
     }
@@ -1764,6 +1858,18 @@ public class IcebergScanPlanProviderTest {
                 "a metadata table is still not base-spec partitioned -> no 
path_partition_keys");
     }
 
+    @Test
+    public void jniSystemTableNeedsNoRollingUpgradeFence() {
+        Table table = createTable("sys_upgrade", SCHEMA, 
PartitionSpec.unpartitioned());
+        IcebergScanPlanProvider provider = providerOver(table);
+
+        Map<String, String> props = provider.getScanNodeProperties(
+                null, IcebergTableHandle.forSystemTable("db1", "sys_upgrade", 
"snapshots", -1L, null, -1L),
+                Collections.emptyList(), Optional.empty());
+
+        
Assertions.assertFalse(props.containsKey(ScanNodePropertyKeys.REQUIRED_CURRENT_BACKEND_SEMANTICS));
+    }
+
     @Test
     public void 
getScanNodePropertiesForPositionDeletesRowRequiresCurrentBackendSemantics() {
         Table table = tableWithPositionDelete(
@@ -1832,6 +1938,97 @@ public class IcebergScanPlanProviderTest {
         
Assertions.assertTrue(pinned.get(0).getPath().get().endsWith("f1.parquet"));
     }
 
+    @Test
+    public void planScanHistoricalPredicateSurvivesColumnRename() {
+        assertHistoricalPredicatePlansAfterSchemaEvolution(false);
+    }
+
+    @Test
+    public void planScanHistoricalPredicateSurvivesColumnDrop() {
+        assertHistoricalPredicatePlansAfterSchemaEvolution(true);
+    }
+
+    private void assertHistoricalPredicatePlansAfterSchemaEvolution(boolean 
dropColumn) {
+        Schema historicalSchema = new Schema(
+                Types.NestedField.optional(1, "x", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "y", Types.IntegerType.get()),
+                Types.NestedField.optional(3, "part", 
Types.IntegerType.get()));
+        Table table = createTable(
+                "historical_predicate_after_" + (dropColumn ? "drop" : 
"rename"),
+                historicalSchema, PartitionSpec.unpartitioned(),
+                Collections.singletonMap(TableProperties.FORMAT_VERSION, "2"));
+        table.newFastAppend()
+                .appendFile(dataFile(table.spec(), 
"s3://b/db/historical.parquet", 1024, null, null))
+                .commit();
+        long historicalSnapshotId = table.currentSnapshot().snapshotId();
+        int historicalSchemaId = table.currentSnapshot().schemaId();
+
+        if (dropColumn) {
+            table.updateSchema().deleteColumn("x").commit();
+        } else {
+            table.updateSchema().renameColumn("x", "renamed_x").commit();
+        }
+
+        assertHistoricalPredicatePlans(table, historicalSnapshotId, 
historicalSchemaId);
+
+        table.newFastAppend()
+                .appendFile(dataFile(table.spec(), 
"s3://b/db/current.parquet", 1024, null, null))
+                .commit();
+
+        assertHistoricalPredicatePlans(table, historicalSnapshotId, 
historicalSchemaId);
+    }
+
+    private static void assertHistoricalPredicatePlans(
+            Table table, long historicalSnapshotId, int historicalSchemaId) {
+        IcebergTableHandle historicalHandle = new IcebergTableHandle("db1", 
"t1")
+                .withSnapshot(historicalSnapshotId, null, historicalSchemaId);
+        List<ConnectorScanRange> ranges = providerOver(table).planScan(
+                emptySession(), ConnectorScanRequest.builder(historicalHandle, 
Collections.emptyList())
+                        .filter(Optional.of(eqInt("x", 1)))
+                        .build());
+
+        // Historical predicates must remain bound to the snapshot schema 
after later schema evolution.
+        Assertions.assertEquals(1, ranges.size());
+        
Assertions.assertTrue(ranges.get(0).getPath().get().endsWith("historical.parquet"));
+    }
+
+    @Test
+    public void 
streamingDispatchRebindsPartitionSpecsForHistoricalSchemaBeforeAndAfterSnapshotAdvance()
+            throws IOException {
+        Schema historicalSchema = new Schema(
+                Types.NestedField.optional(1, "x", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "value", 
Types.StringType.get()));
+        PartitionSpec historicalSpec = 
PartitionSpec.builderFor(historicalSchema).identity("x").build();
+        Table table = createTable("historical_batch_estimate", 
historicalSchema, historicalSpec,
+                Collections.singletonMap(TableProperties.FORMAT_VERSION, "2"));
+        table.newAppend().appendFile(dataFile(table.spec(),
+                "s3://b/db/historical_batch_estimate/old.parquet", 1024, null, 
"x=1")).commit();
+        long historicalSnapshotId = table.currentSnapshot().snapshotId();
+        int historicalSchemaId = table.currentSnapshot().schemaId();
+        table.updateSchema().renameColumn("x", "renamed_x").commit();
+
+        assertHistoricalStreamingDispatch(table, historicalSnapshotId, 
historicalSchemaId);
+
+        table.newAppend().appendFile(dataFile(table.spec(),
+                "s3://b/db/historical_batch_estimate/new.parquet", 1024, null, 
"x=2")).commit();
+        assertHistoricalStreamingDispatch(table, historicalSnapshotId, 
historicalSchemaId);
+    }
+
+    private static void assertHistoricalStreamingDispatch(
+            Table table, long historicalSnapshotId, int historicalSchemaId) 
throws IOException {
+        IcebergTableHandle handle = new IcebergTableHandle("db1", 
"historical_batch_estimate")
+                .withSnapshot(historicalSnapshotId, null, historicalSchemaId);
+        IcebergScanPlanProvider provider = providerOver(table);
+        ConnectorSession session = batchSession(1, true);
+        Optional<ConnectorExpression> filter = Optional.of(eqInt("x", 1));
+
+        Assertions.assertEquals(1L, provider.streamingSplitEstimate(session, 
handle, filter, false));
+        List<ConnectorScanRange> ranges = drain(provider.streamSplits(
+                session, handle, Collections.emptyList(), filter, -1L));
+        Assertions.assertEquals(1, ranges.size());
+        
Assertions.assertTrue(ranges.get(0).getPath().get().endsWith("old.parquet"));
+    }
+
     @Test
     public void planScanResolvedEmptySnapshotIgnoresConcurrentFirstAppend() {
         // MERGE may resolve its read snapshot while the target has no 
snapshots, then race with the first append.
@@ -1945,6 +2142,29 @@ public class IcebergScanPlanProviderTest {
         
Assertions.assertTrue(pinned.get(0).getPath().get().endsWith("f1.parquet"));
     }
 
+    @Test
+    public void planScanPinnedToBranchBindsPredicateToCurrentSchema() {
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "score", 
Types.IntegerType.get()));
+        Table table = createTable("branch_predicate", schema, 
PartitionSpec.unpartitioned());
+        table.newAppend().appendFile(dataFile(
+                table.spec(), "s3://b/db/branch_predicate/f1.parquet", 1024, 
null, null)).commit();
+        long branchSnapshotId = table.currentSnapshot().snapshotId();
+        table.manageSnapshots().createBranch("b1", branchSnapshotId).commit();
+        table.updateSchema().renameColumn("score", "grade").commit();
+        IcebergTableHandle branchHandle = new IcebergTableHandle("db1", 
"branch_predicate")
+                .withSnapshot(branchSnapshotId, "b1", 
table.schema().schemaId());
+
+        List<ConnectorScanRange> ranges = providerOver(table).planScan(
+                emptySession(), ConnectorScanRequest.builder(branchHandle, 
Collections.emptyList())
+                        .filter(Optional.of(eqInt("grade", 1)))
+                        .build());
+
+        Assertions.assertEquals(1, ranges.size());
+        
Assertions.assertTrue(ranges.get(0).getPath().get().endsWith("f1.parquet"));
+    }
+
     @Test
     public void countPushdownFollowsTheSnapshotPin() {
         // f1=1000/100=10 records (S1); + f2=2000/100=20 -> latest 
total-records 30. Pinned to S1 the count is
@@ -3274,6 +3494,68 @@ public class IcebergScanPlanProviderTest {
         Assertions.assertTrue(cache.size() >= 2, "the data + delete manifests 
must both be cached");
     }
 
+    @Test
+    public void 
streamSplitsManifestCacheResolvesDroppedEqualityDeleteFieldAfterReload() throws 
IOException {
+        Schema schema = new Schema(
+                Types.NestedField.optional(1, "old_key", 
Types.IntegerType.get()),
+                Types.NestedField.optional(2, "value", 
Types.StringType.get()));
+        Table table = createPersistedTable("dropped_cache_key", schema, 
PartitionSpec.unpartitioned(),
+                Collections.singletonMap(TableProperties.FORMAT_VERSION, "2"));
+        table.newAppend().appendFile(dataFile(table.spec(),
+                "s3://b/db/dropped_cache_key/f1.parquet", 1024, null, 
null)).commit();
+        table.newRowDelta().addDeletes(equalityDeleteFile(
+                "s3://b/db/dropped_cache_key/eq.parquet", FileFormat.PARQUET, 
1)).commit();
+        table.updateSchema().deleteColumn("old_key").commit();
+        Table reloaded = reloadPersistedTable(table);
+        IcebergManifestCache cache = new IcebergManifestCache();
+
+        List<ConnectorScanRange> ranges = 
drain(manifestProvider(manifestCacheProps(), reloaded, cache)
+                .streamSplits(emptySession(), new IcebergTableHandle("db1", 
"dropped_cache_key"),
+                        Collections.emptyList(), Optional.empty(), -1L));
+
+        Assertions.assertEquals(1, ranges.size());
+        Assertions.assertEquals(1, deleteCount(ranges.get(0)));
+        Assertions.assertEquals(0L, cache.takeStats("q")[2],
+                "historical equality-delete keys must not force the cache path 
to fail");
+    }
+
+    @Test
+    public void manifestCacheUsesLatestSchemaForSchemaOnlyMvccPin() throws 
IOException {
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.required(2, "k", Types.IntegerType.get()));
+        Table table = createPersistedTable("latest_schema_cache", schema, 
PartitionSpec.unpartitioned(),
+                Collections.singletonMap(TableProperties.FORMAT_VERSION, "3"));
+        Map<Integer, ByteBuffer> oldBounds = Collections.singletonMap(
+                2, Conversions.toByteBuffer(Types.IntegerType.get(), 1));
+        DataFile oldFile = DataFiles.builder(table.spec())
+                .withPath(table.location() + "/old.parquet")
+                .withFileSizeInBytes(100)
+                .withRecordCount(1)
+                .withMetrics(new Metrics(1L, null, null, null, null, 
oldBounds, oldBounds))
+                .withFormat(FileFormat.PARQUET)
+                .build();
+        table.newAppend().appendFile(oldFile).commit();
+        long pinnedSnapshotId = table.currentSnapshot().snapshotId();
+        
table.updateSchema().allowIncompatibleChanges().deleteColumn("k").commit();
+        table.updateSchema().addRequiredColumn(
+                "k", Types.IntegerType.get(), Literal.of(7)).commit();
+        Table reloaded = reloadPersistedTable(table);
+        IcebergTableHandle latestPin = new IcebergTableHandle("db1", 
"latest_schema_cache")
+                .withSnapshot(pinnedSnapshotId, null, 
reloaded.schema().schemaId());
+
+        IcebergManifestCache cache = new IcebergManifestCache();
+        List<ConnectorScanRange> ranges = manifestProvider(
+                manifestCacheProps(), reloaded, cache).planScan(
+                        emptySession(), 
ConnectorScanRequest.builder(latestPin, Collections.emptyList())
+                                .filter(Optional.of(eqInt("k", 7))).build());
+
+        Assertions.assertEquals(1, ranges.size(),
+                "the new field's initial default matches old files and must 
not be pruned by retired-field stats");
+        Assertions.assertEquals(1, cache.size(), "the assertion must exercise 
the manifest-cache path");
+        Assertions.assertEquals(0L, cache.takeStats("q")[2], "the cache path 
must not fall back to the SDK");
+    }
+
     // --- T09: vended credentials (extractVendedToken + static/vended 
location.* + URI threading) ---
 
     @Test
@@ -3842,7 +4124,7 @@ public class IcebergScanPlanProviderTest {
     @Test
     public void planScanForSystemTableSerializesEachFileScanTaskAsJniSplit() {
         // A $snapshots handle plans through the metadata table 
(MetadataTableUtils.createMetadataTableInstance):
-        // each metadata FileScanTask is serialized 
(SerializationUtil.serializeToBase64) and emitted as a JNI
+        // each metadata FileScanTask is serialized with the Iceberg 1.10.1 
Schema UID and emitted as a JNI
         // split carrying ONLY serialized_split + FORMAT_JNI + 
table_level_row_count=-1, mirroring legacy
         // IcebergScanNode.doGetSystemTableSplits + setIcebergParams. 
MUTATION: routing the sys handle through
         // the normal data-file path (resolveTable + buildRange) -> the range 
carries the f1.parquet path and no
@@ -3862,6 +4144,8 @@ public class IcebergScanPlanProviderTest {
             String serialized = ((IcebergScanRange) 
range).getSerializedSplit();
             Assertions.assertNotNull(serialized, "every sys split must carry a 
serialized FileScanTask");
             Assertions.assertFalse(serialized.isEmpty());
+            
Assertions.assertEquals(IcebergSystemTableSerialization.ICEBERG_1_10_1_SCHEMA_UID,
+                    IcebergSystemTableSerialization.schemaUid(serialized));
             TFileRangeDesc rangeDesc = populate(range);
             Assertions.assertEquals(TFileFormatType.FORMAT_JNI, 
rangeDesc.getFormatType());
             Assertions.assertEquals(serialized,
@@ -3874,7 +4158,8 @@ public class IcebergScanPlanProviderTest {
     public void 
planScanForSystemTableSplitDeserializesThroughTheBeJniReaderPath() throws 
Exception {
         // The strongest FE-reachable byte-shape parity check: the 
serialized_split must be consumable EXACTLY
         // as BE's IcebergSysTableJniScanner consumes it —
-        // SerializationUtil.deserializeFromBase64(...).asDataTask().rows() — 
and must carry the METADATA-table
+        // 
IcebergSystemTableSerialization.deserializeFromBase64(...).asDataTask().rows() 
— and must carry the
+        // METADATA-table
         // schema ($snapshots), not the base table's. (Cross-version / 
classloader interop is P6.8 docker e2e.)
         // MUTATION: serializing anything other than the FileScanTask (e.g. 
the DataFile) -> deserialize /
         // asDataTask() fails or yields the wrong schema -> red.
@@ -3891,7 +4176,8 @@ public class IcebergScanPlanProviderTest {
         long snapshotRows = 0;
         for (ConnectorScanRange range : ranges) {
             FileScanTask task =
-                    
SerializationUtil.deserializeFromBase64(((IcebergScanRange) 
range).getSerializedSplit());
+                    IcebergSystemTableSerialization.deserializeFromBase64(
+                            ((IcebergScanRange) range).getSerializedSplit());
             // the deserialized task exposes the $snapshots metadata schema, 
not the base table's columns.
             Assertions.assertNotNull(task.schema().findField("snapshot_id"),
                     "the serialized split must carry the metadata-table 
($snapshots) schema");
@@ -4049,7 +4335,8 @@ public class IcebergScanPlanProviderTest {
     private static String firstSysSplitResidual(List<ConnectorScanRange> 
ranges) throws Exception {
         Assertions.assertFalse(ranges.isEmpty(), "the metadata table must plan 
at least one split");
         FileScanTask task =
-                SerializationUtil.deserializeFromBase64(((IcebergScanRange) 
ranges.get(0)).getSerializedSplit());
+                IcebergSystemTableSerialization.deserializeFromBase64(
+                        ((IcebergScanRange) 
ranges.get(0)).getSerializedSplit());
         return task.residual().toString();
     }
 
@@ -4176,7 +4463,8 @@ public class IcebergScanPlanProviderTest {
         long rows = 0;
         for (ConnectorScanRange range : ranges) {
             FileScanTask task =
-                    
SerializationUtil.deserializeFromBase64(((IcebergScanRange) 
range).getSerializedSplit());
+                    IcebergSystemTableSerialization.deserializeFromBase64(
+                            ((IcebergScanRange) range).getSerializedSplit());
             try (CloseableIterable<StructLike> closeable = 
task.asDataTask().rows()) {
                 Iterator<StructLike> it = closeable.iterator();
                 while (it.hasNext()) {
diff --git a/fe/pom.xml b/fe/pom.xml
index 593653f1052..bcc40349bb2 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -246,7 +246,7 @@ under the License.
         <module>fe-grpc</module>
     </modules>
     <properties>
-        
<doris.hive.catalog.shade.version>3.1.1</doris.hive.catalog.shade.version>
+        
<doris.hive.catalog.shade.version>3.1.3</doris.hive.catalog.shade.version>
         <!-- iceberg 1.9.1 depends avro on 1.12 -->
         <avro.version>1.12.1</avro.version>
         <parquet.version>1.17.0</parquet.version>
@@ -361,7 +361,7 @@ under the License.
         <!-- ATTN: avro version must be consistent with Iceberg version -->
         <!-- Please modify iceberg.version and avro.version together,
          you can find avro version info in iceberg mvn repository -->
-        <iceberg.version>1.10.1</iceberg.version>
+        <iceberg.version>1.11.0</iceberg.version>
         <!-- 0.56.1 has bug that "SplitMode" in query response may not be 
set-->
         <maxcompute.version>0.53.2-public</maxcompute.version>
         <!-- FE-only POC: validate whether Arrow Java 19.0.0 can compile 
before touching BE/C++ thirdparty. -->
diff --git 
a/regression-test/data/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.out
 
b/regression-test/data/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.out
index 7546005e2f2..cf3ccac5bc8 100644
--- 
a/regression-test/data/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.out
+++ 
b/regression-test/data/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.out
@@ -329,4 +329,3 @@ phone       text    Yes     true    \N
 5      Eve     91.3    [email protected]        \N
 6      Frank   88.7    [email protected]       \N
 7      Grace   93.2    [email protected]       555-0123
-
diff --git 
a/regression-test/data/external_table_p0/iceberg/test_iceberg_sys_table.out 
b/regression-test/data/external_table_p0/iceberg/test_iceberg_sys_table.out
index 6a5298b356f..3692b06a99e 100644
--- a/regression-test/data/external_table_p0/iceberg/test_iceberg_sys_table.out
+++ b/regression-test/data/external_table_p0/iceberg/test_iceberg_sys_table.out
@@ -478,6 +478,7 @@ deleted_data_files_count    int     Yes     true    \N      
NONE
 deleted_delete_files_count     int     Yes     true    \N      NONE
 existing_data_files_count      int     Yes     true    \N      NONE
 existing_delete_files_count    int     Yes     true    \N      NONE
+key_metadata   text    Yes     true    \N      NONE
 length bigint  Yes     true    \N      NONE
 partition_spec_id      int     Yes     true    \N      NONE
 partition_summaries    array<struct<contains_null:boolean not 
null,contains_nan:boolean not null,lower_bound:text,upper_bound:text>>   Yes    
 true    \N      NONE
@@ -986,6 +987,7 @@ deleted_data_files_count    int     Yes     true    \N      
NONE
 deleted_delete_files_count     int     Yes     true    \N      NONE
 existing_data_files_count      int     Yes     true    \N      NONE
 existing_delete_files_count    int     Yes     true    \N      NONE
+key_metadata   text    Yes     true    \N      NONE
 length bigint  Yes     true    \N      NONE
 partition_spec_id      int     Yes     true    \N      NONE
 partition_summaries    array<struct<contains_null:boolean not 
null,contains_nan:boolean not null,lower_bound:text,upper_bound:text>>   Yes    
 true    \N      NONE
diff --git 
a/regression-test/suites/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.groovy
 
b/regression-test/suites/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.groovy
index b3f05f1d606..017f2bf498d 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/iceberg_schema_change_ddl_with_branch.groovy
@@ -198,7 +198,7 @@ suite("iceberg_schema_change_ddl_with_branch", 
"p0,external") {
     
     // All branches expose the current table columns: id, name, grade, email, 
phone.
 
-    // Verify all branches have the latest columns
+    // Branches bind predicates against the current table schema even when 
their head snapshot is historical.
     qt_all_branches_have_grade """ SELECT id, grade FROM 
${branch_table_name}@branch(branch1) WHERE grade > 0 ORDER BY id """
     qt_all_branches_have_email """ SELECT id, email FROM 
${branch_table_name}@branch(branch2) WHERE email IS NOT NULL ORDER BY id """
     qt_all_branches_have_phone """ SELECT id, phone FROM 
${branch_table_name}@branch(branch3) WHERE phone IS NOT NULL ORDER BY id """


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to