Add FixedSizeList support for recursive struct schema adaptation#22980
Add FixedSizeList support for recursive struct schema adaptation#22980kosiew wants to merge 11 commits into
Conversation
…untime parity - Updated runtime casting to recognize FixedSizeList when source and target list sizes match, allowing recursive adaptation of the child struct via a new helper. - Extended planner/runtime parity by updating `validate_data_type_compatibility` and `requires_nested_struct_cast` to handle equal-size FixedSizeList children. test: add regression coverage for nested field changes - Added tests for: - Additive nullable nested-field evolution - Planner acceptance for compatible cases - Non-nullable added-field rejection - All-null column handling - Planner/runtime parity on incompatible nested type changes
- Introduced `downcast_array!` macro to simplify downcasting of array types and improve error handling with descriptive messages. - Refactored existing functions `cast_list_column`, `cast_list_view_column`, and `cast_fixed_size_list_column` to utilize the new macro for improved readability and maintainability. - Added helper function `create_fsl_test_fields` to streamline test field creation for fixed-size list struct tests. - Updated related tests to use the new helper function for improved clarity and reduced redundancy.
- Prevent validation from accepting unsupported length changes - Ensure planner behavior aligns with runtime casting - Add regression test for rejected mismatch case
…t_fields for clarity
- Added regression test for null FixedSizeList parent hiding invalid child values. - Implemented runtime retry mechanism for child slots masked as null when cast fails due to hidden null-parent values. - Added runtime parity test to ensure missing non-nullable nested fields are still rejected.
- Removed redundant FixedSizeList size-mismatch match arm - Bound repeated source_values and target_type - Renamed masked fallback local for better readability - Inlined single-use mask wrapper - Added test helpers to reduce duplication in tests - Reused fields in parity test for efficiency - Reused s1 and s2 in requires_nested_struct_cast test to minimize duplication
| let source_values = source_list.values(); | ||
| let target_type = target_inner_field.data_type(); | ||
|
|
||
| validate_data_type_compatibility( |
There was a problem hiding this comment.
The other cast_*_column methods don't do this validate call, is this significant?
There was a problem hiding this comment.
This fixed-size-list path differs from the list/list-view paths because it has a retry that masks child values hidden behind null parent list slots. The schema compatibility check ensures that retry is only used for value-level failures in hidden child positions, not to accidentally relax planner/runtime type compatibility. The parity tests for incompatible nested type and missing non-nullable fields cover this. I’ll add a comment or restructure the code so this guard is explicit.
There was a problem hiding this comment.
I still don't think we need to compatibility check again here on this path, as we're just meant to be casting at this point 🤔
There was a problem hiding this comment.
- Removed fixed-size-list compatibility check from the normal cast path.
- Moved compatibility guard into the null-parent retry path only.
- Adjusted cast_struct_column all-null fast path to follow struct compatibility validation, ensuring all-null structs reject missing non-nullable fields.
| let cast_values = match cast_column(source_values, target_type, cast_options) { | ||
| Ok(cast_values) => cast_values, | ||
| Err(error) => match source_list.nulls() { | ||
| Some(parent_nulls) if parent_nulls.null_count() > 0 => { | ||
| let hidden_child_nulls = parent_nulls.expand(target_list_size as usize); | ||
| let masked_values = |
There was a problem hiding this comment.
I dont understand whats happening here; why do we have this fallback if we fail to cast?
There was a problem hiding this comment.
The fallback is intentional. FixedSizeListArray stores len * list_size child values even for null parent list entries, so recursive casting can fail on child values that are semantically hidden by a null parent. The fallback expands the parent null bitmap to child positions, masks those hidden child values to null, and retries; visible child values are still cast normally. test_cast_fixed_size_list_struct_ignores_hidden_child_values_for_null_parent covers this case. I’ll add a targeted comment and keep the fallback limited to this null-parent masking case.
There was a problem hiding this comment.
This is interesting; it technically applies for the other list methods too but those would be rare cases where the null slots are non-empty (and have values that would error on cast), not to mention this would only occur when cast options has safe=false
Also I think this can also occur in the arrow-rs upstream cast code, so might need to raise an issue there too 🤔
…_array! - Removed local `downcast_array!` implementation - Used Arrow methods: - `source_col.as_list::<O>()` - `source_col.as_list_view::<O>()` - `source_col.as_fixed_size_list()` - Added `AsArray` import
- Added comment before `validate_data_type_compatibility` to explain planner/runtime guard. - Restructured fallback into an explicit helper: - Only attempts masked retry when parent nulls are present. - Returns original cast error in all other cases. - Clarified the split between guard and fallback logic.
… behavior - Explain hidden child slots handling for null parents - Clarify that retries occur only for parent-null masking - State that behavior remains unchanged for scenarios without parent nulls, retaining original cast error
…ing logic - Removed fixed-size-list compatibility check from the normal cast path. - Moved compatibility guard into the null-parent retry path only. - Adjusted cast_struct_column all-null fast path to follow struct compatibility validation, ensuring all-null structs reject missing non-nullable fields.
|
Hi @TheBuilderJR -- I am confused about what this PR is fixing / adding -- it clearly has a bunch of unit tests, but I am missing the bigger picture -- what type of plan / SQL / DataFrame are you running on what type of input that is not working? I think it would help to have a fuller end to end test of what you are doing (e.g. is it reading some nested structure from parquet files and they are getting confused) Otherwise I worry that we won't be able ot maintain this code as it is refactored over time as if we change how it is structured we won't know if we broke the original usecase |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #22980 +/- ##
==========================================
+ Coverage 80.72% 80.73% +0.01%
==========================================
Files 1089 1089
Lines 368911 369187 +276
Branches 368911 369187 +276
==========================================
+ Hits 297785 298057 +272
Misses 53374 53374
- Partials 17752 17756 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| // Before masking and retrying, guard schema compatibility so the retry only | ||
| // handles value-level failures in hidden child slots and cannot make runtime | ||
| // accept schemas that planning rejects. | ||
| if let Err(error) = validate_data_type_compatibility( |
There was a problem hiding this comment.
i still dont agree with having this validate call on the cast path
we dont do anything similar for other list types even though they could technically contain invalid data (that cant be casted) in a non-empty null slot
Which issue does this PR close?
List<Struct>/ nested container types in Parquet scans #20835Rationale for this change
FixedSizeListcontainingStructvalues was not handled by the existing recursive nested adaptation logic used for schema evolution. As a result, planner-time compatibility checks, nested cast detection, and runtime casting did not support additive struct evolution withinFixedSizeListcontainers.This change adds
FixedSizeListsupport and verifies planner/runtime parity so that planning allows exactly the cases runtime can adapt while continuing to reject incompatible schema changes.What changes are included in this PR?
Extend
cast_columnto support recursive casting ofFixedSizeListvalues when source and target list sizes match.Add
FixedSizeListhandling to:requires_nested_struct_castvalidate_data_type_compatibilityImplement recursive casting of nested
Structvalues contained inFixedSizeList.Preserve planner/runtime parity by validating child type compatibility before runtime fallback logic is applied.
Add handling for null-parent
FixedSizeListentries by masking hidden child values before retrying casts, avoiding failures caused by semantically inaccessible child data.Refactor list and list-view casting helpers to use Arrow
AsArrayaccessors.Are these changes tested?
Yes.
The following tests were added:
test_cast_fixed_size_list_structtest_validate_fixed_size_list_struct_compatibilitytest_validate_fixed_size_list_struct_missing_non_nullable_field_rejectedtest_validate_fixed_size_list_struct_size_mismatch_rejectedtest_cast_fixed_size_list_struct_all_nulltest_fixed_size_list_struct_planner_runtime_parity_on_incompatible_typetest_cast_fixed_size_list_struct_missing_non_nullable_field_runtime_rejectedtest_cast_fixed_size_list_struct_ignores_hidden_child_values_for_null_parentExisting coverage in
test_requires_nested_struct_castwas also extended to includeFixedSizeListcases.These tests cover:
Are there any user-facing changes?
No user-facing changes. This is an internal enhancement to nested schema adaptation and casting behavior for
FixedSizeList<Struct>types.LLM-generated code disclosure
This PR includes LLM-generated code and comments. All LLM-generated content has been manually reviewed.