dongjoon-hyun opened a new pull request, #59749:
URL: https://github.com/apache/spark/pull/59749

   ### What changes were proposed in this pull request?
   
   This PR aims to make `HigherOrderFunction.canonicalized` renumber only the 
lambda variables that are bound in the higher-order function, i.e. the 
arguments of its lambda functions, including those of nested higher-order 
functions. A captured lambda variable, which is bound by an enclosing lambda 
function, now keeps its own `exprId`.
   
   As before, when the enclosing higher-order function is canonicalized, a 
nested higher-order function skips the variables that the enclosing one has 
already renamed.
   
   ### Why are the changes needed?
   
   Since SPARK-35580, `HigherOrderFunction.canonicalized` renames every 
`NamedLambdaVariable` in its lambda functions by position, including the 
captured ones. As a result, `filter(ys, y -> y > x1)` and `filter(ys, y -> y > 
x2)` have the same canonical form when `x1` and `x2` have the same data type 
and nullability, and `semanticEquals` returns `true` for them. The optimizer 
rules that use `semanticEquals`, such as `SimplifyConditionals`, 
`BooleanSimplification`, and `SimplifyBinaryComparison`, then simplify lambda 
function bodies incorrectly.
   
   ```sql
   CREATE TABLE t USING parquet AS
     SELECT array(1, 5) AS xs1, array(3, 2) AS xs2, array(1, 2, 3, 4, 5) AS ys;
   
   SELECT zip_with(xs1, xs2, (x1, x2) ->
     IF(x1 > x2, filter(ys, y -> y > x1), filter(ys, y -> y > x2))) FROM t;
   ```
   
   | Lambda function body of `zip_with(xs1, xs2, (x1, x2) -> ...)` | Expected | 
Apache Spark 4.0.4, 4.1.3, 4.2.0, 4.3.0 |
   | --- | --- | --- |
   | `IF(x1 > x2, filter(ys, y -> y > x1), filter(ys, y -> y > x2))` | 
`[[4,5],[]]` | `[[2,3,4,5],[]]` |
   | `CASE WHEN x1 > x2 THEN filter(ys, y -> y > x1) ELSE filter(ys, y -> y > 
x2) END` | `[[4,5],[]]` | `[[4,5],[3,4,5]]` |
   | `exists(ys, y -> y > x1) OR exists(ys, y -> y > x2)` | `[true,true]` | 
`[true,false]` |
   | `filter(ys, y -> y > x1) <=> filter(ys, y -> y > x2)` | `[false,false]` | 
`[true,true]` |
   
   For example, `SimplifyConditionals` replaces the whole `IF` with its first 
branch.
   
   ```
   Project [zip_with(xs1#3, xs2#4, lambdafunction(filter(ys#5, 
lambdafunction((lambda y#8 > lambda x1#6), lambda y#8, false)), lambda x1#6, 
lambda x2#7, false)) AS ...]
   +- Relation spark_catalog.default.t[xs1#3,xs2#4,ys#5] parquet
   ```
   
   A `LocalRelation` source such as `VALUES` hides this bug because 
`ConvertToLocalRelation` evaluates the query before these rules run.
   
   In addition, `ExtractPythonUDFFromLambda`, which exists only in the 
unreleased `master` and `branch-4.x`, identifies a Python UDF call in a lambda 
function by its canonical form. So, it merges two calls that capture different 
lambda variables into one.
   
   ```python
   from pyspark.sql import functions as sf
   
   @sf.udf("int")
   def f(xs):
       return len(xs)
   
   df = spark.range(1).selectExpr(
       "array(1, 2) AS l", "array(10, 20) AS r", "array(0, 5, 15) AS c")
   df.select(sf.zip_with("l", "r", lambda a, b:
       f(sf.filter("c", lambda y: y < a)) + f(sf.filter("c", lambda y: y < 
b)))).collect()
   # [3, 4] is expected, but [2, 2] is returned.
   ```
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes. This fixes the wrong query results that have existed since Apache Spark 
3.2.0 ([v3.2.0](https://github.com/apache/spark/releases/tag/v3.2.0), 
2021-10-06). Two higher-order functions that capture different lambda variables 
are no longer semantically equal, so the optimizer does not treat them as the 
same expression anymore. This also fixes the Python UDF example above in the 
unreleased `master` and `branch-4.x`.
   
   ### How was this patch tested?
   
   Pass the CIs with the newly added test cases.
   
   - `HigherOrderFunctionsSuite`: `semanticEquals` returns `false` for 
higher-order functions that capture different lambda variables, and `true` for 
identical ones that differ only in the IDs of their own lambda variables, 
including nested higher-order functions.
   - `DataFrameFunctionsSuite`: the four queries above on a Parquet table.
   
   Without the fix, both new test cases fail. I also ran the Python example 
above manually. It returns `[2, 2]` without the fix and `[3, 4]` with the fix.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Opus 5.5


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