Skip to content

fix: resolve pass-through columns against the input when merging an extraction projection - #25412

Merged
adriangb merged 3 commits into
apache:mainfrom
adriangb:fix/leaf-projection-merge-qualifier-upstream
Oct 14, 2026
Merged

adriangb merged 3 commits into
apache:mainfrom
adriangb:fix/leaf-projection-merge-qualifier-upstream

Conversation

@adriangb

@adriangb adriangb commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

None filed.

Rationale for this change

This query fails to plan on main:

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

#25388 (draft) edits the same arm of
build_extraction_projection_impl, for a different problem: duplicated
evaluation of KeepInPlace expressions, #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.

#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: #25459

@github-actions github-actions Bot added optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) labels Sep 17, 2026
@adriangb
adriangb requested a balanced review from Copilot September 17, 2026 14:29
@adriangb
adriangb marked this pull request as ready for review September 17, 2026 14:29
@adriangb

Copy link
Copy Markdown
Contributor Author

cc @pepijnve since you're working on these same files

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Same-name pass-through aliases remain excluded from deduplication and can recreate the ambiguous schema.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes projection merging to resolve pass-through columns against the input schema.

Changes:

  • Normalizes column qualifiers before deduplication.
  • Adds an SQL regression test for the planning failure.
File summaries
File Description
extract_leaf_expressions.rs Resolves merged pass-through columns against input schemas.
struct.slt Tests the previously failing union query.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datafusion/optimizer/src/extract_leaf_expressions.rs Outdated
@codecov-commenter

codecov-commenter commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.09677% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.84%. Comparing base (0566b55) to head (7e44ea3).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...tafusion/optimizer/src/extract_leaf_expressions.rs 87.09% 4 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25412      +/-   ##
==========================================
- Coverage   82.84%   82.84%   -0.01%     
==========================================
  Files        1149     1149              
  Lines      453357   453424      +67     
  Branches   453357   453424      +67     
==========================================
+ Hits       375597   375650      +53     
- Misses      54916    54921       +5     
- Partials    22844    22853       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adriangb adriangb left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Summary

The fix is correct and small. I verified all claims in the description. I have 3 non-blocking suggestions (inline).

Root cause

flowchart TD
    A["Filter / Aggregate above<br/>needs: <code>env</code> (bare) or <code>samples.env</code>"] --> M{{"build_extraction_projection_impl<br/>merge into existing Projection"}}
    P["Existing Projection: <code>v, s, env</code> (bare)<br/>input: <code>samples.v, samples.s, samples.env</code>"] --> M
    M -->|main: compare raw Column values| X["<code>samples.env</code> != <code>env</code><br/>→ push again<br/>→ schema has <code>samples.env</code> AND <code>env</code><br/>❌ ambiguous"]
    M -->|PR: resolve both sides against input| O["both → <code>samples.env</code><br/>→ match, no push<br/>✅ plans"]
Loading

Verification

Check Result
cargo test -p datafusion-optimizer --lib extract_leaf_expressions on PR head (61923e2) ✅ 57 passed
Same, with only the fix reverted (tests kept) ❌ 2 failed: both new tests
sqllogictests -- struct projection_pushdown on PR head ✅ pass
Simplified resolve_against (suggestion 1) ✅ 57 passed

Behavior change table

Needed column Input schema main This PR
bare c t.c pushes bare c pushes t.c (input spelling)
t.c t.c, existing has bare c pushes t.c → ambiguous matched, no push
t.c t.c, existing has t.c AS c pushes t.c → ambiguous matched, no push
any column not in input (for example __common_expr_N) skipped skipped (unchanged)

Suggestions

# Where Kind Blocking
1 resolve_against Simplify to one lookup; fix doc claim about duplicate names No
2 merge comment Make shorter No
3 struct.slt Add EXPLAIN to lock the plan shape No

The doc on resolve_against says a duplicate name "resolves to nothing". That is not always true. qualified_field_with_unqualified_name returns the one unqualified field when the name is also held by qualified fields (see dfschema.rs). The behavior is fine; only the doc is wrong. Suggestion 1 fixes both.

Comment thread datafusion/optimizer/src/extract_leaf_expressions.rs Outdated
Comment thread datafusion/optimizer/src/extract_leaf_expressions.rs Outdated
Comment thread datafusion/sqllogictest/test_files/struct.slt
@adriangb

Copy link
Copy Markdown
Contributor Author

@pepijnve if you are able to review this it would be great, we are hitting this in prod so pretty keen to fix it. I also think it's a good starting place to tackle the EPIC we have now.

}
}

/// The way `schema` names `col`, or `None` when `schema` does not hold it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: this sentence took me a couple of tries to grok. The function has an active name (an action), but the first sentence is describing a noun (a property). Might be better to write this in a 'Does computation xyz' style.

// by alias expressions (e.g., CSE's __common_expr_N) exist in the output but
// not the input, and cannot be added as pass-through Column references.
//
// Compare both sides in the input's spelling (see `resolve_against`).

@pepijnve pepijnve Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What are the 'sides' being referred to here? In the final sentence there's a reference to an empty union side. Makes this hard to interpret.

zhuqi-lucas pushed a commit to zhuqi-lucas/arrow-datafusion that referenced this pull request Oct 1, 2026
…puted same-name column (apache#25445)

## Which issue does this PR close?

- Closes apache#25414.

## Rationale for this change

A subquery that computes a column under the same name as its input
column gives wrong results. This happens with the default configuration.

```sql
create table t(a int, s struct<b varchar>) as values (1, {b: 'x'}), (2, {b: 'y'});

select a, s['b'] from (select -a as a, s from t where a > 0);
```

| Query | DataFusion on `main` | DuckDB and PostgreSQL |
|---|---|---|
| `select a, s['b'] from (select -a as a, s from t where a > 0)` | `1 x`
/ `2 y` | `-1 x` / `-2 y` |
| `select a, s['b'] from (select -a as a, s from t limit 10)` | `1 x` /
`2 y` | `-1 x` / `-2 y` |
| `select a, s['b'] from (select a * 10 as a, s from t limit 10)` |
`Internal error` | `10 x` / `20 y` |
| `select a, s['b'] from (select -a as a, s from t) where a < 0` | `1 x`
/ `2 y` | `-1 x` / `-2 y` |
| `select a, count(s['b']) from (select -a as a, s from t where a > 0)
group by a` | groups `1` and `2` | groups `-1` and `-2` |

The fourth query returns rows that fail its own filter. The third query
stops the plan with this message:

```
Internal error: Assertion failed: compatible: Failed due to a difference in schemas:
original schema: ... "a", data_type: Int64 ...
new schema: ... "a", data_type: Int32 ...
```

The results are correct with `set
datafusion.optimizer.enable_leaf_expression_pushdown = false;`.

The cause is in `split_and_push_projection` in
`datafusion/optimizer/src/extract_leaf_expressions.rs`. The rule pushes
the extraction of `s['b']` below the projection that computes `-a AS a`.
It then decides if it must keep a recovery projection, and it decides
from the set of unqualified field names of the pushed plan.

A name says nothing about the value behind it. For this projection:

```text
Projection: (- t.a) AS a, t.s, get_field(t.s, "b") AS __datafusion_extracted_1
  Filter: t.a > Int32(0)
    TableScan: t
```

the pushed plan holds the names `a`, `s` and `__datafusion_extracted_1`
again, but its `a` is the table column `t.a`, not `- t.a`. The two sets
are equal, the rule drops the recovery projection, and the computed
column becomes its own input column:

```text
Projection: a, __datafusion_extracted_1 AS t.s[b]
  Filter: t.a > Int32(0)
    Projection: get_field(t.s, "b") AS __datafusion_extracted_1, t.a, t.s
      TableScan: t
```

The same comparison ignores data types. That is why `a * 10 AS a` fails
the optimizer schema check instead of giving wrong data.

## What changes are included in this PR?

The recovery projection now also stays when a recovery expression
computes a value, that is, when it is not a pass-through of a column.
`passthrough_column` gives that answer, and it already accepts a
requalification such as `t.a AS a`.

The name comparison stays as it is. It catches the leaked-column case
that it was written for, which the expression check does not see. The
comparison still ignores qualifiers, so the `SubqueryAlias`
requalification behaviour does not change.

The change is inside `split_and_push_projection` only.
apache#25412 touches
`build_extraction_projection_impl` in the same file. The two changes are
independent and fix different bugs. The only overlap is that both PRs
append a block to the end of
`datafusion/sqllogictest/test_files/struct.slt`, so the second one to
merge needs a trivial rebase there.

## What is the testing strategy for this PR?

Six SQL shapes go in `datafusion/sqllogictest/test_files/struct.slt`:
through a `Filter`, through a `Limit`, a computed column with a
different data type, an outer filter on the computed column, a group
key, and through a `Sort`. One `EXPLAIN` shows that the projection that
computes `-a` stays in the plan.

A counterfactual run confirms each one. With the fix reverted, the new
block gives 6 errors: the `EXPLAIN`, four wrong results and the internal
error. The `Sort` shape was already correct and guards it.

A unit test, `test_recovery_kept_for_same_name_computed_column` in
`extract_leaf_expressions.rs`, builds the plan shape that loses the
computed column and runs the two leaf rules alone, in their production
order.

All 883 `datafusion-optimizer` tests pass. No existing insta snapshot
changes. All 520 sqllogictest files pass.

## Are there any user-facing changes?

Queries of this shape now give correct results. There are no API
changes.

Part of the leaf-pushdown EPIC:
apache#25459

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
cetra3 added a commit to pydantic/datafusion that referenced this pull request Oct 1, 2026
…xtraction projection (apache#25412)

A projection can list bare column names over a qualified input, which is
what removing the empty side of a `UNION ALL` leaves. When
`build_extraction_projection_impl` merged an extraction projection into
it, a bare `c` and a qualified `t.c` did not match, so the pass-through
column was added a second time and planning failed with "Schema contains
qualified field name ... and unqualified field name ... which would be
ambiguous".

Carried from apache#25412 (open).
@adriangb
adriangb force-pushed the fix/leaf-projection-merge-qualifier-upstream branch 2 times, most recently from b1e3494 to 1e57bdf Compare October 3, 2026 20:20
adriangb added a commit to adriangb/datafusion that referenced this pull request Oct 3, 2026
…xtraction projection (apache#25412)

Squash of apache#25412 for cherry-picking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zhuqi-lucas pushed a commit to zhuqi-lucas/arrow-datafusion that referenced this pull request Oct 6, 2026
…er pushdown (apache#25453)

## Which issue does this PR close?

<!-- No issue: this PR adds test coverage only. -->

- Related to apache#25414,
apache#25412 and
apache#25268.

## Rationale for this change

`ExtractLeafExpressions` and `PushDownLeafProjections` (the option
`datafusion.optimizer.enable_leaf_expression_pushdown`, default `true`)
move expressions such as `s['b']` out of a filter, sort, limit,
aggregate or join, and push them towards the scan. The rules resolve
columns by name. When a plan holds two columns with the same name, or a
column that is computed from a column of the same name, the rules can
drop a projection or merge the wrong pair. The user sees wrong rows or a
planning error, with the default configuration.

The rules had 10 bug fixes since February 2026. Users or an ad-hoc
fuzzer found all of them. The 520 sqllogictest files found none, because
they do not build these shapes.

A differential test finds this class cheaply: run the same query with
the option on and off, and compare the rows. This is a fixed-seed
differential regression suite rather than a fuzzer that accumulates
coverage; the seeds are the same on every run so a failure is
reproducible with one command. Bugs of the class this test covers:

- apache#25414: a computed column
with the same name as its input is dropped. Wrong results.
- apache#25412: pass-through columns
merged in the wrong name space. Planning error.
- apache#24678: a volatile
expression duplicated by leaf extraction. Wrong results.
- apache#25415: a filter on a
volatile group key pushed below the aggregate. Wrong results.
- apache#22895:
`optimize_projections` fails with "No field named ..." when the join
keys hold a `get_field`. Planning error.
- apache#24241:
`push_down_leaf_projections` fails for a self join. Planning error.

The harness reproduces the two open bugs today. With
`INCLUDE_SAME_NAME_ALIAS_SHAPES = true`, 526 of 3000 seeds fail (476
planning errors, 50 silent wrong results). With
`INCLUDE_FALSE_BRANCH_UNION_SHAPES = true`, 404 of 3000 seeds fail.

## What changes are included in this PR?

One new file, `datafusion/core/tests/fuzz_cases/leaf_pushdown_fuzz.rs`.

The generator builds SQL over three wide tables and one narrow table.
Every table has `Int32` columns, a `Utf8` column and two `Struct`
columns. Two struct fields have the same name as a top level column. The
generated shapes are:

- `s['f']` in `SELECT`, `WHERE`, `ORDER BY`, `GROUP BY`, `HAVING`, `JOIN
ON` and under `LIMIT`.
- Derived tables, CTEs, `IN` and `EXISTS` subqueries, `UNION ALL`.
- `named_struct(...)` built in a subquery and read above.
- `arrow_field(a)` used bare and through `['f']` in the same query.
- `random()` in a subquery projection, read two times above. The two
runs cannot be compared with each other, so the query reports its own
consistency in one boolean column.

Each query runs in two `SessionContext`s built from the same batches.
The rows are sorted and compared. An error on one side only is a
failure. An error on both sides is a skip, and the test fails if more
than 30% of the seeds skip.

Three shapes fail on `main` today. Each one is behind a named `const`
switch with the link to its issue, so this PR is green:

- `INCLUDE_SAME_NAME_ALIAS_SHAPES` for
apache#25414.
- `INCLUDE_FALSE_BRANCH_UNION_SHAPES` for
apache#25412.

Flip the switch to `true` when the fix lands.

## What is the testing strategy for this PR?

This PR is a test. Three test entry points:

| test | cases | wall clock |
| --- | --- | --- |
| `leaf_pushdown_fuzz` | 250 | 3.9s |
| `leaf_pushdown_parquet_fuzz` | 125 | included above |
| `leaf_pushdown_parquet_schema_evolution` | 50 | included above |
| `leaf_pushdown_fuzz_extended` | 5000 + 1000 | 87s |

```console
cargo test --profile ci -p datafusion --features extended_tests --test fuzz -- leaf_pushdown
```

The whole `fuzz_cases` tree is already behind the `extended_tests`
feature, so the short tests run in the extended job together with the
long one.

Every case comes from a seed. A failure prints the seed, the SQL and
both result sets, and names the command that replays it:

```console
LEAF_PUSHDOWN_FUZZ_SEED=<seed> LEAF_PUSHDOWN_FUZZ_CASES=1 \
  cargo test --profile ci -p datafusion --features extended_tests \
  --test fuzz -- leaf_pushdown --nocapture
```

## Are there any user-facing changes?

No.

Update: the swap shape (`b AS a, a AS b`) fails for a different reason
than the same-name computed column. It is
apache#25446, which is open. It now
has its own switch, `INCLUDE_SWAP_ALIAS_SHAPES`, so the fix for
apache#25414 can be verified with
`INCLUDE_SAME_NAME_ALIAS_SHAPES` alone. On a branch with
apache#25445 and
apache#25412 merged and both of those
switches on, the only remaining failures are 24 of 250 seeds, all the
swap shape with the ambiguity error.


Known gaps from independent review, to be addressed in this PR: the
generic generator only emits pass-through spellings (`c`, `q.c`, `q.c AS
c`, `c AS c`), so renames and computed columns come only from the
hardcoded same-name arm; `all_true` passes on an empty result; and the
whole `fuzz_cases` tree runs only in the merge-queue `extended_tests`
job, so the short run should move out from behind that gate.

Part of the leaf-pushdown EPIC:
apache#25459

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Omega359 pushed a commit to Omega359/arrow-datafusion that referenced this pull request Oct 11, 2026
…puted same-name column (apache#25445)

## Which issue does this PR close?

- Closes apache#25414.

## Rationale for this change

A subquery that computes a column under the same name as its input
column gives wrong results. This happens with the default configuration.

```sql
create table t(a int, s struct<b varchar>) as values (1, {b: 'x'}), (2, {b: 'y'});

select a, s['b'] from (select -a as a, s from t where a > 0);
```

| Query | DataFusion on `main` | DuckDB and PostgreSQL |
|---|---|---|
| `select a, s['b'] from (select -a as a, s from t where a > 0)` | `1 x`
/ `2 y` | `-1 x` / `-2 y` |
| `select a, s['b'] from (select -a as a, s from t limit 10)` | `1 x` /
`2 y` | `-1 x` / `-2 y` |
| `select a, s['b'] from (select a * 10 as a, s from t limit 10)` |
`Internal error` | `10 x` / `20 y` |
| `select a, s['b'] from (select -a as a, s from t) where a < 0` | `1 x`
/ `2 y` | `-1 x` / `-2 y` |
| `select a, count(s['b']) from (select -a as a, s from t where a > 0)
group by a` | groups `1` and `2` | groups `-1` and `-2` |

The fourth query returns rows that fail its own filter. The third query
stops the plan with this message:

```
Internal error: Assertion failed: compatible: Failed due to a difference in schemas:
original schema: ... "a", data_type: Int64 ...
new schema: ... "a", data_type: Int32 ...
```

The results are correct with `set
datafusion.optimizer.enable_leaf_expression_pushdown = false;`.

The cause is in `split_and_push_projection` in
`datafusion/optimizer/src/extract_leaf_expressions.rs`. The rule pushes
the extraction of `s['b']` below the projection that computes `-a AS a`.
It then decides if it must keep a recovery projection, and it decides
from the set of unqualified field names of the pushed plan.

A name says nothing about the value behind it. For this projection:

```text
Projection: (- t.a) AS a, t.s, get_field(t.s, "b") AS __datafusion_extracted_1
  Filter: t.a > Int32(0)
    TableScan: t
```

the pushed plan holds the names `a`, `s` and `__datafusion_extracted_1`
again, but its `a` is the table column `t.a`, not `- t.a`. The two sets
are equal, the rule drops the recovery projection, and the computed
column becomes its own input column:

```text
Projection: a, __datafusion_extracted_1 AS t.s[b]
  Filter: t.a > Int32(0)
    Projection: get_field(t.s, "b") AS __datafusion_extracted_1, t.a, t.s
      TableScan: t
```

The same comparison ignores data types. That is why `a * 10 AS a` fails
the optimizer schema check instead of giving wrong data.

## What changes are included in this PR?

The recovery projection now also stays when a recovery expression
computes a value, that is, when it is not a pass-through of a column.
`passthrough_column` gives that answer, and it already accepts a
requalification such as `t.a AS a`.

The name comparison stays as it is. It catches the leaked-column case
that it was written for, which the expression check does not see. The
comparison still ignores qualifiers, so the `SubqueryAlias`
requalification behaviour does not change.

The change is inside `split_and_push_projection` only.
apache#25412 touches
`build_extraction_projection_impl` in the same file. The two changes are
independent and fix different bugs. The only overlap is that both PRs
append a block to the end of
`datafusion/sqllogictest/test_files/struct.slt`, so the second one to
merge needs a trivial rebase there.

## What is the testing strategy for this PR?

Six SQL shapes go in `datafusion/sqllogictest/test_files/struct.slt`:
through a `Filter`, through a `Limit`, a computed column with a
different data type, an outer filter on the computed column, a group
key, and through a `Sort`. One `EXPLAIN` shows that the projection that
computes `-a` stays in the plan.

A counterfactual run confirms each one. With the fix reverted, the new
block gives 6 errors: the `EXPLAIN`, four wrong results and the internal
error. The `Sort` shape was already correct and guards it.

A unit test, `test_recovery_kept_for_same_name_computed_column` in
`extract_leaf_expressions.rs`, builds the plan shape that loses the
computed column and runs the two leaf rules alone, in their production
order.

All 883 `datafusion-optimizer` tests pass. No existing insta snapshot
changes. All 520 sqllogictest files pass.

## Are there any user-facing changes?

Queries of this shape now give correct results. There are no API
changes.

Part of the leaf-pushdown EPIC:
apache#25459

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Omega359 pushed a commit to Omega359/arrow-datafusion that referenced this pull request Oct 11, 2026
…er pushdown (apache#25453)

## Which issue does this PR close?

<!-- No issue: this PR adds test coverage only. -->

- Related to apache#25414,
apache#25412 and
apache#25268.

## Rationale for this change

`ExtractLeafExpressions` and `PushDownLeafProjections` (the option
`datafusion.optimizer.enable_leaf_expression_pushdown`, default `true`)
move expressions such as `s['b']` out of a filter, sort, limit,
aggregate or join, and push them towards the scan. The rules resolve
columns by name. When a plan holds two columns with the same name, or a
column that is computed from a column of the same name, the rules can
drop a projection or merge the wrong pair. The user sees wrong rows or a
planning error, with the default configuration.

The rules had 10 bug fixes since February 2026. Users or an ad-hoc
fuzzer found all of them. The 520 sqllogictest files found none, because
they do not build these shapes.

A differential test finds this class cheaply: run the same query with
the option on and off, and compare the rows. This is a fixed-seed
differential regression suite rather than a fuzzer that accumulates
coverage; the seeds are the same on every run so a failure is
reproducible with one command. Bugs of the class this test covers:

- apache#25414: a computed column
with the same name as its input is dropped. Wrong results.
- apache#25412: pass-through columns
merged in the wrong name space. Planning error.
- apache#24678: a volatile
expression duplicated by leaf extraction. Wrong results.
- apache#25415: a filter on a
volatile group key pushed below the aggregate. Wrong results.
- apache#22895:
`optimize_projections` fails with "No field named ..." when the join
keys hold a `get_field`. Planning error.
- apache#24241:
`push_down_leaf_projections` fails for a self join. Planning error.

The harness reproduces the two open bugs today. With
`INCLUDE_SAME_NAME_ALIAS_SHAPES = true`, 526 of 3000 seeds fail (476
planning errors, 50 silent wrong results). With
`INCLUDE_FALSE_BRANCH_UNION_SHAPES = true`, 404 of 3000 seeds fail.

## What changes are included in this PR?

One new file, `datafusion/core/tests/fuzz_cases/leaf_pushdown_fuzz.rs`.

The generator builds SQL over three wide tables and one narrow table.
Every table has `Int32` columns, a `Utf8` column and two `Struct`
columns. Two struct fields have the same name as a top level column. The
generated shapes are:

- `s['f']` in `SELECT`, `WHERE`, `ORDER BY`, `GROUP BY`, `HAVING`, `JOIN
ON` and under `LIMIT`.
- Derived tables, CTEs, `IN` and `EXISTS` subqueries, `UNION ALL`.
- `named_struct(...)` built in a subquery and read above.
- `arrow_field(a)` used bare and through `['f']` in the same query.
- `random()` in a subquery projection, read two times above. The two
runs cannot be compared with each other, so the query reports its own
consistency in one boolean column.

Each query runs in two `SessionContext`s built from the same batches.
The rows are sorted and compared. An error on one side only is a
failure. An error on both sides is a skip, and the test fails if more
than 30% of the seeds skip.

Three shapes fail on `main` today. Each one is behind a named `const`
switch with the link to its issue, so this PR is green:

- `INCLUDE_SAME_NAME_ALIAS_SHAPES` for
apache#25414.
- `INCLUDE_FALSE_BRANCH_UNION_SHAPES` for
apache#25412.

Flip the switch to `true` when the fix lands.

## What is the testing strategy for this PR?

This PR is a test. Three test entry points:

| test | cases | wall clock |
| --- | --- | --- |
| `leaf_pushdown_fuzz` | 250 | 3.9s |
| `leaf_pushdown_parquet_fuzz` | 125 | included above |
| `leaf_pushdown_parquet_schema_evolution` | 50 | included above |
| `leaf_pushdown_fuzz_extended` | 5000 + 1000 | 87s |

```console
cargo test --profile ci -p datafusion --features extended_tests --test fuzz -- leaf_pushdown
```

The whole `fuzz_cases` tree is already behind the `extended_tests`
feature, so the short tests run in the extended job together with the
long one.

Every case comes from a seed. A failure prints the seed, the SQL and
both result sets, and names the command that replays it:

```console
LEAF_PUSHDOWN_FUZZ_SEED=<seed> LEAF_PUSHDOWN_FUZZ_CASES=1 \
  cargo test --profile ci -p datafusion --features extended_tests \
  --test fuzz -- leaf_pushdown --nocapture
```

## Are there any user-facing changes?

No.

Update: the swap shape (`b AS a, a AS b`) fails for a different reason
than the same-name computed column. It is
apache#25446, which is open. It now
has its own switch, `INCLUDE_SWAP_ALIAS_SHAPES`, so the fix for
apache#25414 can be verified with
`INCLUDE_SAME_NAME_ALIAS_SHAPES` alone. On a branch with
apache#25445 and
apache#25412 merged and both of those
switches on, the only remaining failures are 24 of 250 seeds, all the
swap shape with the ambiguity error.


Known gaps from independent review, to be addressed in this PR: the
generic generator only emits pass-through spellings (`c`, `q.c`, `q.c AS
c`, `c AS c`), so renames and computed columns come only from the
hardcoded same-name arm; `all_true` passes on an empty result; and the
whole `fuzz_cases` tree runs only in the merge-queue `extended_tests`
job, so the short run should move out from behind that gate.

Part of the leaf-pushdown EPIC:
apache#25459

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
adriangb pushed a commit to pydantic/datafusion that referenced this pull request Oct 13, 2026
…xtraction projection (apache#25412)

A projection can list bare column names over a qualified input, which is
what removing the empty side of a `UNION ALL` leaves. When
`build_extraction_projection_impl` merged an extraction projection into
it, a bare `c` and a qualified `t.c` did not match, so the pass-through
column was added a second time and planning failed with "Schema contains
qualified field name ... and unqualified field name ... which would be
ambiguous".

Carried from apache#25412 (open).

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@adriangb,

Thanks for the fix. The schema-based column resolution addresses the qualified/unqualified ambiguity, and the same-name alias handling covers the earlier concern. The focused optimizer and SQL regression tests passed. I have one optional test coverage suggestion below, which is not a blocker.

&& input_schema.has_column(resolved_col)
&& let Some(input_col) = resolve_against(input_schema, resolved_col)
&& !existing_cols.contains(&input_col)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we add a test where the existing projection retains only test.a and columns_needed requests bare b? It would verify that the newly appended column is test.b, with qualifier test and name b, rather than unqualified b. The current tests cover deduplication but not this specific case. This is optional and could be done in a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added in 68a2780

…xtraction projection (apache#25412)

Squash of apache#25412 for cherry-picking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriangb
adriangb force-pushed the fix/leaf-projection-merge-qualifier-upstream branch from 37cb051 to 0c0a388 Compare October 14, 2026 05:30
adriangb and others added 2 commits October 14, 2026 00:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriangb

Copy link
Copy Markdown
Contributor Author

Thanks @kosiew

@adriangb
adriangb added this pull request to the merge queue Oct 14, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 14, 2026
@adriangb
adriangb added this pull request to the merge queue Oct 14, 2026
Merged via the queue into apache:main with commit 47135be Oct 14, 2026
42 of 43 checks passed
@adriangb
adriangb deleted the fix/leaf-projection-merge-qualifier-upstream branch October 14, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants