Skip to content

fix: nullability mismatch errors in aggregation when input batches are stricter than the plan schema - #24237

Closed
alamb wants to merge 3 commits into
apache:mainfrom
alamb:fix-24069-spill-emit-nullability
Closed

fix: nullability mismatch errors in aggregation when input batches are stricter than the plan schema#24237
alamb wants to merge 3 commits into
apache:mainfrom
alamb:fix-24069-spill-emit-nullability

Conversation

@alamb

@alamb alamb commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

As reported in #24069 (found via DataFusion Comet, apache/datafusion-comet#5239), collect_list / collect_set (array_agg / array_agg(DISTINCT)) over a struct column fails during aggregation with

ArrowError: column types must match schema types,
  expected List(Struct("colA": Boolean, ...))
  but found  List(Struct("colA": non-null Boolean, ...)) at column index 1

The trigger is input batches whose data types are stricter than the plan schema declares — most commonly a nested struct field that is non-nullable even though the schema declares it nullable. This is explicitly allowed by the Arrow schema containment contract — Field::contains (“if nullability doesn't match, self needs to be nullable”) and Schema::contains (“any record that conforms to other should also conform to self”), which is also the check RecordBatch::with_schema uses to accept a stricter batch under a wider schema — and happens in practice when batches come from FFI / engines like Comet, whose runtime arrays can be stricter than the declared plan schema.

What changes are included in this PR?

Are these changes tested?

Yes

Are there any user-facing changes?

Queries that previously failed (or panicked) with the above error now work. No API changes.

… stricter than the plan schema

DataFusion allows input batches to carry data types that are stricter
than the plan schema declares (see `Field::contains`), most commonly a
nested struct field that is non-nullable even though the schema declares
it nullable. Aggregate output and state columns are derived from the
actual input data, so that stricter type propagated into the emitted
columns and failed the exact type equality check in
`RecordBatch::try_new` against the statically computed output/spill
schema, e.g. for `array_agg` / `array_agg(DISTINCT)` over such structs
on the spill path.

* Add `new_batch_conforming_schema`, which casts columns whose type is
  contained by (but not equal to) the declared field type -- a
  metadata-only change -- and use it wherever aggregation assembles
  output or spill batches.
* `ArrayAggGroupsAccumulator` now builds its `ListArray` item field from
  the actual values' data type instead of the planned type, which
  panicked in `ListArray::new` for the same reason.

Closes apache#24069

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added functions Changes to functions implementation physical-plan Changes to the physical-plan crate labels Aug 10, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.37255% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.99%. Comparing base (d443bab) to head (83ebe75).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/aggregates/mod.rs 85.71% 4 Missing and 9 partials ⚠️
...plan/src/aggregates/aggregate_hash_table/common.rs 0.00% 0 Missing and 2 partials ⚠️
.../aggregates/aggregate_hash_table/common_ordered.rs 0.00% 0 Missing and 2 partials ⚠️
...hysical-plan/src/aggregates/grouped_hash_stream.rs 0.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24237      +/-   ##
==========================================
- Coverage   80.99%   80.99%   -0.01%     
==========================================
  Files        1106     1106              
  Lines      383344   383488     +144     
  Branches   383344   383488     +144     
==========================================
+ Hits       310482   310596     +114     
- Misses      54543    54556      +13     
- Partials    18319    18336      +17     

☔ 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.

alamb and others added 2 commits August 10, 2026 19:14
… fix

This reverts commit 83ebe75f757a4b9430bc9e35de8b71bcbe825fa7. Review
feedback on apache#24237: tolerating stricter column types at each aggregate
batch-assembly site treats the symptom. An end-to-end reproducer (next
commit) shows the same schema divergence also breaks `GroupValues`'
row converter and `interleave` inside accumulators, so the fix needs to
address the root cause instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eclared schema

DataFusion accepts batches whose data types are stricter than the
declared schema (`MemTable::try_new` validates with `Schema::contains`,
which permits a non-nullable field where the schema declares a nullable
one, including nested fields). Aggregating a struct column from such
batches fails in several independent places; see apache#24069.

The new tests register a `MemTable` whose declared schema marks a
nested struct field nullable while the batches carry a non-nullable
one, and run `array_agg` / `array_agg(DISTINCT)` over it via SQL, with
and without a memory limit that forces spilling. They currently fail
(this commit is the reproducer only):

* `array_agg`, with and without spilling: panic in `ListArray::new`
  (`ArrayAggGroupsAccumulator::evaluate` builds the list item field
  from the planner-declared type, the values from the actual data)
* `array_agg(DISTINCT)` without spilling: `RowConverter column schema
  mismatch` (the `single_distinct_to_groupby` rewrite turns the struct
  into a GROUP BY key; `GroupValues`' row converter is built from the
  declared schema but fed the stricter runtime arrays)
* `array_agg(DISTINCT)` with spilling passes on current main, showing
  how configuration-dependent the failures are

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added core Core DataFusion crate and removed functions Changes to functions implementation physical-plan Changes to the physical-plan crate labels Aug 10, 2026
@alamb

alamb commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

This is slop code, we should do the right thing starting with the end to end test

@alamb alamb closed this Aug 11, 2026
@alamb
alamb deleted the fix-24069-spill-emit-nullability branch August 11, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GroupedHashAggregateStream::emit throws ArrowError: column types must match schema types

2 participants