cloud-fan commented on code in PR #59258:
URL: https://github.com/apache/spark/pull/59258#discussion_r4237673518


##########
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] &&

Review Comment:
   **Non-blocking (P2):** `!b.throwable` is not positive evidence that the 
operand is safe to evaluate on additional rows: ordinary ANSI casts and 
arithmetic can fail while inheriting `throwable = false`. A cross-side residual 
such as `CAST(x.s AS INT) < y.b`, combined with `y.b < 5`, now derives a child 
filter `CAST(x.s AS INT) < 5`; an unmatched `x` row that never evaluated the 
residual before can therefore raise `CAST_INVALID_INPUT`. This changes query 
behavior for inner, left-outer, and left-anti paths rather than merely adding a 
pushdown.
   
   **Verification:**
   - **Regression:** Execute an ANSI parsing-cast range predicate with 
unmatched keys through inner, outer, and anti join relocation paths and verify 
that enabling constraint propagation neither evaluates the cast on previously 
unevaluated rows nor changes the result.
   - **Compatibility:** Verify that a lossless numeric widening comparison 
still derives and pushes its range bound, while parsing, narrowing, arithmetic, 
and unknown-expression operands do not.



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

Review Comment:
   **Blocking (P1):** This fixed point rescans the full candidates-by-bounds 
product after every newly discovered bound. For `x1 < x2 < ... < xn < L`, only 
one bound advances per round, but all `n - 1` candidates are compared with 
every settled bound each time, yielding Theta(n^3) semantic comparisons (about 
500 million at `n = 1000`) before execution. Because constraint propagation is 
enabled by default and the input width is unbounded, wide generated predicates 
can make planning impractical even when the inferred filters provide no I/O 
benefit.
   
   **Recommended change:** Replace the recursive full-set fixed point with an 
endpoint-indexed directed work list. Normalize each inequality into a directed 
edge with strictness metadata, expand a foldable equality into both directed 
bounds outside ExpressionSet canonical membership, seed the queue with 
equality- and literal-derived bounds, and propagate only newly discovered 
semantic bounds along adjacent edges. Convert results back to ExpressionSet 
only at the output boundary. Add mixed-chain spelling regressions and a wide 
sparse-chain workload that detects settled-pair rescanning.
   
   **Why this works:** A directed record preserves the endpoint orientation 
that commutative EqualTo canonicalization loses. An adjacency index finds only 
edges whose endpoint matches a newly queued upper or lower bound; the derived 
edge is strict when either input edge is strict, and a semantic-key set 
prevents cycles and duplicate work. Each discovered bound is processed once 
instead of rebuilding and crossing the complete closure on every depth step.
   
   **Scope:** Make inequality closure explicitly directed and incremental while 
preserving the public ExpressionSet result and both existing production call 
routes.
   
   **Compatibility:** Constraint propagation continues to infer the complete 
supported transitive range closure when enabled, including safe 
literal-equality and widening-cast chains, and produces no added bounds when 
disabled.
   
   **Risks:** An incorrect semantic key can merge distinct typed or collated 
bounds or fail to terminate on cycles. Strictness can be weakened if a path 
containing one strict comparison is combined incorrectly. Multiple incomparable 
literal bounds can still require output-proportional work even after avoidable 
rescans are removed.
   
   **Constraints:** Preserve all currently supported strict, non-strict, upper, 
lower, equality-bound, cast-coercion, and collation admission semantics except 
where a separate safety repair narrows unsafe expressions. Keep 
spark.sql.constraintPropagation.enabled as the existing switch and preserve 
legacy no-inference behavior when it is disabled. Return canonical 
ExpressionSet predicates to callers so downstream constraint and filter 
consumers keep their existing contract.
   
   **Success:** Mixed equality/inequality chains derive the same bound for both 
SQL equality operand orders. Every newly discovered directed bound is expanded 
once; advancing chain depth does not trigger a Cartesian rescan of all settled 
candidates and bounds. Strictness and direction of every emitted upper or lower 
bound match the composed path. Plan-local constraints and join-side pushed 
filters observe the same complete canonical closure.
   
   **Verification:**
   - **Regression:** Exercise equality-derived literal bounds followed by 
inequality edges in both equality operand orders and verify identical inferred 
upper and lower bounds through both plan-local and join-side propagation.
   - **Behavior:** Cover combinations of strict and non-strict edges, cycles, 
multiple paths, and multiple foldable bounds and verify the exact directed 
closure without duplicate predicates.
   - **Regression:** Build a wide sparse chain with one terminal literal bound 
and verify planning completes within a bounded workload that would expose full 
settled-set rescanning rather than relying on a small three-edge example.
   - **Inspection:** Confirm the traversal indexes edges by endpoint, queues 
only newly discovered semantic bounds, and has no loop that recomputes the full 
candidates-by-bounds product at each fixed-point depth.



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