This is an automated email from the ASF dual-hosted git repository.

scovich 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 d8d3fa32c1 refactor: add `take_n_true` method to `BooleanArray` (#9823)
d8d3fa32c1 is described below

commit d8d3fa32c107ecae6c77c6f1db7700e2c7eb60ef
Author: Huaijin <[email protected]>
AuthorDate: Wed Jul 1 22:33:23 2026 +0800

    refactor: add `take_n_true` method to `BooleanArray` (#9823)
    
    # Which issue does this PR close?
    
    
    - Closes #9803
    
    # Rationale for this change
    
    - see #9803
    
    # What changes are included in this PR?
    
    move `truncate_filter_after_n_trues` in read_plan.rs to BooleanArray
    `take_n_true` method
    
    # Are these changes tested?
    
    yes, already have test case
    
    # Are there any user-facing changes?
---
 arrow-array/src/array/boolean_array.rs      | 109 ++++++++++++++++++++++++-
 parquet/src/arrow/arrow_reader/read_plan.rs | 122 +++++++++++++---------------
 2 files changed, 165 insertions(+), 66 deletions(-)

diff --git a/arrow-array/src/array/boolean_array.rs 
b/arrow-array/src/array/boolean_array.rs
index 0fa5b5d912..9245f5b337 100644
--- a/arrow-array/src/array/boolean_array.rs
+++ b/arrow-array/src/array/boolean_array.rs
@@ -16,7 +16,7 @@
 // under the License.
 
 use crate::array::print_long_array;
-use crate::builder::BooleanBuilder;
+use crate::builder::{BooleanBufferBuilder, BooleanBuilder};
 use crate::iterator::BooleanIter;
 use crate::{Array, ArrayAccessor, ArrayRef, Scalar};
 use arrow_buffer::{BooleanBuffer, Buffer, MutableBuffer, NullBuffer, bit_util};
@@ -560,6 +560,45 @@ impl BooleanArray {
         }
     }
 
+    /// Returns a new [`BooleanArray`] of the same length where only the first
+    /// `n` non-null `true` positions remain `true`; any `true` positions
+    /// beyond the first `n` are replaced with `false`. The null buffer is
+    /// preserved unchanged.
+    ///
+    /// If this array has at most `n` non-null `true` values, `self` is
+    /// returned unchanged.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # use arrow_array::BooleanArray;
+    /// let a = BooleanArray::from(vec![true, false, true, true, false, true]);
+    /// // Keep only the first 2 `true` positions; later trues become false.
+    /// let r = a.take_n_true(2);
+    /// assert_eq!(r, BooleanArray::from(vec![true, false, true, false, false, 
false]));
+    /// ```
+    pub fn take_n_true(self, n: usize) -> BooleanArray {
+        let len = self.len();
+        // `set_indices` scans 64 bits at a time via `trailing_zeros`, so 
locating
+        // the first set bit beyond the retained prefix is cheaper than 
visiting
+        // every bit. When a null buffer is present, skip set bits whose
+        // corresponding entry is null so only non-null trues count toward `n`
+        // (matching `true_count` semantics).
+        let mut iter = self.values.set_indices();
+        let end = match self.nulls.as_ref() {
+            Some(nulls) => iter.filter(|&i| nulls.is_valid(i)).nth(n),
+            None => iter.nth(n),
+        };
+        let Some(end) = end else {
+            return self;
+        };
+
+        let mut builder = BooleanBufferBuilder::new(len);
+        builder.append_buffer(&self.values.slice(0, end));
+        builder.append_n(len - end, false);
+        BooleanArray::new(builder.finish(), self.nulls)
+    }
+
     /// Deconstruct this array into its constituent parts
     pub fn into_parts(self) -> (BooleanBuffer, Option<NullBuffer>) {
         (self.values, self.nulls)
@@ -1582,4 +1621,72 @@ mod tests {
         let result = left.bitwise_bin_op_mut_or_clone(&right, |a, b| a & b);
         assert_eq!(result, expected);
     }
+
+    #[test]
+    fn test_take_n_true_keeps_first_n_matches() {
+        let a = BooleanArray::from(vec![true, false, true, true, false, true, 
true]);
+        // true positions: 0, 2, 3, 5, 6
+        let r = a.clone().take_n_true(3);
+        assert_eq!(r.len(), a.len());
+        assert_eq!(r.true_count(), 3);
+        let out: Vec<bool> = (0..r.len()).map(|i| r.value(i)).collect();
+        assert_eq!(
+            out,
+            vec![true, false, true, true, false, false, false],
+            "first three trues should survive, the rest become false"
+        );
+    }
+
+    #[test]
+    fn test_take_n_true_passes_through_when_already_small_enough() {
+        let a = BooleanArray::from(vec![true, false, true, false]);
+        let r = a.clone().take_n_true(5);
+        assert_eq!(r.len(), a.len());
+        assert_eq!(r.true_count(), 2);
+        assert_eq!(r, a);
+    }
+
+    #[test]
+    fn test_take_n_true_zero_returns_all_false() {
+        let a = BooleanArray::from(vec![true, true, true]);
+        let r = a.take_n_true(0);
+        assert_eq!(r.len(), 3);
+        assert_eq!(r.true_count(), 0);
+    }
+
+    #[test]
+    fn test_take_n_true_preserves_nulls_and_skips_them() {
+        // Non-null trues: positions 0, 3, 5. Null at 2 must not count toward 
`n`.
+        let a = BooleanArray::from(vec![
+            Some(true),
+            Some(false),
+            None,
+            Some(true),
+            Some(false),
+            Some(true),
+        ]);
+        assert_eq!(a.true_count(), 3);
+        let len = a.len();
+
+        let r = a.take_n_true(2);
+        assert_eq!(r.len(), len);
+        assert_eq!(r.true_count(), 2);
+        // Null buffer is preserved unchanged.
+        assert_eq!(r.null_count(), 1);
+        assert!(r.is_null(2));
+        // First two non-null trues kept; the third (position 5) becomes false.
+        assert!(r.value(0));
+        assert!(!r.value(1));
+        assert!(r.value(3));
+        assert!(!r.value(4));
+        assert!(!r.value(5));
+    }
+
+    #[test]
+    fn test_take_n_true_empty_array() {
+        let a = BooleanArray::from(Vec::<bool>::new());
+        let r = a.take_n_true(5);
+        assert_eq!(r.len(), 0);
+        assert_eq!(r.true_count(), 0);
+    }
 }
diff --git a/parquet/src/arrow/arrow_reader/read_plan.rs 
b/parquet/src/arrow/arrow_reader/read_plan.rs
index 674ae2b8d9..71b64802b0 100644
--- a/parquet/src/arrow/arrow_reader/read_plan.rs
+++ b/parquet/src/arrow/arrow_reader/read_plan.rs
@@ -26,7 +26,7 @@ use crate::arrow::arrow_reader::{
 };
 use crate::errors::{ParquetError, Result};
 use arrow_array::{Array, BooleanArray};
-use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder};
+use arrow_buffer::BooleanBuffer;
 use arrow_select::filter::prep_null_mask_filter;
 use std::collections::VecDeque;
 
@@ -240,17 +240,21 @@ impl ReadPlanBuilder {
             }
             let filter = match filter.null_count() {
                 0 => filter,
+                // RowSelection::from_filters expects non-null filters. Convert
+                // NULL predicate results to false so they are not selected.
                 _ => prep_null_mask_filter(&filter),
             };
 
             processed_rows += input_rows;
 
             match limit {
-                Some(limit) if matched_rows + filter.true_count() >= limit => {
-                    let needed = limit - matched_rows;
-                    let truncated = truncate_filter_after_n_trues(filter, 
needed);
+                Some(limit) if limit - matched_rows <= filter.len() => {
+                    let truncated = filter.take_n_true(limit - matched_rows);
+                    matched_rows += truncated.true_count();
                     filters.push(truncated);
-                    break;
+                    if matched_rows >= limit {
+                        break;
+                    }
                 }
                 _ => {
                     matched_rows += filter.true_count();
@@ -409,35 +413,6 @@ impl LimitedReadPlanBuilder {
     }
 }
 
-/// Produce a new `BooleanArray` of the same length as `filter` in which only
-/// the first `n` `true` positions from `filter` remain `true`; any `true`
-/// positions beyond the first `n` are replaced with `false`.
-///
-/// `filter` must not contain nulls (callers apply [`prep_null_mask_filter`]
-/// first). If `filter` has at most `n` `true` values, a clone is returned.
-fn truncate_filter_after_n_trues(filter: BooleanArray, n: usize) -> 
BooleanArray {
-    if filter.true_count() <= n {
-        return filter;
-    }
-    let len = filter.len();
-    if n == 0 {
-        return BooleanArray::new(BooleanBuffer::new_unset(len), None);
-    }
-    // `set_indices` scans 64 bits at a time via `trailing_zeros`, so locating
-    // the `n`-th set bit is cheaper than visiting every bit. Everything up to
-    // and including that position is copied verbatim; the rest is zeroed.
-    let values = filter.values();
-    let last_kept = values
-        .set_indices()
-        .nth(n - 1)
-        .expect("n - 1 < true_count, checked above");
-
-    let mut builder = BooleanBufferBuilder::new(len);
-    builder.append_buffer(&values.slice(0, last_kept + 1));
-    builder.append_n(len - last_kept - 1, false);
-    BooleanArray::new(builder.finish(), None)
-}
-
 /// A plan reading specific rows from a Parquet Row Group.
 ///
 /// See [`ReadPlanBuilder`] to create `ReadPlan`s
@@ -501,37 +476,6 @@ mod tests {
         );
     }
 
-    #[test]
-    fn truncate_filter_after_n_trues_keeps_first_n_matches() {
-        let f = BooleanArray::from(vec![true, false, true, true, false, true, 
true]);
-        // true positions: 0, 2, 3, 5, 6
-        let t = truncate_filter_after_n_trues(f.clone(), 3);
-        assert_eq!(t.len(), f.len());
-        assert_eq!(t.true_count(), 3);
-        let out: Vec<bool> = (0..t.len()).map(|i| t.value(i)).collect();
-        assert_eq!(
-            out,
-            vec![true, false, true, true, false, false, false],
-            "first three trues should survive, the rest become false"
-        );
-    }
-
-    #[test]
-    fn 
truncate_filter_after_n_trues_passes_through_when_already_small_enough() {
-        let f = BooleanArray::from(vec![true, false, true, false]);
-        let t = truncate_filter_after_n_trues(f.clone(), 5);
-        assert_eq!(t.len(), f.len());
-        assert_eq!(t.true_count(), 2);
-    }
-
-    #[test]
-    fn truncate_filter_after_n_trues_zero_returns_all_false() {
-        let f = BooleanArray::from(vec![true, true, true]);
-        let t = truncate_filter_after_n_trues(f, 0);
-        assert_eq!(t.len(), 3);
-        assert_eq!(t.true_count(), 0);
-    }
-
     #[test]
     fn with_predicate_options_limit_pads_tail_when_no_prior_selection() {
         use crate::arrow::ProjectionMask;
@@ -583,4 +527,52 @@ mod tests {
             "selection must span the full row group, not only the prefix 
evaluated before the limit"
         );
     }
+
+    #[test]
+    fn with_predicate_options_limit_handles_null_filters() {
+        use crate::arrow::ProjectionMask;
+        use crate::arrow::array_reader::StructArrayReader;
+        use crate::arrow::array_reader::test_util::make_int32_page_reader;
+        use crate::arrow::arrow_reader::ArrowPredicateFn;
+        use arrow_schema::{DataType as ArrowType, Field, Fields};
+
+        const TOTAL_ROWS: usize = 100;
+        const LIMIT: usize = 10;
+
+        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
+        let levels = vec![0; TOTAL_ROWS];
+        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0);
+        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
+            "c0",
+            ArrowType::Int32,
+            false,
+        )]));
+        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 
0, false);
+
+        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), 
|batch| {
+            Ok((0..batch.num_rows())
+                .map(|i| match i % 4 {
+                    0 | 2 => Some(true),
+                    1 => None,
+                    _ => Some(false),
+                })
+                .collect::<BooleanArray>())
+        });
+
+        let builder = ReadPlanBuilder::new(16)
+            .with_predicate_options(
+                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
+                    .with_limit(LIMIT, TOTAL_ROWS),
+            )
+            .unwrap();
+
+        let selection = builder
+            .selection()
+            .expect("limit-driven early break must produce a selection");
+
+        assert_eq!(selection.row_count(), LIMIT);
+
+        let total: usize = selection.iter().map(|s| s.row_count).sum();
+        assert_eq!(total, TOTAL_ROWS);
+    }
 }

Reply via email to