Skip to content

Commit db683f4

Browse files
committed
feat: add grouped equivalence properties
1 parent 6e369ce commit db683f4

18 files changed

Lines changed: 909 additions & 43 deletions

File tree

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,25 @@ 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+
/// Equal expression values occur in one contiguous run within each
41+
/// partition, including across record batch boundaries, but the runs have
42+
/// no defined order.
43+
Grouped,
44+
/// This alternative represents unordered data:
4145
#[default]
4246
Unordered,
43-
// Singleton is used for single-valued literal numbers:
47+
/// Singleton is used for single-valued literal numbers:
4448
Singleton,
4549
}
4650

4751
impl SortProperties {
4852
pub fn add(&self, rhs: &Self) -> Self {
4953
match (self, rhs) {
54+
// Addition can collapse distinct values (for example through
55+
// floating-point rounding), which may join non-adjacent groups.
56+
(Self::Grouped, Self::Singleton) | (Self::Singleton, Self::Grouped) => {
57+
Self::Unordered
58+
}
5059
(Self::Singleton, _) => *rhs,
5160
(_, Self::Singleton) => *self,
5261
(Self::Ordered(lhs), Self::Ordered(rhs))
@@ -65,6 +74,11 @@ impl SortProperties {
6574
pub fn sub(&self, rhs: &Self) -> Self {
6675
match (self, rhs) {
6776
(Self::Singleton, Self::Singleton) => Self::Singleton,
77+
// Subtraction can collapse distinct values (for example through
78+
// floating-point rounding), which may join non-adjacent groups.
79+
(Self::Grouped, Self::Singleton) | (Self::Singleton, Self::Grouped) => {
80+
Self::Unordered
81+
}
6882
(Self::Singleton, Self::Ordered(rhs)) => Self::Ordered(SortOptions {
6983
descending: !rhs.descending,
7084
nulls_first: rhs.nulls_first,
@@ -89,6 +103,9 @@ impl SortProperties {
89103
descending: !rhs.descending,
90104
nulls_first: rhs.nulls_first,
91105
}),
106+
// Comparisons can map several non-adjacent grouped values to the
107+
// same boolean value, so they do not preserve grouping.
108+
(Self::Grouped, Self::Singleton) => Self::Unordered,
92109
(_, Self::Singleton) => *self,
93110
(Self::Ordered(lhs), Self::Ordered(rhs))
94111
if lhs.descending != rhs.descending
@@ -147,6 +164,7 @@ mod sort_properties_test {
147164
const ASC_NL: SortProperties = ordered(false, false);
148165
const DESC_NF: SortProperties = ordered(true, true);
149166
const DESC_NL: SortProperties = ordered(true, false);
167+
const GROUPED: SortProperties = SortProperties::Grouped;
150168
const UNORDERED: SortProperties = SortProperties::Unordered;
151169
const SINGLETON: SortProperties = SortProperties::Singleton;
152170

@@ -197,6 +215,27 @@ mod sort_properties_test {
197215
SINGLETON,
198216
SINGLETON,
199217
),
218+
(
219+
"add: may collapse grouped values",
220+
SortProperties::add,
221+
GROUPED,
222+
SINGLETON,
223+
UNORDERED,
224+
),
225+
(
226+
"sub: may collapse grouped values",
227+
SortProperties::sub,
228+
GROUPED,
229+
SINGLETON,
230+
UNORDERED,
231+
),
232+
(
233+
"comparison does not preserve grouping",
234+
SortProperties::gt_or_gteq,
235+
GROUPED,
236+
SINGLETON,
237+
UNORDERED,
238+
),
200239
// `and` keeps ASC NULLS LAST / DESC NULLS FIRST, `or` keeps ASC
201240
// NULLS FIRST / DESC NULLS LAST. Both are commutative.
202241
(
@@ -482,7 +521,7 @@ impl Neg for SortProperties {
482521
#[derive(Debug, Clone)]
483522
pub struct ExprProperties {
484523
/// Properties that describe the sorting behavior of the expression,
485-
/// such as whether it is ordered, unordered, or a singleton value.
524+
/// such as whether it is ordered, grouped, unordered, or a singleton value.
486525
pub sort_properties: SortProperties,
487526
/// A closed interval representing the range of possible values for
488527
/// the expression. Used to compute reliable bounds.

‎datafusion/expr/src/udf.rs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,23 @@ impl ScalarUDF {
354354

355355
/// Calculates the [`SortProperties`] of this function based on its
356356
/// children's properties.
357+
///
358+
/// [`SortProperties::Grouped`] is retained only when the implementation
359+
/// also reports that the transformation is strictly order-preserving. A
360+
/// many-to-one function can otherwise make an output value occur in
361+
/// multiple non-adjacent runs.
357362
pub fn output_ordering(&self, inputs: &[ExprProperties]) -> Result<SortProperties> {
358-
self.inner.output_ordering(inputs)
363+
let sort_properties = self.inner.output_ordering(inputs)?;
364+
if sort_properties == SortProperties::Grouped
365+
&& !self.inner.strictly_order_preserving(inputs)?
366+
{
367+
// A merely monotone function may collapse non-adjacent input
368+
// groups to the same output value. Preserve `Grouped` only for a
369+
// one-to-one transformation.
370+
Ok(SortProperties::Unordered)
371+
} else {
372+
Ok(sort_properties)
373+
}
359374
}
360375

361376
pub fn preserves_lex_ordering(&self, inputs: &[ExprProperties]) -> Result<bool> {
@@ -919,7 +934,12 @@ pub trait ScalarUDFImpl: Debug + DynEq + DynHash + Send + Sync + Any {
919934
Ok(Some(vec![]))
920935
}
921936

922-
/// Calculates the [`SortProperties`] of this function based on its children's properties.
937+
/// Calculates the [`SortProperties`] of this function based on its children's
938+
/// properties.
939+
///
940+
/// The [`ScalarUDF`] wrapper retains a [`SortProperties::Grouped`] result
941+
/// only when [`Self::strictly_order_preserving`] also returns `true` for
942+
/// the same inputs.
923943
fn output_ordering(&self, inputs: &[ExprProperties]) -> Result<SortProperties> {
924944
if !self.preserves_lex_ordering(inputs)? {
925945
return Ok(SortProperties::Unordered);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,18 @@ pub enum FFI_SortProperties {
6767
Ordered(FFI_SortOptions),
6868
Unordered,
6969
Singleton,
70+
/// Equal values occur in one contiguous run within each partition.
71+
///
72+
/// Appended to preserve the discriminants of the existing variants.
73+
Grouped,
7074
}
7175

7276
impl From<&SortProperties> for FFI_SortProperties {
7377
fn from(value: &SortProperties) -> Self {
7478
match value {
7579
SortProperties::Unordered => FFI_SortProperties::Unordered,
7680
SortProperties::Singleton => FFI_SortProperties::Singleton,
81+
SortProperties::Grouped => FFI_SortProperties::Grouped,
7782
SortProperties::Ordered(o) => FFI_SortProperties::Ordered(o.into()),
7883
}
7984
}
@@ -84,6 +89,7 @@ impl From<&FFI_SortProperties> for SortProperties {
8489
match value {
8590
FFI_SortProperties::Unordered => SortProperties::Unordered,
8691
FFI_SortProperties::Singleton => SortProperties::Singleton,
92+
FFI_SortProperties::Grouped => SortProperties::Grouped,
8793
FFI_SortProperties::Ordered(o) => SortProperties::Ordered(o.into()),
8894
}
8995
}
@@ -113,3 +119,14 @@ impl From<&FFI_SortOptions> for SortOptions {
113119
}
114120
}
115121
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use super::*;
126+
127+
#[test]
128+
fn grouped_sort_properties_round_trip() {
129+
let ffi = FFI_SortProperties::from(&SortProperties::Grouped);
130+
assert_eq!(SortProperties::from(&ffi), SortProperties::Grouped);
131+
}
132+
}

‎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)