@@ -19,8 +19,8 @@ use std::sync::Arc;
1919
2020use datafusion_common:: Result ;
2121use 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} ;
2525use datafusion_physical_plan:: coalesce_partitions:: CoalescePartitionsExec ;
2626use datafusion_physical_plan:: limit:: { GlobalLimitExec , LocalLimitExec } ;
@@ -32,6 +32,25 @@ use datafusion_physical_plan::union::UnionExec;
3232use datafusion_physical_plan:: windows:: { BoundedWindowAggExec , WindowAggExec } ;
3333use 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(
220233pub 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