This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11827-f23b77b6b9daf157078d329079453be78ba9f626 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit f7a72ebaa0386b93227e36f52171297391e49634 Author: timmyb32r <[email protected]> AuthorDate: Tue Oct 27 02:30:05 2026 +0000 Reject merging `Field`s of unions with different modes (#11827) # Which issue does this PR close? None — standalone bug fix. # Rationale for this change `Field::try_merge` accepted unions with different modes, even though sparse and dense unions have different physical layouts. The result retained the mode of `self`, silently ignoring the incompatible mode of the other field. # What changes are included in this PR? Return `ArrowError::SchemaError` when the union modes differ. # Are these changes tested? Added a regression test covering rejection of sparse/dense merges in both directions. It also verifies that unions with matching modes can still merge distinct variants. # Are there any significant user-facing changes? # AI assistance Codex identified the issue and generated the fix, regression tests, and initial PR description. Co-authored-by: Jeffrey Vo <[email protected]> --- arrow-schema/src/field.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/arrow-schema/src/field.rs b/arrow-schema/src/field.rs index feebdc29cf..dba5248637 100644 --- a/arrow-schema/src/field.rs +++ b/arrow-schema/src/field.rs @@ -821,9 +821,15 @@ impl Field { ))); } }, - DataType::Union(nested_fields, _) => match &from.data_type { - DataType::Union(from_nested_fields, _) => { - nested_fields.try_merge(from_nested_fields)? + DataType::Union(nested_fields, mode) => match &from.data_type { + DataType::Union(from_nested_fields, from_mode) => { + if mode != from_mode { + return Err(ArrowError::SchemaError(format!( + "Fail to merge schema field '{}' because the from union mode = {:?} does not match {:?}", + self.name, from_mode, mode + ))); + } + nested_fields.try_merge(from_nested_fields)?; } DataType::Null => { self.nullable = true; @@ -1083,6 +1089,46 @@ mod test { "#); } + #[test] + fn test_merge_union_modes() { + for mode in [UnionMode::Sparse, UnionMode::Dense] { + let mut field = Field::new_union( + "u", + [0], + [Field::new("number", DataType::Int32, false)], + mode, + ); + let other_mode = match mode { + UnionMode::Sparse => UnionMode::Dense, + UnionMode::Dense => UnionMode::Sparse, + }; + let incompatible = Field::new_union( + "u", + [0], + [Field::new("number", DataType::Int32, false)], + other_mode, + ); + assert!(matches!( + field.try_merge(&incompatible), + Err(ArrowError::SchemaError(_)) + )); + + let compatible = + Field::new_union("u", [1], [Field::new("text", DataType::Utf8, false)], mode); + field.try_merge(&compatible).unwrap(); + let expected = Field::new_union( + "u", + [0, 1], + [ + Field::new("number", DataType::Int32, false), + Field::new("text", DataType::Utf8, false), + ], + mode, + ); + assert_eq!(field, expected); + } + } + #[test] fn test_merge_incompatible_types() { let mut field = Field::new("c1", DataType::Int64, false);
