use std::cmp::Ordering;
use std::sync::Arc;
use arrow::array::{
Array, ArrayRef, AsArray, BooleanArray, Float64Array, ListArray, StructArray, TimestampMicrosecondArray,
};
use arrow::buffer::OffsetBuffer;
use arrow::datatypes::{DataType, Field, FieldRef, Fields, Float64Type, TimeUnit, TimestampMicrosecondType};
use datafusion::common::{ScalarValue, exec_err, plan_err};
use datafusion::error::Result as DfResult;
use datafusion::logical_expr::function::{AccumulatorArgs, StateFieldsArgs};
use datafusion::logical_expr::utils::format_state_name;
use datafusion::logical_expr::{
Accumulator, AggregateUDF, AggregateUDFImpl, EmitTo, GroupsAccumulator, Signature, Volatility,
};
pub(crate) fn m4_udaf() -> AggregateUDF {
AggregateUDF::from(M4 { signature: Signature::user_defined(Volatility::Immutable) })
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct M4 {
signature: Signature,
}
fn point(tz: Option<Arc<str>>) -> DataType {
DataType::Struct(Fields::from(vec![
Field::new("time", DataType::Timestamp(TimeUnit::Microsecond, tz), true),
Field::new("value", DataType::Float64, true),
]))
}
fn points(tz: Option<Arc<str>>) -> DataType {
DataType::List(Arc::new(Field::new_list_field(point(tz), true)))
}
fn time_zone(arg_types: &[DataType]) -> DfResult<Option<Arc<str>>> {
let time = match arg_types {
[time, _] => Some(time),
[DataType::List(item)] => {
if let DataType::Struct(fields) = item.data_type() {
fields.find("time").map(|(_, f)| f.data_type())
} else {
None
}
}
_ => None,
};
if let Some(DataType::Timestamp(_, tz)) = time {
Ok(tz.clone())
} else {
plan_err!("m4 takes (time, value), or the lists of {{time, value}} other m4 calls return")
}
}
fn args_time_zone(args: &AccumulatorArgs<'_>) -> DfResult<Option<Arc<str>>> {
let types: Vec<DataType> = args.expr_fields.iter().map(|f| f.data_type().clone()).collect();
time_zone(&types)
}
impl AggregateUDFImpl for M4 {
fn name(&self) -> &'static str {
"m4"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn coerce_types(&self, arg_types: &[DataType]) -> DfResult<Vec<DataType>> {
let tz = time_zone(arg_types)?;
match arg_types {
[_, value] if value.is_numeric() => {
Ok(vec![DataType::Timestamp(TimeUnit::Microsecond, tz), DataType::Float64])
}
[_, other] => plan_err!("m4 needs a numeric value, not {other}"),
_ => Ok(vec![points(tz)]),
}
}
fn return_type(&self, arg_types: &[DataType]) -> DfResult<DataType> {
Ok(points(time_zone(arg_types)?))
}
fn state_fields(&self, args: StateFieldsArgs<'_>) -> DfResult<Vec<FieldRef>> {
Ok(vec![Arc::new(Field::new(format_state_name(args.name, "points"), args.return_type().clone(), true))])
}
fn accumulator(&self, args: AccumulatorArgs<'_>) -> DfResult<Box<dyn Accumulator>> {
Ok(Box::new(M4Accumulator { tz: args_time_zone(&args)?, extremes: Extremes::default() }))
}
fn groups_accumulator_supported(&self, _args: AccumulatorArgs<'_>) -> bool {
true
}
fn create_groups_accumulator(&self, args: AccumulatorArgs<'_>) -> DfResult<Box<dyn GroupsAccumulator>> {
Ok(Box::new(M4Groups { tz: args_time_zone(&args)?, groups: vec![] }))
}
}
type Point = (i64, f64);
#[derive(Clone, Copy, Debug, Default)]
struct Extremes {
first: Option<Point>,
last: Option<Point>,
min: Option<Point>,
max: Option<Point>,
}
impl Extremes {
fn add(&mut self, p: Point) {
let by_time = |a: &Point, b: &Point| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1));
let by_value = |a: &Point, b: &Point| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0));
if self.first.is_none_or(|f| by_time(&p, &f) == Ordering::Less) {
self.first = Some(p);
}
if self.last.is_none_or(|l| by_time(&p, &l) == Ordering::Greater) {
self.last = Some(p);
}
if self.min.is_none_or(|m| by_value(&p, &m) == Ordering::Less) {
self.min = Some(p);
}
if self.max.is_none_or(|m| p.1.total_cmp(&m.1).then(m.0.cmp(&p.0)) == Ordering::Greater) {
self.max = Some(p);
}
}
fn points(&self) -> Vec<Point> {
let mut points: Vec<Point> = [self.first, self.min, self.max, self.last].into_iter().flatten().collect();
points.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)));
points.dedup_by(|a, b| a.0 == b.0 && a.1.total_cmp(&b.1) == Ordering::Equal);
points
}
}
fn for_each_point(columns: &[ArrayRef], mut add: impl FnMut(usize, Point)) -> DfResult<()> {
match columns {
[times, values] => {
let times = times.as_primitive::<TimestampMicrosecondType>();
let values = values.as_primitive::<Float64Type>();
for row in 0..times.len() {
if times.is_valid(row) && values.is_valid(row) {
add(row, (times.value(row), values.value(row)));
}
}
}
[lists] => {
let lists = lists.as_list::<i32>();
let Some(points) = lists.values().as_any().downcast_ref::<StructArray>() else {
return exec_err!("m4 merges lists of {{time, value}}");
};
let times = points.column(0).as_primitive::<TimestampMicrosecondType>();
let values = points.column(1).as_primitive::<Float64Type>();
for row in 0..lists.len() {
if lists.is_null(row) {
continue;
}
for i in lists.value_offsets()[row]..lists.value_offsets()[row + 1] {
let i = usize::try_from(i).unwrap_or(usize::MAX);
if points.is_valid(i) && times.is_valid(i) && values.is_valid(i) {
add(row, (times.value(i), values.value(i)));
}
}
}
}
_ => return exec_err!("m4 takes (time, value), or the points of other m4 calls"),
}
Ok(())
}
fn lists<'a>(groups: impl Iterator<Item = &'a Extremes>, tz: Option<Arc<str>>) -> ListArray {
let (mut times, mut values, mut lengths) = (vec![], vec![], vec![]);
for group in groups {
let points = group.points();
lengths.push(points.len());
for (t, v) in points {
times.push(t);
values.push(v);
}
}
let DataType::Struct(fields) = point(tz.clone()) else { unreachable!("a struct") };
let times = TimestampMicrosecondArray::from(times).with_timezone_opt(tz.clone());
let structs =
StructArray::new(fields, vec![Arc::new(times) as ArrayRef, Arc::new(Float64Array::from(values))], None);
let field = Arc::new(Field::new_list_field(point(tz), true));
ListArray::new(field, OffsetBuffer::from_lengths(lengths), Arc::new(structs), None)
}
#[derive(Debug, Default)]
struct M4Accumulator {
tz: Option<Arc<str>>,
extremes: Extremes,
}
impl M4Accumulator {
fn points(&self) -> ScalarValue {
ScalarValue::List(Arc::new(lists(std::iter::once(&self.extremes), self.tz.clone())))
}
}
impl Accumulator for M4Accumulator {
fn update_batch(&mut self, values: &[ArrayRef]) -> DfResult<()> {
for_each_point(values, |_, p| self.extremes.add(p))
}
fn merge_batch(&mut self, states: &[ArrayRef]) -> DfResult<()> {
for_each_point(states, |_, p| self.extremes.add(p))
}
fn state(&mut self) -> DfResult<Vec<ScalarValue>> {
Ok(vec![self.points()])
}
fn evaluate(&mut self) -> DfResult<ScalarValue> {
Ok(self.points())
}
fn size(&self) -> usize {
size_of_val(self)
}
}
#[derive(Debug)]
struct M4Groups {
tz: Option<Arc<str>>,
groups: Vec<Extremes>,
}
impl M4Groups {
fn add(&mut self, columns: &[ArrayRef], groups: &[usize], filter: Option<&BooleanArray>, n: usize) -> DfResult<()> {
self.groups.resize(n, Extremes::default());
let all = &mut self.groups;
for_each_point(columns, |row, p| {
if filter.is_none_or(|f| f.is_valid(row) && f.value(row)) {
all[groups[row]].add(p);
}
})
}
}
impl GroupsAccumulator for M4Groups {
fn update_batch(
&mut self,
values: &[ArrayRef],
group_indices: &[usize],
opt_filter: Option<&BooleanArray>,
total_num_groups: usize,
) -> DfResult<()> {
self.add(values, group_indices, opt_filter, total_num_groups)
}
fn merge_batch(
&mut self,
values: &[ArrayRef],
group_indices: &[usize],
opt_filter: Option<&BooleanArray>,
total_num_groups: usize,
) -> DfResult<()> {
self.add(values, group_indices, opt_filter, total_num_groups)
}
fn evaluate(&mut self, emit_to: EmitTo) -> DfResult<ArrayRef> {
let taken = emit_to.take_needed(&mut self.groups);
Ok(Arc::new(lists(taken.iter(), self.tz.clone())))
}
fn state(&mut self, emit_to: EmitTo) -> DfResult<Vec<ArrayRef>> {
Ok(vec![self.evaluate(emit_to)?])
}
fn size(&self) -> usize {
self.groups.capacity() * size_of::<Extremes>()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn points_of(acc: &mut M4Accumulator) -> Vec<Point> {
let ScalarValue::List(list) = acc.evaluate().unwrap() else { panic!("a list") };
let points = list.value(0);
let points = points.as_struct();
let times = points.column(0).as_primitive::<TimestampMicrosecondType>();
let values = points.column(1).as_primitive::<Float64Type>();
(0..points.len()).map(|i| (times.value(i), values.value(i))).collect()
}
fn update(acc: &mut M4Accumulator, rows: &[(Option<i64>, Option<f64>)]) {
let times: ArrayRef = Arc::new(TimestampMicrosecondArray::from(rows.iter().map(|r| r.0).collect::<Vec<_>>()));
let values: ArrayRef = Arc::new(Float64Array::from(rows.iter().map(|r| r.1).collect::<Vec<_>>()));
acc.update_batch(&[times, values]).unwrap();
}
#[test]
fn first_min_max_last_in_time_order() {
let mut acc = M4Accumulator::default();
update(&mut acc, &[(Some(3), Some(5.0)), (Some(1), Some(2.0)), (Some(2), Some(9.0)), (Some(4), Some(1.0))]);
assert_eq!(points_of(&mut acc), [(1, 2.0), (2, 9.0), (4, 1.0)], "the last point is also the smallest");
let mut one = M4Accumulator::default();
update(&mut one, &[(Some(7), Some(1.5)), (None, Some(0.0)), (Some(8), None)]);
assert_eq!(points_of(&mut one), [(7, 1.5)]);
}
#[test]
fn ties_are_broken_the_same_in_any_order() {
let rows = [
(Some(5), Some(3.0)),
(Some(1), Some(3.0)),
(Some(1), Some(1.0)),
(Some(9), Some(3.0)),
(Some(9), Some(4.0)),
];
let mut forward = M4Accumulator::default();
update(&mut forward, &rows);
let mut backward = M4Accumulator::default();
update(&mut backward, &rows.iter().rev().copied().collect::<Vec<_>>());
assert_eq!(points_of(&mut forward), [(1, 1.0), (9, 4.0)]);
assert_eq!(points_of(&mut forward), points_of(&mut backward));
}
#[test]
fn merged_points_are_the_points_of_all_rows() {
let rows: Vec<(Option<i64>, Option<f64>)> = (0..100).map(|i| (Some(i), Some(((i * 37) % 23) as f64))).collect();
let mut all = M4Accumulator::default();
update(&mut all, &rows);
let mut merged = M4Accumulator::default();
for part in rows.chunks(7) {
let mut acc = M4Accumulator::default();
update(&mut acc, part);
let ScalarValue::List(list) = acc.state().unwrap().remove(0) else { panic!("a list") };
merged.merge_batch(&[list]).unwrap();
}
assert_eq!(points_of(&mut merged), points_of(&mut all));
}
#[test]
fn groups_match_one_group_at_a_time() {
let rows: Vec<(Option<i64>, Option<f64>)> = (0..60).map(|i| (Some(i), Some(((i * 13) % 17) as f64))).collect();
let groups: Vec<usize> = (0..60).map(|i| i % 3).collect();
let filter = BooleanArray::from((0..60).map(|i| i % 5 != 0).collect::<Vec<_>>());
let times: ArrayRef = Arc::new(TimestampMicrosecondArray::from(rows.iter().map(|r| r.0).collect::<Vec<_>>()));
let values: ArrayRef = Arc::new(Float64Array::from(rows.iter().map(|r| r.1).collect::<Vec<_>>()));
let mut all = M4Groups { tz: None, groups: vec![] };
all.update_batch(&[times.clone(), values.clone()], &groups, Some(&filter), 3).unwrap();
let mut merged = M4Groups { tz: None, groups: vec![] };
merged.merge_batch(&all.state(EmitTo::All).unwrap(), &[0, 1, 2], None, 3).unwrap();
let lists = merged.evaluate(EmitTo::All).unwrap();
for g in 0..3 {
let mut one = M4Accumulator::default();
let kept: Vec<_> = (0..60).filter(|&i| groups[i] == g && i % 5 != 0).map(|i| rows[i]).collect();
update(&mut one, &kept);
let ScalarValue::List(expected) = one.evaluate().unwrap() else { panic!("a list") };
assert_eq!(lists.as_list::<i32>().value(g).to_data(), expected.value(0).to_data(), "group {g}");
}
}
}