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 11e899f000 [core] Fix ApplyBitmapIndexRecordReader to stop readBatch
after bitmap selection is exhausted. (#7994)
11e899f000 is described below
commit 11e899f000e8b4d2b16ed1c378442355c497d4a4
Author: wangwj <[email protected]>
AuthorDate: Sun May 31 21:29:31 2026 +0800
[core] Fix ApplyBitmapIndexRecordReader to stop readBatch after bitmap
selection is exhausted. (#7994)
---
.../bitmap/ApplyBitmapIndexFileRecordIterator.java | 13 +-
.../bitmap/ApplyBitmapIndexRecordReader.java | 9 +-
.../bitmap/ApplyBitmapIndexRecordReaderTest.java | 255 +++++++++++++++++++++
.../paimon/table/AppendOnlySimpleTableTest.java | 38 +++
.../apache/paimon/flink/BatchFileStoreITCase.java | 32 +++
5 files changed, 344 insertions(+), 3 deletions(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexFileRecordIterator.java
b/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexFileRecordIterator.java
index eec931d3e9..240d92fb5f 100644
---
a/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexFileRecordIterator.java
+++
b/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexFileRecordIterator.java
@@ -26,6 +26,7 @@ import org.apache.paimon.utils.RoaringBitmap32;
import javax.annotation.Nullable;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* A {@link FileRecordIterator} wraps a {@link FileRecordIterator} and {@link
BitmapIndexResult}.
@@ -35,12 +36,16 @@ public class ApplyBitmapIndexFileRecordIterator implements
FileRecordIterator<In
private final FileRecordIterator<InternalRow> iterator;
private final RoaringBitmap32 bitmap;
private final int last;
+ private final AtomicBoolean exhausted;
- public ApplyBitmapIndexFileRecordIterator(
- FileRecordIterator<InternalRow> iterator, BitmapIndexResult
fileIndexResult) {
+ ApplyBitmapIndexFileRecordIterator(
+ FileRecordIterator<InternalRow> iterator,
+ BitmapIndexResult fileIndexResult,
+ AtomicBoolean exhausted) {
this.iterator = iterator;
this.bitmap = fileIndexResult.get();
this.last = bitmap.last();
+ this.exhausted = exhausted;
}
@Override
@@ -63,9 +68,13 @@ public class ApplyBitmapIndexFileRecordIterator implements
FileRecordIterator<In
}
int position = (int) returnedPosition();
if (position > last) {
+ exhausted.set(true);
return null;
}
if (bitmap.contains(position)) {
+ if (position >= last) {
+ exhausted.set(true);
+ }
return next;
}
}
diff --git
a/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReader.java
b/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReader.java
index 3b1207c8bd..9b17bcc3cc 100644
---
a/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReader.java
+++
b/paimon-common/src/main/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReader.java
@@ -26,6 +26,7 @@ import org.apache.paimon.reader.RecordReader;
import javax.annotation.Nullable;
import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
/** A {@link RecordReader} which apply {@link BitmapIndexResult} to filter
record. */
public class ApplyBitmapIndexRecordReader implements
FileRecordReader<InternalRow> {
@@ -34,6 +35,8 @@ public class ApplyBitmapIndexRecordReader implements
FileRecordReader<InternalRo
private final BitmapIndexResult fileIndexResult;
+ private final AtomicBoolean exhausted = new AtomicBoolean(false);
+
public ApplyBitmapIndexRecordReader(
FileRecordReader<InternalRow> reader, BitmapIndexResult
fileIndexResult) {
this.reader = reader;
@@ -43,12 +46,16 @@ public class ApplyBitmapIndexRecordReader implements
FileRecordReader<InternalRo
@Nullable
@Override
public FileRecordIterator<InternalRow> readBatch() throws IOException {
+ if (exhausted.get()) {
+ return null;
+ }
+
FileRecordIterator<InternalRow> batch = reader.readBatch();
if (batch == null) {
return null;
}
- return new ApplyBitmapIndexFileRecordIterator(batch, fileIndexResult);
+ return new ApplyBitmapIndexFileRecordIterator(batch, fileIndexResult,
exhausted);
}
@Override
diff --git
a/paimon-common/src/test/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReaderTest.java
b/paimon-common/src/test/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReaderTest.java
new file mode 100644
index 0000000000..2fbdc2709c
--- /dev/null
+++
b/paimon-common/src/test/java/org/apache/paimon/fileindex/bitmap/ApplyBitmapIndexRecordReaderTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.fileindex.bitmap;
+
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.reader.FileRecordIterator;
+import org.apache.paimon.reader.FileRecordReader;
+import org.apache.paimon.utils.CloseableIterator;
+import org.apache.paimon.utils.RoaringBitmap32;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link ApplyBitmapIndexRecordReader}. */
+public class ApplyBitmapIndexRecordReaderTest {
+
+ private static final Path DUMMY_PATH = new Path("/dummy");
+
+ @Test
+ public void testLimitStopsAfterFirstBatchWithPreSelectedUnderlying()
throws Exception {
+ // Simulates DataFileRecordReader#readBatchInternal:
iterator.selection(selection)
+ int limit = 10;
+ int batchSize = 20;
+ int totalRows = 100;
+ RoaringBitmap32 limitBitmap = RoaringBitmap32.bitmapOfRange(0,
totalRows).limit(limit);
+ BitmapIndexResult selection = new BitmapIndexResult(() -> limitBitmap);
+ PreSelectedCountingFileRecordReader underlying =
+ new PreSelectedCountingFileRecordReader(totalRows, batchSize,
limitBitmap);
+ ApplyBitmapIndexRecordReader reader =
+ new ApplyBitmapIndexRecordReader(underlying, selection);
+
+ List<Integer> rows = readViaCloseableIterator(reader);
+
+ assertThat(rows).containsExactlyElementsOf(range(limit));
+ assertThat(underlying.readBatchCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testSparseBitmapWithPreSelectedUnderlying() throws Exception {
+ int batchSize = 5;
+ int totalRows = 20;
+ RoaringBitmap32 sparseBitmap = RoaringBitmap32.bitmapOf(0, 2, 4, 6, 8);
+ BitmapIndexResult selection = new BitmapIndexResult(() ->
sparseBitmap);
+ PreSelectedCountingFileRecordReader underlying =
+ new PreSelectedCountingFileRecordReader(totalRows, batchSize,
sparseBitmap);
+ ApplyBitmapIndexRecordReader reader =
+ new ApplyBitmapIndexRecordReader(underlying, selection);
+
+ List<Integer> rows = readViaCloseableIterator(reader);
+
+ assertThat(rows).containsExactly(0, 2, 4, 6, 8);
+ assertThat(underlying.readBatchCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void testLimitStopsAfterFirstBatch() throws Exception {
+ int limit = 10;
+ int batchSize = 20;
+ int totalRows = 100;
+ CountingFileRecordReader underlying = new
CountingFileRecordReader(totalRows, batchSize);
+ BitmapIndexResult selection =
+ new BitmapIndexResult(
+ () -> RoaringBitmap32.bitmapOfRange(0,
totalRows).limit(limit));
+ ApplyBitmapIndexRecordReader reader =
+ new ApplyBitmapIndexRecordReader(underlying, selection);
+
+ List<Integer> rows = readViaCloseableIterator(reader);
+
+ assertThat(rows).containsExactlyElementsOf(range(limit));
+ assertThat(underlying.readBatchCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testForEachRemainingStopsAfterLimit() throws Exception {
+ int limit = 10;
+ int batchSize = 20;
+ int totalRows = 100;
+ CountingFileRecordReader underlying = new
CountingFileRecordReader(totalRows, batchSize);
+ BitmapIndexResult selection =
+ new BitmapIndexResult(
+ () -> RoaringBitmap32.bitmapOfRange(0,
totalRows).limit(limit));
+ ApplyBitmapIndexRecordReader reader =
+ new ApplyBitmapIndexRecordReader(underlying, selection);
+
+ List<Integer> rows = new ArrayList<>();
+ reader.forEachRemaining(row -> rows.add(row.getInt(0)));
+
+ assertThat(rows).containsExactlyElementsOf(range(limit));
+ assertThat(underlying.readBatchCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testSparseBitmapStillStopsAtLastPosition() throws Exception {
+ int batchSize = 5;
+ int totalRows = 20;
+ CountingFileRecordReader underlying = new
CountingFileRecordReader(totalRows, batchSize);
+ BitmapIndexResult selection =
+ new BitmapIndexResult(() -> RoaringBitmap32.bitmapOf(0, 2, 4,
6, 8));
+ ApplyBitmapIndexRecordReader reader =
+ new ApplyBitmapIndexRecordReader(underlying, selection);
+
+ List<Integer> rows = readViaCloseableIterator(reader);
+
+ assertThat(rows).containsExactly(0, 2, 4, 6, 8);
+ assertThat(underlying.readBatchCount()).isEqualTo(2);
+ }
+
+ private static List<Integer>
readViaCloseableIterator(ApplyBitmapIndexRecordReader reader)
+ throws Exception {
+ List<Integer> rows = new ArrayList<>();
+ try (CloseableIterator<InternalRow> iterator =
reader.toCloseableIterator()) {
+ while (iterator.hasNext()) {
+ rows.add(iterator.next().getInt(0));
+ }
+ }
+ return rows;
+ }
+
+ private static List<Integer> range(int endExclusive) {
+ List<Integer> result = new ArrayList<>(endExclusive);
+ for (int i = 0; i < endExclusive; i++) {
+ result.add(i);
+ }
+ return result;
+ }
+
+ /**
+ * Simulates {@code DataFileRecordReader#readBatchInternal}, which applies
{@code
+ * iterator.selection(selection)} before wrapping with {@link
ApplyBitmapIndexRecordReader}.
+ */
+ private static class PreSelectedCountingFileRecordReader
+ implements FileRecordReader<InternalRow> {
+
+ private final int totalRows;
+ private final int batchSize;
+ private final RoaringBitmap32 selectionBitmap;
+ private final AtomicInteger readBatchCount = new AtomicInteger(0);
+ private int nextBatchStart;
+
+ private PreSelectedCountingFileRecordReader(
+ int totalRows, int batchSize, RoaringBitmap32 selectionBitmap)
{
+ this.totalRows = totalRows;
+ this.batchSize = batchSize;
+ this.selectionBitmap = selectionBitmap;
+ }
+
+ int readBatchCount() {
+ return readBatchCount.get();
+ }
+
+ @Override
+ public FileRecordIterator<InternalRow> readBatch() {
+ readBatchCount.incrementAndGet();
+ if (nextBatchStart >= totalRows) {
+ return null;
+ }
+ int batchStart = nextBatchStart;
+ int batchEnd = Math.min(nextBatchStart + batchSize, totalRows);
+ nextBatchStart = batchEnd;
+ return new PositionFileRecordIterator(batchStart,
batchEnd).selection(selectionBitmap);
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ private static class CountingFileRecordReader implements
FileRecordReader<InternalRow> {
+
+ private final int totalRows;
+ private final int batchSize;
+ private final AtomicInteger readBatchCount = new AtomicInteger(0);
+ private int nextBatchStart;
+
+ private CountingFileRecordReader(int totalRows, int batchSize) {
+ this.totalRows = totalRows;
+ this.batchSize = batchSize;
+ }
+
+ int readBatchCount() {
+ return readBatchCount.get();
+ }
+
+ @Override
+ public FileRecordIterator<InternalRow> readBatch() {
+ readBatchCount.incrementAndGet();
+ if (nextBatchStart >= totalRows) {
+ return null;
+ }
+ int batchStart = nextBatchStart;
+ int batchEnd = Math.min(nextBatchStart + batchSize, totalRows);
+ nextBatchStart = batchEnd;
+ return new PositionFileRecordIterator(batchStart, batchEnd);
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ private static class PositionFileRecordIterator implements
FileRecordIterator<InternalRow> {
+
+ private final int end;
+ private int nextPosition;
+ private int returnedPosition = -1;
+
+ private PositionFileRecordIterator(int start, int end) {
+ this.nextPosition = start;
+ this.end = end;
+ }
+
+ @Override
+ public InternalRow next() {
+ if (nextPosition >= end) {
+ return null;
+ }
+ returnedPosition = nextPosition;
+ return GenericRow.of(nextPosition++);
+ }
+
+ @Override
+ public long returnedPosition() {
+ return returnedPosition;
+ }
+
+ @Override
+ public Path filePath() {
+ return DUMMY_PATH;
+ }
+
+ @Override
+ public void releaseBatch() {}
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/AppendOnlySimpleTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/AppendOnlySimpleTableTest.java
index e1bd365b23..1ffd4669d4 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/AppendOnlySimpleTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/AppendOnlySimpleTableTest.java
@@ -77,6 +77,7 @@ import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.BranchMergeHandler;
+import org.apache.paimon.utils.CloseableIterator;
import org.apache.paimon.utils.RoaringBitmap32;
import org.apache.commons.math3.random.RandomDataGenerator;
@@ -1258,6 +1259,43 @@ public class AppendOnlySimpleTableTest extends
SimpleTableTestBase {
Thread.sleep(1_000);
}
+ @Test
+ public void testLimitWithCloseableIterator() throws Exception {
+ RowType rowType = RowType.builder().field("id",
DataTypes.INT()).build();
+ Consumer<Options> configure =
+ options -> {
+ options.set(FILE_FORMAT, FILE_FORMAT_PARQUET);
+ options.set(WRITE_ONLY, true);
+ options.set(SOURCE_SPLIT_TARGET_SIZE,
MemorySize.ofMebiBytes(256));
+ };
+ FileStoreTable table = createUnawareBucketFileStoreTable(rowType,
configure);
+
+ int rowCount = 5000;
+ StreamTableWrite write = table.newWrite(commitUser);
+ StreamTableCommit commit = table.newCommit(commitUser);
+ for (int i = 0; i < rowCount; i++) {
+ write.write(GenericRow.of(i));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+ write.close();
+ commit.close();
+
+ int limit = 10;
+ TableScan.Plan plan = table.newScan().withLimit(limit).plan();
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(limit).createReader(plan.splits());
+ AtomicInteger count = new AtomicInteger(0);
+ try (CloseableIterator<InternalRow> iterator =
reader.toCloseableIterator()) {
+ while (iterator.hasNext()) {
+ iterator.next();
+ count.incrementAndGet();
+ }
+ }
+ assertThat(count.get()).isEqualTo(limit);
+
+ Thread.sleep(1_000);
+ }
+
@Test
public void testWithShardAppendTable() throws Exception {
FileStoreTable table = createFileStoreTable(conf -> conf.set(BUCKET,
-1));
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BatchFileStoreITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BatchFileStoreITCase.java
index 93ef7fbe4d..a3cbc264b9 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BatchFileStoreITCase.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BatchFileStoreITCase.java
@@ -1415,6 +1415,38 @@ public class BatchFileStoreITCase extends
CatalogITCaseBase {
.containsExactlyInAnyOrder(Row.of(1, 11, 111), Row.of(2, 22,
222));
}
+ @Test
+ public void testDedicatedPathLimitTenOnManyRows() {
+ sql("CREATE TABLE limit_many_rows (a INT, b INT, c INT)");
+ StringBuilder insertValues = new StringBuilder();
+ for (int i = 1; i <= 100; i++) {
+ if (i > 1) {
+ insertValues.append(", ");
+ }
+ insertValues.append(String.format("(%d, %d, %d)", i, i * 10, i *
100));
+ }
+ batchSql("INSERT INTO limit_many_rows VALUES " + insertValues);
+
+ List<Row> result =
+ batchSql(
+ "SELECT * FROM limit_many_rows "
+ + "/*+
OPTIONS('scan.dedicated-split-generation'='true') */ "
+ + "LIMIT 10");
+ assertThat(result).hasSize(10);
+ assertThat(result)
+ .containsExactlyInAnyOrder(
+ Row.of(1, 10, 100),
+ Row.of(2, 20, 200),
+ Row.of(3, 30, 300),
+ Row.of(4, 40, 400),
+ Row.of(5, 50, 500),
+ Row.of(6, 60, 600),
+ Row.of(7, 70, 700),
+ Row.of(8, 80, 800),
+ Row.of(9, 90, 900),
+ Row.of(10, 100, 1000));
+ }
+
@Test
public void testBatchReadSourceWithoutSnapshot() {
assertThat(