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