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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,189 @@ 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 two equality-based passes never 
produce a new literal
+   * binding (an `attr = literal` equality), only new inequalities or 
substitutions into existing
+   * predicates, so a single downstream call to [[inferInequalityConstraints]] 
sees every literal
+   * bound they can ever contribute; it does not need to be interleaved with 
them.
+   */
+  def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet 
= {
+    val equalityInferred =
+      inferAdditionalConstraints(constraints) ++ 
inferConstraintsFromLiteralBindings(constraints)
+    equalityInferred ++ inferInequalityConstraints(constraints ++ 
equalityInferred)

Review Comment:
   **Nit (P3):** This line lets chains start from edges or bindings that only 
the equality passes create, but no added test depends on that. In one 
`constraints` evaluation, `a = c AND c < b AND b < 5` gives `a < 5` only 
because `inferAdditionalConstraints` rewrites `c < b` into `a < b`. Likewise `a 
= 5 AND a = b AND c < b` gives `c < 5` only through the derived `b = 5`. If 
this line became `inferInequalityConstraints(constraints)`, both bounds would 
disappear and every added test would still pass. 
InferFiltersFromConstraintsSuite can't catch it, because its FixedPoint(100) 
batch re-derives the bound on a later iteration, while production runs 'Infer 
Filters' Once. A ConstraintPropagationSuite case asserting those two bounds on 
`plan.constraints` would pin it.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,189 @@ 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 two equality-based passes never 
produce a new literal
+   * binding (an `attr = literal` equality), only new inequalities or 
substitutions into existing
+   * predicates, so a single downstream call to [[inferInequalityConstraints]] 
sees every literal
+   * bound they can ever contribute; it does not need to be interleaved with 
them.
+   */
+  def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet 
= {
+    val equalityInferred =
+      inferAdditionalConstraints(constraints) ++ 
inferConstraintsFromLiteralBindings(constraints)
+    equalityInferred ++ inferInequalityConstraints(constraints ++ 
equalityInferred)
+  }
+
+  /**
+   * Infers the full set of constraints derivable by transitively chaining 
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`; a longer chain such as `a < b 
&& b < c && c < 5`
+   * derives both `b < 5` and `a < 5` in this same call.
+   *
+   * This is computed as a worklist over a directed bound graph rather than a 
repeated fixed point
+   * over the full candidate set: each candidate comparison becomes a directed 
edge once, and each
+   * newly discovered bound is propagated only along its adjacent edges, 
instead of rescanning the
+   * full candidates x bounds product after every newly inferred bound (which 
costs O(n^3) on a
+   * sparse n-element chain such as `x1 < x2 < ... < xn < lit`). A site only 
ever keeps its single
+   * tightest bound per direction, so reaching it via multiple literals (e.g. 
`b < 5` and `b < 10`
+   * both reaching `a` through `a < b`) converges to one inferred predicate 
instead of one per
+   * literal; a relaxation only re-enters the worklist when it strictly 
tightens that site's bound,
+   * which bounds the number of relaxations per site by the number of distinct 
literals that can
+   * reach it and guarantees termination.
+   */
+  private def inferInequalityConstraints(constraints: ExpressionSet): 
ExpressionSet = {
+    // Admits only comparisons between binary-stable types. This is a 
conservative guard, not a
+    // strict requirement: chaining never substitutes a value the way
+    // inferConstraintsFromLiteralBindings does, it only reuses an operand 
under the same
+    // collation-aware comparison, so a non-binary-stable collation could in 
principle still order
+    // consistently with its binary form here. EqualTo is admitted only when 
at least one side is
+    // foldable (a literal bound); every other EqualTo -- 
attribute-to-attribute (handled by
+    // inferAdditionalConstraints's transitivity), as well as any other shape 
without a foldable
+    // side such as `a + 1 = b` -- is excluded, since no inference path below 
handles a
+    // non-foldable equality operand.
+    val validComparisons = constraints.toSeq.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 b: BinaryComparison => b.left.foldable || b.right.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. Kept as a Seq, not an ExpressionSet: 
ExpressionSet canonicalizes
+    // EqualTo(l, r) and EqualTo(r, l) to the same entry and would silently 
keep only one of the
+    // two directions just expanded.
+    val canonical = validComparisons.flatMap {
+      case GreaterThan(l, r) => LessThan(r, l) :: Nil
+      case GreaterThanOrEqual(l, r) => LessThanOrEqual(r, l) :: Nil
+      case EqualTo(l, r) => EqualTo(l, r) :: EqualTo(r, l) :: Nil
+      case other => other :: Nil
+    }
+
+    val (initialBounds, candidates) = canonical.partition(hasFoldableOperand)
+    if (initialBounds.isEmpty || candidates.isEmpty) return ExpressionSet()
+
+    // A candidate operand is only safe to copy into a newly inferred, 
single-side filter when
+    // doing so cannot raise an error on rows that never evaluated it before: 
an attribute
+    // reference, or a lossless (non-truncating, non-failing) widening cast of 
one. Arbitrary
+    // expressions -- a parsing/narrowing cast, arithmetic that can overflow 
under ANSI, etc. --
+    // are excluded, since the comparison they came from may have been a 
cross-side join
+    // condition that only ever ran on matched rows.
+    def isSafeToRelocate(e: Expression): Boolean = e match {
+      case _: Attribute => true
+      case Cast(a: Attribute, toType, _, _) => 
UpCastRule.canUpCast(a.dataType, toType)
+      case _ => false
+    }
+
+    // Bound represents a known literal bound on `site`: `site {<,<=} lit` 
when `upper`, or
+    // `lit {<,<=} site` otherwise.
+    case class Bound(upper: Boolean, site: Expression, lit: Expression, 
strict: Boolean)
+
+    def toBound(e: Expression): Option[Bound] = e match {
+      case b: BinaryComparison =>
+        val strict = b.isInstanceOf[LessThan]
+        if (b.right.foldable) Some(Bound(upper = true, b.left, b.right, 
strict))

Review Comment:
   **Nit (P3):** `toBound` accepts any foldable operand as the bound's `lit`. 
The worklist then copies that `lit` into the inferred predicate on the 
relocated site, and `isTighter` calls `eval()` on it during optimization. 
`isSafeToRelocate` checks only the edge endpoint, and `!b.throwable` does not 
flag ANSI casts or arithmetic. `inferConstraintsFromLiteralBindings` runs in 
the same call and can produce exactly such an operand. With ANSI on:
   
   ```sql
   SELECT * FROM (SELECT * FROM x WHERE s = 'abc') x
   LEFT JOIN y ON x.k = y.k AND y.c < x.b AND x.b < CAST(x.s AS INT)
   ```
   
   Substitution turns the preserved-side ON conjunct into `x.b < CAST('abc' AS 
INT)`. The `y.c < x.b` edge then relocates it to `y.c < CAST('abc' AS INT)` on 
the right side, and ConstantFolding raises CAST_INVALID_INPUT during 
optimization, even when no key-matched pair exists. Before this PR, that 
left-only conjunct was never pushed for a LeftOuter join. If x's subquery also 
has `b < 5`, `isTighter` throws inside InferFiltersFromConstraints itself. This 
is the error-on-unevaluated-rows behavior the relocation guard was added to 
rule out, reached through the bound operand instead of the edge endpoint. With 
ANSI off the cast yields NULL and the query is unaffected.
   
   Evaluating the foldable operand once in `toBound`, inside a `NonFatal` 
guard, would close this: drop the bound if evaluation fails, and keep the 
evaluated value on `Bound` so `isTighter` compares stored values. The emitted 
predicate can keep its current operand expression, so existing plan 
expectations stay the same.
   
   **Verification:**
   - **Regression:** Under ANSI, a left-outer join whose preserved-side ON 
conjunct substitutes into a failing cast plans and returns the same rows with 
constraint propagation on and off.
   - **Regression:** A site bounded by both a valid literal and a failing 
foldable is optimized without an exception and still infers the chained bound 
from the valid literal.
   - **Compatibility:** Existing inequality-inference, constraint-propagation, 
and q12 plan-stability expectations remain unchanged.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,189 @@ 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 two equality-based passes never 
produce a new literal

Review Comment:
   **Nit (P3):** This says the equality-based passes never produce a new 
literal binding. `inferAdditionalConstraints` documents exactly that a few 
lines above: (`a = 5`, `a = b`) gives `b = 5`. 
`inferConstraintsFromLiteralBindings` can also produce `b = 5 + 1` from `a = 5` 
and `b = a + 1`. The code is correct only because line 143 passes `constraints 
++ equalityInferred` to the inequality pass. A maintainer who trusts this 
comment could call `inferInequalityConstraints(constraints)` directly and 
silently lose chains such as (`a = 5`, `a = b`, `c < b`) => `c < 5`.
   
   Could the comment give the actual reason? The inequality pass has to see the 
equality passes' output, and because it emits only inequalities, the equality 
passes gain nothing from re-running on its result.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,189 @@ 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 two equality-based passes never 
produce a new literal
+   * binding (an `attr = literal` equality), only new inequalities or 
substitutions into existing
+   * predicates, so a single downstream call to [[inferInequalityConstraints]] 
sees every literal
+   * bound they can ever contribute; it does not need to be interleaved with 
them.
+   */
+  def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet 
= {
+    val equalityInferred =
+      inferAdditionalConstraints(constraints) ++ 
inferConstraintsFromLiteralBindings(constraints)
+    equalityInferred ++ inferInequalityConstraints(constraints ++ 
equalityInferred)
+  }
+
+  /**
+   * Infers the full set of constraints derivable by transitively chaining 
inequality constraints.

Review Comment:
   **Nit (P3):** This method doesn't infer the full transitive set. It emits 
only `site op literal` bounds, keeps the single tightest one per direction and 
site, and bounds only relocatable sites, so `a < c` from `a < b AND b < c` is 
never produced. Separately, the `isSafeToRelocate` comment at line 208 calls 
the admitted cast lossless, but `UpCastRule.canUpCast` also admits Long to 
Float/Double and Timestamp to Long, which lose precision. Relocation is sound 
because those casts can't fail and the same Cast expression is reused, not 
because they are lossless. Could both comments state what the code actually 
guarantees?
   
   **Verification:**
   - **Inspection:** The headline matches the literal-bound, tightest-per-site, 
relocatable-site behavior, and the relocation comment matches what 
UpCastRule.canUpCast admits.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala:
##########
@@ -465,4 +465,350 @@ class InferFiltersFromConstraintsSuite extends PlanTest {
     comparePlans(optimized, correctAnswer)
   }
 
+  test("infer inequality constraints: chained inequalities within a single 
relation") {
+    Seq(($"a" < $"b" && $"b" < 3, $"a" < $"b" && $"b" < 3 && $"a" < 3),
+      ($"a" < $"b" && $"b" <= 3, $"a" < $"b" && $"b" <= 3 && $"a" < 3),
+      ($"a" < $"b" && $"b" === 3, $"a" < $"b" && $"b" === 3 && $"a" < 3),
+      ($"a" <= $"b" && $"b" < 3, $"a" <= $"b" && $"b" < 3 && $"a" < 3),
+      ($"a" <= $"b" && $"b" <= 3, $"a" <= $"b" && $"b" <= 3 && $"a" <= 3),
+      ($"a" <= $"b" && $"b" === 3, $"a" <= $"b" && $"b" === 3 && $"a" <= 3),
+      ($"a" > $"b" && $"b" > 3, $"a" > $"b" && $"b" > 3 && $"a" > 3),
+      ($"a" > $"b" && $"b" >= 3, $"a" > $"b" && $"b" >= 3 && $"a" > 3),
+      ($"a" > $"b" && $"b" === 3, $"a" > $"b" && $"b" === 3 && $"a" > 3),
+      ($"a" >= $"b" && $"b" > 3, $"a" >= $"b" && $"b" > 3 && $"a" > 3),
+      ($"a" >= $"b" && $"b" >= 3, $"a" >= $"b" && $"b" >= 3 && $"a" >= 3),
+      ($"a" >= $"b" && $"b" === 3, $"a" >= $"b" && $"b" === 3 && $"a" >= 3)
+    ).foreach {
+      case (filter, inferred) =>
+        val original = testRelation.where(filter)
+        val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& inferred)
+        comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+    }
+  }
+
+  test("infer inequality constraints: chained inequality across an equi-join") 
{
+    Seq(("left.b".attr < "right.b".attr, $"b" < 1, $"b" < 1),
+      ("left.b".attr < "right.b".attr, $"b" === 1, $"b" < 1),
+      ("left.b".attr < "right.b".attr, $"b" <= 1, $"b" < 1),
+      ("left.b".attr <= "right.b".attr, $"b" <= 1, $"b" <= 1),
+      ("left.b".attr <= "right.b".attr, $"b" === 1, $"b" <= 1),
+      ("left.b".attr > "right.b".attr, $"b" > 1, $"b" > 1),
+      ("left.b".attr > "right.b".attr, $"b" === 1, $"b" > 1),
+      ("left.b".attr > "right.b".attr, $"b" >= 1, $"b" > 1),
+      ("left.b".attr >= "right.b".attr, $"b" >= 1, $"b" >= 1),
+      ("left.b".attr >= "right.b".attr, $"b" === 1, $"b" >= 1)
+    ).foreach {
+      case (cond, filter, inferred) =>
+        val originalLeft = testRelation.subquery("left")
+        val originalRight = testRelation.where(filter).subquery("right")
+
+        val left =
+          testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
inferred).subquery("left")
+        val right =
+          testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
filter).subquery("right")
+        val condition = Some("left.a".attr === "right.a".attr && cond)
+        testConstraintsAfterJoin(originalLeft, originalRight, left, right, 
Inner, condition)
+    }
+  }
+
+  test("infer inequality constraints: chained inequalities through an implicit 
cast") {
+    Seq(($"a" < $"b" && $"b" < 3L, $"a".cast(LongType) < $"b" && $"b" < 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" < $"b" && $"b" <= 3L, $"a".cast(LongType) < $"b" && $"b" <= 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" < $"b" && $"b" === 3L, $"a".cast(LongType) < $"b" && $"b" === 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" <= $"b" && $"b" < 3L, $"a".cast(LongType) <= $"b" && $"b" < 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" <= $"b" && $"b" <= 3L, $"a".cast(LongType) <= $"b" && $"b" <= 3L &&
+        $"a".cast(LongType) <= 3L),
+      ($"a" <= $"b" && $"b" === 3L, $"a".cast(LongType) <= $"b" && $"b" === 3L 
&&
+        $"a".cast(LongType) <= 3L),
+      ($"a" < $"b" && $"b" < 3, $"a".cast(LongType) < $"b" && $"b" < 
Literal(3).cast(LongType)
+        && $"a".cast(LongType) < Literal(3).cast(LongType)),
+      ($"a" > $"b" && $"b" > 3L, $"a".cast(LongType) > $"b" && $"b" > 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" > $"b" && $"b" >= 3L, $"a".cast(LongType) > $"b" && $"b" >= 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" > $"b" && $"b" === 3L, $"a".cast(LongType) > $"b" && $"b" === 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" >= $"b" && $"b" > 3L, $"a".cast(LongType) >= $"b" && $"b" > 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" >= $"b" && $"b" >= 3L, $"a".cast(LongType) >= $"b" && $"b" >= 3L &&
+        $"a".cast(LongType) >= 3L),
+      ($"a" >= $"b" && $"b" === 3L, $"a".cast(LongType) >= $"b" && $"b" === 3L 
&&
+        $"a".cast(LongType) >= 3L),
+      ($"a" > $"b" && $"b" > 3, $"a".cast(LongType) > $"b" && $"b" > 
Literal(3).cast(LongType)
+        && $"a".cast(LongType) > Literal(3).cast(LongType))
+    ).foreach {
+      case (filter, inferred) =>
+        val testRelation = LocalRelation($"a".int, $"b".long)
+        val original = testRelation.where(filter)
+        val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& inferred)
+        comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+    }
+  }
+
+  test("infer inequality constraints: an attr=literal binding on one join side 
bounds " +
+    "an inequality join key on the other side") {
+    val condition = Some("x.a".attr > "y.a".attr)
+    val optimizedLeft = testRelation.where(IsNotNull($"a") && $"a" === 
1).as("x")
+    val optimizedRight = testRelation.where($"a" < 1 && 
IsNotNull($"a")).as("y")
+    val correct = optimizedLeft.join(optimizedRight, Inner, condition)
+
+    Seq(Literal(1) === $"a", $"a" === Literal(1)).foreach { filter =>
+      val original =
+        testRelation.where(filter).as("x").join(testRelation.as("y"), Inner, 
condition)
+      comparePlans(Optimize.execute(original.analyze), correct.analyze)
+    }
+  }
+
+  test("infer inequality constraints: a three-attribute chain bounded by a 
literal") {
+    val original = testRelation.where($"a" < $"b" && $"b" < $"c" && $"c" < 5)
+    val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
IsNotNull($"c")
+      && $"a" < $"b" && $"b" < $"c" && $"a" < 5 && $"b" < 5 && $"c" < 5)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: a range join condition combined with a 
range filter") {
+    val left = testRelation.where($"b" >= 3 && $"b" <= 13).as("x")
+    val right = testRelation.as("y")
+
+    val optimizedLeft = testRelation.where(IsNotNull($"a") && IsNotNull($"b")
+      && $"b" >= 3 && $"b" <= 13).as("x")
+    val optimizedRight = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& IsNotNull($"c")
+      && $"c" > 3 && $"b" <= 13).as("y")
+    val condition = Some("x.a".attr === "y.a".attr
+      && "x.b".attr >= "y.b".attr && "x.b".attr < "y.c".attr)
+    val original = left.join(right, Inner, condition)
+    val optimized = optimizedLeft.join(optimizedRight, Inner, condition)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: chain across a join through an implicit 
cast") {
+    val testRelation1 = LocalRelation($"a".long, $"b".long, $"c".long).as("x")
+    val testRelation2 = LocalRelation($"a".int, $"b".int, $"c".int).as("y")
+
+    // y.b < 13 inferred from y.b < x.b && x.b <= 13
+    val left = testRelation1.where($"b" <= 13L).as("x")
+    val right = testRelation2.as("y")
+
+    val optimizedLeft =
+      testRelation1.where(IsNotNull($"a") && IsNotNull($"b") && $"b" <= 
13L).as("x")
+    val optimizedRight = testRelation2.where(IsNotNull($"a") && IsNotNull($"b")
+      && $"b".cast(LongType) < 13L).as("y")
+
+    val condition = Some("x.a".attr === "y.a".attr && "y.b".attr < "x.b".attr)
+    val original = left.join(right, Inner, condition)
+    val optimized = optimizedLeft.join(optimizedRight, Inner, condition)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: range predicate in left-outer join ON 
clause " +
+    "is pushed to right side when left attr is bounded by inequality") {
+    // Left side: a.k >= 5
+    // Join condition: b.v >= a.k
+    // Transitive inference should deduce b.v >= 5 and push it down to the 
right side of
+    // LeftOuter join.
+    val left = LocalRelation($"k".int, $"key".int).subquery("a")
+    val right = LocalRelation($"v".int, $"key".int).subquery("b")
+
+    val joinCond = ("a.key".attr === "b.key".attr) && ("b.v".attr >= 
"a.k".attr)
+    val originalQuery = left
+      .where("a.k".attr >= 5)
+      .join(right, LeftOuter, Some(joinCond))
+      .analyze
+
+    val optimized = Optimize.execute(originalQuery)
+    var pushedToRight = false
+    optimized.foreach {
+      case Join(_, Filter(cond, _), LeftOuter, _, _) =>
+        pushedToRight = splitConjunctivePredicates(cond).exists {
+          case GreaterThanOrEqual(attr: Attribute, Literal(v: Int, 
IntegerType)) =>
+            attr.name == "v" && v == 5
+          case _ => false
+        }
+      case _ =>
+    }
+    assert(pushedToRight, s"Expected b.v >= 5 pushed to right side; plan 
was:\n$optimized")
+  }
+
+  test("SPARK-30768: do not infer filters from throwable expressions") {
+    val x = testRelation.where($"b" < 10).subquery("x")
+    val y = testRelation.subquery("y")
+    val raiseErr = new RaiseError(Literal("neg")).cast(IntegerType)
+    val caseExpr = CaseWhen(Seq(("y.b".attr < 0, raiseErr)), elseValue = 
Some("y.b".attr))
+    val cond = Some("x.a".attr === "y.a".attr && caseExpr < "x.b".attr)
+    val query = x.join(y, Inner, cond).analyze
+    val optimized = Optimize.execute(query)
+    var inferredThrowable = false
+    optimized.foreach {
+      case Join(_, Filter(cond, _), _, _, _) =>
+        inferredThrowable = cond.throwable
+      case _ =>
+    }
+    assert(!inferredThrowable, s"Unexpected throwable filter condition 
inferred: $optimized")
+  }
+
+  test("SPARK-30768: equality orientation does not affect inequality 
chaining") {
+    Seq($"a" + 1 === 5, Literal(5) === ($"a" + 1)).foreach { eq =>
+      val original = testRelation.where(eq && ($"a" + 1) < $"b")
+      val optimized = testRelation.where(
+        IsNotNull($"a") && IsNotNull($"b") && eq && ($"a" + 1) < $"b" && $"b" 
> 5)
+      comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+    }
+  }
+
+  test("SPARK-30768: equality orientation does not affect join-side inequality 
pushdown") {
+    def hasPushedFilter(plan: LogicalPlan)(pred: Expression => Boolean): 
Boolean = {
+      plan.collectFirst {
+        case Join(_, Filter(cond, _), _, _, _) => 
splitConjunctivePredicates(cond).exists(pred)
+      }.getOrElse(false)
+    }
+
+    Seq($"a" + 1 === 5, Literal(5) === ($"a" + 1)).foreach { eq =>
+      val x = testRelation.where(eq).subquery("x")
+      val y = testRelation.subquery("y")
+
+      // Upper bound on a + 1 propagates to y.b > 5

Review Comment:
   **Nit (P3):** These two labels are reversed. For `(x.a + 1) < y.b`, `x.a + 
1` is not relocatable, so the only edge is the lower edge keyed by `a + 1`, and 
`y.b > 5` comes from the lower bound `5 <= a + 1`. For `y.b < (x.a + 1)` at 
line 680, `y.b < 5` comes from the upper bound `a + 1 <= 5`. The assertions are 
right. Swapping the two comments, and the `optUpper`/`optLower` names, would 
make the test describe the direction it checks.
   
   **Verification:**
   - **Inspection:** The labels attribute `y.b > 5` to the lower bound and `y.b 
< 5` to the upper bound on `a + 1`, and the assertions are unchanged.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala:
##########
@@ -465,4 +465,350 @@ class InferFiltersFromConstraintsSuite extends PlanTest {
     comparePlans(optimized, correctAnswer)
   }
 
+  test("infer inequality constraints: chained inequalities within a single 
relation") {
+    Seq(($"a" < $"b" && $"b" < 3, $"a" < $"b" && $"b" < 3 && $"a" < 3),
+      ($"a" < $"b" && $"b" <= 3, $"a" < $"b" && $"b" <= 3 && $"a" < 3),
+      ($"a" < $"b" && $"b" === 3, $"a" < $"b" && $"b" === 3 && $"a" < 3),
+      ($"a" <= $"b" && $"b" < 3, $"a" <= $"b" && $"b" < 3 && $"a" < 3),
+      ($"a" <= $"b" && $"b" <= 3, $"a" <= $"b" && $"b" <= 3 && $"a" <= 3),
+      ($"a" <= $"b" && $"b" === 3, $"a" <= $"b" && $"b" === 3 && $"a" <= 3),
+      ($"a" > $"b" && $"b" > 3, $"a" > $"b" && $"b" > 3 && $"a" > 3),
+      ($"a" > $"b" && $"b" >= 3, $"a" > $"b" && $"b" >= 3 && $"a" > 3),
+      ($"a" > $"b" && $"b" === 3, $"a" > $"b" && $"b" === 3 && $"a" > 3),
+      ($"a" >= $"b" && $"b" > 3, $"a" >= $"b" && $"b" > 3 && $"a" > 3),
+      ($"a" >= $"b" && $"b" >= 3, $"a" >= $"b" && $"b" >= 3 && $"a" >= 3),
+      ($"a" >= $"b" && $"b" === 3, $"a" >= $"b" && $"b" === 3 && $"a" >= 3)
+    ).foreach {
+      case (filter, inferred) =>
+        val original = testRelation.where(filter)
+        val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& inferred)
+        comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+    }
+  }
+
+  test("infer inequality constraints: chained inequality across an equi-join") 
{
+    Seq(("left.b".attr < "right.b".attr, $"b" < 1, $"b" < 1),
+      ("left.b".attr < "right.b".attr, $"b" === 1, $"b" < 1),
+      ("left.b".attr < "right.b".attr, $"b" <= 1, $"b" < 1),
+      ("left.b".attr <= "right.b".attr, $"b" <= 1, $"b" <= 1),
+      ("left.b".attr <= "right.b".attr, $"b" === 1, $"b" <= 1),
+      ("left.b".attr > "right.b".attr, $"b" > 1, $"b" > 1),
+      ("left.b".attr > "right.b".attr, $"b" === 1, $"b" > 1),
+      ("left.b".attr > "right.b".attr, $"b" >= 1, $"b" > 1),
+      ("left.b".attr >= "right.b".attr, $"b" >= 1, $"b" >= 1),
+      ("left.b".attr >= "right.b".attr, $"b" === 1, $"b" >= 1)
+    ).foreach {
+      case (cond, filter, inferred) =>
+        val originalLeft = testRelation.subquery("left")
+        val originalRight = testRelation.where(filter).subquery("right")
+
+        val left =
+          testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
inferred).subquery("left")
+        val right =
+          testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
filter).subquery("right")
+        val condition = Some("left.a".attr === "right.a".attr && cond)
+        testConstraintsAfterJoin(originalLeft, originalRight, left, right, 
Inner, condition)
+    }
+  }
+
+  test("infer inequality constraints: chained inequalities through an implicit 
cast") {
+    Seq(($"a" < $"b" && $"b" < 3L, $"a".cast(LongType) < $"b" && $"b" < 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" < $"b" && $"b" <= 3L, $"a".cast(LongType) < $"b" && $"b" <= 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" < $"b" && $"b" === 3L, $"a".cast(LongType) < $"b" && $"b" === 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" <= $"b" && $"b" < 3L, $"a".cast(LongType) <= $"b" && $"b" < 3L &&
+        $"a".cast(LongType) < 3L),
+      ($"a" <= $"b" && $"b" <= 3L, $"a".cast(LongType) <= $"b" && $"b" <= 3L &&
+        $"a".cast(LongType) <= 3L),
+      ($"a" <= $"b" && $"b" === 3L, $"a".cast(LongType) <= $"b" && $"b" === 3L 
&&
+        $"a".cast(LongType) <= 3L),
+      ($"a" < $"b" && $"b" < 3, $"a".cast(LongType) < $"b" && $"b" < 
Literal(3).cast(LongType)
+        && $"a".cast(LongType) < Literal(3).cast(LongType)),
+      ($"a" > $"b" && $"b" > 3L, $"a".cast(LongType) > $"b" && $"b" > 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" > $"b" && $"b" >= 3L, $"a".cast(LongType) > $"b" && $"b" >= 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" > $"b" && $"b" === 3L, $"a".cast(LongType) > $"b" && $"b" === 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" >= $"b" && $"b" > 3L, $"a".cast(LongType) >= $"b" && $"b" > 3L &&
+        $"a".cast(LongType) > 3L),
+      ($"a" >= $"b" && $"b" >= 3L, $"a".cast(LongType) >= $"b" && $"b" >= 3L &&
+        $"a".cast(LongType) >= 3L),
+      ($"a" >= $"b" && $"b" === 3L, $"a".cast(LongType) >= $"b" && $"b" === 3L 
&&
+        $"a".cast(LongType) >= 3L),
+      ($"a" > $"b" && $"b" > 3, $"a".cast(LongType) > $"b" && $"b" > 
Literal(3).cast(LongType)
+        && $"a".cast(LongType) > Literal(3).cast(LongType))
+    ).foreach {
+      case (filter, inferred) =>
+        val testRelation = LocalRelation($"a".int, $"b".long)
+        val original = testRelation.where(filter)
+        val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& inferred)
+        comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+    }
+  }
+
+  test("infer inequality constraints: an attr=literal binding on one join side 
bounds " +
+    "an inequality join key on the other side") {
+    val condition = Some("x.a".attr > "y.a".attr)
+    val optimizedLeft = testRelation.where(IsNotNull($"a") && $"a" === 
1).as("x")
+    val optimizedRight = testRelation.where($"a" < 1 && 
IsNotNull($"a")).as("y")
+    val correct = optimizedLeft.join(optimizedRight, Inner, condition)
+
+    Seq(Literal(1) === $"a", $"a" === Literal(1)).foreach { filter =>
+      val original =
+        testRelation.where(filter).as("x").join(testRelation.as("y"), Inner, 
condition)
+      comparePlans(Optimize.execute(original.analyze), correct.analyze)
+    }
+  }
+
+  test("infer inequality constraints: a three-attribute chain bounded by a 
literal") {
+    val original = testRelation.where($"a" < $"b" && $"b" < $"c" && $"c" < 5)
+    val optimized = testRelation.where(IsNotNull($"a") && IsNotNull($"b") && 
IsNotNull($"c")
+      && $"a" < $"b" && $"b" < $"c" && $"a" < 5 && $"b" < 5 && $"c" < 5)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: a range join condition combined with a 
range filter") {
+    val left = testRelation.where($"b" >= 3 && $"b" <= 13).as("x")
+    val right = testRelation.as("y")
+
+    val optimizedLeft = testRelation.where(IsNotNull($"a") && IsNotNull($"b")
+      && $"b" >= 3 && $"b" <= 13).as("x")
+    val optimizedRight = testRelation.where(IsNotNull($"a") && IsNotNull($"b") 
&& IsNotNull($"c")
+      && $"c" > 3 && $"b" <= 13).as("y")
+    val condition = Some("x.a".attr === "y.a".attr
+      && "x.b".attr >= "y.b".attr && "x.b".attr < "y.c".attr)
+    val original = left.join(right, Inner, condition)
+    val optimized = optimizedLeft.join(optimizedRight, Inner, condition)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: chain across a join through an implicit 
cast") {
+    val testRelation1 = LocalRelation($"a".long, $"b".long, $"c".long).as("x")
+    val testRelation2 = LocalRelation($"a".int, $"b".int, $"c".int).as("y")
+
+    // y.b < 13 inferred from y.b < x.b && x.b <= 13
+    val left = testRelation1.where($"b" <= 13L).as("x")
+    val right = testRelation2.as("y")
+
+    val optimizedLeft =
+      testRelation1.where(IsNotNull($"a") && IsNotNull($"b") && $"b" <= 
13L).as("x")
+    val optimizedRight = testRelation2.where(IsNotNull($"a") && IsNotNull($"b")
+      && $"b".cast(LongType) < 13L).as("y")
+
+    val condition = Some("x.a".attr === "y.a".attr && "y.b".attr < "x.b".attr)
+    val original = left.join(right, Inner, condition)
+    val optimized = optimizedLeft.join(optimizedRight, Inner, condition)
+    comparePlans(Optimize.execute(original.analyze), optimized.analyze)
+  }
+
+  test("infer inequality constraints: range predicate in left-outer join ON 
clause " +
+    "is pushed to right side when left attr is bounded by inequality") {
+    // Left side: a.k >= 5
+    // Join condition: b.v >= a.k
+    // Transitive inference should deduce b.v >= 5 and push it down to the 
right side of
+    // LeftOuter join.
+    val left = LocalRelation($"k".int, $"key".int).subquery("a")
+    val right = LocalRelation($"v".int, $"key".int).subquery("b")
+
+    val joinCond = ("a.key".attr === "b.key".attr) && ("b.v".attr >= 
"a.k".attr)
+    val originalQuery = left
+      .where("a.k".attr >= 5)
+      .join(right, LeftOuter, Some(joinCond))
+      .analyze
+
+    val optimized = Optimize.execute(originalQuery)
+    var pushedToRight = false
+    optimized.foreach {
+      case Join(_, Filter(cond, _), LeftOuter, _, _) =>
+        pushedToRight = splitConjunctivePredicates(cond).exists {
+          case GreaterThanOrEqual(attr: Attribute, Literal(v: Int, 
IntegerType)) =>
+            attr.name == "v" && v == 5
+          case _ => false
+        }
+      case _ =>
+    }
+    assert(pushedToRight, s"Expected b.v >= 5 pushed to right side; plan 
was:\n$optimized")
+  }
+
+  test("SPARK-30768: do not infer filters from throwable expressions") {

Review Comment:
   **Nit (P3):** Since `isSafeToRelocate` was added, this test no longer 
exercises `!b.throwable`. Without the guard, `caseExpr < x.b` becomes a 
candidate edge, but `caseExpr` fails `isSafeToRelocate`, so no upper edge on 
`x.b` is recorded. There is also no bound on `caseExpr` to propagate, so 
nothing is inferred and the assertion passes either way.
   
   The guard still matters within one relation. In `Filter(x < caseExpr AND 
caseExpr < 5)`, `caseExpr` is the bounded site and `x` is relocatable, so 
without the guard `x < 5` is inferred and prepended. That short-circuits the 
`raise_error` the original predicate evaluates for rows with `x >= 5`. Using 
that shape and asserting that no `x < 5` is inferred would give the guard a 
real regression signal.
   
   **Verification:**
   - **Regression:** A single-relation filter whose throwable expression is 
bounded by a literal and chained to a relocatable attribute infers no new bound 
on that attribute.



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