This is an automated email from the ASF dual-hosted git repository.

Dandandan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new fcd8a5a04 perf(core): push a consumer's row limit into 
RangeShuffleReaderExec (#2334)
fcd8a5a04 is described below

commit fcd8a5a0486a4e337c698654091e19576d312258
Author: Daniël Heres <[email protected]>
AuthorDate: Mon Aug 17 11:19:52 2026 +0200

    perf(core): push a consumer's row limit into RangeShuffleReaderExec (#2334)
    
    * perf(core): push a consumer's row limit into RangeShuffleReaderExec
    
    `RangeShuffleReaderExec` merges all of its input streams to completion even
    when the `SortPreservingMergeExec` above it only wants the first few rows. 
Its
    `StreamingMerge` was built without a fetch, so a `fetch=20` top-N over a 
wide
    fan-in merged everything and then discarded almost all of it.
    
    Both operators merge on the same ordering, so the consumer's first `n` rows 
can
    only come from the reader's first `n`. The reader now carries an optional
    `fetch` and passes it to `StreamingMergeBuilder`, and the adapter pushes the
    limit down when it plants the reader under such a merge.
    
    DataFusion's own limit pushdown can't do this: the reader is planted at 
adapt
    time, after the optimizer chain has run.
    
    This reduces merge work. It does not lower the merge's peak memory, which is
    set by fan-in times batch size and is paid before the first row is emitted.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * refactor(scheduler): apply the row limit where the reader is built
    
    The limit was applied by a second walk of the plan that re-visited a node 
the
    adapter had just created. The adapter already walks parent-first, so it can
    handle the merge and its exchange together and build the reader with the 
limit
    already set.
    
    Extracts the exchange-to-reader conversion into `build_reader` so both paths
    share it, and drops the extra pass.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * fix(core): carry the pushed-down fetch through the wire format
    
    `RangeShuffleReaderExec` gained a `fetch` field, but the proto message had
    no matching field, so the scheduler's limit was dropped on encode and the
    executor merged every row anyway. Add `fetch` to the proto, write it in the
    encoder, restore it on decode, and preserve it when the task builder
    restricts the reader to a partition subset.
    
    Also simplify the operator (derive `Clone`, update via struct update
    syntax), show `fetch` in EXPLAIN, and reuse `replace_children_if_necessary`
    in the adapter.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01DZS73f3mPrwQ8EPbswFJVa
    
    * test(scheduler): cover the limit reaching the planted range reader
    
    The adapter rebuilds the merge around a new reader, so a limit that fails
    to reach the reader is silent — nothing in the plan shape changes.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01DZS73f3mPrwQ8EPbswFJVa
    
    ---------
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 ballista/core/proto/ballista.proto                 |   2 +
 .../src/execution_plans/range_shuffle_reader.rs    |  91 ++++++---
 ballista/core/src/serde/generated/ballista.rs      |   3 +
 ballista/core/src/serde/mod.rs                     |  18 +-
 ballista/scheduler/src/state/aqe/adapter.rs        | 222 +++++++++++++--------
 ballista/scheduler/src/state/task_builder.rs       |   2 +-
 6 files changed, 230 insertions(+), 108 deletions(-)

diff --git a/ballista/core/proto/ballista.proto 
b/ballista/core/proto/ballista.proto
index bd20ed65d..ae8098bce 100644
--- a/ballista/core/proto/ballista.proto
+++ b/ballista/core/proto/ballista.proto
@@ -235,6 +235,8 @@ message RangeShuffleReaderExecNode {
   // Sort key the reader's k-way merge preserves. Advertised on the reader's
   // `PlanProperties.eq_properties` for downstream consumers.
   repeated datafusion.PhysicalSortExprNode merge_ordering = 4;
+  // Row limit pushed down by a consuming merge. Absent means read everything.
+  optional uint64 fetch = 5;
 }
 
 // CoalescePartitionsRule output: groups upstream partitions into coalesced 
output partitions.
diff --git a/ballista/core/src/execution_plans/range_shuffle_reader.rs 
b/ballista/core/src/execution_plans/range_shuffle_reader.rs
index be8fe8f71..ed43ed271 100644
--- a/ballista/core/src/execution_plans/range_shuffle_reader.rs
+++ b/ballista/core/src/execution_plans/range_shuffle_reader.rs
@@ -82,7 +82,7 @@ use log::debug;
 use std::sync::Arc;
 
 /// Ordering-preserving shuffle reader. See module docs.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct RangeShuffleReaderExec {
     /// Upstream stage that produced these files.
     pub stage_id: usize,
@@ -93,6 +93,8 @@ pub struct RangeShuffleReaderExec {
     /// Sort key the merge preserves. Advertised in 
`PlanProperties.eq_properties`
     /// so downstream operators (BWAG, SMJ build side) see the output ordering.
     merge_ordering: LexOrdering,
+    /// Row limit pushed down by a consumer. `None` means read everything.
+    fetch: Option<usize>,
     metrics: ExecutionPlanMetricsSet,
     properties: Arc<PlanProperties>,
     work_dir: Option<String>,
@@ -126,6 +128,7 @@ impl RangeShuffleReaderExec {
             schema,
             partition,
             merge_ordering,
+            fetch: None,
             metrics: ExecutionPlanMetricsSet::new(),
             properties,
             work_dir: None,
@@ -133,31 +136,25 @@ impl RangeShuffleReaderExec {
         })
     }
 
+    /// Set the row limit at construction, without cloning the reader.
+    pub fn with_fetch_limit(mut self, fetch: Option<usize>) -> Self {
+        self.fetch = fetch;
+        self
+    }
+
     /// Late-bound by the executor.
     pub fn with_work_dir(&self, work_dir: String) -> Self {
         Self {
-            stage_id: self.stage_id,
-            schema: self.schema.clone(),
-            partition: self.partition.clone(),
-            merge_ordering: self.merge_ordering.clone(),
-            metrics: self.metrics.clone(),
-            properties: self.properties.clone(),
             work_dir: Some(work_dir),
-            client_pool: self.client_pool.clone(),
+            ..self.clone()
         }
     }
 
     /// Late-bound by the executor.
     pub fn with_client_pool(&self, client_pool: Arc<dyn BallistaClientPool>) 
-> Self {
         Self {
-            stage_id: self.stage_id,
-            schema: self.schema.clone(),
-            partition: self.partition.clone(),
-            merge_ordering: self.merge_ordering.clone(),
-            metrics: self.metrics.clone(),
-            properties: self.properties.clone(),
-            work_dir: self.work_dir.clone(),
             client_pool: Some(client_pool),
+            ..self.clone()
         }
     }
 
@@ -181,12 +178,20 @@ impl DisplayAs for RangeShuffleReaderExec {
                     self.stage_id,
                     self.partition.len(),
                     self.merge_ordering,
-                )
+                )?;
+                if let Some(fetch) = self.fetch {
+                    write!(f, ", fetch: {fetch}")?;
+                }
+                Ok(())
             }
             DisplayFormatType::TreeRender => {
                 writeln!(f, "upstream_stage={}", self.stage_id)?;
                 writeln!(f, "output_partitions={}", self.partition.len())?;
-                writeln!(f, "ordering={}", self.merge_ordering)
+                writeln!(f, "ordering={}", self.merge_ordering)?;
+                if let Some(fetch) = self.fetch {
+                    writeln!(f, "fetch={fetch}")?;
+                }
+                Ok(())
             }
         }
     }
@@ -231,14 +236,8 @@ impl ExecutionPlan for RangeShuffleReaderExec {
             ));
         }
         Ok(Arc::new(Self {
-            stage_id: self.stage_id,
-            schema: self.schema.clone(),
-            partition: self.partition.clone(),
-            merge_ordering: self.merge_ordering.clone(),
             metrics: ExecutionPlanMetricsSet::new(),
-            properties: self.properties.clone(),
-            work_dir: self.work_dir.clone(),
-            client_pool: self.client_pool.clone(),
+            ..self.as_ref().clone()
         }))
     }
 
@@ -330,6 +329,7 @@ impl ExecutionPlan for RangeShuffleReaderExec {
             .with_schema(self.schema.clone())
             .with_expressions(&self.merge_ordering)
             .with_batch_size(config.batch_size())
+            .with_fetch(self.fetch)
             .with_metrics(baseline)
             .with_reservation(reservation)
             .build()?;
@@ -337,6 +337,19 @@ impl ExecutionPlan for RangeShuffleReaderExec {
         Ok(merged)
     }
 
+    fn fetch(&self) -> Option<usize> {
+        self.fetch
+    }
+
+    /// Output is sorted on `merge_ordering`, so the first `n` rows can only
+    /// come from the first `n` of each input.
+    fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn 
ExecutionPlan>> {
+        Some(Arc::new(Self {
+            fetch: limit,
+            ..self.clone()
+        }))
+    }
+
     fn metrics(&self) -> 
Option<datafusion::physical_plan::metrics::MetricsSet> {
         Some(self.metrics.clone_inner())
     }
@@ -371,6 +384,7 @@ mod tests {
     use datafusion::arrow::ipc::writer::StreamWriter;
     use datafusion::physical_expr::PhysicalSortExpr;
     use datafusion::physical_expr::expressions::Column;
+    use datafusion::physical_plan::{ChildrenPropertiesMode, 
ReplaceChildrenOptions};
     use datafusion::prelude::SessionContext;
     use std::fs::{File, create_dir_all};
     use tempfile::tempdir;
@@ -550,6 +564,35 @@ mod tests {
         assert!(batches.is_empty());
     }
 
+    /// A limit must survive a rebuild, or the merge quietly goes back to
+    /// reading everything.
+    #[test]
+    fn fetch_roundtrips_through_with_fetch() {
+        use datafusion::physical_plan::ExecutionPlan;
+        let schema =
+            Arc::new(Schema::new(vec![Field::new("v", DataType::Float64, 
false)]));
+        let merge_ordering = 
LexOrdering::new(vec![PhysicalSortExpr::new_default(
+            Arc::new(Column::new("v", 0)),
+        )])
+        .unwrap();
+        let reader =
+            RangeShuffleReaderExec::try_new(3, vec![vec![]; 4], schema, 
merge_ordering)
+                .unwrap();
+        assert_eq!(reader.fetch(), None, "reader starts unlimited");
+
+        let limited = ExecutionPlan::with_fetch(&reader, Some(20))
+            .expect("RangeShuffleReaderExec must accept a pushed-down limit");
+        assert_eq!(limited.fetch(), Some(20));
+
+        let rebuilt = Arc::clone(&limited)
+            .replace_children(
+                vec![],
+                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
+            )
+            .expect("rebuild");
+        assert_eq!(rebuilt.fetch(), Some(20), "fetch must survive a rebuild");
+    }
+
     /// The reader must advertise its merge ordering so downstream operators
     /// see the sortedness invariant (BWAG's RANGE-frame cursor, SMJ build 
side).
     #[test]
diff --git a/ballista/core/src/serde/generated/ballista.rs 
b/ballista/core/src/serde/generated/ballista.rs
index a868ff2b3..9e05f2ed2 100644
--- a/ballista/core/src/serde/generated/ballista.rs
+++ b/ballista/core/src/serde/generated/ballista.rs
@@ -300,6 +300,9 @@ pub struct RangeShuffleReaderExecNode {
     pub merge_ordering: ::prost::alloc::vec::Vec<
         ::datafusion_proto::protobuf::PhysicalSortExprNode,
     >,
+    /// Row limit pushed down by a consuming merge. Absent means read 
everything.
+    #[prost(uint64, optional, tag = "5")]
+    pub fetch: ::core::option::Option<u64>,
 }
 /// CoalescePartitionsRule output: groups upstream partitions into coalesced 
output partitions.
 /// Empty when no coalesce is applied (the optional field on the parent 
message is absent).
diff --git a/ballista/core/src/serde/mod.rs b/ballista/core/src/serde/mod.rs
index 642feec2c..56d2b8eb8 100644
--- a/ballista/core/src/serde/mod.rs
+++ b/ballista/core/src/serde/mod.rs
@@ -542,12 +542,15 @@ impl PhysicalExtensionCodec for 
BallistaPhysicalExtensionCodec {
                         "RangeShuffleReaderExec: merge_ordering must be 
non-empty",
                     )
                 })?;
-                Ok(Arc::new(RangeShuffleReaderExec::try_new(
+                let reader = RangeShuffleReaderExec::try_new(
                     stage_id,
                     partition_location,
                     schema,
                     merge_ordering,
-                )?))
+                )?;
+                Ok(Arc::new(
+                    reader.with_fetch_limit(range_reader.fetch.map(|f| f as 
usize)),
+                ))
             }
             PhysicalPlanType::UnresolvedShuffle(unresolved_shuffle) => {
                 let schema: SchemaRef =
@@ -934,6 +937,7 @@ impl PhysicalExtensionCodec for 
BallistaPhysicalExtensionCodec {
                         partition,
                         schema: Some(exec.schema().as_ref().try_into()?),
                         merge_ordering,
+                        fetch: exec.fetch().map(|f| f as u64),
                     },
                 )),
             };
@@ -1430,7 +1434,8 @@ mod test {
             schema.clone(),
             merge_ordering.clone(),
         )
-        .unwrap();
+        .unwrap()
+        .with_fetch_limit(Some(20));
 
         let codec = BallistaPhysicalExtensionCodec::default();
         let mut buf: Vec<u8> = vec![];
@@ -1462,6 +1467,13 @@ mod test {
             decoded.merge_ordering().first().expr.to_string(),
             sort_expr.expr.to_string(),
         );
+        // Dropped on the wire, the limit would silently stop applying: the
+        // reader only ever executes after being decoded on an executor.
+        assert_eq!(
+            ExecutionPlan::fetch(decoded),
+            Some(20),
+            "fetch must round-trip"
+        );
         // The ordering must land on `PlanProperties.eq_properties` — 
downstream
         // consumers (BWAG, SMJ build side) read it there.
         let advertised = decoded
diff --git a/ballista/scheduler/src/state/aqe/adapter.rs 
b/ballista/scheduler/src/state/aqe/adapter.rs
index 29d137615..9e39c5ffb 100644
--- a/ballista/scheduler/src/state/aqe/adapter.rs
+++ b/ballista/scheduler/src/state/aqe/adapter.rs
@@ -28,7 +28,10 @@ use ballista_core::execution_plans::{
 use datafusion::common::exec_err;
 use datafusion::config::ConfigOptions;
 use datafusion::error::DataFusionError;
-use datafusion::physical_plan::{ExecutionPlanProperties, Partitioning};
+use 
datafusion::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
+use datafusion::physical_plan::{
+    ExecutionPlanProperties, Partitioning, replace_children_if_necessary,
+};
 use datafusion::scalar::ScalarValue;
 use datafusion::{
     common::tree_node::{Transformed, TreeNode, TreeNodeRecursion},
@@ -48,94 +51,120 @@ pub(crate) struct BallistaAdapter {
 /// ShuffleWriterExec/SortShuffleWriterExec and [ShuffleReaderExec]
 ///
 impl BallistaAdapter {
-    fn transform_children(
+    /// Build the reader that replaces `exchange`, recording its upstream
+    /// stage as an input of this stage. `fetch` is a row limit from the
+    /// consumer; only the ordered reader can honor it.
+    fn build_reader(
         &mut self,
-        plan: Arc<dyn ExecutionPlan>,
-    ) -> datafusion::error::Result<Transformed<Arc<dyn ExecutionPlan>>> {
-        if let Some(exchange) = plan.downcast_ref::<ExchangeExec>() {
-            let schema = exchange.schema().clone();
-            let partitions = exchange.shuffle_partitions().ok_or_else(|| {
-                DataFusionError::Execution(
-                    "partitions have to be resolved at this point".to_string(),
-                )
-            })?;
+        exchange: &ExchangeExec,
+        fetch: Option<usize>,
+    ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
+        let schema = exchange.schema().clone();
+        let partitions = exchange.shuffle_partitions().ok_or_else(|| {
+            DataFusionError::Execution(
+                "partitions have to be resolved at this point".to_string(),
+            )
+        })?;
 
-            let stage_id = exchange.stage_id().ok_or_else(|| {
-                DataFusionError::Execution(
-                    "stage ID has to be generated at this point".to_string(),
-                )
-            })?;
-            let mut stage_output = StageOutput::new();
-            for partition in partitions.iter().flatten().cloned() {
-                stage_output.add_partition(partition);
-            }
-            stage_output.complete = true;
-            self.inputs.insert(stage_id, stage_output);
-            let partitioning = exchange.properties().partitioning.clone();
+        let stage_id = exchange.stage_id().ok_or_else(|| {
+            DataFusionError::Execution(
+                "stage ID has to be generated at this point".to_string(),
+            )
+        })?;
+        let mut stage_output = StageOutput::new();
+        for partition in partitions.iter().flatten().cloned() {
+            stage_output.add_partition(partition);
+        }
+        stage_output.complete = true;
+        self.inputs.insert(stage_id, stage_output);
+        let partitioning = exchange.properties().partitioning.clone();
 
-            let reader: Arc<dyn ExecutionPlan> =
-                match (exchange.coalesce(), exchange.broadcast) {
-                    (Some(cp), false) => {
-                        // Concatenate M-shape locations into K-shape per 
CoalescePlan.groups.
-                        let k_shape: Vec<Vec<_>> = cp
-                            .groups
-                            .iter()
-                            .map(|pg| {
-                                let mut concat = Vec::new();
-                                for &idx in &pg.upstream_indices {
-                                    if let Some(inner) = partitions.get(idx as 
usize) {
-                                        concat.extend_from_slice(inner);
-                                    }
-                                }
-                                concat
-                            })
-                            .collect();
-                        let new_partitioning = match &partitioning {
-                            Partitioning::Hash(keys, _m) => {
-                                Partitioning::Hash(keys.clone(), 
cp.groups.len())
+        Ok(match (exchange.coalesce(), exchange.broadcast) {
+            (Some(cp), false) => {
+                // Concatenate M-shape locations into K-shape per 
CoalescePlan.groups.
+                let k_shape: Vec<Vec<_>> = cp
+                    .groups
+                    .iter()
+                    .map(|pg| {
+                        let mut concat = Vec::new();
+                        for &idx in &pg.upstream_indices {
+                            if let Some(inner) = partitions.get(idx as usize) {
+                                concat.extend_from_slice(inner);
                             }
-                            _ => 
Partitioning::UnknownPartitioning(cp.groups.len()),
-                        };
-                        Arc::new(ShuffleReaderExec::try_new_coalesced(
-                            stage_id,
-                            k_shape,
-                            (*cp).clone(),
-                            schema,
-                            new_partitioning,
-                        )?)
-                    }
-                    (None, false) => {
-                        // Ordered-writer path: when the child declared an 
output
-                        // ordering, preserve it across the shuffle boundary 
with a
-                        // k-way merge instead of the arrival-order concat 
that the
-                        // regular reader does.
-                        if let Some(ordering) = 
exchange.input().output_ordering() {
-                            Arc::new(RangeShuffleReaderExec::try_new(
-                                stage_id,
-                                partitions,
-                                schema,
-                                ordering.clone(),
-                            )?)
-                        } else {
-                            Arc::new(ShuffleReaderExec::try_new(
-                                stage_id,
-                                partitions,
-                                schema,
-                                partitioning,
-                            )?)
                         }
+                        concat
+                    })
+                    .collect();
+                let new_partitioning = match &partitioning {
+                    Partitioning::Hash(keys, _m) => {
+                        Partitioning::Hash(keys.clone(), cp.groups.len())
                     }
-                    (_, true) => Arc::new(ShuffleReaderExec::try_new_broadcast(
+                    _ => Partitioning::UnknownPartitioning(cp.groups.len()),
+                };
+                Arc::new(ShuffleReaderExec::try_new_coalesced(
+                    stage_id,
+                    k_shape,
+                    (*cp).clone(),
+                    schema,
+                    new_partitioning,
+                )?)
+            }
+            (None, false) => {
+                // Ordered-writer path: when the child declared an output
+                // ordering, preserve it across the shuffle boundary with a
+                // k-way merge instead of the arrival-order concat that the
+                // regular reader does.
+                if let Some(ordering) = exchange.input().output_ordering() {
+                    Arc::new(
+                        RangeShuffleReaderExec::try_new(
+                            stage_id,
+                            partitions,
+                            schema,
+                            ordering.clone(),
+                        )?
+                        .with_fetch_limit(fetch),
+                    )
+                } else {
+                    Arc::new(ShuffleReaderExec::try_new(
                         stage_id,
-                        exchange.shuffle_partitions_flattened(),
+                        partitions,
                         schema,
-                        
exchange.input().output_partitioning().partition_count(),
-                    )?),
-                };
-            Ok(Transformed::yes(reader))
-        } else {
-            Ok(Transformed::no(plan))
+                        partitioning,
+                    )?)
+                }
+            }
+            (_, true) => Arc::new(ShuffleReaderExec::try_new_broadcast(
+                stage_id,
+                exchange.shuffle_partitions_flattened(),
+                schema,
+                exchange.input().output_partitioning().partition_count(),
+            )?),
+        })
+    }
+
+    fn transform_children(
+        &mut self,
+        plan: Arc<dyn ExecutionPlan>,
+    ) -> datafusion::error::Result<Transformed<Arc<dyn ExecutionPlan>>> {
+        // A merge with a row limit on top of an exchange: build the reader
+        // with the limit already set. Both merge on the same ordering, so the
+        // consumer's first `n` rows come from the reader's first `n`.
+        if let Some(spm) = plan.downcast_ref::<SortPreservingMergeExec>()
+            && let Some(fetch) = spm.fetch()
+            && let Some(exchange) = spm.input().downcast_ref::<ExchangeExec>()
+        {
+            let reader = self.build_reader(exchange, Some(fetch))?;
+            return Ok(Transformed::yes(replace_children_if_necessary(
+                plan,
+                vec![reader],
+            )?));
         }
+
+        if let Some(exchange) = plan.downcast_ref::<ExchangeExec>() {
+            return Ok(Transformed::yes(self.build_reader(exchange, None)?));
+        }
+
+        Ok(Transformed::no(plan))
     }
 
     /// Converts Adaptive plan to plan which ballista expects
@@ -384,4 +413,37 @@ mod tests {
         );
         assert!(out.downcast_ref::<RangeShuffleReaderExec>().is_none());
     }
+
+    /// The consuming merge's row limit must reach the reader. The merge is
+    /// rebuilt around the new reader, so a dropped limit is silent.
+    #[test]
+    fn pushes_consumer_limit_into_range_reader() {
+        let schema = f64_schema();
+        let empty: Vec<Vec<RecordBatch>> = vec![vec![]];
+        let source =
+            MemorySourceConfig::try_new_exec(&empty, schema.clone(), 
None).unwrap();
+        let sort_lex = LexOrdering::new(vec![asc(&schema, "v")]).unwrap();
+        let sorted = Arc::new(
+            SortExec::new(sort_lex.clone(), 
source).with_preserve_partitioning(true),
+        ) as Arc<dyn ExecutionPlan>;
+
+        let exchange = ExchangeExec::new(sorted, None, 0);
+        exchange.set_stage_id(1);
+        exchange.resolve_shuffle_partitions(vec![vec![]]);
+        let merge = Arc::new(
+            SortPreservingMergeExec::new(sort_lex, Arc::new(exchange))
+                .with_fetch(Some(7)),
+        ) as Arc<dyn ExecutionPlan>;
+
+        let mut adapter = BallistaAdapter::default();
+        let out = adapter.transform_children(merge).unwrap().data;
+
+        let children = out.children();
+        let reader = children[0]
+            .downcast_ref::<RangeShuffleReaderExec>()
+            .unwrap_or_else(|| {
+                panic!("expected a range reader, got {}", children[0].name())
+            });
+        assert_eq!(reader.fetch(), Some(7));
+    }
 }
diff --git a/ballista/scheduler/src/state/task_builder.rs 
b/ballista/scheduler/src/state/task_builder.rs
index f1ed152a2..a9ab5e502 100644
--- a/ballista/scheduler/src/state/task_builder.rs
+++ b/ballista/scheduler/src/state/task_builder.rs
@@ -336,7 +336,7 @@ fn select_output_partitions(
         ) else {
             return Ok(None);
         };
-        return Ok(Some(Arc::new(restricted)));
+        return Ok(Some(Arc::new(restricted.with_fetch_limit(reader.fetch()))));
     }
 
     // DataSourceExec: file-backed or in-memory scans.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to