use crate::hyperloglog::{HLL_HASH_STATE, HyperLogLog, NUM_REGISTERS, count_from_hashes};
use arrow::array::{
Array, ArrayRef, BinaryArray, BinaryBuilder, BooleanArray, PrimitiveArray,
UInt64Array,
};
use arrow::buffer::NullBuffer;
use arrow::datatypes::{
ArrowPrimitiveType, DataType, Date32Type, Date64Type, Decimal32Type, Decimal64Type,
Decimal128Type, Decimal256Type, DurationMicrosecondType, DurationMillisecondType,
DurationNanosecondType, DurationSecondType, Field, FieldRef, Int32Type, Int64Type,
IntervalDayTimeType, IntervalMonthDayNanoType, IntervalUnit, IntervalYearMonthType,
Time32MillisecondType, Time32SecondType, Time64MicrosecondType, Time64NanosecondType,
TimeUnit, TimestampMicrosecondType, TimestampMillisecondType,
TimestampNanosecondType, TimestampSecondType, UInt32Type, UInt64Type,
};
use datafusion_common::ScalarValue;
use datafusion_common::hash_utils::create_hashes;
use datafusion_common::{
DataFusionError, Result, downcast_value, internal_datafusion_err, internal_err,
not_impl_err,
};
use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
use datafusion_expr::utils::format_state_name;
use datafusion_expr::{
Accumulator, AggregateUDFImpl, Documentation, EmitTo, GroupsAccumulator, Signature,
Volatility,
};
use datafusion_functions_aggregate_common::aggregate::count_distinct::{
Bitmap65536DistinctCountAccumulator, Bitmap65536DistinctCountAccumulatorI16,
BoolArray256DistinctCountAccumulator, BoolArray256DistinctCountAccumulatorI8,
BooleanDistinctCountAccumulator,
};
use datafusion_functions_aggregate_common::aggregate::groups_accumulator::nulls::filter_to_nulls;
use datafusion_functions_aggregate_common::noop_accumulator::NoopAccumulator;
use datafusion_macros::user_doc;
use std::fmt::{Debug, Formatter};
use std::hash::Hash;
use std::mem::{size_of, size_of_val};
use std::sync::Arc;
make_udaf_expr_and_func!(
ApproxDistinct,
approx_distinct,
expression,
"approximate number of distinct input values",
approx_distinct_udaf
);
impl<T: Hash + ?Sized> From<&HyperLogLog<T>> for ScalarValue {
fn from(v: &HyperLogLog<T>) -> ScalarValue {
let values = v.as_ref().to_vec();
ScalarValue::Binary(Some(values))
}
}
impl<T: Hash + ?Sized> TryFrom<&[u8]> for HyperLogLog<T> {
type Error = DataFusionError;
fn try_from(v: &[u8]) -> Result<HyperLogLog<T>> {
let arr: [u8; 16384] = v.try_into().map_err(|_| {
internal_datafusion_err!("Impossibly got invalid binary array from states")
})?;
Ok(HyperLogLog::<T>::new_with_registers(arr))
}
}
impl<T: Hash + ?Sized> TryFrom<&ScalarValue> for HyperLogLog<T> {
type Error = DataFusionError;
fn try_from(v: &ScalarValue) -> Result<HyperLogLog<T>> {
if let ScalarValue::Binary(Some(slice)) = v {
slice.as_slice().try_into()
} else {
internal_err!(
"Impossibly got invalid scalar value while converting to HyperLogLog"
)
}
}
}
#[derive(Debug)]
struct ApproxDistinctBitmapWrapper<A: Accumulator> {
inner: A,
}
impl<A: Accumulator> Accumulator for ApproxDistinctBitmapWrapper<A> {
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
self.inner.update_batch(values)
}
fn evaluate(&mut self) -> Result<ScalarValue> {
match self.inner.evaluate()? {
ScalarValue::Int64(Some(v)) => Ok(ScalarValue::UInt64(Some(v as u64))),
other => internal_err!("unexpected: {other}"),
}
}
fn size(&self) -> usize {
self.inner.size()
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
self.inner.state()
}
fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
self.inner.merge_batch(states)
}
}
#[derive(Debug)]
struct HLLAccumulator {
hll: HyperLogLog<u8>,
hashes: Vec<u64>,
}
impl HLLAccumulator {
pub fn new() -> Self {
Self {
hll: HyperLogLog::new(),
hashes: Vec::new(),
}
}
}
impl Accumulator for HLLAccumulator {
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
let array = values[0].as_ref();
self.hashes.clear();
self.hashes.resize(array.len(), 0);
create_hashes([array], &HLL_HASH_STATE, &mut self.hashes)?;
match array.logical_nulls() {
None => {
for &hash in &self.hashes {
self.hll.add_hashed(hash);
}
}
Some(nulls) => {
for row in 0..array.len() {
if nulls.is_valid(row) {
self.hll.add_hashed(self.hashes[row]);
}
}
}
}
Ok(())
}
fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
assert_eq!(1, states.len(), "expect only 1 element in the states");
let binary_array = downcast_value!(states[0], BinaryArray);
for v in binary_array.iter() {
let v = v.ok_or_else(|| {
internal_datafusion_err!("Impossibly got empty binary array from states")
})?;
let other = v.try_into()?;
self.hll.merge(&other);
}
Ok(())
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
let value = ScalarValue::from(&self.hll);
Ok(vec![value])
}
fn evaluate(&mut self) -> Result<ScalarValue> {
Ok(ScalarValue::UInt64(Some(self.hll.count() as u64)))
}
fn size(&self) -> usize {
size_of_val(self) + self.hashes.capacity() * size_of::<u64>()
}
}
#[derive(Debug)]
struct NumericHLLAccumulator<T>
where
T: ArrowPrimitiveType,
T::Native: Hash,
{
hll: HyperLogLog<T::Native>,
}
impl<T> NumericHLLAccumulator<T>
where
T: ArrowPrimitiveType,
T::Native: Hash,
{
pub fn new() -> Self {
Self {
hll: HyperLogLog::new(),
}
}
}
impl<T> Accumulator for NumericHLLAccumulator<T>
where
T: ArrowPrimitiveType + Debug,
T::Native: Hash,
{
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
let array: &PrimitiveArray<T> = downcast_value!(values[0], PrimitiveArray, T);
self.hll.extend(array.into_iter().flatten());
Ok(())
}
fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
assert_eq!(1, states.len(), "expect only 1 element in the states");
let binary_array = downcast_value!(states[0], BinaryArray);
for v in binary_array.iter() {
let v = v.ok_or_else(|| {
internal_datafusion_err!("Impossibly got empty binary array from states")
})?;
let other = v.try_into()?;
self.hll.merge(&other);
}
Ok(())
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
let value = ScalarValue::from(&self.hll);
Ok(vec![value])
}
fn evaluate(&mut self) -> Result<ScalarValue> {
Ok(ScalarValue::UInt64(Some(self.hll.count() as u64)))
}
fn size(&self) -> usize {
size_of_val(self)
}
}
const SPARSE_LIMIT: usize = 256;
#[derive(Clone, Debug)]
enum GroupHll {
Sparse(Vec<u64>),
Dense(Box<HyperLogLog<u8>>),
}
impl Default for GroupHll {
fn default() -> Self {
GroupHll::Sparse(Vec::new())
}
}
fn fold_sparse_to_hll(hashes: &[u64]) -> HyperLogLog<u8> {
let mut hll = HyperLogLog::<u8>::new();
for &h in hashes {
hll.add_hashed(h);
}
hll
}
impl GroupHll {
#[inline]
fn add_hash(&mut self, hash: u64) -> isize {
match self {
GroupHll::Dense(hll) => {
hll.add_hashed(hash);
0
}
GroupHll::Sparse(v) => {
let cap_before = v.capacity();
v.push(hash);
if v.len() >= 2 * SPARSE_LIMIT {
return self.compact_or_promote(cap_before);
}
((v.capacity() - cap_before) * size_of::<u64>()) as isize
}
}
}
#[cold]
fn compact_or_promote(&mut self, cap_before: usize) -> isize {
let GroupHll::Sparse(v) = self else {
return 0;
};
v.sort_unstable();
v.dedup();
if v.len() > SPARSE_LIMIT {
*self = GroupHll::Dense(Box::new(fold_sparse_to_hll(v)));
(NUM_REGISTERS as isize) - ((cap_before * size_of::<u64>()) as isize)
} else {
((v.capacity() - cap_before) * size_of::<u64>()) as isize
}
}
fn merge_serialized(&mut self, bytes: &[u8]) -> Result<isize> {
if bytes.is_empty() {
return Ok(0);
}
if bytes.len() == NUM_REGISTERS {
let other: HyperLogLog<u8> = bytes.try_into()?;
Ok(self.merge_dense(&other))
} else {
if !bytes.len().is_multiple_of(size_of::<u64>()) {
return internal_err!(
"approx_distinct: malformed sparse state: length {} is not a multiple of {}",
bytes.len(),
size_of::<u64>()
);
}
if bytes.len() > SPARSE_LIMIT * size_of::<u64>() {
return internal_err!(
"approx_distinct: malformed sparse state: length {} exceeds sparse limit {}",
bytes.len(),
SPARSE_LIMIT * size_of::<u64>()
);
}
let mut delta = 0;
for chunk in bytes.chunks_exact(size_of::<u64>()) {
let h = u64::from_le_bytes(chunk.try_into().unwrap());
delta += self.add_hash(h);
}
Ok(delta)
}
}
fn merge_dense(&mut self, other: &HyperLogLog<u8>) -> isize {
match self {
GroupHll::Dense(hll) => {
hll.merge(other);
0
}
GroupHll::Sparse(v) => {
let cap_before = v.capacity();
let mut hll = other.clone();
for &h in v.iter() {
hll.add_hashed(h);
}
*self = GroupHll::Dense(Box::new(hll));
(NUM_REGISTERS as isize) - ((cap_before * size_of::<u64>()) as isize)
}
}
}
fn count(&self) -> u64 {
match self {
GroupHll::Dense(hll) => hll.count() as u64,
GroupHll::Sparse(v) => count_from_hashes(v) as u64,
}
}
fn heap_bytes(&self) -> usize {
match self {
GroupHll::Sparse(v) => v.capacity() * size_of::<u64>(),
GroupHll::Dense(_) => NUM_REGISTERS,
}
}
fn serialize(&mut self, scratch: &mut Vec<u8>) {
scratch.clear();
match self {
GroupHll::Dense(hll) => {
let registers: &[u8] = (**hll).as_ref();
scratch.extend_from_slice(registers);
}
GroupHll::Sparse(v) => {
v.sort_unstable();
v.dedup();
if v.len() > SPARSE_LIMIT {
scratch.extend_from_slice(fold_sparse_to_hll(v).as_ref());
} else {
for &h in v.iter() {
scratch.extend_from_slice(&h.to_le_bytes());
}
}
}
}
}
}
struct HllGroupsAccumulator {
groups: Vec<GroupHll>,
allocated_bytes: usize,
hashes: Vec<u64>,
}
impl HllGroupsAccumulator {
fn new() -> Self {
Self {
groups: Vec::new(),
allocated_bytes: 0,
hashes: Vec::new(),
}
}
#[inline]
fn ensure_groups(&mut self, total_num_groups: usize) {
if total_num_groups > self.groups.len() {
self.groups.resize_with(total_num_groups, GroupHll::default);
}
}
#[inline]
fn apply_delta(&mut self, delta: isize) {
self.allocated_bytes =
(self.allocated_bytes as isize).saturating_add(delta).max(0) as usize;
}
}
impl GroupsAccumulator for HllGroupsAccumulator {
fn update_batch(
&mut self,
values: &[ArrayRef],
group_indices: &[usize],
opt_filter: Option<&BooleanArray>,
total_num_groups: usize,
) -> Result<()> {
self.ensure_groups(total_num_groups);
let array = values[0].as_ref();
self.hashes.clear();
self.hashes.resize(array.len(), 0);
create_hashes([array], &HLL_HASH_STATE, &mut self.hashes)?;
let mut delta: isize = 0;
let filter_nulls = opt_filter.map(filter_to_nulls);
let value_nulls = array.logical_nulls();
let combined_nulls =
NullBuffer::union(filter_nulls.as_ref(), value_nulls.as_ref());
match combined_nulls {
None => {
for (row, &hash) in self.hashes.iter().enumerate() {
delta += self.groups[group_indices[row]].add_hash(hash);
}
}
Some(nulls) => {
for row in nulls.valid_indices() {
delta += self.groups[group_indices[row]].add_hash(self.hashes[row]);
}
}
}
self.apply_delta(delta);
Ok(())
}
fn merge_batch(
&mut self,
values: &[ArrayRef],
group_indices: &[usize],
total_num_groups: usize,
) -> Result<()> {
self.ensure_groups(total_num_groups);
let states = downcast_value!(values[0], BinaryArray);
let mut delta: isize = 0;
for (row, &group_index) in group_indices.iter().enumerate() {
if states.is_valid(row) {
delta += self.groups[group_index].merge_serialized(states.value(row))?;
}
}
self.apply_delta(delta);
Ok(())
}
fn evaluate(&mut self, emit_to: EmitTo) -> Result<ArrayRef> {
let groups = emit_to.take_needed(&mut self.groups);
let mut freed = 0;
let counts: UInt64Array = groups
.iter()
.map(|g| {
freed += g.heap_bytes();
Some(g.count())
})
.collect();
self.allocated_bytes = self.allocated_bytes.saturating_sub(freed);
Ok(Arc::new(counts))
}
fn state(&mut self, emit_to: EmitTo) -> Result<Vec<ArrayRef>> {
let mut groups = emit_to.take_needed(&mut self.groups);
let mut builder = BinaryBuilder::new();
let mut scratch: Vec<u8> = Vec::new();
let mut freed = 0;
for g in groups.iter_mut() {
freed += g.heap_bytes();
g.serialize(&mut scratch);
builder.append_value(&scratch);
}
self.allocated_bytes = self.allocated_bytes.saturating_sub(freed);
Ok(vec![Arc::new(builder.finish())])
}
fn convert_to_state(
&self,
values: &[ArrayRef],
opt_filter: Option<&BooleanArray>,
) -> Result<Vec<ArrayRef>> {
assert_eq!(values.len(), 1, "single argument to convert_to_state");
let array = values[0].as_ref();
let mut hashes = vec![0; array.len()];
create_hashes([array], &HLL_HASH_STATE, &mut hashes)?;
let filter_nulls = opt_filter.map(filter_to_nulls);
let value_nulls = array.logical_nulls();
let combined_nulls =
NullBuffer::union(filter_nulls.as_ref(), value_nulls.as_ref());
let mut builder = BinaryBuilder::new();
let mut scratch = Vec::new();
for (row, hash) in hashes.into_iter().enumerate() {
if combined_nulls
.as_ref()
.is_none_or(|nulls| nulls.is_valid(row))
{
scratch.clear();
scratch.extend_from_slice(&hash.to_le_bytes());
builder.append_value(&scratch);
} else {
builder.append_value([]);
}
}
Ok(vec![Arc::new(builder.finish())])
}
fn size(&self) -> usize {
self.groups.capacity() * size_of::<GroupHll>()
+ self.allocated_bytes
+ self.hashes.capacity() * size_of::<u64>()
}
}
impl Debug for ApproxDistinct {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApproxDistinct")
.field("name", &self.name())
.field("signature", &self.signature)
.finish()
}
}
impl Default for ApproxDistinct {
fn default() -> Self {
Self::new()
}
}
#[user_doc(
doc_section(label = "Approximate Functions"),
description = "Returns the approximate number of distinct input values calculated using the HyperLogLog algorithm.",
syntax_example = "approx_distinct(expression)",
sql_example = r#"```sql
> SELECT approx_distinct(column_name) FROM table_name;
+-----------------------------------+
| approx_distinct(column_name) |
+-----------------------------------+
| 42 |
+-----------------------------------+
```"#,
standard_argument(name = "expression",)
)]
#[derive(PartialEq, Eq, Hash)]
pub struct ApproxDistinct {
signature: Signature,
}
impl ApproxDistinct {
pub fn new() -> Self {
Self {
signature: Signature::any(1, Volatility::Immutable),
}
}
}
#[cold]
fn get_fixed_domain_approx_accumulator(
data_type: &DataType,
) -> Result<Box<dyn Accumulator>> {
match data_type {
DataType::Boolean => Ok(Box::new(ApproxDistinctBitmapWrapper {
inner: BooleanDistinctCountAccumulator::new(),
})),
DataType::UInt8 => Ok(Box::new(ApproxDistinctBitmapWrapper {
inner: BoolArray256DistinctCountAccumulator::new(),
})),
DataType::Int8 => Ok(Box::new(ApproxDistinctBitmapWrapper {
inner: BoolArray256DistinctCountAccumulatorI8::new(),
})),
DataType::UInt16 => Ok(Box::new(ApproxDistinctBitmapWrapper {
inner: Bitmap65536DistinctCountAccumulator::new(),
})),
DataType::Int16 => Ok(Box::new(ApproxDistinctBitmapWrapper {
inner: Bitmap65536DistinctCountAccumulatorI16::new(),
})),
_ => internal_err!("unsupported small int type: {}", data_type),
}
}
#[cold]
fn get_fixed_domain_state_field(
name: &str,
data_type: &DataType,
) -> Result<Vec<FieldRef>> {
Ok(vec![
Field::new_list(
format_state_name(name, "approx_distinct"),
Field::new_list_field(data_type.clone(), true),
false,
)
.into(),
])
}
impl AggregateUDFImpl for ApproxDistinct {
fn name(&self) -> &str {
"approx_distinct"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _: &[DataType]) -> Result<DataType> {
Ok(DataType::UInt64)
}
fn default_value(&self, _data_type: &DataType) -> Result<ScalarValue> {
Ok(ScalarValue::UInt64(Some(0)))
}
fn is_nullable(&self) -> bool {
false
}
fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
let data_type = args.input_fields[0].data_type();
match data_type {
DataType::Null => Ok(vec![
Field::new(
format_state_name(args.name, self.name()),
DataType::Null,
true,
)
.into(),
]),
DataType::Boolean
| DataType::UInt8
| DataType::Int8
| DataType::UInt16
| DataType::Int16 => get_fixed_domain_state_field(args.name, data_type),
_ => Ok(vec![
Field::new(
format_state_name(args.name, "hll_registers"),
DataType::Binary,
false,
)
.into(),
]),
}
}
fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
let data_type = acc_args.expr_fields[0].data_type();
let accumulator: Box<dyn Accumulator> = match data_type {
DataType::Boolean
| DataType::UInt8
| DataType::Int8
| DataType::UInt16
| DataType::Int16 => {
return get_fixed_domain_approx_accumulator(data_type);
}
DataType::UInt32 => Box::new(NumericHLLAccumulator::<UInt32Type>::new()),
DataType::UInt64 => Box::new(NumericHLLAccumulator::<UInt64Type>::new()),
DataType::Int32 => Box::new(NumericHLLAccumulator::<Int32Type>::new()),
DataType::Int64 => Box::new(NumericHLLAccumulator::<Int64Type>::new()),
DataType::Date32 => Box::new(NumericHLLAccumulator::<Date32Type>::new()),
DataType::Date64 => Box::new(NumericHLLAccumulator::<Date64Type>::new()),
DataType::Time32(TimeUnit::Second) => {
Box::new(NumericHLLAccumulator::<Time32SecondType>::new())
}
DataType::Time32(TimeUnit::Millisecond) => {
Box::new(NumericHLLAccumulator::<Time32MillisecondType>::new())
}
DataType::Time64(TimeUnit::Microsecond) => {
Box::new(NumericHLLAccumulator::<Time64MicrosecondType>::new())
}
DataType::Time64(TimeUnit::Nanosecond) => {
Box::new(NumericHLLAccumulator::<Time64NanosecondType>::new())
}
DataType::Timestamp(TimeUnit::Second, _) => {
Box::new(NumericHLLAccumulator::<TimestampSecondType>::new())
}
DataType::Timestamp(TimeUnit::Millisecond, _) => {
Box::new(NumericHLLAccumulator::<TimestampMillisecondType>::new())
}
DataType::Timestamp(TimeUnit::Microsecond, _) => {
Box::new(NumericHLLAccumulator::<TimestampMicrosecondType>::new())
}
DataType::Timestamp(TimeUnit::Nanosecond, _) => {
Box::new(NumericHLLAccumulator::<TimestampNanosecondType>::new())
}
DataType::Interval(IntervalUnit::YearMonth) => {
Box::new(NumericHLLAccumulator::<IntervalYearMonthType>::new())
}
DataType::Interval(IntervalUnit::DayTime) => {
Box::new(NumericHLLAccumulator::<IntervalDayTimeType>::new())
}
DataType::Interval(IntervalUnit::MonthDayNano) => {
Box::new(NumericHLLAccumulator::<IntervalMonthDayNanoType>::new())
}
DataType::Decimal32(_, _) => {
Box::new(NumericHLLAccumulator::<Decimal32Type>::new())
}
DataType::Decimal64(_, _) => {
Box::new(NumericHLLAccumulator::<Decimal64Type>::new())
}
DataType::Decimal128(_, _) => {
Box::new(NumericHLLAccumulator::<Decimal128Type>::new())
}
DataType::Decimal256(_, _) => {
Box::new(NumericHLLAccumulator::<Decimal256Type>::new())
}
DataType::Duration(TimeUnit::Second) => {
Box::new(NumericHLLAccumulator::<DurationSecondType>::new())
}
DataType::Duration(TimeUnit::Millisecond) => {
Box::new(NumericHLLAccumulator::<DurationMillisecondType>::new())
}
DataType::Duration(TimeUnit::Microsecond) => {
Box::new(NumericHLLAccumulator::<DurationMicrosecondType>::new())
}
DataType::Duration(TimeUnit::Nanosecond) => {
Box::new(NumericHLLAccumulator::<DurationNanosecondType>::new())
}
DataType::Utf8
| DataType::LargeUtf8
| DataType::Utf8View
| DataType::Binary
| DataType::BinaryView
| DataType::FixedSizeBinary(_)
| DataType::List(_)
| DataType::LargeList(_)
| DataType::FixedSizeList(_, _)
| DataType::ListView(_)
| DataType::LargeListView(_)
| DataType::Map(_, _)
| DataType::Struct(_)
| DataType::Union(_, _)
| DataType::LargeBinary => Box::new(HLLAccumulator::new()),
DataType::Null => {
Box::new(NoopAccumulator::new(ScalarValue::UInt64(Some(0))))
}
other => {
return not_impl_err!(
"Support for 'approx_distinct' for data type {other} is not implemented"
);
}
};
Ok(accumulator)
}
fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool {
is_hll_groups_type(args.expr_fields[0].data_type())
}
fn create_groups_accumulator(
&self,
args: AccumulatorArgs,
) -> Result<Box<dyn GroupsAccumulator>> {
let data_type = args.expr_fields[0].data_type();
if is_hll_groups_type(data_type) {
Ok(Box::new(HllGroupsAccumulator::new()))
} else {
not_impl_err!(
"GroupsAccumulator for 'approx_distinct' is not implemented for data type {data_type}"
)
}
}
fn documentation(&self) -> Option<&Documentation> {
self.doc()
}
}
fn is_hll_groups_type(data_type: &DataType) -> bool {
matches!(
data_type,
DataType::UInt32
| DataType::UInt64
| DataType::Int32
| DataType::Int64
| DataType::Date32
| DataType::Date64
| DataType::Time32(TimeUnit::Second)
| DataType::Time32(TimeUnit::Millisecond)
| DataType::Time64(TimeUnit::Microsecond)
| DataType::Time64(TimeUnit::Nanosecond)
| DataType::Timestamp(TimeUnit::Second, _)
| DataType::Timestamp(TimeUnit::Millisecond, _)
| DataType::Timestamp(TimeUnit::Microsecond, _)
| DataType::Timestamp(TimeUnit::Nanosecond, _)
| DataType::Interval(IntervalUnit::YearMonth)
| DataType::Interval(IntervalUnit::DayTime)
| DataType::Interval(IntervalUnit::MonthDayNano)
| DataType::Decimal32(_, _)
| DataType::Decimal64(_, _)
| DataType::Decimal128(_, _)
| DataType::Decimal256(_, _)
| DataType::Duration(_)
| DataType::Utf8
| DataType::LargeUtf8
| DataType::Utf8View
| DataType::Binary
| DataType::BinaryView
| DataType::FixedSizeBinary(_)
| DataType::LargeBinary
| DataType::List(_)
| DataType::LargeList(_)
| DataType::FixedSizeList(_, _)
| DataType::ListView(_)
| DataType::LargeListView(_)
| DataType::Map(_, _)
| DataType::Struct(_)
| DataType::Union(_, _)
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::hash::BuildHasher;
#[cfg(not(feature = "force_hash_collisions"))]
mod real_hash_test {
use super::*;
use arrow::array::{
AsArray, Decimal32Array, Decimal64Array, Decimal128Array, Decimal256Array,
Int64Array, IntervalDayTimeArray, IntervalMonthDayNanoArray,
IntervalYearMonthArray, StringViewArray,
};
use arrow::datatypes::{IntervalDayTime, IntervalMonthDayNano, i256};
use std::sync::Arc;
const LONG: &str = "this string is definitely longer than twelve bytes";
fn distinct_count(acc: &mut HLLAccumulator) -> u64 {
match acc.evaluate().unwrap() {
ScalarValue::UInt64(Some(v)) => v,
other => panic!("unexpected evaluate result: {other:?}"),
}
}
fn assert_count_numerical_acc_and_group_acc<T>(array: ArrayRef, expected: u64)
where
T: ArrowPrimitiveType + Debug,
T::Native: Hash,
{
assert!(
is_hll_groups_type(array.data_type()),
"{} should be groups-capable",
array.data_type()
);
let mut acc = NumericHLLAccumulator::<T>::new();
acc.update_batch(&[Arc::clone(&array)]).unwrap();
let per_group_count = match acc.evaluate().unwrap() {
ScalarValue::UInt64(Some(v)) => v,
other => panic!("unexpected evaluate result: {other:?}"),
};
let group_indices = vec![0usize; array.len()];
let mut acc = HllGroupsAccumulator::new();
acc.update_batch(std::slice::from_ref(&array), &group_indices, None, 1)
.unwrap();
let groups_count = acc
.evaluate(EmitTo::All)
.unwrap()
.as_any()
.downcast_ref::<UInt64Array>()
.unwrap()
.value(0);
assert_eq!(
per_group_count,
groups_count,
"paths disagree for {}",
array.data_type()
);
assert_eq!(
per_group_count,
expected,
"wrong count for {}",
array.data_type()
);
}
#[test]
fn decimal_support_numerical_acc_and_group_acc() {
let decimal_32: ArrayRef = Arc::new(
Decimal32Array::from(vec![
1i32,
2,
2,
3,
3,
3,
0,
0,
123_456_789,
999_999_999,
999_999_999,
])
.with_precision_and_scale(9, 2)
.unwrap(),
);
assert_count_numerical_acc_and_group_acc::<Decimal32Type>(decimal_32, 6);
let decimal_64: ArrayRef = Arc::new(
Decimal64Array::from(vec![
1i64,
2,
2,
3,
3,
3,
0,
0,
1_234_567_890_123,
9_999_999_999_999,
9_999_999_999_999,
])
.with_precision_and_scale(18, 2)
.unwrap(),
);
assert_count_numerical_acc_and_group_acc::<Decimal64Type>(decimal_64, 6);
let decimal_128: ArrayRef = Arc::new(
Decimal128Array::from(vec![
1i128,
2,
2,
3,
3,
3,
0,
0,
1_234_567_890,
9_999_999_999,
9_999_999_999,
])
.with_precision_and_scale(38, 2)
.unwrap(),
);
assert_count_numerical_acc_and_group_acc::<Decimal128Type>(decimal_128, 6);
let big_256_a =
i256::from_string("123456789012345678901234567890123456").unwrap();
let big_256_b =
i256::from_string("987654321098765432109876543210987654").unwrap();
let decimal_256: ArrayRef = Arc::new(
Decimal256Array::from(vec![
i256::from_i128(1),
i256::from_i128(2),
i256::from_i128(2),
i256::from_i128(3),
i256::from_i128(3),
i256::from_i128(3),
i256::from_i128(0),
i256::from_i128(0),
big_256_a,
big_256_b,
big_256_b,
])
.with_precision_and_scale(40, 2)
.unwrap(),
);
assert_count_numerical_acc_and_group_acc::<Decimal256Type>(decimal_256, 6);
}
#[test]
fn interval_support_numerical_acc_and_group_acc() {
let year_month: ArrayRef =
Arc::new(IntervalYearMonthArray::from(vec![1, 2, 2, 3, 3, 3, 0, 0]));
assert_count_numerical_acc_and_group_acc::<IntervalYearMonthType>(
year_month, 4,
);
let day_time: ArrayRef = Arc::new(IntervalDayTimeArray::from(vec![
IntervalDayTime::new(1, 0),
IntervalDayTime::new(1, 0),
IntervalDayTime::new(1, 5),
IntervalDayTime::new(2, 0),
]));
assert_count_numerical_acc_and_group_acc::<IntervalDayTimeType>(day_time, 3);
let month_day_nano: ArrayRef =
Arc::new(IntervalMonthDayNanoArray::from(vec![
IntervalMonthDayNano::new(1, 0, 0),
IntervalMonthDayNano::new(1, 0, 0),
IntervalMonthDayNano::new(1, 0, 5),
IntervalMonthDayNano::new(0, 2, 0),
IntervalMonthDayNano::new(0, 0, 0),
]));
assert_count_numerical_acc_and_group_acc::<IntervalMonthDayNanoType>(
month_day_nano,
4,
);
}
#[test]
fn update_batch_nullable_filter_excludes_null_filter_rows() {
let values: ArrayRef = Arc::new(Int64Array::from(vec![1i64, 2, 3, 4, 5]));
let filter =
BooleanArray::from(vec![Some(true), None, Some(false), None, Some(true)]);
let mut acc = HllGroupsAccumulator::new();
let group_indices = vec![0usize; 5];
acc.update_batch(&[values], &group_indices, Some(&filter), 1)
.unwrap();
let result = acc.evaluate(EmitTo::All).unwrap();
let counts = result.as_any().downcast_ref::<UInt64Array>().unwrap();
let expected = reference_count(&[h(1), h(5)]);
assert_eq!(counts.value(0), expected);
}
#[test]
fn groups_convert_to_state_roundtrips_through_merge() {
let values: ArrayRef = Arc::new(Int64Array::from(vec![
Some(1),
Some(2),
Some(2),
None,
Some(3),
]));
let filter = BooleanArray::from(vec![
Some(true),
Some(true),
Some(true),
Some(true),
None,
]);
let group_indices = vec![0usize, 1, 0, 1, 0];
let mut direct = HllGroupsAccumulator::new();
direct
.update_batch(
std::slice::from_ref(&values),
&group_indices,
Some(&filter),
2,
)
.unwrap();
let direct = direct
.evaluate(EmitTo::All)
.unwrap()
.as_any()
.downcast_ref::<UInt64Array>()
.unwrap()
.clone();
let converter = HllGroupsAccumulator::new();
let state = converter
.convert_to_state(std::slice::from_ref(&values), Some(&filter))
.unwrap();
assert_eq!(state[0].null_count(), 0);
let mut merged = HllGroupsAccumulator::new();
merged.merge_batch(&state, &group_indices, 2).unwrap();
let merged = merged
.evaluate(EmitTo::All)
.unwrap()
.as_any()
.downcast_ref::<UInt64Array>()
.unwrap()
.clone();
assert_eq!(direct, merged);
}
#[test]
fn groups_convert_to_state_preserves_empty_and_filtered_rows() {
let converter = HllGroupsAccumulator::new();
let empty_values: ArrayRef =
Arc::new(Int64Array::from(Vec::<Option<i64>>::new()));
let state = converter
.convert_to_state(std::slice::from_ref(&empty_values), None)
.unwrap();
assert_eq!(state[0].len(), 0);
assert_eq!(state[0].null_count(), 0);
let values: ArrayRef =
Arc::new(Int64Array::from(vec![Some(1), Some(2), None]));
let filter = BooleanArray::from(vec![Some(false), None, Some(false)]);
let group_indices = vec![0usize, 1, 0];
let state = converter
.convert_to_state(std::slice::from_ref(&values), Some(&filter))
.unwrap();
assert_eq!(state[0].len(), values.len());
assert_eq!(state[0].null_count(), 0);
let state = state[0].as_any().downcast_ref::<BinaryArray>().unwrap();
for row in 0..state.len() {
assert_eq!(state.value(row), b"");
}
let mut merged = HllGroupsAccumulator::new();
merged
.merge_batch(&[Arc::new(state.clone())], &group_indices, 2)
.unwrap();
let result = merged
.evaluate(EmitTo::All)
.unwrap()
.as_any()
.downcast_ref::<UInt64Array>()
.unwrap()
.clone();
assert_eq!(result, UInt64Array::from(vec![0, 0]));
}
#[test]
fn utf8view_groups_short_string_hashed_consistently_across_batches() {
let batch1: ArrayRef = Arc::new(StringViewArray::from(vec!["aaa", "bbb"]));
assert!(batch1.as_string_view().data_buffers().is_empty());
let batch2: ArrayRef = Arc::new(StringViewArray::from(vec!["aaa", LONG]));
assert!(!batch2.as_string_view().data_buffers().is_empty());
let group_indices = vec![0usize, 0];
let mut acc = HllGroupsAccumulator::new();
acc.update_batch(&[batch1], &group_indices, None, 1)
.unwrap();
acc.update_batch(&[batch2], &group_indices, None, 1)
.unwrap();
let result = acc.evaluate(EmitTo::All).unwrap();
let counts = result.as_any().downcast_ref::<UInt64Array>().unwrap();
assert_eq!(counts.value(0), 3);
}
#[test]
fn utf8view_acc_split_batches_match_single_mixed_batch() {
let mixed: ArrayRef =
Arc::new(StringViewArray::from(vec!["aaa", "bbb", LONG, "aaa"]));
let mut acc_single = HLLAccumulator::new();
acc_single.update_batch(&[mixed]).unwrap();
let inline_only: ArrayRef =
Arc::new(StringViewArray::from(vec!["aaa", "bbb"]));
let with_buffer: ArrayRef =
Arc::new(StringViewArray::from(vec!["aaa", LONG]));
assert!(inline_only.as_string_view().data_buffers().is_empty());
assert!(!with_buffer.as_string_view().data_buffers().is_empty());
let mut acc_split = HLLAccumulator::new();
acc_split.update_batch(&[inline_only]).unwrap();
acc_split.update_batch(&[with_buffer]).unwrap();
assert_eq!(
distinct_count(&mut acc_single),
distinct_count(&mut acc_split)
);
assert_eq!(distinct_count(&mut acc_single), 3);
}
}
fn h(v: u64) -> u64 {
HLL_HASH_STATE.hash_one(v)
}
fn reference_count(hashes: &[u64]) -> u64 {
let mut hll = HyperLogLog::<u8>::new();
for &hash in hashes {
hll.add_hashed(hash);
}
hll.count() as u64
}
fn serialize(g: &mut GroupHll) -> Vec<u8> {
let mut buf = Vec::new();
g.serialize(&mut buf);
buf
}
#[test]
fn sparse_stays_sparse_for_small_groups() {
let mut g = GroupHll::default();
let hashes: Vec<u64> = (0..50).map(h).collect();
for &hash in &hashes {
g.add_hash(hash);
}
for &hash in &hashes {
g.add_hash(hash);
}
assert!(
matches!(g, GroupHll::Sparse(_)),
"small group must be sparse"
);
assert_eq!(g.count(), reference_count(&hashes));
let serialized = serialize(&mut g);
assert!(serialized.len() < NUM_REGISTERS);
assert!(serialized.len() <= SPARSE_LIMIT * size_of::<u64>());
}
#[test]
fn promotes_to_dense_for_large_groups() {
let mut g = GroupHll::default();
let hashes: Vec<u64> = (0..(SPARSE_LIMIT as u64 * 4)).map(h).collect();
for &hash in &hashes {
g.add_hash(hash);
}
assert!(matches!(g, GroupHll::Dense(_)), "large group must be dense");
assert_eq!(g.count(), reference_count(&hashes));
}
#[test]
fn serialize_then_merge_roundtrips() {
for n in [0u64, 10, SPARSE_LIMIT as u64 * 4] {
let hashes: Vec<u64> = (0..n).map(h).collect();
let mut src = GroupHll::default();
for &hash in &hashes {
src.add_hash(hash);
}
let bytes = serialize(&mut src);
let mut dst = GroupHll::default();
dst.merge_serialized(&bytes).unwrap();
assert_eq!(dst.count(), reference_count(&hashes), "n = {n}");
}
}
#[test]
fn sparse_limit_group_serializes_as_mergeable_sparse_state() {
let hashes: Vec<u64> = (0..SPARSE_LIMIT as u64).map(h).collect();
let mut src = GroupHll::default();
for &hash in &hashes {
src.add_hash(hash);
}
assert!(matches!(src, GroupHll::Sparse(_)));
let bytes = serialize(&mut src);
assert_eq!(bytes.len(), SPARSE_LIMIT * size_of::<u64>());
let mut dst = GroupHll::default();
dst.merge_serialized(&bytes).unwrap();
assert_eq!(dst.count(), reference_count(&hashes));
}
#[test]
fn medium_sparse_group_serializes_as_mergeable_dense_state() {
let n = SPARSE_LIMIT as u64 + 44;
let hashes: Vec<u64> = (0..n).map(h).collect();
let mut src = GroupHll::default();
for &hash in &hashes {
src.add_hash(hash);
}
assert!(
matches!(src, GroupHll::Sparse(_)),
"group should not promote during update before the compaction threshold"
);
let bytes = serialize(&mut src);
assert_eq!(bytes.len(), NUM_REGISTERS);
let mut dst = GroupHll::default();
dst.merge_serialized(&bytes).unwrap();
assert_eq!(dst.count(), reference_count(&hashes));
}
#[test]
fn merge_combines_disjoint_groups() {
let left: Vec<u64> = (0..100).map(h).collect();
let right: Vec<u64> = (100..(SPARSE_LIMIT as u64 * 4)).map(h).collect();
let all: Vec<u64> = left.iter().chain(right.iter()).copied().collect();
let mut a = GroupHll::default();
for &hash in &left {
a.add_hash(hash);
}
let mut b = GroupHll::default();
for &hash in &right {
b.add_hash(hash);
}
let b_bytes = serialize(&mut b);
a.merge_serialized(&b_bytes).unwrap();
assert_eq!(a.count(), reference_count(&all));
}
#[test]
fn empty_group_counts_zero() {
let mut g = GroupHll::default();
assert_eq!(g.count(), 0);
let bytes = serialize(&mut g);
assert!(bytes.is_empty());
let mut dst = GroupHll::default();
dst.merge_serialized(&bytes).unwrap();
assert_eq!(dst.count(), 0);
}
}