Skip to content

fix: infer UNION output types before analysis - #27139

Merged
alamb merged 2 commits into
apache:mainfrom
kumarUjjawal:kumarUjjawal/datafusion-27023-fix
Nov 6, 2026
Merged

alamb merged 2 commits into
apache:mainfrom
kumarUjjawal:kumarUjjawal/datafusion-27023-fix

Conversation

@kumarUjjawal

@kumarUjjawal kumarUjjawal commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

DataFrame::schema() previously reported the first UNION input's type while execution used the common type. UNION construction now infers the common output type.

What changes are included in this PR?

  • Infer UNION and CASE result types before analysis, with cast insertion deferred to the analyzer.
  • Derive final UNION types from actual inputs with the shared coerce_union_type helper.
  • A branch-56 backport remains a maintainer decision because public inference behavior and one executed Arrow type change.

Synthetic nested CASE planning (ci-bench, SQL through physical planning):

Depth Main (µs) Patch (µs) Extra (µs)
8 101.2 105.5 +4.3
32 270.1 317.6 +47.5
64 502.8 684.0 +181.2

These three points suggest roughly quadratic extra work. Extrapolating that trend to depth 1,000 gives about 45 ms of extra planning. The benchmark did not measure depth 1,000.

What is the testing strategy for this PR?

Focused core and expression tests (316), 64 doctests, five SLT files, all-features Clippy, Rustdoc, and formatting pass.

Are there any user-facing changes?

In this query, x changes from Dictionary(Int32, Utf8) to Utf8; decoded values remain unchanged. Rustdoc describes UNION and CASE schema inference.

SELECT 1 AS y
UNION ALL BY NAME
SELECT arrow_cast('a', 'Dictionary(Int32, Utf8)') AS x;

@kumarUjjawal kumarUjjawal added api change Changes the API exposed to users of the crate bug Something isn't working labels Nov 5, 2026
@kumarUjjawal
kumarUjjawal requested a review from alamb November 5, 2026 17:14
@sdf-jkl

sdf-jkl commented Nov 5, 2026

Copy link
Copy Markdown
Member

I was just about to post a fix too 😭

@sdf-jkl

sdf-jkl commented Nov 5, 2026

Copy link
Copy Markdown
Member

Should've taken the issue yesterday

@alamb alamb 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.

This is definitely a patch, but I think it is ok for now.

It seems like to fix this for real, we should do what @findepi told us (namely build correct Union plans in the first place)

/// alone is not enough here -- it reflects the raw, pre-`Analyzer` plan (see
/// `SessionState::create_logical_plan`), which for a `UNION` still has the
/// first leg's un-coerced type.
/// This test checks the schema after the Analyzer and Optimizer run,

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.

thank you for trimming the AI verbosity

in this section pertains to features and changes that have already been merged
to the main branch and are awaiting release in this version.

### UNION and CASE infer common output types before analysis

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.

This is a bug fix. Why does it need a upgrade guide entry?

`datafusion_expr::type_coercion::other::coerce_union_type` is a new helper
that infers one UNION column type from input fields.

For `UNION BY NAME`, missing columns participate as `NULL` during type

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.

If we want to document the behavior of how schema resolution works, it should be in the code documentation I think

Comment thread datafusion/expr/src/expr_schema.rs Outdated
.transpose()?;
// If the branches have no common type, keep the old provisional
// type. The analyzer reports the error.
Ok(

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.

we could probably make this more concse / less indented with some intermediate variables

pub fn coerce_union_schema(inputs: &[Arc<LogicalPlan>]) -> Result<DFSchema> {
coerce_union_schema_with_schema(&inputs[1..], inputs[0].schema())
let Some(first) = inputs.first() else {
return plan_err!("UNION requires at least one input");

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.

this is a nice cleanup

}

#[tokio::test]
async fn union_coercion_preserves_values() -> Result<()> {

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.

I feel like most of these tests can be written as .slt tests (but better). I recommend leaving some tests here that cover the expected schema case but don't run the query -- instead run the queries as a .slt tests

@alamb

alamb commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Thank you for running with this @kumarUjjawal

sdf-jkl added a commit to sdf-jkl/datafusion that referenced this pull request Nov 5, 2026
Incorporate deferred CASE/UNION coercion errors, dictionary coercion from
actual inputs, and an empty-input guard, informed by:
apache#27139

Keep regression coverage in SQL and document the dictionary output-type
change for UNION BY NAME.
@alamb
alamb added this pull request to the merge queue Nov 6, 2026
@alamb

alamb commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Thank you @kumarUjjawal

Merged via the queue into apache:main with commit 13bd9d1 Nov 6, 2026
44 of 45 checks passed
mbutrovich pushed a commit that referenced this pull request Nov 6, 2026
…27139) (#27178)

## Which issue does this PR close?

- Backport of #27139 to
`branch-56`
- Part of #24461
- Closes #27023

Co-authored-by: Kumar Ujjawal <ujjawalpathak6@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate bug Something isn't working v57.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

REGRESSION: UNION: DataFrame::schema() reports the first input's type, but execution returns the coerced type

3 participants