-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: zip now treats nulls as false in provided mask regardless of the underlying bit value
#8711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,8 +17,9 @@ | |||||||||
|
|
||||||||||
| //! [`zip`]: Combine values from two arrays based on boolean mask | ||||||||||
|
|
||||||||||
| use crate::filter::SlicesIterator; | ||||||||||
| use crate::filter::{SlicesIterator, prep_null_mask_filter}; | ||||||||||
| use arrow_array::*; | ||||||||||
| use arrow_buffer::BooleanBuffer; | ||||||||||
| use arrow_data::transform::MutableArrayData; | ||||||||||
| use arrow_schema::ArrowError; | ||||||||||
|
|
||||||||||
|
|
@@ -127,7 +128,8 @@ pub fn zip( | |||||||||
| // keep track of how much is filled | ||||||||||
| let mut filled = 0; | ||||||||||
|
|
||||||||||
| SlicesIterator::new(mask).for_each(|(start, end)| { | ||||||||||
| let mask = maybe_prep_null_mask_filter(mask); | ||||||||||
| SlicesIterator::from(&mask).for_each(|(start, end)| { | ||||||||||
| // the gap needs to be filled with falsy values | ||||||||||
| if start > filled { | ||||||||||
| if falsy_is_scalar { | ||||||||||
|
|
@@ -166,9 +168,22 @@ pub fn zip( | |||||||||
| Ok(make_array(data)) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| fn maybe_prep_null_mask_filter(predicate: &BooleanArray) -> BooleanBuffer { | ||||||||||
| // Nulls are treated as false | ||||||||||
| if predicate.null_count() == 0 { | ||||||||||
| predicate.values().clone() | ||||||||||
| } else { | ||||||||||
| let cleaned = prep_null_mask_filter(predicate); | ||||||||||
| let (boolean_buffer, _) = cleaned.into_parts(); | ||||||||||
| boolean_buffer | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #[cfg(test)] | ||||||||||
| mod test { | ||||||||||
| use super::*; | ||||||||||
| use arrow_array::cast::AsArray; | ||||||||||
| use arrow_buffer::{BooleanBuffer, NullBuffer}; | ||||||||||
|
|
||||||||||
| #[test] | ||||||||||
| fn test_zip_kernel_one() { | ||||||||||
|
|
@@ -279,4 +294,110 @@ mod test { | |||||||||
| let expected = Int32Array::from(vec![None, None, Some(42), Some(42), None]); | ||||||||||
| assert_eq!(actual, &expected); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #[test] | ||||||||||
| fn test_zip_kernel_primitive_scalar_with_boolean_array_mask_with_nulls_should_be_treated_as_false() | ||||||||||
| { | ||||||||||
| let scalar_truthy = Scalar::new(Int32Array::from_value(42, 1)); | ||||||||||
| let scalar_falsy = Scalar::new(Int32Array::from_value(123, 1)); | ||||||||||
|
|
||||||||||
| let mask = { | ||||||||||
| let booleans = BooleanBuffer::from(vec![true, true, false, true, false, false]); | ||||||||||
| let nulls = NullBuffer::from(vec![ | ||||||||||
| true, true, true, | ||||||||||
| false, // null treated as false even though in the original mask it was true | ||||||||||
| true, true, | ||||||||||
| ]); | ||||||||||
| BooleanArray::new(booleans, Some(nulls)) | ||||||||||
| }; | ||||||||||
| let out = zip(&mask, &scalar_truthy, &scalar_falsy).unwrap(); | ||||||||||
| let actual = out.as_any().downcast_ref::<Int32Array>().unwrap(); | ||||||||||
| let expected = Int32Array::from(vec![ | ||||||||||
| Some(42), | ||||||||||
| Some(42), | ||||||||||
| Some(123), | ||||||||||
| Some(123), // true in mask but null | ||||||||||
| Some(123), | ||||||||||
| Some(123), | ||||||||||
| ]); | ||||||||||
| assert_eq!(actual, &expected); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #[test] | ||||||||||
| fn test_zip_primitive_array_with_nulls_is_mask_should_be_treated_as_false() { | ||||||||||
| let scalar_truthy = Int32Array::from_iter_values(vec![1, 2, 3, 4, 5, 6]); | ||||||||||
| let scalar_falsy = Int32Array::from_iter_values(vec![7, 8, 9, 10, 11, 12]); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit is that these are not scalars (they are arrays)
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will fix now, thanks |
||||||||||
|
|
||||||||||
| let mask = { | ||||||||||
| let booleans = BooleanBuffer::from(vec![true, true, false, true, false, false]); | ||||||||||
| let nulls = NullBuffer::from(vec![ | ||||||||||
| true, true, true, | ||||||||||
| false, // null treated as false even though in the original mask it was true | ||||||||||
| true, true, | ||||||||||
| ]); | ||||||||||
| BooleanArray::new(booleans, Some(nulls)) | ||||||||||
| }; | ||||||||||
| let out = zip(&mask, &scalar_truthy, &scalar_falsy).unwrap(); | ||||||||||
| let actual = out.as_any().downcast_ref::<Int32Array>().unwrap(); | ||||||||||
| let expected = Int32Array::from(vec![ | ||||||||||
| Some(1), | ||||||||||
| Some(2), | ||||||||||
| Some(9), | ||||||||||
| Some(10), // true in mask but null | ||||||||||
| Some(11), | ||||||||||
| Some(12), | ||||||||||
| ]); | ||||||||||
| assert_eq!(actual, &expected); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #[test] | ||||||||||
| fn test_zip_string_array_with_nulls_is_mask_should_be_treated_as_false() { | ||||||||||
| let scalar_truthy = StringArray::from_iter_values(vec!["1", "2", "3", "4", "5", "6"]); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto here, these are not scalar |
||||||||||
| let scalar_falsy = StringArray::from_iter_values(vec!["7", "8", "9", "10", "11", "12"]); | ||||||||||
|
|
||||||||||
| let mask = { | ||||||||||
| let booleans = BooleanBuffer::from(vec![true, true, false, true, false, false]); | ||||||||||
| let nulls = NullBuffer::from(vec![ | ||||||||||
| true, true, true, | ||||||||||
| false, // null treated as false even though in the original mask it was true | ||||||||||
| true, true, | ||||||||||
| ]); | ||||||||||
| BooleanArray::new(booleans, Some(nulls)) | ||||||||||
| }; | ||||||||||
| let out = zip(&mask, &scalar_truthy, &scalar_falsy).unwrap(); | ||||||||||
| let actual = out.as_string::<i32>(); | ||||||||||
| let expected = StringArray::from_iter_values(vec![ | ||||||||||
| "1", "2", "9", "10", // true in mask but null | ||||||||||
| "11", "12", | ||||||||||
| ]); | ||||||||||
| assert_eq!(actual, &expected); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| #[test] | ||||||||||
| fn test_zip_kernel_large_string_scalar_with_boolean_array_mask_with_nulls_should_be_treated_as_false() | ||||||||||
|
alamb marked this conversation as resolved.
|
||||||||||
| { | ||||||||||
| let scalar_truthy = Scalar::new(LargeStringArray::from_iter_values(["test"])); | ||||||||||
| let scalar_falsy = Scalar::new(LargeStringArray::from_iter_values(["something else"])); | ||||||||||
|
|
||||||||||
| let mask = { | ||||||||||
| let booleans = BooleanBuffer::from(vec![true, true, false, true, false, false]); | ||||||||||
| let nulls = NullBuffer::from(vec![ | ||||||||||
| true, true, true, | ||||||||||
| false, // null treated as false even though in the original mask it was true | ||||||||||
| true, true, | ||||||||||
| ]); | ||||||||||
| BooleanArray::new(booleans, Some(nulls)) | ||||||||||
| }; | ||||||||||
| let out = zip(&mask, &scalar_truthy, &scalar_falsy).unwrap(); | ||||||||||
| let actual = out.as_any().downcast_ref::<LargeStringArray>().unwrap(); | ||||||||||
| let expected = LargeStringArray::from_iter(vec![ | ||||||||||
| Some("test"), | ||||||||||
| Some("test"), | ||||||||||
| Some("something else"), | ||||||||||
| Some("something else"), // true in mask but null | ||||||||||
| Some("something else"), | ||||||||||
| Some("something else"), | ||||||||||
| ]); | ||||||||||
| assert_eq!(actual, &expected); | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a follow on PR this would be a nice improvement to make to
prep_null_mask_filteritself -- handling arrays without nulls