use std::sync::Arc;
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::ipc::reader::StreamReader;
use arrow::ipc::writer::StreamWriter;
use datafusion::common::{Result, TableReference};
use datafusion::datasource::TableProvider;
use datafusion::datasource::file_format::FileFormatFactory;
use datafusion::execution::TaskContext;
use datafusion::logical_expr::{
AggregateUDF, AggregateUDFImpl, Extension, LogicalPlan, ScalarUDF, ScalarUDFImpl, Signature,
TypeSignature, Volatility, WindowUDF, WindowUDFImpl,
};
use datafusion::physical_expr::PhysicalExpr;
use datafusion::physical_plan::ExecutionPlan;
use datafusion_proto::logical_plan::{DefaultLogicalExtensionCodec, LogicalExtensionCodec};
use datafusion_proto::physical_plan::{DefaultPhysicalExtensionCodec, PhysicalExtensionCodec};
use pyo3::prelude::*;
use pyo3::sync::PyOnceLock;
use pyo3::types::{PyBytes, PyTuple};
use crate::errors::to_datafusion_err;
use crate::udaf::PythonFunctionAggregateUDF;
use crate::udf::PythonFunctionScalarUDF;
use crate::udwf::PythonFunctionWindowUDF;
pub(crate) const PY_SCALAR_UDF_FAMILY: &[u8] = b"DFPYUDF";
pub(crate) const PY_AGG_UDF_FAMILY: &[u8] = b"DFPYUDA";
pub(crate) const PY_WINDOW_UDF_FAMILY: &[u8] = b"DFPYUDW";
pub(crate) const WIRE_VERSION_CURRENT: u8 = 1;
pub(crate) const WIRE_VERSION_MIN_SUPPORTED: u8 = 1;
fn write_wire_header(buf: &mut Vec<u8>, family: &[u8], py_version: (u8, u8)) {
buf.extend_from_slice(family);
buf.push(WIRE_VERSION_CURRENT);
buf.push(py_version.0);
buf.push(py_version.1);
}
fn strip_wire_header<'a>(
buf: &'a [u8],
family: &[u8],
kind: &str,
expected_py: (u8, u8),
) -> Result<Option<&'a [u8]>> {
if !buf.starts_with(family) {
return Ok(None);
}
let version_idx = family.len();
let Some(&version) = buf.get(version_idx) else {
return Err(datafusion::error::DataFusionError::Execution(format!(
"Truncated inline Python {kind} payload: missing wire-format version byte"
)));
};
if !(WIRE_VERSION_MIN_SUPPORTED..=WIRE_VERSION_CURRENT).contains(&version) {
return Err(datafusion::error::DataFusionError::Execution(format!(
"Inline Python {kind} payload wire-format version v{version}; \
this build supports v{WIRE_VERSION_MIN_SUPPORTED}..=v{WIRE_VERSION_CURRENT}. \
Align datafusion-python versions on sender and receiver."
)));
}
let py_major_idx = version_idx + 1;
let Some(&encoded_major) = buf.get(py_major_idx) else {
return Err(datafusion::error::DataFusionError::Execution(format!(
"Truncated inline Python {kind} payload: missing Python major version byte"
)));
};
let py_minor_idx = version_idx + 2;
let Some(&encoded_minor) = buf.get(py_minor_idx) else {
return Err(datafusion::error::DataFusionError::Execution(format!(
"Truncated inline Python {kind} payload: missing Python minor version byte"
)));
};
let (current_major, current_minor) = expected_py;
if encoded_major != current_major || encoded_minor != current_minor {
return Err(datafusion::error::DataFusionError::Execution(format!(
"Inline Python {kind} payload was serialized on Python \
{encoded_major}.{encoded_minor} but this process is running Python \
{current_major}.{current_minor}. cloudpickle payloads are not portable \
across Python minor versions. Align Python versions on sender and receiver."
)));
}
Ok(Some(&buf[py_minor_idx + 1..]))
}
#[derive(Debug)]
pub struct PythonLogicalCodec {
inner: Arc<dyn LogicalExtensionCodec>,
python_udf_inlining: bool,
}
impl PythonLogicalCodec {
pub fn new(inner: Arc<dyn LogicalExtensionCodec>) -> Self {
Self {
inner,
python_udf_inlining: true,
}
}
pub fn inner(&self) -> &Arc<dyn LogicalExtensionCodec> {
&self.inner
}
pub fn with_python_udf_inlining(mut self, enabled: bool) -> Self {
self.python_udf_inlining = enabled;
self
}
pub fn python_udf_inlining(&self) -> bool {
self.python_udf_inlining
}
}
impl Default for PythonLogicalCodec {
fn default() -> Self {
Self::new(Arc::new(DefaultLogicalExtensionCodec {}))
}
}
impl LogicalExtensionCodec for PythonLogicalCodec {
fn try_decode(
&self,
buf: &[u8],
inputs: &[LogicalPlan],
ctx: &TaskContext,
) -> Result<Extension> {
self.inner.try_decode(buf, inputs, ctx)
}
fn try_encode(&self, node: &Extension, buf: &mut Vec<u8>) -> Result<()> {
self.inner.try_encode(node, buf)
}
fn try_decode_table_provider(
&self,
buf: &[u8],
table_ref: &TableReference,
schema: SchemaRef,
ctx: &TaskContext,
) -> Result<Arc<dyn TableProvider>> {
self.inner
.try_decode_table_provider(buf, table_ref, schema, ctx)
}
fn try_encode_table_provider(
&self,
table_ref: &TableReference,
node: Arc<dyn TableProvider>,
buf: &mut Vec<u8>,
) -> Result<()> {
self.inner.try_encode_table_provider(table_ref, node, buf)
}
fn try_decode_file_format(
&self,
buf: &[u8],
ctx: &TaskContext,
) -> Result<Arc<dyn FileFormatFactory>> {
self.inner.try_decode_file_format(buf, ctx)
}
fn try_encode_file_format(
&self,
buf: &mut Vec<u8>,
node: Arc<dyn FileFormatFactory>,
) -> Result<()> {
self.inner.try_encode_file_format(buf, node)
}
fn try_encode_udf(&self, node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_scalar_udf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udf(node, buf)
}
fn try_decode_udf(&self, name: &str, buf: &[u8]) -> Result<Arc<ScalarUDF>> {
if self.python_udf_inlining {
if let Some(udf) = try_decode_python_scalar_udf(buf)? {
return Ok(udf);
}
} else {
refuse_if_inline(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", name)?;
}
self.inner.try_decode_udf(name, buf)
}
fn try_encode_udaf(&self, node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_udaf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udaf(node, buf)
}
fn try_decode_udaf(&self, name: &str, buf: &[u8]) -> Result<Arc<AggregateUDF>> {
if self.python_udf_inlining {
if let Some(udaf) = try_decode_python_udaf(buf)? {
return Ok(udaf);
}
} else {
refuse_if_inline(buf, PY_AGG_UDF_FAMILY, "aggregate UDF", name)?;
}
self.inner.try_decode_udaf(name, buf)
}
fn try_encode_udwf(&self, node: &WindowUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_udwf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udwf(node, buf)
}
fn try_decode_udwf(&self, name: &str, buf: &[u8]) -> Result<Arc<WindowUDF>> {
if self.python_udf_inlining {
if let Some(udwf) = try_decode_python_udwf(buf)? {
return Ok(udwf);
}
} else {
refuse_if_inline(buf, PY_WINDOW_UDF_FAMILY, "window UDF", name)?;
}
self.inner.try_decode_udwf(name, buf)
}
}
fn refuse_if_inline(buf: &[u8], family: &[u8], kind: &str, name: &str) -> Result<()> {
if !buf.starts_with(family) {
return Ok(());
}
Python::attach(|py| match read_framed_payload(py, buf, family, kind)? {
Some(_) => Err(refuse_inline_payload(kind, name)),
None => Ok(()),
})
}
fn refuse_inline_payload(kind: &str, name: &str) -> datafusion::error::DataFusionError {
datafusion::error::DataFusionError::Execution(format!(
"Refusing to deserialize inline Python {kind} '{name}': Python UDF \
inlining is disabled on this session. Two remediations: \
(1) ask the sender to re-encode with inlining disabled so '{name}' \
travels by name, and register '{name}' on this receiver; or \
(2) enable inlining on this receiver (accepts the cloudpickle \
execution risk on inbound payloads). Receivers cannot re-encode \
bytes they did not produce."
))
}
#[derive(Debug)]
pub struct PythonPhysicalCodec {
inner: Arc<dyn PhysicalExtensionCodec>,
python_udf_inlining: bool,
}
impl PythonPhysicalCodec {
pub fn new(inner: Arc<dyn PhysicalExtensionCodec>) -> Self {
Self {
inner,
python_udf_inlining: true,
}
}
pub fn inner(&self) -> &Arc<dyn PhysicalExtensionCodec> {
&self.inner
}
pub fn with_python_udf_inlining(mut self, enabled: bool) -> Self {
self.python_udf_inlining = enabled;
self
}
pub fn python_udf_inlining(&self) -> bool {
self.python_udf_inlining
}
}
impl Default for PythonPhysicalCodec {
fn default() -> Self {
Self::new(Arc::new(DefaultPhysicalExtensionCodec {}))
}
}
impl PhysicalExtensionCodec for PythonPhysicalCodec {
fn try_decode(
&self,
buf: &[u8],
inputs: &[Arc<dyn ExecutionPlan>],
ctx: &TaskContext,
) -> Result<Arc<dyn ExecutionPlan>> {
self.inner.try_decode(buf, inputs, ctx)
}
fn try_encode(&self, node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
self.inner.try_encode(node, buf)
}
fn try_encode_udf(&self, node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_scalar_udf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udf(node, buf)
}
fn try_decode_udf(&self, name: &str, buf: &[u8]) -> Result<Arc<ScalarUDF>> {
if self.python_udf_inlining {
if let Some(udf) = try_decode_python_scalar_udf(buf)? {
return Ok(udf);
}
} else {
refuse_if_inline(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", name)?;
}
self.inner.try_decode_udf(name, buf)
}
fn try_encode_expr(&self, node: &Arc<dyn PhysicalExpr>, buf: &mut Vec<u8>) -> Result<()> {
self.inner.try_encode_expr(node, buf)
}
fn try_decode_expr(
&self,
buf: &[u8],
inputs: &[Arc<dyn PhysicalExpr>],
) -> Result<Arc<dyn PhysicalExpr>> {
self.inner.try_decode_expr(buf, inputs)
}
fn try_encode_udaf(&self, node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_udaf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udaf(node, buf)
}
fn try_decode_udaf(&self, name: &str, buf: &[u8]) -> Result<Arc<AggregateUDF>> {
if self.python_udf_inlining {
if let Some(udaf) = try_decode_python_udaf(buf)? {
return Ok(udaf);
}
} else {
refuse_if_inline(buf, PY_AGG_UDF_FAMILY, "aggregate UDF", name)?;
}
self.inner.try_decode_udaf(name, buf)
}
fn try_encode_udwf(&self, node: &WindowUDF, buf: &mut Vec<u8>) -> Result<()> {
if self.python_udf_inlining && try_encode_python_udwf(node, buf)? {
return Ok(());
}
self.inner.try_encode_udwf(node, buf)
}
fn try_decode_udwf(&self, name: &str, buf: &[u8]) -> Result<Arc<WindowUDF>> {
if self.python_udf_inlining {
if let Some(udwf) = try_decode_python_udwf(buf)? {
return Ok(udwf);
}
} else {
refuse_if_inline(buf, PY_WINDOW_UDF_FAMILY, "window UDF", name)?;
}
self.inner.try_decode_udwf(name, buf)
}
}
pub(crate) fn try_encode_python_scalar_udf(node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<bool> {
let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionScalarUDF>() else {
return Ok(false);
};
Python::attach(|py| -> Result<bool> {
let bytes = encode_python_scalar_udf(py, py_udf).map_err(to_datafusion_err)?;
append_framed_payload(py, buf, PY_SCALAR_UDF_FAMILY, &bytes)?;
Ok(true)
})
}
pub(crate) fn try_decode_python_scalar_udf(buf: &[u8]) -> Result<Option<Arc<ScalarUDF>>> {
if !buf.starts_with(PY_SCALAR_UDF_FAMILY) {
return Ok(None);
}
Python::attach(|py| -> Result<Option<Arc<ScalarUDF>>> {
let Some(payload) = read_framed_payload(py, buf, PY_SCALAR_UDF_FAMILY, "scalar UDF")?
else {
return Ok(None);
};
let udf = decode_python_scalar_udf(py, payload).map_err(to_datafusion_err)?;
Ok(Some(Arc::new(ScalarUDF::new_from_impl(udf))))
})
}
fn encode_python_scalar_udf(py: Python<'_>, udf: &PythonFunctionScalarUDF) -> PyResult<Vec<u8>> {
let signature = udf.signature();
let input_dtypes = signature_input_dtypes(signature, "PythonFunctionScalarUDF")?;
let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
let return_schema_bytes = build_single_field_schema_bytes(udf.return_field().as_ref())?;
let volatility = volatility_wire_str(signature.volatility);
let payload = PyTuple::new(
py,
[
udf.name().into_pyobject(py)?.into_any(),
udf.func().bind(py).clone().into_any(),
PyBytes::new(py, &input_schema_bytes).into_any(),
PyBytes::new(py, &return_schema_bytes).into_any(),
volatility.into_pyobject(py)?.into_any(),
],
)?;
cloudpickle(py)?
.call_method1("dumps", (payload,))?
.extract::<Vec<u8>>()
}
fn decode_python_scalar_udf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionScalarUDF> {
let tuple = cloudpickle(py)?
.call_method1("loads", (PyBytes::new(py, payload),))?
.cast_into::<PyTuple>()?;
let name: String = tuple.get_item(0)?.extract()?;
let func: Py<PyAny> = tuple.get_item(1)?.unbind();
let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
let volatility_str: String = tuple.get_item(4)?.extract()?;
let input_types = read_input_dtypes(&input_schema_bytes)?;
let return_field = read_single_return_field(&return_schema_bytes, "PythonFunctionScalarUDF")?;
let volatility = parse_volatility_str(&volatility_str)?;
Ok(PythonFunctionScalarUDF::from_parts(
name,
func,
input_types,
return_field,
volatility,
))
}
fn schema_to_ipc_bytes(schema: &Schema) -> arrow::error::Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::new();
{
let mut writer = StreamWriter::try_new(&mut buf, schema)?;
writer.finish()?;
}
Ok(buf)
}
fn schema_from_ipc_bytes(bytes: &[u8]) -> arrow::error::Result<Schema> {
let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)?;
Ok(reader.schema().as_ref().clone())
}
fn signature_input_dtypes(signature: &Signature, kind: &str) -> PyResult<Vec<DataType>> {
match &signature.type_signature {
TypeSignature::Exact(types) => Ok(types.clone()),
other => Err(pyo3::exceptions::PyValueError::new_err(format!(
"{kind} expected Signature::Exact, got {other:?}"
))),
}
}
fn build_input_schema_bytes(dtypes: &[DataType]) -> PyResult<Vec<u8>> {
let fields: Vec<Field> = dtypes
.iter()
.enumerate()
.map(|(i, dt)| Field::new(format!("arg_{i}"), dt.clone(), true))
.collect();
schema_to_ipc_bytes(&Schema::new(fields)).map_err(arrow_to_py_err)
}
fn build_single_field_schema_bytes(field: &Field) -> PyResult<Vec<u8>> {
schema_to_ipc_bytes(&Schema::new(vec![field.clone()])).map_err(arrow_to_py_err)
}
fn build_schema_bytes(fields: Vec<Field>) -> PyResult<Vec<u8>> {
schema_to_ipc_bytes(&Schema::new(fields)).map_err(arrow_to_py_err)
}
fn read_input_dtypes(bytes: &[u8]) -> PyResult<Vec<DataType>> {
let schema = schema_from_ipc_bytes(bytes).map_err(arrow_to_py_err)?;
Ok(schema
.fields()
.iter()
.map(|f| f.data_type().clone())
.collect())
}
fn read_single_return_field(bytes: &[u8], kind: &str) -> PyResult<Field> {
let schema = schema_from_ipc_bytes(bytes).map_err(arrow_to_py_err)?;
let field = schema.fields().first().ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err(format!(
"{kind} return schema must contain exactly one field"
))
})?;
Ok(field.as_ref().clone())
}
fn arrow_to_py_err(e: arrow::error::ArrowError) -> PyErr {
pyo3::exceptions::PyValueError::new_err(format!("{e}"))
}
fn parse_volatility_str(s: &str) -> PyResult<Volatility> {
datafusion_python_util::parse_volatility(s)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
}
fn volatility_wire_str(v: Volatility) -> &'static str {
match v {
Volatility::Immutable => "immutable",
Volatility::Stable => "stable",
Volatility::Volatile => "volatile",
}
}
fn current_python_version(py: Python<'_>) -> PyResult<(u8, u8)> {
let version_info = py.import("sys")?.getattr("version_info")?;
let major: u8 = version_info.getattr("major")?.extract()?;
let minor: u8 = version_info.getattr("minor")?.extract()?;
Ok((major, minor))
}
fn append_framed_payload(
py: Python<'_>,
buf: &mut Vec<u8>,
family: &[u8],
payload: &[u8],
) -> Result<()> {
let py_version = current_python_version(py).map_err(to_datafusion_err)?;
write_wire_header(buf, family, py_version);
buf.extend_from_slice(payload);
Ok(())
}
fn read_framed_payload<'a>(
py: Python<'_>,
buf: &'a [u8],
family: &[u8],
kind: &str,
) -> Result<Option<&'a [u8]>> {
let py_version = current_python_version(py).map_err(to_datafusion_err)?;
strip_wire_header(buf, family, kind, py_version)
}
fn cloudpickle<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
static CLOUDPICKLE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
CLOUDPICKLE
.get_or_try_init(py, || Ok(py.import("cloudpickle")?.unbind().into_any()))
.map(|cached| cached.bind(py).clone())
}
pub(crate) fn try_encode_python_udwf(node: &WindowUDF, buf: &mut Vec<u8>) -> Result<bool> {
let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionWindowUDF>() else {
return Ok(false);
};
Python::attach(|py| -> Result<bool> {
let bytes = encode_python_udwf(py, py_udf).map_err(to_datafusion_err)?;
append_framed_payload(py, buf, PY_WINDOW_UDF_FAMILY, &bytes)?;
Ok(true)
})
}
pub(crate) fn try_decode_python_udwf(buf: &[u8]) -> Result<Option<Arc<WindowUDF>>> {
if !buf.starts_with(PY_WINDOW_UDF_FAMILY) {
return Ok(None);
}
Python::attach(|py| -> Result<Option<Arc<WindowUDF>>> {
let Some(payload) = read_framed_payload(py, buf, PY_WINDOW_UDF_FAMILY, "window UDF")?
else {
return Ok(None);
};
let udf = decode_python_udwf(py, payload).map_err(to_datafusion_err)?;
Ok(Some(Arc::new(WindowUDF::new_from_impl(udf))))
})
}
fn encode_python_udwf(py: Python<'_>, udf: &PythonFunctionWindowUDF) -> PyResult<Vec<u8>> {
let signature = WindowUDFImpl::signature(udf);
let input_dtypes = signature_input_dtypes(signature, "PythonFunctionWindowUDF")?;
let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
let return_field = Field::new("result", udf.return_type().clone(), true);
let return_schema_bytes = build_single_field_schema_bytes(&return_field)?;
let volatility = volatility_wire_str(signature.volatility);
let payload = PyTuple::new(
py,
[
WindowUDFImpl::name(udf).into_pyobject(py)?.into_any(),
udf.evaluator().bind(py).clone().into_any(),
PyBytes::new(py, &input_schema_bytes).into_any(),
PyBytes::new(py, &return_schema_bytes).into_any(),
volatility.into_pyobject(py)?.into_any(),
],
)?;
cloudpickle(py)?
.call_method1("dumps", (payload,))?
.extract::<Vec<u8>>()
}
fn decode_python_udwf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionWindowUDF> {
let tuple = cloudpickle(py)?
.call_method1("loads", (PyBytes::new(py, payload),))?
.cast_into::<PyTuple>()?;
let name: String = tuple.get_item(0)?.extract()?;
let evaluator: Py<PyAny> = tuple.get_item(1)?.unbind();
let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
let volatility_str: String = tuple.get_item(4)?.extract()?;
let input_types = read_input_dtypes(&input_schema_bytes)?;
let return_type = read_single_return_field(&return_schema_bytes, "PythonFunctionWindowUDF")?
.data_type()
.clone();
let volatility = parse_volatility_str(&volatility_str)?;
Ok(PythonFunctionWindowUDF::new(
name,
evaluator,
input_types,
return_type,
volatility,
))
}
pub(crate) fn try_encode_python_udaf(node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<bool> {
let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionAggregateUDF>() else {
return Ok(false);
};
Python::attach(|py| -> Result<bool> {
let bytes = encode_python_udaf(py, py_udf).map_err(to_datafusion_err)?;
append_framed_payload(py, buf, PY_AGG_UDF_FAMILY, &bytes)?;
Ok(true)
})
}
pub(crate) fn try_decode_python_udaf(buf: &[u8]) -> Result<Option<Arc<AggregateUDF>>> {
if !buf.starts_with(PY_AGG_UDF_FAMILY) {
return Ok(None);
}
Python::attach(|py| -> Result<Option<Arc<AggregateUDF>>> {
let Some(payload) = read_framed_payload(py, buf, PY_AGG_UDF_FAMILY, "aggregate UDF")?
else {
return Ok(None);
};
let udf = decode_python_udaf(py, payload).map_err(to_datafusion_err)?;
Ok(Some(Arc::new(AggregateUDF::new_from_impl(udf))))
})
}
fn encode_python_udaf(py: Python<'_>, udf: &PythonFunctionAggregateUDF) -> PyResult<Vec<u8>> {
let signature = AggregateUDFImpl::signature(udf);
let input_dtypes = signature_input_dtypes(signature, "PythonFunctionAggregateUDF")?;
let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
let return_field = Field::new("result", udf.return_type().clone(), true);
let return_schema_bytes = build_single_field_schema_bytes(&return_field)?;
let state_fields: Vec<Field> = udf
.state_fields_ref()
.iter()
.map(|f| f.as_ref().clone())
.collect();
let state_schema_bytes = build_schema_bytes(state_fields)?;
let volatility = volatility_wire_str(signature.volatility);
let payload = PyTuple::new(
py,
[
AggregateUDFImpl::name(udf).into_pyobject(py)?.into_any(),
udf.accumulator().bind(py).clone().into_any(),
PyBytes::new(py, &input_schema_bytes).into_any(),
PyBytes::new(py, &return_schema_bytes).into_any(),
PyBytes::new(py, &state_schema_bytes).into_any(),
volatility.into_pyobject(py)?.into_any(),
],
)?;
cloudpickle(py)?
.call_method1("dumps", (payload,))?
.extract::<Vec<u8>>()
}
fn decode_python_udaf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionAggregateUDF> {
let tuple = cloudpickle(py)?
.call_method1("loads", (PyBytes::new(py, payload),))?
.cast_into::<PyTuple>()?;
let name: String = tuple.get_item(0)?.extract()?;
let accumulator: Py<PyAny> = tuple.get_item(1)?.unbind();
let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
let state_schema_bytes: Vec<u8> = tuple.get_item(4)?.extract()?;
let volatility_str: String = tuple.get_item(5)?.extract()?;
let input_types = read_input_dtypes(&input_schema_bytes)?;
let return_type = read_single_return_field(&return_schema_bytes, "PythonFunctionAggregateUDF")?
.data_type()
.clone();
let state_schema = schema_from_ipc_bytes(&state_schema_bytes).map_err(arrow_to_py_err)?;
let state_fields: Vec<arrow::datatypes::FieldRef> =
state_schema.fields().iter().cloned().collect();
let volatility = parse_volatility_str(&volatility_str)?;
Ok(PythonFunctionAggregateUDF::from_parts(
name,
accumulator,
input_types,
return_type,
state_fields,
volatility,
))
}
#[cfg(test)]
mod wire_header_tests {
use super::*;
const TEST_PY: (u8, u8) = (3, 12);
#[test]
fn strip_returns_none_when_family_absent() {
let buf = b"OTHER_PAYLOAD";
assert!(matches!(
strip_wire_header(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY),
Ok(None)
));
}
#[test]
fn strip_errors_on_truncated_version_byte() {
let buf = PY_SCALAR_UDF_FAMILY;
let err = strip_wire_header(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
assert!(format!("{err}").contains("missing wire-format version byte"));
}
#[test]
fn strip_errors_on_too_new_version() {
let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
buf.push(WIRE_VERSION_CURRENT.saturating_add(1));
buf.push(TEST_PY.0);
buf.push(TEST_PY.1);
buf.extend_from_slice(b"payload");
let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("wire-format version v"));
assert!(msg.contains("supports"));
assert!(msg.contains("Align datafusion-python versions"));
}
#[test]
fn strip_errors_on_too_old_version() {
if WIRE_VERSION_MIN_SUPPORTED == 0 {
return;
}
let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
buf.push(WIRE_VERSION_MIN_SUPPORTED - 1);
buf.push(TEST_PY.0);
buf.push(TEST_PY.1);
buf.extend_from_slice(b"payload");
assert!(strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).is_err());
}
#[test]
fn strip_errors_on_truncated_py_major() {
let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
buf.push(WIRE_VERSION_CURRENT);
let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
assert!(format!("{err}").contains("missing Python major version byte"));
}
#[test]
fn strip_errors_on_truncated_py_minor() {
let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
buf.push(WIRE_VERSION_CURRENT);
buf.push(TEST_PY.0);
let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
assert!(format!("{err}").contains("missing Python minor version byte"));
}
#[test]
fn strip_errors_on_py_minor_mismatch() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, (3, 11));
buf.extend_from_slice(b"payload");
let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", (3, 12)).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("Python 3.11"));
assert!(msg.contains("Python 3.12"));
assert!(msg.contains("not portable across Python minor versions"));
}
#[test]
fn strip_errors_on_py_major_mismatch() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, (3, 12));
buf.extend_from_slice(b"payload");
assert!(strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", (4, 0)).is_err());
}
#[test]
fn write_then_strip_round_trips_scalar_payload() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, TEST_PY);
buf.extend_from_slice(b"scalar-payload");
let payload = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY)
.unwrap()
.unwrap();
assert_eq!(payload, b"scalar-payload");
}
#[test]
fn write_then_strip_round_trips_agg_payload() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_AGG_UDF_FAMILY, TEST_PY);
buf.extend_from_slice(b"agg-payload");
let payload = strip_wire_header(&buf, PY_AGG_UDF_FAMILY, "aggregate UDF", TEST_PY)
.unwrap()
.unwrap();
assert_eq!(payload, b"agg-payload");
}
#[test]
fn write_then_strip_round_trips_window_payload() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_WINDOW_UDF_FAMILY, TEST_PY);
buf.extend_from_slice(b"window-payload");
let payload = strip_wire_header(&buf, PY_WINDOW_UDF_FAMILY, "window UDF", TEST_PY)
.unwrap()
.unwrap();
assert_eq!(payload, b"window-payload");
}
#[test]
fn strip_does_not_match_a_different_family() {
let mut buf = Vec::new();
write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, TEST_PY);
buf.extend_from_slice(b"payload");
assert!(matches!(
strip_wire_header(&buf, PY_WINDOW_UDF_FAMILY, "window UDF", TEST_PY),
Ok(None)
));
}
}