Skip to content

[branch-55] fix: adapt input batches with stricter nested nullability to planned schema in aggregation (#24394) - #24699

Merged
timsaucer merged 2 commits into
apache:branch-55from
timsaucer:fix/backport-24394
Aug 28, 2026
Merged

[branch-55] fix: adapt input batches with stricter nested nullability to planned schema in aggregation (#24394)#24699
timsaucer merged 2 commits into
apache:branch-55from
timsaucer:fix/backport-24394

Conversation

@timsaucer

Copy link
Copy Markdown
Member

This is a back port of #24394 for branch-55.

The original issue is #24069

Note that the original PR was marked as an auto-detected api change, but I believe that is a false positive.

…schema in aggregation (apache#24394)

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes apache#123` indicates that this PR will close issue apache#123.
-->

Closes apache#24069.

<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

In DataFusion, in-memory table sources such as `MemTable::try_new`
accept `RecordBatch`es whose schemas are stricter than the table's
declared schema via `Schema::contains(&batches_schema)` (e.g. nullable
nested fields declared on the table vs non-nullable nested fields in the
input batches).

However, `MemoryStream` previously advertised the declared table schema
while emitting the underlying stricter `RecordBatch`es without adapting
them. When downstream operators (such as `AggregateExec` with
`array_agg` or distinct aggregation) received batches with stricter
nested schemas, runtime type mismatch errors occurred (e.g. apache#24069).

<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

1. **`datafusion_common::nested_struct::adapt_batch_to_schema`**:
- Adapts `RecordBatch`es whose nested schemas are stricter than a target
schema.
   - Recursively reconstructs compatible nested Struct/List types.
- Explicitly handles Dense and Sparse Union schema conformance while
preserving type IDs and dense offsets without copying buffer data.
   - Requires exact Union type-ID sets and matching modes.
- Does not broaden generic SQL CAST semantics
(`requires_nested_struct_cast` remains untouched).

2. **`MemoryStream` Producer Boundary Normalization
(`datafusion-physical-plan/src/memory.rs`)**:
   - Fixes the producer-side invariant exposed by apache#24069.
- When batches have stricter schemas accepted by `MemTable::try_new`,
`MemoryStream::poll_next` normalizes emitted batches using
`adapt_batch_to_schema(batch, &self.schema)` whenever the runtime batch
schema differs from `self.schema` and
`self.schema.contains(batch.schema())`.
- Ensures all `RecordBatch`es emitted by `MemoryStream` conform exactly
to `stream.schema()`.

3. **Regression Coverage**:
- Direct `MemoryStream` regressions in `memory.rs` verifying emitted
batches match the advertised schema, including projection handling.
- Unit tests in `nested_struct.rs` covering nested Struct and
Dense/Sparse Union adaptation, unpacked scalar values, type IDs,
offsets, reordered IDs, and incompatible Union layouts.
- End-to-end SQL aggregation integration tests in
`nested_nullability.rs` covering standard, DISTINCT, and spilling
aggregations for apache#24069.

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

Yes:
- `datafusion-common` unit tests for `adapt_batch_to_schema` and Union
adaptation (`test_adapt_batch_to_schema_*`).
- `datafusion-physical-plan` unit tests for `MemoryStream` emitted batch
schema conformance and projection
(`test_memory_stream_emitted_batch_matches_declared_schema*`).
- `datafusion` core SQL integration tests in
`datafusion/core/tests/sql/aggregates/nested_nullability.rs`.

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
-->

No. Queries aggregating in-memory tables whose batches have stricter
nested nullability than the table schema now succeed as expected.

(cherry picked from commit 2326917)
@github-actions github-actions Bot added core Core DataFusion crate common Related to common crate physical-plan Changes to the physical-plan crate labels Aug 26, 2026
@timsaucer

Copy link
Copy Markdown
Member Author

FYI @patrickswedish as the original author, would you mind reviewing this back port?

@codecov-commenter

codecov-commenter commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.59701% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.19%. Comparing base (045b590) to head (f23582e).
⚠️ Report is 2 commits behind head on branch-55.

Files with missing lines Patch % Lines
datafusion/common/src/nested_struct.rs 90.37% 28 Missing and 16 partials ⚠️
datafusion/physical-plan/src/memory.rs 91.07% 10 Missing and 9 partials ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           branch-55   #24699      +/-   ##
=============================================
+ Coverage      81.16%   81.19%   +0.02%     
=============================================
  Files           1110     1110              
  Lines         386911   387713     +802     
  Branches      386911   387713     +802     
=============================================
+ Hits          314050   314788     +738     
- Misses         54370    54405      +35     
- Partials       18491    18520      +29     

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

Copy link
Copy Markdown
Contributor

FYI @patrickswedish as the original author, would you mind reviewing this back port?

Thanks for asking me to review the backport. I compared it against the merged #24394 and the actual branch-55 base rather than treating it as a mechanical cherry-pick.

The Struct/Union path looks correctly carried over, and moving normalization to the MemoryStream producer boundary is still the right invariant.

One backport-specific gap stood out: #24394 originally merged on top of #23914, which had already added recursive Map schema adaptation in nested_struct. branch-55 does not have that prerequisite.

In this backport, adapt_batch_to_schema can now run for contained runtime-schema mismatches, but branch-55's cast_column still has no DataType::Map adaptation arm. So a stricter nested Map schema accepted by Schema::contains may fall through to Arrow's generic cast path rather than the recursive schema-conformance path present under the original PR.

Before approval, I think it would be worth adding one focused regression:

declared schema: Map<..., Struct>
runtime batch: Map<..., Struct>
confirm Schema::contains accepts it
exercise adapt_batch_to_schema / MemoryStream

If Arrow 56 already handles this case correctly, then no extra code is needed. If it fails, we should either backport the minimal Map prerequisite from #23914 or explicitly constrain the branch-55 adapter to shapes this branch can safely normalize.

Everything else I checked looks aligned with the original fix direction.

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

Thanks @timsaucer and @patrickswedish

maybe @kosiew the original reviewer can weigh in here too

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

@timsaucer,

Thanks for working on this backport. The overall approach looks good to me. I left a couple of non-blocking suggestions for additional regression coverage around Union and Map schema adaptation.

@@ -673,4 +684,128 @@ mod lazy_memory_tests {

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.

The new Union adaptation has good coverage at the helper level, but I think it would also be useful to exercise it through MemoryStream, where we enforce the producer-side schema invariant. Could we add a small Union case with a nullable declared child and a non-nullable runtime child, then assert that emitted_batch.schema() == stream.schema()? That would give us coverage for the Union reconstruction path at the actual integration boundary.

target_mode,
cast_options,
),
_ => Ok(cast_with_options(source_col, target_type, cast_options)?),

@kosiew kosiew Aug 27, 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.

Could we add a focused MemoryStream regression for a contained Map<..., Struct> where the runtime nested field is non-nullable and the declared nested field is nullable? My understanding is that this should already work because shapes accepted by Schema::contains retain the same Map entry/key/value structure, and Arrow's generic Map cast should recursively cast the value Struct and rebuild it with the target fields. A regression here would confirm that assumption for this branch.

If it fails, we should either backport the minimal Map prerequisite from 
https://github.com/apache/datafusion/pull/23914 or explicitly constrain 
the branch-55 adapter to shapes this branch can safely normalize.

as suggested by @patrickswedish

…tation

Adds two `MemoryStream` regression tests requested during review of the
apache#24394 backport:

- Dense Union with nullable declared children and non-nullable runtime
  children, exercising the Union reconstruction path at the producer
  boundary and asserting preserved type IDs and child values.
- `Map<Utf8, Struct<v>>` where the runtime nested field is non-nullable
  and the declared nested field is nullable. This passes on branch-55
  without a `DataType::Map` arm in `cast_column`: `Schema::contains`
  accepts the stricter shape and Arrow's generic Map cast recursively
  casts the value Struct and rebuilds it with the target fields. No
  backport of the Map prerequisite from apache#23914 is required.

Both assert `emitted_batch.schema() == stream.schema()`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timsaucer
timsaucer merged commit 795c861 into apache:branch-55 Aug 28, 2026
37 checks passed
@timsaucer
timsaucer deleted the fix/backport-24394 branch August 28, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common Related to common crate core Core DataFusion crate physical-plan Changes to the physical-plan crate

5 participants