Skip to content

Commit 4cbcc31

Browse files
committed
feat: add grouped equivalence properties
1 parent bb3dc16 commit 4cbcc31

18 files changed

Lines changed: 901 additions & 42 deletions

File tree

‎datafusion/expr-common/src/sort_properties.rs‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,24 @@ use arrow::datatypes::DataType;
3737
pub enum SortProperties {
3838
/// Use the ordinary [`SortOptions`] struct to represent ordered data:
3939
Ordered(SortOptions),
40-
// This alternative represents unordered data:
40+
/// Within each partition, all rows with the same value for this expression
41+
/// form one contiguous run. The runs may occur in any order.
42+
Grouped,
43+
/// This alternative represents unordered data:
4144
#[default]
4245
Unordered,
43-
// Singleton is used for single-valued literal numbers:
46+
/// Singleton is used for single-valued literal numbers:
4447
Singleton,
4548
}
4649

4750
impl SortProperties {
4851
pub fn add(&self, rhs: &Self) -> Self {
4952
match (self, rhs) {
53+
// Addition can collapse distinct values (for example through
54+
// floating-point rounding), which may join non-adjacent groups.
55+
(Self::Grouped, Self::Singleton) | (Self::Singleton, Self::Grouped) => {
56+
Self::Unordered
57+
}
5058
(Self::Singleton, _) => *rhs,
5159
(_, Self::Singleton) => *self,
5260
(Self::Ordered(lhs), Self::Ordered(rhs))
@@ -65,6 +73,11 @@ impl SortProperties {
6573
pub fn sub(&self, rhs: &Self) -> Self {
6674
match (self, rhs) {
6775
(Self::Singleton, Self::Singleton) => Self::Singleton,
76+
// Subtraction can collapse distinct values (for example through
77+
// floating-point rounding), which may join non-adjacent groups.
78+
(Self::Grouped, Self::Singleton) | (Self::Singleton, Self::Grouped) => {
79+
Self::Unordered
80+
}
6881
(Self::Singleton, Self::Ordered(rhs)) => Self::Ordered(SortOptions {
6982
descending: !rhs.descending,
7083
nulls_first: rhs.nulls_first,
@@ -89,6 +102,9 @@ impl SortProperties {
89102
descending: !rhs.descending,
90103
nulls_first: rhs.nulls_first,
91104
}),
105+
// Comparisons can map several non-adjacent grouped values to the
106+
// same boolean value, so they do not preserve grouping.
107+
(Self::Grouped, Self::Singleton) => Self::Unordered,
92108
(_, Self::Singleton) => *self,
93109
(Self::Ordered(lhs), Self::Ordered(rhs))
94110
if lhs.descending != rhs.descending
@@ -147,6 +163,7 @@ mod sort_properties_test {
147163
const ASC_NL: SortProperties = ordered(false, false);
148164
const DESC_NF: SortProperties = ordered(true, true);
149165
const DESC_NL: SortProperties = ordered(true, false);
166+
const GROUPED: SortProperties = SortProperties::Grouped;
150167
const UNORDERED: SortProperties = SortProperties::Unordered;
151168
const SINGLETON: SortProperties = SortProperties::Singleton;
152169

@@ -197,6 +214,27 @@ mod sort_properties_test {
197214
SINGLETON,
198215
SINGLETON,
199216
),
217+
(
218+
"add: may collapse grouped values",
219+
SortProperties::add,
220+
GROUPED,
221+
SINGLETON,
222+
UNORDERED,
223+
),
224+
(
225+
"sub: may collapse grouped values",
226+
SortProperties::sub,
227+
GROUPED,
228+
SINGLETON,
229+
UNORDERED,
230+
),
231+
(
232+
"comparison does not preserve grouping",
233+
SortProperties::gt_or_gteq,
234+
GROUPED,
235+
SINGLETON,
236+
UNORDERED,
237+
),
200238
// `and` keeps ASC NULLS LAST / DESC NULLS FIRST, `or` keeps ASC
201239
// NULLS FIRST / DESC NULLS LAST. Both are commutative.
202240
(
@@ -482,7 +520,7 @@ impl Neg for SortProperties {
482520
#[derive(Debug, Clone)]
483521
pub struct ExprProperties {
484522
/// Properties that describe the sorting behavior of the expression,
485-
/// such as whether it is ordered, unordered, or a singleton value.
523+
/// such as whether it is ordered, grouped, unordered, or a singleton value.
486524
pub sort_properties: SortProperties,
487525
/// A closed interval representing the range of possible values for
488526
/// the expression. Used to compute reliable bounds.

‎datafusion/expr/src/udf.rs‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,20 @@ impl ScalarUDF {
360360

361361
/// Calculates the [`SortProperties`] of this function based on its
362362
/// children's properties.
363+
///
364+
/// [`SortProperties::Grouped`] is retained only when the implementation
365+
/// also reports that the transformation is strictly order-preserving. A
366+
/// many-to-one function can otherwise make an output value occur in
367+
/// multiple non-adjacent runs.
363368
pub fn output_ordering(&self, inputs: &[ExprProperties]) -> Result<SortProperties> {
364-
self.inner.output_ordering(inputs)
369+
let sort_properties = self.inner.output_ordering(inputs)?;
370+
if sort_properties == SortProperties::Grouped
371+
&& !self.inner.strictly_order_preserving(inputs)?
372+
{
373+
Ok(SortProperties::Unordered)
374+
} else {
375+
Ok(sort_properties)
376+
}
365377
}
366378

367379
pub fn preserves_lex_ordering(&self, inputs: &[ExprProperties]) -> Result<bool> {
@@ -936,7 +948,12 @@ pub trait ScalarUDFImpl: Debug + DynEq + DynHash + Send + Sync + Any {
936948
Ok(Some(vec![]))
937949
}
938950

939-
/// Calculates the [`SortProperties`] of this function based on its children's properties.
951+
/// Calculates the [`SortProperties`] of this function based on its children's
952+
/// properties.
953+
///
954+
/// The [`ScalarUDF`] wrapper retains a [`SortProperties::Grouped`] result
955+
/// only when [`Self::strictly_order_preserving`] also returns `true` for
956+
/// the same inputs.
940957
fn output_ordering(&self, inputs: &[ExprProperties]) -> Result<SortProperties> {
941958
if !self.preserves_lex_ordering(inputs)? {
942959
return Ok(SortProperties::Unordered);

‎datafusion/ffi/src/expr/expr_properties.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ pub enum FFI_SortProperties {
6767
Ordered(FFI_SortOptions),
6868
Unordered,
6969
Singleton,
70+
Grouped,
7071
}
7172

7273
impl From<&SortProperties> for FFI_SortProperties {
7374
fn from(value: &SortProperties) -> Self {
7475
match value {
7576
SortProperties::Unordered => FFI_SortProperties::Unordered,
7677
SortProperties::Singleton => FFI_SortProperties::Singleton,
78+
SortProperties::Grouped => FFI_SortProperties::Grouped,
7779
SortProperties::Ordered(o) => FFI_SortProperties::Ordered(o.into()),
7880
}
7981
}
@@ -84,6 +86,7 @@ impl From<&FFI_SortProperties> for SortProperties {
8486
match value {
8587
FFI_SortProperties::Unordered => SortProperties::Unordered,
8688
FFI_SortProperties::Singleton => SortProperties::Singleton,
89+
FFI_SortProperties::Grouped => SortProperties::Grouped,
8790
FFI_SortProperties::Ordered(o) => SortProperties::Ordered(o.into()),
8891
}
8992
}
@@ -113,3 +116,14 @@ impl From<&FFI_SortOptions> for SortOptions {
113116
}
114117
}
115118
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn grouped_sort_properties_round_trip() {
126+
let ffi = FFI_SortProperties::from(&SortProperties::Grouped);
127+
assert_eq!(SortProperties::from(&ffi), SortProperties::Grouped);
128+
}
129+
}

‎datafusion/ffi/src/session/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ mod tests {
986986
let physical_plan = foreign_session.create_physical_plan(&logical_plan).await?;
987987
assert_eq!(
988988
format!("{physical_plan:?}"),
989-
"EmptyExec { schema: Schema { fields: [], metadata: {} }, partitions: 1, cache: PlanProperties { eq_properties: EquivalenceProperties { eq_group: EquivalenceGroup { map: {}, classes: [] }, oeq_class: OrderingEquivalenceClass { orderings: [] }, oeq_cache: OrderingEquivalenceCache { normal_cls: OrderingEquivalenceClass { orderings: [] }, leading_map: {} }, constraints: Constraints { inner: [] }, schema: Schema { fields: [], metadata: {} } }, partitioning: UnknownPartitioning(1), emission_type: Incremental, boundedness: Bounded, evaluation_type: Lazy, scheduling_type: Cooperative, output_ordering: None } }"
989+
"EmptyExec { schema: Schema { fields: [], metadata: {} }, partitions: 1, cache: PlanProperties { eq_properties: EquivalenceProperties { eq_group: EquivalenceGroup { map: {}, classes: [] }, oeq_class: OrderingEquivalenceClass { orderings: [] }, geq_class: GroupingEquivalenceClass { groupings: [] }, oeq_cache: OrderingEquivalenceCache { normal_cls: OrderingEquivalenceClass { orderings: [] }, leading_map: {} }, geq_cache: GroupingEquivalenceClass { groupings: [] }, constraints: Constraints { inner: [] }, schema: Schema { fields: [], metadata: {} } }, partitioning: UnknownPartitioning(1), emission_type: Incremental, boundedness: Bounded, evaluation_type: Lazy, scheduling_type: Cooperative, output_ordering: None } }"
990990
);
991991

992992
assert_eq!(

‎datafusion/ffi/src/tests/udf_udaf_udwf.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use arrow_schema::DataType;
2121
use datafusion_catalog::TableFunctionImpl;
2222
use datafusion_common::ScalarValue;
2323
use datafusion_common::config::ConfigOptions;
24-
use datafusion_expr::sort_properties::ExprProperties;
24+
use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
2525
use datafusion_expr::{
2626
AggregateUDF, ColumnarValue, ExpressionPlacement, ScalarFunctionArgs, ScalarUDF,
2727
ScalarUDFImpl, Signature, Volatility, WindowUDF,
@@ -167,6 +167,11 @@ impl ScalarUDFImpl for PlacementUDF {
167167
&self,
168168
inputs: &[ExprProperties],
169169
) -> datafusion_common::Result<bool> {
170+
// This test-only sentinel verifies that the new `Grouped` variant
171+
// survives the cross-library `ExprProperties` conversion.
172+
if matches!(inputs, [input] if input.sort_properties == SortProperties::Grouped) {
173+
return Ok(true);
174+
}
170175
Ok(inputs.iter().all(|input| input.preserves_lex_ordering))
171176
}
172177
}

‎datafusion/ffi/src/udf/mod.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ impl ScalarUDFImpl for ForeignScalarUDF {
553553
#[cfg(test)]
554554
mod tests {
555555
use super::*;
556+
use datafusion_expr::sort_properties::SortProperties;
556557

557558
#[derive(Debug, PartialEq, Eq, Hash)]
558559
struct PlacementUDF {
@@ -594,6 +595,13 @@ mod tests {
594595
return internal_err!("preserves_lex_ordering requires an input");
595596
}
596597

598+
// This test-only sentinel verifies that the new `Grouped` variant
599+
// travels through the foreign path.
600+
if matches!(inputs, [input] if input.sort_properties == SortProperties::Grouped)
601+
{
602+
return Ok(true);
603+
}
604+
597605
Ok(inputs.iter().all(|input| input.preserves_lex_ordering))
598606
}
599607

@@ -692,6 +700,8 @@ mod tests {
692700
.preserves_lex_ordering(&[preserves, does_not_preserve])
693701
.unwrap()
694702
);
703+
let grouped = ExprProperties::new_unknown().with_order(SortProperties::Grouped);
704+
assert!(foreign_udf.preserves_lex_ordering(&[grouped]).unwrap());
695705
assert!(foreign_udf.preserves_lex_ordering(&[]).is_err());
696706

697707
let updated = foreign_udf

‎datafusion/ffi/tests/ffi_udf.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ mod tests {
2727
use datafusion::prelude::{SessionContext, col};
2828
use datafusion_execution::config::SessionConfig;
2929
use datafusion_expr::lit;
30-
use datafusion_expr::sort_properties::ExprProperties;
30+
use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
3131
use datafusion_ffi::tests::create_record_batch;
3232
use datafusion_ffi::tests::utils::get_module;
3333
use std::sync::Arc;
@@ -119,6 +119,9 @@ mod tests {
119119
assert!(foreign_func.preserves_lex_ordering(std::slice::from_ref(&preserves))?);
120120
assert!(!foreign_func.preserves_lex_ordering(&[preserves, does_not_preserve])?);
121121

122+
let grouped = ExprProperties::new_unknown().with_order(SortProperties::Grouped);
123+
assert!(foreign_func.preserves_lex_ordering(&[grouped])?);
124+
122125
Ok(())
123126
}
124127

‎datafusion/functions/src/datetime/date_bin.rs‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,12 +812,14 @@ mod tests {
812812
use crate::datetime::date_bin::{DateBinFunc, date_bin_nanos_interval};
813813
use arrow::array::types::TimestampNanosecondType;
814814
use arrow::array::{Array, IntervalDayTimeArray, TimestampNanosecondArray};
815+
use arrow::compute::SortOptions;
815816
use arrow::compute::kernels::cast_utils::string_to_timestamp_nanos;
816817
use arrow::datatypes::{DataType, Field, FieldRef, TimeUnit};
817818

818819
use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano};
819820
use datafusion_common::{DataFusionError, ScalarValue};
820-
use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl};
821+
use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
822+
use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl};
821823

822824
use chrono::TimeDelta;
823825
use datafusion_common::config::ConfigOptions;
@@ -867,6 +869,26 @@ mod tests {
867869
);
868870
}
869871

872+
#[test]
873+
fn date_bin_does_not_propagate_grouping() -> Result<(), DataFusionError> {
874+
let udf = ScalarUDF::from(DateBinFunc::new());
875+
let step = ExprProperties::new_unknown().with_order(SortProperties::Singleton);
876+
let grouped = ExprProperties::new_unknown().with_order(SortProperties::Grouped);
877+
878+
assert_eq!(
879+
udf.output_ordering(&[step.clone(), grouped])?,
880+
SortProperties::Unordered
881+
);
882+
883+
let ordered = ExprProperties::new_unknown()
884+
.with_order(SortProperties::Ordered(SortOptions::default()));
885+
assert_eq!(
886+
udf.output_ordering(&[step, ordered])?,
887+
SortProperties::Ordered(SortOptions::default())
888+
);
889+
Ok(())
890+
}
891+
870892
#[test]
871893
fn test_date_bin() {
872894
let return_field = &Arc::new(Field::new(

0 commit comments

Comments
 (0)