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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 50e6d56c642 branch-4.1: [fix](multi-catalog) Preserve external 
partition metadata (#66012)
50e6d56c642 is described below

commit 50e6d56c642609d140f945913b5d2c7692b8e74c
Author: Socrates <[email protected]>
AuthorDate: Wed Aug 5 07:45:04 2026 +0800

    branch-4.1: [fix](multi-catalog) Preserve external partition metadata 
(#66012)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #62821, #65581, #66010
    
    Problem Summary: branch-4.1 already backported the Paimon and BE
    partition metadata changes through #65581, but Hive-style scans, Hudi,
    Iceberg, and file load paths still had incomplete FE metadata
    propagation. Hive default partitions were encoded as `\N`, which lost
    the distinction between NULL and a literal `\N`. Iceberg attached
    partition metadata only when runtime partition pruning was enabled, used
    unstable per-map ordering, and did not safely handle files written with
    different partition specs.
    
    This change:
    
    - propagates aligned partition values and explicit NULL flags through
    Hive-style scans, Hudi, and both legacy and Nereids file load paths;
    - preserves a literal `\N` as data while representing Hive default
    partitions as NULL;
    - sends stable Iceberg identity-partition metadata for every split,
    keyed by both spec ID and partition data;
    - restricts Iceberg path partition keys to identity columns shared by
    all partition specs, which is safe for both FileScannerV2 and the legacy
    scanner's scan-level slot mapping.
    
    ### Release note
    
    Fix partition-column materialization, NULL handling, and Iceberg
    partition evolution compatibility for external table scans and file
    loads on branch-4.1.
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - `./run-fe-ut.sh --run
    
org.apache.doris.common.util.BrokerUtilTest,org.apache.doris.datasource.hive.source.HiveScanNodeTest,org.apache.doris.datasource.iceberg.IcebergUtilsTest,org.apache.doris.datasource.iceberg.source.IcebergScanNodeTest,org.apache.doris.datasource.paimon.source.PaimonScanNodeTest`
    - Behavior changed: Yes. External partition metadata and NULL values are
    materialized consistently per split.
    - Does this need documentation: No
---
 .../org/apache/doris/common/util/BrokerUtil.java   |  71 ++++++---
 .../org/apache/doris/datasource/FileGroupInfo.java |  24 +--
 .../apache/doris/datasource/FileQueryScanNode.java |   8 +-
 .../datasource/hive/HiveExternalMetaCache.java     |   4 +-
 .../doris/datasource/hudi/source/HudiScanNode.java |   6 +-
 .../doris/datasource/iceberg/IcebergUtils.java     |  66 +++++++-
 .../datasource/iceberg/source/IcebergScanNode.java | 116 ++++++++++----
 .../doris/nereids/load/NereidsFileGroupInfo.java   |  24 +--
 .../apache/doris/common/util/BrokerUtilTest.java   |  37 ++++-
 .../doris/datasource/iceberg/IcebergUtilsTest.java |  59 +++++++
 .../iceberg/source/IcebergScanNodeTest.java        | 171 ++++++++++++++++++++-
 11 files changed, 500 insertions(+), 86 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/util/BrokerUtil.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/util/BrokerUtil.java
index 76baa5fec5b..fd49d44edda 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/util/BrokerUtil.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/BrokerUtil.java
@@ -69,6 +69,7 @@ import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 public class BrokerUtil {
     private static final Logger LOG = LogManager.getLogger(BrokerUtil.class);
@@ -155,7 +156,7 @@ public class BrokerUtil {
 
     public static List<String> parseColumnsFromPath(String filePath, 
List<String> columnsFromPath)
             throws UserException {
-        return parseColumnsFromPath(filePath, columnsFromPath, true, false);
+        return parseColumnsFromPathWithNullInfo(filePath, columnsFromPath, 
true, false).getValues();
     }
 
     public static List<String> parseColumnsFromPath(
@@ -164,23 +165,35 @@ public class BrokerUtil {
             boolean caseSensitive,
             boolean isACID)
             throws UserException {
+        return parseColumnsFromPathWithNullInfo(filePath, columnsFromPath, 
caseSensitive, isACID)
+                .getValues();
+    }
+
+    public static ParsedColumnsFromPath parseColumnsFromPathWithNullInfo(
+            String filePath,
+            List<String> columnsFromPath,
+            boolean caseSensitive,
+            boolean isACID)
+            throws UserException {
         if (columnsFromPath == null || columnsFromPath.isEmpty()) {
-            return Collections.emptyList();
+            return new ParsedColumnsFromPath(Collections.emptyList(), 
Collections.emptyList());
         }
         // if it is ACID, the path count is 3. The hdfs path is 
hdfs://xxx/table_name/par=xxx/delta(or base)_xxx/.
         int pathCount = isACID ? 3 : 2;
+        List<String> expectedColumns = columnsFromPath;
         if (!caseSensitive) {
-            for (int i = 0; i < columnsFromPath.size(); i++) {
-                String path = columnsFromPath.remove(i);
-                columnsFromPath.add(i, path.toLowerCase());
+            expectedColumns = new ArrayList<>(columnsFromPath.size());
+            for (String path : columnsFromPath) {
+                expectedColumns.add(path.toLowerCase(Locale.ROOT));
             }
         }
         String[] strings = filePath.split("/");
         if (strings.length < 2) {
             throw new UserException("Fail to parse columnsFromPath, expected: "
-                    + columnsFromPath + ", filePath: " + filePath);
+                    + expectedColumns + ", filePath: " + filePath);
         }
-        String[] columns = new String[columnsFromPath.size()];
+        String[] columns = new String[expectedColumns.size()];
+        Boolean[] columnsFromPathIsNull = new Boolean[expectedColumns.size()];
         int size = 0;
         boolean skipOnce = true;
         for (int i = strings.length - pathCount; i >= 0; i--) {
@@ -194,31 +207,52 @@ public class BrokerUtil {
                     continue;
                 }
                 throw new UserException("Fail to parse columnsFromPath, 
expected: "
-                        + columnsFromPath + ", filePath: " + filePath);
+                        + expectedColumns + ", filePath: " + filePath);
             }
             skipOnce = false;
             String[] pair = str.split("=", 2);
             if (pair.length != 2) {
                 throw new UserException("Fail to parse columnsFromPath, 
expected: "
-                        + columnsFromPath + ", filePath: " + filePath);
+                        + expectedColumns + ", filePath: " + filePath);
             }
-            String parsedColumnName = caseSensitive ? pair[0] : 
pair[0].toLowerCase();
-            int index = columnsFromPath.indexOf(parsedColumnName);
+            String parsedColumnName = caseSensitive ? pair[0] : 
pair[0].toLowerCase(Locale.ROOT);
+            int index = expectedColumns.indexOf(parsedColumnName);
             if (index == -1) {
                 continue;
             }
-            columns[index] = 
HiveExternalMetaCache.HIVE_DEFAULT_PARTITION.equals(pair[1])
-                ? FeConstants.null_string : pair[1];
+            boolean isNull = 
HiveExternalMetaCache.HIVE_DEFAULT_PARTITION.equals(pair[1]);
+            columns[index] = isNull ? "" : pair[1];
+            columnsFromPathIsNull[index] = isNull;
             size++;
-            if (size >= columnsFromPath.size()) {
+            if (size >= expectedColumns.size()) {
                 break;
             }
         }
-        if (size != columnsFromPath.size()) {
+        if (size != expectedColumns.size()) {
             throw new UserException("Fail to parse columnsFromPath, expected: "
-                    + columnsFromPath + ", filePath: " + filePath);
+                    + expectedColumns + ", filePath: " + filePath);
+        }
+        return new ParsedColumnsFromPath(
+                Lists.newArrayList(columns), 
Lists.newArrayList(columnsFromPathIsNull));
+    }
+
+    public static ParsedColumnsFromPath 
parseColumnsFromPathWithNullInfoForLoad(
+            String filePath,
+            List<String> columnsFromPath,
+            boolean caseSensitive,
+            boolean isACID)
+            throws UserException {
+        ParsedColumnsFromPath parsed = parseColumnsFromPathWithNullInfo(
+                filePath, columnsFromPath, caseSensitive, isACID);
+        List<String> values = new ArrayList<>(parsed.getValues());
+        List<Boolean> isNull = new ArrayList<>(parsed.getIsNull());
+        for (int i = 0; i < values.size(); i++) {
+            if (FeConstants.null_string.equals(values.get(i))) {
+                values.set(i, "");
+                isNull.set(i, true);
+            }
         }
-        return Lists.newArrayList(columns);
+        return new ParsedColumnsFromPath(values, isNull);
     }
 
     public static ParsedColumnsFromPath normalizeColumnsFromPath(List<String> 
columnsFromPath) {
@@ -228,8 +262,7 @@ public class BrokerUtil {
         List<String> values = new ArrayList<>(columnsFromPath.size());
         List<Boolean> isNull = new ArrayList<>(columnsFromPath.size());
         for (String value : columnsFromPath) {
-            boolean nullValue = value == null || 
FeConstants.null_string.equals(value)
-                    || 
HiveExternalMetaCache.HIVE_DEFAULT_PARTITION.equals(value);
+            boolean nullValue = value == null || 
HiveExternalMetaCache.HIVE_DEFAULT_PARTITION.equals(value);
             values.add(nullValue ? "" : value);
             isNull.add(nullValue);
         }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/FileGroupInfo.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/FileGroupInfo.java
index d81ba7daa84..d6320277ef3 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/FileGroupInfo.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/FileGroupInfo.java
@@ -266,11 +266,12 @@ public class FileGroupInfo {
                         
Util.getOrInferCompressType(context.fileGroup.getFileFormatProperties().getCompressionType(),
                                 fileStatus.path);
                 context.params.setCompressType(compressType);
-                List<String> columnsFromPath = 
BrokerUtil.parseColumnsFromPath(fileStatus.path,
-                        context.fileGroup.getColumnNamesFromPath());
+                BrokerUtil.ParsedColumnsFromPath columnsFromPath =
+                        
BrokerUtil.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
+                                context.fileGroup.getColumnNamesFromPath(), 
true, false);
                 List<String> columnsFromPathKeys = 
context.fileGroup.getColumnNamesFromPath();
-                TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus, 
fileStatus.size, columnsFromPath,
-                        columnsFromPathKeys);
+                TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus, 
fileStatus.size,
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
locations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
             }
             scanRangeLocations.add(locations);
@@ -312,15 +313,16 @@ public class FileGroupInfo {
                     
Util.getOrInferCompressType(context.fileGroup.getFileFormatProperties().getCompressionType(),
                             fileStatus.path);
             context.params.setCompressType(compressType);
-            List<String> columnsFromPath = 
BrokerUtil.parseColumnsFromPath(fileStatus.path,
-                    context.fileGroup.getColumnNamesFromPath());
+            BrokerUtil.ParsedColumnsFromPath columnsFromPath =
+                    
BrokerUtil.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
+                            context.fileGroup.getColumnNamesFromPath(), true, 
false);
             List<String> columnsFromPathKeys = 
context.fileGroup.getColumnNamesFromPath();
             // Assign scan range locations only for broker load.
             // stream load has only one file, and no need to set multi scan 
ranges.
             if (tmpBytes > bytesPerInstance && jobType != JobType.STREAM_LOAD) 
{
                 long rangeBytes = bytesPerInstance - curInstanceBytes;
                 TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, rangeBytes,
-                        columnsFromPath, columnsFromPathKeys);
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
curLocations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
                 curFileOffset += rangeBytes;
 
@@ -329,8 +331,8 @@ public class FileGroupInfo {
                 curLocations = newLocations(context.params, brokerDesc, 
backendPolicy);
                 curInstanceBytes = 0;
             } else {
-                TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, leftBytes, columnsFromPath,
-                        columnsFromPathKeys);
+                TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, leftBytes,
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
curLocations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
                 curFileOffset = 0;
                 curInstanceBytes += leftBytes;
@@ -401,7 +403,8 @@ public class FileGroupInfo {
     }
 
     private TFileRangeDesc createFileRangeDesc(long curFileOffset, 
TBrokerFileStatus fileStatus, long rangeBytes,
-            List<String> columnsFromPath, List<String> columnsFromPathKeys) {
+            List<String> columnsFromPath, List<String> columnsFromPathKeys,
+            List<Boolean> columnsFromPathIsNull) {
         TFileRangeDesc rangeDesc = new TFileRangeDesc();
         if (jobType == JobType.BULK_LOAD) {
             rangeDesc.setPath(fileStatus.path);
@@ -410,6 +413,7 @@ public class FileGroupInfo {
             rangeDesc.setFileSize(fileStatus.size);
             rangeDesc.setColumnsFromPath(columnsFromPath);
             rangeDesc.setColumnsFromPathKeys(columnsFromPathKeys);
+            rangeDesc.setColumnsFromPathIsNull(columnsFromPathIsNull);
             if (getFileType() == TFileType.FILE_HDFS) {
                 URI fileUri = new Path(fileStatus.path).toUri();
                 rangeDesc.setFsName(fileUri.getScheme() + "://" + 
fileUri.getAuthority());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java
index d83326a1838..a7a0e7b381a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/FileQueryScanNode.java
@@ -507,11 +507,11 @@ public abstract class FileQueryScanNode extends 
FileScanNode {
             HiveSplit hiveSplit = (HiveSplit) fileSplit;
             isACID = hiveSplit.isACID();
         }
-        List<String> rawPartitionValues = fileSplit.getPartitionValues() == 
null
-                ? BrokerUtil.parseColumnsFromPath(fileSplit.getPathString(), 
pathPartitionKeys,
-                false, isACID) : fileSplit.getPartitionValues();
         BrokerUtil.ParsedColumnsFromPath partitionValues =
-                BrokerUtil.normalizeColumnsFromPath(rawPartitionValues);
+                fileSplit.getPartitionValues() == null
+                        ? BrokerUtil.parseColumnsFromPathWithNullInfo(
+                                fileSplit.getPathString(), pathPartitionKeys, 
false, isACID)
+                        : 
BrokerUtil.normalizeColumnsFromPath(fileSplit.getPartitionValues());
 
         TFileRangeDesc rangeDesc = createFileRangeDesc(fileSplit, 
partitionValues.getValues(),
                 pathPartitionKeys, partitionValues.getIsNull());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
index b52ef1f92a2..73986138c51 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HiveExternalMetaCache.java
@@ -28,7 +28,6 @@ import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Config;
-import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.security.authentication.AuthenticationConfig;
 import org.apache.doris.common.security.authentication.HadoopAuthenticator;
@@ -436,9 +435,10 @@ public class HiveExternalMetaCache extends 
AbstractExternalMetaCache {
             try {
                 FileCacheValue result = getFileCache(catalog, finalLocation, 
key.inputFormat,
                         key.getPartitionValues(), directoryLister, table);
+                // Replace default hive partition with null to distinguish it 
from a literal "\N".
                 for (int i = 0; i < result.getValuesSize(); i++) {
                     if 
(HIVE_DEFAULT_PARTITION.equals(result.getPartitionValues().get(i))) {
-                        result.getPartitionValues().set(i, 
FeConstants.null_string);
+                        result.getPartitionValues().set(i, null);
                     }
                 }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java
index 91c6da61a57..6c6888e3792 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hudi/source/HudiScanNode.java
@@ -27,6 +27,7 @@ import org.apache.doris.catalog.PartitionItem;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.common.util.BrokerUtil;
 import org.apache.doris.common.util.FileFormatUtils;
 import org.apache.doris.common.util.LocationPath;
 import org.apache.doris.datasource.ExternalTable;
@@ -330,8 +331,11 @@ public class HudiScanNode extends HiveScanNode {
                 formPathKeys.add(entry.getKey());
                 formPathValues.add(entry.getValue());
             }
+            BrokerUtil.ParsedColumnsFromPath parsedColumnsFromPath =
+                    BrokerUtil.normalizeColumnsFromPath(formPathValues);
             rangeDesc.setColumnsFromPathKeys(formPathKeys);
-            rangeDesc.setColumnsFromPath(formPathValues);
+            rangeDesc.setColumnsFromPath(parsedColumnsFromPath.getValues());
+            
rangeDesc.setColumnsFromPathIsNull(parsedColumnsFromPath.getIsNull());
         }
         rangeDesc.setTableFormatParams(tableFormatFileDesc);
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
index 84eed01e42c..df081f72013 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
@@ -800,12 +800,22 @@ public class IcebergUtils {
     }
 
     public static List<String> getIdentityPartitionColumns(Table table) {
+        return getIdentityPartitionColumns(table, false, false);
+    }
+
+    public static List<String> getIdentityPartitionColumns(Table table,
+            boolean enableMappingVarbinary, boolean enableMappingTimestampTz) {
         LinkedHashSet<String> partitionColumns = new LinkedHashSet<>();
         for (PartitionSpec spec : table.specs().values()) {
             for (PartitionField partitionField : spec.fields()) {
                 if (!partitionField.transform().isIdentity()) {
                     continue;
                 }
+                NestedField sourceField = 
table.schema().findField(partitionField.sourceId());
+                if (sourceField == null || !isSupportedPartitionValueType(
+                        sourceField.type(), enableMappingVarbinary, 
enableMappingTimestampTz)) {
+                    continue;
+                }
                 String columnName = 
table.schema().findColumnName(partitionField.sourceId());
                 if (columnName != null) {
                     partitionColumns.add(columnName);
@@ -815,8 +825,47 @@ public class IcebergUtils {
         return new ArrayList<>(partitionColumns);
     }
 
+    /**
+     * Get identity partition columns that exist in all partition specs.
+     * The legacy file scanner uses partition columns in the first scan range 
for all ranges,
+     * so only common identity partition columns can be used for partition 
pruning.
+     */
+    public static List<String> getCommonIdentityPartitionColumns(Table table) {
+        return getCommonIdentityPartitionColumns(table, false, false);
+    }
+
+    public static List<String> getCommonIdentityPartitionColumns(Table table,
+            boolean enableMappingVarbinary, boolean enableMappingTimestampTz) {
+        LinkedHashSet<Integer> commonSourceIds = new LinkedHashSet<>();
+        for (PartitionField field : table.spec().fields()) {
+            NestedField sourceField = 
table.schema().findField(field.sourceId());
+            if (field.transform().isIdentity() && sourceField != null
+                    && isSupportedPartitionValueType(
+                            sourceField.type(), enableMappingVarbinary, 
enableMappingTimestampTz)) {
+                commonSourceIds.add(field.sourceId());
+            }
+        }
+        for (PartitionSpec spec : table.specs().values()) {
+            Set<Integer> specIdentitySourceIds = spec.fields().stream()
+                    .filter(field -> field.transform().isIdentity())
+                    .map(PartitionField::sourceId)
+                    .collect(Collectors.toSet());
+            commonSourceIds.retainAll(specIdentitySourceIds);
+        }
+        return commonSourceIds.stream()
+                .map(table.schema()::findColumnName)
+                .filter(columnName -> columnName != null)
+                .collect(Collectors.toList());
+    }
+
     public static Map<String, String> 
getIdentityPartitionInfoMap(PartitionData partitionData,
             PartitionSpec partitionSpec, Table table, String timeZone) {
+        return getIdentityPartitionInfoMap(partitionData, partitionSpec, 
table, timeZone, false, false);
+    }
+
+    public static Map<String, String> 
getIdentityPartitionInfoMap(PartitionData partitionData,
+            PartitionSpec partitionSpec, Table table, String timeZone,
+            boolean enableMappingVarbinary, boolean enableMappingTimestampTz) {
         Map<String, String> partitionInfoMap = Maps.newLinkedHashMap();
         List<NestedField> fields = 
partitionData.getPartitionType().asNestedType().fields();
         List<PartitionField> partitionFields = partitionSpec.fields();
@@ -829,8 +878,8 @@ public class IcebergUtils {
             if (!partitionField.transform().isIdentity()) {
                 continue;
             }
-            TypeID partitionTypeId = field.type().typeId();
-            if (partitionTypeId == TypeID.BINARY || partitionTypeId == 
TypeID.FIXED) {
+            if (!isSupportedPartitionValueType(
+                    field.type(), enableMappingVarbinary, 
enableMappingTimestampTz)) {
                 continue;
             }
 
@@ -849,6 +898,19 @@ public class IcebergUtils {
         return partitionInfoMap;
     }
 
+    private static boolean 
isSupportedPartitionValueType(org.apache.iceberg.types.Type type,
+            boolean enableMappingVarbinary, boolean enableMappingTimestampTz) {
+        TypeID typeId = type.typeId();
+        if (typeId == TypeID.BINARY || typeId == TypeID.FIXED) {
+            return false;
+        }
+        if (enableMappingVarbinary && typeId == TypeID.UUID) {
+            return false;
+        }
+        return !enableMappingTimestampTz || typeId != TypeID.TIMESTAMP
+                || !((TimestampType) type).shouldAdjustToUTC();
+    }
+
     public static List<String> getPartitionValues(PartitionData partitionData, 
PartitionSpec partitionSpec,
             String timeZone) {
         List<NestedField> fields = 
partitionData.getPartitionType().asNestedType().fields();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java
index 9eea2a95736..957ab6ed55e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java
@@ -25,6 +25,7 @@ import org.apache.doris.analysis.TupleDescriptor;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.Pair;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.profile.SummaryProfile;
 import org.apache.doris.common.security.authentication.ExecutionAuthenticator;
@@ -153,8 +154,13 @@ public class IcebergScanNode extends FileQueryScanNode {
     private long countFromSnapshot;
     private static final long COUNT_WITH_PARALLEL_SPLITS = 10000;
     private long targetSplitSize = 0;
-    // This is used to avoid repeatedly calculating partition info map for the 
same partition data.
-    private Map<PartitionData, Map<String, String>> partitionMapInfos;
+    // Used to avoid repeatedly calculating partition info map for the same
+    // partition data and spec.
+    private Map<Pair<Integer, PartitionData>, Map<String, String>> 
partitionMapInfos;
+    private List<String> orderedPathPartitionKeys;
+    private List<String> orderedPartitionMetadataKeys;
+    private boolean enableMappingVarbinaryForPartitionMetadata;
+    private boolean enableMappingTimestampTzForPartitionMetadata;
     private boolean isPartitionedTable;
     private int formatVersion;
     private ExecutionAuthenticator preExecutionAuthenticator;
@@ -251,6 +257,7 @@ public class IcebergScanNode extends FileQueryScanNode {
             icebergTable = source.getIcebergTable();
             icebergTable = useFrozenTableGeneration(icebergTable);
             partitionMapInfos = new HashMap<>();
+            initializePartitionMetadata();
             isPartitionedTable = icebergTable.spec().isPartitioned();
             // Metadata tables (system tables) are not BaseTable instances, so 
we need to handle this case
             if (icebergTable instanceof BaseTable) {
@@ -409,21 +416,75 @@ public class IcebergScanNode extends FileQueryScanNode {
             
deleteFilesDescByReferencedDataFile.put(icebergSplit.getOriginalPath(), 
nonEqualityDeleteFileDesc);
         }
         tableFormatFileDesc.setIcebergParams(fileDesc);
-        Map<String, String> partitionValues = 
icebergSplit.getIcebergPartitionValues();
-        if (partitionValues != null) {
-            List<String> fromPathKeys = new ArrayList<>();
-            List<String> fromPathValues = new ArrayList<>();
-            List<Boolean> fromPathIsNull = new ArrayList<>();
-            for (Map.Entry<String, String> entry : partitionValues.entrySet()) 
{
-                fromPathKeys.add(entry.getKey());
-                fromPathValues.add(entry.getValue() != null ? entry.getValue() 
: "");
-                fromPathIsNull.add(entry.getValue() == null);
+        setPartitionValues(rangeDesc, 
icebergSplit.getIcebergPartitionValues());
+        rangeDesc.setTableFormatParams(tableFormatFileDesc);
+    }
+
+    private List<String> getOrderedPathPartitionKeys() {
+        if (orderedPathPartitionKeys == null) {
+            initializePartitionMetadata();
+        }
+        return orderedPathPartitionKeys;
+    }
+
+    private List<String> getOrderedPartitionMetadataKeys() {
+        if (orderedPartitionMetadataKeys == null) {
+            initializePartitionMetadata();
+        }
+        return orderedPartitionMetadataKeys;
+    }
+
+    private void initializePartitionMetadata() {
+        if (isSystemTable || icebergTable == null) {
+            orderedPathPartitionKeys = Collections.emptyList();
+            orderedPartitionMetadataKeys = Collections.emptyList();
+            return;
+        }
+        enableMappingVarbinaryForPartitionMetadata = 
getEnableMappingVarbinary();
+        enableMappingTimestampTzForPartitionMetadata = 
getEnableMappingTimestampTz();
+        orderedPathPartitionKeys = Collections.unmodifiableList(
+                IcebergUtils.getCommonIdentityPartitionColumns(icebergTable,
+                        enableMappingVarbinaryForPartitionMetadata,
+                        enableMappingTimestampTzForPartitionMetadata));
+        if (sessionVariable.enableFileScannerV2) {
+            orderedPartitionMetadataKeys = Collections.unmodifiableList(
+                    IcebergUtils.getIdentityPartitionColumns(icebergTable,
+                            enableMappingVarbinaryForPartitionMetadata,
+                            enableMappingTimestampTzForPartitionMetadata));
+        } else {
+            orderedPartitionMetadataKeys = orderedPathPartitionKeys;
+        }
+    }
+
+    @VisibleForTesting
+    void setPartitionValues(TFileRangeDesc rangeDesc, Map<String, String> 
partitionValues) {
+        rangeDesc.unsetColumnsFromPathKeys();
+        rangeDesc.unsetColumnsFromPath();
+        rangeDesc.unsetColumnsFromPathIsNull();
+
+        List<String> orderedPartitionKeys = getOrderedPartitionMetadataKeys();
+        if (orderedPartitionKeys.isEmpty() || partitionValues == null || 
partitionValues.isEmpty()) {
+            return;
+        }
+
+        List<String> fromPathKeys = new 
ArrayList<>(orderedPartitionKeys.size());
+        List<String> fromPathValues = new 
ArrayList<>(orderedPartitionKeys.size());
+        List<Boolean> fromPathIsNull = new 
ArrayList<>(orderedPartitionKeys.size());
+        for (String partitionKey : orderedPartitionKeys) {
+            if (!partitionValues.containsKey(partitionKey)) {
+                continue;
             }
-            rangeDesc.setColumnsFromPathKeys(fromPathKeys);
-            rangeDesc.setColumnsFromPath(fromPathValues);
-            rangeDesc.setColumnsFromPathIsNull(fromPathIsNull);
+            String partitionValue = partitionValues.get(partitionKey);
+            fromPathKeys.add(partitionKey);
+            fromPathValues.add(partitionValue == null ? "" : partitionValue);
+            fromPathIsNull.add(partitionValue == null);
         }
-        rangeDesc.setTableFormatParams(tableFormatFileDesc);
+        if (fromPathKeys.isEmpty()) {
+            return;
+        }
+        rangeDesc.setColumnsFromPathKeys(fromPathKeys);
+        rangeDesc.setColumnsFromPath(fromPathValues);
+        rangeDesc.setColumnsFromPathIsNull(fromPathIsNull);
     }
 
     private void setIcebergPositionDeleteSysTableParams(TFileRangeDesc 
rangeDesc, IcebergSplit icebergSplit,
@@ -1082,11 +1143,11 @@ public class IcebergScanNode extends FileQueryScanNode {
                 split.setPartitionSpecId(specId);
                 split.setPartitionDataJson(IcebergUtils.getPartitionDataJson(
                         partitionData, partitionSpec, 
sessionVariable.getTimeZone()));
-            }
-            if (sessionVariable.isEnableRuntimeFilterPartitionPrune()) {
                 Map<String, String> partitionInfoMap = 
partitionMapInfos.computeIfAbsent(
-                        partitionData, k -> 
IcebergUtils.getIdentityPartitionInfoMap(
-                                partitionData, partitionSpec, icebergTable, 
sessionVariable.getTimeZone()));
+                        Pair.of(specId, partitionData), k -> 
IcebergUtils.getIdentityPartitionInfoMap(
+                                partitionData, partitionSpec, icebergTable, 
sessionVariable.getTimeZone(),
+                                enableMappingVarbinaryForPartitionMetadata,
+                                enableMappingTimestampTzForPartitionMetadata));
                 // A spec may mix identity and transformed fields. Keep its 
identity values so a
                 // runtime filter can prune that split without treating source 
columns as constants
                 // for files written under another evolved spec.
@@ -1094,7 +1155,7 @@ public class IcebergScanNode extends FileQueryScanNode {
                     split.setIcebergPartitionValues(partitionInfoMap);
                 }
             } else {
-                partitionMapInfos.put(partitionData, null);
+                partitionMapInfos.put(Pair.of(specId, null), 
Collections.emptyMap());
             }
         }
         return split;
@@ -1538,20 +1599,7 @@ public class IcebergScanNode extends FileQueryScanNode {
 
     @Override
     public List<String> getPathPartitionKeys() throws UserException {
-        // return 
icebergTable.spec().fields().stream().map(PartitionField::name).map(String::toLowerCase)
-        //         .collect(Collectors.toList());
-        /**First, iceberg partition columns are based on existing fields, 
which will be stored in the actual data file.
-         * Second, iceberg partition columns support Partition transforms. In 
this case, the path partition key is not
-         * equal to the column name of the partition column, so remove this 
code and get all the columns you want to
-         * read from the file.
-         * Related code:
-         *  be/src/vec/exec/scan/vfile_scanner.cpp:
-         *      VFileScanner::_init_expr_ctxes()
-         *          if (slot_info.is_file_slot) {
-         *              xxxx
-         *          }
-         */
-        return new ArrayList<>();
+        return getOrderedPathPartitionKeys();
     }
 
     private void recordManifestCacheAccess(boolean cacheHit) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
index b0862277a63..d8885a22399 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/load/NereidsFileGroupInfo.java
@@ -279,11 +279,12 @@ public class NereidsFileGroupInfo {
                         
context.fileGroup.getFileFormatProperties().getCompressionType(),
                         fileStatus.path);
                 context.params.setCompressType(compressType);
-                List<String> columnsFromPath = 
BrokerUtil.parseColumnsFromPath(fileStatus.path,
-                        context.fileGroup.getColumnNamesFromPath());
+                BrokerUtil.ParsedColumnsFromPath columnsFromPath =
+                        
BrokerUtil.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
+                                context.fileGroup.getColumnNamesFromPath(), 
true, false);
                 List<String> columnsFromPathKeys = 
context.fileGroup.getColumnNamesFromPath();
-                TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus, 
fileStatus.size, columnsFromPath,
-                        columnsFromPathKeys);
+                TFileRangeDesc rangeDesc = createFileRangeDesc(0, fileStatus, 
fileStatus.size,
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
locations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
             }
             scanRangeLocations.add(locations);
@@ -331,15 +332,16 @@ public class NereidsFileGroupInfo {
                     
context.fileGroup.getFileFormatProperties().getCompressionType(),
                     fileStatus.path);
             context.params.setCompressType(compressType);
-            List<String> columnsFromPath = 
BrokerUtil.parseColumnsFromPath(fileStatus.path,
-                    context.fileGroup.getColumnNamesFromPath());
+            BrokerUtil.ParsedColumnsFromPath columnsFromPath =
+                    
BrokerUtil.parseColumnsFromPathWithNullInfoForLoad(fileStatus.path,
+                            context.fileGroup.getColumnNamesFromPath(), true, 
false);
             List<String> columnsFromPathKeys = 
context.fileGroup.getColumnNamesFromPath();
             // Assign scan range locations only for broker load.
             // stream load has only one file, and no need to set multi scan 
ranges.
             if (tmpBytes > bytesPerInstance && jobType != 
FileGroupInfo.JobType.STREAM_LOAD) {
                 long rangeBytes = bytesPerInstance - curInstanceBytes;
                 TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, rangeBytes,
-                        columnsFromPath, columnsFromPathKeys);
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
curLocations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
                 curFileOffset += rangeBytes;
 
@@ -348,8 +350,8 @@ public class NereidsFileGroupInfo {
                 curLocations = newLocations(context.params, brokerDesc, 
backendPolicy);
                 curInstanceBytes = 0;
             } else {
-                TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, leftBytes, columnsFromPath,
-                        columnsFromPathKeys);
+                TFileRangeDesc rangeDesc = createFileRangeDesc(curFileOffset, 
fileStatus, leftBytes,
+                        columnsFromPath.getValues(), columnsFromPathKeys, 
columnsFromPath.getIsNull());
                 
curLocations.getScanRange().getExtScanRange().getFileScanRange().addToRanges(rangeDesc);
                 curFileOffset = 0;
                 curInstanceBytes += leftBytes;
@@ -420,7 +422,8 @@ public class NereidsFileGroupInfo {
     }
 
     private TFileRangeDesc createFileRangeDesc(long curFileOffset, 
TBrokerFileStatus fileStatus, long rangeBytes,
-            List<String> columnsFromPath, List<String> columnsFromPathKeys) {
+            List<String> columnsFromPath, List<String> columnsFromPathKeys,
+            List<Boolean> columnsFromPathIsNull) {
         TFileRangeDesc rangeDesc = new TFileRangeDesc();
         if (jobType == FileGroupInfo.JobType.BULK_LOAD) {
             rangeDesc.setPath(fileStatus.path);
@@ -429,6 +432,7 @@ public class NereidsFileGroupInfo {
             rangeDesc.setFileSize(fileStatus.size);
             rangeDesc.setColumnsFromPath(columnsFromPath);
             rangeDesc.setColumnsFromPathKeys(columnsFromPathKeys);
+            rangeDesc.setColumnsFromPathIsNull(columnsFromPathIsNull);
             if (getFileType() == TFileType.FILE_HDFS) {
                 URI fileUri = new Path(fileStatus.path).toUri();
                 rangeDesc.setFsName(fileUri.getScheme() + "://" + 
fileUri.getAuthority());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
index 0c24316cdc4..aa644f941a4 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/util/BrokerUtilTest.java
@@ -158,14 +158,47 @@ public class BrokerUtilTest {
 
     }
 
+    @Test
+    public void parseColumnsFromPathPreservesNullMetadataAndInputKeys() throws 
Exception {
+        List<String> partitionKeys = Lists.newArrayList("region", "dt");
+        BrokerUtil.ParsedColumnsFromPath parsed = 
BrokerUtil.parseColumnsFromPathWithNullInfo(
+                "hdfs://host/table/Region=cn/Dt=" + 
HiveExternalMetaCache.HIVE_DEFAULT_PARTITION
+                        + "/data.parquet",
+                partitionKeys, false, false);
+
+        Assert.assertEquals(Lists.newArrayList("cn", ""), parsed.getValues());
+        Assert.assertEquals(Lists.newArrayList(false, true), 
parsed.getIsNull());
+        Assert.assertEquals(Lists.newArrayList("region", "dt"), partitionKeys);
+
+        parsed = BrokerUtil.parseColumnsFromPathWithNullInfo(
+                "hdfs://host/table/p=\\N/data.orc", 
Collections.singletonList("p"), true, false);
+        Assert.assertEquals(Collections.singletonList("\\N"), 
parsed.getValues());
+        Assert.assertEquals(Collections.singletonList(false), 
parsed.getIsNull());
+
+        parsed = BrokerUtil.parseColumnsFromPathWithNullInfo(
+                "hdfs://host/table/p=value/delta_1_1/bucket_00000",
+                Collections.singletonList("p"), true, true);
+        Assert.assertEquals(Collections.singletonList("value"), 
parsed.getValues());
+        Assert.assertEquals(Collections.singletonList(false), 
parsed.getIsNull());
+    }
+
     @Test
     public void normalizeColumnsFromPathPreservesNullInfo() {
         BrokerUtil.ParsedColumnsFromPath parsed = 
BrokerUtil.normalizeColumnsFromPath(
                 Lists.newArrayList("p1", FeConstants.null_string,
                         HiveExternalMetaCache.HIVE_DEFAULT_PARTITION, null));
 
-        Assert.assertEquals(Lists.newArrayList("p1", "", "", ""), 
parsed.getValues());
-        Assert.assertEquals(Lists.newArrayList(false, true, true, true), 
parsed.getIsNull());
+        Assert.assertEquals(Lists.newArrayList("p1", "\\N", "", ""), 
parsed.getValues());
+        Assert.assertEquals(Lists.newArrayList(false, false, true, true), 
parsed.getIsNull());
+    }
+
+    @Test
+    public void parseColumnsFromPathForLoadKeepsLegacyNullSemantics() throws 
Exception {
+        BrokerUtil.ParsedColumnsFromPath parsed = 
BrokerUtil.parseColumnsFromPathWithNullInfoForLoad(
+                "hdfs://host/table/p=\\N/data.orc", 
Collections.singletonList("p"), true, false);
+
+        Assert.assertEquals(Collections.singletonList(""), parsed.getValues());
+        Assert.assertEquals(Collections.singletonList(true), 
parsed.getIsNull());
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
index 835959c6c0c..2dd1f300c24 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
@@ -471,6 +471,35 @@ public class IcebergUtilsTest {
         Assert.assertEquals(Arrays.asList("Dt", "id"), 
IcebergUtils.getIdentityPartitionColumns(table));
     }
 
+    @Test
+    public void testGetCommonIdentityPartitionColumnsUsesSafeIntersection() {
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "id", Types.IntegerType.get()),
+                Types.NestedField.required(2, "Dt", Types.StringType.get()),
+                Types.NestedField.required(3, "ts", 
Types.TimestampType.withoutZone()));
+        PartitionSpec oldSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(1)
+                .identity("id")
+                .identity("Dt")
+                .build();
+        PartitionSpec currentSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(2)
+                .identity("Dt")
+                .day("ts")
+                .build();
+        Map<Integer, PartitionSpec> specs = new LinkedHashMap<>();
+        specs.put(oldSpec.specId(), oldSpec);
+        specs.put(currentSpec.specId(), currentSpec);
+
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.schema()).thenReturn(schema);
+        Mockito.when(table.spec()).thenReturn(currentSpec);
+        Mockito.when(table.specs()).thenReturn(specs);
+
+        Assert.assertEquals(Collections.singletonList("Dt"),
+                IcebergUtils.getCommonIdentityPartitionColumns(table));
+    }
+
     @Test
     public void testGetIdentityPartitionInfoMapReturnsIdentityColumnsOnly() {
         Schema schema = new Schema(
@@ -492,6 +521,36 @@ public class IcebergUtilsTest {
         Assert.assertEquals(Collections.singletonMap("Dt", "2025-01-01"), 
partitionInfoMap);
     }
 
+    @Test
+    public void testMappedTypesAreExcludedFromPartitionMetadata() {
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "Dt", Types.StringType.get()),
+                Types.NestedField.required(2, "uuid_col", 
Types.UUIDType.get()),
+                Types.NestedField.required(3, "ts_tz", 
Types.TimestampType.withZone()));
+        PartitionSpec partitionSpec = PartitionSpec.builderFor(schema)
+                .identity("Dt")
+                .identity("uuid_col")
+                .identity("ts_tz")
+                .build();
+        PartitionData partitionData = new 
PartitionData(partitionSpec.partitionType());
+        partitionData.set(0, "2026-08-03");
+        partitionData.set(1, 
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"));
+        partitionData.set(2, 0L);
+
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.schema()).thenReturn(schema);
+        Mockito.when(table.spec()).thenReturn(partitionSpec);
+        
Mockito.when(table.specs()).thenReturn(Collections.singletonMap(partitionSpec.specId(),
 partitionSpec));
+
+        Assert.assertEquals(Collections.singletonList("Dt"),
+                IcebergUtils.getIdentityPartitionColumns(table, true, true));
+        Assert.assertEquals(Collections.singletonList("Dt"),
+                IcebergUtils.getCommonIdentityPartitionColumns(table, true, 
true));
+        Assert.assertEquals(Collections.singletonMap("Dt", "2026-08-03"),
+                IcebergUtils.getIdentityPartitionInfoMap(
+                        partitionData, partitionSpec, table, "Asia/Shanghai", 
true, true));
+    }
+
     @Test
     public void 
testGetIdentityPartitionInfoMapSupportsFloatingPointPartitions() {
         Schema schema = new Schema(
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/source/IcebergScanNodeTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/source/IcebergScanNodeTest.java
index bf9799afed3..670754614dd 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/source/IcebergScanNodeTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/source/IcebergScanNodeTest.java
@@ -90,7 +90,10 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -116,19 +119,26 @@ public class IcebergScanNodeTest {
     private static class TestIcebergScanNode extends IcebergScanNode {
         private final boolean enableMappingVarbinary;
         private final boolean batchMode;
+        private final boolean enableMappingTimestampTz;
         private TableScan tableScan;
 
         TestIcebergScanNode(SessionVariable sv) {
-            this(sv, false, false);
+            this(sv, false, false, false);
         }
 
         TestIcebergScanNode(SessionVariable sv, boolean 
enableMappingVarbinary) {
-            this(sv, enableMappingVarbinary, false);
+            this(sv, enableMappingVarbinary, false, false);
         }
 
         TestIcebergScanNode(SessionVariable sv, boolean 
enableMappingVarbinary, boolean batchMode) {
+            this(sv, enableMappingVarbinary, false, batchMode);
+        }
+
+        TestIcebergScanNode(SessionVariable sv, boolean enableMappingVarbinary,
+                boolean enableMappingTimestampTz, boolean batchMode) {
             super(new PlanNodeId(0), new TupleDescriptor(new TupleId(0)), sv, 
ScanContext.EMPTY);
             this.enableMappingVarbinary = enableMappingVarbinary;
+            this.enableMappingTimestampTz = enableMappingTimestampTz;
             this.batchMode = batchMode;
         }
 
@@ -155,6 +165,11 @@ public class IcebergScanNodeTest {
             return enableMappingVarbinary;
         }
 
+        @Override
+        protected boolean getEnableMappingTimestampTz() {
+            return enableMappingTimestampTz;
+        }
+
         @Override
         public List<String> getPathPartitionKeys() {
             return Collections.emptyList();
@@ -230,6 +245,153 @@ public class IcebergScanNodeTest {
                 .getFields().get(1).getFieldPtr().getName());
     }
 
+    @Test
+    public void testSetPartitionValuesBuildsStableAlignedMetadata() throws 
Exception {
+        TestIcebergScanNode node = new TestIcebergScanNode(new 
SessionVariable());
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "Region", 
Types.StringType.get()),
+                Types.NestedField.required(2, "Dt", Types.StringType.get()));
+        PartitionSpec spec = PartitionSpec.builderFor(schema)
+                .identity("Region")
+                .identity("Dt")
+                .build();
+        Map<Integer, PartitionSpec> specs = new LinkedHashMap<>();
+        specs.put(spec.specId(), spec);
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.schema()).thenReturn(schema);
+        Mockito.when(table.spec()).thenReturn(spec);
+        Mockito.when(table.specs()).thenReturn(specs);
+        setIcebergTable(node, table);
+
+        Map<String, String> partitionValues = new HashMap<>();
+        partitionValues.put("Dt", null);
+        partitionValues.put("Region", "cn");
+        TFileRangeDesc rangeDesc = new TFileRangeDesc();
+        node.setPartitionValues(rangeDesc, partitionValues);
+
+        Assert.assertEquals(Arrays.asList("Region", "Dt"), 
rangeDesc.getColumnsFromPathKeys());
+        Assert.assertEquals(Arrays.asList("cn", ""), 
rangeDesc.getColumnsFromPath());
+        Assert.assertEquals(Arrays.asList(false, true), 
rangeDesc.getColumnsFromPathIsNull());
+    }
+
+    @Test
+    public void testSetPartitionValuesUsesPerSpecMetadataWithFileScannerV2() 
throws Exception {
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableFileScannerV2 = true;
+        TestIcebergScanNode node = new TestIcebergScanNode(sessionVariable);
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "Region", 
Types.StringType.get()),
+                Types.NestedField.required(2, "Dt", Types.StringType.get()),
+                Types.NestedField.required(3, "Category", 
Types.StringType.get()));
+        PartitionSpec oldSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(1)
+                .identity("Region")
+                .identity("Dt")
+                .build();
+        PartitionSpec currentSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(2)
+                .identity("Dt")
+                .identity("Category")
+                .build();
+        Map<Integer, PartitionSpec> specs = new LinkedHashMap<>();
+        specs.put(oldSpec.specId(), oldSpec);
+        specs.put(currentSpec.specId(), currentSpec);
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.schema()).thenReturn(schema);
+        Mockito.when(table.spec()).thenReturn(currentSpec);
+        Mockito.when(table.specs()).thenReturn(specs);
+        setIcebergTable(node, table);
+
+        Map<String, String> partitionValues = new HashMap<>();
+        partitionValues.put("Category", "books");
+        partitionValues.put("Dt", null);
+        TFileRangeDesc rangeDesc = new TFileRangeDesc();
+        node.setPartitionValues(rangeDesc, partitionValues);
+
+        Assert.assertEquals(Arrays.asList("Dt", "Category"), 
rangeDesc.getColumnsFromPathKeys());
+        Assert.assertEquals(Arrays.asList("", "books"), 
rangeDesc.getColumnsFromPath());
+        Assert.assertEquals(Arrays.asList(true, false), 
rangeDesc.getColumnsFromPathIsNull());
+    }
+
+    @Test
+    public void 
testSetPartitionValuesKeepsCommonMetadataWithLegacyFileScanner() throws 
Exception {
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableFileScannerV2 = false;
+        TestIcebergScanNode node = new TestIcebergScanNode(sessionVariable);
+        Schema schema = new Schema(
+                Types.NestedField.required(1, "Region", 
Types.StringType.get()),
+                Types.NestedField.required(2, "Dt", Types.StringType.get()),
+                Types.NestedField.required(3, "Category", 
Types.StringType.get()));
+        PartitionSpec oldSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(1)
+                .identity("Region")
+                .identity("Dt")
+                .build();
+        PartitionSpec currentSpec = PartitionSpec.builderFor(schema)
+                .withSpecId(2)
+                .identity("Dt")
+                .identity("Category")
+                .build();
+        Map<Integer, PartitionSpec> specs = new LinkedHashMap<>();
+        specs.put(oldSpec.specId(), oldSpec);
+        specs.put(currentSpec.specId(), currentSpec);
+        Table table = Mockito.mock(Table.class);
+        Mockito.when(table.schema()).thenReturn(schema);
+        Mockito.when(table.spec()).thenReturn(currentSpec);
+        Mockito.when(table.specs()).thenReturn(specs);
+        setIcebergTable(node, table);
+
+        Map<String, String> partitionValues = new HashMap<>();
+        partitionValues.put("Region", "cn");
+        partitionValues.put("Dt", "2026-07-31");
+        TFileRangeDesc rangeDesc = new TFileRangeDesc();
+        node.setPartitionValues(rangeDesc, partitionValues);
+
+        Assert.assertEquals(Collections.singletonList("Dt"), 
rangeDesc.getColumnsFromPathKeys());
+        Assert.assertEquals(Collections.singletonList("2026-07-31"), 
rangeDesc.getColumnsFromPath());
+        Assert.assertEquals(Collections.singletonList(false), 
rangeDesc.getColumnsFromPathIsNull());
+    }
+
+    @Test
+    public void testSetPartitionValuesSkipsValuesUnsupportedByMappedTypes() 
throws Exception {
+        for (boolean enableFileScannerV2 : Arrays.asList(false, true)) {
+            SessionVariable sessionVariable = new SessionVariable();
+            sessionVariable.enableFileScannerV2 = enableFileScannerV2;
+            sessionVariable.setTimeZone("Asia/Shanghai");
+            TestIcebergScanNode node = new 
TestIcebergScanNode(sessionVariable, true, true, false);
+            Schema schema = new Schema(
+                    Types.NestedField.required(1, "Dt", 
Types.StringType.get()),
+                    Types.NestedField.required(2, "uuid_col", 
Types.UUIDType.get()),
+                    Types.NestedField.required(3, "ts_tz", 
Types.TimestampType.withZone()));
+            PartitionSpec spec = PartitionSpec.builderFor(schema)
+                    .identity("Dt")
+                    .identity("uuid_col")
+                    .identity("ts_tz")
+                    .build();
+            Table table = Mockito.mock(Table.class);
+            Mockito.when(table.schema()).thenReturn(schema);
+            Mockito.when(table.spec()).thenReturn(spec);
+            
Mockito.when(table.specs()).thenReturn(Collections.singletonMap(spec.specId(), 
spec));
+            setIcebergTable(node, table);
+
+            Map<String, String> partitionValues = new LinkedHashMap<>();
+            partitionValues.put("Dt", "2026-08-03");
+            partitionValues.put("uuid_col", 
"123e4567-e89b-12d3-a456-426614174000");
+            partitionValues.put("ts_tz", "2026-08-03T16:00:00");
+            TFileRangeDesc rangeDesc = new TFileRangeDesc();
+            node.setPartitionValues(rangeDesc, partitionValues);
+
+            Assert.assertEquals(Collections.singletonList("Dt"), 
rangeDesc.getColumnsFromPathKeys());
+            Assert.assertEquals(Collections.singletonList("2026-08-03"), 
rangeDesc.getColumnsFromPath());
+            Assert.assertEquals(Collections.singletonList(false), 
rangeDesc.getColumnsFromPathIsNull());
+
+            Mockito.clearInvocations(table);
+            node.setPartitionValues(new TFileRangeDesc(), partitionValues);
+            Mockito.verify(table, Mockito.never()).specs();
+            Mockito.verify(table, Mockito.never()).schema();
+        }
+    }
+
     @Test
     public void testExtractNameMappingDistinguishesAbsentAndEmpty() throws 
Exception {
         TestIcebergScanNode node = new TestIcebergScanNode(new 
SessionVariable());
@@ -879,6 +1041,11 @@ public class IcebergScanNodeTest {
         Field icebergTableField = 
IcebergScanNode.class.getDeclaredField("icebergTable");
         icebergTableField.setAccessible(true);
         icebergTableField.set(node, table);
+        for (String fieldName : Arrays.asList("orderedPathPartitionKeys", 
"orderedPartitionMetadataKeys")) {
+            Field field = IcebergScanNode.class.getDeclaredField(fieldName);
+            field.setAccessible(true);
+            field.set(node, null);
+        }
     }
 
     private static void setIcebergSource(IcebergScanNode node, IcebergSource 
source) throws Exception {


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

Reply via email to