dongjoon-hyun commented on PR #58580:
URL: https://github.com/apache/spark/pull/58580#issuecomment-5579953790

   Reviewed this together with `UnwrapCastInBinaryComparison`, 
`PushDownUtils.pushRuntimeFilters`, `InSubqueryExec` and the in-memory test 
fixtures.
   
   The change looks correct to me, and the approach is the right one. It fixes 
a real bug where type coercion on the join keys silently disabled DSv2 runtime 
filter pushdown entirely. Walking through the semantics:
   
   - Wrapping the values in `InSet(col, values)` and running `unwrapCast` on it 
is exactly the code the optimizer applies to a literal `InSet`, so there is no 
room for the two to disagree.
   - `simplifyIn` keeps only values that survive a round-trip cast, so the 
dropped ones (out of range, rounded) could not have matched any row to begin 
with. The pushed filter never becomes more selective than the original, which 
is the property that matters here.
   - `falseIfNotNull(col)` is NULL when `col` is null and FALSE otherwise; as a 
filter both drop the row, so translating it to `AlwaysFalse` is right.
   - `simplifyIn` returns `falseIfNotNull` when both `nullList` and 
`canCastList` are empty, so the `InSet` branch always has a non-empty `hset`. 
An empty `IN` can't reach the source.
   - Nulls are preserved through `nullList`, matching the previous 
`values.map(LiteralValue(...))` behavior.
   - When the child is not a `Cast`, `unwrapCast` returns `None` on its first 
pattern, so the existing path is preserved.
   
   A few comments below, none of them blocking.
   
   ### 1. Matching on the shape of `falseIfNotNull` is fragile
   
   ```scala
   case And(IsNull(_), Literal(null, BooleanType)) =>
     Some(new AlwaysFalse())
   ```
   
   This couples the caller to the *internal representation* of 
`falseIfNotNull`. If `UnwrapCastInBinaryComparison` ever switches to, say, 
`If(IsNull(e), Literal(null), FalseLiteral)`, this silently falls through to 
`case _` and we go back to logging a warning and losing the pruning — compiling 
and passing tests all the way. That is a cross-module coupling that is easy to 
miss.
   
   Two options: also open up `falseIfNotNull` and compare with 
`semanticEquals`, or — probably better — expose a narrow helper instead of the 
general `unwrapCast`, since the caller only ever passes an `InSet` and only 
understands two of the many shapes `unwrapCast` can return. Something like:
   
   ```scala
   private[sql] def unwrapCastInSet(inSet: InSet): Option[...]
   ```
   
   Right now that contract exists only in the reviewer's head.
   
   ### 2. The new scaladoc doesn't state the contract the new caller depends on
   
   > Returns None if the expression is not rewritten.
   
   What the new call site actually depends on is that the result may be a 
`falseIfNotNull` shape rather than an `InSet`. Since the method is being 
widened to `private[sql]`, it would help to spell out the possible result 
shapes — that also documents the coupling in (1).
   
   ### 3. The `InSet`/`Set` is built even when there is no cast
   
   ```scala
   val inSet = InSet(in.child, values.toSet)
   ```
   
   On the common path (no cast) this pays for the `Set` conversion and, as a 
side effect, **deduplicates and reorders the values pushed to the source**. The 
cost is negligible, but the behavior change is unrelated to the purpose of this 
PR. Restricting the unwrap attempt to `in.child` being a `Cast` keeps the 
existing path byte-for-byte and makes the intent clearer.
   
   Also, `InSubqueryExec` already holds the same expression as a private `lazy 
val inSet` (subquery.scala:126) — worth deciding explicitly whether to reuse it 
or to accept the duplication.
   
   ### 4. The same gap remains in `translateScalarSubqueryFilterV2`
   
   The sibling runtime path in `pushRuntimeFilters` has the identical problem:
   
   ```scala
   case DynamicPruningExpression(e) => 
DataSourceV2Strategy.translateRuntimeFilterV2(e)
   case o => DataSourceV2Strategy.translateScalarSubqueryFilterV2(o)   // <-- 
here
   ```
   
   `cast(a as bigint) = <scalar subquery>` cannot be unwrapped by the optimizer 
either, because the other side is not a `Literal` until runtime, and 
`translateFilterV2` does not strip the cast after literalization. Same root 
cause, same symptom. Is that in scope here, or worth a follow-up JIRA?
   
   ### 5. Minor
   
   - The blank line before `case other =>` was dropped; keeping the surrounding 
style would be nice.
   - For an `InSubqueryExec` whose `result` is still null, a non-pushable child 
previously fell through to `case other` and only logged a warning; it now 
throws `SparkException.internalError`. Unreachable at execution time and 
arguably more consistent, but worth confirming it's intentional.
   - The description says the end-to-end test covers V1 filters, V2 predicates 
and Catalyst expressions. The Catalyst variant 
(`SupportsRuntimeCatalystFiltering`) doesn't go through 
`translateRuntimeFilterV2` at all and passes without this fix — still a fine 
regression test on the other two, but the claim is a bit broader than the 
coverage.
   - The commit message says `Assisted-by:` while the PR description says 
`Generated-by:`; worth aligning with the template.
   


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