Skip to content

Commit beced87

Browse files
committed
Test out pushdecoder
1 parent 52c2229 commit beced87

1 file changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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+
18+
//! [`PushParquetOpener`] for reading Parquet files using [`ParquetPushDecoder`]
19+
20+
use crate::opener::build_pruning_predicates;
21+
use crate::row_filter;
22+
use crate::row_group_filter::RowGroupAccessPlanFilter;
23+
use crate::{
24+
ParquetAccessPlan, ParquetFileMetrics, ParquetFileReaderFactory,
25+
apply_file_schema_type_coercions,
26+
};
27+
use arrow::record_batch::RecordBatch;
28+
use datafusion_common::{DataFusionError, Result};
29+
use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener};
30+
use datafusion_datasource::{PartitionedFile, TableSchema};
31+
use datafusion_physical_expr::projection::ProjectionExprs;
32+
use datafusion_physical_expr::utils::reassign_expr_columns;
33+
use datafusion_physical_expr_adapter::PhysicalExprAdapterFactory;
34+
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
35+
use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder};
36+
use futures::StreamExt;
37+
use parquet::DecodeResult;
38+
use parquet::arrow::ProjectionMask;
39+
use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
40+
use parquet::arrow::async_reader::AsyncFileReader;
41+
use parquet::arrow::push_decoder::ParquetPushDecoderBuilder;
42+
use std::sync::Arc;
43+
44+
/// A [`FileOpener`] for Parquet files that uses [`ParquetPushDecoder`] for
45+
/// decoding. This gives fine-grained control over IO vs CPU scheduling.
46+
pub(super) struct PushParquetOpener {
47+
pub partition_index: usize,
48+
pub projection: ProjectionExprs,
49+
pub batch_size: usize,
50+
pub limit: Option<usize>,
51+
pub predicate: Option<Arc<dyn PhysicalExpr>>,
52+
pub table_schema: TableSchema,
53+
pub metadata_size_hint: Option<usize>,
54+
pub metrics: ExecutionPlanMetricsSet,
55+
pub parquet_file_reader_factory: Arc<dyn ParquetFileReaderFactory>,
56+
pub pushdown_filters: bool,
57+
pub reorder_filters: bool,
58+
pub enable_row_group_stats_pruning: bool,
59+
pub expr_adapter_factory: Arc<dyn PhysicalExprAdapterFactory>,
60+
}
61+
62+
impl FileOpener for PushParquetOpener {
63+
fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
64+
let file_name = partitioned_file.object_meta.location.to_string();
65+
let file_metrics =
66+
ParquetFileMetrics::new(self.partition_index, &file_name, &self.metrics);
67+
68+
let metadata_size_hint = partitioned_file
69+
.metadata_size_hint
70+
.or(self.metadata_size_hint);
71+
72+
let mut async_file_reader: Box<dyn AsyncFileReader> =
73+
self.parquet_file_reader_factory.create_reader(
74+
self.partition_index,
75+
partitioned_file.clone(),
76+
metadata_size_hint,
77+
&self.metrics,
78+
)?;
79+
80+
let batch_size = self.batch_size;
81+
let limit = self.limit;
82+
let projection = self.projection.clone();
83+
let logical_file_schema = Arc::clone(self.table_schema.file_schema());
84+
let predicate = self.predicate.clone();
85+
let pushdown_filters = self.pushdown_filters;
86+
let reorder_filters = self.reorder_filters;
87+
let enable_row_group_stats_pruning = self.enable_row_group_stats_pruning;
88+
let expr_adapter_factory = Arc::clone(&self.expr_adapter_factory);
89+
let extensions = partitioned_file.extensions.clone();
90+
let predicate_creation_errors = MetricBuilder::new(&self.metrics)
91+
.global_counter("num_predicate_creation_errors");
92+
93+
let output_schema = Arc::new(
94+
self.projection
95+
.project_schema(self.table_schema.table_schema())?,
96+
);
97+
98+
Ok(Box::pin(async move {
99+
// Load metadata
100+
let options = ArrowReaderOptions::new();
101+
let reader_metadata =
102+
ArrowReaderMetadata::load_async(&mut async_file_reader, options.clone())
103+
.await?;
104+
105+
let mut physical_file_schema = Arc::clone(reader_metadata.schema());
106+
107+
// Apply schema coercions (e.g. Utf8 -> Utf8View)
108+
if let Some(merged) = apply_file_schema_type_coercions(
109+
&logical_file_schema,
110+
&physical_file_schema,
111+
) {
112+
physical_file_schema = Arc::new(merged);
113+
}
114+
115+
let options = options.with_schema(Arc::clone(&physical_file_schema));
116+
let reader_metadata = ArrowReaderMetadata::try_new(
117+
Arc::clone(reader_metadata.metadata()),
118+
options,
119+
)?;
120+
121+
// Adapt predicate and projection to the physical file schema
122+
let rewriter = expr_adapter_factory.create(
123+
Arc::clone(&logical_file_schema),
124+
Arc::clone(&physical_file_schema),
125+
)?;
126+
let predicate = predicate.map(|p| rewriter.rewrite(p)).transpose()?;
127+
let projection = projection.try_map_exprs(|p| rewriter.rewrite(p))?;
128+
129+
// Build pruning predicates
130+
let (pruning_predicate, _page_pruning_predicate) = build_pruning_predicates(
131+
predicate.as_ref(),
132+
&physical_file_schema,
133+
&predicate_creation_errors,
134+
);
135+
136+
// Determine which row groups to read
137+
let file_metadata = Arc::clone(reader_metadata.metadata());
138+
let rg_metadata = file_metadata.row_groups();
139+
let num_row_groups = rg_metadata.len();
140+
141+
let access_plan = match extensions {
142+
Some(ext) => ext
143+
.downcast_ref::<ParquetAccessPlan>()
144+
.cloned()
145+
.unwrap_or_else(|| ParquetAccessPlan::new_all(num_row_groups)),
146+
None => ParquetAccessPlan::new_all(num_row_groups),
147+
};
148+
149+
let mut row_groups_filter = RowGroupAccessPlanFilter::new(access_plan);
150+
151+
// Prune row groups by statistics
152+
if let Some(ref pred) = pruning_predicate {
153+
if enable_row_group_stats_pruning {
154+
row_groups_filter.prune_by_statistics(
155+
&physical_file_schema,
156+
reader_metadata.parquet_schema(),
157+
rg_metadata,
158+
pred,
159+
&file_metrics,
160+
);
161+
}
162+
}
163+
164+
let access_plan = row_groups_filter.build();
165+
let row_group_indexes = access_plan.row_group_indexes();
166+
167+
// Build the push decoder
168+
let mut builder =
169+
ParquetPushDecoderBuilder::new_with_metadata(reader_metadata.clone())
170+
.with_batch_size(batch_size);
171+
172+
// Set row filter for predicate pushdown
173+
if let Some(ref predicate) = predicate {
174+
if pushdown_filters {
175+
let row_filter = row_filter::build_row_filter(
176+
predicate,
177+
&physical_file_schema,
178+
file_metadata.as_ref(),
179+
reorder_filters,
180+
&file_metrics,
181+
);
182+
match row_filter {
183+
Ok(Some(filter)) => {
184+
builder = builder.with_row_filter(filter);
185+
}
186+
Ok(None) => {}
187+
Err(e) => {
188+
log::debug!(
189+
"Ignoring error building row filter for '{predicate:?}': {e}"
190+
);
191+
}
192+
}
193+
}
194+
}
195+
196+
if !row_group_indexes.is_empty() {
197+
builder = builder.with_row_groups(row_group_indexes);
198+
} else {
199+
// All row groups pruned - return empty stream
200+
return Ok(futures::stream::empty().boxed());
201+
}
202+
203+
// Set row selection from the access plan
204+
let row_selection = access_plan.into_overall_row_selection(rg_metadata)?;
205+
if let Some(selection) = row_selection {
206+
builder = builder.with_row_selection(selection);
207+
}
208+
209+
if let Some(limit) = limit {
210+
builder = builder.with_limit(limit);
211+
}
212+
213+
// Apply projection
214+
let projection = projection.clone();
215+
let indices = projection.column_indices();
216+
let mask = ProjectionMask::roots(reader_metadata.parquet_schema(), indices);
217+
builder = builder.with_projection(mask);
218+
219+
let decoder = builder.build()?;
220+
221+
// Create a stream that drives the decoder by fetching data as needed
222+
let stream = futures::stream::unfold(
223+
(decoder, async_file_reader, projection, output_schema),
224+
|(mut decoder, mut reader, projection, output_schema)| async move {
225+
loop {
226+
match decoder.try_decode() {
227+
Ok(DecodeResult::NeedsData(ranges)) => {
228+
match reader.get_byte_ranges(ranges.clone()).await {
229+
Ok(data) => {
230+
if let Err(e) = decoder.push_ranges(ranges, data)
231+
{
232+
return Some((
233+
Err(DataFusionError::from(e)),
234+
(
235+
decoder,
236+
reader,
237+
projection,
238+
output_schema,
239+
),
240+
));
241+
}
242+
}
243+
Err(e) => {
244+
return Some((
245+
Err(DataFusionError::from(e)),
246+
(decoder, reader, projection, output_schema),
247+
));
248+
}
249+
}
250+
}
251+
Ok(DecodeResult::Data(batch)) => {
252+
// Apply projection expressions and schema
253+
let result =
254+
apply_projection(batch, &projection, &output_schema);
255+
return Some((
256+
result,
257+
(decoder, reader, projection, output_schema),
258+
));
259+
}
260+
Ok(DecodeResult::Finished) => {
261+
return None;
262+
}
263+
Err(e) => {
264+
return Some((
265+
Err(DataFusionError::from(e)),
266+
(decoder, reader, projection, output_schema),
267+
));
268+
}
269+
}
270+
}
271+
},
272+
);
273+
274+
Ok(stream.boxed())
275+
}))
276+
}
277+
}
278+
279+
/// Apply projection expressions to the decoded batch and ensure correct schema
280+
fn apply_projection(
281+
batch: RecordBatch,
282+
projection: &ProjectionExprs,
283+
output_schema: &Arc<arrow::datatypes::Schema>,
284+
) -> Result<RecordBatch> {
285+
let stream_schema = batch.schema();
286+
let projection = projection
287+
.clone()
288+
.try_map_exprs(|expr| reassign_expr_columns(expr, &stream_schema))?;
289+
let projector = projection.make_projector(&stream_schema)?;
290+
let mut batch = projector.project_batch(&batch)?;
291+
292+
if batch.schema() != *output_schema {
293+
let (_schema, arrays, num_rows) = batch.into_parts();
294+
let options =
295+
arrow::record_batch::RecordBatchOptions::new().with_row_count(Some(num_rows));
296+
batch = RecordBatch::try_new_with_options(
297+
Arc::clone(output_schema),
298+
arrays,
299+
&options,
300+
)?;
301+
}
302+
303+
Ok(batch)
304+
}

0 commit comments

Comments
 (0)