Skip to content

fix(arrow): strip metadata from list elements and map entries - #3303

Open
NoahKusaba wants to merge 6 commits into
apache:mainfrom
NoahKusaba:fix/strip-metadata-nested-fields
Open

NoahKusaba wants to merge 6 commits into
apache:mainfrom
NoahKusaba:fix/strip-metadata-nested-fields

Conversation

@NoahKusaba

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

What changes are included in this PR?

strip_metadata_from_schema failed with Field stack underflow in list on any schema with a list or map column. MetadataStripVisitor pushes each field's name and nullability onto a stack in before_field and pops it when rebuilding the field. For list elements and map keys and values, the traversal calls before_list_element, before_map_key and before_map_value instead of before_field, so rebuilding them popped from an empty stack. The visitor now pushes in those three hooks too.

While here:

  • The stack holds each field's name and nullability instead of a Field with a DataType::Null placeholder, and one pop_field helper rebuilds fields for struct, list, map and primitive.
  • The doc comment states two normalizations the function already made, which are now reachable for maps: a map's entries field is renamed to DEFAULT_MAP_FIELD_NAME, as schema_to_arrow_schema names it, and a dictionary-encoded field becomes its value type. Callers comparing stripped schemas rely on both. For example, datafusion-iceberg compares an INSERT's input, whose map entries field DataFusion names entries, against the table's schema.

Are these changes tested?

Three unit tests in arrow::schema, each comparing the whole stripped schema with the expected one:

  • test_strip_metadata_from_nested_schema: lists, large and fixed-size lists, sorted and unsorted maps, and a list of structs holding a map of lists, with metadata on the schema and every field. Fails without the fix with the error from the issue.
  • test_strip_metadata_renames_map_entries
  • test_strip_metadata_unwraps_dictionaries

AI Disclosure

  • Used Claude Code to review the fix, simplify the visitor, write the tests and draft this description. I reviewed the change.

strip_metadata_from_schema failed with "Field stack underflow in list" on
any schema with a list or map column. MetadataStripVisitor pushed a field
onto its stack only in before_field, which the visitor does not call for
list elements, map keys or map values, so rebuilding those popped from an
empty stack. Push in before_list_element, before_map_key and
before_map_value too.

The stack now holds each field's name and nullability rather than a
Field with a placeholder type, and one pop_field helper rebuilds fields
for struct, list, map and primitive.

Document the two normalizations the function makes besides removing
metadata, which callers comparing stripped schemas rely on: a map's
entries field is renamed to DEFAULT_MAP_FIELD_NAME, and a
dictionary-encoded field becomes its value type.

Closes apache#3297
@NoahKusaba

Copy link
Copy Markdown
Contributor Author

cc @mbutrovich, @comphead

Comment thread crates/iceberg/src/arrow/schema.rs Outdated
Comment thread crates/iceberg/src/arrow/schema.rs Outdated
@NoahKusaba
NoahKusaba requested a review from comphead October 1, 2026 10:52

@comphead comphead left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small docs points.

  • ArrowSchemaVisitor::before_field and after_field are documented as called around a "struct/list/map field". The traversal calls them for every schema and struct field, primitives included, and never for list elements or map keys and values, which have their own hooks. A visitor that pushes in before_field only, like MetadataStripVisitor did, underflows on lists and maps. Consider rewording to "Called before each field of the schema or of a struct, whatever its type. List elements and map keys and values use before_list_element, before_map_key and before_map_value instead." and mirroring it on after_field.
  • The comment on test_strip_metadata_renames_map_entries says a map from arrow-rs or DataFusion compares equal to a table's once stripped. That holds for DataFusion's map() (entries, key, value), but not for an arrow-rs MapBuilder default (entries, keys, values), as the test's own keys and values fixture shows. Rewording it to say only the entries field is renamed would match what the test asserts.

Comment thread crates/iceberg/src/arrow/schema.rs Outdated
/// Two other differences are normalized away too, so that schemas that differ only in
/// them compare equal once stripped:
/// - A map's entries field is renamed to [`DEFAULT_MAP_FIELD_NAME`] and made non-nullable,
/// as [`schema_to_arrow_schema`] builds it. Its key and value fields keep their names.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says map key and value names are kept, but it is silent on list element names. Are they kept on purpose? arrow-rs names list elements item (Field::new_list_field) and schema_to_arrow_schema names them element, so a stripped List(item) still differs from a stripped table List(element).

If keeping them is the intent, one more sentence here would complete the contract. If callers need them normalized, LIST_FIELD_NAME is the name to rebuild with. That may not matter for datafusion-iceberg if DataFusion already casts the input to the table's nested names, which is unverified.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping them is intentional: renaming list elements would change what callers see, and the map rename only exists to match schema_to_arrow_schema. I added a paragraph in 1980676 that says all other names are kept, including list elements, and calls out the item vs element case.

@comphead comphead left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @NoahKusaba it looks good to me, some nits

NoahKusaba added a commit to NoahKusaba/datafusion-iceberg that referenced this pull request Oct 1, 2026
These depend on iceberg-rust's strip_metadata_from_schema handling list
and map columns (apache/iceberg-rust#3303) and fail until that lands.
…ma naming

before_field and after_field run for every schema and struct field, not
for list elements or map keys and values. strip_metadata_from_schema
keeps list element names, and renames only a map's entries field.
@NoahKusaba

Copy link
Copy Markdown
Contributor Author

@comphead Thanks! Both doc points are addressed in 1980676: I used your wording for before_field/after_field, and the test comment now says only the entries field is renamed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

strip_metadata_from_schema fails on list and map columns

2 participants