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 9731d53979 [core] Push down DV-aware bitmap selection for merged 
groups (#10387)
9731d53979 is described below

commit 9731d5397952fd6fe556dcba1d36852cd8300ddf
Author: sanshi <[email protected]>
AuthorDate: Mon Oct 5 20:40:19 2026 +0800

    [core] Push down DV-aware bitmap selection for merged groups (#10387)
---
 .../paimon/operation/DataEvolutionSplitRead.java   | 29 +++++++----
 .../paimon/table/DataEvolutionFileIndexTest.java   | 58 ++++++++++++++++++++++
 2 files changed, 76 insertions(+), 11 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
index 470a15f4a5..b053834633 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
@@ -286,10 +286,13 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                             DeletionVectorWithRange deletionVector =
                                     readDeletionVector(needMergeFiles, 
deletionVectorFactory);
                             if (deletionVector != null
-                                    && !deletionVector.deletionVector.isEmpty()
-                                    && skipByFileIndex(
-                                            fileIndexResults, rowRanges, 
deletionVector)) {
-                                return new EmptyFileRecordReader<>();
+                                    && 
!deletionVector.deletionVector.isEmpty()) {
+                                fileIndexResults =
+                                        intersectFileIndexResults(
+                                                fileIndexResults, rowRanges, 
deletionVector);
+                                if (fileIndexResults == null) {
+                                    return new EmptyFileRecordReader<>();
+                                }
                             }
                             BitmapIndexResult groupSelection =
                                     buildGroupSelection(needMergeFiles, 
fileIndexResults);
@@ -994,16 +997,19 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
     /**
      * Applies a group deletion vector to already evaluated file-index results.
      *
-     * <p>A merged group can only be skipped when one of the files responsible 
for the final
-     * predicate values has no live candidate rows. This method only 
intersects the saved results;
-     * it does not reopen or reevaluate any file index. The offset passed to 
the evaluator maps
-     * positions in the group's DV anchor range to positions local to the 
current file.
+     * <p>A merged group can be skipped when one of the files responsible for 
the final predicate
+     * values has no live candidate rows. Otherwise, the intersected results 
are retained for the
+     * group-level bitmap selection. This method only intersects the saved 
results; it does not
+     * reopen or reevaluate any file index. The offset passed to the evaluator 
maps positions in the
+     * group's DV anchor range to positions local to the current file.
      */
-    private boolean skipByFileIndex(
+    @Nullable
+    private List<FileIndexResultEntry> intersectFileIndexResults(
             List<FileIndexResultEntry> fileIndexResults,
             List<Range> rowRanges,
             DeletionVectorWithRange deletionVector) {
         DeletionVector dv = deletionVector.deletionVector;
+        List<FileIndexResultEntry> intersectedResults = new 
ArrayList<>(fileIndexResults.size());
         for (FileIndexResultEntry entry : fileIndexResults) {
             long fileOffset =
                     deletionVectorOffset(entry.file.nonNullRowIdRange(), 
rowRanges, deletionVector);
@@ -1011,10 +1017,11 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                     FileIndexEvaluator.intersectDeletionVector(
                             entry.result, entry.file, dv, fileOffset);
             if (!result.remain()) {
-                return true;
+                return null;
             }
+            intersectedResults.add(new FileIndexResultEntry(entry.file, 
result));
         }
-        return false;
+        return intersectedResults;
     }
 
     /**
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
index e323817166..c2629f4f98 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.table;
 
 import org.apache.paimon.CoreOptions;
+import org.apache.paimon.append.ForceSingleBatchReader;
 import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryString;
@@ -26,10 +27,12 @@ import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.Timestamp;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
+import org.apache.paimon.deletionvectors.ApplyDeletionVectorReader;
 import org.apache.paimon.deletionvectors.BitmapDeletionVector;
 import org.apache.paimon.deletionvectors.DeletionVector;
 import org.apache.paimon.deletionvectors.append.BaseAppendDeleteFileMaintainer;
 import org.apache.paimon.fileindex.FileIndexOptions;
+import org.apache.paimon.fileindex.bitmap.ApplyBitmapIndexRecordReader;
 import org.apache.paimon.fileindex.bitmap.BitmapFileIndexFactory;
 import org.apache.paimon.fileindex.bloomfilter.BloomFilterFileIndexFactory;
 import org.apache.paimon.fileindex.bsi.BitSliceIndexBitmapFileIndexFactory;
@@ -42,6 +45,7 @@ import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.reader.DataEvolutionFileReader;
 import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
@@ -65,6 +69,7 @@ import org.junit.jupiter.params.provider.ValueSource;
 
 import javax.annotation.Nullable;
 
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -649,6 +654,31 @@ public class DataEvolutionFileIndexTest extends 
DataEvolutionTestBase {
         assertRow(assertSingleRow(query(neighbour, equalF1(f1(50)))), 50);
     }
 
+    @Test
+    public void testMergedGroupDvAwareBitmapSelectionIsPushedDown() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+        FileStoreTable table = createTable("merged_bitmap_dv_partial", 
options);
+        writeSplitColumns(table, ROW_COUNT, bitmapOptions("f1"), 
Collections.emptyMap());
+
+        deleteRows(table, 51);
+
+        Predicate filter = PredicateBuilder.or(equalF1(f1(50)), 
equalF1(f1(51)));
+        FileStoreTable latest = getTable(identifier(table.name()));
+        ReadBuilder readBuilder = latest.newReadBuilder().withFilter(filter);
+        DataSplit split = (DataSplit) 
readBuilder.newScan().plan().splits().get(0);
+        InternalRowSerializer serializer = new 
InternalRowSerializer(latest.rowType());
+        List<InternalRow> rows = new ArrayList<>();
+        try (RecordReader<InternalRow> reader = 
readBuilder.newRead().createReader(split)) {
+            // Bypass the final DV filter. The result is still correct only 
when the DV-aware bitmap
+            // {50} (rather than the original index bitmap {50, 51}) reached 
every field reader.
+            removeFinalDeletionVectorReaders(reader);
+            reader.forEachRemaining(row -> rows.add(serializer.copy(row)));
+        }
+
+        assertRow(assertSingleRow(rows), 50);
+    }
+
     @Test
     public void testMergedGroupFileIndexSkipsBeforeReadingDeletionVector() 
throws Exception {
         Map<String, String> options = new HashMap<>();
@@ -1094,6 +1124,34 @@ public class DataEvolutionFileIndexTest extends 
DataEvolutionTestBase {
         return rows;
     }
 
+    @SuppressWarnings("unchecked")
+    private static void 
removeFinalDeletionVectorReaders(RecordReader<InternalRow> reader)
+            throws Exception {
+        assertThat(reader).isInstanceOf(DataEvolutionFileReader.class);
+        RecordReader<InternalRow>[] fieldReaders =
+                (RecordReader<InternalRow>[]) readField(reader, "readers");
+        for (int i = 0; i < fieldReaders.length; i++) {
+            if (fieldReaders[i] == null) {
+                continue;
+            }
+
+            
assertThat(fieldReaders[i]).isInstanceOf(ForceSingleBatchReader.class);
+            RecordReader<InternalRow> fieldReader =
+                    (RecordReader<InternalRow>) readField(fieldReaders[i], 
"multiBatchReader");
+            
assertThat(fieldReader).isInstanceOf(ApplyDeletionVectorReader.class);
+            RecordReader<InternalRow> bitmapReader =
+                    ((ApplyDeletionVectorReader) fieldReader).reader();
+            
assertThat(bitmapReader).isInstanceOf(ApplyBitmapIndexRecordReader.class);
+            fieldReaders[i] = new ForceSingleBatchReader(bitmapReader);
+        }
+    }
+
+    private static Object readField(Object target, String name) throws 
Exception {
+        Field field = target.getClass().getDeclaredField(name);
+        field.setAccessible(true);
+        return field.get(target);
+    }
+
     private void assertMergedGroup(FileStoreTable table) throws Exception {
         FileStoreTable latest = getTable(identifier(table.name()));
         List<Split> splits = latest.newReadBuilder().newScan().plan().splits();

Reply via email to