Skip to main content

datafusion_datasource/
sink.rs

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//! Execution plan for writing data to [`DataSink`]s
19
20use std::any::Any;
21use std::fmt;
22use std::fmt::Debug;
23use std::sync::Arc;
24
25use arrow::array::{ArrayRef, RecordBatch, UInt64Array};
26use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
27use datafusion_common::tree_node::TreeNodeRecursion;
28use datafusion_common::{Result, assert_eq_or_internal_err};
29use datafusion_execution::TaskContext;
30use datafusion_physical_expr::{Distribution, EquivalenceProperties, PhysicalExpr};
31use datafusion_physical_expr_common::sort_expr::{LexRequirement, OrderingRequirements};
32use datafusion_physical_plan::metrics::MetricsSet;
33use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
34use datafusion_physical_plan::{
35    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan,
36    ExecutionPlanProperties, InputDistributionRequirements, Partitioning, PlanProperties,
37    ReplaceChildrenOptions, SendableRecordBatchStream, execute_input_stream,
38};
39
40use async_trait::async_trait;
41use datafusion_physical_plan::execution_plan::{EvaluationType, SchedulingType};
42use futures::StreamExt;
43
44/// `DataSink` implements writing streams of [`RecordBatch`]es to
45/// user defined destinations.
46///
47/// The `Display` impl is used to format the sink for explain plan
48/// output.
49#[async_trait]
50pub trait DataSink: Any + DisplayAs + Debug + Send + Sync {
51    /// Return a snapshot of the [MetricsSet] for this
52    /// [DataSink].
53    ///
54    /// See [ExecutionPlan::metrics()] for more details
55    fn metrics(&self) -> Option<MetricsSet> {
56        None
57    }
58
59    /// Returns the sink schema
60    fn schema(&self) -> &SchemaRef;
61
62    // TODO add desired input ordering
63    // How does this sink want its input ordered?
64
65    /// Writes the data to the sink, returns the number of values written
66    ///
67    /// This method will be called exactly once during each DML
68    /// statement. Thus prior to return, the sink should do any commit
69    /// or rollback required.
70    async fn write_all(
71        &self,
72        data: SendableRecordBatchStream,
73        context: &Arc<TaskContext>,
74    ) -> Result<u64>;
75
76    /// Serialize this sink into a full protobuf plan node, if it knows how.
77    ///
78    /// Implementations can use `ctx` to encode the input plan, sink-specific
79    /// expressions, and [`DataSinkExec::encode_sort_order`].
80    ///
81    /// Returning `Ok(None)` lets the caller try its extension codec instead.
82    #[cfg(feature = "proto")]
83    fn try_to_proto(
84        &self,
85        _exec: &DataSinkExec,
86        _ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
87    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
88        Ok(None)
89    }
90}
91
92impl dyn DataSink {
93    /// Returns true if the inner type is `T`.
94    pub fn is<T: DataSink>(&self) -> bool {
95        (self as &dyn Any).is::<T>()
96    }
97
98    /// Returns a reference to the inner value as the type `T` if it is of that type.
99    pub fn downcast_ref<T: DataSink>(&self) -> Option<&T> {
100        (self as &dyn Any).downcast_ref()
101    }
102}
103
104/// Execution plan for writing record batches to a [`DataSink`]
105///
106/// Returns a single row with the number of values written
107#[derive(Clone)]
108pub struct DataSinkExec {
109    /// Input plan that produces the record batches to be written.
110    input: Arc<dyn ExecutionPlan>,
111    /// Sink to which to write
112    sink: Arc<dyn DataSink>,
113    /// Schema describing the structure of the output data.
114    count_schema: SchemaRef,
115    /// Optional required sort order for output data.
116    sort_order: Option<LexRequirement>,
117    cache: Arc<PlanProperties>,
118}
119
120impl Debug for DataSinkExec {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        write!(f, "DataSinkExec schema: {}", self.count_schema)
123    }
124}
125
126impl DataSinkExec {
127    /// Create a plan to write to `sink`
128    /// Note: DataSinkExec requires its input to have a single partition.
129    /// If the input has multiple partitions, the physical optimizer will
130    /// automatically insert a Merge-related operator to merge them.
131    /// If you construct PhysicalPlan without going through the physical optimizer,
132    /// you must ensure that the input has a single partition.
133    pub fn new(
134        input: Arc<dyn ExecutionPlan>,
135        sink: Arc<dyn DataSink>,
136        sort_order: Option<LexRequirement>,
137    ) -> Self {
138        let count_schema = make_count_schema();
139        let cache = Self::create_schema(&input, count_schema);
140        Self {
141            input,
142            sink,
143            count_schema: make_count_schema(),
144            sort_order,
145            cache: Arc::new(cache),
146        }
147    }
148
149    /// Input execution plan
150    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
151        &self.input
152    }
153
154    /// Returns insert sink
155    pub fn sink(&self) -> &dyn DataSink {
156        self.sink.as_ref()
157    }
158
159    /// Optional sort order for output data
160    pub fn sort_order(&self) -> &Option<LexRequirement> {
161        &self.sort_order
162    }
163
164    /// Encode the optional sink ordering for a protobuf plan node.
165    #[cfg(feature = "proto")]
166    pub fn encode_sort_order(
167        &self,
168        ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
169    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalSortExprNodeCollection>>
170    {
171        use datafusion_physical_expr::PhysicalSortExpr;
172        use datafusion_proto_models::protobuf;
173
174        self.sort_order
175            .as_ref()
176            .map(|requirements| {
177                requirements
178                    .iter()
179                    .map(|requirement| {
180                        let expr: PhysicalSortExpr = requirement.to_owned().into();
181                        Ok(protobuf::PhysicalSortExprNode {
182                            expr: Some(Box::new(ctx.encode_expr(&expr.expr)?)),
183                            asc: !expr.options.descending,
184                            nulls_first: expr.options.nulls_first,
185                        })
186                    })
187                    .collect::<Result<Vec<_>>>()
188                    .map(|physical_sort_expr_nodes| {
189                        protobuf::PhysicalSortExprNodeCollection {
190                            physical_sort_expr_nodes,
191                        }
192                    })
193            })
194            .transpose()
195    }
196
197    /// Decode the optional sink ordering from a protobuf plan node.
198    #[cfg(feature = "proto")]
199    pub fn decode_sort_order(
200        collection: Option<
201            &datafusion_proto_models::protobuf::PhysicalSortExprNodeCollection,
202        >,
203        ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
204        schema: &Schema,
205    ) -> Result<Option<LexRequirement>> {
206        use arrow::compute::SortOptions;
207        use datafusion_physical_expr::PhysicalSortExpr;
208
209        let Some(collection) = collection else {
210            return Ok(None);
211        };
212        let sort_exprs = collection
213            .physical_sort_expr_nodes
214            .iter()
215            .map(|node| {
216                let expr = node.expr.as_ref().ok_or_else(|| {
217                    datafusion_common::internal_datafusion_err!(
218                        "Unexpected empty physical expression"
219                    )
220                })?;
221                Ok(PhysicalSortExpr {
222                    expr: ctx.decode_expr(expr, schema)?,
223                    options: SortOptions {
224                        descending: !node.asc,
225                        nulls_first: node.nulls_first,
226                    },
227                })
228            })
229            .collect::<Result<Vec<_>>>()?;
230        Ok(LexRequirement::new(sort_exprs.into_iter().map(Into::into)))
231    }
232
233    fn create_schema(
234        input: &Arc<dyn ExecutionPlan>,
235        schema: SchemaRef,
236    ) -> PlanProperties {
237        let eq_properties = EquivalenceProperties::new(schema);
238        PlanProperties::new(
239            eq_properties,
240            Partitioning::UnknownPartitioning(1),
241            input.pipeline_behavior(),
242            input.boundedness(),
243        )
244        .with_scheduling_type(SchedulingType::Cooperative)
245        .with_evaluation_type(EvaluationType::Eager)
246    }
247}
248
249impl DisplayAs for DataSinkExec {
250    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
251        match t {
252            DisplayFormatType::Default | DisplayFormatType::Verbose => {
253                write!(f, "DataSinkExec: sink=")?;
254                self.sink.fmt_as(t, f)
255            }
256            DisplayFormatType::TreeRender => self.sink().fmt_as(t, f),
257        }
258    }
259}
260
261impl ExecutionPlan for DataSinkExec {
262    fn name(&self) -> &'static str {
263        "DataSinkExec"
264    }
265
266    /// Return a reference to Any that can be used for downcasting
267    fn properties(&self) -> &Arc<PlanProperties> {
268        &self.cache
269    }
270
271    fn benefits_from_input_partitioning(&self) -> Vec<bool> {
272        // DataSink is responsible for dynamically partitioning its
273        // own input at execution time.
274        vec![false]
275    }
276
277    fn required_input_distribution(&self) -> Vec<Distribution> {
278        self.input_distribution_requirements().into_per_child()
279    }
280
281    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
282        // DataSink is responsible for dynamically partitioning its
283        // own input at execution time, and so requires a single input partition.
284        InputDistributionRequirements::new(vec![
285            Distribution::SinglePartition;
286            self.children().len()
287        ])
288    }
289
290    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
291        // The required input ordering is set externally (e.g. by a `ListingTable`).
292        // Otherwise, there is no specific requirement (i.e. `sort_order` is `None`).
293        vec![self.sort_order.as_ref().cloned().map(Into::into)]
294    }
295
296    fn maintains_input_order(&self) -> Vec<bool> {
297        // Maintains ordering in the sense that the written file will reflect
298        // the ordering of the input. For more context, see:
299        //
300        // https://github.com/apache/datafusion/pull/6354#discussion_r1195284178
301        vec![true]
302    }
303
304    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
305        vec![&self.input]
306    }
307
308    fn replace_children(
309        self: Arc<Self>,
310        children: Vec<Arc<dyn ExecutionPlan>>,
311        _: ReplaceChildrenOptions,
312    ) -> Result<Arc<dyn ExecutionPlan>> {
313        Ok(Arc::new(Self::new(
314            Arc::clone(&children[0]),
315            Arc::clone(&self.sink),
316            self.sort_order.clone(),
317        )))
318    }
319
320    fn with_new_children(
321        self: Arc<Self>,
322        children: Vec<Arc<dyn ExecutionPlan>>,
323    ) -> Result<Arc<dyn ExecutionPlan>> {
324        self.replace_children(
325            children,
326            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
327        )
328    }
329
330    fn apply_expressions(
331        &self,
332        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
333    ) -> Result<TreeNodeRecursion> {
334        Ok(TreeNodeRecursion::Continue)
335    }
336
337    /// Execute the plan and return a stream of `RecordBatch`es for
338    /// the specified partition.
339    fn execute(
340        &self,
341        partition: usize,
342        context: Arc<TaskContext>,
343    ) -> Result<SendableRecordBatchStream> {
344        assert_eq_or_internal_err!(
345            partition,
346            0,
347            "DataSinkExec can only be called on partition 0!"
348        );
349        let data = execute_input_stream(
350            Arc::clone(&self.input),
351            Arc::clone(self.sink.schema()),
352            0,
353            Arc::clone(&context),
354        )?;
355
356        let count_schema = Arc::clone(&self.count_schema);
357        let sink = Arc::clone(&self.sink);
358
359        let stream = futures::stream::once(async move {
360            sink.write_all(data, &context).await.map(make_count_batch)
361        })
362        .boxed();
363
364        Ok(Box::pin(RecordBatchStreamAdapter::new(
365            count_schema,
366            stream,
367        )))
368    }
369
370    /// Returns the metrics of the underlying [DataSink]
371    fn metrics(&self) -> Option<MetricsSet> {
372        self.sink.metrics()
373    }
374
375    /// Delegates protobuf serialization to the underlying sink.
376    #[cfg(feature = "proto")]
377    fn try_to_proto(
378        &self,
379        ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
380    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
381        self.sink().try_to_proto(self, ctx)
382    }
383}
384
385/// Create a output record batch with a count
386///
387/// ```text
388/// +-------+,
389/// | count |,
390/// +-------+,
391/// | 6     |,
392/// +-------+,
393/// ```
394fn make_count_batch(count: u64) -> RecordBatch {
395    let array = Arc::new(UInt64Array::from(vec![count])) as ArrayRef;
396
397    RecordBatch::try_from_iter_with_nullable(vec![("count", array, false)]).unwrap()
398}
399
400fn make_count_schema() -> SchemaRef {
401    // Define a schema.
402    Arc::new(Schema::new(vec![Field::new(
403        "count",
404        DataType::UInt64,
405        false,
406    )]))
407}