Skip to content

Commit 0c7d830

Browse files
authored
Fix PartialOrd for logical plan nodes and expressions (#17438)
* Fix `PartialOrd` for DDL & DML Before the changes, `PartialOrd` could return `Some(Equal)` for two values that are not equal in `PartialEq` sense. This is violation of `PartialOrd` contract. The fix is to consult eq inside ord implementation. If ord thinks two instances are equal, but they are not equal in Eq sense, they are considered incomparable. * Fix `PartialOrd` for logical plan nodes and expressions Before the changes, `PartialOrd` could return `Some(Equal)` for two values that are not equal in `PartialEq` sense. This is violation of `PartialOrd` contract. The fix is to consult eq inside ord implementation. If ord thinks two instances are equal, but they are not equal in Eq sense, they are considered incomparable. * Link to issue
1 parent 5b833b9 commit 0c7d830

File tree

11 files changed

+99
-22
lines changed

11 files changed

+99
-22
lines changed

datafusion/expr/src/expr.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,10 @@ impl PartialOrd for Alias {
748748
let Some(Ordering::Equal) = cmp else {
749749
return cmp;
750750
};
751-
self.name.partial_cmp(&other.name)
751+
self.name
752+
.partial_cmp(&other.name)
753+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
754+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
752755
}
753756
}
754757

datafusion/expr/src/logical_plan/ddl.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ impl PartialOrd for CreateExternalTable {
292292
unbounded: &other.unbounded,
293293
constraints: &other.constraints,
294294
};
295-
comparable_self.partial_cmp(&comparable_other)
295+
comparable_self
296+
.partial_cmp(&comparable_other)
297+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
298+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
296299
}
297300
}
298301

@@ -348,6 +351,8 @@ impl PartialOrd for CreateCatalog {
348351
Some(Ordering::Equal) => self.if_not_exists.partial_cmp(&other.if_not_exists),
349352
cmp => cmp,
350353
}
354+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
355+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
351356
}
352357
}
353358

@@ -369,6 +374,8 @@ impl PartialOrd for CreateCatalogSchema {
369374
Some(Ordering::Equal) => self.if_not_exists.partial_cmp(&other.if_not_exists),
370375
cmp => cmp,
371376
}
377+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
378+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
372379
}
373380
}
374381

@@ -390,6 +397,8 @@ impl PartialOrd for DropTable {
390397
Some(Ordering::Equal) => self.if_exists.partial_cmp(&other.if_exists),
391398
cmp => cmp,
392399
}
400+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
401+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
393402
}
394403
}
395404

@@ -411,6 +420,8 @@ impl PartialOrd for DropView {
411420
Some(Ordering::Equal) => self.if_exists.partial_cmp(&other.if_exists),
412421
cmp => cmp,
413422
}
423+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
424+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
414425
}
415426
}
416427

@@ -437,6 +448,8 @@ impl PartialOrd for DropCatalogSchema {
437448
},
438449
cmp => cmp,
439450
}
451+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
452+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
440453
}
441454
}
442455

@@ -486,7 +499,10 @@ impl PartialOrd for CreateFunction {
486499
return_type: &other.return_type,
487500
params: &other.params,
488501
};
489-
comparable_self.partial_cmp(&comparable_other)
502+
comparable_self
503+
.partial_cmp(&comparable_other)
504+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
505+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
490506
}
491507
}
492508

@@ -566,6 +582,8 @@ impl PartialOrd for DropFunction {
566582
Some(Ordering::Equal) => self.if_exists.partial_cmp(&other.if_exists),
567583
cmp => cmp,
568584
}
585+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
586+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
569587
}
570588
}
571589

@@ -608,7 +626,10 @@ impl PartialOrd for CreateIndex {
608626
unique: &other.unique,
609627
if_not_exists: &other.if_not_exists,
610628
};
611-
comparable_self.partial_cmp(&comparable_other)
629+
comparable_self
630+
.partial_cmp(&comparable_other)
631+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
632+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
612633
}
613634
}
614635

datafusion/expr/src/logical_plan/dml.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ impl PartialOrd for CopyTo {
8181
},
8282
cmp => cmp,
8383
}
84+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
85+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
8486
}
8587
}
8688

@@ -217,6 +219,8 @@ impl PartialOrd for DmlStatement {
217219
},
218220
cmp => cmp,
219221
}
222+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
223+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
220224
}
221225
}
222226

datafusion/expr/src/logical_plan/extension.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub trait UserDefinedLogicalNode: fmt::Debug + Send + Sync {
150150
/// directly because it must remain object safe.
151151
fn dyn_hash(&self, state: &mut dyn Hasher);
152152

153-
/// Compare `other`, respecting requirements from [std::cmp::Eq].
153+
/// Compare `other`, respecting requirements from [Eq].
154154
///
155155
/// Note: consider using [`UserDefinedLogicalNodeCore`] instead of
156156
/// [`UserDefinedLogicalNode`] directly.
@@ -188,6 +188,9 @@ pub trait UserDefinedLogicalNode: fmt::Debug + Send + Sync {
188188
/// Note: [`UserDefinedLogicalNode`] is not constrained by [`Eq`]
189189
/// directly because it must remain object safe.
190190
fn dyn_eq(&self, other: &dyn UserDefinedLogicalNode) -> bool;
191+
192+
/// Compare `other`, respecting requirements from [PartialOrd].
193+
/// Must return `Some(Equal)` if and only if `self.dyn_eq(other)`.
191194
fn dyn_ord(&self, other: &dyn UserDefinedLogicalNode) -> Option<Ordering>;
192195

193196
/// Returns `true` if a limit can be safely pushed down through this
@@ -312,7 +315,7 @@ pub trait UserDefinedLogicalNodeCore:
312315
}
313316

314317
/// Automatically derive UserDefinedLogicalNode to `UserDefinedLogicalNode`
315-
/// to avoid boiler plate for implementing `as_any`, `Hash` and `PartialEq`
318+
/// to avoid boiler plate for implementing `as_any`, `Hash`, `PartialEq` and `PartialOrd`.
316319
impl<T: UserDefinedLogicalNodeCore> UserDefinedLogicalNode for T {
317320
fn as_any(&self) -> &dyn Any {
318321
self

datafusion/expr/src/logical_plan/plan.rs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,7 +2060,10 @@ pub struct EmptyRelation {
20602060
// Manual implementation needed because of `schema` field. Comparison excludes this field.
20612061
impl PartialOrd for EmptyRelation {
20622062
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2063-
self.produce_one_row.partial_cmp(&other.produce_one_row)
2063+
self.produce_one_row
2064+
.partial_cmp(&other.produce_one_row)
2065+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2066+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
20642067
}
20652068
}
20662069

@@ -2114,7 +2117,10 @@ pub struct Values {
21142117
// Manual implementation needed because of `schema` field. Comparison excludes this field.
21152118
impl PartialOrd for Values {
21162119
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2117-
self.values.partial_cmp(&other.values)
2120+
self.values
2121+
.partial_cmp(&other.values)
2122+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2123+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
21182124
}
21192125
}
21202126

@@ -2139,6 +2145,8 @@ impl PartialOrd for Projection {
21392145
Some(Ordering::Equal) => self.input.partial_cmp(&other.input),
21402146
cmp => cmp,
21412147
}
2148+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2149+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
21422150
}
21432151
}
21442152

@@ -2249,6 +2257,8 @@ impl PartialOrd for SubqueryAlias {
22492257
Some(Ordering::Equal) => self.alias.partial_cmp(&other.alias),
22502258
cmp => cmp,
22512259
}
2260+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2261+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
22522262
}
22532263
}
22542264

@@ -2606,7 +2616,10 @@ impl PartialOrd for TableScan {
26062616
filters: &other.filters,
26072617
fetch: &other.fetch,
26082618
};
2609-
comparable_self.partial_cmp(&comparable_other)
2619+
comparable_self
2620+
.partial_cmp(&comparable_other)
2621+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2622+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
26102623
}
26112624
}
26122625

@@ -2929,7 +2942,10 @@ impl Union {
29292942
// Manual implementation needed because of `schema` field. Comparison excludes this field.
29302943
impl PartialOrd for Union {
29312944
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2932-
self.inputs.partial_cmp(&other.inputs)
2945+
self.inputs
2946+
.partial_cmp(&other.inputs)
2947+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
2948+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
29332949
}
29342950
}
29352951

@@ -3210,7 +3226,10 @@ impl PartialOrd for Explain {
32103226
stringified_plans: &other.stringified_plans,
32113227
logical_optimization_succeeded: &other.logical_optimization_succeeded,
32123228
};
3213-
comparable_self.partial_cmp(&comparable_other)
3229+
comparable_self
3230+
.partial_cmp(&comparable_other)
3231+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
3232+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
32143233
}
32153234
}
32163235

@@ -3233,6 +3252,8 @@ impl PartialOrd for Analyze {
32333252
Some(Ordering::Equal) => self.input.partial_cmp(&other.input),
32343253
cmp => cmp,
32353254
}
3255+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
3256+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
32363257
}
32373258
}
32383259

@@ -3460,7 +3481,10 @@ impl PartialOrd for DistinctOn {
34603481
sort_expr: &other.sort_expr,
34613482
input: &other.input,
34623483
};
3463-
comparable_self.partial_cmp(&comparable_other)
3484+
comparable_self
3485+
.partial_cmp(&comparable_other)
3486+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
3487+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
34643488
}
34653489
}
34663490

@@ -3647,6 +3671,8 @@ impl PartialOrd for Aggregate {
36473671
}
36483672
cmp => cmp,
36493673
}
3674+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
3675+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
36503676
}
36513677
}
36523678

@@ -3919,7 +3945,10 @@ impl PartialOrd for Join {
39193945
join_constraint: &other.join_constraint,
39203946
null_equality: &other.null_equality,
39213947
};
3922-
comparable_self.partial_cmp(&comparable_other)
3948+
comparable_self
3949+
.partial_cmp(&comparable_other)
3950+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
3951+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
39233952
}
39243953
}
39253954

@@ -4084,7 +4113,10 @@ impl PartialOrd for Unnest {
40844113
dependency_indices: &other.dependency_indices,
40854114
options: &other.options,
40864115
};
4087-
comparable_self.partial_cmp(&comparable_other)
4116+
comparable_self
4117+
.partial_cmp(&comparable_other)
4118+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
4119+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
40884120
}
40894121
}
40904122

datafusion/expr/src/udaf.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -766,16 +766,14 @@ impl PartialEq for dyn AggregateUDFImpl {
766766
}
767767
}
768768

769-
// TODO (https://github.com/apache/datafusion/issues/17064) PartialOrd is not consistent with PartialEq for `dyn AggregateUDFImpl` and it should be
770-
// Manual implementation of `PartialOrd`
771-
// There might be some wackiness with it, but this is based on the impl of eq for AggregateUDFImpl
772-
// https://users.rust-lang.org/t/how-to-compare-two-trait-objects-for-equality/88063/5
773769
impl PartialOrd for dyn AggregateUDFImpl {
774770
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
775771
match self.name().partial_cmp(other.name()) {
776772
Some(Ordering::Equal) => self.signature().partial_cmp(other.signature()),
777773
cmp => cmp,
778774
}
775+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
776+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
779777
}
780778
}
781779

datafusion/expr/src/udwf.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,13 +434,14 @@ impl PartialEq for dyn WindowUDFImpl {
434434
}
435435
}
436436

437-
// TODO (https://github.com/apache/datafusion/issues/17064) PartialOrd is not consistent with PartialEq for `dyn WindowUDFImpl` and it should be
438437
impl PartialOrd for dyn WindowUDFImpl {
439438
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
440439
match self.name().partial_cmp(other.name()) {
441440
Some(Ordering::Equal) => self.signature().partial_cmp(other.signature()),
442441
cmp => cmp,
443442
}
443+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
444+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
444445
}
445446
}
446447

datafusion/optimizer/src/optimize_projections/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,8 @@ mod tests {
902902
Some(Ordering::Equal) => self.input.partial_cmp(&other.input),
903903
cmp => cmp,
904904
}
905+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
906+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
905907
}
906908
}
907909

@@ -989,6 +991,8 @@ mod tests {
989991
}
990992
cmp => cmp,
991993
}
994+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
995+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
992996
}
993997
}
994998

datafusion/optimizer/src/push_down_filter.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1966,7 +1966,10 @@ mod tests {
19661966
// Manual implementation needed because of `schema` field. Comparison excludes this field.
19671967
impl PartialOrd for NoopPlan {
19681968
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1969-
self.input.partial_cmp(&other.input)
1969+
self.input
1970+
.partial_cmp(&other.input)
1971+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
1972+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
19701973
}
19711974
}
19721975

datafusion/optimizer/src/push_down_limit.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,10 @@ mod test {
312312
// Manual implementation needed because of `schema` field. Comparison excludes this field.
313313
impl PartialOrd for NoopPlan {
314314
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
315-
self.input.partial_cmp(&other.input)
315+
self.input
316+
.partial_cmp(&other.input)
317+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
318+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
316319
}
317320
}
318321

@@ -365,7 +368,10 @@ mod test {
365368
// Manual implementation needed because of `schema` field. Comparison excludes this field.
366369
impl PartialOrd for NoLimitNoopPlan {
367370
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
368-
self.input.partial_cmp(&other.input)
371+
self.input
372+
.partial_cmp(&other.input)
373+
// TODO (https://github.com/apache/datafusion/issues/17477) avoid recomparing all fields
374+
.filter(|cmp| *cmp != Ordering::Equal || self == other)
369375
}
370376
}
371377

0 commit comments

Comments
 (0)