This is an automated email from the ASF dual-hosted git repository.
fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git
The following commit(s) were added to refs/heads/main by this push:
new 717bee78b [spark] Implement SupportsPushDownLimit DSv2 interface
(#3346)
717bee78b is described below
commit 717bee78b4e5fee7325a06956fbdb683cb65049d
Author: Yang Zhang <[email protected]>
AuthorDate: Mon Jun 1 23:06:30 2026 +0800
[spark] Implement SupportsPushDownLimit DSv2 interface (#3346)
* support pushdown limit
* fix comments
* rebase
---
.../spark/read/FlussAppendPartitionReader.scala | 3 +-
.../org/apache/fluss/spark/read/FlussBatch.scala | 10 ++++--
.../fluss/spark/read/FlussMicroBatchStream.scala | 4 +--
.../fluss/spark/read/FlussPartitionReader.scala | 8 ++++-
.../spark/read/FlussPartitionReaderFactory.scala | 4 +++
.../org/apache/fluss/spark/read/FlussScan.scala | 24 ++++++++++++--
.../apache/fluss/spark/read/FlussScanBuilder.scala | 25 +++++++++++++--
.../spark/read/FlussUpsertPartitionReader.scala | 3 +-
.../spark/read/lake/FlussLakeAppendBatch.scala | 5 ++-
.../read/lake/FlussLakeAppendPartitionReader.scala | 3 +-
.../fluss/spark/read/lake/FlussLakeBatch.scala | 3 +-
.../lake/FlussLakePartitionReaderFactory.scala | 4 +++
.../spark/read/lake/FlussLakeUpsertBatch.scala | 6 ++--
.../read/lake/FlussLakeUpsertPartitionReader.scala | 3 +-
.../apache/fluss/spark/SparkLogTableReadTest.scala | 27 ++++++++++++++++
.../fluss/spark/SparkPrimaryKeyTableReadTest.scala | 18 +++++++++++
.../spark/lake/SparkLakeLogTableReadTest.scala | 37 ++++++++++++++++++++++
.../SparkLakePrimaryKeyTableReadTestBase.scala | 36 +++++++++++++++++++++
.../spark/lake/SparkLakeTableReadTestBase.scala | 8 +++--
19 files changed, 210 insertions(+), 21 deletions(-)
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussAppendPartitionReader.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussAppendPartitionReader.scala
index 2aae08012..354c9b8f2 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussAppendPartitionReader.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussAppendPartitionReader.scala
@@ -28,9 +28,10 @@ class FlussAppendPartitionReader(
tablePath: TablePath,
projection: Array[Int],
pushedPredicate: Option[Predicate],
+ limit: Option[Int],
flussPartition: FlussAppendInputPartition,
flussConfig: Configuration)
- extends FlussPartitionReader(tablePath, flussConfig) {
+ extends FlussPartitionReader(tablePath, flussConfig, limit) {
override protected lazy val projectedRowType: RowType =
rowType.project(projection)
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
index 03e473a82..312df3e85 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
@@ -39,6 +39,7 @@ abstract class FlussBatch(
tablePath: TablePath,
tableInfo: TableInfo,
readSchema: StructType,
+ limit: Option[Int],
flussConfig: Configuration)
extends Batch
with AutoCloseable {
@@ -114,9 +115,10 @@ class FlussAppendBatch(
readSchema: StructType,
pushedPredicate: Option[Predicate],
partitionPredicate: Option[Predicate],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
- extends FlussBatch(tablePath, tableInfo, readSchema, flussConfig) {
+ extends FlussBatch(tablePath, tableInfo, readSchema, limit, flussConfig) {
override val startOffsetsInitializer: OffsetsInitializer = {
FlussOffsetInitializers.startOffsetsInitializer(options, flussConfig)
@@ -202,6 +204,7 @@ class FlussAppendBatch(
tablePath,
projection,
pushedPredicate,
+ limit,
options,
flussConfig)
}
@@ -214,9 +217,10 @@ class FlussUpsertBatch(
tableInfo: TableInfo,
readSchema: StructType,
partitionPredicate: Option[Predicate],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
- extends FlussBatch(tablePath, tableInfo, readSchema, flussConfig) {
+ extends FlussBatch(tablePath, tableInfo, readSchema, limit, flussConfig) {
override val startOffsetsInitializer: OffsetsInitializer = {
val offsetsInitializer =
FlussOffsetInitializers.startOffsetsInitializer(options, flussConfig)
@@ -253,6 +257,6 @@ class FlussUpsertBatch(
}
override def createReaderFactory(): PartitionReaderFactory = {
- new FlussUpsertPartitionReaderFactory(tablePath, projection, options,
flussConfig)
+ new FlussUpsertPartitionReaderFactory(tablePath, projection, limit,
options, flussConfig)
}
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussMicroBatchStream.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussMicroBatchStream.scala
index f17dbd460..f7bf6722e 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussMicroBatchStream.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussMicroBatchStream.scala
@@ -271,7 +271,7 @@ class FlussAppendMicroBatchStream(
checkpointLocation) {
override def createReaderFactory(): PartitionReaderFactory = {
- new FlussAppendPartitionReaderFactory(tablePath, projection, None,
options, flussConfig)
+ new FlussAppendPartitionReaderFactory(tablePath, projection, None, None,
options, flussConfig)
}
override def planInputPartitions(start: Offset, end: Offset):
Array[InputPartition] = {
@@ -352,6 +352,6 @@ class FlussUpsertMicroBatchStream(
}
override def createReaderFactory(): PartitionReaderFactory = {
- new FlussUpsertPartitionReaderFactory(tablePath, projection, options,
flussConfig)
+ new FlussUpsertPartitionReaderFactory(tablePath, projection, None,
options, flussConfig)
}
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReader.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReader.scala
index 5d4330385..be54682a6 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReader.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReader.scala
@@ -34,7 +34,10 @@ import org.apache.spark.sql.connector.read.PartitionReader
import java.time.Duration
-abstract class FlussPartitionReader(tablePath: TablePath, flussConfig:
Configuration)
+abstract class FlussPartitionReader(
+ tablePath: TablePath,
+ flussConfig: Configuration,
+ limit: Option[Int])
extends PartitionReader[InternalRow]
with Logging {
@@ -57,6 +60,9 @@ abstract class FlussPartitionReader(tablePath: TablePath,
flussConfig: Configura
def next0(): Boolean
override def next(): Boolean = {
+ if (limit.exists(numRowsRead >= _)) {
+ return false
+ }
val hasNext = next0()
if (hasNext) {
numRowsRead += 1
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReaderFactory.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReaderFactory.scala
index 2204bda36..13361532e 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReaderFactory.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussPartitionReaderFactory.scala
@@ -30,6 +30,7 @@ class FlussAppendPartitionReaderFactory(
tablePath: TablePath,
projection: Array[Int],
pushedPredicate: Option[Predicate],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends PartitionReaderFactory {
@@ -40,6 +41,7 @@ class FlussAppendPartitionReaderFactory(
tablePath,
projection,
pushedPredicate,
+ limit,
flussPartition,
flussConfig
)
@@ -50,6 +52,7 @@ class FlussAppendPartitionReaderFactory(
class FlussUpsertPartitionReaderFactory(
tablePath: TablePath,
projection: Array[Int],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends PartitionReaderFactory {
@@ -59,6 +62,7 @@ class FlussUpsertPartitionReaderFactory(
new FlussUpsertPartitionReader(
tablePath,
projection,
+ limit,
upsertPartition,
flussConfig
)
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScan.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScan.scala
index f9d0be1bb..cce6c8c13 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScan.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScan.scala
@@ -43,6 +43,8 @@ trait FlussScan extends Scan {
def partitionPredicate: Option[FlussPredicate] = None
+ def limit: Option[Int] = None
+
protected def scanType: String
override def readSchema(): StructType = {
@@ -54,10 +56,14 @@ trait FlussScan extends Scan {
val withPushed =
if (pushedSparkPredicates.isEmpty) base
else s"$base [PushedPredicates: ${pushedSparkPredicates.mkString("[", ",
", "]")}]"
- partitionPredicate match {
+ val withPartition = partitionPredicate match {
case Some(p) => s"$withPushed [PartitionFilter: $p]"
case None => withPushed
}
+ limit match {
+ case Some(l) => s"$withPartition [Limit: $l]"
+ case None => withPartition
+ }
}
override def supportedCustomMetrics(): Array[CustomMetric] =
@@ -72,6 +78,7 @@ case class FlussAppendScan(
pushedPredicate: Option[FlussPredicate],
override val partitionPredicate: Option[FlussPredicate],
override val pushedSparkPredicates: Seq[Predicate],
+ override val limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends FlussScan {
@@ -85,6 +92,7 @@ case class FlussAppendScan(
readSchema,
pushedPredicate,
partitionPredicate,
+ limit,
options,
flussConfig)
}
@@ -108,6 +116,7 @@ case class FlussLakeAppendScan(
pushedPredicate: Option[FlussPredicate],
override val partitionPredicate: Option[FlussPredicate],
override val pushedSparkPredicates: Seq[Predicate],
+ override val limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends FlussScan {
@@ -121,6 +130,7 @@ case class FlussLakeAppendScan(
readSchema,
pushedPredicate,
partitionPredicate,
+ limit,
options,
flussConfig)
}
@@ -142,6 +152,7 @@ case class FlussUpsertScan(
tableInfo: TableInfo,
requiredSchema: Option[StructType],
override val partitionPredicate: Option[FlussPredicate],
+ override val limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends FlussScan {
@@ -149,7 +160,14 @@ case class FlussUpsertScan(
override protected val scanType: String = "Upsert"
override def toBatch: Batch = {
- new FlussUpsertBatch(tablePath, tableInfo, readSchema, partitionPredicate,
options, flussConfig)
+ new FlussUpsertBatch(
+ tablePath,
+ tableInfo,
+ readSchema,
+ partitionPredicate,
+ limit,
+ options,
+ flussConfig)
}
override def toMicroBatchStream(checkpointLocation: String):
MicroBatchStream = {
@@ -171,6 +189,7 @@ case class FlussLakeUpsertScan(
pushedPredicate: Option[FlussPredicate],
override val partitionPredicate: Option[FlussPredicate],
override val pushedSparkPredicates: Seq[Predicate],
+ override val limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
extends FlussScan {
@@ -184,6 +203,7 @@ case class FlussLakeUpsertScan(
readSchema,
pushedPredicate,
partitionPredicate,
+ limit,
options,
flussConfig)
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScanBuilder.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScanBuilder.scala
index 6a9ece662..a31e5e218 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScanBuilder.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussScanBuilder.scala
@@ -24,7 +24,7 @@ import org.apache.fluss.spark.read.lake.{FlussLakeBatch,
FlussLakeUtils}
import org.apache.fluss.spark.utils.{SparkPartitionPredicate,
SparkPredicateConverter}
import org.apache.spark.sql.connector.expressions.filter.Predicate
-import org.apache.spark.sql.connector.read.{Scan, ScanBuilder,
SupportsPushDownRequiredColumns, SupportsPushDownV2Filters}
+import org.apache.spark.sql.connector.read.{Scan, ScanBuilder,
SupportsPushDownLimit, SupportsPushDownRequiredColumns,
SupportsPushDownV2Filters}
import org.apache.spark.sql.types.StructType
import org.apache.spark.sql.util.CaseInsensitiveStringMap
@@ -33,13 +33,22 @@ import java.util.{Collections, IdentityHashMap, Set => JSet}
import scala.collection.JavaConverters._
/** An interface that extends from Spark [[ScanBuilder]]. */
-trait FlussScanBuilder extends ScanBuilder with
SupportsPushDownRequiredColumns {
+trait FlussScanBuilder
+ extends ScanBuilder
+ with SupportsPushDownRequiredColumns
+ with SupportsPushDownLimit {
protected var requiredSchema: Option[StructType] = None
+ protected var limit: Option[Int] = None
override def pruneColumns(requiredSchema: StructType): Unit = {
this.requiredSchema = Some(requiredSchema)
}
+
+ override def pushLimit(limit: Int): Boolean = {
+ this.limit = Some(limit)
+ true
+ }
}
/** Extracts a partition-key predicate so the scan can skip partitions that
can't match. */
@@ -133,6 +142,7 @@ class FlussAppendScanBuilder(
pushedPredicate,
partitionPredicate,
acceptedPredicates.toSeq,
+ limit,
options,
flussConfig)
}
@@ -154,6 +164,7 @@ class FlussLakeAppendScanBuilder(
pushedPredicate,
partitionPredicate,
acceptedPredicates.toSeq,
+ limit,
options,
flussConfig)
}
@@ -168,7 +179,14 @@ class FlussUpsertScanBuilder(
extends FlussSupportsPushDownV2Filters {
override def build(): Scan = {
- FlussUpsertScan(tablePath, tableInfo, requiredSchema, partitionPredicate,
options, flussConfig)
+ FlussUpsertScan(
+ tablePath,
+ tableInfo,
+ requiredSchema,
+ partitionPredicate,
+ limit,
+ options,
+ flussConfig)
}
}
@@ -188,6 +206,7 @@ class FlussLakeUpsertScanBuilder(
pushedPredicate,
partitionPredicate,
acceptedPredicates.toSeq,
+ limit,
options,
flussConfig)
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussUpsertPartitionReader.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussUpsertPartitionReader.scala
index 65050f45e..7a74ee171 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussUpsertPartitionReader.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussUpsertPartitionReader.scala
@@ -46,9 +46,10 @@ import scala.collection.mutable
class FlussUpsertPartitionReader(
tablePath: TablePath,
projection: Array[Int],
+ limit: Option[Int],
flussPartition: FlussUpsertInputPartition,
flussConfig: Configuration)
- extends FlussPartitionReader(tablePath, flussConfig)
+ extends FlussPartitionReader(tablePath, flussConfig, limit)
with Logging {
override protected lazy val projectedRowType: RowType =
rowType.project(projectionWithPks)
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendBatch.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendBatch.scala
index f5b747e30..50bfc7f1c 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendBatch.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendBatch.scala
@@ -42,9 +42,10 @@ class FlussLakeAppendBatch(
readSchema: StructType,
pushedPredicate: Option[FlussPredicate],
partitionPredicate: Option[FlussPredicate],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
- extends FlussLakeBatch(tablePath, tableInfo, readSchema, options,
flussConfig) {
+ extends FlussLakeBatch(tablePath, tableInfo, readSchema, limit, options,
flussConfig) {
// Required by FlussLakeBatch but unused — lake snapshot determines start
offsets.
override val startOffsetsInitializer: OffsetsInitializer =
OffsetsInitializer.earliest()
@@ -59,6 +60,7 @@ class FlussLakeAppendBatch(
tablePath,
projection,
logTailPredicate,
+ limit,
options,
flussConfig)
} else {
@@ -68,6 +70,7 @@ class FlussLakeAppendBatch(
projection,
pushedPredicate,
logTailPredicate,
+ limit,
flussConfig)
}
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendPartitionReader.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendPartitionReader.scala
index 6c64ed902..a1ffeac4c 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendPartitionReader.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeAppendPartitionReader.scala
@@ -33,8 +33,9 @@ class FlussLakeAppendPartitionReader(
partition: FlussLakeInputPartition,
lakeSource: LakeSource[LakeSplit],
projection: Array[Int],
+ limit: Option[Int],
flussConfig: Configuration)
- extends FlussPartitionReader(tablePath, flussConfig)
+ extends FlussPartitionReader(tablePath, flussConfig, limit)
with Logging {
private var recordIterator: CloseableIterator[LogRecord] = _
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeBatch.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeBatch.scala
index d61aea9d1..bed1017b8 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeBatch.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeBatch.scala
@@ -37,9 +37,10 @@ abstract class FlussLakeBatch(
tablePath: TablePath,
tableInfo: TableInfo,
readSchema: StructType,
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
- extends FlussBatch(tablePath, tableInfo, readSchema, flussConfig) {
+ extends FlussBatch(tablePath, tableInfo, readSchema, limit, flussConfig) {
override val stoppingOffsetsInitializer: OffsetsInitializer = {
FlussOffsetInitializers.stoppingOffsetsInitializer(true, options,
flussConfig)
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakePartitionReaderFactory.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakePartitionReaderFactory.scala
index 30014b3c9..d69b4c08d 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakePartitionReaderFactory.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakePartitionReaderFactory.scala
@@ -35,6 +35,7 @@ class FlussLakePartitionReaderFactory(
projection: Array[Int],
flussPredicate: Option[FlussPredicate],
logTailPredicate: Option[FlussPredicate],
+ limit: Option[Int],
flussConfig: Configuration)
extends PartitionReaderFactory {
@@ -53,12 +54,14 @@ class FlussLakePartitionReaderFactory(
lakeOnlySplit,
lakeSource,
projection,
+ limit,
flussConfig)
case logSplit: FlussAppendInputPartition =>
new FlussAppendPartitionReader(
tablePath,
projection,
logTailPredicate,
+ limit,
logSplit,
flussConfig)
case mixedSplit: FlussLakeUpsertInputPartition =>
@@ -66,6 +69,7 @@ class FlussLakePartitionReaderFactory(
tablePath,
lakeSource,
projection,
+ limit,
mixedSplit,
flussConfig)
case _ =>
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertBatch.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertBatch.scala
index 882d94143..c37a7ec48 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertBatch.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertBatch.scala
@@ -45,9 +45,10 @@ class FlussLakeUpsertBatch(
readSchema: StructType,
pushedPredicate: Option[FlussPredicate],
partitionPredicate: Option[FlussPredicate],
+ limit: Option[Int],
options: CaseInsensitiveStringMap,
flussConfig: Configuration)
- extends FlussLakeBatch(tablePath, tableInfo, readSchema, options,
flussConfig) {
+ extends FlussLakeBatch(tablePath, tableInfo, readSchema, limit, options,
flussConfig) {
override val startOffsetsInitializer: OffsetsInitializer = {
val offsetsInitializer =
FlussOffsetInitializers.startOffsetsInitializer(options, flussConfig)
@@ -59,7 +60,7 @@ class FlussLakeUpsertBatch(
override def createReaderFactory(): PartitionReaderFactory = {
if (isFallback) {
- new FlussUpsertPartitionReaderFactory(tablePath, projection, options,
flussConfig)
+ new FlussUpsertPartitionReaderFactory(tablePath, projection, limit,
options, flussConfig)
} else {
// PK kv-tail reader does not consume server-side log filters.
new FlussLakePartitionReaderFactory(
@@ -68,6 +69,7 @@ class FlussLakeUpsertBatch(
projection,
pushedPredicate,
None,
+ limit,
flussConfig)
}
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertPartitionReader.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertPartitionReader.scala
index ae259dc9b..449fbc062 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertPartitionReader.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/lake/FlussLakeUpsertPartitionReader.scala
@@ -39,9 +39,10 @@ class FlussLakeUpsertPartitionReader(
tablePath: TablePath,
lakeSource: LakeSource[LakeSplit],
projection: Array[Int],
+ limit: Option[Int],
flussPartition: FlussLakeUpsertInputPartition,
flussConfig: Configuration)
- extends FlussPartitionReader(tablePath, flussConfig)
+ extends FlussPartitionReader(tablePath, flussConfig, limit)
with Logging {
private val lakeSplits = flussPartition.lakeSplits
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
index 7c7fb8a5c..42fdb1987 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
@@ -640,4 +640,31 @@ class SparkLogTableReadTest extends FlussSparkTestBase {
assert(numRowsRead == 5L, s"Expected 5 rows read, got $numRowsRead")
}
}
+
+ test("Spark Read: limit pushdown") {
+ withTable("t") {
+ sql(s"""
+ |CREATE TABLE $DEFAULT_DATABASE.t (id INT, name STRING)
+ |""".stripMargin)
+
+ sql(s"""
+ |INSERT INTO $DEFAULT_DATABASE.t VALUES
+ |(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e')
+ |""".stripMargin)
+
+ val dfNoLimit = sql(s"SELECT * FROM $DEFAULT_DATABASE.t")
+ assert(flussAppendScans(dfNoLimit).flatMap(_.limit).isEmpty)
+
+ val dfLimit = sql(s"SELECT * FROM $DEFAULT_DATABASE.t LIMIT 2")
+ assert(flussAppendScans(dfLimit).flatMap(_.limit).distinct == Seq(2))
+
+ // Verify limit pushdown actually reduces rows read via metrics
+ dfLimit.collect()
+ val batchScanExec = dfLimit.queryExecution.executedPlan.collectFirst {
+ case b: BatchScanExec => b
+ }.get
+ val numRowsRead = batchScanExec.metrics(FlussMetrics.NUM_ROWS_READ).value
+ assert(numRowsRead == 2L, s"Expected 2 rows read with limit pushdown,
got $numRowsRead")
+ }
+ }
}
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkPrimaryKeyTableReadTest.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkPrimaryKeyTableReadTest.scala
index 84754f548..4e30d552d 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkPrimaryKeyTableReadTest.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkPrimaryKeyTableReadTest.scala
@@ -501,6 +501,24 @@ class SparkPrimaryKeyTableReadTest extends
FlussSparkTestBase {
body
}
+ test("Spark Read: primary key table limit pushdown") {
+ withPkPartitionedTable {
+ val dfNoLimit = sql(s"SELECT * FROM $DEFAULT_DATABASE.t")
+ assert(flussUpsertScan(dfNoLimit).flatMap(_.limit).isEmpty)
+
+ val dfLimit = sql(s"SELECT * FROM $DEFAULT_DATABASE.t WHERE dt =
'2026-01-01' LIMIT 1")
+ assert(flussUpsertScan(dfLimit).flatMap(_.limit).contains(1))
+
+ // Verify limit pushdown actually reduces rows read via metrics
+ dfLimit.collect()
+ val batchScanExec = dfLimit.queryExecution.executedPlan.collectFirst {
+ case b: BatchScanExec => b
+ }.get
+ val numRowsRead = batchScanExec.metrics(FlussMetrics.NUM_ROWS_READ).value
+ assert(numRowsRead == 1L, s"Expected 1 rows read with limit pushdown,
got $numRowsRead")
+ }
+ }
+
private def partitionPredicate(df: DataFrame):
Option[org.apache.fluss.predicate.Predicate] = {
flussUpsertScan(df).flatMap(_.partitionPredicate)
}
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeLogTableReadTest.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeLogTableReadTest.scala
index b47bdfeeb..5fd910968 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeLogTableReadTest.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeLogTableReadTest.scala
@@ -20,8 +20,10 @@ package org.apache.fluss.spark.lake
import org.apache.fluss.config.{ConfigOptions, Configuration}
import org.apache.fluss.metadata.DataLakeFormat
import org.apache.fluss.spark.SparkConnectorOptions.BUCKET_NUMBER
+import org.apache.fluss.spark.read.FlussMetrics
import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
import java.nio.file.Files
@@ -616,6 +618,41 @@ abstract class SparkLakeLogTableReadTest extends
SparkLakeTableReadTestBase {
}
}
+ test("Spark Lake Read: log table union read with limit pushdown") {
+ withTable("t_union_limit") {
+ sql(s"""
+ |CREATE TABLE $DEFAULT_DATABASE.t_union_limit (id INT, name
STRING)
+ | TBLPROPERTIES (
+ | '${ConfigOptions.TABLE_DATALAKE_ENABLED.key()}' = true,
+ | '${ConfigOptions.TABLE_DATALAKE_FRESHNESS.key()}' = '1s',
+ | '${BUCKET_NUMBER.key()}' = 1)
+ |""".stripMargin)
+
+ sql(s"""
+ |INSERT INTO $DEFAULT_DATABASE.t_union_limit VALUES
+ |(1, "alpha"), (2, "beta"), (3, "gamma")
+ |""".stripMargin)
+
+ tierToLake("t_union_limit")
+
+ sql(s"""
+ |INSERT INTO $DEFAULT_DATABASE.t_union_limit VALUES
+ |(4, "delta"), (5, "epsilon")
+ |""".stripMargin)
+
+ val df = sql(s"SELECT * FROM $DEFAULT_DATABASE.t_union_limit LIMIT 2")
+ assert(flussScan(df).flatMap(_.limit).distinct == Seq(2))
+
+ // Verify limit pushdown actually reduces rows read via metrics
+ df.collect()
+ val batchScanExec = df.queryExecution.executedPlan.collectFirst {
+ case b: BatchScanExec => b
+ }.get
+ val numRowsRead = batchScanExec.metrics(FlussMetrics.NUM_ROWS_READ).value
+ assert(numRowsRead == 2L, s"Expected 2 rows read with limit pushdown,
got $numRowsRead")
+ }
+ }
+
test("Spark Lake Read: non-FULL startup mode skips lake path") {
withTable("t_earliest") {
sql(s"""
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakePrimaryKeyTableReadTestBase.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakePrimaryKeyTableReadTestBase.scala
index f33157665..e8d7b1795 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakePrimaryKeyTableReadTestBase.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakePrimaryKeyTableReadTestBase.scala
@@ -20,9 +20,11 @@ package org.apache.fluss.spark.lake
import org.apache.fluss.config.{ConfigOptions, Configuration}
import org.apache.fluss.metadata.DataLakeFormat
import org.apache.fluss.spark.SparkConnectorOptions.{BUCKET_NUMBER,
PRIMARY_KEY}
+import org.apache.fluss.spark.read.FlussMetrics
import org.apache.fluss.spark.read.FlussUpsertInputPartition
import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
import java.nio.file.Files
@@ -459,6 +461,40 @@ abstract class SparkLakePrimaryKeyTableReadTestBase
extends SparkLakeTableReadTe
}
}
+ test("Spark Lake Read: union with limit pushdown") {
+ withTable("t_pk_union_limit") {
+ sql(s"""
+ |CREATE TABLE $DEFAULT_DATABASE.t_pk_union_limit (id INT, name
STRING, score INT)
+ | TBLPROPERTIES (
+ | '${ConfigOptions.TABLE_DATALAKE_ENABLED.key()}' = true,
+ | '${ConfigOptions.TABLE_DATALAKE_FRESHNESS.key()}' = '1s',
+ | '${PRIMARY_KEY.key()}' = 'id',
+ | '${BUCKET_NUMBER.key()}' = 1)
+ |""".stripMargin)
+ sql(s"""
+ |INSERT INTO $DEFAULT_DATABASE.t_pk_union_limit VALUES
+ |(1, 'alice', 90), (2, 'bob', 85), (3, 'charlie', 95)
+ |""".stripMargin)
+ tierToLake("t_pk_union_limit")
+ sql(s"""
+ |INSERT INTO $DEFAULT_DATABASE.t_pk_union_limit VALUES
+ |(4, 'dave', 88), (5, 'eve', 92)
+ |""".stripMargin)
+
+ val query =
+ sql(s"SELECT id, score FROM $DEFAULT_DATABASE.t_pk_union_limit LIMIT
2")
+ assert(flussScan(query).flatMap(_.limit).distinct == Seq(2))
+
+ // Verify limit pushdown actually reduces rows read via metrics
+ query.collect()
+ val batchScanExec = query.queryExecution.executedPlan.collectFirst {
+ case b: BatchScanExec => b
+ }.get
+ val numRowsRead = batchScanExec.metrics(FlussMetrics.NUM_ROWS_READ).value
+ assert(numRowsRead == 2L, s"Expected 2 rows read with limit pushdown,
got $numRowsRead")
+ }
+ }
+
test("Spark Lake Read: primary key table projection with type-dependent
columns") {
withTable("t") {
val tablePath = createTablePath("t")
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeTableReadTestBase.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeTableReadTestBase.scala
index 14108636e..5c507372d 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeTableReadTestBase.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/lake/SparkLakeTableReadTestBase.scala
@@ -133,14 +133,18 @@ abstract class SparkLakeTableReadTestBase extends
FlussSparkTestBase {
}
}
- protected def pushedPredicates(df: DataFrame): Array[Predicate] = {
+ protected def flussScan(df: DataFrame): Seq[FlussScan] = {
val scans =
df.queryExecution.executedPlan.collect {
case b: BatchScanExec => b.scan
} ++ df.queryExecution.optimizedPlan.collect {
case DataSourceV2ScanRelation(_, scan, _, _, _) => scan
}
- scans
+ scans.collect { case s: FlussScan => s }
+ }
+
+ protected def pushedPredicates(df: DataFrame): Array[Predicate] = {
+ flussScan(df)
.collect { case f: FlussScan => f.pushedSparkPredicates }
.flatten
.toArray