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 if !keys.is_empty() && keys.len() < 3 && apply.is_none() {
33 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 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 let e = e.clone();
195
196 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 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 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 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 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: &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 { .. } | IR::Cache { .. } | IR::GroupBy { .. } | IR::Sink { 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, &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 && !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 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 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 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 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 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}