peter-toth commented on code in PR #57437:
URL: https://github.com/apache/spark/pull/57437#discussion_r3682057237


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/subquery.scala:
##########
@@ -127,23 +127,36 @@ case class InSubqueryExec(
 
   override def nullable: Boolean = child.nullable
   override def toString: String = s"$child IN ${plan.name}"
-  override def withNewPlan(plan: BaseSubqueryExec): InSubqueryExec = copy(plan 
= plan)
+  override def withNewPlan(plan: BaseSubqueryExec): InSubqueryExec =
+    copy(plan = plan, resultBroadcast = null, result = null)

Review Comment:
   **Finding 2.** Clearing `result` is what this PR needs; clearing 
`resultBroadcast` reaches a path the PR isn't targeting.
   
   `resultBroadcast` is only ever assigned for non-DPP subqueries — 
`updateResult` guards it with `!isDynamicPruning && !isResultUnavailable` — so 
the unavailable marker can only ever live in `result`, and for DPP 
`resultBroadcast` is always null anyway. The one case where the extra reset 
does something is an ordinary `IN` subquery: `ReuseAdaptiveSubquery` calls 
`withNewPlan(ReusedSubqueryExec(...))`, and if that lands after 
`updateResult()` has run, the new instance now drops a broadcast that base kept 
and has to re-run `updateResult()` and re-broadcast. It recovers (a fresh 
instance re-`prepare`s), so nothing breaks — but it's a silent behavior change 
to a shared method, with no test and nothing in the description.
   
   ```suggestion
       copy(plan = plan, result = null)
   ```
   
   If clearing the broadcast is deliberate — say you want a plan swap to always 
recompute — a one-line comment saying so would stop the next reader from 
"simplifying" it back.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/DynamicPruning.scala:
##########
@@ -80,7 +86,20 @@ case class DynamicPruningSubquery(
       // DynamicPruningSubquery should only have a single broadcasting key 
since
       // there are no usage for multiple broadcasting keys at the moment.
       broadcastKeyIndices.size == 1 &&
-      child.dataType == buildKeys(broadcastKeyIndices.head).dataType
+      child.dataType == buildKeys(broadcastKeyIndices.head).dataType &&
+      broadcastValueProjection.forall { projection =>

Review Comment:
   **Finding 3.** This is the one place in the PR where a bad projection fails 
the query rather than being discarded.
   
   Every clause here is already guaranteed by 
`ReusableBroadcastValueProjection.find`:
   
   - `sourcePlan.deterministic`, `sourceHashKeys.nonEmpty`, and the hash-key 
determinism come from the `candidate.filter` block plus `isSafeSourceHashKey`;
   - `sourceHashKeys` / `valueExpression` 
`references.subsetOf(sourcePlan.outputSet)` come from `ExtractEquiJoinKeys`' 
`canEvaluate` and from `find`'s own `value.references.subsetOf(left.outputSet)`;
   - `valueExpression.deterministic` comes from `isSafeValueExpression`;
   - `valueExpression.dataType == pruningKey.dataType` follows because the 
value starts as `filteringKeys.head`, `PartitionPruning` sets `indices = 
Seq(joinKeys.indexOf(filteringKeys.head))`, and `replaceAlias` is 
type-preserving — so it is the same expression the line above already 
type-checks against `pruningKey`.
   
   So these clauses can only fire for a projection that did not come from 
`find`. When they do, the effect is `resolved == false` on a node inside an 
already-optimized plan: the containing `Filter` reports unresolved, 
`LogicalPlanIntegrity.validateOptimizedPlan` flags it under plan-change 
validation, and outside validation an unresolved node just keeps flowing. That 
inverts the contract the description states — "If Spark cannot verify the 
broadcast, cannot safely evaluate the expression ... it discards the entire 
projected domain and continues without this optimization." Your own test pins 
the fail-closed shape:
   
   ```scala
   assert(!pruning.copy()(Some(projection.copy(sourceHashKeys = 
Seq(missing)))).resolved)
   ```
   
   Keeping the checks but not the hard failure, either:
   
   ```scala
   // drop rather than un-resolve; both planner rules read this instead of the 
raw field
   private[sql] lazy val usableBroadcastValueProjection: 
Option[BroadcastValueProjection] =
     broadcastValueProjection.filter(isSafeProjection)
   ```
   
   or move them to a `require` in `BroadcastValueProjection`'s producer, so a 
malformed projection is a bug at the producer instead of a plan-validation 
failure later. Either way `DynamicPruningSubquery.resolved` stays about the DPP 
itself, and an unusable projection behaves like every other failure in this 
feature.
   



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