blob: 397aa6727991fb7bf20517aafaae8829d0941956 [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.
//! The Python surface, and the one thing it deliberately lacks.
//!
//! This library exposes **no** `__datafusion_session_components__`, so it
//! cannot be installed with `SessionContext.with_extensions`. Callers register
//! the three functions and install the two codecs by hand, which is the older
//! and more error-prone path -- and currently the honest one for a function
//! library, because `SessionExtensionComponents` carries codec fields only.
//! There is nowhere for a UDF to go.
//!
//! Keeping one library on the manual path is the point: a real deployment
//! mixes libraries built against different versions of the protocol, and the
//! example should show what that costs rather than pretend every dependency
//! has caught up.
use std::sync::Arc;
use std::sync::atomic::Ordering;
use datafusion::logical_expr::{AggregateUDF, ScalarUDF, WindowUDF};
use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
use datafusion_ffi::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
use datafusion_ffi::udaf::FFI_AggregateUDF;
use datafusion_ffi::udf::FFI_ScalarUDF;
use datafusion_ffi::udwf::FFI_WindowUDF;
use datafusion_proto::logical_plan::LogicalExtensionCodec;
use datafusion_proto::physical_plan::PhysicalExtensionCodec;
use datafusion_python_util::{
create_logical_extension_capsule, create_physical_extension_capsule,
ffi_task_context_provider_from_pycapsule, get_tokio_runtime,
};
use pyo3::prelude::*;
use pyo3::types::PyCapsule;
use crate::codec::{CodecCounters, DfxUdfsLogicalCodec, DfxUdfsPhysicalCodec};
use crate::functions::{NetRevenue, RevenueRank, WeightedAvg};
/// Wire ids, pinned so a rename of the exporting class cannot invalidate
/// plans already written.
const LOGICAL_CODEC_ID: &str = "dfx_udfs.logical.v1";
const PHYSICAL_CODEC_ID: &str = "dfx_udfs.physical.v1";
/// `dfx_net_revenue(extendedprice, discount, tax)`.
#[pyclass(name = "NetRevenueUDF", module = "dfx_udfs")]
#[derive(Default)]
pub(crate) struct PyNetRevenue;
#[pymethods]
impl PyNetRevenue {
#[new]
fn new() -> Self {
Self
}
fn __datafusion_scalar_udf__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCapsule>> {
let func = Arc::new(ScalarUDF::from(NetRevenue::default()));
PyCapsule::new_with_value(py, FFI_ScalarUDF::from(func), cr"datafusion_scalar_udf")
}
}
/// `dfx_weighted_avg(value, weight)`.
#[pyclass(name = "WeightedAvgUDAF", module = "dfx_udfs")]
#[derive(Default)]
pub(crate) struct PyWeightedAvg;
#[pymethods]
impl PyWeightedAvg {
#[new]
fn new() -> Self {
Self
}
fn __datafusion_aggregate_udf__<'py>(
&self,
py: Python<'py>,
) -> PyResult<Bound<'py, PyCapsule>> {
let func = Arc::new(AggregateUDF::from(WeightedAvg::default()));
PyCapsule::new_with_value(
py,
FFI_AggregateUDF::from(func),
cr"datafusion_aggregate_udf",
)
}
}
/// `dfx_revenue_rank()`, as a window function.
#[pyclass(name = "RevenueRankUDWF", module = "dfx_udfs")]
#[derive(Default)]
pub(crate) struct PyRevenueRank;
#[pymethods]
impl PyRevenueRank {
#[new]
fn new() -> Self {
Self
}
fn __datafusion_window_udf__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyCapsule>> {
let func = Arc::new(WindowUDF::from(RevenueRank::default()));
PyCapsule::new_with_value(py, FFI_WindowUDF::from(func), cr"datafusion_window_udf")
}
}
/// Shared decode counters, so a test can see which codec answered.
#[pyclass(from_py_object, name = "CodecObservations", module = "dfx_udfs")]
#[derive(Default, Clone)]
pub(crate) struct PyCodecObservations {
counters: Arc<CodecCounters>,
}
#[pymethods]
impl PyCodecObservations {
#[new]
fn new() -> Self {
Self::default()
}
/// How often a codec rebuilt one of this library's functions from its name.
///
/// Zero after a successful query on a session that registered the
/// functions: the registry is tried first, so the codec is only reached
/// when the receiving session does *not* have them.
fn decode_calls(&self) -> usize {
self.counters.decoded.load(Ordering::SeqCst)
}
/// How often a codec was asked about a name it does not own.
///
/// Non-zero is expected. A name-only payload has no codec id to route on,
/// so it is offered to every installed codec in turn.
fn declined_calls(&self) -> usize {
self.counters.declined.load(Ordering::SeqCst)
}
/// Build the logical codec, sharing these counters.
fn logical_codec(&self) -> PyLogicalCodec {
PyLogicalCodec {
counters: Arc::clone(&self.counters),
}
}
/// Build the physical codec, sharing these counters.
fn physical_codec(&self) -> PyPhysicalCodec {
PyPhysicalCodec {
counters: Arc::clone(&self.counters),
}
}
}
/// Install with `ctx.with_logical_extension_codec(...)`.
#[pyclass(name = "DfxUdfsLogicalCodec", module = "dfx_udfs")]
pub(crate) struct PyLogicalCodec {
counters: Arc<CodecCounters>,
}
#[pymethods]
impl PyLogicalCodec {
#[new]
fn new() -> Self {
Self {
counters: Arc::default(),
}
}
#[getter]
fn __datafusion_codec_id__(&self) -> &'static str {
LOGICAL_CODEC_ID
}
fn __datafusion_logical_extension_codec__<'py>(
&self,
py: Python<'py>,
session: Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyCapsule>> {
let provider = ffi_task_context_provider_from_pycapsule(&session)?;
let runtime = get_tokio_runtime().handle().clone();
let codec: Arc<dyn LogicalExtensionCodec> =
Arc::new(DfxUdfsLogicalCodec::new(Arc::clone(&self.counters)));
let ffi = FFI_LogicalExtensionCodec::new(codec, Some(runtime), provider);
create_logical_extension_capsule(py, &ffi)
}
}
/// Install with `ctx.with_physical_extension_codec(...)`.
#[pyclass(name = "DfxUdfsPhysicalCodec", module = "dfx_udfs")]
pub(crate) struct PyPhysicalCodec {
counters: Arc<CodecCounters>,
}
#[pymethods]
impl PyPhysicalCodec {
#[new]
fn new() -> Self {
Self {
counters: Arc::default(),
}
}
#[getter]
fn __datafusion_codec_id__(&self) -> &'static str {
PHYSICAL_CODEC_ID
}
fn __datafusion_physical_extension_codec__<'py>(
&self,
py: Python<'py>,
session: Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyCapsule>> {
let provider = ffi_task_context_provider_from_pycapsule(&session)?;
let runtime = get_tokio_runtime().handle().clone();
let codec: Arc<dyn PhysicalExtensionCodec + Send> =
Arc::new(DfxUdfsPhysicalCodec::new(Arc::clone(&self.counters)));
let ffi = FFI_PhysicalExtensionCodec::new(codec, Some(runtime), provider);
create_physical_extension_capsule(py, &ffi)
}
}