[Data] Raise a clear error when _backfill_missing_fields gets a non-struct column - #65257
[Data] Raise a clear error when _backfill_missing_fields gets a non-struct column#65257jackylee-ch wants to merge 2 commits into
Conversation
…truct column `_align_struct_fields` guards top-level columns with `isinstance(column.type, pa.StructType)`, but its recursive call only checks the target field type, so a nested field that is a struct in one block and a primitive in another reached `_backfill_missing_fields` and failed with `TypeError: 'pyarrow.lib.DataType' object is not iterable`. Add the same check at the top of the function. Signed-off-by: jackylee-ch <qcsd2011@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request adds a check in _backfill_missing_fields to raise a ValueError when a nested non-struct field is aligned with a struct type, preventing unexpected failures during iteration. It also includes unit tests for this scenario. The review feedback points out a critical edge case where a nested field is entirely null (type pa.null()); the new check would incorrectly raise a ValueError for this valid promotion. It is recommended to explicitly handle null types by returning a null array of the target struct type.
| if not pa.types.is_struct(column.type): | ||
| raise ValueError( | ||
| f"Column of type {column.type} cannot be aligned with struct type " | ||
| f"{unified_struct_type}. A block holds a non-struct value where the " | ||
| "unified schema expects a struct." | ||
| ) |
There was a problem hiding this comment.
If a nested field is entirely null in a block, its inferred type in that block's schema might be pa.null(). During schema unification, this null type is successfully promoted to the target StructType from other blocks. However, when aligning the block, _backfill_missing_fields will be recursively called with column having type pa.null().
With the current check, this will raise a ValueError because pa.null() is not a struct type, even though promoting a null nested field to a struct is a perfectly valid operation.
To support this, we should explicitly handle the pa.types.is_null(column.type) case by returning a null array of the target struct type.
if pa.types.is_null(column.type):
return pa.nulls(len(column), type=unified_struct_type)
if not pa.types.is_struct(column.type):
raise ValueError(
f"Column of type {column.type} cannot be aligned with struct type "
f"{unified_struct_type}. A block holds a non-struct value where the "
"unified schema expects a struct."
)A nested field that is entirely null in one block infers as `pa.null()`, which `unify_schemas` promotes to the struct type from the other blocks. The new guard rejected it as a conflict. Return nulls of the target struct type for that case; only genuinely non-struct types now raise. Signed-off-by: jackylee-ch <qcsd2011@gmail.com>
|
Confirmed — fixed in 1691281. An all-null nested field does infer as Added the |
Description
_align_struct_fieldsguards top-level columns withisinstance(column.type, pa.StructType), but its recursive call only checks the target field type. So a nested field that is a struct in one block and a primitive in another passed the outer guard and failed inside_backfill_missing_fieldswhile iteratingcolumn.type.unify_schemaspermits the mismatch:_reconcile_fieldkeeps only the struct arms, so the unified type claimsinneris a struct even though one block holds anint64.This adds that same check at the top of the function. An all-null nested field infers as
pa.null()and is filled with nulls of the target type rather than rejected.Raising seemed safer than the issue's other option (backfilling a null struct), since coercing a schema conflict silently alters data — happy to switch.
The issue's repro no longer reaches this path: for a top-level mismatch the outer guard skips alignment and
pa.concat_tablesraises a readableArrowTypeError. Only the nested case leaked theTypeError.Related issues
Fixes #61656
Additional information
Tests: the nested mismatch via
_align_struct_fieldsand viaconcat, plus the all-null case.unit/test_transform_pyarrow.py81 passed (was 78);unit/test_arrow_block.py66,test_map.py240,test_union.py5,datasource/test_json.py58 passed;pre-commitclean; reverting either branch fails its test. Bazel/C++ not run locally.#61656 is unassigned and no open PR touches this file; two earlier attempts (#62170, #63960) were closed by the stale bot without maintainer objection. AI assistance (Claude) was used; I reviewed every changed line and ran the tests above.