Skip to content

Avoid duplicating keep-in-place expressions - #25388

Draft
pepijnve wants to merge 6 commits into
apache:mainfrom
pepijnve:issue_25329
Draft

pepijnve wants to merge 6 commits into
apache:mainfrom
pepijnve:issue_25329

Conversation

@pepijnve

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

What changes are included in this PR?

  • Don't allow movement of keep-in-place expressions in filter pushdown
  • Avoid duplication of keep-in-place expressions in during leaf expression extraction

What is the testing strategy for this PR?

To be completed

Are there any user-facing changes?

No

@github-actions github-actions Bot added the optimizer Optimizer rules label Sep 16, 2026
@pepijnve

Copy link
Copy Markdown
Contributor Author

@adriangb if you have a moment, I could use your input on this one. I got myself into an infinite in push_extraction_pairs when the double projection with the deferred extractions is created. This had me wondering if I put the 'don't duplicate' guard in the wrong place in this optimiser. Since you seem to be the original author of this code, you might have some ideas on how to achieve the same effect in a more appropriate way.

@pepijnve

pepijnve commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

There are still some test failures which is why this is still marked draft.

For this particular case, I'm wondering if the expected output is actually an improvement made by the optimiser or not. The arithmetic isn't super expensive, but we are evaluating it twice for all rows and we're not really saving any work in the scan.

────────────┬───────────────────────────────────────────────────────────────────
    1       │-Projection: b * Int32(3) AS a, test.c
    2       │-  Projection: test.a * Int32(2) + test.c AS b, test.c
    3       │-    TableScan: test, full_filters=[(test.a * Int32(2) + test.c) * Int32(3) = Int64(1)]
          1 │+Filter: a = Int64(1)
          2 │+  Projection: b * Int32(3) AS a, test.c
          3 │+    Projection: test.a * Int32(2) + test.c AS b, test.c
          4 │+      TableScan: test
────────────┴───────────────────────────────────────────────────────────────────

The number of expressions being marked as 'keep in place' is probably a bit too broad at the moment though. I'm concerned that this change will prevent too many filter pushdown opportunities as can be seen in this failure.

────────────┬───────────────────────────────────────────────────────────────────
    1     1 │ OpaqueRequirementsExtension
    2     2 │   Sort: t.a ASC NULLS FIRST, t.ts ASC NULLS FIRST
    3       │-    Projection: t.a, CAST(t.ts AS Timestamp(ms, "UTC")) AS ts
    4       │-      Filter: __common_expr_3 > TimestampMillisecond(1000, Some("UTC")) AND __common_expr_3 < TimestampMillisecond(2000, Some("UTC"))
    5       │-        Projection: CAST(t.ts AS Timestamp(ms, "UTC")) AS __common_expr_3, t.a, t.ts
    6       │-          TableScan: t projection=[a, ts], partial_filters=[CAST(t.ts AS Timestamp(ms, "UTC")) > TimestampMillisecond(1000, Some("UTC")), CAST(t.ts AS Timestamp(ms, "UTC")) < TimestampMillisecond(2000, Some("UTC"))]
          3 │+    Filter: t.ts > TimestampMillisecond(1000, Some("UTC")) AND t.ts < TimestampMillisecond(2000, Some("UTC"))
          4 │+      Projection: t.a, CAST(t.ts AS Timestamp(ms, "UTC")) AS ts
          5 │+        TableScan: t projection=[a, ts]
────────────┴───────────────────────────────────────────────────────────────────

@github-actions github-actions Bot added the logical-expr Logical plan and expressions label Sep 17, 2026
comphead pushed a commit to comphead/arrow-datafusion that referenced this pull request Oct 14, 2026
…xtraction projection (apache#25412)

## Which issue does this PR close?

None filed.

## Rationale for this change

This query fails to plan on `main`:

```sql
create table t(v int, s struct<a int>, env varchar) as values (1, {a: 10}, 'prod'), (2, {a: 20}, 'dev');

with samples as (
  select v, s, env from t
),
expanded as (
  select v, s, env from samples
  union all
  select v, s, env from samples where 1 = 2
)
select env, sum(s['a']) from expanded group by env;
```

```
Optimizer rule 'push_down_leaf_projections' failed
caused by
Schema error: Schema contains qualified field name samples.env and unqualified
field name env which would be ambiguous
```

`enable_leaf_expression_pushdown` is on by default, so this is a plain
planning
failure for a valid statement. A `union all` whose one side is provably
empty is
what produces the shape, and query generators emit that: a dashboard
panel adds a
second branch behind a comparison of two constants and the panel stops
working.

## What changes are included in this PR?

`build_extraction_projection_impl` merges an extraction projection into
the
projection below it, and then adds the pass-through columns the merged
projection
does not already carry. It compared the columns it was about to add
against the
projection's own expressions without putting the two in the same space.

A projection can list bare column names over a qualified input. Removing
the empty
side of the union leaves exactly that. The comparison then misses, the
column is
added a second time under its bare name, and `Projection::try_new`
rejects a schema
that holds `samples.env` and a bare `env` together.

This PR resolves both sides against the input schema before the
comparison, and
pushes the column under the name the input gives it. A needed column
that is not in
the input, or that is ambiguous there, is skipped. Before this change
such a column
was added and `Projection::try_new` failed at the merge.
A same-name alias such as `t.c AS c` counts as a pass-through on the
existing side too, the same as a bare `t.c`.

## Are these changes tested?

Yes. The statement above is added to `struct.slt`, with a result check
and an
`EXPLAIN` plan pin. It fails on `main` with the error above and passes
with this change.

Three unit tests in `extract_leaf_expressions.rs` cover the merge
directly. All three fail on `main`:
- `test_merge_bare_column_into_qualified_projection`: a filter names `a`
over a projection that outputs `test.a`.
- `test_merge_into_projection_with_same_name_alias`: the projection
spells the pass-through as `test.a AS a`.
- `test_merge_appends_qualified_column_for_bare_request`: the projection
keeps only `test.a` and the merge needs a bare `b`. The appended column
must be `test.b`. On `main` it is a bare `b`.

`cargo test --workspace --exclude datafusion-sqllogictest` passes: 126
suites,
12479 tests, 0 failures. The sqllogictest suite passes too: 520 files, 0
failures. `cargo clippy -p datafusion-optimizer --all-targets` and
`cargo fmt --all -- --check` are clean.

## Are there any user-facing changes?

A statement of this shape plans instead of failing. No API change.

## Note on overlapping work

apache#25388 (draft) edits the same
arm of
`build_extraction_projection_impl`, for a different problem: duplicated
evaluation of `KeepInPlace` expressions,
apache#25329.
It does not touch the qualifier comparison this PR changes, so the two
are
independent in behaviour, but whichever lands second will need a small
rebase at
the tail of the `columns_needed` loop.

apache#25282 also appends to the end
of
`struct.slt`, as this PR does. Keeping both blocks is the whole
resolution.

Part of the leaf-pushdown EPIC:
apache#25459

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
adriangb added a commit to pydantic/datafusion that referenced this pull request Oct 29, 2026
The EXPLAIN test spells `file_row_index()` two times in the query, so it
does not check that a rule never copies a volatile definition. Add a query
that defines it one time in a subquery and references it from a filter and
the outer projection. The plans must show one call, and the results must
agree with that one value.

The nested-projection test shape in this PR comes from
apache#25388.

Co-authored-by: Pepijn Van Eeckhoudt <pepijn@vaneeckhoudt.net>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

logical-expr Logical plan and expressions optimizer Optimizer rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CommonSubexprEliminate + leaf-expression pushdown duplicate a UDF call wrapped in get_field

1 participant