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

morningman 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 fd268c27f5c [improvement](fe) Upgrade iceberg to 1.12.0 (#69031)
fd268c27f5c is described below

commit fd268c27f5c7e42175a91bd20b4723291b0ebdc1
Author: Andrei Tserakhau <[email protected]>
AuthorDate: Thu Oct 15 11:18:34 2026 +0200

    [improvement](fe) Upgrade iceberg to 1.12.0 (#69031)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: Upgrade the Iceberg Java SDK from 1.11.0 to 1.12.0.
    1.12.0. This change only upgrades the dependency and adapts Doris to it.
    
    - Bump `iceberg.version` 1.11.0 -> 1.12.0, and `avro.version` 1.12.1 ->
    1.12.2 to stay consistent with what iceberg 1.12.0 is built against (the
    pom requires the two to move together).
    - Bump the explicit caffeine pin in fe-connector-iceberg 2.9.3 -> 3.2.4.
    Iceberg 1.12.0 moved to caffeine 3. The pin is a direct dependency, so
    it wins Maven mediation; leaving it at 2.9.3 would package an older
    caffeine into the iceberg plugin than iceberg-core is built against.
    - Re-vendor `org.apache.iceberg.DeleteFileIndex` from 1.12.0 and
    re-apply the Doris changes (public class/builder methods, schemasById
    comment, equality-field-id sets). The vendored class shadows the SDK
    class in the same package, and 1.12.0 changed it (an extra `specsById`
    constructor argument and delete/data file partition-match validation),
    so keeping the 1.11.0 copy would break the SDK's own callers at runtime.
    Since Doris passes `specsById` to the builder, the cached scan planning
    path now also rejects delete files whose partition spec or tuple does
    not match the data file, the same as upstream iceberg.
    - Pass `table.specs()` to `ManifestFiles.read` in
    `IcebergManifestCache`. Iceberg 1.12.0 removed the deprecated
    `read(ManifestFile, FileIO)` overload, which read the partition specs
    from the manifest file's own metadata (only possible for Avro
    manifests). This matches the delete manifest path next to it
    (`readDeleteManifest(..., table.specs())`) and
    `IcebergRemoveOrphanFilesAction`.
    - Keep the AWS SDK at 2.29.52 and default iceberg-aws's HTTP client to
    `urlconnection`. In 1.12.0, iceberg-aws's default `http-client.type`
    (`apache`) builds an `Apache5HttpClient` from
    `software.amazon.awssdk:apache5-client`, which first shipped in AWS SDK
    2.32. Doris pins 2.29.52 on purpose (#58566: newer SDKs add default S3
    integrity checksums that break S3-compatible services), so with the
    default every S3FileIO, Glue and STS client iceberg builds failed with
    `NoClassDefFoundError:
    software/amazon/awssdk/http/apache5/Apache5HttpClient$Builder`.
    `IcebergCatalogFactory.buildBaseCatalogProperties`, the base of every
    catalog flavor, now sets `http-client.type=urlconnection` unless the
    catalog sets it. S3FileIO keeps the setting when it is serialized, so
    the BE iceberg metadata scanner, which rebuilds the FileIO from it, uses
    the same client; `url-connection-client` is added to that scanner, which
    only bundled `apache-client` and `netty-nio-client`. An explicit
    `http-client.type=apache` is rejected at CREATE/ALTER CATALOG instead of
    failing on the first S3 or Glue call.
    
    `SchemaAwareDataTableScan` (the other vendored class) needs no change:
    `DataTableScan`, `BaseTableScan`, `SnapshotScan` and `TableScanContext`
    are identical between 1.11.0 and 1.12.0.
    
    ### Release note
    
    Upgrade the Iceberg Java SDK to 1.12.0. Iceberg's AWS clients (S3FileIO,
    Glue, STS) now use the URLConnection HTTP client by default, and the
    `http-client.type=apache` catalog property is no longer accepted.
    Iceberg 1.12.0 no longer reads the S3 remote-signing properties
    `s3.signer.uri` and `s3.signer.endpoint` and no longer defaults the
    signer endpoint to `v1/aws/s3/sign`, so an Iceberg REST server that
    enables S3 remote signing must send `signer.uri` / `signer.endpoint` or
    serve the table-scoped sign endpoint. Iceberg system tables
    (`$snapshots`, `$files`, ...) cannot be queried while FE and BE are on
    different Iceberg versions (1.11 and 1.12), e.g. during a rolling
    upgrade; they work again once every node is upgraded.
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - Added
    
IcebergCatalogFactoryTest#buildBaseDefaultsIcebergAwsHttpClientToUrlConnection
    (builds a real S3 client builder with the default) and
    #buildBaseKeepsAnExplicitHttpClientType, and
    IcebergCatalogPropertiesTest#createTimeRulesRejectTheApacheHttpClientType;
    IcebergCatalogOptionsSnapshotTest snapshots now include the default.
    - `build.sh --fe` passes. Ran every iceberg FE unit test with
    run-fe-ut.sh (fe-connector-iceberg, fe-connector-metastore-iceberg,
    be-java-extensions/iceberg-metadata-scanner): 1630 tests, 0 failures, 5
    skipped.
        - Iceberg regression suites not run.
    - Behavior changed: Yes.
    - Iceberg's AWS clients use the URLConnection HTTP client by default, so
    `http-client.apache.*` properties no longer apply, and
    `http-client.type=apache` is rejected at CREATE/ALTER CATALOG.
    - Iceberg scans whose delete files have a partition spec or partition
    tuple that does not match the data file now fail with a
    ValidationException (iceberg 1.12.0 behavior) instead of applying the
    deletes.
    - S3 remote signing: iceberg 1.12.0 no longer reads `s3.signer.uri` /
    `s3.signer.endpoint` and no longer defaults the signer endpoint to
    `v1/aws/s3/sign`. A REST catalog whose server turns on remote signing
    but sends only those legacy keys, or relies on that default, fails to
    read manifests. No compatibility shim is added: Doris does not use
    remote signing to read data (BE uses static or vended credentials), and
    `signer.uri` / `signer.endpoint` are deprecated in 1.12.0 and scheduled
    for removal in 1.13.0.
    - Iceberg system tables cannot be queried while FE and BE are on
    different Iceberg versions (1.11 and 1.12), e.g. during a rolling
    upgrade: iceberg 1.12.0 changed the serialized form of `BaseFile` and
    `PartitionData`, which system table tasks carry. BE reports the version
    mismatch instead of a bare `InvalidClassException`. Normal data scans
    are not affected.
    - Does this need documentation: No
    
    Issue Number: related #68916
    
    Co-authored-by: Isaac <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../iceberg-metadata-scanner/pom.xml               |  6 ++
 .../doris/iceberg/IcebergSerializationCompat.java  | 11 ++-
 .../iceberg/IcebergSerializationCompatTest.java    | 83 ++++++++++++++++++++++
 fe/fe-connector/fe-connector-iceberg/pom.xml       |  4 +-
 .../iceberg/IcebergCacheSizeEstimator.java         |  5 +-
 .../connector/iceberg/IcebergCatalogFactory.java   | 26 +++++--
 .../iceberg/IcebergCatalogProperties.java          | 10 +++
 .../doris/connector/iceberg/IcebergConnector.java  | 18 +++--
 .../connector/iceberg/IcebergManifestCache.java    | 45 ++++++++----
 .../connector/iceberg/IcebergManifestEntryKey.java | 24 +++++--
 .../connector/iceberg/IcebergScanPlanProvider.java | 11 +--
 .../iceberg/IcebergSystemTableSerialization.java   | 11 ++-
 .../java/org/apache/iceberg/DeleteFileIndex.java   | 48 +++++++++++--
 .../iceberg/IcebergCatalogFactoryTest.java         | 41 +++++++++++
 .../iceberg/IcebergCatalogOptionsSnapshotTest.java | 20 ++++++
 .../iceberg/IcebergCatalogPropertiesTest.java      | 19 +++++
 .../iceberg/IcebergConnectorCacheTest.java         |  2 +-
 .../IcebergConnectorTestConnectionTest.java        | 63 +++++++++++++++-
 .../iceberg/IcebergManifestCacheTest.java          | 43 ++++++++---
 .../iceberg/IcebergManifestEntryKeyTest.java       | 33 ++++++---
 .../iceberg/IcebergScanPlanProviderTest.java       | 54 ++++++++++++++
 fe/pom.xml                                         |  6 +-
 22 files changed, 520 insertions(+), 63 deletions(-)

diff --git a/fe/be-java-extensions/iceberg-metadata-scanner/pom.xml 
b/fe/be-java-extensions/iceberg-metadata-scanner/pom.xml
index b6d275327ae..8b63a2428d4 100644
--- a/fe/be-java-extensions/iceberg-metadata-scanner/pom.xml
+++ b/fe/be-java-extensions/iceberg-metadata-scanner/pom.xml
@@ -101,6 +101,12 @@ under the License.
             <groupId>software.amazon.awssdk</groupId>
             <artifactId>auth</artifactId>
         </dependency>
+        <!-- The HTTP client the FE sets for iceberg-aws 
(http-client.type=urlconnection); the serialized S3FileIO
+             carries that setting here. iceberg's default "apache" type needs 
a newer AWS SDK than Doris ships. -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>url-connection-client</artifactId>
+        </dependency>
         <!--
           Hadoop: a task's FileIO is whatever the table was created with, and 
for every non-S3
           catalog that is HadoopFileIO (directly or behind ResolvingFileIO). 
It used to be provided
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
index dda1c43c1b2..14ebf8c8cb2 100644
--- 
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
@@ -17,10 +17,12 @@
 
 package org.apache.doris.iceberg;
 
+import org.apache.iceberg.IcebergBuild;
 import org.apache.iceberg.Schema;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InvalidClassException;
 import java.io.ObjectInputStream;
 import java.io.ObjectStreamClass;
 import java.io.UncheckedIOException;
@@ -44,10 +46,15 @@ final class IcebergSerializationCompat {
         try (ByteArrayInputStream input = new ByteArrayInputStream(bytes);
                 ObjectInputStream objectInput = new 
SchemaCompatibleObjectInputStream(input)) {
             return (T) objectInput.readObject();
+        } catch (InvalidClassException | ClassNotFoundException e) {
+            // A class in the task differs from, or is missing in, this BE's 
Iceberg: the split was serialized by an
+            // FE on another Iceberg version. Only Schema is shimmed; Iceberg 
1.12 changed BaseFile, PartitionData and
+            // their anonymous classes, which every task carries, so the task 
cannot be read here.
+            throw new IllegalStateException("Cannot read an Iceberg system 
table split serialized with a different"
+                    + " Iceberg version than this BE's (" + 
IcebergBuild.version() + "). Iceberg system tables are"
+                    + " unavailable while FE and BE run different Doris 
versions, e.g. during a rolling upgrade.", e);
         } catch (IOException e) {
             throw new UncheckedIOException("Failed to deserialize object", e);
-        } catch (ClassNotFoundException e) {
-            throw new RuntimeException("Could not read object ", e);
         }
     }
 
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
index c23715d4adf..3d28b18146f 100644
--- 
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
@@ -17,7 +17,10 @@
 
 package org.apache.doris.iceberg;
 
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Assertions;
@@ -25,10 +28,18 @@ import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InvalidClassException;
 import java.io.ObjectOutputStream;
+import java.io.ObjectStreamConstants;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 
 public class IcebergSerializationCompatTest {
+    // BaseFile's serialVersionUID in Iceberg 1.11.0; 1.12.0 changed it (and 
added a field).
+    private static final long ICEBERG_1_11_0_BASE_FILE_UID = 
-8856745894322453272L;
+
     // 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"
@@ -66,6 +77,78 @@ public class IcebergSerializationCompatTest {
         Assertions.assertTrue(expected.sameSchema(actual));
     }
 
+    @Test
+    public void reportsATaskFromAnotherIcebergVersion() throws IOException {
+        // WHY: every system-table task carries a DataFile (StaticDataTask's 
metadata file is built like this one),
+        // and an FE on Iceberg 1.11 writes BaseFile with its 1.11 UID. 
MUTATION: dropping the InvalidClassException
+        // mapping -> a bare UncheckedIOException with no hint at the version 
skew -> red.
+        byte[] bytes = 
serialize(DataFiles.builder(PartitionSpec.unpartitioned())
+                
.withPath("/dummyPath").withFileSizeInBytes(0L).withRecordCount(1L)
+                .withFormat(FileFormat.METADATA).build());
+        rewriteUid(bytes, "org.apache.iceberg.BaseFile", 
ICEBERG_1_11_0_BASE_FILE_UID);
+
+        IllegalStateException e = 
Assertions.assertThrows(IllegalStateException.class,
+                () -> 
IcebergSerializationCompat.deserializeFromBase64(Base64.getEncoder().encodeToString(bytes)));
+        Assertions.assertTrue(e.getMessage().contains("different Iceberg 
version"), e.getMessage());
+        Assertions.assertInstanceOf(InvalidClassException.class, e.getCause());
+    }
+
+    @Test
+    public void reportsAClassThisIcebergDoesNotHave() throws IOException {
+        // WHY: Iceberg 1.12 renumbered anonymous classes a task carries (e.g. 
BaseFile$1), so a task from another
+        // version can name a class that does not exist here. MUTATION: 
mapping only InvalidClassException -> red.
+        byte[] bytes = serialize(new Probe());
+        String name = Probe.class.getName();
+        replaceAll(bytes, name.getBytes(StandardCharsets.UTF_8),
+                (name.substring(0, name.length() - 1) + 
"X").getBytes(StandardCharsets.UTF_8));
+
+        IllegalStateException e = 
Assertions.assertThrows(IllegalStateException.class,
+                () -> 
IcebergSerializationCompat.deserializeFromBase64(Base64.getEncoder().encodeToString(bytes)));
+        Assertions.assertInstanceOf(ClassNotFoundException.class, 
e.getCause());
+    }
+
+    private static final class Probe implements Serializable {
+        private static final long serialVersionUID = 1L;
+    }
+
+    private static byte[] serialize(Object value) throws IOException {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try (ObjectOutputStream objectOutput = new ObjectOutputStream(output)) 
{
+            objectOutput.writeObject(value);
+        }
+        return output.toByteArray();
+    }
+
+    /** Overwrites the serialVersionUID that follows the class descriptor 
named {@code className}. */
+    private static void rewriteUid(byte[] bytes, String className, long uid) {
+        byte[] name = className.getBytes(StandardCharsets.UTF_8);
+        for (int i = 0; i + 3 + name.length + Long.BYTES <= bytes.length; i++) 
{
+            if (bytes[i] == ObjectStreamConstants.TC_CLASSDESC && bytes[i + 1] 
== (byte) (name.length >>> 8)
+                    && bytes[i + 2] == (byte) name.length && matchesAt(bytes, 
i + 3, name)) {
+                ByteBuffer.wrap(bytes, i + 3 + name.length, 
Long.BYTES).putLong(uid);
+                return;
+            }
+        }
+        Assertions.fail("no class descriptor for " + className);
+    }
+
+    private static void replaceAll(byte[] bytes, byte[] from, byte[] to) {
+        for (int i = 0; i + from.length <= bytes.length; i++) {
+            if (matchesAt(bytes, i, from)) {
+                System.arraycopy(to, 0, bytes, i, to.length);
+            }
+        }
+    }
+
+    private static boolean matchesAt(byte[] bytes, int offset, byte[] pattern) 
{
+        for (int j = 0; j < pattern.length; j++) {
+            if (bytes[offset + j] != pattern[j]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private static String serializeToBase64(Object value) throws IOException {
         ByteArrayOutputStream output = new ByteArrayOutputStream();
         try (ObjectOutputStream objectOutput = new ObjectOutputStream(output)) 
{
diff --git a/fe/fe-connector/fe-connector-iceberg/pom.xml 
b/fe/fe-connector/fe-connector-iceberg/pom.xml
index 856594ff29f..282499f2988 100644
--- a/fe/fe-connector/fe-connector-iceberg/pom.xml
+++ b/fe/fe-connector/fe-connector-iceberg/pom.xml
@@ -115,13 +115,13 @@ under the License.
         <!-- Caffeine: needed on the COMPILE classpath for the vendored 
split-package
              org.apache.iceberg.DeleteFileIndex (T08). iceberg-core 1.10.1 
made DeleteFileIndex package-private,
              so the connector ships its own public copy (mirrors fe-core) to 
drive manifest-level scan planning;
-             that copy imports com.github.benmanes.caffeine. iceberg-core 
already brings caffeine 2.9.3 at runtime
+             that copy imports com.github.benmanes.caffeine. iceberg-core 
already brings caffeine 3.2.4 at runtime
              scope (so it is in the plugin-zip), but runtime-scoped 
transitives are not on the compile classpath
              — hence this explicit compile-scope declaration (same version, 
deduped). -->
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
-            <version>2.9.3</version>
+            <version>3.2.4</version>
         </dependency>
 
         <!-- iceberg-bundled-guava: needed on the COMPILE classpath for the 
vendored split-package
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCacheSizeEstimator.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCacheSizeEstimator.java
index fdffb7dc184..bbd05df2982 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCacheSizeEstimator.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCacheSizeEstimator.java
@@ -246,7 +246,10 @@ final class IcebergCacheSizeEstimator {
     }
 
     static long estimateManifestKey(IcebergManifestEntryKey key) {
-        return add(MANIFEST_KEY_SHALLOW_BYTES, 
JvmSizeUtils.stringSize(key.getManifestPath()));
+        long bytes = add(MANIFEST_KEY_SHALLOW_BYTES, 
JvmSizeUtils.stringSize(key.getManifestPath()));
+        // The key retains its bound partition type. A non-empty value's files 
share it, but an empty value (a delete
+        // manifest whose entries are all DELETED) holds nothing that would 
account for it.
+        return add(bytes, estimateIcebergType(key.getPartitionType()));
     }
 
     static long estimateManifestValue(ManifestCacheValue value) {
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
index 6374647c60f..9620d6fec20 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogFactory.java
@@ -34,6 +34,7 @@ import org.apache.iceberg.CatalogUtil;
 import org.apache.iceberg.aws.AssumeRoleAwsClientFactory;
 import org.apache.iceberg.aws.AwsClientProperties;
 import org.apache.iceberg.aws.AwsProperties;
+import org.apache.iceberg.aws.HttpClientProperties;
 import org.apache.iceberg.aws.s3.S3FileIOProperties;
 import org.apache.iceberg.rest.auth.OAuth2Properties;
 
@@ -141,10 +142,11 @@ public final class IcebergCatalogFactory {
      * mirroring the legacy {@code 
AbstractIcebergProperties.initializeCatalog} base: (1) seed from ALL
      * raw props (legacy {@code getOrigProps()} copy-all — arbitrary 
user/iceberg keys pass through to
      * the SDK; the per-flavor appenders + the connector add the derived keys 
on top), (2) map
-     * {@code warehouse} to {@link CatalogProperties#WAREHOUSE_LOCATION}, (3) 
add manifest-cache keys.
-     * PURE: depends only on its arguments; {@code metaCacheWeightLimited} is
-     * {@code IcebergConnector#hasMetaCacheWeightLimit}. The flavor's {@code 
catalog-impl} and the {@code type}
-     * removal are applied by the caller (the connector / per-flavor path).
+     * {@code warehouse} to {@link CatalogProperties#WAREHOUSE_LOCATION}, (3) 
add manifest-cache keys,
+     * (4) default iceberg-aws's HTTP client to {@code urlconnection}. PURE: 
depends only on its arguments;
+     * {@code metaCacheWeightLimited} is {@code 
IcebergConnector#hasMetaCacheWeightLimit}. The flavor's
+     * {@code catalog-impl} and the {@code type} removal are applied by the 
caller (the connector / per-flavor
+     * path).
      */
     public static Map<String, String> buildBaseCatalogProperties(Map<String, 
String> props,
             boolean metaCacheWeightLimited) {
@@ -154,9 +156,25 @@ public final class IcebergCatalogFactory {
             opts.put(CatalogProperties.WAREHOUSE_LOCATION, warehouse);
         }
         appendManifestCacheProperties(props, opts, metaCacheWeightLimited);
+        applyDefaultAwsHttpClient(opts);
         return opts;
     }
 
+    /**
+     * Defaults iceberg-aws's HTTP client to {@code urlconnection} unless 
{@code opts} sets one. A blank value counts as
+     * unset: iceberg-aws turns an empty type back into its own "apache" 
default. iceberg-aws's default
+     * "apache" client type is Apache5HttpClient since iceberg 1.12, which 
needs an AWS SDK newer than the one Doris
+     * pins (2.29.x, see #58566), so every S3FileIO / Glue / STS client 
iceberg builds from a map without this key fails
+     * with NoClassDefFoundError; url-connection-client ships with that SDK. 
Every map that ends up building an
+     * iceberg AWS client must go through here. The key travels with 
S3FileIO's properties, so the BE metadata
+     * scanner, which rebuilds the FileIO from them, uses the same client.
+     */
+    static void applyDefaultAwsHttpClient(Map<String, String> opts) {
+        if (StringUtils.isBlank(opts.get(HttpClientProperties.CLIENT_TYPE))) {
+            opts.put(HttpClientProperties.CLIENT_TYPE, 
HttpClientProperties.CLIENT_TYPE_URLCONNECTION);
+        }
+    }
+
     /**
      * Mirrors legacy {@code 
AbstractIcebergProperties.addManifestCacheProperties}: pass through any
      * explicitly-set {@code io.manifest.cache.*} keys, then — only when the 
user did NOT set
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogProperties.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogProperties.java
index fe9844a82c3..b0981af2789 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogProperties.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogProperties.java
@@ -22,6 +22,8 @@ import 
org.apache.doris.connector.metastore.spi.MetaStoreProviders;
 import org.apache.doris.foundation.property.ConnectorPropertiesUtils;
 import org.apache.doris.foundation.property.ConnectorProperty;
 
+import org.apache.iceberg.aws.HttpClientProperties;
+
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Locale;
@@ -144,6 +146,14 @@ public final class IcebergCatalogProperties {
         if (TYPE_DLF.equals(flavor) && externalCatalogName != null) {
             throw new IllegalArgumentException("external_catalog.name is not 
supported for Iceberg DLF catalogs");
         }
+        // iceberg-aws's "apache" client is Apache5HttpClient, which the AWS 
SDK Doris ships does not contain;
+        // fail here rather than with a NoClassDefFoundError on the first S3 
or Glue call.
+        if 
(HttpClientProperties.CLIENT_TYPE_APACHE.equalsIgnoreCase(raw.get(HttpClientProperties.CLIENT_TYPE)))
 {
+            throw new 
IllegalArgumentException(HttpClientProperties.CLIENT_TYPE + "="
+                    + HttpClientProperties.CLIENT_TYPE_APACHE + " is not 
supported: it requires an AWS SDK newer than"
+                    + " the one Doris ships. Use " + 
HttpClientProperties.CLIENT_TYPE_URLCONNECTION
+                    + " (the default) instead.");
+        }
         // Selects the backend by flavor and enforces its fail-fast rules -- 
REST (security/creds enums,
         // OAuth2, signing, AK/SK), Glue (AK/SK-together, endpoint https, 
at-least-one-credential), JDBC
         // (uri/catalog_name/warehouse), the shared HMS connection checks; 
hadoop/s3tables are no-op (their
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
index 8e910135238..afc79d39544 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnector.java
@@ -173,6 +173,8 @@ public class IcebergConnector implements Connector {
 
     /** The key BE looks up (case-sensitively) to learn which location to 
probe. */
     private static final String BE_TEST_LOCATION = "test_location";
+    /** Prefix of iceberg-aws's HTTP client settings ({@code 
http-client.type}, proxy, timeouts). */
+    private static final String HTTP_CLIENT_PROPERTY_PREFIX = "http-client.";
 
     private final Map<String, String> properties;
     // The bound connector-level facts (flavor, type-mapping switches, 
namespace level), the single
@@ -469,12 +471,12 @@ public class IcebergConnector implements Connector {
 
         // Map Doris s3.* keys to Iceberg S3FileIO keys and force static 
credentials (disable
         // remote/vended signing) so the probe validates exactly what the user 
configured.
-        Map<String, String> ioProps = buildStorageProbeProperties(catalogType, 
chosenStorage.get());
+        Map<String, String> ioProps = buildStorageProbeProperties(catalogType, 
chosenStorage.get(), properties);
 
         // Load S3FileIO reflectively via CatalogUtil so this module needs no 
compile-time AWS SDK
         // dependency; the AWS SDK is resolved from the shared runtime 
classpath at execution time.
         // Pin the TCCL to the plugin loader for the probe: iceberg-aws builds 
its S3 client lazily and
-        // resolves org.apache.iceberg.aws.ApacheHttpClientConfigurations via 
DynMethods (default loader =
+        // resolves its org.apache.iceberg.aws.*HttpClientConfigurations class 
via DynMethods (default loader =
         // TCCL). This CREATE-CATALOG thread runs under the default 'app' 
TCCL, which would return the
         // fe-core copy of the class and ClassCast against the child-loaded 
plugin copy the rest of the
         // iceberg-aws stack uses — the SAME split-brain 
TcclPinningConnectorContext guards on the commit
@@ -510,8 +512,8 @@ public class IcebergConnector implements Connector {
         return probeStorageFromBackend(location);
     }
 
-    static Map<String, String> buildStorageProbeProperties(
-            String catalogType, S3CompatibleFileSystemProperties 
storageProperties) {
+    static Map<String, String> buildStorageProbeProperties(String catalogType,
+            S3CompatibleFileSystemProperties storageProperties, Map<String, 
String> catalogProperties) {
         String endpoint = storageProperties.getEndpoint();
         if (IcebergCatalogProperties.TYPE_DLF.equalsIgnoreCase(catalogType)
                 && "OSS".equals(storageProperties.providerName())) {
@@ -530,6 +532,14 @@ public class IcebergConnector implements Connector {
         if (!isBlank(storageProperties.getRegion())) {
             ioProps.put("client.region", storageProperties.getRegion());
         }
+        // The probe builds its own S3FileIO, so it must reach the store the 
way the catalog does: same HTTP client
+        // and proxy settings, and the same default client type as every other 
iceberg AWS client.
+        for (Map.Entry<String, String> e : catalogProperties.entrySet()) {
+            if (e.getKey().startsWith(HTTP_CLIENT_PROPERTY_PREFIX)) {
+                ioProps.put(e.getKey(), e.getValue());
+            }
+        }
+        IcebergCatalogFactory.applyDefaultAwsHttpClient(ioProps);
         return ioProps;
     }
 
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestCache.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestCache.java
index 96084f19db2..ca54be70761 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestCache.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestCache.java
@@ -24,12 +24,14 @@ import org.apache.doris.connector.cache.MetaCacheDefinition;
 import org.apache.doris.connector.cache.MetaCacheSizeEstimators;
 import org.apache.doris.connector.cache.ScopePath;
 
+import com.google.common.base.Preconditions;
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DeleteFile;
 import org.apache.iceberg.ManifestContent;
 import org.apache.iceberg.ManifestFile;
 import org.apache.iceberg.ManifestFiles;
 import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Table;
 
 import java.io.IOException;
@@ -200,20 +202,22 @@ final class IcebergManifestCache {
      * loader runs OUTSIDE Caffeine's compute lock (manual miss-load; 
single-flight per key), so a same-key
      * miss parses at most once.
      */
-    ManifestCacheValue getManifestCacheValue(ManifestFile manifest, Table 
table) {
-        IcebergManifestEntryKey key = IcebergManifestEntryKey.of(manifest);
+    ManifestCacheValue getManifestCacheValue(ManifestFile manifest, Table 
table,
+            Map<Integer, PartitionSpec> specsById) {
+        IcebergManifestEntryKey key = keyOf(manifest, specsById);
         return entry.get(key, k -> loadManifestCacheValue(
-                manifest, table, k.getContent(), entry.isWeightBounded()));
+                manifest, table, specsById, k.getContent(), 
entry.isWeightBounded()));
     }
 
     /**
-     * Same as {@link #getManifestCacheValue(ManifestFile, Table)} but tallies 
this access as a hit or miss under
+     * Same as {@link #getManifestCacheValue(ManifestFile, Table, Map)} but 
tallies this access as a hit or miss under
      * {@code queryId} (a hit == the entry was already cached BEFORE this 
load, matching the legacy
      * {@code getIfPresent(key) != null} probe). Within one statement the 
manifest-level plan processes manifests
      * sequentially, so the per-query counters need no synchronization. A 
blank queryId is not recorded.
      */
-    ManifestCacheValue getManifestCacheValue(ManifestFile manifest, Table 
table, String queryId) {
-        IcebergManifestEntryKey key = IcebergManifestEntryKey.of(manifest);
+    ManifestCacheValue getManifestCacheValue(ManifestFile manifest, Table 
table,
+            Map<Integer, PartitionSpec> specsById, String queryId) {
+        IcebergManifestEntryKey key = keyOf(manifest, specsById);
         boolean hit = entry.getIfPresent(key) != null;
         if (queryId != null && !queryId.isEmpty()) {
             ScanStats stats = touch(queryId);
@@ -224,7 +228,7 @@ final class IcebergManifestCache {
             }
         }
         return entry.get(key, k -> loadManifestCacheValue(
-                manifest, table, k.getContent(), entry.isWeightBounded()));
+                manifest, table, specsById, k.getContent(), 
entry.isWeightBounded()));
     }
 
     /**
@@ -270,22 +274,34 @@ final class IcebergManifestCache {
         statsByQuery.entrySet().removeIf(e -> nowNanos - 
e.getValue().lastTouchNanos >= statsTtlNanos);
     }
 
+    /**
+     * {@code specsById} is the scan's spec map (bound to the scan's schema), 
the same one its residuals and delete
+     * index use: the reader converts partition values to these types, so they 
must match what the scan evaluates.
+     */
+    private static IcebergManifestEntryKey keyOf(ManifestFile manifest, 
Map<Integer, PartitionSpec> specsById) {
+        PartitionSpec boundSpec = specsById.get(manifest.partitionSpecId());
+        Preconditions.checkArgument(boundSpec != null, "No bound partition 
spec %s for iceberg manifest %s",
+                manifest.partitionSpecId(), manifest.path());
+        return IcebergManifestEntryKey.of(manifest, boundSpec);
+    }
+
     private static ManifestCacheValue loadManifestCacheValue(ManifestFile 
manifest, Table table,
-            ManifestContent content, boolean estimateWeight) {
+            Map<Integer, PartitionSpec> specsById, ManifestContent content, 
boolean estimateWeight) {
         try {
             if (content == ManifestContent.DELETES) {
-                return 
ManifestCacheValue.forDeleteFiles(loadDeleteFiles(manifest, table), 
estimateWeight);
+                return 
ManifestCacheValue.forDeleteFiles(loadDeleteFiles(manifest, table, specsById), 
estimateWeight);
             }
-            return ManifestCacheValue.forDataFiles(loadDataFiles(manifest, 
table), estimateWeight);
+            return ManifestCacheValue.forDataFiles(loadDataFiles(manifest, 
table, specsById), estimateWeight);
         } catch (IOException e) {
             throw new RuntimeException("Failed to read iceberg manifest " + 
manifest.path(), e);
         }
     }
 
-    private static List<DataFile> loadDataFiles(ManifestFile manifest, Table 
table) throws IOException {
+    private static List<DataFile> loadDataFiles(ManifestFile manifest, Table 
table,
+            Map<Integer, PartitionSpec> specsById) throws IOException {
         List<DataFile> dataFiles = new ArrayList<>();
         // .copy() is mandatory: the ManifestReader iterator reuses the same 
object across iterations.
-        try (ManifestReader<DataFile> reader = ManifestFiles.read(manifest, 
table.io())) {
+        try (ManifestReader<DataFile> reader = ManifestFiles.read(manifest, 
table.io(), specsById)) {
             for (DataFile dataFile : reader) {
                 dataFiles.add(dataFile.copy());
             }
@@ -293,10 +309,11 @@ final class IcebergManifestCache {
         return dataFiles;
     }
 
-    private static List<DeleteFile> loadDeleteFiles(ManifestFile manifest, 
Table table) throws IOException {
+    private static List<DeleteFile> loadDeleteFiles(ManifestFile manifest, 
Table table,
+            Map<Integer, PartitionSpec> specsById) throws IOException {
         List<DeleteFile> deleteFiles = new ArrayList<>();
         try (ManifestReader<DeleteFile> reader =
-                ManifestFiles.readDeleteManifest(manifest, table.io(), 
table.specs())) {
+                ManifestFiles.readDeleteManifest(manifest, table.io(), 
specsById)) {
             for (DeleteFile deleteFile : reader) {
                 deleteFiles.add(deleteFile.copy());
             }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKey.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKey.java
index 90552bef6d5..9e40896d52b 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKey.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKey.java
@@ -19,6 +19,8 @@ package org.apache.doris.connector.iceberg;
 
 import org.apache.iceberg.ManifestContent;
 import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.types.Types;
 
 import java.util.Objects;
 
@@ -31,18 +33,25 @@ import java.util.Objects;
  * cached payload, and a table-level invalidation (REFRESH TABLE) 
intentionally does NOT drop it (legacy
  * {@code testInvalidateTableKeepsManifestCache} parity). Runtime loader 
context (the manifest/table instances)
  * must not be stored here.
+ *
+ * <p>The key also carries the partition type the manifest is decoded with: 
the reader converts partition values to
+ * the bound spec's types, and after a type promotion (e.g. INT to BIGINT) a 
scan pinned to the old schema and a scan
+ * on the current one bind the same manifest to different types. Each binding 
needs its own decoded copy.
  */
 public class IcebergManifestEntryKey {
     private final String manifestPath;
     private final ManifestContent content;
+    private final Types.StructType partitionType;
 
-    public IcebergManifestEntryKey(String manifestPath, ManifestContent 
content) {
+    public IcebergManifestEntryKey(String manifestPath, ManifestContent 
content, Types.StructType partitionType) {
         this.manifestPath = Objects.requireNonNull(manifestPath, "manifestPath 
can not be null");
         this.content = Objects.requireNonNull(content, "content can not be 
null");
+        this.partitionType = Objects.requireNonNull(partitionType, 
"partitionType can not be null");
     }
 
-    public static IcebergManifestEntryKey of(ManifestFile manifest) {
-        return new IcebergManifestEntryKey(manifest.path(), 
manifest.content());
+    /** The key for {@code manifest} decoded with {@code boundSpec}, the 
scan's spec for its partition spec id. */
+    public static IcebergManifestEntryKey of(ManifestFile manifest, 
PartitionSpec boundSpec) {
+        return new IcebergManifestEntryKey(manifest.path(), 
manifest.content(), boundSpec.partitionType());
     }
 
     public String getManifestPath() {
@@ -53,6 +62,10 @@ public class IcebergManifestEntryKey {
         return content;
     }
 
+    public Types.StructType getPartitionType() {
+        return partitionType;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -63,11 +76,12 @@ public class IcebergManifestEntryKey {
         }
         IcebergManifestEntryKey that = (IcebergManifestEntryKey) o;
         return Objects.equals(manifestPath, that.manifestPath)
-                && content == that.content;
+                && content == that.content
+                && partitionType.equals(that.partitionType);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(manifestPath, content);
+        return Objects.hash(manifestPath, content, partitionType);
     }
 }
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 b612be4f42d..73beceb47ed 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
@@ -2838,7 +2838,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
             if (!ManifestEvaluator.forPartitionFilter(filterExpr, spec, 
caseSensitive).eval(manifest)) {
                 continue;
             }
-            deleteFiles.addAll(manifestCacheGet(manifest, table, 
statsQueryId).getDeleteFiles());
+            deleteFiles.addAll(manifestCacheGet(manifest, table, specsById, 
statsQueryId).getDeleteFiles());
         }
         DeleteFileIndex deleteIndex = DeleteFileIndex.builderFor(deleteFiles)
                 .schemasById(table.schemas())
@@ -2864,10 +2864,11 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
     }
 
     /** Dispatch a manifest read to the stats-tallying or no-stats cache 
overload (see cacheBackedFileScanTasks). */
-    private ManifestCacheValue manifestCacheGet(ManifestFile manifest, Table 
table, String statsQueryId) {
+    private ManifestCacheValue manifestCacheGet(ManifestFile manifest, Table 
table,
+            Map<Integer, PartitionSpec> specsById, String statsQueryId) {
         return statsQueryId != null
-                ? manifestCache.getManifestCacheValue(manifest, table, 
statsQueryId)
-                : manifestCache.getManifestCacheValue(manifest, table);
+                ? manifestCache.getManifestCacheValue(manifest, table, 
specsById, statsQueryId)
+                : manifestCache.getManifestCacheValue(manifest, table, 
specsById);
     }
 
     /**
@@ -2956,7 +2957,7 @@ public class IcebergScanPlanProvider implements 
ConnectorScanPlanProvider {
                 }
                 currentResidual = residual;
                 currentSpecJson = PartitionSpecParser.toJson(spec);
-                dataFileIt = manifestCacheGet(manifest, table, 
statsQueryId).getDataFiles().iterator();
+                dataFileIt = manifestCacheGet(manifest, table, specsById, 
statsQueryId).getDataFiles().iterator();
             }
         }
 
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
index 97c702a4901..a66abd229af 100644
--- 
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
@@ -26,7 +26,14 @@ 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. */
+/**
+ * Keeps the Java-serialized Iceberg system-table task wire compatible with 
Iceberg 1.10.1.
+ *
+ * <p>That only covers Schema, the one class whose UID 1.11 changed. Iceberg 
1.12 changed the serialized form of
+ * classes every task carries (BaseFile, PartitionData and their anonymous 
inner classes), so a task cannot cross
+ * between 1.11 and 1.12: Iceberg system tables are unavailable while FE and 
BE run different Iceberg versions,
+ * and the BE reader reports that instead of a bare InvalidClassException.
+ */
 final class IcebergSystemTableSerialization {
     static final long ICEBERG_1_10_1_SCHEMA_UID = 6812231194765760118L;
     private static final long ICEBERG_1_11_0_SCHEMA_UID = 
-1265875184407129845L;
@@ -56,7 +63,7 @@ final class IcebergSystemTableSerialization {
 
     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.
+        // to the legacy UID lets 1.10 and 1.11 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);
         }
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 4c45cfc1b3e..bddcb5a237d 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
@@ -67,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.11.0/core/src/main/java/org/apache/iceberg/DeleteFileIndex.java
+ * Copied from 
https://github.com/apache/iceberg/blob/apache-iceberg-1.12.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)
@@ -88,6 +88,7 @@ public class DeleteFileIndex {
   private final PartitionMap<PositionDeletes> posDeletesByPartition;
   private final Map<String, PositionDeletes> posDeletesByPath;
   private final Map<String, DeleteFile> dvByPath;
+  private final LoadingCache<Integer, Comparator<StructLike>> 
partitionComparatorsBySpecId;
   private final boolean hasEqDeletes;
   private final boolean hasPosDeletes;
   private final boolean isEmpty;
@@ -97,12 +98,18 @@ public class DeleteFileIndex {
       PartitionMap<EqualityDeletes> eqDeletesByPartition,
       PartitionMap<PositionDeletes> posDeletesByPartition,
       Map<String, PositionDeletes> posDeletesByPath,
-      Map<String, DeleteFile> dvByPath) {
+      Map<String, DeleteFile> dvByPath,
+      Map<Integer, PartitionSpec> specsById) {
     this.globalDeletes = globalDeletes;
     this.eqDeletesByPartition = eqDeletesByPartition;
     this.posDeletesByPartition = posDeletesByPartition;
     this.posDeletesByPath = posDeletesByPath;
     this.dvByPath = dvByPath;
+    this.partitionComparatorsBySpecId =
+        specsById == null
+            ? null
+            : Caffeine.newBuilder()
+                .build(specId -> 
Comparators.forType(specsById.get(specId).partitionType()));
     this.hasEqDeletes = globalDeletes != null || eqDeletesByPartition != null;
     this.hasPosDeletes =
         posDeletesByPartition != null || posDeletesByPath != null || dvByPath 
!= null;
@@ -209,7 +216,15 @@ public class DeleteFileIndex {
     }
 
     PositionDeletes deletes = posDeletesByPath.get(dataFile.location());
-    return deletes == null ? EMPTY_DELETES : deletes.filter(seq);
+    if (deletes == null) {
+      return EMPTY_DELETES;
+    }
+
+    DeleteFile[] matchingDeletes = deletes.filter(seq);
+    for (DeleteFile deleteFile : matchingDeletes) {
+      validatePartitionMatch(deleteFile, dataFile);
+    }
+    return matchingDeletes;
   }
 
   private DeleteFile findDV(long seq, DataFile dataFile) {
@@ -224,10 +239,34 @@ public class DeleteFileIndex {
           "DV data sequence number (%s) must be greater than or equal to data 
file sequence number (%s)",
           dv.dataSequenceNumber(),
           seq);
+      validatePartitionMatch(dv, dataFile);
     }
     return dv;
   }
 
+  private void validatePartitionMatch(DeleteFile deleteFile, DataFile 
dataFile) {
+    ValidationException.check(
+        deleteFile.specId() == dataFile.specId(),
+        "Mismatched partition specs (%s, %s) for delete file %s and data file 
%s:"
+            + " metadata is corrupted",
+        deleteFile.specId(),
+        dataFile.specId(),
+        deleteFile.location(),
+        dataFile.location());
+    if (partitionComparatorsBySpecId != null) {
+      Comparator<StructLike> partitionComparator =
+          partitionComparatorsBySpecId.get(deleteFile.specId());
+      ValidationException.check(
+          partitionComparator.compare(deleteFile.partition(), 
dataFile.partition()) == 0,
+          "Mismatched partition tuples (%s, %s) for delete file %s and data 
file %s:"
+              + " metadata is corrupted",
+          deleteFile.partition(),
+          dataFile.partition(),
+          deleteFile.location(),
+          dataFile.location());
+    }
+  }
+
   @SuppressWarnings("checkstyle:CyclomaticComplexity")
   private static boolean canContainEqDeletesForFile(
       DataFile dataFile, EqualityDeleteFile deleteFile) {
@@ -540,7 +579,8 @@ public class DeleteFileIndex {
           eqDeletesByPartition.isEmpty() ? null : eqDeletesByPartition,
           posDeletesByPartition.isEmpty() ? null : posDeletesByPartition,
           posDeletesByPath.isEmpty() ? null : posDeletesByPath,
-          dvByPath.isEmpty() ? null : dvByPath);
+          dvByPath.isEmpty() ? null : dvByPath,
+          specsById.isEmpty() ? null : specsById);
     }
 
     private void add(Map<String, DeleteFile> dvByPath, DeleteFile dv) {
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
index 6a3f31dbb1f..3fc0f6c3d99 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogFactoryTest.java
@@ -27,6 +27,7 @@ import 
org.apache.doris.filesystem.properties.StorageProperties;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.iceberg.aws.AwsClientProperties;
+import org.apache.iceberg.aws.HttpClientProperties;
 import org.apache.iceberg.rest.responses.ConfigResponse;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -37,6 +38,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentials;
 import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
 import 
software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
 import 
software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
+import software.amazon.awssdk.services.s3.S3Client;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -215,6 +217,45 @@ public class IcebergCatalogFactoryTest {
         Assertions.assertEquals("false", 
opts.get("io.manifest.cache-enabled"));
     }
 
+    @Test
+    public void buildBaseDefaultsIcebergAwsHttpClientToUrlConnection() {
+        // WHY: iceberg 1.12's default "apache" client type is 
Apache5HttpClient, absent from the AWS SDK Doris
+        // pins, so without this default every S3FileIO / Glue / STS client 
fails with NoClassDefFoundError.
+        // Building a client here runs on the plugin's own SDK, so it fails 
exactly as production would.
+        // MUTATION: dropping the default -> "apache" -> NoClassDefFoundError 
-> red.
+        Map<String, String> opts = 
IcebergCatalogFactory.buildBaseCatalogProperties(
+                props("iceberg.catalog.type", "rest"), false);
+        Assertions.assertEquals(HttpClientProperties.CLIENT_TYPE_URLCONNECTION,
+                opts.get(HttpClientProperties.CLIENT_TYPE));
+        Assertions.assertDoesNotThrow(
+                () -> new 
HttpClientProperties(opts).applyHttpClientConfigurations(S3Client.builder()));
+    }
+
+    @Test
+    public void buildBaseTreatsABlankHttpClientTypeAsUnset() {
+        // WHY: iceberg-aws maps an empty http-client.type back to its 
"apache" default (Apache5HttpClient), so an
+        // empty value set at CREATE or by ALTER must get the supported 
default too. MUTATION: putIfAbsent keeps "" ->
+        // NoClassDefFoundError on the client build -> red.
+        for (String blank : new String[] {"", " "}) {
+            Map<String, String> opts = 
IcebergCatalogFactory.buildBaseCatalogProperties(
+                    props("iceberg.catalog.type", "rest", 
HttpClientProperties.CLIENT_TYPE, blank), false);
+            
Assertions.assertEquals(HttpClientProperties.CLIENT_TYPE_URLCONNECTION,
+                    opts.get(HttpClientProperties.CLIENT_TYPE));
+            Assertions.assertDoesNotThrow(
+                    () -> new 
HttpClientProperties(opts).applyHttpClientConfigurations(S3Client.builder()));
+        }
+    }
+
+    @Test
+    public void buildBaseKeepsAnExplicitHttpClientType() {
+        // WHY: the default only fills a missing key; a user's choice is 
passed through (and "apache" is rejected
+        // at CREATE/ALTER by IcebergCatalogProperties, not silently rewritten 
here). MUTATION: put instead of
+        // putIfAbsent -> the user's value is overwritten -> red.
+        Map<String, String> opts = 
IcebergCatalogFactory.buildBaseCatalogProperties(
+                props("iceberg.catalog.type", "rest", 
HttpClientProperties.CLIENT_TYPE, "apache"), false);
+        Assertions.assertEquals("apache", 
opts.get(HttpClientProperties.CLIENT_TYPE));
+    }
+
     @Test
     public void buildBaseMetaCacheEnabledButZeroTtlDoesNotEnable() {
         // WHY: legacy isCacheEnabled = enable && ttl != 0 && capacity != 0; 
an explicit ttl-second=0
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogOptionsSnapshotTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogOptionsSnapshotTest.java
index cd3b22b0d4f..6c24483f98f 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogOptionsSnapshotTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogOptionsSnapshotTest.java
@@ -87,6 +87,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restMinimalSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest.example/api";,
                         "warehouse", "s3://bucket/wh",
                         "catalog-impl", "org.apache.iceberg.rest.RESTCatalog",
@@ -106,6 +107,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restOAuth2CredentialFlowSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "iceberg.rest.uri", "https://rest/api";,
                         "uri", "https://rest/api";,
                         "iceberg.rest.prefix", "main",
@@ -148,6 +150,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restOAuth2TokenFlowSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest/api";,
                         "iceberg.rest.security.type", "oauth2",
                         "iceberg.rest.oauth2.token", "tkn-123",
@@ -174,6 +177,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restTrimsSurroundingWhitespaceSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "iceberg.rest.uri", "  https://rest/api  ",
                         "iceberg.rest.security.type", "oauth2",
                         "iceberg.rest.oauth2.token", " tkn-123 ",
@@ -196,6 +200,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restSigningGlueWithBoundS3Snapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest/api";,
                         "iceberg.rest.signing-name", "glue",
                         "iceberg.rest.signing-region", "us-east-1",
@@ -233,6 +238,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restSigningGlueAssumeRoleSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest/api";,
                         "iceberg.rest.signing-name", "glue",
                         "iceberg.rest.signing-region", "us-east-1",
@@ -266,6 +272,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restSigningOtherNameWithRestCredentialsSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest/api";,
                         "iceberg.rest.signing-name", "custom",
                         "iceberg.rest.signing-region", "cn-north-1",
@@ -301,6 +308,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void restSigningProviderChainSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "rest",
+                        "http-client.type", "urlconnection",
                         "uri", "https://rest/api";,
                         "iceberg.rest.signing-name", "custom",
                         "iceberg.rest.credentials_provider_type", 
"instance-profile",
@@ -331,6 +339,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void glueStaticCredentialsSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "glue",
+                        "http-client.type", "urlconnection",
                         "warehouse", "s3://bucket/wh",
                         "glue.endpoint", 
"https://glue.us-east-1.amazonaws.com";,
                         "glue.access_key", "GAK",
@@ -366,6 +375,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void glueAssumeRoleWithoutWarehouseSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "glue",
+                        "http-client.type", "urlconnection",
                         "glue.endpoint", 
"https://glue.eu-west-1.amazonaws.com";,
                         "glue.role_arn", "arn:aws:iam::1:role/r",
                         "glue.external_id", "eid",
@@ -388,6 +398,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void glueExplicitRegionAliasSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "glue",
+                        "http-client.type", "urlconnection",
                         "warehouse", "s3://bucket/wh",
                         "glue.endpoint", 
"https://glue.us-east-1.amazonaws.com";,
                         "aws.glue.region", "ap-southeast-1",
@@ -415,6 +426,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void glueRegionFallsBackToUsEast1Snapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "glue",
+                        "http-client.type", "urlconnection",
                         "warehouse", "s3://bucket/wh",
                         "glue.endpoint", 
"https://vpce-1234.glue.privatelink.aws";,
                         "glue.role_arn", "arn:aws:iam::1:role/r",
@@ -443,6 +455,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void jdbcWithoutUriEmitsEmptyUriSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "jdbc",
+                        "http-client.type", "urlconnection",
                         "warehouse", "s3://bucket/wh",
                         "uri", "",
                         "catalog-impl", "org.apache.iceberg.jdbc.JdbcCatalog"),
@@ -460,6 +473,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void jdbcFullSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "jdbc",
+                        "http-client.type", "urlconnection",
                         "uri", "jdbc:postgresql://h/db",
                         "warehouse", "s3://bucket/wh",
                         "iceberg.jdbc.user", "u",
@@ -495,6 +509,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void jdbcUriAliasOrderSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "jdbc",
+                        "http-client.type", "urlconnection",
                         "uri", "jdbc:postgresql://plain/db",
                         "iceberg.jdbc.uri", "jdbc:postgresql://prefixed/db",
                         "warehouse", "s3://bucket/wh",
@@ -519,6 +534,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void hmsSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "hms",
+                        "http-client.type", "urlconnection",
                         "hive.metastore.uris", "thrift://h:9083",
                         "warehouse", "s3://bucket/wh",
                         "catalog-impl", DorisHiveCatalog.class.getName()),
@@ -536,6 +552,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void hadoopWithNonGenericStoreSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "hadoop",
+                        "http-client.type", "urlconnection",
                         "warehouse", "oss://bucket/wh",
                         "catalog-impl", 
"org.apache.iceberg.hadoop.HadoopCatalog",
                         "s3.endpoint", "https://oss";,
@@ -556,6 +573,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     public void manifestCacheDerivationSnapshot() {
         assertOptions(
                 props("iceberg.catalog.type", "hadoop",
+                        "http-client.type", "urlconnection",
                         "warehouse", "s3://bucket/wh",
                         "meta.cache.iceberg.manifest.enable", "true",
                         "meta.cache.iceberg.manifest.ttl-second", "3600",
@@ -576,6 +594,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     @Test
     public void s3TablesWithStaticCredentialsSnapshot() {
         Map<String, String> expected = props("iceberg.catalog.type", 
"s3tables",
+                "http-client.type", "urlconnection",
                 "warehouse", "arn:aws:s3tables:us-east-1:1:bucket/b",
                 "s3.role_arn", "arn:aws:iam::1:role/ignored",
                 "client.region", "us-east-1",
@@ -594,6 +613,7 @@ public class IcebergCatalogOptionsSnapshotTest {
     @Test
     public void s3TablesWithoutBoundStorageSnapshot() {
         Map<String, String> expected = props("iceberg.catalog.type", 
"s3tables",
+                "http-client.type", "urlconnection",
                 "warehouse", "arn:aws:s3tables:us-east-1:1:bucket/b",
                 "s3.region", "us-east-1",
                 "client.region", "us-east-1");
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogPropertiesTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogPropertiesTest.java
index 29c18e33677..99e13b5ad20 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogPropertiesTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergCatalogPropertiesTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.connector.iceberg;
 
+import org.apache.iceberg.aws.HttpClientProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -135,6 +136,24 @@ public class IcebergCatalogPropertiesTest {
         Assertions.assertTrue(on.isEnableMappingTimestampTz(), "parsing must 
stay case-insensitive");
     }
 
+    @Test
+    public void createTimeRulesRejectTheApacheHttpClientType() {
+        // WHY: iceberg-aws's "apache" client needs an AWS SDK newer than 
Doris ships; accepting it would only defer
+        // the failure to a NoClassDefFoundError on the first S3 or Glue call. 
The rebuild path (of()) must still
+        // accept it so an existing catalog comes back. MUTATION: dropping the 
check -> accepted -> red.
+        Map<String, String> p = props("iceberg.catalog.type", "hadoop", 
"warehouse", "s3://b/wh",
+                HttpClientProperties.CLIENT_TYPE, "Apache");
+        IllegalArgumentException e = 
Assertions.assertThrows(IllegalArgumentException.class,
+                () -> 
IcebergCatalogProperties.of(p).checkCreateTimeOnlyRules());
+        
Assertions.assertTrue(e.getMessage().contains(HttpClientProperties.CLIENT_TYPE),
+                "the message must name the offending key: " + e.getMessage());
+        Assertions.assertDoesNotThrow(() -> IcebergCatalogProperties.of(p));
+        Assertions.assertDoesNotThrow(() -> IcebergCatalogProperties.of(props(
+                "iceberg.catalog.type", "hadoop", "warehouse", "s3://b/wh",
+                HttpClientProperties.CLIENT_TYPE, 
HttpClientProperties.CLIENT_TYPE_URLCONNECTION))
+                .checkCreateTimeOnlyRules());
+    }
+
     @Test
     public void createTimeRulesRejectABadMetaCacheValue() {
         // WHY: this restores the legacy fail-fast the SPI cutover dropped. 
Without it a typo'd ttl is
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 74ec5b7589b..be4736618e3 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
@@ -214,7 +214,7 @@ public class IcebergConnectorCacheTest {
         Table table = tableWithOneManifest();
         ManifestFile manifest = 
table.currentSnapshot().dataManifests(table.io()).get(0);
         IcebergManifestCache manifestCache = connector.manifestCacheForTest();
-        manifestCache.getManifestCacheValue(manifest, table);
+        manifestCache.getManifestCacheValue(manifest, table, table.specs());
         Assertions.assertEquals(1, manifestCache.size(), "the manifest is 
cached after a load");
 
         // REFRESH TABLE must NOT drop the manifest cache (path-keyed 
immutable content; legacy parity).
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTestConnectionTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTestConnectionTest.java
index c1e665b905a..4db495c6414 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTestConnectionTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorTestConnectionTest.java
@@ -21,8 +21,10 @@ import org.apache.doris.connector.spi.ConnectorContext;
 import org.apache.doris.connector.spi.ConnectorStorageContext;
 import org.apache.doris.connector.spi.ConnectorTestResult;
 
+import org.apache.iceberg.aws.HttpClientProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.services.s3.S3Client;
 
 import java.lang.reflect.Method;
 import java.util.HashMap;
@@ -125,7 +127,7 @@ public class IcebergConnectorTestConnectionTest {
                 .usePathStyle("false");
 
         Map<String, String> ioProps = 
IcebergConnector.buildStorageProbeProperties(
-                IcebergCatalogProperties.TYPE_DLF, oss);
+                IcebergCatalogProperties.TYPE_DLF, oss, new HashMap<>());
 
         
Assertions.assertEquals("http://s3.oss-cn-hangzhou-internal.aliyuncs.com";, 
ioProps.get("s3.endpoint"));
         Assertions.assertEquals("bound-ak", ioProps.get("s3.access-key-id"));
@@ -135,6 +137,65 @@ public class IcebergConnectorTestConnectionTest {
         Assertions.assertEquals("false", ioProps.get("s3.path-style-access"));
     }
 
+    @Test
+    public void storageProbePropertiesBuildAnS3ClientOnTheShippedSdk() {
+        // WHY: the probe builds its own S3FileIO from this map, outside 
buildBaseCatalogProperties. iceberg 1.12's
+        // default "apache" client is Apache5HttpClient, absent from the AWS 
SDK Doris ships, so CREATE CATALOG failed
+        // with NoClassDefFoundError (an Error the probe's catch(Exception) 
never turns into a test result).
+        // MUTATION: drop the default -> "apache" -> NoClassDefFoundError -> 
red.
+        Map<String, String> ioProps = 
IcebergConnector.buildStorageProbeProperties(
+                IcebergCatalogProperties.TYPE_REST,
+                new 
FakeS3CompatibleStorageProperties("S3").endpoint("http://127.0.0.1:1";)
+                        .region("us-east-1").accessKey("ak").secretKey("sk"),
+                new HashMap<>());
+
+        Assertions.assertEquals(HttpClientProperties.CLIENT_TYPE_URLCONNECTION,
+                ioProps.get(HttpClientProperties.CLIENT_TYPE));
+        Assertions.assertDoesNotThrow(
+                () -> new 
HttpClientProperties(ioProps).applyHttpClientConfigurations(S3Client.builder()));
+    }
+
+    @Test
+    public void storageProbeTreatsABlankCatalogHttpClientTypeAsUnset() {
+        // WHY: the probe copies the catalog's http-client.* keys, so a blank 
type must not slip through to iceberg-aws,
+        // which would turn it into "apache". MUTATION: copying "" without 
normalizing -> NoClassDefFoundError -> red.
+        Map<String, String> catalogProperties = new HashMap<>();
+        catalogProperties.put(HttpClientProperties.CLIENT_TYPE, "");
+
+        Map<String, String> ioProps = 
IcebergConnector.buildStorageProbeProperties(
+                IcebergCatalogProperties.TYPE_REST,
+                new 
FakeS3CompatibleStorageProperties("S3").endpoint("http://127.0.0.1:1";)
+                        .region("us-east-1").accessKey("ak").secretKey("sk"),
+                catalogProperties);
+
+        Assertions.assertEquals(HttpClientProperties.CLIENT_TYPE_URLCONNECTION,
+                ioProps.get(HttpClientProperties.CLIENT_TYPE));
+        Assertions.assertDoesNotThrow(
+                () -> new 
HttpClientProperties(ioProps).applyHttpClientConfigurations(S3Client.builder()));
+    }
+
+    @Test
+    public void storageProbeUsesTheCatalogHttpClientSettings() {
+        // WHY: the probe must reach storage the way the catalog will (proxy, 
timeouts), so the catalog's
+        // http-client.* keys win over the default. MUTATION: not copying them 
-> the probe ignores the proxy -> red.
+        Map<String, String> catalogProperties = new HashMap<>();
+        catalogProperties.put(HttpClientProperties.CLIENT_TYPE, 
HttpClientProperties.CLIENT_TYPE_URLCONNECTION);
+        catalogProperties.put("http-client.proxy-endpoint", 
"http://proxy:3128";);
+        catalogProperties.put("s3.endpoint", "http://catalog-only-key";);
+
+        Map<String, String> ioProps = 
IcebergConnector.buildStorageProbeProperties(
+                IcebergCatalogProperties.TYPE_REST,
+                new 
FakeS3CompatibleStorageProperties("S3").endpoint("http://127.0.0.1:1";)
+                        .region("us-east-1").accessKey("ak").secretKey("sk"),
+                catalogProperties);
+
+        Assertions.assertEquals("http://proxy:3128";, 
ioProps.get("http-client.proxy-endpoint"));
+        Assertions.assertEquals(HttpClientProperties.CLIENT_TYPE_URLCONNECTION,
+                ioProps.get(HttpClientProperties.CLIENT_TYPE));
+        Assertions.assertEquals("http://127.0.0.1:1";, 
ioProps.get("s3.endpoint"),
+                "only http-client.* keys are copied; the storage binder stays 
the source of s3.*");
+    }
+
     @Test
     public void 
dlfQualifiedOssWarehouseUsesSelectedStorageNormalizerForProbe() throws 
Exception {
         String warehouse = 
"oss://bucket.oss-cn-hangzhou.aliyuncs.com/warehouse";
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestCacheTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestCacheTest.java
index c777f0ddfd3..d4e8edc6d42 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestCacheTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestCacheTest.java
@@ -18,9 +18,12 @@
 package org.apache.doris.connector.iceberg;
 
 import org.apache.doris.connector.cache.CatalogMetaCache;
+import org.apache.doris.connector.cache.JvmSizeUtils;
+import org.apache.doris.connector.cache.MetaCacheSizeEstimate;
 
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.ManifestContent;
 import org.apache.iceberg.ManifestFile;
 import org.apache.iceberg.Metrics;
 import org.apache.iceberg.PartitionSpec;
@@ -34,6 +37,7 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -92,7 +96,7 @@ public class IcebergManifestCacheTest {
         ManifestFile manifest = manifests.get(0);
 
         IcebergManifestCache cache = new IcebergManifestCache();
-        ManifestCacheValue first = cache.getManifestCacheValue(manifest, 
table);
+        ManifestCacheValue first = cache.getManifestCacheValue(manifest, 
table, table.specs());
         // WHY: a DATA manifest yields the two appended data files. MUTATION: 
returning the delete-file branch
         // (empty) -> 0 -> red.
         Assertions.assertEquals(2, first.getDataFiles().size());
@@ -101,7 +105,7 @@ public class IcebergManifestCacheTest {
 
         // A second read of the SAME manifest path returns the SAME cached 
value (no re-parse). MUTATION: not
         // caching (re-loading) -> a different instance -> red.
-        ManifestCacheValue second = cache.getManifestCacheValue(manifest, 
table);
+        ManifestCacheValue second = cache.getManifestCacheValue(manifest, 
table, table.specs());
         Assertions.assertSame(first, second, "the manifest payload must be 
served from the cache on a repeat");
         Assertions.assertEquals(1, cache.size());
     }
@@ -114,14 +118,36 @@ public class IcebergManifestCacheTest {
             IcebergManifestCache cache = new IcebergManifestCache(owner,
                     
Collections.singletonMap("meta.cache.iceberg.manifest.max-weight", "1MB"));
 
-            ManifestCacheValue first = cache.getManifestCacheValue(manifest, 
table);
-            ManifestCacheValue second = cache.getManifestCacheValue(manifest, 
table);
+            ManifestCacheValue first = cache.getManifestCacheValue(manifest, 
table, table.specs());
+            ManifestCacheValue second = cache.getManifestCacheValue(manifest, 
table, table.specs());
 
             Assertions.assertSame(first, second);
             Assertions.assertEquals(2, first.getDataFiles().size());
         }
     }
 
+    @Test
+    public void manifestKeyWeightCountsTheBoundPartitionType() {
+        // WHY: the key retains the bound partition type, and an empty value 
(a delete manifest whose entries are all
+        // DELETED) holds no file that references it, so only the key estimate 
can account for it. MUTATION:
+        // estimateManifestKey ignoring the partition type -> both entries 
weigh the same -> red.
+        ManifestCacheValue empty = 
ManifestCacheValue.forDeleteFiles(Collections.emptyList(), true);
+        MetaCacheSizeEstimate narrow = 
IcebergCacheSizeEstimator.estimateManifestEntry(deleteManifestKey(1), empty);
+        MetaCacheSizeEstimate wide = 
IcebergCacheSizeEstimator.estimateManifestEntry(deleteManifestKey(10), empty);
+        Assertions.assertTrue(narrow.isComplete() && wide.isComplete());
+        long extraFieldBytes = wide.getBytes() - narrow.getBytes();
+        Assertions.assertTrue(extraFieldBytes >= 9 * 
JvmSizeUtils.instanceSize(Types.NestedField.class),
+                "narrow=" + narrow.getBytes() + ", wide=" + wide.getBytes());
+    }
+
+    private static IcebergManifestEntryKey deleteManifestKey(int 
partitionFieldCount) {
+        List<Types.NestedField> fields = new ArrayList<>();
+        for (int i = 0; i < partitionFieldCount; i++) {
+            fields.add(Types.NestedField.optional(1000 + i, "p" + i, 
Types.IntegerType.get()));
+        }
+        return new IcebergManifestEntryKey("/m/deletes.avro", 
ManifestContent.DELETES, Types.StructType.of(fields));
+    }
+
     @Test
     public void equalityDeleteFieldIdsLoadOncePerSnapshot() {
         IcebergManifestCache cache = new IcebergManifestCache();
@@ -172,9 +198,9 @@ public class IcebergManifestCacheTest {
         // maxSize 1: the first load fills it; a re-read still hits (same 
key). The wholesale-flush valve is the
         // legacy behavior — re-reads are harmless since the value is 
immutable. Smoke that size stays bounded.
         IcebergManifestCache cache = new IcebergManifestCache(1);
-        cache.getManifestCacheValue(manifest, table);
+        cache.getManifestCacheValue(manifest, table, table.specs());
         Assertions.assertEquals(1, cache.size());
-        cache.getManifestCacheValue(manifest, table);
+        cache.getManifestCacheValue(manifest, table, table.specs());
         Assertions.assertTrue(cache.size() <= 1, "a bounded cache must not 
grow past its max size");
     }
 
@@ -183,7 +209,7 @@ public class IcebergManifestCacheTest {
         Table table = tableWithTwoDataFiles();
         ManifestFile manifest = 
table.currentSnapshot().dataManifests(table.io()).get(0);
         IcebergManifestCache cache = new IcebergManifestCache();
-        cache.getManifestCacheValue(manifest, table);
+        cache.getManifestCacheValue(manifest, table, table.specs());
         AtomicInteger equalityLoads = new AtomicInteger();
         cache.getOrLoadEqualityDeleteFieldIds(table.location(), 
table.currentSnapshot().snapshotId(), () -> {
             equalityLoads.incrementAndGet();
@@ -205,7 +231,8 @@ public class IcebergManifestCacheTest {
     public void copiesDataFilesSoReaderReuseDoesNotAlias() {
         Table table = tableWithTwoDataFiles();
         ManifestFile manifest = 
table.currentSnapshot().dataManifests(table.io()).get(0);
-        List<DataFile> files = new 
IcebergManifestCache().getManifestCacheValue(manifest, table).getDataFiles();
+        List<DataFile> files = new IcebergManifestCache()
+                .getManifestCacheValue(manifest, table, 
table.specs()).getDataFiles();
         // WHY: ManifestReader reuses one object across iterations, so the 
cache must .copy() each file. Two
         // distinct paths prove the entries were copied out (not the same 
reused instance). MUTATION: dropping
         // .copy() -> both entries alias the last file -> both paths equal -> 
red.
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKeyTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKeyTest.java
index 96371cfa606..ecd4c08d213 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKeyTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergManifestEntryKeyTest.java
@@ -18,19 +18,25 @@
 package org.apache.doris.connector.iceberg;
 
 import org.apache.iceberg.ManifestContent;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 /**
- * Unit tests for {@link IcebergManifestEntryKey} (T08). The key is path + 
content; two manifests that share a
- * path (across tables) hit the same cache entry, which is what lets a REFRESH 
TABLE keep the manifest cache.
+ * Unit tests for {@link IcebergManifestEntryKey} (T08). The key is path + 
content + bound partition type; two
+ * manifests that share a path (across tables) hit the same cache entry, which 
is what lets a REFRESH TABLE keep the
+ * manifest cache.
  */
 public class IcebergManifestEntryKeyTest {
+    private static final Types.StructType INT_PARTITION = Types.StructType.of(
+            Types.NestedField.optional(1000, "p", Types.IntegerType.get()));
+    private static final Types.StructType LONG_PARTITION = Types.StructType.of(
+            Types.NestedField.optional(1000, "p", Types.LongType.get()));
 
     @Test
     public void samePathSameContentAreEqual() {
-        IcebergManifestEntryKey a = new 
IcebergManifestEntryKey("/m/shared.avro", ManifestContent.DATA);
-        IcebergManifestEntryKey b = new 
IcebergManifestEntryKey("/m/shared.avro", ManifestContent.DATA);
+        IcebergManifestEntryKey a = new 
IcebergManifestEntryKey("/m/shared.avro", ManifestContent.DATA, INT_PARTITION);
+        IcebergManifestEntryKey b = new 
IcebergManifestEntryKey("/m/shared.avro", ManifestContent.DATA, INT_PARTITION);
         // WHY: the path-keyed cache must treat two references to the same 
manifest path as one entry (cross-table
         // sharing). MUTATION: keying on identity instead of (path, content) 
-> not equal -> red.
         Assertions.assertEquals(a, b);
@@ -39,8 +45,9 @@ public class IcebergManifestEntryKeyTest {
 
     @Test
     public void differentContentSamePathAreNotEqual() {
-        IcebergManifestEntryKey data = new 
IcebergManifestEntryKey("/m/x.avro", ManifestContent.DATA);
-        IcebergManifestEntryKey deletes = new 
IcebergManifestEntryKey("/m/x.avro", ManifestContent.DELETES);
+        IcebergManifestEntryKey data = new 
IcebergManifestEntryKey("/m/x.avro", ManifestContent.DATA, INT_PARTITION);
+        IcebergManifestEntryKey deletes =
+                new IcebergManifestEntryKey("/m/x.avro", 
ManifestContent.DELETES, INT_PARTITION);
         // A data manifest and a delete manifest are distinct cache entries 
even at the same path. MUTATION:
         // ignoring content -> equal -> red.
         Assertions.assertNotEquals(data, deletes);
@@ -49,7 +56,17 @@ public class IcebergManifestEntryKeyTest {
     @Test
     public void differentPathAreNotEqual() {
         Assertions.assertNotEquals(
-                new IcebergManifestEntryKey("/m/a.avro", ManifestContent.DATA),
-                new IcebergManifestEntryKey("/m/b.avro", 
ManifestContent.DATA));
+                new IcebergManifestEntryKey("/m/a.avro", ManifestContent.DATA, 
INT_PARTITION),
+                new IcebergManifestEntryKey("/m/b.avro", ManifestContent.DATA, 
INT_PARTITION));
+    }
+
+    @Test
+    public void differentBoundPartitionTypeAreNotEqual() {
+        // The reader converts partition values to the bound spec's types, so 
a manifest decoded for a scan before an
+        // INT -> BIGINT promotion and one decoded for a current scan hold 
different values. MUTATION: ignoring the
+        // partition type -> equal -> one scan reads the other's Integer/Long 
values -> red.
+        Assertions.assertNotEquals(
+                new IcebergManifestEntryKey("/m/x.avro", ManifestContent.DATA, 
INT_PARTITION),
+                new IcebergManifestEntryKey("/m/x.avro", ManifestContent.DATA, 
LONG_PARTITION));
     }
 }
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 77d39888fdf..305fbf81347 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
@@ -2497,6 +2497,60 @@ public class IcebergScanPlanProviderTest {
         assertHistoricalStreamingDispatch(table, historicalSnapshotId, 
historicalSchemaId);
     }
 
+    @Test
+    public void 
cachedStreamingScansKeepPartitionTypesAcrossIntToBigintPromotion() throws 
IOException {
+        // WHY: the manifest cache shares one decoded manifest across scans, 
but a scan pinned to the schema before an
+        // INT -> BIGINT promotion of an identity partition binds its residual 
to INT, while a current scan binds it to
+        // LONG. Values decoded for one binding must not reach the other: the 
historical scan failed with
+        // "Wrong class, expected java.lang.Integer, but was java.lang.Long" 
(and the reverse for the current scan).
+        // The position delete in p=1 runs the same partitions through the 
delete index. MUTATION: reading cached
+        // manifests with table.specs() (or the manifest's own spec) for every 
scan -> one of the two orders -> red.
+        Schema schema = new Schema(
+                Types.NestedField.optional(1, "p", Types.IntegerType.get()),
+                Types.NestedField.optional(2, "value", 
Types.StringType.get()));
+        PartitionSpec spec = 
PartitionSpec.builderFor(schema).identity("p").build();
+        Table table = createTable("promoted_partition", schema, spec,
+                Collections.singletonMap(TableProperties.FORMAT_VERSION, "2"));
+        String location = "s3://b/db/promoted_partition/";
+        table.newRowDelta()
+                .addRows(dataFile(table.spec(), location + "old.parquet", 
1024, null, "p=1"))
+                .addDeletes(FileMetadata.deleteFileBuilder(table.spec())
+                        .ofPositionDeletes()
+                        .withPath(location + "old-deletes.parquet")
+                        .withFormat(FileFormat.PARQUET)
+                        .withFileSizeInBytes(128L)
+                        .withRecordCount(1L)
+                        .withPartitionPath("p=1")
+                        .build())
+                .commit();
+        long historicalSnapshotId = table.currentSnapshot().snapshotId();
+        int historicalSchemaId = table.currentSnapshot().schemaId();
+        table.updateSchema().updateColumn("p", Types.LongType.get()).commit();
+        table.newAppend().appendFile(dataFile(table.spec(), location + 
"new.parquet", 1024, null, "p=2")).commit();
+
+        IcebergTableHandle historical = new IcebergTableHandle("db1", 
"promoted_partition")
+                .withSnapshot(historicalSnapshotId, null, historicalSchemaId);
+        IcebergTableHandle current = new IcebergTableHandle("db1", 
"promoted_partition")
+                .withSnapshot(table.currentSnapshot().snapshotId(), null, 
table.schema().schemaId());
+        ConnectorExpression intFilter = eqInt("p", 1);
+        ConnectorExpression bigintFilter = new 
ConnectorComparison(ConnectorComparison.Operator.EQ,
+                new ConnectorColumnRef("p", ConnectorType.of("BIGINT")),
+                new ConnectorLiteral(ConnectorType.of("BIGINT"), 1L));
+
+        for (boolean historicalFirst : new boolean[] {true, false}) {
+            IcebergScanPlanProvider provider =
+                    manifestProvider(manifestCacheProps(), table, new 
IcebergManifestCache());
+            for (int i = 0; i < 2; i++) {
+                boolean scanHistorical = (i == 0) == historicalFirst;
+                List<ConnectorScanRange> ranges = 
drain(provider.streamSplits(batchSession(1, true),
+                        scanHistorical ? historical : current, 
Collections.emptyList(),
+                        Optional.of(scanHistorical ? intFilter : 
bigintFilter), -1L));
+                Assertions.assertEquals(Collections.singletonList(location + 
"old.parquet"), sortedPaths(ranges),
+                        (scanHistorical ? "historical" : "current") + " scan, 
historical first=" + historicalFirst);
+            }
+        }
+    }
+
     private static void assertHistoricalStreamingDispatch(
             Table table, long historicalSnapshotId, int historicalSchemaId) 
throws IOException {
         IcebergTableHandle handle = new IcebergTableHandle("db1", 
"historical_batch_estimate")
diff --git a/fe/pom.xml b/fe/pom.xml
index b2226f40cdf..a0f247bfe64 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -246,7 +246,9 @@ under the License.
         <module>fe-grpc</module>
     </modules>
     <properties>
-        <!-- iceberg 1.9.1 depends avro on 1.12 -->
+        <!-- iceberg 1.12.0 is built against avro 1.12.2, but stays on 1.12.1: 
avro 1.12.2 forbids SpecificDatumReader
+             from instantiating classes outside 
org.apache.avro.SERIALIZABLE_PACKAGES (AVRO-4189), which breaks Hudi's
+             commit metadata readers. Every avro API iceberg 1.12.0 references 
exists in 1.12.1. -->
         <avro.version>1.12.1</avro.version>
         <parquet.version>1.18.0</parquet.version>
         <spark.version>3.4.3</spark.version>
@@ -360,7 +362,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.11.0</iceberg.version>
+        <iceberg.version>1.12.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. -->


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

Reply via email to