blob: 9ef5c258d1e83ce686314e16842c94db277ee054 [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 functions this library contributes.
//!
//! Deliberately arithmetic on TPC-H columns rather than anything clever: the
//! interesting part of this crate is that a function has to be *reachable* in
//! whichever process ends up evaluating it, not what the function computes.
use std::sync::Arc;
use arrow::array::{Array, ArrayRef, AsArray, Float64Array};
use arrow::datatypes::{DataType, Field, FieldRef, Float64Type};
use datafusion::common::{Result, ScalarValue, exec_err};
use datafusion::logical_expr::function::{
AccumulatorArgs, PartitionEvaluatorArgs, StateFieldsArgs, WindowUDFFieldArgs,
};
use datafusion::logical_expr::{
Accumulator, AggregateUDF, AggregateUDFImpl, ColumnarValue, PartitionEvaluator,
ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility, WindowUDF, WindowUDFImpl,
};
use datafusion_functions_window::rank::rank_udwf;
/// Name of the scalar function, used by the codec as the whole encoding.
pub(crate) const NET_REVENUE: &str = "dfx_net_revenue";
/// Name of the aggregate function.
pub(crate) const WEIGHTED_AVG: &str = "dfx_weighted_avg";
/// Name of the window function.
pub(crate) const REVENUE_RANK: &str = "dfx_revenue_rank";
/// `extendedprice * (1 - discount) * (1 + tax)`, the TPC-H revenue expression.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct NetRevenue {
signature: Signature,
}
impl Default for NetRevenue {
fn default() -> Self {
Self {
signature: Signature::exact(
vec![DataType::Float64, DataType::Float64, DataType::Float64],
Volatility::Immutable,
),
}
}
}
impl ScalarUDFImpl for NetRevenue {
fn name(&self) -> &str {
NET_REVENUE
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
Ok(DataType::Float64)
}
/// # Why `to_array(number_rows)` and not `values_to_arrays`
///
/// `values_to_arrays` infers its length from the arguments, and when they
/// are *all* scalars there is nothing to infer from, so it produces one
/// row. This function would then hand back a single value for a batch of
/// a hundred, into a column the rest of the plan sizes at a hundred.
///
/// `number_rows` is the batch's own count and is the only argument-
/// independent answer, which is why it is passed. Taking it also makes
/// the output length a stated contract rather than something read off
/// whichever argument happened to be examined first.
///
/// Not reachable from SQL today: an all-literal call is constant, so
/// `SimplifyExpressions` folds it before execution and this code sees a
/// one-row batch that agrees with `number_rows`. That is precisely what
/// makes the shape worth copying correctly -- the bug is invisible until
/// something suppresses the fold.
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let rows = args.number_rows;
let [price, discount, tax] = args.args.as_slice() else {
return exec_err!("{NET_REVENUE} takes 3 arguments, got {}", args.args.len());
};
let price = price.to_array(rows)?;
let discount = discount.to_array(rows)?;
let tax = tax.to_array(rows)?;
let price = price.as_primitive::<Float64Type>();
let discount = discount.as_primitive::<Float64Type>();
let tax = tax.as_primitive::<Float64Type>();
let values: Float64Array = (0..rows)
.map(|row| {
if price.is_null(row) || discount.is_null(row) || tax.is_null(row) {
return None;
}
Some(price.value(row) * (1.0 - discount.value(row)) * (1.0 + tax.value(row)))
})
.collect();
Ok(ColumnarValue::Array(Arc::new(values)))
}
}
/// `sum(value * weight) / sum(weight)`.
///
/// Written out rather than delegating to a built-in because the state is the
/// point: two running sums, which is what lets DataFusion compute this in a
/// partial aggregate on each worker and merge the results on the driver. An
/// aggregate that could only be evaluated over the whole input at once would
/// not survive being split across processes.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct WeightedAvg {
signature: Signature,
}
impl Default for WeightedAvg {
fn default() -> Self {
Self {
signature: Signature::exact(
vec![DataType::Float64, DataType::Float64],
Volatility::Immutable,
),
}
}
}
impl AggregateUDFImpl for WeightedAvg {
fn name(&self) -> &str {
WEIGHTED_AVG
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
Ok(DataType::Float64)
}
fn accumulator(&self, _args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
Ok(Box::new(WeightedAvgAccumulator::default()))
}
/// The two partial sums, in the order [`WeightedAvgAccumulator::state`]
/// returns them.
fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
Ok(vec![
Arc::new(Field::new(
format!("{}[weighted_sum]", args.name),
DataType::Float64,
false,
)),
Arc::new(Field::new(
format!("{}[weight_sum]", args.name),
DataType::Float64,
false,
)),
])
}
}
#[derive(Debug, Default)]
struct WeightedAvgAccumulator {
weighted_sum: f64,
weight_sum: f64,
}
impl Accumulator for WeightedAvgAccumulator {
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
let [value, weight] = values else {
return exec_err!("{WEIGHTED_AVG} takes 2 arguments, got {}", values.len());
};
let value = value.as_primitive::<Float64Type>();
let weight = weight.as_primitive::<Float64Type>();
for row in 0..value.len() {
// A null in either argument contributes to neither sum, so the
// result is the weighted average of the rows that had both.
if value.is_null(row) || weight.is_null(row) {
continue;
}
self.weighted_sum += value.value(row) * weight.value(row);
self.weight_sum += weight.value(row);
}
Ok(())
}
/// Merge partial states, which is the step that runs on the driver over
/// results computed on the workers.
fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
let [weighted_sum, weight_sum] = states else {
return exec_err!("{WEIGHTED_AVG} has 2 state columns, got {}", states.len());
};
let weighted_sum = weighted_sum.as_primitive::<Float64Type>();
let weight_sum = weight_sum.as_primitive::<Float64Type>();
for row in 0..weighted_sum.len() {
self.weighted_sum += weighted_sum.value(row);
self.weight_sum += weight_sum.value(row);
}
Ok(())
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
Ok(vec![
ScalarValue::Float64(Some(self.weighted_sum)),
ScalarValue::Float64(Some(self.weight_sum)),
])
}
fn evaluate(&mut self) -> Result<ScalarValue> {
// No rows, or every weight zero: null rather than a division by zero,
// matching what `avg` does for an empty input.
if self.weight_sum == 0.0 {
return Ok(ScalarValue::Float64(None));
}
Ok(ScalarValue::Float64(Some(
self.weighted_sum / self.weight_sum,
)))
}
fn size(&self) -> usize {
std::mem::size_of_val(self)
}
}
/// Ranks rows within a window, under a name this library owns.
///
/// Delegates to the built-in `rank`: the reason it is here is to give the
/// library a window function whose *name* has to resolve on whichever process
/// evaluates it, which is the same portability question the other two raise.
#[derive(Debug, Clone)]
pub(crate) struct RevenueRank {
inner: Arc<WindowUDF>,
}
impl Default for RevenueRank {
fn default() -> Self {
Self { inner: rank_udwf() }
}
}
impl PartialEq for RevenueRank {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl Eq for RevenueRank {}
impl std::hash::Hash for RevenueRank {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
impl WindowUDFImpl for RevenueRank {
fn name(&self) -> &str {
REVENUE_RANK
}
fn signature(&self) -> &Signature {
self.inner.signature()
}
fn partition_evaluator(
&self,
args: PartitionEvaluatorArgs,
) -> Result<Box<dyn PartitionEvaluator>> {
self.inner.inner().partition_evaluator(args)
}
fn field(&self, field_args: WindowUDFFieldArgs) -> Result<FieldRef> {
self.inner.inner().field(field_args)
}
fn coerce_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
self.inner.coerce_types(arg_types)
}
}
/// Rebuild one of this library's functions from its name alone.
///
/// This is the whole decode path: the names are the encoding, so a process
/// that has this library's codec installed can reconstruct any of them
/// without the driver having sent bytes and without the function having been
/// registered locally.
pub(crate) fn scalar_by_name(name: &str) -> Option<Arc<ScalarUDF>> {
(name == NET_REVENUE).then(|| Arc::new(ScalarUDF::from(NetRevenue::default())))
}
pub(crate) fn aggregate_by_name(name: &str) -> Option<Arc<AggregateUDF>> {
(name == WEIGHTED_AVG).then(|| Arc::new(AggregateUDF::from(WeightedAvg::default())))
}
pub(crate) fn window_by_name(name: &str) -> Option<Arc<WindowUDF>> {
(name == REVENUE_RANK).then(|| Arc::new(WindowUDF::from(RevenueRank::default())))
}