blob: 38eb794b3518590f32327b1dca72f819cb53b93c [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.
//! Numeric aggregators: sum, product, min, max.
//!
//! `sum` operates on every integer / floating / Decimal numeric type.
//! `product` accepts the same numeric family except DECIMAL — basic mode does
//! not yet implement BigDecimal-style scale rebasing for Decimal product, so
//! Decimal columns are rejected at construction. Integer overflow on either
//! aggregator is reported as [`Error::DataInvalid`] so silent wrap cannot
//! produce misleading aggregated values. A Decimal `sum` whose result no
//! longer fits the declared precision yields a NULL cell, matching Java
//! `DecimalUtils.add` / `Decimal.fromBigDecimal` (which return null on
//! precision overflow rather than throwing).
//!
//! `min` / `max` extend to every ordered Paimon type: numerics, Decimal,
//! Date, Time, Timestamp, and Char/VarChar. Comparison is by native value
//! order (numeric for numbers, lexicographic for strings). Float NaN is
//! treated as greater than any other value, matching Java's
//! `Float.compare` / `Double.compare`.
//!
//! Reference: Java `FieldSumAgg`, `FieldProductAgg`, `FieldMinAgg`,
//! `FieldMaxAgg` under `org.apache.paimon.mergetree.compact.aggregate`.
//!
//! [`Error::DataInvalid`]: crate::Error::DataInvalid
use std::sync::Arc;
use arrow_array::builder::Decimal128Builder;
use arrow_array::{
Array, ArrayRef, Date32Array, Decimal128Array, Float32Array, Float64Array, Int16Array,
Int32Array, Int64Array, Int8Array, StringArray, Time32MillisecondArray,
TimestampMicrosecondArray, TimestampMillisecondArray, TimestampNanosecondArray,
};
use arrow_schema::TimeUnit;
use super::{unsupported_type_error, FieldAggregator};
use crate::spec::DataType;
// ---------------------------------------------------------------------------
// Sum
// ---------------------------------------------------------------------------
/// `sum` accumulator state, parameterized by the column's numeric kind.
#[derive(Debug)]
enum SumState {
I8(Option<i8>),
I16(Option<i16>),
I32(Option<i32>),
I64(Option<i64>),
F32(Option<f32>),
F64(Option<f64>),
Decimal128 {
precision: u8,
scale: i8,
acc: Option<i128>,
},
}
#[derive(Debug)]
pub(crate) struct SumAgg {
field_name: String,
state: SumState,
}
impl SumAgg {
pub(crate) fn new(field_name: &str, data_type: &DataType) -> crate::Result<Self> {
let state = match data_type {
DataType::TinyInt(_) => SumState::I8(None),
DataType::SmallInt(_) => SumState::I16(None),
DataType::Int(_) => SumState::I32(None),
DataType::BigInt(_) => SumState::I64(None),
DataType::Float(_) => SumState::F32(None),
DataType::Double(_) => SumState::F64(None),
DataType::Decimal(d) => SumState::Decimal128 {
precision: decimal_precision(d.precision(), field_name)?,
scale: decimal_scale(d.scale(), field_name)?,
acc: None,
},
other => return Err(unsupported_type_error("sum", field_name, other)),
};
Ok(Self {
field_name: field_name.to_string(),
state,
})
}
}
impl FieldAggregator for SumAgg {
fn name(&self) -> &'static str {
"sum"
}
fn reset(&mut self) {
match &mut self.state {
SumState::I8(acc) => *acc = None,
SumState::I16(acc) => *acc = None,
SumState::I32(acc) => *acc = None,
SumState::I64(acc) => *acc = None,
SumState::F32(acc) => *acc = None,
SumState::F64(acc) => *acc = None,
SumState::Decimal128 { acc, .. } => *acc = None,
}
}
fn agg(&mut self, array: &dyn Array, row_idx: usize) -> crate::Result<()> {
if array.is_null(row_idx) {
return Ok(());
}
match &mut self.state {
SumState::I8(acc) => {
let v = downcast::<Int8Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_add(v)
.ok_or_else(|| overflow_error("sum", &self.field_name))?,
});
}
SumState::I16(acc) => {
let v = downcast::<Int16Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_add(v)
.ok_or_else(|| overflow_error("sum", &self.field_name))?,
});
}
SumState::I32(acc) => {
let v = downcast::<Int32Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_add(v)
.ok_or_else(|| overflow_error("sum", &self.field_name))?,
});
}
SumState::I64(acc) => {
let v = downcast::<Int64Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_add(v)
.ok_or_else(|| overflow_error("sum", &self.field_name))?,
});
}
SumState::F32(acc) => {
let v = downcast::<Float32Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(acc.map_or(v, |prev| prev + v));
}
SumState::F64(acc) => {
let v = downcast::<Float64Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(acc.map_or(v, |prev| prev + v));
}
SumState::Decimal128 { acc, .. } => {
let v = downcast::<Decimal128Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_add(v)
.ok_or_else(|| overflow_error("sum", &self.field_name))?,
});
}
}
Ok(())
}
fn result(&self) -> crate::Result<ArrayRef> {
Ok(match &self.state {
SumState::I8(acc) => Arc::new(Int8Array::from(vec![*acc])),
SumState::I16(acc) => Arc::new(Int16Array::from(vec![*acc])),
SumState::I32(acc) => Arc::new(Int32Array::from(vec![*acc])),
SumState::I64(acc) => Arc::new(Int64Array::from(vec![*acc])),
SumState::F32(acc) => Arc::new(Float32Array::from(vec![*acc])),
SumState::F64(acc) => Arc::new(Float64Array::from(vec![*acc])),
SumState::Decimal128 {
precision,
scale,
acc,
} => {
// Java parity: `DecimalUtils.add` -> `Decimal.fromBigDecimal`
// returns null when the summed value no longer fits the
// declared precision, so an overflowing sum yields a NULL cell
// rather than a silently out-of-range Decimal.
let fitted = acc.filter(|v| decimal_fits_precision(*v, *precision));
decimal_array(*precision, *scale, fitted, "sum", &self.field_name)?
}
})
}
}
// ---------------------------------------------------------------------------
// Product
// ---------------------------------------------------------------------------
#[derive(Debug)]
enum ProductState {
I8(Option<i8>),
I16(Option<i16>),
I32(Option<i32>),
I64(Option<i64>),
F32(Option<f32>),
F64(Option<f64>),
// DECIMAL `product` is intentionally rejected at construction (see
// `ProductAgg::new`); add a variant here when the BigDecimal-style
// scale handling lands.
}
#[derive(Debug)]
pub(crate) struct ProductAgg {
field_name: String,
state: ProductState,
}
impl ProductAgg {
pub(crate) fn new(field_name: &str, data_type: &DataType) -> crate::Result<Self> {
let state = match data_type {
DataType::TinyInt(_) => ProductState::I8(None),
DataType::SmallInt(_) => ProductState::I16(None),
DataType::Int(_) => ProductState::I32(None),
DataType::BigInt(_) => ProductState::I64(None),
DataType::Float(_) => ProductState::F32(None),
DataType::Double(_) => ProductState::F64(None),
// Decimal `product` would need BigDecimal-style scale rebasing
// (multiply raw i128, then divide by 10^scale, with precision
// checks). The basic mode does not implement that yet, so we
// reject DECIMAL columns explicitly rather than silently produce
// a scale-shifted result.
DataType::Decimal(_) => {
return Err(crate::Error::ConfigInvalid {
message: format!(
"Aggregate function 'product' on DECIMAL field '{field_name}' is not \
supported in the basic mode; use a BIGINT/DOUBLE column or wait for a \
follow-up commit that adds Decimal product semantics aligned with Java \
BigDecimal"
),
});
}
other => return Err(unsupported_type_error("product", field_name, other)),
};
Ok(Self {
field_name: field_name.to_string(),
state,
})
}
}
impl FieldAggregator for ProductAgg {
fn name(&self) -> &'static str {
"product"
}
fn reset(&mut self) {
match &mut self.state {
ProductState::I8(acc) => *acc = None,
ProductState::I16(acc) => *acc = None,
ProductState::I32(acc) => *acc = None,
ProductState::I64(acc) => *acc = None,
ProductState::F32(acc) => *acc = None,
ProductState::F64(acc) => *acc = None,
}
}
fn agg(&mut self, array: &dyn Array, row_idx: usize) -> crate::Result<()> {
if array.is_null(row_idx) {
return Ok(());
}
match &mut self.state {
ProductState::I8(acc) => {
let v = downcast::<Int8Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_mul(v)
.ok_or_else(|| overflow_error("product", &self.field_name))?,
});
}
ProductState::I16(acc) => {
let v = downcast::<Int16Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_mul(v)
.ok_or_else(|| overflow_error("product", &self.field_name))?,
});
}
ProductState::I32(acc) => {
let v = downcast::<Int32Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_mul(v)
.ok_or_else(|| overflow_error("product", &self.field_name))?,
});
}
ProductState::I64(acc) => {
let v = downcast::<Int64Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(match *acc {
None => v,
Some(prev) => prev
.checked_mul(v)
.ok_or_else(|| overflow_error("product", &self.field_name))?,
});
}
ProductState::F32(acc) => {
let v = downcast::<Float32Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(acc.map_or(v, |prev| prev * v));
}
ProductState::F64(acc) => {
let v = downcast::<Float64Array>(array, &self.field_name)?.value(row_idx);
*acc = Some(acc.map_or(v, |prev| prev * v));
}
}
Ok(())
}
fn result(&self) -> crate::Result<ArrayRef> {
Ok(match &self.state {
ProductState::I8(acc) => Arc::new(Int8Array::from(vec![*acc])),
ProductState::I16(acc) => Arc::new(Int16Array::from(vec![*acc])),
ProductState::I32(acc) => Arc::new(Int32Array::from(vec![*acc])),
ProductState::I64(acc) => Arc::new(Int64Array::from(vec![*acc])),
ProductState::F32(acc) => Arc::new(Float32Array::from(vec![*acc])),
ProductState::F64(acc) => Arc::new(Float64Array::from(vec![*acc])),
})
}
}
// ---------------------------------------------------------------------------
// Min / Max — generic comparator-driven implementation
// ---------------------------------------------------------------------------
/// `min` / `max` accumulator state. Each variant stores `Option<T>` where
/// `None` means "no non-null value seen yet for the current group".
#[derive(Debug)]
enum MinMaxState {
I8(Option<i8>),
I16(Option<i16>),
I32(Option<i32>),
I64(Option<i64>),
F32(Option<f32>),
F64(Option<f64>),
Decimal128 {
precision: u8,
scale: i8,
acc: Option<i128>,
},
Date32(Option<i32>),
/// Paimon `TIME` is encoded as Arrow `Time32(Millisecond)` regardless of
/// declared precision, so a single accumulator variant suffices.
Time32Ms(Option<i32>),
Timestamp {
unit: TimeUnit,
acc: Option<i64>,
},
Utf8(Option<String>),
}
fn make_minmax_state(
field_name: &str,
data_type: &DataType,
op: &str,
) -> crate::Result<MinMaxState> {
Ok(match data_type {
DataType::TinyInt(_) => MinMaxState::I8(None),
DataType::SmallInt(_) => MinMaxState::I16(None),
DataType::Int(_) => MinMaxState::I32(None),
DataType::BigInt(_) => MinMaxState::I64(None),
DataType::Float(_) => MinMaxState::F32(None),
DataType::Double(_) => MinMaxState::F64(None),
DataType::Decimal(d) => MinMaxState::Decimal128 {
precision: decimal_precision(d.precision(), field_name)?,
scale: decimal_scale(d.scale(), field_name)?,
acc: None,
},
DataType::Date(_) => MinMaxState::Date32(None),
DataType::Time(_) => MinMaxState::Time32Ms(None),
DataType::Timestamp(t) => MinMaxState::Timestamp {
unit: timestamp_time_unit(t.precision())?,
acc: None,
},
DataType::Char(_) | DataType::VarChar(_) => MinMaxState::Utf8(None),
other => return Err(unsupported_type_error(op, field_name, other)),
})
}
fn timestamp_time_unit(precision: u32) -> crate::Result<TimeUnit> {
match precision {
0..=3 => Ok(TimeUnit::Millisecond),
4..=6 => Ok(TimeUnit::Microsecond),
7..=9 => Ok(TimeUnit::Nanosecond),
other => Err(crate::Error::Unsupported {
message: format!("Unsupported TIMESTAMP precision {other} for min/max aggregator"),
}),
}
}
fn agg_minmax(
state: &mut MinMaxState,
array: &dyn Array,
row_idx: usize,
field_name: &str,
keep_smaller: bool,
) -> crate::Result<()> {
if array.is_null(row_idx) {
return Ok(());
}
macro_rules! update_primitive {
($acc:expr, $ty:ty) => {{
let v = downcast::<$ty>(array, field_name)?.value(row_idx);
*$acc = Some(match *$acc {
None => v,
Some(prev) => {
if (keep_smaller && v < prev) || (!keep_smaller && v > prev) {
v
} else {
prev
}
}
});
}};
}
macro_rules! update_float {
($acc:expr, $ty:ty) => {{
let v = downcast::<$ty>(array, field_name)?.value(row_idx);
// Match Java `Float.compare` / `Double.compare`, which order NaN
// greater than any other value (including +Infinity). Using
// `total_cmp` makes that ordering explicit and deterministic.
*$acc = Some(match *$acc {
None => v,
Some(prev) => {
let cmp = v.total_cmp(&prev);
let take_new = if keep_smaller {
cmp.is_lt()
} else {
cmp.is_gt()
};
if take_new {
v
} else {
prev
}
}
});
}};
}
match state {
MinMaxState::I8(acc) => update_primitive!(acc, Int8Array),
MinMaxState::I16(acc) => update_primitive!(acc, Int16Array),
MinMaxState::I32(acc) => update_primitive!(acc, Int32Array),
MinMaxState::I64(acc) => update_primitive!(acc, Int64Array),
MinMaxState::F32(acc) => update_float!(acc, Float32Array),
MinMaxState::F64(acc) => update_float!(acc, Float64Array),
MinMaxState::Decimal128 { acc, .. } => update_primitive!(acc, Decimal128Array),
MinMaxState::Date32(acc) => update_primitive!(acc, Date32Array),
MinMaxState::Time32Ms(acc) => update_primitive!(acc, Time32MillisecondArray),
MinMaxState::Timestamp { unit, acc } => match unit {
TimeUnit::Millisecond => update_primitive!(acc, TimestampMillisecondArray),
TimeUnit::Microsecond => update_primitive!(acc, TimestampMicrosecondArray),
TimeUnit::Nanosecond => update_primitive!(acc, TimestampNanosecondArray),
other => {
return Err(crate::Error::DataInvalid {
message: format!(
"Timestamp with unit {other:?} not expected for field '{field_name}'"
),
source: None,
});
}
},
MinMaxState::Utf8(acc) => {
let v = downcast::<StringArray>(array, field_name)?.value(row_idx);
*acc = Some(match acc.take() {
None => v.to_string(),
Some(prev) => {
let take_new = if keep_smaller {
v < prev.as_str()
} else {
v > prev.as_str()
};
if take_new {
v.to_string()
} else {
prev
}
}
});
}
}
Ok(())
}
fn minmax_result(state: &MinMaxState, agg_name: &str, field_name: &str) -> crate::Result<ArrayRef> {
Ok(match state {
MinMaxState::I8(acc) => Arc::new(Int8Array::from(vec![*acc])),
MinMaxState::I16(acc) => Arc::new(Int16Array::from(vec![*acc])),
MinMaxState::I32(acc) => Arc::new(Int32Array::from(vec![*acc])),
MinMaxState::I64(acc) => Arc::new(Int64Array::from(vec![*acc])),
MinMaxState::F32(acc) => Arc::new(Float32Array::from(vec![*acc])),
MinMaxState::F64(acc) => Arc::new(Float64Array::from(vec![*acc])),
MinMaxState::Decimal128 {
precision,
scale,
acc,
} => decimal_array(*precision, *scale, *acc, agg_name, field_name)?,
MinMaxState::Date32(acc) => Arc::new(Date32Array::from(vec![*acc])),
MinMaxState::Time32Ms(acc) => Arc::new(Time32MillisecondArray::from(vec![*acc])),
MinMaxState::Timestamp { unit, acc } => match unit {
TimeUnit::Millisecond => Arc::new(TimestampMillisecondArray::from(vec![*acc])),
TimeUnit::Microsecond => Arc::new(TimestampMicrosecondArray::from(vec![*acc])),
TimeUnit::Nanosecond => Arc::new(TimestampNanosecondArray::from(vec![*acc])),
other => {
return Err(crate::Error::DataInvalid {
message: format!(
"Timestamp with unit {other:?} not expected for field '{field_name}'"
),
source: None,
});
}
},
MinMaxState::Utf8(acc) => Arc::new(StringArray::from(vec![acc.clone()])),
})
}
fn reset_minmax(state: &mut MinMaxState) {
match state {
MinMaxState::I8(acc) => *acc = None,
MinMaxState::I16(acc) => *acc = None,
MinMaxState::I32(acc) => *acc = None,
MinMaxState::I64(acc) => *acc = None,
MinMaxState::F32(acc) => *acc = None,
MinMaxState::F64(acc) => *acc = None,
MinMaxState::Decimal128 { acc, .. } => *acc = None,
MinMaxState::Date32(acc) => *acc = None,
MinMaxState::Time32Ms(acc) => *acc = None,
MinMaxState::Timestamp { acc, .. } => *acc = None,
MinMaxState::Utf8(acc) => *acc = None,
}
}
#[derive(Debug)]
pub(crate) struct MinAgg {
field_name: String,
state: MinMaxState,
}
impl MinAgg {
pub(crate) fn new(field_name: &str, data_type: &DataType) -> crate::Result<Self> {
Ok(Self {
field_name: field_name.to_string(),
state: make_minmax_state(field_name, data_type, "min")?,
})
}
}
impl FieldAggregator for MinAgg {
fn name(&self) -> &'static str {
"min"
}
fn reset(&mut self) {
reset_minmax(&mut self.state);
}
fn agg(&mut self, array: &dyn Array, row_idx: usize) -> crate::Result<()> {
agg_minmax(&mut self.state, array, row_idx, &self.field_name, true)
}
fn result(&self) -> crate::Result<ArrayRef> {
minmax_result(&self.state, "min", &self.field_name)
}
}
#[derive(Debug)]
pub(crate) struct MaxAgg {
field_name: String,
state: MinMaxState,
}
impl MaxAgg {
pub(crate) fn new(field_name: &str, data_type: &DataType) -> crate::Result<Self> {
Ok(Self {
field_name: field_name.to_string(),
state: make_minmax_state(field_name, data_type, "max")?,
})
}
}
impl FieldAggregator for MaxAgg {
fn name(&self) -> &'static str {
"max"
}
fn reset(&mut self) {
reset_minmax(&mut self.state);
}
fn agg(&mut self, array: &dyn Array, row_idx: usize) -> crate::Result<()> {
agg_minmax(&mut self.state, array, row_idx, &self.field_name, false)
}
fn result(&self) -> crate::Result<ArrayRef> {
minmax_result(&self.state, "max", &self.field_name)
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn downcast<'a, T: Array + 'static>(
array: &'a dyn Array,
field_name: &str,
) -> crate::Result<&'a T> {
array
.as_any()
.downcast_ref::<T>()
.ok_or_else(|| crate::Error::DataInvalid {
message: format!(
"Aggregate column '{field_name}' received Arrow array of unexpected \
type {:?}; expected {}",
array.data_type(),
std::any::type_name::<T>()
),
source: None,
})
}
fn decimal_precision(precision: u32, field_name: &str) -> crate::Result<u8> {
u8::try_from(precision).map_err(|_| crate::Error::Unsupported {
message: format!(
"Decimal precision {precision} on field '{field_name}' exceeds u8 (Arrow limit)"
),
})
}
fn decimal_scale(scale: u32, field_name: &str) -> crate::Result<i8> {
i8::try_from(scale as i32).map_err(|_| crate::Error::Unsupported {
message: format!(
"Decimal scale {scale} on field '{field_name}' is out of i8 range (Arrow limit)"
),
})
}
fn overflow_error(agg_name: &str, field_name: &str) -> crate::Error {
crate::Error::DataInvalid {
message: format!("Aggregate function '{agg_name}' overflowed on field '{field_name}'"),
source: None,
}
}
/// Whether `value` (an unscaled Decimal128 raw value) fits within `precision`
/// decimal digits, i.e. `|value| < 10^precision`. Decimal128 precision is at
/// most 38, so `10^precision` always fits in `u128`; the `checked_pow` guard
/// degrades to "fits" only for impossible precisions.
fn decimal_fits_precision(value: i128, precision: u8) -> bool {
10u128
.checked_pow(precision as u32)
.map(|limit| value.unsigned_abs() < limit)
.unwrap_or(true)
}
fn decimal_array(
precision: u8,
scale: i8,
value: Option<i128>,
agg_name: &str,
field_name: &str,
) -> crate::Result<ArrayRef> {
let mut builder = Decimal128Builder::with_capacity(1)
.with_precision_and_scale(precision, scale)
.map_err(|e| crate::Error::DataInvalid {
message: format!(
"Aggregate function '{agg_name}' failed to build Decimal128 array for \
field '{field_name}': {e}"
),
source: Some(Box::new(e)),
})?;
match value {
Some(v) => builder.append_value(v),
None => builder.append_null(),
}
Ok(Arc::new(builder.finish()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{
BigIntType, CharType, DateType, DecimalType, DoubleType, FloatType, IntType, SmallIntType,
TimeType, TimestampType, TinyIntType, VarCharType,
};
use arrow_array::builder::Decimal128Builder;
fn sum_agg(dt: DataType) -> SumAgg {
SumAgg::new("v", &dt).unwrap()
}
fn min_agg(dt: DataType) -> MinAgg {
MinAgg::new("v", &dt).unwrap()
}
fn max_agg(dt: DataType) -> MaxAgg {
MaxAgg::new("v", &dt).unwrap()
}
fn collect_i32(arr: ArrayRef) -> Option<i32> {
let a = arr.as_any().downcast_ref::<Int32Array>().unwrap();
if a.is_null(0) {
None
} else {
Some(a.value(0))
}
}
fn collect_i64(arr: ArrayRef) -> Option<i64> {
let a = arr.as_any().downcast_ref::<Int64Array>().unwrap();
if a.is_null(0) {
None
} else {
Some(a.value(0))
}
}
fn collect_string(arr: ArrayRef) -> Option<String> {
let a = arr.as_any().downcast_ref::<StringArray>().unwrap();
if a.is_null(0) {
None
} else {
Some(a.value(0).to_string())
}
}
#[test]
fn test_sum_int_aggregates_non_null_values() {
let mut agg = sum_agg(DataType::Int(IntType::new()));
let arr = Int32Array::from(vec![Some(1), None, Some(2), Some(3)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i32(agg.result().unwrap()), Some(6));
}
#[test]
fn test_sum_all_null_returns_null() {
let mut agg = sum_agg(DataType::BigInt(BigIntType::new()));
let arr = Int64Array::from(vec![None::<i64>, None]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i64(agg.result().unwrap()), None);
}
#[test]
fn test_sum_rejects_overflow() {
let mut agg = sum_agg(DataType::Int(IntType::new()));
let arr = Int32Array::from(vec![i32::MAX, 1]);
agg.agg(&arr, 0).unwrap();
let err = agg.agg(&arr, 1).unwrap_err();
assert!(
matches!(err, crate::Error::DataInvalid { message, .. } if message.contains("overflowed"))
);
}
#[test]
fn test_sum_rejects_non_numeric_type() {
let err = SumAgg::new("v", &DataType::VarChar(VarCharType::new(255).unwrap())).unwrap_err();
assert!(matches!(err, crate::Error::ConfigInvalid { message } if message.contains("sum")));
}
#[test]
fn test_sum_reset_clears_state() {
let mut agg = sum_agg(DataType::Int(IntType::new()));
let arr = Int32Array::from(vec![Some(10)]);
agg.agg(&arr, 0).unwrap();
agg.reset();
assert_eq!(collect_i32(agg.result().unwrap()), None);
}
#[test]
fn test_sum_float_skips_null_and_handles_partial() {
let mut agg = sum_agg(DataType::Double(DoubleType::new()));
let arr = Float64Array::from(vec![Some(1.5), None, Some(2.5)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let a = agg.result().unwrap();
let v = a.as_any().downcast_ref::<Float64Array>().unwrap().value(0);
assert!((v - 4.0).abs() < 1e-9);
}
#[test]
fn test_sum_decimal_aggregates_raw_values() {
let mut agg = sum_agg(DataType::Decimal(DecimalType::new(10, 2).unwrap()));
let mut b = Decimal128Builder::with_capacity(2)
.with_precision_and_scale(10, 2)
.unwrap();
b.append_value(100); // 1.00
b.append_value(250); // 2.50
let arr = b.finish();
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let out = agg.result().unwrap();
let out_arr = out.as_any().downcast_ref::<Decimal128Array>().unwrap();
assert_eq!(out_arr.value(0), 350); // 3.50
}
#[test]
fn test_sum_decimal_in_range_keeps_value() {
// DECIMAL(3,2): 1.23 + 4.56 = 5.79 (raw 579) still fits precision 3.
let mut agg = sum_agg(DataType::Decimal(DecimalType::new(3, 2).unwrap()));
let mut b = Decimal128Builder::with_capacity(2)
.with_precision_and_scale(3, 2)
.unwrap();
b.append_value(123); // 1.23
b.append_value(456); // 4.56
let arr = b.finish();
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let out = agg.result().unwrap();
let out_arr = out.as_any().downcast_ref::<Decimal128Array>().unwrap();
assert!(!out_arr.is_null(0));
assert_eq!(out_arr.value(0), 579); // 5.79
}
#[test]
fn test_sum_decimal_precision_overflow_yields_null() {
// DECIMAL(3,2) tops out at 9.99 (raw 999). 9.99 + 0.01 = 10.00 (raw
// 1000) needs precision 4, so the sum no longer fits and must become
// NULL — matching Java `Decimal.fromBigDecimal` returning null instead
// of persisting an out-of-range value.
let mut agg = sum_agg(DataType::Decimal(DecimalType::new(3, 2).unwrap()));
let mut b = Decimal128Builder::with_capacity(2)
.with_precision_and_scale(3, 2)
.unwrap();
b.append_value(999); // 9.99
b.append_value(1); // 0.01
let arr = b.finish();
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let out = agg.result().unwrap();
let out_arr = out.as_any().downcast_ref::<Decimal128Array>().unwrap();
assert!(
out_arr.is_null(0),
"precision-overflowing decimal sum must be NULL, got {}",
out_arr.value(0)
);
}
#[test]
fn test_product_int_aggregates() {
let mut agg = ProductAgg::new("v", &DataType::Int(IntType::new())).unwrap();
let arr = Int32Array::from(vec![Some(2), None, Some(3), Some(4)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i32(agg.result().unwrap()), Some(24));
}
#[test]
fn test_product_rejects_overflow() {
let mut agg = ProductAgg::new("v", &DataType::SmallInt(SmallIntType::new())).unwrap();
let arr = Int16Array::from(vec![i16::MAX, 2]);
agg.agg(&arr, 0).unwrap();
let err = agg.agg(&arr, 1).unwrap_err();
assert!(matches!(err, crate::Error::DataInvalid { .. }));
}
#[test]
fn test_product_all_null_returns_null() {
let mut agg = ProductAgg::new("v", &DataType::Int(IntType::new())).unwrap();
let arr = Int32Array::from(vec![None::<i32>, None]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i32(agg.result().unwrap()), None);
}
#[test]
fn test_product_rejects_decimal_until_scale_handling_lands() {
// DECIMAL multiplication needs BigDecimal-style scale rebasing; the
// basic mode rejects it explicitly instead of silently shifting the
// implied scale.
let err =
ProductAgg::new("v", &DataType::Decimal(DecimalType::new(10, 2).unwrap())).unwrap_err();
assert!(
matches!(err, crate::Error::ConfigInvalid { ref message } if message.contains("DECIMAL"))
);
}
#[test]
fn test_min_int_picks_smallest_skipping_null() {
let mut agg = min_agg(DataType::Int(IntType::new()));
let arr = Int32Array::from(vec![Some(3), None, Some(1), Some(2)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i32(agg.result().unwrap()), Some(1));
}
#[test]
fn test_max_string_picks_lex_largest() {
let mut agg = max_agg(DataType::Char(CharType::new(8).unwrap()));
let arr = StringArray::from(vec![Some("ant"), None, Some("zebra"), Some("bee")]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(
collect_string(agg.result().unwrap()),
Some("zebra".to_string())
);
}
#[test]
fn test_min_max_treat_nan_as_largest() {
// Match Java `Float.compare(NaN, x) > 0`: NaN is greater than every
// other value, so min skips it and max picks it.
let mut min = min_agg(DataType::Float(FloatType::new()));
let arr = Float32Array::from(vec![Some(f32::NAN), Some(1.0), Some(0.5)]);
for i in 0..arr.len() {
min.agg(&arr, i).unwrap();
}
let v = min.result().unwrap();
let v = v.as_any().downcast_ref::<Float32Array>().unwrap().value(0);
assert!((v - 0.5).abs() < 1e-6);
let mut max = max_agg(DataType::Float(FloatType::new()));
for i in 0..arr.len() {
max.agg(&arr, i).unwrap();
}
let v = max.result().unwrap();
let v = v.as_any().downcast_ref::<Float32Array>().unwrap().value(0);
assert!(v.is_nan(), "max should pick NaN, got {v}");
}
#[test]
fn test_min_max_all_null_returns_null() {
let mut agg = max_agg(DataType::Int(IntType::new()));
let arr = Int32Array::from(vec![None::<i32>, None]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
assert_eq!(collect_i32(agg.result().unwrap()), None);
}
#[test]
fn test_min_rejects_unsupported_type() {
// Boolean has no <, > defined for min/max in Paimon basic mode.
let err =
MinAgg::new("v", &DataType::Boolean(crate::spec::BooleanType::new())).unwrap_err();
assert!(matches!(err, crate::Error::ConfigInvalid { message } if message.contains("min")));
}
#[test]
fn test_min_max_date_and_timestamp_supported() {
// Date32
let mut agg = min_agg(DataType::Date(DateType::new()));
let arr = Date32Array::from(vec![Some(100), Some(50), Some(200)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let v = agg.result().unwrap();
let v = v.as_any().downcast_ref::<Date32Array>().unwrap().value(0);
assert_eq!(v, 50);
// Timestamp(6) → Microsecond
let mut agg = max_agg(DataType::Timestamp(TimestampType::new(6).unwrap()));
let arr =
TimestampMicrosecondArray::from(vec![Some(1_000_000), Some(2_000_000), Some(500_000)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let v = agg.result().unwrap();
let v = v
.as_any()
.downcast_ref::<TimestampMicrosecondArray>()
.unwrap()
.value(0);
assert_eq!(v, 2_000_000);
}
#[test]
fn test_min_max_time_supported() {
// Paimon `TIME` is always stored as Arrow Time32(Millisecond) by
// `paimon_type_to_arrow`, so milliseconds is the only carrier here.
let mut agg = min_agg(DataType::Time(TimeType::new(3).unwrap()));
let arr = Time32MillisecondArray::from(vec![Some(60_000), Some(30_000), Some(90_000)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let v = agg.result().unwrap();
let v = v
.as_any()
.downcast_ref::<Time32MillisecondArray>()
.unwrap()
.value(0);
assert_eq!(v, 30_000);
}
#[test]
fn test_tinyint_sum_supported() {
let mut agg = sum_agg(DataType::TinyInt(TinyIntType::new()));
let arr = Int8Array::from(vec![Some(1i8), Some(2)]);
for i in 0..arr.len() {
agg.agg(&arr, i).unwrap();
}
let out = agg.result().unwrap();
let v = out.as_any().downcast_ref::<Int8Array>().unwrap().value(0);
assert_eq!(v, 3);
}
}