| // 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. |
| |
| //! A codec whose functions need no payload at all. |
| //! |
| //! Most extension codecs answer with bytes. This one owns a fixed catalog of |
| //! functions that are fully described by their names, so `try_encode_udf` |
| //! writes nothing and `try_decode_udf` rebuilds the function from `name` |
| //! alone. DataFusion supports that shape directly: an encoder that writes no |
| //! bytes leaves `fun_definition` unset, and the decoder then tries the |
| //! `FunctionRegistry` first and the codec second — see the |
| //! `None => ctx.udf(..).or_else(|_| codec.try_decode_udf(name, &[]))` arm in |
| //! `datafusion-proto`'s `from_proto.rs`. |
| //! |
| //! It exists here to pin that arm. Because there are no bytes, there is |
| //! nothing to tag with the codec's identity, so this is the one path where |
| //! `PythonLogicalCodec` still offers a payload to every installed codec in |
| //! turn. A change that wrapped empty encodings in an envelope would set |
| //! `fun_definition`, skip the registry lookup permanently, and break both this |
| //! codec and plain by-name round trips — with no other test noticing. |
| |
| use std::fmt; |
| use std::sync::Arc; |
| use std::sync::atomic::{AtomicUsize, Ordering}; |
| |
| use arrow_schema::DataType; |
| use datafusion::common::error::Result; |
| use datafusion::common::not_impl_err; |
| use datafusion::logical_expr::{ |
| ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, TypeSignature, |
| Volatility, |
| }; |
| use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; |
| use datafusion_proto::logical_plan::{DefaultLogicalExtensionCodec, LogicalExtensionCodec}; |
| use datafusion_python_util::{ffi_task_context_provider_from_pycapsule, get_tokio_runtime}; |
| use pyo3::prelude::*; |
| use pyo3::types::PyCapsule; |
| |
| /// Prefix marking the functions this library owns. A name is the entire |
| /// encoding, so the prefix is the whole ownership test. |
| const NAME_PREFIX: &str = "name_only_"; |
| |
| /// Scalar function reconstructed purely from its name. |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| struct NameOnlyUdf { |
| name: String, |
| signature: Signature, |
| } |
| |
| impl NameOnlyUdf { |
| fn new(name: impl Into<String>) -> Self { |
| Self { |
| name: name.into(), |
| signature: Signature::new(TypeSignature::Any(1), Volatility::Immutable), |
| } |
| } |
| } |
| |
| impl ScalarUDFImpl for NameOnlyUdf { |
| fn name(&self) -> &str { |
| &self.name |
| } |
| |
| fn signature(&self) -> &Signature { |
| &self.signature |
| } |
| |
| fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> { |
| Ok(DataType::Int64) |
| } |
| |
| fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> { |
| Ok(args.args[0].clone()) |
| } |
| } |
| |
| #[derive(Default)] |
| struct Counters { |
| encode_udf: AtomicUsize, |
| decode_udf: AtomicUsize, |
| } |
| |
| impl fmt::Debug for Counters { |
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| formatter.debug_struct("Counters").finish_non_exhaustive() |
| } |
| } |
| |
| struct NameOnlyLogicalExtensionCodec { |
| inner: DefaultLogicalExtensionCodec, |
| counters: Arc<Counters>, |
| } |
| |
| impl fmt::Debug for NameOnlyLogicalExtensionCodec { |
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| formatter |
| .debug_struct("NameOnlyLogicalExtensionCodec") |
| .finish_non_exhaustive() |
| } |
| } |
| |
| impl LogicalExtensionCodec for NameOnlyLogicalExtensionCodec { |
| fn try_decode( |
| &self, |
| buf: &[u8], |
| inputs: &[datafusion::logical_expr::LogicalPlan], |
| ctx: &datafusion::execution::TaskContext, |
| ) -> Result<datafusion::logical_expr::Extension> { |
| self.inner.try_decode(buf, inputs, ctx) |
| } |
| |
| fn try_encode( |
| &self, |
| node: &datafusion::logical_expr::Extension, |
| buf: &mut Vec<u8>, |
| ) -> Result<()> { |
| self.inner.try_encode(node, buf) |
| } |
| |
| fn try_decode_table_provider( |
| &self, |
| buf: &[u8], |
| table_ref: &datafusion::common::TableReference, |
| schema: arrow_schema::SchemaRef, |
| ctx: &datafusion::execution::TaskContext, |
| ) -> Result<Arc<dyn datafusion::datasource::TableProvider>> { |
| self.inner |
| .try_decode_table_provider(buf, table_ref, schema, ctx) |
| } |
| |
| fn try_encode_table_provider( |
| &self, |
| table_ref: &datafusion::common::TableReference, |
| node: Arc<dyn datafusion::datasource::TableProvider>, |
| buf: &mut Vec<u8>, |
| ) -> Result<()> { |
| self.inner.try_encode_table_provider(table_ref, node, buf) |
| } |
| |
| /// Writes nothing on purpose. The name is the whole encoding, so there is |
| /// no payload to emit, and returning `Ok` with an empty buffer is how a |
| /// codec says "encoded by name" to DataFusion. |
| fn try_encode_udf(&self, node: &ScalarUDF, _buf: &mut Vec<u8>) -> Result<()> { |
| if node.name().starts_with(NAME_PREFIX) { |
| self.counters.encode_udf.fetch_add(1, Ordering::SeqCst); |
| } |
| Ok(()) |
| } |
| |
| /// Rebuilds the function from `name`, with no registry entry and no bytes. |
| fn try_decode_udf(&self, name: &str, buf: &[u8]) -> Result<Arc<ScalarUDF>> { |
| if !name.starts_with(NAME_PREFIX) { |
| return not_impl_err!("Not a name-only function: {name}"); |
| } |
| if !buf.is_empty() { |
| return not_impl_err!( |
| "name-only functions carry no payload, but {} bytes were supplied for {name}", |
| buf.len() |
| ); |
| } |
| self.counters.decode_udf.fetch_add(1, Ordering::SeqCst); |
| Ok(Arc::new(ScalarUDF::from(NameOnlyUdf::new(name)))) |
| } |
| } |
| |
| /// The function [`NameOnlyUdfCodec`] owns, exported so a session can register |
| /// it and build a plan that references it. |
| /// |
| /// Only the *encoding* session needs it registered. The decoding session |
| /// deliberately does not, which is what forces the codec's name-only decode |
| /// path to run. |
| #[pyclass( |
| from_py_object, |
| name = "NameOnlyFunction", |
| module = "datafusion_ffi_example", |
| subclass |
| )] |
| #[derive(Debug, Clone)] |
| pub(crate) struct NameOnlyFunction; |
| |
| #[pymethods] |
| impl NameOnlyFunction { |
| #[new] |
| fn new() -> Self { |
| Self |
| } |
| |
| fn __datafusion_scalar_udf__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCapsule>> { |
| let func = Arc::new(ScalarUDF::from(NameOnlyUdf::new(format!( |
| "{NAME_PREFIX}identity" |
| )))); |
| PyCapsule::new_with_value( |
| py, |
| datafusion_ffi::udf::FFI_ScalarUDF::from(func), |
| cr"datafusion_scalar_udf", |
| ) |
| } |
| } |
| |
| /// Codec owning functions that are reconstructible from their names alone. |
| /// |
| /// A real library shaped like this would be one shipping a fixed catalog of |
| /// built-ins: nothing about a call site varies, so there is nothing to encode. |
| #[pyclass( |
| from_py_object, |
| name = "NameOnlyUdfCodec", |
| module = "datafusion_ffi_example", |
| subclass |
| )] |
| #[derive(Clone)] |
| pub(crate) struct NameOnlyUdfCodec { |
| counters: Arc<Counters>, |
| } |
| |
| #[pymethods] |
| impl NameOnlyUdfCodec { |
| #[new] |
| fn new() -> Self { |
| Self { |
| counters: Arc::new(Counters::default()), |
| } |
| } |
| |
| /// Name of the function this codec can rebuild, for use in a query. |
| #[staticmethod] |
| fn function_name() -> String { |
| format!("{NAME_PREFIX}identity") |
| } |
| |
| fn encode_udf_calls(&self) -> usize { |
| self.counters.encode_udf.load(Ordering::SeqCst) |
| } |
| |
| fn decode_udf_calls(&self) -> usize { |
| self.counters.decode_udf.load(Ordering::SeqCst) |
| } |
| |
| fn __datafusion_logical_extension_codec__<'py>( |
| &self, |
| py: Python<'py>, |
| session: Bound<'py, PyAny>, |
| ) -> PyResult<Bound<'py, PyCapsule>> { |
| let inner: Arc<dyn LogicalExtensionCodec> = Arc::new(NameOnlyLogicalExtensionCodec { |
| inner: DefaultLogicalExtensionCodec {}, |
| counters: Arc::clone(&self.counters), |
| }); |
| |
| let runtime = get_tokio_runtime().handle().clone(); |
| let ctx_provider = ffi_task_context_provider_from_pycapsule(&session)?; |
| let ffi = FFI_LogicalExtensionCodec::new(inner, Some(runtime), ctx_provider); |
| |
| PyCapsule::new_with_value(py, ffi, cr"datafusion_logical_extension_codec") |
| } |
| } |