Repository navigation
Wrong results: with pushdown_filters = true, a filter on a volatile subquery column is evaluated again in the Parquet scan #26267
Description
Describe the bug
With datafusion.execution.parquet.pushdown_filters = true, a filter on a subquery column that comes from a volatile function returns wrong results.
The query drops rows that match the filter and returns rows that do not match the filter.
The physical plan has no FilterExec. The Parquet scan has the predicate random() < 0.1, which calls random() again. The values that the scan tests are not the values that the projection returns.
Tested on main at 2c568b4 with datafusion-cli.
To Reproduce
COPY (SELECT value AS n FROM generate_series(1, 1000)) TO '/tmp/ev.parquet';
CREATE EXTERNAL TABLE ev STORED AS PARQUET LOCATION '/tmp/ev.parquet';
SET datafusion.explain.format = 'indent';
SET datafusion.execution.parquet.pushdown_filters = true;
-- Query 1: the predicate is true for every value of x, so the result must be 1000.
SELECT count(*) FROM (SELECT random() AS x FROM ev) WHERE x < 0.5 OR x >= 0.5;
-- Query 2: every row in the result must have x < 0.1, so max(x) must be less than 0.1.
SELECT count(*), max(x) FROM (SELECT random() AS x FROM ev) WHERE x < 0.1;
EXPLAIN SELECT count(*), max(x) FROM (SELECT random() AS x FROM ev) WHERE x < 0.1;Actual output of query 1 (the number changes on each run, it is near 750):
+----------+
| count(*) |
+----------+
| 729 |
+----------+
Actual output of query 2 (max(x) is larger than 0.1):
+----------+--------------------+
| count(*) | max(x) |
+----------+--------------------+
| 110 | 0.9983553383621974 |
+----------+--------------------+
EXPLAIN output. The logical plan is correct: the Filter stays above the Projection. The physical plan has no FilterExec, and the scan has predicate=random() < 0.1:
logical_plan
Projection: count(Int64(1)) AS count(*), max(x)
Aggregate: groupBy=[[]], aggr=[[count(Int64(1)), max(x)]]
Filter: x < Float64(0.1)
Projection: random() AS x
TableScan: ev projection=[]
physical_plan
ProjectionExec: expr=[count(Int64(1))@0 as count(*), max(x)@1 as max(x)]
AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1)), max(x)]
CoalescePartitionsExec
AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1)), max(x)]
ProjectionExec: expr=[random() as x]
RepartitionExec: partitioning=RoundRobinBatch(12), input_partitions=1
DataSourceExec: file_groups={1 group: [[tmp/ev.parquet]]}, file_type=parquet, predicate=random() < 0.1
Expected behavior
Query 1 returns 1000.
Query 2 returns a max(x) that is less than 0.1.
With datafusion.execution.parquet.pushdown_filters = false (the default) the same session gives these correct results:
+----------+
| count(*) |
+----------+
| 1000 |
+----------+
+----------+---------------------+
| count(*) | max(x) |
+----------+---------------------+
| 93 | 0.09977364854487647 |
+----------+---------------------+
A filter must not move through a projection when that changes the filter into a volatile expression.
Additional context
Other observations at the same commit:
-
uuid()shows the same bug:SELECT count(*) FROM (SELECT n, uuid() AS x FROM ev) WHERE x < '8' OR x >= '8'returned745, not1000. -
The bug also occurs when the subquery selects other columns (
SELECT n, random() AS x FROM ev) and when the volatile function is a part of a larger expression (n + random() AS xgivespredicate=CAST(n AS Float64) + random() < 100on the scan). -
With
pushdown_filters = falsethe results are correct, because theFilterExecstays in the plan. But the scan also haspredicate=random() < 0.1in that configuration:FilterExec: x@0 < 0.1 ProjectionExec: expr=[random() as x] RepartitionExec: partitioning=RoundRobinBatch(12), input_partitions=1 DataSourceExec: file_groups={1 group: [[tmp/ev.parquet]]}, file_type=parquet, predicate=random() < 0.1 -
CSV and in-memory tables give correct results. These sources do not accept the filter, so the
FilterExecstays in the plan. -
A volatile predicate written directly on the table (
SELECT count(*) FROM ev WHERE random() < 0.1) is not pushed down. TheFilterExecstays in the plan and the scan has no predicate. -
EXPLAIN VERBOSEshows that the plan changes in the physicalFilterPushdownrule. The logicalPushDownFilterrule does not move the filter.
Suspected cause, from reading the code (no fix was tested):
ProjectionExec::gather_filters_for_pushdownindatafusion/physical-plan/src/projection.rsreplaces each column of the parent filter with the projection expression (unproject_expr) and marks the result as supported. It does not check if the projection expression is volatile. Thusx@0 < 0.1becomesrandom() < 0.1.push_down_filtersindatafusion/physical-optimizer/src/filter_pushdown.rsremoves volatile predicates (allow_pushdown_for_expr) only from the list that it gives togather_filters_for_pushdown. It gives the fullparent_predicateslist, with the volatile predicates, tohandle_child_pushdown_result.DataSourceExec::handle_child_pushdown_resultindatafusion/datasource/src/source.rsgives all of these filters to the data source. The Parquet source accepts them, and then theFilterExecis removed.
This was found during work on #26239.
🤖 Generated with Claude Code
Activity
mikamikasuki commentedon Oct 13, 2026
I’m withdrawing this take and will not pursue this issue.
mikamikasuki commentedon Oct 13, 2026
untake
Tiancheng-Xu commentedon Oct 14, 2026
take