Skip to main content

polars_stream/physical_plan/
mod.rs

1use std::num::NonZeroUsize;
2use std::sync::Arc;
3
4use polars_core::frame::DataFrame;
5#[cfg(any(
6    feature = "dtype-date",
7    feature = "dtype-datetime",
8    feature = "dtype-time"
9))]
10use polars_core::prelude::DataType;
11use polars_core::prelude::{IdxSize, InitHashMaps, PlHashMap, PlIndexMap, SortMultipleOptions};
12use polars_core::schema::{Schema, SchemaRef};
13use polars_error::PolarsResult;
14use polars_io::RowIndex;
15use polars_io::cloud::CloudOptions;
16use polars_ops::frame::JoinArgs;
17#[cfg(any(
18    feature = "dtype-date",
19    feature = "dtype-datetime",
20    feature = "dtype-time"
21))]
22use polars_plan::dsl::StrptimeOptions;
23use polars_plan::dsl::deletion::DeletionFilesList;
24use polars_plan::dsl::{
25    CastColumnsPolicy, ColumnsUdf, FileSinkOptions, JoinTypeOptionsIR, MissingColumnsPolicy,
26    PartitionedSinkOptionsIR, PredicateFileSkip, ScanSources, TableStatistics,
27};
28use polars_plan::plans::expr_ir::ExprIR;
29use polars_plan::plans::hive::HivePartitionsDf;
30use polars_plan::plans::{AExpr, DataFrameUdf, DynamicPred, FunctionArgMap, IR};
31
32mod fmt;
33mod io;
34mod lower_expr;
35mod lower_group_by;
36mod lower_ir;
37mod to_graph;
38
39pub use fmt::{NodeStyle, visualize_plan};
40use polars_plan::prelude::PlanCallback;
41#[cfg(feature = "dynamic_group_by")]
42use polars_time::DynamicGroupOptions;
43use polars_time::{ClosedWindow, Duration};
44use polars_utils::arena::{Arena, Node};
45use polars_utils::pl_str::PlSmallStr;
46use polars_utils::slice_enum::Slice;
47use polars_utils::{UnitVec, unitvec};
48use slotmap::{SecondaryMap, SlotMap};
49pub use to_graph::physical_plan_to_graph;
50
51pub use self::lower_ir::StreamingLowerIRContext;
52use crate::nodes::io_sources::multi_scan::components::forbid_extra_columns::ForbidExtraColumns;
53use crate::nodes::io_sources::multi_scan::components::projection::builder::ProjectionBuilder;
54use crate::nodes::io_sources::multi_scan::reader_interface::builder::FileReaderBuilder;
55use crate::physical_plan::lower_expr::ExprCache;
56
57slotmap::new_key_type! {
58    /// Key used for physical nodes.
59    pub struct PhysNodeKey;
60}
61
62impl PhysNodeKey {
63    pub fn as_ffi(&self) -> u64 {
64        self.0.as_ffi()
65    }
66}
67
68/// A node in the physical plan.
69///
70/// A physical plan is created when the `IR` is translated to a directed
71/// acyclic graph of operations that can run on the streaming engine.
72#[derive(Clone, Debug)]
73pub struct PhysNode {
74    output_schemas: UnitVec<Arc<Schema>>,
75    kind: PhysNodeKind,
76}
77
78impl PhysNode {
79    pub fn new(output_schema: Arc<Schema>, kind: PhysNodeKind) -> Self {
80        Self {
81            output_schemas: unitvec![output_schema],
82            kind,
83        }
84    }
85
86    pub fn new_multi_output(output_schemas: UnitVec<Arc<Schema>>, kind: PhysNodeKind) -> Self {
87        Self {
88            output_schemas,
89            kind,
90        }
91    }
92
93    pub fn output_schema(&self, port_idx: usize) -> &Arc<Schema> {
94        &self.output_schemas[port_idx]
95    }
96
97    pub fn output_schema_mut(&mut self, port_idx: usize) -> &mut Arc<Schema> {
98        &mut self.output_schemas[port_idx]
99    }
100
101    pub fn kind(&self) -> &PhysNodeKind {
102        &self.kind
103    }
104}
105
106/// A handle representing a physical stream of data with a fixed schema in the
107/// physical plan. It consists of a reference to a physical node as well as the
108/// output port on that node to connect to receive the stream.
109#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
110pub struct PhysStream {
111    pub node: PhysNodeKey,
112    pub port: usize,
113}
114
115impl PhysStream {
116    #[allow(unused)]
117    pub fn new(node: PhysNodeKey, port: usize) -> Self {
118        Self { node, port }
119    }
120
121    // Convenience method to refer to the first output port of a physical node.
122    pub fn first(node: PhysNodeKey) -> Self {
123        Self { node, port: 0 }
124    }
125
126    pub fn output_schema<'sm>(&self, sm: &'sm SlotMap<PhysNodeKey, PhysNode>) -> &'sm Arc<Schema> {
127        sm[self.node].output_schema(self.port)
128    }
129
130    pub fn output_schema_mut<'sm>(
131        &self,
132        sm: &'sm mut SlotMap<PhysNodeKey, PhysNode>,
133    ) -> &'sm mut Arc<Schema> {
134        sm[self.node].output_schema_mut(self.port)
135    }
136}
137
138/// Behaviour when handling multiple DataFrames with different heights.
139
140#[derive(Clone, Debug, Copy)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142#[cfg_attr(
143    feature = "physical_plan_visualization_schema",
144    derive(schemars::JsonSchema)
145)]
146pub enum ZipBehavior {
147    /// Fill the shorter DataFrames with nulls to the height of the longest DataFrame.
148    NullExtend,
149    /// All inputs must be the same height, or have length 1 in which case they are broadcast.
150    Broadcast,
151    /// Raise an error if the DataFrames have different heights.
152    Strict,
153}
154
155#[derive(Clone, Debug)]
156pub enum PhysNodeKind {
157    InMemorySource {
158        df: Arc<DataFrame>,
159        disable_morsel_split: bool,
160    },
161
162    Select {
163        input: PhysStream,
164        selectors: Vec<ExprIR>,
165        extend_original: bool,
166    },
167
168    InputIndependentSelect {
169        selectors: Vec<ExprIR>,
170    },
171
172    WithRowIndex {
173        input: PhysStream,
174        name: PlSmallStr,
175        offset: Option<IdxSize>,
176    },
177
178    Reduce {
179        input: PhysStream,
180        exprs: Vec<ExprIR>,
181    },
182
183    StreamingSlice {
184        input: PhysStream,
185        offset: usize,
186        length: usize,
187    },
188
189    NegativeSlice {
190        input: PhysStream,
191        offset: i64,
192        length: usize,
193    },
194
195    DynamicSlice {
196        input: PhysStream,
197        offset: PhysStream,
198        length: PhysStream,
199    },
200
201    Shift {
202        input: PhysStream,
203        offset: PhysStream,
204        fill: Option<PhysStream>,
205    },
206
207    Filter {
208        input: PhysStream,
209        predicate: ExprIR,
210    },
211
212    SimpleProjection {
213        input: PhysStream,
214        // Key: output column, value: input column.
215        columns: PlIndexMap<PlSmallStr, PlSmallStr>,
216    },
217
218    InMemorySink {
219        input: PhysStream,
220    },
221
222    CallbackSink {
223        input: PhysStream,
224        function: PlanCallback<DataFrame, bool>,
225        maintain_order: bool,
226        chunk_size: Option<NonZeroUsize>,
227    },
228
229    FileSink {
230        input: PhysStream,
231        options: FileSinkOptions,
232    },
233
234    PartitionedSink {
235        input: PhysStream,
236        options: PartitionedSinkOptionsIR,
237    },
238
239    SinkMultiple {
240        sinks: Vec<PhysNodeKey>,
241    },
242
243    /// Generic fallback for (as-of-yet) unsupported streaming mappings.
244    /// Fully sinks all data to an in-memory data frame and uses the in-memory
245    /// engine to perform the map.
246    InMemoryMap {
247        input: PhysStream,
248        map: Arc<dyn DataFrameUdf>,
249        /// A formatted string of what the in-memory map is. This usually calls format on the IR.
250        format_str: Option<String>,
251    },
252
253    Map {
254        input: PhysStream,
255        map: Arc<dyn DataFrameUdf>,
256        /// A formatted string of what the map is. This usually calls format on the IR.
257        format_str: Option<String>,
258    },
259
260    ColumnarFunction {
261        inputs: Vec<PhysStream>,
262        func: Arc<dyn ColumnsUdf>,
263        arg_map: Option<FunctionArgMap>,
264        output_name: PlSmallStr,
265        format_str: Option<String>,
266    },
267
268    /// Streaming strptime without an explicit format.
269    #[cfg(any(
270        feature = "dtype-date",
271        feature = "dtype-datetime",
272        feature = "dtype-time"
273    ))]
274    StrptimeInfer {
275        input: PhysStream,
276        dtype: DataType,
277        options: StrptimeOptions,
278
279        /// Ambiguous can be `raise`, `earliest`, `latest` and `null`.
280        ///
281        /// If it is broadcast and it is `raise` or `null`, we can actually execute it in this
282        /// node. So
283        /// - `false` -> "null"
284        /// - `true`  -> "raise"
285        ambiguous_is_raise: bool,
286    },
287
288    SortedGroupBy {
289        input: PhysStream,
290        key: PlSmallStr,
291        aggs: Vec<ExprIR>,
292        slice: Option<(IdxSize, IdxSize)>,
293    },
294
295    Sort {
296        input: PhysStream,
297        by_column: Vec<ExprIR>,
298        slice: Option<(i64, usize)>,
299        sort_options: SortMultipleOptions,
300    },
301
302    TopK {
303        input: PhysStream,
304        k: PhysStream,
305        by_column: Vec<ExprIR>,
306        reverse: Vec<bool>,
307        nulls_last: Vec<bool>,
308        dyn_pred: Option<DynamicPred>,
309    },
310
311    Repeat {
312        value: PhysStream,
313        repeats: PhysStream,
314    },
315
316    #[cfg(feature = "cum_agg")]
317    CumAgg {
318        input: PhysStream,
319        kind: crate::nodes::cum_agg::CumAggKind,
320    },
321
322    // Parameter is the input stream
323    GatherEvery {
324        input: PhysStream,
325        n: usize,
326        offset: usize,
327    },
328    ForwardFill {
329        input: PhysStream,
330        limit: Option<IdxSize>,
331    },
332    BackwardFill {
333        input: PhysStream,
334        limit: Option<IdxSize>,
335    },
336    #[cfg(feature = "interpolate")]
337    Interpolate {
338        input: PhysStream,
339        method: polars_ops::series::InterpolationMethod,
340    },
341    Rle(PhysStream),
342    RleId(PhysStream),
343    SortedUnique {
344        input: PhysStream,
345        keys: Vec<PlSmallStr>,
346    },
347    PeakMinMax {
348        input: PhysStream,
349        is_peak_max: bool,
350    },
351
352    OrderedUnion {
353        inputs: Vec<PhysStream>,
354    },
355
356    UnorderedUnion {
357        inputs: Vec<PhysStream>,
358    },
359
360    Zip {
361        inputs: Vec<PhysStream>,
362        zip_behavior: ZipBehavior,
363    },
364
365    #[allow(unused)]
366    Multiplexer {
367        input: PhysStream,
368    },
369
370    MultiScan {
371        scan_sources: ScanSources,
372
373        file_reader_builder: Arc<dyn FileReaderBuilder>,
374        cloud_options: Option<Arc<CloudOptions>>,
375
376        /// Columns to project from the file.
377        file_projection_builder: ProjectionBuilder,
378        /// Final output schema of morsels being sent out of MultiScan.
379        output_schema: SchemaRef,
380
381        row_index: Option<RowIndex>,
382        pre_slice: Option<Slice>,
383        predicate: Option<ExprIR>,
384        predicate_file_skip_applied: Option<PredicateFileSkip>,
385
386        hive_parts: Option<HivePartitionsDf>,
387        include_file_paths: Option<PlSmallStr>,
388        cast_columns_policy: CastColumnsPolicy,
389        missing_columns_policy: MissingColumnsPolicy,
390        forbid_extra_columns: Option<ForbidExtraColumns>,
391
392        deletion_files: Option<DeletionFilesList>,
393        table_statistics: Option<TableStatistics>,
394
395        /// Schema of columns contained in the file. Does not contain external columns (e.g. hive / row_index).
396        file_schema: SchemaRef,
397        disable_morsel_split: bool,
398    },
399
400    #[cfg(feature = "python")]
401    PythonScan {
402        options: polars_plan::plans::python::PythonOptions,
403    },
404
405    GroupBy {
406        inputs: Vec<PhysStream>,
407        // Must have the same schema when applied for each input.
408        key_per_input: Vec<Vec<ExprIR>>,
409        // Must be a 'simple' expression, a singular column feeding into a single aggregate, or Len.
410        aggs_per_input: Vec<Vec<ExprIR>>,
411    },
412
413    #[cfg(feature = "dynamic_group_by")]
414    DynamicGroupBy {
415        input: PhysStream,
416        options: DynamicGroupOptions,
417        aggs: Vec<ExprIR>,
418        slice: Option<(IdxSize, IdxSize)>,
419    },
420
421    #[cfg(feature = "dynamic_group_by")]
422    RollingGroupBy {
423        input: PhysStream,
424        index_column: PlSmallStr,
425        period: Duration,
426        offset: Duration,
427        closed: ClosedWindow,
428        slice: Option<(IdxSize, IdxSize)>,
429        aggs: Vec<ExprIR>,
430    },
431
432    #[cfg(feature = "is_first_distinct")]
433    IsFirstDistinct {
434        input: PhysStream,
435        out_name: PlSmallStr,
436        columns: Vec<PlSmallStr>,
437    },
438
439    EquiJoin {
440        input_left: PhysStream,
441        input_right: PhysStream,
442        left_on: Vec<ExprIR>,
443        right_on: Vec<ExprIR>,
444        args: JoinArgs,
445    },
446
447    MergeJoin {
448        input_left: PhysStream,
449        input_right: PhysStream,
450        left_on: Vec<PlSmallStr>,
451        right_on: Vec<PlSmallStr>,
452        tmp_left_key_col: Option<PlSmallStr>,
453        tmp_right_key_col: Option<PlSmallStr>,
454        descending: bool,
455        nulls_last: bool,
456        keys_row_encoded: bool,
457        args: JoinArgs,
458    },
459
460    SemiAntiJoin {
461        input_left: PhysStream,
462        input_right: PhysStream,
463        left_on: Vec<ExprIR>,
464        right_on: Vec<ExprIR>,
465        args: JoinArgs,
466        output_bool: bool,
467    },
468
469    CrossJoin {
470        input_left: PhysStream,
471        input_right: PhysStream,
472        args: JoinArgs,
473    },
474
475    AsOfJoin {
476        input_left: PhysStream,
477        input_right: PhysStream,
478        left_on: PlSmallStr,
479        right_on: PlSmallStr,
480        tmp_left_key_col: Option<PlSmallStr>,
481        tmp_right_key_col: Option<PlSmallStr>,
482        by_descending: Option<Vec<bool>>,
483        by_nulls_last: Option<Vec<bool>>,
484        args: JoinArgs,
485    },
486
487    #[cfg(feature = "iejoin")]
488    RangeJoin {
489        input_left: PhysStream,
490        input_right: PhysStream,
491        left_on: Vec<PlSmallStr>,
492        right_on: Vec<PlSmallStr>,
493        tmp_left_key_cols: Vec<Option<PlSmallStr>>,
494        tmp_right_key_cols: Vec<Option<PlSmallStr>>,
495        descending: bool,
496        args: JoinArgs,
497        options: polars_ops::frame::IEJoinOptions,
498    },
499
500    /// Generic fallback for (as-of-yet) unsupported streaming joins.
501    /// Fully sinks all data to in-memory data frames and uses the in-memory
502    /// engine to perform the join.
503    InMemoryJoin {
504        input_left: PhysStream,
505        input_right: PhysStream,
506        left_on: Vec<ExprIR>,
507        right_on: Vec<ExprIR>,
508        args: JoinArgs,
509        options: Option<JoinTypeOptionsIR>,
510    },
511
512    #[cfg(feature = "merge_sorted")]
513    MergeSorted {
514        input_left: PhysStream,
515        input_right: PhysStream,
516        maintain_order: bool,
517    },
518
519    Gather {
520        input: PhysStream,
521        idxs: PhysStream,
522        null_on_oob: bool,
523    },
524
525    #[cfg(feature = "ewma")]
526    EwmMean {
527        input: PhysStream,
528        options: polars_ops::series::EWMOptions,
529    },
530
531    #[cfg(feature = "ewma")]
532    EwmVar {
533        input: PhysStream,
534        options: polars_ops::series::EWMOptions,
535    },
536
537    #[cfg(feature = "ewma")]
538    EwmStd {
539        input: PhysStream,
540        options: polars_ops::series::EWMOptions,
541    },
542}
543
544fn visit_node_inputs_mut(
545    roots: Vec<PhysNodeKey>,
546    phys_sm: &mut SlotMap<PhysNodeKey, PhysNode>,
547    mut visit: impl FnMut(&mut PhysStream),
548) {
549    let mut to_visit = roots;
550    let mut seen: SecondaryMap<PhysNodeKey, ()> =
551        to_visit.iter().copied().map(|n| (n, ())).collect();
552    macro_rules! rec {
553        ($n:expr) => {
554            let n = $n;
555            if seen.insert(n, ()).is_none() {
556                to_visit.push(n)
557            }
558        };
559    }
560    while let Some(node) = to_visit.pop() {
561        match &mut phys_sm[node].kind {
562            PhysNodeKind::InMemorySource { .. }
563            | PhysNodeKind::MultiScan { .. }
564            | PhysNodeKind::InputIndependentSelect { .. } => {},
565            #[cfg(feature = "python")]
566            PhysNodeKind::PythonScan { .. } => {},
567            PhysNodeKind::Select { input, .. }
568            | PhysNodeKind::WithRowIndex { input, .. }
569            | PhysNodeKind::Reduce { input, .. }
570            | PhysNodeKind::StreamingSlice { input, .. }
571            | PhysNodeKind::NegativeSlice { input, .. }
572            | PhysNodeKind::Filter { input, .. }
573            | PhysNodeKind::SimpleProjection { input, .. }
574            | PhysNodeKind::InMemorySink { input }
575            | PhysNodeKind::CallbackSink { input, .. }
576            | PhysNodeKind::FileSink { input, .. }
577            | PhysNodeKind::PartitionedSink { input, .. }
578            | PhysNodeKind::InMemoryMap { input, .. }
579            | PhysNodeKind::SortedGroupBy { input, .. }
580            | PhysNodeKind::Map { input, .. }
581            | PhysNodeKind::Sort { input, .. }
582            | PhysNodeKind::Multiplexer { input }
583            | PhysNodeKind::GatherEvery { input, .. }
584            | PhysNodeKind::ForwardFill { input, .. }
585            | PhysNodeKind::BackwardFill { input, .. }
586            | PhysNodeKind::Rle(input)
587            | PhysNodeKind::RleId(input)
588            | PhysNodeKind::SortedUnique { input, .. }
589            | PhysNodeKind::PeakMinMax { input, .. } => {
590                rec!(input.node);
591                visit(input);
592            },
593
594            #[cfg(feature = "interpolate")]
595            PhysNodeKind::Interpolate { input, .. } => {
596                rec!(input.node);
597                visit(input);
598            },
599
600            #[cfg(feature = "is_first_distinct")]
601            PhysNodeKind::IsFirstDistinct { input, .. } => {
602                rec!(input.node);
603                visit(input);
604            },
605
606            #[cfg(any(
607                feature = "dtype-date",
608                feature = "dtype-datetime",
609                feature = "dtype-time"
610            ))]
611            PhysNodeKind::StrptimeInfer { input, .. } => {
612                rec!(input.node);
613                visit(input);
614            },
615
616            #[cfg(feature = "dynamic_group_by")]
617            PhysNodeKind::DynamicGroupBy { input, .. } => {
618                rec!(input.node);
619                visit(input);
620            },
621            #[cfg(feature = "dynamic_group_by")]
622            PhysNodeKind::RollingGroupBy { input, .. } => {
623                rec!(input.node);
624                visit(input);
625            },
626
627            #[cfg(feature = "cum_agg")]
628            PhysNodeKind::CumAgg { input, .. } => {
629                rec!(input.node);
630                visit(input);
631            },
632
633            PhysNodeKind::InMemoryJoin {
634                input_left,
635                input_right,
636                ..
637            }
638            | PhysNodeKind::EquiJoin {
639                input_left,
640                input_right,
641                ..
642            }
643            | PhysNodeKind::MergeJoin {
644                input_left,
645                input_right,
646                ..
647            }
648            | PhysNodeKind::SemiAntiJoin {
649                input_left,
650                input_right,
651                ..
652            }
653            | PhysNodeKind::CrossJoin {
654                input_left,
655                input_right,
656                ..
657            }
658            | PhysNodeKind::AsOfJoin {
659                input_left,
660                input_right,
661                ..
662            } => {
663                rec!(input_left.node);
664                rec!(input_right.node);
665                visit(input_left);
666                visit(input_right);
667            },
668
669            #[cfg(feature = "iejoin")]
670            PhysNodeKind::RangeJoin {
671                input_left,
672                input_right,
673                ..
674            } => {
675                rec!(input_left.node);
676                rec!(input_right.node);
677                visit(input_left);
678                visit(input_right);
679            },
680
681            #[cfg(feature = "merge_sorted")]
682            PhysNodeKind::MergeSorted {
683                input_left,
684                input_right,
685                ..
686            } => {
687                rec!(input_left.node);
688                rec!(input_right.node);
689                visit(input_left);
690                visit(input_right);
691            },
692
693            PhysNodeKind::Gather { input, idxs, .. } => {
694                rec!(input.node);
695                rec!(idxs.node);
696                visit(input);
697                visit(idxs);
698            },
699
700            PhysNodeKind::TopK { input, k, .. } => {
701                rec!(input.node);
702                rec!(k.node);
703                visit(input);
704                visit(k);
705            },
706
707            PhysNodeKind::DynamicSlice {
708                input,
709                offset,
710                length,
711            } => {
712                rec!(input.node);
713                rec!(offset.node);
714                rec!(length.node);
715                visit(input);
716                visit(offset);
717                visit(length);
718            },
719
720            PhysNodeKind::Shift {
721                input,
722                offset,
723                fill,
724            } => {
725                rec!(input.node);
726                rec!(offset.node);
727                if let Some(fill) = fill {
728                    rec!(fill.node);
729                }
730                visit(input);
731                visit(offset);
732                if let Some(fill) = fill {
733                    visit(fill);
734                }
735            },
736
737            PhysNodeKind::Repeat { value, repeats } => {
738                rec!(value.node);
739                rec!(repeats.node);
740                visit(value);
741                visit(repeats);
742            },
743
744            PhysNodeKind::GroupBy { inputs, .. }
745            | PhysNodeKind::OrderedUnion { inputs }
746            | PhysNodeKind::UnorderedUnion { inputs }
747            | PhysNodeKind::Zip { inputs, .. }
748            | PhysNodeKind::ColumnarFunction { inputs, .. } => {
749                for input in inputs {
750                    rec!(input.node);
751                    visit(input);
752                }
753            },
754
755            PhysNodeKind::SinkMultiple { sinks } => {
756                for sink in sinks {
757                    rec!(*sink);
758                    visit(&mut PhysStream::first(*sink));
759                }
760            },
761
762            #[cfg(feature = "ewma")]
763            PhysNodeKind::EwmMean { input, options: _ }
764            | PhysNodeKind::EwmVar { input, options: _ }
765            | PhysNodeKind::EwmStd { input, options: _ } => {
766                rec!(input.node);
767                visit(input)
768            },
769        }
770    }
771}
772
773fn insert_multiplexers(roots: Vec<PhysNodeKey>, phys_sm: &mut SlotMap<PhysNodeKey, PhysNode>) {
774    let mut refcount: PlIndexMap<_, usize> = PlIndexMap::new();
775    visit_node_inputs_mut(roots.clone(), phys_sm, |i| {
776        *refcount.entry(*i).or_insert(0) += 1;
777    });
778
779    let mut multiplexer_map: PlHashMap<PhysStream, PhysStream> = refcount
780        .into_iter()
781        .filter(|(_stream, refcount)| *refcount > 1)
782        .map(|(stream, refcount)| {
783            let input_schema = Arc::clone(stream.output_schema(phys_sm));
784            let multiplexer_node = phys_sm.insert(PhysNode::new_multi_output(
785                (0..refcount).map(|_| Arc::clone(&input_schema)).collect(),
786                PhysNodeKind::Multiplexer { input: stream },
787            ));
788            (stream, PhysStream::first(multiplexer_node))
789        })
790        .collect();
791
792    visit_node_inputs_mut(roots, phys_sm, |i| {
793        if let Some(m) = multiplexer_map.get_mut(i) {
794            *i = *m;
795            m.port += 1;
796        }
797    });
798}
799
800pub fn build_physical_plan(
801    root: Node,
802    ir_arena: &mut Arena<IR>,
803    expr_arena: &mut Arena<AExpr>,
804    phys_sm: &mut SlotMap<PhysNodeKey, PhysNode>,
805    ctx: StreamingLowerIRContext<'_>,
806) -> PolarsResult<PhysNodeKey> {
807    let mut schema_cache = PlHashMap::with_capacity(ir_arena.len());
808    let mut expr_cache = ExprCache::with_capacity(expr_arena.len());
809    let mut cache_nodes = PlHashMap::new();
810    let phys_root = lower_ir::lower_ir(
811        root,
812        ir_arena,
813        expr_arena,
814        phys_sm,
815        &mut schema_cache,
816        &mut expr_cache,
817        &mut cache_nodes,
818        ctx,
819        None,
820    )?;
821    insert_multiplexers(vec![phys_root.node], phys_sm);
822    Ok(phys_root.node)
823}