Skip to content

Commit 4fef178

Browse files
committed
Preserve mask execution across skipped parquet pages
1 parent f3c5dc5 commit 4fef178

5 files changed

Lines changed: 322 additions & 109 deletions

File tree

parquet/src/arrow/arrow_reader/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ impl ParquetRecordBatchReader {
13861386
// Stream the record batch reader using contiguous segments of the selection
13871387
// mask, avoiding the need to materialize intermediate `RowSelector` ranges.
13881388
while !mask_cursor.is_empty() {
1389-
let Some(mask_chunk) = mask_cursor.next_mask_chunk(batch_size) else {
1389+
let Some(mask_chunk) = mask_cursor.next_chunk(batch_size)? else {
13901390
return Ok(None);
13911391
};
13921392

parquet/src/arrow/arrow_reader/read_plan.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
//! from a Parquet file
2020
2121
use crate::arrow::array_reader::ArrayReader;
22-
use crate::arrow::arrow_reader::selection::RowSelectionPolicy;
23-
use crate::arrow::arrow_reader::selection::RowSelectionStrategy;
22+
use crate::arrow::arrow_reader::selection::{
23+
LoadedRowRanges, RowSelectionPolicy, RowSelectionStrategy,
24+
};
2425
use crate::arrow::arrow_reader::{
2526
ArrowPredicate, ParquetRecordBatchReader, RowSelection, RowSelectionCursor, RowSelector,
2627
};
@@ -29,6 +30,7 @@ use arrow_array::{Array, BooleanArray};
2930
use arrow_buffer::BooleanBuffer;
3031
use arrow_select::filter::prep_null_mask_filter;
3132
use std::collections::VecDeque;
33+
use std::sync::Arc;
3234

3335
/// Options for [`ReadPlanBuilder::with_predicate_options`].
3436
pub struct PredicateOptions<'a> {
@@ -84,6 +86,8 @@ pub struct ReadPlanBuilder {
8486
selection: Option<RowSelection>,
8587
/// Policy to use when materializing the row selection
8688
row_selection_policy: RowSelectionPolicy,
89+
/// Row ranges with page data loaded for the current projection.
90+
loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
8791
}
8892

8993
impl ReadPlanBuilder {
@@ -93,6 +97,7 @@ impl ReadPlanBuilder {
9397
batch_size,
9498
selection: None,
9599
row_selection_policy: RowSelectionPolicy::default(),
100+
loaded_row_ranges: None,
96101
}
97102
}
98103

@@ -110,6 +115,11 @@ impl ReadPlanBuilder {
110115
self
111116
}
112117

118+
pub(crate) fn with_loaded_row_ranges(mut self, ranges: Option<LoadedRowRanges>) -> Self {
119+
self.loaded_row_ranges = ranges.map(Arc::new);
120+
self
121+
}
122+
113123
/// Returns the current row selection policy
114124
pub fn row_selection_policy(&self) -> &RowSelectionPolicy {
115125
&self.row_selection_policy
@@ -304,6 +314,7 @@ impl ReadPlanBuilder {
304314
batch_size,
305315
selection,
306316
row_selection_policy: _,
317+
loaded_row_ranges,
307318
} = self;
308319

309320
let selection = selection.map(|s| s.trim());
@@ -314,7 +325,7 @@ impl ReadPlanBuilder {
314325
let selectors: Vec<RowSelector> = trimmed.into();
315326
match selection_strategy {
316327
RowSelectionStrategy::Mask => {
317-
RowSelectionCursor::new_mask_from_selectors(selectors)
328+
RowSelectionCursor::new_mask_from_selectors(selectors, loaded_row_ranges)
318329
}
319330
RowSelectionStrategy::Selectors => RowSelectionCursor::new_selectors(selectors),
320331
}

parquet/src/arrow/arrow_reader/selection.rs

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,17 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use crate::arrow::ProjectionMask;
1918
use crate::errors::ParquetError;
20-
use crate::file::page_index::offset_index::{OffsetIndexMetaData, PageLocation};
19+
use crate::file::page_index::offset_index::PageLocation;
2120
use arrow_array::{Array, BooleanArray};
2221
use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder};
2322
use arrow_select::filter::SlicesIterator;
2423
use std::cmp::Ordering;
2524
use std::collections::VecDeque;
2625
use std::ops::Range;
26+
use std::sync::Arc;
2727

2828
/// Policy for picking a strategy to materialise [`RowSelection`] during execution.
29-
///
30-
/// Note that this is a user-provided preference, and the actual strategy used
31-
/// may differ based on safety considerations (e.g. page skipping).
3229
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3330
pub enum RowSelectionPolicy {
3431
/// Use a queue of [`RowSelector`] values
@@ -50,8 +47,8 @@ impl Default for RowSelectionPolicy {
5047

5148
/// Fully resolved strategy for materializing [`RowSelection`] during execution.
5249
///
53-
/// This is determined from a combination of user preference (via [`RowSelectionPolicy`])
54-
/// and safety considerations (e.g. page skipping).
50+
/// This is determined by [`RowSelectionPolicy`], including selector density for
51+
/// [`RowSelectionPolicy::Auto`].
5552
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5653
pub(crate) enum RowSelectionStrategy {
5754
/// Use a queue of [`RowSelector`] values
@@ -250,37 +247,32 @@ impl RowSelection {
250247
ranges
251248
}
252249

253-
/// Returns true if this selection would skip any data pages within the provided columns
254-
fn selection_skips_any_page(
250+
/// Returns the complete row ranges of the pages selected by [`Self::scan_ranges`].
251+
pub(crate) fn row_ranges_for_selected_pages(
255252
&self,
256-
projection: &ProjectionMask,
257-
columns: &[OffsetIndexMetaData],
258-
) -> bool {
259-
columns.iter().enumerate().any(|(leaf_idx, column)| {
260-
if !projection.leaf_included(leaf_idx) {
261-
return false;
262-
}
263-
264-
let locations = column.page_locations();
265-
if locations.is_empty() {
266-
return false;
253+
page_locations: &[PageLocation],
254+
total_rows: usize,
255+
) -> Vec<Range<usize>> {
256+
let mut selected_pages = self.scan_ranges(page_locations).into_iter().peekable();
257+
let mut row_ranges = Vec::new();
258+
259+
for (idx, page) in page_locations.iter().enumerate() {
260+
let Some(selected_page) = selected_pages.peek() else {
261+
break;
262+
};
263+
if selected_page.start != page.offset as u64 {
264+
continue;
267265
}
266+
selected_pages.next();
268267

269-
let ranges = self.scan_ranges(locations);
270-
!ranges.is_empty() && ranges.len() < locations.len()
271-
})
272-
}
273-
274-
/// Returns true if selectors should be forced, preventing mask materialisation
275-
pub(crate) fn should_force_selectors(
276-
&self,
277-
projection: &ProjectionMask,
278-
offset_index: Option<&[OffsetIndexMetaData]>,
279-
) -> bool {
280-
match offset_index {
281-
Some(columns) => self.selection_skips_any_page(projection, columns),
282-
None => false,
268+
let end = page_locations
269+
.get(idx + 1)
270+
.map(|next| next.first_row_index as usize)
271+
.unwrap_or(total_rows);
272+
row_ranges.push(page.first_row_index as usize..end);
283273
}
274+
275+
row_ranges
284276
}
285277

286278
/// Splits off the first `row_count` from this [`RowSelection`]
@@ -770,6 +762,8 @@ pub struct MaskCursor {
770762
mask: BooleanBuffer,
771763
/// Current absolute offset into the selection
772764
position: usize,
765+
/// Row ranges whose backing pages are loaded for every projected column.
766+
loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
773767
}
774768

775769
impl MaskCursor {
@@ -824,6 +818,54 @@ impl MaskCursor {
824818
})
825819
}
826820

821+
/// Returns the next mask chunk without crossing a row range that was not loaded.
822+
pub(crate) fn next_chunk(
823+
&mut self,
824+
batch_size: usize,
825+
) -> Result<Option<MaskChunk>, ParquetError> {
826+
if self.loaded_row_ranges.is_none() {
827+
return Ok(self.next_mask_chunk(batch_size));
828+
}
829+
830+
let start_position = self.position;
831+
let mut cursor = start_position;
832+
while cursor < self.mask.len() && !self.mask.value(cursor) {
833+
cursor += 1;
834+
}
835+
836+
if cursor == self.mask.len() {
837+
self.position = cursor;
838+
return Ok(None);
839+
}
840+
841+
let loaded_range_end = self
842+
.loaded_row_ranges
843+
.as_ref()
844+
.and_then(|ranges| ranges.end_containing(cursor))
845+
.ok_or_else(|| {
846+
ParquetError::General(format!(
847+
"Internal Error: selected row {cursor} has no loaded page range"
848+
))
849+
})?;
850+
851+
let mask_start = cursor;
852+
let mut selected_rows = 0;
853+
while cursor < loaded_range_end && cursor < self.mask.len() && selected_rows < batch_size {
854+
if self.mask.value(cursor) {
855+
selected_rows += 1;
856+
}
857+
cursor += 1;
858+
}
859+
860+
self.position = cursor;
861+
Ok(Some(MaskChunk {
862+
initial_skip: mask_start - start_position,
863+
chunk_rows: cursor - mask_start,
864+
selected_rows,
865+
mask_start,
866+
}))
867+
}
868+
827869
/// Materialise the boolean values for a mask-backed chunk
828870
pub fn mask_values_for(&self, chunk: &MaskChunk) -> Result<BooleanArray, ParquetError> {
829871
if chunk.mask_start.saturating_add(chunk.chunk_rows) > self.mask.len() {
@@ -885,6 +927,39 @@ pub struct MaskChunk {
885927
pub mask_start: usize,
886928
}
887929

930+
/// Row ranges whose backing pages are loaded for every projected column.
931+
#[derive(Clone, Debug)]
932+
pub(crate) struct LoadedRowRanges(Vec<Range<usize>>);
933+
934+
impl LoadedRowRanges {
935+
pub(crate) fn from_selection(selection: RowSelection) -> Self {
936+
let selectors: Vec<RowSelector> = selection.into();
937+
let mut position = 0;
938+
let ranges = selectors
939+
.into_iter()
940+
.filter_map(|selector| {
941+
let start = position;
942+
position += selector.row_count;
943+
(!selector.skip).then_some(start..position)
944+
})
945+
.collect();
946+
Self(ranges)
947+
}
948+
949+
fn end_containing(&self, row: usize) -> Option<usize> {
950+
let idx = self.0.partition_point(|range| range.end <= row);
951+
self.0
952+
.get(idx)
953+
.filter(|range| range.start <= row)
954+
.map(|range| range.end)
955+
}
956+
957+
#[cfg(test)]
958+
pub(crate) fn ranges(&self) -> &[Range<usize>] {
959+
&self.0
960+
}
961+
}
962+
888963
/// Cursor for iterating a [`RowSelection`] during execution within a
889964
/// [`ReadPlan`](crate::arrow::arrow_reader::ReadPlan).
890965
///
@@ -902,10 +977,14 @@ pub enum RowSelectionCursor {
902977

903978
impl RowSelectionCursor {
904979
/// Create a [`MaskCursor`] cursor backed by a bitmask, from an existing set of selectors
905-
pub(crate) fn new_mask_from_selectors(selectors: Vec<RowSelector>) -> Self {
980+
pub(crate) fn new_mask_from_selectors(
981+
selectors: Vec<RowSelector>,
982+
loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
983+
) -> Self {
906984
Self::Mask(MaskCursor {
907985
mask: boolean_mask_from_selectors(&selectors),
908986
position: 0,
987+
loaded_row_ranges,
909988
})
910989
}
911990

@@ -1466,6 +1545,10 @@ mod tests {
14661545

14671546
// assert_eq!(mask, vec![false, true, true, false, true, true, false]);
14681547
assert_eq!(ranges, vec![10..20, 20..30, 40..50, 50..60]);
1548+
assert_eq!(
1549+
selection.row_ranges_for_selected_pages(&index, 70),
1550+
vec![10..20, 20..30, 40..50, 50..60]
1551+
);
14691552

14701553
let selection = RowSelection::from(vec![
14711554
// Skip first page
@@ -1537,6 +1620,24 @@ mod tests {
15371620
assert_eq!(ranges, vec![10..20, 20..30, 30..40]);
15381621
}
15391622

1623+
#[test]
1624+
fn test_loaded_mask_chunk_stops_at_trimmed_mask_end() {
1625+
let loaded = LoadedRowRanges::from_selection(RowSelection::from_consecutive_ranges(
1626+
std::iter::once(0..5),
1627+
10,
1628+
));
1629+
let RowSelectionCursor::Mask(mut cursor) = RowSelectionCursor::new_mask_from_selectors(
1630+
vec![RowSelector::select(1)],
1631+
Some(loaded.into()),
1632+
) else {
1633+
unreachable!()
1634+
};
1635+
1636+
let chunk = cursor.next_chunk(10).unwrap().unwrap();
1637+
assert_eq!(chunk.chunk_rows, 1);
1638+
assert!(cursor.is_empty());
1639+
}
1640+
15401641
#[test]
15411642
fn test_from_ranges() {
15421643
let ranges = [1..3, 4..6, 6..6, 8..8, 9..10];

0 commit comments

Comments
 (0)