| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| use std::fmt::{self, Debug}; |
| use std::hash::{Hash, Hasher}; |
| use std::sync::Arc; |
| |
| use arrow::array::{make_array, Array, ArrayData, ArrayRef}; |
| use arrow::datatypes::{DataType as ArrowDataType, Field as ArrowField}; |
| use arrow::pyarrow::{FromPyArrow, ToPyArrow}; |
| use datafusion::common::{DataFusionError, Result as DFResult}; |
| use datafusion::logical_expr::{ |
| ColumnarValue, ScalarFunctionArgs, ScalarUDF as DFScalarUDF, ScalarUDFImpl, Signature, |
| Volatility, |
| }; |
| use pyo3::exceptions::PyTypeError; |
| use pyo3::prelude::*; |
| use pyo3::types::{PyList, PyTuple}; |
| |
| fn parse_arrow_type(type_name: &str) -> PyResult<ArrowDataType> { |
| match type_name.to_ascii_lowercase().as_str() { |
| "bool" | "boolean" => Ok(ArrowDataType::Boolean), |
| "int8" => Ok(ArrowDataType::Int8), |
| "int16" => Ok(ArrowDataType::Int16), |
| "int" | "int32" | "integer" => Ok(ArrowDataType::Int32), |
| "bigint" | "int64" | "long" => Ok(ArrowDataType::Int64), |
| "float" | "float32" => Ok(ArrowDataType::Float32), |
| "double" | "float64" => Ok(ArrowDataType::Float64), |
| "string" | "utf8" => Ok(ArrowDataType::Utf8), |
| "large_string" | "large_utf8" => Ok(ArrowDataType::LargeUtf8), |
| "binary" => Ok(ArrowDataType::Binary), |
| "large_binary" => Ok(ArrowDataType::LargeBinary), |
| other => Err(PyTypeError::new_err(format!( |
| "Unsupported Arrow type for Python UDF: {other}" |
| ))), |
| } |
| } |
| |
| fn parse_arrow_type_like(value: &Bound<'_, PyAny>) -> PyResult<ArrowDataType> { |
| if let Ok(field) = ArrowField::from_pyarrow_bound(value) { |
| return Ok(field.data_type().clone()); |
| } |
| if let Ok(data_type) = ArrowDataType::from_pyarrow_bound(value) { |
| return Ok(data_type); |
| } |
| if let Ok(type_name) = value.extract::<String>() { |
| return parse_arrow_type(&type_name); |
| } |
| |
| Err(PyTypeError::new_err( |
| "Expected a pyarrow.DataType, pyarrow.Field, or supported Arrow type name", |
| )) |
| } |
| |
| fn parse_input_types(input_fields: &Bound<'_, PyAny>) -> PyResult<Vec<ArrowDataType>> { |
| if let Ok(fields) = input_fields.cast::<PyList>() { |
| return fields |
| .iter() |
| .map(|field| parse_arrow_type_like(&field)) |
| .collect(); |
| } |
| if let Ok(fields) = input_fields.cast::<PyTuple>() { |
| return fields |
| .iter() |
| .map(|field| parse_arrow_type_like(&field)) |
| .collect(); |
| } |
| |
| Ok(vec![parse_arrow_type_like(input_fields)?]) |
| } |
| |
| fn parse_volatility(volatility: &Bound<'_, PyAny>) -> PyResult<Volatility> { |
| let value = if let Ok(value) = volatility.extract::<String>() { |
| value |
| } else if let Ok(name) = volatility.getattr("name") { |
| name.extract::<String>()? |
| } else { |
| volatility.str()?.to_str()?.to_string() |
| }; |
| |
| match value.to_ascii_lowercase().as_str() { |
| "immutable" => Ok(Volatility::Immutable), |
| "stable" => Ok(Volatility::Stable), |
| "volatile" => Ok(Volatility::Volatile), |
| other => Err(PyTypeError::new_err(format!( |
| "Unsupported UDF volatility: {other}. Expected immutable, stable, or volatile" |
| ))), |
| } |
| } |
| |
| fn sanitize_udf_name(name: &str) -> String { |
| let mut sanitized = name |
| .chars() |
| .map(|ch| { |
| if ch.is_ascii_alphanumeric() || ch == '_' { |
| ch.to_ascii_lowercase() |
| } else { |
| '_' |
| } |
| }) |
| .collect::<String>() |
| .trim_matches('_') |
| .to_string(); |
| |
| if sanitized.is_empty() { |
| sanitized.push_str("python_udf"); |
| } |
| if sanitized |
| .chars() |
| .next() |
| .is_some_and(|ch| ch.is_ascii_digit()) |
| { |
| sanitized.insert(0, '_'); |
| } |
| sanitized |
| } |
| |
| fn default_udf_name(py: Python<'_>, func: &Py<PyAny>) -> PyResult<String> { |
| let func = func.bind(py); |
| if let Ok(name) = func.getattr("__name__") { |
| return Ok(sanitize_udf_name(&name.extract::<String>()?)); |
| } |
| if let Ok(name) = func.getattr("__qualname__") { |
| return Ok(sanitize_udf_name(&name.extract::<String>()?)); |
| } |
| let name = func |
| .getattr("__class__")? |
| .getattr("__name__")? |
| .extract::<String>()?; |
| Ok(sanitize_udf_name(&name)) |
| } |
| |
| fn df_execution_error(message: impl Into<String>) -> DataFusionError { |
| DataFusionError::Execution(message.into()) |
| } |
| |
| fn columnar_value_to_array(value: &ColumnarValue, num_rows: usize) -> DFResult<ArrayRef> { |
| match value { |
| ColumnarValue::Array(array) => Ok(Arc::clone(array)), |
| ColumnarValue::Scalar(scalar) => scalar.to_array_of_size(num_rows), |
| } |
| } |
| |
| struct PyScalarUDF { |
| name: String, |
| func: Py<PyAny>, |
| return_type: ArrowDataType, |
| signature: Signature, |
| } |
| |
| impl PyScalarUDF { |
| fn new( |
| name: String, |
| func: Py<PyAny>, |
| return_type: ArrowDataType, |
| signature: Signature, |
| ) -> Self { |
| Self { |
| name, |
| func, |
| return_type, |
| signature, |
| } |
| } |
| } |
| |
| pub(crate) fn build_python_scalar_udf( |
| name: String, |
| func: Py<PyAny>, |
| return_type: ArrowDataType, |
| signature: Signature, |
| ) -> DFScalarUDF { |
| DFScalarUDF::new_from_impl(PyScalarUDF::new(name, func, return_type, signature)) |
| } |
| |
| #[pyclass(name = "PythonScalarUDF")] |
| pub struct PyPythonScalarUDFObject { |
| name: String, |
| udf: DFScalarUDF, |
| } |
| |
| impl PyPythonScalarUDFObject { |
| fn create( |
| py: Python<'_>, |
| name: String, |
| func: Py<PyAny>, |
| input_fields: &Bound<'_, PyAny>, |
| return_field: &Bound<'_, PyAny>, |
| volatility: &Bound<'_, PyAny>, |
| ) -> PyResult<Self> { |
| if !func.bind(py).is_callable() { |
| return Err(PyTypeError::new_err("`func` argument must be callable")); |
| } |
| |
| let input_types = parse_input_types(input_fields)?; |
| let return_type = parse_arrow_type_like(return_field)?; |
| let volatility = parse_volatility(volatility)?; |
| let signature = Signature::exact(input_types, volatility); |
| let udf = PyScalarUDF::new(name.clone(), func, return_type, signature); |
| Ok(Self { |
| name, |
| udf: DFScalarUDF::new_from_impl(udf), |
| }) |
| } |
| |
| pub(crate) fn datafusion_udf(&self) -> DFScalarUDF { |
| self.udf.clone() |
| } |
| } |
| |
| #[pymethods] |
| impl PyPythonScalarUDFObject { |
| #[new] |
| fn new( |
| py: Python<'_>, |
| name: String, |
| func: Py<PyAny>, |
| input_fields: Bound<'_, PyAny>, |
| return_field: Bound<'_, PyAny>, |
| volatility: Bound<'_, PyAny>, |
| ) -> PyResult<Self> { |
| Self::create(py, name, func, &input_fields, &return_field, &volatility) |
| } |
| |
| #[staticmethod] |
| #[pyo3(signature = (func, input_fields, return_field, volatility, name = None))] |
| fn udf( |
| py: Python<'_>, |
| func: Py<PyAny>, |
| input_fields: Bound<'_, PyAny>, |
| return_field: Bound<'_, PyAny>, |
| volatility: Bound<'_, PyAny>, |
| name: Option<String>, |
| ) -> PyResult<Self> { |
| let name = match name { |
| Some(name) => name, |
| None => default_udf_name(py, &func)?, |
| }; |
| Self::create(py, name, func, &input_fields, &return_field, &volatility) |
| } |
| |
| #[getter] |
| fn name(&self) -> &str { |
| &self.name |
| } |
| |
| fn __repr__(&self) -> String { |
| format!("PythonScalarUDF({})", self.name) |
| } |
| } |
| |
| #[pyfunction] |
| #[pyo3(signature = (func, input_fields, return_field, volatility, name = None))] |
| pub(crate) fn udf( |
| py: Python<'_>, |
| func: Py<PyAny>, |
| input_fields: Bound<'_, PyAny>, |
| return_field: Bound<'_, PyAny>, |
| volatility: Bound<'_, PyAny>, |
| name: Option<String>, |
| ) -> PyResult<PyPythonScalarUDFObject> { |
| PyPythonScalarUDFObject::udf(py, func, input_fields, return_field, volatility, name) |
| } |
| |
| impl Debug for PyScalarUDF { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("PyScalarUDF") |
| .field("name", &self.name) |
| .field("signature", &self.signature) |
| .field("return_type", &self.return_type) |
| .finish_non_exhaustive() |
| } |
| } |
| |
| impl PartialEq for PyScalarUDF { |
| fn eq(&self, other: &Self) -> bool { |
| self.name == other.name |
| && self.return_type == other.return_type |
| && self.signature == other.signature |
| } |
| } |
| |
| impl Eq for PyScalarUDF {} |
| |
| impl Hash for PyScalarUDF { |
| fn hash<H: Hasher>(&self, state: &mut H) { |
| self.name.hash(state); |
| self.return_type.hash(state); |
| self.signature.hash(state); |
| } |
| } |
| |
| impl ScalarUDFImpl for PyScalarUDF { |
| fn name(&self) -> &str { |
| &self.name |
| } |
| |
| fn signature(&self) -> &Signature { |
| &self.signature |
| } |
| |
| fn return_type(&self, _arg_types: &[ArrowDataType]) -> DFResult<ArrowDataType> { |
| Ok(self.return_type.clone()) |
| } |
| |
| fn invoke_with_args(&self, args: ScalarFunctionArgs) -> DFResult<ColumnarValue> { |
| let arrays = args |
| .args |
| .iter() |
| .map(|value| columnar_value_to_array(value, args.number_rows)) |
| .collect::<DFResult<Vec<_>>>()?; |
| |
| let output = Python::try_attach(|py| -> PyResult<ArrayRef> { |
| let py_args = arrays |
| .iter() |
| .map(|array| array.to_data().to_pyarrow(py)) |
| .collect::<PyResult<Vec<_>>>()?; |
| let py_args = PyTuple::new(py, py_args)?; |
| let output = self.func.bind(py).call1(py_args)?; |
| Ok(make_array(ArrayData::from_pyarrow_bound(&output)?)) |
| }) |
| .ok_or_else(|| df_execution_error("Python interpreter is not available"))? |
| .map_err(|err| df_execution_error(format!("Python UDF '{}' failed: {err}", self.name)))?; |
| |
| if output.len() != args.number_rows { |
| return Err(df_execution_error(format!( |
| "Python UDF '{}' returned {} rows, expected {}", |
| self.name, |
| output.len(), |
| args.number_rows |
| ))); |
| } |
| if output.data_type() != &self.return_type { |
| return Err(df_execution_error(format!( |
| "Python UDF '{}' returned {:?}, expected {:?}", |
| self.name, |
| output.data_type(), |
| self.return_type |
| ))); |
| } |
| |
| Ok(ColumnarValue::Array(output)) |
| } |
| } |