This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new bfee84476d Add mutable bitwise operations to `BooleanArray` and
`NullBuffer::union_many` (#9692)
bfee84476d is described below
commit bfee84476da7d73e89c73adf80b974bdac6c8a29
Author: Matt Butrovich <[email protected]>
AuthorDate: Tue Apr 14 13:12:37 2026 -0400
Add mutable bitwise operations to `BooleanArray` and
`NullBuffer::union_many` (#9692)
## Which issue does this PR close?
- Closes #8809.
## Rationale for this change
Several DataFusion PRs
([#21464](https://github.com/apache/datafusion/pull/21464),
[#21468](https://github.com/apache/datafusion/pull/21468),
[#21471](https://github.com/apache/datafusion/pull/21471),
[#21475](https://github.com/apache/datafusion/pull/21475),
[#21477](https://github.com/apache/datafusion/pull/21477),
[#21482](https://github.com/apache/datafusion/pull/21482),
[#21532](https://github.com/apache/datafusion/pull/21532)) optimize NULL
handling in scalar functions by replacing row-by-row null buffer
construction with bulk `NullBuffer::union`. When 3+ null buffers need
combining, they chain binary `union` calls, each allocating a new
`BooleanBuffer`.
`NullBuffer::union_many` reduces this to 1 allocation (clone + in-place
ANDs). For example, from
[#21482](https://github.com/apache/datafusion/pull/21482):
Before:
```rust
[array.nulls(), from_array.nulls(), to_array.nulls(), stride.and_then(|s|
s.nulls())]
.into_iter()
.fold(None, |acc, nulls| NullBuffer::union(acc.as_ref(), nulls))
```
After:
```rust
NullBuffer::union_many([
array.nulls(),
from_array.nulls(),
to_array.nulls(),
stride.and_then(|s| s.nulls()),
])
```
Per @alamb's
[suggestion](https://github.com/apache/arrow-rs/pull/9692#issuecomment-2828576685),
this PR also implements the general-purpose mutable bitwise operations
on `BooleanArray` from #8809, following the `PrimitiveArray::unary` /
`unary_mut` pattern. This builds on the
`BitAndAssign`/`BitOrAssign`/`BitXorAssign` operators added to
`BooleanBuffer` in #9567.
## What changes are included in this PR?
**`NullBuffer::union_many(impl IntoIterator<Item =
Option<&NullBuffer>>)`**: combines multiple null buffers in a single
allocation (clone + in-place `&=`). Used by DataFusion for bulk null
handling.
**`BooleanArray` bitwise operations** (6 new public methods):
Unary (`op: FnMut(u64) -> u64`):
- `bitwise_unary(&self, op)` — always allocates a new array
- `bitwise_unary_mut(self, op) -> Result<Self, Self>` — in-place if
uniquely owned, `Err(self)` if shared
- `bitwise_unary_mut_or_clone(self, op)` — in-place if uniquely owned,
allocates if shared
Binary (`op: FnMut(u64, u64) -> u64`):
- `bitwise_bin_op(&self, rhs, op)` — always allocates, unions null
buffers
- `bitwise_bin_op_mut(self, rhs, op) -> Result<Self, Self>` — in-place
if uniquely owned, `Err(self)` if shared, unions null buffers
- `bitwise_bin_op_mut_or_clone(self, rhs, op)` — in-place if uniquely
owned, allocates if shared, unions null buffers
Note: #8809 proposed the binary variants take a raw buffer and
`right_offset_in_bits`. This PR takes `&BooleanArray` instead, which
encapsulates both and matches existing patterns like
`BooleanArray::from_binary`.
## Are these changes tested?
Yes. 23 tests for the `BooleanArray` bitwise methods and 6 tests for
`union_many`, covering:
- Basic correctness (AND, OR, NOT)
- Null handling (both nullable, one nullable, no nulls, null union)
- Buffer ownership (uniquely owned → in-place, shared → `Err` /
fallback)
- Edge cases (empty arrays, sliced arrays with non-zero offset,
misaligned left/right offsets)
## Are there any user-facing changes?
Six new public methods on `BooleanArray` and one new public method on
`NullBuffer`.
---------
Co-authored-by: Andrew Lamb <[email protected]>
---
arrow-array/src/array/boolean_array.rs | 568 +++++++++++++++++++++++++++++++++
arrow-buffer/src/buffer/null.rs | 63 ++++
2 files changed, 631 insertions(+)
diff --git a/arrow-array/src/array/boolean_array.rs
b/arrow-array/src/array/boolean_array.rs
index ee3413e183..22a1ba7653 100644
--- a/arrow-array/src/array/boolean_array.rs
+++ b/arrow-array/src/array/boolean_array.rs
@@ -364,6 +364,250 @@ impl BooleanArray {
Self::new(values, nulls)
}
+ /// Apply a bitwise operation to this array's values using u64 operations,
+ /// returning a new [`BooleanArray`].
+ ///
+ /// The null buffer is preserved unchanged.
+ ///
+ /// See [`BooleanBuffer::from_bitwise_unary_op`] for details on the
operation.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let array = BooleanArray::from(vec![true, false, true]);
+ /// let result = array.bitwise_unary(|x| !x);
+ /// assert_eq!(result, BooleanArray::from(vec![false, true, false]));
+ /// ```
+ pub fn bitwise_unary<F>(&self, op: F) -> BooleanArray
+ where
+ F: FnMut(u64) -> u64,
+ {
+ let values = BooleanBuffer::from_bitwise_unary_op(
+ self.values.values(),
+ self.values.offset(),
+ self.values.len(),
+ op,
+ );
+ BooleanArray::new(values, self.nulls.clone())
+ }
+
+ /// Try to apply a bitwise operation to this array's values in place using
+ /// u64 operations.
+ ///
+ /// If the underlying buffer is uniquely owned, the operation is applied
+ /// in place and `Ok` is returned. If the buffer is shared, `Err(self)` is
+ /// returned so the caller can fall back to
[`bitwise_unary`](Self::bitwise_unary).
+ ///
+ /// The null buffer is preserved unchanged.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let array = BooleanArray::from(vec![true, false, true]);
+ /// let result = array.bitwise_unary_mut(|x| !x).unwrap();
+ /// assert_eq!(result, BooleanArray::from(vec![false, true, false]));
+ /// ```
+ pub fn bitwise_unary_mut<F>(self, op: F) -> Result<BooleanArray,
BooleanArray>
+ where
+ F: FnMut(u64) -> u64,
+ {
+ self.try_bitwise_unary_in_place(op)
+ .map_err(|(array, _op)| array)
+ }
+
+ /// Apply a bitwise operation to this array's values in place if the buffer
+ /// is uniquely owned, or clone and apply if shared.
+ ///
+ /// This is a convenience wrapper around
[`bitwise_unary_mut`](Self::bitwise_unary_mut)
+ /// that falls back to [`bitwise_unary`](Self::bitwise_unary) when the
buffer is shared.
+ ///
+ /// The null buffer is preserved unchanged.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let array = BooleanArray::from(vec![true, false, true]);
+ /// let result = array.bitwise_unary_mut_or_clone(|x| !x);
+ /// assert_eq!(result, BooleanArray::from(vec![false, true, false]));
+ /// ```
+ pub fn bitwise_unary_mut_or_clone<F>(self, op: F) -> BooleanArray
+ where
+ F: FnMut(u64) -> u64,
+ {
+ match self.try_bitwise_unary_in_place(op) {
+ Ok(array) => array,
+ Err((array, op)) => array.bitwise_unary(op),
+ }
+ }
+
+ /// Try to apply a unary op in place. Returns `op` back on failure so
+ /// callers can fall back to an allocating path without requiring `F:
Clone`.
+ fn try_bitwise_unary_in_place<F>(self, op: F) -> Result<BooleanArray,
(BooleanArray, F)>
+ where
+ F: FnMut(u64) -> u64,
+ {
+ let (values, nulls) = self.into_parts();
+ let offset = values.offset();
+ let len = values.len();
+ let buffer = values.into_inner();
+ match buffer.into_mutable() {
+ Ok(mut buf) => {
+ bit_util::apply_bitwise_unary_op(buf.as_slice_mut(), offset,
len, op);
+ let values = BooleanBuffer::new(buf.into(), offset, len);
+ Ok(BooleanArray::new(values, nulls))
+ }
+ Err(buffer) => {
+ let values = BooleanBuffer::new(buffer, offset, len);
+ Err((BooleanArray::new(values, nulls), op))
+ }
+ }
+ }
+
+ /// Apply a bitwise binary operation to this array and `rhs` using u64
+ /// operations, returning a new [`BooleanArray`].
+ ///
+ /// Null buffers are unioned: the result is null where either input is
null.
+ ///
+ /// See [`BooleanBuffer::from_bitwise_binary_op`] for details on the
operation.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self` and `rhs` have different lengths.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let a = BooleanArray::from(vec![true, false, true, true]);
+ /// let b = BooleanArray::from(vec![true, true, false, true]);
+ /// let result = a.bitwise_bin_op(&b, |a, b| a & b);
+ /// assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ /// ```
+ pub fn bitwise_bin_op<F>(&self, rhs: &BooleanArray, op: F) -> BooleanArray
+ where
+ F: FnMut(u64, u64) -> u64,
+ {
+ assert_eq!(self.len(), rhs.len());
+ let nulls = NullBuffer::union(self.nulls(), rhs.nulls());
+ let values = BooleanBuffer::from_bitwise_binary_op(
+ self.values.values(),
+ self.values.offset(),
+ rhs.values.values(),
+ rhs.values.offset(),
+ self.values.len(),
+ op,
+ );
+ BooleanArray::new(values, nulls)
+ }
+
+ /// Try to apply a bitwise binary operation to this array and `rhs` in
+ /// place using u64 operations.
+ ///
+ /// If this array's underlying buffer is uniquely owned, the operation is
+ /// applied in place and `Ok` is returned. If the buffer is shared,
+ /// `Err(self)` is returned so the caller can fall back to
+ /// [`bitwise_bin_op`](Self::bitwise_bin_op).
+ ///
+ /// Null buffers are unioned: the result is null where either input is
null.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self` and `rhs` have different lengths.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let a = BooleanArray::from(vec![true, false, true, true]);
+ /// let b = BooleanArray::from(vec![true, true, false, true]);
+ /// let result = a.bitwise_bin_op_mut(&b, |a, b| a & b).unwrap();
+ /// assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ /// ```
+ pub fn bitwise_bin_op_mut<F>(
+ self,
+ rhs: &BooleanArray,
+ op: F,
+ ) -> Result<BooleanArray, BooleanArray>
+ where
+ F: FnMut(u64, u64) -> u64,
+ {
+ self.try_bitwise_bin_op_in_place(rhs, op)
+ .map_err(|(array, _op)| array)
+ }
+
+ /// Apply a bitwise binary operation to this array and `rhs` in place if
the
+ /// buffer is uniquely owned, or clone and apply if shared.
+ ///
+ /// This is a convenience wrapper around
[`bitwise_bin_op_mut`](Self::bitwise_bin_op_mut)
+ /// that falls back to [`bitwise_bin_op`](Self::bitwise_bin_op) when the
buffer is shared.
+ ///
+ /// Null buffers are unioned: the result is null where either input is
null.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self` and `rhs` have different lengths.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use arrow_array::BooleanArray;
+ /// let a = BooleanArray::from(vec![true, false, true, true]);
+ /// let b = BooleanArray::from(vec![true, true, false, true]);
+ /// let result = a.bitwise_bin_op_mut_or_clone(&b, |a, b| a & b);
+ /// assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ /// ```
+ pub fn bitwise_bin_op_mut_or_clone<F>(self, rhs: &BooleanArray, op: F) ->
BooleanArray
+ where
+ F: FnMut(u64, u64) -> u64,
+ {
+ match self.try_bitwise_bin_op_in_place(rhs, op) {
+ Ok(array) => array,
+ Err((array, op)) => array.bitwise_bin_op(rhs, op),
+ }
+ }
+
+ /// Try to apply a binary op in place. Returns `op` back on failure so
+ /// callers can fall back to an allocating path without requiring `F:
Clone`.
+ fn try_bitwise_bin_op_in_place<F>(
+ self,
+ rhs: &BooleanArray,
+ op: F,
+ ) -> Result<BooleanArray, (BooleanArray, F)>
+ where
+ F: FnMut(u64, u64) -> u64,
+ {
+ assert_eq!(self.len(), rhs.len());
+ let (values, nulls) = self.into_parts();
+ let offset = values.offset();
+ let len = values.len();
+ let buffer = values.into_inner();
+ match buffer.into_mutable() {
+ Ok(mut buf) => {
+ bit_util::apply_bitwise_binary_op(
+ buf.as_slice_mut(),
+ offset,
+ rhs.values.inner(),
+ rhs.values.offset(),
+ len,
+ op,
+ );
+ // Defer null union to the success path so the Err path returns
+ // self's original nulls, avoiding a redundant union in callers
+ // that fall back to bitwise_bin_op.
+ let nulls = NullBuffer::union(nulls.as_ref(), rhs.nulls());
+ let values = BooleanBuffer::new(buf.into(), offset, len);
+ Ok(BooleanArray::new(values, nulls))
+ }
+ Err(buffer) => {
+ let values = BooleanBuffer::new(buffer, offset, len);
+ Err((BooleanArray::new(values, nulls), op))
+ }
+ }
+ }
+
/// Deconstruct this array into its constituent parts
pub fn into_parts(self) -> (BooleanBuffer, Option<NullBuffer>) {
(self.values, self.nulls)
@@ -643,6 +887,41 @@ impl From<BooleanBuffer> for BooleanArray {
#[cfg(test)]
mod tests {
use super::*;
+
+ // Captures the values-buffer identity for a BooleanArray so tests can
assert
+ // whether an operation reused the original allocation or produced a new
one.
+ struct PointerInfo {
+ ptr: *const u8,
+ offset: usize,
+ len: usize,
+ }
+
+ impl PointerInfo {
+ // Record the current values buffer pointer plus bit offset/length. The
+ // offset/length checks ensure a logically equivalent slice wasn't
rebuilt
+ // with a different view over the same allocation.
+ fn new(array: &BooleanArray) -> Self {
+ Self {
+ ptr: array.values().inner().as_ptr(),
+ offset: array.values().offset(),
+ len: array.values().len(),
+ }
+ }
+
+ // Assert that the array still points at the exact same values buffer
and
+ // preserves the same bit view.
+ fn assert_same(&self, array: &BooleanArray) {
+ assert_eq!(array.values().inner().as_ptr(), self.ptr);
+ assert_eq!(array.values().offset(), self.offset);
+ assert_eq!(array.values().len(), self.len);
+ }
+
+ // Assert that the array now points at a different values allocation,
+ // indicating the operation fell back to an allocating path.
+ fn assert_different(&self, array: &BooleanArray) {
+ assert_ne!(array.values().inner().as_ptr(), self.ptr);
+ }
+ }
use arrow_buffer::Buffer;
use rand::{Rng, rng};
@@ -1062,4 +1341,293 @@ mod tests {
assert_eq!(arr.has_false(), expected_has_false, "len={len}");
}
}
+
+ #[test]
+ fn test_bitwise_unary_not() {
+ let arr = BooleanArray::from(vec![true, false, true, false]);
+ let result = arr.bitwise_unary(|x| !x);
+ let expected = BooleanArray::from(vec![false, true, false, true]);
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_bitwise_unary_preserves_nulls() {
+ let arr = BooleanArray::from(vec![Some(true), None, Some(false),
Some(true)]);
+ let result = arr.bitwise_unary(|x| !x);
+
+ assert_eq!(result.null_count(), 1);
+ assert!(result.is_null(1));
+ assert!(!result.value(0));
+ assert!(result.value(2));
+ assert!(!result.value(3));
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_unshared() {
+ let arr = BooleanArray::from(vec![true, false, true, false]);
+ let info = PointerInfo::new(&arr);
+ let result = arr.bitwise_unary_mut(|x| !x).unwrap();
+ let expected = BooleanArray::from(vec![false, true, false, true]);
+ assert_eq!(result, expected);
+ info.assert_same(&result);
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_shared() {
+ let arr = BooleanArray::from(vec![true, false, true, false]);
+ let info = PointerInfo::new(&arr);
+ let _shared = arr.clone();
+ let result = arr.bitwise_unary_mut(|x| !x);
+ assert!(result.is_err());
+
+ let returned = result.unwrap_err();
+ assert_eq!(returned, BooleanArray::from(vec![true, false, true,
false]));
+ info.assert_same(&returned);
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_with_nulls() {
+ let arr = BooleanArray::from(vec![Some(true), None, Some(false)]);
+ let result = arr.bitwise_unary_mut(|x| !x).unwrap();
+
+ assert_eq!(result.null_count(), 1);
+ assert!(result.is_null(1));
+ assert!(!result.value(0));
+ assert!(result.value(2));
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_or_clone_shared() {
+ let arr = BooleanArray::from(vec![true, false, true]);
+ let info = PointerInfo::new(&arr);
+ let _shared = arr.clone();
+ let result = arr.bitwise_unary_mut_or_clone(|x| !x);
+ assert_eq!(result, BooleanArray::from(vec![false, true, false]));
+ info.assert_different(&result);
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_or_clone_unshared() {
+ // Covers the uniquely-owned fast path in bitwise_unary_mut_or_clone.
+ let arr = BooleanArray::from(vec![true, false, true]);
+ let info = PointerInfo::new(&arr);
+ let result = arr.bitwise_unary_mut_or_clone(|x| !x);
+ assert_eq!(result, BooleanArray::from(vec![false, true, false]));
+ info.assert_same(&result);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_and() {
+ let a = BooleanArray::from(vec![true, false, true, true]);
+ let b = BooleanArray::from(vec![true, true, false, true]);
+ let result = a.bitwise_bin_op(&b, |a, b| a & b);
+ assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_or() {
+ let a = BooleanArray::from(vec![true, false, true, false]);
+ let b = BooleanArray::from(vec![false, true, false, false]);
+ let result = a.bitwise_bin_op(&b, |a, b| a | b);
+ assert_eq!(result, BooleanArray::from(vec![true, true, true, false]));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_null_union() {
+ let a = BooleanArray::from(vec![Some(true), None, Some(true),
Some(false)]);
+ let b = BooleanArray::from(vec![Some(true), Some(true), None,
Some(true)]);
+ let result = a.bitwise_bin_op(&b, |a, b| a & b);
+
+ assert_eq!(result.null_count(), 2);
+ assert!(result.is_null(1));
+ assert!(result.is_null(2));
+ assert!(result.value(0));
+ assert!(!result.value(3));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_one_nullable() {
+ let a = BooleanArray::from(vec![Some(true), None, Some(true)]);
+ let b = BooleanArray::from(vec![false, true, true]);
+ let result = a.bitwise_bin_op(&b, |a, b| a & b);
+
+ assert_eq!(result.null_count(), 1);
+ assert!(result.is_null(1));
+ assert!(!result.value(0));
+ assert!(result.value(2));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_no_nulls() {
+ let a = BooleanArray::from(vec![true, false, true]);
+ let b = BooleanArray::from(vec![false, true, true]);
+ let result = a.bitwise_bin_op(&b, |a, b| a | b);
+
+ assert!(result.nulls().is_none());
+ assert_eq!(result, BooleanArray::from(vec![true, true, true]));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_unshared() {
+ let a = BooleanArray::from(vec![true, false, true, true]);
+ let info = PointerInfo::new(&a);
+ let b = BooleanArray::from(vec![true, true, false, true]);
+ let result = a.bitwise_bin_op_mut(&b, |a, b| a & b).unwrap();
+ assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ info.assert_same(&result);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_shared() {
+ let a = BooleanArray::from(vec![true, false, true, true]);
+ let info = PointerInfo::new(&a);
+ let _shared = a.clone();
+ let result = a.bitwise_bin_op_mut(
+ &BooleanArray::from(vec![true, true, false, true]),
+ |a, b| a & b,
+ );
+ assert!(result.is_err());
+ let returned = result.unwrap_err();
+ info.assert_same(&returned);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_with_nulls() {
+ let a = BooleanArray::from(vec![Some(true), None, Some(true),
Some(false)]);
+ let b = BooleanArray::from(vec![Some(true), Some(true), None,
Some(true)]);
+ let result = a.bitwise_bin_op_mut(&b, |a, b| a & b).unwrap();
+
+ assert_eq!(result.null_count(), 2);
+ assert!(result.is_null(1));
+ assert!(result.is_null(2));
+ assert!(result.value(0));
+ assert!(!result.value(3));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_or_clone_shared() {
+ let a = BooleanArray::from(vec![true, false, true, true]);
+ let info = PointerInfo::new(&a);
+ let _shared = a.clone();
+ let b = BooleanArray::from(vec![true, true, false, true]);
+ let result = a.bitwise_bin_op_mut_or_clone(&b, |a, b| a & b);
+ assert_eq!(result, BooleanArray::from(vec![true, false, false, true]));
+ info.assert_different(&result);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_or_clone_shared_with_nulls() {
+ // When the buffer is shared, _mut_or_clone falls back to
bitwise_bin_op.
+ // The null union must only be applied once, not double-applied.
+ let a = BooleanArray::from(vec![Some(true), None, Some(true),
Some(false)]);
+ let info = PointerInfo::new(&a);
+ let _shared = a.clone();
+ let b = BooleanArray::from(vec![Some(true), Some(true), None,
Some(true)]);
+
+ let expected = a.bitwise_bin_op(&b, |a, b| a & b);
+ let result = a.bitwise_bin_op_mut_or_clone(&b, |a, b| a & b);
+
+ assert_eq!(result, expected);
+ assert_eq!(result.null_count(), 2);
+ assert!(result.is_null(1));
+ assert!(result.is_null(2));
+ info.assert_different(&result);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_or_clone_unshared_with_nulls() {
+ // Covers the uniquely-owned fast path in bitwise_bin_op_mut_or_clone,
+ // including null union on the in-place path.
+ let a = BooleanArray::from(vec![Some(true), None, Some(true),
Some(false)]);
+ let info = PointerInfo::new(&a);
+ let b = BooleanArray::from(vec![Some(true), Some(true), None,
Some(true)]);
+ let result = a.bitwise_bin_op_mut_or_clone(&b, |a, b| a & b);
+
+ assert_eq!(result.null_count(), 2);
+ assert!(result.is_null(1));
+ assert!(result.is_null(2));
+ assert!(result.value(0));
+ assert!(!result.value(3));
+ info.assert_same(&result);
+ }
+
+ #[test]
+ fn test_bitwise_unary_empty() {
+ let arr = BooleanArray::from(Vec::<bool>::new());
+ let result = arr.bitwise_unary(|x| !x);
+ assert_eq!(result.len(), 0);
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_empty() {
+ let a = BooleanArray::from(Vec::<bool>::new());
+ let b = BooleanArray::from(Vec::<bool>::new());
+ let result = a.bitwise_bin_op(&b, |a, b| a & b);
+ assert_eq!(result.len(), 0);
+ }
+
+ #[test]
+ fn test_bitwise_unary_sliced() {
+ // Slicing creates a non-zero offset into the underlying buffer.
+ let arr = BooleanArray::from(vec![true, false, true, true, false]);
+ let sliced = arr.slice(1, 3); // [false, true, true]
+
+ let result = sliced.bitwise_unary(|x| !x);
+ assert_eq!(result.len(), 3);
+ assert!(result.value(0));
+ assert!(!result.value(1));
+ assert!(!result.value(2));
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_sliced() {
+ // Slicing shares the buffer, so _mut must return Err.
+ let arr = BooleanArray::from(vec![true, false, true, true, false]);
+ let sliced = arr.slice(1, 3);
+ assert!(sliced.bitwise_unary_mut(|x| !x).is_err());
+ }
+
+ #[test]
+ fn test_bitwise_unary_mut_or_clone_sliced() {
+ // Slicing shares the buffer, so _mut_or_clone falls back to
allocating.
+ let arr = BooleanArray::from(vec![true, false, true, true, false]);
+ let sliced = arr.slice(1, 3); // [false, true, true]
+
+ let result = sliced.bitwise_unary_mut_or_clone(|x| !x);
+ assert_eq!(result.len(), 3);
+ assert!(result.value(0));
+ assert!(!result.value(1));
+ assert!(!result.value(2));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_different_offsets() {
+ // Left and right sliced to different offsets exercises misaligned
+ // bit handling in from_bitwise_binary_op.
+ let left_full = BooleanArray::from(vec![false, true, false, true,
true]);
+ let right_full = BooleanArray::from(vec![true, true, true, false,
true, false]);
+
+ let left = left_full.slice(1, 3); // [true, false, true]
+ let right = right_full.slice(2, 3); // [true, false, true]
+
+ let result = left.bitwise_bin_op(&right, |a, b| a & b);
+ assert_eq!(result.len(), 3);
+ assert!(result.value(0));
+ assert!(!result.value(1));
+ assert!(result.value(2));
+ }
+
+ #[test]
+ fn test_bitwise_bin_op_mut_or_clone_different_offsets() {
+ // Both sliced (shared buffers), so falls back to allocating path.
+ let left_full = BooleanArray::from(vec![false, true, true, false,
true]);
+ let right_full = BooleanArray::from(vec![true, true, false, false,
true, false]);
+
+ let left = left_full.slice(1, 3); // [true, true, false]
+ let right = right_full.slice(2, 3); // [false, false, true]
+
+ let expected = left.bitwise_bin_op(&right, |a, b| a & b);
+ let result = left.bitwise_bin_op_mut_or_clone(&right, |a, b| a & b);
+ assert_eq!(result, expected);
+ }
}
diff --git a/arrow-buffer/src/buffer/null.rs b/arrow-buffer/src/buffer/null.rs
index 6046369c62..729beaa061 100644
--- a/arrow-buffer/src/buffer/null.rs
+++ b/arrow-buffer/src/buffer/null.rs
@@ -84,6 +84,22 @@ impl NullBuffer {
}
}
+ /// Computes the union of the nulls in multiple optional [`NullBuffer`]s
+ ///
+ /// See [`union`](Self::union)
+ pub fn union_many<'a>(
+ nulls: impl IntoIterator<Item = Option<&'a NullBuffer>>,
+ ) -> Option<NullBuffer> {
+ // Unwrap to BooleanBuffer because BitAndAssign is not implemented for
NullBuffer
+ let mut buffers = nulls.into_iter().filter_map(|nb|
nb.map(NullBuffer::inner));
+ let first = buffers.next()?;
+ let mut result = first.clone();
+ for buf in buffers {
+ result &= buf;
+ }
+ Some(Self::new(result))
+ }
+
/// Returns true if all nulls in `other` also exist in self
pub fn contains(&self, other: &NullBuffer) -> bool {
if other.null_count == 0 {
@@ -336,4 +352,51 @@ mod tests {
let result = NullBuffer::from_unsliced_buffer(buf, 0);
assert!(result.is_none());
}
+
+ #[test]
+ fn test_union_many_all_none() {
+ let result = NullBuffer::union_many([None, None, None]);
+ assert!(result.is_none());
+ }
+
+ #[test]
+ fn test_union_many_single_some() {
+ let a = NullBuffer::from(&[true, false, true, true]);
+ let result = NullBuffer::union_many([Some(&a)]);
+ assert_eq!(result, Some(a));
+ }
+
+ #[test]
+ fn test_union_many_two_inputs() {
+ let a = NullBuffer::from(&[true, false, true, true]);
+ let b = NullBuffer::from(&[true, true, false, true]);
+ let result = NullBuffer::union_many([Some(&a), Some(&b)]);
+ let expected = NullBuffer::union(Some(&a), Some(&b));
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_union_many_three_inputs() {
+ let a = NullBuffer::from(&[true, false, true, true]);
+ let b = NullBuffer::from(&[true, true, false, true]);
+ let c = NullBuffer::from(&[false, true, true, true]);
+ let result = NullBuffer::union_many([Some(&a), Some(&b), Some(&c)]);
+ let expected = NullBuffer::from(&[false, false, false, true]);
+ assert_eq!(result, Some(expected));
+ }
+
+ #[test]
+ fn test_union_many_mixed_none() {
+ let a = NullBuffer::from(&[true, false, true, true]);
+ let b = NullBuffer::from(&[false, true, true, true]);
+ let result = NullBuffer::union_many([Some(&a), None, Some(&b)]);
+ let expected = NullBuffer::union(Some(&a), Some(&b));
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_union_many_empty_slice() {
+ let result = NullBuffer::union_many([] as [Option<&NullBuffer>; 0]);
+ assert!(result.is_none());
+ }
}