jzhan-2026 opened a new pull request, #59814:
URL: https://github.com/apache/spark/pull/59814

   ### What changes were proposed in this pull request?
   
   With `spark.sql.crossJoin.enabled=false`, `ConvertToCatalyst` now keeps a 
transpiled Python UDF interpreted when it is the only condition tying a join's 
two sides together. `CheckCartesianProducts` then fails the query exactly as it 
does with transpilation off.
   
   The new `keepCartesianGuard` step runs on each operator before conversion:
   
   - It looks at a `Join` condition, and at a `Filter` above a join 
(`join(...).where(udf(...))`, or a comma join in SQL). Predicate pushdown later 
moves that Filter into the join.
   - It only looks at the join types `CheckCartesianProducts` checks: `Inner`, 
`LeftOuter`, `RightOuter`, `FullOuter`. An explicit `Cross` join is left alone.
   - A join counts as unkeyed when no conjunct spans both sides, apart from 
conjuncts that hold a cross-side Python UDF. Interpreted, those are pulled out 
of the join by `ExtractPythonUDFFromJoinCondition`, so they are no key either 
way.
   - For an unkeyed join, only the cross-side call goes back to Python. A 
nested call that reads one side is still lowered.
   
   When something else keys the join (e.g. `a == b AND udf(a, b)`), or cross 
joins are allowed, the call is lowered as before.
   
   ### Why are the changes needed?
   
   Repro (from the JIRA, with Arrow UDFs off so the UDF is batched):
   
   ```python
   spark.conf.set("spark.sql.crossJoin.enabled", "false")
   same = lambda a, b: a == b
   eq = udf(same, BooleanType())
   left.join(right, eq("a", "b")).collect()
   ```
   
   - Transpilation off: `AnalysisException: Detected implicit cartesian 
product`.
   - Transpilation on (before this PR): no error. The plan is a 
`CartesianProduct`.
   
   `CheckCartesianProducts` only asks whether some conjunct references both 
sides. Interpreted, the UDF is pulled out of the join into a Filter, so the 
join has no condition and the check fires. Lowered, the UDF becomes a native 
`CASE WHEN ...` that references both sides, so the check passes. But no join 
key can be extracted from it, so the planner still picks a `CartesianProduct`.
   
   A user who disabled cross joins to catch accidental quadratic joins silently 
lost that protection when they enabled transpilation.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes, only for the experimental 
`spark.sql.experimental.optimizer.transpilePyUDFs`, which is unreleased. With 
cross joins disabled, a Python UDF that is the only cross-side join condition 
fails with the cartesian product error again, matching transpilation off. 
Nothing changes when `spark.sql.crossJoin.enabled=true` (the default).
   
   ### How was this patch tested?
   
   - New tests in `ConvertToCatalystSuite`:
     - The UDF stays Python for all four checked join types, and for a Filter 
above a join.
     - It is still lowered when the join has another key, when cross joins are 
allowed, for an explicit `Cross` join, and for a call that reads one side.
     - A one-side call nested in a cross-side call is still lowered.
   - New `test_udf_transpile_keeps_the_cross_join_guard` in 
`test_udf_transpile_unit.py`, end to end:
     - The join-condition and `where` forms both raise the cartesian product 
error.
     - A keyed join is still fully lowered, with no `EvalPython`.
   - `build/sbt 'catalyst/testOnly *ConvertToCatalystSuite'`: 42 passed.
   - `python/run-tests --testnames "pyspark.sql.tests.test_udf_transpile_unit 
UDFTranspileUnitTests.test_udf_transpile_keeps_the_cross_join_guard"`: passed.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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