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


##########
sql/core/src/test/scala/org/apache/spark/sql/SubquerySuite.scala:
##########
@@ -3227,4 +3227,284 @@ class SubquerySuite extends SharedSparkSession
       }
     }
   }
+
+  test("SPARK-59351: nested subquery referencing the inner query becomes an 
existence join") {
+    // SPARK-45580 covers the case where the nested subquery references the 
outer query, in which
+    // case its existence join is built on top of the outer plan. Here the 
nested subquery
+    // references the query it is nested in, so the existence join has to be 
built on top of the
+    // subquery plan instead.
+    withTempView("t1", "t2", "t3", "t3n", "t4", "t5") {
+      Seq((1), (2), (3), (7), 
(9)).toDF("a").persist().createOrReplaceTempView("t1")
+      Seq((1), (8), (9)).toDF("c1").persist().createOrReplaceTempView("t2")
+      // t3 shares a value with t2, t5 does not, so the nested subquery 
decides the answer below.
+      Seq((3), (9)).toDF("col1").persist().createOrReplaceTempView("t3")
+      Seq((3), (7)).toDF("col1").persist().createOrReplaceTempView("t5")
+      Seq(Some(3), Some(9), 
None).toDF("col1").persist().createOrReplaceTempView("t3n")
+      // A correlated nested IN over t4 is false for every c1 in t2, while the 
same IN without its
+      // correlated predicate is true for 1 and 9, so the correlation decides 
the answer below.
+      Seq((1, 9), (9, 1)).toDF("col1", 
"k").persist().createOrReplaceTempView("t4")
+
+      // Checks the result, and that every node of the optimized plan can 
produce the attributes
+      // it references. The latter is what this fix is about: an existence 
join whose condition
+      // references an attribute produced by neither of its children can still 
return the right
+      // answer when the nested relation is empty at runtime, because the 
invalid condition is
+      // then never bound. checkAnswer alone would not catch it, as the 
missing input checks it
+      // runs only look at the root of the plan.
+      def checkAnswerAndPlan(query: String, expected: Seq[Row]): Unit = {
+        val df = sql(query)
+        val plan = df.queryExecution.optimizedPlan
+        val invalidNodes = plan.collect { case p if p.missingInput.nonEmpty => 
p }
+        assert(invalidNodes.isEmpty,
+          s"""Plan nodes reference non-reachable attributes:
+             |${invalidNodes.mkString("\n")}
+             |$plan""".stripMargin)
+        checkAnswer(df, expected)
+      }
+
+      // EXISTS rewritten as a left semi join. The correlated predicate is a 
disjunction, so it
+      // is pulled up as a whole and carries the nested IN-subquery, which 
references c1, out of
+      // the subquery plan.
+      val query1 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR c1 IN (SELECT col1 FROM t3)
+          |)""".stripMargin
+      checkAnswerAndPlan(query1, Row(1) :: Row(2) :: Row(3) :: Row(7) :: 
Row(9) :: Nil)
+
+      // Same over t5, which shares no value with t2, so the nested IN is 
false for every c1 and
+      // only the correlated predicate can hold: a mistake making it true 
would return every row.
+      val query2 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR c1 IN (SELECT col1 FROM t5)
+          |)""".stripMargin
+      checkAnswerAndPlan(query2, Row(1) :: Row(9) :: Nil)
+
+      // NOT EXISTS rewritten as a left anti join.
+      val query3 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE NOT EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR c1 IN (SELECT col1 FROM t5)
+          |)""".stripMargin
+      checkAnswerAndPlan(query3, Row(2) :: Row(3) :: Row(7) :: Nil)
+
+      // IN-subquery rewritten as a left semi join. The hoisted predicate is a 
> c1 rather than
+      // the key equality a = c1, so the answer is a IN (t2 INTERSECT t3) and 
depends on the
+      // nested subquery: dropping it would leave no row at all.
+      val query4 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE a IN (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a > c1
+          |  OR c1 IN (SELECT col1 FROM t3)
+          |)""".stripMargin
+      checkAnswerAndPlan(query4, Row(9) :: Nil)
+
+      // NOT IN-subquery rewritten as a null-aware left anti join, with a 
nested EXISTS. Only the
+      // nested EXISTS keeps 9 out of the answer, as a > c1 alone does not 
hold for it.
+      val query5 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE a NOT IN (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a > c1
+          |  OR EXISTS (SELECT 1 FROM t3 WHERE col1 = c1)
+          |)""".stripMargin
+      checkAnswerAndPlan(query5, Row(1) :: Row(2) :: Row(3) :: Row(7) :: Nil)
+
+      // A nested NOT IN-subquery keeps its null-aware semantics: c1 NOT IN 
(3, 9, NULL) is never
+      // true, so only the correlated predicate can be satisfied. Compare with 
query7, where the
+      // same NOT IN over a relation without NULL holds for c1 = 1 and returns 
every row.
+      val query6 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR c1 NOT IN (SELECT col1 FROM t3n)
+          |)""".stripMargin
+      checkAnswerAndPlan(query6, Row(1) :: Row(9) :: Nil)
+
+      val query7 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR c1 NOT IN (SELECT col1 FROM t3)
+          |)""".stripMargin
+      checkAnswerAndPlan(query7, Row(1) :: Row(2) :: Row(3) :: Row(7) :: 
Row(9) :: Nil)
+
+      // A nested subquery correlated to the query it is nested in, on a 
column it does not
+      // project, so its correlated predicate changes the answer: without it 
the nested IN would
+      // hold for c1 = 1 and row 1 would be returned as well.
+      val query8 =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a > c1
+          |  OR c1 IN (SELECT col1 FROM t4 WHERE k = c1)
+          |)""".stripMargin
+      checkAnswerAndPlan(query8, Row(2) :: Row(3) :: Row(7) :: Row(9) :: Nil)
+    }
+  }
+
+  test("SPARK-59351: nested subquery referencing both the outer and the inner 
query") {
+    withTempView("t1", "t2", "t3") {
+      Seq((1), (2), (3)).toDF("a").persist().createOrReplaceTempView("t1")
+      Seq((1), (8), (9)).toDF("c1").persist().createOrReplaceTempView("t2")
+      Seq((3), (9)).toDF("col1").persist().createOrReplaceTempView("t3")
+
+      // The nested subquery references the outer query through its values and 
the inner query
+      // through its own correlated predicate, so it can be rewritten into an 
existence join on
+      // neither side and stays in the join condition. Planning it there as an 
in-subquery filter
+      // would drop its correlated predicate col1 = c1 and return an extra 
row, so it is rejected.

Review Comment:
   **Nit (P3):** This fixture does not actually return an extra row when `col1 
= c1` is dropped. For every `a` present in `t2`, the existing `a = c1` disjunct 
already makes that value match; for every absent `a`, dropping the nested 
correlation can only contribute other `c1` values. With these rows both forms 
return 2 and 3. Please either choose distinguishing data/predicates or remove 
the result-change claim.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -158,20 +158,23 @@ object RewritePredicateSubquery extends Rule[LogicalPlan] 
with PredicateHelper {
       // Filter the plan by applying left semi and left anti joins.
       withSubquery.foldLeft(newFilter) {
         case (p, Exists(sub, _, _, conditions, subHint)) =>
-          val (joinCond, outerPlan) = rewriteExistentialExpr(conditions, p)
-          val join = buildJoin(outerPlan, 
rewriteDomainJoinsIfPresent(outerPlan, sub, joinCond),
+          val (joinCond, outerPlan, newSub) =
+            rewriteExistentialExprInJoinCondition(conditions, p, sub)

Review Comment:
   **Blocking (P1):** The EXISTS arms classify ownership against the raw `sub`, 
while both IN arms call `dedupSubqueryOnSelfJoin` first. For a self-correlated 
EXISTS with colliding ExprIds, `effectivelyReferencesPlan` can therefore see 
the same nested expression on both sides and leave it for the both-plan 
rejection, even though the established deduplication would separate the 
attributes. Please deduplicate before routing here and in the NOT EXISTS arm, 
or otherwise ensure routing uses the deduplicated subplan.
   
   **Recommended change:** Normalize ExprId conflicts for EXISTS/NOT EXISTS 
before attribute-owner routing, keep the routed condition synchronized with 
aliases, and cover nested subqueries in self-correlated EXISTS and NOT EXISTS.
   
   **Why this works:** Introduce a single preparation step for each 
predicate-subquery arm that produces an unambiguous outer/subplan pair and 
corresponding expressions before effective-reference classification. Reuse the 
prepared plan through routing and final join construction rather than 
deduplicating after classification.
   
   **Scope:** Make self-join deduplication and nested-subquery routing order 
consistent across all four predicate-subquery forms.
   
   **Compatibility:** IN and NOT IN keep their existing pre-routing 
deduplication semantics and all four arms retain correct correlated predicates.
   
   **Risks:** Moving deduplication without rewriting correlated condition 
references can create ambiguous or stale join expressions.
   
   **Constraints:** Preserve existing SPARK-21835 self-join semantics and the 
genuine both-plans rejection.
   
   **Success:** A self-correlated EXISTS nested subquery that semantically 
references one plan is routed to that plan despite pre-dedup ExprId overlap. A 
genuinely both-side correlated nested subquery is still rejected.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -450,10 +471,206 @@ 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
+  }
+
+  /**
+   * Returns true if `e` effectively references any of the attributes produced 
by `plan`, see
+   * [[effectiveReferences]].
+   */
+  private def effectivelyReferencesPlan(e: Expression, plan: LogicalPlan): 
Boolean = {
+    effectiveReferences(e).intersect(plan.outputSet).nonEmpty
+  }
+
+  /**
+   * The attributes that `e` references and can still evaluate. For an 
existential sub-query
+   * expression these are the ones of the values it compares and of the 
correlated condition that
+   * [[PullupCorrelatedPredicates]] hoisted into it, rather than the ones of 
`references`, which
+   * also include the outer attributes the pull-up retains for idempotency. 
Those outlive the
+   * condition that referenced them once BooleanSimplification has eliminated 
it, as in
+   * `a IN (SELECT col1 FROM t3 WHERE false AND col1 = c1)`, which keeps `c1` 
as an outer
+   * attribute of a sub-query that no longer reads it, and which must still be 
rewritten against
+   * the outer plan rather than treated as referencing the sub-query plan.
+   */
+  private def effectiveReferences(e: Expression): AttributeSet = e match {
+    case Exists(_, _, _, joinCond, _) => 
AttributeSet(joinCond.flatMap(_.references))
+    case Not(in: InSubquery) => effectiveReferences(in)
+    case InSubquery(values, ListQuery(_, _, _, _, joinCond, _)) =>
+      AttributeSet(values.flatMap(_.references) ++ 
joinCond.flatMap(_.references))
+    case _ => e.references
+  }
+
+  /**
+   * 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.enabled` 
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 => !effectivelyReferencesPlan(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 hasCorrelatedCondition(sq) && effectivelyReferencesPlan(sq, 
subPlan) &&
+          effectivelyReferencesPlan(sq, outerPlan) => sq
+    })
+    if (referencingBothPlans.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedSubqueryReferencingOuterAndInnerQueryError(
+        referencingBothPlans)
+    }
+    // An IN sub-query whose result can be unknown cannot be represented by 
the `exists` attribute
+    // of an existence join once that result is observable, see above.
+    val unknownResult = conditions
+      .flatMap(unknownSensitiveInSubqueries(_, inPredicate = true))
+      .filter(sq => effectivelyReferencesPlan(sq, subPlan) &&
+        !effectivelyReferencesPlan(sq, outerPlan))
+    if (unknownResult.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedInSubqueryWithUnknownResultError(unknownResult)
+    }
+    val (newCond, newSubPlan, _) = rewriteExistentialExprWithAttrs(conditions, 
subPlan,
+      e => effectivelyReferencesPlan(e, subPlan) && 
!effectivelyReferencesPlan(e, outerPlan))
+    (newCond, newSubPlan)
+  }
+
+  /**
+   * Collects the IN sub-queries in `expr` whose result can be unknown and is 
not consumed by a
+   * predicate, so that rewriting them into an existence join, whose `exists` 
attribute is FALSE
+   * where IN is unknown, would be observable. See 
[[rewriteExistentialExprInJoinCondition]].
+   *
+   * `inPredicate` states whether the value of `expr` is only ever tested for 
being TRUE, which
+   * holds for the operands of AND, OR and NOT within a condition that ends up 
in a Filter or a

Review Comment:
   **Nit (P3):** `NOT` is not a context where UNKNOWN and false are 
indistinguishable: `NOT false` is true, while `NOT UNKNOWN` remains UNKNOWN. 
The direct NOT IN case is handled safely for a different reason (its null-aware 
join rewrite). Please narrow this contract to the contexts where only truth is 
observed and explain the separate NOT IN handling.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -450,10 +471,206 @@ 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
+  }
+
+  /**
+   * Returns true if `e` effectively references any of the attributes produced 
by `plan`, see
+   * [[effectiveReferences]].
+   */
+  private def effectivelyReferencesPlan(e: Expression, plan: LogicalPlan): 
Boolean = {
+    effectiveReferences(e).intersect(plan.outputSet).nonEmpty
+  }
+
+  /**
+   * The attributes that `e` references and can still evaluate. For an 
existential sub-query
+   * expression these are the ones of the values it compares and of the 
correlated condition that
+   * [[PullupCorrelatedPredicates]] hoisted into it, rather than the ones of 
`references`, which
+   * also include the outer attributes the pull-up retains for idempotency. 
Those outlive the
+   * condition that referenced them once BooleanSimplification has eliminated 
it, as in
+   * `a IN (SELECT col1 FROM t3 WHERE false AND col1 = c1)`, which keeps `c1` 
as an outer
+   * attribute of a sub-query that no longer reads it, and which must still be 
rewritten against
+   * the outer plan rather than treated as referencing the sub-query plan.
+   */
+  private def effectiveReferences(e: Expression): AttributeSet = e match {
+    case Exists(_, _, _, joinCond, _) => 
AttributeSet(joinCond.flatMap(_.references))
+    case Not(in: InSubquery) => effectiveReferences(in)
+    case InSubquery(values, ListQuery(_, _, _, _, joinCond, _)) =>
+      AttributeSet(values.flatMap(_.references) ++ 
joinCond.flatMap(_.references))
+    case _ => e.references
+  }
+
+  /**
+   * 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.enabled` 
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 => !effectivelyReferencesPlan(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 hasCorrelatedCondition(sq) && effectivelyReferencesPlan(sq, 
subPlan) &&
+          effectivelyReferencesPlan(sq, outerPlan) => sq
+    })
+    if (referencingBothPlans.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedSubqueryReferencingOuterAndInnerQueryError(
+        referencingBothPlans)
+    }
+    // An IN sub-query whose result can be unknown cannot be represented by 
the `exists` attribute
+    // of an existence join once that result is observable, see above.
+    val unknownResult = conditions
+      .flatMap(unknownSensitiveInSubqueries(_, inPredicate = true))
+      .filter(sq => effectivelyReferencesPlan(sq, subPlan) &&
+        !effectivelyReferencesPlan(sq, outerPlan))
+    if (unknownResult.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedInSubqueryWithUnknownResultError(unknownResult)
+    }
+    val (newCond, newSubPlan, _) = rewriteExistentialExprWithAttrs(conditions, 
subPlan,
+      e => effectivelyReferencesPlan(e, subPlan) && 
!effectivelyReferencesPlan(e, outerPlan))
+    (newCond, newSubPlan)
+  }
+
+  /**
+   * Collects the IN sub-queries in `expr` whose result can be unknown and is 
not consumed by a
+   * predicate, so that rewriting them into an existence join, whose `exists` 
attribute is FALSE
+   * where IN is unknown, would be observable. See 
[[rewriteExistentialExprInJoinCondition]].
+   *
+   * `inPredicate` states whether the value of `expr` is only ever tested for 
being TRUE, which
+   * holds for the operands of AND, OR and NOT within a condition that ends up 
in a Filter or a
+   * join condition. Unknown and FALSE cannot be told apart there.
+   */
+  private def unknownSensitiveInSubqueries(
+      expr: Expression,
+      inPredicate: Boolean): Seq[Expression] = expr match {
+    case And(left, right) =>
+      unknownSensitiveInSubqueries(left, inPredicate) ++
+        unknownSensitiveInSubqueries(right, inPredicate)
+    case Or(left, right) =>
+      unknownSensitiveInSubqueries(left, inPredicate) ++
+        unknownSensitiveInSubqueries(right, inPredicate)
+    // NOT IN is rewritten with a null-aware join condition, which is 
two-valued as well.
+    case Not(in: InSubquery) =>
+      if (!inPredicate && inSubqueryMayBeUnknown(in)) Seq(in) else Nil
+    case Not(child) => unknownSensitiveInSubqueries(child, inPredicate)
+    case in: InSubquery =>
+      if (!inPredicate && inSubqueryMayBeUnknown(in)) Seq(in) else Nil
+    // The join condition of a sub-query expression is a predicate, evaluated 
by the join that
+    // the sub-query is rewritten into.
+    case sq: SubqueryExpression =>
+      sq.children.flatMap(unknownSensitiveInSubqueries(_, inPredicate = true))
+    case other =>
+      other.children.flatMap(unknownSensitiveInSubqueries(_, inPredicate = 
false))
+  }
+
+  /**
+   * Returns true if `e` is a positive IN sub-query whose row comparison can 
evaluate to unknown,
+   * that is one that can return NULL rather than only TRUE or FALSE. An 
existence join cannot
+   * represent that third value, see 
[[rewriteExistentialExprInJoinCondition]]. NOT IN is excluded,
+   * as [[rewriteExistentialExprWithAttrs]] gives it a null-aware join 
condition of its own.
+   */
+  private def inSubqueryMayBeUnknown(e: Expression): Boolean = e match {
+    case InSubquery(values, listQuery) =>
+      values.zip(listQuery.plan.output).exists { case (v, o) => v.nullable || 
o.nullable }

Review Comment:
   **Non-blocking (P2):** The new tests make only the list-query output 
nullable, so they do not protect the `v.nullable` half of this guard. A 
nullable compared value with a non-nullable, nonmatching right side can produce 
the same observable UNKNOWN. Please add a case for that independent source; 
otherwise removing `v.nullable ||` leaves all of the new tests green.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -398,14 +405,26 @@ object RewritePredicateSubquery extends Rule[LogicalPlan] 
with PredicateHelper {
     (newExpr, newPlan)
   }
 
+  /**
+   * Same as [[rewriteExistentialExpr]], but it also returns the newly 
introduced attributes, and
+   * it only rewrites the existential sub-queries for which `canRewrite` 
returns true. A sub-query
+   * that is not rewritten stays in the returned expression as it is, and is 
not descended into:
+   * rewriting an existential sub-query nested in its join condition would 
graft an existence join
+   * onto the plan for an `exists` reference that the sub-query left in place 
may never evaluate.
+   */
   private def rewriteExistentialExprWithAttrs(
     exprs: Seq[Expression],
-    plan: LogicalPlan): (Option[Expression], LogicalPlan, Seq[Attribute]) = {
+    plan: LogicalPlan,
+    canRewrite: Expression => Boolean = _ => true): (Option[Expression], 
LogicalPlan,
+      Seq[Attribute]) = {
     var newPlan = plan
     val introducedAttrs = ArrayBuffer.empty[Attribute]
-    val newExprs = exprs.map { e =>
-      e.transformDownWithPruning(_.containsAnyPattern(EXISTS_SUBQUERY, 
IN_SUBQUERY)) {
-        case Exists(sub, _, _, conditions, subHint) =>
+    def rewrite(expr: Expression): Expression = {

Review Comment:
   **Non-blocking (P2):** This hand-written traversal no longer provides the 
metadata handling that `transformDownWithPruning` did: replacements were 
created under the original node's `CurrentOrigin` and copied its TreeNode tags. 
The new attributes and rewritten subquery expressions are returned directly, so 
diagnostics can inherit the enclosing filter's origin and tags can be lost. 
Please preserve the replaced node's origin and tags around each replacement, or 
retain equivalent TreeNode transform semantics.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -450,10 +471,206 @@ 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
+  }
+
+  /**
+   * Returns true if `e` effectively references any of the attributes produced 
by `plan`, see
+   * [[effectiveReferences]].
+   */
+  private def effectivelyReferencesPlan(e: Expression, plan: LogicalPlan): 
Boolean = {
+    effectiveReferences(e).intersect(plan.outputSet).nonEmpty
+  }
+
+  /**
+   * The attributes that `e` references and can still evaluate. For an 
existential sub-query
+   * expression these are the ones of the values it compares and of the 
correlated condition that
+   * [[PullupCorrelatedPredicates]] hoisted into it, rather than the ones of 
`references`, which
+   * also include the outer attributes the pull-up retains for idempotency. 
Those outlive the
+   * condition that referenced them once BooleanSimplification has eliminated 
it, as in
+   * `a IN (SELECT col1 FROM t3 WHERE false AND col1 = c1)`, which keeps `c1` 
as an outer
+   * attribute of a sub-query that no longer reads it, and which must still be 
rewritten against
+   * the outer plan rather than treated as referencing the sub-query plan.
+   */
+  private def effectiveReferences(e: Expression): AttributeSet = e match {
+    case Exists(_, _, _, joinCond, _) => 
AttributeSet(joinCond.flatMap(_.references))
+    case Not(in: InSubquery) => effectiveReferences(in)
+    case InSubquery(values, ListQuery(_, _, _, _, joinCond, _)) =>
+      AttributeSet(values.flatMap(_.references) ++ 
joinCond.flatMap(_.references))
+    case _ => e.references
+  }
+
+  /**
+   * 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.enabled` 
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 => !effectivelyReferencesPlan(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 hasCorrelatedCondition(sq) && effectivelyReferencesPlan(sq, 
subPlan) &&
+          effectivelyReferencesPlan(sq, outerPlan) => sq
+    })
+    if (referencingBothPlans.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedSubqueryReferencingOuterAndInnerQueryError(
+        referencingBothPlans)
+    }
+    // An IN sub-query whose result can be unknown cannot be represented by 
the `exists` attribute
+    // of an existence join once that result is observable, see above.
+    val unknownResult = conditions
+      .flatMap(unknownSensitiveInSubqueries(_, inPredicate = true))
+      .filter(sq => effectivelyReferencesPlan(sq, subPlan) &&
+        !effectivelyReferencesPlan(sq, outerPlan))
+    if (unknownResult.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedInSubqueryWithUnknownResultError(unknownResult)
+    }
+    val (newCond, newSubPlan, _) = rewriteExistentialExprWithAttrs(conditions, 
subPlan,
+      e => effectivelyReferencesPlan(e, subPlan) && 
!effectivelyReferencesPlan(e, outerPlan))
+    (newCond, newSubPlan)
+  }
+
+  /**
+   * Collects the IN sub-queries in `expr` whose result can be unknown and is 
not consumed by a
+   * predicate, so that rewriting them into an existence join, whose `exists` 
attribute is FALSE
+   * where IN is unknown, would be observable. See 
[[rewriteExistentialExprInJoinCondition]].
+   *
+   * `inPredicate` states whether the value of `expr` is only ever tested for 
being TRUE, which
+   * holds for the operands of AND, OR and NOT within a condition that ends up 
in a Filter or a
+   * join condition. Unknown and FALSE cannot be told apart there.
+   */
+  private def unknownSensitiveInSubqueries(
+      expr: Expression,
+      inPredicate: Boolean): Seq[Expression] = expr match {
+    case And(left, right) =>
+      unknownSensitiveInSubqueries(left, inPredicate) ++
+        unknownSensitiveInSubqueries(right, inPredicate)
+    case Or(left, right) =>
+      unknownSensitiveInSubqueries(left, inPredicate) ++
+        unknownSensitiveInSubqueries(right, inPredicate)
+    // NOT IN is rewritten with a null-aware join condition, which is 
two-valued as well.
+    case Not(in: InSubquery) =>

Review Comment:
   **Non-blocking (P2):** When this branch is rejected, `Seq(in)` discards the 
`Not`, so `nestedInSubqueryWithUnknownResultError` renders the user's NOT IN 
expression as IN. Preserve the full `Not(in)` for the diagnostic. Please also 
assert `subqueryExpression` for this error and both parameters of the new 
both-plans error; the current condition-only checks cannot catch this 
regression.



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