1use std::sync::Arc;
90
91use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
92use arrow::ipc::reader::StreamReader;
93use arrow::ipc::writer::StreamWriter;
94use datafusion::common::{Result, TableReference};
95use datafusion::datasource::TableProvider;
96use datafusion::datasource::file_format::FileFormatFactory;
97use datafusion::execution::TaskContext;
98use datafusion::logical_expr::{
99 AggregateUDF, AggregateUDFImpl, Extension, LogicalPlan, ScalarUDF, ScalarUDFImpl, Signature,
100 TypeSignature, Volatility, WindowUDF, WindowUDFImpl,
101};
102use datafusion::physical_expr::PhysicalExpr;
103use datafusion::physical_plan::ExecutionPlan;
104use datafusion_proto::logical_plan::{DefaultLogicalExtensionCodec, LogicalExtensionCodec};
105use datafusion_proto::physical_plan::{DefaultPhysicalExtensionCodec, PhysicalExtensionCodec};
106use pyo3::prelude::*;
107use pyo3::sync::PyOnceLock;
108use pyo3::types::{PyBytes, PyTuple};
109
110use crate::errors::to_datafusion_err;
111use crate::udaf::PythonFunctionAggregateUDF;
112use crate::udf::PythonFunctionScalarUDF;
113use crate::udwf::PythonFunctionWindowUDF;
114
115pub(crate) const PY_SCALAR_UDF_FAMILY: &[u8] = b"DFPYUDF";
133
134pub(crate) const PY_AGG_UDF_FAMILY: &[u8] = b"DFPYUDA";
139
140pub(crate) const PY_WINDOW_UDF_FAMILY: &[u8] = b"DFPYUDW";
144
145pub(crate) const WIRE_VERSION_CURRENT: u8 = 1;
147
148pub(crate) const WIRE_VERSION_MIN_SUPPORTED: u8 = 1;
151
152fn write_wire_header(buf: &mut Vec<u8>, family: &[u8], py_version: (u8, u8)) {
157 buf.extend_from_slice(family);
158 buf.push(WIRE_VERSION_CURRENT);
159 buf.push(py_version.0);
160 buf.push(py_version.1);
161}
162
163fn strip_wire_header<'a>(
177 buf: &'a [u8],
178 family: &[u8],
179 kind: &str,
180 expected_py: (u8, u8),
181) -> Result<Option<&'a [u8]>> {
182 if !buf.starts_with(family) {
183 return Ok(None);
184 }
185 let version_idx = family.len();
186 let Some(&version) = buf.get(version_idx) else {
187 return Err(datafusion::error::DataFusionError::Execution(format!(
188 "Truncated inline Python {kind} payload: missing wire-format version byte"
189 )));
190 };
191 if !(WIRE_VERSION_MIN_SUPPORTED..=WIRE_VERSION_CURRENT).contains(&version) {
192 return Err(datafusion::error::DataFusionError::Execution(format!(
193 "Inline Python {kind} payload wire-format version v{version}; \
194 this build supports v{WIRE_VERSION_MIN_SUPPORTED}..=v{WIRE_VERSION_CURRENT}. \
195 Align datafusion-python versions on sender and receiver."
196 )));
197 }
198 let py_major_idx = version_idx + 1;
199 let Some(&encoded_major) = buf.get(py_major_idx) else {
200 return Err(datafusion::error::DataFusionError::Execution(format!(
201 "Truncated inline Python {kind} payload: missing Python major version byte"
202 )));
203 };
204 let py_minor_idx = version_idx + 2;
205 let Some(&encoded_minor) = buf.get(py_minor_idx) else {
206 return Err(datafusion::error::DataFusionError::Execution(format!(
207 "Truncated inline Python {kind} payload: missing Python minor version byte"
208 )));
209 };
210 let (current_major, current_minor) = expected_py;
211 if encoded_major != current_major || encoded_minor != current_minor {
212 return Err(datafusion::error::DataFusionError::Execution(format!(
213 "Inline Python {kind} payload was serialized on Python \
214 {encoded_major}.{encoded_minor} but this process is running Python \
215 {current_major}.{current_minor}. cloudpickle payloads are not portable \
216 across Python minor versions. Align Python versions on sender and receiver."
217 )));
218 }
219 Ok(Some(&buf[py_minor_idx + 1..]))
220}
221
222#[derive(Debug)]
233pub struct PythonLogicalCodec {
234 inner: Arc<dyn LogicalExtensionCodec>,
235 python_udf_inlining: bool,
236}
237
238impl PythonLogicalCodec {
239 pub fn new(inner: Arc<dyn LogicalExtensionCodec>) -> Self {
240 Self {
241 inner,
242 python_udf_inlining: true,
243 }
244 }
245
246 pub fn inner(&self) -> &Arc<dyn LogicalExtensionCodec> {
247 &self.inner
248 }
249
250 pub fn with_python_udf_inlining(mut self, enabled: bool) -> Self {
266 self.python_udf_inlining = enabled;
267 self
268 }
269
270 pub fn python_udf_inlining(&self) -> bool {
271 self.python_udf_inlining
272 }
273}
274
275impl Default for PythonLogicalCodec {
276 fn default() -> Self {
277 Self::new(Arc::new(DefaultLogicalExtensionCodec {}))
278 }
279}
280
281impl LogicalExtensionCodec for PythonLogicalCodec {
282 fn try_decode(
283 &self,
284 buf: &[u8],
285 inputs: &[LogicalPlan],
286 ctx: &TaskContext,
287 ) -> Result<Extension> {
288 self.inner.try_decode(buf, inputs, ctx)
289 }
290
291 fn try_encode(&self, node: &Extension, buf: &mut Vec<u8>) -> Result<()> {
292 self.inner.try_encode(node, buf)
293 }
294
295 fn try_decode_table_provider(
296 &self,
297 buf: &[u8],
298 table_ref: &TableReference,
299 schema: SchemaRef,
300 ctx: &TaskContext,
301 ) -> Result<Arc<dyn TableProvider>> {
302 self.inner
303 .try_decode_table_provider(buf, table_ref, schema, ctx)
304 }
305
306 fn try_encode_table_provider(
307 &self,
308 table_ref: &TableReference,
309 node: Arc<dyn TableProvider>,
310 buf: &mut Vec<u8>,
311 ) -> Result<()> {
312 self.inner.try_encode_table_provider(table_ref, node, buf)
313 }
314
315 fn try_decode_file_format(
316 &self,
317 buf: &[u8],
318 ctx: &TaskContext,
319 ) -> Result<Arc<dyn FileFormatFactory>> {
320 self.inner.try_decode_file_format(buf, ctx)
321 }
322
323 fn try_encode_file_format(
324 &self,
325 buf: &mut Vec<u8>,
326 node: Arc<dyn FileFormatFactory>,
327 ) -> Result<()> {
328 self.inner.try_encode_file_format(buf, node)
329 }
330
331 fn try_encode_udf(&self, node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<()> {
332 if self.python_udf_inlining && try_encode_python_scalar_udf(node, buf)? {
333 return Ok(());
334 }
335 self.inner.try_encode_udf(node, buf)
336 }
337
338 fn try_decode_udf(&self, name: &str, buf: &[u8]) -> Result<Arc<ScalarUDF>> {
339 if self.python_udf_inlining {
340 if let Some(udf) = try_decode_python_scalar_udf(buf)? {
341 return Ok(udf);
342 }
343 } else {
344 refuse_if_inline(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", name)?;
345 }
346 self.inner.try_decode_udf(name, buf)
347 }
348
349 fn try_encode_udaf(&self, node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<()> {
350 if self.python_udf_inlining && try_encode_python_udaf(node, buf)? {
351 return Ok(());
352 }
353 self.inner.try_encode_udaf(node, buf)
354 }
355
356 fn try_decode_udaf(&self, name: &str, buf: &[u8]) -> Result<Arc<AggregateUDF>> {
357 if self.python_udf_inlining {
358 if let Some(udaf) = try_decode_python_udaf(buf)? {
359 return Ok(udaf);
360 }
361 } else {
362 refuse_if_inline(buf, PY_AGG_UDF_FAMILY, "aggregate UDF", name)?;
363 }
364 self.inner.try_decode_udaf(name, buf)
365 }
366
367 fn try_encode_udwf(&self, node: &WindowUDF, buf: &mut Vec<u8>) -> Result<()> {
368 if self.python_udf_inlining && try_encode_python_udwf(node, buf)? {
369 return Ok(());
370 }
371 self.inner.try_encode_udwf(node, buf)
372 }
373
374 fn try_decode_udwf(&self, name: &str, buf: &[u8]) -> Result<Arc<WindowUDF>> {
375 if self.python_udf_inlining {
376 if let Some(udwf) = try_decode_python_udwf(buf)? {
377 return Ok(udwf);
378 }
379 } else {
380 refuse_if_inline(buf, PY_WINDOW_UDF_FAMILY, "window UDF", name)?;
381 }
382 self.inner.try_decode_udwf(name, buf)
383 }
384}
385
386fn refuse_if_inline(buf: &[u8], family: &[u8], kind: &str, name: &str) -> Result<()> {
403 if !buf.starts_with(family) {
404 return Ok(());
405 }
406 Python::attach(|py| match read_framed_payload(py, buf, family, kind)? {
407 Some(_) => Err(refuse_inline_payload(kind, name)),
408 None => Ok(()),
409 })
410}
411
412fn refuse_inline_payload(kind: &str, name: &str) -> datafusion::error::DataFusionError {
415 datafusion::error::DataFusionError::Execution(format!(
420 "Refusing to deserialize inline Python {kind} '{name}': Python UDF \
421 inlining is disabled on this session. Two remediations: \
422 (1) ask the sender to re-encode with inlining disabled so '{name}' \
423 travels by name, and register '{name}' on this receiver; or \
424 (2) enable inlining on this receiver (accepts the cloudpickle \
425 execution risk on inbound payloads). Receivers cannot re-encode \
426 bytes they did not produce."
427 ))
428}
429
430#[derive(Debug)]
443pub struct PythonPhysicalCodec {
444 inner: Arc<dyn PhysicalExtensionCodec>,
445 python_udf_inlining: bool,
446}
447
448impl PythonPhysicalCodec {
449 pub fn new(inner: Arc<dyn PhysicalExtensionCodec>) -> Self {
450 Self {
451 inner,
452 python_udf_inlining: true,
453 }
454 }
455
456 pub fn inner(&self) -> &Arc<dyn PhysicalExtensionCodec> {
457 &self.inner
458 }
459
460 pub fn with_python_udf_inlining(mut self, enabled: bool) -> Self {
465 self.python_udf_inlining = enabled;
466 self
467 }
468
469 pub fn python_udf_inlining(&self) -> bool {
470 self.python_udf_inlining
471 }
472}
473
474impl Default for PythonPhysicalCodec {
475 fn default() -> Self {
476 Self::new(Arc::new(DefaultPhysicalExtensionCodec {}))
477 }
478}
479
480impl PhysicalExtensionCodec for PythonPhysicalCodec {
481 fn try_decode(
482 &self,
483 buf: &[u8],
484 inputs: &[Arc<dyn ExecutionPlan>],
485 ctx: &TaskContext,
486 ) -> Result<Arc<dyn ExecutionPlan>> {
487 self.inner.try_decode(buf, inputs, ctx)
488 }
489
490 fn try_encode(&self, node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
491 self.inner.try_encode(node, buf)
492 }
493
494 fn try_encode_udf(&self, node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<()> {
495 if self.python_udf_inlining && try_encode_python_scalar_udf(node, buf)? {
496 return Ok(());
497 }
498 self.inner.try_encode_udf(node, buf)
499 }
500
501 fn try_decode_udf(&self, name: &str, buf: &[u8]) -> Result<Arc<ScalarUDF>> {
502 if self.python_udf_inlining {
503 if let Some(udf) = try_decode_python_scalar_udf(buf)? {
504 return Ok(udf);
505 }
506 } else {
507 refuse_if_inline(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", name)?;
508 }
509 self.inner.try_decode_udf(name, buf)
510 }
511
512 fn try_encode_expr(&self, node: &Arc<dyn PhysicalExpr>, buf: &mut Vec<u8>) -> Result<()> {
513 self.inner.try_encode_expr(node, buf)
514 }
515
516 fn try_decode_expr(
517 &self,
518 buf: &[u8],
519 inputs: &[Arc<dyn PhysicalExpr>],
520 ) -> Result<Arc<dyn PhysicalExpr>> {
521 self.inner.try_decode_expr(buf, inputs)
522 }
523
524 fn try_encode_udaf(&self, node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<()> {
525 if self.python_udf_inlining && try_encode_python_udaf(node, buf)? {
526 return Ok(());
527 }
528 self.inner.try_encode_udaf(node, buf)
529 }
530
531 fn try_decode_udaf(&self, name: &str, buf: &[u8]) -> Result<Arc<AggregateUDF>> {
532 if self.python_udf_inlining {
533 if let Some(udaf) = try_decode_python_udaf(buf)? {
534 return Ok(udaf);
535 }
536 } else {
537 refuse_if_inline(buf, PY_AGG_UDF_FAMILY, "aggregate UDF", name)?;
538 }
539 self.inner.try_decode_udaf(name, buf)
540 }
541
542 fn try_encode_udwf(&self, node: &WindowUDF, buf: &mut Vec<u8>) -> Result<()> {
543 if self.python_udf_inlining && try_encode_python_udwf(node, buf)? {
544 return Ok(());
545 }
546 self.inner.try_encode_udwf(node, buf)
547 }
548
549 fn try_decode_udwf(&self, name: &str, buf: &[u8]) -> Result<Arc<WindowUDF>> {
550 if self.python_udf_inlining {
551 if let Some(udwf) = try_decode_python_udwf(buf)? {
552 return Ok(udwf);
553 }
554 } else {
555 refuse_if_inline(buf, PY_WINDOW_UDF_FAMILY, "window UDF", name)?;
556 }
557 self.inner.try_decode_udwf(name, buf)
558 }
559}
560
561pub(crate) fn try_encode_python_scalar_udf(node: &ScalarUDF, buf: &mut Vec<u8>) -> Result<bool> {
576 let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionScalarUDF>() else {
577 return Ok(false);
578 };
579
580 Python::attach(|py| -> Result<bool> {
581 let bytes = encode_python_scalar_udf(py, py_udf).map_err(to_datafusion_err)?;
582 append_framed_payload(py, buf, PY_SCALAR_UDF_FAMILY, &bytes)?;
583 Ok(true)
584 })
585}
586
587pub(crate) fn try_decode_python_scalar_udf(buf: &[u8]) -> Result<Option<Arc<ScalarUDF>>> {
592 if !buf.starts_with(PY_SCALAR_UDF_FAMILY) {
593 return Ok(None);
594 }
595 Python::attach(|py| -> Result<Option<Arc<ScalarUDF>>> {
596 let Some(payload) = read_framed_payload(py, buf, PY_SCALAR_UDF_FAMILY, "scalar UDF")?
597 else {
598 return Ok(None);
599 };
600 let udf = decode_python_scalar_udf(py, payload).map_err(to_datafusion_err)?;
601 Ok(Some(Arc::new(ScalarUDF::new_from_impl(udf))))
602 })
603}
604
605fn encode_python_scalar_udf(py: Python<'_>, udf: &PythonFunctionScalarUDF) -> PyResult<Vec<u8>> {
613 let signature = udf.signature();
614 let input_dtypes = signature_input_dtypes(signature, "PythonFunctionScalarUDF")?;
615 let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
616 let return_schema_bytes = build_single_field_schema_bytes(udf.return_field().as_ref())?;
617 let volatility = volatility_wire_str(signature.volatility);
618
619 let payload = PyTuple::new(
620 py,
621 [
622 udf.name().into_pyobject(py)?.into_any(),
623 udf.func().bind(py).clone().into_any(),
624 PyBytes::new(py, &input_schema_bytes).into_any(),
625 PyBytes::new(py, &return_schema_bytes).into_any(),
626 volatility.into_pyobject(py)?.into_any(),
627 ],
628 )?;
629
630 cloudpickle(py)?
631 .call_method1("dumps", (payload,))?
632 .extract::<Vec<u8>>()
633}
634
635fn decode_python_scalar_udf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionScalarUDF> {
637 let tuple = cloudpickle(py)?
638 .call_method1("loads", (PyBytes::new(py, payload),))?
639 .cast_into::<PyTuple>()?;
640
641 let name: String = tuple.get_item(0)?.extract()?;
642 let func: Py<PyAny> = tuple.get_item(1)?.unbind();
643 let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
644 let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
645 let volatility_str: String = tuple.get_item(4)?.extract()?;
646
647 let input_types = read_input_dtypes(&input_schema_bytes)?;
648 let return_field = read_single_return_field(&return_schema_bytes, "PythonFunctionScalarUDF")?;
649 let volatility = parse_volatility_str(&volatility_str)?;
650
651 Ok(PythonFunctionScalarUDF::from_parts(
652 name,
653 func,
654 input_types,
655 return_field,
656 volatility,
657 ))
658}
659
660fn schema_to_ipc_bytes(schema: &Schema) -> arrow::error::Result<Vec<u8>> {
664 let mut buf: Vec<u8> = Vec::new();
665 {
666 let mut writer = StreamWriter::try_new(&mut buf, schema)?;
667 writer.finish()?;
668 }
669 Ok(buf)
670}
671
672fn schema_from_ipc_bytes(bytes: &[u8]) -> arrow::error::Result<Schema> {
675 let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)?;
676 Ok(reader.schema().as_ref().clone())
677}
678
679fn signature_input_dtypes(signature: &Signature, kind: &str) -> PyResult<Vec<DataType>> {
684 match &signature.type_signature {
685 TypeSignature::Exact(types) => Ok(types.clone()),
686 other => Err(pyo3::exceptions::PyValueError::new_err(format!(
687 "{kind} expected Signature::Exact, got {other:?}"
688 ))),
689 }
690}
691
692fn build_input_schema_bytes(dtypes: &[DataType]) -> PyResult<Vec<u8>> {
701 let fields: Vec<Field> = dtypes
702 .iter()
703 .enumerate()
704 .map(|(i, dt)| Field::new(format!("arg_{i}"), dt.clone(), true))
705 .collect();
706 schema_to_ipc_bytes(&Schema::new(fields)).map_err(arrow_to_py_err)
707}
708
709fn build_single_field_schema_bytes(field: &Field) -> PyResult<Vec<u8>> {
713 schema_to_ipc_bytes(&Schema::new(vec![field.clone()])).map_err(arrow_to_py_err)
714}
715
716fn build_schema_bytes(fields: Vec<Field>) -> PyResult<Vec<u8>> {
718 schema_to_ipc_bytes(&Schema::new(fields)).map_err(arrow_to_py_err)
719}
720
721fn read_input_dtypes(bytes: &[u8]) -> PyResult<Vec<DataType>> {
724 let schema = schema_from_ipc_bytes(bytes).map_err(arrow_to_py_err)?;
725 Ok(schema
726 .fields()
727 .iter()
728 .map(|f| f.data_type().clone())
729 .collect())
730}
731
732fn read_single_return_field(bytes: &[u8], kind: &str) -> PyResult<Field> {
737 let schema = schema_from_ipc_bytes(bytes).map_err(arrow_to_py_err)?;
738 let field = schema.fields().first().ok_or_else(|| {
739 pyo3::exceptions::PyValueError::new_err(format!(
740 "{kind} return schema must contain exactly one field"
741 ))
742 })?;
743 Ok(field.as_ref().clone())
744}
745
746fn arrow_to_py_err(e: arrow::error::ArrowError) -> PyErr {
747 pyo3::exceptions::PyValueError::new_err(format!("{e}"))
748}
749
750fn parse_volatility_str(s: &str) -> PyResult<Volatility> {
751 datafusion_python_util::parse_volatility(s)
752 .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
753}
754
755fn volatility_wire_str(v: Volatility) -> &'static str {
760 match v {
761 Volatility::Immutable => "immutable",
762 Volatility::Stable => "stable",
763 Volatility::Volatile => "volatile",
764 }
765}
766
767fn current_python_version(py: Python<'_>) -> PyResult<(u8, u8)> {
775 let version_info = py.import("sys")?.getattr("version_info")?;
776 let major: u8 = version_info.getattr("major")?.extract()?;
777 let minor: u8 = version_info.getattr("minor")?.extract()?;
778 Ok((major, minor))
779}
780
781fn append_framed_payload(
786 py: Python<'_>,
787 buf: &mut Vec<u8>,
788 family: &[u8],
789 payload: &[u8],
790) -> Result<()> {
791 let py_version = current_python_version(py).map_err(to_datafusion_err)?;
792 write_wire_header(buf, family, py_version);
793 buf.extend_from_slice(payload);
794 Ok(())
795}
796
797fn read_framed_payload<'a>(
803 py: Python<'_>,
804 buf: &'a [u8],
805 family: &[u8],
806 kind: &str,
807) -> Result<Option<&'a [u8]>> {
808 let py_version = current_python_version(py).map_err(to_datafusion_err)?;
809 strip_wire_header(buf, family, kind, py_version)
810}
811
812fn cloudpickle<'py>(py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
824 static CLOUDPICKLE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
825 CLOUDPICKLE
826 .get_or_try_init(py, || Ok(py.import("cloudpickle")?.unbind().into_any()))
827 .map(|cached| cached.bind(py).clone())
828}
829
830pub(crate) fn try_encode_python_udwf(node: &WindowUDF, buf: &mut Vec<u8>) -> Result<bool> {
839 let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionWindowUDF>() else {
840 return Ok(false);
841 };
842
843 Python::attach(|py| -> Result<bool> {
844 let bytes = encode_python_udwf(py, py_udf).map_err(to_datafusion_err)?;
845 append_framed_payload(py, buf, PY_WINDOW_UDF_FAMILY, &bytes)?;
846 Ok(true)
847 })
848}
849
850pub(crate) fn try_decode_python_udwf(buf: &[u8]) -> Result<Option<Arc<WindowUDF>>> {
851 if !buf.starts_with(PY_WINDOW_UDF_FAMILY) {
852 return Ok(None);
853 }
854 Python::attach(|py| -> Result<Option<Arc<WindowUDF>>> {
855 let Some(payload) = read_framed_payload(py, buf, PY_WINDOW_UDF_FAMILY, "window UDF")?
856 else {
857 return Ok(None);
858 };
859 let udf = decode_python_udwf(py, payload).map_err(to_datafusion_err)?;
860 Ok(Some(Arc::new(WindowUDF::new_from_impl(udf))))
861 })
862}
863
864fn encode_python_udwf(py: Python<'_>, udf: &PythonFunctionWindowUDF) -> PyResult<Vec<u8>> {
865 let signature = WindowUDFImpl::signature(udf);
866 let input_dtypes = signature_input_dtypes(signature, "PythonFunctionWindowUDF")?;
867 let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
868 let return_field = Field::new("result", udf.return_type().clone(), true);
869 let return_schema_bytes = build_single_field_schema_bytes(&return_field)?;
870 let volatility = volatility_wire_str(signature.volatility);
871
872 let payload = PyTuple::new(
873 py,
874 [
875 WindowUDFImpl::name(udf).into_pyobject(py)?.into_any(),
876 udf.evaluator().bind(py).clone().into_any(),
877 PyBytes::new(py, &input_schema_bytes).into_any(),
878 PyBytes::new(py, &return_schema_bytes).into_any(),
879 volatility.into_pyobject(py)?.into_any(),
880 ],
881 )?;
882
883 cloudpickle(py)?
884 .call_method1("dumps", (payload,))?
885 .extract::<Vec<u8>>()
886}
887
888fn decode_python_udwf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionWindowUDF> {
889 let tuple = cloudpickle(py)?
890 .call_method1("loads", (PyBytes::new(py, payload),))?
891 .cast_into::<PyTuple>()?;
892
893 let name: String = tuple.get_item(0)?.extract()?;
894 let evaluator: Py<PyAny> = tuple.get_item(1)?.unbind();
895 let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
896 let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
897 let volatility_str: String = tuple.get_item(4)?.extract()?;
898
899 let input_types = read_input_dtypes(&input_schema_bytes)?;
900 let return_type = read_single_return_field(&return_schema_bytes, "PythonFunctionWindowUDF")?
901 .data_type()
902 .clone();
903 let volatility = parse_volatility_str(&volatility_str)?;
904
905 Ok(PythonFunctionWindowUDF::new(
906 name,
907 evaluator,
908 input_types,
909 return_type,
910 volatility,
911 ))
912}
913
914pub(crate) fn try_encode_python_udaf(node: &AggregateUDF, buf: &mut Vec<u8>) -> Result<bool> {
924 let Some(py_udf) = node.inner().downcast_ref::<PythonFunctionAggregateUDF>() else {
925 return Ok(false);
926 };
927
928 Python::attach(|py| -> Result<bool> {
929 let bytes = encode_python_udaf(py, py_udf).map_err(to_datafusion_err)?;
930 append_framed_payload(py, buf, PY_AGG_UDF_FAMILY, &bytes)?;
931 Ok(true)
932 })
933}
934
935pub(crate) fn try_decode_python_udaf(buf: &[u8]) -> Result<Option<Arc<AggregateUDF>>> {
936 if !buf.starts_with(PY_AGG_UDF_FAMILY) {
937 return Ok(None);
938 }
939 Python::attach(|py| -> Result<Option<Arc<AggregateUDF>>> {
940 let Some(payload) = read_framed_payload(py, buf, PY_AGG_UDF_FAMILY, "aggregate UDF")?
941 else {
942 return Ok(None);
943 };
944 let udf = decode_python_udaf(py, payload).map_err(to_datafusion_err)?;
945 Ok(Some(Arc::new(AggregateUDF::new_from_impl(udf))))
946 })
947}
948
949fn encode_python_udaf(py: Python<'_>, udf: &PythonFunctionAggregateUDF) -> PyResult<Vec<u8>> {
950 let signature = AggregateUDFImpl::signature(udf);
951 let input_dtypes = signature_input_dtypes(signature, "PythonFunctionAggregateUDF")?;
952 let input_schema_bytes = build_input_schema_bytes(&input_dtypes)?;
953 let return_field = Field::new("result", udf.return_type().clone(), true);
954 let return_schema_bytes = build_single_field_schema_bytes(&return_field)?;
955 let state_fields: Vec<Field> = udf
956 .state_fields_ref()
957 .iter()
958 .map(|f| f.as_ref().clone())
959 .collect();
960 let state_schema_bytes = build_schema_bytes(state_fields)?;
961 let volatility = volatility_wire_str(signature.volatility);
962
963 let payload = PyTuple::new(
964 py,
965 [
966 AggregateUDFImpl::name(udf).into_pyobject(py)?.into_any(),
967 udf.accumulator().bind(py).clone().into_any(),
968 PyBytes::new(py, &input_schema_bytes).into_any(),
969 PyBytes::new(py, &return_schema_bytes).into_any(),
970 PyBytes::new(py, &state_schema_bytes).into_any(),
971 volatility.into_pyobject(py)?.into_any(),
972 ],
973 )?;
974
975 cloudpickle(py)?
976 .call_method1("dumps", (payload,))?
977 .extract::<Vec<u8>>()
978}
979
980fn decode_python_udaf(py: Python<'_>, payload: &[u8]) -> PyResult<PythonFunctionAggregateUDF> {
981 let tuple = cloudpickle(py)?
982 .call_method1("loads", (PyBytes::new(py, payload),))?
983 .cast_into::<PyTuple>()?;
984
985 let name: String = tuple.get_item(0)?.extract()?;
986 let accumulator: Py<PyAny> = tuple.get_item(1)?.unbind();
987 let input_schema_bytes: Vec<u8> = tuple.get_item(2)?.extract()?;
988 let return_schema_bytes: Vec<u8> = tuple.get_item(3)?.extract()?;
989 let state_schema_bytes: Vec<u8> = tuple.get_item(4)?.extract()?;
990 let volatility_str: String = tuple.get_item(5)?.extract()?;
991
992 let input_types = read_input_dtypes(&input_schema_bytes)?;
993 let return_type = read_single_return_field(&return_schema_bytes, "PythonFunctionAggregateUDF")?
994 .data_type()
995 .clone();
996 let state_schema = schema_from_ipc_bytes(&state_schema_bytes).map_err(arrow_to_py_err)?;
1002 let state_fields: Vec<arrow::datatypes::FieldRef> =
1003 state_schema.fields().iter().cloned().collect();
1004 let volatility = parse_volatility_str(&volatility_str)?;
1005
1006 Ok(PythonFunctionAggregateUDF::from_parts(
1007 name,
1008 accumulator,
1009 input_types,
1010 return_type,
1011 state_fields,
1012 volatility,
1013 ))
1014}
1015
1016#[cfg(test)]
1017mod wire_header_tests {
1018 use super::*;
1019
1020 const TEST_PY: (u8, u8) = (3, 12);
1021
1022 #[test]
1023 fn strip_returns_none_when_family_absent() {
1024 let buf = b"OTHER_PAYLOAD";
1025 assert!(matches!(
1026 strip_wire_header(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY),
1027 Ok(None)
1028 ));
1029 }
1030
1031 #[test]
1032 fn strip_errors_on_truncated_version_byte() {
1033 let buf = PY_SCALAR_UDF_FAMILY;
1034 let err = strip_wire_header(buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
1035 assert!(format!("{err}").contains("missing wire-format version byte"));
1036 }
1037
1038 #[test]
1039 fn strip_errors_on_too_new_version() {
1040 let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
1041 buf.push(WIRE_VERSION_CURRENT.saturating_add(1));
1042 buf.push(TEST_PY.0);
1043 buf.push(TEST_PY.1);
1044 buf.extend_from_slice(b"payload");
1045 let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
1046 let msg = format!("{err}");
1047 assert!(msg.contains("wire-format version v"));
1048 assert!(msg.contains("supports"));
1049 assert!(msg.contains("Align datafusion-python versions"));
1050 }
1051
1052 #[test]
1053 fn strip_errors_on_too_old_version() {
1054 if WIRE_VERSION_MIN_SUPPORTED == 0 {
1055 return;
1056 }
1057 let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
1058 buf.push(WIRE_VERSION_MIN_SUPPORTED - 1);
1059 buf.push(TEST_PY.0);
1060 buf.push(TEST_PY.1);
1061 buf.extend_from_slice(b"payload");
1062 assert!(strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).is_err());
1063 }
1064
1065 #[test]
1066 fn strip_errors_on_truncated_py_major() {
1067 let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
1068 buf.push(WIRE_VERSION_CURRENT);
1069 let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
1070 assert!(format!("{err}").contains("missing Python major version byte"));
1071 }
1072
1073 #[test]
1074 fn strip_errors_on_truncated_py_minor() {
1075 let mut buf = PY_SCALAR_UDF_FAMILY.to_vec();
1076 buf.push(WIRE_VERSION_CURRENT);
1077 buf.push(TEST_PY.0);
1078 let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY).unwrap_err();
1079 assert!(format!("{err}").contains("missing Python minor version byte"));
1080 }
1081
1082 #[test]
1083 fn strip_errors_on_py_minor_mismatch() {
1084 let mut buf = Vec::new();
1085 write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, (3, 11));
1086 buf.extend_from_slice(b"payload");
1087 let err = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", (3, 12)).unwrap_err();
1088 let msg = format!("{err}");
1089 assert!(msg.contains("Python 3.11"));
1090 assert!(msg.contains("Python 3.12"));
1091 assert!(msg.contains("not portable across Python minor versions"));
1092 }
1093
1094 #[test]
1095 fn strip_errors_on_py_major_mismatch() {
1096 let mut buf = Vec::new();
1097 write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, (3, 12));
1098 buf.extend_from_slice(b"payload");
1099 assert!(strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", (4, 0)).is_err());
1100 }
1101
1102 #[test]
1103 fn write_then_strip_round_trips_scalar_payload() {
1104 let mut buf = Vec::new();
1105 write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, TEST_PY);
1106 buf.extend_from_slice(b"scalar-payload");
1107
1108 let payload = strip_wire_header(&buf, PY_SCALAR_UDF_FAMILY, "scalar UDF", TEST_PY)
1109 .unwrap()
1110 .unwrap();
1111 assert_eq!(payload, b"scalar-payload");
1112 }
1113
1114 #[test]
1115 fn write_then_strip_round_trips_agg_payload() {
1116 let mut buf = Vec::new();
1117 write_wire_header(&mut buf, PY_AGG_UDF_FAMILY, TEST_PY);
1118 buf.extend_from_slice(b"agg-payload");
1119
1120 let payload = strip_wire_header(&buf, PY_AGG_UDF_FAMILY, "aggregate UDF", TEST_PY)
1121 .unwrap()
1122 .unwrap();
1123 assert_eq!(payload, b"agg-payload");
1124 }
1125
1126 #[test]
1127 fn write_then_strip_round_trips_window_payload() {
1128 let mut buf = Vec::new();
1129 write_wire_header(&mut buf, PY_WINDOW_UDF_FAMILY, TEST_PY);
1130 buf.extend_from_slice(b"window-payload");
1131
1132 let payload = strip_wire_header(&buf, PY_WINDOW_UDF_FAMILY, "window UDF", TEST_PY)
1133 .unwrap()
1134 .unwrap();
1135 assert_eq!(payload, b"window-payload");
1136 }
1137
1138 #[test]
1139 fn strip_does_not_match_a_different_family() {
1140 let mut buf = Vec::new();
1141 write_wire_header(&mut buf, PY_SCALAR_UDF_FAMILY, TEST_PY);
1142 buf.extend_from_slice(b"payload");
1143 assert!(matches!(
1144 strip_wire_header(&buf, PY_WINDOW_UDF_FAMILY, "window UDF", TEST_PY),
1145 Ok(None)
1146 ));
1147 }
1148}