Skip to main content

parquet/arrow/arrow_reader/
filter.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::arrow::ProjectionMask;
19use crate::arrow::arrow_reader::{RowSelection, RowSelectionPolicy};
20use crate::schema::types::SchemaDescriptor;
21use arrow_array::{Array, BooleanArray, RecordBatch};
22use arrow_buffer::BooleanBuffer;
23use arrow_schema::ArrowError;
24use arrow_select::filter::{SlicesIterator, filter_record_batch, prep_null_mask_filter};
25use std::fmt::{Debug, Formatter};
26
27/// A predicate operating on [`RecordBatch`]
28///
29/// See also:
30/// * [`RowFilter`] for more information  on applying filters during the
31///   Parquet decoding process.
32/// * [`ArrowPredicateFn`] for a concrete implementation based on a function
33pub trait ArrowPredicate: Send + 'static {
34    /// Returns the [`ProjectionMask`] that describes the columns required
35    /// to evaluate this predicate.
36    ///
37    /// All projected columns will be provided in the `batch` passed to
38    /// [`evaluate`](Self::evaluate). The projection mask should be as small as
39    /// possible because any columns needed for the overall projection mask are
40    /// decoded again after a predicate is applied.
41    fn projection(&self) -> &ProjectionMask;
42
43    /// Evaluate this predicate for the given [`RecordBatch`] containing the columns
44    /// identified by [`Self::projection`]
45    ///
46    /// Must return a [`BooleanArray`] that has the same length as the input
47    /// `batch` where each row indicates whether the row should be returned:
48    /// * `true`:the row should be returned
49    /// * `false` or `null`: the row should not be returned
50    fn evaluate(&mut self, batch: RecordBatch) -> Result<BooleanArray, ArrowError>;
51}
52
53/// An [`ArrowPredicate`] created from an [`FnMut`] and a [`ProjectionMask`]
54///
55/// See [`RowFilter`] for more information on applying filters during the
56/// Parquet decoding process.
57///
58/// The function is passed `RecordBatch`es with only the columns specified in
59/// the [`ProjectionMask`].
60///
61/// The function must return a [`BooleanArray`] that has the same length as the
62/// input `batch` where each row indicates whether the row should be returned:
63/// * `true`: the row should be returned
64/// * `false` or `null`: the row should not be returned
65///
66/// # Example:
67///
68/// Given an input schema: `"a:int64", "b:int64"`, you can create a predicate that
69/// evaluates `b > 0` like this:
70///
71/// ```
72/// # use std::sync::Arc;
73/// # use arrow::compute::kernels::cmp::gt;
74/// # use arrow_array::{BooleanArray, Int64Array, RecordBatch};
75/// # use arrow_array::cast::AsArray;
76/// # use arrow_array::types::Int64Type;
77/// # use parquet::arrow::arrow_reader::ArrowPredicateFn;
78/// # use parquet::arrow::ProjectionMask;
79/// # use parquet::schema::types::{SchemaDescriptor, Type};
80/// # use parquet::basic; // note there are two `Type`s that are different
81/// # // Schema for a table with one columns: "a" (int64) and "b" (int64)
82/// # let descriptor = SchemaDescriptor::new(
83/// #  Arc::new(
84/// #    Type::group_type_builder("my_schema")
85/// #      .with_fields(vec![
86/// #        Arc::new(
87/// #         Type::primitive_type_builder("a", basic::Type::INT64)
88/// #          .build().unwrap()
89/// #        ),
90/// #        Arc::new(
91/// #         Type::primitive_type_builder("b", basic::Type::INT64)
92/// #          .build().unwrap()
93/// #        ),
94/// #     ])
95/// #     .build().unwrap()
96/// #  )
97/// # );
98/// // Create a mask for selecting only the second column "b" (index 1)
99/// let projection_mask = ProjectionMask::leaves(&descriptor, [1]);
100/// // Closure that evaluates "b > 0"
101/// let predicate = |batch: RecordBatch| {
102///    let scalar_0 = Int64Array::new_scalar(0);
103///    let column = batch.column(0).as_primitive::<Int64Type>();
104///    // call the gt kernel to compute `>` which returns a BooleanArray
105///    gt(column, &scalar_0)
106///  };
107/// // Create ArrowPredicateFn that can be passed to RowFilter
108/// let arrow_predicate = ArrowPredicateFn::new(projection_mask, predicate);
109/// ```
110pub struct ArrowPredicateFn<F> {
111    f: F,
112    projection: ProjectionMask,
113}
114
115impl<F> ArrowPredicateFn<F>
116where
117    F: FnMut(RecordBatch) -> Result<BooleanArray, ArrowError> + Send + 'static,
118{
119    /// Create a new [`ArrowPredicateFn`] that invokes `f` on the columns
120    /// specified in `projection`.
121    pub fn new(projection: ProjectionMask, f: F) -> Self {
122        Self { f, projection }
123    }
124}
125
126impl<F> ArrowPredicate for ArrowPredicateFn<F>
127where
128    F: FnMut(RecordBatch) -> Result<BooleanArray, ArrowError> + Send + 'static,
129{
130    fn projection(&self) -> &ProjectionMask {
131        &self.projection
132    }
133
134    fn evaluate(&mut self, batch: RecordBatch) -> Result<BooleanArray, ArrowError> {
135        (self.f)(batch)
136    }
137}
138
139/// Filter applied *during* the parquet read process
140///
141/// See example on [`ArrowReaderBuilder::with_row_filter`]
142///
143/// [`RowFilter`] applies predicates in order, after decoding only the columns
144/// required. As predicates eliminate rows, fewer rows from subsequent columns
145/// may be required, thus potentially reducing IO and decode. This process is
146/// also known as *push down* filtering and  *late materialization*.
147///
148/// A `RowFilter` consists of a list of [`ArrowPredicate`]s. Only the rows for which
149/// all the predicates evaluate to `true` will be returned.
150/// Any [`RowSelection`] provided to the reader will be applied prior
151/// to the first predicate, and each predicate in turn will then be used to compute
152/// a more refined [`RowSelection`] used when evaluating the subsequent predicates.
153///
154/// Once all predicates have been evaluated, the final [`RowSelection`] is applied
155/// to the top-level [`ProjectionMask`] to produce the final output [`RecordBatch`].
156///
157/// This design has a couple of implications:
158///
159/// * [`RowFilter`] can be used to skip entire pages, and thus IO, in addition to CPU decode overheads
160/// * Columns may be decoded multiple times if they appear in multiple [`ProjectionMask`].
161///   Consecutive predicates whose projection is the same single top-level, non-repeated
162///   column are evaluated together on one decoded stream, so ordering such predicates
163///   next to each other avoids decoding that column again
164/// * IO will be deferred until needed by a [`ProjectionMask`]
165///
166/// As such there is a trade-off between a single large predicate, or multiple predicates,
167/// that will depend on the shape of the data. Whilst multiple smaller predicates may
168/// minimise the amount of data scanned/decoded, it may not be faster overall.
169///
170/// For example, if a predicate that needs a single column of data filters out all but
171/// 1% of the rows, applying it as one of the early `ArrowPredicateFn` will likely significantly
172/// improve performance.
173///
174/// As a counter example, if a predicate needs several columns of data to evaluate but
175/// leaves 99% of the rows, it may be better to not filter the data from parquet and
176/// apply the filter after the RecordBatch has been fully decoded.
177///
178/// Additionally, even if a predicate eliminates a moderate number of rows, it may still be faster
179/// to filter the data after the RecordBatch has been fully decoded, if the eliminated rows are
180/// not contiguous.
181///
182/// [`RowSelection`]: crate::arrow::arrow_reader::RowSelection
183/// [`ArrowReaderBuilder::with_row_filter`]: crate::arrow::arrow_reader::ArrowReaderBuilder::with_row_filter
184pub struct RowFilter {
185    /// A list of [`ArrowPredicate`]
186    pub(crate) predicates: Vec<Box<dyn ArrowPredicate>>,
187}
188
189impl Debug for RowFilter {
190    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
191        write!(f, "RowFilter {{ {} predicates: }}", self.predicates.len())
192    }
193}
194
195impl RowFilter {
196    /// Create a new [`RowFilter`] from an array of [`ArrowPredicate`]
197    pub fn new(predicates: Vec<Box<dyn ArrowPredicate>>) -> Self {
198        Self { predicates }
199    }
200    /// Returns the inner predicates
201    pub fn predicates(&self) -> &Vec<Box<dyn ArrowPredicate>> {
202        &self.predicates
203    }
204    /// Returns the inner predicates, consuming self
205    pub fn into_predicates(self) -> Vec<Box<dyn ArrowPredicate>> {
206        self.predicates
207    }
208
209    /// Fuse consecutive predicates on the same single top-level, non-repeated leaf.
210    /// This avoids repeated decoding or predicate-cache replay of that column.
211    pub(crate) fn fuse_same_projection(self, parquet_schema: &SchemaDescriptor) -> Self {
212        let mut predicates: Vec<Box<dyn ArrowPredicate>> =
213            Vec::with_capacity(self.predicates.len());
214        let mut group: Vec<Box<dyn ArrowPredicate>> = Vec::new();
215        let mut flush_group = |group: &mut Vec<Box<dyn ArrowPredicate>>| {
216            if group.len() > 1 && can_fuse_projection(group[0].projection(), parquet_schema) {
217                let group = std::mem::take(group);
218                predicates.push(Box::new(FusedPredicate::new(group)));
219            } else {
220                predicates.append(group);
221            }
222        };
223
224        for predicate in self.predicates {
225            if group
226                .last()
227                .is_some_and(|last| last.projection() != predicate.projection())
228            {
229                flush_group(&mut group);
230            }
231            group.push(predicate);
232        }
233        flush_group(&mut group);
234
235        Self { predicates }
236    }
237}
238
239/// Restrict fusion to one top-level, non-repeated leaf to limit compaction costs.
240fn can_fuse_projection(projection: &ProjectionMask, parquet_schema: &SchemaDescriptor) -> bool {
241    let mut leaf_indices =
242        (0..parquet_schema.num_columns()).filter(|idx| projection.leaf_included(*idx));
243    let Some(leaf_idx) = leaf_indices.next() else {
244        return false;
245    };
246    if leaf_indices.next().is_some() {
247        return false;
248    }
249
250    let column = parquet_schema.column(leaf_idx);
251    column.path().parts().len() == 1 && column.max_rep_level() == 0
252}
253
254/// Evaluate same-projection predicates in order on one decoded batch.
255/// Later predicates see only surviving rows, sliced when contiguous or compacted
256/// otherwise.
257struct FusedPredicate {
258    /// At least two predicates, all with the same projection.
259    predicates: Vec<Box<dyn ArrowPredicate>>,
260    /// Chooses how intermediate selections are composed. The output is always
261    /// a [`BooleanArray`], so this is independent of the reader's
262    /// [`RowSelectionPolicy`], which governs decoding.
263    composition_policy: RowSelectionPolicy,
264}
265
266impl FusedPredicate {
267    /// Create a fused predicate from at least two predicates that share one
268    /// projection.
269    fn new(predicates: Vec<Box<dyn ArrowPredicate>>) -> Self {
270        debug_assert!(predicates.len() > 1);
271        debug_assert!(
272            predicates
273                .windows(2)
274                .all(|pair| pair[0].projection() == pair[1].projection())
275        );
276        Self {
277            predicates,
278            composition_policy: RowSelectionPolicy::default(),
279        }
280    }
281}
282
283impl Debug for FusedPredicate {
284    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
285        write!(
286            f,
287            "FusedPredicate {{ {} predicates }}",
288            self.predicates.len()
289        )
290    }
291}
292
293impl ArrowPredicate for FusedPredicate {
294    fn projection(&self) -> &ProjectionMask {
295        self.predicates[0].projection()
296    }
297
298    fn evaluate(&mut self, batch: RecordBatch) -> Result<BooleanArray, ArrowError> {
299        let num_rows = batch.num_rows();
300        // Positions in the original batch; None until a predicate rejects rows.
301        let mut selection: Option<RowSelection> = None;
302        let mut filtered_batch = batch;
303        let last_predicate_idx = self.predicates.len() - 1;
304
305        for (idx, predicate) in self.predicates.iter_mut().enumerate() {
306            let filter = evaluate_predicate(predicate.as_mut(), filtered_batch.clone())?;
307            // No mapping is needed if all preceding predicates accepted every row.
308            if idx == last_predicate_idx && selection.is_none() {
309                return Ok(filter);
310            }
311            let true_count = filter.true_count();
312            if true_count == 0 {
313                return Ok(BooleanArray::new(BooleanBuffer::new_unset(num_rows), None));
314            }
315            if true_count == filter.len() {
316                continue;
317            }
318
319            let predicate_selection = RowSelection::from_boolean_buffer(filter.values().clone());
320            selection = Some(match selection.take() {
321                // Only the accumulated selection drives the composition
322                // algorithm, so adapt it once, right before it is used.
323                Some(prev) => self
324                    .composition_policy
325                    .apply(prev)
326                    .and_then(&predicate_selection),
327                None => predicate_selection,
328            });
329            if idx != last_predicate_idx {
330                filtered_batch = narrow_batch(&filtered_batch, &filter, true_count)?;
331            }
332        }
333
334        let mask = match selection {
335            Some(selection) => selection.into_boolean_buffer(),
336            None => BooleanBuffer::new_set(num_rows),
337        };
338        debug_assert_eq!(mask.len(), num_rows);
339        Ok(BooleanArray::new(mask, None))
340    }
341}
342
343/// Validate the predicate result length and treat nulls as false.
344fn evaluate_predicate(
345    predicate: &mut dyn ArrowPredicate,
346    batch: RecordBatch,
347) -> Result<BooleanArray, ArrowError> {
348    let input_rows = batch.num_rows();
349    let filter = predicate.evaluate(batch)?;
350    if filter.len() != input_rows {
351        return Err(ArrowError::InvalidArgumentError(format!(
352            "ArrowPredicate predicate returned {} rows, expected {input_rows}",
353            filter.len()
354        )));
355    }
356    Ok(match filter.null_count() {
357        0 => filter,
358        _ => prep_null_mask_filter(&filter),
359    })
360}
361
362/// Restrict `batch` to the rows `filter` accepts, slicing zero-copy when they
363/// form one contiguous range and compacting otherwise.
364///
365/// `filter` must be null-free and accept some but not all rows.
366fn narrow_batch(
367    batch: &RecordBatch,
368    filter: &BooleanArray,
369    true_count: usize,
370) -> Result<RecordBatch, ArrowError> {
371    let mut slices = SlicesIterator::new(filter);
372    let (start, end) = slices
373        .next()
374        .expect("a partially selected filter has a true slice");
375    if end - start == true_count {
376        Ok(batch.slice(start, end - start))
377    } else {
378        filter_record_batch(batch, filter)
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::arrow::array_reader::ArrayReader;
386    use crate::arrow::array_reader::StructArrayReader;
387    use crate::arrow::array_reader::test_util::make_int32_page_reader;
388    use crate::arrow::arrow_reader::ReadPlanBuilder;
389    use crate::schema::parser::parse_message_type;
390    use arrow_array::Int32Array;
391    use arrow_array::cast::AsArray;
392    use arrow_array::types::Int32Type;
393    use arrow_schema::{DataType, Field, Fields, Schema};
394    use std::sync::Arc;
395
396    fn test_schema() -> SchemaDescriptor {
397        let schema = parse_message_type(
398            "message schema {
399                REQUIRED INT32 a;
400                REQUIRED INT32 b;
401                REQUIRED INT32 c;
402                REQUIRED GROUP nested { REQUIRED INT32 d; }
403                REPEATED INT32 e;
404            }",
405        )
406        .unwrap();
407        SchemaDescriptor::new(Arc::new(schema))
408    }
409
410    #[test]
411    fn can_fuse_projection_requires_one_top_level_leaf() {
412        let schema = test_schema();
413
414        let one_leaf = ProjectionMask::leaves(&schema, [1]);
415        let two_leaves = ProjectionMask::leaves(&schema, [0, 2]);
416        let nested_leaf = ProjectionMask::leaves(&schema, [3]);
417        let repeated_leaf = ProjectionMask::leaves(&schema, [4]);
418        assert!(can_fuse_projection(&one_leaf, &schema));
419        assert!(!can_fuse_projection(&two_leaves, &schema));
420        assert!(!can_fuse_projection(&nested_leaf, &schema));
421        assert!(!can_fuse_projection(&repeated_leaf, &schema));
422        assert!(!can_fuse_projection(
423            &ProjectionMask::none(schema.num_columns()),
424            &schema
425        ));
426    }
427
428    fn accept_all(projection: ProjectionMask) -> Box<dyn ArrowPredicate> {
429        Box::new(ArrowPredicateFn::new(projection, |batch| {
430            Ok(BooleanArray::from(vec![true; batch.num_rows()]))
431        }))
432    }
433
434    #[test]
435    fn fuse_same_projection_groups_consecutive_eligible_predicates() {
436        let schema = test_schema();
437        let a = ProjectionMask::leaves(&schema, [0]);
438        let b = ProjectionMask::leaves(&schema, [1]);
439        let ac = ProjectionMask::leaves(&schema, [0, 2]);
440        let nested = ProjectionMask::leaves(&schema, [3]);
441
442        let filter = RowFilter::new(vec![
443            // fused
444            accept_all(a.clone()),
445            accept_all(a.clone()),
446            // single predicate: kept as-is
447            accept_all(b.clone()),
448            // two leaves: not eligible for fusion
449            accept_all(ac.clone()),
450            accept_all(ac.clone()),
451            // nested leaf: not eligible for fusion
452            accept_all(nested.clone()),
453            accept_all(nested.clone()),
454            // fused
455            accept_all(a.clone()),
456            accept_all(a.clone()),
457            accept_all(a.clone()),
458        ])
459        .fuse_same_projection(&schema);
460
461        let projections: Vec<_> = filter
462            .predicates()
463            .iter()
464            .map(|predicate| predicate.projection().clone())
465            .collect();
466        assert_eq!(
467            projections,
468            vec![a.clone(), b, ac.clone(), ac, nested.clone(), nested, a]
469        );
470        assert_eq!(format!("{filter:?}"), "RowFilter { 7 predicates: }");
471    }
472
473    #[test]
474    fn fuse_same_projection_keeps_single_predicates_and_empty_filters() {
475        let schema = test_schema();
476        let a = ProjectionMask::leaves(&schema, [0]);
477        let b = ProjectionMask::leaves(&schema, [1]);
478
479        let filter =
480            RowFilter::new(vec![accept_all(a), accept_all(b)]).fuse_same_projection(&schema);
481        assert_eq!(filter.predicates().len(), 2);
482
483        let filter = RowFilter::new(vec![]).fuse_same_projection(&schema);
484        assert!(filter.predicates().is_empty());
485    }
486
487    fn int_batch(values: impl IntoIterator<Item = i32>) -> RecordBatch {
488        let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)]));
489        let values = Int32Array::from_iter_values(values);
490        RecordBatch::try_new(schema, vec![Arc::new(values)]).unwrap()
491    }
492
493    /// A predicate over the `v` column that asserts `precondition` holds for
494    /// every row it sees before returning `f(v)`.
495    fn int_predicate(
496        precondition: impl Fn(i32) -> bool + Send + 'static,
497        f: impl Fn(i32) -> Option<bool> + Send + 'static,
498    ) -> Box<dyn ArrowPredicate> {
499        Box::new(ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
500            let values = batch.column(0).as_primitive::<Int32Type>();
501            assert!(
502                values.values().iter().all(|v| precondition(*v)),
503                "predicate saw rows rejected by a predecessor"
504            );
505            Ok(values.values().iter().map(|v| f(*v)).collect())
506        }))
507    }
508
509    /// Fragmented survivors: roughly every other row.
510    fn even() -> Box<dyn ArrowPredicate> {
511        int_predicate(|_| true, |v| Some(v % 2 == 0))
512    }
513
514    /// Clustered survivors: one contiguous range.
515    fn in_range() -> Box<dyn ArrowPredicate> {
516        int_predicate(|_| true, |v| Some((10..40).contains(&v)))
517    }
518
519    /// Applies after `even`: mixes nulls (rejected) into the result.
520    fn divisible_by_three_after_even() -> Box<dyn ArrowPredicate> {
521        int_predicate(
522            |v| v % 2 == 0,
523            |v| if v % 7 == 0 { None } else { Some(v % 3 == 0) },
524        )
525    }
526
527    fn all_true() -> Box<dyn ArrowPredicate> {
528        int_predicate(|_| true, |_| Some(true))
529    }
530
531    fn all_false() -> Box<dyn ArrowPredicate> {
532        int_predicate(|_| true, |_| Some(false))
533    }
534
535    fn never_called() -> Box<dyn ArrowPredicate> {
536        Box::new(ArrowPredicateFn::new(ProjectionMask::all(), |_| {
537            panic!("predicate evaluated after every row was rejected")
538        }))
539    }
540
541    /// Evaluate `predicates` one after another, each on the rows kept by its
542    /// predecessors, the way `RowFilter` applies separate predicates.
543    fn sequential(batch: &RecordBatch, predicates: &mut [Box<dyn ArrowPredicate>]) -> Vec<bool> {
544        let mut kept = vec![true; batch.num_rows()];
545        for predicate in predicates {
546            // Like `RowFilter`, stop once every row has been rejected.
547            if !kept.contains(&true) {
548                break;
549            }
550            let survivors = filter_record_batch(batch, &BooleanArray::from(kept.clone())).unwrap();
551            let filter = predicate.evaluate(survivors).unwrap();
552            for (filter_idx, keep) in kept.iter_mut().filter(|keep| **keep).enumerate() {
553                *keep = filter.is_valid(filter_idx) && filter.value(filter_idx);
554            }
555        }
556        kept
557    }
558
559    const POLICIES: [RowSelectionPolicy; 4] = [
560        RowSelectionPolicy::Selectors,
561        RowSelectionPolicy::Mask,
562        RowSelectionPolicy::Auto { threshold: 32 },
563        RowSelectionPolicy::Auto { threshold: 2 },
564    ];
565
566    type PredicateChain = fn() -> Vec<Box<dyn ArrowPredicate>>;
567
568    #[test]
569    fn fused_predicate_matches_sequential_evaluation() {
570        let cases: Vec<PredicateChain> = vec![
571            || vec![even(), divisible_by_three_after_even()],
572            || vec![even(), in_range(), divisible_by_three_after_even()],
573            || vec![in_range(), even()],
574            || vec![even(), all_true(), divisible_by_three_after_even()],
575            || vec![all_true(), even()],
576            || vec![all_true(), all_true()],
577            || vec![even(), all_false(), never_called()],
578            || vec![all_false(), never_called()],
579        ];
580
581        for num_rows in [0, 1, 7, 97, 200] {
582            let batch = int_batch(0..num_rows);
583            for make_predicates in &cases {
584                let expected = sequential(&batch, &mut make_predicates());
585                for policy in POLICIES {
586                    let mut fused = FusedPredicate {
587                        predicates: make_predicates(),
588                        composition_policy: policy,
589                    };
590                    let actual = fused.evaluate(batch.clone()).unwrap();
591                    assert_eq!(actual.null_count(), 0);
592                    assert_eq!(
593                        actual.values().iter().collect::<Vec<_>>(),
594                        expected,
595                        "{num_rows} rows, {policy:?}"
596                    );
597                }
598            }
599        }
600    }
601
602    #[test]
603    fn fused_predicate_rejects_wrong_result_length() {
604        let short = Box::new(ArrowPredicateFn::new(ProjectionMask::all(), |_| {
605            Ok(BooleanArray::from(vec![true]))
606        }));
607        let mut fused = FusedPredicate::new(vec![all_true(), short]);
608        let err = fused.evaluate(int_batch(0..4)).unwrap_err();
609        assert!(
610            err.to_string()
611                .contains("ArrowPredicate predicate returned 1 rows, expected 4"),
612            "{err}"
613        );
614    }
615
616    #[test]
617    fn narrow_batch_slices_contiguous_survivors() {
618        let batch = int_batch(0..6);
619        let filter = BooleanArray::from(vec![false, false, true, true, true, false]);
620        let narrowed = narrow_batch(&batch, &filter, 3).unwrap();
621        assert_eq!(
622            narrowed.column(0).as_primitive::<Int32Type>().values(),
623            &[2, 3, 4]
624        );
625        // Slicing shares the input buffer instead of copying it.
626        assert_eq!(narrowed.column(0).to_data().buffers()[0].as_ptr(), unsafe {
627            batch.column(0).to_data().buffers()[0].as_ptr().add(2 * 4)
628        });
629
630        let filter = BooleanArray::from(vec![false, true, false, true, false, false]);
631        let narrowed = narrow_batch(&batch, &filter, 2).unwrap();
632        assert_eq!(
633            narrowed.column(0).as_primitive::<Int32Type>().values(),
634            &[1, 3]
635        );
636    }
637
638    /// Fusing predicates into one `ReadPlanBuilder::with_predicate` call
639    /// produces the same selection as applying them one at a time.
640    #[test]
641    fn fused_predicate_matches_predicate_major_read_plan() {
642        let data: Vec<i32> = (0..97).collect();
643        let make_reader = || {
644            let levels = vec![0; data.len()];
645            let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
646            let struct_type =
647                DataType::Struct(Fields::from(vec![Field::new("c0", DataType::Int32, false)]));
648            Box::new(StructArrayReader::new(
649                struct_type,
650                vec![leaf],
651                0,
652                0,
653                false,
654                None,
655            )) as Box<dyn ArrayReader>
656        };
657        let make_predicates = || vec![even(), divisible_by_three_after_even()];
658
659        let prior = RowSelection::from_filters(&[BooleanArray::from(
660            (0..data.len()).map(|idx| idx % 5 != 0).collect::<Vec<_>>(),
661        )]);
662        for initial in [None, Some(prior)] {
663            let mut sequential = ReadPlanBuilder::new(7).with_selection(initial.clone());
664            for predicate in &mut make_predicates() {
665                sequential = sequential
666                    .with_predicate(make_reader(), predicate.as_mut())
667                    .unwrap();
668            }
669
670            for policy in POLICIES {
671                let mut fused = FusedPredicate::new(make_predicates());
672                let plan = ReadPlanBuilder::new(7)
673                    .with_selection(initial.clone())
674                    .with_row_selection_policy(policy)
675                    .with_predicate(make_reader(), &mut fused)
676                    .unwrap();
677                assert_eq!(plan.selection(), sequential.selection(), "{policy:?}");
678            }
679        }
680    }
681}