blob: 9b82c4bd1130027a3c123fac82c348ddbab9edc8 [file]
// 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")
}
}