pratham76 commented on code in PR #58656:
URL: https://github.com/apache/spark/pull/58656#discussion_r4064435485


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -450,10 +471,171 @@ object RewritePredicateSubquery extends 
Rule[LogicalPlan] with PredicateHelper {
             ExistenceJoin(exists), newConditions, joinHint)
           introducedAttrs += exists
           exists
+        // A sub-query that `canRewrite` declined is left as it is, children 
included.
+        case sq @ (_: Exists | Not(_: InSubquery) | _: InSubquery) => sq
+        case other => other.mapChildren(rewrite)
       }
     }
+    val newExprs = exprs.map(rewrite)
     (newExprs.reduceOption(And), newPlan, introducedAttrs.toSeq)
   }
+
+  /**
+   * Returns true if `e` references any of the attributes produced by `plan`.
+   */
+  private def referencesPlan(e: Expression, plan: LogicalPlan): Boolean = {
+    e.references.intersect(plan.outputSet).nonEmpty
+  }
+
+  /**
+   * Rewrites the existential sub-queries that are nested in the correlated 
predicates which
+   * [[PullupCorrelatedPredicates]] hoisted out of a predicate sub-query, and 
which therefore end
+   * up in the condition of the semi/anti join that replaces that sub-query.
+   *
+   * A hoisted predicate can carry a nested existential sub-query out of the 
sub-query plan,
+   * because a correlated predicate is hoisted as a whole when it is a 
disjunction. For example
+   *
+   *   SELECT * FROM t1 WHERE EXISTS (
+   *     SELECT 1 FROM t2 WHERE t1.a = t2.c1 OR t2.c1 IN (SELECT col1 FROM t3))
+   *
+   * hoists `a = c1 OR c1 IN (SELECT col1 FROM t3)` into the join condition. 
Such a nested
+   * sub-query must be rewritten against the plan that produces the attributes 
it references:
+   * the one above references `c1`, which is produced by the sub-query plan 
and not by the outer
+   * plan, so its existence join has to be built on top of the sub-query plan. 
Building it on top
+   * of the outer plan instead yields a join whose condition references an 
attribute that neither
+   * of its children can produce (SPARK-59351).
+   *
+   * A nested sub-query that references both plans can be rewritten into an 
existence join on
+   * neither side, so it is left in the join condition, where both plans are 
in scope. Leaving it
+   * there is only correct while it is uncorrelated: the join condition of a 
correlated one is
+   * dropped when it is planned as an in-subquery filter, which would silently 
change the result,
+   * so a correlated one is reported as unsupported instead. Note that such a 
sub-query can be
+   * correlated only to the sub-query plan, as being correlated to the outer 
plan as well would
+   * require two levels of correlation, which the Analyzer rejects.
+   *
+   * A sub-query left here is further subject to the rewrite of predicate 
sub-queries in join
+   * conditions, which rejects one referencing both plans under the default 
configuration; it
+   * survives only with 
`spark.sql.optimizer.decorrelatePredicateSubqueriesInJoinPredicate`
+   * disabled.
+   *
+   * An existence join yields only whether a row matched, so its `exists` 
attribute cannot tell
+   * FALSE from unknown, while `IN` is three-valued. The two are 
indistinguishable while the value
+   * only feeds a predicate, which is what a hoisted condition normally does, 
but not when it
+   * reaches something else, e.g. `(c1 IN (SELECT col1 FROM t3)) <=> false`, 
which is FALSE for a
+   * NULL that matches nothing and TRUE for the `exists` attribute. An IN 
sub-query whose row
+   * comparison can evaluate to unknown is therefore rejected in that position 
rather than
+   * rewritten. NOT IN is rewritten with a null-aware join condition of its 
own, which is equally
+   * two-valued, so it is rejected there too.
+   *
+   * Returns the rewritten condition along with the updated outer and 
sub-query plans.
+   */
+  private def rewriteExistentialExprInJoinCondition(
+      conditions: Seq[Expression],
+      outerPlan: LogicalPlan,
+      subPlan: LogicalPlan): (Option[Expression], LogicalPlan, LogicalPlan) = {
+    val (subCond, newSubPlan) =
+      rewriteExistentialExprInSubqueryPlan(conditions, outerPlan, subPlan)
+    // The sub-queries that do not reference the sub-query plan are rewritten 
against the outer
+    // plan, as they only reference attributes of the outer plan, if any.
+    val (newCond, newOuterPlan, _) = rewriteExistentialExprWithAttrs(
+      subCond.toSeq, outerPlan, e => !referencesPlan(e, subPlan))
+    (newCond, newOuterPlan, newSubPlan)
+  }
+
+  /**
+   * Rewrites the existential sub-queries in `conditions` that can only be 
evaluated by the
+   * sub-query plan, that is those referencing the sub-query plan but not the 
outer plan, into
+   * existence joins on top of the sub-query plan. See
+   * [[rewriteExistentialExprInJoinCondition]] for details.
+   *
+   * Returns the rewritten condition along with the updated sub-query plan.
+   */
+  private def rewriteExistentialExprInSubqueryPlan(
+      conditions: Seq[Expression],
+      outerPlan: LogicalPlan,
+      subPlan: LogicalPlan): (Option[Expression], LogicalPlan) = {
+    // A sub-query left in the join condition loses its own join condition 
when it is planned
+    // there, so a correlated one would silently return a wrong result: reject 
it instead. Note
+    // that this walks the whole expression, including the join condition of a 
sub-query that the
+    // rewrite below declines to descend into. That is deliberate: a 
correlated sub-query hidden
+    // under a declined one would equally be planned without its join 
condition, or reach
+    // execution unevaluable, so it must be rejected even though nothing would 
have rewritten it.
+    val referencingBothPlans = conditions.flatMap(_.collect {
+      case sq @ (_: Exists | _: InSubquery)
+        if isCorrelatedSubquery(sq) && referencesPlan(sq, subPlan) &&

Review Comment:
   You are right, and it was worse than a spurious rejection: it was a 
regression against released
   Spark. Your query works on 4.0.1, returning `1`:
   ```sql
   SELECT * FROM t1 WHERE EXISTS (
     SELECT 1 FROM t2 WHERE a = c1 OR a IN (SELECT col1 FROM t3 WHERE false AND 
col1 = c1));
   ```
   ```
   -- 4.0.1 optimized plan: the existence join sits on the outer plan, which is 
valid, because the
   -- only attribute the nested subquery still reads is a
   Project [a#18]
   +- Join LeftSemi, ((a#18 = c1#19) OR exists#22)
      :- Join ExistenceJoin(exists#22), (a#18 = col1#20)
      :  :- LocalRelation [a#18]
      :  +- LocalRelation <empty>, [col1#20]
      +- Project [c1#19]
   ```
   This PR rejected it with `0A000`, so a working query would have started 
failing.
   Classifying correlation from the surviving condition, as you asked, was 
necessary but not
   sufficient on its own: with that alone the subquery was no longer rejected 
by the new guard, but it
   was still classified as referencing the subquery plan, because `references` 
carries the retained
   `c1`. It therefore stayed in the join condition and the pre-existing rewrite 
of predicate
   subqueries in join conditions rejected it under the default configuration 
instead, with
   `UNSUPPORTED_CORRELATED_EXPRESSION_IN_JOIN_CONDITION`. I only noticed 
because I checked the query
   against 4.0.1 rather than against this PR alone.
   So the routing uses the surviving state too:
   * `hasCorrelatedCondition` replaces `isCorrelated`, and reads the hoisted 
join condition, which is
     what would actually be lost if the subquery were left in a join condition. 
`rewriteDomainJoins`
     already reasons this way -- its comment notes that BooleanSimplification 
eliminating every
     correlated predicate leaves `joinCond` as `None` and hence no domain join 
behind;
   * `effectiveReferences` gives the attributes of the compared values and of 
that hoisted condition,
     excluding the outer attributes the pull-up retains for idempotency, and 
the routing and both
     guards use it.
   Your query is now rewritten against the outer plan as it is on 4.0.1, under 
both settings of
   `decorrelatePredicateSubqueriesInJoinPredicate.enabled`, and the genuinely 
correlated shape is
   still rejected under both:
   
   | | default conf | conf off |
   |---|---|---|
   | `false AND col1 = c1` (eliminated) | `1` | `1` |
   | `col1 = c1` (genuine) | `0A000` | `0A000` |
   
   Added to the regression test, with a comment on why the subquery keeps `c1` 
while no longer reading
   it. Reverting either half of the classification fails that test, reporting
   `The subquery (t1.a IN (listquery(t2.c1))) ... references both the outer 
query and the subquery it
   is nested in`, where `listquery(t2.c1)` is the retained attribute.
   Note that `referencesPlan` and `effectivelyReferencesPlan` now both exist: 
the first is the plain
   `references` test, still shared with the `case j: Join` handler, and the 
second is used by the
   nested subquery routing. They are deliberately different questions -- 
whether an expression names
   an attribute at all, and whether it can still read one -- so I kept both 
rather than changing the
   pre-existing handler's classification, which is outside this JIRA. Happy to 
align that one too if
   you would prefer it.
   



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