Skip to content

fix: don't duplicate volatile expressions when pushing projection into file scan#23395

Open
fordN wants to merge 1 commit into
apache:mainfrom
fordN:fix/projection-pushdown-volatile-duplication
Open

fix: don't duplicate volatile expressions when pushing projection into file scan#23395
fordN wants to merge 1 commit into
apache:mainfrom
fordN:fix/projection-pushdown-volatile-duplication

Conversation

@fordN

@fordN fordN commented Jul 8, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

The reported bug showed that when volatile (non-deterministic) expressions are referenced multiple times in an outer function the query results can be incorrect.

To produce correct results, a volatile expression (e.g. random(), uuid()) aliased once in a subquery and referenced multiple times must be evaluated once and reused. Since 52.0.0 this evaluation pattern for volatile functions has been broken; the physical projection-pushdown rule merges the outer projection into the file DataSourceExec, inlining the aliased volatile expression at each reference site. Instead of being evaluated once, the volatile expression is evaluated N times, so the references diverge:

SELECT s.r AS x, s.r AS y FROM (SELECT random() AS r FROM t) AS s;  -- x != y on >= 52.0.0

This was correct in 51.0.0 and regressed in 52.0.0/53.0.0. It reproduces on file scans (Parquet/CSV) but not in-memory tables, and was surfaced downstream in Ibis. Worth noting, #10337 appears to report the same class of bug but a different cause (I'll try to look into that issue soon as well).

What changes are included in this PR?

FileScanConfig::try_swapping_with_projection now declines to merge a projection into the file source when the merge would inline a volatile expression that the incoming projection references. The check reuses the existing is_volatile() utility from datafusion_physical_expr_common — the same volatility gate the physical ProjectionPushdown and FilterPushdown rules already apply — so file-source projection merging is now consistent with them. Deterministic expressions still merge freely.

The guard blocks when a volatile inner expression is referenced at least once (not only more than once), because a single outer expression can itself duplicate the value (e.g.r + r).

Are these changes tested?

Yes:

  • Unit tests in file_scan_config.rs:
    • volatile referenced ≥1× → blocked;
    • deterministic computed and column-only → allowed;
    • unreferenced volatile → allowed;
    • single-expression self-reference (r + r) → blocked;
    • volatile nested in arithmetic → blocked;
    • empty → allowed.
  • A regression test in projection_pushdown.slt asserting x = y for the aliased-random() pattern over a Parquet scan.
  • Full datafusion-sqllogictest suite passes.

Are there any user-facing changes?

No API changes! Query results for the affected pattern are corrected (a volatile expression aliased in a subquery is no longer duplicated by projection pushdown into a file scan). Physical plans for such queries now retain a ProjectionExec above the scan rather than inlining the volatile expression into the DataSourceExec projection.

…o file scan

A volatile expression (e.g. `random()`, `uuid()`) aliased once in a subquery
and referenced multiple times must be evaluated once and reused. The physical
projection-pushdown rule merged the outer projection into the file
`DataSourceExec`, inlining the aliased volatile expression at every reference
site and turning one call into N independent calls, so the references diverged.
This regressed in 52.0.0 and reproduces on file scans (parquet/csv) but not
in-memory tables.

Guard `FileScanConfig::try_swapping_with_projection`: decline the merge when a
volatile expression in the scan's existing projection is referenced by the
incoming projection, reusing the existing `is_volatile()` utility -- the same
gate the physical `ProjectionPushdown` and `FilterPushdown` rules already
apply. Deterministic expressions still merge freely.

Closes apache#23220.
@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) datasource Changes to the datasource crate labels Jul 8, 2026
@fordN

fordN commented Jul 8, 2026

Copy link
Copy Markdown
Author

First-time contributor here! So I'll need a committer to approve the CI workflow.
cc. @alamb since you were involved in related discussion on #12599

Comment on lines +676 to +689
let mut referenced = vec![false; inner_exprs.len()];
for proj_expr in outer_exprs {
for col in collect_columns(&proj_expr.expr) {
let idx = col.index();
if idx < referenced.len() {
referenced[idx] = true;
}
}
}

referenced
.iter()
.enumerate()
.any(|(idx, &referenced)| referenced && is_volatile(&inner_exprs[idx].expr))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is safe but imprecise. A better implementation would count references with expr.apply(...), like try_collapse_projection_chain already does and block only when a volatile inner expression is referenced more than once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

datasource Changes to the datasource crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Projection pushdown into file scan duplicates non-deterministic functions (regression in 52.0.0)

2 participants