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

924060929 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 d8e75cf63cd [fix](iceberg) Project Iceberg system table scans (#65262)
d8e75cf63cd is described below

commit d8e75cf63cd8372a1d7e4b68d629859d9dfa75dc
Author: Socrates <[email protected]>
AuthorDate: Fri Jul 17 16:03:48 2026 +0800

    [fix](iceberg) Project Iceberg system table scans (#65262)
    
    Iceberg system table scans serialize Iceberg SDK `FileScanTask` objects
    and the Java metadata scanner materializes rows from the task schema.
    Doris did not pass the actually required system table columns into the
    Iceberg SDK scan, so metadata tables such as `$files` and `$data_files`
    were planned with the full schema. For files metadata tables, the full
    schema can include virtual `readable_metrics` even when the SQL only
    requests columns such as `file_size_in_bytes`.
    
    This PR applies SDK top-level column projection only for Iceberg system
    table scans. Normal Iceberg data table scans continue to rely on Doris
    BE scan range params for column pruning.
    
    ### What changed?
    
    - Add system-table-only `TableScan.select(...)` projection in
    `IcebergScanNode` before planning the Iceberg SDK scan.
    - Skip synthesized/global row id and Iceberg row lineage columns when
    building the SDK projection list.
    - Add regression coverage for ORC and Parquet Iceberg tables with
    `map<boolean, boolean>` columns, verifying projected `$data_files` and
    `$files` queries.
---
 .../doris/iceberg/IcebergSysTableJniScanner.java   |  57 +++----
 .../apache/doris/datasource/FileQueryScanNode.java |   8 +-
 .../iceberg/IcebergSysExternalTable.java           |   4 +
 .../datasource/iceberg/source/IcebergScanNode.java |  65 +++++++-
 .../trees/plans/logical/LogicalFileScan.java       |   7 +-
 .../iceberg/source/IcebergScanNodeTest.java        |  70 ++++++++
 .../trees/plans/logical/LogicalFileScanTest.java   |  22 +++
 .../test_iceberg_system_table_projection.groovy    | 177 +++++++++++++++++++++
 8 files changed, 367 insertions(+), 43 deletions(-)

diff --git 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
index b014a42706f..4e04d5bfa1d 100644
--- 
a/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
+++ 
b/fe/be-java-extensions/iceberg-metadata-scanner/src/main/java/org/apache/doris/iceberg/IcebergSysTableJniScanner.java
@@ -28,15 +28,11 @@ import com.google.common.base.Preconditions;
 import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.io.CloseableIterator;
-import org.apache.iceberg.types.Types.NestedField;
-import org.apache.iceberg.types.Types.StructType;
 import org.apache.iceberg.util.SerializationUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 import java.util.TimeZone;
 import java.util.stream.Collectors;
@@ -50,7 +46,7 @@ public class IcebergSysTableJniScanner extends JniScanner {
     private final ClassLoader classLoader;
     private final PreExecutionAuthenticator preExecutionAuthenticator;
     private final FileScanTask scanTask;
-    private final List<SelectedField> fields;
+    private final int requiredFieldCount;
     private final String timezone;
     private CloseableIterator<StructLike> reader;
 
@@ -60,15 +56,22 @@ public class IcebergSysTableJniScanner extends JniScanner {
         Preconditions.checkArgument(serializedSplitParams != null && 
!serializedSplitParams.isEmpty(),
                 "serialized_split should not be empty");
         this.scanTask = 
SerializationUtil.deserializeFromBase64(serializedSplitParams);
-        String[] requiredFields = params.get("required_fields").split(",");
-        this.fields = selectSchema(scanTask.schema().asStruct(), 
requiredFields);
+        String requiredFieldsParam = params.get("required_fields");
+        Preconditions.checkArgument(requiredFieldsParam != null && 
!requiredFieldsParam.isEmpty(),
+                "required_fields should not be empty");
+        String[] requiredFields = requiredFieldsParam.split(",");
+        this.requiredFieldCount = requiredFields.length;
         this.timezone = params.getOrDefault("time_zone", 
TimeZone.getDefault().getID());
         Map<String, String> hadoopOptionParams = params.entrySet().stream()
                 .filter(kv -> kv.getKey().startsWith(HADOOP_OPTION_PREFIX))
                 .collect(Collectors
                         .toMap(kv1 -> 
kv1.getKey().substring(HADOOP_OPTION_PREFIX.length()), kv1 -> kv1.getValue()));
         this.preExecutionAuthenticator = 
PreExecutionAuthenticatorCache.getAuthenticator(hadoopOptionParams);
-        ColumnType[] requiredTypes = 
parseRequiredTypes(params.get("required_types").split("#"), requiredFields);
+        String requiredTypesParam = params.get("required_types");
+        Preconditions.checkArgument(requiredTypesParam != null && 
!requiredTypesParam.isEmpty(),
+                "required_types should not be empty");
+        String[] requiredTypeStrings = requiredTypesParam.split("#");
+        ColumnType[] requiredTypes = parseRequiredTypes(requiredTypeStrings, 
requiredFields);
         initTableInfo(requiredTypes, requiredFields, batchSize);
     }
 
@@ -106,9 +109,10 @@ public class IcebergSysTableJniScanner extends JniScanner {
                     break;
                 }
                 StructLike row = reader.next();
-                for (int i = 0; i < fields.size(); i++) {
-                    SelectedField field = fields.get(i);
-                    Object value = row.get(field.sourceIndex, 
field.field.type().typeId().javaClass());
+                for (int i = 0; i < requiredFieldCount; i++) {
+                    // FE keeps the fields requested by BE at the start of the 
Iceberg projection.
+                    // FileScanTask.schema() is not the row schema for every 
DataTask implementation.
+                    Object value = row.get(i, Object.class);
                     ColumnValue columnValue = new 
IcebergSysTableColumnValue(value, timezone);
                     appendData(i, columnValue);
                 }
@@ -129,35 +133,10 @@ public class IcebergSysTableJniScanner extends JniScanner 
{
         }
     }
 
-    private static List<SelectedField> selectSchema(StructType schema, 
String[] requiredFields) {
-        List<NestedField> schemaFields = schema.fields();
-        List<SelectedField> selectedFields = new ArrayList<>();
-        for (String requiredField : requiredFields) {
-            NestedField field = schema.field(requiredField);
-            if (field == null) {
-                throw new IllegalArgumentException("RequiredField " + 
requiredField + " not found in schema");
-            }
-            int sourceIndex = schemaFields.indexOf(field);
-            if (sourceIndex < 0) {
-                throw new IllegalArgumentException(
-                        "RequiredField " + requiredField + " not found in 
source schema fields");
-            }
-            selectedFields.add(new SelectedField(sourceIndex, field));
-        }
-        return selectedFields;
-    }
-
-    private static final class SelectedField {
-        private final int sourceIndex;
-        private final NestedField field;
-
-        private SelectedField(int sourceIndex, NestedField field) {
-            this.sourceIndex = sourceIndex;
-            this.field = field;
-        }
-    }
-
     private static ColumnType[] parseRequiredTypes(String[] typeStrings, 
String[] requiredFields) {
+        Preconditions.checkArgument(typeStrings.length == 
requiredFields.length,
+                "required_types size %s does not match required_fields size 
%s",
+                typeStrings.length, requiredFields.length);
         ColumnType[] requiredTypes = new ColumnType[typeStrings.length];
         for (int i = 0; i < typeStrings.length; i++) {
             String type = typeStrings[i];
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 55da4f30ad7..e510f05bfb6 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
@@ -181,7 +181,7 @@ public abstract class FileQueryScanNode extends 
FileScanNode {
             slotInfo.setSlotId(slot.getId().asInt());
             TColumnCategory category = classifyColumn(slot, partitionKeys);
             slotInfo.setCategory(category);
-            slotInfo.setIsFileSlot(category == TColumnCategory.REGULAR || 
category == TColumnCategory.GENERATED);
+            slotInfo.setIsFileSlot(isFileSlot(category));
             params.addToRequiredSlots(slotInfo);
         }
         setDefaultValueExprs(getTargetTable(), destSlotDescByName, null, 
params, false);
@@ -210,7 +210,7 @@ public abstract class FileQueryScanNode extends 
FileScanNode {
             slotInfo.setSlotId(slot.getId().asInt());
             TColumnCategory category = classifyColumn(slot, partitionKeys);
             slotInfo.setCategory(category);
-            slotInfo.setIsFileSlot(category == TColumnCategory.REGULAR || 
category == TColumnCategory.GENERATED);
+            slotInfo.setIsFileSlot(isFileSlot(category));
             params.addToRequiredSlots(slotInfo);
         }
         // Update required slots and column_idxs in scanRangeLocations.
@@ -228,6 +228,10 @@ public abstract class FileQueryScanNode extends 
FileScanNode {
         return TColumnCategory.REGULAR;
     }
 
+    protected boolean isFileSlot(TColumnCategory category) {
+        return category == TColumnCategory.REGULAR || category == 
TColumnCategory.GENERATED;
+    }
+
     public void setTableSample(TableSample tSample) {
         this.tableSample = tSample;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergSysExternalTable.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergSysExternalTable.java
index aeb539d1f3f..14047dd44ff 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergSysExternalTable.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergSysExternalTable.java
@@ -80,6 +80,10 @@ public class IcebergSysExternalTable extends ExternalTable {
         return sysTableType;
     }
 
+    public boolean isPositionDeletesTable() {
+        return 
MetadataTableType.POSITION_DELETES.name().equalsIgnoreCase(sysTableType);
+    }
+
     public Table getSysIcebergTable() {
         if (sysIcebergTable == null) {
             synchronized (this) {
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 70ac1d26b10..b592883f860 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
@@ -96,6 +96,7 @@ import org.apache.iceberg.SplittableScanTask;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TableScan;
+import org.apache.iceberg.expressions.Binder;
 import org.apache.iceberg.expressions.Expression;
 import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.expressions.InclusiveMetricsEvaluator;
@@ -108,6 +109,7 @@ import org.apache.iceberg.mapping.MappedFields;
 import org.apache.iceberg.mapping.NameMapping;
 import org.apache.iceberg.mapping.NameMappingParser;
 import org.apache.iceberg.types.Type;
+import org.apache.iceberg.types.TypeUtil;
 import org.apache.iceberg.types.Types.NestedField;
 import org.apache.iceberg.util.ScanTaskUtil;
 import org.apache.iceberg.util.SerializationUtil;
@@ -121,11 +123,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.OptionalLong;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -674,11 +678,70 @@ public class IcebergScanNode extends FileQueryScanNode {
             this.pushdownIcebergPredicates.add(predicate.toString());
         }
 
+        // Doris reads normal Iceberg table files in BE and applies column 
pruning through scan range params.
+        // System tables are different: Iceberg SDK DataTask materializes rows 
using the projected scan
+        // schema. Keep Doris file slots in the same order as the JNI reader's 
required fields.
+        if (isSystemTable) {
+            Schema projectedSchema = 
getSystemTableProjectedSchema(expressions, scan.isCaseSensitive());
+            Preconditions.checkState(!projectedSchema.columns().isEmpty(),
+                    "Iceberg system table scan must materialize at least one 
file slot");
+            scan = scan.project(projectedSchema);
+        }
+
         icebergTableScan = 
scan.planWith(source.getCatalog().getThreadPoolWithPreAuth());
 
         return icebergTableScan;
     }
 
+    @VisibleForTesting
+    Schema getSystemTableProjectedSchema(List<Expression> expressions, boolean 
caseSensitive)
+            throws UserException {
+        List<NestedField> projectedFields = new ArrayList<>();
+        Set<Integer> projectedFieldIds = new HashSet<>();
+        List<String> partitionKeys = getPathPartitionKeys();
+        for (SlotDescriptor slot : desc.getSlots()) {
+            Column column = slot.getColumn();
+            String columnName = column.getName();
+            if (!isFileSlot(classifyColumn(slot, partitionKeys))) {
+                continue;
+            }
+
+            NestedField field = caseSensitive
+                    ? icebergTable.schema().findField(columnName)
+                    : 
icebergTable.schema().caseInsensitiveFindField(columnName);
+            if (field == null) {
+                throw new UserException("Column " + columnName + " not found 
in Iceberg system table schema");
+            }
+            if (projectedFieldIds.add(field.fieldId())) {
+                projectedFields.add(field);
+            }
+        }
+
+        Set<Integer> filterFieldIds = Binder.boundReferences(
+                icebergTable.schema().asStruct(), expressions, caseSensitive);
+        for (Integer fieldId : filterFieldIds) {
+            NestedField field = getTopLevelSystemTableField(fieldId);
+            if (field == null) {
+                throw new UserException(
+                        "Column with field id " + fieldId + " not found in 
Iceberg system table schema");
+            }
+            if (!projectedFieldIds.contains(field.fieldId())) {
+                throw new UserException("Iceberg system table filter column " 
+ field.name()
+                        + " is not materialized by the planner");
+            }
+        }
+        return new Schema(projectedFields);
+    }
+
+    private NestedField getTopLevelSystemTableField(int fieldId) {
+        for (NestedField field : icebergTable.schema().columns()) {
+            if (field.fieldId() == fieldId || 
TypeUtil.getProjectedIds(field.type()).contains(fieldId)) {
+                return field;
+            }
+        }
+        return null;
+    }
+
     private CloseableIterable<FileScanTask> planFileScanTask(TableScan scan) {
         if (!IcebergUtils.isManifestCacheEnabled(source.getCatalog())) {
             return splitFiles(scan);
@@ -1199,7 +1262,7 @@ public class IcebergScanNode extends FileQueryScanNode {
     private boolean isPositionDeletesSystemTable() {
         TableIf targetTable = source.getTargetTable();
         return targetTable instanceof IcebergSysExternalTable
-                && 
"position_deletes".equalsIgnoreCase(((IcebergSysExternalTable) 
targetTable).getSysTableType());
+                && ((IcebergSysExternalTable) 
targetTable).isPositionDeletesTable();
     }
 
     private List<Split> doGetPositionDeletesSystemTableSplits() throws 
UserException {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
index f34ea0d633d..e70933b5b64 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
@@ -233,8 +233,13 @@ public class LogicalFileScan extends 
LogicalCatalogRelation implements SupportPr
     @Override
     public boolean supportPruneNestedColumn() {
         ExternalTable table = getTable();
-        if (table instanceof IcebergExternalTable || table instanceof 
IcebergSysExternalTable) {
+        if (table instanceof IcebergExternalTable) {
             return true;
+        } else if (table instanceof IcebergSysExternalTable) {
+            // Position deletes use the native reader, which supports nested 
column pruning. Other
+            // Iceberg system tables are materialized as StructLike rows by 
the SDK and consumed by
+            // ordinal in the JNI reader, so their nested struct layout must 
remain unchanged.
+            return ((IcebergSysExternalTable) table).isPositionDeletesTable();
         } else if (table instanceof HMSExternalTable) {
             HMSExternalTable hmsTable = (HMSExternalTable) table;
             if (hmsTable.getDlaType() == HMSExternalTable.DLAType.HUDI) {
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 2a331f57462..d4273f6faa4 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
@@ -17,8 +17,12 @@
 
 package org.apache.doris.datasource.iceberg.source;
 
+import org.apache.doris.analysis.SlotDescriptor;
+import org.apache.doris.analysis.SlotId;
 import org.apache.doris.analysis.TupleDescriptor;
 import org.apache.doris.analysis.TupleId;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Type;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.util.LocationPath;
 import org.apache.doris.datasource.TableFormatType;
@@ -38,6 +42,8 @@ import org.apache.iceberg.Schema;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableScan;
+import org.apache.iceberg.expressions.Expression;
+import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.util.ScanTaskUtil;
 import org.junit.Assert;
@@ -88,6 +94,64 @@ public class IcebergScanNodeTest {
         protected boolean getEnableMappingVarbinary() {
             return enableMappingVarbinary;
         }
+
+        @Override
+        public List<String> getPathPartitionKeys() {
+            return Collections.emptyList();
+        }
+
+        void addSlot(int slotId, Column column) {
+            SlotDescriptor slot = new SlotDescriptor(new SlotId(slotId), 
desc.getId());
+            slot.setColumn(column);
+            desc.addSlot(slot);
+        }
+    }
+
+    @Test
+    public void testSystemTableProjectionMatchesFileSlotOrder() throws 
Exception {
+        Schema systemTableSchema = new Schema(
+                Types.NestedField.required(1, "file_path", 
Types.StringType.get()),
+                Types.NestedField.required(2, "record_count", 
Types.LongType.get()),
+                Types.NestedField.optional(3, "readable_metrics", 
Types.StructType.of(
+                        Types.NestedField.optional(4, "id", 
Types.StructType.of(
+                                Types.NestedField.optional(5, "lower_bound", 
Types.IntegerType.get()))))));
+        Table systemTable = Mockito.mock(Table.class);
+        Mockito.when(systemTable.schema()).thenReturn(systemTableSchema);
+
+        TestIcebergScanNode node = new TestIcebergScanNode(new 
SessionVariable());
+        setIcebergTable(node, systemTable);
+        node.addSlot(1, new Column("RECORD_COUNT", Type.BIGINT));
+        node.addSlot(2, new Column(Column.GLOBAL_ROWID_COL + "system_table", 
Type.BIGINT));
+        node.addSlot(3, new Column("FILE_PATH", Type.STRING));
+
+        List<Expression> filters = 
Collections.singletonList(Expressions.greaterThan("record_count", 0L));
+        Schema projectedSchema = node.getSystemTableProjectedSchema(filters, 
false);
+
+        Assert.assertEquals(2, projectedSchema.columns().size());
+        Assert.assertEquals("record_count", 
projectedSchema.columns().get(0).name());
+        Assert.assertEquals("file_path", 
projectedSchema.columns().get(1).name());
+        Assert.assertNull(projectedSchema.findField("readable_metrics"));
+    }
+
+    @Test
+    public void testSystemTableProjectionRejectsUnmaterializedFilterColumn() 
throws Exception {
+        Schema systemTableSchema = new Schema(
+                Types.NestedField.required(1, "file_path", 
Types.StringType.get()),
+                Types.NestedField.required(2, "record_count", 
Types.LongType.get()));
+        Table systemTable = Mockito.mock(Table.class);
+        Mockito.when(systemTable.schema()).thenReturn(systemTableSchema);
+
+        TestIcebergScanNode node = new TestIcebergScanNode(new 
SessionVariable());
+        setIcebergTable(node, systemTable);
+        node.addSlot(1, new Column("record_count", Type.BIGINT));
+
+        try {
+            node.getSystemTableProjectedSchema(
+                    Collections.singletonList(Expressions.equal("file_path", 
"data.parquet")), true);
+            Assert.fail("Filter columns must be materialized by the planner");
+        } catch (UserException e) {
+            Assert.assertTrue(e.getMessage().contains("filter column file_path 
is not materialized"));
+        }
     }
 
     @Test
@@ -142,6 +206,12 @@ public class IcebergScanNodeTest {
         Mockito.verify(node, Mockito.never()).createTableScan();
     }
 
+    private static void setIcebergTable(IcebergScanNode node, Table table) 
throws Exception {
+        Field icebergTableField = 
IcebergScanNode.class.getDeclaredField("icebergTable");
+        icebergTableField.setAccessible(true);
+        icebergTableField.set(node, table);
+    }
+
     @Test
     public void testDetermineTargetFileSplitSizeHonorsMaxFileSplitNum() throws 
Exception {
         SessionVariable sv = new SessionVariable();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScanTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScanTest.java
index 865bba61e1f..ac0c76f8090 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScanTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScanTest.java
@@ -20,6 +20,7 @@ package org.apache.doris.nereids.trees.plans.logical;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.datasource.iceberg.IcebergExternalTable;
+import org.apache.doris.datasource.iceberg.IcebergSysExternalTable;
 import org.apache.doris.datasource.iceberg.IcebergUtils;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import 
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan.SelectedPartitions;
@@ -36,6 +37,27 @@ import java.util.stream.Collectors;
 
 public class LogicalFileScanTest {
 
+    @Test
+    public void testNestedColumnPruningForIcebergSystemTables() {
+        IcebergSysExternalTable positionDeletes = 
Mockito.mock(IcebergSysExternalTable.class);
+        Mockito.when(positionDeletes.initSelectedPartitions(Mockito.any()))
+                .thenReturn(SelectedPartitions.NOT_PRUNED);
+        
Mockito.when(positionDeletes.isPositionDeletesTable()).thenReturn(true);
+        LogicalFileScan positionDeletesScan = new LogicalFileScan(new 
RelationId(1), positionDeletes,
+                Collections.singletonList("db"), Collections.emptyList(),
+                Optional.empty(), Optional.empty(), Optional.empty(), 
Optional.empty());
+        Assertions.assertTrue(positionDeletesScan.supportPruneNestedColumn());
+
+        IcebergSysExternalTable jniSystemTable = 
Mockito.mock(IcebergSysExternalTable.class);
+        Mockito.when(jniSystemTable.initSelectedPartitions(Mockito.any()))
+                .thenReturn(SelectedPartitions.NOT_PRUNED);
+        
Mockito.when(jniSystemTable.isPositionDeletesTable()).thenReturn(false);
+        LogicalFileScan jniSystemTableScan = new LogicalFileScan(new 
RelationId(2), jniSystemTable,
+                Collections.singletonList("db"), Collections.emptyList(),
+                Optional.empty(), Optional.empty(), Optional.empty(), 
Optional.empty());
+        Assertions.assertFalse(jniSystemTableScan.supportPruneNestedColumn());
+    }
+
     @Test
     public void 
testComputeOutputIncludesInvisibleRowLineageColumnsForIcebergTable() {
         Column rowIdColumn = new Column(IcebergUtils.ICEBERG_ROW_ID_COL, 
Type.BIGINT, true);
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_system_table_projection.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_system_table_projection.groovy
new file mode 100644
index 00000000000..a7d5acf7596
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_system_table_projection.groovy
@@ -0,0 +1,177 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_iceberg_system_table_projection", "p0,external,iceberg") {
+    String enabled = context.config.otherConfigs.get("enableIcebergTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable iceberg test.")
+        return
+    }
+
+    String catalogName = "test_iceberg_system_table_projection"
+    String dbName = "test_iceberg_system_table_projection_db"
+    String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+    String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+
+    sql """drop catalog if exists ${catalogName}"""
+    sql """
+    CREATE CATALOG ${catalogName} PROPERTIES (
+        'type'='iceberg',
+        'iceberg.catalog.type'='rest',
+        'uri' = 'http://${externalEnvIp}:${restPort}',
+        "s3.access_key" = "admin",
+        "s3.secret_key" = "password",
+        "s3.endpoint" = "http://${externalEnvIp}:${minioPort}";,
+        "s3.region" = "us-east-1"
+    );"""
+
+    sql """switch ${catalogName}"""
+    sql """create database if not exists ${dbName}"""
+    sql """use ${dbName}"""
+
+    def verifySystemTableProjection = { String tableName, String writeFormat ->
+        sql """DROP TABLE IF EXISTS ${tableName}"""
+        sql """
+        CREATE TABLE ${tableName} (
+            id int,
+            t_map_boolean map<boolean, boolean>,
+            dt int
+        ) ENGINE=iceberg
+        PARTITION BY LIST (dt) ()
+        PROPERTIES (
+            "write-format" = "${writeFormat}"
+        );
+        """
+        sql """
+        INSERT INTO ${tableName}
+        VALUES
+            (1, MAP(true, false), 20260702);
+        """
+
+        List<List<Object>> dataFiles = sql """
+            SELECT file_size_in_bytes
+            FROM ${tableName}\$data_files
+            ORDER BY file_size_in_bytes;
+        """
+        assertEquals(1, dataFiles.size())
+        assertTrue(((Number) dataFiles[0][0]).longValue() > 0)
+
+        List<List<Object>> filesSize = sql """
+            SELECT file_size_in_bytes
+            FROM ${tableName}\$files
+            ORDER BY file_size_in_bytes;
+        """
+        assertEquals(1, filesSize.size())
+        assertTrue(((Number) filesSize[0][0]).longValue() > 0)
+
+        List<List<Object>> files = sql """
+            SELECT file_path, record_count
+            FROM ${tableName}\$files
+            ORDER BY file_path;
+        """
+        assertEquals(1, files.size())
+        assertTrue(files[0][0].toString().contains(tableName))
+        assertEquals(1L, ((Number) files[0][1]).longValue())
+
+        List<List<Object>> snapshots = sql """
+            SELECT snapshot_id, parent_id, operation
+            FROM ${tableName}\$snapshots
+            ORDER BY committed_at;
+        """
+        assertEquals(1, snapshots.size())
+        long snapshotId = ((Number) snapshots[0][0]).longValue()
+        assertTrue(snapshotId > 0)
+        assertEquals(null, snapshots[0][1])
+        assertEquals("append", snapshots[0][2])
+
+        List<List<Object>> history = sql """
+            SELECT snapshot_id, parent_id, is_current_ancestor
+            FROM ${tableName}\$history
+            ORDER BY made_current_at;
+        """
+        assertEquals(1, history.size())
+        assertEquals(snapshotId, ((Number) history[0][0]).longValue())
+        assertEquals(null, history[0][1])
+        assertEquals(true, history[0][2])
+    }
+
+    def verifyReadableMetricsProjection = { String tableName, String 
writeFormat ->
+        sql """DROP TABLE IF EXISTS ${tableName}"""
+        sql """
+        CREATE TABLE ${tableName} (
+            id int,
+            name string,
+            dt int
+        ) ENGINE=iceberg
+        PARTITION BY LIST (dt) ()
+        PROPERTIES (
+            "write-format" = "${writeFormat}"
+        );
+        """
+        sql """
+        INSERT INTO ${tableName}
+        VALUES
+            (1, 'alice', 20260702),
+            (2, 'bob', 20260702);
+        """
+
+        List<List<Object>> files = sql """
+            SELECT readable_metrics
+            FROM ${tableName}\$files
+            ORDER BY file_path;
+        """
+        assertEquals(1, files.size())
+        String readableMetrics = files[0][0].toString()
+        assertTrue(readableMetrics.contains("\"id\""))
+        assertTrue(readableMetrics.contains("\"name\""))
+        assertTrue(readableMetrics.contains("\"lower_bound\""))
+        assertTrue(readableMetrics.contains("\"upper_bound\""))
+
+        // `name` is not the first field of readable_metrics. Keep nested 
column pruning enabled
+        // to verify that the system table reader preserves the SDK struct 
field ordinals.
+        List<List<Object>> nameUpperBound = sql """
+            SELECT /*+ SET_VAR(enable_prune_nested_column=true) */ 
readable_metrics.name.upper_bound
+            FROM ${tableName}\$files
+            ORDER BY file_path;
+        """
+        assertEquals(1, nameUpperBound.size())
+        assertEquals("bob", nameUpperBound[0][0])
+    }
+
+    verifySystemTableProjection("test_iceberg_system_table_projection_orc", 
"orc")
+    
verifySystemTableProjection("test_iceberg_system_table_projection_parquet", 
"parquet")
+
+    test {
+        sql """SELECT COUNT(*) FROM 
test_iceberg_system_table_projection_orc\$data_files"""
+        result([[1L]])
+    }
+    test {
+        sql """SELECT COUNT(*) FROM 
test_iceberg_system_table_projection_orc\$files"""
+        result([[1L]])
+    }
+    test {
+        sql """SELECT COUNT(*) FROM 
test_iceberg_system_table_projection_parquet\$data_files"""
+        result([[1L]])
+    }
+    test {
+        sql """SELECT COUNT(*) FROM 
test_iceberg_system_table_projection_parquet\$files"""
+        result([[1L]])
+    }
+
+    
verifyReadableMetricsProjection("test_iceberg_system_table_projection_readable_metrics",
 "orc")
+}


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

Reply via email to