Skip to main content

polars_mem_engine/planner/
lp.rs

1use polars_core::prelude::*;
2use polars_core::runtime::RAYON;
3use polars_expr::state::ExecutionState;
4use polars_plan::plans::expr_ir::ExprIR;
5use polars_plan::prelude::sink::CallbackSinkType;
6use polars_utils::unique_id::UniqueId;
7use rayon::iter::{IndexedParallelIterator as _, IntoParallelIterator as _, ParallelIterator as _};
8use recursive::recursive;
9
10#[cfg(feature = "python")]
11use self::python_dsl::PythonScanSource;
12use super::*;
13use crate::executors::{self, CachePrefiller, Executor, GroupByStreamingExec, SinkExecutor};
14use crate::scan_predicate::functions::create_scan_predicate;
15
16pub type StreamingExecutorBuilder =
17    fn(Node, &mut Arena<IR>, &mut Arena<AExpr>) -> PolarsResult<Box<dyn Executor>>;
18
19fn partitionable_gb(
20    keys: &[ExprIR],
21    aggs: &[ExprIR],
22    input_schema: &Schema,
23    expr_arena: &Arena<AExpr>,
24    apply: &Option<PlanCallback<DataFrame, DataFrame>>,
25) -> bool {
26    // checks:
27    //      1. complex expressions in the group_by itself are also not partitionable
28    //          in this case anything more than col("foo")
29    //      2. a custom function cannot be partitioned
30    //      3. we don't bother with more than 2 keys, as the cardinality likely explodes
31    //         by the combinations
32    if !keys.is_empty() && keys.len() < 3 && apply.is_none() {
33        // complex expressions in the group_by itself are also not partitionable
34        // in this case anything more than col("foo")
35        for key in keys {
36            if (expr_arena).iter(key.node()).count() > 1
37                || has_aexpr(key.node(), expr_arena, |ae| match ae {
38                    AExpr::Literal(lv) => !lv.is_scalar(),
39                    _ => false,
40                })
41            {
42                return false;
43            }
44        }
45
46        can_pre_agg_exprs(aggs, expr_arena, input_schema)
47    } else {
48        false
49    }
50}
51
52#[derive(Clone)]
53struct ConversionState {
54    has_cache_child: bool,
55    has_cache_parent: bool,
56}
57
58impl ConversionState {
59    fn new() -> PolarsResult<Self> {
60        Ok(ConversionState {
61            has_cache_child: false,
62            has_cache_parent: false,
63        })
64    }
65
66    fn with_new_branch<K, F: FnOnce(&mut Self) -> K>(&mut self, func: F) -> K {
67        let mut new_state = self.clone();
68        new_state.has_cache_child = false;
69        let out = func(&mut new_state);
70        self.has_cache_child = new_state.has_cache_child;
71        out
72    }
73}
74
75pub fn create_physical_plan(
76    root: Node,
77    lp_arena: &mut Arena<IR>,
78    expr_arena: &mut Arena<AExpr>,
79    build_streaming_executor: Option<StreamingExecutorBuilder>,
80) -> PolarsResult<Box<dyn Executor>> {
81    let mut state = ConversionState::new()?;
82    let mut cache_nodes = Default::default();
83    let plan = create_physical_plan_impl(
84        root,
85        lp_arena,
86        expr_arena,
87        &mut state,
88        &mut cache_nodes,
89        build_streaming_executor,
90    )?;
91
92    if cache_nodes.is_empty() {
93        Ok(plan)
94    } else {
95        Ok(Box::new(CachePrefiller {
96            caches: cache_nodes,
97            phys_plan: plan,
98        }))
99    }
100}
101
102pub struct MultiplePhysicalPlans {
103    pub cache_prefiller: Option<Box<dyn Executor>>,
104    pub physical_plans: Vec<Box<dyn Executor>>,
105}
106
107impl MultiplePhysicalPlans {
108    pub fn execute(mut self) -> PolarsResult<Vec<DataFrame>> {
109        let mut state = ExecutionState::new();
110        if let Some(mut cache_prefiller) = self.cache_prefiller {
111            cache_prefiller.execute(&mut state)?;
112        }
113        // Chunked iter to avoid rayon stack overflow.
114        let out = RAYON.install(|| {
115            self.physical_plans
116                .chunks_mut(RAYON.current_num_threads() * 3)
117                .map(|chunk| {
118                    chunk
119                        .into_par_iter()
120                        .enumerate()
121                        .map(|(idx, input)| {
122                            let mut input = std::mem::take(input);
123                            let mut state = state.split();
124                            state.branch_idx += idx;
125
126                            let df = input.execute(&mut state)?;
127                            Ok(df)
128                        })
129                        .collect::<PolarsResult<Vec<_>>>()
130                })
131                .collect::<PolarsResult<Vec<_>>>()
132        });
133        Ok(out?.into_iter().flatten().collect())
134    }
135}
136
137pub fn create_multiple_physical_plans(
138    roots: &[Node],
139    lp_arena: &mut Arena<IR>,
140    expr_arena: &mut Arena<AExpr>,
141    build_streaming_executor: Option<StreamingExecutorBuilder>,
142) -> PolarsResult<MultiplePhysicalPlans> {
143    let mut state = ConversionState::new()?;
144    let mut cache_nodes = Default::default();
145    let plans = state.with_new_branch(|new_state| {
146        roots
147            .iter()
148            .map(|&node| {
149                create_physical_plan_impl(
150                    node,
151                    lp_arena,
152                    expr_arena,
153                    new_state,
154                    &mut cache_nodes,
155                    build_streaming_executor,
156                )
157            })
158            .collect::<PolarsResult<Vec<_>>>()
159    })?;
160
161    let cache_prefiller = (!cache_nodes.is_empty()).then(|| {
162        struct Empty;
163        impl Executor for Empty {
164            fn execute(&mut self, _cache: &mut ExecutionState) -> PolarsResult<DataFrame> {
165                Ok(DataFrame::empty())
166            }
167        }
168        Box::new(CachePrefiller {
169            caches: cache_nodes,
170            phys_plan: Box::new(Empty),
171        }) as _
172    });
173
174    Ok(MultiplePhysicalPlans {
175        cache_prefiller,
176        physical_plans: plans,
177    })
178}
179
180#[cfg(feature = "python")]
181#[allow(clippy::type_complexity)]
182pub fn python_scan_predicate(
183    options: &mut PythonOptions,
184    expr_arena: &mut Arena<AExpr>,
185    state: &mut ExpressionConversionState,
186) -> PolarsResult<(
187    Option<Arc<dyn polars_expr::prelude::PhysicalExpr>>,
188    Option<Vec<u8>>,
189)> {
190    let mut predicate_serialized = None;
191    let predicate = if let PythonPredicate::Polars(e) = &options.predicate {
192        // Clone the expression so we can release the borrow on `options`
193        // before mutating `options.predicate` below.
194        let e = e.clone();
195
196        //  Convert to pyarrow expression if possible
197        if matches!(options.python_source, PythonScanSource::Pyarrow) {
198            use polars_core::config::verbose_print_sensitive;
199            use polars_plan::plans::MintermIter;
200            use polars_plan::plans::python::ArrowPredicate;
201            use polars_plan::plans::python::pyarrow::aexpr_to_pyarrow;
202            use polars_utils::python_function::PythonObject;
203            use pyo3::prelude::*;
204
205            // If there is a `head`, that comes before the filter and we post-apply
206            // the predicate in the engine.
207            let residual_predicate_expr_ir = if options.n_rows.is_none() {
208                let mut residual_predicate_nodes: Vec<Node> = vec![];
209                let mut convertible_nodes: Vec<Node> = vec![];
210
211                // Try converting all the nodes arena style
212                let pyarrow_predicate: Option<PythonObject> = Python::attach(
213                    |py| -> PolarsResult<Option<PythonObject>> {
214                        let pc = py.import("pyarrow.compute").map_err(
215                            |e| polars_err!(ComputeError: "could not import pyarrow.compute: {}", e),
216                        )?;
217                        let mut combined: Option<Bound<'_, PyAny>> = None;
218                        for node in MintermIter::new(e.node(), expr_arena) {
219                            if let Some(pa) = aexpr_to_pyarrow(py, &pc, node, expr_arena) {
220                                convertible_nodes.push(node);
221                                // Combine with and operator:
222                                // Need to catch error to satisfy rust, but I'm not sure how this would fail without
223                                // patching the and overload.
224                                combined = Some(match combined {
225                                    None => pa,
226                                    Some(prev) => prev.call_method1("__and__", (pa,)).map_err(
227                                        |e| polars_err!(ComputeError: "pyarrow __and__ failed: {}", e),
228                                    )?,
229                                });
230                            } else {
231                                residual_predicate_nodes.push(node);
232                            }
233                        }
234                        Ok(combined.map(|b| PythonObject(b.unbind())))
235                    },
236                )?;
237
238                if let Some(pyarrow_predicate) = pyarrow_predicate {
239                    let combined_node = convertible_nodes
240                        .into_iter()
241                        .reduce(|acc, node| {
242                            expr_arena.add(AExpr::BinaryExpr {
243                                left: acc,
244                                op: Operator::And,
245                                right: node,
246                            })
247                        })
248                        .unwrap();
249                    let predicate_expr_ir = ExprIR::from_node(combined_node, expr_arena);
250
251                    let has_residual = !residual_predicate_nodes.is_empty();
252                    options.predicate = PythonPredicate::PyArrow(ArrowPredicate {
253                        predicate: predicate_expr_ir,
254                        pyarrow_predicate,
255                        has_residual,
256                    });
257
258                    residual_predicate_nodes
259                        .into_iter()
260                        .fold(None, |acc, node| {
261                            Some(acc.map_or(node, |acc_node| {
262                                expr_arena.add(AExpr::BinaryExpr {
263                                    left: acc_node,
264                                    op: Operator::And,
265                                    right: node,
266                                })
267                            }))
268                        })
269                        .map(|node| ExprIR::from_node(node, expr_arena))
270                } else {
271                    Some(e.clone())
272                }
273            } else {
274                Some(e.clone())
275            };
276
277            verbose_print_sensitive(|| {
278                let predicate_pa_verbose_msg = match &options.predicate {
279                    PythonPredicate::PyArrow(p) => Python::attach(|py| {
280                        p.pyarrow_predicate
281                            .bind(py)
282                            .repr()
283                            .ok()
284                            .and_then(|s| s.extract::<String>().ok())
285                            .unwrap_or_else(|| "<repr failed>".to_string())
286                    }),
287                    _ => "<conversion failed>".to_string(),
288                };
289
290                format!(
291                    "python_scan_predicate: \
292                    predicate node: {}, \
293                    converted pyarrow predicate: {}, \
294                    residual predicate: {:?}",
295                    ExprIRDisplay::display_node(e.node(), expr_arena),
296                    predicate_pa_verbose_msg,
297                    residual_predicate_expr_ir
298                        .as_ref()
299                        .map(|e| ExprIRDisplay::display_node(e.node(), expr_arena)),
300                )
301            });
302
303            residual_predicate_expr_ir
304                .map(|expr_ir| create_physical_expr(&expr_ir, expr_arena, &options.schema, state))
305                .transpose()?
306        }
307        // Convert to physical expression for the case the reader cannot consume the predicate.
308        else {
309            let dsl_expr = e.to_expr(expr_arena);
310            predicate_serialized = polars_plan::plans::python::predicate::serialize(&dsl_expr)?;
311
312            Some(create_physical_expr(
313                &e,
314                expr_arena,
315                &options.schema,
316                state,
317            )?)
318        }
319    } else {
320        None
321    };
322
323    Ok((predicate, predicate_serialized))
324}
325
326#[recursive]
327fn create_physical_plan_impl(
328    root: Node,
329    lp_arena: &mut Arena<IR>,
330    expr_arena: &mut Arena<AExpr>,
331    state: &mut ConversionState,
332    // Cache nodes in order of discovery
333    cache_nodes: &mut PlIndexMap<UniqueId, executors::CachePrefill>,
334    build_streaming_executor: Option<StreamingExecutorBuilder>,
335) -> PolarsResult<Box<dyn Executor>> {
336    use IR::*;
337
338    let get_streaming_executor_builder = || {
339        build_streaming_executor.expect(
340            "get_streaming_executor_builder() failed (hint: missing feature new-streaming?)",
341        )
342    };
343
344    macro_rules! recurse {
345        ($node:expr, $state: expr) => {
346            create_physical_plan_impl(
347                $node,
348                lp_arena,
349                expr_arena,
350                $state,
351                cache_nodes,
352                build_streaming_executor,
353            )
354        };
355    }
356
357    let logical_plan = if state.has_cache_parent
358        || matches!(
359            lp_arena.get(root),
360            IR::Scan { .. } // Needed for the streaming impl
361                | IR::Cache { .. } // Needed for plans branching from the same cache node
362                | IR::GroupBy { .. } // Needed for the streaming impl
363                | IR::Sink { // Needed for the streaming impl
364                    payload:
365                        SinkTypeIR::File(_) | SinkTypeIR::Partitioned { .. },
366                    ..
367                }
368        ) {
369        lp_arena.get(root).clone()
370    } else {
371        lp_arena.take(root)
372    };
373
374    match logical_plan {
375        #[cfg(feature = "python")]
376        PythonScan { mut options } => {
377            let mut expr_conv_state = ExpressionConversionState::new(true);
378            let (predicate, predicate_serialized) =
379                python_scan_predicate(&mut options, expr_arena, &mut expr_conv_state)?;
380            Ok(Box::new(executors::PythonScanExec {
381                options,
382                predicate,
383                predicate_serialized,
384            }))
385        },
386        Sink { input, payload } => match payload {
387            SinkTypeIR::Memory => Ok(Box::new(SinkExecutor {
388                input: recurse!(input, state)?,
389                name: PlSmallStr::from_static("mem"),
390                f: Box::new(move |df, _state| Ok(Some(df))),
391            })),
392            SinkTypeIR::Callback(CallbackSinkType {
393                function,
394                maintain_order: _,
395                chunk_size,
396            }) => {
397                let chunk_size = chunk_size.map_or(usize::MAX, Into::into);
398
399                Ok(Box::new(SinkExecutor {
400                    input: recurse!(input, state)?,
401                    name: PlSmallStr::from_static("batches"),
402                    f: Box::new(move |mut buffer, _state| {
403                        while buffer.height() > 0 {
404                            let df;
405                            (df, buffer) = buffer.split_at(buffer.height().min(chunk_size) as i64);
406                            let should_stop = function.call(df)?;
407                            if should_stop {
408                                break;
409                            }
410                        }
411                        Ok(Some(DataFrame::empty()))
412                    }),
413                }))
414            },
415            SinkTypeIR::File(_) | SinkTypeIR::Partitioned { .. } => {
416                get_streaming_executor_builder()(root, lp_arena, expr_arena)
417            },
418        },
419        SinkMultiple { .. } => {
420            polars_bail!(InvalidOperation: "lazy multisinks only supported on streaming engine")
421        },
422        Union { inputs, options } => {
423            let inputs = state.with_new_branch(|new_state| {
424                inputs
425                    .into_iter()
426                    .map(|node| recurse!(node, new_state))
427                    .collect::<PolarsResult<Vec<_>>>()
428            });
429            let inputs = inputs?;
430            Ok(Box::new(executors::UnionExec { inputs, options }))
431        },
432        HConcat {
433            inputs, options, ..
434        } => {
435            let inputs = state.with_new_branch(|new_state| {
436                inputs
437                    .into_iter()
438                    .map(|node| recurse!(node, new_state))
439                    .collect::<PolarsResult<Vec<_>>>()
440            });
441
442            let inputs = inputs?;
443
444            Ok(Box::new(executors::HConcatExec { inputs, options }))
445        },
446        Slice { input, offset, len } => {
447            let input = recurse!(input, state)?;
448            Ok(Box::new(executors::SliceExec { input, offset, len }))
449        },
450        Filter { input, predicate } => {
451            let streamable = is_elementwise_rec(predicate.node(), expr_arena);
452            let input_schema = lp_arena.get(input).schema(lp_arena).into_owned();
453            let input = recurse!(input, state)?;
454            let mut state = ExpressionConversionState::new(true);
455            let predicate =
456                create_physical_expr(&predicate, expr_arena, &input_schema, &mut state)?;
457            Ok(Box::new(executors::FilterExec::new(
458                predicate,
459                input,
460                state.has_windows,
461                streamable,
462            )))
463        },
464        #[allow(unused_variables)]
465        Scan {
466            sources,
467            file_info,
468            hive_parts,
469            output_schema,
470            scan_type,
471            predicate,
472            predicate_file_skip_applied,
473            unified_scan_args,
474        } => {
475            let mut expr_conversion_state = ExpressionConversionState::new(true);
476
477            let mut create_skip_batch_predicate = unified_scan_args.table_statistics.is_some();
478            #[cfg(feature = "parquet")]
479            {
480                if let FileScanIR::Parquet { options, .. } = scan_type.as_ref() {
481                    create_skip_batch_predicate |= options.use_statistics;
482                }
483            }
484
485            let predicate = predicate
486                .map(|predicate| {
487                    create_scan_predicate(
488                        &predicate,
489                        expr_arena,
490                        output_schema.as_ref().unwrap_or(&file_info.schema),
491                        None, // hive_schema
492                        &mut expr_conversion_state,
493                        create_skip_batch_predicate,
494                        false,
495                    )
496                })
497                .transpose()?;
498
499            match *scan_type {
500                FileScanIR::Anonymous { function, .. } => {
501                    Ok(Box::new(executors::AnonymousScanExec {
502                        function,
503                        predicate,
504                        unified_scan_args,
505                        file_info,
506                        output_schema,
507                        predicate_has_windows: expr_conversion_state.has_windows,
508                    }))
509                },
510                #[cfg_attr(
511                    not(any(
512                        feature = "parquet",
513                        feature = "ipc",
514                        feature = "csv",
515                        feature = "json",
516                        feature = "scan_lines"
517                    )),
518                    expect(unreachable_patterns)
519                )]
520                _ => get_streaming_executor_builder()(root, lp_arena, expr_arena),
521            }
522        },
523
524        Select {
525            expr,
526            input,
527            schema: _schema,
528            options,
529            ..
530        } => {
531            let input_schema = lp_arena.get(input).schema(lp_arena).into_owned();
532            let input = recurse!(input, state)?;
533            let mut state =
534                ExpressionConversionState::new(RAYON.current_num_threads() > expr.len());
535            let phys_expr =
536                create_physical_expressions_from_irs(&expr, expr_arena, &input_schema, &mut state)?;
537
538            let allow_vertical_parallelism = options.should_broadcast && expr.iter().all(|e| is_elementwise_rec(e.node(), expr_arena))
539                // If all columns are literal we would get a 1 row per thread.
540                && !phys_expr.iter().all(|p| {
541                    p.is_literal()
542                });
543
544            Ok(Box::new(executors::ProjectionExec {
545                input,
546                expr: phys_expr,
547                has_windows: state.has_windows,
548                input_schema,
549                #[cfg(test)]
550                schema: _schema,
551                options,
552                allow_vertical_parallelism,
553            }))
554        },
555        DataFrameScan {
556            df, output_schema, ..
557        } => Ok(Box::new(executors::DataFrameExec {
558            df,
559            projection: output_schema.map(|s| s.iter_names_cloned().collect()),
560        })),
561        Sort {
562            input,
563            by_column,
564            slice,
565            sort_options,
566        } => {
567            debug_assert!(!by_column.is_empty());
568            let input_schema = lp_arena.get(input).schema(lp_arena);
569            let by_column = create_physical_expressions_from_irs(
570                &by_column,
571                expr_arena,
572                input_schema.as_ref(),
573                &mut ExpressionConversionState::new(true),
574            )?;
575            let input = recurse!(input, state)?;
576            Ok(Box::new(executors::SortExec {
577                input,
578                by_column,
579                slice: slice.map(|t| (t.0, t.1)),
580                sort_options,
581            }))
582        },
583        Cache { input, id } => {
584            state.has_cache_parent = true;
585            state.has_cache_child = true;
586
587            if let Some(cache) = cache_nodes.get_mut(&id) {
588                Ok(Box::new(cache.make_exec()))
589            } else {
590                let input = recurse!(input, state)?;
591
592                let mut prefill = executors::CachePrefill::new_cache(input, id);
593                let exec = prefill.make_exec();
594
595                cache_nodes.insert(id, prefill);
596
597                Ok(Box::new(exec))
598            }
599        },
600        Distinct { input, options } => {
601            let input = recurse!(input, state)?;
602            Ok(Box::new(executors::UniqueExec { input, options }))
603        },
604        GroupBy {
605            input,
606            keys,
607            aggs,
608            apply,
609            schema: output_schema,
610            maintain_order,
611            options,
612        } => {
613            let input_schema = lp_arena.get(input).schema(lp_arena).into_owned();
614            let options = Arc::try_unwrap(options).unwrap_or_else(|options| (*options).clone());
615            let phys_keys = create_physical_expressions_from_irs(
616                &keys,
617                expr_arena,
618                &input_schema,
619                &mut ExpressionConversionState::new(true),
620            )?;
621            let phys_aggs = create_physical_expressions_from_irs(
622                &aggs,
623                expr_arena,
624                &input_schema,
625                &mut ExpressionConversionState::new(true),
626            )?;
627
628            let _slice = options.slice;
629            #[cfg(feature = "dynamic_group_by")]
630            if let Some(options) = options.dynamic {
631                let input = recurse!(input, state)?;
632                return Ok(Box::new(executors::GroupByDynamicExec {
633                    input,
634                    keys: phys_keys,
635                    aggs: phys_aggs,
636                    options,
637                    input_schema,
638                    output_schema,
639                    slice: _slice,
640                    apply,
641                }));
642            }
643
644            #[cfg(feature = "dynamic_group_by")]
645            if let Some(options) = options.rolling {
646                let input = recurse!(input, state)?;
647                return Ok(Box::new(executors::GroupByRollingExec {
648                    input,
649                    keys: phys_keys,
650                    aggs: phys_aggs,
651                    options,
652                    input_schema,
653                    output_schema,
654                    slice: _slice,
655                    apply,
656                }));
657            }
658
659            // We first check if we can partition the group_by on the latest moment.
660            let partitionable = partitionable_gb(&keys, &aggs, &input_schema, expr_arena, &apply);
661            if partitionable && build_streaming_executor.is_some() {
662                let from_partitioned_ds = lp_arena.iter(input).any(|(_, lp)| {
663                    if let Union { options, .. } = lp {
664                        options.from_partitioned_ds
665                    } else {
666                        false
667                    }
668                });
669                let builder = get_streaming_executor_builder();
670
671                let input = recurse!(input, state)?;
672
673                let gb_root = if state.has_cache_parent {
674                    lp_arena.add(lp_arena.get(root).clone())
675                } else {
676                    root
677                };
678
679                let executor = Box::new(GroupByStreamingExec::new(
680                    input,
681                    builder,
682                    gb_root,
683                    lp_arena,
684                    expr_arena,
685                    phys_keys,
686                    phys_aggs,
687                    maintain_order,
688                    output_schema,
689                    _slice,
690                    from_partitioned_ds,
691                ));
692
693                Ok(executor)
694            } else {
695                let input = recurse!(input, state)?;
696                Ok(Box::new(executors::GroupByExec::new(
697                    input,
698                    phys_keys,
699                    phys_aggs,
700                    apply,
701                    maintain_order,
702                    input_schema,
703                    output_schema,
704                    options.slice,
705                )))
706            }
707        },
708        Join {
709            input_left,
710            input_right,
711            left_on,
712            right_on,
713            options,
714            schema,
715            ..
716        } => {
717            let schema_left = lp_arena.get(input_left).schema(lp_arena).into_owned();
718            let schema_right = lp_arena.get(input_right).schema(lp_arena).into_owned();
719
720            let (input_left, input_right) = state.with_new_branch(|new_state| {
721                (
722                    recurse!(input_left, new_state),
723                    recurse!(input_right, new_state),
724                )
725            });
726            let input_left = input_left?;
727            let input_right = input_right?;
728
729            // Todo! remove the force option. It can deadlock.
730            let parallel = if options.force_parallel {
731                true
732            } else {
733                options.allow_parallel
734            };
735
736            let left_on = create_physical_expressions_from_irs(
737                &left_on,
738                expr_arena,
739                &schema_left,
740                &mut ExpressionConversionState::new(true),
741            )?;
742            let right_on = create_physical_expressions_from_irs(
743                &right_on,
744                expr_arena,
745                &schema_right,
746                &mut ExpressionConversionState::new(true),
747            )?;
748            let options = Arc::try_unwrap(options).unwrap_or_else(|options| (*options).clone());
749
750            // Convert the join options, to the physical join options. This requires the physical
751            // planner, so we do this last minute.
752            let join_type_options = options
753                .options
754                .map(|o| {
755                    o.compile(|e| {
756                        let phys_expr = create_physical_expr(
757                            e,
758                            expr_arena,
759                            &schema,
760                            &mut ExpressionConversionState::new(false),
761                        )?;
762
763                        let execution_state = ExecutionState::default();
764
765                        Ok(Arc::new(move |df: DataFrame| {
766                            let mask = phys_expr.evaluate(&df, &execution_state)?;
767                            let mask = mask.as_materialized_series();
768                            let mask = mask.bool()?;
769                            df.filter_seq(mask)
770                        }))
771                    })
772                })
773                .transpose()?;
774
775            Ok(Box::new(executors::JoinExec::new(
776                input_left,
777                input_right,
778                left_on,
779                right_on,
780                parallel,
781                options.args,
782                join_type_options,
783            )))
784        },
785        Gather {
786            input,
787            idxs,
788            null_on_oob,
789        } => {
790            let input = recurse!(input, state)?;
791            let idxs = recurse!(idxs, state)?;
792            Ok(Box::new(executors::GatherExec::new(
793                input,
794                idxs,
795                null_on_oob,
796            )))
797        },
798        HStack {
799            input,
800            exprs,
801            schema: output_schema,
802            options,
803        } => {
804            let input_schema = lp_arena.get(input).schema(lp_arena).into_owned();
805            let input = recurse!(input, state)?;
806
807            let allow_vertical_parallelism = options.should_broadcast
808                && exprs
809                    .iter()
810                    .all(|e| is_elementwise_rec(e.node(), expr_arena));
811
812            let mut state =
813                ExpressionConversionState::new(RAYON.current_num_threads() > exprs.len());
814
815            let phys_exprs = create_physical_expressions_from_irs(
816                &exprs,
817                expr_arena,
818                &input_schema,
819                &mut state,
820            )?;
821            Ok(Box::new(executors::StackExec {
822                input,
823                has_windows: state.has_windows,
824                exprs: phys_exprs,
825                input_schema,
826                output_schema,
827                options,
828                allow_vertical_parallelism,
829            }))
830        },
831        MapFunction {
832            input, function, ..
833        } => {
834            let input = recurse!(input, state)?;
835            Ok(Box::new(executors::UdfExec { input, function }))
836        },
837        ExtContext {
838            input, contexts, ..
839        } => {
840            let input = recurse!(input, state)?;
841            let contexts = contexts
842                .into_iter()
843                .map(|node| recurse!(node, state))
844                .collect::<PolarsResult<_>>()?;
845            Ok(Box::new(executors::ExternalContext { input, contexts }))
846        },
847        SimpleProjection { input, columns } => {
848            let input = recurse!(input, state)?;
849            let exec = executors::ProjectionSimple { input, columns };
850            Ok(Box::new(exec))
851        },
852        #[cfg(feature = "merge_sorted")]
853        MergeSorted {
854            input_left,
855            input_right,
856            key,
857            // In the in-memory engine, merge_sorted is always order-maintaining.
858            maintain_order: _,
859        } => {
860            let (input_left, input_right) = state.with_new_branch(|new_state| {
861                (
862                    recurse!(input_left, new_state),
863                    recurse!(input_right, new_state),
864                )
865            });
866            let input_left = input_left?;
867            let input_right = input_right?;
868
869            let exec = executors::MergeSorted {
870                input_left,
871                input_right,
872                key,
873            };
874            Ok(Box::new(exec))
875        },
876        UnoptimizedDispatch { .. } => get_streaming_executor_builder()(root, lp_arena, expr_arena),
877        Invalid => unreachable!(),
878    }
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884
885    #[test]
886    fn test_create_multiple_physical_plans_reused_cache() {
887        // Check that reusing the same cache node doesn't panic.
888        // CSE creates duplicate cache nodes with the same ID, but cloud reuses them.
889
890        let mut ir = Arena::new();
891
892        let schema = Schema::from_iter([(PlSmallStr::from_static("x"), DataType::Float32)]);
893        let scan = ir.add(IR::DataFrameScan {
894            df: Arc::new(DataFrame::empty_with_schema(&schema)),
895            schema: Arc::new(schema),
896            output_schema: None,
897        });
898
899        let cache = ir.add(IR::Cache {
900            input: scan,
901            id: UniqueId::new(),
902        });
903
904        let left_sink = ir.add(IR::Sink {
905            input: cache,
906            payload: SinkTypeIR::Memory,
907        });
908        let right_sink = ir.add(IR::Sink {
909            input: cache,
910            payload: SinkTypeIR::Memory,
911        });
912
913        let _multiplan = create_multiple_physical_plans(
914            &[left_sink, right_sink],
915            &mut ir,
916            &mut Arena::new(),
917            None,
918        )
919        .unwrap();
920    }
921}