Skip to content

Commit ef46819

Browse files
authored
Merge branch 'main' into metadata-join-swap
2 parents 8fe3468 + 096012e commit ef46819

8 files changed

Lines changed: 417 additions & 196 deletions

File tree

datafusion/physical-optimizer/src/window_topn.rs

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ use datafusion_physical_expr::window::StandardWindowExpr;
6161
use datafusion_physical_plan::ExecutionPlan;
6262
use datafusion_physical_plan::filter::FilterExec;
6363
use datafusion_physical_plan::projection::ProjectionExec;
64+
use datafusion_physical_plan::repartition::RepartitionExec;
6465
use datafusion_physical_plan::sorts::partitioned_topk::{
6566
PartitionedTopKExec, WindowFnKind,
6667
};
@@ -140,24 +141,25 @@ impl WindowTopN {
140141
// Step 2: Extract limit from predicate (rn <= K, rn < K, etc.)
141142
let (col_idx, limit_n) = extract_window_limit(filter.predicate())?;
142143

143-
// Step 3: Walk through optional ProjectionExec to find BoundedWindowAggExec
144+
// Step 3: Walk through optional ProjectionExec and RepartitionExec to find BoundedWindowAggExec
144145
let child = filter.input();
145-
let (window_exec, proj_between) = find_window_below(child)?;
146+
let (window_exec, intermediates) = find_window_below(child)?;
146147

147148
// Step 4: Verify col_idx references a supported window function output column
148-
let input_field_count = window_exec.input().schema().fields().len();
149+
let window_exec_typed = window_exec.downcast_ref::<BoundedWindowAggExec>()?;
150+
let sort_exec = window_exec_typed.input().downcast_ref::<SortExec>()?;
151+
let input_field_count = window_exec_typed.input().schema().fields().len();
149152
if col_idx < input_field_count {
150153
return None; // Filter is on an input column, not a window column
151154
}
152155
let window_expr_idx = col_idx - input_field_count;
153-
let window_exprs = window_exec.window_expr();
156+
let window_exprs = window_exec_typed.window_expr();
154157
if window_expr_idx >= window_exprs.len() {
155158
return None;
156159
}
157160
let fn_kind = supported_window_fn(&window_exprs[window_expr_idx])?;
158161

159-
// Step 5: Verify child of window is SortExec
160-
let sort_exec = window_exec.input().downcast_ref::<SortExec>()?;
162+
// Step 5: child of window is SortExec (verified above)
161163
let sort_child = sort_exec.input();
162164

163165
// Step 6: Determine partition_prefix_len from the window expression
@@ -190,28 +192,19 @@ impl WindowTopN {
190192
.ok()?;
191193

192194
// Step 8: Rebuild window with new child
193-
let new_window = Arc::clone(&child_as_arc(window_exec))
195+
let mut result = window_exec
194196
.with_new_children(vec![Arc::new(partitioned_topk)])
195197
.ok()?;
196198

197-
// Step 9: If ProjectionExec was between Filter and Window, rebuild it
198-
let result = match proj_between {
199-
Some(proj) => Arc::clone(&child_as_arc(proj))
200-
.with_new_children(vec![new_window])
201-
.ok()?,
202-
None => new_window,
203-
};
199+
// Step 9: Rebuild intermediate nodes (ProjectionExec/RepartitionExec)
200+
for node in intermediates.into_iter().rev() {
201+
result = node.with_new_children(vec![result]).ok()?;
202+
}
204203

205204
Some(result)
206205
}
207206
}
208207

209-
/// Helper to get an `Arc<dyn ExecutionPlan>` from a reference.
210-
/// We need this because `with_new_children` takes `Arc<Self>`.
211-
fn child_as_arc<T: ExecutionPlan + Clone + 'static>(plan: &T) -> Arc<dyn ExecutionPlan> {
212-
Arc::new(plan.clone())
213-
}
214-
215208
impl PhysicalOptimizerRule for WindowTopN {
216209
fn optimize(
217210
&self,
@@ -336,29 +329,32 @@ fn supported_window_fn(
336329
}
337330
}
338331

332+
type PlanAndIntermediates = (Arc<dyn ExecutionPlan>, Vec<Arc<dyn ExecutionPlan>>);
333+
339334
/// Walk below a plan node looking for a [`BoundedWindowAggExec`].
340335
///
341-
/// Handles two cases:
342-
/// - Direct child: `FilterExec → BoundedWindowAggExec`
343-
/// - With projection: `FilterExec → ProjectionExec → BoundedWindowAggExec`
336+
/// Handles sequences of `ProjectionExec` and `RepartitionExec`.
337+
/// This is safe because `PartitionedTopKExec` can be pushed below them:
338+
/// projections only provide aliases, and pushing the limit below repartitions
339+
/// is safe because the limit is computed per-partition.
344340
///
345-
/// Returns the window exec and an optional `ProjectionExec` in between,
346-
/// or `None` if no `BoundedWindowAggExec` is found within one or two levels.
347-
fn find_window_below(
348-
plan: &Arc<dyn ExecutionPlan>,
349-
) -> Option<(&BoundedWindowAggExec, Option<&ProjectionExec>)> {
350-
// Direct child is BoundedWindowAggExec
351-
if let Some(window) = plan.downcast_ref::<BoundedWindowAggExec>() {
352-
return Some((window, None));
353-
}
354-
355-
// Child is ProjectionExec with BoundedWindowAggExec below
356-
if let Some(proj) = plan.downcast_ref::<ProjectionExec>() {
357-
let proj_child = proj.input();
358-
if let Some(window) = proj_child.downcast_ref::<BoundedWindowAggExec>() {
359-
return Some((window, Some(proj)));
341+
/// Returns the window exec and a list of intermediate nodes to rebuild,
342+
/// or `None` if no `BoundedWindowAggExec` is found.
343+
fn find_window_below(plan: &Arc<dyn ExecutionPlan>) -> Option<PlanAndIntermediates> {
344+
let mut current = Arc::clone(plan);
345+
let mut intermediates = Vec::new();
346+
347+
loop {
348+
if current.downcast_ref::<BoundedWindowAggExec>().is_some() {
349+
return Some((current, intermediates));
350+
} else if current.downcast_ref::<ProjectionExec>().is_some()
351+
|| current.downcast_ref::<RepartitionExec>().is_some()
352+
{
353+
let next = Arc::clone(current.children().first()?);
354+
intermediates.push(current);
355+
current = next;
356+
} else {
357+
return None;
360358
}
361359
}
362-
363-
None
364360
}

datafusion/physical-plan/src/joins/utils.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ use crate::metrics::{
3333
};
3434
use crate::projection::{ProjectionExec, ProjectionExpr};
3535
use crate::{
36-
ColumnStatistics, ExecutionPlan, ExecutionPlanProperties, Partitioning, Statistics,
36+
ColumnStatistics, ExecutionPlan, ExecutionPlanProperties, Partitioning,
37+
RangePartitioning, Statistics,
3738
};
3839
// compatibility
3940
pub use super::join_filter::JoinFilter;
@@ -68,7 +69,7 @@ use datafusion_common::stats::Precision;
6869
use datafusion_common::utils::normalize_float_zero;
6970
use datafusion_common::{
7071
DataFusionError, JoinSide, JoinType, NullEquality, Result, SharedResult,
71-
not_impl_err, plan_err,
72+
internal_datafusion_err, not_impl_err, plan_err,
7273
};
7374
use datafusion_expr::Operator;
7475
use datafusion_expr::interval_arithmetic::Interval;
@@ -146,9 +147,19 @@ pub fn adjust_right_output_partitioning(
146147
Partitioning::Hash(new_exprs, *size)
147148
}
148149
Partitioning::Range(range) => {
149-
// Range partitioning optimizer propagation is tracked in
150-
// https://github.com/apache/datafusion/issues/22395
151-
Partitioning::UnknownPartitioning(range.partition_count())
150+
let ordering = add_offset_to_physical_sort_exprs(
151+
range.ordering().iter().cloned(),
152+
left_columns_len as _,
153+
)?;
154+
let ordering = LexOrdering::new(ordering).ok_or_else(|| {
155+
internal_datafusion_err!(
156+
"Offsetting range partitioning produced an empty ordering"
157+
)
158+
})?;
159+
Partitioning::Range(RangePartitioning::new(
160+
ordering,
161+
range.split_points().to_vec(),
162+
))
152163
}
153164
result => result.clone(),
154165
};
@@ -2468,7 +2479,7 @@ mod tests {
24682479
use arrow::datatypes::{DataType, Fields};
24692480
use arrow::error::{ArrowError, Result as ArrowResult};
24702481
use datafusion_common::stats::Precision::{Absent, Exact, Inexact};
2471-
use datafusion_common::{ScalarValue, arrow_datafusion_err, arrow_err};
2482+
use datafusion_common::{ScalarValue, SplitPoint, arrow_datafusion_err, arrow_err};
24722483
use datafusion_physical_expr::PhysicalSortExpr;
24732484

24742485
use rstest::rstest;
@@ -4131,6 +4142,53 @@ mod tests {
41314142
assert_eq!(result.column_statistics[1].byte_size, Inexact(256));
41324143
}
41334144

4145+
#[test]
4146+
fn test_adjust_right_output_partitioning_preserves_range() -> Result<()> {
4147+
let split_points = vec![
4148+
SplitPoint::new(vec![
4149+
ScalarValue::Int32(Some(10)),
4150+
ScalarValue::Int32(Some(100)),
4151+
]),
4152+
SplitPoint::new(vec![
4153+
ScalarValue::Int32(Some(20)),
4154+
ScalarValue::Int32(Some(50)),
4155+
]),
4156+
];
4157+
let range = RangePartitioning::try_new(
4158+
LexOrdering::new([
4159+
PhysicalSortExpr::new(
4160+
Arc::new(Column::new("a", 0)),
4161+
SortOptions::new(false, true),
4162+
),
4163+
PhysicalSortExpr::new(
4164+
Arc::new(Column::new("b", 2)),
4165+
SortOptions::new(true, false),
4166+
),
4167+
])
4168+
.unwrap(),
4169+
split_points.clone(),
4170+
)?;
4171+
4172+
let adjusted = adjust_right_output_partitioning(&Partitioning::Range(range), 3)?;
4173+
let expected = Partitioning::Range(RangePartitioning::new(
4174+
LexOrdering::new([
4175+
PhysicalSortExpr::new(
4176+
Arc::new(Column::new("a", 3)),
4177+
SortOptions::new(false, true),
4178+
),
4179+
PhysicalSortExpr::new(
4180+
Arc::new(Column::new("b", 5)),
4181+
SortOptions::new(true, false),
4182+
),
4183+
])
4184+
.unwrap(),
4185+
split_points,
4186+
));
4187+
4188+
assert_eq!(adjusted, expected);
4189+
Ok(())
4190+
}
4191+
41344192
#[test]
41354193
fn test_calculate_join_output_ordering() -> Result<()> {
41364194
let left_ordering = LexOrdering::new(vec![

datafusion/physical-plan/src/sorts/cursor.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
use std::cmp::Ordering;
19+
use std::fmt::Debug;
1920
use std::sync::Arc;
2021

2122
use arrow::array::{
@@ -32,7 +33,7 @@ use datafusion_execution::memory_pool::MemoryReservation;
3233
///
3334
/// This is a trait as there are several specialized implementations, such as for
3435
/// single columns or for normalized multi column keys ([`Rows`])
35-
pub trait CursorValues {
36+
pub trait CursorValues: Debug + Sync + Send {
3637
fn len(&self) -> usize;
3738

3839
/// Returns true if `l[l_idx] == r[r_idx]`
@@ -298,6 +299,7 @@ impl<T: ArrowNativeTypeOp> CursorValues for PrimitiveValues<T> {
298299
}
299300
}
300301

302+
#[derive(Debug)]
301303
pub struct ByteArrayValues<T: OffsetSizeTrait> {
302304
offsets: OffsetBuffer<T>,
303305
values: Buffer,

0 commit comments

Comments
 (0)