diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java b/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java index 7d528db6c347..634c95e6eaaa 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java @@ -188,9 +188,11 @@ private Optional applyPushDownLimit() { } private Optional applyPushDownTopN() { - // Auth drops rows at read time, so split-level TopN pruning could drop authorized rows. + // A read-time filter (WHERE or auth) drops rows after split pruning, so split-level TopN + // pruning could keep too few splits. Skip it when either is present. if (topN == null || pushDownLimit != null + || snapshotReader.hasNonPartitionFilter() || authHasNonPartitionFilter || !schema.primaryKeys().isEmpty()) { return Optional.empty(); diff --git a/paimon-core/src/test/java/org/apache/paimon/table/source/TableScanTest.java b/paimon-core/src/test/java/org/apache/paimon/table/source/TableScanTest.java index 6c25574b0215..cd417058a8d4 100644 --- a/paimon-core/src/test/java/org/apache/paimon/table/source/TableScanTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/table/source/TableScanTest.java @@ -421,6 +421,25 @@ public void testPushDownTopN() throws Exception { .isEqualTo(60); assertThat(((DataSplit) splits2.get(2)).nullCount(field.id(), evolutions)).isEqualTo(2); + // A non-partition filter must disable TopN pushdown, else it prunes splits by sort-column + // stats unaware the filter removes rows. "b" >= 0 matches every row, so all 9 splits are + // kept instead of pruned to 3. + PredicateBuilder builder = new PredicateBuilder(table.rowType()); + TableScan.Plan planWithFilter = + table.newScan() + .withFilter(builder.greaterOrEqual(2, 0L)) + .withTopN(new TopN(ref, ASCENDING, NULLS_FIRST, 1)) + .plan(); + assertThat(planWithFilter.splits().size()).isEqualTo(9); + + // A partition-only filter keeps TopN pushdown enabled (surviving rows all pass): still 3. + TableScan.Plan planWithPartitionFilter = + table.newScan() + .withFilter(builder.greaterOrEqual(0, 0)) + .withTopN(new TopN(ref, ASCENDING, NULLS_FIRST, 1)) + .plan(); + assertThat(planWithPartitionFilter.splits().size()).isEqualTo(3); + // with bottom1 null last TableScan.Plan plan3 = table.newScan().withTopN(new TopN(ref, ASCENDING, NULLS_LAST, 1)).plan();