1919package org .apache .paimon .operation ;
2020
2121import org .apache .paimon .CoreOptions ;
22+ import org .apache .paimon .CoreOptions .MergeEngine ;
2223import org .apache .paimon .KeyValue ;
2324import org .apache .paimon .KeyValueFileStore ;
2425import org .apache .paimon .data .BinaryRow ;
4142import org .apache .paimon .mergetree .compact .MergeFunctionWrapper ;
4243import org .apache .paimon .mergetree .compact .ReducerMergeFunctionWrapper ;
4344import org .apache .paimon .predicate .Predicate ;
45+ import org .apache .paimon .reader .LimitRecordReader ;
4446import org .apache .paimon .reader .ReaderSupplier ;
4547import org .apache .paimon .reader .RecordReader ;
4648import org .apache .paimon .schema .TableSchema ;
5456import org .apache .paimon .utils .ProjectedRow ;
5557import org .apache .paimon .utils .UserDefinedSeqComparator ;
5658
59+ import org .slf4j .Logger ;
60+ import org .slf4j .LoggerFactory ;
61+
5762import javax .annotation .Nullable ;
5863
5964import java .io .IOException ;
7479 */
7580public class MergeFileSplitRead implements SplitRead <KeyValue > {
7681
82+ private static final Logger LOG = LoggerFactory .getLogger (MergeFileSplitRead .class );
83+
7784 private final TableSchema tableSchema ;
7885 private final FileIO fileIO ;
7986 private final KeyValueFileReaderFactory .Builder readerFactoryBuilder ;
@@ -82,14 +89,19 @@ public class MergeFileSplitRead implements SplitRead<KeyValue> {
8289 private final MergeSorter mergeSorter ;
8390 private final List <String > sequenceFields ;
8491 private final boolean sequenceOrder ;
92+ private final CoreOptions coreOptions ;
8593
8694 @ Nullable private RowType readKeyType ;
8795 @ Nullable private RowType outerReadType ;
8896
8997 @ Nullable private List <Predicate > filtersForKeys ;
9098 @ Nullable private List <Predicate > filtersForAll ;
9199
100+ @ Nullable private Integer limit ;
101+
92102 private boolean forceKeepDelete = false ;
103+ private boolean mergeReadLimitLogEmitted = false ;
104+ private boolean applyMergeReadLimit = true ;
93105
94106 public MergeFileSplitRead (
95107 CoreOptions options ,
@@ -100,6 +112,7 @@ public MergeFileSplitRead(
100112 MergeFunctionFactory <KeyValue > mfFactory ,
101113 KeyValueFileReaderFactory .Builder readerFactoryBuilder ) {
102114 this .tableSchema = schema ;
115+ this .coreOptions = options ;
103116 this .readerFactoryBuilder = readerFactoryBuilder ;
104117 this .fileIO = readerFactoryBuilder .fileIO ();
105118 this .keyComparator = keyComparator ;
@@ -177,6 +190,26 @@ public MergeFileSplitRead forceKeepDelete() {
177190 return this ;
178191 }
179192
193+ @ Override
194+ public MergeFileSplitRead withLimit (@ Nullable Integer limit ) {
195+ this .limit = limit ;
196+ return this ;
197+ }
198+
199+ @ Override
200+ public MergeFileSplitRead withApplyMergeReadLimit (boolean applyMergeReadLimit ) {
201+ this .applyMergeReadLimit = applyMergeReadLimit ;
202+ return this ;
203+ }
204+
205+ @ Override
206+ public boolean isMergeReadLimitActive () {
207+ if (!applyMergeReadLimit || limit == null || limit <= 0 ) {
208+ return false ;
209+ }
210+ return mergeReadLimitDisabledReason () == null ;
211+ }
212+
180213 @ Override
181214 public MergeFileSplitRead withFilter (Predicate predicate ) {
182215 if (predicate == null ) {
@@ -312,6 +345,7 @@ public RecordReader<KeyValue> createMergeReader(
312345 reader = new DropDeleteReader (reader );
313346 }
314347
348+ reader = LimitRecordReader .limit (reader , effectiveReadLimit ());
315349 return projectOuter (projectKey (reader ));
316350 }
317351
@@ -334,7 +368,92 @@ public RecordReader<KeyValue> createNoMergeReader(
334368 suppliers .add (() -> readerFactory .createRecordReader (file ));
335369 }
336370
337- return projectOuter (ConcatRecordReader .create (suppliers ));
371+ return LimitRecordReader .limit (
372+ projectOuter (ConcatRecordReader .create (suppliers )), effectiveReadLimit ());
373+ }
374+
375+ /**
376+ * Limit on merge read is only safe when filters are fully applied before truncation. This
377+ * aligns with {@link KeyValueFileStoreScan#limitPushdownEnabled()} and additionally rejects
378+ * non-primary-key filters, which are not pushed down to overlapping L0 sections (see {@link
379+ * #withFilter(Predicate)}).
380+ */
381+ @ Nullable
382+ private Integer effectiveReadLimit () {
383+ if (!applyMergeReadLimit ) {
384+ return null ;
385+ }
386+ if (limit == null || limit <= 0 ) {
387+ return null ;
388+ }
389+
390+ String disabledReason = mergeReadLimitDisabledReason ();
391+ if (disabledReason != null ) {
392+ logMergeReadLimitOnce (disabledReason );
393+ return null ;
394+ }
395+
396+ logMergeReadLimitOnce (null );
397+ return limit ;
398+ }
399+
400+ /** Returns why merge read limit is unsafe, or {@code null} if limit can be applied. */
401+ @ Nullable
402+ private String mergeReadLimitDisabledReason () {
403+ if (forceKeepDelete ) {
404+ return "forceKeepDelete is enabled" ;
405+ }
406+
407+ MergeEngine mergeEngine = coreOptions .mergeEngine ();
408+ if (mergeEngine == MergeEngine .PARTIAL_UPDATE ) {
409+ return "merge-engine is partial-update" ;
410+ }
411+ if (mergeEngine == MergeEngine .AGGREGATE ) {
412+ return "merge-engine is aggregation" ;
413+ }
414+
415+ if (coreOptions .deletionVectorsEnabled ()) {
416+ return "deletion-vectors is enabled" ;
417+ }
418+
419+ if (hasNonPrimaryKeyFilter ()) {
420+ return "non-primary-key filter is present" ;
421+ }
422+
423+ return null ;
424+ }
425+
426+ private void logMergeReadLimitOnce (@ Nullable String disabledReason ) {
427+ if (mergeReadLimitLogEmitted ) {
428+ return ;
429+ }
430+ mergeReadLimitLogEmitted = true ;
431+ if (disabledReason != null ) {
432+ LOG .info (
433+ "Merge read limit {} is disabled: {}. Limit will not be applied during merge read." ,
434+ limit ,
435+ disabledReason );
436+ } else {
437+ LOG .info ("Applying merge read limit {} during merge read." , limit );
438+ }
439+ }
440+
441+ private boolean hasNonPrimaryKeyFilter () {
442+ if (filtersForAll == null || filtersForAll .isEmpty ()) {
443+ return false ;
444+ }
445+
446+ List <String > primaryKeys = tableSchema .trimmedPrimaryKeys ();
447+ Set <String > nonPrimaryKeys =
448+ tableSchema .fieldNames ().stream ()
449+ .filter (name -> !primaryKeys .contains (name ))
450+ .collect (Collectors .toSet ());
451+ for (Predicate filter : filtersForAll ) {
452+ if (containsFields (filter , nonPrimaryKeys )) {
453+ return true ;
454+ }
455+ }
456+ return false ;
338457 }
339458
340459 /**
0 commit comments