wangyum commented on code in PR #59258:
URL: https://github.com/apache/spark/pull/59258#discussion_r4272953577
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,229 @@ 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 pass must see the
equality passes' output
+ * to chain through equality-derived edges and bounds. Because the
inequality pass emits only
+ * inequalities, the equality-based passes gain nothing from re-running on
its result, so a
Review Comment:
Updated the doc
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala:
##########
@@ -126,6 +128,229 @@ 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 pass must see the
equality passes' output
+ * to chain through equality-derived edges and bounds. Because the
inequality pass emits only
+ * inequalities, the equality-based passes gain nothing from re-running on
its result, so a
+ * single downstream call to [[inferInequalityConstraints]] suffices without
interleaving.
+ */
+ def inferAllAdditionalConstraints(constraints: ExpressionSet): ExpressionSet
= {
+ val equalityInferred =
+ inferAdditionalConstraints(constraints) ++
inferConstraintsFromLiteralBindings(constraints)
+ equalityInferred ++ inferInequalityConstraints(constraints ++
equalityInferred)
+ }
+
+ /**
+ * True when `e` cannot raise an error during evaluation: a non-null
literal, or an upcast of
+ * one admitted by [[UpCastRule.canUpCast]]. Used by
[[inferInequalityConstraints]] to decide
+ * which literals are safe to treat as chain anchors without risking an ANSI
eval failure.
+ * Arbitrary expressions (such as a parsing cast `CAST('abc' AS INT)` or
arithmetic that can
+ * fail under ANSI) are excluded.
+ */
+ @tailrec
+ private def isSafeBoundLiteral(e: Expression): Boolean = e match {
+ case l: Literal => l.value != null
+ case Cast(child, toType, _, _) =>
+ UpCastRule.canUpCast(child.dataType, toType) && isSafeBoundLiteral(child)
+ case _ => false
+ }
+
+ private def hasBoundOperand(e: Expression): Boolean = e match {
+ case b: BinaryComparison => isSafeBoundLiteral(b.left) ||
isSafeBoundLiteral(b.right)
+ case _ => false
+ }
+
+ /**
+ * True when `e` is safe to copy into a newly inferred, single-side filter
produced by
+ * [[inferInequalityConstraints]] without risking an error on rows that
never evaluated it
+ * before: an attribute reference, or an upcast of one admitted by
[[UpCastRule.canUpCast]].
+ * While some upcasts may lose precision (e.g. Long to Float/Double),
relocation is sound
+ * because they cannot fail and reuse the existing Cast expression.
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.
+ */
+ private def isSafeToRelocate(e: Expression): Boolean = e match {
+ case _: Attribute => true
+ case Cast(a: Attribute, toType, _, _) => UpCastRule.canUpCast(a.dataType,
toType)
+ case _ => false
+ }
+
+ /**
+ * A known literal bound discovered while chaining inequality constraints in
+ * [[inferInequalityConstraints]]: `site {<,<=} lit` when `upper`, or `lit
{<,<=} site`
+ * otherwise. `value` stores the evaluated literal value to avoid repeatedly
evaluating `lit`
+ * during optimization.
+ */
+ private case class InequalityBound(
+ upper: Boolean,
+ site: Expression,
+ lit: Expression,
+ strict: Boolean,
+ value: Any)
+
+ /**
+ * Converts a canonicalized comparison into an [[InequalityBound]] when
exactly one side is a
+ * safe literal bound ([[isSafeBoundLiteral]]); `None` for
attribute-to-attribute comparisons.
+ */
+ private def toBound(e: Expression): Option[InequalityBound] = e match {
+ case b: BinaryComparison =>
+ val strict = b.isInstanceOf[LessThan]
+ if (isSafeBoundLiteral(b.right) && !isSafeBoundLiteral(b.left)) {
+ Some(InequalityBound(upper = true, b.left, b.right, strict,
b.right.eval()))
+ } else if (isSafeBoundLiteral(b.left) && !isSafeBoundLiteral(b.right)) {
+ Some(InequalityBound(upper = false, b.right, b.left, strict,
b.left.eval()))
+ } else {
+ None
+ }
+ case _ => None
+ }
+
+ /**
+ * Infers tightest literal inequality bounds on relocatable sites by
transitively chaining
+ * inequality constraints with literal bounds. 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.
+ * Note that attribute-to-attribute inequalities without literal bounds
(e.g., `a < c` from
+ * `a < b && b < c`) are not inferred.
+ *
+ * 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
+ // a safe literal bound; every other EqualTo -- attribute-to-attribute
(handled by
+ // inferAdditionalConstraints's transitivity), as well as any other shape
without a safe
+ // literal bound such as `a + 1 = b` -- is excluded, since no inference
path below handles a
+ // non-literal equality operand.
+ val validComparisons = constraints.toSeq.filter {
+ case b: BinaryComparison =>
+ b.deterministic && !b.throwable && !b.isInstanceOf[EqualNullSafe] &&
Review Comment:
add the test
--
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]