| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| //! Pushing a struct's null mask down into its children. |
| //! |
| //! Arrow stores a `StructArray`'s children with their own validity, independent of the parent's |
| //! null buffer, so at a row where the struct itself is null a child buffer can still hold a value |
| //! — parquet writes exactly that for `optional group c { required int32 a; }`, where the child leaf |
| //! has nowhere to record a null of its own. Spark's rule is that a field of a null struct is null, |
| //! so anything that reads the children has to combine the parent's nulls into them first. |
| //! |
| //! Getting this wrong is silent: `isnotnull(structCol.field)` reports true for a null struct |
| //! (<https://github.com/apache/datafusion-comet/issues/4432>), and hashing a null struct picks up |
| //! whatever sits in the child slot |
| //! (<https://github.com/apache/datafusion-comet/issues/5753>). |
| |
| use arrow::array::{make_array, Array, ArrayRef, StructArray}; |
| use arrow::buffer::NullBuffer; |
| use arrow::error::ArrowError; |
| use std::sync::Arc; |
| |
| /// The parent's nulls unioned into one child, or the child unchanged when there is nothing to push. |
| /// |
| /// Skips the work when the parent has no null to contribute, which covers both no null buffer at |
| /// all and an all-valid buffer — the latter is what slicing leaves behind. `NullBuffer` stores its |
| /// null count, so the test is O(1). |
| /// |
| /// The union only ever adds nulls, so the child's data buffers are untouched and are not |
| /// revalidated. Callers on a hot path depend on that: the hash kernels reach this once per element |
| /// of an `array<struct<..>>`, and re-checking a string child would rescan its whole UTF-8 values |
| /// buffer each time. |
| pub fn child_with_parent_nulls( |
| struct_array: &StructArray, |
| ordinal: usize, |
| ) -> Result<ArrayRef, ArrowError> { |
| let child = struct_array.column(ordinal); |
| match struct_array.nulls() { |
| Some(parent) if parent.null_count() > 0 => { |
| let combined = NullBuffer::union(Some(parent), child.nulls()); |
| // SAFETY: only nulls are added; every data buffer and child array is carried over |
| // unchanged, which is the same argument `StructArray::flatten` makes. |
| let data = child.to_data().into_builder().nulls(combined); |
| Ok(make_array(unsafe { data.build_unchecked() })) |
| } |
| _ => Ok(Arc::clone(child)), |
| } |
| } |
| |
| /// Every child with the parent's nulls unioned in, in field order. |
| /// |
| /// `StructArray::flatten` does the same union but also rebuilds `Fields`, re-marking non-nullable |
| /// fields as nullable. Callers that only want the arrays would discard that, so this avoids |
| /// building it. |
| pub fn children_with_parent_nulls(struct_array: &StructArray) -> Result<Vec<ArrayRef>, ArrowError> { |
| match struct_array.nulls() { |
| Some(parent) if parent.null_count() > 0 => (0..struct_array.num_columns()) |
| .map(|i| child_with_parent_nulls(struct_array, i)) |
| .collect(), |
| _ => Ok(struct_array.columns().to_vec()), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use arrow::array::Int32Array; |
| use arrow::datatypes::{DataType, Field, Fields}; |
| |
| fn struct_with(child: ArrayRef, nulls: Option<NullBuffer>) -> StructArray { |
| let fields: Fields = vec![Arc::new(Field::new("a", DataType::Int32, true))].into(); |
| StructArray::new(fields, vec![child], nulls) |
| } |
| |
| /// The case both bugs came from: the parent is null where the child still holds a value. |
| #[test] |
| fn hidden_child_value_under_a_null_parent_becomes_null() { |
| let child: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(999)])); |
| let sa = struct_with(child, Some(NullBuffer::from(vec![true, false]))); |
| |
| let out = child_with_parent_nulls(&sa, 0).unwrap(); |
| assert!(!out.is_null(0)); |
| assert!(out.is_null(1), "a field of a null struct must read as null"); |
| |
| let all = children_with_parent_nulls(&sa).unwrap(); |
| assert_eq!(all.len(), 1); |
| assert!(all[0].is_null(1)); |
| } |
| |
| /// No null buffer: the child is returned as is, not rebuilt. |
| #[test] |
| fn no_parent_nulls_returns_the_child_itself() { |
| let child: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(2)])); |
| let sa = struct_with(Arc::clone(&child), None); |
| |
| let out = child_with_parent_nulls(&sa, 0).unwrap(); |
| assert!( |
| Arc::ptr_eq(&out, sa.column(0)), |
| "expected the same array back" |
| ); |
| assert_eq!(out.null_count(), 0); |
| } |
| |
| /// A present but all-valid buffer, which is what slicing leaves behind, is also skipped. |
| #[test] |
| fn all_valid_parent_buffer_is_skipped() { |
| let child: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3)])); |
| let sa = struct_with(child, Some(NullBuffer::from(vec![true, true, true]))); |
| |
| let out = child_with_parent_nulls(&sa, 0).unwrap(); |
| assert!( |
| Arc::ptr_eq(&out, sa.column(0)), |
| "expected the same array back" |
| ); |
| } |
| |
| /// A child null the parent does not have must survive the union. |
| #[test] |
| fn child_nulls_are_preserved() { |
| let child: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])); |
| let sa = struct_with(child, Some(NullBuffer::from(vec![true, true, false]))); |
| |
| let out = child_with_parent_nulls(&sa, 0).unwrap(); |
| assert!(!out.is_null(0)); |
| assert!(out.is_null(1), "the child's own null must remain"); |
| assert!(out.is_null(2), "the parent's null must be applied"); |
| } |
| |
| /// The old guard compared null *counts*, which can match while the nulls sit at different rows. |
| #[test] |
| fn equal_null_counts_at_different_rows_still_need_the_union() { |
| use arrow::array::Int32Array; |
| let child: ArrayRef = Arc::new(Int32Array::from(vec![None, Some(2)])); |
| // parent null at row 1, child null at row 0: counts equal (1 == 1), positions differ. |
| let sa = struct_with(child, Some(NullBuffer::from(vec![true, false]))); |
| assert_eq!(sa.null_count(), sa.column(0).null_count()); |
| |
| let out = child_with_parent_nulls(&sa, 0).unwrap(); |
| assert!(out.is_null(0), "child's own null"); |
| assert!(out.is_null(1), "parent's null must still be applied"); |
| assert_eq!(out.null_count(), 2); |
| } |
| |
| /// An empty struct has no children to union into. |
| #[test] |
| fn empty_struct_has_no_children() { |
| let sa = StructArray::new_empty_fields(2, Some(NullBuffer::from(vec![true, false]))); |
| assert!(children_with_parent_nulls(&sa).unwrap().is_empty()); |
| } |
| } |