HyukjinKwon commented on code in PR #59258:
URL: https://github.com/apache/spark/pull/59258#discussion_r4237130134


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +125,92 @@ trait ConstraintHelper {
     inferred -- constraints
   }
 
+  /**
+   * Infers the full additional constraint set for a given set of constraints 
by combining the
+   * equality-based inferences above ([[inferAdditionalConstraints]],
+   * [[inferConstraintsFromLiteralBindings]]) with transitive inequality 
chaining
+   * ([[inferInequalityConstraints]]). The inequality step is iterated to a 
fixed point because a
+   * single pass only chains pairs present in the input set: e.g. `a < b && b 
< c && c < 5`
+   * requires one pass to derive `b < 5` and a second to derive `a < 5` from 
it.
+   */
+  def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet 
= {
+    @tailrec
+    def chainInequalities(inferred: ExpressionSet): ExpressionSet = {
+      val newlyInferred = inferInequalityConstraints(constraints ++ inferred)
+      if (newlyInferred.isEmpty) inferred else chainInequalities(inferred ++ 
newlyInferred)
+    }
+
+    chainInequalities(
+      inferAdditionalConstraints(constraints) ++
+        inferConstraintsFromLiteralBindings(constraints))
+  }
+
+  /**
+   * Infers an additional set of constraints from a given set of inequality 
constraints.
+   * For e.g., if an operator has constraints of the form (`a > b`, `b > 5`), 
this returns an
+   * additional constraint of the form `a > 5`.
+   */
+  private def inferInequalityConstraints(constraints: ExpressionSet): 
ExpressionSet = {
+    // Substituting through a comparison is only safe under binary-stable 
collations, for the
+    // same reason as in inferConstraintsFromLiteralBindings: 
non-binary-stable collations may

Review Comment:
   **Nit (P3):** The cited reason doesn't apply here. Lines 98-100 justify the 
literal-binding guard by noting that non-binary-stable collations "may equate 
strings that are binary-distinct, so substituting the literal would change 
results". That is about substituting through an equality, not about ordering. 
Chaining never substitutes a value: the inferred bound reuses the candidate's 
operand and the bound's literal under the same collation-aware comparison. So 
the comment makes the guard look required when it is really conservative. 
Keeping the guard is fine.
   
   The comment also says only attribute-to-attribute equality is excluded. The 
filter at line 164 actually drops every `EqualTo` without a foldable side, 
including `a + 1 = b`. `inferAdditionalConstraints` handles only Attribute and 
Cast(Attribute) operands, so no path handles that case, and `a + 1 = b AND b < 
5` infers no `a + 1 < 5`.
   
   Could the comment describe the binary-stable restriction on its own terms 
(e.g. as a conservative choice) and state the full equality exclusion?
   
   **Verification:**
   - **Inspection:** The comment above validComparisons describes the 
binary-stable restriction with a rationale consistent with the referenced 
literal-binding comment and describes the full equality exclusion that the 
filter implements.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +125,92 @@ trait ConstraintHelper {
     inferred -- constraints
   }
 
+  /**
+   * Infers the full additional constraint set for a given set of constraints 
by combining the
+   * equality-based inferences above ([[inferAdditionalConstraints]],
+   * [[inferConstraintsFromLiteralBindings]]) with transitive inequality 
chaining
+   * ([[inferInequalityConstraints]]). The inequality step is iterated to a 
fixed point because a
+   * single pass only chains pairs present in the input set: e.g. `a < b && b 
< c && c < 5`
+   * requires one pass to derive `b < 5` and a second to derive `a < 5` from 
it.
+   */
+  def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet 
= {
+    @tailrec
+    def chainInequalities(inferred: ExpressionSet): ExpressionSet = {
+      val newlyInferred = inferInequalityConstraints(constraints ++ inferred)
+      if (newlyInferred.isEmpty) inferred else chainInequalities(inferred ++ 
newlyInferred)
+    }
+
+    chainInequalities(
+      inferAdditionalConstraints(constraints) ++
+        inferConstraintsFromLiteralBindings(constraints))
+  }
+
+  /**
+   * Infers an additional set of constraints from a given set of inequality 
constraints.
+   * For e.g., if an operator has constraints of the form (`a > b`, `b > 5`), 
this returns an
+   * additional constraint of the form `a > 5`.
+   */
+  private def inferInequalityConstraints(constraints: ExpressionSet): 
ExpressionSet = {
+    // Substituting through a comparison is only safe under binary-stable 
collations, for the
+    // same reason as in inferConstraintsFromLiteralBindings: 
non-binary-stable collations may
+    // order values differently than their binary representation would 
suggest. Attribute-to-
+    // attribute equality is excluded here too: its transitivity is already 
handled by
+    // inferAdditionalConstraints via substitution, so chaining it here would 
only re-derive the
+    // same fact as a redundant <= / >= pair instead of the clean substituted 
equality that path
+    // already produces.
+    val validComparisons = constraints.filter {
+      case b: BinaryComparison =>
+        b.deterministic && !b.throwable && !b.isInstanceOf[EqualNullSafe] &&
+          (!b.isInstanceOf[EqualTo] || b.left.foldable || b.right.foldable) &&
+          isBinaryStable(b.left.dataType) && isBinaryStable(b.right.dataType)
+      case _ => false
+    }
+
+    def hasFoldableOperand(e: Expression): Boolean = e match {
+      case BinaryComparison(l, r) => l.foldable || r.foldable
+      case _ => false
+    }
+
+    if (!validComparisons.exists(hasFoldableOperand)) {
+      return ExpressionSet()
+    }
+
+    // Canonicalize comparisons into LessThan / LessThanOrEqual direction. 
EqualTo is expanded
+    // into both directions, unlike GreaterThan/GreaterThanOrEqual, because an 
equality acts as
+    // both an upper and a lower bound and the matching below is 
direction-sensitive on which
+    // side carries the literal.
+    val canonical = validComparisons.flatMap {

Review Comment:
   **Nit (P3):** `validComparisons` is an `ExpressionSet`, so this `flatMap` 
adds each result through `ExpressionSet.add`, which skips any expression whose 
canonical form is already present. `EqualTo` canonicalizes by ordering its 
operands by hash code. That gives `EqualTo(r, l)` the same canonical form as 
`EqualTo(l, r)`, so it is dropped, and only the orientation the user wrote 
reaches `bounds`. This contradicts the comment above, which says equality is 
expanded into both directions. The bound match below is direction-sensitive. As 
a result, `WHERE a + 1 = 5 AND a + 1 < b` no longer infers `b > 5`, while 
`WHERE 5 = a + 1 AND a + 1 < b` still does. The join form `x.a + 1 = 5` with 
`x.a + 1 < y.b` likewise pushes nothing to `y`. The previous revision's 
`toSet[Expression]` kept both orientations. Results stay correct; the pushdown 
is silently missed.
   
   The new tests can't catch this. Every equality bound in them is a bare 
`Attribute = Literal`, for example `$"a" > $"b" && $"b" === 3` in 
InferFiltersFromConstraintsSuite. For those, 
`inferConstraintsFromLiteralBindings` derives the same filters by substitution, 
so the expectations hold even with the reverse orientation gone.
   
   Iterating a `Seq` here (e.g. `validComparisons.toSeq.flatMap`), so that 
`partition` and the for-comprehension below also run over `Seq`s, keeps both 
orientations. It also removes the hash ordering still open in the earlier 
thread on this method.
   
   **Verification:**
   - **Regression:** An optimizer test bounds a non-attribute expression with 
an equality written in each operand order and asserts that the same transitive 
bound is inferred for both orders.
   - **Regression:** A Filter whose chaining infers at least five bounds in one 
pass yields the same inferred-conjunct order when its attributes are re-created 
with different exprIds, compared without order normalization.
   - **Compatibility:** Existing inequality-inference, constraint-propagation, 
and q12 plan-stability expectations remain unchanged.



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