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