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

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


The following commit(s) were added to refs/heads/master by this push:
     new cde07773c6 [core] Preserve merge correctness when pushing filters into 
chain scans (#9979)
cde07773c6 is described below

commit cde07773c6866920a346b93cf12b743e848e1ace
Author: Xiangyi Zhu <[email protected]>
AuthorDate: Sun Sep 20 13:46:05 2026 +0800

    [core] Preserve merge correctness when pushing filters into chain scans 
(#9979)
---
 .../apache/paimon/table/ChainGroupReadTable.java   |  25 +-
 .../paimon/table/ChainTableFileStoreTableTest.java | 429 +++++++++++++++++++++
 2 files changed, 448 insertions(+), 6 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java 
b/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
index c141d9b8e2..5bfeec5129 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
@@ -225,7 +225,7 @@ public class ChainGroupReadTable extends 
FallbackReadFileStoreTable {
         private final ChainGroupReadTable chainGroupReadTable;
         private final RecordComparator chainPartitionComparator;
         private final ChainPartitionProjector partitionProjector;
-        private Predicate dataPredicate;
+        private Predicate keyPredicate;
         private Filter<Integer> bucketFilter;
         protected boolean preloadTargetSnapshot = true;
 
@@ -267,15 +267,28 @@ public class ChainGroupReadTable extends 
FallbackReadFileStoreTable {
         public ChainTableBatchScan withFilter(Predicate predicate) {
             super.withFilter(predicate);
             if (predicate == null) {
-                dataPredicate = null;
+                keyPredicate = null;
             } else {
                 Pair<Optional<PartitionPredicate>, List<Predicate>> pair =
                         
PartitionPredicate.splitPartitionPredicatesAndDataPredicates(
                                 predicate,
                                 tableSchema.logicalRowType(),
                                 tableSchema.partitionKeys());
-                dataPredicate =
-                        pair.getRight().isEmpty() ? null : 
PredicateBuilder.and(pair.getRight());
+                List<String> fieldNames = tableSchema.fieldNames();
+                List<String> primaryKeys = tableSchema.trimmedPrimaryKeys();
+                int[] keyMapping = new int[fieldNames.size()];
+                for (int i = 0; i < keyMapping.length; i++) {
+                    keyMapping[i] = primaryKeys.contains(fieldNames.get(i)) ? 
i : -1;
+                }
+                // Branch scans are incomplete merge inputs. A value filter 
could remove an
+                // update or delete needed to suppress an older matching row 
in another branch.
+                // Keep only an inclusive key predicate, retaining the 
original row indices.
+                keyPredicate =
+                        pair.getRight().isEmpty()
+                                ? null
+                                : PredicateBuilder.transformFieldMapping(
+                                                
PredicateBuilder.and(pair.getRight()), keyMapping)
+                                        .orElse(null);
             }
             return this;
         }
@@ -540,8 +553,8 @@ public class ChainGroupReadTable extends 
FallbackReadFileStoreTable {
                                     ? 
chainGroupReadTable.newSnapshotScan(scanCreator)
                                     : 
chainGroupReadTable.newDeltaScan(scanCreator))
                             .withoutAuthPartitionPushdown();
-            if (dataPredicate != null) {
-                scan.withFilter(dataPredicate);
+            if (keyPredicate != null) {
+                scan.withFilter(keyPredicate);
             }
             if (bucketFilter != null) {
                 scan.withBucketFilter(bucketFilter);
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/ChainTableFileStoreTableTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/ChainTableFileStoreTableTest.java
index b65b2e6ec0..a651cfc2c9 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/ChainTableFileStoreTableTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/ChainTableFileStoreTableTest.java
@@ -29,7 +29,10 @@ import 
org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.disk.IOManagerImpl;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.options.MemorySize;
 import org.apache.paimon.options.Options;
+import org.apache.paimon.predicate.CastTransform;
 import org.apache.paimon.predicate.FieldRef;
 import org.apache.paimon.predicate.FieldTransform;
 import org.apache.paimon.predicate.Predicate;
@@ -45,6 +48,7 @@ import org.apache.paimon.table.sink.CommitMessage;
 import org.apache.paimon.table.sink.InnerTableWrite;
 import org.apache.paimon.table.sink.StreamTableCommit;
 import org.apache.paimon.table.source.ChainSplit;
+import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.DataTableScan;
 import org.apache.paimon.table.source.DeletionFile;
 import org.apache.paimon.table.source.InnerTableRead;
@@ -65,15 +69,18 @@ import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Random;
 import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -113,6 +120,428 @@ public class ChainTableFileStoreTableTest {
         tableName = "chain_t_" + uuid.replace("-", "");
     }
 
+    private static Stream<Arguments> filterPushdownParams() {
+        return Stream.of("parquet", "orc")
+                .flatMap(
+                        format ->
+                                Stream.of(false, true)
+                                        .flatMap(
+                                                dv ->
+                                                        Stream.of(false, true)
+                                                                .map(
+                                                                        
keyRange ->
+                                                                               
 Arguments.of(
+                                                                               
         format, dv,
+                                                                               
         keyRange))));
+    }
+
+    @ParameterizedTest
+    @MethodSource("filterPushdownParams")
+    public void testFilterPushdownAcrossChainMerge(
+            String format, boolean deletionVectors, boolean keyRangeSplit) 
throws Exception {
+        createChainTable(
+                options -> {
+                    options.set(CoreOptions.FILE_FORMAT, format);
+                    options.set(DELETION_VECTORS_ENABLED, deletionVectors);
+                    
options.set(CoreOptions.CHAIN_TABLE_KEY_RANGE_SPLIT_ENABLED, keyRangeSplit);
+                    options.set(CoreOptions.SOURCE_SPLIT_TARGET_SIZE, new 
MemorySize(1));
+                });
+        FileStoreTable table = loadTable();
+        FileStoreTable snapshot = table.switchToBranch(SNAPSHOT_BRANCH);
+        FileStoreTable delta = table.switchToBranch(DELTA_BRANCH);
+        // Separate files let branch-local stats discard individual versions 
independently.
+        writeWithCommit(snapshot, row(1L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(snapshot, row(2L, 1L, "new", "CN", "20250810", "20"));
+        if (!deletionVectors) {
+            writeWithCommit(snapshot, row(3L, 1L, "old", "CN", "20250810", 
"20"));
+        }
+        writeWithCommit(snapshot, row(4L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(snapshot, row(5L, 1L, null, "CN", "20250810", "20"));
+        writeWithCommit(snapshot, row(6L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(snapshot, row(1L, 1L, "other-group", "US", "20250810", 
"20"));
+        writeWithCommit(delta, row(1L, 2L, "new", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(2L, 2L, "old", "CN", "20250810", "21"));
+        if (!deletionVectors) {
+            writeWithCommit(delta, row(RowKind.DELETE, 3L, 2L, "new", "CN", 
"20250810", "21"));
+        }
+        writeWithCommit(delta, row(4L, 2L, null, "CN", "20250810", "21"));
+        writeWithCommit(delta, row(5L, 2L, "old", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(6L, 2L, "new", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(7L, 1L, "old", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(6L, 3L, "latest", "CN", "20250810", "22"));
+        writeWithCommit(delta, row(1L, 2L, "other-group-new", "US", 
"20250810", "22"));
+        table = loadTable();
+        Map<String, String> partition =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"22");
+        List<GenericRow> baseline = getResult(table, partition);
+        assertThat(baseline)
+                .containsExactlyInAnyOrder(
+                        row(1L, 2L, "new", "CN", "20250810", "22"),
+                        row(2L, 2L, "old", "CN", "20250810", "22"),
+                        row(4L, 2L, null, "CN", "20250810", "22"),
+                        row(5L, 2L, "old", "CN", "20250810", "22"),
+                        row(6L, 3L, "latest", "CN", "20250810", "22"),
+                        row(7L, 1L, "old", "CN", "20250810", "22"));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate old = builder.equal(2, BinaryString.fromString("old"));
+        Predicate newer = builder.equal(2, BinaryString.fromString("new"));
+        for (Predicate filter :
+                Arrays.asList(
+                        old,
+                        newer,
+                        builder.isNull(2),
+                        builder.isNotNull(2),
+                        old.negate().get(),
+                        builder.greaterThan(1, 1L),
+                        builder.equal(0, 1L),
+                        builder.equal(
+                                new CastTransform(
+                                        new FieldRef(0, "k", 
DataTypes.BIGINT()),
+                                        DataTypes.STRING()),
+                                BinaryString.fromString("1")),
+                        PredicateBuilder.and(builder.equal(0, 1L), old),
+                        PredicateBuilder.or(builder.equal(0, 1L), old),
+                        PredicateBuilder.or(builder.equal(0, 1L), 
builder.equal(0, 6L)),
+                        PredicateBuilder.or(
+                                PredicateBuilder.and(builder.equal(0, 1L), 
old),
+                                PredicateBuilder.and(builder.equal(0, 6L), 
newer)),
+                        builder.equal(2, BinaryString.fromString("absent")),
+                        PredicateBuilder.alwaysTrue(),
+                        PredicateBuilder.alwaysFalse())) {
+            assertFilterMatchesMergedRows(table, partition, filter, baseline);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"parquet", "orc"})
+    public void testPartialUpdateFilterPushdownAcrossChainMerge(String format) 
throws Exception {
+        createPartialUpdateChainTable(
+                options -> {
+                    options.set(CoreOptions.FILE_FORMAT, format);
+                    options.set(PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE, true);
+                });
+        FileStoreTable table = loadTable();
+        FileStoreTable snapshot = table.switchToBranch(SNAPSHOT_BRANCH);
+        FileStoreTable delta = table.switchToBranch(DELTA_BRANCH);
+        writeWithCommit(snapshot, row(1L, 1L, "a", null, "20250810"));
+        writeWithCommit(snapshot, row(2L, 1L, "a", "old", "20250810"));
+        writeWithCommit(delta, row(1L, 2L, null, "b", "20250811"));
+        writeWithCommit(delta, row(RowKind.DELETE, 2L, 2L, null, null, 
"20250811"));
+        table = loadTable();
+        Map<String, String> partition = ImmutableMap.of("dt", "20250811");
+        List<GenericRow> baseline = getResult(table, partition);
+        assertThat(baseline).containsExactly(row(1L, 2L, "a", "b", 
"20250811"));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate a = builder.equal(2, BinaryString.fromString("a"));
+        Predicate b = builder.equal(3, BinaryString.fromString("b"));
+        for (Predicate filter :
+                Arrays.asList(
+                        a,
+                        b,
+                        PredicateBuilder.and(a, b),
+                        PredicateBuilder.or(a, b),
+                        builder.isNull(3),
+                        builder.equal(0, 2L),
+                        PredicateBuilder.and(builder.equal(0, 1L), a))) {
+            assertFilterMatchesMergedRows(table, partition, filter, baseline);
+        }
+    }
+
+    @Test
+    public void testChainFilterPreservesKeyAndCompleteSnapshotPruning() throws 
Exception {
+        createChainTable(options -> options.set(BUCKET, 2));
+        FileStoreTable table = loadTable();
+        FileStoreTable snapshot = table.switchToBranch(SNAPSHOT_BRANCH);
+        FileStoreTable delta = table.switchToBranch(DELTA_BRANCH);
+        for (long key = 1; key <= 4; key++) {
+            writeWithCommit(snapshot, row(key, 1L, "old", "CN", "20250810", 
"20"));
+            writeWithCommit(delta, row(key, 2L, "new", "CN", "20250810", 
"21"));
+        }
+        table = loadTable();
+        Map<String, String> partition =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"21");
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate key = builder.equal(0, 1L);
+        Predicate old = builder.equal(2, BinaryString.fromString("old"));
+        long allBytes = plannedFileBytes(table, partition, 
PredicateBuilder.alwaysTrue());
+        long keyBytes = plannedFileBytes(table, partition, key);
+        assertThat(keyBytes).isPositive().isLessThan(allBytes);
+        assertThat(plannedFileBytes(table, partition, 
PredicateBuilder.and(key, old)))
+                .isEqualTo(keyBytes);
+        Predicate keyOr = PredicateBuilder.or(key, builder.equal(0, 2L));
+        Predicate mixedOr =
+                PredicateBuilder.or(
+                        PredicateBuilder.and(key, old),
+                        PredicateBuilder.and(builder.equal(0, 2L), 
builder.isNull(2)));
+        assertThat(plannedFileBytes(table, partition, mixedOr))
+                .isEqualTo(plannedFileBytes(table, partition, keyOr));
+        assertThat(plannedFileBytes(table, partition, 
mixedOr)).isLessThan(allBytes);
+        assertThat(plannedFileBytes(table, partition, PredicateBuilder.or(key, 
old)))
+                .isEqualTo(allBytes);
+        Map<String, String> complete =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"20");
+        assertThat(plannedFileBytes(table, complete, old)).isPositive();
+        assertThat(
+                        plannedFileBytes(
+                                table,
+                                complete,
+                                builder.equal(2, 
BinaryString.fromString("absent"))))
+                .isZero();
+        assertThat(
+                        table.newReadBuilder()
+                                .withPartitionFilter(partition)
+                                .withFilter(PredicateBuilder.and(key, old))
+                                .withBucketFilter(bucket -> false)
+                                .newScan()
+                                .plan()
+                                .splits())
+                .isEmpty();
+        long bucketBytes = 0;
+        for (int bucket = 0; bucket < 2; bucket++) {
+            final int selectedBucket = bucket;
+            TableScan.Plan plan =
+                    table.newReadBuilder()
+                            .withPartitionFilter(partition)
+                            .withFilter(PredicateBuilder.alwaysTrue())
+                            .withBucketFilter(candidate -> candidate == 
selectedBucket)
+                            .newScan()
+                            .plan();
+            long bytes =
+                    plan.splits().stream()
+                            .flatMap(this::plannedDataFiles)
+                            .mapToLong(file -> file.fileSize())
+                            .sum();
+            assertThat(bytes).isPositive().isLessThan(allBytes);
+            bucketBytes += bytes;
+        }
+        assertThat(bucketBytes).isEqualTo(allBytes);
+    }
+
+    @Test
+    public void testChainKeyFilterAfterSchemaReorder() throws Exception {
+        createChainTable(options -> {});
+        writeWithCommit(
+                loadTable().switchToBranch(SNAPSHOT_BRANCH),
+                row(9L, 1L, "old", "CN", "20250810", "20"));
+        Path path = new Path(tempDir.toUri().toString(), tableName);
+        for (String branch : Arrays.asList(DEFAULT_MAIN_BRANCH, 
SNAPSHOT_BRANCH, DELTA_BRANCH)) {
+            new FileSystemSchemaManager(LocalFileIO.create(), path, branch)
+                    
.commitChanges(SchemaChange.updateColumnPosition(SchemaChange.Move.last("k")));
+        }
+        FileStoreTable table = loadTable();
+        writeWithCommit(
+                table.switchToBranch(DELTA_BRANCH), row(2L, "new", "CN", 
"20250810", "21", 9L));
+        table = loadTable();
+        assertThat(table.rowType().getFieldIndex("k")).isEqualTo(5);
+        Map<String, String> partition =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"21");
+        List<GenericRow> baseline = getResult(table, partition);
+        assertThat(baseline).containsExactly(row(2L, "new", "CN", "20250810", 
"21", 9L));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        assertFilterMatchesMergedRows(table, partition, builder.equal(5, 9L), 
baseline);
+        assertFilterMatchesMergedRows(
+                table,
+                partition,
+                PredicateBuilder.and(
+                        builder.equal(5, 9L), builder.equal(1, 
BinaryString.fromString("old"))),
+                baseline);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"parquet", "orc"})
+    public void testFilterPushdownWithoutSnapshotAnchor(String format) throws 
Exception {
+        createChainTable(options -> options.set(CoreOptions.FILE_FORMAT, 
format));
+        FileStoreTable delta = loadTable().switchToBranch(DELTA_BRANCH);
+        writeWithCommit(delta, row(1L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(delta, row(2L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(delta, row(1L, 2L, "new", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(RowKind.DELETE, 2L, 2L, "new", "CN", 
"20250810", "21"));
+        FileStoreTable table = loadTable();
+        Map<String, String> partition =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"21");
+        List<GenericRow> baseline = getResult(table, partition);
+        assertThat(baseline).containsExactly(row(1L, 2L, "new", "CN", 
"20250810", "21"));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        for (Predicate filter :
+                Arrays.asList(
+                        builder.equal(2, BinaryString.fromString("old")),
+                        builder.equal(2, BinaryString.fromString("new")),
+                        builder.equal(0, 2L),
+                        PredicateBuilder.and(builder.equal(0, 1L), 
builder.greaterThan(1, 1L)))) {
+            assertFilterMatchesMergedRows(table, partition, filter, baseline);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"parquet", "orc"})
+    public void testFilterPushdownAgainstVersionHistory(String format) throws 
Exception {
+        createChainTable(options -> options.set(CoreOptions.FILE_FORMAT, 
format));
+        FileStoreTable table = loadTable();
+        Map<Long, GenericRow> latest = new HashMap<>();
+        for (long key = 0; key < 24; key++) {
+            String value = key % 4 == 0 ? null : "v" + key % 3;
+            writeWithCommit(
+                    table.switchToBranch(SNAPSHOT_BRANCH),
+                    row(key, 0L, value, "CN", "20250810", "20"));
+            latest.put(key, row(key, 0L, value, "CN", "20250810", "23"));
+        }
+        Random random = new Random(20260919L);
+        FileStoreTable delta = table.switchToBranch(DELTA_BRANCH);
+        for (int version = 1; version <= 3; version++) {
+            List<GenericRow> updates = new ArrayList<>();
+            for (long key = 0; key < 28; key++) {
+                if (random.nextInt(3) == 0) {
+                    continue;
+                }
+                boolean delete = random.nextInt(5) == 0;
+                String value = random.nextInt(4) == 0 ? null : "v" + 
random.nextInt(3);
+                updates.add(
+                        row(
+                                delete ? RowKind.DELETE : RowKind.INSERT,
+                                key,
+                                (long) version,
+                                value,
+                                "CN",
+                                "20250810",
+                                Integer.toString(20 + version)));
+                if (delete) {
+                    latest.remove(key);
+                } else {
+                    latest.put(key, row(key, (long) version, value, "CN", 
"20250810", "23"));
+                }
+            }
+            writeWithCommit(delta, updates.toArray(new GenericRow[0]));
+        }
+        table = loadTable();
+        Map<String, String> partition =
+                ImmutableMap.of("region", "CN", "dt", "20250810", "hour", 
"23");
+        List<GenericRow> expectedRows = new ArrayList<>(latest.values());
+        assertThat(getResult(table, 
partition)).containsExactlyInAnyOrderElementsOf(expectedRows);
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        for (int i = 0; i < 24; i++) {
+            Predicate key = builder.greaterOrEqual(0, (long) 
random.nextInt(28));
+            Predicate value = builder.equal(2, BinaryString.fromString("v" + 
random.nextInt(3)));
+            Predicate predicate;
+            switch (i % 4) {
+                case 0:
+                    predicate = PredicateBuilder.and(key, value);
+                    break;
+                case 1:
+                    predicate = PredicateBuilder.or(key, value);
+                    break;
+                case 2:
+                    predicate =
+                            PredicateBuilder.or(
+                                    PredicateBuilder.and(key, 
builder.isNull(2)),
+                                    PredicateBuilder.and(builder.lessThan(0, 
12L), value));
+                    break;
+                default:
+                    predicate = PredicateBuilder.and(key, 
value.negate().get());
+            }
+            assertFilterMatchesMergedRows(table, partition, predicate, 
expectedRows);
+        }
+    }
+
+    @Test
+    public void testFilterCombiningLogicalPartitionsAndValues() throws 
Exception {
+        createChainTable(options -> {});
+        FileStoreTable table = loadTable();
+        FileStoreTable snapshot = table.switchToBranch(SNAPSHOT_BRANCH);
+        FileStoreTable delta = table.switchToBranch(DELTA_BRANCH);
+        writeWithCommit(snapshot, row(1L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(snapshot, row(2L, 1L, "old", "CN", "20250810", "20"));
+        writeWithCommit(delta, row(1L, 2L, "new", "CN", "20250810", "21"));
+        writeWithCommit(delta, row(1L, 3L, "latest", "CN", "20250810", "22"));
+        table = loadTable();
+        List<GenericRow> baseline = getResult(table, null);
+        assertThat(baseline)
+                .containsExactlyInAnyOrder(
+                        row(1L, 1L, "old", "CN", "20250810", "20"),
+                        row(2L, 1L, "old", "CN", "20250810", "20"),
+                        row(1L, 2L, "new", "CN", "20250810", "21"),
+                        row(2L, 1L, "old", "CN", "20250810", "21"),
+                        row(1L, 3L, "latest", "CN", "20250810", "22"),
+                        row(2L, 1L, "old", "CN", "20250810", "22"));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate filter =
+                PredicateBuilder.or(
+                        PredicateBuilder.and(
+                                builder.equal(5, 
BinaryString.fromString("22")),
+                                builder.equal(0, 1L)),
+                        PredicateBuilder.and(
+                                builder.equal(5, 
BinaryString.fromString("21")),
+                                builder.equal(2, 
BinaryString.fromString("old"))));
+        assertFilterMatchesMergedRows(table, null, filter, baseline);
+    }
+
+    @Test
+    public void testKeyFilterRespectsQueryAuthMask() throws Exception {
+        createChainTableWithQueryAuth();
+        Transform mask = new FieldTransform(new FieldRef(1, "seq", 
DataTypes.BIGINT()));
+        FileStoreTable table =
+                loadTable(
+                        queryAuthEnvironment(
+                                () ->
+                                        new TableQueryAuthResult(
+                                                null,
+                                                Collections.singletonMap(
+                                                        "k", 
JsonSerdeUtil.toFlatJson(mask)))));
+        List<GenericRow> baseline = getResult(table, QUERIED_PARTITION);
+        assertThat(baseline)
+                .containsExactlyInAnyOrder(
+                        row(2L, 2L, "1-1", "CN", "20250810", "21"),
+                        row(1L, 1L, "2", "CN", "20250810", "21"),
+                        row(1L, 1L, "4", "CN", "20250810", "21"));
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        assertFilterMatchesMergedRows(table, QUERIED_PARTITION, 
builder.equal(0, 1L), baseline);
+    }
+
+    private void assertFilterMatchesMergedRows(
+            FileStoreTable table,
+            Map<String, String> partition,
+            Predicate filter,
+            List<GenericRow> mergedRows)
+            throws Exception {
+        List<GenericRow> expected =
+                
mergedRows.stream().filter(filter::test).collect(Collectors.toList());
+        ReadBuilder readBuilder =
+                
table.newReadBuilder().withPartitionFilter(partition).withFilter(filter);
+        List<GenericRow> actual = new ArrayList<>();
+        InternalRowSerializer serializer = new 
InternalRowSerializer(table.rowType());
+        try (RecordReader<InternalRow> reader =
+                
readBuilder.newRead().createReader(readBuilder.newScan().plan())) {
+            reader.forEachRemaining(
+                    record -> {
+                        // Keep the residual: extra rows are allowed by 
pushdown, missing versions
+                        // are not.
+                        if (filter.test(record)) {
+                            actual.add((GenericRow) serializer.copy(record));
+                        }
+                    });
+        }
+        assertThat(actual)
+                .as("Pushed predicate %s", filter)
+                .containsExactlyInAnyOrderElementsOf(expected);
+    }
+
+    private Stream<DataFileMeta> plannedDataFiles(Split split) {
+        if (split instanceof FallbackReadFileStoreTable.FallbackSplit) {
+            return 
plannedDataFiles(((FallbackReadFileStoreTable.FallbackSplit) split).wrapped());
+        }
+        return split instanceof ChainSplit
+                ? ((ChainSplit) split).dataFiles().stream()
+                : ((DataSplit) split).dataFiles().stream();
+    }
+
+    private long plannedFileBytes(
+            FileStoreTable table, Map<String, String> partition, Predicate 
filter) {
+        return 
table.newReadBuilder().withPartitionFilter(partition).withFilter(filter).newScan()
+                .plan().splits().stream()
+                .flatMap(this::plannedDataFiles)
+                .mapToLong(file -> file.fileSize())
+                .sum();
+    }
+
     @Test
     public void testChainTableRejectsAggregate() throws Exception {
         assertThatThrownBy(

Reply via email to