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-11826-efb25a30d3deeaac40af73443a441d1a0252c129
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit 901a171dbf03341a79d35bdb495332384590e236
Author: timmyb32r <[email protected]>
AuthorDate: Tue Oct 27 02:41:40 2026 +0000

    Fix incorrect reversed `DataType::contains` check for union type (#11826)
    
    # Which issue does this PR close?
    
    None — standalone bug fix.
    
    # Rationale for this change
    
    DataType::contains checks whether self is a superset of other.
    For Union types, the check iterated over self's variants instead
    of other's, reversing the containment relationship.
    
    # What changes are included in this PR?
    
    Check that every variant in other has a matching type ID and
    compatible field in self.
    
    # Are these changes tested?
    
    Added regression tests for both sparse and dense unions covering
    subset containment, empty unions, reordered fields, nullability,
    and incompatible type IDs, field types, and union modes.
    
    # Are there any significant user-facing changes?
    
    <!--
    Significant user-facing changes include:
    - Breaking public API change
    - Significant change to existing functionality (e.g. function returns
    something different)
    
    These need to be explicitly called out to ensure we have proper
    documentation if
    required and to ensure we follow proper semantic versioning.
    
    DO NOT restate the changes of this PR; if a bug was fixed and already
    explained
    above, we don't need this to be restated as a user-facing change.
    -->
    
    # 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/datatype.rs | 69 ++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 66 insertions(+), 3 deletions(-)

diff --git a/arrow-schema/src/datatype.rs b/arrow-schema/src/datatype.rs
index a947d5ad15..e8a0998987 100644
--- a/arrow-schema/src/datatype.rs
+++ b/arrow-schema/src/datatype.rs
@@ -836,9 +836,11 @@ impl DataType {
             (DataType::Struct(f1), DataType::Struct(f2)) => f1.contains(f2),
             (DataType::Union(f1, s1), DataType::Union(f2, s2)) => {
                 s1 == s2
-                    && f1
-                        .iter()
-                        .all(|f1| f2.iter().any(|f2| f1.0 == f2.0 && 
f1.1.contains(f2.1)))
+                    && f2.iter().all(|(other_id, other_field)| {
+                        f1.iter().any(|(self_id, self_field)| {
+                            self_id == other_id && 
self_field.contains(other_field)
+                        })
+                    })
             }
             (DataType::Dictionary(k1, v1), DataType::Dictionary(k2, v2)) => {
                 k1.contains(k2) && v1.contains(v2)
@@ -913,6 +915,67 @@ pub const DECIMAL_DEFAULT_SCALE: i8 = 10;
 mod tests {
     use super::*;
 
+    #[test]
+    fn test_union_contains_subset() {
+        for mode in [UnionMode::Sparse, UnionMode::Dense] {
+            let number = Field::new("number", DataType::Int32, false);
+            let text = Field::new("text", DataType::Utf8, false);
+            let superset = DataType::Union(
+                UnionFields::try_new([0, 1], [number.clone(), 
text.clone()]).unwrap(),
+                mode,
+            );
+            let subset =
+                DataType::Union(UnionFields::try_new([0], 
[number.clone()]).unwrap(), mode);
+            let reordered =
+                DataType::Union(UnionFields::try_new([1, 0], [text, 
number]).unwrap(), mode);
+            let empty = DataType::Union(UnionFields::empty(), mode);
+
+            assert!(superset.contains(&subset));
+            assert!(!subset.contains(&superset));
+            assert!(superset.contains(&superset));
+            assert!(superset.contains(&reordered));
+            assert!(reordered.contains(&superset));
+            assert!(superset.contains(&empty));
+            assert!(!empty.contains(&superset));
+            assert!(empty.contains(&empty));
+        }
+    }
+
+    #[test]
+    fn test_union_contains_field_compatibility() {
+        for mode in [UnionMode::Sparse, UnionMode::Dense] {
+            let field = Field::new("number", DataType::Int32, false);
+            let union = DataType::Union(UnionFields::try_new([0], 
[field.clone()]).unwrap(), mode);
+            let nullable = DataType::Union(
+                UnionFields::try_new([0], 
[field.clone().with_nullable(true)]).unwrap(),
+                mode,
+            );
+            assert!(nullable.contains(&union));
+            assert!(!union.contains(&nullable));
+
+            let different_id =
+                DataType::Union(UnionFields::try_new([1], 
[field.clone()]).unwrap(), mode);
+            assert!(!union.contains(&different_id));
+            assert!(!different_id.contains(&union));
+
+            let different_type = DataType::Union(
+                UnionFields::try_new([0], 
[field.clone().with_data_type(DataType::Utf8)]).unwrap(),
+                mode,
+            );
+            assert!(!union.contains(&different_type));
+            assert!(!different_type.contains(&union));
+
+            let other_mode = match mode {
+                UnionMode::Sparse => UnionMode::Dense,
+                UnionMode::Dense => UnionMode::Sparse,
+            };
+            let different_mode =
+                DataType::Union(UnionFields::try_new([0], [field]).unwrap(), 
other_mode);
+            assert!(!union.contains(&different_mode));
+            assert!(!different_mode.contains(&union));
+        }
+    }
+
     #[test]
     #[cfg(feature = "serde")]
     fn serde_struct_type() {

Reply via email to