Skip to content

Commit 15eb10b

Browse files
committed
fix: keep heterogeneous constants in required orderings (add_sort_above)
`add_sort_above` and `add_sort_above_with_distribution` pruned every expression that equivalence properties reported as constant, via `is_expr_constant(..).is_none()`. That treats `AcrossPartitions::Heterogeneous` the same as `AcrossPartitions::Uniform`, which is wrong. A `Heterogeneous` constant is constant *within* each partition but varies *across* partitions (e.g. `sample` in `SELECT 1 AS sample, c FROM t UNION ALL SELECT 2 AS sample, c FROM t`). Both helpers build a partition-preserving `SortExec` whose output is merged across partitions downstream, where such a column is still the leading ordering discriminator. Dropping it silently discards the global ordering, producing non-deterministic output order for `ORDER BY sample, c` once the plan is rooted at a sink / output requirement. Only `AcrossPartitions::Uniform` is globally constant and safe to drop. Introduce `is_uniform_constant` and use it at both prune sites. Adds a regression test that builds a union with a heterogeneous-constant column and asserts the key survives `add_sort_above`.
1 parent f87e817 commit 15eb10b

1 file changed

Lines changed: 97 additions & 10 deletions

File tree

  • datafusion/physical-optimizer/src

datafusion/physical-optimizer/src/utils.rs

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use std::sync::Arc;
1919

2020
use datafusion_common::Result;
2121
use datafusion_physical_expr::{
22-
Distribution, EquivalenceProperties, LexOrdering, LexRequirement, Partitioning,
23-
PhysicalExpr, physical_exprs_equal,
22+
AcrossPartitions, Distribution, EquivalenceProperties, LexOrdering, LexRequirement,
23+
Partitioning, PhysicalExpr, physical_exprs_equal,
2424
};
2525
use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec;
2626
use datafusion_physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
@@ -32,6 +32,25 @@ use datafusion_physical_plan::union::UnionExec;
3232
use datafusion_physical_plan::windows::{BoundedWindowAggExec, WindowAggExec};
3333
use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties};
3434

35+
/// Returns `true` when `expr` is a *globally* constant column in `eq_properties`
36+
/// and can therefore be safely dropped from a required ordering.
37+
///
38+
/// Only [`AcrossPartitions::Uniform`] qualifies. An
39+
/// [`AcrossPartitions::Heterogeneous`] value is constant *within* each partition
40+
/// but may differ *across* partitions, so it must be kept in the sort key: the
41+
/// `SortExec` these helpers build preserves partitioning, and its output is
42+
/// merged across partitions downstream, where the column is still an ordering
43+
/// discriminator. Dropping it there silently loses the global ordering.
44+
fn is_uniform_constant(
45+
eq_properties: &EquivalenceProperties,
46+
expr: &Arc<dyn PhysicalExpr>,
47+
) -> bool {
48+
matches!(
49+
eq_properties.is_expr_constant(expr),
50+
Some(AcrossPartitions::Uniform(_))
51+
)
52+
}
53+
3554
/// This utility function adds a `SortExec` above an operator according to the
3655
/// given ordering requirements while preserving the original partitioning.
3756
///
@@ -45,10 +64,7 @@ pub fn add_sort_above<T: Clone + Default>(
4564
) -> PlanContext<T> {
4665
let mut sort_reqs: Vec<_> = sort_requirements.into();
4766
sort_reqs.retain(|sort_expr| {
48-
node.plan
49-
.equivalence_properties()
50-
.is_expr_constant(&sort_expr.expr)
51-
.is_none()
67+
!is_uniform_constant(node.plan.equivalence_properties(), &sort_expr.expr)
5268
});
5369
let sort_exprs = sort_reqs.into_iter().map(Into::into).collect::<Vec<_>>();
5470
let Some(ordering) = LexOrdering::new(sort_exprs) else {
@@ -73,10 +89,7 @@ pub fn add_sort_above_with_distribution<T: Clone + Default>(
7389
) -> PlanContext<T> {
7490
let mut sort_reqs: Vec<_> = sort_requirements.into();
7591
sort_reqs.retain(|sort_expr| {
76-
node.plan
77-
.equivalence_properties()
78-
.is_expr_constant(&sort_expr.expr)
79-
.is_none()
92+
!is_uniform_constant(node.plan.equivalence_properties(), &sort_expr.expr)
8093
});
8194
let sort_exprs = sort_reqs.into_iter().map(Into::into).collect::<Vec<_>>();
8295
let Some(ordering) = LexOrdering::new(sort_exprs) else {
@@ -220,3 +233,77 @@ pub(crate) fn range_partitioning_satisfies_key_partitioning(
220233
pub fn is_limit(plan: &Arc<dyn ExecutionPlan>) -> bool {
221234
plan.is::<GlobalLimitExec>() || plan.is::<LocalLimitExec>()
222235
}
236+
237+
#[cfg(test)]
238+
mod tests {
239+
use super::*;
240+
241+
use arrow::compute::SortOptions;
242+
use arrow::datatypes::{DataType, Field, Schema};
243+
use datafusion_physical_expr::PhysicalSortRequirement;
244+
use datafusion_physical_expr::expressions::{col, lit};
245+
use datafusion_physical_expr::projection::ProjectionExpr;
246+
use datafusion_physical_plan::displayable;
247+
use datafusion_physical_plan::empty::EmptyExec;
248+
use datafusion_physical_plan::projection::ProjectionExec;
249+
250+
/// A `UnionExec` whose `sample` column is 1 on the left branch and 2 on the
251+
/// right makes `sample` a `Heterogeneous` constant: constant within each
252+
/// partition, but different across them. A required ordering of
253+
/// `[sample, other]` must retain `sample`; the `SortExec` built here
254+
/// preserves partitioning and its output is merged across partitions
255+
/// downstream, where `sample` is still the leading ordering discriminator.
256+
/// Regression test: `add_sort_above` used to drop every constant, uniform
257+
/// or not, and silently discarded the global ordering.
258+
#[test]
259+
fn add_sort_above_keeps_heterogeneous_constant_key() -> Result<()> {
260+
let schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, true)]));
261+
262+
let branch = |value: i32| -> Result<Arc<dyn ExecutionPlan>> {
263+
let source = Arc::new(EmptyExec::new(Arc::clone(&schema)));
264+
let projection = ProjectionExec::try_new(
265+
vec![
266+
ProjectionExpr {
267+
expr: lit(value),
268+
alias: "sample".to_string(),
269+
},
270+
ProjectionExpr {
271+
expr: col("c", &schema)?,
272+
alias: "other".to_string(),
273+
},
274+
],
275+
source,
276+
)?;
277+
Ok(Arc::new(projection))
278+
};
279+
280+
let union: Arc<dyn ExecutionPlan> =
281+
UnionExec::try_new(vec![branch(1)?, branch(2)?])?;
282+
let union_schema = union.schema();
283+
284+
// Precondition: the union reports `sample` as a heterogeneous constant.
285+
let sample = col("sample", &union_schema)?;
286+
assert!(matches!(
287+
union.equivalence_properties().is_expr_constant(&sample),
288+
Some(AcrossPartitions::Heterogeneous)
289+
));
290+
291+
let asc = SortOptions::default();
292+
let requirement: LexRequirement = [
293+
PhysicalSortRequirement::new(sample, Some(asc)),
294+
PhysicalSortRequirement::new(col("other", &union_schema)?, Some(asc)),
295+
]
296+
.into();
297+
298+
let node = PlanContext::<()>::new_default(union);
299+
let result = add_sort_above(node, requirement, None);
300+
301+
let plan = displayable(result.plan.as_ref()).indent(true).to_string();
302+
assert!(
303+
plan.contains("sample@0 ASC"),
304+
"add_sort_above dropped the heterogeneous-constant sort key:\n{plan}"
305+
);
306+
307+
Ok(())
308+
}
309+
}

0 commit comments

Comments
 (0)