Skip to main content

polars_python/lazyframe/visitor/
expr_nodes.rs

1#[cfg(feature = "iejoin")]
2use polars::prelude::InequalityOperator;
3use polars::series::ops::NullBehavior;
4use polars_compute::rolling::{QuantileMethod, RollingFnParams};
5use polars_core::chunked_array::ops::FillNullStrategy;
6#[cfg(feature = "string_normalize")]
7use polars_ops::chunked_array::UnicodeForm;
8use polars_ops::prelude::RankMethod;
9use polars_ops::series::InterpolationMethod;
10#[cfg(feature = "search_sorted")]
11use polars_ops::series::SearchSortedSide;
12use polars_plan::plans::{
13    DynLiteralValue, IRBooleanFunction, IRFunctionExpr, IRPowFunction, IRRollingFunction,
14    IRRollingFunctionBy, IRStringFunction, IRStructFunction, IRTemporalFunction,
15};
16use polars_plan::prelude::{
17    AExpr, GroupbyOptions, IRAggExpr, LiteralValue, Operator, WindowMapping,
18};
19use polars_time::prelude::RollingGroupOptions;
20use polars_time::{ClosedWindow, Duration, DynamicGroupOptions};
21use pyo3::IntoPyObjectExt;
22use pyo3::exceptions::PyNotImplementedError;
23use pyo3::prelude::*;
24use pyo3::types::{PyInt, PyTuple};
25
26use crate::Wrap;
27use crate::series::PySeries;
28
29#[pyclass(frozen)]
30pub struct Alias {
31    #[pyo3(get)]
32    expr: usize,
33    #[pyo3(get)]
34    name: Py<PyAny>,
35}
36
37#[pyclass(frozen)]
38pub struct Column {
39    #[pyo3(get)]
40    name: Py<PyAny>,
41}
42
43#[pyclass(frozen)]
44pub struct Literal {
45    #[pyo3(get)]
46    value: Py<PyAny>,
47    #[pyo3(get)]
48    dtype: Py<PyAny>,
49}
50
51#[pyclass(name = "Operator", eq, frozen, skip_from_py_object)]
52#[derive(Copy, Clone, PartialEq)]
53pub enum PyOperator {
54    Eq,
55    EqValidity,
56    NotEq,
57    NotEqValidity,
58    Lt,
59    LtEq,
60    Gt,
61    GtEq,
62    Plus,
63    Minus,
64    Multiply,
65    Divide,
66    TrueDivide,
67    FloorDivide,
68    Modulus,
69    And,
70    Or,
71    Xor,
72    LogicalAnd,
73    LogicalOr,
74}
75
76#[pymethods]
77impl PyOperator {
78    fn __hash__(&self) -> isize {
79        *self as isize
80    }
81}
82
83impl<'py> IntoPyObject<'py> for Wrap<Operator> {
84    type Target = PyOperator;
85    type Output = Bound<'py, Self::Target>;
86    type Error = PyErr;
87
88    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
89        match self.0 {
90            Operator::Eq => PyOperator::Eq,
91            Operator::EqValidity => PyOperator::EqValidity,
92            Operator::NotEq => PyOperator::NotEq,
93            Operator::NotEqValidity => PyOperator::NotEqValidity,
94            Operator::Lt => PyOperator::Lt,
95            Operator::LtEq => PyOperator::LtEq,
96            Operator::Gt => PyOperator::Gt,
97            Operator::GtEq => PyOperator::GtEq,
98            Operator::Plus => PyOperator::Plus,
99            Operator::Minus => PyOperator::Minus,
100            Operator::Multiply => PyOperator::Multiply,
101            Operator::RustDivide => PyOperator::Divide,
102            Operator::TrueDivide => PyOperator::TrueDivide,
103            Operator::FloorDivide => PyOperator::FloorDivide,
104            Operator::Modulus => PyOperator::Modulus,
105            Operator::And => PyOperator::And,
106            Operator::Or => PyOperator::Or,
107            Operator::Xor => PyOperator::Xor,
108            Operator::LogicalAnd => PyOperator::LogicalAnd,
109            Operator::LogicalOr => PyOperator::LogicalOr,
110        }
111        .into_pyobject(py)
112    }
113}
114
115#[cfg(feature = "iejoin")]
116impl<'py> IntoPyObject<'py> for Wrap<InequalityOperator> {
117    type Target = PyOperator;
118    type Output = Bound<'py, Self::Target>;
119    type Error = PyErr;
120
121    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
122        match self.0 {
123            InequalityOperator::Lt => PyOperator::Lt,
124            InequalityOperator::LtEq => PyOperator::LtEq,
125            InequalityOperator::Gt => PyOperator::Gt,
126            InequalityOperator::GtEq => PyOperator::GtEq,
127        }
128        .into_pyobject(py)
129    }
130}
131
132#[pyclass(name = "StringFunction", eq, frozen, skip_from_py_object)]
133#[derive(Copy, Clone, PartialEq)]
134pub enum PyStringFunction {
135    ConcatHorizontal,
136    ConcatVertical,
137    Contains,
138    CountMatches,
139    EndsWith,
140    Extract,
141    ExtractAll,
142    ExtractGroups,
143    Find,
144    ToInteger,
145    LenBytes,
146    LenChars,
147    Lowercase,
148    JsonDecode,
149    JsonPathMatch,
150    Replace,
151    Reverse,
152    PadStart,
153    PadEnd,
154    Slice,
155    Head,
156    Tail,
157    HexEncode,
158    HexDecode,
159    Base64Encode,
160    Base64Decode,
161    StartsWith,
162    StripChars,
163    StripCharsStart,
164    StripCharsEnd,
165    StripPrefix,
166    StripSuffix,
167    SplitExact,
168    SplitN,
169    Strptime,
170    Split,
171    SplitRegex,
172    ToDecimal,
173    Titlecase,
174    Uppercase,
175    ZFill,
176    ContainsAny,
177    ReplaceMany,
178    EscapeRegex,
179    Normalize,
180}
181
182#[pymethods]
183impl PyStringFunction {
184    fn __hash__(&self) -> isize {
185        *self as isize
186    }
187}
188
189#[pyclass(name = "BooleanFunction", eq, frozen, skip_from_py_object)]
190#[derive(Copy, Clone, PartialEq)]
191pub enum PyBooleanFunction {
192    Any,
193    All,
194    IsEmpty,
195    HasNulls,
196    IsNull,
197    IsNotNull,
198    IsFinite,
199    IsInfinite,
200    IsNan,
201    IsNotNan,
202    IsFirstDistinct,
203    IsLastDistinct,
204    IsUnique,
205    IsDuplicated,
206    IsBetween,
207    IsIn,
208    IsClose,
209    AllHorizontal,
210    AnyHorizontal,
211    Not,
212}
213
214#[pymethods]
215impl PyBooleanFunction {
216    fn __hash__(&self) -> isize {
217        *self as isize
218    }
219}
220
221#[pyclass(name = "TemporalFunction", eq, frozen, skip_from_py_object)]
222#[derive(Copy, Clone, PartialEq)]
223pub enum PyTemporalFunction {
224    Millennium,
225    Century,
226    Year,
227    IsLeapYear,
228    IsoYear,
229    Quarter,
230    Month,
231    DaysInMonth,
232    Week,
233    WeekDay,
234    Day,
235    OrdinalDay,
236    Time,
237    Date,
238    Datetime,
239    Duration,
240    Hour,
241    Minute,
242    Second,
243    Millisecond,
244    Microsecond,
245    Nanosecond,
246    TotalDays,
247    TotalHours,
248    TotalMinutes,
249    TotalSeconds,
250    TotalMilliseconds,
251    TotalMicroseconds,
252    TotalNanoseconds,
253    ToString,
254    CastTimeUnit,
255    WithTimeUnit,
256    ConvertTimeZone,
257    TimeStamp,
258    Truncate,
259    OffsetBy,
260    MonthStart,
261    MonthEnd,
262    BaseUtcOffset,
263    DSTOffset,
264    Round,
265    Replace,
266    ReplaceTimeZone,
267    Combine,
268    DatetimeFunction,
269}
270
271#[pymethods]
272impl PyTemporalFunction {
273    fn __hash__(&self) -> isize {
274        *self as isize
275    }
276}
277
278#[pyclass(name = "StructFunction", eq, frozen, skip_from_py_object)]
279#[derive(Copy, Clone, PartialEq)]
280pub enum PyStructFunction {
281    FieldByName,
282    RenameFields,
283    PrefixFields,
284    SuffixFields,
285    JsonEncode,
286    WithFields,
287    MapFieldNames,
288}
289
290#[pymethods]
291impl PyStructFunction {
292    fn __hash__(&self) -> isize {
293        *self as isize
294    }
295}
296
297#[pyclass(name = "RollingFunction", eq, frozen, skip_from_py_object)]
298#[derive(Copy, Clone, PartialEq)]
299pub enum PyRollingFunction {
300    Min,
301    Max,
302    Mean,
303    Sum,
304    Quantile,
305    Var,
306    Std,
307    Rank,
308    Skew,
309    Kurtosis,
310}
311
312#[pymethods]
313impl PyRollingFunction {
314    fn __hash__(&self) -> isize {
315        *self as isize
316    }
317}
318
319#[pyclass(frozen)]
320pub struct BinaryExpr {
321    #[pyo3(get)]
322    left: usize,
323    #[pyo3(get)]
324    op: Py<PyAny>,
325    #[pyo3(get)]
326    right: usize,
327}
328
329#[pyclass(frozen)]
330pub struct Cast {
331    #[pyo3(get)]
332    expr: usize,
333    #[pyo3(get)]
334    dtype: Py<PyAny>,
335    // 0: strict
336    // 1: non-strict
337    // 2: overflow
338    #[pyo3(get)]
339    options: u8,
340}
341
342#[pyclass(frozen)]
343pub struct Sort {
344    #[pyo3(get)]
345    expr: usize,
346    #[pyo3(get)]
347    /// maintain_order, nulls_last, descending
348    options: (bool, bool, bool),
349}
350
351#[pyclass(frozen)]
352pub struct Gather {
353    #[pyo3(get)]
354    expr: usize,
355    #[pyo3(get)]
356    idx: usize,
357    #[pyo3(get)]
358    scalar: bool,
359}
360
361#[pyclass(frozen)]
362pub struct Filter {
363    #[pyo3(get)]
364    input: usize,
365    #[pyo3(get)]
366    by: usize,
367}
368
369#[pyclass(frozen)]
370pub struct SortBy {
371    #[pyo3(get)]
372    expr: usize,
373    #[pyo3(get)]
374    by: Vec<usize>,
375    #[pyo3(get)]
376    /// maintain_order, nulls_last, descending
377    sort_options: (bool, Vec<bool>, Vec<bool>),
378}
379
380#[pyclass(frozen)]
381pub struct Agg {
382    #[pyo3(get)]
383    name: Py<PyAny>,
384    #[pyo3(get)]
385    arguments: Vec<usize>,
386    #[pyo3(get)]
387    // Arbitrary control options
388    options: Py<PyAny>,
389}
390
391#[pyclass(frozen)]
392pub struct Ternary {
393    #[pyo3(get)]
394    predicate: usize,
395    #[pyo3(get)]
396    truthy: usize,
397    #[pyo3(get)]
398    falsy: usize,
399}
400
401#[pyclass(frozen)]
402pub struct Function {
403    #[pyo3(get)]
404    input: Vec<usize>,
405    #[pyo3(get)]
406    function_data: Py<PyAny>,
407    #[pyo3(get)]
408    options: Py<PyAny>,
409}
410
411#[pyclass(frozen)]
412pub struct Slice {
413    #[pyo3(get)]
414    input: usize,
415    #[pyo3(get)]
416    offset: usize,
417    #[pyo3(get)]
418    length: usize,
419}
420
421#[pyclass(frozen)]
422pub struct Len {}
423
424#[pyclass(frozen)]
425pub struct Window {
426    #[pyo3(get)]
427    function: usize,
428    #[pyo3(get)]
429    partition_by: Vec<usize>,
430    #[pyo3(get)]
431    order_by: Option<usize>,
432    #[pyo3(get)]
433    order_by_descending: bool,
434    #[pyo3(get)]
435    order_by_nulls_last: bool,
436    #[pyo3(get)]
437    options: Py<PyAny>,
438}
439
440#[pyclass(frozen)]
441pub struct Rolling {
442    #[pyo3(get)]
443    function: usize,
444    #[pyo3(get)]
445    index_column: usize,
446    #[pyo3(get)]
447    period: Py<PyAny>,
448    #[pyo3(get)]
449    offset: Py<PyAny>,
450    #[pyo3(get)]
451    closed_window: Py<PyAny>,
452}
453
454#[pyclass(name = "WindowMapping", frozen)]
455pub struct PyWindowMapping {
456    inner: WindowMapping,
457}
458
459#[pymethods]
460impl PyWindowMapping {
461    #[getter]
462    fn kind(&self) -> &str {
463        self.inner.into()
464    }
465}
466
467impl<'py> IntoPyObject<'py> for Wrap<Duration> {
468    type Target = PyTuple;
469    type Output = Bound<'py, Self::Target>;
470    type Error = PyErr;
471
472    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
473        (
474            self.0.months(),
475            self.0.weeks(),
476            self.0.days(),
477            self.0.nanoseconds(),
478            self.0.parsed_int,
479            self.0.negative(),
480        )
481            .into_pyobject(py)
482    }
483}
484
485impl<'py> IntoPyObject<'py> for Wrap<ClosedWindow> {
486    type Target = PyAny;
487    type Output = Bound<'py, Self::Target>;
488    type Error = PyErr;
489
490    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
491        let s = match self.0 {
492            ClosedWindow::Left => "left",
493            ClosedWindow::Right => "right",
494            ClosedWindow::Both => "both",
495            ClosedWindow::None => "none",
496        };
497        Ok(s.into_pyobject(py)?.into_any())
498    }
499}
500
501#[pyclass(name = "RollingGroupOptions", frozen)]
502pub struct PyRollingGroupOptions {
503    inner: RollingGroupOptions,
504}
505
506#[pymethods]
507impl PyRollingGroupOptions {
508    #[getter]
509    fn index_column(&self) -> &str {
510        self.inner.index_column.as_str()
511    }
512
513    #[getter]
514    fn period(&self) -> Wrap<Duration> {
515        Wrap(self.inner.period)
516    }
517
518    #[getter]
519    fn offset(&self) -> Wrap<Duration> {
520        Wrap(self.inner.offset)
521    }
522
523    #[getter]
524    fn closed_window(&self) -> &str {
525        self.inner.closed_window.into()
526    }
527}
528
529#[pyclass(name = "DynamicGroupOptions", frozen)]
530pub struct PyDynamicGroupOptions {
531    inner: DynamicGroupOptions,
532}
533
534#[pymethods]
535impl PyDynamicGroupOptions {
536    #[getter]
537    fn index_column(&self) -> &str {
538        self.inner.index_column.as_str()
539    }
540
541    #[getter]
542    fn every(&self) -> Wrap<Duration> {
543        Wrap(self.inner.every)
544    }
545
546    #[getter]
547    fn period(&self) -> Wrap<Duration> {
548        Wrap(self.inner.period)
549    }
550
551    #[getter]
552    fn offset(&self) -> Wrap<Duration> {
553        Wrap(self.inner.offset)
554    }
555
556    #[getter]
557    fn label(&self) -> &str {
558        self.inner.label.into()
559    }
560
561    #[getter]
562    fn include_boundaries(&self) -> bool {
563        self.inner.include_boundaries
564    }
565
566    #[getter]
567    fn closed_window(&self) -> &str {
568        self.inner.closed_window.into()
569    }
570    #[getter]
571    fn start_by(&self) -> &str {
572        self.inner.start_by.into()
573    }
574}
575
576#[pyclass(name = "GroupbyOptions", frozen)]
577pub struct PyGroupbyOptions {
578    inner: GroupbyOptions,
579}
580
581impl PyGroupbyOptions {
582    pub(crate) fn new(inner: GroupbyOptions) -> Self {
583        Self { inner }
584    }
585}
586
587#[pymethods]
588impl PyGroupbyOptions {
589    #[getter]
590    fn slice(&self) -> Option<(i64, usize)> {
591        self.inner.slice
592    }
593
594    #[getter]
595    fn dynamic(&self) -> Option<PyDynamicGroupOptions> {
596        self.inner
597            .dynamic
598            .as_ref()
599            .map(|f| PyDynamicGroupOptions { inner: f.clone() })
600    }
601
602    #[getter]
603    fn rolling(&self) -> Option<PyRollingGroupOptions> {
604        self.inner
605            .rolling
606            .as_ref()
607            .map(|f| PyRollingGroupOptions { inner: f.clone() })
608    }
609}
610
611pub(crate) fn into_py(py: Python<'_>, expr: &AExpr) -> PyResult<Py<PyAny>> {
612    match expr {
613        AExpr::Element => Err(PyNotImplementedError::new_err("element")),
614        AExpr::Explode { .. } => Err(PyNotImplementedError::new_err("explode")),
615        AExpr::Column(name) => Column {
616            name: name.into_py_any(py)?,
617        }
618        .into_py_any(py),
619        AExpr::StructField(_) => Err(PyNotImplementedError::new_err("field")),
620        AExpr::Literal(lit) => {
621            use polars_core::prelude::AnyValue;
622            let dtype: Py<PyAny> = Wrap(lit.get_datatype()).into_py_any(py)?;
623            let py_value = match lit {
624                LiteralValue::Dyn(d) => match d {
625                    DynLiteralValue::Int(v) => v.into_py_any(py)?,
626                    DynLiteralValue::Float(v) => v.into_py_any(py)?,
627                    DynLiteralValue::Str(v) => v.into_py_any(py)?,
628                    DynLiteralValue::List(_) => todo!(),
629                },
630                LiteralValue::Scalar(sc) => {
631                    match sc.as_any_value() {
632                        // AnyValue conversion of duration to python's
633                        // datetime.timedelta drops nanoseconds because
634                        // there is no support for them. See
635                        // https://github.com/python/cpython/issues/59648
636                        AnyValue::Duration(delta, _) => delta.into_py_any(py)?,
637                        any => Wrap(any).into_py_any(py)?,
638                    }
639                },
640                LiteralValue::Range(_) => {
641                    return Err(PyNotImplementedError::new_err("range literal"));
642                },
643                LiteralValue::Series(s) => PySeries::new((**s).clone()).into_py_any(py)?,
644            };
645
646            Literal {
647                value: py_value,
648                dtype,
649            }
650        }
651        .into_py_any(py),
652        AExpr::BinaryExpr { left, op, right } => BinaryExpr {
653            left: left.0,
654            op: Wrap(*op).into_py_any(py)?,
655            right: right.0,
656        }
657        .into_py_any(py),
658        AExpr::Cast {
659            expr,
660            dtype,
661            options,
662        } => Cast {
663            expr: expr.0,
664            dtype: Wrap(dtype.clone()).into_py_any(py)?,
665            options: *options as u8,
666        }
667        .into_py_any(py),
668        AExpr::Sort { expr, options } => Sort {
669            expr: expr.0,
670            options: (
671                options.maintain_order,
672                options.nulls_last,
673                options.descending,
674            ),
675        }
676        .into_py_any(py),
677        AExpr::Gather {
678            expr,
679            idx,
680            returns_scalar,
681            null_on_oob: _,
682        } => Gather {
683            expr: expr.0,
684            idx: idx.0,
685            scalar: *returns_scalar,
686        }
687        .into_py_any(py),
688        AExpr::Filter { input, by } => Filter {
689            input: input.0,
690            by: by.0,
691        }
692        .into_py_any(py),
693        AExpr::SortBy {
694            expr,
695            by,
696            sort_options,
697        } => SortBy {
698            expr: expr.0,
699            by: by.iter().map(|n| n.0).collect(),
700            sort_options: (
701                sort_options.maintain_order,
702                sort_options.nulls_last.clone(),
703                sort_options.descending.clone(),
704            ),
705        }
706        .into_py_any(py),
707        AExpr::Agg(aggexpr) => match aggexpr {
708            IRAggExpr::Min {
709                input,
710                propagate_nans,
711            } => Agg {
712                name: "min".into_py_any(py)?,
713                arguments: vec![input.0],
714                options: propagate_nans.into_py_any(py)?,
715            },
716            IRAggExpr::Max {
717                input,
718                propagate_nans,
719            } => Agg {
720                name: "max".into_py_any(py)?,
721                arguments: vec![input.0],
722                options: propagate_nans.into_py_any(py)?,
723            },
724            IRAggExpr::Median(n) => Agg {
725                name: "median".into_py_any(py)?,
726                arguments: vec![n.0],
727                options: py.None(),
728            },
729            IRAggExpr::NUnique(n) => Agg {
730                name: "n_unique".into_py_any(py)?,
731                arguments: vec![n.0],
732                options: py.None(),
733            },
734            IRAggExpr::First(n) => Agg {
735                name: "first".into_py_any(py)?,
736                arguments: vec![n.0],
737                options: py.None(),
738            },
739            IRAggExpr::FirstNonNull(n) => Agg {
740                name: "first_non_null".into_py_any(py)?,
741                arguments: vec![n.0],
742                options: py.None(),
743            },
744            IRAggExpr::Last(n) => Agg {
745                name: "last".into_py_any(py)?,
746                arguments: vec![n.0],
747                options: py.None(),
748            },
749            IRAggExpr::LastNonNull(n) => Agg {
750                name: "last_non_null".into_py_any(py)?,
751                arguments: vec![n.0],
752                options: py.None(),
753            },
754            IRAggExpr::Item {
755                input: n,
756                allow_empty,
757            } => Agg {
758                name: "item".into_py_any(py)?,
759                arguments: vec![n.0],
760                options: allow_empty.into_py_any(py)?,
761            },
762            IRAggExpr::Mean(n) => Agg {
763                name: "mean".into_py_any(py)?,
764                arguments: vec![n.0],
765                options: py.None(),
766            },
767            IRAggExpr::Implode {
768                input: n,
769                maintain_order,
770            } => Agg {
771                name: "implode".into_py_any(py)?,
772                arguments: vec![n.0],
773                options: maintain_order.into_py_any(py)?,
774            },
775            IRAggExpr::Sum(n) => Agg {
776                name: "sum".into_py_any(py)?,
777                arguments: vec![n.0],
778                options: py.None(),
779            },
780            IRAggExpr::Count {
781                input: n,
782                include_nulls,
783            } => Agg {
784                name: "count".into_py_any(py)?,
785                arguments: vec![n.0],
786                options: include_nulls.into_py_any(py)?,
787            },
788            IRAggExpr::Std(n, ddof) => Agg {
789                name: "std".into_py_any(py)?,
790                arguments: vec![n.0],
791                options: ddof.into_py_any(py)?,
792            },
793            IRAggExpr::Var(n, ddof) => Agg {
794                name: "var".into_py_any(py)?,
795                arguments: vec![n.0],
796                options: ddof.into_py_any(py)?,
797            },
798            IRAggExpr::AggGroups(n) => Agg {
799                name: "agg_groups".into_py_any(py)?,
800                arguments: vec![n.0],
801                options: py.None(),
802            },
803        }
804        .into_py_any(py),
805        AExpr::Ternary {
806            predicate,
807            truthy,
808            falsy,
809        } => Ternary {
810            predicate: predicate.0,
811            truthy: truthy.0,
812            falsy: falsy.0,
813        }
814        .into_py_any(py),
815        AExpr::AnonymousFunction { .. } => Err(PyNotImplementedError::new_err("anonymousfunction")),
816        AExpr::AnonymousAgg { .. } => {
817            Err(PyNotImplementedError::new_err("anonymous_streaming_agg"))
818        },
819        AExpr::Function {
820            input,
821            function,
822            // TODO: expose options
823            options: _,
824        } => Function {
825            input: input.iter().map(|n| n.node().0).collect(),
826            function_data: match function {
827                IRFunctionExpr::ArrayExpr(_) => {
828                    return Err(PyNotImplementedError::new_err("array expr"));
829                },
830                IRFunctionExpr::BinaryExpr(_) => {
831                    return Err(PyNotImplementedError::new_err("binary expr"));
832                },
833                IRFunctionExpr::Categorical(_) => {
834                    return Err(PyNotImplementedError::new_err("categorical expr"));
835                },
836                IRFunctionExpr::Extension(_) => {
837                    return Err(PyNotImplementedError::new_err("extension expr"));
838                },
839                IRFunctionExpr::ListExpr(_) => {
840                    return Err(PyNotImplementedError::new_err("list expr"));
841                },
842                IRFunctionExpr::Bitwise(_) => {
843                    return Err(PyNotImplementedError::new_err("bitwise expr"));
844                },
845                IRFunctionExpr::StringExpr(strfun) => match strfun {
846                    IRStringFunction::Format { .. } => {
847                        return Err(PyNotImplementedError::new_err("bitwise expr"));
848                    },
849                    IRStringFunction::ConcatHorizontal {
850                        delimiter,
851                        ignore_nulls,
852                    } => (
853                        PyStringFunction::ConcatHorizontal,
854                        delimiter.as_str(),
855                        ignore_nulls,
856                    )
857                        .into_py_any(py),
858                    IRStringFunction::ConcatVertical {
859                        delimiter,
860                        ignore_nulls,
861                    } => (
862                        PyStringFunction::ConcatVertical,
863                        delimiter.as_str(),
864                        ignore_nulls,
865                    )
866                        .into_py_any(py),
867                    #[cfg(feature = "regex")]
868                    IRStringFunction::Contains { literal, strict } => {
869                        (PyStringFunction::Contains, literal, strict).into_py_any(py)
870                    },
871                    IRStringFunction::CountMatches(literal) => {
872                        (PyStringFunction::CountMatches, literal).into_py_any(py)
873                    },
874                    IRStringFunction::EndsWith => (PyStringFunction::EndsWith,).into_py_any(py),
875                    IRStringFunction::Extract(group_index) => {
876                        (PyStringFunction::Extract, group_index).into_py_any(py)
877                    },
878                    IRStringFunction::ExtractAll => (PyStringFunction::ExtractAll,).into_py_any(py),
879                    #[cfg(feature = "extract_groups")]
880                    IRStringFunction::ExtractGroups { dtype, pat } => (
881                        PyStringFunction::ExtractGroups,
882                        &Wrap(dtype.clone()),
883                        pat.as_str(),
884                    )
885                        .into_py_any(py),
886                    #[cfg(feature = "regex")]
887                    IRStringFunction::Find { literal, strict } => {
888                        (PyStringFunction::Find, literal, strict).into_py_any(py)
889                    },
890                    IRStringFunction::ToInteger { dtype: _, strict } => {
891                        (PyStringFunction::ToInteger, strict).into_py_any(py)
892                    },
893                    IRStringFunction::LenBytes => (PyStringFunction::LenBytes,).into_py_any(py),
894                    IRStringFunction::LenChars => (PyStringFunction::LenChars,).into_py_any(py),
895                    IRStringFunction::Lowercase => (PyStringFunction::Lowercase,).into_py_any(py),
896                    #[cfg(feature = "extract_jsonpath")]
897                    IRStringFunction::JsonDecode(_) => {
898                        (PyStringFunction::JsonDecode, <Option<usize>>::None).into_py_any(py)
899                    },
900                    #[cfg(feature = "extract_jsonpath")]
901                    IRStringFunction::JsonPathMatch => {
902                        (PyStringFunction::JsonPathMatch,).into_py_any(py)
903                    },
904                    #[cfg(feature = "regex")]
905                    IRStringFunction::Replace { n, literal } => {
906                        (PyStringFunction::Replace, n, literal).into_py_any(py)
907                    },
908                    #[cfg(feature = "string_normalize")]
909                    IRStringFunction::Normalize { form } => (
910                        PyStringFunction::Normalize,
911                        match form {
912                            UnicodeForm::NFC => "nfc",
913                            UnicodeForm::NFKC => "nfkc",
914                            UnicodeForm::NFD => "nfd",
915                            UnicodeForm::NFKD => "nfkd",
916                        },
917                    )
918                        .into_py_any(py),
919                    IRStringFunction::Reverse => (PyStringFunction::Reverse,).into_py_any(py),
920                    IRStringFunction::PadStart { fill_char } => {
921                        (PyStringFunction::PadStart, fill_char).into_py_any(py)
922                    },
923                    IRStringFunction::PadEnd { fill_char } => {
924                        (PyStringFunction::PadEnd, fill_char).into_py_any(py)
925                    },
926                    IRStringFunction::Slice => (PyStringFunction::Slice,).into_py_any(py),
927                    IRStringFunction::Head => (PyStringFunction::Head,).into_py_any(py),
928                    IRStringFunction::Tail => (PyStringFunction::Tail,).into_py_any(py),
929                    IRStringFunction::HexEncode => (PyStringFunction::HexEncode,).into_py_any(py),
930                    #[cfg(feature = "binary_encoding")]
931                    IRStringFunction::HexDecode(strict) => {
932                        (PyStringFunction::HexDecode, strict).into_py_any(py)
933                    },
934                    IRStringFunction::Base64Encode => {
935                        (PyStringFunction::Base64Encode,).into_py_any(py)
936                    },
937                    #[cfg(feature = "binary_encoding")]
938                    IRStringFunction::Base64Decode(strict) => {
939                        (PyStringFunction::Base64Decode, strict).into_py_any(py)
940                    },
941                    IRStringFunction::StartsWith => (PyStringFunction::StartsWith,).into_py_any(py),
942                    IRStringFunction::StripChars => (PyStringFunction::StripChars,).into_py_any(py),
943                    IRStringFunction::StripCharsStart => {
944                        (PyStringFunction::StripCharsStart,).into_py_any(py)
945                    },
946                    IRStringFunction::StripCharsEnd => {
947                        (PyStringFunction::StripCharsEnd,).into_py_any(py)
948                    },
949                    IRStringFunction::StripPrefix => {
950                        (PyStringFunction::StripPrefix,).into_py_any(py)
951                    },
952                    IRStringFunction::StripSuffix => {
953                        (PyStringFunction::StripSuffix,).into_py_any(py)
954                    },
955                    IRStringFunction::SplitExact { n, inclusive } => {
956                        (PyStringFunction::SplitExact, n, inclusive).into_py_any(py)
957                    },
958                    IRStringFunction::SplitN(n) => (PyStringFunction::SplitN, n).into_py_any(py),
959                    IRStringFunction::Strptime(_, options) => (
960                        PyStringFunction::Strptime,
961                        options.format.as_ref().map(|s| s.as_str()),
962                        options.strict,
963                        options.exact,
964                        options.cache,
965                    )
966                        .into_py_any(py),
967                    IRStringFunction::Split(inclusive) => {
968                        (PyStringFunction::Split, inclusive).into_py_any(py)
969                    },
970                    IRStringFunction::SplitRegex { inclusive, strict } => {
971                        (PyStringFunction::SplitRegex, inclusive, strict).into_py_any(py)
972                    },
973                    IRStringFunction::ToDecimal { scale } => {
974                        (PyStringFunction::ToDecimal, scale).into_py_any(py)
975                    },
976                    #[cfg(feature = "nightly")]
977                    IRStringFunction::Titlecase => (PyStringFunction::Titlecase,).into_py_any(py),
978                    IRStringFunction::Uppercase => (PyStringFunction::Uppercase,).into_py_any(py),
979                    IRStringFunction::ZFill => (PyStringFunction::ZFill,).into_py_any(py),
980                    #[cfg(feature = "find_many")]
981                    IRStringFunction::ContainsAny {
982                        ascii_case_insensitive,
983                    } => (PyStringFunction::ContainsAny, ascii_case_insensitive).into_py_any(py),
984                    #[cfg(feature = "find_many")]
985                    IRStringFunction::ReplaceMany {
986                        ascii_case_insensitive,
987                        leftmost,
988                    } => (
989                        PyStringFunction::ReplaceMany,
990                        ascii_case_insensitive,
991                        leftmost,
992                    )
993                        .into_py_any(py),
994                    #[cfg(feature = "find_many")]
995                    IRStringFunction::ExtractMany { .. } => {
996                        return Err(PyNotImplementedError::new_err("extract_many"));
997                    },
998                    #[cfg(feature = "find_many")]
999                    IRStringFunction::FindMany { .. } => {
1000                        return Err(PyNotImplementedError::new_err("find_many"));
1001                    },
1002                    #[cfg(feature = "regex")]
1003                    IRStringFunction::EscapeRegex => {
1004                        (PyStringFunction::EscapeRegex,).into_py_any(py)
1005                    },
1006                },
1007                IRFunctionExpr::StructExpr(fun) => match fun {
1008                    IRStructFunction::FieldByName(name) => {
1009                        (PyStructFunction::FieldByName, name.as_str()).into_py_any(py)
1010                    },
1011                    IRStructFunction::RenameFields(names) => {
1012                        (PyStructFunction::RenameFields, names[0].as_str()).into_py_any(py)
1013                    },
1014                    IRStructFunction::PrefixFields(prefix) => {
1015                        (PyStructFunction::PrefixFields, prefix.as_str()).into_py_any(py)
1016                    },
1017                    IRStructFunction::SuffixFields(prefix) => {
1018                        (PyStructFunction::SuffixFields, prefix.as_str()).into_py_any(py)
1019                    },
1020                    #[cfg(feature = "json")]
1021                    IRStructFunction::JsonEncode => (PyStructFunction::JsonEncode,).into_py_any(py),
1022                    IRStructFunction::MapFieldNames(_) => {
1023                        return Err(PyNotImplementedError::new_err("map_field_names"));
1024                    },
1025                },
1026                IRFunctionExpr::TemporalExpr(fun) => match fun {
1027                    IRTemporalFunction::Millennium => {
1028                        (PyTemporalFunction::Millennium,).into_py_any(py)
1029                    },
1030                    IRTemporalFunction::Century => (PyTemporalFunction::Century,).into_py_any(py),
1031                    IRTemporalFunction::Year => (PyTemporalFunction::Year,).into_py_any(py),
1032                    IRTemporalFunction::IsLeapYear => {
1033                        (PyTemporalFunction::IsLeapYear,).into_py_any(py)
1034                    },
1035                    IRTemporalFunction::IsoYear => (PyTemporalFunction::IsoYear,).into_py_any(py),
1036                    IRTemporalFunction::Quarter => (PyTemporalFunction::Quarter,).into_py_any(py),
1037                    IRTemporalFunction::Month => (PyTemporalFunction::Month,).into_py_any(py),
1038                    IRTemporalFunction::Week => (PyTemporalFunction::Week,).into_py_any(py),
1039                    IRTemporalFunction::WeekDay => (PyTemporalFunction::WeekDay,).into_py_any(py),
1040                    IRTemporalFunction::Day => (PyTemporalFunction::Day,).into_py_any(py),
1041                    IRTemporalFunction::OrdinalDay => {
1042                        (PyTemporalFunction::OrdinalDay,).into_py_any(py)
1043                    },
1044                    IRTemporalFunction::Time => (PyTemporalFunction::Time,).into_py_any(py),
1045                    IRTemporalFunction::Date => (PyTemporalFunction::Date,).into_py_any(py),
1046                    IRTemporalFunction::Datetime => (PyTemporalFunction::Datetime,).into_py_any(py),
1047                    IRTemporalFunction::Duration(time_unit) => {
1048                        (PyTemporalFunction::Duration, Wrap(*time_unit)).into_py_any(py)
1049                    },
1050                    IRTemporalFunction::Hour => (PyTemporalFunction::Hour,).into_py_any(py),
1051                    IRTemporalFunction::Minute => (PyTemporalFunction::Minute,).into_py_any(py),
1052                    IRTemporalFunction::Second => (PyTemporalFunction::Second,).into_py_any(py),
1053                    IRTemporalFunction::Millisecond => {
1054                        (PyTemporalFunction::Millisecond,).into_py_any(py)
1055                    },
1056                    IRTemporalFunction::Microsecond => {
1057                        (PyTemporalFunction::Microsecond,).into_py_any(py)
1058                    },
1059                    IRTemporalFunction::Nanosecond => {
1060                        (PyTemporalFunction::Nanosecond,).into_py_any(py)
1061                    },
1062                    IRTemporalFunction::DaysInMonth => {
1063                        (PyTemporalFunction::DaysInMonth,).into_py_any(py)
1064                    },
1065                    IRTemporalFunction::TotalDays { fractional } => {
1066                        (PyTemporalFunction::TotalDays, fractional).into_py_any(py)
1067                    },
1068                    IRTemporalFunction::TotalHours { fractional } => {
1069                        (PyTemporalFunction::TotalHours, fractional).into_py_any(py)
1070                    },
1071                    IRTemporalFunction::TotalMinutes { fractional } => {
1072                        (PyTemporalFunction::TotalMinutes, fractional).into_py_any(py)
1073                    },
1074                    IRTemporalFunction::TotalSeconds { fractional } => {
1075                        (PyTemporalFunction::TotalSeconds, fractional).into_py_any(py)
1076                    },
1077                    IRTemporalFunction::TotalMilliseconds { fractional } => {
1078                        (PyTemporalFunction::TotalMilliseconds, fractional).into_py_any(py)
1079                    },
1080                    IRTemporalFunction::TotalMicroseconds { fractional } => {
1081                        (PyTemporalFunction::TotalMicroseconds, fractional).into_py_any(py)
1082                    },
1083                    IRTemporalFunction::TotalNanoseconds { fractional } => {
1084                        (PyTemporalFunction::TotalNanoseconds, fractional).into_py_any(py)
1085                    },
1086                    IRTemporalFunction::ToString(format) => {
1087                        (PyTemporalFunction::ToString, format).into_py_any(py)
1088                    },
1089                    IRTemporalFunction::CastTimeUnit(time_unit) => {
1090                        (PyTemporalFunction::CastTimeUnit, Wrap(*time_unit)).into_py_any(py)
1091                    },
1092                    IRTemporalFunction::WithTimeUnit(time_unit) => {
1093                        (PyTemporalFunction::WithTimeUnit, Wrap(*time_unit)).into_py_any(py)
1094                    },
1095                    #[cfg(feature = "timezones")]
1096                    IRTemporalFunction::ConvertTimeZone(time_zone) => {
1097                        (PyTemporalFunction::ConvertTimeZone, time_zone.as_str()).into_py_any(py)
1098                    },
1099                    IRTemporalFunction::TimeStamp(time_unit) => {
1100                        (PyTemporalFunction::TimeStamp, Wrap(*time_unit)).into_py_any(py)
1101                    },
1102                    IRTemporalFunction::Truncate => (PyTemporalFunction::Truncate,).into_py_any(py),
1103                    IRTemporalFunction::OffsetBy => (PyTemporalFunction::OffsetBy,).into_py_any(py),
1104                    IRTemporalFunction::MonthStart => {
1105                        (PyTemporalFunction::MonthStart,).into_py_any(py)
1106                    },
1107                    IRTemporalFunction::MonthEnd => (PyTemporalFunction::MonthEnd,).into_py_any(py),
1108                    #[cfg(feature = "timezones")]
1109                    IRTemporalFunction::BaseUtcOffset => {
1110                        (PyTemporalFunction::BaseUtcOffset,).into_py_any(py)
1111                    },
1112                    #[cfg(feature = "timezones")]
1113                    IRTemporalFunction::DSTOffset => {
1114                        (PyTemporalFunction::DSTOffset,).into_py_any(py)
1115                    },
1116                    IRTemporalFunction::Round => (PyTemporalFunction::Round,).into_py_any(py),
1117                    IRTemporalFunction::Replace => (PyTemporalFunction::Replace).into_py_any(py),
1118                    #[cfg(feature = "timezones")]
1119                    IRTemporalFunction::ReplaceTimeZone(time_zone, non_existent) => (
1120                        PyTemporalFunction::ReplaceTimeZone,
1121                        time_zone.as_ref().map(|s| s.as_str()),
1122                        Into::<&str>::into(non_existent),
1123                    )
1124                        .into_py_any(py),
1125                    IRTemporalFunction::Combine(time_unit) => {
1126                        (PyTemporalFunction::Combine, Wrap(*time_unit)).into_py_any(py)
1127                    },
1128                    IRTemporalFunction::DatetimeFunction {
1129                        time_unit,
1130                        time_zone,
1131                    } => (
1132                        PyTemporalFunction::DatetimeFunction,
1133                        Wrap(*time_unit),
1134                        time_zone.as_ref().map(|s| s.as_str()),
1135                    )
1136                        .into_py_any(py),
1137                },
1138                IRFunctionExpr::Boolean(boolfun) => match boolfun {
1139                    IRBooleanFunction::Any { ignore_nulls } => {
1140                        (PyBooleanFunction::Any, *ignore_nulls).into_py_any(py)
1141                    },
1142                    IRBooleanFunction::All { ignore_nulls } => {
1143                        (PyBooleanFunction::All, *ignore_nulls).into_py_any(py)
1144                    },
1145                    IRBooleanFunction::IsEmpty { ignore_nulls } => {
1146                        (PyBooleanFunction::IsEmpty, *ignore_nulls).into_py_any(py)
1147                    },
1148                    IRBooleanFunction::HasNulls => (PyBooleanFunction::HasNulls,).into_py_any(py),
1149                    IRBooleanFunction::IsNull => (PyBooleanFunction::IsNull,).into_py_any(py),
1150                    IRBooleanFunction::IsNotNull => (PyBooleanFunction::IsNotNull,).into_py_any(py),
1151                    IRBooleanFunction::IsFinite => (PyBooleanFunction::IsFinite,).into_py_any(py),
1152                    IRBooleanFunction::IsInfinite => {
1153                        (PyBooleanFunction::IsInfinite,).into_py_any(py)
1154                    },
1155                    IRBooleanFunction::IsNan => (PyBooleanFunction::IsNan,).into_py_any(py),
1156                    IRBooleanFunction::IsNotNan => (PyBooleanFunction::IsNotNan,).into_py_any(py),
1157                    IRBooleanFunction::IsFirstDistinct => {
1158                        (PyBooleanFunction::IsFirstDistinct,).into_py_any(py)
1159                    },
1160                    IRBooleanFunction::IsLastDistinct => {
1161                        (PyBooleanFunction::IsLastDistinct,).into_py_any(py)
1162                    },
1163                    IRBooleanFunction::IsUnique => (PyBooleanFunction::IsUnique,).into_py_any(py),
1164                    IRBooleanFunction::IsDuplicated => {
1165                        (PyBooleanFunction::IsDuplicated,).into_py_any(py)
1166                    },
1167                    IRBooleanFunction::IsBetween { closed } => {
1168                        (PyBooleanFunction::IsBetween, Into::<&str>::into(closed)).into_py_any(py)
1169                    },
1170                    #[cfg(feature = "is_in")]
1171                    IRBooleanFunction::IsIn { nulls_equal } => {
1172                        (PyBooleanFunction::IsIn, nulls_equal).into_py_any(py)
1173                    },
1174                    IRBooleanFunction::IsClose {
1175                        abs_tol,
1176                        rel_tol,
1177                        nans_equal,
1178                    } => (PyBooleanFunction::IsClose, abs_tol.0, rel_tol.0, nans_equal)
1179                        .into_py_any(py),
1180                    IRBooleanFunction::AllHorizontal => {
1181                        (PyBooleanFunction::AllHorizontal,).into_py_any(py)
1182                    },
1183                    IRBooleanFunction::AnyHorizontal => {
1184                        (PyBooleanFunction::AnyHorizontal,).into_py_any(py)
1185                    },
1186                    IRBooleanFunction::Not => (PyBooleanFunction::Not,).into_py_any(py),
1187                },
1188                IRFunctionExpr::Abs => ("abs",).into_py_any(py),
1189                #[cfg(feature = "hist")]
1190                IRFunctionExpr::Hist {
1191                    bin_count,
1192                    include_category,
1193                    include_breakpoint,
1194                } => ("hist", bin_count, include_category, include_breakpoint).into_py_any(py),
1195                IRFunctionExpr::NullCount => ("null_count",).into_py_any(py),
1196                IRFunctionExpr::Pow(f) => match f {
1197                    IRPowFunction::Generic => ("pow",).into_py_any(py),
1198                    IRPowFunction::Sqrt => ("sqrt",).into_py_any(py),
1199                    IRPowFunction::Cbrt => ("cbrt",).into_py_any(py),
1200                },
1201                IRFunctionExpr::Hash(seed, seed_1, seed_2, seed_3) => {
1202                    ("hash", seed, seed_1, seed_2, seed_3).into_py_any(py)
1203                },
1204                IRFunctionExpr::ArgWhere => ("argwhere",).into_py_any(py),
1205                #[cfg(feature = "index_of")]
1206                IRFunctionExpr::IndexOf => ("index_of",).into_py_any(py),
1207                #[cfg(feature = "search_sorted")]
1208                IRFunctionExpr::SearchSorted { side, descending } => (
1209                    "search_sorted",
1210                    match side {
1211                        SearchSortedSide::Any => "any",
1212                        SearchSortedSide::Left => "left",
1213                        SearchSortedSide::Right => "right",
1214                    },
1215                    descending,
1216                )
1217                    .into_py_any(py),
1218                IRFunctionExpr::Range(_) => return Err(PyNotImplementedError::new_err("range")),
1219                #[cfg(feature = "trigonometry")]
1220                IRFunctionExpr::Trigonometry(trigfun) => {
1221                    use polars_plan::plans::IRTrigonometricFunction;
1222
1223                    match trigfun {
1224                        IRTrigonometricFunction::Cos => ("cos",),
1225                        IRTrigonometricFunction::Cot => ("cot",),
1226                        IRTrigonometricFunction::Sin => ("sin",),
1227                        IRTrigonometricFunction::Tan => ("tan",),
1228                        IRTrigonometricFunction::ArcCos => ("arccos",),
1229                        IRTrigonometricFunction::ArcSin => ("arcsin",),
1230                        IRTrigonometricFunction::ArcTan => ("arctan",),
1231                        IRTrigonometricFunction::Cosh => ("cosh",),
1232                        IRTrigonometricFunction::Sinh => ("sinh",),
1233                        IRTrigonometricFunction::Tanh => ("tanh",),
1234                        IRTrigonometricFunction::ArcCosh => ("arccosh",),
1235                        IRTrigonometricFunction::ArcSinh => ("arcsinh",),
1236                        IRTrigonometricFunction::ArcTanh => ("arctanh",),
1237                        IRTrigonometricFunction::Degrees => ("degrees",),
1238                        IRTrigonometricFunction::Radians => ("radians",),
1239                    }
1240                    .into_py_any(py)
1241                },
1242                #[cfg(feature = "trigonometry")]
1243                IRFunctionExpr::Atan2 => ("atan2",).into_py_any(py),
1244                #[cfg(feature = "sign")]
1245                IRFunctionExpr::Sign => ("sign",).into_py_any(py),
1246                IRFunctionExpr::FillNull => ("fill_null",).into_py_any(py),
1247                IRFunctionExpr::RollingExpr { function, options } => {
1248                    let py_function = match function {
1249                        IRRollingFunction::Min => PyRollingFunction::Min,
1250                        IRRollingFunction::Max => PyRollingFunction::Max,
1251                        IRRollingFunction::Mean => PyRollingFunction::Mean,
1252                        IRRollingFunction::Sum => PyRollingFunction::Sum,
1253                        IRRollingFunction::Quantile => PyRollingFunction::Quantile,
1254                        IRRollingFunction::Var => PyRollingFunction::Var,
1255                        IRRollingFunction::Std => PyRollingFunction::Std,
1256                        IRRollingFunction::Rank => PyRollingFunction::Rank,
1257                        IRRollingFunction::Skew => PyRollingFunction::Skew,
1258                        IRRollingFunction::Kurtosis => PyRollingFunction::Kurtosis,
1259                        IRRollingFunction::CorrCov { .. } => {
1260                            return Err(PyNotImplementedError::new_err("rolling corr/cov"));
1261                        },
1262                        IRRollingFunction::Map(_) => {
1263                            return Err(PyNotImplementedError::new_err("rolling map"));
1264                        },
1265                    };
1266                    let fn_params: Py<PyAny> = match &options.fn_params {
1267                        None => ().into_py_any(py)?,
1268                        Some(RollingFnParams::Quantile(q)) => {
1269                            (q.prob, Into::<&str>::into(q.method)).into_py_any(py)?
1270                        },
1271                        Some(RollingFnParams::Var(v)) => (v.ddof,).into_py_any(py)?,
1272                        Some(RollingFnParams::Rank { method, seed }) => {
1273                            let method = Into::<&str>::into(method);
1274                            (method, *seed).into_py_any(py)?
1275                        },
1276                        Some(RollingFnParams::Skew { bias }) => (*bias,).into_py_any(py)?,
1277                        Some(RollingFnParams::Kurtosis { fisher, bias }) => {
1278                            (*fisher, *bias).into_py_any(py)?
1279                        },
1280                    };
1281                    // Tuple consumed by external engines (e.g., cudf-polars):
1282                    // (RollingFunction, window_size, min_periods, weights, center, fn_params)
1283                    (
1284                        py_function,
1285                        options.window_size,
1286                        options.min_periods,
1287                        &options.weights,
1288                        options.center,
1289                        fn_params,
1290                    )
1291                        .into_py_any(py)
1292                },
1293                IRFunctionExpr::RollingExprBy { function_by, .. } => match function_by {
1294                    IRRollingFunctionBy::MinBy => {
1295                        return Err(PyNotImplementedError::new_err("rolling min by"));
1296                    },
1297                    IRRollingFunctionBy::MaxBy => {
1298                        return Err(PyNotImplementedError::new_err("rolling max by"));
1299                    },
1300                    IRRollingFunctionBy::MeanBy => {
1301                        return Err(PyNotImplementedError::new_err("rolling mean by"));
1302                    },
1303                    IRRollingFunctionBy::SumBy => {
1304                        return Err(PyNotImplementedError::new_err("rolling sum by"));
1305                    },
1306                    IRRollingFunctionBy::QuantileBy => {
1307                        return Err(PyNotImplementedError::new_err("rolling quantile by"));
1308                    },
1309                    IRRollingFunctionBy::VarBy => {
1310                        return Err(PyNotImplementedError::new_err("rolling var by"));
1311                    },
1312                    IRRollingFunctionBy::StdBy => {
1313                        return Err(PyNotImplementedError::new_err("rolling std by"));
1314                    },
1315                    IRRollingFunctionBy::RankBy => {
1316                        return Err(PyNotImplementedError::new_err("rolling rank by"));
1317                    },
1318                },
1319                IRFunctionExpr::Rechunk => ("rechunk",).into_py_any(py),
1320                IRFunctionExpr::ShiftAndFill => ("shift_and_fill",).into_py_any(py),
1321                IRFunctionExpr::Shift => ("shift",).into_py_any(py),
1322                IRFunctionExpr::DropNans => ("drop_nans",).into_py_any(py),
1323                IRFunctionExpr::DropNulls => ("drop_nulls",).into_py_any(py),
1324                IRFunctionExpr::Quantile { method } => {
1325                    let method = match method {
1326                        QuantileMethod::Nearest => "nearest",
1327                        QuantileMethod::Lower => "lower",
1328                        QuantileMethod::Higher => "higher",
1329                        QuantileMethod::Midpoint => "midpoint",
1330                        QuantileMethod::Linear => "linear",
1331                        QuantileMethod::Equiprobable => "equiprobable",
1332                    };
1333                    ("quantile", method).into_py_any(py)
1334                },
1335                IRFunctionExpr::Mode { maintain_order } => {
1336                    ("mode", *maintain_order).into_py_any(py)
1337                },
1338                IRFunctionExpr::Skew(bias) => ("skew", bias).into_py_any(py),
1339                IRFunctionExpr::Kurtosis(fisher, bias) => {
1340                    ("kurtosis", fisher, bias).into_py_any(py)
1341                },
1342                IRFunctionExpr::Reshape(_) => {
1343                    return Err(PyNotImplementedError::new_err("reshape"));
1344                },
1345                #[cfg(feature = "repeat_by")]
1346                IRFunctionExpr::RepeatBy => ("repeat_by",).into_py_any(py),
1347                IRFunctionExpr::ArgUnique => ("arg_unique",).into_py_any(py),
1348                IRFunctionExpr::ArgMin => ("arg_min",).into_py_any(py),
1349                IRFunctionExpr::ArgMax => ("arg_max",).into_py_any(py),
1350                IRFunctionExpr::MinBy => ("min_by",).into_py_any(py),
1351                IRFunctionExpr::MaxBy => ("max_by",).into_py_any(py),
1352                IRFunctionExpr::ArgSort {
1353                    descending,
1354                    nulls_last,
1355                } => ("arg_max", descending, nulls_last).into_py_any(py),
1356                IRFunctionExpr::Product => ("product",).into_py_any(py),
1357                IRFunctionExpr::Repeat => ("repeat",).into_py_any(py),
1358                IRFunctionExpr::Rank { options, seed } => {
1359                    let method = match options.method {
1360                        RankMethod::Average => "average",
1361                        RankMethod::Min => "min",
1362                        RankMethod::Max => "max",
1363                        RankMethod::Dense => "dense",
1364                        RankMethod::Ordinal => "ordinal",
1365                        RankMethod::Random => "random",
1366                    };
1367                    ("rank", method, options.descending, seed.map(|s| s as i64)).into_py_any(py)
1368                },
1369                IRFunctionExpr::Clip { has_min, has_max } => {
1370                    ("clip", has_min, has_max).into_py_any(py)
1371                },
1372                IRFunctionExpr::AsStruct => ("as_struct",).into_py_any(py),
1373                #[cfg(feature = "top_k")]
1374                IRFunctionExpr::TopK { descending } => ("top_k", descending).into_py_any(py),
1375                IRFunctionExpr::CumCount { reverse } => ("cum_count", reverse).into_py_any(py),
1376                IRFunctionExpr::CumSum { reverse } => ("cum_sum", reverse).into_py_any(py),
1377                IRFunctionExpr::CumProd { reverse } => ("cum_prod", reverse).into_py_any(py),
1378                IRFunctionExpr::CumMin { reverse } => ("cum_min", reverse).into_py_any(py),
1379                IRFunctionExpr::CumMax { reverse } => ("cum_max", reverse).into_py_any(py),
1380                IRFunctionExpr::Reverse => ("reverse",).into_py_any(py),
1381                IRFunctionExpr::ValueCounts {
1382                    sort,
1383                    parallel,
1384                    name,
1385                    normalize,
1386                } => ("value_counts", sort, parallel, name.as_str(), normalize).into_py_any(py),
1387                IRFunctionExpr::UniqueCounts => ("unique_counts",).into_py_any(py),
1388                IRFunctionExpr::ApproxNUnique => ("approx_n_unique",).into_py_any(py),
1389                IRFunctionExpr::Coalesce => ("coalesce",).into_py_any(py),
1390                IRFunctionExpr::Diff(null_behaviour) => (
1391                    "diff",
1392                    match null_behaviour {
1393                        NullBehavior::Drop => "drop",
1394                        NullBehavior::Ignore => "ignore",
1395                    },
1396                )
1397                    .into_py_any(py),
1398                #[cfg(feature = "pct_change")]
1399                IRFunctionExpr::PctChange => ("pct_change",).into_py_any(py),
1400                IRFunctionExpr::Interpolate(method) => (
1401                    "interpolate",
1402                    match method {
1403                        InterpolationMethod::Linear => "linear",
1404                        InterpolationMethod::Nearest => "nearest",
1405                    },
1406                )
1407                    .into_py_any(py),
1408                IRFunctionExpr::InterpolateBy => ("interpolate_by",).into_py_any(py),
1409                IRFunctionExpr::Entropy { base, normalize } => {
1410                    ("entropy", base, normalize).into_py_any(py)
1411                },
1412                IRFunctionExpr::Log => ("log",).into_py_any(py),
1413                IRFunctionExpr::Log1p => ("log1p",).into_py_any(py),
1414                IRFunctionExpr::Exp => ("exp",).into_py_any(py),
1415                IRFunctionExpr::Unique(maintain_order) => {
1416                    ("unique", maintain_order).into_py_any(py)
1417                },
1418                IRFunctionExpr::Round { decimals, mode } => {
1419                    ("round", decimals, Into::<&str>::into(mode)).into_py_any(py)
1420                },
1421                IRFunctionExpr::RoundSF { digits } => ("round_sig_figs", digits).into_py_any(py),
1422                IRFunctionExpr::Truncate { decimals } => ("truncate", decimals).into_py_any(py),
1423                IRFunctionExpr::Floor => ("floor",).into_py_any(py),
1424                IRFunctionExpr::Ceil => ("ceil",).into_py_any(py),
1425                IRFunctionExpr::Fused(_) => return Err(PyNotImplementedError::new_err("fused")),
1426                IRFunctionExpr::ConcatExpr { .. } => {
1427                    return Err(PyNotImplementedError::new_err("concat expr"));
1428                },
1429                IRFunctionExpr::Correlation { .. } => {
1430                    return Err(PyNotImplementedError::new_err("corr"));
1431                },
1432                #[cfg(feature = "peaks")]
1433                IRFunctionExpr::PeakMin => ("peak_max",).into_py_any(py),
1434                #[cfg(feature = "peaks")]
1435                IRFunctionExpr::PeakMax => ("peak_min",).into_py_any(py),
1436                #[cfg(feature = "cutqcut")]
1437                IRFunctionExpr::Cut { .. } => return Err(PyNotImplementedError::new_err("cut")),
1438                #[cfg(feature = "cutqcut")]
1439                IRFunctionExpr::QCut { .. } => return Err(PyNotImplementedError::new_err("qcut")),
1440                #[cfg(feature = "rle")]
1441                IRFunctionExpr::RLE => ("rle",).into_py_any(py),
1442                #[cfg(feature = "rle")]
1443                IRFunctionExpr::RLEID => ("rle_id",).into_py_any(py),
1444                IRFunctionExpr::ToPhysical => ("to_physical",).into_py_any(py),
1445                IRFunctionExpr::Random { .. } => {
1446                    return Err(PyNotImplementedError::new_err("random"));
1447                },
1448                IRFunctionExpr::SetSortedFlag(sorted) => {
1449                    ("set_sorted", sorted.descending, sorted.nulls_last).into_py_any(py)
1450                },
1451                #[cfg(feature = "ffi_plugin")]
1452                IRFunctionExpr::FfiPlugin { .. } => {
1453                    return Err(PyNotImplementedError::new_err("ffi plugin"));
1454                },
1455                IRFunctionExpr::FoldHorizontal { .. } => {
1456                    Err(PyNotImplementedError::new_err("fold"))
1457                },
1458                IRFunctionExpr::ReduceHorizontal { .. } => {
1459                    Err(PyNotImplementedError::new_err("reduce"))
1460                },
1461                IRFunctionExpr::CumReduceHorizontal { .. } => {
1462                    Err(PyNotImplementedError::new_err("cum_reduce"))
1463                },
1464                IRFunctionExpr::CumFoldHorizontal { .. } => {
1465                    Err(PyNotImplementedError::new_err("cum_fold"))
1466                },
1467                IRFunctionExpr::SumHorizontal { ignore_nulls } => {
1468                    ("sum_horizontal", ignore_nulls).into_py_any(py)
1469                },
1470                IRFunctionExpr::MaxHorizontal => ("max_horizontal",).into_py_any(py),
1471                IRFunctionExpr::MeanHorizontal { ignore_nulls } => {
1472                    ("mean_horizontal", ignore_nulls).into_py_any(py)
1473                },
1474                IRFunctionExpr::MinHorizontal => ("min_horizontal",).into_py_any(py),
1475                IRFunctionExpr::EwmMean { options: _ } => {
1476                    return Err(PyNotImplementedError::new_err("ewm mean"));
1477                },
1478                IRFunctionExpr::EwmStd { options: _ } => {
1479                    return Err(PyNotImplementedError::new_err("ewm std"));
1480                },
1481                IRFunctionExpr::EwmVar { options: _ } => {
1482                    return Err(PyNotImplementedError::new_err("ewm var"));
1483                },
1484                IRFunctionExpr::Replace => ("replace",).into_py_any(py),
1485                IRFunctionExpr::ReplaceStrict { return_dtype: _ } => {
1486                    // Can ignore the return dtype because it is encoded in the schema.
1487                    ("replace_strict",).into_py_any(py)
1488                },
1489                IRFunctionExpr::Negate => ("negate",).into_py_any(py),
1490                IRFunctionExpr::FillNullWithStrategy(strategy) => {
1491                    let (strategy_str, py_limit): (&str, Py<PyAny>) = match strategy {
1492                        FillNullStrategy::Forward(limit) => {
1493                            let py_limit = limit
1494                                .map(|v| PyInt::new(py, v).into())
1495                                .unwrap_or_else(|| py.None());
1496                            ("forward", py_limit)
1497                        },
1498                        FillNullStrategy::Backward(limit) => {
1499                            let py_limit = limit
1500                                .map(|v| PyInt::new(py, v).into())
1501                                .unwrap_or_else(|| py.None());
1502                            ("backward", py_limit)
1503                        },
1504                        FillNullStrategy::Min => ("min", py.None()),
1505                        FillNullStrategy::Max => ("max", py.None()),
1506                        FillNullStrategy::Mean => ("mean", py.None()),
1507                        FillNullStrategy::Zero => ("zero", py.None()),
1508                        FillNullStrategy::One => ("one", py.None()),
1509                    };
1510
1511                    ("fill_null_with_strategy", strategy_str, py_limit).into_py_any(py)
1512                },
1513                IRFunctionExpr::GatherEvery { n, offset } => {
1514                    ("gather_every", offset, n).into_py_any(py)
1515                },
1516                IRFunctionExpr::Reinterpret(dtype) => {
1517                    ("reinterpret", &Wrap(dtype.clone())).into_py_any(py)
1518                },
1519                IRFunctionExpr::ExtendConstant => ("extend_constant",).into_py_any(py),
1520                IRFunctionExpr::Business(_) => {
1521                    return Err(PyNotImplementedError::new_err("business"));
1522                },
1523                #[cfg(feature = "top_k")]
1524                IRFunctionExpr::TopKBy { descending } => ("top_k_by", descending).into_py_any(py),
1525                IRFunctionExpr::EwmMeanBy { half_life: _ } => {
1526                    return Err(PyNotImplementedError::new_err("ewm_mean_by"));
1527                },
1528                IRFunctionExpr::RowEncode(..) => {
1529                    return Err(PyNotImplementedError::new_err("row_encode"));
1530                },
1531                IRFunctionExpr::RowDecode(..) => {
1532                    return Err(PyNotImplementedError::new_err("row_decode"));
1533                },
1534                IRFunctionExpr::DynamicPred { pred } => {
1535                    ("dynamic_pred", pred.id().map(|u| u.as_u128())).into_py_any(py)
1536                },
1537            }?,
1538            options: py.None(),
1539        }
1540        .into_py_any(py),
1541        AExpr::Rolling {
1542            function,
1543            index_column,
1544            period,
1545            offset,
1546            closed_window,
1547        } => Rolling {
1548            function: function.0,
1549            index_column: index_column.0,
1550            period: Wrap(*period).into_py_any(py)?,
1551            offset: Wrap(*offset).into_py_any(py)?,
1552            closed_window: Wrap(*closed_window).into_py_any(py)?,
1553        }
1554        .into_py_any(py),
1555        AExpr::Over {
1556            function,
1557            partition_by,
1558            order_by,
1559            mapping,
1560        } => {
1561            let function = function.0;
1562            let partition_by = partition_by.iter().map(|n| n.0).collect();
1563            let order_by_descending = order_by
1564                .map(|(_, options)| options.descending)
1565                .unwrap_or(false);
1566            let order_by_nulls_last = order_by
1567                .map(|(_, options)| options.nulls_last)
1568                .unwrap_or(false);
1569            let order_by = order_by.map(|(n, _)| n.0);
1570
1571            let options = PyWindowMapping { inner: *mapping }.into_py_any(py)?;
1572            Window {
1573                function,
1574                partition_by,
1575                order_by,
1576                order_by_descending,
1577                order_by_nulls_last,
1578                options,
1579            }
1580            .into_py_any(py)
1581        },
1582        AExpr::Slice {
1583            input,
1584            offset,
1585            length,
1586        } => Slice {
1587            input: input.0,
1588            offset: offset.0,
1589            length: length.0,
1590        }
1591        .into_py_any(py),
1592        AExpr::Len => Len {}.into_py_any(py),
1593        AExpr::Eval { .. } => Err(PyNotImplementedError::new_err("list.eval")),
1594        AExpr::StructEval { .. } => Err(PyNotImplementedError::new_err("struct.with_fields")),
1595    }
1596}