blob: f7c206273253c22b38cf3ffa897d698523e8f67c [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Execution plan for reading Parquet files
use std::{any::Any, fmt, fmt::Formatter, ops::Range, sync::Arc};
use arrow::{
array::{Array, ArrayRef, AsArray, ListArray},
datatypes::{DataType, SchemaRef},
};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use blaze_jni_bridge::{
conf, conf::BooleanConf, jni_call_static, jni_new_global_ref, jni_new_string,
};
use bytes::Bytes;
use datafusion::{
common::DataFusionError,
datasource::physical_plan::{
parquet::{page_filter::PagePruningPredicate, ParquetOpener},
FileMeta, FileScanConfig, FileStream, OnError, ParquetFileMetrics,
ParquetFileReaderFactory,
},
error::Result,
execution::context::TaskContext,
parquet::{
arrow::async_reader::{fetch_parquet_metadata, AsyncFileReader},
errors::ParquetError,
file::metadata::ParquetMetaData,
},
physical_optimizer::pruning::PruningPredicate,
physical_plan::{
expressions::PhysicalSortExpr,
metrics::{
BaselineMetrics, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet, Time,
},
stream::RecordBatchStreamAdapter,
DisplayAs, DisplayFormatType, ExecutionPlan, Metric, Partitioning, PhysicalExpr,
RecordBatchStream, SendableRecordBatchStream, Statistics,
},
};
use datafusion_ext_commons::{
batch_size, df_execution_err,
hadoop_fs::{FsDataInputStream, FsProvider},
};
use fmt::Debug;
use futures::{future::BoxFuture, stream::once, FutureExt, StreamExt, TryStreamExt};
use object_store::ObjectMeta;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use crate::common::output::TaskOutputter;
#[no_mangle]
fn schema_adapter_cast_column(
col: &ArrayRef,
data_type: &DataType,
) -> Result<ArrayRef, DataFusionError> {
macro_rules! handle_decimal {
($s:ident, $t:ident, $tnative:ty, $prec:expr, $scale:expr) => {{
use arrow::{array::*, datatypes::*};
type DecimalBuilder = paste::paste! {[<$t Builder>]};
type IntType = paste::paste! {[<$s Type>]};
let col = col.as_primitive::<IntType>();
let mut decimal_builder = DecimalBuilder::new();
for i in 0..col.len() {
if col.is_valid(i) {
decimal_builder.append_value(col.value(i) as $tnative);
} else {
decimal_builder.append_null();
}
}
Ok(Arc::new(
decimal_builder
.finish()
.with_precision_and_scale($prec, $scale)?,
))
}};
}
match data_type {
DataType::Decimal128(prec, scale) => match col.data_type() {
DataType::Int8 => handle_decimal!(Int8, Decimal128, i128, *prec, *scale),
DataType::Int16 => handle_decimal!(Int16, Decimal128, i128, *prec, *scale),
DataType::Int32 => handle_decimal!(Int32, Decimal128, i128, *prec, *scale),
DataType::Int64 => handle_decimal!(Int64, Decimal128, i128, *prec, *scale),
DataType::Decimal128(p, s) if p == prec && s == scale => Ok(col.clone()),
_ => df_execution_err!(
"schema_adapter_cast_column unsupported type: {:?} => {:?}",
col.data_type(),
data_type,
),
},
DataType::List(to_field) => match col.data_type() {
DataType::List(_from_field) => {
let col = col.as_list::<i32>();
let from_inner = col.values();
let to_inner = schema_adapter_cast_column(from_inner, to_field.data_type())?;
Ok(Arc::new(ListArray::try_new(
to_field.clone(),
col.offsets().clone(),
to_inner,
col.nulls().cloned(),
)?))
}
_ => df_execution_err!(
"schema_adapter_cast_column unsupported type: {:?} => {:?}",
col.data_type(),
data_type,
),
},
_ => datafusion_ext_commons::cast::cast_scan_input_array(col.as_ref(), data_type),
}
}
/// Execution plan for scanning one or more Parquet partitions
#[derive(Debug, Clone)]
pub struct ParquetExec {
fs_resource_id: String,
base_config: FileScanConfig,
projected_statistics: Statistics,
projected_schema: SchemaRef,
projected_output_ordering: Vec<Vec<PhysicalSortExpr>>,
metrics: ExecutionPlanMetricsSet,
predicate: Option<Arc<dyn PhysicalExpr>>,
pruning_predicate: Option<Arc<PruningPredicate>>,
page_pruning_predicate: Option<Arc<PagePruningPredicate>>,
}
impl ParquetExec {
/// Create a new Parquet reader execution plan provided file list and
/// schema.
pub fn new(
base_config: FileScanConfig,
fs_resource_id: String,
predicate: Option<Arc<dyn PhysicalExpr>>,
) -> Self {
let metrics = ExecutionPlanMetricsSet::new();
let predicate_creation_errors =
MetricBuilder::new(&metrics).global_counter("num_predicate_creation_errors");
let file_schema = &base_config.file_schema;
let pruning_predicate = predicate
.clone()
.and_then(|predicate_expr| {
match PruningPredicate::try_new(predicate_expr, file_schema.clone()) {
Ok(pruning_predicate) => Some(Arc::new(pruning_predicate)),
Err(e) => {
log::warn!("Could not create pruning predicate: {e}");
predicate_creation_errors.add(1);
None
}
}
})
.filter(|p| !p.allways_true());
let page_pruning_predicate = predicate.as_ref().and_then(|predicate_expr| {
match PagePruningPredicate::try_new(predicate_expr, file_schema.clone()) {
Ok(pruning_predicate) => Some(Arc::new(pruning_predicate)),
Err(e) => {
log::warn!("Could not create page pruning predicate: {}", e);
predicate_creation_errors.add(1);
None
}
}
});
let (projected_schema, projected_statistics, projected_output_ordering) =
base_config.project();
Self {
fs_resource_id,
base_config,
projected_schema,
projected_statistics,
projected_output_ordering,
metrics,
predicate,
pruning_predicate,
page_pruning_predicate,
}
}
}
impl DisplayAs for ParquetExec {
fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
let limit = self.base_config.limit;
let file_group = self
.base_config
.file_groups
.iter()
.flatten()
.cloned()
.collect::<Vec<_>>();
write!(
f,
"ParquetExec: limit={:?}, file_group={:?}, predicate={}",
limit,
file_group,
self.pruning_predicate
.as_ref()
.map(|pre| format!("{}", pre.predicate_expr()))
.unwrap_or(format!("<empty>")),
)
}
}
impl ExecutionPlan for ParquetExec {
fn as_any(&self) -> &dyn Any {
self
}
fn schema(&self) -> SchemaRef {
Arc::clone(&self.projected_schema)
}
fn children(&self) -> Vec<Arc<dyn ExecutionPlan>> {
vec![]
}
fn output_partitioning(&self) -> Partitioning {
Partitioning::UnknownPartitioning(self.base_config.file_groups.len())
}
fn output_ordering(&self) -> Option<&[PhysicalSortExpr]> {
self.projected_output_ordering
.first()
.map(|ordering| ordering.as_slice())
}
// in datafusion 20.0.0 ExecutionPlan trait not include relies_on_input_order
// fn relies_on_input_order(&self) -> bool {
// false
// }
fn with_new_children(
self: Arc<Self>,
_: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
Ok(self)
}
fn execute(
&self,
partition_index: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
let baseline_metrics = BaselineMetrics::new(&self.metrics, partition_index);
let elapsed_compute = baseline_metrics.elapsed_compute();
let _timer = elapsed_compute.timer();
let io_time = Time::default();
let io_time_metric = Arc::new(Metric::new(
MetricValue::Time {
name: "io_time".into(),
time: io_time.clone(),
},
Some(partition_index),
));
self.metrics.register(io_time_metric);
// get fs object from jni bridge resource
let resource_id = jni_new_string!(&self.fs_resource_id)?;
let fs = jni_call_static!(JniBridge.getResource(resource_id.as_obj()) -> JObject)?;
let fs_provider = Arc::new(FsProvider::new(jni_new_global_ref!(fs.as_obj())?, &io_time));
let projection = match self.base_config.file_column_projection_indices() {
Some(proj) => proj,
None => (0..self.base_config.file_schema.fields().len()).collect(),
};
let page_filtering_enabled = conf::PARQUET_ENABLE_PAGE_FILTERING.value()?;
let bloom_filter_enabled = conf::PARQUET_ENABLE_BLOOM_FILTER.value()?;
let opener = ParquetOpener {
partition_index,
projection: Arc::from(projection),
batch_size: batch_size(),
limit: self.base_config.limit,
predicate: self.predicate.clone(),
pruning_predicate: self.pruning_predicate.clone(),
page_pruning_predicate: self.page_pruning_predicate.clone(),
table_schema: self.base_config.file_schema.clone(),
metadata_size_hint: None,
metrics: self.metrics.clone(),
parquet_file_reader_factory: Arc::new(FsReaderFactory::new(fs_provider)),
pushdown_filters: page_filtering_enabled,
reorder_filters: page_filtering_enabled,
enable_page_index: page_filtering_enabled,
enable_bloom_filter: bloom_filter_enabled,
};
let baseline_metrics_cloned = baseline_metrics.clone();
let mut file_stream =
FileStream::new(&self.base_config, partition_index, opener, &self.metrics)?;
if conf::IGNORE_CORRUPTED_FILES.value()? {
file_stream = file_stream.with_on_error(OnError::Skip);
}
let mut stream = Box::pin(file_stream);
let context_cloned = context.clone();
let timed_stream = Box::pin(RecordBatchStreamAdapter::new(
self.schema(),
once(async move {
context_cloned.output_with_sender(
"ParquetScan",
stream.schema(),
move |sender| async move {
let mut timer = baseline_metrics_cloned.elapsed_compute().timer();
while let Some(batch) = stream.next().await.transpose()? {
sender.send(Ok(batch), Some(&mut timer)).await;
}
Ok(())
},
)
})
.try_flatten(),
));
Ok(timed_stream)
}
fn metrics(&self) -> Option<MetricsSet> {
Some(self.metrics.clone_inner())
}
fn statistics(&self) -> Result<Statistics> {
Ok(self.projected_statistics.clone())
}
}
#[derive(Clone)]
pub struct FsReaderFactory {
fs_provider: Arc<FsProvider>,
}
impl FsReaderFactory {
pub fn new(fs_provider: Arc<FsProvider>) -> Self {
Self { fs_provider }
}
}
impl Debug for FsReaderFactory {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "FsReaderFactory")
}
}
impl ParquetFileReaderFactory for FsReaderFactory {
fn create_reader(
&self,
partition_index: usize,
file_meta: FileMeta,
_metadata_size_hint: Option<usize>,
metrics: &ExecutionPlanMetricsSet,
) -> Result<Box<dyn AsyncFileReader + Send>> {
let reader = ParquetFileReaderRef(Arc::new(ParquetFileReader {
fs_provider: self.fs_provider.clone(),
input: OnceCell::new(),
metrics: ParquetFileMetrics::new(
partition_index,
file_meta
.object_meta
.location
.filename()
.unwrap_or("__default_filename__"),
metrics,
),
meta: file_meta.object_meta,
}));
Ok(Box::new(reader))
}
}
struct ParquetFileReader {
fs_provider: Arc<FsProvider>,
input: OnceCell<Arc<FsDataInputStream>>,
meta: ObjectMeta,
metrics: ParquetFileMetrics,
}
#[derive(Clone)]
struct ParquetFileReaderRef(Arc<ParquetFileReader>);
impl ParquetFileReader {
fn get_input(&self) -> datafusion::parquet::errors::Result<Arc<FsDataInputStream>> {
let input = self
.input
.get_or_try_init(|| {
let path = BASE64_URL_SAFE_NO_PAD
.decode(self.meta.location.filename().expect("missing filename"))
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
.or_else(|_| {
let filename = self.meta.location.filename();
df_execution_err!("cannot decode filename: {filename:?}")
})?;
let fs = self.fs_provider.provide(&path)?;
Ok(Arc::new(fs.open(&path)?))
})
.map_err(|e| ParquetError::External(e))?;
Ok(input.clone())
}
fn read_fully(&self, range: Range<usize>) -> Result<Bytes> {
let mut bytes = vec![0u8; range.len()];
self.get_input()?
.read_fully(range.start as u64, &mut bytes)?;
Ok(Bytes::from(bytes))
}
}
impl AsyncFileReader for ParquetFileReaderRef {
fn get_bytes(
&mut self,
range: Range<usize>,
) -> BoxFuture<'_, datafusion::parquet::errors::Result<Bytes>> {
let inner = self.0.clone();
inner.metrics.bytes_scanned.add(range.end - range.start);
async move {
tokio::task::spawn_blocking(move || {
inner
.read_fully(range)
.map_err(|e| ParquetError::External(Box::new(e)))
})
.await
.expect("tokio spawn_blocking error")
}
.boxed()
}
fn get_metadata(
&mut self,
) -> BoxFuture<'_, datafusion::parquet::errors::Result<Arc<ParquetMetaData>>> {
const METADATA_CACHE_SIZE: usize = 5; // TODO: make it configurable
type ParquetMetaDataSlot = tokio::sync::OnceCell<Arc<ParquetMetaData>>;
type ParquetMetaDataCacheTable = Vec<(ObjectMeta, ParquetMetaDataSlot)>;
static METADATA_CACHE: OnceCell<Mutex<ParquetMetaDataCacheTable>> = OnceCell::new();
let inner = self.0.clone();
let meta_size = inner.meta.size;
let size_hint = Some(1048576);
let cache_slot = (move || {
let mut metadata_cache = METADATA_CACHE.get_or_init(|| Mutex::new(Vec::new())).lock();
// find existed cache slot
for (cache_meta, cache_slot) in metadata_cache.iter() {
if cache_meta.location == self.0.meta.location {
return cache_slot.clone();
}
}
// reserve a new cache slot
if metadata_cache.len() >= METADATA_CACHE_SIZE {
metadata_cache.remove(0); // remove eldest
}
let cache_slot = ParquetMetaDataSlot::default();
metadata_cache.push((self.0.meta.clone(), cache_slot.clone()));
cache_slot
})();
// fetch metadata from file and update to cache
async move {
cache_slot
.get_or_try_init(move || async move {
fetch_parquet_metadata(
move |range| {
let inner = inner.clone();
inner.metrics.bytes_scanned.add(range.end - range.start);
async move {
tokio::task::spawn_blocking(move || {
inner
.read_fully(range)
.map_err(|e| ParquetError::External(Box::new(e)))
})
.await
.expect("tokio spawn_blocking error")
}
},
meta_size,
size_hint,
)
.await
.map(|parquet_metadata| Arc::new(parquet_metadata))
})
.map(|parquet_metadata| parquet_metadata.cloned())
.await
}
.boxed()
}
}