blob: c33036a67c4ce2a09ee04bc719b68c8f7bfa3f24 [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.
//! This library's own execution plan node.
//!
//! A leaf, and deliberately so. A node with children hands them to the
//! framework to encode with the *host's* codec, which is the right thing but
//! means the interesting part of a codec -- what it writes down -- is somebody
//! else's problem. Everything this node needs to run lives in the node
//! itself: which files, which columns, how many rows. That is what
//! [`crate::codec`] writes to the wire, and it is why a plan built here can be
//! decoded in a process that has never seen this table registered.
//!
//! One output partition per file. That is the axis a distributed engine
//! splits along: partition `i` reads file `i` and nothing else, so two workers
//! never touch the same bytes and no coordination is needed.
use std::fmt;
use std::sync::Arc;
use arrow::datatypes::SchemaRef;
use datafusion::common::Result;
use datafusion::common::tree_node::TreeNodeRecursion;
use datafusion::datasource::listing::PartitionedFile;
use datafusion::datasource::physical_plan::{FileScanConfigBuilder, ParquetSource};
use datafusion::datasource::source::DataSourceExec;
use datafusion::execution::TaskContext;
use datafusion::execution::object_store::ObjectStoreUrl;
use datafusion::physical_expr::{EquivalenceProperties, Partitioning, PhysicalExpr};
use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
use datafusion::physical_plan::{
DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, SendableRecordBatchStream,
};
/// One Parquet file, and the size the object store will report for it.
///
/// The size travels with the path because `PartitionedFile` needs it up front
/// and a decoding process should not have to stat the file to rebuild a plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FileSlice {
pub(crate) path: String,
pub(crate) size: u64,
}
/// Scans a fixed list of Parquet files, one file per output partition.
#[derive(Debug)]
pub(crate) struct PartitionedParquetExec {
/// Output partition `i` reads `files[i]`.
pub(crate) files: Vec<FileSlice>,
/// The table's full schema, before projection.
pub(crate) table_schema: SchemaRef,
/// Column indices into `table_schema`, or `None` for all of them.
pub(crate) projection: Option<Vec<usize>>,
pub(crate) limit: Option<usize>,
properties: Arc<PlanProperties>,
}
impl PartitionedParquetExec {
pub(crate) fn new(
files: Vec<FileSlice>,
table_schema: SchemaRef,
projection: Option<Vec<usize>>,
limit: Option<usize>,
) -> Result<Self> {
let projected_schema = match projection.as_ref() {
Some(indices) => Arc::new(table_schema.project(indices)?),
None => Arc::clone(&table_schema),
};
// `UnknownPartitioning`, not `Hash`: the rows are split by which file
// they happen to live in, which says nothing about their values. A
// plan that claimed a hash partitioning here would let the optimizer
// skip a repartition it actually needs.
let properties = Arc::new(PlanProperties::new(
EquivalenceProperties::new(projected_schema),
Partitioning::UnknownPartitioning(files.len()),
EmissionType::Incremental,
Boundedness::Bounded,
));
Ok(Self {
files,
table_schema,
projection,
limit,
properties,
})
}
/// Build the stock scan for a single one of our files.
///
/// Reusing `DataSourceExec` for the actual reading is the point: this node
/// exists to own the *description* of the scan across a process boundary,
/// not to reimplement Parquet.
fn scan_for(&self, partition: usize) -> Result<Arc<DataSourceExec>> {
// `Internal`: `execute` is only ever called with a partition this node
// said it had, so an out-of-range one is a caller's bug rather than
// bad data. Contrast the payload errors in [`crate::codec`].
let slice = self.files.get(partition).ok_or_else(|| {
datafusion::common::internal_datafusion_err!(
"PartitionedParquetExec has {} partition(s), asked for {partition}",
self.files.len()
)
})?;
let source = Arc::new(ParquetSource::new(Arc::clone(&self.table_schema)));
let config = FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), source)
.with_file(PartitionedFile::new(slice.path.clone(), slice.size))
.with_projection_indices(self.projection.clone())?
.with_limit(self.limit)
.build();
Ok(DataSourceExec::from_data_source(config))
}
}
impl DisplayAs for PartitionedParquetExec {
fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PartitionedParquetExec: files={}", self.files.len())?;
if let Some(projection) = self.projection.as_ref() {
write!(f, ", projection={projection:?}")?;
}
if let Some(limit) = self.limit {
write!(f, ", limit={limit}")?;
}
Ok(())
}
}
impl ExecutionPlan for PartitionedParquetExec {
fn name(&self) -> &str {
Self::static_name()
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.properties
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![]
}
fn apply_expressions(
&self,
_f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
// The projection is column indices, not expressions, and any pushed
// down filter is held by the `DataSourceExec` this node builds.
Ok(TreeNodeRecursion::Continue)
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
if !children.is_empty() {
return datafusion::common::internal_err!(
"PartitionedParquetExec is a leaf, got {} children",
children.len()
);
}
Ok(self)
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
// Partition 0 of the single-file scan: each of our partitions is one
// whole file, so the inner scan only ever has one of its own.
self.scan_for(partition)?.execute(0, context)
}
}