blob: a8adb46b96ffa8f812aeb6393ec389f8df52cc35 [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 writing data to [`DataSink`]s
use std::any::Any;
use std::fmt;
use std::fmt::Debug;
use std::sync::Arc;
use arrow::array::{ArrayRef, RecordBatch, UInt64Array};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion_common::{internal_err, Result};
use datafusion_execution::TaskContext;
use datafusion_physical_expr::{Distribution, EquivalenceProperties};
use datafusion_physical_expr_common::sort_expr::{LexRequirement, OrderingRequirements};
use datafusion_physical_plan::metrics::MetricsSet;
use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
use datafusion_physical_plan::{
execute_input_stream, DisplayAs, DisplayFormatType, ExecutionPlan,
ExecutionPlanProperties, Partitioning, PlanProperties, SendableRecordBatchStream,
};
use async_trait::async_trait;
use datafusion_physical_plan::execution_plan::{EvaluationType, SchedulingType};
use futures::StreamExt;
/// `DataSink` implements writing streams of [`RecordBatch`]es to
/// user defined destinations.
///
/// The `Display` impl is used to format the sink for explain plan
/// output.
#[async_trait]
pub trait DataSink: DisplayAs + Debug + Send + Sync {
/// Returns the data sink as [`Any`] so that it can be
/// downcast to a specific implementation.
fn as_any(&self) -> &dyn Any;
/// Return a snapshot of the [MetricsSet] for this
/// [DataSink].
///
/// See [ExecutionPlan::metrics()] for more details
fn metrics(&self) -> Option<MetricsSet> {
None
}
/// Returns the sink schema
fn schema(&self) -> &SchemaRef;
// TODO add desired input ordering
// How does this sink want its input ordered?
/// Writes the data to the sink, returns the number of values written
///
/// This method will be called exactly once during each DML
/// statement. Thus prior to return, the sink should do any commit
/// or rollback required.
async fn write_all(
&self,
data: SendableRecordBatchStream,
context: &Arc<TaskContext>,
) -> Result<u64>;
}
/// Execution plan for writing record batches to a [`DataSink`]
///
/// Returns a single row with the number of values written
#[derive(Clone)]
pub struct DataSinkExec {
/// Input plan that produces the record batches to be written.
input: Arc<dyn ExecutionPlan>,
/// Sink to which to write
sink: Arc<dyn DataSink>,
/// Schema describing the structure of the output data.
count_schema: SchemaRef,
/// Optional required sort order for output data.
sort_order: Option<LexRequirement>,
cache: PlanProperties,
}
impl Debug for DataSinkExec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DataSinkExec schema: {:?}", self.count_schema)
}
}
impl DataSinkExec {
/// Create a plan to write to `sink`
/// Note: DataSinkExec requires its input to have a single partition.
/// If the input has multiple partitions, the physical optimizer will
/// automatically insert a Merge-related operator to merge them.
/// If you construct PhysicalPlan without going through the physical optimizer,
/// you must ensure that the input has a single partition.
pub fn new(
input: Arc<dyn ExecutionPlan>,
sink: Arc<dyn DataSink>,
sort_order: Option<LexRequirement>,
) -> Self {
let count_schema = make_count_schema();
let cache = Self::create_schema(&input, count_schema);
Self {
input,
sink,
count_schema: make_count_schema(),
sort_order,
cache,
}
}
/// Input execution plan
pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
&self.input
}
/// Returns insert sink
pub fn sink(&self) -> &dyn DataSink {
self.sink.as_ref()
}
/// Optional sort order for output data
pub fn sort_order(&self) -> &Option<LexRequirement> {
&self.sort_order
}
fn create_schema(
input: &Arc<dyn ExecutionPlan>,
schema: SchemaRef,
) -> PlanProperties {
let eq_properties = EquivalenceProperties::new(schema);
PlanProperties::new(
eq_properties,
Partitioning::UnknownPartitioning(1),
input.pipeline_behavior(),
input.boundedness(),
)
.with_scheduling_type(SchedulingType::Cooperative)
.with_evaluation_type(EvaluationType::Eager)
}
}
impl DisplayAs for DataSinkExec {
fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
write!(f, "DataSinkExec: sink=")?;
self.sink.fmt_as(t, f)
}
DisplayFormatType::TreeRender => self.sink().fmt_as(t, f),
}
}
}
impl ExecutionPlan for DataSinkExec {
fn name(&self) -> &'static str {
"DataSinkExec"
}
/// Return a reference to Any that can be used for downcasting
fn as_any(&self) -> &dyn Any {
self
}
fn properties(&self) -> &PlanProperties {
&self.cache
}
fn benefits_from_input_partitioning(&self) -> Vec<bool> {
// DataSink is responsible for dynamically partitioning its
// own input at execution time.
vec![false]
}
fn required_input_distribution(&self) -> Vec<Distribution> {
// DataSink is responsible for dynamically partitioning its
// own input at execution time, and so requires a single input partition.
vec![Distribution::SinglePartition; self.children().len()]
}
fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
// The required input ordering is set externally (e.g. by a `ListingTable`).
// Otherwise, there is no specific requirement (i.e. `sort_order` is `None`).
vec![self.sort_order.as_ref().cloned().map(Into::into)]
}
fn maintains_input_order(&self) -> Vec<bool> {
// Maintains ordering in the sense that the written file will reflect
// the ordering of the input. For more context, see:
//
// https://github.com/apache/datafusion/pull/6354#discussion_r1195284178
vec![true]
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.input]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
Ok(Arc::new(Self::new(
Arc::clone(&children[0]),
Arc::clone(&self.sink),
self.sort_order.clone(),
)))
}
/// Execute the plan and return a stream of `RecordBatch`es for
/// the specified partition.
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
if partition != 0 {
return internal_err!("DataSinkExec can only be called on partition 0!");
}
let data = execute_input_stream(
Arc::clone(&self.input),
Arc::clone(self.sink.schema()),
0,
Arc::clone(&context),
)?;
let count_schema = Arc::clone(&self.count_schema);
let sink = Arc::clone(&self.sink);
let stream = futures::stream::once(async move {
sink.write_all(data, &context).await.map(make_count_batch)
})
.boxed();
Ok(Box::pin(RecordBatchStreamAdapter::new(
count_schema,
stream,
)))
}
/// Returns the metrics of the underlying [DataSink]
fn metrics(&self) -> Option<MetricsSet> {
self.sink.metrics()
}
}
/// Create a output record batch with a count
///
/// ```text
/// +-------+,
/// | count |,
/// +-------+,
/// | 6 |,
/// +-------+,
/// ```
fn make_count_batch(count: u64) -> RecordBatch {
let array = Arc::new(UInt64Array::from(vec![count])) as ArrayRef;
RecordBatch::try_from_iter_with_nullable(vec![("count", array, false)]).unwrap()
}
fn make_count_schema() -> SchemaRef {
// Define a schema.
Arc::new(Schema::new(vec![Field::new(
"count",
DataType::UInt64,
false,
)]))
}