Skip to main content

datafusion_physical_plan/
async_func.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
18use crate::coalesce::LimitedBatchCoalescer;
19use crate::metrics::{ExecutionPlanMetricsSet, MetricsSet};
20use crate::stream::{EmptyRecordBatchStream, RecordBatchStreamAdapter};
21use crate::{
22    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan,
23    ExecutionPlanProperties, PlanProperties, ReplaceChildrenOptions,
24    validate_child_count,
25};
26use arrow::array::RecordBatch;
27use arrow_schema::{FieldRef, Fields, Schema, SchemaRef};
28use datafusion_common::Result;
29use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
30use datafusion_execution::{RecordBatchStream, SendableRecordBatchStream, TaskContext};
31use datafusion_physical_expr::ScalarFunctionExpr;
32use datafusion_physical_expr::async_scalar_function::AsyncFuncExpr;
33use datafusion_physical_expr::equivalence::ProjectionMapping;
34use datafusion_physical_expr::expressions::Column;
35use datafusion_physical_expr_common::metrics::{BaselineMetrics, RecordOutput};
36use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
37use futures::Stream;
38use futures::stream::StreamExt;
39use log::trace;
40use std::pin::Pin;
41use std::sync::Arc;
42use std::task::{Context, Poll, ready};
43
44/// This structure evaluates a set of async expressions on a record
45/// batch producing a new record batch
46///
47/// The schema of the output of the AsyncFuncExec is:
48/// Input columns followed by one column for each async expression
49#[derive(Debug, Clone)]
50pub struct AsyncFuncExec {
51    /// The async expressions to evaluate
52    async_exprs: Vec<Arc<AsyncFuncExpr>>,
53    input: Arc<dyn ExecutionPlan>,
54    cache: Arc<PlanProperties>,
55    metrics: ExecutionPlanMetricsSet,
56}
57
58impl AsyncFuncExec {
59    pub fn try_new(
60        async_exprs: Vec<Arc<AsyncFuncExpr>>,
61        input: Arc<dyn ExecutionPlan>,
62    ) -> Result<Self> {
63        let async_fields = async_exprs
64            .iter()
65            .map(|async_expr| async_expr.return_field(input.schema().as_ref()))
66            .collect::<Result<Vec<FieldRef>>>()?;
67
68        // compute the output schema: input schema then async expressions
69        let fields: Fields = input
70            .schema()
71            .fields()
72            .iter()
73            .cloned()
74            .chain(async_fields)
75            .collect();
76
77        let schema = Arc::new(Schema::new(fields));
78        let tuples = async_exprs
79            .iter()
80            .map(|expr| (Arc::clone(&expr.func), expr.name().to_string()))
81            .collect::<Vec<_>>();
82        let async_expr_mapping = ProjectionMapping::try_new(tuples, &input.schema())?;
83        let cache =
84            AsyncFuncExec::compute_properties(&input, schema, &async_expr_mapping)?;
85        Ok(Self {
86            input,
87            async_exprs,
88            cache: Arc::new(cache),
89            metrics: ExecutionPlanMetricsSet::new(),
90        })
91    }
92
93    /// This function creates the cache object that stores the plan properties
94    /// such as schema, equivalence properties, ordering, partitioning, etc.
95    fn compute_properties(
96        input: &Arc<dyn ExecutionPlan>,
97        schema: SchemaRef,
98        async_expr_mapping: &ProjectionMapping,
99    ) -> Result<PlanProperties> {
100        Ok(PlanProperties::new(
101            input
102                .equivalence_properties()
103                .project(async_expr_mapping, schema),
104            input.output_partitioning().clone(),
105            input.pipeline_behavior(),
106            input.boundedness(),
107        ))
108    }
109
110    #[deprecated(
111        since = "55.0.0",
112        note = "unused by DataFusion; `AsyncFuncExec` serializes itself via `AsyncFuncExec::try_to_proto`, which reads the field directly. There is no replacement; please open an issue if you have a use case for it."
113    )]
114    pub fn async_exprs(&self) -> &[Arc<AsyncFuncExpr>] {
115        &self.async_exprs
116    }
117
118    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
119        &self.input
120    }
121}
122
123impl DisplayAs for AsyncFuncExec {
124    fn fmt_as(
125        &self,
126        t: DisplayFormatType,
127        f: &mut std::fmt::Formatter,
128    ) -> std::fmt::Result {
129        let expr: Vec<String> = self
130            .async_exprs
131            .iter()
132            .map(|async_expr| async_expr.to_string())
133            .collect();
134        let exprs = expr.join(", ");
135        match t {
136            DisplayFormatType::Default | DisplayFormatType::Verbose => {
137                write!(f, "AsyncFuncExec: async_expr=[{exprs}]")
138            }
139            DisplayFormatType::TreeRender => {
140                writeln!(f, "format=async_expr")?;
141                writeln!(f, "async_expr={exprs}")?;
142                Ok(())
143            }
144        }
145    }
146}
147
148impl ExecutionPlan for AsyncFuncExec {
149    fn name(&self) -> &str {
150        "async_func"
151    }
152
153    fn properties(&self) -> &Arc<PlanProperties> {
154        &self.cache
155    }
156
157    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
158        vec![&self.input]
159    }
160
161    fn apply_expressions(
162        &self,
163        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
164    ) -> Result<TreeNodeRecursion> {
165        crate::apply_expression_roots(
166            self.async_exprs
167                .iter()
168                .cloned()
169                .map(|expr| expr as Arc<dyn PhysicalExpr>),
170            f,
171        )
172    }
173
174    fn replace_children(
175        self: Arc<Self>,
176        mut children: Vec<Arc<dyn ExecutionPlan>>,
177        options: ReplaceChildrenOptions,
178    ) -> Result<Arc<dyn ExecutionPlan>> {
179        validate_child_count!(self, children);
180        match options.children_properties {
181            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
182                input: children.swap_remove(0),
183                metrics: ExecutionPlanMetricsSet::new(),
184                ..Self::clone(&*self)
185            })),
186            ChildrenPropertiesMode::Recompute => Ok(Arc::new(AsyncFuncExec::try_new(
187                self.async_exprs.clone(),
188                children.swap_remove(0),
189            )?)),
190        }
191    }
192
193    fn with_new_children(
194        self: Arc<Self>,
195        children: Vec<Arc<dyn ExecutionPlan>>,
196    ) -> Result<Arc<dyn ExecutionPlan>> {
197        self.replace_children(
198            children,
199            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
200        )
201    }
202
203    fn with_new_children_and_same_properties(
204        self: Arc<Self>,
205        children: Vec<Arc<dyn ExecutionPlan>>,
206    ) -> Result<Arc<dyn ExecutionPlan>> {
207        self.replace_children(
208            children,
209            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
210        )
211    }
212
213    fn execute(
214        &self,
215        partition: usize,
216        context: Arc<TaskContext>,
217    ) -> Result<SendableRecordBatchStream> {
218        trace!(
219            "Start AsyncFuncExpr::execute for partition {} of context session_id {} and task_id {:?}",
220            partition,
221            context.session_id(),
222            context.task_id()
223        );
224
225        // first execute the input stream
226        let input_stream = self.input.execute(partition, Arc::clone(&context))?;
227
228        // TODO: Track `elapsed_compute` in `BaselineMetrics`
229        // Issue: <https://github.com/apache/datafusion/issues/19658>
230        let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
231
232        // now, for each record batch, evaluate the async expressions and add the columns to the result
233        let async_exprs_captured = Arc::new(self.async_exprs.clone());
234        let schema_captured = self.schema();
235        let config_options_ref = Arc::clone(context.session_config().options());
236
237        let coalesced_input_stream = CoalesceInputStream {
238            input_stream,
239            batch_coalescer: LimitedBatchCoalescer::new(
240                Arc::clone(&self.input.schema()),
241                config_options_ref.execution.batch_size.get(),
242                None,
243            ),
244        };
245
246        let stream_with_async_functions = coalesced_input_stream.then(move |batch| {
247            // need to clone *again* to capture the async_exprs and schema in the
248            // stream and satisfy lifetime requirements.
249            let async_exprs_captured = Arc::clone(&async_exprs_captured);
250            let schema_captured = Arc::clone(&schema_captured);
251            let config_options = Arc::clone(&config_options_ref);
252            let baseline_metrics_captured = baseline_metrics.clone();
253
254            async move {
255                let batch = batch?;
256                // append the result of evaluating the async expressions to the output
257                let mut output_arrays = batch.columns().to_vec();
258                for async_expr in async_exprs_captured.iter() {
259                    let output = async_expr
260                        .invoke_with_args(&batch, Arc::clone(&config_options))
261                        .await?;
262                    output_arrays.push(output.to_array(batch.num_rows())?);
263                }
264                let batch = RecordBatch::try_new(schema_captured, output_arrays)?;
265
266                Ok(batch.record_output(&baseline_metrics_captured))
267            }
268        });
269
270        // Adapt the stream with the output schema
271        let adapter =
272            RecordBatchStreamAdapter::new(self.schema(), stream_with_async_functions);
273        Ok(Box::pin(adapter))
274    }
275
276    fn metrics(&self) -> Option<MetricsSet> {
277        Some(self.metrics.clone_inner())
278    }
279
280    #[cfg(feature = "proto")]
281    fn try_to_proto(
282        &self,
283        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
284    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
285        use datafusion_proto_models::protobuf;
286
287        // Exhaustive destructure: adding a field to `AsyncFuncExec` without
288        // deciding how it is serialized is a compile error, not a silent
289        // round-trip gap.
290        let Self {
291            async_exprs,
292            input,
293            // Derived at construction by `AsyncFuncExec::compute_properties`.
294            cache: _,
295            // Runtime execution state, rebuilt empty on decode.
296            metrics: _,
297        } = self;
298
299        let input = ctx.encode_child(input)?;
300        let async_expr_names = async_exprs.iter().map(|e| e.name().to_string()).collect();
301        let async_exprs = ctx.encode_expressions(async_exprs.iter().map(|e| &e.func))?;
302        Ok(Some(protobuf::PhysicalPlanNode {
303            physical_plan_type: Some(
304                protobuf::physical_plan_node::PhysicalPlanType::AsyncFunc(Box::new(
305                    protobuf::AsyncFuncExecNode {
306                        input: Some(Box::new(input)),
307                        async_exprs,
308                        async_expr_names,
309                    },
310                )),
311            ),
312        }))
313    }
314}
315
316#[cfg(feature = "proto")]
317impl AsyncFuncExec {
318    /// Reconstruct an [`AsyncFuncExec`] from its protobuf representation.
319    ///
320    /// The exact inverse of [`ExecutionPlan::try_to_proto`]: it takes the whole
321    /// [`PhysicalPlanNode`] so every plan's `try_from_proto` shares one
322    /// signature. Child plans and expressions are decoded recursively via the
323    /// [`ExecutionPlanDecodeCtx`].
324    ///
325    /// [`PhysicalPlanNode`]: datafusion_proto_models::protobuf::PhysicalPlanNode
326    /// [`ExecutionPlan::try_to_proto`]: crate::ExecutionPlan::try_to_proto
327    /// [`ExecutionPlanDecodeCtx`]: crate::proto::ExecutionPlanDecodeCtx
328    pub fn try_from_proto(
329        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
330        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
331    ) -> Result<Arc<dyn ExecutionPlan>> {
332        use datafusion_common::assert_eq_or_internal_err;
333        use datafusion_proto_models::protobuf;
334        let async_func = crate::expect_plan_variant!(
335            node,
336            protobuf::physical_plan_node::PhysicalPlanType::AsyncFunc,
337            "AsyncFuncExec",
338        );
339        // Exhaustive destructure: a new field on `AsyncFuncExecNode` is a
340        // compile error here rather than a silently ignored wire field.
341        let protobuf::AsyncFuncExecNode {
342            input,
343            async_exprs,
344            async_expr_names,
345        } = async_func.as_ref();
346
347        let input =
348            ctx.decode_required_child(input.as_deref(), "AsyncFuncExec", "input")?;
349        let input_schema = input.schema();
350        assert_eq_or_internal_err!(
351            async_exprs.len(),
352            async_expr_names.len(),
353            "AsyncFuncExecNode async_exprs length does not match async_expr_names"
354        );
355        let async_exprs = async_exprs
356            .iter()
357            .zip(async_expr_names.iter())
358            .map(|(expr, name)| {
359                let physical_expr = ctx.decode_expr(expr, input_schema.as_ref())?;
360                Ok(Arc::new(AsyncFuncExpr::try_new(
361                    name.clone(),
362                    physical_expr,
363                    input_schema.as_ref(),
364                )?))
365            })
366            .collect::<Result<Vec<_>>>()?;
367        Ok(Arc::new(AsyncFuncExec::try_new(async_exprs, input)?))
368    }
369}
370
371struct CoalesceInputStream {
372    input_stream: Pin<Box<dyn RecordBatchStream + Send>>,
373    batch_coalescer: LimitedBatchCoalescer,
374}
375
376impl Stream for CoalesceInputStream {
377    type Item = Result<RecordBatch>;
378
379    fn poll_next(
380        mut self: Pin<&mut Self>,
381        cx: &mut Context<'_>,
382    ) -> Poll<Option<Self::Item>> {
383        let mut completed = false;
384
385        loop {
386            if let Some(batch) = self.batch_coalescer.next_completed_batch() {
387                return Poll::Ready(Some(Ok(batch)));
388            }
389
390            if completed {
391                return Poll::Ready(None);
392            }
393
394            match ready!(self.input_stream.poll_next_unpin(cx)) {
395                Some(Ok(batch)) => {
396                    if let Err(err) = self.batch_coalescer.push_batch(batch) {
397                        return Poll::Ready(Some(Err(err)));
398                    }
399                }
400                Some(err) => {
401                    return Poll::Ready(Some(err));
402                }
403                None => {
404                    completed = true;
405                    // Release the input pipeline's resources.
406                    let input_schema = self.input_stream.schema();
407                    self.input_stream =
408                        Box::pin(EmptyRecordBatchStream::new(input_schema));
409                    if let Err(err) = self.batch_coalescer.finish() {
410                        return Poll::Ready(Some(Err(err)));
411                    }
412                }
413            }
414        }
415    }
416}
417
418const ASYNC_FN_PREFIX: &str = "__async_fn_";
419
420/// Maps async_expressions to new columns
421///
422/// The output of the async functions are appended, in order, to the end of the input schema
423#[derive(Debug)]
424pub struct AsyncMapper {
425    /// the number of columns in the input plan
426    /// used to generate the output column names.
427    /// the first async expr is `__async_fn_0`, the second is `__async_fn_1`, etc
428    num_input_columns: usize,
429    /// the expressions to map
430    pub async_exprs: Vec<Arc<AsyncFuncExpr>>,
431}
432
433impl AsyncMapper {
434    pub fn new(num_input_columns: usize) -> Self {
435        Self {
436            num_input_columns,
437            async_exprs: Vec::new(),
438        }
439    }
440
441    pub fn is_empty(&self) -> bool {
442        self.async_exprs.is_empty()
443    }
444
445    pub fn next_column_name(&self) -> String {
446        format!("{}{}", ASYNC_FN_PREFIX, self.async_exprs.len())
447    }
448
449    /// Finds any references to async functions in the expression and adds them to the map
450    pub fn find_references(
451        &mut self,
452        physical_expr: &Arc<dyn PhysicalExpr>,
453        schema: &Schema,
454    ) -> Result<()> {
455        // recursively look for references to async functions
456        physical_expr.apply(|expr| {
457            if let Some(scalar_func_expr) = expr.downcast_ref::<ScalarFunctionExpr>()
458                && scalar_func_expr.fun().as_async().is_some()
459            {
460                let next_name = self.next_column_name();
461                self.async_exprs.push(Arc::new(AsyncFuncExpr::try_new(
462                    next_name,
463                    Arc::clone(expr),
464                    schema,
465                )?));
466            }
467            Ok(TreeNodeRecursion::Continue)
468        })?;
469        Ok(())
470    }
471
472    /// If the expression matches any of the async functions, return the new column
473    pub fn map_expr(
474        &self,
475        expr: Arc<dyn PhysicalExpr>,
476    ) -> Transformed<Arc<dyn PhysicalExpr>> {
477        // find the first matching async function if any
478        let Some(idx) =
479            self.async_exprs
480                .iter()
481                .enumerate()
482                .find_map(|(idx, async_expr)| {
483                    if async_expr.func == Arc::clone(&expr) {
484                        Some(idx)
485                    } else {
486                        None
487                    }
488                })
489        else {
490            return Transformed::no(expr);
491        };
492        // rewrite in terms of the output column
493        Transformed::yes(self.output_column(idx))
494    }
495
496    /// return the output column for the async function at index idx
497    pub fn output_column(&self, idx: usize) -> Arc<dyn PhysicalExpr> {
498        let async_expr = &self.async_exprs[idx];
499        let output_idx = self.num_input_columns + idx;
500        Arc::new(Column::new(async_expr.name(), output_idx))
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use std::sync::Arc;
507
508    use arrow::array::{RecordBatch, UInt32Array};
509    use arrow_schema::{DataType, Field, Schema};
510    use datafusion_common::Result;
511    use datafusion_execution::{TaskContext, config::SessionConfig};
512    use futures::StreamExt;
513
514    use crate::{ExecutionPlan, async_func::AsyncFuncExec, test::TestMemoryExec};
515
516    #[tokio::test]
517    async fn test_async_fn_with_coalescing() -> Result<()> {
518        let schema =
519            Arc::new(Schema::new(vec![Field::new("c0", DataType::UInt32, false)]));
520
521        let batch = RecordBatch::try_new(
522            Arc::clone(&schema),
523            vec![Arc::new(UInt32Array::from(vec![1, 2, 3, 4, 5, 6]))],
524        )?;
525
526        let batches: Vec<RecordBatch> = std::iter::repeat_n(batch, 50).collect();
527
528        let session_config = SessionConfig::new().with_batch_size(200);
529        let task_ctx = TaskContext::default().with_session_config(session_config);
530        let task_ctx = Arc::new(task_ctx);
531
532        let test_exec =
533            TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
534        let exec = AsyncFuncExec::try_new(vec![], test_exec)?;
535
536        let mut stream = exec.execute(0, Arc::clone(&task_ctx))?;
537        let batch = stream
538            .next()
539            .await
540            .expect("expected to get a record batch")?;
541        assert_eq!(200, batch.num_rows());
542        let batch = stream
543            .next()
544            .await
545            .expect("expected to get a record batch")?;
546        assert_eq!(100, batch.num_rows());
547
548        Ok(())
549    }
550}