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 dd0bbf6a3b [core][python] Fix mixed row-id OR pushdown (#8496)
dd0bbf6a3b is described below
commit dd0bbf6a3bd8a80ee659517598e8b8fc822b7ca8
Author: QuakeWang <[email protected]>
AuthorDate: Tue Jul 7 20:39:05 2026 +0800
[core][python] Fix mixed row-id OR pushdown (#8496)
Data evolution scans reused the full filter for row-range extraction,
stats pruning, and global index evaluation.
For mixed predicates such as `_ROW_ID = 1 OR f0 > 5`, the row-id visitor
cannot fully convert the predicate to row ranges. The old path could
still remove or push `_ROW_ID` branches into stats/global-index pruning,
which may narrow scan semantics, hit system-field handling bugs, or
incorrectly enable limit/top-n split pruning.
This PR keeps the full predicate for read-time filtering and limit/top-n
safety, while only pushing row-id-safe residual predicates into scan
pruning and global index evaluation. Python follows the same rule and
excludes `_ROW_ID` from stats predicates.
---
.../paimon/globalindex/DataEvolutionBatchScan.java | 49 +++++++---
.../table/source/snapshot/SnapshotReader.java | 9 ++
.../table/source/snapshot/SnapshotReaderImpl.java | 24 ++++-
.../apache/paimon/table/system/AuditLogTable.java | 10 ++
.../globalindex/DataEvolutionBatchScanTest.java | 73 +++++++++++++++
.../paimon/table/BtreeGlobalIndexTableTest.java | 21 +++++
.../paimon/table/DataEvolutionTableTest.java | 102 +++++++++++++++++++++
paimon-python/pypaimon/read/push_down_utils.py | 6 ++
.../pypaimon/read/scanner/file_scanner.py | 11 ++-
paimon-python/pypaimon/tests/range_test.py | 3 +
.../pypaimon/tests/reader_predicate_test.py | 23 ++++-
11 files changed, 305 insertions(+), 26 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
b/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
index 76ae6195d5..39634f012c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
+++
b/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
@@ -27,7 +27,9 @@ import org.apache.paimon.metrics.MetricRegistry;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.predicate.CompoundPredicate;
import org.apache.paimon.predicate.LeafPredicate;
+import org.apache.paimon.predicate.Or;
import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.predicate.RowIdPredicateVisitor;
import org.apache.paimon.predicate.TopN;
import org.apache.paimon.table.FileStoreTable;
@@ -85,39 +87,52 @@ public class DataEvolutionBatchScan implements
DataTableScan {
return this;
}
- predicate.visit(new
RowIdPredicateVisitor()).ifPresent(this::withRowRanges);
- predicate = removeRowIdFilter(predicate);
+ Optional<List<Range>> rowRanges = predicate.visit(new
RowIdPredicateVisitor());
+ if (rowRanges.isPresent()) {
+ withRowRanges(rowRanges.get());
+ }
this.filter = predicate;
- batchScan.withFilter(predicate);
+
+ batchScan.snapshotReader().withFilter(predicate,
rowIdSafeResidualFilter(predicate));
return this;
}
- private Predicate removeRowIdFilter(Predicate filter) {
+ private Predicate rowIdSafeResidualFilter(Predicate filter) {
if (filter instanceof LeafPredicate
&& ((LeafPredicate)
filter).fieldNames().contains(ROW_ID.name())) {
return null;
} else if (filter instanceof CompoundPredicate) {
CompoundPredicate compoundPredicate = (CompoundPredicate) filter;
+ if (compoundPredicate.function() instanceof Or) {
+ return containsRowId(compoundPredicate) ? null : filter;
+ }
List<Predicate> newChildren = new ArrayList<>();
for (Predicate child : compoundPredicate.children()) {
- Predicate newChild = removeRowIdFilter(child);
+ Predicate newChild = rowIdSafeResidualFilter(child);
if (newChild != null) {
newChildren.add(newChild);
}
}
- if (newChildren.isEmpty()) {
- return null;
- } else if (newChildren.size() == 1) {
- return newChildren.get(0);
- } else {
- return new CompoundPredicate(compoundPredicate.function(),
newChildren);
- }
+ return PredicateBuilder.andNullable(newChildren);
}
return filter;
}
+ private boolean containsRowId(Predicate filter) {
+ if (filter instanceof LeafPredicate) {
+ return ((LeafPredicate)
filter).fieldNames().contains(ROW_ID.name());
+ } else if (filter instanceof CompoundPredicate) {
+ for (Predicate child : ((CompoundPredicate) filter).children()) {
+ if (containsRowId(child)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
@Override
public InnerTableScan withReadType(@Nullable RowType readType) {
batchScan.withReadType(readType);
@@ -270,19 +285,23 @@ public class DataEvolutionBatchScan implements
DataTableScan {
if (!options.globalIndexEnabled()) {
return Optional.empty();
}
+ Predicate globalIndexFilter = rowIdSafeResidualFilter(filter);
+ if (globalIndexFilter == null) {
+ return Optional.empty();
+ }
PartitionPredicate partitionFilter =
batchScan.snapshotReader().manifestsReader().partitionFilter();
Optional<GlobalIndexScanner> optionalScanner =
- GlobalIndexScanner.create(table, partitionFilter, filter);
+ GlobalIndexScanner.create(table, partitionFilter,
globalIndexFilter);
if (!optionalScanner.isPresent()) {
return Optional.empty();
}
try (GlobalIndexScanner scanner = optionalScanner.get()) {
- Optional<GlobalIndexResult> result = scanner.scan(filter);
+ Optional<GlobalIndexResult> result =
scanner.scan(globalIndexFilter);
if (result.isPresent()) {
LOG.info("Scan table '{}' with global index.", table.name());
- return
Optional.of(result.get().or(scanner.unindexedRows(filter)));
+ return
Optional.of(result.get().or(scanner.unindexedRows(globalIndexFilter)));
}
return Optional.empty();
} catch (IOException e) {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReader.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReader.java
index 1ea0854eea..90e680e6d1 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReader.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReader.java
@@ -75,6 +75,15 @@ public interface SnapshotReader {
SnapshotReader withFilter(Predicate predicate);
+ /**
+ * Applies a full read-time filter and an optional scan-pruning filter.
+ *
+ * <p>{@code predicate} is used to determine whether the reader has a
non-partition filter which
+ * must still be evaluated at read time. {@code pushdownPredicate} is the
only predicate used
+ * for scan pruning such as partition, stats, and bucket pruning.
+ */
+ SnapshotReader withFilter(Predicate predicate, @Nullable Predicate
pushdownPredicate);
+
SnapshotReader withPartitionFilter(Map<String, String> partitionSpec);
SnapshotReader withPartitionFilter(Predicate predicate);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReaderImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReaderImpl.java
index 0a752f2564..839b4cef79 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReaderImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/snapshot/SnapshotReaderImpl.java
@@ -233,17 +233,31 @@ public class SnapshotReaderImpl implements SnapshotReader
{
@Override
public SnapshotReader withFilter(Predicate predicate) {
+ return withFilter(predicate, predicate);
+ }
+
+ @Override
+ public SnapshotReader withFilter(Predicate predicate, @Nullable Predicate
pushdownPredicate) {
Pair<Optional<PartitionPredicate>, List<Predicate>> pair =
splitPartitionPredicatesAndDataPredicates(
predicate, tableSchema.logicalRowType(),
tableSchema.partitionKeys());
- if (pair.getLeft().isPresent()) {
- scan.withPartitionFilter(pair.getLeft().get());
- }
if (!pair.getRight().isEmpty()) {
this.hasNonPartitionFilter = true;
- nonPartitionFilterConsumer.accept(scan,
PredicateBuilder.and(pair.getRight()));
}
- scan.withCompleteFilter(predicate);
+
+ Pair<Optional<PartitionPredicate>, List<Predicate>> pushdownPair =
+ splitPartitionPredicatesAndDataPredicates(
+ pushdownPredicate,
+ tableSchema.logicalRowType(),
+ tableSchema.partitionKeys());
+ if (pushdownPair.getLeft().isPresent()) {
+ scan.withPartitionFilter(pushdownPair.getLeft().get());
+ }
+ if (!pushdownPair.getRight().isEmpty()) {
+ this.hasNonPartitionFilter = true;
+ nonPartitionFilterConsumer.accept(scan,
PredicateBuilder.and(pushdownPair.getRight()));
+ }
+ scan.withCompleteFilter(pushdownPredicate);
return this;
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/system/AuditLogTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/system/AuditLogTable.java
index 1e912d4068..71ca70d8fe 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/system/AuditLogTable.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/system/AuditLogTable.java
@@ -340,6 +340,16 @@ public class AuditLogTable implements DataTable,
ReadonlyTable {
return this;
}
+ @Override
+ public SnapshotReader withFilter(Predicate predicate, Predicate
pushdownPredicate) {
+ Optional<Predicate> converted = convert(predicate);
+ Optional<Predicate> convertedPushdown = convert(pushdownPredicate);
+ if (converted.isPresent()) {
+ wrapped.withFilter(converted.get(),
convertedPushdown.orElse(null));
+ }
+ return this;
+ }
+
@Override
public SnapshotReader withPartitionFilter(Map<String, String>
partitionSpec) {
wrapped.withPartitionFilter(partitionSpec);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/globalindex/DataEvolutionBatchScanTest.java
b/paimon-core/src/test/java/org/apache/paimon/globalindex/DataEvolutionBatchScanTest.java
index 30744ae542..4c7cc83d07 100644
---
a/paimon-core/src/test/java/org/apache/paimon/globalindex/DataEvolutionBatchScanTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/globalindex/DataEvolutionBatchScanTest.java
@@ -20,12 +20,20 @@ package org.apache.paimon.globalindex;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.DataTableBatchScan;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.snapshot.SnapshotReader;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.Range;
import org.apache.paimon.utils.RowRangeIndex;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.Arrays;
@@ -34,11 +42,63 @@ import java.util.List;
import java.util.Random;
import static org.apache.paimon.stats.SimpleStats.EMPTY_STATS;
+import static org.apache.paimon.table.SpecialFields.ROW_ID;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
/** Tests for {@link DataEvolutionBatchScan}. */
public class DataEvolutionBatchScanTest {
+ @Test
+ public void testWithFilterKeepsMixedOrWhenRowRangeExtractionFails() {
+ PredicateBuilder builder = new PredicateBuilder(rowTypeWithRowId());
+ Predicate predicate = PredicateBuilder.or(builder.equal(2, 1L),
builder.greaterThan(0, 5));
+
+ DataTableBatchScan batchScan = mock(DataTableBatchScan.class);
+ SnapshotReader snapshotReader = mockSnapshotReader(batchScan);
+ new DataEvolutionBatchScan(null, batchScan).withFilter(predicate);
+
+ verify(snapshotReader).withFilter(predicate, null);
+ verify(batchScan, never()).withFilter(any(Predicate.class));
+ }
+
+ @Test
+ public void testWithFilterRemovesRowIdAfterRowRangeExtractionSucceeds() {
+ PredicateBuilder builder = new PredicateBuilder(rowTypeWithRowId());
+ Predicate nonRowIdPredicate = builder.greaterThan(0, 5);
+ Predicate predicate = PredicateBuilder.and(builder.equal(2, 1L),
nonRowIdPredicate);
+
+ DataTableBatchScan batchScan = mock(DataTableBatchScan.class);
+ SnapshotReader snapshotReader = mockSnapshotReader(batchScan);
+ new DataEvolutionBatchScan(null, batchScan).withFilter(predicate);
+
+ ArgumentCaptor<Predicate> captor =
ArgumentCaptor.forClass(Predicate.class);
+ verify(snapshotReader).withFilter(same(predicate), captor.capture());
+ assertThat(captor.getValue()).isSameAs(nonRowIdPredicate);
+ }
+
+ @Test
+ public void testWithFilterDropsNestedMixedOrFromStatsResidual() {
+ PredicateBuilder builder = new PredicateBuilder(rowTypeWithRowId());
+ Predicate nonRowIdPredicate = builder.lessThan(0, 100);
+ Predicate mixedOr = PredicateBuilder.or(builder.equal(2, 1L),
builder.greaterThan(1, 5));
+ Predicate predicate =
+ PredicateBuilder.and(builder.between(2, 0L, 10L),
nonRowIdPredicate, mixedOr);
+
+ DataTableBatchScan batchScan = mock(DataTableBatchScan.class);
+ SnapshotReader snapshotReader = mockSnapshotReader(batchScan);
+ new DataEvolutionBatchScan(null, batchScan).withFilter(predicate);
+
+ ArgumentCaptor<Predicate> captor =
ArgumentCaptor.forClass(Predicate.class);
+ verify(snapshotReader).withFilter(same(predicate), captor.capture());
+ assertThat(captor.getValue()).isSameAs(nonRowIdPredicate);
+ }
+
@Test
public void testWrapToIndexSplitsRandomly() {
Random random = new Random();
@@ -137,6 +197,19 @@ public class DataEvolutionBatchScanTest {
.containsExactly(new Range(4200, 4450), new Range(4650, 4700));
}
+ private static RowType rowTypeWithRowId() {
+ return RowType.of(
+ new DataField(0, "f0", DataTypes.INT()),
+ new DataField(1, "f1", DataTypes.INT()),
+ new DataField(2, ROW_ID.name(), DataTypes.BIGINT()));
+ }
+
+ private static SnapshotReader mockSnapshotReader(DataTableBatchScan
batchScan) {
+ SnapshotReader snapshotReader = mock(SnapshotReader.class);
+ when(batchScan.snapshotReader()).thenReturn(snapshotReader);
+ return snapshotReader;
+ }
+
private static List<Range> expectedRanges(long min, long max, List<Range>
rowRanges) {
List<Range> expected = new ArrayList<>();
for (Range range : rowRanges) {
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
index 0723ee5239..9ae9666200 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
@@ -133,6 +133,27 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
assertThat(readF1).containsExactly("a200", "a300", "a400", "a56789");
}
+ @Test
+ public void testMixedRowIdOrSkipsGlobalIndexScan() throws Exception {
+ write(10L);
+ createIndex("f1");
+
+ FileStoreTable table = (FileStoreTable) catalog.getTable(identifier());
+ PredicateBuilder builder =
+ new
PredicateBuilder(SpecialFields.rowTypeWithRowId(table.rowType()));
+ int rowIdIndex = table.rowType().getFieldCount();
+ Predicate predicate =
+ PredicateBuilder.or(
+ builder.equal(rowIdIndex, 1L),
+ builder.equal(1, BinaryString.fromString("a7")));
+
+ ReadBuilder readBuilder = table.newReadBuilder().withFilter(predicate);
+ List<Split> splits = readBuilder.newScan().plan().splits();
+
+ assertThat(splits).isNotEmpty();
+ assertThat(splits).allMatch(split -> split instanceof DataSplit);
+ }
+
@Test
public void testBTreeGlobalIndexSearchModeControlsUnindexedData() throws
Exception {
write(500L);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionTableTest.java
index 89742874c1..51cf16a114 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionTableTest.java
@@ -51,6 +51,7 @@ import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.table.source.Split;
import org.apache.paimon.table.source.StreamTableScan;
import org.apache.paimon.table.source.TableScan;
+import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.Range;
@@ -516,6 +517,84 @@ public class DataEvolutionTableTest extends
DataEvolutionTestBase {
assertThat(readBuilder.newScan().plan().splits().isEmpty()).isTrue();
}
+ @Test
+ public void testMixedRowIdOrFilterDoesNotPruneByUnsafeStatsResidual()
throws Exception {
+ createTableDefault();
+ Schema schema = schemaDefault();
+ FileStoreTable table = getTableDefault();
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ RowType writeType = schema.rowType().project(Arrays.asList("f0",
"f1"));
+
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(GenericRow.of(1, BinaryString.fromString("a")));
+ write.write(GenericRow.of(2, BinaryString.fromString("b")));
+ List<CommitMessage> commitables = write.prepareCommit();
+ setFirstRowId(commitables, 0L);
+ commit.commit(commitables);
+ }
+
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(GenericRow.of(6, BinaryString.fromString("c")));
+ write.write(GenericRow.of(7, BinaryString.fromString("d")));
+ List<CommitMessage> commitables = write.prepareCommit();
+ setFirstRowId(commitables, 2L);
+ commit.commit(commitables);
+ }
+
+ PredicateBuilder predicateBuilder = new
PredicateBuilder(rowTypeWithRowId(schema));
+ int rowIdIndex = schema.rowType().getFieldCount();
+ Predicate topLevelMixedOr =
+ PredicateBuilder.or(
+ predicateBuilder.equal(rowIdIndex, 1L),
predicateBuilder.greaterThan(0, 5));
+ assertThat(plannedFirstRowIds(table,
topLevelMixedOr)).isEqualTo(Arrays.asList(0L, 2L));
+
+ Predicate nestedMixedOr =
+ PredicateBuilder.and(
+ predicateBuilder.between(rowIdIndex, 0L, 10L),
+ PredicateBuilder.or(
+ predicateBuilder.equal(rowIdIndex, 1L),
+ predicateBuilder.greaterThan(0, 5)));
+ assertThat(plannedFirstRowIds(table,
nestedMixedOr)).isEqualTo(Arrays.asList(0L, 2L));
+ }
+
+ @Test
+ public void testMixedRowIdOrFilterDisablesUnsafeLimitPushDown() throws
Exception {
+ createTableDefault();
+ Schema schema = schemaDefault();
+ FileStoreTable table = getTableDefault();
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ RowType writeType = schema.rowType().project(Arrays.asList("f0",
"f1"));
+
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(GenericRow.of(1, BinaryString.fromString("a")));
+ List<CommitMessage> commitables = write.prepareCommit();
+ setFirstRowId(commitables, 0L);
+ commit.commit(commitables);
+ }
+
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = builder.newCommit()) {
+ write.write(GenericRow.of(60, BinaryString.fromString("b")));
+ List<CommitMessage> commitables = write.prepareCommit();
+ setFirstRowId(commitables, 1L);
+ commit.commit(commitables);
+ }
+
+ PredicateBuilder predicateBuilder = new
PredicateBuilder(rowTypeWithRowId(schema));
+ int rowIdIndex = schema.rowType().getFieldCount();
+ Predicate predicate =
+ PredicateBuilder.or(
+ predicateBuilder.equal(rowIdIndex, 999L),
+ predicateBuilder.greaterThan(0, 50));
+
+ TableScan.Plan plan =
+
table.newReadBuilder().withFilter(predicate).withLimit(1).newScan().plan();
+ assertThat(plannedFirstRowIds(plan)).isEqualTo(Arrays.asList(0L, 1L));
+ }
+
@Test
public void testLimitPushDownWithoutFilter() throws Exception {
createTableDefault();
@@ -1987,6 +2066,29 @@ public class DataEvolutionTableTest extends
DataEvolutionTestBase {
.sum();
}
+ private static List<Long> plannedFirstRowIds(FileStoreTable table,
Predicate filter) {
+ return
plannedFirstRowIds(table.newReadBuilder().withFilter(filter).newScan().plan());
+ }
+
+ private static List<Long> plannedFirstRowIds(TableScan.Plan plan) {
+ return plan.splits().stream()
+ .flatMap(
+ split ->
+ (split instanceof DataSplit
+ ? ((DataSplit)
split).dataFiles()
+ : ((IndexedSplit)
split).dataSplit().dataFiles())
+ .stream())
+ .map(DataFileMeta::firstRowId)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ private static RowType rowTypeWithRowId(Schema schema) {
+ List<DataField> fields = new ArrayList<>(schema.rowType().getFields());
+ fields.add(SpecialFields.ROW_ID);
+ return new RowType(fields);
+ }
+
private static long countMatchingRows(ReadBuilder rb) throws Exception {
RecordReader<InternalRow> reader =
rb.newRead().createReader(rb.newScan().plan());
AtomicInteger cnt = new AtomicInteger(0);
diff --git a/paimon-python/pypaimon/read/push_down_utils.py
b/paimon-python/pypaimon/read/push_down_utils.py
index ee564a4c79..9c12872d9f 100644
--- a/paimon-python/pypaimon/read/push_down_utils.py
+++ b/paimon-python/pypaimon/read/push_down_utils.py
@@ -150,6 +150,12 @@ def remove_row_id_filter(predicate: Predicate) ->
Optional[Predicate]:
filtered.append(r)
return PredicateBuilder.and_predicates(filtered)
if predicate.method == "or":
+ fields = _get_all_fields(predicate)
+ if (
+ SpecialFields.ROW_ID.name in fields
+ and fields != {SpecialFields.ROW_ID.name}
+ ):
+ return predicate
new_children = []
for c in predicate.literals or []:
r = remove_row_id_filter(c)
diff --git a/paimon-python/pypaimon/read/scanner/file_scanner.py
b/paimon-python/pypaimon/read/scanner/file_scanner.py
index 57fe7e27b2..153b424b13 100755
--- a/paimon-python/pypaimon/read/scanner/file_scanner.py
+++ b/paimon-python/pypaimon/read/scanner/file_scanner.py
@@ -52,6 +52,7 @@ from pypaimon.read.scanner.primary_key_table_split_generator
import \
from pypaimon.read.split import DataSplit
from pypaimon.snapshot.snapshot import Snapshot
from pypaimon.table.bucket_mode import BucketMode
+from pypaimon.table.special_fields import SpecialFields
from pypaimon.table.source.deletion_file import DeletionFile
@@ -221,7 +222,15 @@ class FileScanner:
self.table: FileStoreTable = table
self.manifest_scanner = manifest_scanner
self.predicate = predicate
- self.predicate_for_stats = remove_row_id_filter(predicate) if
predicate else None
+ row_ranges = (
+ _row_ranges_from_predicate(predicate) if predicate else None
+ )
+ if predicate and row_ranges is not None:
+ self.predicate_for_stats = remove_row_id_filter(predicate)
+ else:
+ self.predicate_for_stats = predicate
+ self.predicate_for_stats = exclude_predicate_with_fields(
+ self.predicate_for_stats, {SpecialFields.ROW_ID.name})
# Partition columns aren't in data files, so skip them for value-stats
pruning.
self.predicate_for_stats = exclude_predicate_with_fields(
self.predicate_for_stats, set(self.table.partition_keys))
diff --git a/paimon-python/pypaimon/tests/range_test.py
b/paimon-python/pypaimon/tests/range_test.py
index 0956ddfaf4..500fe47de9 100644
--- a/paimon-python/pypaimon/tests/range_test.py
+++ b/paimon-python/pypaimon/tests/range_test.py
@@ -75,6 +75,9 @@ class RangeTest(unittest.TestCase):
pred_or = Predicate('or', None, None, [pred_eq5, pred_eq3])
assert _row_ranges_from_predicate(pred_or) == [Range(3, 3), Range(5,
5)]
+ pred_mixed_or = Predicate(
+ 'or', None, None, [pred_eq5, pred_other])
+ assert _row_ranges_from_predicate(pred_mixed_or) is None
pred_between_1_5 = Predicate('between', 0, SpecialFields.ROW_ID.name,
[1, 5])
pred_between_3_7 = Predicate('between', 0, SpecialFields.ROW_ID.name,
[3, 7])
pred_or_overlap = Predicate('or', None, None, [pred_between_1_5,
pred_between_3_7])
diff --git a/paimon-python/pypaimon/tests/reader_predicate_test.py
b/paimon-python/pypaimon/tests/reader_predicate_test.py
index 4508a2063c..89e4efef6a 100644
--- a/paimon-python/pypaimon/tests/reader_predicate_test.py
+++ b/paimon-python/pypaimon/tests/reader_predicate_test.py
@@ -104,12 +104,25 @@ class ReaderPredicateTest(unittest.TestCase):
self.assertIsNotNone(result)
self.assertEqual(result.field, 'f0')
self.assertEqual(result.method, 'greaterThan')
- or_mixed = pb.or_predicates([pb.equal('_ROW_ID', 1),
pb.greater_than('f0', 5)])
+ or_mixed = pb.or_predicates([
+ pb.equal('_ROW_ID', 1),
+ pb.greater_than('f0', 5),
+ ])
result = push_down_utils.remove_row_id_filter(or_mixed)
- self.assertIsNotNone(result, "OR: strip _ROW_ID child, keep f0>5 (same
as Java)")
- self.assertEqual(result.field, 'f0')
- self.assertEqual(result.method, 'greaterThan')
- or_no_row_id = pb.or_predicates([pb.greater_than('f0', 5),
pb.less_than('f0', 10)])
+ self.assertIsNotNone(result)
+ self.assertEqual(result.method, 'or')
+ self.assertEqual(len(result.literals), 2)
+ self.assertEqual(result.literals[0].field, '_ROW_ID')
+ self.assertEqual(result.literals[1].field, 'f0')
+ or_row_id = pb.or_predicates([
+ pb.equal('_ROW_ID', 1),
+ pb.equal('_ROW_ID', 2),
+ ])
+ self.assertIsNone(push_down_utils.remove_row_id_filter(or_row_id))
+ or_no_row_id = pb.or_predicates([
+ pb.greater_than('f0', 5),
+ pb.less_than('f0', 10),
+ ])
result = push_down_utils.remove_row_id_filter(or_no_row_id)
self.assertIsNotNone(result)
self.assertEqual(result.method, 'or')