Repository navigation
fix: do not duplicate volatile expressions when extracting leaf expressions - #24720
Conversation
…ssions
`extract_leaf_expressions` / `push_down_leaf_projections` build an extraction
projection by *merging* into the input projection, which inlines the definition
of every column the extracted expression references. When that definition is
volatile, the inlined copy is an independent evaluation:
SELECT s, s['a'] AS field
FROM (SELECT named_struct('a', random()) AS s FROM generate_series(1, 3));
planned to
Projection: named_struct(Utf8("a"), random()) AS s, random() AS field
so `field` disagreed with `s['a']` on every row. Extracting out of a `Filter`
is worse: `WHERE s['a'] > 0.5` tested a second draw, and rows whose `s['a']`
was <= 0.5 were returned.
Skip the extraction when a referenced column's definition is volatile. This is
the same invariant `FileScanConfig::try_swapping_with_projection` already
enforces for the physical projection-pushdown path (apache#23220).
kosiew
left a comment
There was a problem hiding this comment.
@jaideeppyne, thanks for working on this. The volatility guard looks well scoped, and the projection and filter regression coverage captures the original issue nicely. I have one non-blocking suggestion for covering the routed join-input path as well.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24720 +/- ##
==========================================
+ Coverage 81.45% 81.60% +0.14%
==========================================
Files 1120 1123 +3
Lines 401289 408997 +7708
Branches 401289 408997 +7708
==========================================
+ Hits 326871 333758 +6887
- Misses 55295 55627 +332
- Partials 19123 19612 +489 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I added the non-blocking join-side regression in e07285a and resolved the thread. The new sqllogictest asserts the extracted field still equals the same join side’s materialized |
|
🚀 |
…ssions (apache#24720) (cherry picked from commit 27e81e9) Backport to DF54: preserve the existing schema-recovery logic and add the BTreeSet import required on this base.
) ## Which issue does this PR close? - Part of apache#25459. It does not fix a bug. It adds a test helper for the class of bug where the optimizer evaluates a volatile call more times than the query says. ## Rationale for this change Tests for these bugs used `random()` and inferred the number of evaluations from the result: they compared two reads of the same value, ran 10000–100000 rows and checked that no row broke a filter, or wrapped arguments in `case when random() >= 0 then x end`. These tests are indirect, and a reader cannot see what they check. A function that counts its own evaluations makes them exact. An earlier version of this PR added a check that walked the optimized plan. Run on every query of the sqllogictest suite with the bugs reverted, it caught none of them, and it could not see duplication in the physical plan. This version replaces it. ## What changes are included in this PR? Three functions, registered in every sqllogictest context (`datafusion/sqllogictest/src/test_context/evaluation_counter.rs`): - `counted_volatile(x)` returns `x` and is volatile, like `random()` - `counted_immutable(x)` returns `x` and is immutable - `evaluations()` returns the number of rows that the two functions above were evaluated for since its last call, and resets the count ``` query I SELECT count(*) FROM (SELECT counted_volatile(a) AS r FROM t) WHERE r > 5; ---- 5 query I SELECT evaluations(); ---- 10 ``` The count is per row, not per call, so it does not depend on batch size, and it is correct for `CASE` and `COALESCE`, which evaluate a branch on part of a batch. Commits, one per step: 1. Add the functions. 2. Port the tests for apache#25415 (`push_down_filter_regression.slt`). 3. Port the tests for apache#23220 and apache#24678 (`projection_pushdown.slt`). 4. Port the tests for apache#25477 (`coalesce.slt`). 5. Port the tests that two volatile calls are not merged (`functions.slt`, `aggregates_simplify.slt`). 6. Add `volatile_evaluations.slt`: filter pushdown, joins, unions, `HAVING`, and two known double evaluations: apache#26910 (preimage, a bug) and apache#25457 (`BETWEEN`, accepted behaviour). Not ported: `cse.slt` (its `EXPLAIN` tests check which subexpressions are hoisted, not only how many evaluations), `cte_materialized.slt` (its results are already exact), and the files that use `random()` only as an expression that cannot be pushed down. ## Are these changes tested? Each ported test was checked against its fix reverted: | Fix | Count with the fix reverted | |---|---| | apache#25416 (volatile group key) | 13 and 17, expected 10 | | apache#24720 (leaf extraction) | 20, expected 10 | | apache#23395 (file scan projection, physical plan) | 6, expected 3 | | apache#25561 (`COALESCE`) | 30, expected 20 | ## Are there any user-facing changes? No. The functions exist only in the sqllogictest runner. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jay Zhan <jayzhan211@gmail.com>
Which issue does this PR close?
Rationale for this change
A column alias denotes one value per row, and
WHERE pmay only return rows for whichpheld for that row. Today the leaf-expression extraction passes break both:s['a']fieldfieldis defined ass['a']but differs from it on every row, because the plan isThe
Filterform is worse — it returns rows that fail their own predicate:Setting
datafusion.optimizer.enable_leaf_expression_pushdown = falsereturns the correct answer in both cases, so the rewrite alone changes the meaning of the query.Root cause.
build_extraction_projection_implmerges an extraction into the input projection by resolving column references throughbuild_projection_replace_map, i.e. by inlining each referenced column's defining expression. Inlining a volatile definition produces a second, independent evaluation. There was no volatility check in the file.This is the same invariant
FileScanConfig::try_swapping_with_projectionalready enforces for the physical projection-pushdown path viawould_duplicate_costly_exprs(#23220) — the logical extraction path was missing it.What changes are included in this PR?
volatile_output_columns()— a projection's output columns whose definition is volatile.would_duplicate_volatile()— true when an extraction references one of them.extract_from_plan(pass 1: Filter/Sort/Limit/Aggregate/Join),split_and_push_projection(pass 2), andtry_push_into_inputs(multi-input/Union routing). Each already had a "leave the plan alone" return path.The guard is targeted rather than blanket: for
ORDER BY s['a']the extraction still happens, stacked above the volatile projection instead of merged into it, so the optimization is kept and the result is correct.Relationship to #23691
@fornwall wondered on the issue whether #23691 already covers this. I checked out that branch and ran both shapes against it:
SELECT s, s['a']is incidentally fixed there, butWHERE s['a'] > 0.5still duplicatesrandom()and still returns rows failing the predicate. #23691 guardsKeepInPlacecompute cost insplit_and_push_projectiononly; volatility is a separate concern (one duplication is already wrong, regardless of cost or placement) and pass 1 is a different code path. The two changes look independent to me and I believe they compose, but I'd appreciate a second opinion on that from whoever reviews #23691.Are these changes tested?
Yes.
datafusion/sqllogictest/test_files/projection_pushdown.slt— a new section beside the existing Projection pushdown into file scan duplicates non-deterministic functions (regression in 52.0.0) #23220 volatile section: twoEXPLAINs pinningrandom()to a single occurrence, and two deterministicbool_and(...)correctness queries. All four fail onmainand pass here.extract_leaf_expressions.rscovering the pass-2 projection merge and the pass-1Filterextraction, using a new test-onlyPlacementTestUDF::with_volatility().cargo test -p datafusion-optimizer(796),cargo test -p datafusion --lib --tests(2049) and the full 504-file sqllogictest suite all pass;cargo fmt --allandcargo clippy --all-targets --all-features -- -D warningsare clean.Are there any user-facing changes?
Queries that were silently returning wrong results now return correct ones. No API change. In the affected shapes the extraction is skipped, which can cost a small amount of column pruning — only when the referenced column is defined by a volatile expression.
Per the ASF generative-tooling policy and DataFusion's AI-assisted contribution guidance: this patch was prepared with AI assistance. The core idea is the one described above — the merge path inlines a referenced column's defining expression, which duplicates a volatile definition — and the open question about how this composes with #23691 is flagged deliberately rather than glossed over.