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 bfa0b36d26 [core] Push down bitmap indexes for data evolution merged 
reads (#10289)
bfa0b36d26 is described below

commit bfa0b36d267cd22ba65fa6624a20acc4926771a9
Author: sanshi <[email protected]>
AuthorDate: Fri Oct 2 09:45:09 2026 +0800

    [core] Push down bitmap indexes for data evolution merged reads (#10289)
---
 .../paimon/operation/DataEvolutionSplitRead.java   | 114 +++++++++++++++++----
 .../paimon/table/DataEvolutionFileIndexTest.java   |  42 +++++++-
 2 files changed, 135 insertions(+), 21 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 696831fbf6..60902d352a 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
@@ -100,9 +100,10 @@ import static 
org.apache.paimon.utils.Preconditions.checkNotNull;
  * A union {@link SplitRead} to read multiple inner files to merge columns.
  *
  * <p>Filters can only be pushed down where they can not interfere with the 
column merging: a file
- * read without merging gets both the file index and the format level push 
down, while a merged
- * group only uses the file index to skip the whole group, as dropping rows in 
one of the merged
- * readers would break the positional alignment between them.
+ * read without merging gets both the file index and the format level push 
down, while an eligible
+ * merged group shares one bitmap selection across all aligned field readers. 
Other merged groups
+ * only use the file index to skip the whole group, as dropping rows in one 
reader would break the
+ * positional alignment between them.
  *
  * <p>Only a filter whose every field belongs to the read type is pushed down, 
see {@link
  * #readTypeFilters}, and only to the files that wrote those fields, see 
{@link #fileFilters}.
@@ -267,13 +268,16 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                                             fileIndexResults, rowRanges, 
deletionVector)) {
                                 return new EmptyFileRecordReader<>();
                             }
+                            BitmapIndexResult groupSelection =
+                                    buildGroupSelection(needMergeFiles, 
fileIndexResults);
                             return createUnionReader(
                                     needMergeFiles,
                                     partition,
                                     dataFilePathFactory,
                                     rowRanges,
                                     readRowType,
-                                    deletionVector);
+                                    deletionVector,
+                                    groupSelection);
                         });
             }
         }
@@ -296,7 +300,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
             DataFilePathFactory dataFilePathFactory,
             List<Range> rowRanges,
             RowType readRowType,
-            @Nullable DeletionVectorWithRange deletionVector)
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
             throws IOException {
         List<DataEvolutionVectorReadPlanner.ReadRange> vectorRanges =
                 DataEvolutionVectorReadPlanner.plan(
@@ -341,7 +346,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                                         dataFilePathFactory,
                                         readRanges,
                                         readRowType,
-                                        deletionVector));
+                                        deletionVector,
+                                        null));
             }
             return ConcatRecordReader.create(suppliers);
         }
@@ -365,7 +371,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                 dataFilePathFactory,
                 rowRanges,
                 readRowType,
-                deletionVector);
+                deletionVector,
+                groupSelection);
     }
 
     private RecordReader<InternalRow> createUnionReader(
@@ -375,7 +382,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
             DataFilePathFactory dataFilePathFactory,
             List<Range> rowRanges,
             RowType readRowType,
-            @Nullable DeletionVectorWithRange deletionVector)
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
             throws IOException {
 
         long rowCount = fieldsFiles.get(0).rowCount();
@@ -424,7 +432,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                     formatBuilder,
                     rowRanges,
                     readRowType,
-                    deletionVector);
+                    deletionVector,
+                    groupSelection);
         }
 
         // Build the per-bunch readers from the planned partial read row types.
@@ -464,7 +473,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                                     formatReaderMapping,
                                     rowRanges,
                                     partialReadRowType,
-                                    deletionVector));
+                                    deletionVector,
+                                    groupSelection));
         }
 
         return nestedFieldEnabled
@@ -482,7 +492,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
             Builder formatBuilder,
             List<Range> rowRanges,
             RowType readRowType,
-            @Nullable DeletionVectorWithRange deletionVector)
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
             throws IOException {
         DataFileMeta firstFile = bunch.files().get(0);
         // Use the physical schema: the full table schema may declare columns 
this file never wrote.
@@ -500,7 +511,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                 mapping,
                 rowRanges,
                 readRowType,
-                deletionVector);
+                deletionVector,
+                groupSelection);
     }
 
     private boolean nestedFieldEnabledFor(List<DataFileMeta> files) {
@@ -565,7 +577,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
             FormatReaderMapping formatReaderMapping,
             List<Range> rowRanges,
             RowType readRowType,
-            @Nullable DeletionVectorWithRange deletionVector)
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
             throws IOException {
         if (bunch instanceof DataBunch) {
             // for data bunch, directly read the single file
@@ -576,7 +589,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                     formatReaderMapping,
                     rowRanges,
                     readRowType,
-                    deletionVector);
+                    deletionVector,
+                    groupSelection);
         } else if (bunch instanceof VectorFileBunch) {
             // for vector bunch, sequential read all data files and concat them
             return sequentialReadFiles(
@@ -585,7 +599,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                     dataFilePathFactory,
                     formatReaderMapping,
                     rowRanges,
-                    deletionVector);
+                    deletionVector,
+                    groupSelection);
         } else if (bunch instanceof BlobFileBunch) {
             // for blob bunch, fallback on placeholders
             BlobFileBunch blobBunch = (BlobFileBunch) bunch;
@@ -619,7 +634,8 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
             DataFilePathFactory dataFilePathFactory,
             FormatReaderMapping formatReaderMapping,
             List<Range> rowRanges,
-            @Nullable DeletionVectorWithRange deletionVector)
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
             throws IOException {
         List<ReaderSupplier<InternalRow>> readerSuppliers = new ArrayList<>();
         for (DataFileMeta file : files) {
@@ -636,7 +652,7 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
                                             dataFilePathFactory.toPath(file),
                                             file.fileSize()),
                                     deletionVector,
-                                    null));
+                                    groupSelection));
         }
         return ConcatRecordReader.create(readerSuppliers);
     }
@@ -727,14 +743,35 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
         return createFileReader(
                 partition,
                 file,
+                dataFilePathFactory,
                 formatReaderMapping,
                 rowRanges,
                 readRowType,
-                readTarget(file, dataFilePathFactory, rowRanges),
                 deletionVector,
                 null);
     }
 
+    private FileRecordReader<InternalRow> createFileReader(
+            BinaryRow partition,
+            DataFileMeta file,
+            DataFilePathFactory dataFilePathFactory,
+            FormatReaderMapping formatReaderMapping,
+            List<Range> rowRanges,
+            RowType readRowType,
+            @Nullable DeletionVectorWithRange deletionVector,
+            @Nullable BitmapIndexResult groupSelection)
+            throws IOException {
+        return createFileReader(
+                partition,
+                file,
+                formatReaderMapping,
+                rowRanges,
+                readRowType,
+                readTarget(file, dataFilePathFactory, rowRanges),
+                deletionVector,
+                groupSelection);
+    }
+
     private FileRecordReader<InternalRow> createFileReader(
             BinaryRow partition,
             DataFileMeta file,
@@ -912,6 +949,47 @@ public class DataEvolutionSplitRead implements 
SplitRead<InternalRow> {
         return false;
     }
 
+    /**
+     * Builds one row selection for a normal merged group from the cached 
file-index results.
+     *
+     * <p>Each result is a necessary condition for the final row, so 
independent bitmap results are
+     * intersected. An OR predicate is already evaluated as one bitmap by 
{@link
+     * FileIndexEvaluator}; treating its result as one condition preserves the 
OR semantics. Results
+     * that cannot provide an exact bitmap remain conservative and do not 
restrict the group.
+     */
+    @Nullable
+    private BitmapIndexResult buildGroupSelection(
+            List<DataFileMeta> files, List<FileIndexResultEntry> 
fileIndexResults) {
+        if (!canPushDownGroupSelection(files)) {
+            return null;
+        }
+
+        BitmapIndexResult selection = null;
+        for (FileIndexResultEntry entry : fileIndexResults) {
+            if (entry.result instanceof BitmapIndexResult) {
+                BitmapIndexResult bitmap = (BitmapIndexResult) entry.result;
+                selection = selection == null ? bitmap : (BitmapIndexResult) 
selection.and(bitmap);
+            }
+        }
+        return selection;
+    }
+
+    private boolean canPushDownGroupSelection(List<DataFileMeta> files) {
+        if (files.isEmpty()) {
+            return false;
+        }
+
+        Range groupRange = files.get(0).nonNullRowIdRange();
+        for (DataFileMeta file : files) {
+            if (isBlobFile(file.fileName())
+                    || isVectorStoreFile(file.fileName())
+                    || !groupRange.equals(file.nonNullRowIdRange())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private Builder formatBuilder(
             RowType readRowType, @Nullable List<Predicate> filters, boolean 
nestedFieldEnabled) {
         return new Builder(
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 30f4be2dca..c350c424f6 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
@@ -152,12 +152,47 @@ public class DataEvolutionFileIndexTest extends 
DataEvolutionTestBase {
         writeSplitColumns(table, ROW_COUNT, Collections.emptyMap(), 
bloomOptions("f2", null));
         assertMergedGroup(table);
 
-        // a merged group is never row filtered, but the rows it returns must 
stay aligned
+        // A non-bitmap index cannot select rows, but the merged fields must 
stay aligned.
         List<InternalRow> rows = readWithFilter(table, equalF2(f2(50)));
         assertThat(rows).hasSize(ROW_COUNT);
         assertAligned(rows);
     }
 
+    @Test
+    public void testMergedGroupBitmapIndexSelectsMatchingRowsOnly() throws 
Exception {
+        FileStoreTable table = createTable("merged_bitmap", 
Collections.emptyMap());
+        writeSplitColumns(table, ROW_COUNT, Collections.emptyMap(), 
bitmapOptions("f2"));
+        assertMergedGroup(table);
+
+        List<InternalRow> rows = readWithFilter(table, equalF2(f2(50)));
+        assertThat(rows).hasSize(1);
+        assertRow(rows.get(0), 50);
+    }
+
+    @Test
+    public void testMergedGroupBitmapIndexIntersectsFieldSelections() throws 
Exception {
+        FileStoreTable table = createTable("merged_bitmap_intersect", 
Collections.emptyMap());
+        writeSplitColumns(table, ROW_COUNT, bitmapOptions("f1"), 
bitmapOptions("f2"));
+        assertMergedGroup(table);
+
+        Predicate filter = PredicateBuilder.and(equalF1(f1(50)), 
equalF2(f2(50)));
+        List<InternalRow> rows = readWithFilter(table, filter);
+        assertThat(rows).hasSize(1);
+        assertRow(rows.get(0), 50);
+    }
+
+    @Test
+    public void testMergedGroupBitmapIndexPreservesOrSelection() throws 
Exception {
+        FileStoreTable table = createTable("merged_bitmap_or", 
Collections.emptyMap());
+        writeSplitColumns(table, ROW_COUNT, Collections.emptyMap(), 
bitmapOptions("f2"));
+        assertMergedGroup(table);
+
+        Predicate filter = PredicateBuilder.or(equalF2(f2(50)), 
equalF2(f2(51)));
+        List<InternalRow> rows = readWithFilter(table, filter);
+        assertThat(rows).extracting(row -> 
row.getInt(0)).containsExactlyInAnyOrder(50, 51);
+        assertAligned(rows);
+    }
+
     @Test
     public void testQueryMergedGroup() throws Exception {
         FileStoreTable table = createTable("merged_group_query", 
Collections.emptyMap());
@@ -579,11 +614,12 @@ public class DataEvolutionFileIndexTest extends 
DataEvolutionTestBase {
         assertThat(latest.fileIO().delete(anchorPath, false)).isTrue();
 
         // The deleted row is the only bitmap hit in the second merged group. 
The missing anchor
-        // file therefore proves that the group was skipped before any union 
reader opened it.
+        // file therefore proves that the group was skipped before any union 
reader opened it;
+        // the first group contributes its matching row through the shared 
bitmap selection.
         RowType readType =
                 
rowTypeWithRowId(rowType()).project(SpecialFields.ROW_ID.name(), "f1", "f2");
         List<InternalRow> rows = readWithFilter(table, equalF1(f1(50)), 
readType);
-        assertThat(rowIds(rows)).containsExactlyElementsOf(rowIds(0, 
ROW_COUNT));
+        assertThat(rowIds(rows)).containsExactly(50L);
 
         FileStoreTable neighbour = createTable("merged_bitmap_dv_neighbour", 
options);
         writeSplitColumns(neighbour, ROW_COUNT, bitmapOptions("f1"), 
Collections.emptyMap());

Reply via email to