fix: adapt input batches with stricter nested nullability to planned schema in aggregation - #24394
Conversation
|
Can you fix the CI tests? |
8b2c541 to
47de466
Compare
|
Hi @kosiew, thank you very much for reviewing and pointing that out! I have resolved the missing \RecordBatchStream\ import, addressed the qualification and integration test requirements, and fixed \AggregateExec::execute_input\ to adapt batches directly to \self.input.schema(). The entire local validation battery (\cargo fmt --all --check, \cargo test -p datafusion-common --lib nested_struct::adapt_schema_tests, \cargo test -p datafusion-physical-plan --lib aggregates, and \cargo test -p datafusion --test core_integration nested_nullability, plus \cargo clippy --all-targets -- -D warnings) is now completely green with zero errors. The branch has been squashed into 1 clean atomic commit (\47de466) and pushed. Whenever you have a chance, please approve the workflow runs on GitHub Actions. Thank you! |
|
@patrickswedish |
47de466 to
8390472
Compare
|
@kosiew Resolved the conflicts against current \main, including the overlapping aggregate metrics changes, and fixed the schema-adaptation regression exposed by \ est_no_pushdown_through_global_aggregate_with_name_collision. Re-ran the aggregate test suites and filter-pushdown integration tests successfully, and the branch is clean and mergeable again. Thanks! |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24394 +/- ##
========================================
Coverage 81.24% 81.24%
========================================
Files 1113 1113
Lines 392744 392907 +163
Branches 392744 392907 +163
========================================
+ Hits 319090 319232 +142
- Misses 54900 54912 +12
- Partials 18754 18763 +9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
kosiew
left a comment
There was a problem hiding this comment.
Thanks for the follow-up work here. The earlier items around the RecordBatchStream import, integration coverage, adapting against self.input.schema(), and the conflict resolution all look addressed.
I found one remaining issue with Union schemas that I think needs to be fixed before this lands. DataType::contains can accept a stricter incoming Union schema, but the adapter then delegates to Arrow's cast implementation, which does not support Union-to-Union casts. As a result, an input schema that should be compatible can still fail at runtime when it reaches the aggregate wrapper.
I reproduced this directly with adapt_batch_to_schema. The existing adapt_schema_tests also pass, so this looks isolated to Union adaptation rather than the broader schema adaptation work.
Once Union fields are adapted recursively, or unsupported Union shapes are rejected before reaching the cast path, I think this should be in good shape. It would also be useful to add both a direct adapter regression and an aggregate execution regression covering stricter Union child nullability.
| ); | ||
| } | ||
| needs_column_adaptation = true; | ||
| let adapted_col = cast_column(col, target_field.data_type(), &cast_options)?; |
There was a problem hiding this comment.
I think there is still an issue here for Union schemas.
DataType::contains accepts a target Union when its child field is nullable and the corresponding incoming Union child is non-nullable. That means the incoming schema is considered a valid stricter version of the target schema.
We then reach cast_column, but Arrow does not support casting a Union to another Union. Its Union cast support is for extracting a child into a non-Union target. In practice, this means a compatible stricter Union batch now fails with cannot cast Union ... to Union ... instead of being adapted to the declared input schema.
Could we handle Union arrays explicitly here by recursively adapting each child to the corresponding target field, then rebuilding the UnionArray with the target UnionFields while preserving the type IDs, dense offsets, and mode?
Another option would be to reject Union shapes that the adapter cannot actually construct, even if DataType::contains currently considers them compatible.
It would be good to add a direct adapt_batch_to_schema regression for stricter Union child nullability, plus an aggregate execution regression that exercises the same case through AggregateExec.
There was a problem hiding this comment.
Hi @kosiew,
Thank you for the guidance! We have addressed this in the latest update:
-
Narrowed Schema Conformance for Unions (cast_union_column & �alidate_union_schema_compatibility):
- Explicitly handles Union arrays (both Sparse and Dense modes) in
ested_struct::cast_column. - Requires matching union modes, exact type ID set equality, and recursive containment ( arget_child.contains(source_child)).
- Recursively adapts matching children using cast_column and reconstructs the UnionArray with arget_fields, preserving ype_ids and dense offsets buffers without copying buffer data.
- Rejects unsupported field set evolution (extra/missing type IDs).
- Removed Union from
equires_nested_struct_cast so that generic SQL CAST semantics across DataFusion are untouched.
- Explicitly handles Union arrays (both Sparse and Dense modes) in
-
Unit and Integration Regressions:
- Added unit tests verifying exact unpacked scalar values (10, "b", 30), row-level ype_ids, offsets, and target schema containment for Dense and Sparse unions.
- Added non-contiguous/reordered type-ID test ([(1, int), (3, str)] -> [(3, str), (1, int)]).
- Added negative tests verifying rejection of field-set mismatches and mode mismatches.
- Added direct AggregateExec input boundary integration tests in
ested_nullability.rs asserting unpacked �rray_agg(b) values and output schema nullability.
8390472 to
37aca08
Compare
|
Hi @kosiew, Thank you very much for catching this edge case and for the detailed review! We have implemented recursive adaptation for Union arrays and addressed all your points:
The branch has been rebased onto the latest \main, formatted with \cargo fmt, and validated with \cargo clippy --all-targets --all-features -- -D warnings\ and the full test suite. |
37aca08 to
2b005d9
Compare
alamb
left a comment
There was a problem hiding this comment.
Thank you @patrickswedish and @kosiew -- I left some comments. Let me know what you think
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod adapt_schema_tests { |
There was a problem hiding this comment.
There seems to be a lot of repetition in these tests -- perhaps some of the hints here could be applied: https://datafusion.apache.org/contributor-guide/pr_review.html#review-the-test-coverage
There was a problem hiding this comment.
Hi @alamb,
Thank you for pointing us to the PR review test coverage guide!
We refactored the test suite in \datafusion/common/src/nested_struct.rs\ and \datafusion/core/tests/sql/aggregates/nested_nullability.rs:
- Extracted shared test field builders (\ est_two_field_union, etc.) to eliminate repeated boilerplate across test cases.
- Streamlined unit tests to focus on distinct semantic cases: Sparse adaptation, Dense adaptation, non-contiguous/reordered type-ID mappings, nested Structs, mode mismatches, and field-set mismatches.
- Retained exact row-level unpacked value assertions (\10, \b\, \30), active type IDs, and dense offsets without test repetition.
- Removed oversized duplicate tests from
ested_nullability.rs\ to keep integration tests concise and focused on the end-to-end bug report reproducer.
| dynamic_filter: Option<Arc<AggrDynFilter>>, | ||
| } | ||
|
|
||
| /// A stream wrapper that ensures every yielded batch matches the declared input schema. |
There was a problem hiding this comment.
I feel like this is a solution to a symptom (nullability mismatch) rather than the underlying problem (an operator is not declaring its output schema correctly and producing record batches with the wrong shape)
There was a problem hiding this comment.
Hi @alamb,
Thank you for this key architectural insight! You are completely right: fixing this at the \AggregateExec\ boundary was treating a symptom rather than addressing the producer contract.
Root Cause Analysis
In DataFusion, in-memory table sources like \MemTable::try_new\ accept batches whose schemas are stricter than the declared table schema using \Schema::contains(&batches_schema)\ (e.g. nullable nested fields in the declared table schema vs non-nullable nested fields in the input batches).
When \MemTable::scan\ creates \MemorySourceConfig\ / \MemoryExec, \MemoryStream\ was constructed with the declared schema, but its \poll_next\ emitted the underlying stricter \RecordBatch\�s without adapting them. Downstream operators (like \AggregateExec) received batches that did not conform to the stream's advertised output schema.
Architectural Solution
- Reverted AggregateExec Changes: Completely removed \AdaptedInputRecordBatchStream\ and \AggregateExec::execute_input, restoring all aggregate physical plans to clean upstream state.
- Fixed Producer Invariant in \MemoryStream\ (\physical-plan/src/memory.rs):
- \MemoryStream::poll_next\ now normalizes emitted batches using \�dapt_batch_to_schema(batch, &self.schema)\ whenever a batch differs from \self.schema\ and \self.schema.contains(batch.schema()).
- Every \RecordBatch\ emitted by \MemoryStream\ is guaranteed to conform to \stream.schema().
- Retained Narrow Schema Conformance in
ested_struct\ (\common/src/nested_struct.rs):- \�dapt_batch_to_schema\ supports Structs, Lists, and Unions (Dense/Sparse) without changing general DataFusion CAST behavior (
equires_nested_struct_cast\ remains unchanged).
- \�dapt_batch_to_schema\ supports Structs, Lists, and Unions (Dense/Sparse) without changing general DataFusion CAST behavior (
- Unit and Integration Regressions:
- Added unit tests in \physical-plan/src/memory.rs\ directly verifying that \MemoryStream\ emits batches matching \self.schema\ (with and without projection).
- Retained end-to-end SQL aggregation regressions in
ested_nullability.rs\ covering standard, distinct, and spilling aggregations.
8855c2e to
ef07bd2
Compare
…pache#24069) Ensures batches emitted by MemoryStream conform to its advertised schema by adapting batches when runtime nested data types have stricter nullability than the table's declared schema (e.g. accepted by MemTable via Schema::contains). Also extends datafusion_common::nested_struct::adapt_batch_to_schema with narrow schema-conformance support for Arrow UnionArray (sparse and dense) without changing general SQL CAST behavior. Fixes apache#24069 Closes apache#24394
Update SummaryThank you @alamb and @kosiew for the thorough review and guidance! Following the feedback, we have restructured the fix around the producer boundary rather than wrapping individual downstream consumer operators:
|
…pache#24069) Ensures batches emitted by MemoryStream conform to its advertised schema by adapting batches when runtime nested data types have stricter nullability than the table's declared schema (e.g. accepted by MemTable via Schema::contains). Also extends datafusion_common::nested_struct::adapt_batch_to_schema with narrow schema-conformance support for Arrow UnionArray (sparse and dense) without changing general SQL CAST behavior. Fixes apache#24069 Closes apache#24394
ef07bd2 to
da6f3c5
Compare
Which issue does this PR close?
Closes #24069.
Rationale for this change
In DataFusion, in-memory table sources such as
MemTable::try_newacceptRecordBatches whose schemas are stricter than the table's declared schema viaSchema::contains(&batches_schema)(e.g. nullable nested fields declared on the table vs non-nullable nested fields in the input batches).However,
MemoryStreampreviously advertised the declared table schema while emitting the underlying stricterRecordBatches without adapting them. When downstream operators (such asAggregateExecwitharray_aggor distinct aggregation) received batches with stricter nested schemas, runtime type mismatch errors occurred (e.g. #24069).What changes are included in this PR?
datafusion_common::nested_struct::adapt_batch_to_schema:RecordBatches whose nested schemas are stricter than a target schema.requires_nested_struct_castremains untouched).MemoryStreamProducer Boundary Normalization (datafusion-physical-plan/src/memory.rs):GroupedHashAggregateStream::emitthrows ArrowError: column types must match schema types #24069.MemTable::try_new,MemoryStream::poll_nextnormalizes emitted batches usingadapt_batch_to_schema(batch, &self.schema)whenever the runtime batch schema differs fromself.schemaandself.schema.contains(batch.schema()).RecordBatches emitted byMemoryStreamconform exactly tostream.schema().Regression Coverage:
MemoryStreamregressions inmemory.rsverifying emitted batches match the advertised schema, including projection handling.nested_struct.rscovering nested Struct and Dense/Sparse Union adaptation, unpacked scalar values, type IDs, offsets, reordered IDs, and incompatible Union layouts.nested_nullability.rscovering standard, DISTINCT, and spilling aggregations forGroupedHashAggregateStream::emitthrows ArrowError: column types must match schema types #24069.Are these changes tested?
Yes:
datafusion-commonunit tests foradapt_batch_to_schemaand Union adaptation (test_adapt_batch_to_schema_*).datafusion-physical-planunit tests forMemoryStreamemitted batch schema conformance and projection (test_memory_stream_emitted_batch_matches_declared_schema*).datafusioncore SQL integration tests indatafusion/core/tests/sql/aggregates/nested_nullability.rs.Are there any user-facing changes?
No. Queries aggregating in-memory tables whose batches have stricter nested nullability than the table schema now succeed as expected.