Skip to content

Add nested struct schema evolution support for Map types - #23914

Merged
kosiew merged 19 commits into
apache:mainfrom
kosiew:map-evolution-03-20835
Aug 21, 2026
Merged

Add nested struct schema evolution support for Map types#23914
kosiew merged 19 commits into
apache:mainfrom
kosiew:map-evolution-03-20835

Conversation

@kosiew

@kosiew kosiew commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #20835
This is the last of a series of PR to close #20835

Rationale for this change

Map schema adaptation did not have explicit recursive evolution support when Map keys or values contain nested Structs.

This PR defines and implements Map evolution semantics so nested Struct fields can evolve consistently with existing Struct schema adaptation while preserving Map key identity. Both Map keys and values may evolve when they recursively contain Structs. Struct fields are matched by case-sensitive name, nullable target fields may be added as NULL, and source-only value fields may be omitted.

Because Map keys determine lookup identity, key evolution is more restrictive: source key Struct fields cannot be removed, primitive key casts are limited to known injective widenings, and sorted Maps require an unchanged key type. Source and target sorted flags must also match.

What changes are included in this PR?

  • Adds Map handling to the central nested Struct compatibility and casting paths.

  • Matches Map entry key/value children by semantic position rather than their technical entry field names, while continuing to match nested Struct fields by name.

  • Supports recursive Struct evolution in both Map keys and Map values.

  • Restricts Map key evolution to preserve key identity:

    • Source Struct key fields cannot be removed.
    • Newly added Struct key fields must be nullable.
    • Primitive key type changes are limited to explicitly supported injective widenings.
    • Sorted Maps require an unchanged key type.
  • Requires source and target Map sorted flags to match.

  • Validates Map entry invariants, including non-nullable entries and keys.

  • Compacts sliced Maps and entries hidden by null parent rows before recursively casting their children, so unreachable entries do not affect casts.

  • Uses StructArray::try_new when rebuilding Struct arrays so invalid nullability introduced by safe casts is reported as an error.

  • Keeps Maps without semantic Struct children on Arrow's existing Map cast path.

  • Adds upgrade documentation describing the Map nested Struct evolution semantics and migration considerations.

Are these changes tested?

Yes. The patch adds unit, integration, and SQL logic coverage for the new behavior, including:

  • Safe casts producing nulls for non-nullable Struct fields and Map values.
  • Primitive Map casts continuing to use Arrow's cast behavior.
  • Int32Int64 and UInt32Int64 Map key casts.
  • Supported and unsupported unsigned-to-signed injective key casts.
  • Positional matching of Map key/value entry fields with nested Struct adaptation.
  • Rejection of Map key Struct field removal and non-injective key casts.
  • Rejection of Map key Struct evolution with no field-name overlap.
  • Sorted Map key evolution rejection and sorted Map value evolution.
  • Rejection of changes to the Map sorted flag.
  • Null Map parents and sliced Maps with otherwise invalid unreachable values.
  • Struct evolution in both Map keys and values.
  • All-null Map columns with Struct values.
  • Map entry invariant validation.
  • Planner/runtime error parity for invalid Map evolution.
  • Recursive requires_nested_struct_cast behavior for Maps.
  • End-to-end Parquet Map value Struct schema evolution and incompatible evolution rejection.
  • SQL logic coverage for primitive Map assignment casts, duplicate/null Map keys, and UInt32/Int32 key coercion to Int64.

Are there any user-facing changes?

Yes. Maps whose keys or values recursively contain Structs now support schema evolution through DataFusion's nested adaptation path.

Nested Struct fields are matched by case-sensitive name. Nullable target fields can be added and populated with NULL; incompatible changes and missing non-nullable target fields are rejected. Map key evolution has additional restrictions to preserve key identity, and sorted Maps require an unchanged key type and matching sorted flag.

Ordinary Maps without semantic Struct children continue to use Arrow's existing Map cast behavior.

The upgrade guide for 55.0.0 documents these semantics and the migration considerations.

LLM-generated code disclosure

This PR includes LLM-generated code and comments. All LLM-generated content has been manually reviewed.

kosiew added 8 commits July 26, 2026 20:34
- Adapt recursive Map key/value structures.
- Implement shared planner/runtime validation.
- Preserve offsets and nulls; require matching sorted flags.
- Enforce Arrow Map entry/key invariants.
- Handle nullable additions, extras, null/all-null maps, and incompatible/non-nullable rejections.
- Add Parquet end-to-end regression tests.
…etadata internally

- Updated cast_map_column to accept &MapArray and reduce redundant arguments
- Extracted a shared error assertion helper for planner/runtime components
- Map key/value children now matched positionally while preserving target technical names.
- Implemented safe key evolution with no source key-field removal, injective primitive widening, nullable additions, and unchanged key types for sorted Maps.
- Compacted hidden null-parents and sliced unreachable entries before casting.
- Updated public rustdoc to document Map evolution semantics.
- Expanded Parquet test coverage for differing physical/logical entry names.
- Added planner/runtime regressions tests for uniqueness, sortedness, nulls, slices, and unsafe casts.
- Allow sorted targets to be set to false.
- Reject transitioning from sorted=false to sorted=true.
- Maintain requirement for unchanged key types in sorted targets.
- Allow downgraded unsorted targets to utilize existing safe key evolution.
- Update public Rust documentation.
- Add planner/runtime parity and value/key assertions.
- Reused validated target Map children and eliminated duplicate validation and map_entry_fields.
- Implemented common planner/runtime error assertion helper.
- Removed redundant sortedness assertions that are already covered by exact type equality.
- Public APIs remain unchanged.
…structs at planner boundary

- Added regression test for empty key struct to ensure nullable-only target key field is rejected in both planner and runtime.
…chema helpers

- Added `test_map_value_struct_incompatible_schema_evolution_rejected` for end-to-end negative testing of Parquet.
- Refactored shared map fixture and schema helper functions for improved clarity and reusability.
- Removed unused `let _ =` in nested_struct.rs
- Added local name lookup/set for map key struct validation in nested_struct.rs
- Extracted shared map E2E setup helper in expr_adapter.rs
- Added nullable/non-nullable schema wrapper helpers, avoiding bool at call sites in expr_adapter.rs
@github-actions github-actions Bot added core Core DataFusion crate common Related to common crate labels Jul 27, 2026
@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.40420% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.31%. Comparing base (f1f0449) to head (e2c55a5).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/common/src/nested_struct.rs 97.40% 15 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23914      +/-   ##
==========================================
+ Coverage   81.27%   82.31%   +1.03%     
==========================================
  Files        1116     1117       +1     
  Lines      395017   415524   +20507     
  Branches   395017   415524   +20507     
==========================================
+ Hits       321055   342032   +20977     
- Misses      55166    55208      +42     
+ Partials    18796    18284     -512     

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

kosiew added 2 commits July 27, 2026 12:16
- Updated the sorted to unsorted test to enforce rejection.
- Clarified documentation for requires_nested_struct_cast Map behavior.
- Extracted compact_map_entries() with explanation for identity take.
- Extracted is_injective_map_key_cast() and added documentation for conservative policy.
… cast_map_column and rewording to “specialized Map casting path”
@kosiew
kosiew marked this pull request as ready for review July 27, 2026 05:04
@kosiew

kosiew commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@TheBuilderJR
Can you test this PR?

@TheBuilderJR

Copy link
Copy Markdown
Contributor

yep lgtm. thanks @kosiew

@kosiew
kosiew requested a review from adriangb August 19, 2026 05:59

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

Overall looks great!

I think this requires an upgrade guide entry since it's a change in behavior.

Could we add / move some tests to SLTs in https://github.com/apache/datafusion/blob/main/datafusion/sqllogictest/test_files/map.slt

Comment thread datafusion/common/src/nested_struct.rs
Comment thread datafusion/common/src/nested_struct.rs
Comment thread datafusion/common/src/nested_struct.rs Outdated
Comment thread datafusion/common/src/nested_struct.rs
@adriangb

Copy link
Copy Markdown
Contributor

This may be a bug:

❯ datafusion-cli
DataFusion CLI v54.0.0
> SELECT map([arrow_cast(1,'UInt32')],['a']) UNION ALL SELECT map([arrow_cast(2,'Int32')],['b']);
+----------------------------------------------------------------------------+
| map(make_array(arrow_cast(Int64(1),Utf8("UInt32"))),make_array(Utf8("a"))) |
+----------------------------------------------------------------------------+
| {1: a}                                                                     |
| {2: b}                                                                     |
+----------------------------------------------------------------------------+
2 row(s) fetched.
Elapsed 0.062 seconds.

> \q

datafusion on  map-evolution-03-20835 [$?] is 📦 v54.1.0 via  v24.9.0 via 🐍 v3.12.8 via 🦀 v1.97.0 on ☁️  adrian@pydantic.dev(us-east4) took 2s
❯ cargo run -p datafusion-cli
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
     Running `target/debug/datafusion-cli`
DataFusion CLI v54.1.0
> SELECT map([arrow_cast(1,'UInt32')],['a']) UNION ALL SELECT map([arrow_cast(2,'Int32')],['b']);
Optimizer rule 'simplify_expressions' failed
caused by
This feature is not implemented: Unsupported CAST from Map("entries": non-null Struct("key": non-null UInt32, "value": Utf8), unsorted) to Map("entries": non-null Struct("key": non-null Int64, "value": Utf8), unsorted)

kosiew added 5 commits August 20, 2026 15:10
…and prevent panic on invalid casts

- Change `StructArray::new` to `try_new(...)` in struct and map casts, using `?` to propagate errors.
- Safe invalid casts now return Arrow/DataFusion errors instead of panicking.
- Add regression tests for non‑null struct field and map value.
- Added Map-vs-List-family rationale comment.
… non‑injective same‑width signed targets, fix UInt32::MAX → Int64 runtime regression, and handle UNION SLT regression with result‑type assertion
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Aug 20, 2026
…n adaptation for structs, add regression tests, update rustdoc

- Semantic Map key/value inspection no longer triggers nested path for technical entries
- Plain Maps → Arrow cast/validation
- Struct-containing Maps retain specialized adaptation
- Add unit + map.slt INSERT/duplicate/null-key regression tests
- Update rustdoc
@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 20, 2026
…ible map regressions; keep array/schema‑layout cases as Rust unit and Parquet integration tests
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 20, 2026
@adriangb

Copy link
Copy Markdown
Contributor

@kosiew ill let you decide when this is ready to merge

@kosiew
kosiew force-pushed the map-evolution-03-20835 branch from 2dc4db2 to e2c55a5 Compare August 21, 2026 11:43
@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Aug 21, 2026
@kosiew
kosiew added this pull request to the merge queue Aug 21, 2026
Merged via the queue into apache:main with commit e2bdc2c Aug 21, 2026
77 checks passed
@kosiew
kosiew deleted the map-evolution-03-20835 branch August 21, 2026 12:24
@kosiew

kosiew commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

🚀
@adriangb
Thanks for your review and feedback.

timsaucer added a commit to timsaucer/datafusion that referenced this pull request Aug 28, 2026
…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>
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 documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support additive schema evolution for List<Struct> / nested container types in Parquet scans

4 participants