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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -398,62 +406,476 @@ 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) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          val existenceJoin = ExistenceJoin(exists)
-          val newCondition = conditions.reduceLeftOption(And)
-          newPlan =
-            buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
-              existenceJoin, newCondition, subHint)
-          introducedAttrs += exists
-          exists
-        case Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          // To handle a null-aware predicate not-in-subquery in nested 
conditions
-          // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we transform
-          // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
-          //
-          // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
-          // is transformed into a plan below;
-          // == Optimized Logical Plan ==
-          // Project [id#78, v#79]
-          // +- Filter ((v#79 > 0) OR NOT exists#83)
-          //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
-          //     :- Relation[id#78,v#79] parquet
-          //     +- Relation[id#80] parquet
-          val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
-          val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan,
-            rewriteDomainJoinsIfPresent(newPlan, newSub, Some(finalJoinCond)),
-            ExistenceJoin(exists), Some(finalJoinCond), joinHint)
-          introducedAttrs += exists
-          Not(exists)
-        case InSubquery(values, ListQuery(sub, _, _, _, conditions, subHint)) 
=>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, newSub, 
newConditions),
-            ExistenceJoin(exists), newConditions, joinHint)
-          introducedAttrs += exists
-          exists
+    // Builds the replacement of a sub-query expression under that 
expression's origin and with
+    // its tags, as TreeNode.transformDownWithPruning does for the nodes a 
rule replaces. Without
+    // it every node built below, the `exists` attribute and the join 
conditions included, would
+    // take the origin of the enclosing operator, and a runtime error raised 
from the rewritten
+    // condition would quote that operator rather than the sub-query fragment.
+    def replacing(sq: Expression)(replacement: => Expression): Expression = {
+      val newExpr = CurrentOrigin.withOrigin(sq.origin)(replacement)
+      newExpr.copyTagsFrom(sq)
+      newExpr
+    }
+    def rewrite(expr: Expression): Expression = {
+      if (!expr.containsAnyPattern(EXISTS_SUBQUERY, IN_SUBQUERY)) {
+        return expr
+      }
+      expr match {
+        case sq @ Exists(sub, _, _, conditions, subHint) if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            val existenceJoin = ExistenceJoin(exists)
+            val newCondition = conditions.reduceLeftOption(And)
+            newPlan =
+              buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
+                existenceJoin, newCondition, subHint)
+            introducedAttrs += exists
+            exists
+          }
+        case sq @ Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint)))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            // To handle a null-aware predicate not-in-subquery in nested 
conditions
+            // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we 
transform
+            // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
+            //
+            // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
+            // is transformed into a plan below;
+            // == Optimized Logical Plan ==
+            // Project [id#78, v#79]
+            // +- Filter ((v#79 > 0) OR NOT exists#83)
+            //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
+            //     :- Relation[id#78,v#79] parquet
+            //     +- Relation[id#80] parquet
+            val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
+            val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan,
+              rewriteDomainJoinsIfPresent(newPlan, newSub, 
Some(finalJoinCond)),
+              ExistenceJoin(exists), Some(finalJoinCond), joinHint)
+            introducedAttrs += exists
+            Not(exists)
+          }
+        case sq @ InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, 
newSub, newConditions),
+              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 = {
+    // Iterates the references and probes the output set, rather than 
materialising the
+    // intersection: AttributeSet.intersect builds a set from its argument, so 
the operand order
+    // there would decide whether this walks the references or the whole 
output of `plan`. The
+    // membership probes still construct wrappers, so this is cheaper, not 
allocation-free.
+    e.references.exists(plan.outputSet.contains)
+  }
+
+  /**
+   * 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).exists(plan.outputSet.contains)
+  }
+
+  /**
+   * Which of an outer plan and a sub-query plan a set of references can be 
evaluated by, as
+   * [[planReferences]] determines it.
+   */
+  private case class PlanReferences(subPlanOnly: Boolean, bothPlans: Boolean)
+
+  /**
+   * Classifies `references` against `outerPlan` and `subPlan` in a single 
pass: whether they can
+   * only be evaluated by `subPlan`, and whether they need both plans.
+   *
+   * An attribute that both plans produce is attributed to `subPlan` rather 
than to `outerPlan`,
+   * which is what the `subPlan` probe coming first does. The two plans share 
no ExprId in any
+   * shape reached by the suite: `DeduplicateRelations` has given the 
sub-query its own, and the
+   * EXISTS arms classify against the raw sub-query plan while the IN arms 
classify against the
+   * plan `dedupSubqueryOnSelfJoin` has already separated from the outer plan. 
The order is
+   * therefore defensive rather than something a query exercises, and it is 
the safe one: a
+   * nested sub-query resolves in the scope of the sub-query it is nested in, 
so an attribute
+   * both plans produce is one of that sub-query's own, and attributing it to 
`outerPlan` instead
+   * would let a single shared attribute satisfy both sides of the 
classification and have a
+   * nested sub-query referencing only the inner query rejected as referencing 
both.
+   *
+   * An attribute neither plan produces leaves both answers false, which 
declines the rewrite
+   * without rejecting the sub-query, as a sub-query referencing neither plan 
can be left in the
+   * join condition unharmed.
+   */
+  private def planReferences(
+      references: AttributeSet,
+      outerPlan: LogicalPlan,
+      subPlan: LogicalPlan): PlanReferences = {
+    var subPlanReference = false
+    var outerPlanOnlyReference = false
+    val attrs = references.iterator
+    // Stops as soon as both plans are known to be referenced, which is all 
the two answers need.
+    while (attrs.hasNext && !(subPlanReference && outerPlanOnlyReference)) {
+      val attr = attrs.next()
+      if (subPlan.outputSet.contains(attr)) {
+        subPlanReference = true
+      } else if (outerPlan.outputSet.contains(attr)) {
+        outerPlanOnlyReference = true
+      }
+    }
+    PlanReferences(
+      subPlanOnly = subPlanReference && !outerPlanOnlyReference,
+      bothPlans = subPlanReference && outerPlanOnlyReference)
+  }
+
+  /**
+   * 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.
+   *
+   * The sets are merged with `AttributeSet.fromAttributeSets` rather than 
built from a flattened
+   * sequence of attributes: `AttributeSet.apply` would take one allocation 
per attribute of a
+   * sequence as long as the total number of references, which a row-valued IN 
over a wide row
+   * makes unnecessarily large.
+   */
+  private def effectiveReferences(e: Expression): AttributeSet = e match {
+    case Exists(_, _, _, joinCond, _) =>
+      AttributeSet.fromAttributeSets(joinCond.map(_.references))
+    case Not(in: InSubquery) => effectiveReferences(in)
+    case InSubquery(values, ListQuery(_, _, _, _, joinCond, _)) =>
+      AttributeSet.fromAttributeSets(values.map(_.references) ++ 
joinCond.map(_.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 cannot be rewritten into an 
existence join on
+   * either side, since neither alone can evaluate it, so it has to stay 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 that rewrite when
+   * 
`spark.sql.optimizer.decorrelatePredicateSubqueriesInJoinPredicate.enabled` is 
disabled, and
+   * also when it is an uncorrelated IN sub-query and
+   * 
`spark.sql.optimizer.optimizeUncorrelatedInSubqueriesInJoinCondition.enabled` 
is disabled,
+   * since it is then not among the sub-queries that rewrite considers.
+   *
+   * 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. This 
accepts on the
+    // negation of the test above rather than on a positive one, which rests 
on the invariant that
+    // `effectiveReferences` sees every attribute the rewrite can put in a 
join condition: one it
+    // cannot see would be rewritten here and end up in a condition the outer 
plan cannot evaluate.
+    // A positive test is not available without also refusing the sub-queries 
whose retained outer
+    // attributes no longer correspond to a condition, which must keep being 
rewritten here.
+    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) = {
+    // The rejection collector below is a plain `collect`, which visits every 
node of the hoisted
+    // conditions whether or not it can hold a sub-query, so skip the 
classification entirely when
+    // they hold none at all, which is the common case for a correlated EXISTS 
or IN.
+    // [[collectUnknownSensitiveInSubqueries]] prunes subquery-free subtrees 
on its own, so this
+    // guard only saves it the top-level call.
+    if (!conditions.exists(_.containsAnyPattern(EXISTS_SUBQUERY, 
IN_SUBQUERY))) {
+      return (conditions.reduceOption(And), subPlan)
+    }
+    // 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. The
+    // same holds for a correlated sub-query nested below one that is left 
there, since the
+    // rewrite leaves a declined sub-query whole, children included, and 
nothing else will reach
+    // it: see carriesCorrelatedSubquery. Note that this walks the whole 
expression, including the
+    // join condition of a sub-query the rewrite declines to descend into, 
which is deliberate for
+    // the same reason.
+    //
+    // Which plan a candidate references is asked of it up to three times 
below, once for the
+    // rejection of a sub-query referencing both plans, once to keep only the 
unknown-sensitive
+    // ones the sub-query plan owns, and once more by the rewrite itself. Both 
answers come from
+    // the same reference set in a single pass over it, and the result is 
memoised per candidate,
+    // so neither the set nor the scan is repeated. The memo is keyed by 
identity: the candidates
+    // are nodes of the expressions being classified, and comparing two of 
them structurally would
+    // compare the sub-query plans they hold.
+    val classified = new java.util.IdentityHashMap[Expression, 
PlanReferences]()
+    def classify(sq: Expression): PlanReferences = {
+      var references = classified.get(sq)
+      if (references == null) {
+        references = planReferences(effectiveReferences(sq), outerPlan, 
subPlan)
+        classified.put(sq, references)
+      }
+      references
+    }
+    def ownedBySubPlan(sq: Expression): Boolean = classify(sq).subPlanOnly
+    def referencesBothPlans(sq: Expression): Boolean = classify(sq).bothPlans
+    val referencingBothPlans = conditions.flatMap(_.collect {
+      case sq @ (_: Exists | _: InSubquery)
+        if referencesBothPlans(sq) &&

Review Comment:
   **Blocking (P1):** This gate checks carried descendant correlation only for 
an existential that itself references both plans. A subplan-owned parent can 
therefore pass validation even when one of its retained condition carriers 
contains a deeper correlated ListQuery. The parent rewrite replaces that 
expression atomically without visiting the child, and with join-predicate 
subquery decorrelation disabled PlanSubqueries later ignores the child's 
nonempty joinCond, so distinguishing data can silently return extra rows.
   
   **Verification:**
   - **Regression:** Exercise a subplan-owned parent existential carrying a 
deeper correlated IN with distinguishing non-null data and verify structured 
rejection, rather than an extra row, with join-predicate decorrelation both 
enabled and disabled.
   - **Behavior:** Cover neighboring uncorrelated and directly rewritable 
nested existential shapes to show that the added validation does not disable 
valid owner-based routing.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -398,62 +406,476 @@ 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) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          val existenceJoin = ExistenceJoin(exists)
-          val newCondition = conditions.reduceLeftOption(And)
-          newPlan =
-            buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
-              existenceJoin, newCondition, subHint)
-          introducedAttrs += exists
-          exists
-        case Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          // To handle a null-aware predicate not-in-subquery in nested 
conditions
-          // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we transform
-          // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
-          //
-          // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
-          // is transformed into a plan below;
-          // == Optimized Logical Plan ==
-          // Project [id#78, v#79]
-          // +- Filter ((v#79 > 0) OR NOT exists#83)
-          //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
-          //     :- Relation[id#78,v#79] parquet
-          //     +- Relation[id#80] parquet
-          val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
-          val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan,
-            rewriteDomainJoinsIfPresent(newPlan, newSub, Some(finalJoinCond)),
-            ExistenceJoin(exists), Some(finalJoinCond), joinHint)
-          introducedAttrs += exists
-          Not(exists)
-        case InSubquery(values, ListQuery(sub, _, _, _, conditions, subHint)) 
=>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, newSub, 
newConditions),
-            ExistenceJoin(exists), newConditions, joinHint)
-          introducedAttrs += exists
-          exists
+    // Builds the replacement of a sub-query expression under that 
expression's origin and with
+    // its tags, as TreeNode.transformDownWithPruning does for the nodes a 
rule replaces. Without
+    // it every node built below, the `exists` attribute and the join 
conditions included, would
+    // take the origin of the enclosing operator, and a runtime error raised 
from the rewritten
+    // condition would quote that operator rather than the sub-query fragment.
+    def replacing(sq: Expression)(replacement: => Expression): Expression = {
+      val newExpr = CurrentOrigin.withOrigin(sq.origin)(replacement)
+      newExpr.copyTagsFrom(sq)
+      newExpr
+    }
+    def rewrite(expr: Expression): Expression = {
+      if (!expr.containsAnyPattern(EXISTS_SUBQUERY, IN_SUBQUERY)) {
+        return expr
+      }
+      expr match {
+        case sq @ Exists(sub, _, _, conditions, subHint) if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            val existenceJoin = ExistenceJoin(exists)
+            val newCondition = conditions.reduceLeftOption(And)
+            newPlan =
+              buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
+                existenceJoin, newCondition, subHint)
+            introducedAttrs += exists
+            exists
+          }
+        case sq @ Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint)))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            // To handle a null-aware predicate not-in-subquery in nested 
conditions
+            // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we 
transform
+            // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
+            //
+            // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
+            // is transformed into a plan below;
+            // == Optimized Logical Plan ==
+            // Project [id#78, v#79]
+            // +- Filter ((v#79 > 0) OR NOT exists#83)
+            //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
+            //     :- Relation[id#78,v#79] parquet
+            //     +- Relation[id#80] parquet
+            val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
+            val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan,
+              rewriteDomainJoinsIfPresent(newPlan, newSub, 
Some(finalJoinCond)),
+              ExistenceJoin(exists), Some(finalJoinCond), joinHint)
+            introducedAttrs += exists
+            Not(exists)
+          }
+        case sq @ InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, 
newSub, newConditions),
+              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 = {
+    // Iterates the references and probes the output set, rather than 
materialising the
+    // intersection: AttributeSet.intersect builds a set from its argument, so 
the operand order
+    // there would decide whether this walks the references or the whole 
output of `plan`. The
+    // membership probes still construct wrappers, so this is cheaper, not 
allocation-free.
+    e.references.exists(plan.outputSet.contains)
+  }
+
+  /**
+   * 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).exists(plan.outputSet.contains)
+  }
+
+  /**
+   * Which of an outer plan and a sub-query plan a set of references can be 
evaluated by, as
+   * [[planReferences]] determines it.
+   */
+  private case class PlanReferences(subPlanOnly: Boolean, bothPlans: Boolean)
+
+  /**
+   * Classifies `references` against `outerPlan` and `subPlan` in a single 
pass: whether they can
+   * only be evaluated by `subPlan`, and whether they need both plans.
+   *
+   * An attribute that both plans produce is attributed to `subPlan` rather 
than to `outerPlan`,
+   * which is what the `subPlan` probe coming first does. The two plans share 
no ExprId in any

Review Comment:
   **Nit (P3):** This comment says that no shape reached by the suite shares an 
ExprId, but the added RewriteSubquerySuite fixture deliberately gives the outer 
and subquery plans the same attribute, asserts their overlap, and runs 
Optimize. The rationale should distinguish analyzer-produced SQL plans from 
that optimizer-level fixture instead of describing the subplan-first branch as 
unexercised defensive code.
   
   **Verification:**
   - **Inspection:** Compare the ownership rationale with the shared-attribute 
fixture and confirm that their reachability and expected subplan-first 
classification statements agree.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -398,62 +406,476 @@ 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) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          val existenceJoin = ExistenceJoin(exists)
-          val newCondition = conditions.reduceLeftOption(And)
-          newPlan =
-            buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
-              existenceJoin, newCondition, subHint)
-          introducedAttrs += exists
-          exists
-        case Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))) =>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          // To handle a null-aware predicate not-in-subquery in nested 
conditions
-          // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we transform
-          // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
-          //
-          // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
-          // is transformed into a plan below;
-          // == Optimized Logical Plan ==
-          // Project [id#78, v#79]
-          // +- Filter ((v#79 > 0) OR NOT exists#83)
-          //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
-          //     :- Relation[id#78,v#79] parquet
-          //     +- Relation[id#80] parquet
-          val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
-          val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan,
-            rewriteDomainJoinsIfPresent(newPlan, newSub, Some(finalJoinCond)),
-            ExistenceJoin(exists), Some(finalJoinCond), joinHint)
-          introducedAttrs += exists
-          Not(exists)
-        case InSubquery(values, ListQuery(sub, _, _, _, conditions, subHint)) 
=>
-          val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
-          // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
-          val joinHint = JoinHint(None, subHint)
-          newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, newSub, 
newConditions),
-            ExistenceJoin(exists), newConditions, joinHint)
-          introducedAttrs += exists
-          exists
+    // Builds the replacement of a sub-query expression under that 
expression's origin and with
+    // its tags, as TreeNode.transformDownWithPruning does for the nodes a 
rule replaces. Without
+    // it every node built below, the `exists` attribute and the join 
conditions included, would
+    // take the origin of the enclosing operator, and a runtime error raised 
from the rewritten
+    // condition would quote that operator rather than the sub-query fragment.
+    def replacing(sq: Expression)(replacement: => Expression): Expression = {
+      val newExpr = CurrentOrigin.withOrigin(sq.origin)(replacement)
+      newExpr.copyTagsFrom(sq)
+      newExpr
+    }
+    def rewrite(expr: Expression): Expression = {
+      if (!expr.containsAnyPattern(EXISTS_SUBQUERY, IN_SUBQUERY)) {
+        return expr
+      }
+      expr match {
+        case sq @ Exists(sub, _, _, conditions, subHint) if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            val existenceJoin = ExistenceJoin(exists)
+            val newCondition = conditions.reduceLeftOption(And)
+            newPlan =
+              buildJoin(newPlan, rewriteDomainJoinsIfPresent(newPlan, sub, 
newCondition),
+                existenceJoin, newCondition, subHint)
+            introducedAttrs += exists
+            exists
+          }
+        case sq @ Not(InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint)))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            // To handle a null-aware predicate not-in-subquery in nested 
conditions
+            // (e.g., `v > 0 OR t1.id NOT IN (SELECT id FROM t2)`), we 
transform
+            // `inCondition` (t1.id=t2.id) into `(inCondition) OR 
ISNULL(inCondition)`.
+            //
+            // For example, `SELECT * FROM t1 WHERE v > 0 OR t1.id NOT IN 
(SELECT id FROM t2)`
+            // is transformed into a plan below;
+            // == Optimized Logical Plan ==
+            // Project [id#78, v#79]
+            // +- Filter ((v#79 > 0) OR NOT exists#83)
+            //   +- Join ExistenceJoin(exists#83), ((id#78 = id#80) OR 
isnull((id#78 = id#80)))
+            //     :- Relation[id#78,v#79] parquet
+            //     +- Relation[id#80] parquet
+            val nullAwareJoinConds = inConditions.map(c => Or(c, IsNull(c)))
+            val finalJoinCond = (nullAwareJoinConds ++ 
conditions).reduceLeft(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan,
+              rewriteDomainJoinsIfPresent(newPlan, newSub, 
Some(finalJoinCond)),
+              ExistenceJoin(exists), Some(finalJoinCond), joinHint)
+            introducedAttrs += exists
+            Not(exists)
+          }
+        case sq @ InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))
+            if canRewrite(sq) =>
+          replacing(sq) {
+            val exists = AttributeReference("exists", BooleanType, nullable = 
false)()
+            // Deduplicate conflicting attributes if any.
+            val newSub = dedupSubqueryOnSelfJoin(newPlan, sub, Some(values))
+            val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
+            val newConditions = (inConditions ++ 
conditions).reduceLeftOption(And)
+            val joinHint = JoinHint(None, subHint)
+            newPlan = Join(newPlan, rewriteDomainJoinsIfPresent(newPlan, 
newSub, newConditions),
+              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 = {
+    // Iterates the references and probes the output set, rather than 
materialising the
+    // intersection: AttributeSet.intersect builds a set from its argument, so 
the operand order
+    // there would decide whether this walks the references or the whole 
output of `plan`. The
+    // membership probes still construct wrappers, so this is cheaper, not 
allocation-free.
+    e.references.exists(plan.outputSet.contains)
+  }
+
+  /**
+   * 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).exists(plan.outputSet.contains)
+  }
+
+  /**
+   * Which of an outer plan and a sub-query plan a set of references can be 
evaluated by, as
+   * [[planReferences]] determines it.
+   */
+  private case class PlanReferences(subPlanOnly: Boolean, bothPlans: Boolean)
+
+  /**
+   * Classifies `references` against `outerPlan` and `subPlan` in a single 
pass: whether they can
+   * only be evaluated by `subPlan`, and whether they need both plans.
+   *
+   * An attribute that both plans produce is attributed to `subPlan` rather 
than to `outerPlan`,
+   * which is what the `subPlan` probe coming first does. The two plans share 
no ExprId in any
+   * shape reached by the suite: `DeduplicateRelations` has given the 
sub-query its own, and the
+   * EXISTS arms classify against the raw sub-query plan while the IN arms 
classify against the
+   * plan `dedupSubqueryOnSelfJoin` has already separated from the outer plan. 
The order is
+   * therefore defensive rather than something a query exercises, and it is 
the safe one: a
+   * nested sub-query resolves in the scope of the sub-query it is nested in, 
so an attribute
+   * both plans produce is one of that sub-query's own, and attributing it to 
`outerPlan` instead
+   * would let a single shared attribute satisfy both sides of the 
classification and have a
+   * nested sub-query referencing only the inner query rejected as referencing 
both.
+   *
+   * An attribute neither plan produces leaves both answers false, which 
declines the rewrite
+   * without rejecting the sub-query, as a sub-query referencing neither plan 
can be left in the
+   * join condition unharmed.
+   */
+  private def planReferences(
+      references: AttributeSet,
+      outerPlan: LogicalPlan,
+      subPlan: LogicalPlan): PlanReferences = {
+    var subPlanReference = false
+    var outerPlanOnlyReference = false
+    val attrs = references.iterator
+    // Stops as soon as both plans are known to be referenced, which is all 
the two answers need.
+    while (attrs.hasNext && !(subPlanReference && outerPlanOnlyReference)) {
+      val attr = attrs.next()
+      if (subPlan.outputSet.contains(attr)) {
+        subPlanReference = true
+      } else if (outerPlan.outputSet.contains(attr)) {
+        outerPlanOnlyReference = true
+      }
+    }
+    PlanReferences(
+      subPlanOnly = subPlanReference && !outerPlanOnlyReference,
+      bothPlans = subPlanReference && outerPlanOnlyReference)
+  }
+
+  /**
+   * 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.
+   *
+   * The sets are merged with `AttributeSet.fromAttributeSets` rather than 
built from a flattened
+   * sequence of attributes: `AttributeSet.apply` would take one allocation 
per attribute of a
+   * sequence as long as the total number of references, which a row-valued IN 
over a wide row
+   * makes unnecessarily large.
+   */
+  private def effectiveReferences(e: Expression): AttributeSet = e match {
+    case Exists(_, _, _, joinCond, _) =>
+      AttributeSet.fromAttributeSets(joinCond.map(_.references))
+    case Not(in: InSubquery) => effectiveReferences(in)
+    case InSubquery(values, ListQuery(_, _, _, _, joinCond, _)) =>
+      AttributeSet.fromAttributeSets(values.map(_.references) ++ 
joinCond.map(_.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 cannot be rewritten into an 
existence join on
+   * either side, since neither alone can evaluate it, so it has to stay 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 that rewrite when
+   * 
`spark.sql.optimizer.decorrelatePredicateSubqueriesInJoinPredicate.enabled` is 
disabled, and
+   * also when it is an uncorrelated IN sub-query and
+   * 
`spark.sql.optimizer.optimizeUncorrelatedInSubqueriesInJoinCondition.enabled` 
is disabled,
+   * since it is then not among the sub-queries that rewrite considers.
+   *
+   * 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. This 
accepts on the
+    // negation of the test above rather than on a positive one, which rests 
on the invariant that
+    // `effectiveReferences` sees every attribute the rewrite can put in a 
join condition: one it
+    // cannot see would be rewritten here and end up in a condition the outer 
plan cannot evaluate.
+    // A positive test is not available without also refusing the sub-queries 
whose retained outer
+    // attributes no longer correspond to a condition, which must keep being 
rewritten here.
+    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) = {
+    // The rejection collector below is a plain `collect`, which visits every 
node of the hoisted
+    // conditions whether or not it can hold a sub-query, so skip the 
classification entirely when
+    // they hold none at all, which is the common case for a correlated EXISTS 
or IN.
+    // [[collectUnknownSensitiveInSubqueries]] prunes subquery-free subtrees 
on its own, so this
+    // guard only saves it the top-level call.
+    if (!conditions.exists(_.containsAnyPattern(EXISTS_SUBQUERY, 
IN_SUBQUERY))) {
+      return (conditions.reduceOption(And), subPlan)
+    }
+    // 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. The
+    // same holds for a correlated sub-query nested below one that is left 
there, since the
+    // rewrite leaves a declined sub-query whole, children included, and 
nothing else will reach
+    // it: see carriesCorrelatedSubquery. Note that this walks the whole 
expression, including the
+    // join condition of a sub-query the rewrite declines to descend into, 
which is deliberate for
+    // the same reason.
+    //
+    // Which plan a candidate references is asked of it up to three times 
below, once for the
+    // rejection of a sub-query referencing both plans, once to keep only the 
unknown-sensitive
+    // ones the sub-query plan owns, and once more by the rewrite itself. Both 
answers come from
+    // the same reference set in a single pass over it, and the result is 
memoised per candidate,
+    // so neither the set nor the scan is repeated. The memo is keyed by 
identity: the candidates
+    // are nodes of the expressions being classified, and comparing two of 
them structurally would
+    // compare the sub-query plans they hold.
+    val classified = new java.util.IdentityHashMap[Expression, 
PlanReferences]()
+    def classify(sq: Expression): PlanReferences = {
+      var references = classified.get(sq)
+      if (references == null) {
+        references = planReferences(effectiveReferences(sq), outerPlan, 
subPlan)
+        classified.put(sq, references)
+      }
+      references
+    }
+    def ownedBySubPlan(sq: Expression): Boolean = classify(sq).subPlanOnly
+    def referencesBothPlans(sq: Expression): Boolean = classify(sq).bothPlans
+    val referencingBothPlans = conditions.flatMap(_.collect {
+      case sq @ (_: Exists | _: InSubquery)
+        if referencesBothPlans(sq) &&
+          (hasCorrelatedCondition(sq) || carriesCorrelatedSubquery(sq)) => 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(_, truthOnly = true))
+      .filter(ownedBySubPlan)
+    if (unknownResult.nonEmpty) {
+      throw 
QueryCompilationErrors.nestedInSubqueryWithUnknownResultError(unknownResult)
+    }
+    val (newCond, newSubPlan, _) =
+      rewriteExistentialExprWithAttrs(conditions, subPlan, ownedBySubPlan)
+    (newCond, newSubPlan)
+  }
+
+  /**
+   * Collects the IN sub-queries in `expr` whose result can be unknown and 
whose value is observed
+   * beyond being TRUE, so that rewriting them into an existence join, whose 
`exists` attribute is
+   * FALSE where IN is unknown, would be observable. See 
[[rewriteExistentialExprInJoinCondition]].
+   *
+   * `truthOnly` states whether the value of `expr` is only ever tested for 
being TRUE, which is
+   * where unknown and FALSE cannot be told apart. That holds for the operands 
of AND and OR within
+   * a condition that ends up in a Filter or a join condition, and for the 
positions
+   * [[ReplaceNullWithFalseInPredicate]] rewrites for the same reason: the 
operand of an
+   * `EqualNullSafe` against TRUE, and the predicate of an `If` or of a 
`CaseWhen` branch. The
+   * result branches of a conditional are consumed wherever the conditional 
itself is, so they
+   * inherit its context rather than being value positions of their own.
+   *
+   * It does not hold under NOT, where `NOT FALSE` is TRUE while `NOT unknown` 
is unknown. A NOT
+   * directly over an IN sub-query is still safe, but for another reason: it 
is rewritten with a
+   * null-aware join condition of its own, which is TRUE where the comparison 
is unknown, so the
+   * negation of its `exists` attribute is FALSE there, as SQL requires.
+   */
+  private def unknownSensitiveInSubqueries(
+      expr: Expression,
+      truthOnly: Boolean): Seq[Expression] = {
+    val reported = ArrayBuffer.empty[Expression]
+    collectUnknownSensitiveInSubqueries(expr, truthOnly, reported)
+    reported.toSeq
+  }
+
+  /**
+   * Appends what [[unknownSensitiveInSubqueries]] reports to `reported`, in 
the order the
+   * expression is walked. The results are accumulated rather than 
concatenated on the way back up,
+   * so a condition with many reportable sub-queries does not copy the prefix 
at every node.
+   */
+  private def collectUnknownSensitiveInSubqueries(
+      expr: Expression,
+      truthOnly: Boolean,
+      reported: ArrayBuffer[Expression]): Unit = expr match {
+    case _ if !expr.containsAnyPattern(EXISTS_SUBQUERY, IN_SUBQUERY) =>
+    case And(left, right) =>
+      collectUnknownSensitiveInSubqueries(left, truthOnly, reported)
+      collectUnknownSensitiveInSubqueries(right, truthOnly, reported)
+    case Or(left, right) =>
+      collectUnknownSensitiveInSubqueries(left, truthOnly, reported)
+      collectUnknownSensitiveInSubqueries(right, truthOnly, reported)
+    // `null <=> true` and `false <=> true` are both FALSE, so only truth is 
observed here whatever
+    // consumes the comparison.
+    case EqualNullSafe(left, Literal.TrueLiteral) =>
+      collectUnknownSensitiveInSubqueries(left, truthOnly = true, reported)
+    case EqualNullSafe(Literal.TrueLiteral, right) =>
+      collectUnknownSensitiveInSubqueries(right, truthOnly = true, reported)
+    // COALESCE(x, false) maps both unknown and FALSE to FALSE, so only truth 
is observed. A
+    // different default does not: COALESCE(x, true) is TRUE where x is 
unknown and FALSE where x
+    // is FALSE.
+    case Coalesce(child +: rest) if 
rest.headOption.contains(Literal.FalseLiteral) =>
+      collectUnknownSensitiveInSubqueries(child, truthOnly = true, reported)
+    case If(predicate, trueValue, falseValue) =>
+      collectUnknownSensitiveInSubqueries(predicate, truthOnly = true, 
reported)
+      collectUnknownSensitiveInSubqueries(trueValue, truthOnly, reported)
+      collectUnknownSensitiveInSubqueries(falseValue, truthOnly, reported)
+    case CaseWhen(branches, elseValue) =>
+      branches.foreach { case (condition, value) =>
+        collectUnknownSensitiveInSubqueries(condition, truthOnly = true, 
reported)
+        collectUnknownSensitiveInSubqueries(value, truthOnly, reported)
+      }
+      elseValue.foreach(collectUnknownSensitiveInSubqueries(_, truthOnly, 
reported))
+    // A NOT directly over an IN sub-query keeps its own null-aware rewrite, 
so only its operands
+    // are examined. Under any other NOT, unknown is observable and 
`truthOnly` does not carry.
+    case not @ Not(in: InSubquery) => collectOrDescend(not, in, truthOnly, 
reported)
+    case Not(child) =>
+      collectUnknownSensitiveInSubqueries(child, truthOnly = false, reported)
+    case in: InSubquery => collectOrDescend(in, in, truthOnly, reported)
+    case sq: SubqueryExpression =>
+      // Only the hoisted join condition of a sub-query expression is a 
predicate, evaluated by
+      // the join it is rewritten into. Its outer attributes are value 
expressions.
+      sq.getJoinCond.foreach(
+        collectUnknownSensitiveInSubqueries(_, truthOnly = true, reported))
+      sq.getOuterAttrs.foreach(
+        collectUnknownSensitiveInSubqueries(_, truthOnly = false, reported))
+    case other =>
+      other.children.foreach(
+        collectUnknownSensitiveInSubqueries(_, truthOnly = false, reported))
+  }
+
+  /**
+   * Appends `report` to `reported` if the IN sub-query `in` would lose an 
observable unknown, and
+   * otherwise descends into the values it compares and into its list query. 
The descent matters
+   * because [[InSubquery]] is a [[Predicate]] rather than a 
[[SubqueryExpression]], so an IN
+   * sub-query nested in the values or in the hoisted join condition of 
another one is only reached
+   * from here.
+   */
+  private def collectOrDescend(
+      report: Expression,
+      in: InSubquery,
+      truthOnly: Boolean,
+      reported: ArrayBuffer[Expression]): Unit = {
+    if (!truthOnly && inSubqueryMayBeUnknown(in)) {
+      reported += report
+    } else {
+      in.values.foreach(collectUnknownSensitiveInSubqueries(_, truthOnly = 
false, reported))
+      collectUnknownSensitiveInSubqueries(in.query, truthOnly, reported)
+    }
+  }
+
+  /**
+   * Returns true if the row comparison of the IN sub-query `in` can evaluate 
to unknown, that is
+   * if it can return NULL rather than only TRUE or FALSE. An existence join 
cannot represent that
+   * third value, see [[rewriteExistentialExprInJoinCondition]].
+   *
+   * This deliberately does not use `InSubquery.nullable`, which under
+   * `spark.sql.legacy.inSubqueryNullability` (SPARK-43413) reports only the 
nullability of the
+   * compared values and ignores that of the list query's columns. What is 
protected here is the
+   * three-valued result the rewrite would drop, which does not depend on that 
configuration, so
+   * the nullability is re-derived from both sides.
+   */
+  private def inSubqueryMayBeUnknown(in: InSubquery): Boolean = {
+    // Iterators, so a wide row-valued IN stops at the first nullable side 
rather than pairing up
+    // the whole arity first.
+    in.values.iterator.zip(in.query.childOutputs.iterator).exists {

Review Comment:
   **Non-blocking (P2):** This guard scans every value/output pair, but the 
added UNKNOWN-sensitive regressions are all scalar. If the implementation 
accidentally inspected only the first pair, they would still pass while a 
nullable later compared value or list output bypassed rejection and collapsed 
UNKNOWN to FALSE. A row-valued case with a non-nullable first pair and 
nullability isolated to a later pair is needed to protect the all-fields 
contract.
   
   **Verification:**
   - **Regression:** Exercise row-valued nested IN with a non-nullable first 
pair and nullability isolated to a later compared value, then to a later list 
output, and verify structured UNKNOWN-result rejection in both cases.
   - **Behavior:** Exercise the corresponding fully non-nullable row-valued 
shape and verify that owner-based rewriting remains supported.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteSubquerySuite.scala:
##########
@@ -161,4 +163,69 @@ class RewriteSubquerySuite extends PlanTest {
     val existsExpected = LocalRelation(existsQuery.analyze.output).analyze
     comparePlans(existsOptimized, existsExpected)
   }
+
+  test("SPARK-59351: nested subquery of an EXISTS whose plan shares attributes 
with the outer " +
+    "plan") {
+    // The EXISTS arms route the nested subquery against the raw subquery 
plan, while the IN arms
+    // route it against the plan dedupSubqueryOnSelfJoin has already separated 
from the outer
+    // plan. A self-join can leave an ExprId shared between the two plans for 
the optimizer
+    // (SPARK-21835), and the analyzer is not involved here, so build that 
state directly: `a` is
+    // the same attribute in the outer and in the subquery plan.
+    val a = $"a".int
+    val outer = LocalRelation(a, $"b".int)
+    val sub = LocalRelation(a)
+    val nested = LocalRelation($"col1".int)
+    assert(outer.outputSet.intersect(sub.outputSet).nonEmpty,
+      "the two plans must share an attribute for this test to mean anything")
+
+    // The nested subquery references `a`, which only the subquery plan is 
meant to give it, and
+    // is correlated to it, so it must be rewritten against the subquery plan. 
Classifying `a` as
+    // a reference to the outer plan as well would report it as referencing 
both plans.
+    val nestedInSubquery = InSubquery(
+      Seq(a),
+      ListQuery(nested.select($"col1"), joinCond = Seq(EqualTo($"col1", a))))
+    val query = Filter(
+      Exists(sub, joinCond = Seq(Or(EqualTo($"b", a), nestedInSubquery))),
+      outer)
+
+    // The shared attribute is attributed to the subquery plan, so the nested 
subquery is routed
+    // there rather than reported as referencing both plans. What then reports 
this state is the
+    // conflict check that dedupSubqueryOnSelfJoin has always run over the 
semi join condition,
+    // which names the duplicated attribute: an attribute shared between the 
two plans that the
+    // hoisted condition mentions cannot be turned into a semi join whatever 
the routing does.
+    val e = intercept[AnalysisException](Optimize.execute(query))
+    assert(e.getCondition == "_LEGACY_ERROR_TEMP_1212",
+      s"expected the pre-existing conflict check to report this, got 
${e.getCondition}")
+  }
+
+  test("SPARK-59351: the rewrite keeps the origin and the tags of the 
sub-query it replaces") {
+    // The rewrite of a nested existential sub-query builds its replacement by 
hand rather than
+    // through TreeNode.transformDownWithPruning, so it has to reproduce what 
that does: apply the
+    // rule under the origin of the node being replaced and copy its tags. 
Without this a runtime
+    // error raised from the rewritten condition quotes the enclosing operator 
instead of the
+    // sub-query fragment, which no result or plan assertion would notice.
+    val outer = LocalRelation($"a".int)
+    val inner = LocalRelation($"x".int)
+    val tag = TreeNodeTag[String]("SPARK-59351")
+    val origin = Origin(line = Some(42), startPosition = Some(7))
+    val a = outer.output.head
+    val x = inner.output.head
+    // Correlated, so that the rule's unary node handler picks the projection 
up at all.
+    val exists = CurrentOrigin.withOrigin(origin) {
+      Exists(inner, outerAttrs = Seq(a), joinCond = Seq(EqualTo(a, x)))
+    }
+    exists.setTagValue(tag, "kept")
+
+    // An Exists outside a Filter goes through the unary node handler, which 
is the path that
+    // builds the replacement by hand.
+    val rewritten = RewritePredicateSubquery(Project(Seq(Alias(exists, 
"e")()), outer))
+    val replacements = rewritten.expressions.flatMap(_.collect {
+      case attr: AttributeReference if attr.name == "exists" => attr
+    })
+    assert(replacements.size == 1, s"expected one exists attribute 
in\n$rewritten")
+    assert(replacements.head.origin == origin,

Review Comment:
   **Non-blocking (P2):** This regression exercises only Exists, whose EqualTo 
was already constructed under the target origin, and it inspects only the 
projected replacement attribute. It would remain green if the positive-IN or 
NOT-IN arms assigned the enclosing operator's origin to their synthesized 
equality, null-aware wrapper, or combined condition, moving the SQLQueryContext 
reported by later failures. Those generated condition nodes need origin 
assertions as well.
   
   **Verification:**
   - **Regression:** Exercise positive IN and NOT IN replacements under a 
distinctive source origin and assert that each generated equality, null-aware 
wrapper, and combined condition used for diagnostics retains that origin.



##########
sql/core/src/test/scala/org/apache/spark/sql/SubquerySuite.scala:
##########
@@ -3227,4 +3224,426 @@ class SubquerySuite extends SharedSparkSession
       }
     }
   }
+
+  /**
+   * Checks the result of `query`, that every node of its optimized plan can 
produce the attributes
+   * it references, and that the existence join a nested subquery was 
rewritten into sits on the
+   * subquery side of the semi/anti join.
+   *
+   * The plan checks are what SPARK-59351 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, and a
+   * rewrite that placed the join on the other side would return the right 
answer here too. Note
+   * that missingInput cannot see a subquery expression that survives in a 
condition, since
+   * QueryPlan.references folds only the outer attributes of one and collect 
does not walk its
+   * plan, so it is not a substitute for asserting the shape.
+   */
+  private def checkAnswerAndPlan(
+      query: String,
+      expected: Seq[Row],
+      existenceJoinOnSubqueryPlan: Boolean = true): 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)
+    if (existenceJoinOnSubqueryPlan) {
+      val semiOrAnti = plan.collectFirst {
+        case j: Join if j.joinType == LeftSemi || j.joinType == LeftAnti => j
+      }
+      assert(semiOrAnti.nonEmpty, s"expected a semi or anti join in\n$plan")
+      val existenceJoins = plan.collect {
+        case j: Join if j.joinType.isInstanceOf[ExistenceJoin] => j
+      }
+      assert(existenceJoins.nonEmpty, s"expected an existence join in\n$plan")
+      // The existence join must be under the right side of the semi/anti 
join, which is the
+      // subquery plan, and not under its left side, which is the outer plan.
+      assert(existenceJoins.forall(j => 
semiOrAnti.get.right.exists(_.fastEquals(j))),
+        s"the existence join must sit on the subquery side in\n$plan")
+    }
+    checkAnswer(df, expected)
+  }
+
+  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")
+
+      // 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", "t4") {
+      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")
+      // Correlated, this is false for every c1 in t2; without its correlated 
predicate it holds
+      // for 1 and 9, so the correlation decides the answer below.
+      Seq((1, 9), (9, 1)).toDF("col1", 
"k").persist().createOrReplaceTempView("t4")
+
+      // 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
+      // drops its correlated predicate col1 = c1, which is why it is rejected 
rather than left.
+      // These rows do not distinguish the two forms; the EXISTS form of this 
shape does, and
+      // returned an extra row before the rejection was added.
+      //
+      // NOT IN is the outer predicate on purpose: its rewrite returns a bare 
Join, which
+      // RewritePredicateSubquery does not offer to its own handling of 
predicate subqueries in
+      // join conditions, so this rejection is the only one that can fire. 
With EXISTS the rewrite
+      // returns a Project over the join, which that handling then rejects on 
its own under the
+      // default configuration, and the assertion would hold either way.
+      val correlated =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE a NOT IN (
+          |  SELECT c1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR a IN (SELECT col1 FROM t3 WHERE col1 = c1)
+          |)""".stripMargin
+      Seq("true", "false").foreach { decorrelateInJoinCondition =>
+        
withSQLConf(SQLConf.DECORRELATE_PREDICATE_SUBQUERIES_IN_JOIN_CONDITION.key ->
+          decorrelateInJoinCondition) {
+          val e = intercept[AnalysisException](sql(correlated).collect())
+          assert(e.getCondition == "UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY." 
+
+            "NESTED_SUBQUERY_REFERENCING_OUTER_AND_INNER_QUERY")
+          // The message must name the subquery that was rejected.
+          val reported = e.getMessageParameters.get("subqueryExpression")
+          assert(reported.contains("IN") && reported.contains("listquery"),
+            s"unexpected subqueryExpression: $reported")
+        }
+      }
+
+      // A correlated predicate that BooleanSimplification has eliminated:
+      // PullupCorrelatedPredicates retains c1 as an outer attribute of the 
nested subquery for
+      // idempotency, but the subquery
+      // no longer reads it, so there is no condition to lose. It must still 
be rewritten against
+      // the outer plan, which is the only plan it effectively references, 
rather than be treated
+      // as referencing the subquery plan and rejected.
+      val simplifiedAwayCorrelation =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT 1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR a IN (SELECT col1 FROM t3 WHERE false AND col1 = c1)
+          |)""".stripMargin
+      Seq("true", "false").foreach { decorrelateInJoinCondition =>
+        
withSQLConf(SQLConf.DECORRELATE_PREDICATE_SUBQUERIES_IN_JOIN_CONDITION.key ->
+          decorrelateInJoinCondition) {
+          checkAnswer(sql(simplifiedAwayCorrelation), Row(1) :: Nil)
+        }
+      }
+
+      // A subquery that stays in the join condition stays there whole, since 
the rewrite does
+      // not descend into one it declines, so a correlated subquery below it 
is left as well.
+      // Its own references make it look routable, being owned by the subquery 
plan, but
+      // nothing reaches it and planning a list query as an in-subquery filter 
drops the
+      // predicate the pull-up hoisted into it. The enclosing subquery is 
rejected for
+      // carrying it: PostgreSQL and DuckDB return 1 and 3 here, and losing 
the correlation
+      // returns 2 as well.
+      val carriedCorrelation =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT 1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR (a + (CASE WHEN c1 IN (SELECT col1 FROM t4 WHERE k = c1) THEN 
1 ELSE 0 END))
+          |     IN (SELECT col1 FROM t3)
+          |)""".stripMargin
+      Seq("true", "false").foreach { decorrelateInJoinCondition =>
+        
withSQLConf(SQLConf.DECORRELATE_PREDICATE_SUBQUERIES_IN_JOIN_CONDITION.key ->
+          decorrelateInJoinCondition) {
+          val e = 
intercept[AnalysisException](sql(carriedCorrelation).collect())
+          assert(e.getCondition == "UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY." 
+
+            "NESTED_SUBQUERY_REFERENCING_OUTER_AND_INNER_QUERY")
+        }
+      }
+
+      // An uncorrelated nested subquery referencing both queries has no 
correlated predicate to
+      // lose, so it stays in the join condition and is planned as an 
in-subquery filter. The
+      // configuration is disabled because the rewrite of predicate subqueries 
in join conditions
+      // rejects a subquery referencing both join inputs, which is what this 
one becomes.
+      val uncorrelated =
+        """
+          |SELECT *
+          |FROM t1
+          |WHERE EXISTS (
+          |  SELECT 1
+          |  FROM t2
+          |  WHERE a = c1
+          |  OR (a + c1) IN (SELECT col1 FROM t3)
+          |)""".stripMargin
+      withSQLConf(
+        SQLConf.DECORRELATE_PREDICATE_SUBQUERIES_IN_JOIN_CONDITION.key -> 
"false") {
+        // No existence join here: this subquery stays in the join condition, 
which is the
+        // point of the case, so only the result and the reachability of the 
plan are
+        // checked.
+        checkAnswerAndPlan(uncorrelated, Row(1) :: Row(2) :: Nil,
+          existenceJoinOnSubqueryPlan = false)
+      }
+    }
+  }
+
+  test("SPARK-59351: nested IN subquery whose result can be unknown") {
+    withTempView("t1", "t2", "t2n", "t3n", "t3", "t4", "tb") {
+      Seq((1), (2), (3)).toDF("a").persist().createOrReplaceTempView("t1")
+      Seq((1), (8), (9)).toDF("c1").persist().createOrReplaceTempView("t2")
+      // A nullable compared value, against a subquery column that is not 
nullable.
+      Seq(Some(1), None).toDF("c1").persist().createOrReplaceTempView("t2n")
+      Seq((1, 1), (9, 9)).toDF("col1", 
"k").persist().createOrReplaceTempView("t4")
+      // A boolean relation, to compare the result of a nested IN subquery 
against.
+      Seq(false).toDF("b").persist().createOrReplaceTempView("tb")
+      // The NULL matches no value of t2, so c1 IN (SELECT col1 FROM t3n) is 
unknown there, and
+      // c1 NOT IN (SELECT col1 FROM t3n) is never true.
+      Seq(Some(3), None).toDF("col1").persist().createOrReplaceTempView("t3n")
+      Seq((3), (9)).toDF("col1").persist().createOrReplaceTempView("t3")
+
+      // An existence join only records whether a row matched, so its exists 
attribute is false
+      // where IN is unknown. That is not observable while the value feeds a 
predicate, but it is
+      // once it reaches anything else, so these are rejected rather than 
rewritten. Both
+      // configurations are checked, as the rejection must not depend on the 
rewrite of predicate
+      // subqueries in join conditions.
+      Seq(
+        "(c1 IN (SELECT col1 FROM t3n)) <=> false",
+        "(c1 IN (SELECT col1 FROM t3n)) IS NULL",
+        // COALESCE to a non-false default does tell unknown from false.
+        "COALESCE(c1 IN (SELECT col1 FROM t3n), true)",
+        "(c1 NOT IN (SELECT col1 FROM t3n)) <=> false").foreach { predicate =>
+        Seq("true", "false").foreach { decorrelateInJoinCondition =>
+          
withSQLConf(SQLConf.DECORRELATE_PREDICATE_SUBQUERIES_IN_JOIN_CONDITION.key ->
+            decorrelateInJoinCondition) {
+            val e = intercept[AnalysisException] {
+              sql(s"SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE a = 
c1 OR $predicate)")
+                .collect()
+            }
+            assert(e.getCondition == 
"UNSUPPORTED_SUBQUERY_EXPRESSION_CATEGORY." +
+              "NESTED_IN_SUBQUERY_WITH_UNKNOWN_RESULT")
+            // A NOT IN must be reported as a NOT IN, not as the IN underneath 
it.
+            val reported = e.getMessageParameters.get("subqueryExpression")
+            assert(reported.startsWith("(NOT ") == predicate.contains("NOT 
IN"),
+              s"unexpected subqueryExpression for $predicate: $reported")
+          }
+        }
+      }
+
+      // The ordinary case: the value of the nested IN feeds a predicate, 
where unknown and
+      // false cannot be told apart, so it is still rewritten even though it 
can be unknown.
+      checkAnswerAndPlan(
+        """SELECT * FROM t1 WHERE EXISTS (
+          |  SELECT 1 FROM t2 WHERE a = c1 OR c1 IN (SELECT col1 FROM 
t3n))""".stripMargin,
+        Row(1) :: Nil)
+
+      // And a nested IN that cannot be unknown is rewritten even outside a 
predicate.
+      checkAnswerAndPlan(
+        """SELECT * FROM t1 WHERE EXISTS (
+          |  SELECT 1 FROM t2
+          |  WHERE a = c1 OR ((c1 IN (SELECT col1 FROM t3)) <=> 
false))""".stripMargin,
+        Row(1) :: Row(2) :: Row(3) :: Nil)
+
+      // Only truth is observed by `<=> true`, by an IF predicate and by a 
CASE WHEN
+      // condition, so those are rewritten rather than rejected. 
SimplifyConditionals folds
+      // the latter two into the first, so ordinary SQL reaches this position.
+      //
+      // The result branches of a conditional are consumed wherever the 
conditional is, so
+      // under a filter predicate they are truth-only as well. The last two 
cases do not
+      // fold, so they exercise that inheritance rather than the rewrites 
above.
+      Seq(
+        "(c1 IN (SELECT col1 FROM t3n)) <=> true",
+        "IF(c1 IN (SELECT col1 FROM t3n), true, false)",
+        "CASE WHEN c1 IN (SELECT col1 FROM t3n) THEN true ELSE false END",
+        "IF(c1 > 0, c1 IN (SELECT col1 FROM t3n), false)",
+        "CASE WHEN c1 > 0 THEN c1 IN (SELECT col1 FROM t3n) ELSE false END",
+        "CASE WHEN c1 > 100 THEN false ELSE c1 IN (SELECT col1 FROM t3n) END",
+        // COALESCE to false maps unknown and false alike, so only truth is 
observed.
+        "COALESCE(c1 IN (SELECT col1 FROM t3n), false)")
+        .foreach { predicate =>
+          checkAnswerAndPlan(
+            s"SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE a = c1 OR 
$predicate)",
+            Row(1) :: Nil)
+        }
+
+      // The unknown can come from the compared value rather than from the 
subquery column.
+      // spark.sql.legacy.inSubqueryNullability makes InSubquery.nullable 
ignore the
+      // nullability of the list query columns, but it always keeps that of 
the compared
+      // values, and the guard re-derives both, so this case is rejected under 
either value.
+      Seq("true", "false").foreach { legacyNullability =>

Review Comment:
   **Non-blocking (P2):** The legacy-nullability loop uses nullable compared 
values, so both flag values still report them as nullable. The 
RHS-only-nullable cases run only under the default setting. A regression to 
InSubquery.nullable would therefore pass this suite even though legacy=true 
suppresses RHS-derived nullability and can let an observable UNKNOWN be 
rewritten as FALSE. Please cover a non-nullable compared value and nullable 
list output under both settings.
   
   **Verification:**
   - **Compatibility:** With a non-nullable compared value and nullable list 
output, verify the same UNKNOWN-result rejection and structured expression 
parameters under both legacy nullability settings.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -158,20 +159,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)
+          val join = buildJoin(outerPlan, 
rewriteDomainJoinsIfPresent(outerPlan, newSub, joinCond),
             LeftSemi, joinCond, subHint)
           Project(p.output, join)
         case (p, Not(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)
+          val join = buildJoin(outerPlan, 
rewriteDomainJoinsIfPresent(outerPlan, newSub, joinCond),
             LeftAnti, joinCond, subHint)
           Project(p.output, join)
         case (p, InSubquery(values, ListQuery(sub, _, _, _, conditions, 
subHint))) =>
           // Deduplicate conflicting attributes if any.
-          val newSub = dedupSubqueryOnSelfJoin(p, sub, Some(values))
-          val inConditions = values.zip(newSub.output).map(EqualTo.tupled)
-          val (joinCond, outerPlan) = rewriteExistentialExpr(inConditions ++ 
conditions, p)
+          val dedupSub = dedupSubqueryOnSelfJoin(p, sub, Some(values))
+          val inConditions = values.zip(dedupSub.output).map(EqualTo.tupled)
+          val (joinCond, outerPlan, newSub) =
+            rewriteExistentialExprInJoinCondition(inConditions ++ conditions, 
p, dedupSub)

Review Comment:
   The estimator predates this PR, but this change newly places it on the 
supported build side and can disable broadcast. Please fix both statistics 
modes and add the build-side plan regression here rather than defer it.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:4139416473","thread_id":"inline:4139416473","verdict_sha256":"d615aea3940ba179c60ebaf5c4a82a1c974b3542a4ada38121d6365f9d19a83c"}
 -->



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