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 f005439ecb [core] Avoid unsafe LIMIT split pruning for format tables 
(#8703)
f005439ecb is described below

commit f005439ecb92e921f0790c4781de48ed7f75e9e2
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Fri Jul 17 14:44:16 2026 +0800

    [core] Avoid unsafe LIMIT split pruning for format tables (#8703)
    
    Format table scan planning currently truncates planned splits using a
    pushed row `LIMIT`. This is unsafe because `FormatDataSplit` has no
    row-count information, and an earlier split may produce no rows while a
    later split contains valid data.
    
    This change keeps the zero-limit fast path but retains all splits for
    positive limits. Reader-side limiting still stops reading after enough
    rows are returned. Positive limits may expose more splits to connectors,
    which is necessary to avoid skipping valid rows. This is the
    format-table counterpart of #7665.
---
 .../paimon/table/format/FormatTableScan.java       | 10 ++----
 .../paimon/table/format/FormatTableScanTest.java   | 37 ++++++++++++++++++++++
 2 files changed, 40 insertions(+), 7 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java 
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
index a75e4d92d0..cb0edefc82 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableScan.java
@@ -166,13 +166,9 @@ public class FormatTableScan implements InnerTableScan {
                 } else {
                     splits.addAll(createSplits(fileIO, new 
Path(table.location()), null));
                 }
-                if (limit != null) {
-                    if (limit <= 0) {
-                        return new ArrayList<>();
-                    }
-                    if (splits.size() > limit) {
-                        return splits.subList(0, limit);
-                    }
+                // Keep all splits for a positive limit because 
FormatDataSplit has no row count.
+                if (limit != null && limit <= 0) {
+                    return new ArrayList<>();
                 }
             } catch (IOException e) {
                 throw new RuntimeException("Failed to scan files", e);
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableScanTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableScanTest.java
index 18f22c8112..3a93233f89 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableScanTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/format/FormatTableScanTest.java
@@ -22,6 +22,7 @@ import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.format.csv.CsvOptions;
 import org.apache.paimon.format.json.JsonOptions;
@@ -31,8 +32,11 @@ import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.partition.PartitionPredicate;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.table.FormatTable;
+import org.apache.paimon.table.source.ReadBuilder;
 import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableScan;
 import 
org.apache.paimon.testutils.junit.parameterized.ParameterizedTestExtension;
 import org.apache.paimon.testutils.junit.parameterized.Parameters;
 import org.apache.paimon.types.DataTypes;
@@ -45,6 +49,7 @@ import org.junit.jupiter.api.io.TempDir;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -54,7 +59,9 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.apache.paimon.CoreOptions.FILE_FORMAT;
 import static 
org.apache.paimon.CoreOptions.FORMAT_TABLE_PARTITION_ONLY_VALUE_IN_PATH;
+import static org.apache.paimon.CoreOptions.SOURCE_SPLIT_OPEN_FILE_COST;
 import static org.apache.paimon.CoreOptions.SOURCE_SPLIT_TARGET_SIZE;
 import static 
org.apache.paimon.utils.PartitionPathUtils.searchPartSpecAndPaths;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -738,6 +745,36 @@ public class FormatTableScanTest {
         }
     }
 
+    @TestTemplate
+    void testPositiveLimitDoesNotPruneUnknownRowCountSplits() throws 
IOException {
+        Path tableLocation = new Path(tmpPath.toUri());
+        LocalFileIO fileIO = LocalFileIO.create();
+        Path headerOnlyFile = new Path(tableLocation, "00-header-only.csv");
+        Path dataFile = new Path(tableLocation, "01-data.csv");
+        fileIO.mkdirs(tableLocation);
+        fileIO.writeFile(headerOnlyFile, "id,name\n", false);
+        fileIO.writeFile(dataFile, "id,name\n42,later\n", false);
+
+        Map<String, String> options = new HashMap<>();
+        options.put(FILE_FORMAT.key(), "csv");
+        options.put(CsvOptions.INCLUDE_HEADER.key(), "true");
+        options.put(SOURCE_SPLIT_TARGET_SIZE.key(), "32b");
+        options.put(SOURCE_SPLIT_OPEN_FILE_COST.key(), "32b");
+        FormatTable formatTable =
+                createFormatTableWithOptions(tableLocation, 
FormatTable.Format.CSV, options);
+        
assertThat(formatTable.newReadBuilder().newScan().plan().splits()).hasSize(2);
+
+        ReadBuilder readBuilder = formatTable.newReadBuilder().withLimit(1);
+        TableScan.Plan plan = readBuilder.newScan().plan();
+        List<String> rows = new ArrayList<>();
+        try (RecordReader<InternalRow> reader = 
readBuilder.newRead().createReader(plan)) {
+            reader.forEachRemaining(
+                    row -> rows.add(row.getInt(0) + "," + 
row.getString(1).toString()));
+        }
+
+        assertThat(rows).containsExactly("42,later");
+    }
+
     @TestTemplate
     public void testCreateSplitsWhenDefineLineDelimiter() throws IOException {
         for (String format : Arrays.asList("csv", "json")) {

Reply via email to