Skip to content

Commit 1ba902e

Browse files
authored
Fix nullif kernel (#9087)
# Which issue does this PR close? - Closes #9085 # Rationale for this change Fix a regression introduced in #8996 # What changes are included in this PR? 1. Add test coverage for nullif kernel 1. Undeprecate `bitwise_unary_op_helper` 2. Document subtle differences 3. Restore nullif kernel from #8996 # Are these changes tested Yes # Are there any user-facing changes? Fix (not yet released) bug
1 parent 49c27d6 commit 1ba902e

3 files changed

Lines changed: 136 additions & 28 deletions

File tree

arrow-buffer/src/buffer/boolean.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ impl BooleanBuffer {
165165
/// * `op` must only apply bitwise operations
166166
/// on the relevant bits; the input `u64` may contain irrelevant bits
167167
/// and may be processed differently on different endian architectures.
168+
/// * `op` may be called with input bits outside the requested range
168169
/// * The output always has zero offset
169170
///
170171
/// # See Also

arrow-buffer/src/buffer/ops.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ use crate::BooleanBuffer;
2020
use crate::util::bit_util::ceil;
2121

2222
/// Apply a bitwise operation `op` to four inputs and return the result as a Buffer.
23-
/// The inputs are treated as bitmaps, meaning that offsets and length are specified in number of bits.
23+
///
24+
/// The inputs are treated as bitmaps, meaning that offsets and length are
25+
/// specified in number of bits.
26+
///
27+
/// NOTE: The operation `op` is applied to chunks of 64 bits (u64) and any bits
28+
/// outside the offsets and len are set to zero out before calling `op`.
2429
pub fn bitwise_quaternary_op_helper<F>(
2530
buffers: [&Buffer; 4],
2631
offsets: [usize; 4],
@@ -60,7 +65,12 @@ where
6065
}
6166

6267
/// Apply a bitwise operation `op` to two inputs and return the result as a Buffer.
63-
/// The inputs are treated as bitmaps, meaning that offsets and length are specified in number of bits.
68+
///
69+
/// The inputs are treated as bitmaps, meaning that offsets and length are
70+
/// specified in number of bits.
71+
///
72+
/// NOTE: The operation `op` is applied to chunks of 64 bits (u64) and any bits
73+
/// outside the offsets and len are set to zero out before calling `op`.
6474
pub fn bitwise_bin_op_helper<F>(
6575
left: &Buffer,
6676
left_offset_in_bits: usize,
@@ -93,21 +103,42 @@ where
93103
}
94104

95105
/// Apply a bitwise operation `op` to one input and return the result as a Buffer.
96-
/// The input is treated as a bitmap, meaning that offset and length are specified in number of bits.
97-
#[deprecated(
98-
since = "57.2.0",
99-
note = "use BooleanBuffer::from_bitwise_unary_op instead"
100-
)]
106+
///
107+
/// The input is treated as a bitmap, meaning that offset and length are
108+
/// specified in number of bits.
109+
///
110+
/// NOTE: The operation `op` is applied to chunks of 64 bits (u64) and any bits
111+
/// outside the offsets and len are set to zero out before calling `op`.
101112
pub fn bitwise_unary_op_helper<F>(
102113
left: &Buffer,
103114
offset_in_bits: usize,
104115
len_in_bits: usize,
105-
op: F,
116+
mut op: F,
106117
) -> Buffer
107118
where
108119
F: FnMut(u64) -> u64,
109120
{
110-
BooleanBuffer::from_bitwise_unary_op(left, offset_in_bits, len_in_bits, op).into_inner()
121+
// reserve capacity and set length so we can get a typed view of u64 chunks
122+
let mut result =
123+
MutableBuffer::new(ceil(len_in_bits, 8)).with_bitset(len_in_bits / 64 * 8, false);
124+
125+
let left_chunks = left.bit_chunks(offset_in_bits, len_in_bits);
126+
127+
let result_chunks = result.typed_data_mut::<u64>().iter_mut();
128+
129+
result_chunks
130+
.zip(left_chunks.iter())
131+
.for_each(|(res, left)| {
132+
*res = op(left);
133+
});
134+
135+
let remainder_bytes = ceil(left_chunks.remainder_len(), 8);
136+
let rem = op(left_chunks.remainder_bits());
137+
// we are counting its starting from the least significant bit, to to_le_bytes should be correct
138+
let rem = &rem.to_le_bytes()[0..remainder_bytes];
139+
result.extend_from_slice(rem);
140+
141+
result.into()
111142
}
112143

113144
/// Apply a bitwise and to two inputs and return the result as a Buffer.

arrow-select/src/nullif.rs

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
2020
use arrow_array::{Array, ArrayRef, BooleanArray, make_array};
2121
use arrow_buffer::buffer::bitwise_bin_op_helper;
22-
use arrow_buffer::{BooleanBuffer, NullBuffer};
22+
use arrow_buffer::{BooleanBuffer, NullBuffer, bitwise_unary_op_helper};
2323
use arrow_schema::{ArrowError, DataType};
2424

2525
/// Returns a new array with the same values and the validity bit to false where
@@ -91,13 +91,11 @@ pub fn nullif(left: &dyn Array, right: &BooleanArray) -> Result<ArrayRef, ArrowE
9191
}
9292
None => {
9393
let mut null_count = 0;
94-
let buffer =
95-
BooleanBuffer::from_bitwise_unary_op(right.inner(), right.offset(), len, |b| {
96-
let t = !b;
97-
null_count += t.count_zeros() as usize;
98-
t
99-
})
100-
.into_inner();
94+
let buffer = bitwise_unary_op_helper(right.inner(), right.offset(), len, |b| {
95+
let t = !b;
96+
null_count += t.count_zeros() as usize;
97+
t
98+
});
10199
(buffer, null_count)
102100
}
103101
};
@@ -122,7 +120,8 @@ mod tests {
122120
use arrow_array::{Int32Array, NullArray, StringArray, StructArray};
123121
use arrow_data::ArrayData;
124122
use arrow_schema::{Field, Fields};
125-
use rand::{Rng, rng};
123+
use rand::prelude::StdRng;
124+
use rand::{Rng, SeedableRng};
126125

127126
#[test]
128127
fn test_nullif_int_array() {
@@ -494,38 +493,115 @@ mod tests {
494493
let r_data = r.to_data();
495494
r_data.validate().unwrap();
496495

497-
assert_eq!(r.as_ref(), &expected);
496+
assert_eq!(
497+
r.as_ref(),
498+
&expected,
499+
"expected nulls: {:#?}\n\n\
500+
result nulls: {:#?}\n\n\\
501+
expected values: {:#?}\n\n\
502+
result values: {:#?}",
503+
expected.nulls(),
504+
r.nulls(),
505+
expected.values(),
506+
r.as_primitive::<Int32Type>().values()
507+
);
508+
validate_nulls(expected.nulls());
509+
validate_nulls(r.nulls());
510+
}
511+
512+
/// Ensures that the null count matches the actual number of nulls.
513+
fn validate_nulls(nulls: Option<&NullBuffer>) {
514+
let Some(nulls) = nulls else {
515+
return;
516+
};
517+
let mut actual_null_count = 0;
518+
for i in 0..nulls.len() {
519+
if nulls.is_null(i) {
520+
actual_null_count += 1;
521+
}
522+
}
523+
assert_eq!(actual_null_count, nulls.null_count());
498524
}
499525

500526
#[test]
501527
fn nullif_fuzz() {
502-
let mut rng = rng();
528+
let mut rng = StdRng::seed_from_u64(7337);
503529

504530
let arrays = [
505-
Int32Array::from(vec![0; 128]),
506-
(0..128)
507-
.map(|_| rng.random_bool(0.5).then_some(0))
531+
Int32Array::from(vec![0; 1024]), // no nulls
532+
(0..1024) // 50% nulls
533+
.map(|_| rng.random_bool(0.5).then_some(1))
508534
.collect(),
509535
];
510536

511537
for a in arrays {
512-
let a_slices = [(0, 128), (64, 64), (0, 64), (32, 32), (0, 0), (32, 0)];
513-
538+
let a_slices = [
539+
(0, 128),
540+
(0, 129),
541+
(64, 64),
542+
(0, 64),
543+
(32, 32),
544+
(0, 0),
545+
(32, 0),
546+
(5, 800),
547+
(33, 53),
548+
(77, 101),
549+
];
514550
for (a_offset, a_length) in a_slices {
515551
let a = a.slice(a_offset, a_length);
516552

517553
for i in 1..65 {
518554
let b_start_offset = rng.random_range(0..i);
519555
let b_end_offset = rng.random_range(0..i);
520556

557+
// b with 50% nulls
521558
let b: BooleanArray = (0..a_length + b_start_offset + b_end_offset)
522559
.map(|_| rng.random_bool(0.5).then(|| rng.random_bool(0.5)))
523560
.collect();
524-
let b = b.slice(b_start_offset, a_length);
525-
526-
test_nullif(&a, &b);
561+
let b_sliced = b.slice(b_start_offset, a_length);
562+
test_nullif(&a, &b_sliced);
563+
564+
// b with no nulls (and no null buffer)
565+
let b = remove_null_buffer(&b);
566+
let b_sliced = b.slice(b_start_offset, a_length);
567+
test_nullif(&a, &b_sliced);
568+
569+
// b with no nulls (but with a null buffer)
570+
let b = remove_null_values(&b);
571+
let b_sliced = b.slice(b_start_offset, a_length);
572+
test_nullif(&a, &b_sliced);
527573
}
528574
}
529575
}
530576
}
577+
578+
/// Returns a new BooleanArray with no null buffer
579+
fn remove_null_buffer(array: &BooleanArray) -> BooleanArray {
580+
make_array(
581+
array
582+
.into_data()
583+
.into_builder()
584+
.nulls(None)
585+
.build()
586+
.unwrap(),
587+
)
588+
.as_boolean()
589+
.clone()
590+
}
591+
592+
/// Returns a new BooleanArray with a null buffer where all values are valid
593+
fn remove_null_values(array: &BooleanArray) -> BooleanArray {
594+
let len = array.len();
595+
let new_nulls = NullBuffer::from_iter(std::iter::repeat_n(true, len));
596+
make_array(
597+
array
598+
.into_data()
599+
.into_builder()
600+
.nulls(Some(new_nulls))
601+
.build()
602+
.unwrap(),
603+
)
604+
.as_boolean()
605+
.clone()
606+
}
531607
}

0 commit comments

Comments
 (0)