Skip to main content

datafusion_python/
udwf.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::ops::Range;
19use std::ptr::NonNull;
20use std::sync::Arc;
21
22use arrow::array::{Array, ArrayData, ArrayRef, make_array};
23use datafusion::arrow::datatypes::DataType;
24use datafusion::arrow::pyarrow::{FromPyArrow, PyArrowType, ToPyArrow};
25use datafusion::error::{DataFusionError, Result};
26use datafusion::logical_expr::function::{PartitionEvaluatorArgs, WindowUDFFieldArgs};
27use datafusion::logical_expr::window_state::WindowAggState;
28use datafusion::logical_expr::{
29    PartitionEvaluator, PartitionEvaluatorFactory, Signature, Volatility, WindowUDF, WindowUDFImpl,
30};
31use datafusion::scalar::ScalarValue;
32use datafusion_ffi::udwf::FFI_WindowUDF;
33use datafusion_python_util::parse_volatility;
34use pyo3::exceptions::PyValueError;
35use pyo3::prelude::*;
36use pyo3::types::{PyCapsule, PyList, PyTuple};
37
38use crate::common::data_type::PyScalarValue;
39use crate::errors::{PyDataFusionResult, to_datafusion_err};
40use crate::expr::PyExpr;
41
42#[derive(Debug)]
43struct RustPartitionEvaluator {
44    evaluator: Py<PyAny>,
45}
46
47impl RustPartitionEvaluator {
48    fn new(evaluator: Py<PyAny>) -> Self {
49        Self { evaluator }
50    }
51}
52
53impl PartitionEvaluator for RustPartitionEvaluator {
54    fn memoize(&mut self, _state: &mut WindowAggState) -> Result<()> {
55        Python::attach(|py| self.evaluator.bind(py).call_method0("memoize").map(|_| ()))
56            .map_err(|e| DataFusionError::Execution(format!("{e}")))
57    }
58
59    fn get_range(&self, idx: usize, n_rows: usize) -> Result<Range<usize>> {
60        Python::attach(|py| {
61            let py_args = vec![idx.into_pyobject(py)?, n_rows.into_pyobject(py)?];
62            let py_args = PyTuple::new(py, py_args)?;
63
64            self.evaluator
65                .bind(py)
66                .call_method1("get_range", py_args)
67                .and_then(|v| {
68                    let tuple: Bound<'_, PyTuple> = v.extract()?;
69                    if tuple.len() != 2 {
70                        return Err(PyValueError::new_err(format!(
71                            "Expected get_range to return tuple of length 2. Received length {}",
72                            tuple.len()
73                        )));
74                    }
75
76                    let start: usize = tuple.get_item(0).unwrap().extract()?;
77                    let end: usize = tuple.get_item(1).unwrap().extract()?;
78
79                    Ok(Range { start, end })
80                })
81        })
82        .map_err(|e| DataFusionError::Execution(format!("{e}")))
83    }
84
85    fn is_causal(&self) -> bool {
86        Python::attach(|py| {
87            self.evaluator
88                .bind(py)
89                .call_method0("is_causal")
90                .and_then(|v| v.extract())
91                .unwrap_or(false)
92        })
93    }
94
95    fn evaluate_all(&mut self, values: &[ArrayRef], num_rows: usize) -> Result<ArrayRef> {
96        Python::attach(|py| {
97            let py_values = PyList::new(
98                py,
99                values
100                    .iter()
101                    .map(|arg| arg.into_data().to_pyarrow(py).unwrap()),
102            )?;
103            let py_num_rows = num_rows.into_pyobject(py)?;
104            let py_args = PyTuple::new(py, vec![py_values.as_any(), &py_num_rows])?;
105
106            self.evaluator
107                .bind(py)
108                .call_method1("evaluate_all", py_args)
109                .map(|v| {
110                    let array_data = ArrayData::from_pyarrow_bound(&v).unwrap();
111                    make_array(array_data)
112                })
113        })
114        .map_err(to_datafusion_err)
115    }
116
117    fn evaluate(&mut self, values: &[ArrayRef], range: &Range<usize>) -> Result<ScalarValue> {
118        Python::attach(|py| {
119            let py_values = PyList::new(
120                py,
121                values
122                    .iter()
123                    .map(|arg| arg.into_data().to_pyarrow(py).unwrap()),
124            )?;
125            let range_tuple = PyTuple::new(py, vec![range.start, range.end])?;
126            let py_args = PyTuple::new(py, vec![py_values.as_any(), range_tuple.as_any()])?;
127
128            self.evaluator
129                .bind(py)
130                .call_method1("evaluate", py_args)
131                .and_then(|v| v.extract::<PyScalarValue>())
132                .map(|v| v.0)
133        })
134        .map_err(to_datafusion_err)
135    }
136
137    fn evaluate_all_with_rank(
138        &self,
139        num_rows: usize,
140        ranks_in_partition: &[Range<usize>],
141    ) -> Result<ArrayRef> {
142        Python::attach(|py| {
143            let ranks = ranks_in_partition
144                .iter()
145                .map(|r| PyTuple::new(py, vec![r.start, r.end]))
146                .collect::<PyResult<Vec<_>>>()?;
147
148            // 1. cast args to Pyarrow array
149            let py_args = vec![
150                num_rows.into_pyobject(py)?.into_any(),
151                PyList::new(py, ranks)?.into_any(),
152            ];
153
154            let py_args = PyTuple::new(py, py_args)?;
155
156            // 2. call function
157            self.evaluator
158                .bind(py)
159                .call_method1("evaluate_all_with_rank", py_args)
160                .map(|v| {
161                    let array_data = ArrayData::from_pyarrow_bound(&v).unwrap();
162                    make_array(array_data)
163                })
164        })
165        .map_err(to_datafusion_err)
166    }
167
168    fn supports_bounded_execution(&self) -> bool {
169        Python::attach(|py| {
170            self.evaluator
171                .bind(py)
172                .call_method0("supports_bounded_execution")
173                .and_then(|v| v.extract())
174                .unwrap_or(false)
175        })
176    }
177
178    fn uses_window_frame(&self) -> bool {
179        Python::attach(|py| {
180            self.evaluator
181                .bind(py)
182                .call_method0("uses_window_frame")
183                .and_then(|v| v.extract())
184                .unwrap_or(false)
185        })
186    }
187
188    fn include_rank(&self) -> bool {
189        Python::attach(|py| {
190            self.evaluator
191                .bind(py)
192                .call_method0("include_rank")
193                .and_then(|v| v.extract())
194                .unwrap_or(false)
195        })
196    }
197}
198
199fn instantiate_partition_evaluator(evaluator: &Py<PyAny>) -> Result<Box<dyn PartitionEvaluator>> {
200    let instance = Python::attach(|py| {
201        evaluator
202            .call0(py)
203            .map_err(|e| DataFusionError::Execution(e.to_string()))
204    })?;
205    Ok(Box::new(RustPartitionEvaluator::new(instance)))
206}
207
208/// Wrap a Python evaluator factory in a `PartitionEvaluatorFactory`.
209///
210/// Retained for downstream callers that previously consumed this
211/// helper to build a [`PartitionEvaluatorFactory`] for factory-based
212/// APIs. New in-crate code should construct a
213/// [`PythonFunctionWindowUDF`] directly so the codec can downcast and
214/// ship it inline.
215pub fn to_rust_partition_evaluator(evaluator: Py<PyAny>) -> PartitionEvaluatorFactory {
216    Arc::new(move || instantiate_partition_evaluator(&evaluator))
217}
218
219/// Represents an WindowUDF
220#[pyclass(
221    from_py_object,
222    frozen,
223    name = "WindowUDF",
224    module = "datafusion",
225    subclass
226)]
227#[derive(Debug, Clone)]
228pub struct PyWindowUDF {
229    pub(crate) function: WindowUDF,
230}
231
232#[pymethods]
233impl PyWindowUDF {
234    #[new]
235    #[pyo3(signature=(name, evaluator, input_types, return_type, volatility))]
236    fn new(
237        name: &str,
238        evaluator: Py<PyAny>,
239        input_types: Vec<PyArrowType<DataType>>,
240        return_type: PyArrowType<DataType>,
241        volatility: &str,
242    ) -> PyResult<Self> {
243        let return_type = return_type.0;
244        let input_types: Vec<DataType> = input_types.into_iter().map(|t| t.0).collect();
245
246        let function = WindowUDF::from(PythonFunctionWindowUDF::new(
247            name,
248            evaluator,
249            input_types,
250            return_type,
251            parse_volatility(volatility)?,
252        ));
253        Ok(Self { function })
254    }
255
256    /// creates a new PyExpr with the call of the udf
257    #[pyo3(signature = (*args))]
258    fn __call__(&self, args: Vec<PyExpr>) -> PyResult<PyExpr> {
259        let args = args.iter().map(|e| e.expr.clone()).collect();
260        Ok(self.function.call(args).into())
261    }
262
263    #[staticmethod]
264    pub fn from_pycapsule(func: Bound<'_, PyAny>) -> PyDataFusionResult<Self> {
265        let capsule = if func.hasattr("__datafusion_window_udf__")? {
266            func.getattr("__datafusion_window_udf__")?.call0()?
267        } else {
268            func
269        };
270
271        let capsule = capsule.cast::<PyCapsule>().map_err(to_datafusion_err)?;
272        let data: NonNull<FFI_WindowUDF> = capsule
273            .pointer_checked(Some(c"datafusion_window_udf"))?
274            .cast();
275        let udwf = unsafe { data.as_ref() };
276        let udwf: Arc<dyn WindowUDFImpl> = udwf.into();
277
278        Ok(Self {
279            function: WindowUDF::new_from_shared_impl(udwf),
280        })
281    }
282
283    fn __repr__(&self) -> PyResult<String> {
284        Ok(format!("WindowUDF({})", self.function.name()))
285    }
286
287    #[getter]
288    fn name(&self) -> &str {
289        self.function.name()
290    }
291}
292
293/// `WindowUDFImpl` for Python-defined window UDFs.
294///
295/// Holds the Python evaluator factory directly so the codec can
296/// downcast and cloudpickle it across process boundaries. Replaces
297/// the prior factory-erased `MultiColumnWindowUDF`; the old name is
298/// kept as a type alias below for backward compatibility.
299#[derive(Debug)]
300pub struct PythonFunctionWindowUDF {
301    name: String,
302    evaluator: Py<PyAny>,
303    signature: Signature,
304    return_type: DataType,
305}
306
307/// Backward-compatible alias for downstream crates that referenced the
308/// previous struct name. New code should use [`PythonFunctionWindowUDF`].
309pub type MultiColumnWindowUDF = PythonFunctionWindowUDF;
310
311impl PythonFunctionWindowUDF {
312    pub fn new(
313        name: impl Into<String>,
314        evaluator: Py<PyAny>,
315        input_types: Vec<DataType>,
316        return_type: DataType,
317        volatility: Volatility,
318    ) -> Self {
319        let name = name.into();
320        let signature = Signature::exact(input_types, volatility);
321        Self {
322            name,
323            evaluator,
324            signature,
325            return_type,
326        }
327    }
328
329    /// Stored Python callable that produces a fresh partition
330    /// evaluator instance per partition. Consumed by the codec to
331    /// cloudpickle the evaluator factory across process boundaries.
332    pub(crate) fn evaluator(&self) -> &Py<PyAny> {
333        &self.evaluator
334    }
335
336    pub(crate) fn return_type(&self) -> &DataType {
337        &self.return_type
338    }
339}
340
341impl Eq for PythonFunctionWindowUDF {}
342impl PartialEq for PythonFunctionWindowUDF {
343    fn eq(&self, other: &Self) -> bool {
344        self.name == other.name
345            && self.signature == other.signature
346            && self.return_type == other.return_type
347            // Pointer-identity fast path: `Arc`-shared clones of the
348            // same UDF skip the GIL roundtrip. Falls through to Python
349            // `__eq__` only for two distinct callables.
350            && (self.evaluator.as_ptr() == other.evaluator.as_ptr()
351                || Python::attach(|py| {
352                    // See `PythonFunctionScalarUDF::eq` for the
353                    // rationale on swallowing the exception as `false`
354                    // and logging at `debug`. FIXME: revisit if
355                    // upstream `WindowUDFImpl` exposes a fallible
356                    // `PartialEq`.
357                    self.evaluator
358                        .bind(py)
359                        .eq(other.evaluator.bind(py))
360                        .unwrap_or_else(|e| {
361                            log::debug!(
362                                target: "datafusion_python::udwf",
363                                "PythonFunctionWindowUDF {:?} __eq__ raised; treating as unequal: {e}",
364                                self.name,
365                            );
366                            false
367                        })
368                }))
369    }
370}
371
372impl std::hash::Hash for PythonFunctionWindowUDF {
373    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
374        // See `PythonFunctionScalarUDF`'s `Hash` impl for the
375        // rationale: hash the identifying header only and let
376        // `PartialEq` disambiguate evaluators.
377        self.name.hash(state);
378        self.signature.hash(state);
379        self.return_type.hash(state);
380    }
381}
382
383impl WindowUDFImpl for PythonFunctionWindowUDF {
384    fn name(&self) -> &str {
385        &self.name
386    }
387
388    fn signature(&self) -> &Signature {
389        &self.signature
390    }
391
392    fn field(&self, field_args: WindowUDFFieldArgs) -> Result<arrow::datatypes::FieldRef> {
393        // TODO: Should nullable always be `true`?
394        Ok(arrow::datatypes::Field::new(field_args.name(), self.return_type.clone(), true).into())
395    }
396
397    // TODO: Enable passing partition_evaluator_args to python?
398    fn partition_evaluator(
399        &self,
400        _partition_evaluator_args: PartitionEvaluatorArgs,
401    ) -> Result<Box<dyn PartitionEvaluator>> {
402        instantiate_partition_evaluator(&self.evaluator)
403    }
404}