Repository navigation
fix: preserve required ordering in limit pushdown #24231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cd7d651
c48afbf
90126f4
7614aeb
6ff41f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,6 +70,7 @@ use datafusion_common::error::Result; | |
| use datafusion_common::stats::Precision; | ||
| use datafusion_common::tree_node::{Transformed, TreeNodeRecursion}; | ||
| use datafusion_common::utils::combine_limit; | ||
| use datafusion_physical_expr::LexOrdering; | ||
| use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec; | ||
| use datafusion_physical_plan::empty::EmptyExec; | ||
| use datafusion_physical_plan::limit::{GlobalLimitExec, LocalLimitExec}; | ||
|
|
@@ -92,11 +93,11 @@ pub struct LimitPushdown {} | |
| /// | ||
| /// [`LimitPushdown`]: crate::limit_pushdown::LimitPushdown | ||
| #[derive(Default, Clone, Debug)] | ||
| pub struct GlobalRequirements { | ||
| pub(crate) struct GlobalRequirements { | ||
| fetch: Option<usize>, | ||
| skip: usize, | ||
| satisfied: bool, | ||
| preserve_order: bool, | ||
| required_ordering: Option<LexOrdering>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| impl LimitPushdown { | ||
|
|
@@ -116,7 +117,7 @@ impl PhysicalOptimizerRule for LimitPushdown { | |
| fetch: None, | ||
| skip: 0, | ||
| satisfied: false, | ||
| preserve_order: false, | ||
| required_ordering: None, | ||
| }; | ||
| pushdown_limits(plan, global_state) | ||
| } | ||
|
|
@@ -134,7 +135,7 @@ struct LimitInfo { | |
| input: Arc<dyn ExecutionPlan>, | ||
| fetch: Option<usize>, | ||
| skip: usize, | ||
| preserve_order: bool, | ||
| required_ordering: Option<LexOrdering>, | ||
| } | ||
|
|
||
| /// This function is the main helper function of the `LimitPushDown` rule. | ||
|
|
@@ -144,7 +145,7 @@ struct LimitInfo { | |
| /// | ||
| /// If a limit is encountered, a [`TreeNodeRecursion::Stop`] is returned. Otherwise, | ||
| /// return a [`TreeNodeRecursion::Continue`]. | ||
| pub fn pushdown_limit_helper( | ||
| pub(crate) fn pushdown_limit_helper( | ||
| mut pushdown_plan: Arc<dyn ExecutionPlan>, | ||
| mut global_state: GlobalRequirements, | ||
| ) -> Result<(Transformed<Arc<dyn ExecutionPlan>>, GlobalRequirements)> { | ||
|
|
@@ -160,7 +161,7 @@ pub fn pushdown_limit_helper( | |
| ); | ||
| global_state.skip = skip; | ||
| global_state.fetch = fetch; | ||
| global_state.preserve_order = limit_info.preserve_order; | ||
| global_state.required_ordering = limit_info.required_ordering; | ||
| global_state.satisfied = false; | ||
|
|
||
| if let Some(fetch) = fetch | ||
|
|
@@ -219,6 +220,7 @@ pub fn pushdown_limit_helper( | |
| pushdown_plan, | ||
| global_state.skip, | ||
| None, | ||
| global_state.required_ordering.clone(), | ||
| )), | ||
| global_state, | ||
| )) | ||
|
|
@@ -243,8 +245,12 @@ pub fn pushdown_limit_helper( | |
| // Execution plans can't (yet) handle skip, so if we have one, | ||
| // we still need to add a global limit | ||
| if global_state.skip > 0 { | ||
| new_plan = | ||
| add_global_limit(new_plan, global_state.skip, global_state.fetch); | ||
| new_plan = add_global_limit( | ||
| new_plan, | ||
| global_state.skip, | ||
| global_state.fetch, | ||
| global_state.required_ordering.clone(), | ||
| ); | ||
| } | ||
| global_state.fetch = skip_and_fetch; | ||
| global_state.skip = 0; | ||
|
|
@@ -260,6 +266,7 @@ pub fn pushdown_limit_helper( | |
| pushdown_plan, | ||
| global_state.skip, | ||
| global_fetch, | ||
| global_state.required_ordering.clone(), | ||
| )), | ||
| global_state, | ||
| )) | ||
|
|
@@ -278,7 +285,7 @@ pub fn pushdown_limit_helper( | |
| if global_state.satisfied { | ||
| if let Some(plan_with_fetch) = maybe_fetchable { | ||
| let plan_with_preserve_order = plan_with_fetch | ||
| .with_preserve_order(global_state.preserve_order) | ||
| .with_preserve_order(global_state.required_ordering.is_some()) | ||
| .unwrap_or(plan_with_fetch); | ||
| Ok((Transformed::yes(plan_with_preserve_order), global_state)) | ||
| } else { | ||
|
|
@@ -288,20 +295,26 @@ pub fn pushdown_limit_helper( | |
| global_state.satisfied = true; | ||
| pushdown_plan = if let Some(plan_with_fetch) = maybe_fetchable { | ||
| let plan_with_preserve_order = plan_with_fetch | ||
| .with_preserve_order(global_state.preserve_order) | ||
| .with_preserve_order(global_state.required_ordering.is_some()) | ||
| .unwrap_or(plan_with_fetch); | ||
|
|
||
| if global_skip > 0 { | ||
| add_global_limit( | ||
| plan_with_preserve_order, | ||
| global_skip, | ||
| Some(global_fetch), | ||
| global_state.required_ordering.clone(), | ||
| ) | ||
| } else { | ||
| plan_with_preserve_order | ||
| } | ||
| } else { | ||
| add_limit(pushdown_plan, global_skip, global_fetch) | ||
| add_limit( | ||
| pushdown_plan, | ||
| global_skip, | ||
| global_fetch, | ||
| global_state.required_ordering.clone(), | ||
| ) | ||
| }; | ||
| Ok((Transformed::yes(pushdown_plan), global_state)) | ||
| } | ||
|
|
@@ -417,15 +430,15 @@ fn extract_limit(plan: &Arc<dyn ExecutionPlan>) -> Option<LimitInfo> { | |
| input: Arc::clone(global_limit.input()), | ||
| fetch: global_limit.fetch(), | ||
| skip: global_limit.skip(), | ||
| preserve_order: global_limit.required_ordering().is_some(), | ||
| required_ordering: global_limit.required_ordering().clone(), | ||
| }) | ||
| } else { | ||
| plan.downcast_ref::<LocalLimitExec>() | ||
| .map(|local_limit| LimitInfo { | ||
| input: Arc::clone(local_limit.input()), | ||
| fetch: Some(local_limit.fetch()), | ||
| skip: 0, | ||
| preserve_order: local_limit.required_ordering().is_some(), | ||
| required_ordering: local_limit.required_ordering().clone(), | ||
| }) | ||
| } | ||
| } | ||
|
|
@@ -435,17 +448,22 @@ fn combines_input_partitions(plan: &Arc<dyn ExecutionPlan>) -> bool { | |
| plan.is::<CoalescePartitionsExec>() || plan.is::<SortPreservingMergeExec>() | ||
| } | ||
|
|
||
| /// Adds a limit to the plan, chooses between global and local limits based on | ||
| /// skip value and the number of partitions. | ||
| /// Adds a limit to the plan, choosing between global and local limits | ||
| /// based on the skip value and the number of partitions. | ||
| fn add_limit( | ||
| pushdown_plan: Arc<dyn ExecutionPlan>, | ||
| skip: usize, | ||
| fetch: usize, | ||
| required_ordering: Option<LexOrdering>, | ||
| ) -> Arc<dyn ExecutionPlan> { | ||
| if skip > 0 || pushdown_plan.output_partitioning().partition_count() == 1 { | ||
| add_global_limit(pushdown_plan, skip, Some(fetch)) | ||
| add_global_limit(pushdown_plan, skip, Some(fetch), required_ordering) | ||
| } else { | ||
| Arc::new(LocalLimitExec::new(pushdown_plan, fetch + skip)) as _ | ||
| let mut limit = LocalLimitExec::new(pushdown_plan, fetch + skip); | ||
|
|
||
| limit.set_required_ordering(required_ordering); | ||
|
|
||
| Arc::new(limit) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -454,8 +472,10 @@ fn add_global_limit( | |
| pushdown_plan: Arc<dyn ExecutionPlan>, | ||
| skip: usize, | ||
| fetch: Option<usize>, | ||
| required_ordering: Option<LexOrdering>, | ||
| ) -> Arc<dyn ExecutionPlan> { | ||
| Arc::new(GlobalLimitExec::new(pushdown_plan, skip, fetch)) as _ | ||
| let mut limit = GlobalLimitExec::new(pushdown_plan, skip, fetch); | ||
| limit.set_required_ordering(required_ordering); | ||
| Arc::new(limit) | ||
| } | ||
|
|
||
| // See tests in datafusion/core/tests/physical_optimizer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,7 +67,6 @@ use datafusion_physical_plan::sorts::sort::SortExec; | |
| use datafusion_physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec; | ||
| use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties}; | ||
| use std::sync::Arc; | ||
|
|
||
| /// A PhysicalOptimizerRule that attempts to push down sort requirements to data sources. | ||
| /// | ||
| /// See module-level documentation for details. | ||
|
|
@@ -108,10 +107,16 @@ impl PhysicalOptimizerRule for PushdownSort { | |
| match sort_input.try_pushdown_sort(required_ordering)? { | ||
| SortOrderPushdownResult::Exact { inner } => { | ||
| // Preserve fetch (LIMIT) from the eliminated SortExec. | ||
| // Use LocalLimitExec (not Global) since input is multi-partition. | ||
| // Use LocalLimitExec (not Global) since input is multi-partition: | ||
| // this rule runs after distribution enforcement, and | ||
| // GlobalLimitExec requires a single input partition. | ||
| let inner = if let Some(fetch) = sort_child.fetch() { | ||
| inner.with_fetch(Some(fetch)).unwrap_or_else(|| { | ||
| Arc::new(LocalLimitExec::new(inner, fetch)) | ||
| let mut limit = LocalLimitExec::new(inner, fetch); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two new Fix: assert on the node in let plan = sort_exec_with_fetch(ordering.clone(), Some(10), source);
let optimized = PushdownSort::new()
.optimize(plan, &ConfigOptions::new())
.unwrap();
let limit = optimized.downcast_ref::<GlobalLimitExec>().unwrap();
assert_eq!(limit.required_ordering().as_ref(), Some(&ordering)); |
||
| limit.set_required_ordering(Some( | ||
| sort_child.expr().clone(), | ||
| )); | ||
| Arc::new(limit) | ||
| }) | ||
| } else { | ||
| inner | ||
|
|
@@ -171,7 +176,9 @@ impl PhysicalOptimizerRule for PushdownSort { | |
| // wrapping with GlobalLimitExec. | ||
| if let Some(fetch) = sort_exec.fetch() { | ||
| let inner = inner.with_fetch(Some(fetch)).unwrap_or_else(|| { | ||
| Arc::new(GlobalLimitExec::new(inner, 0, Some(fetch))) | ||
| let mut limit = GlobalLimitExec::new(inner, 0, Some(fetch)); | ||
| limit.set_required_ordering(Some(sort_exec.expr().clone())); | ||
| Arc::new(limit) | ||
| }); | ||
| Ok(Transformed::yes(inner)) | ||
| } else { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GlobalRequirementsandpushdown_limit_helperwent frompubtopub(crate). That is a larger break than theUnwindSafeloss it was meant to avoid, and the PR body does not declare it. An external caller that compiles onmainnow fails:Fix: keep both public. If the removal is intended, declare it under "user-facing changes" and request the
api changelabel.