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 63d7ca5e3b [core] Fix file index pushdown for bitmap64 deletion 
vectors (#9141)
63d7ca5e3b is described below

commit 63d7ca5e3bdc71a086777491772878af4fbb615d
Author: QuakeWang <[email protected]>
AuthorDate: Mon Aug 17 13:33:46 2026 +0800

    [core] Fix file index pushdown for bitmap64 deletion vectors (#9141)
---
 .../apache/paimon/reader/LimitRecordReader.java    | 26 ++++++
 .../paimon/reader/LimitRecordReaderTest.java       | 93 ++++++++++++++++++++++
 .../org/apache/paimon/io/FileIndexEvaluator.java   |  7 ++
 .../apache/paimon/operation/RawFileSplitRead.java  |  8 +-
 .../apache/paimon/io/FileIndexEvaluatorTest.java   | 79 ++++++++++++++++++
 .../paimon/operation/RawFileSplitReadTest.java     | 57 ++++++++++++-
 6 files changed, 266 insertions(+), 4 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java 
b/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
index 256453497f..2f0766e29a 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
@@ -18,6 +18,8 @@
 
 package org.apache.paimon.reader;
 
+import org.apache.paimon.fs.Path;
+
 import javax.annotation.Nullable;
 
 import java.io.IOException;
@@ -59,6 +61,9 @@ public final class LimitRecordReader<T> implements 
RecordReader<T> {
         if (iterator instanceof ScoreRecordIterator) {
             return new LimitScoreRecordIterator<>((ScoreRecordIterator<T>) 
iterator);
         }
+        if (iterator instanceof FileRecordIterator) {
+            return new LimitFileRecordIterator<>((FileRecordIterator<T>) 
iterator);
+        }
         return new LimitRecordIterator<>(iterator);
     }
 
@@ -114,4 +119,25 @@ public final class LimitRecordReader<T> implements 
RecordReader<T> {
             return iterator.returnedRowId();
         }
     }
+
+    private class LimitFileRecordIterator<T> extends LimitRecordIterator<T>
+            implements FileRecordIterator<T> {
+
+        private final FileRecordIterator<T> iterator;
+
+        private LimitFileRecordIterator(FileRecordIterator<T> iterator) {
+            super(iterator);
+            this.iterator = iterator;
+        }
+
+        @Override
+        public long returnedPosition() {
+            return iterator.returnedPosition();
+        }
+
+        @Override
+        public Path filePath() {
+            return iterator.filePath();
+        }
+    }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/reader/LimitRecordReaderTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/reader/LimitRecordReaderTest.java
new file mode 100644
index 0000000000..6e5224d98d
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/reader/LimitRecordReaderTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.reader;
+
+import org.apache.paimon.fs.Path;
+
+import org.junit.jupiter.api.Test;
+
+import javax.annotation.Nullable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link LimitRecordReader}. */
+public class LimitRecordReaderTest {
+
+    @Test
+    public void testPreservesFileRecordIterator() throws Exception {
+        FileRecordIterator<Integer> fileIterator =
+                new FileRecordIterator<Integer>() {
+                    private int position = -1;
+
+                    @Override
+                    public long returnedPosition() {
+                        return position;
+                    }
+
+                    @Override
+                    public Path filePath() {
+                        return new Path("test-file.parquet");
+                    }
+
+                    @Nullable
+                    @Override
+                    public Integer next() {
+                        position++;
+                        return position < 3 ? position : null;
+                    }
+
+                    @Override
+                    public void releaseBatch() {}
+                };
+
+        FileRecordReader<Integer> fileReader =
+                new FileRecordReader<Integer>() {
+                    private boolean batchReturned;
+
+                    @Nullable
+                    @Override
+                    public FileRecordIterator<Integer> readBatch() {
+                        if (batchReturned) {
+                            return null;
+                        }
+                        batchReturned = true;
+                        return fileIterator;
+                    }
+
+                    @Override
+                    public void close() {}
+                };
+
+        try (RecordReader<Integer> reader = 
LimitRecordReader.limit(fileReader, 2)) {
+            RecordReader.RecordIterator<Integer> batch = reader.readBatch();
+            assertThat(batch).isInstanceOf(FileRecordIterator.class);
+
+            FileRecordIterator<?> limited = (FileRecordIterator<?>) batch;
+            assertThat(limited.filePath()).isEqualTo(new 
Path("test-file.parquet"));
+            assertThat(limited.next()).isEqualTo(0);
+            assertThat(limited.returnedPosition()).isEqualTo(0);
+            assertThat(limited.next()).isEqualTo(1);
+            assertThat(limited.returnedPosition()).isEqualTo(1);
+            assertThat(limited.next()).isNull();
+            limited.releaseBatch();
+
+            assertThat(reader.readBatch()).isNull();
+        }
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/io/FileIndexEvaluator.java 
b/paimon-core/src/main/java/org/apache/paimon/io/FileIndexEvaluator.java
index 1a78622717..a82194cc76 100644
--- a/paimon-core/src/main/java/org/apache/paimon/io/FileIndexEvaluator.java
+++ b/paimon-core/src/main/java/org/apache/paimon/io/FileIndexEvaluator.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.io;
 
+import org.apache.paimon.deletionvectors.Bitmap64DeletionVector;
 import org.apache.paimon.deletionvectors.BitmapDeletionVector;
 import org.apache.paimon.deletionvectors.DeletionVector;
 import org.apache.paimon.fileindex.FileIndexPredicate;
@@ -52,6 +53,12 @@ public class FileIndexEvaluator {
             DataFileMeta file,
             @Nullable DeletionVector dv)
             throws IOException {
+        // File index selections use 32-bit positions. Fall back when they 
cannot safely represent
+        // the file or its deletion vector.
+        if (file.rowCount() > RoaringBitmap32.MAX_VALUE || dv instanceof 
Bitmap64DeletionVector) {
+            return FileIndexResult.REMAIN;
+        }
+
         if (isNullOrEmpty(dataFilter) && topN == null) {
             if (limit == null) {
                 return FileIndexResult.REMAIN;
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java
index c66b0f2c2c..f5fe777b29 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java
@@ -41,6 +41,7 @@ import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.TopN;
 import org.apache.paimon.reader.EmptyFileRecordReader;
 import org.apache.paimon.reader.FileRecordReader;
+import org.apache.paimon.reader.LimitRecordReader;
 import org.apache.paimon.reader.ReadBatchSizer;
 import org.apache.paimon.reader.ReaderSupplier;
 import org.apache.paimon.reader.RecordReader;
@@ -221,7 +222,12 @@ public class RawFileSplitRead implements 
SplitRead<InternalRow> {
                             null));
         }
 
-        return ConcatRecordReader.create(suppliers);
+        RecordReader<InternalRow> reader = 
ConcatRecordReader.create(suppliers);
+        // Apply the final limit after deletion vectors when no later 
predicate can drop rows.
+        if (topN == null && (filters == null || filters.isEmpty())) {
+            return LimitRecordReader.limit(reader, limit);
+        }
+        return reader;
     }
 
     FileRecordReader<InternalRow> createFileReader(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/io/FileIndexEvaluatorTest.java 
b/paimon-core/src/test/java/org/apache/paimon/io/FileIndexEvaluatorTest.java
index 5b91c64813..891ae8380a 100644
--- a/paimon-core/src/test/java/org/apache/paimon/io/FileIndexEvaluatorTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/io/FileIndexEvaluatorTest.java
@@ -18,12 +18,14 @@
 
 package org.apache.paimon.io;
 
+import org.apache.paimon.deletionvectors.Bitmap64DeletionVector;
 import org.apache.paimon.deletionvectors.BitmapDeletionVector;
 import org.apache.paimon.fileindex.FileIndexFormat;
 import org.apache.paimon.fileindex.FileIndexResult;
 import org.apache.paimon.fileindex.FileIndexWriter;
 import org.apache.paimon.fileindex.bitmap.BitmapFileIndex;
 import org.apache.paimon.fileindex.bitmap.BitmapFileIndexFactory;
+import org.apache.paimon.fileindex.bitmap.BitmapIndexResult;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.schema.TableSchema;
@@ -32,6 +34,7 @@ import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.RoaringBitmap32;
 
 import org.junit.jupiter.api.Test;
 
@@ -46,6 +49,64 @@ import static org.assertj.core.api.Assertions.assertThat;
 /** Tests for {@link FileIndexEvaluator}. */
 public class FileIndexEvaluatorTest {
 
+    @Test
+    public void testLimitIntersectsBitmapDeletionVector() throws Exception {
+        BitmapDeletionVector deletionVector = new BitmapDeletionVector();
+        for (int position = 0; position < 5; position++) {
+            deletionVector.delete(position);
+        }
+
+        FileIndexResult result =
+                FileIndexEvaluator.evaluate(
+                        null,
+                        null,
+                        Collections.emptyList(),
+                        null,
+                        10,
+                        null,
+                        DataFileTestUtils.newFile("data.avro", 0, 0, 19, 0L),
+                        deletionVector);
+
+        assertThat(result).isInstanceOf(BitmapIndexResult.class);
+        assertThat(((BitmapIndexResult) result).get())
+                .isEqualTo(RoaringBitmap32.bitmapOfRange(5, 15));
+    }
+
+    @Test
+    public void testBitmap64DeletionVectorFallsBack() throws Exception {
+        Bitmap64DeletionVector deletionVector = new Bitmap64DeletionVector();
+        deletionVector.delete(0);
+
+        FileIndexResult result =
+                FileIndexEvaluator.evaluate(
+                        null,
+                        null,
+                        Collections.emptyList(),
+                        null,
+                        10,
+                        null,
+                        DataFileTestUtils.newFile("data.avro", 0, 0, 19, 0L),
+                        deletionVector);
+
+        assertThat(result).isSameAs(FileIndexResult.REMAIN);
+    }
+
+    @Test
+    public void testLargeFileFallsBack() throws Exception {
+        FileIndexResult result =
+                FileIndexEvaluator.evaluate(
+                        null,
+                        null,
+                        Collections.emptyList(),
+                        null,
+                        10,
+                        null,
+                        fileWithRowCount(Integer.MAX_VALUE + 2L),
+                        null);
+
+        assertThat(result).isSameAs(FileIndexResult.REMAIN);
+    }
+
     @Test
     public void testDataFilterIntersectsDeletionVector() throws Exception {
         BitmapDeletionVector deletionVector = new BitmapDeletionVector();
@@ -95,6 +156,24 @@ public class FileIndexEvaluatorTest {
                 null);
     }
 
+    private static DataFileMeta fileWithRowCount(long rowCount) {
+        return DataFileMeta.forAppend(
+                "data.avro",
+                0,
+                rowCount,
+                SimpleStats.EMPTY_STATS,
+                0,
+                0,
+                0,
+                Collections.emptyList(),
+                null,
+                null,
+                null,
+                null,
+                null,
+                null);
+    }
+
     private static byte[] embeddedBitmapIndex() throws IOException {
         BitmapFileIndex bitmapFileIndex = new BitmapFileIndex(DataTypes.INT(), 
new Options());
         FileIndexWriter indexWriter = bitmapFileIndex.createWriter();
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/RawFileSplitReadTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/RawFileSplitReadTest.java
index cc67dd32ed..c1e12d61d3 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/RawFileSplitReadTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/RawFileSplitReadTest.java
@@ -18,10 +18,13 @@
 
 package org.apache.paimon.operation;
 
+import org.apache.paimon.AppendOnlyFileStore;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.deletionvectors.Bitmap64DeletionVector;
+import org.apache.paimon.deletionvectors.DeletionVector;
 import org.apache.paimon.format.FileFormat;
 import org.apache.paimon.format.FlushingFileFormat;
 import org.apache.paimon.format.FormatReaderFactory;
@@ -44,11 +47,15 @@ import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.InnerTableRead;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.IOExceptionSupplier;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -134,6 +141,39 @@ class RawFileSplitReadTest {
         }
     }
 
+    @Test
+    void testLimitAfterBitmap64DeletionVector() throws Exception {
+        List<InternalRow> rows = new ArrayList<>();
+        for (int i = 0; i < 20; i++) {
+            rows.add(GenericRow.of(BinaryString.fromString("value-" + i), i));
+        }
+        FileStoreTable table = createTable("bitmap64-limit", rows);
+        DataSplit split = singleSplit(table);
+        assertThat(split.dataFiles()).hasSize(1);
+
+        DeletionVector deletionVector = new Bitmap64DeletionVector();
+        for (int position = 0; position < 5; position++) {
+            deletionVector.delete(position);
+        }
+        String fileName = split.dataFiles().get(0).fileName();
+        Map<String, IOExceptionSupplier<DeletionVector>> 
deletionVectorFactories =
+                Collections.singletonMap(fileName, () -> deletionVector);
+
+        RawFileSplitRead read = ((AppendOnlyFileStore) 
table.store()).newRead();
+        read.withLimit(10);
+        AtomicInteger count = new AtomicInteger();
+        try (RecordReader<InternalRow> reader =
+                read.createReader(
+                        split.partition(),
+                        split.bucket(),
+                        split.dataFiles(),
+                        deletionVectorFactories)) {
+            reader.forEachRemaining(ignored -> count.incrementAndGet());
+        }
+
+        assertThat(count).hasValue(10);
+    }
+
     @Test
     void testTableReadSharesBatchSizer() throws Exception {
         FileStoreTable table = createTable("dynamic-batch-size", 20);
@@ -164,10 +204,21 @@ class RawFileSplitReadTest {
     }
 
     private FileStoreTable createTable(String directory) throws Exception {
-        return createTable(directory, 1);
+        return createTable(
+                directory,
+                
Collections.singletonList(GenericRow.of(BinaryString.fromString("value"), 42)));
     }
 
     private FileStoreTable createTable(String directory, int rowCount) throws 
Exception {
+        List<InternalRow> rows = new ArrayList<>();
+        for (int i = 0; i < rowCount; i++) {
+            rows.add(GenericRow.of(BinaryString.fromString("value"), i + 42));
+        }
+        return createTable(directory, rows);
+    }
+
+    private FileStoreTable createTable(String directory, List<? extends 
InternalRow> rows)
+            throws Exception {
         Path tablePath = new Path(tempDir.resolve(directory).toUri());
         Options options = new Options();
         options.set(CoreOptions.PATH, tablePath.toString());
@@ -188,8 +239,8 @@ class RawFileSplitReadTest {
         BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
         try (BatchTableWrite write = writeBuilder.newWrite();
                 BatchTableCommit commit = writeBuilder.newCommit()) {
-            for (int i = 0; i < rowCount; i++) {
-                write.write(GenericRow.of(BinaryString.fromString("value"), i 
+ 42));
+            for (InternalRow row : rows) {
+                write.write(row);
             }
             commit.commit(write.prepareCommit());
         }

Reply via email to