Skip to main content

polars_lazy/frame/
mod.rs

1//! Lazy variant of a [DataFrame].
2#[cfg(feature = "python")]
3mod python;
4
5mod cached_arenas;
6mod err;
7#[cfg(not(target_arch = "wasm32"))]
8mod exitable;
9
10use std::num::NonZeroUsize;
11use std::sync::mpsc::{Receiver, sync_channel};
12use std::sync::{Arc, Mutex};
13
14pub use anonymous_scan::*;
15#[cfg(feature = "csv")]
16pub use csv::*;
17#[cfg(not(target_arch = "wasm32"))]
18pub use exitable::*;
19pub use file_list_reader::*;
20#[cfg(feature = "json")]
21pub use ndjson::*;
22#[cfg(feature = "parquet")]
23pub use parquet::*;
24use polars_compute::rolling::QuantileMethod;
25use polars_core::error::feature_gated;
26#[cfg(feature = "pivot")]
27use polars_core::frame::PivotColumnNaming;
28use polars_core::prelude::*;
29use polars_core::query_result::QueryResult;
30#[cfg(feature = "is_between")]
31use polars_defs::expr::ClosedInterval;
32use polars_defs::join::{
33    JoinArgs, JoinBuildSide, JoinCoalesce, JoinType, JoinValidation, MaintainOrderJoin,
34};
35use polars_defs::time::group_by::{DynamicGroupOptions, RollingGroupOptions};
36use polars_io::RowIndex;
37use polars_mem_engine::scan_predicate::functions::apply_scan_predicate_to_scan_ir;
38use polars_mem_engine::{Executor, create_multiple_physical_plans, create_physical_plan};
39use polars_observer::{PlannedQuery, QueryObserver};
40pub use polars_plan::frame::{AllowedOptimizations, OptFlags};
41use polars_plan::plans::ExecutionHooks;
42use polars_plan::prelude::ir_plan_to_description;
43use polars_utils::pl_str::PlSmallStr;
44
45use crate::frame::cached_arenas::CachedArena;
46use crate::prelude::*;
47
48pub trait IntoLazy {
49    fn lazy(self) -> LazyFrame;
50}
51
52impl IntoLazy for DataFrame {
53    /// Convert the `DataFrame` into a `LazyFrame`
54    fn lazy(self) -> LazyFrame {
55        let lp = DslBuilder::from_existing_df(self).build();
56        LazyFrame {
57            logical_plan: lp,
58            opt_state: Default::default(),
59            cached_arena: Default::default(),
60        }
61    }
62}
63
64/// Join implementation handed to the optimizer for its hive partition rewrite.
65fn hive_join(
66    left: &DataFrame,
67    right: &DataFrame,
68    left_on: &str,
69    right_on: &str,
70    args: JoinArgs,
71) -> PolarsResult<DataFrame> {
72    use polars_ops::frame::DataFrameJoinOps;
73
74    left.join(right, [left_on], [right_on], args, None)
75}
76
77impl IntoLazy for LazyFrame {
78    fn lazy(self) -> LazyFrame {
79        self
80    }
81}
82
83/// Lazy abstraction over an eager `DataFrame`.
84///
85/// It really is an abstraction over a logical plan. The methods of this struct will incrementally
86/// modify a logical plan until output is requested (via [`collect`](crate::frame::LazyFrame::collect)).
87#[derive(Clone, Default)]
88#[must_use]
89pub struct LazyFrame {
90    pub logical_plan: DslPlan,
91    pub(crate) opt_state: OptFlags,
92    pub(crate) cached_arena: Arc<Mutex<Option<CachedArena>>>,
93}
94
95impl From<DslPlan> for LazyFrame {
96    fn from(plan: DslPlan) -> Self {
97        Self {
98            logical_plan: plan,
99            opt_state: OptFlags::default(),
100            cached_arena: Default::default(),
101        }
102    }
103}
104
105impl LazyFrame {
106    pub(crate) fn from_inner(
107        logical_plan: DslPlan,
108        opt_state: OptFlags,
109        cached_arena: Arc<Mutex<Option<CachedArena>>>,
110    ) -> Self {
111        Self {
112            logical_plan,
113            opt_state,
114            cached_arena,
115        }
116    }
117
118    pub(crate) fn get_plan_builder(self) -> DslBuilder {
119        DslBuilder::from(self.logical_plan)
120    }
121
122    fn get_opt_state(&self) -> OptFlags {
123        self.opt_state
124    }
125
126    pub fn from_logical_plan(logical_plan: DslPlan, opt_state: OptFlags) -> Self {
127        LazyFrame {
128            logical_plan,
129            opt_state,
130            cached_arena: Default::default(),
131        }
132    }
133
134    /// Get current optimizations.
135    pub fn get_current_optimizations(&self) -> OptFlags {
136        self.opt_state
137    }
138
139    /// Set allowed optimizations.
140    pub fn with_optimizations(mut self, opt_state: OptFlags) -> Self {
141        self.opt_state = opt_state;
142        self
143    }
144
145    /// Turn off all optimizations.
146    pub fn without_optimizations(self) -> Self {
147        self.with_optimizations(OptFlags::from_bits_truncate(0) | OptFlags::TYPE_COERCION)
148    }
149
150    /// Toggle projection pushdown optimization.
151    pub fn with_projection_pushdown(mut self, toggle: bool) -> Self {
152        self.opt_state.set(OptFlags::PROJECTION_PUSHDOWN, toggle);
153        self
154    }
155
156    /// Toggle cluster with columns optimization.
157    pub fn with_cluster_with_columns(mut self, toggle: bool) -> Self {
158        self.opt_state.set(OptFlags::CLUSTER_WITH_COLUMNS, toggle);
159        self
160    }
161
162    /// Check if operations are order dependent and unset maintaining_order if
163    /// the order would not be observed.
164    pub fn with_check_order(mut self, toggle: bool) -> Self {
165        self.opt_state.set(OptFlags::CHECK_ORDER_OBSERVE, toggle);
166        self
167    }
168
169    /// Toggle predicate pushdown optimization.
170    pub fn with_predicate_pushdown(mut self, toggle: bool) -> Self {
171        self.opt_state.set(OptFlags::PREDICATE_PUSHDOWN, toggle);
172        self
173    }
174
175    /// Toggle type coercion optimization.
176    pub fn with_type_coercion(mut self, toggle: bool) -> Self {
177        self.opt_state.set(OptFlags::TYPE_COERCION, toggle);
178        self
179    }
180
181    /// Toggle type check optimization.
182    pub fn with_type_check(mut self, toggle: bool) -> Self {
183        self.opt_state.set(OptFlags::TYPE_CHECK, toggle);
184        self
185    }
186
187    /// Toggle expression simplification optimization on or off.
188    pub fn with_simplify_expr(mut self, toggle: bool) -> Self {
189        self.opt_state.set(OptFlags::SIMPLIFY_EXPR, toggle);
190        self
191    }
192
193    /// Toggle common subplan elimination optimization on or off
194    #[cfg(feature = "cse")]
195    pub fn with_comm_subplan_elim(mut self, toggle: bool) -> Self {
196        self.opt_state.set(OptFlags::COMM_SUBPLAN_ELIM, toggle);
197        self
198    }
199
200    /// Toggle common subexpression elimination optimization on or off
201    #[cfg(feature = "cse")]
202    pub fn with_comm_subexpr_elim(mut self, toggle: bool) -> Self {
203        self.opt_state.set(OptFlags::COMM_SUBEXPR_ELIM, toggle);
204        self
205    }
206
207    /// Toggle slice pushdown optimization.
208    pub fn with_slice_pushdown(mut self, toggle: bool) -> Self {
209        self.opt_state.set(OptFlags::SLICE_PUSHDOWN, toggle);
210        self
211    }
212
213    #[cfg(feature = "streaming")]
214    pub fn with_streaming(mut self, toggle: bool) -> Self {
215        self.opt_state.set(OptFlags::STREAMING, toggle);
216        self
217    }
218
219    pub fn with_gpu(mut self, toggle: bool) -> Self {
220        self.opt_state.set(OptFlags::GPU, toggle);
221        self
222    }
223
224    /// Try to estimate the number of rows so that joins can determine which side to keep in memory.
225    pub fn with_row_estimate(mut self, toggle: bool) -> Self {
226        self.opt_state.set(OptFlags::ROW_ESTIMATE, toggle);
227        self
228    }
229
230    /// Run every node eagerly. This turns off multi-node optimizations.
231    pub fn _with_eager(mut self, toggle: bool) -> Self {
232        self.opt_state.set(OptFlags::EAGER, toggle);
233        self
234    }
235
236    /// Return a String describing the naive (un-optimized) logical plan.
237    pub fn describe_plan(&self) -> PolarsResult<String> {
238        Ok(self.clone().to_alp()?.describe())
239    }
240
241    /// Return a String describing the naive (un-optimized) logical plan in tree format.
242    pub fn describe_plan_tree(&self) -> PolarsResult<String> {
243        Ok(self.clone().to_alp()?.describe_tree_format())
244    }
245
246    /// Return a String describing the optimized logical plan.
247    ///
248    /// Returns `Err` if optimizing the logical plan fails.
249    pub fn describe_optimized_plan(&self) -> PolarsResult<String> {
250        Ok(self.clone().to_alp_optimized()?.describe())
251    }
252
253    /// Return a String describing the optimized logical plan in tree format.
254    ///
255    /// Returns `Err` if optimizing the logical plan fails.
256    pub fn describe_optimized_plan_tree(&self) -> PolarsResult<String> {
257        Ok(self.clone().to_alp_optimized()?.describe_tree_format())
258    }
259
260    /// Return a String describing the logical plan.
261    ///
262    /// If `optimized` is `true`, explains the optimized plan. If `optimized` is `false`,
263    /// explains the naive, un-optimized plan.
264    pub fn explain(&self, optimized: bool) -> PolarsResult<String> {
265        if optimized {
266            self.describe_optimized_plan()
267        } else {
268            self.describe_plan()
269        }
270    }
271
272    /// Add a sort operation to the logical plan.
273    ///
274    /// Sorts the LazyFrame by the column name specified using the provided options.
275    ///
276    /// # Example
277    ///
278    /// Sort DataFrame by 'sepal_width' column:
279    /// ```rust
280    /// # use polars_core::prelude::*;
281    /// # use polars_lazy::prelude::*;
282    /// fn sort_by_a(df: DataFrame) -> LazyFrame {
283    ///     df.lazy().sort(["sepal_width"], Default::default())
284    /// }
285    /// ```
286    /// Sort by a single column with specific order:
287    /// ```
288    /// # use polars_core::prelude::*;
289    /// # use polars_lazy::prelude::*;
290    /// fn sort_with_specific_order(df: DataFrame, descending: bool) -> LazyFrame {
291    ///     df.lazy().sort(
292    ///         ["sepal_width"],
293    ///         SortMultipleOptions::new()
294    ///             .with_order_descending(descending)
295    ///     )
296    /// }
297    /// ```
298    /// Sort by multiple columns with specifying order for each column:
299    /// ```
300    /// # use polars_core::prelude::*;
301    /// # use polars_lazy::prelude::*;
302    /// fn sort_by_multiple_columns_with_specific_order(df: DataFrame) -> LazyFrame {
303    ///     df.lazy().sort(
304    ///         ["sepal_width", "sepal_length"],
305    ///         SortMultipleOptions::new()
306    ///             .with_order_descending_multi([false, true])
307    ///     )
308    /// }
309    /// ```
310    /// See [`SortMultipleOptions`] for more options.
311    pub fn sort(self, by: impl IntoVec<PlSmallStr>, sort_options: SortMultipleOptions) -> Self {
312        let opt_state = self.get_opt_state();
313        let lp = self
314            .get_plan_builder()
315            .sort(by.into_vec().into_iter().map(col).collect(), sort_options)
316            .build();
317        Self::from_logical_plan(lp, opt_state)
318    }
319
320    /// Add a sort operation to the logical plan.
321    ///
322    /// Sorts the LazyFrame by the provided list of expressions, which will be turned into
323    /// concrete columns before sorting.
324    ///
325    /// See [`SortMultipleOptions`] for more options.
326    ///
327    /// # Example
328    ///
329    /// ```rust
330    /// use polars_core::prelude::*;
331    /// use polars_lazy::prelude::*;
332    ///
333    /// /// Sort DataFrame by 'sepal_width' column
334    /// fn example(df: DataFrame) -> LazyFrame {
335    ///       df.lazy()
336    ///         .sort_by_exprs(vec![col("sepal_width")], Default::default())
337    /// }
338    /// ```
339    pub fn sort_by_exprs<E: AsRef<[Expr]>>(
340        self,
341        by_exprs: E,
342        sort_options: SortMultipleOptions,
343    ) -> Self {
344        let by_exprs = by_exprs.as_ref().to_vec();
345        if by_exprs.is_empty() {
346            self
347        } else {
348            let opt_state = self.get_opt_state();
349            let lp = self.get_plan_builder().sort(by_exprs, sort_options).build();
350            Self::from_logical_plan(lp, opt_state)
351        }
352    }
353
354    pub fn top_k<E: AsRef<[Expr]>>(
355        self,
356        k: IdxSize,
357        by_exprs: E,
358        sort_options: SortMultipleOptions,
359    ) -> Self {
360        // this will optimize to top-k
361        self.sort_by_exprs(
362            by_exprs,
363            sort_options.with_order_reversed().with_nulls_last(true),
364        )
365        .slice(0, k)
366    }
367
368    pub fn bottom_k<E: AsRef<[Expr]>>(
369        self,
370        k: IdxSize,
371        by_exprs: E,
372        sort_options: SortMultipleOptions,
373    ) -> Self {
374        // this will optimize to bottom-k
375        self.sort_by_exprs(by_exprs, sort_options.with_nulls_last(true))
376            .slice(0, k)
377    }
378
379    /// Reverse the `DataFrame` from top to bottom.
380    ///
381    /// Row `i` becomes row `number_of_rows - i - 1`.
382    ///
383    /// # Example
384    ///
385    /// ```rust
386    /// use polars_core::prelude::*;
387    /// use polars_lazy::prelude::*;
388    ///
389    /// fn example(df: DataFrame) -> LazyFrame {
390    ///       df.lazy()
391    ///         .reverse()
392    /// }
393    /// ```
394    pub fn reverse(self) -> Self {
395        self.select(vec![col(PlSmallStr::from_static("*")).reverse()])
396    }
397
398    /// Rename columns in the DataFrame.
399    ///
400    /// `existing` and `new` are iterables of the same length containing the old and
401    /// corresponding new column names. Renaming happens to all `existing` columns
402    /// simultaneously, not iteratively. If `strict` is true, all columns in `existing`
403    /// must be present in the `LazyFrame` when `rename` is called; otherwise, only
404    /// those columns that are actually found will be renamed (others will be ignored).
405    pub fn rename<I, J, T, S>(self, existing: I, new: J, strict: bool) -> Self
406    where
407        I: IntoIterator<Item = T>,
408        J: IntoIterator<Item = S>,
409        T: AsRef<str>,
410        S: AsRef<str>,
411    {
412        let iter = existing.into_iter();
413        let cap = iter.size_hint().0;
414        let mut existing_vec: Vec<PlSmallStr> = Vec::with_capacity(cap);
415        let mut new_vec: Vec<PlSmallStr> = Vec::with_capacity(cap);
416
417        // TODO! should this error if `existing` and `new` have different lengths?
418        // Currently, the longer of the two is truncated.
419        for (existing, new) in iter.zip(new) {
420            let existing = existing.as_ref();
421            let new = new.as_ref();
422            if new != existing {
423                existing_vec.push(existing.into());
424                new_vec.push(new.into());
425            }
426        }
427
428        self.map_private(DslFunction::Rename {
429            existing: existing_vec.into(),
430            new: new_vec.into(),
431            strict,
432        })
433    }
434
435    /// Removes columns from the DataFrame.
436    /// Note that it's better to only select the columns you need
437    /// and let the projection pushdown optimize away the unneeded columns.
438    ///
439    /// Any given columns that are not in the schema will give a [`PolarsError::ColumnNotFound`]
440    /// error while materializing the [`LazyFrame`].
441    pub fn drop(self, columns: Selector) -> Self {
442        let opt_state = self.get_opt_state();
443        let lp = self.get_plan_builder().drop(columns).build();
444        Self::from_logical_plan(lp, opt_state)
445    }
446
447    /// Shift the values by a given period and fill the parts that will be empty due to this operation
448    /// with `Nones`.
449    ///
450    /// See the method on [Series](polars_core::series::SeriesTrait::shift) for more info on the `shift` operation.
451    pub fn shift<E: Into<Expr>>(self, n: E) -> Self {
452        self.select(vec![col(PlSmallStr::from_static("*")).shift(n.into())])
453    }
454
455    /// Shift the values by a given period and fill the parts that will be empty due to this operation
456    /// with the result of the `fill_value` expression.
457    ///
458    /// See the method on [Series](polars_core::series::SeriesTrait::shift) for more info on the `shift` operation.
459    pub fn shift_and_fill<E: Into<Expr>, IE: Into<Expr>>(self, n: E, fill_value: IE) -> Self {
460        self.select(vec![
461            col(PlSmallStr::from_static("*")).shift_and_fill(n.into(), fill_value.into()),
462        ])
463    }
464
465    /// Fill None values in the DataFrame with an expression.
466    pub fn fill_null<E: Into<Expr>>(self, fill_value: E) -> LazyFrame {
467        let opt_state = self.get_opt_state();
468        let lp = self.get_plan_builder().fill_null(fill_value.into()).build();
469        Self::from_logical_plan(lp, opt_state)
470    }
471
472    /// Fill NaN values in the DataFrame with an expression.
473    pub fn fill_nan<E: Into<Expr>>(self, fill_value: E) -> LazyFrame {
474        let opt_state = self.get_opt_state();
475        let lp = self.get_plan_builder().fill_nan(fill_value.into()).build();
476        Self::from_logical_plan(lp, opt_state)
477    }
478
479    /// Caches the result into a new LazyFrame.
480    ///
481    /// This should be used to prevent computations running multiple times.
482    pub fn cache(self) -> Self {
483        let opt_state = self.get_opt_state();
484        let lp = self.get_plan_builder().cache().build();
485        Self::from_logical_plan(lp, opt_state)
486    }
487
488    /// Cast named frame columns, resulting in a new LazyFrame with updated dtypes
489    pub fn cast(self, dtypes: PlHashMap<&str, DataType>, strict: bool) -> Self {
490        let cast_cols: Vec<Expr> = dtypes
491            .into_iter()
492            .map(|(name, dt)| {
493                let name = PlSmallStr::from_str(name);
494
495                if strict {
496                    col(name).strict_cast(dt)
497                } else {
498                    col(name).cast(dt)
499                }
500            })
501            .collect();
502
503        if cast_cols.is_empty() {
504            self
505        } else {
506            self.with_columns(cast_cols)
507        }
508    }
509
510    /// Cast all frame columns to the given dtype, resulting in a new LazyFrame
511    pub fn cast_all(self, dtype: impl Into<DataTypeExpr>, strict: bool) -> Self {
512        self.with_columns(vec![if strict {
513            col(PlSmallStr::from_static("*")).strict_cast(dtype)
514        } else {
515            col(PlSmallStr::from_static("*")).cast(dtype)
516        }])
517    }
518
519    pub fn optimize(
520        self,
521        lp_arena: &mut Arena<IR>,
522        expr_arena: &mut Arena<AExpr>,
523    ) -> PolarsResult<Node> {
524        self.optimize_with_scratch(lp_arena, expr_arena, &mut vec![])
525    }
526
527    pub fn to_alp_optimized(mut self) -> PolarsResult<IRPlan> {
528        let (mut lp_arena, mut expr_arena) = self.get_arenas();
529        let node = self.optimize_with_scratch(&mut lp_arena, &mut expr_arena, &mut vec![])?;
530
531        Ok(IRPlan::new(node, lp_arena, expr_arena))
532    }
533
534    pub fn to_alp(mut self) -> PolarsResult<IRPlan> {
535        let (mut lp_arena, mut expr_arena) = self.get_arenas();
536        let node = to_alp(
537            self.logical_plan,
538            &mut expr_arena,
539            &mut lp_arena,
540            &mut self.opt_state,
541        )?;
542        let plan = IRPlan::new(node, lp_arena, expr_arena);
543        Ok(plan)
544    }
545
546    pub(crate) fn optimize_with_scratch(
547        self,
548        ir_arena: &mut Arena<IR>,
549        expr_arena: &mut Arena<AExpr>,
550        scratch: &mut Vec<Node>,
551    ) -> PolarsResult<Node> {
552        let mut opt_flags = self.opt_state;
553        // Unset CSE
554        // This can be turned on again during ir-conversion.
555        #[allow(clippy::eq_op)]
556        #[cfg(feature = "cse")]
557        if opt_flags.contains(OptFlags::EAGER) {
558            opt_flags &= !(OptFlags::COMM_SUBEXPR_ELIM | OptFlags::COMM_SUBEXPR_ELIM);
559        }
560        let root = to_alp(self.logical_plan, expr_arena, ir_arena, &mut opt_flags)?;
561
562        let lp_top = optimize(
563            root,
564            opt_flags,
565            ir_arena,
566            expr_arena,
567            scratch,
568            ExecutionHooks {
569                apply_scan_predicate_to_scan_ir,
570                hive_join,
571            },
572        )?;
573
574        Ok(lp_top)
575    }
576
577    fn prepare_collect_post_opt<P>(
578        mut self,
579        check_sink: bool,
580        post_opt: P,
581    ) -> PolarsResult<(ExecutionState, Box<dyn Executor>, bool)>
582    where
583        P: FnOnce(Node, &mut Arena<IR>, &mut Arena<AExpr>) -> PolarsResult<()>,
584    {
585        let (mut lp_arena, mut expr_arena) = self.get_arenas();
586
587        let mut scratch = vec![];
588        let lp_top = self.optimize_with_scratch(&mut lp_arena, &mut expr_arena, &mut scratch)?;
589        post_opt(lp_top, &mut lp_arena, &mut expr_arena)?;
590
591        // sink should be replaced
592        let no_file_sink = if check_sink {
593            !matches!(
594                lp_arena.get(lp_top),
595                IR::Sink {
596                    payload: SinkTypeIR::File { .. },
597                    ..
598                }
599            )
600        } else {
601            true
602        };
603        let physical_plan = create_physical_plan(
604            lp_top,
605            &mut lp_arena,
606            &mut expr_arena,
607            BUILD_STREAMING_EXECUTOR,
608        )?;
609
610        let state = ExecutionState::new();
611        Ok((state, physical_plan, no_file_sink))
612    }
613
614    // post_opt: A function that is called after optimization. This can be used to modify the IR jit.
615    pub fn _collect_post_opt<P>(self, post_opt: P) -> PolarsResult<DataFrame>
616    where
617        P: FnOnce(Node, &mut Arena<IR>, &mut Arena<AExpr>) -> PolarsResult<()>,
618    {
619        let (mut state, mut physical_plan, _) = self.prepare_collect_post_opt(false, post_opt)?;
620        physical_plan.execute(&mut state)
621    }
622
623    #[allow(unused_mut)]
624    fn prepare_collect(
625        self,
626        check_sink: bool,
627    ) -> PolarsResult<(ExecutionState, Box<dyn Executor>, bool)> {
628        self.prepare_collect_post_opt(check_sink, |_, _, _| Ok(()))
629    }
630
631    /// Execute all the lazy operations and collect them into a [`DataFrame`] using a specified
632    /// `engine`.
633    ///
634    /// The query is optimized prior to execution.
635    pub fn collect_with_engine(mut self, engine: Engine) -> PolarsResult<QueryResult> {
636        let engine = match engine {
637            Engine::Streaming => Engine::Streaming,
638            _ if std::env::var("POLARS_FORCE_STREAMING").as_deref() == Ok("1") => Engine::Streaming,
639            Engine::Auto => {
640                if self.opt_state.eager() {
641                    Engine::InMemory
642                } else {
643                    Engine::Streaming
644                }
645            },
646            v => v,
647        };
648
649        if engine != Engine::Streaming
650            && std::env::var("POLARS_AUTO_STREAMING").as_deref() == Ok("1")
651        {
652            feature_gated!("streaming", {
653                if let Some(r) = self.clone()._collect_with_streaming_suppress_todo_panic() {
654                    return r;
655                }
656            })
657        }
658        match engine {
659            Engine::Streaming => {
660                feature_gated!("streaming", self = self.with_streaming(true))
661            },
662            Engine::Gpu => self = self.with_gpu(true),
663            _ => (),
664        }
665
666        let observer = self
667            .opt_state
668            .query_monitoring()
669            .then(polars_observer::new_query_observer)
670            .flatten();
671
672        if let Some(o) = observer.as_ref() {
673            o.on_query_started()
674        }
675
676        let mut ir_plan = self.to_alp_optimized().inspect_err(|err| {
677            if let Some(o) = observer.as_ref() {
678                o.on_query_failed(err)
679            }
680        })?;
681        ir_plan.ensure_root_node_is_sink();
682
683        match engine {
684            Engine::Streaming => feature_gated!("streaming", {
685                polars_stream::run_query(
686                    ir_plan.lp_top,
687                    &mut ir_plan.lp_arena,
688                    &mut ir_plan.expr_arena,
689                    observer,
690                )
691            }),
692            Engine::InMemory | Engine::Gpu => run_in_memory_query(
693                ir_plan.lp_top,
694                &mut ir_plan.lp_arena,
695                &mut ir_plan.expr_arena,
696                engine,
697                observer,
698            ),
699            Engine::Auto => unreachable!(),
700        }
701    }
702
703    pub fn explain_all(plans: Vec<DslPlan>, opt_state: OptFlags) -> PolarsResult<String> {
704        let sink_multiple = LazyFrame {
705            logical_plan: DslPlan::SinkMultiple { inputs: plans },
706            opt_state,
707            cached_arena: Default::default(),
708        };
709        sink_multiple.explain(true)
710    }
711
712    pub fn collect_all_with_engine(
713        plans: Vec<DslPlan>,
714        engine: Engine,
715        opt_state: OptFlags,
716    ) -> PolarsResult<Vec<DataFrame>> {
717        if plans.is_empty() {
718            return Ok(Vec::new());
719        }
720
721        LazyFrame {
722            logical_plan: DslPlan::SinkMultiple { inputs: plans },
723            opt_state,
724            cached_arena: Default::default(),
725        }
726        .collect_with_engine(engine)
727        .map(|r| r.unwrap_multiple())
728    }
729
730    /// Execute all the lazy operations and collect them into a [`DataFrame`].
731    ///
732    /// The query is optimized prior to execution.
733    ///
734    /// # Example
735    ///
736    /// ```rust
737    /// use polars_core::prelude::*;
738    /// use polars_lazy::prelude::*;
739    ///
740    /// fn example(df: DataFrame) -> PolarsResult<DataFrame> {
741    ///     df.lazy()
742    ///       .group_by([col("foo")])
743    ///       .agg([col("bar").sum(), col("ham").mean().alias("avg_ham")])
744    ///       .collect()
745    /// }
746    /// ```
747    pub fn collect(self) -> PolarsResult<DataFrame> {
748        self.collect_with_engine(Engine::Auto).map(|r| match r {
749            QueryResult::Single(df) => df,
750            // TODO: Should return query results
751            QueryResult::Multiple(_) => DataFrame::empty(),
752        })
753    }
754
755    /// Collect the query in batches.
756    ///
757    /// If lazy is true the query will not start until the first poll (or until
758    /// start is called on CollectBatches).
759    #[cfg(feature = "async")]
760    pub fn collect_batches(
761        self,
762        engine: Engine,
763        maintain_order: bool,
764        chunk_size: Option<NonZeroUsize>,
765        lazy: bool,
766    ) -> PolarsResult<CollectBatches> {
767        let (send, recv) = sync_channel(1);
768        let runner_send = send.clone();
769        let ldf = self.sink_batches(
770            PlanCallback::new(move |df| {
771                // Stop if receiver has closed.
772                let send_result = send.send(Ok(df));
773                Ok(send_result.is_err())
774            }),
775            maintain_order,
776            chunk_size,
777        )?;
778        let runner = move || {
779            // We use spawn_blocking here as it has a high blocking thread pool limit.
780            polars_core::runtime::ASYNC.spawn_blocking(move || {
781                if let Err(e) = ldf.collect_with_engine(engine) {
782                    runner_send.send(Err(e)).ok();
783                }
784            });
785        };
786
787        let mut collect_batches = CollectBatches {
788            recv,
789            runner: Some(Box::new(runner)),
790        };
791        if !lazy {
792            collect_batches.start();
793        }
794        Ok(collect_batches)
795    }
796
797    pub fn sink_batches(
798        mut self,
799        function: PlanCallback<DataFrame, bool>,
800        maintain_order: bool,
801        chunk_size: Option<NonZeroUsize>,
802    ) -> PolarsResult<Self> {
803        use polars_plan::prelude::sink::CallbackSinkType;
804
805        polars_ensure!(
806            !matches!(self.logical_plan, DslPlan::Sink { .. }),
807            InvalidOperation: "cannot create a sink on top of another sink"
808        );
809
810        self.logical_plan = DslPlan::Sink {
811            input: Arc::new(self.logical_plan),
812            payload: SinkType::Callback(CallbackSinkType {
813                function,
814                maintain_order,
815                chunk_size,
816            }),
817        };
818
819        Ok(self)
820    }
821
822    /// Collect with the streaming engine. Returns `None` if the streaming engine panics with a todo!.
823    #[cfg(feature = "streaming")]
824    fn _collect_with_streaming_suppress_todo_panic(
825        mut self,
826    ) -> Option<PolarsResult<polars_core::query_result::QueryResult>> {
827        self.opt_state |= OptFlags::STREAMING;
828        let mut ir_plan = match self.to_alp_optimized() {
829            Ok(v) => v,
830            Err(e) => return Some(Err(e)),
831        };
832
833        ir_plan.ensure_root_node_is_sink();
834
835        let f = || {
836            polars_stream::run_query(
837                ir_plan.lp_top,
838                &mut ir_plan.lp_arena,
839                &mut ir_plan.expr_arena,
840                None,
841            )
842        };
843
844        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
845            Ok(v) => Some(v),
846            Err(e) => {
847                // Fallback to normal engine if error is due to not being implemented
848                // and auto_streaming is set, otherwise propagate error.
849                if e.downcast_ref::<&str>()
850                    .is_some_and(|s| s.starts_with("not yet implemented"))
851                {
852                    if polars_core::config::verbose() {
853                        eprintln!(
854                            "caught unimplemented error in new streaming engine, falling back to normal engine"
855                        );
856                    }
857                    None
858                } else {
859                    std::panic::resume_unwind(e)
860                }
861            },
862        }
863    }
864
865    pub fn sink(
866        mut self,
867        sink_type: SinkDestination,
868        file_format: FileWriteFormat,
869        unified_sink_args: UnifiedSinkArgs,
870    ) -> PolarsResult<Self> {
871        polars_ensure!(
872            !matches!(self.logical_plan, DslPlan::Sink { .. }),
873            InvalidOperation: "cannot create a sink on top of another sink"
874        );
875
876        self.logical_plan = DslPlan::Sink {
877            input: Arc::new(self.logical_plan),
878            payload: match sink_type {
879                SinkDestination::File { target } => SinkType::File(FileSinkOptions {
880                    target,
881                    file_format,
882                    unified_sink_args,
883                }),
884                SinkDestination::Partitioned {
885                    base_path,
886                    file_path_provider,
887                    partition_strategy,
888                    max_rows_per_file,
889                    approximate_bytes_per_file,
890                } => SinkType::Partitioned(PartitionedSinkOptions {
891                    base_path,
892                    file_path_provider,
893                    partition_strategy,
894                    file_format,
895                    unified_sink_args,
896                    max_rows_per_file,
897                    approximate_bytes_per_file,
898                }),
899            },
900        };
901        Ok(self)
902    }
903
904    /// Filter frame rows that match a predicate expression.
905    ///
906    /// The expression must yield boolean values (note that rows where the
907    /// predicate resolves to `null` are *not* included in the resulting frame).
908    ///
909    /// # Example
910    ///
911    /// ```rust
912    /// use polars_core::prelude::*;
913    /// use polars_lazy::prelude::*;
914    ///
915    /// fn example(df: DataFrame) -> LazyFrame {
916    ///       df.lazy()
917    ///         .filter(col("sepal_width").is_not_null())
918    ///         .select([col("sepal_width"), col("sepal_length")])
919    /// }
920    /// ```
921    pub fn filter(self, predicate: Expr) -> Self {
922        let opt_state = self.get_opt_state();
923        let lp = self.get_plan_builder().filter(predicate).build();
924        Self::from_logical_plan(lp, opt_state)
925    }
926
927    /// Remove frame rows that match a predicate expression.
928    ///
929    /// The expression must yield boolean values (note that rows where the
930    /// predicate resolves to `null` are *not* removed from the resulting frame).
931    ///
932    /// # Example
933    ///
934    /// ```rust
935    /// use polars_core::prelude::*;
936    /// use polars_lazy::prelude::*;
937    ///
938    /// fn example(df: DataFrame) -> LazyFrame {
939    ///       df.lazy()
940    ///         .remove(col("sepal_width").is_null())
941    ///         .select([col("sepal_width"), col("sepal_length")])
942    /// }
943    /// ```
944    pub fn remove(self, predicate: Expr) -> Self {
945        self.filter(predicate.neq_missing(lit(true)))
946    }
947
948    /// Select (and optionally rename, with [`alias`](crate::dsl::Expr::alias)) columns from the query.
949    ///
950    /// Columns can be selected with [`col`];
951    /// If you want to select all columns use `col(PlSmallStr::from_static("*"))`.
952    ///
953    /// # Example
954    ///
955    /// ```rust
956    /// use polars_core::prelude::*;
957    /// use polars_lazy::prelude::*;
958    ///
959    /// /// This function selects column "foo" and column "bar".
960    /// /// Column "bar" is renamed to "ham".
961    /// fn example(df: DataFrame) -> LazyFrame {
962    ///       df.lazy()
963    ///         .select([col("foo"),
964    ///                   col("bar").alias("ham")])
965    /// }
966    ///
967    /// /// This function selects all columns except "foo"
968    /// fn exclude_a_column(df: DataFrame) -> LazyFrame {
969    ///       df.lazy()
970    ///         .select([all().exclude_cols(["foo"]).as_expr()])
971    /// }
972    /// ```
973    pub fn select<E: AsRef<[Expr]>>(self, exprs: E) -> Self {
974        let exprs = exprs.as_ref().to_vec();
975        self.select_impl(
976            exprs,
977            ProjectionOptions {
978                run_parallel: true,
979                duplicate_check: true,
980                should_broadcast: true,
981                maintain_dataframe_height: false,
982            },
983        )
984    }
985
986    pub fn select_seq<E: AsRef<[Expr]>>(self, exprs: E) -> Self {
987        let exprs = exprs.as_ref().to_vec();
988        self.select_impl(
989            exprs,
990            ProjectionOptions {
991                run_parallel: false,
992                duplicate_check: true,
993                should_broadcast: true,
994                maintain_dataframe_height: false,
995            },
996        )
997    }
998
999    fn select_impl(self, exprs: Vec<Expr>, options: ProjectionOptions) -> Self {
1000        let opt_state = self.get_opt_state();
1001        let lp = self.get_plan_builder().project(exprs, options).build();
1002        Self::from_logical_plan(lp, opt_state)
1003    }
1004
1005    /// Performs a "group-by" on a `LazyFrame`, producing a [`LazyGroupBy`], which can subsequently be aggregated.
1006    ///
1007    /// Takes a list of expressions to group on.
1008    ///
1009    /// # Example
1010    ///
1011    /// ```rust
1012    /// use polars_core::prelude::*;
1013    /// use polars_lazy::prelude::*;
1014    ///
1015    /// fn example(df: DataFrame) -> LazyFrame {
1016    ///       df.lazy()
1017    ///        .group_by([col("date")])
1018    ///        .agg([
1019    ///            col("rain").min().alias("min_rain"),
1020    ///            col("rain").sum().alias("sum_rain"),
1021    ///            col("rain").quantile(lit(0.5), QuantileMethod::Nearest).alias("median_rain"),
1022    ///        ])
1023    /// }
1024    /// ```
1025    pub fn group_by<E: AsRef<[IE]>, IE: Into<Expr> + Clone>(self, by: E) -> LazyGroupBy {
1026        let keys = by
1027            .as_ref()
1028            .iter()
1029            .map(|e| e.clone().into())
1030            .collect::<Vec<_>>();
1031        let opt_state = self.get_opt_state();
1032
1033        #[cfg(feature = "dynamic_group_by")]
1034        {
1035            LazyGroupBy {
1036                logical_plan: self.logical_plan,
1037                opt_state,
1038                keys,
1039                predicates: vec![],
1040                maintain_order: false,
1041                dynamic_options: None,
1042                rolling_options: None,
1043            }
1044        }
1045
1046        #[cfg(not(feature = "dynamic_group_by"))]
1047        {
1048            LazyGroupBy {
1049                logical_plan: self.logical_plan,
1050                opt_state,
1051                keys,
1052                predicates: vec![],
1053                maintain_order: false,
1054            }
1055        }
1056    }
1057
1058    /// Create rolling groups based on a time column.
1059    ///
1060    /// Also works for index values of type UInt32, UInt64, Int32, or Int64.
1061    ///
1062    /// Different from a [`group_by_dynamic`][`Self::group_by_dynamic`], the windows are now determined by the
1063    /// individual values and are not of constant intervals. For constant intervals use
1064    /// *group_by_dynamic*
1065    #[cfg(feature = "dynamic_group_by")]
1066    pub fn rolling<E: AsRef<[Expr]>>(
1067        mut self,
1068        index_column: Expr,
1069        group_by: E,
1070        mut options: RollingGroupOptions,
1071    ) -> LazyGroupBy {
1072        if let Expr::Column(name) = index_column {
1073            options.index_column = name;
1074        } else {
1075            let output_field = index_column
1076                .to_field(&self.collect_schema().unwrap())
1077                .unwrap();
1078            return self.with_column(index_column).rolling(
1079                Expr::Column(output_field.name().clone()),
1080                group_by,
1081                options,
1082            );
1083        }
1084        let opt_state = self.get_opt_state();
1085        LazyGroupBy {
1086            logical_plan: self.logical_plan,
1087            opt_state,
1088            predicates: vec![],
1089            keys: group_by.as_ref().to_vec(),
1090            maintain_order: true,
1091            dynamic_options: None,
1092            rolling_options: Some(options),
1093        }
1094    }
1095
1096    /// Group based on a time value (or index value of type Int32, Int64).
1097    ///
1098    /// Time windows are calculated and rows are assigned to windows. Different from a
1099    /// normal group_by is that a row can be member of multiple groups. The time/index
1100    /// window could be seen as a rolling window, with a window size determined by
1101    /// dates/times/values instead of slots in the DataFrame.
1102    ///
1103    /// A window is defined by:
1104    ///
1105    /// - every: interval of the window
1106    /// - period: length of the window
1107    /// - offset: offset of the window
1108    ///
1109    /// The `group_by` argument should be empty `[]` if you don't want to combine this
1110    /// with a ordinary group_by on these keys.
1111    #[cfg(feature = "dynamic_group_by")]
1112    pub fn group_by_dynamic<E: AsRef<[Expr]>>(
1113        mut self,
1114        index_column: Expr,
1115        group_by: E,
1116        mut options: DynamicGroupOptions,
1117    ) -> LazyGroupBy {
1118        if let Expr::Column(name) = index_column {
1119            options.index_column = name;
1120        } else {
1121            let output_field = index_column
1122                .to_field(&self.collect_schema().unwrap())
1123                .unwrap();
1124            return self.with_column(index_column).group_by_dynamic(
1125                Expr::Column(output_field.name().clone()),
1126                group_by,
1127                options,
1128            );
1129        }
1130        let opt_state = self.get_opt_state();
1131        LazyGroupBy {
1132            logical_plan: self.logical_plan,
1133            opt_state,
1134            predicates: vec![],
1135            keys: group_by.as_ref().to_vec(),
1136            maintain_order: true,
1137            dynamic_options: Some(options),
1138            rolling_options: None,
1139        }
1140    }
1141
1142    /// Similar to [`group_by`][`Self::group_by`], but order of the DataFrame is maintained.
1143    pub fn group_by_stable<E: AsRef<[IE]>, IE: Into<Expr> + Clone>(self, by: E) -> LazyGroupBy {
1144        let keys = by
1145            .as_ref()
1146            .iter()
1147            .map(|e| e.clone().into())
1148            .collect::<Vec<_>>();
1149        let opt_state = self.get_opt_state();
1150
1151        #[cfg(feature = "dynamic_group_by")]
1152        {
1153            LazyGroupBy {
1154                logical_plan: self.logical_plan,
1155                opt_state,
1156                keys,
1157                predicates: vec![],
1158                maintain_order: true,
1159                dynamic_options: None,
1160                rolling_options: None,
1161            }
1162        }
1163
1164        #[cfg(not(feature = "dynamic_group_by"))]
1165        {
1166            LazyGroupBy {
1167                logical_plan: self.logical_plan,
1168                opt_state,
1169                keys,
1170                predicates: vec![],
1171                maintain_order: true,
1172            }
1173        }
1174    }
1175
1176    /// Left anti join this query with another lazy query.
1177    ///
1178    /// Matches on the values of the expressions `left_on` and `right_on`. For more
1179    /// flexible join logic, see [`join`](LazyFrame::join) or
1180    /// [`join_builder`](LazyFrame::join_builder).
1181    ///
1182    /// # Example
1183    ///
1184    /// ```rust
1185    /// use polars_core::prelude::*;
1186    /// use polars_lazy::prelude::*;
1187    /// fn anti_join_dataframes(ldf: LazyFrame, other: LazyFrame) -> PolarsResult<LazyFrame> {
1188    ///         ldf
1189    ///         .anti_join(other, col("foo"), col("bar").cast(DataType::String))
1190    /// }
1191    /// ```
1192    #[cfg(feature = "semi_anti_join")]
1193    pub fn anti_join<E: Into<Expr>>(
1194        self,
1195        other: LazyFrame,
1196        left_on: E,
1197        right_on: E,
1198    ) -> PolarsResult<LazyFrame> {
1199        self.join(
1200            other,
1201            [left_on.into()],
1202            [right_on.into()],
1203            JoinArgs::new(JoinType::Anti),
1204        )
1205    }
1206
1207    /// Creates the Cartesian product from both frames, preserving the order of the left keys.
1208    #[cfg(feature = "cross_join")]
1209    pub fn cross_join(self, other: LazyFrame, suffix: Option<PlSmallStr>) -> LazyFrame {
1210        self.join(
1211            other,
1212            vec![],
1213            vec![],
1214            JoinArgs::new(JoinType::Cross).with_suffix(suffix),
1215        )
1216        .unwrap()
1217    }
1218
1219    /// Left outer join this query with another lazy query.
1220    ///
1221    /// Matches on the values of the expressions `left_on` and `right_on`. For more
1222    /// flexible join logic, see [`join`](LazyFrame::join) or
1223    /// [`join_builder`](LazyFrame::join_builder).
1224    ///
1225    /// # Example
1226    ///
1227    /// ```rust
1228    /// use polars_core::prelude::*;
1229    /// use polars_lazy::prelude::*;
1230    /// fn left_join_dataframes(ldf: LazyFrame, other: LazyFrame) -> LazyFrame {
1231    ///         ldf
1232    ///         .left_join(other, col("foo"), col("bar"))
1233    /// }
1234    /// ```
1235    pub fn left_join<E: Into<Expr>>(self, other: LazyFrame, left_on: E, right_on: E) -> LazyFrame {
1236        self.join(
1237            other,
1238            [left_on.into()],
1239            [right_on.into()],
1240            JoinArgs::new(JoinType::Left),
1241        )
1242        .unwrap()
1243    }
1244
1245    /// Inner join this query with another lazy query.
1246    ///
1247    /// Matches on the values of the expressions `left_on` and `right_on`. For more
1248    /// flexible join logic, see [`join`](LazyFrame::join) or
1249    /// [`join_builder`](LazyFrame::join_builder).
1250    ///
1251    /// # Example
1252    ///
1253    /// ```rust
1254    /// use polars_core::prelude::*;
1255    /// use polars_lazy::prelude::*;
1256    /// fn inner_join_dataframes(ldf: LazyFrame, other: LazyFrame) -> LazyFrame {
1257    ///         ldf
1258    ///         .inner_join(other, col("foo"), col("bar").cast(DataType::String))
1259    /// }
1260    /// ```
1261    pub fn inner_join<E: Into<Expr>>(self, other: LazyFrame, left_on: E, right_on: E) -> LazyFrame {
1262        self.join(
1263            other,
1264            [left_on.into()],
1265            [right_on.into()],
1266            JoinArgs::new(JoinType::Inner),
1267        )
1268        .unwrap()
1269    }
1270
1271    /// Full outer join this query with another lazy query.
1272    ///
1273    /// Matches on the values of the expressions `left_on` and `right_on`. For more
1274    /// flexible join logic, see [`join`](LazyFrame::join) or
1275    /// [`join_builder`](LazyFrame::join_builder).
1276    ///
1277    /// # Example
1278    ///
1279    /// ```rust
1280    /// use polars_core::prelude::*;
1281    /// use polars_lazy::prelude::*;
1282    /// fn full_join_dataframes(ldf: LazyFrame, other: LazyFrame) -> LazyFrame {
1283    ///         ldf
1284    ///         .full_join(other, col("foo"), col("bar"))
1285    /// }
1286    /// ```
1287    pub fn full_join<E: Into<Expr>>(self, other: LazyFrame, left_on: E, right_on: E) -> LazyFrame {
1288        self.join(
1289            other,
1290            [left_on.into()],
1291            [right_on.into()],
1292            JoinArgs::new(JoinType::Full),
1293        )
1294        .unwrap()
1295    }
1296
1297    /// Left semi join this query with another lazy query.
1298    ///
1299    /// Matches on the values of the expressions `left_on` and `right_on`. For more
1300    /// flexible join logic, see [`join`](LazyFrame::join) or
1301    /// [`join_builder`](LazyFrame::join_builder).
1302    ///
1303    /// # Example
1304    ///
1305    /// ```rust
1306    /// use polars_core::prelude::*;
1307    /// use polars_lazy::prelude::*;
1308    /// fn semi_join_dataframes(ldf: LazyFrame, other: LazyFrame) -> LazyFrame {
1309    ///         ldf
1310    ///         .semi_join(other, col("foo"), col("bar").cast(DataType::String))
1311    /// }
1312    /// ```
1313    #[cfg(feature = "semi_anti_join")]
1314    pub fn semi_join<E: Into<Expr>>(self, other: LazyFrame, left_on: E, right_on: E) -> LazyFrame {
1315        self.join(
1316            other,
1317            [left_on.into()],
1318            [right_on.into()],
1319            JoinArgs::new(JoinType::Semi),
1320        )
1321        .unwrap()
1322    }
1323
1324    /// Generic function to join two LazyFrames.
1325    ///
1326    /// `join` can join on multiple columns, given as two list of expressions, and with a
1327    /// [`JoinType`] specified by `how`. Non-joined column names in the right DataFrame
1328    /// that already exist in this DataFrame are suffixed with `"_right"`. For control
1329    /// over how columns are renamed and parallelization options, use
1330    /// [`join_builder`](LazyFrame::join_builder).
1331    ///
1332    /// Any provided `args.slice` parameter is not considered, but set by the internal optimizer.
1333    ///
1334    /// # Example
1335    ///
1336    /// ```rust
1337    /// use polars_core::prelude::*;
1338    /// use polars_lazy::prelude::*;
1339    ///
1340    /// fn example(ldf: LazyFrame, other: LazyFrame) -> PolarsResult<LazyFrame> {
1341    ///         ldf
1342    ///         .join(other, [col("foo"), col("bar")], [col("foo"), col("bar")], JoinArgs::new(JoinType::Inner))
1343    /// }
1344    /// ```
1345    pub fn join<E: AsRef<[Expr]>>(
1346        self,
1347        other: LazyFrame,
1348        left_on: E,
1349        right_on: E,
1350        args: JoinArgs,
1351    ) -> PolarsResult<LazyFrame> {
1352        let left_on = left_on.as_ref().to_vec();
1353        let right_on = right_on.as_ref().to_vec();
1354
1355        self._join_impl(other, left_on, right_on, args)
1356    }
1357
1358    fn _join_impl(
1359        self,
1360        other: LazyFrame,
1361        left_on: Vec<Expr>,
1362        right_on: Vec<Expr>,
1363        args: JoinArgs,
1364    ) -> PolarsResult<LazyFrame> {
1365        let JoinArgs {
1366            how,
1367            validation,
1368            suffix,
1369            slice,
1370            nulls_equal,
1371            coalesce,
1372            maintain_order,
1373            build_side,
1374        } = args;
1375
1376        if slice.is_some() {
1377            panic!("impl error: slice is not handled")
1378        }
1379
1380        let mut builder = self
1381            .join_builder()
1382            .with(other)
1383            .left_on(left_on)
1384            .right_on(right_on)
1385            .how(how)
1386            .validate(validation)
1387            .join_nulls(nulls_equal)
1388            .coalesce(coalesce)
1389            .maintain_order(maintain_order)
1390            .build_side(build_side);
1391
1392        if let Some(suffix) = suffix {
1393            builder = builder.suffix(suffix);
1394        }
1395
1396        // Note: args.slice is set by the optimizer
1397        builder.finish()
1398    }
1399
1400    /// Consume `self` and return a [`JoinBuilder`] to customize a join on this LazyFrame.
1401    ///
1402    /// After the `JoinBuilder` has been created and set up, calling
1403    /// [`finish()`](JoinBuilder::finish) on it will give back the `LazyFrame`
1404    /// representing the `join` operation.
1405    pub fn join_builder(self) -> JoinBuilder {
1406        JoinBuilder::new(self)
1407    }
1408
1409    /// Gathers rows from this DataFrame based on the indices in idxs.
1410    ///
1411    /// idxs must only have a single column of indices.
1412    pub fn gather(self, idxs: LazyFrame, null_on_oob: bool) -> LazyFrame {
1413        let opt_state = self.get_opt_state();
1414        let lp = self
1415            .get_plan_builder()
1416            .gather(idxs.logical_plan, null_on_oob)
1417            .build();
1418        Self::from_logical_plan(lp, opt_state)
1419    }
1420
1421    /// Add or replace a column, given as an expression, to a DataFrame.
1422    ///
1423    /// # Example
1424    ///
1425    /// ```rust
1426    /// use polars_core::prelude::*;
1427    /// use polars_lazy::prelude::*;
1428    /// fn add_column(df: DataFrame) -> LazyFrame {
1429    ///     df.lazy()
1430    ///         .with_column(
1431    ///             when(col("sepal_length").lt(lit(5.0)))
1432    ///             .then(lit(10))
1433    ///             .otherwise(lit(1))
1434    ///             .alias("new_column_name"),
1435    ///         )
1436    /// }
1437    /// ```
1438    pub fn with_column(self, expr: Expr) -> LazyFrame {
1439        let opt_state = self.get_opt_state();
1440        let lp = self
1441            .get_plan_builder()
1442            .with_columns(
1443                vec![expr],
1444                ProjectionOptions {
1445                    run_parallel: false,
1446                    duplicate_check: true,
1447                    should_broadcast: true,
1448                    maintain_dataframe_height: false,
1449                },
1450            )
1451            .build();
1452        Self::from_logical_plan(lp, opt_state)
1453    }
1454
1455    /// Add or replace multiple columns, given as expressions, to a DataFrame.
1456    ///
1457    /// # Example
1458    ///
1459    /// ```rust
1460    /// use polars_core::prelude::*;
1461    /// use polars_lazy::prelude::*;
1462    /// fn add_columns(df: DataFrame) -> LazyFrame {
1463    ///     df.lazy()
1464    ///         .with_columns(
1465    ///             vec![lit(10).alias("foo"), lit(100).alias("bar")]
1466    ///          )
1467    /// }
1468    /// ```
1469    pub fn with_columns<E: AsRef<[Expr]>>(self, exprs: E) -> LazyFrame {
1470        let exprs = exprs.as_ref().to_vec();
1471        self.with_columns_impl(
1472            exprs,
1473            ProjectionOptions {
1474                run_parallel: true,
1475                duplicate_check: true,
1476                should_broadcast: true,
1477                maintain_dataframe_height: false,
1478            },
1479        )
1480    }
1481
1482    /// Add or replace multiple columns to a DataFrame, but evaluate them sequentially.
1483    pub fn with_columns_seq<E: AsRef<[Expr]>>(self, exprs: E) -> LazyFrame {
1484        let exprs = exprs.as_ref().to_vec();
1485        self.with_columns_impl(
1486            exprs,
1487            ProjectionOptions {
1488                run_parallel: false,
1489                duplicate_check: true,
1490                should_broadcast: true,
1491                maintain_dataframe_height: false,
1492            },
1493        )
1494    }
1495
1496    /// Match or evolve to a certain schema.
1497    pub fn match_to_schema(
1498        self,
1499        schema: SchemaRef,
1500        per_column: Arc<[MatchToSchemaPerColumn]>,
1501        extra_columns: ExtraColumnsPolicy,
1502    ) -> LazyFrame {
1503        let opt_state = self.get_opt_state();
1504        let lp = self
1505            .get_plan_builder()
1506            .match_to_schema(schema, per_column, extra_columns)
1507            .build();
1508        Self::from_logical_plan(lp, opt_state)
1509    }
1510
1511    pub fn pipe_with_schema(
1512        self,
1513        callback: PlanCallback<(Vec<DslPlan>, Vec<SchemaRef>), DslPlan>,
1514    ) -> Self {
1515        let opt_state = self.get_opt_state();
1516        let lp = self
1517            .get_plan_builder()
1518            .pipe_with_schema(vec![], callback)
1519            .build();
1520        Self::from_logical_plan(lp, opt_state)
1521    }
1522
1523    pub fn pipe_with_schemas(
1524        self,
1525        others: Vec<LazyFrame>,
1526        callback: PlanCallback<(Vec<DslPlan>, Vec<SchemaRef>), DslPlan>,
1527    ) -> Self {
1528        let opt_state = self.get_opt_state();
1529        let lp = self
1530            .get_plan_builder()
1531            .pipe_with_schema(
1532                others.into_iter().map(|lf| lf.logical_plan).collect(),
1533                callback,
1534            )
1535            .build();
1536        Self::from_logical_plan(lp, opt_state)
1537    }
1538
1539    fn with_columns_impl(self, exprs: Vec<Expr>, options: ProjectionOptions) -> LazyFrame {
1540        let opt_state = self.get_opt_state();
1541        let lp = self.get_plan_builder().with_columns(exprs, options).build();
1542        Self::from_logical_plan(lp, opt_state)
1543    }
1544
1545    /// Aggregate all the columns as their maximum values.
1546    ///
1547    /// Aggregated columns will have the same names as the original columns.
1548    pub fn max(self) -> Self {
1549        self.map_private(DslFunction::Stats(StatsFunction::Max))
1550    }
1551
1552    /// Aggregate all the columns as their minimum values.
1553    ///
1554    /// Aggregated columns will have the same names as the original columns.
1555    pub fn min(self) -> Self {
1556        self.map_private(DslFunction::Stats(StatsFunction::Min))
1557    }
1558
1559    /// Aggregate all the columns as their sum values.
1560    ///
1561    /// Aggregated columns will have the same names as the original columns.
1562    ///
1563    /// - Boolean columns will sum to a `u32` containing the number of `true`s.
1564    /// - For integer columns, the ordinary checks for overflow are performed:
1565    ///   if running in `debug` mode, overflows will panic, whereas in `release` mode overflows will
1566    ///   silently wrap.
1567    /// - String columns will sum to None.
1568    pub fn sum(self) -> Self {
1569        self.map_private(DslFunction::Stats(StatsFunction::Sum))
1570    }
1571
1572    /// Aggregate all the columns as their mean values.
1573    ///
1574    /// - Boolean and integer columns are converted to `f64` before computing the mean.
1575    /// - String columns will have a mean of None.
1576    pub fn mean(self) -> Self {
1577        self.map_private(DslFunction::Stats(StatsFunction::Mean))
1578    }
1579
1580    /// Aggregate all the columns as their median values.
1581    ///
1582    /// - Boolean and integer results are converted to `f64`. However, they are still
1583    ///   susceptible to overflow before this conversion occurs.
1584    /// - String columns will sum to None.
1585    pub fn median(self) -> Self {
1586        self.map_private(DslFunction::Stats(StatsFunction::Median))
1587    }
1588
1589    /// Aggregate all the columns as their quantile values.
1590    pub fn quantile(self, quantile: Expr, method: QuantileMethod) -> Self {
1591        self.map_private(DslFunction::Stats(StatsFunction::Quantile {
1592            quantile,
1593            method,
1594        }))
1595    }
1596
1597    /// Aggregate all the columns as their standard deviation values.
1598    ///
1599    /// `ddof` is the "Delta Degrees of Freedom"; `N - ddof` will be the denominator when
1600    /// computing the variance, where `N` is the number of rows.
1601    /// > In standard statistical practice, `ddof=1` provides an unbiased estimator of the
1602    /// > variance of a hypothetical infinite population. `ddof=0` provides a maximum
1603    /// > likelihood estimate of the variance for normally distributed variables. The
1604    /// > standard deviation computed in this function is the square root of the estimated
1605    /// > variance, so even with `ddof=1`, it will not be an unbiased estimate of the
1606    /// > standard deviation per se.
1607    ///
1608    /// Source: [Numpy](https://numpy.org/doc/stable/reference/generated/numpy.std.html#)
1609    pub fn std(self, ddof: u8) -> Self {
1610        self.map_private(DslFunction::Stats(StatsFunction::Std { ddof }))
1611    }
1612
1613    /// Aggregate all the columns as their variance values.
1614    ///
1615    /// `ddof` is the "Delta Degrees of Freedom"; `N - ddof` will be the denominator when
1616    /// computing the variance, where `N` is the number of rows.
1617    /// > In standard statistical practice, `ddof=1` provides an unbiased estimator of the
1618    /// > variance of a hypothetical infinite population. `ddof=0` provides a maximum
1619    /// > likelihood estimate of the variance for normally distributed variables.
1620    ///
1621    /// Source: [Numpy](https://numpy.org/doc/stable/reference/generated/numpy.var.html#)
1622    pub fn var(self, ddof: u8) -> Self {
1623        self.map_private(DslFunction::Stats(StatsFunction::Var { ddof }))
1624    }
1625
1626    /// Apply explode operation. [See eager explode](polars_core::frame::DataFrame::explode).
1627    pub fn explode(self, columns: Selector, options: ExplodeOptions) -> LazyFrame {
1628        self.explode_impl(columns, options, false)
1629    }
1630
1631    /// Apply explode operation. [See eager explode](polars_core::frame::DataFrame::explode).
1632    fn explode_impl(
1633        self,
1634        columns: Selector,
1635        options: ExplodeOptions,
1636        allow_empty: bool,
1637    ) -> LazyFrame {
1638        let opt_state = self.get_opt_state();
1639        let lp = self
1640            .get_plan_builder()
1641            .explode(columns, options, allow_empty)
1642            .build();
1643        Self::from_logical_plan(lp, opt_state)
1644    }
1645
1646    /// Aggregate all the columns as the sum of their null value count.
1647    pub fn null_count(self) -> LazyFrame {
1648        self.select(vec![col(PlSmallStr::from_static("*")).null_count()])
1649    }
1650
1651    /// Drop non-unique rows and maintain the order of kept rows.
1652    ///
1653    /// `subset` is an optional `Vec` of column names to consider for uniqueness; if
1654    /// `None`, all columns are considered.
1655    pub fn unique_stable(
1656        self,
1657        subset: Option<Selector>,
1658        keep_strategy: UniqueKeepStrategy,
1659    ) -> LazyFrame {
1660        let subset = subset.map(|s| vec![Expr::Selector(s)]);
1661        self.unique_stable_generic(subset, keep_strategy)
1662    }
1663
1664    pub fn unique_stable_generic(
1665        self,
1666        subset: Option<Vec<Expr>>,
1667        keep_strategy: UniqueKeepStrategy,
1668    ) -> LazyFrame {
1669        let opt_state = self.get_opt_state();
1670        let options = DistinctOptionsDSL {
1671            subset,
1672            maintain_order: true,
1673            keep_strategy,
1674        };
1675        let lp = self.get_plan_builder().distinct(options).build();
1676        Self::from_logical_plan(lp, opt_state)
1677    }
1678
1679    /// Drop non-unique rows without maintaining the order of kept rows.
1680    ///
1681    /// The order of the kept rows may change; to maintain the original row order, use
1682    /// [`unique_stable`](LazyFrame::unique_stable).
1683    ///
1684    /// `subset` is an optional `Vec` of column names to consider for uniqueness; if None,
1685    /// all columns are considered.
1686    pub fn unique(self, subset: Option<Selector>, keep_strategy: UniqueKeepStrategy) -> LazyFrame {
1687        let subset = subset.map(|s| vec![Expr::Selector(s)]);
1688        self.unique_generic(subset, keep_strategy)
1689    }
1690
1691    pub fn unique_generic(
1692        self,
1693        subset: Option<Vec<Expr>>,
1694        keep_strategy: UniqueKeepStrategy,
1695    ) -> LazyFrame {
1696        let opt_state = self.get_opt_state();
1697        let options = DistinctOptionsDSL {
1698            subset,
1699            maintain_order: false,
1700            keep_strategy,
1701        };
1702        let lp = self.get_plan_builder().distinct(options).build();
1703        Self::from_logical_plan(lp, opt_state)
1704    }
1705
1706    /// Drop rows containing one or more NaN values.
1707    ///
1708    /// `subset` is an optional `Vec` of column names to consider for NaNs; if None, all
1709    /// floating point columns are considered.
1710    pub fn drop_nans(self, subset: Option<Selector>) -> LazyFrame {
1711        let opt_state = self.get_opt_state();
1712        let lp = self.get_plan_builder().drop_nans(subset).build();
1713        Self::from_logical_plan(lp, opt_state)
1714    }
1715
1716    /// Drop rows containing one or more None values.
1717    ///
1718    /// `subset` is an optional `Vec` of column names to consider for nulls; if None, all
1719    /// columns are considered.
1720    pub fn drop_nulls(self, subset: Option<Selector>) -> LazyFrame {
1721        let opt_state = self.get_opt_state();
1722        let lp = self.get_plan_builder().drop_nulls(subset).build();
1723        Self::from_logical_plan(lp, opt_state)
1724    }
1725
1726    /// Slice the DataFrame using an offset (starting row) and a length.
1727    ///
1728    /// If `offset` is negative, it is counted from the end of the DataFrame. For
1729    /// instance, `lf.slice(-5, 3)` gets three rows, starting at the row fifth from the
1730    /// end.
1731    ///
1732    /// If `offset` and `len` are such that the slice extends beyond the end of the
1733    /// DataFrame, the portion between `offset` and the end will be returned. In this
1734    /// case, the number of rows in the returned DataFrame will be less than `len`.
1735    pub fn slice(self, offset: i64, len: IdxSize) -> LazyFrame {
1736        let opt_state = self.get_opt_state();
1737        let lp = self.get_plan_builder().slice(offset, len).build();
1738        Self::from_logical_plan(lp, opt_state)
1739    }
1740
1741    /// Remove all the rows of the LazyFrame.
1742    pub fn clear(self) -> LazyFrame {
1743        self.slice(0, 0)
1744    }
1745
1746    /// Get the first row.
1747    ///
1748    /// Equivalent to `self.slice(0, 1)`.
1749    pub fn first(self) -> LazyFrame {
1750        self.slice(0, 1)
1751    }
1752
1753    /// Get the last row.
1754    ///
1755    /// Equivalent to `self.slice(-1, 1)`.
1756    pub fn last(self) -> LazyFrame {
1757        self.slice(-1, 1)
1758    }
1759
1760    /// Get the last `n` rows.
1761    ///
1762    /// Equivalent to `self.slice(-(n as i64), n)`.
1763    pub fn tail(self, n: IdxSize) -> LazyFrame {
1764        let neg_tail = -(n as i64);
1765        self.slice(neg_tail, n)
1766    }
1767
1768    #[cfg(feature = "pivot")]
1769    #[expect(clippy::too_many_arguments)]
1770    pub fn pivot(
1771        self,
1772        on: Selector,
1773        on_columns: Arc<DataFrame>,
1774        index: Selector,
1775        values: Selector,
1776        agg: Expr,
1777        maintain_order: bool,
1778        separator: PlSmallStr,
1779        column_naming: PivotColumnNaming,
1780    ) -> LazyFrame {
1781        let opt_state = self.get_opt_state();
1782        let lp = self
1783            .get_plan_builder()
1784            .pivot(
1785                on,
1786                on_columns,
1787                index,
1788                values,
1789                agg,
1790                maintain_order,
1791                separator,
1792                column_naming,
1793            )
1794            .build();
1795        Self::from_logical_plan(lp, opt_state)
1796    }
1797
1798    /// Unpivot the DataFrame from wide to long format.
1799    ///
1800    /// See [`UnpivotArgsIR`] for information on how to unpivot a DataFrame.
1801    #[cfg(feature = "pivot")]
1802    pub fn unpivot(self, args: UnpivotArgsDSL) -> LazyFrame {
1803        let opt_state = self.get_opt_state();
1804        let lp = self.get_plan_builder().unpivot(args).build();
1805        Self::from_logical_plan(lp, opt_state)
1806    }
1807
1808    /// Limit the DataFrame to the first `n` rows.
1809    pub fn limit(self, n: IdxSize) -> LazyFrame {
1810        self.slice(0, n)
1811    }
1812
1813    /// Apply a function/closure once the logical plan get executed.
1814    ///
1815    /// The function has access to the whole materialized DataFrame at the time it is
1816    /// called.
1817    ///
1818    /// To apply specific functions to specific columns, use [`Expr::map`] in conjunction
1819    /// with `LazyFrame::with_column` or `with_columns`.
1820    ///
1821    /// ## Warning
1822    /// This can blow up in your face if the schema is changed due to the operation. The
1823    /// optimizer relies on a correct schema.
1824    ///
1825    /// You can toggle certain optimizations off.
1826    pub fn map<F>(
1827        self,
1828        function: F,
1829        optimizations: AllowedOptimizations,
1830        schema: Option<Arc<dyn UdfSchema>>,
1831        name: Option<&'static str>,
1832    ) -> LazyFrame
1833    where
1834        F: 'static + Fn(DataFrame) -> PolarsResult<DataFrame> + Send + Sync,
1835    {
1836        let opt_state = self.get_opt_state();
1837        let lp = self
1838            .get_plan_builder()
1839            .map(
1840                function,
1841                optimizations,
1842                schema,
1843                PlSmallStr::from_static(name.unwrap_or("ANONYMOUS UDF")),
1844            )
1845            .build();
1846        Self::from_logical_plan(lp, opt_state)
1847    }
1848
1849    #[cfg(feature = "python")]
1850    pub fn map_python(
1851        self,
1852        function: polars_utils::python_function::PythonFunction,
1853        optimizations: AllowedOptimizations,
1854        schema: Option<SchemaRef>,
1855        validate_output: bool,
1856    ) -> LazyFrame {
1857        let opt_state = self.get_opt_state();
1858        let lp = self
1859            .get_plan_builder()
1860            .map_python(function, optimizations, schema, validate_output)
1861            .build();
1862        Self::from_logical_plan(lp, opt_state)
1863    }
1864
1865    pub(crate) fn map_private(self, function: DslFunction) -> LazyFrame {
1866        let opt_state = self.get_opt_state();
1867        let lp = self.get_plan_builder().map_private(function).build();
1868        Self::from_logical_plan(lp, opt_state)
1869    }
1870
1871    /// Add a new column at index 0 that counts the rows.
1872    ///
1873    /// `name` is the name of the new column. `offset` is where to start counting from; if
1874    /// `None`, it is set to `0`.
1875    ///
1876    /// # Warning
1877    /// This can have a negative effect on query performance. This may for instance block
1878    /// predicate pushdown optimization.
1879    pub fn with_row_index<S>(self, name: S, offset: Option<IdxSize>) -> LazyFrame
1880    where
1881        S: Into<PlSmallStr>,
1882    {
1883        let name = name.into();
1884
1885        match &self.logical_plan {
1886            v @ DslPlan::Scan {
1887                scan_type,
1888                unified_scan_args,
1889                ..
1890            } if unified_scan_args.row_index.is_none()
1891                && !matches!(
1892                    &**scan_type,
1893                    FileScanDsl::Anonymous { .. } | FileScanDsl::ExpandedPaths { .. }
1894                ) =>
1895            {
1896                let DslPlan::Scan {
1897                    sources,
1898                    mut unified_scan_args,
1899                    scan_type,
1900                    cached_ir: _,
1901                } = v.clone()
1902                else {
1903                    unreachable!()
1904                };
1905
1906                unified_scan_args.row_index = Some(RowIndex {
1907                    name,
1908                    offset: offset.unwrap_or(0),
1909                });
1910
1911                DslPlan::Scan {
1912                    sources,
1913                    unified_scan_args,
1914                    scan_type,
1915                    cached_ir: Default::default(),
1916                }
1917                .into()
1918            },
1919            _ => self.map_private(DslFunction::RowIndex { name, offset }),
1920        }
1921    }
1922
1923    /// Return the number of non-null elements for each column.
1924    pub fn count(self) -> LazyFrame {
1925        self.select(vec![col(PlSmallStr::from_static("*")).count()])
1926    }
1927
1928    /// Unnest the given `Struct` columns: the fields of the `Struct` type will be
1929    /// inserted as columns.
1930    #[cfg(feature = "dtype-struct")]
1931    pub fn unnest(self, cols: Selector, separator: Option<PlSmallStr>) -> Self {
1932        self.map_private(DslFunction::Unnest {
1933            columns: cols,
1934            separator,
1935        })
1936    }
1937
1938    #[cfg(feature = "merge_sorted")]
1939    pub fn merge_sorted<I, S>(
1940        self,
1941        other: LazyFrame,
1942        key: I,
1943        maintain_order: bool,
1944    ) -> PolarsResult<LazyFrame>
1945    where
1946        I: IntoIterator<Item = S>,
1947        S: Into<PlSmallStr>,
1948    {
1949        let key: Arc<[PlSmallStr]> = key.into_iter().map(Into::into).collect();
1950
1951        polars_ensure!(
1952            !key.is_empty(),
1953            ComputeError: "merge_sorted requires at least one key column"
1954        );
1955
1956        let lp = DslPlan::MergeSorted {
1957            input_left: Arc::new(self.logical_plan),
1958            input_right: Arc::new(other.logical_plan),
1959            key,
1960            maintain_order,
1961        };
1962        Ok(LazyFrame::from_logical_plan(lp, self.opt_state))
1963    }
1964
1965    pub fn hint(self, hint: HintIR) -> PolarsResult<LazyFrame> {
1966        let lp = DslPlan::MapFunction {
1967            input: Arc::new(self.logical_plan),
1968            function: DslFunction::Hint(hint),
1969        };
1970        Ok(LazyFrame::from_logical_plan(lp, self.opt_state))
1971    }
1972}
1973
1974/// Utility struct for lazy group_by operation.
1975#[derive(Clone)]
1976pub struct LazyGroupBy {
1977    pub logical_plan: DslPlan,
1978    opt_state: OptFlags,
1979    keys: Vec<Expr>,
1980    predicates: Vec<Expr>,
1981    maintain_order: bool,
1982    #[cfg(feature = "dynamic_group_by")]
1983    dynamic_options: Option<DynamicGroupOptions>,
1984    #[cfg(feature = "dynamic_group_by")]
1985    rolling_options: Option<RollingGroupOptions>,
1986}
1987
1988impl From<LazyGroupBy> for LazyFrame {
1989    fn from(lgb: LazyGroupBy) -> Self {
1990        Self {
1991            logical_plan: lgb.logical_plan,
1992            opt_state: lgb.opt_state,
1993            cached_arena: Default::default(),
1994        }
1995    }
1996}
1997
1998impl LazyGroupBy {
1999    /// Filter groups with a predicate after aggregation.
2000    ///
2001    /// Similarly to the [LazyGroupBy::agg] method, the predicate must run an aggregation as it
2002    /// is evaluated on the groups.
2003    /// This method can be chained in which case all predicates must evaluate to `true` for a
2004    /// group to be kept.
2005    ///
2006    /// # Example
2007    ///
2008    /// ```rust
2009    /// use polars_core::prelude::*;
2010    /// use polars_lazy::prelude::*;
2011    ///
2012    /// fn example(df: DataFrame) -> LazyFrame {
2013    ///       df.lazy()
2014    ///        .group_by_stable([col("date")])
2015    ///        .having(col("rain").sum().gt(lit(10)))
2016    ///        .agg([col("rain").min().alias("min_rain")])
2017    /// }
2018    /// ```
2019    pub fn having(mut self, predicate: Expr) -> Self {
2020        self.predicates.push(predicate);
2021        self
2022    }
2023
2024    /// Group by and aggregate.
2025    ///
2026    /// Select a column with [col] and choose an aggregation.
2027    /// If you want to aggregate all columns use `col(PlSmallStr::from_static("*"))`.
2028    ///
2029    /// # Example
2030    ///
2031    /// ```rust
2032    /// use polars_core::prelude::*;
2033    /// use polars_lazy::prelude::*;
2034    ///
2035    /// fn example(df: DataFrame) -> LazyFrame {
2036    ///       df.lazy()
2037    ///        .group_by_stable([col("date")])
2038    ///        .agg([
2039    ///            col("rain").min().alias("min_rain"),
2040    ///            col("rain").sum().alias("sum_rain"),
2041    ///            col("rain").quantile(lit(0.5), QuantileMethod::Nearest).alias("median_rain"),
2042    ///        ])
2043    /// }
2044    /// ```
2045    pub fn agg<E: AsRef<[Expr]>>(self, aggs: E) -> LazyFrame {
2046        #[cfg(feature = "dynamic_group_by")]
2047        let lp = DslBuilder::from(self.logical_plan)
2048            .group_by(
2049                self.keys,
2050                self.predicates,
2051                aggs,
2052                None,
2053                self.maintain_order,
2054                self.dynamic_options,
2055                self.rolling_options,
2056            )
2057            .build();
2058
2059        #[cfg(not(feature = "dynamic_group_by"))]
2060        let lp = DslBuilder::from(self.logical_plan)
2061            .group_by(self.keys, self.predicates, aggs, None, self.maintain_order)
2062            .build();
2063        LazyFrame::from_logical_plan(lp, self.opt_state)
2064    }
2065
2066    /// Return first n rows of each group
2067    pub fn head(self, n: Option<usize>) -> LazyFrame {
2068        let keys = self
2069            .keys
2070            .iter()
2071            .filter_map(|expr| expr_output_name(expr).ok())
2072            .collect::<Vec<_>>();
2073
2074        self.agg([all().as_expr().head(n)]).explode_impl(
2075            all() - by_name(keys.iter().cloned(), false, false),
2076            ExplodeOptions {
2077                empty_as_null: true,
2078                keep_nulls: true,
2079            },
2080            true,
2081        )
2082    }
2083
2084    /// Return last n rows of each group
2085    pub fn tail(self, n: Option<usize>) -> LazyFrame {
2086        let keys = self
2087            .keys
2088            .iter()
2089            .filter_map(|expr| expr_output_name(expr).ok())
2090            .collect::<Vec<_>>();
2091
2092        self.agg([all().as_expr().tail(n)]).explode_impl(
2093            all() - by_name(keys.iter().cloned(), false, false),
2094            ExplodeOptions {
2095                empty_as_null: true,
2096                keep_nulls: true,
2097            },
2098            true,
2099        )
2100    }
2101
2102    /// Apply a function over the groups as a new DataFrame.
2103    ///
2104    /// **It is not recommended that you use this as materializing the DataFrame is very
2105    /// expensive.**
2106    pub fn apply(self, f: PlanCallback<DataFrame, DataFrame>, schema: SchemaRef) -> LazyFrame {
2107        if !self.predicates.is_empty() {
2108            panic!("not yet implemented: `apply` cannot be used with `having` predicates");
2109        }
2110
2111        #[cfg(feature = "dynamic_group_by")]
2112        let options = GroupbyOptions {
2113            dynamic: self.dynamic_options,
2114            rolling: self.rolling_options,
2115            slice: None,
2116        };
2117
2118        #[cfg(not(feature = "dynamic_group_by"))]
2119        let options = GroupbyOptions { slice: None };
2120
2121        let lp = DslPlan::GroupBy {
2122            input: Arc::new(self.logical_plan),
2123            keys: self.keys,
2124            predicates: vec![],
2125            aggs: vec![],
2126            apply: Some((f, schema)),
2127            maintain_order: self.maintain_order,
2128            options: Arc::new(options),
2129        };
2130        LazyFrame::from_logical_plan(lp, self.opt_state)
2131    }
2132}
2133
2134#[must_use]
2135pub struct JoinBuilder {
2136    lf: LazyFrame,
2137    how: JoinType,
2138    other: Option<LazyFrame>,
2139    left_on: Vec<Expr>,
2140    right_on: Vec<Expr>,
2141    allow_parallel: bool,
2142    force_parallel: bool,
2143    suffix: Option<PlSmallStr>,
2144    validation: JoinValidation,
2145    nulls_equal: bool,
2146    coalesce: JoinCoalesce,
2147    maintain_order: MaintainOrderJoin,
2148    build_side: Option<JoinBuildSide>,
2149}
2150impl JoinBuilder {
2151    /// Create the `JoinBuilder` with the provided `LazyFrame` as the left table.
2152    pub fn new(lf: LazyFrame) -> Self {
2153        Self {
2154            lf,
2155            other: None,
2156            how: JoinType::Inner,
2157            left_on: vec![],
2158            right_on: vec![],
2159            allow_parallel: true,
2160            force_parallel: false,
2161            suffix: None,
2162            validation: Default::default(),
2163            nulls_equal: false,
2164            coalesce: Default::default(),
2165            maintain_order: Default::default(),
2166            build_side: None,
2167        }
2168    }
2169
2170    /// The right table in the join.
2171    pub fn with(mut self, other: LazyFrame) -> Self {
2172        self.other = Some(other);
2173        self
2174    }
2175
2176    /// Select the join type.
2177    pub fn how(mut self, how: JoinType) -> Self {
2178        self.how = how;
2179        self
2180    }
2181
2182    pub fn validate(mut self, validation: JoinValidation) -> Self {
2183        self.validation = validation;
2184        self
2185    }
2186
2187    /// The expressions you want to join both tables on.
2188    ///
2189    /// The passed expressions must be valid in both `LazyFrame`s in the join.
2190    /// Selectors expand independently against each input schema. The results are
2191    /// paired in order and both sides must produce the same nonzero number of keys.
2192    pub fn on<E: AsRef<[Expr]>>(mut self, on: E) -> Self {
2193        let on = on.as_ref().to_vec();
2194        self.left_on.clone_from(&on);
2195        self.right_on = on;
2196        self
2197    }
2198
2199    /// The expressions you want to join the left table on.
2200    ///
2201    /// The passed expressions and selectors must be valid in the left table.
2202    /// The total expanded key count must be nonzero and match the right side.
2203    pub fn left_on<E: AsRef<[Expr]>>(mut self, on: E) -> Self {
2204        self.left_on = on.as_ref().to_vec();
2205        self
2206    }
2207
2208    /// The expressions you want to join the right table on.
2209    ///
2210    /// The passed expressions and selectors must be valid in the right table.
2211    /// The total expanded key count must be nonzero and match the left side.
2212    pub fn right_on<E: AsRef<[Expr]>>(mut self, on: E) -> Self {
2213        self.right_on = on.as_ref().to_vec();
2214        self
2215    }
2216
2217    /// Allow parallel table evaluation.
2218    pub fn allow_parallel(mut self, allow: bool) -> Self {
2219        self.allow_parallel = allow;
2220        self
2221    }
2222
2223    /// Force parallel table evaluation.
2224    pub fn force_parallel(mut self, force: bool) -> Self {
2225        self.force_parallel = force;
2226        self
2227    }
2228
2229    /// Join on null values. By default null values will never produce matches.
2230    pub fn join_nulls(mut self, nulls_equal: bool) -> Self {
2231        self.nulls_equal = nulls_equal;
2232        self
2233    }
2234
2235    /// Suffix to add duplicate column names in join.
2236    /// Defaults to `"_right"` if this method is never called.
2237    pub fn suffix<S>(mut self, suffix: S) -> Self
2238    where
2239        S: Into<PlSmallStr>,
2240    {
2241        self.suffix = Some(suffix.into());
2242        self
2243    }
2244
2245    /// Whether to coalesce join columns.
2246    pub fn coalesce(mut self, coalesce: JoinCoalesce) -> Self {
2247        self.coalesce = coalesce;
2248        self
2249    }
2250
2251    /// Whether to preserve the row order.
2252    pub fn maintain_order(mut self, maintain_order: MaintainOrderJoin) -> Self {
2253        self.maintain_order = maintain_order;
2254        self
2255    }
2256
2257    /// Whether to prefer a specific build side.
2258    pub fn build_side(mut self, build_side: Option<JoinBuildSide>) -> Self {
2259        self.build_side = build_side;
2260        self
2261    }
2262
2263    /// Finish builder
2264    pub fn finish(self) -> PolarsResult<LazyFrame> {
2265        let opt_state = self.lf.opt_state;
2266        let other = self.other.expect("'with' not set in join builder");
2267
2268        let args = JoinArgs {
2269            how: self.how,
2270            validation: self.validation,
2271            suffix: self.suffix,
2272            slice: None,
2273            nulls_equal: self.nulls_equal,
2274            coalesce: self.coalesce,
2275            maintain_order: self.maintain_order,
2276            build_side: self.build_side,
2277        };
2278
2279        let lp = self
2280            .lf
2281            .get_plan_builder()
2282            .join(
2283                other.logical_plan,
2284                self.left_on,
2285                self.right_on,
2286                JoinOptions {
2287                    allow_parallel: self.allow_parallel,
2288                    force_parallel: self.force_parallel,
2289                    args,
2290                }
2291                .into(),
2292            )?
2293            .build();
2294        Ok(LazyFrame::from_logical_plan(lp, opt_state))
2295    }
2296
2297    // Finish with join predicates
2298    pub fn join_where(self, predicates: Vec<Expr>) -> LazyFrame {
2299        let opt_state = self.lf.opt_state;
2300        let other = self.other.expect("with not set");
2301
2302        // Decompose `And` conjunctions into their component expressions
2303        fn decompose_and(predicate: Expr, expanded_predicates: &mut Vec<Expr>) {
2304            if let Expr::BinaryExpr {
2305                op: Operator::And,
2306                left,
2307                right,
2308            } = predicate
2309            {
2310                decompose_and((*left).clone(), expanded_predicates);
2311                decompose_and((*right).clone(), expanded_predicates);
2312            } else {
2313                expanded_predicates.push(predicate);
2314            }
2315        }
2316        let mut expanded_predicates = Vec::with_capacity(predicates.len() * 2);
2317        for predicate in predicates {
2318            decompose_and(predicate, &mut expanded_predicates);
2319        }
2320        let predicates: Vec<Expr> = expanded_predicates;
2321
2322        // Decompose `is_between` predicates to allow for cleaner expression of range joins
2323        #[cfg(feature = "is_between")]
2324        let predicates: Vec<Expr> = {
2325            let mut expanded_predicates = Vec::with_capacity(predicates.len() * 2);
2326            for predicate in predicates {
2327                if let Expr::Function {
2328                    function: FunctionExpr::Boolean(BooleanFunction::IsBetween { closed }),
2329                    input,
2330                    ..
2331                } = &predicate
2332                {
2333                    if let [expr, lower, upper] = input.as_slice() {
2334                        match closed {
2335                            ClosedInterval::Both => {
2336                                expanded_predicates.push(expr.clone().gt_eq(lower.clone()));
2337                                expanded_predicates.push(expr.clone().lt_eq(upper.clone()));
2338                            },
2339                            ClosedInterval::Right => {
2340                                expanded_predicates.push(expr.clone().gt(lower.clone()));
2341                                expanded_predicates.push(expr.clone().lt_eq(upper.clone()));
2342                            },
2343                            ClosedInterval::Left => {
2344                                expanded_predicates.push(expr.clone().gt_eq(lower.clone()));
2345                                expanded_predicates.push(expr.clone().lt(upper.clone()));
2346                            },
2347                            ClosedInterval::None => {
2348                                expanded_predicates.push(expr.clone().gt(lower.clone()));
2349                                expanded_predicates.push(expr.clone().lt(upper.clone()));
2350                            },
2351                        }
2352                        continue;
2353                    }
2354                }
2355                expanded_predicates.push(predicate);
2356            }
2357            expanded_predicates
2358        };
2359
2360        let args = JoinArgs {
2361            how: self.how,
2362            validation: self.validation,
2363            suffix: self.suffix,
2364            slice: None,
2365            nulls_equal: self.nulls_equal,
2366            coalesce: self.coalesce,
2367            maintain_order: self.maintain_order,
2368            build_side: self.build_side,
2369        };
2370        let options = JoinOptions {
2371            allow_parallel: self.allow_parallel,
2372            force_parallel: self.force_parallel,
2373            args,
2374        };
2375
2376        let lp = DslPlan::Join {
2377            input_left: Arc::new(self.lf.logical_plan),
2378            input_right: Arc::new(other.logical_plan),
2379            condition: JoinCondition::NonEqui { predicates },
2380            options: Arc::from(options),
2381        };
2382
2383        LazyFrame::from_logical_plan(lp, opt_state)
2384    }
2385}
2386
2387pub const BUILD_STREAMING_EXECUTOR: Option<polars_mem_engine::StreamingExecutorBuilder> = {
2388    #[cfg(not(feature = "streaming"))]
2389    {
2390        None
2391    }
2392    #[cfg(feature = "streaming")]
2393    {
2394        Some(polars_stream::build_streaming_query_executor)
2395    }
2396};
2397
2398fn run_in_memory_query(
2399    node: Node,
2400    ir_arena: &mut Arena<IR>,
2401    expr_arena: &mut Arena<AExpr>,
2402    engine: Engine,
2403    observer: Option<Box<dyn QueryObserver>>,
2404) -> PolarsResult<QueryResult> {
2405    let _guard = observer
2406        .as_ref()
2407        .map(|o| o.on_query_planned(to_planned_query(node, ir_arena, expr_arena)));
2408
2409    let result = if let IR::SinkMultiple { inputs } = ir_arena.get(node) {
2410        polars_ensure!(
2411            engine != Engine::Gpu,
2412            InvalidOperation:
2413            "collect_all is not supported for the gpu engine"
2414        );
2415
2416        let physical_plan = create_multiple_physical_plans(
2417            inputs.clone().as_slice(),
2418            ir_arena,
2419            expr_arena,
2420            BUILD_STREAMING_EXECUTOR,
2421        )?;
2422        physical_plan.execute().map(QueryResult::Multiple)
2423    } else {
2424        let mut physical_plan =
2425            create_physical_plan(node, ir_arena, expr_arena, BUILD_STREAMING_EXECUTOR)?;
2426        let mut state = ExecutionState::new();
2427        physical_plan.execute(&mut state).map(QueryResult::Single)
2428    };
2429
2430    result.inspect_err(|err| {
2431        if let Some(o) = observer.as_ref() {
2432            o.on_query_failed(err);
2433        }
2434    })
2435}
2436
2437fn to_planned_query(node: Node, ir_arena: &Arena<IR>, expr_arena: &Arena<AExpr>) -> PlannedQuery {
2438    let ir = ir_plan_to_description(&[node], ir_arena, expr_arena);
2439    PlannedQuery::new(ir)
2440}
2441
2442pub struct CollectBatches {
2443    recv: Receiver<PolarsResult<DataFrame>>,
2444    runner: Option<Box<dyn FnOnce() + Send + 'static>>,
2445}
2446
2447impl CollectBatches {
2448    /// Start running the query, if not already.
2449    pub fn start(&mut self) {
2450        if let Some(runner) = self.runner.take() {
2451            runner()
2452        }
2453    }
2454}
2455
2456impl Iterator for CollectBatches {
2457    type Item = PolarsResult<DataFrame>;
2458
2459    fn next(&mut self) -> Option<Self::Item> {
2460        self.start();
2461        self.recv.recv().ok()
2462    }
2463}