[SPARK-58310][SQL] Avoid runtime Bloom-filter subqueries containing Python UDFs#57485
[SPARK-58310][SQL] Avoid runtime Bloom-filter subqueries containing Python UDFs#57485sunchao wants to merge 2 commits into
Conversation
viirya
left a comment
There was a problem hiding this comment.
Focused, fail-safe fix, and the P2 refinement in the second commit is the right call — checking the pruned aggregate (post ColumnPruning/ConstantFolding) rather than the raw creation plan means a Python UDF that only feeds an output column, and is pruned out of the actual Bloom subquery, still gets its filter injected. The test pins both sides of that: the crash is gone, and getNumBloomFilters > 0 confirms the safe application-side and creation-side-output-only-UDF cases are still filtered. Coverage spans scalar Python and Arrow/Pandas, with a baseline comparison and a real Parquet round trip.
The guard is consistent with the existing isSimpleExpression treatment of PYTHON_UDF (which only guards the key/predicate expressions) — it closes the complementary gap where the key is simple but the pruned creation-side plan still carries a Python UDF. PYTHON_UDF is the right and sufficient pattern here: only Python UDFs need a separate eval operator that ExtractPythonUDFs inserts; Scala UDFs are directly codegen-able.
I looked at whether this could be fixed at the source (running extraction on the new subquery) instead of skipping the filter, and I don't think it can within a reasonable scope, for a concrete reason worth recording: ExtractPythonUDFs.apply uses plan.transformUpWithPruning, and per QueryPlan's contract the TreeNode transforms "do not traverse into query plans inside subqueries." So the ScalarSubquery(aggregate) that InjectRuntimeFilter builds is invisible to ExtractPythonUDFs no matter where it runs — reordering the batches wouldn't help (InjectRuntimeFilter already precedes the Extract Python UDFs batch), and re-running extraction wouldn't either (it's uncorrelated, so it never gets rewritten into a join and picked up later, unlike the SPARK-26293 case the rule comments describe). Making extraction descend into subqueries would mean changing a core rule's traversal, which is well out of scope for this fix.
One non-blocking thought: the alternative that would preserve the filter is to run ExtractPythonUDFs locally on the aggregate before wrapping it in the subquery, and only bail if a UDF survives. But that injects a BatchEvalPython/ArrowEvalPython on the Bloom creation side, which undercuts the premise that the creation side is cheap — so skipping the filter may well be the better trade anyway. Worth a sentence in the code comment (why the check sits after pruning, and that preserving the filter would require in-subquery extraction) so the next reader doesn't re-derive it; and if you think the source-side fix has value, a follow-up JIRA could capture it. Fine to leave as-is either way.
LGTM.
Why are the changes needed?
With adaptive execution and runtime Bloom filters enabled, a nested broadcast join can cause
InjectRuntimeFilterto copy a creation-side plan containing a Python UDF into a newly constructed Bloom-filter scalar subquery. Runtime filters are introduced after subquery optimization, so the copied Python UDF never passes through Python-UDF extraction. The physical plan subsequently attempts to generate JVM code for the unevaluable Python expression while executingBloomFilterAggregate, causing an otherwise valid query to fail throughSubqueryExecandObjectHashAggregateExec.The problem affects ordinary Apache Spark and does not depend on an external table provider. A self-contained PySpark setup is:
The following query reproduces the failure; substituting
normalize_runtime_bloom_pandas_urlreproduces the Arrow/Pandas variant:What changes were proposed in this PR?
Before constructing a runtime Bloom filter, return the unchanged application-side plan when the extracted creation-side logical plan contains the
PYTHON_UDFtree pattern. This guard is deliberately limited to unsafe Bloom-filter creation. It does not disable runtime filtering, does not reject a Python UDF on the application side, and does not change Python-UDF extraction or adaptive execution.Add
SPARK-58310coverage toInjectRuntimeFilterSuitethat:Does this PR introduce any user-facing change?
Yes. Queries that previously failed because a runtime Bloom-filter subquery attempted to code-generate a Python UDF now complete successfully. Safe runtime Bloom-filter optimizations remain enabled.
How was this PR tested?
First, the new
SPARK-58310regression was run against unmodified Apache Sparkmasterand failed with the expected Python-UDF scalar-subquery/ObjectHashAggregateExecerror. The same regression then passed after the creation-side guard was applied, with bothpandasandpyarrowinstalled.The following focused upstream regression suites and Scala-style checks were also run on the public Apache Spark branch:
How was this patch tested?
The upstream before/after reproduction, Python and Arrow regression, adjacent SQL suites, and Scala-style checks are described above.
Was this patch authored or co-authored using generative AI tooling?
Yes. Generated-by: OpenAI Codex (GPT-5).