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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala:
##########
@@ -398,62 +406,412 @@ 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

Review Comment:
   Addressed — sorry this thread went unanswered while the fix landed; it has 
been on the branch since `ab77ebe5`.
   Confirmed, and the composition is worse than silently wrong rows in the 
example I could build — it crashes. The gap is exactly where you place it: the 
three passes do not cover each other.
   * the both-plans rejection only matched a sub-query that is *itself* 
correlated and references both plans;
   * the unknown-result rejection only matches a nullable IN whose own value is 
observed beyond truth;
   * the rewrite declines the both-plan parent and, since the fix for 
`r4022391750`, does not descend into it.
   
   A correlated IN owned by the sub-query plan, nested in that parent, fell 
through all three: its own effective references are inner-only, so it looks 
routable, but nothing reaches it, and `PlanSubqueries` plans the `ListQuery` as 
an in-subquery filter and drops the predicate `PullupCorrelatedPredicates` 
hoisted into it. The no-descent behaviour is right; what was missing is that 
the validation did not know about that boundary.
   Reproduced with `t1(a) = 1,2,3`, `t2(c1) = 1,8,9`, `t3(col1) = 3,9` and 
`t4(col1, k) = (1,9),(9,1)`, where the correlation is decisive and no NULL is 
involved:
   ```sql
   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));
   ```
   PostgreSQL and DuckDB return `1, 3`; dropping the correlated predicate 
returns `1, 2, 3`. Before the fix, with 
`decorrelatePredicateSubqueriesInJoinPredicate.enabled=false`, it failed with
   ```
   java.lang.ClassCastException: class java.lang.Integer cannot be cast to class
   org.apache.spark.sql.catalyst.InternalRow
   ```
   and under the default configuration the pre-existing join-condition rewrite 
rejected the parent first, which is why nothing caught this.
   
   ### Fix is as follows:
   
   A sub-query that stays in the join condition stays there whole, so the 
both-plans rejection now also applies when such a sub-query *carries* a 
correlated existential descendant:
   
   ```scala
   case sq @ (_: Exists | _: InSubquery)
     if referencesBothPlans(sq) &&
       (hasCorrelatedCondition(sq) || carriesCorrelatedSubquery(sq)) => sq
   ```
   
   `carriesCorrelatedSubquery` looks strictly below the node, since the node 
itself is already classified by `hasCorrelatedCondition`, and reports any 
`Exists` or `InSubquery` with a non-empty hoisted condition. Reporting the 
enclosing sub-query keeps `NESTED_SUBQUERY_REFERENCING_OUTER_AND_INNER_QUERY` 
accurate, as that is the sub-query which references both plans and cannot be 
supported, and it is the one the user wrote.
   
   Against your success criteria: no correlated descendant blocked beneath a 
declined parent reaches physical planning; the distinguishing example now 
raises the structured error under both settings of that configuration rather 
than crashing; and the behaviours you asked to preserve are unchanged — the 
uncorrelated both-plan residual with no correlated descendant still returns `1, 
2`, the simplified-away correlation case is still rewritten against the outer 
plan, and direct routing and the nullable IN/NOT IN handling are untouched, 
each still covered by its own assertion.
   
   I did not take the carrier-aware traversal from your recommended change. 
Rejecting the carrier rather than classifying each descendant as reachable or 
blocked gives the same guarantee here with much less machinery, and it cannot 
reject a descendant the rewrite would have consumed, since it only fires for a 
sub-query that is itself staying in the condition. If you would rather have the 
reachability state threaded through the walk, say so and I will do that instead.
   
   One scope note, since it came up in `r4257086397`: that later finding is a 
*different* boundary — a rewritten subplan-owned parent rather than a declined 
both-plan one — and I have answered it in its own thread.



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