sunchao commented on code in PR #25053:
URL: https://github.com/apache/datafusion/pull/25053#discussion_r4027940670


##########
datafusion/physical-expr/src/equivalence/properties/joins.rs:
##########
@@ -107,6 +95,112 @@ pub fn join_equivalence_properties(
     Ok(result)
 }
 
+/// Append build orderings only when equal probe ordering values identify at 
most
+/// one build row. The suffix is then constant within each probe ordering 
group,
+/// even if probe rows repeat. For an outer join preserving the probe side, all
+/// join keys must be fixed within the group, and the caller must rule out 
filters,
+/// so the group cannot mix matched build rows with NULL-extended rows.
+fn unique_build_join_orderings(
+    probe: &EquivalenceProperties,
+    build: &EquivalenceProperties,
+    on: &[(PhysicalExprRef, PhysicalExprRef)],
+    probe_side: JoinSide,
+    preserves_unmatched_probe: bool,
+    null_equality: NullEquality,
+) -> Result<OrderingEquivalenceClass> {
+    if build.constraints().is_empty() || build.oeq_class().is_empty() {
+        return Ok(OrderingEquivalenceClass::default());
+    }
+    let on = on
+        .iter()
+        .map(|(left, right)| {
+            let (probe_key, build_key) = match probe_side {
+                JoinSide::Left => (left, right),
+                JoinSide::Right => (right, left),
+                JoinSide::None => unreachable!(),
+            };
+            (
+                probe.eq_group().normalize_expr(Arc::clone(probe_key)),
+                build.eq_group().normalize_expr(Arc::clone(build_key)),
+            )
+        })
+        .collect::<Vec<_>>();
+    let mut valid_orderings = Vec::new();
+    for ordering in probe.oeq_class().iter() {
+        // Within a group of equal ordering values, these expressions are
+        // constant. Keep this assumption local to the ordering proof.
+        let mut group = probe.eq_group().clone();
+        for sort in ordering {
+            let expr = probe.eq_group().normalize_expr(Arc::clone(&sort.expr));
+            group.add_constant(ConstExpr::from(expr));
+        }
+        let probe_key_is_fixed = |key: &PhysicalExprRef| {
+            !is_volatile(key) && group.is_expr_constant(key).is_some()

Review Comment:
   [P2] Check volatility before normalizing join keys
   
   A filter such as p = volatile_flip(p) records an equivalence that normalizes 
the join key p + volatile_flip(p) into p + p. This volatility check then misses 
the function and incorrectly accepts the ordering suffix. The native reproducer 
returns [10,20] for LIMIT 2 instead of [10,10]. The previous PR revision passed 
this case; the current exception reintroduces the original failure. Preserve 
the original key's volatility before normalization.



##########
datafusion/physical-expr/src/equivalence/properties/joins.rs:
##########
@@ -107,6 +95,112 @@ pub fn join_equivalence_properties(
     Ok(result)
 }
 
+/// Append build orderings only when equal probe ordering values identify at 
most
+/// one build row. The suffix is then constant within each probe ordering 
group,
+/// even if probe rows repeat. For an outer join preserving the probe side, all
+/// join keys must be fixed within the group, and the caller must rule out 
filters,
+/// so the group cannot mix matched build rows with NULL-extended rows.
+fn unique_build_join_orderings(
+    probe: &EquivalenceProperties,
+    build: &EquivalenceProperties,
+    on: &[(PhysicalExprRef, PhysicalExprRef)],
+    probe_side: JoinSide,
+    preserves_unmatched_probe: bool,
+    null_equality: NullEquality,
+) -> Result<OrderingEquivalenceClass> {
+    if build.constraints().is_empty() || build.oeq_class().is_empty() {
+        return Ok(OrderingEquivalenceClass::default());
+    }
+    let on = on
+        .iter()
+        .map(|(left, right)| {
+            let (probe_key, build_key) = match probe_side {
+                JoinSide::Left => (left, right),
+                JoinSide::Right => (right, left),
+                JoinSide::None => unreachable!(),
+            };
+            (
+                probe.eq_group().normalize_expr(Arc::clone(probe_key)),
+                build.eq_group().normalize_expr(Arc::clone(build_key)),
+            )
+        })
+        .collect::<Vec<_>>();
+    let mut valid_orderings = Vec::new();
+    for ordering in probe.oeq_class().iter() {
+        // Within a group of equal ordering values, these expressions are
+        // constant. Keep this assumption local to the ordering proof.
+        let mut group = probe.eq_group().clone();
+        for sort in ordering {
+            let expr = probe.eq_group().normalize_expr(Arc::clone(&sort.expr));
+            group.add_constant(ConstExpr::from(expr));
+        }
+        let probe_key_is_fixed = |key: &PhysicalExprRef| {
+            !is_volatile(key) && group.is_expr_constant(key).is_some()
+        };
+
+        // Outer joins must have the same match status throughout the group.
+        if preserves_unmatched_probe
+            && !on
+                .iter()
+                .all(|(probe_key, _)| probe_key_is_fixed(probe_key))
+        {
+            continue;
+        }
+        if !ordering_covers_unique_build_key(
+            build,
+            &on,
+            probe_key_is_fixed,
+            null_equality,
+        ) {
+            continue;
+        }
+        valid_orderings.push(ordering.clone());
+    }
+    let mut probe_orderings = OrderingEquivalenceClass::new(valid_orderings);
+    if probe_orderings.is_empty() {
+        return Ok(probe_orderings);
+    }
+    let mut build_orderings = build.oeq_class().clone();
+    match probe_side {
+        JoinSide::Left => 
build_orderings.add_offset(probe.schema.fields().len() as _)?,
+        JoinSide::Right => 
probe_orderings.add_offset(build.schema.fields().len() as _)?,
+        JoinSide::None => unreachable!(),
+    }
+    Ok(probe_orderings.join_suffix(&build_orderings))
+}
+
+/// Check whether the probe ordering determines a unique build key.
+/// Join keys must already be normalized against their input equivalence 
groups.
+fn ordering_covers_unique_build_key(
+    build: &EquivalenceProperties,
+    on: &[(PhysicalExprRef, PhysicalExprRef)],
+    probe_key_is_fixed: impl Fn(&PhysicalExprRef) -> bool,
+    null_equality: NullEquality,
+) -> bool {
+    build.constraints().iter().any(|constraint| {
+        let (Constraint::PrimaryKey(indices) | Constraint::Unique(indices)) = 
constraint;
+        !indices.is_empty()
+            && indices.iter().all(|&index| {
+                let Some(field) = build.schema.fields().get(index) else {
+                    return false;
+                };
+                // UNIQUE permits repeated NULLs, which only invalidate the
+                // uniqueness proof when NULL join keys can match each other.
+                if matches!(constraint, Constraint::Unique(_))
+                    && field.is_nullable()
+                    && null_equality == NullEquality::NullEqualsNull
+                {
+                    return false;

Review Comment:
   [P2] Account for nonnullable probe keys under null-safe equality
   
   A nullable UNIQUE build key still guarantees at most one match when the 
corresponding probe key cannot be NULL, even with NullEqualsNull. This guard 
rejects that safe case. For an ordered unbounded probe, ORDER BY probe.k, 
build.v LIMIT 1 returns immediately on base 5747e87 but gains PartialSortExec 
on this head and waits indefinitely if the key group never finishes. The finite 
control passes. Include probe-key nullability in the uniqueness proof.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to