|
| 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