#![expect(rustdoc::private_intra_doc_links)]
use std::borrow::Cow;
use std::sync::Arc;
use super::{DisplayAs, ExecutionPlanProperties, PlanProperties};
use crate::aggregates::{
aggregate_stream::AggregateStream,
grouped_hash_stream::GroupedHashAggregateStream,
grouped_topk_stream::GroupedTopKAggregateStream,
hash_stream::{FinalHashAggregateStream, PartialHashAggregateStream},
ordered_final_stream::OrderedFinalAggregateStream,
ordered_partial_stream::OrderedPartialAggregateStream,
partial_reduce_stream::PartialReduceHashAggregateStream,
single_stream::SingleHashAggregateStream,
};
use crate::execution_plan::{
CardinalityEffect, EmissionType, plan_contains_expression_id,
};
use crate::filter_pushdown::{
ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
FilterPushdownPropagation,
};
use crate::metrics::{ExecutionPlanMetricsSet, MetricsSet};
use crate::statistics::{ChildStats, StatisticsArgs};
use crate::{ChildrenPropertiesMode, ReplaceChildrenOptions, validate_child_count};
use crate::{
DisplayFormatType, Distribution, ExecutionPlan, InputDistributionRequirements,
InputOrderMode, SendableRecordBatchStream, Statistics,
};
use datafusion_common::config::ConfigOptions;
use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use arrow::array::{ArrayRef, UInt8Array, UInt16Array, UInt32Array, UInt64Array};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::record_batch::RecordBatch;
use arrow_schema::FieldRef;
use datafusion_common::stats::Precision;
use datafusion_common::tree_node::TreeNodeRecursion;
use datafusion_common::{
ColumnStatistics, Constraint, Constraints, Result, ScalarValue,
assert_eq_or_internal_err, internal_err, not_impl_err,
};
use datafusion_execution::TaskContext;
use datafusion_execution::memory_pool::MemoryLimit;
use datafusion_expr::{Accumulator, Aggregate};
use datafusion_physical_expr::aggregate::AggregateFunctionExpr;
use datafusion_physical_expr::equivalence::ProjectionMapping;
use datafusion_physical_expr::expressions::{Column, DynamicFilterPhysicalExpr, lit};
use datafusion_physical_expr::{
ConstExpr, EquivalenceProperties, physical_exprs_contains,
};
use datafusion_physical_expr_common::physical_expr::{PhysicalExpr, fmt_sql};
use datafusion_physical_expr_common::sort_expr::{
LexOrdering, LexRequirement, OrderingRequirements, PhysicalSortRequirement,
};
use datafusion_expr::utils::AggregateOrderSensitivity;
use datafusion_physical_expr_common::utils::evaluate_expressions_to_arrays;
use itertools::Itertools;
use topk::hash_table::is_supported_hash_key_type;
use topk::heap::is_supported_heap_type;
mod aggregate_hash_table;
mod aggregate_stream;
pub mod group_values;
mod grouped_hash_stream;
mod grouped_topk_stream;
mod hash_stream;
pub mod order;
mod ordered_final_stream;
mod ordered_partial_stream;
mod partial_reduce_stream;
mod single_stream;
mod skip_partial;
mod topk;
pub fn topk_types_supported(key_type: &DataType, value_type: &DataType) -> bool {
is_supported_hash_key_type(key_type) && is_supported_heap_type(value_type)
}
const AGGREGATION_HASH_SEED: datafusion_common::hash_utils::RandomState =
datafusion_common::hash_utils::RandomState::with_seed(15395726432021054657);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum AggregateInputMode {
Raw,
Partial,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum AggregateOutputMode {
Partial,
Final,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum AggregateMode {
Partial,
Final,
FinalPartitioned,
Single,
SinglePartitioned,
PartialReduce,
}
impl AggregateMode {
pub fn input_mode(&self) -> AggregateInputMode {
match self {
AggregateMode::Partial
| AggregateMode::Single
| AggregateMode::SinglePartitioned => AggregateInputMode::Raw,
AggregateMode::Final
| AggregateMode::FinalPartitioned
| AggregateMode::PartialReduce => AggregateInputMode::Partial,
}
}
pub fn output_mode(&self) -> AggregateOutputMode {
match self {
AggregateMode::Final
| AggregateMode::FinalPartitioned
| AggregateMode::Single
| AggregateMode::SinglePartitioned => AggregateOutputMode::Final,
AggregateMode::Partial | AggregateMode::PartialReduce => {
AggregateOutputMode::Partial
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct PhysicalGroupBy {
expr: Vec<(Arc<dyn PhysicalExpr>, String)>,
null_expr: Vec<(Arc<dyn PhysicalExpr>, String)>,
groups: Vec<Vec<bool>>,
has_grouping_set: bool,
}
impl PhysicalGroupBy {
pub fn new(
expr: Vec<(Arc<dyn PhysicalExpr>, String)>,
null_expr: Vec<(Arc<dyn PhysicalExpr>, String)>,
groups: Vec<Vec<bool>>,
has_grouping_set: bool,
) -> Self {
Self {
expr,
null_expr,
groups,
has_grouping_set,
}
}
pub fn new_single(expr: Vec<(Arc<dyn PhysicalExpr>, String)>) -> Self {
let num_exprs = expr.len();
Self {
expr,
null_expr: vec![],
groups: vec![vec![false; num_exprs]],
has_grouping_set: false,
}
}
pub fn exprs_nullable(&self) -> Vec<bool> {
let mut exprs_nullable = vec![false; self.expr.len()];
for group in self.groups.iter() {
group.iter().enumerate().for_each(|(index, is_null)| {
if *is_null {
exprs_nullable[index] = true;
}
})
}
exprs_nullable
}
pub fn is_true_no_grouping(&self) -> bool {
self.is_empty() && !self.has_grouping_set
}
pub fn expr(&self) -> &[(Arc<dyn PhysicalExpr>, String)] {
&self.expr
}
pub fn null_expr(&self) -> &[(Arc<dyn PhysicalExpr>, String)] {
&self.null_expr
}
pub fn groups(&self) -> &[Vec<bool>] {
&self.groups
}
pub fn has_grouping_set(&self) -> bool {
self.has_grouping_set
}
pub fn is_empty(&self) -> bool {
self.expr.is_empty()
}
pub fn is_single(&self) -> bool {
!self.has_grouping_set
}
pub fn input_exprs(&self) -> Vec<Arc<dyn PhysicalExpr>> {
self.expr
.iter()
.map(|(expr, _alias)| Arc::clone(expr))
.collect()
}
fn num_output_exprs(&self) -> usize {
let mut num_exprs = self.expr.len();
if self.has_grouping_set {
num_exprs += 1
}
num_exprs
}
pub fn output_exprs(&self) -> Vec<Arc<dyn PhysicalExpr>> {
let num_output_exprs = self.num_output_exprs();
let mut output_exprs = Vec::with_capacity(num_output_exprs);
output_exprs.extend(
self.expr
.iter()
.enumerate()
.take(num_output_exprs)
.map(|(index, (_, name))| Arc::new(Column::new(name, index)) as _),
);
if self.has_grouping_set {
output_exprs.push(Arc::new(Column::new(
Aggregate::INTERNAL_GROUPING_ID,
self.expr.len(),
)) as _);
}
output_exprs
}
pub fn num_group_exprs(&self) -> usize {
self.expr.len() + usize::from(self.has_grouping_set)
}
fn grouping_id_data_type(&self) -> DataType {
Aggregate::grouping_id_type(self.expr.len(), max_duplicate_ordinal(&self.groups))
}
pub fn group_schema(&self, schema: &Schema) -> Result<SchemaRef> {
Ok(Arc::new(Schema::new(self.group_fields(schema)?)))
}
fn group_fields(&self, input_schema: &Schema) -> Result<Vec<FieldRef>> {
let mut fields = Vec::with_capacity(self.num_group_exprs());
for ((expr, name), group_expr_nullable) in
self.expr.iter().zip(self.exprs_nullable())
{
fields.push(
Field::new(
name,
expr.data_type(input_schema)?,
group_expr_nullable || expr.nullable(input_schema)?,
)
.with_metadata(expr.return_field(input_schema)?.metadata().clone())
.into(),
);
}
if self.has_grouping_set {
fields.push(
Field::new(
Aggregate::INTERNAL_GROUPING_ID,
self.grouping_id_data_type(),
false,
)
.into(),
);
}
Ok(fields)
}
fn output_fields(&self, input_schema: &Schema) -> Result<Vec<FieldRef>> {
let mut fields = self.group_fields(input_schema)?;
fields.truncate(self.num_output_exprs());
Ok(fields)
}
pub fn as_final(&self) -> PhysicalGroupBy {
let expr: Vec<_> =
self.output_exprs()
.into_iter()
.zip(
self.expr.iter().map(|t| t.1.clone()).chain(std::iter::once(
Aggregate::INTERNAL_GROUPING_ID.to_owned(),
)),
)
.collect();
let num_exprs = expr.len();
let groups = if self.expr.is_empty() && !self.has_grouping_set {
vec![]
} else {
vec![vec![false; num_exprs]]
};
Self {
expr,
null_expr: vec![],
groups,
has_grouping_set: false,
}
}
}
impl PartialEq for PhysicalGroupBy {
fn eq(&self, other: &PhysicalGroupBy) -> bool {
self.expr.len() == other.expr.len()
&& self
.expr
.iter()
.zip(other.expr.iter())
.all(|((expr1, name1), (expr2, name2))| expr1.eq(expr2) && name1 == name2)
&& self.null_expr.len() == other.null_expr.len()
&& self
.null_expr
.iter()
.zip(other.null_expr.iter())
.all(|((expr1, name1), (expr2, name2))| expr1.eq(expr2) && name1 == name2)
&& self.groups == other.groups
&& self.has_grouping_set == other.has_grouping_set
}
}
#[expect(clippy::large_enum_variant)]
enum StreamType {
AggregateStream(AggregateStream),
PartialHash(PartialHashAggregateStream),
PartialReduceHash(PartialReduceHashAggregateStream),
FinalHash(FinalHashAggregateStream),
SingleHash(SingleHashAggregateStream),
OrderedPartialAggregate(OrderedPartialAggregateStream),
OrderedFinalAggregate(OrderedFinalAggregateStream),
GroupedHash(GroupedHashAggregateStream),
GroupedPriorityQueue(GroupedTopKAggregateStream),
}
impl From<StreamType> for SendableRecordBatchStream {
fn from(stream: StreamType) -> Self {
match stream {
StreamType::AggregateStream(stream) => Box::pin(stream),
StreamType::PartialHash(stream) => Box::pin(stream),
StreamType::PartialReduceHash(stream) => Box::pin(stream),
StreamType::FinalHash(stream) => Box::pin(stream),
StreamType::SingleHash(stream) => Box::pin(stream),
StreamType::OrderedPartialAggregate(stream) => stream.into_stream(),
StreamType::OrderedFinalAggregate(stream) => Box::pin(stream),
StreamType::GroupedHash(stream) => Box::pin(stream),
StreamType::GroupedPriorityQueue(stream) => Box::pin(stream),
}
}
}
#[derive(Debug, Clone)]
struct AggrDynFilter {
filter: Arc<DynamicFilterPhysicalExpr>,
supported_accumulators_info: Vec<PerAccumulatorDynFilter>,
}
#[derive(Debug, Clone)]
struct PerAccumulatorDynFilter {
aggr_type: DynamicFilterAggregateType,
aggr_index: usize,
shared_bound: Arc<Mutex<ScalarValue>>,
}
#[derive(Debug, Clone)]
enum DynamicFilterAggregateType {
Min,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LimitOptions {
pub limit: usize,
pub descending: Option<bool>,
}
impl LimitOptions {
pub fn new(limit: usize) -> Self {
Self {
limit,
descending: None,
}
}
pub fn new_with_order(limit: usize, descending: bool) -> Self {
Self {
limit,
descending: Some(descending),
}
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn descending(&self) -> Option<bool> {
self.descending
}
}
#[derive(Debug, Clone)]
pub struct AggregateExec {
mode: AggregateMode,
group_by: Arc<PhysicalGroupBy>,
aggr_expr: Arc<[Arc<AggregateFunctionExpr>]>,
filter_expr: Arc<[Option<Arc<dyn PhysicalExpr>>]>,
limit_options: Option<LimitOptions>,
pub input: Arc<dyn ExecutionPlan>,
schema: SchemaRef,
pub input_schema: SchemaRef,
metrics: ExecutionPlanMetricsSet,
required_input_ordering: Option<OrderingRequirements>,
input_order_mode: InputOrderMode,
cache: Arc<PlanProperties>,
dynamic_filter: Option<Arc<AggrDynFilter>>,
}
impl AggregateExec {
pub fn with_new_aggr_exprs(
&self,
aggr_expr: impl Into<Arc<[Arc<AggregateFunctionExpr>]>>,
) -> Self {
Self {
aggr_expr: aggr_expr.into(),
required_input_ordering: self.required_input_ordering.clone(),
metrics: ExecutionPlanMetricsSet::new(),
input_order_mode: self.input_order_mode.clone(),
cache: Arc::clone(&self.cache),
mode: self.mode,
group_by: Arc::clone(&self.group_by),
filter_expr: Arc::clone(&self.filter_expr),
limit_options: self.limit_options,
input: Arc::clone(&self.input),
schema: Arc::clone(&self.schema),
input_schema: Arc::clone(&self.input_schema),
dynamic_filter: self.dynamic_filter.clone(),
}
}
pub fn with_new_limit_options(&self, limit_options: Option<LimitOptions>) -> Self {
Self {
limit_options,
required_input_ordering: self.required_input_ordering.clone(),
metrics: ExecutionPlanMetricsSet::new(),
input_order_mode: self.input_order_mode.clone(),
cache: Arc::clone(&self.cache),
mode: self.mode,
group_by: Arc::clone(&self.group_by),
aggr_expr: Arc::clone(&self.aggr_expr),
filter_expr: Arc::clone(&self.filter_expr),
input: Arc::clone(&self.input),
schema: Arc::clone(&self.schema),
input_schema: Arc::clone(&self.input_schema),
dynamic_filter: self.dynamic_filter.clone(),
}
}
pub fn cache(&self) -> &PlanProperties {
&self.cache
}
pub fn try_new(
mode: AggregateMode,
group_by: impl Into<Arc<PhysicalGroupBy>>,
aggr_expr: Vec<Arc<AggregateFunctionExpr>>,
filter_expr: Vec<Option<Arc<dyn PhysicalExpr>>>,
input: Arc<dyn ExecutionPlan>,
input_schema: SchemaRef,
) -> Result<Self> {
let group_by = group_by.into();
let schema = create_schema(&input.schema(), &group_by, &aggr_expr, mode)?;
let schema = Arc::new(schema);
AggregateExec::try_new_with_schema(
mode,
group_by,
aggr_expr,
filter_expr,
input,
input_schema,
schema,
)
}
fn try_new_with_schema(
mode: AggregateMode,
group_by: impl Into<Arc<PhysicalGroupBy>>,
mut aggr_expr: Vec<Arc<AggregateFunctionExpr>>,
filter_expr: impl Into<Arc<[Option<Arc<dyn PhysicalExpr>>]>>,
input: Arc<dyn ExecutionPlan>,
input_schema: SchemaRef,
schema: SchemaRef,
) -> Result<Self> {
let group_by = group_by.into();
let filter_expr = filter_expr.into();
assert_eq_or_internal_err!(
aggr_expr.len(),
filter_expr.len(),
"Inconsistent aggregate expr: {:?} and filter expr: {:?} for AggregateExec, their size should match",
aggr_expr,
filter_expr
);
let input_eq_properties = input.equivalence_properties();
let groupby_exprs = group_by.input_exprs();
let (new_sort_exprs, indices) =
input_eq_properties.find_longest_permutation(&groupby_exprs)?;
let mut new_requirements = new_sort_exprs
.into_iter()
.map(PhysicalSortRequirement::from)
.collect::<Vec<_>>();
let req = get_finer_aggregate_exprs_requirement(
&mut aggr_expr,
&group_by,
input_eq_properties,
&mode,
)?;
new_requirements.extend(req);
let required_input_ordering =
LexRequirement::new(new_requirements).map(OrderingRequirements::new_soft);
let indices: Vec<usize> = indices
.into_iter()
.filter(|idx| group_by.groups.iter().all(|group| !group[*idx]))
.collect();
let input_order_mode = if indices.len() == groupby_exprs.len()
&& !indices.is_empty()
&& group_by.groups.len() == 1
{
InputOrderMode::Sorted
} else if !indices.is_empty() {
InputOrderMode::PartiallySorted(indices)
} else {
InputOrderMode::Linear
};
let group_expr_mapping =
ProjectionMapping::try_new(group_by.expr.clone(), &input.schema())?;
let cache = Self::compute_properties(
&input,
Arc::clone(&schema),
&group_expr_mapping,
group_by.is_true_no_grouping(),
&mode,
&input_order_mode,
aggr_expr.as_ref(),
)?;
let mut exec = AggregateExec {
mode,
group_by,
aggr_expr: aggr_expr.into(),
filter_expr,
input,
schema,
input_schema,
metrics: ExecutionPlanMetricsSet::new(),
required_input_ordering,
limit_options: None,
input_order_mode,
cache: Arc::new(cache),
dynamic_filter: None,
};
exec.init_dynamic_filter();
Ok(exec)
}
pub fn mode(&self) -> &AggregateMode {
&self.mode
}
pub fn with_limit_options(mut self, limit_options: Option<LimitOptions>) -> Self {
self.limit_options = limit_options;
self
}
pub fn limit_options(&self) -> Option<LimitOptions> {
self.limit_options
}
pub fn group_expr(&self) -> &PhysicalGroupBy {
&self.group_by
}
pub fn output_group_expr(&self) -> Vec<Arc<dyn PhysicalExpr>> {
self.group_by.output_exprs()
}
pub fn aggr_expr(&self) -> &[Arc<AggregateFunctionExpr>] {
&self.aggr_expr
}
pub fn filter_expr(&self) -> &[Option<Arc<dyn PhysicalExpr>>] {
&self.filter_expr
}
#[deprecated(
since = "55.0.0",
note = "Use ExecutionPlan::dynamic_expressions_produced instead"
)]
pub fn dynamic_filter_expr(&self) -> Option<&Arc<DynamicFilterPhysicalExpr>> {
self.dynamic_filter.as_ref().map(|df| &df.filter)
}
pub fn with_dynamic_filter_expr(
mut self,
filter: Arc<DynamicFilterPhysicalExpr>,
) -> Result<Self> {
let Some(dyn_filter) = self.dynamic_filter.as_ref() else {
return internal_err!("Aggregate does not support dynamic filtering");
};
let cols = self.cols_for_dynamic_filter(&dyn_filter.supported_accumulators_info);
if cols.len() != filter.children().len() {
return internal_err!(
"Dynamic filter expression is incompatible with aggregate due to mismatched number of columns"
);
}
for (col, child) in cols.iter().zip(filter.children()) {
if !col.eq(child) {
return internal_err!(
"Dynamic filter expression is incompatible with aggregate due to mismatched column references {col} != {child}"
);
}
}
self.dynamic_filter = Some(Arc::new(AggrDynFilter {
filter,
supported_accumulators_info: dyn_filter.supported_accumulators_info.clone(),
}));
Ok(self)
}
pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
&self.input
}
pub fn input_schema(&self) -> SchemaRef {
Arc::clone(&self.input_schema)
}
fn execute_typed(
&self,
partition: usize,
context: &Arc<TaskContext>,
) -> Result<StreamType> {
if self.group_by.is_true_no_grouping() {
return Ok(StreamType::AggregateStream(AggregateStream::new(
self, context, partition,
)?));
}
if let Some(config) = self.limit_options
&& !self.is_unordered_unfiltered_group_by_distinct()
{
return Ok(StreamType::GroupedPriorityQueue(
GroupedTopKAggregateStream::new(self, context, partition, config.limit)?,
));
}
if context
.session_config()
.options()
.execution
.enable_migration_aggregate
{
if self.should_use_ordered_partial_aggregate_stream(context) {
return Ok(StreamType::OrderedPartialAggregate(
OrderedPartialAggregateStream::new(self, context, partition)?,
));
}
if self.should_use_partial_hash_stream(context) {
return Ok(StreamType::PartialHash(PartialHashAggregateStream::new(
self, context, partition,
)?));
}
if self.should_use_partial_reduce_hash_stream(context) {
return Ok(StreamType::PartialReduceHash(
PartialReduceHashAggregateStream::new(self, context, partition)?,
));
}
if self.should_use_ordered_final_aggregate_stream(context) {
return Ok(StreamType::OrderedFinalAggregate(
OrderedFinalAggregateStream::new(self, context, partition)?,
));
}
if self.should_use_final_hash_stream(context) {
return Ok(StreamType::FinalHash(FinalHashAggregateStream::new(
self, context, partition,
)?));
}
if self.should_use_single_hash_stream(context) {
return Ok(StreamType::SingleHash(SingleHashAggregateStream::new(
self, context, partition,
)?));
}
}
Ok(StreamType::GroupedHash(GroupedHashAggregateStream::new(
self, context, partition,
)?))
}
fn should_use_partial_hash_stream(&self, _context: &TaskContext) -> bool {
self.mode == AggregateMode::Partial
&& self.input_order_mode == InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
&& self.limit_options_supported_by_hash_stream()
}
fn should_use_ordered_partial_aggregate_stream(
&self,
_context: &TaskContext,
) -> bool {
self.mode == AggregateMode::Partial
&& self.input_order_mode != InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
&& self.limit_options_supported_by_hash_stream()
}
fn should_use_final_hash_stream(&self, _context: &TaskContext) -> bool {
matches!(
self.mode,
AggregateMode::Final | AggregateMode::FinalPartitioned
) && self.limit_options_supported_by_hash_stream()
&& self.input_order_mode == InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
}
fn should_use_partial_reduce_hash_stream(&self, context: &TaskContext) -> bool {
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
return false;
}
self.mode == AggregateMode::PartialReduce
&& self.limit_options.is_none()
&& self.input_order_mode == InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
}
fn should_use_single_hash_stream(&self, _context: &TaskContext) -> bool {
matches!(
self.mode,
AggregateMode::Single | AggregateMode::SinglePartitioned
) && self.limit_options.is_none()
&& self.input_order_mode == InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
}
fn should_use_ordered_final_aggregate_stream(&self, _context: &TaskContext) -> bool {
matches!(
self.mode,
AggregateMode::Final | AggregateMode::FinalPartitioned
) && self.limit_options_supported_by_hash_stream()
&& self.input_order_mode != InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
}
fn limit_options_supported_by_hash_stream(&self) -> bool {
self.limit_options.is_none() || self.is_unordered_unfiltered_group_by_distinct()
}
pub fn get_minmax_desc(&self) -> Option<(FieldRef, bool)> {
let agg_expr = self.aggr_expr.iter().exactly_one().ok()?;
agg_expr.get_minmax_desc()
}
pub fn is_unordered_unfiltered_group_by_distinct(&self) -> bool {
if self
.limit_options()
.and_then(|config| config.descending)
.is_some()
{
return false;
}
if self.group_expr().is_empty() && !self.group_expr().has_grouping_set() {
return false;
}
if !self.aggr_expr().is_empty() {
return false;
}
if self.filter_expr().iter().any(|e| e.is_some()) {
return false;
}
if !self.aggr_expr().iter().all(|e| e.order_bys().is_empty()) {
return false;
}
if self.properties().output_ordering().is_some() {
return false;
}
if let Some(requirement) = self.required_input_ordering().swap_remove(0) {
return matches!(requirement, OrderingRequirements::Hard(_));
}
true
}
pub fn compute_properties(
input: &Arc<dyn ExecutionPlan>,
schema: SchemaRef,
group_expr_mapping: &ProjectionMapping,
is_true_no_grouping: bool,
mode: &AggregateMode,
input_order_mode: &InputOrderMode,
aggr_exprs: &[Arc<AggregateFunctionExpr>],
) -> Result<PlanProperties> {
let mut eq_properties = input
.equivalence_properties()
.project(group_expr_mapping, schema);
if is_true_no_grouping {
let new_constants = aggr_exprs.iter().enumerate().map(|(idx, func)| {
let column = Arc::new(Column::new(func.name(), idx));
ConstExpr::from(column as Arc<dyn PhysicalExpr>)
});
eq_properties.add_constants(new_constants)?;
}
let mut constraints = eq_properties.constraints().to_vec();
let new_constraint = Constraint::Unique(
group_expr_mapping
.iter()
.flat_map(|(_, target_cols)| {
target_cols.iter().flat_map(|(expr, _)| {
expr.downcast_ref::<Column>().map(|c| c.index())
})
})
.collect(),
);
constraints.push(new_constraint);
eq_properties =
eq_properties.with_constraints(Constraints::new_unverified(constraints));
let input_partitioning = input.output_partitioning().clone();
let output_partitioning = match mode.input_mode() {
AggregateInputMode::Raw => {
let input_eq_properties = input.equivalence_properties();
input_partitioning.project(group_expr_mapping, input_eq_properties)
}
AggregateInputMode::Partial => input_partitioning.clone(),
};
let emission_type = if *input_order_mode == InputOrderMode::Linear {
EmissionType::Final
} else {
input.pipeline_behavior()
};
Ok(PlanProperties::new(
eq_properties,
output_partitioning,
emission_type,
input.boundedness(),
))
}
pub fn input_order_mode(&self) -> &InputOrderMode {
&self.input_order_mode
}
fn statistics_inner(
&self,
child_statistics: &Statistics,
partition: Option<usize>,
) -> Result<Statistics> {
let column_statistics = {
let mut column_statistics = Statistics::unknown_column(&self.schema());
for (idx, (expr, _)) in self.group_by.expr.iter().enumerate() {
if let Some(col) = expr.downcast_ref::<Column>() {
let child_col_stats =
&child_statistics.column_statistics[col.index()];
column_statistics[idx].max_value = child_col_stats.max_value.clone();
column_statistics[idx].min_value = child_col_stats.min_value.clone();
column_statistics[idx].distinct_count =
child_col_stats.distinct_count;
}
}
column_statistics
};
match self.exact_output_rows_without_group_exprs(partition) {
Some(output_rows) => {
let total_byte_size =
Self::calculate_scaled_byte_size(child_statistics, output_rows);
Ok(Statistics {
num_rows: Precision::Exact(output_rows),
column_statistics,
total_byte_size,
})
}
None => {
let num_rows = self.estimate_num_rows(child_statistics, partition);
let column_statistics = self.nullify_group_columns_for_empty_input(
column_statistics,
child_statistics,
&num_rows,
);
let total_byte_size = num_rows
.get_value()
.and_then(|&output_rows| {
Self::calculate_scaled_byte_size(child_statistics, output_rows)
.get_value()
.map(|&bytes| Precision::Inexact(bytes))
})
.unwrap_or(Precision::Absent);
Ok(Statistics {
num_rows,
column_statistics,
total_byte_size,
})
}
}
}
fn exact_output_rows_without_group_exprs(
&self,
partition: Option<usize>,
) -> Option<usize> {
let logical_rows = self.logical_rows_without_group_exprs()?;
Some(self.scale_logical_rows(logical_rows, partition))
}
fn scale_logical_rows(&self, logical_rows: usize, partition: Option<usize>) -> usize {
match (self.mode.output_mode(), partition) {
(AggregateOutputMode::Final, _) => logical_rows,
(AggregateOutputMode::Partial, Some(_)) => logical_rows,
(AggregateOutputMode::Partial, None) => {
logical_rows * self.cache.output_partitioning().partition_count()
}
}
}
fn output_rows_for_empty_input(&self, partition: Option<usize>) -> usize {
let empty_grouping_sets = self
.group_by
.groups
.iter()
.filter(|nulls| nulls.iter().all(|is_null| *is_null))
.count();
self.scale_logical_rows(empty_grouping_sets, partition)
}
fn nullify_group_columns_for_empty_input(
&self,
mut column_statistics: Vec<ColumnStatistics>,
child_statistics: &Statistics,
num_rows: &Precision<usize>,
) -> Vec<ColumnStatistics> {
let empty_input = child_statistics.num_rows.get_value() == Some(&0);
let emits_rows = num_rows.get_value().is_some_and(|&rows| rows > 0);
if !empty_input || !emits_rows {
return column_statistics;
}
let schema = self.schema();
for (idx, column_stats) in column_statistics
.iter_mut()
.take(self.group_by.expr.len())
.enumerate()
{
let typed_null = ScalarValue::try_from(schema.field(idx).data_type())
.unwrap_or(ScalarValue::Null);
let mut null_bound = Precision::Exact(typed_null);
if matches!(num_rows, Precision::Inexact(_)) {
null_bound = null_bound.to_inexact();
}
column_stats.min_value = null_bound.clone();
column_stats.max_value = null_bound;
column_stats.distinct_count = num_rows.map(|_| 0);
column_stats.null_count = *num_rows;
}
column_statistics
}
fn logical_rows_without_group_exprs(&self) -> Option<usize> {
if self.group_by.is_true_no_grouping() {
Some(1)
} else if self.group_by.expr.is_empty() {
Some(self.group_by.groups.len())
} else {
None
}
}
fn estimate_num_rows(
&self,
child_statistics: &Statistics,
partition: Option<usize>,
) -> Precision<usize> {
let ndv = if !self.group_by.expr.is_empty() {
self.compute_group_ndv(child_statistics)
} else {
None
};
let limit = self.limit_options.as_ref().map(|lo| lo.limit);
if let Some(&value) = child_statistics.num_rows.get_value() {
if value > 1 {
let mut num_rows = child_statistics.num_rows.to_inexact();
if let Some(ndv) = ndv {
num_rows = num_rows.map(|n| n.min(ndv));
}
if let Some(limit) = limit {
num_rows = num_rows.map(|n| n.min(limit));
}
num_rows
} else if value == 0 {
child_statistics
.num_rows
.map(|_| self.output_rows_for_empty_input(partition))
} else {
let grouping_set_num = self.group_by.groups.len();
let mut num_rows =
child_statistics.num_rows.map(|x| x * grouping_set_num);
if let Some(limit) = limit {
num_rows = num_rows.map(|n| n.min(limit));
}
num_rows
}
} else {
match (ndv, limit) {
(Some(n), Some(l)) => Precision::Inexact(n.min(l)),
(Some(n), None) => Precision::Inexact(n),
(None, Some(l)) => Precision::Inexact(l),
(None, None) => Precision::Absent,
}
}
}
fn compute_group_ndv(&self, child_statistics: &Statistics) -> Option<usize> {
let mut total: usize = 0;
for group_mask in &self.group_by.groups {
let mut set_product: usize = 1;
for (j, (expr, _)) in self.group_by.expr.iter().enumerate() {
if group_mask[j] {
continue;
}
let col = expr.downcast_ref::<Column>()?;
let col_stats = &child_statistics.column_statistics[col.index()];
let ndv = *col_stats.distinct_count.get_value()?;
let null_adjustment = match col_stats.null_count.get_value() {
Some(&n) if n > 0 => 1usize,
_ => 0,
};
set_product = set_product
.saturating_mul(ndv.saturating_add(null_adjustment).max(1));
}
total = total.saturating_add(set_product);
}
Some(total)
}
fn init_dynamic_filter(&mut self) {
if (!self.group_by.is_empty()) || (self.mode != AggregateMode::Partial) {
debug_assert!(
self.dynamic_filter.is_none(),
"The current operator node does not support dynamic filter"
);
return;
}
if self.dynamic_filter.is_some() {
return;
}
let mut aggr_dyn_filters = Vec::new();
let mut all_cols: Vec<Arc<dyn PhysicalExpr>> = Vec::new();
for (i, aggr_expr) in self.aggr_expr.iter().enumerate() {
let fun_name = aggr_expr.fun().name();
let aggr_type = if fun_name.eq_ignore_ascii_case("min") {
DynamicFilterAggregateType::Min
} else if fun_name.eq_ignore_ascii_case("max") {
DynamicFilterAggregateType::Max
} else {
return;
};
if let [arg] = aggr_expr.expressions().as_slice()
&& arg.is::<Column>()
{
all_cols.push(Arc::clone(arg));
aggr_dyn_filters.push(PerAccumulatorDynFilter {
aggr_type,
aggr_index: i,
shared_bound: Arc::new(Mutex::new(ScalarValue::Null)),
});
}
}
if !aggr_dyn_filters.is_empty() {
self.dynamic_filter = Some(Arc::new(AggrDynFilter {
filter: Arc::new(DynamicFilterPhysicalExpr::new(all_cols, lit(true))),
supported_accumulators_info: aggr_dyn_filters,
}))
}
}
fn cols_for_dynamic_filter(
&self,
supported_accumulators_info: &[PerAccumulatorDynFilter],
) -> Vec<Arc<dyn PhysicalExpr>> {
let all_cols: Vec<Arc<dyn PhysicalExpr>> = supported_accumulators_info
.iter()
.filter_map(|info| {
if let [arg] = &self.aggr_expr[info.aggr_index].expressions().as_slice()
&& arg.is::<Column>()
{
return Some(Arc::clone(arg));
}
None
})
.collect();
debug_assert!(all_cols.len() == supported_accumulators_info.len());
all_cols
}
#[inline]
fn calculate_scaled_byte_size(
input_stats: &Statistics,
target_row_count: usize,
) -> Precision<usize> {
match (
input_stats.num_rows.get_value(),
input_stats.total_byte_size.get_value(),
) {
(Some(&input_rows), Some(&input_bytes)) if input_rows > 0 => {
let bytes_per_row = input_bytes as f64 / input_rows as f64;
let scaled_bytes =
(bytes_per_row * target_row_count as f64).ceil() as usize;
Precision::Inexact(scaled_bytes)
}
_ => Precision::Absent,
}
}
}
impl DisplayAs for AggregateExec {
fn fmt_as(
&self,
t: DisplayFormatType,
f: &mut std::fmt::Formatter,
) -> std::fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
let format_expr_with_alias =
|(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
let e = e.to_string();
if &e != alias {
format!("{e} as {alias}")
} else {
e
}
};
write!(f, "AggregateExec: mode={:?}", self.mode)?;
let g: Vec<String> = if self.group_by.is_single() {
self.group_by
.expr
.iter()
.map(format_expr_with_alias)
.collect()
} else {
self.group_by
.groups
.iter()
.map(|group| {
let terms = group
.iter()
.enumerate()
.map(|(idx, is_null)| {
if *is_null {
format_expr_with_alias(
&self.group_by.null_expr[idx],
)
} else {
format_expr_with_alias(&self.group_by.expr[idx])
}
})
.collect::<Vec<String>>()
.join(", ");
format!("({terms})")
})
.collect()
};
write!(f, ", gby=[{}]", g.join(", "))?;
let a: Vec<String> = self
.aggr_expr
.iter()
.map(|agg| format_aggregate_exec_expr(agg).to_string())
.collect();
write!(f, ", aggr=[{}]", a.join(", "))?;
if let Some(config) = self.limit_options {
write!(f, ", lim=[{}]", config.limit)?;
}
if self.input_order_mode != InputOrderMode::Linear {
write!(f, ", ordering_mode={:?}", self.input_order_mode)?;
}
}
DisplayFormatType::TreeRender => {
let format_expr_with_alias =
|(e, alias): &(Arc<dyn PhysicalExpr>, String)| -> String {
let expr_sql = fmt_sql(e.as_ref()).to_string();
if &expr_sql != alias {
format!("{expr_sql} as {alias}")
} else {
expr_sql
}
};
let g: Vec<String> = if self.group_by.is_single() {
self.group_by
.expr
.iter()
.map(format_expr_with_alias)
.collect()
} else {
self.group_by
.groups
.iter()
.map(|group| {
let terms = group
.iter()
.enumerate()
.map(|(idx, is_null)| {
if *is_null {
format_expr_with_alias(
&self.group_by.null_expr[idx],
)
} else {
format_expr_with_alias(&self.group_by.expr[idx])
}
})
.collect::<Vec<String>>()
.join(", ");
format!("({terms})")
})
.collect()
};
let a: Vec<String> = self
.aggr_expr
.iter()
.map(|agg| format_tree_aggregate_expr(agg).to_string())
.collect();
writeln!(f, "mode={:?}", self.mode)?;
if !g.is_empty() {
writeln!(f, "group_by={}", g.join(", "))?;
}
if !a.is_empty() {
writeln!(f, "aggr={}", a.join(", "))?;
}
if let Some(config) = self.limit_options {
writeln!(f, "limit={}", config.limit)?;
}
}
}
Ok(())
}
}
fn format_aggregate_exec_expr(agg: &AggregateFunctionExpr) -> Cow<'_, str> {
match agg.human_display_alias() {
Some(_) => format_human_display(agg.human_display(), agg.human_display_alias())
.unwrap_or_else(|| Cow::Borrowed(agg.name())),
None => Cow::Borrowed(agg.name()),
}
}
fn format_tree_aggregate_expr(agg: &AggregateFunctionExpr) -> Cow<'_, str> {
format_human_display(agg.human_display(), agg.human_display_alias())
.unwrap_or_else(|| Cow::Borrowed(agg.name()))
}
fn format_human_display<'a>(
human_display: Option<&'a str>,
alias: Option<&'a str>,
) -> Option<Cow<'a, str>> {
human_display.map(|human_display| match alias {
Some(alias) => Cow::Owned(format!("{human_display} as {alias}")),
None => Cow::Borrowed(human_display),
})
}
impl ExecutionPlan for AggregateExec {
fn name(&self) -> &'static str {
"AggregateExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.cache
}
fn required_input_distribution(&self) -> Vec<Distribution> {
self.input_distribution_requirements().into_per_child()
}
fn input_distribution_requirements(&self) -> InputDistributionRequirements {
InputDistributionRequirements::new(match &self.mode {
AggregateMode::Partial | AggregateMode::PartialReduce => {
vec![Distribution::UnspecifiedDistribution]
}
AggregateMode::FinalPartitioned | AggregateMode::SinglePartitioned => {
vec![Distribution::KeyPartitioned(self.group_by.input_exprs())]
}
AggregateMode::Final | AggregateMode::Single => {
vec![Distribution::SinglePartition]
}
})
}
fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
vec![self.required_input_ordering.clone()]
}
fn maintains_input_order(&self) -> Vec<bool> {
vec![self.input_order_mode != InputOrderMode::Linear]
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.input]
}
fn replace_children(
self: Arc<Self>,
mut children: Vec<Arc<dyn ExecutionPlan>>,
options: ReplaceChildrenOptions,
) -> Result<Arc<dyn ExecutionPlan>> {
validate_child_count!(self, children);
match options.children_properties {
ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
input: children.swap_remove(0),
metrics: ExecutionPlanMetricsSet::new(),
..Self::clone(&*self)
})),
ChildrenPropertiesMode::Recompute => {
let mut me = AggregateExec::try_new_with_schema(
self.mode,
Arc::clone(&self.group_by),
self.aggr_expr.to_vec(),
Arc::clone(&self.filter_expr),
Arc::clone(&children[0]),
Arc::clone(&self.input_schema),
Arc::clone(&self.schema),
)?;
me.limit_options = self.limit_options;
me.dynamic_filter.clone_from(&self.dynamic_filter);
Ok(Arc::new(me))
}
}
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
self.replace_children(
children,
ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
)
}
fn apply_expressions(
&self,
f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
let group_by = self.group_by.input_exprs();
let aggregates = self.aggr_expr.iter().flat_map(|aggr| {
let expressions = aggr.all_expressions();
expressions
.args
.into_iter()
.chain(expressions.order_by_exprs)
});
let filters = self.filter_expr.iter().flatten().cloned();
let dynamic_filter = self.dynamic_filter.iter().map(|dynamic_filter| {
Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter.filter)
as Arc<dyn PhysicalExpr>
});
crate::apply_expression_roots(
group_by
.into_iter()
.chain(aggregates)
.chain(filters)
.chain(dynamic_filter),
f,
)
}
fn dynamic_expressions_produced(&self) -> Vec<Arc<dyn PhysicalExpr>> {
self.dynamic_filter
.iter()
.map(|dynamic_filter| {
Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter.filter)
as Arc<dyn PhysicalExpr>
})
.collect()
}
fn with_new_children_and_same_properties(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
self.replace_children(
children,
ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
)
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
self.execute_typed(partition, &context)
.map(|stream| stream.into())
}
fn metrics(&self) -> Option<MetricsSet> {
Some(self.metrics.clone_inner())
}
fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
vec![ChildStats::At(partition)]
}
fn statistics_from_inputs(
&self,
input_stats: &[Arc<Statistics>],
args: &StatisticsArgs,
) -> Result<Arc<Statistics>> {
let child_statistics = Arc::clone(&input_stats[0]);
Ok(Arc::new(
self.statistics_inner(&child_statistics, args.partition())?,
))
}
fn cardinality_effect(&self) -> CardinalityEffect {
CardinalityEffect::LowerEqual
}
fn gather_filters_for_pushdown(
&self,
phase: FilterPushdownPhase,
parent_filters: Vec<Arc<dyn PhysicalExpr>>,
config: &ConfigOptions,
) -> Result<FilterDescription> {
let mut allowed_indices: HashSet<usize> =
(0..self.group_by.expr().len()).collect();
for null_mask in self.group_by.groups() {
allowed_indices.retain(|idx| null_mask.get(*idx) != Some(&true));
}
let child = self.children()[0];
let may_emit_on_empty_input = self.group_by.is_true_no_grouping()
|| self
.group_by
.groups()
.iter()
.any(|null_mask| null_mask.iter().all(|is_null| *is_null));
let mut child_desc = if may_emit_on_empty_input {
ChildFilterDescription::all_unsupported(&parent_filters)
} else {
ChildFilterDescription::from_child_with_allowed_indices(
&parent_filters,
allowed_indices,
child,
)?
};
if phase == FilterPushdownPhase::Post
&& config.optimizer.enable_aggregate_dynamic_filter_pushdown
&& let Some(self_dyn_filter) = &self.dynamic_filter
{
let dyn_filter = Arc::clone(&self_dyn_filter.filter);
child_desc = child_desc.with_self_filter(dyn_filter);
}
Ok(FilterDescription::new().with_child(child_desc))
}
fn handle_child_pushdown_result(
&self,
phase: FilterPushdownPhase,
child_pushdown_result: ChildPushdownResult,
_config: &ConfigOptions,
) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
let mut result = FilterPushdownPropagation::if_any(child_pushdown_result.clone());
if phase == FilterPushdownPhase::Post
&& let Some(dyn_filter) = &self.dynamic_filter
{
let child_accepts_dyn_filter = dyn_filter
.filter
.expression_id()
.map(|id| plan_contains_expression_id(&self.input, id))
.transpose()?
.unwrap_or(false);
if !child_accepts_dyn_filter {
let mut new_node = self.clone();
new_node.dynamic_filter = None;
result = result
.with_updated_node(Arc::new(new_node) as Arc<dyn ExecutionPlan>);
}
}
Ok(result)
}
#[cfg(feature = "proto")]
fn try_to_proto(
&self,
ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
use datafusion_proto_models::protobuf;
let Self {
mode,
group_by,
aggr_expr,
filter_expr,
limit_options,
input,
schema: _,
input_schema,
metrics: _,
required_input_ordering: _,
input_order_mode: _,
cache: _,
dynamic_filter,
} = self;
let input = ctx.encode_child(input)?;
let group_expr =
ctx.encode_expressions(group_by.expr().iter().map(|(expr, _)| expr))?;
let group_expr_name = group_by
.expr()
.iter()
.map(|(_, name)| name.to_owned())
.collect();
let null_expr =
ctx.encode_expressions(group_by.null_expr().iter().map(|(expr, _)| expr))?;
let groups = group_by.groups().iter().flatten().copied().collect();
let aggr_expr_name = aggr_expr
.iter()
.map(|expr| expr.name().to_string())
.collect();
let aggr_expr = aggr_expr
.iter()
.map(|expr| encode_aggregate_expr(expr, ctx))
.collect::<Result<Vec<_>>>()?;
let filter_expr = filter_expr
.iter()
.map(|filter| {
Ok(protobuf::MaybeFilter {
expr: filter
.as_ref()
.map(|expr| ctx.encode_expr(expr))
.transpose()?,
})
})
.collect::<Result<Vec<_>>>()?;
let mode = match mode {
AggregateMode::Partial => protobuf::AggregateMode::Partial,
AggregateMode::Final => protobuf::AggregateMode::Final,
AggregateMode::FinalPartitioned => protobuf::AggregateMode::FinalPartitioned,
AggregateMode::Single => protobuf::AggregateMode::Single,
AggregateMode::SinglePartitioned => {
protobuf::AggregateMode::SinglePartitioned
}
AggregateMode::PartialReduce => protobuf::AggregateMode::PartialReduce,
};
let limit = limit_options.map(|options| protobuf::AggLimit {
limit: options.limit() as u64,
descending: options.descending(),
});
let dynamic_filter = match dynamic_filter {
Some(dynamic_filter) => {
let expr: Arc<dyn PhysicalExpr> =
Arc::clone(&dynamic_filter.filter) as Arc<dyn PhysicalExpr>;
Some(ctx.encode_expr(&expr)?)
}
None => None,
};
Ok(Some(protobuf::PhysicalPlanNode {
physical_plan_type: Some(
protobuf::physical_plan_node::PhysicalPlanType::Aggregate(Box::new(
protobuf::AggregateExecNode {
group_expr,
group_expr_name,
aggr_expr,
filter_expr,
aggr_expr_name,
mode: mode as i32,
input: Some(Box::new(input)),
input_schema: Some(input_schema.as_ref().try_into()?),
null_expr,
groups,
limit,
has_grouping_set: group_by.has_grouping_set(),
dynamic_filter,
schema: Some(self.schema.as_ref().try_into()?),
},
)),
),
}))
}
}
#[cfg(feature = "proto")]
const HUMAN_DISPLAY_ALIAS_PREFIX: &str = "\u{1f}datafusion_human_display_alias_v1:";
#[cfg(feature = "proto")]
fn encode_human_display_alias(human_display: &str, alias: &str) -> String {
format!(
"{HUMAN_DISPLAY_ALIAS_PREFIX}{}:{alias}{human_display}",
alias.len()
)
}
#[cfg(feature = "proto")]
fn split_human_display_alias<'a>(
human_display: &'a str,
name: &'a str,
) -> (&'a str, Option<&'a str>) {
if let Some(encoded) = human_display.strip_prefix(HUMAN_DISPLAY_ALIAS_PREFIX)
&& let Some((alias_len, encoded)) = encoded.split_once(':')
&& let Ok(alias_len) = alias_len.parse::<usize>()
&& let Some(alias) = encoded.get(..alias_len)
&& let Some(human_display) = encoded.get(alias_len..)
&& alias == name
&& !human_display.is_empty()
{
return (human_display, Some(alias));
}
(human_display, None)
}
#[cfg(feature = "proto")]
fn encode_aggregate_expr(
aggr_expr: &Arc<AggregateFunctionExpr>,
ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
) -> Result<datafusion_proto_models::protobuf::PhysicalExprNode> {
use datafusion_proto_models::protobuf;
let expressions = aggr_expr.expressions();
let expr = ctx.encode_expressions(expressions.iter())?;
let ordering_req =
datafusion_physical_expr_common::sort_expr::sort_exprs_try_to_proto(
aggr_expr.order_bys(),
&ctx.expr_ctx(),
)?;
let name = aggr_expr.fun().name().to_string();
let fun_definition = ctx.encode_udaf(aggr_expr.fun())?;
let human_display = match (aggr_expr.human_display(), aggr_expr.human_display_alias())
{
(Some(display), Some(alias)) => encode_human_display_alias(display, alias),
(Some(display), None) => display.to_string(),
(None, _) => String::new(),
};
Ok(protobuf::PhysicalExprNode {
expr_id: None,
expr_type: Some(protobuf::physical_expr_node::ExprType::AggregateExpr(
protobuf::PhysicalAggregateExprNode {
aggregate_function: Some(
protobuf::physical_aggregate_expr_node::AggregateFunction::UserDefinedAggrFunction(name),
),
expr,
ordering_req,
distinct: aggr_expr.is_distinct(),
ignore_nulls: aggr_expr.ignore_nulls(),
fun_definition,
human_display,
is_reversed: aggr_expr.is_reversed(),
},
)),
})
}
#[cfg(feature = "proto")]
impl AggregateExec {
pub fn try_from_proto(
node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
) -> Result<Arc<dyn ExecutionPlan>> {
use datafusion_physical_expr::aggregate::AggregateExprBuilder;
use datafusion_proto_models::protobuf;
use protobuf::physical_aggregate_expr_node::AggregateFunction;
use protobuf::physical_expr_node::ExprType;
let hash_agg = crate::expect_plan_variant!(
node,
protobuf::physical_plan_node::PhysicalPlanType::Aggregate,
"AggregateExec",
);
let protobuf::AggregateExecNode {
group_expr,
aggr_expr,
mode,
input,
group_expr_name,
aggr_expr_name,
input_schema,
null_expr,
groups,
filter_expr,
limit,
has_grouping_set,
dynamic_filter,
schema,
} = hash_agg.as_ref();
let input =
ctx.decode_required_child(input.as_deref(), "AggregateExec", "input")?;
let mode = protobuf::AggregateMode::try_from(*mode).map_err(|_| {
datafusion_common::internal_datafusion_err!(
"Received an AggregateNode message with unknown AggregateMode {mode}"
)
})?;
let mode = match mode {
protobuf::AggregateMode::Partial => AggregateMode::Partial,
protobuf::AggregateMode::Final => AggregateMode::Final,
protobuf::AggregateMode::FinalPartitioned => AggregateMode::FinalPartitioned,
protobuf::AggregateMode::Single => AggregateMode::Single,
protobuf::AggregateMode::SinglePartitioned => {
AggregateMode::SinglePartitioned
}
protobuf::AggregateMode::PartialReduce => AggregateMode::PartialReduce,
};
let num_expr = group_expr.len();
let child_schema = input.schema();
let group_expr = group_expr
.iter()
.zip(group_expr_name.iter())
.map(|(expr, name)| {
Ok((
ctx.decode_expr(expr, child_schema.as_ref())?,
name.to_string(),
))
})
.collect::<Result<Vec<_>>>()?;
let null_expr = null_expr
.iter()
.zip(group_expr_name.iter())
.map(|(expr, name)| {
Ok((
ctx.decode_expr(expr, child_schema.as_ref())?,
name.to_string(),
))
})
.collect::<Result<Vec<_>>>()?;
let groups = if groups.is_empty() {
vec![]
} else {
groups
.chunks(num_expr)
.map(|group| group.to_vec())
.collect()
};
let input_schema = input_schema.as_ref().ok_or_else(|| {
datafusion_common::internal_datafusion_err!(
"input_schema in AggregateNode is missing."
)
})?;
let input_schema: SchemaRef = SchemaRef::new(input_schema.try_into()?);
let filter_expr = filter_expr
.iter()
.map(|filter| {
filter
.expr
.as_ref()
.map(|expr| ctx.decode_expr(expr, input_schema.as_ref()))
.transpose()
})
.collect::<Result<Vec<_>>>()?;
let aggr_expr = aggr_expr
.iter()
.zip(aggr_expr_name.iter())
.map(|(expr, name)| {
let expr_type = expr.expr_type.as_ref().ok_or_else(|| {
datafusion_common::internal_datafusion_err!(
"Unexpected empty aggregate physical expression"
)
})?;
let ExprType::AggregateExpr(aggregate) = expr_type else {
return internal_err!(
"Invalid aggregate expression for AggregateExec"
);
};
let args = aggregate
.expr
.iter()
.map(|expr| ctx.decode_expr(expr, input_schema.as_ref()))
.collect::<Result<Vec<_>>>()?;
let order_by =
datafusion_physical_expr_common::sort_expr::sort_exprs_try_from_proto(
&aggregate.ordering_req,
&ctx.expr_ctx(input_schema.as_ref()),
)?;
let Some(AggregateFunction::UserDefinedAggrFunction(udaf_name)) =
aggregate.aggregate_function.as_ref()
else {
return internal_err!(
"Invalid AggregateExpr, missing aggregate_function"
);
};
let udaf =
ctx.decode_udaf(udaf_name, aggregate.fun_definition.as_deref())?;
let (human_display, human_display_alias) =
split_human_display_alias(&aggregate.human_display, name);
let builder = AggregateExprBuilder::new(udaf, args)
.schema(Arc::clone(&input_schema))
.alias(name)
.with_ignore_nulls(aggregate.ignore_nulls)
.with_distinct(aggregate.distinct)
.order_by(order_by)
.with_reversed(aggregate.is_reversed)
.human_display(human_display);
let builder = if let Some(alias) = human_display_alias {
builder.human_display_alias(alias)
} else {
builder
};
builder.build().map(Arc::new)
})
.collect::<Result<Vec<_>>>()?;
let group_by =
PhysicalGroupBy::new(group_expr, null_expr, groups, *has_grouping_set);
let aggregate = if let Some(schema) = schema {
let schema = SchemaRef::new(schema.try_into()?);
AggregateExec::try_new_with_schema(
mode,
group_by,
aggr_expr,
filter_expr,
input,
Arc::clone(&input_schema),
schema,
)
} else {
AggregateExec::try_new(
mode,
group_by,
aggr_expr,
filter_expr,
input,
Arc::clone(&input_schema),
)
}?;
let aggregate = if let Some(limit) = limit {
let options = match limit.descending {
Some(descending) => {
LimitOptions::new_with_order(limit.limit as usize, descending)
}
None => LimitOptions::new(limit.limit as usize),
};
aggregate.with_limit_options(Some(options))
} else {
aggregate
};
let aggregate = if let Some(dynamic_filter) = dynamic_filter {
let dynamic_filter =
ctx.decode_expr(dynamic_filter, input_schema.as_ref())?;
let dynamic_filter = (dynamic_filter
as Arc<dyn std::any::Any + Send + Sync>)
.downcast::<DynamicFilterPhysicalExpr>()
.map_err(|_| {
datafusion_common::internal_datafusion_err!(
"AggregateExec dynamic_filter did not decode to a DynamicFilterPhysicalExpr"
)
})?;
aggregate.with_dynamic_filter_expr(dynamic_filter)?
} else {
let mut aggregate = aggregate;
aggregate.dynamic_filter = None;
aggregate
};
Ok(Arc::new(aggregate))
}
}
fn create_schema(
input_schema: &Schema,
group_by: &PhysicalGroupBy,
aggr_expr: &[Arc<AggregateFunctionExpr>],
mode: AggregateMode,
) -> Result<Schema> {
let mut fields = Vec::with_capacity(group_by.num_output_exprs() + aggr_expr.len());
fields.extend(group_by.output_fields(input_schema)?);
match mode.output_mode() {
AggregateOutputMode::Final => {
for expr in aggr_expr {
fields.push(expr.field())
}
}
AggregateOutputMode::Partial => {
for expr in aggr_expr {
fields.extend(expr.state_fields()?.iter().cloned());
}
}
}
Ok(Schema::new_with_metadata(
fields,
input_schema.metadata().clone(),
))
}
fn get_aggregate_expr_req(
aggr_expr: &AggregateFunctionExpr,
group_by: &PhysicalGroupBy,
agg_mode: &AggregateMode,
include_soft_requirement: bool,
) -> Option<LexOrdering> {
if agg_mode.input_mode() == AggregateInputMode::Partial {
return None;
}
match aggr_expr.order_sensitivity() {
AggregateOrderSensitivity::Insensitive => return None,
AggregateOrderSensitivity::HardRequirement => {}
AggregateOrderSensitivity::SoftRequirement => {
if !include_soft_requirement {
return None;
}
}
AggregateOrderSensitivity::Beneficial => return None,
}
let mut sort_exprs = aggr_expr.order_bys().to_vec();
if group_by.is_single() {
let physical_exprs = group_by.input_exprs();
sort_exprs.retain(|sort_expr| {
!physical_exprs_contains(&physical_exprs, &sort_expr.expr)
});
}
LexOrdering::new(sort_exprs)
}
pub fn concat_slices<T: Clone>(lhs: &[T], rhs: &[T]) -> Vec<T> {
[lhs, rhs].concat()
}
fn determine_finer(
current: &Option<LexOrdering>,
candidate: &LexOrdering,
) -> Option<bool> {
if let Some(ordering) = current {
candidate.partial_cmp(ordering).map(|cmp| cmp.is_gt())
} else {
Some(true)
}
}
pub fn get_finer_aggregate_exprs_requirement(
aggr_exprs: &mut [Arc<AggregateFunctionExpr>],
group_by: &PhysicalGroupBy,
eq_properties: &EquivalenceProperties,
agg_mode: &AggregateMode,
) -> Result<Vec<PhysicalSortRequirement>> {
let mut requirement = None;
for include_soft_requirement in [false, true] {
for aggr_expr in aggr_exprs.iter_mut() {
let Some(aggr_req) = get_aggregate_expr_req(
aggr_expr,
group_by,
agg_mode,
include_soft_requirement,
)
.and_then(|o| eq_properties.normalize_sort_exprs(o)) else {
continue;
};
let forward_finer = determine_finer(&requirement, &aggr_req);
if let Some(finer) = forward_finer {
if !finer {
continue;
} else if eq_properties.ordering_satisfy(aggr_req.clone())? {
requirement = Some(aggr_req);
continue;
}
}
if let Some(reverse_aggr_expr) = aggr_expr.reverse_expr() {
let Some(rev_aggr_req) = get_aggregate_expr_req(
&reverse_aggr_expr,
group_by,
agg_mode,
include_soft_requirement,
)
.and_then(|o| eq_properties.normalize_sort_exprs(o)) else {
*aggr_expr = Arc::new(reverse_aggr_expr);
continue;
};
if let Some(finer) = determine_finer(&requirement, &rev_aggr_req) {
if !finer {
*aggr_expr = Arc::new(reverse_aggr_expr);
} else if eq_properties.ordering_satisfy(rev_aggr_req.clone())? {
*aggr_expr = Arc::new(reverse_aggr_expr);
requirement = Some(rev_aggr_req);
} else {
requirement = Some(aggr_req);
}
} else if forward_finer.is_some() {
requirement = Some(aggr_req);
} else {
if !include_soft_requirement {
return not_impl_err!(
"Conflicting ordering requirements in aggregate functions is not supported"
);
}
}
}
}
}
Ok(requirement.map_or_else(Vec::new, |o| o.into_iter().map(Into::into).collect()))
}
pub fn aggregate_expressions(
aggr_expr: &[Arc<AggregateFunctionExpr>],
mode: &AggregateMode,
col_idx_base: usize,
) -> Result<Vec<Vec<Arc<dyn PhysicalExpr>>>> {
match mode.input_mode() {
AggregateInputMode::Raw => Ok(aggr_expr
.iter()
.map(|agg| {
let mut result = agg.expressions();
result.extend(agg.order_bys().iter().map(|item| Arc::clone(&item.expr)));
result
})
.collect()),
AggregateInputMode::Partial => {
let mut col_idx_base = col_idx_base;
aggr_expr
.iter()
.map(|agg| {
let exprs = merge_expressions(col_idx_base, agg)?;
col_idx_base += exprs.len();
Ok(exprs)
})
.collect()
}
}
}
fn merge_expressions(
index_base: usize,
expr: &AggregateFunctionExpr,
) -> Result<Vec<Arc<dyn PhysicalExpr>>> {
expr.state_fields().map(|fields| {
fields
.iter()
.enumerate()
.map(|(idx, f)| Arc::new(Column::new(f.name(), index_base + idx)) as _)
.collect()
})
}
pub type AccumulatorItem = Box<dyn Accumulator>;
pub fn create_accumulators(
aggr_expr: &[Arc<AggregateFunctionExpr>],
) -> Result<Vec<AccumulatorItem>> {
aggr_expr
.iter()
.map(|expr| expr.create_accumulator())
.collect()
}
pub fn finalize_aggregation(
accumulators: &mut [AccumulatorItem],
mode: &AggregateMode,
) -> Result<Vec<ArrayRef>> {
match mode.output_mode() {
AggregateOutputMode::Final => {
accumulators
.iter_mut()
.map(|accumulator| accumulator.evaluate().and_then(|v| v.to_array()))
.collect()
}
AggregateOutputMode::Partial => {
accumulators
.iter_mut()
.map(|accumulator| {
accumulator.state().and_then(|e| {
e.iter()
.map(|v| v.to_array())
.collect::<Result<Vec<ArrayRef>>>()
})
})
.flatten_ok()
.collect()
}
}
}
pub fn evaluate_many(
expr: &[Vec<Arc<dyn PhysicalExpr>>],
batch: &RecordBatch,
) -> Result<Vec<Vec<ArrayRef>>> {
expr.iter()
.map(|expr| evaluate_expressions_to_arrays(expr, batch))
.collect()
}
fn evaluate_optional(
expr: &[Option<Arc<dyn PhysicalExpr>>],
batch: &RecordBatch,
) -> Result<Vec<Option<ArrayRef>>> {
expr.iter()
.map(|expr| {
expr.as_ref()
.map(|expr| {
expr.evaluate(batch)
.and_then(|v| v.into_array(batch.num_rows()))
})
.transpose()
})
.collect()
}
pub(crate) fn group_id_array(
group: &[bool],
ordinal: usize,
max_ordinal: usize,
num_rows: usize,
) -> Result<ArrayRef> {
let n = group.len();
if n > 64 {
return not_impl_err!(
"Grouping sets with more than 64 columns are not supported"
);
}
let ordinal_bits = usize::BITS as usize - max_ordinal.leading_zeros() as usize;
let total_bits = n + ordinal_bits;
if total_bits > 64 {
return not_impl_err!(
"Grouping sets with {n} columns and a maximum duplicate ordinal of \
{max_ordinal} require {total_bits} bits, which exceeds 64"
);
}
let semantic_id = group.iter().fold(0u64, |acc, &is_null| {
(acc << 1) | if is_null { 1 } else { 0 }
});
let full_id = semantic_id | ((ordinal as u64) << n);
if total_bits <= 8 {
Ok(Arc::new(UInt8Array::from(vec![full_id as u8; num_rows])))
} else if total_bits <= 16 {
Ok(Arc::new(UInt16Array::from(vec![full_id as u16; num_rows])))
} else if total_bits <= 32 {
Ok(Arc::new(UInt32Array::from(vec![full_id as u32; num_rows])))
} else {
Ok(Arc::new(UInt64Array::from(vec![full_id; num_rows])))
}
}
pub(crate) fn max_duplicate_ordinal(groups: &[Vec<bool>]) -> usize {
let mut counts: HashMap<&[bool], usize> = HashMap::new();
for group in groups {
*counts.entry(group).or_insert(0) += 1;
}
counts.into_values().max().unwrap_or(0).saturating_sub(1)
}
pub fn evaluate_group_by(
group_by: &PhysicalGroupBy,
batch: &RecordBatch,
) -> Result<Vec<Vec<ArrayRef>>> {
let max_ordinal = max_duplicate_ordinal(&group_by.groups);
let mut ordinal_per_pattern: HashMap<&[bool], usize> = HashMap::new();
let exprs = evaluate_expressions_to_arrays(
group_by.expr.iter().map(|(expr, _)| expr),
batch,
)?;
let null_exprs = evaluate_expressions_to_arrays(
group_by.null_expr.iter().map(|(expr, _)| expr),
batch,
)?;
group_by
.groups
.iter()
.map(|group| {
let ordinal = ordinal_per_pattern.entry(group).or_insert(0);
let current_ordinal = *ordinal;
*ordinal += 1;
let mut group_values = Vec::with_capacity(group_by.num_group_exprs());
group_values.extend(group.iter().enumerate().map(|(idx, is_null)| {
if *is_null {
Arc::clone(&null_exprs[idx])
} else {
Arc::clone(&exprs[idx])
}
}));
if !group_by.is_single() {
group_values.push(group_id_array(
group,
current_ordinal,
max_ordinal,
batch.num_rows(),
)?);
}
Ok(group_values)
})
.collect()
}
#[cfg(test)]
mod tests {
use std::task::{Context, Poll};
use super::*;
use crate::RecordBatchStream;
use crate::coalesce_partitions::CoalescePartitionsExec;
use crate::common;
use crate::common::collect;
use crate::empty::EmptyExec;
use crate::execution_plan::Boundedness;
use crate::expressions::col;
use crate::filter::FilterExecBuilder;
use crate::metrics::MetricValue;
use crate::statistics::{StatisticsArgs, StatisticsContext};
use crate::test::TestMemoryExec;
use crate::test::assert_is_pending;
use crate::test::exec::{
BlockingExec, StatisticsExec, assert_strong_count_converges_to_zero,
};
use arrow::array::{
BooleanArray, DictionaryArray, Float32Array, Float64Array, Int32Array,
Int64Array, StructArray, UInt32Array, UInt64Array,
};
use arrow::compute::{SortOptions, concat_batches};
use arrow::datatypes::Int32Type;
use datafusion_common::test_util::{batches_to_sort_string, batches_to_string};
use datafusion_common::{DataFusionError, internal_err};
use datafusion_execution::config::SessionConfig;
use datafusion_execution::memory_pool::FairSpillPool;
use datafusion_execution::runtime_env::RuntimeEnvBuilder;
use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs};
use datafusion_expr::{
Accumulator, AggregateUDF, AggregateUDFImpl, EmitTo, GroupsAccumulator,
Signature, Volatility,
};
use datafusion_functions_aggregate::approx_percentile_cont::approx_percentile_cont_udaf;
use datafusion_functions_aggregate::array_agg::array_agg_udaf;
use datafusion_functions_aggregate::average::avg_udaf;
use datafusion_functions_aggregate::count::count_udaf;
use datafusion_functions_aggregate::first_last::{first_value_udaf, last_value_udaf};
use datafusion_functions_aggregate::median::median_udaf;
use datafusion_functions_aggregate::min_max::min_udaf;
use datafusion_functions_aggregate::sum::sum_udaf;
use datafusion_physical_expr::Partitioning;
use datafusion_physical_expr::PhysicalSortExpr;
use datafusion_physical_expr::aggregate::AggregateExprBuilder;
use datafusion_physical_expr::expressions::{Literal, NotExpr};
use crate::projection::ProjectionExec;
use crate::repartition::RepartitionExec;
use datafusion_physical_expr::projection::ProjectionExpr;
use futures::{FutureExt, Stream, StreamExt};
use insta::{allow_duplicates, assert_snapshot};
#[cfg(feature = "proto")]
#[test]
fn split_human_display_alias_ignores_mismatched_alias() {
let encoded = encode_human_display_alias("sum(value)", "revenue");
assert_eq!(
split_human_display_alias(&encoded, "other"),
(encoded.as_str(), None)
);
}
#[cfg(feature = "proto")]
#[test]
fn split_human_display_alias_keeps_malformed_prefix_literal() {
let display = format!("{HUMAN_DISPLAY_ALIAS_PREFIX}not-an-encoding");
assert_eq!(
split_human_display_alias(&display, "agg"),
(display.as_str(), None)
);
}
fn create_test_schema() -> Result<SchemaRef> {
let a = Field::new("a", DataType::Int32, true);
let b = Field::new("b", DataType::Int32, true);
let c = Field::new("c", DataType::Int32, true);
let d = Field::new("d", DataType::Int32, true);
let e = Field::new("e", DataType::Int32, true);
let schema = Arc::new(Schema::new(vec![a, b, c, d, e]));
Ok(schema)
}
fn some_data() -> (Arc<Schema>, Vec<RecordBatch>) {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
(
Arc::clone(&schema),
vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![2, 3, 4, 4])),
Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0])),
],
)
.unwrap(),
RecordBatch::try_new(
schema,
vec![
Arc::new(UInt32Array::from(vec![2, 3, 3, 4])),
Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0])),
],
)
.unwrap(),
],
)
}
fn some_data_v2() -> (Arc<Schema>, Vec<RecordBatch>) {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
(
Arc::clone(&schema),
vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![2, 3, 4, 4])),
Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![2, 3, 3, 4])),
Arc::new(Float64Array::from(vec![0.0, 1.0, 2.0, 3.0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![2, 3, 3, 4])),
Arc::new(Float64Array::from(vec![3.0, 4.0, 5.0, 6.0])),
],
)
.unwrap(),
RecordBatch::try_new(
schema,
vec![
Arc::new(UInt32Array::from(vec![2, 3, 3, 4])),
Arc::new(Float64Array::from(vec![2.0, 3.0, 4.0, 5.0])),
],
)
.unwrap(),
],
)
}
fn new_spill_ctx(batch_size: usize, max_memory: usize) -> Arc<TaskContext> {
let session_config = SessionConfig::new().with_batch_size(batch_size);
let runtime = RuntimeEnvBuilder::new()
.with_memory_pool(Arc::new(FairSpillPool::new(max_memory)))
.build_arc()
.unwrap();
let task_ctx = TaskContext::default()
.with_session_config(session_config)
.with_runtime(runtime);
Arc::new(task_ctx)
}
fn migrated_hash_session_config(batch_size: usize) -> SessionConfig {
SessionConfig::new()
.with_batch_size(batch_size)
.set_bool("datafusion.execution.enable_migration_aggregate", true)
}
fn new_migrated_hash_ctx(batch_size: usize) -> Arc<TaskContext> {
Arc::new(
TaskContext::default()
.with_session_config(migrated_hash_session_config(batch_size)),
)
}
fn new_finite_memory_migrated_hash_ctx(
batch_size: usize,
max_memory: usize,
) -> Result<Arc<TaskContext>> {
let runtime = RuntimeEnvBuilder::default()
.with_memory_limit(max_memory, 1.0)
.build_arc()?;
Ok(Arc::new(
TaskContext::default()
.with_runtime(runtime)
.with_session_config(migrated_hash_session_config(batch_size)),
))
}
async fn check_grouping_sets(
input: Arc<dyn ExecutionPlan>,
spill: bool,
) -> Result<()> {
let input_schema = input.schema();
let grouping_set = PhysicalGroupBy::new(
vec![
(col("a", &input_schema)?, "a".to_string()),
(col("b", &input_schema)?, "b".to_string()),
],
vec![
(lit(ScalarValue::UInt32(None)), "a".to_string()),
(lit(ScalarValue::Float64(None)), "b".to_string()),
],
vec![
vec![false, true], vec![true, false], vec![false, false], ],
true,
);
let aggregates = vec![Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![lit(1i8)])
.schema(Arc::clone(&input_schema))
.alias("COUNT(1)")
.build()?,
)];
let task_ctx = if spill {
new_spill_ctx(4, 500)
} else {
Arc::new(TaskContext::default())
};
let partial_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
grouping_set.clone(),
aggregates.clone(),
vec![None],
input,
Arc::clone(&input_schema),
)?);
let result =
collect(partial_aggregate.execute(0, Arc::clone(&task_ctx))?).await?;
if spill {
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&result),
@r"
+---+-----+---------------+-----------------+
| a | b | __grouping_id | COUNT(1)[count] |
+---+-----+---------------+-----------------+
| | 1.0 | 2 | 1 |
| | 1.0 | 2 | 1 |
| | 2.0 | 2 | 1 |
| | 2.0 | 2 | 1 |
| | 3.0 | 2 | 1 |
| | 3.0 | 2 | 1 |
| | 4.0 | 2 | 1 |
| | 4.0 | 2 | 1 |
| 2 | | 1 | 1 |
| 2 | | 1 | 1 |
| 2 | 1.0 | 0 | 1 |
| 2 | 1.0 | 0 | 1 |
| 3 | | 1 | 1 |
| 3 | | 1 | 2 |
| 3 | 2.0 | 0 | 2 |
| 3 | 3.0 | 0 | 1 |
| 4 | | 1 | 1 |
| 4 | | 1 | 2 |
| 4 | 3.0 | 0 | 1 |
| 4 | 4.0 | 0 | 2 |
+---+-----+---------------+-----------------+
"
);
}
} else {
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&result),
@r"
+---+-----+---------------+-----------------+
| a | b | __grouping_id | COUNT(1)[count] |
+---+-----+---------------+-----------------+
| | 1.0 | 2 | 2 |
| | 2.0 | 2 | 2 |
| | 3.0 | 2 | 2 |
| | 4.0 | 2 | 2 |
| 2 | | 1 | 2 |
| 2 | 1.0 | 0 | 2 |
| 3 | | 1 | 3 |
| 3 | 2.0 | 0 | 2 |
| 3 | 3.0 | 0 | 1 |
| 4 | | 1 | 3 |
| 4 | 3.0 | 0 | 1 |
| 4 | 4.0 | 0 | 2 |
+---+-----+---------------+-----------------+
"
);
}
};
let merge = Arc::new(CoalescePartitionsExec::new(partial_aggregate));
let final_grouping_set = grouping_set.as_final();
let task_ctx = if spill {
new_spill_ctx(4, 3160)
} else {
task_ctx
};
let merged_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Final,
final_grouping_set,
aggregates,
vec![None],
merge,
input_schema,
)?);
let result = collect(merged_aggregate.execute(0, Arc::clone(&task_ctx))?).await?;
let batch = concat_batches(&result[0].schema(), &result)?;
assert_eq!(batch.num_columns(), 4);
assert_eq!(batch.num_rows(), 12);
allow_duplicates! {
assert_snapshot!(
batches_to_sort_string(&result),
@r"
+---+-----+---------------+----------+
| a | b | __grouping_id | COUNT(1) |
+---+-----+---------------+----------+
| | 1.0 | 2 | 2 |
| | 2.0 | 2 | 2 |
| | 3.0 | 2 | 2 |
| | 4.0 | 2 | 2 |
| 2 | | 1 | 2 |
| 2 | 1.0 | 0 | 2 |
| 3 | | 1 | 3 |
| 3 | 2.0 | 0 | 2 |
| 3 | 3.0 | 0 | 1 |
| 4 | | 1 | 3 |
| 4 | 3.0 | 0 | 1 |
| 4 | 4.0 | 0 | 2 |
+---+-----+---------------+----------+
"
);
}
let metrics = merged_aggregate.metrics().unwrap();
let output_rows = metrics.output_rows().unwrap();
assert_eq!(12, output_rows);
Ok(())
}
async fn check_aggregates(input: Arc<dyn ExecutionPlan>, spill: bool) -> Result<()> {
let input_schema = input.schema();
let grouping_set = PhysicalGroupBy::new(
vec![(col("a", &input_schema)?, "a".to_string())],
vec![],
vec![vec![false]],
false,
);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &input_schema)?])
.schema(Arc::clone(&input_schema))
.alias("AVG(b)")
.build()?,
)];
let task_ctx = if spill {
new_spill_ctx(2, 1600)
} else {
Arc::new(TaskContext::default())
};
let partial_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
grouping_set.clone(),
aggregates.clone(),
vec![None],
input,
Arc::clone(&input_schema),
)?);
let result =
collect(partial_aggregate.execute(0, Arc::clone(&task_ctx))?).await?;
if spill {
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+---------------+-------------+
| a | AVG(b)[count] | AVG(b)[sum] |
+---+---------------+-------------+
| 2 | 1 | 1.0 |
| 2 | 1 | 1.0 |
| 3 | 1 | 2.0 |
| 3 | 2 | 5.0 |
| 4 | 1 | 4.0 |
| 4 | 2 | 7.0 |
+---+---------------+-------------+
");
}
} else {
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+---------------+-------------+
| a | AVG(b)[count] | AVG(b)[sum] |
+---+---------------+-------------+
| 2 | 2 | 2.0 |
| 3 | 3 | 7.0 |
| 4 | 3 | 11.0 |
+---+---------------+-------------+
");
}
};
let merge = Arc::new(CoalescePartitionsExec::new(partial_aggregate));
let final_grouping_set = grouping_set.as_final();
let merged_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Final,
final_grouping_set,
aggregates,
vec![None],
merge,
input_schema,
)?);
let final_stats = StatisticsContext::new()
.compute(merged_aggregate.as_ref(), &StatisticsArgs::new())?;
assert!(final_stats.total_byte_size.get_value().is_some());
let task_ctx = if spill {
new_spill_ctx(2, 4640)
} else {
Arc::clone(&task_ctx)
};
let result = collect(merged_aggregate.execute(0, task_ctx)?).await?;
let batch = concat_batches(&result[0].schema(), &result)?;
assert_eq!(batch.num_columns(), 2);
assert_eq!(batch.num_rows(), 3);
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+--------------------+
| a | AVG(b) |
+---+--------------------+
| 2 | 1.0 |
| 3 | 2.3333333333333335 |
| 4 | 3.6666666666666665 |
+---+--------------------+
");
}
let metrics = merged_aggregate.metrics().unwrap();
let output_rows = metrics.output_rows().unwrap();
let spill_count = metrics.spill_count().unwrap();
let spilled_bytes = metrics.spilled_bytes().unwrap();
let spilled_rows = metrics.spilled_rows().unwrap();
assert_eq!(3, output_rows);
if spill {
assert!(spill_count > 0);
assert!(spilled_bytes > 0);
assert!(spilled_rows > 0);
} else {
assert_eq!(0, spill_count);
assert_eq!(0, spilled_bytes);
assert_eq!(0, spilled_rows);
}
Ok(())
}
#[derive(Debug)]
struct TestYieldingExec {
pub yield_first: bool,
cache: Arc<PlanProperties>,
}
impl TestYieldingExec {
fn new(yield_first: bool) -> Self {
let schema = some_data().0;
let cache = Self::compute_properties(schema);
Self {
yield_first,
cache: Arc::new(cache),
}
}
fn compute_properties(schema: SchemaRef) -> PlanProperties {
PlanProperties::new(
EquivalenceProperties::new(schema),
Partitioning::UnknownPartitioning(1),
EmissionType::Incremental,
Boundedness::Bounded,
)
}
}
impl DisplayAs for TestYieldingExec {
fn fmt_as(
&self,
t: DisplayFormatType,
f: &mut std::fmt::Formatter,
) -> std::fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
write!(f, "TestYieldingExec")
}
DisplayFormatType::TreeRender => {
write!(f, "")
}
}
}
}
impl ExecutionPlan for TestYieldingExec {
fn name(&self) -> &'static str {
"TestYieldingExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.cache
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![]
}
fn replace_children(
self: Arc<Self>,
_: Vec<Arc<dyn ExecutionPlan>>,
_: ReplaceChildrenOptions,
) -> Result<Arc<dyn ExecutionPlan>> {
internal_err!("Children cannot be replaced in {self:?}")
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> Result<Arc<dyn ExecutionPlan>> {
self.replace_children(
children,
ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
)
}
fn apply_expressions(
&self,
_f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
Ok(TreeNodeRecursion::Continue)
}
fn execute(
&self,
_partition: usize,
_context: Arc<TaskContext>,
) -> Result<SendableRecordBatchStream> {
let stream = if self.yield_first {
TestYieldingStream::New
} else {
TestYieldingStream::Yielded
};
Ok(Box::pin(stream))
}
fn statistics_from_inputs(
&self,
_input_stats: &[Arc<Statistics>],
args: &StatisticsArgs,
) -> Result<Arc<Statistics>> {
if args.partition().is_some() {
return Ok(Arc::new(Statistics::new_unknown(self.schema().as_ref())));
}
let (_, batches) = some_data();
Ok(Arc::new(common::compute_record_batch_statistics(
&[batches],
&self.schema(),
None,
)))
}
}
enum TestYieldingStream {
New,
Yielded,
ReturnedBatch1,
ReturnedBatch2,
}
impl Stream for TestYieldingStream {
type Item = Result<RecordBatch>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match &*self {
TestYieldingStream::New => {
*(self.as_mut()) = TestYieldingStream::Yielded;
cx.waker().wake_by_ref();
Poll::Pending
}
TestYieldingStream::Yielded => {
*(self.as_mut()) = TestYieldingStream::ReturnedBatch1;
Poll::Ready(Some(Ok(some_data().1[0].clone())))
}
TestYieldingStream::ReturnedBatch1 => {
*(self.as_mut()) = TestYieldingStream::ReturnedBatch2;
Poll::Ready(Some(Ok(some_data().1[1].clone())))
}
TestYieldingStream::ReturnedBatch2 => Poll::Ready(None),
}
}
}
impl RecordBatchStream for TestYieldingStream {
fn schema(&self) -> SchemaRef {
some_data().0
}
}
#[tokio::test]
async fn aggregate_source_not_yielding() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(false));
check_aggregates(input, false).await
}
#[tokio::test]
async fn aggregate_grouping_sets_source_not_yielding() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(false));
check_grouping_sets(input, false).await
}
#[tokio::test]
async fn aggregate_source_with_yielding() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(true));
check_aggregates(input, false).await
}
#[tokio::test]
async fn aggregate_grouping_sets_with_yielding() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(true));
check_grouping_sets(input, false).await
}
#[tokio::test]
async fn aggregate_source_not_yielding_with_spill() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(false));
check_aggregates(input, true).await
}
#[tokio::test]
async fn aggregate_grouping_sets_source_not_yielding_with_spill() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(false));
check_grouping_sets(input, true).await
}
#[tokio::test]
async fn aggregate_source_with_yielding_with_spill() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(true));
check_aggregates(input, true).await
}
#[tokio::test]
async fn aggregate_grouping_sets_with_yielding_with_spill() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(true));
check_grouping_sets(input, true).await
}
fn test_median_agg_expr(schema: SchemaRef) -> Result<AggregateFunctionExpr> {
AggregateExprBuilder::new(median_udaf(), vec![col("a", &schema)?])
.schema(schema)
.alias("MEDIAN(a)")
.build()
}
#[tokio::test]
async fn test_oom() -> Result<()> {
let input: Arc<dyn ExecutionPlan> = Arc::new(TestYieldingExec::new(true));
let input_schema = input.schema();
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(1, 1.0)
.build_arc()?;
let task_ctx = TaskContext::default().with_runtime(runtime);
let task_ctx = Arc::new(task_ctx);
let groups_none = PhysicalGroupBy::default();
let groups_some = PhysicalGroupBy::new(
vec![(col("a", &input_schema)?, "a".to_string())],
vec![],
vec![vec![false]],
false,
);
let aggregates_v0: Vec<Arc<AggregateFunctionExpr>> =
vec![Arc::new(test_median_agg_expr(Arc::clone(&input_schema))?)];
let aggregates_v2: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &input_schema)?])
.schema(Arc::clone(&input_schema))
.alias("AVG(b)")
.build()?,
)];
for (version, groups, aggregates) in [
(0, groups_none, aggregates_v0),
(2, groups_some, aggregates_v2),
] {
let n_aggr = aggregates.len();
let partial_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
groups,
aggregates,
vec![None; n_aggr],
Arc::clone(&input),
Arc::clone(&input_schema),
)?);
let stream = partial_aggregate.execute_typed(0, &task_ctx)?;
match version {
0 => {
assert!(matches!(stream, StreamType::AggregateStream(_)));
}
1 => {
assert!(matches!(stream, StreamType::GroupedHash(_)));
}
2 => {
assert!(matches!(stream, StreamType::SingleHash(_)));
}
_ => panic!("Unknown version: {version}"),
}
let stream: SendableRecordBatchStream = stream.into();
let err = collect(stream).await.unwrap_err();
let err = err.find_root();
assert!(
matches!(err, DataFusionError::ResourcesExhausted(_)),
"Wrong error type: {err}",
);
}
Ok(())
}
#[tokio::test]
async fn partial_grouped_aggregate_uses_raw_partial_stream() -> Result<()> {
let (schema, batches) = some_data();
let input = TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
let group_by =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![DataType::Float64],
vec![DataType::Int32],
DataType::Int64,
)));
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(udaf, vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("input_type_asserting(b)")
.build()?,
)];
let partial_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
aggregates.clone(),
vec![None],
input,
Arc::clone(&schema),
)?);
let task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.with_batch_size(2)
.set_bool("datafusion.execution.enable_migration_aggregate", true),
),
);
let partial_stream = partial_aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(partial_stream, StreamType::PartialHash(_)));
let fallback_task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.with_batch_size(2)
.set_bool("datafusion.execution.enable_migration_aggregate", false),
),
);
let stream = partial_aggregate.execute_typed(0, &fallback_task_ctx)?;
assert!(matches!(stream, StreamType::GroupedHash(_)));
let stream: SendableRecordBatchStream = partial_stream.into();
let batches = collect(stream).await?;
assert_eq!(
batches
.iter()
.map(RecordBatch::num_rows)
.collect::<Vec<_>>(),
vec![2, 1]
);
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
let merge = Arc::new(CoalescePartitionsExec::new(partial_aggregate));
let final_aggregate = AggregateExec::try_new(
AggregateMode::Final,
group_by.as_final(),
aggregates,
vec![None],
merge,
Arc::clone(&schema),
)?;
let final_stream = final_aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(final_stream, StreamType::FinalHash(_)));
let stream = final_aggregate.execute_typed(0, &fallback_task_ctx)?;
assert!(matches!(stream, StreamType::GroupedHash(_)));
let stream: SendableRecordBatchStream = final_stream.into();
let batches = collect(stream).await?;
assert_eq!(
batches
.iter()
.map(RecordBatch::num_rows)
.collect::<Vec<_>>(),
vec![2, 1]
);
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
Ok(())
}
#[tokio::test]
async fn partial_grouped_aggregate_materializes_before_slicing() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, false),
Field::new("value", DataType::Int32, false),
]));
let input_batches = vec![RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(Int32Array::from(vec![10, 20, 30])),
],
)?];
let input =
TestMemoryExec::try_new_exec(&[input_batches], Arc::clone(&schema), None)?;
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let udaf = Arc::new(AggregateUDF::from(NoFirstEmitUdaf::new()));
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(udaf, vec![col("value", &schema)?])
.schema(Arc::clone(&schema))
.alias("no_first_emit(value)")
.build()?,
)];
let aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggregates,
vec![None],
input,
Arc::clone(&schema),
)?);
let task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.with_batch_size(2)
.set_bool("datafusion.execution.enable_migration_aggregate", true)
.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(2.0)),
),
),
);
let stream = aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::PartialHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let batches = collect(stream).await?;
assert_eq!(
batches
.iter()
.map(RecordBatch::num_rows)
.collect::<Vec<_>>(),
vec![2, 1]
);
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
assert_snapshot!(batches_to_sort_string(&batches), @r"
+-----+-----------------------------+
| key | no_first_emit(value)[count] |
+-----+-----------------------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
+-----+-----------------------------+
");
Ok(())
}
#[tokio::test]
async fn limited_distinct_aggregate_uses_migrated_hash_streams() -> Result<()> {
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::UInt32, false)]));
let input_batches = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(UInt32Array::from(vec![1, 2, 1]))],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(UInt32Array::from(vec![3, 4]))],
)?,
];
let group_by =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.set_bool("datafusion.execution.enable_migration_aggregate", true),
),
);
let partial_input = TestMemoryExec::try_new_exec(
std::slice::from_ref(&input_batches),
Arc::clone(&schema),
None,
)?;
let partial_aggregate = Arc::new(
AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
vec![],
vec![],
partial_input,
Arc::clone(&schema),
)?
.with_limit_options(Some(LimitOptions::new(2))),
);
let partial_stream = partial_aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(partial_stream, StreamType::PartialHash(_)));
let stream: SendableRecordBatchStream = partial_stream.into();
let partial_output = collect(stream).await?;
assert_eq!(
partial_output
.iter()
.map(RecordBatch::num_rows)
.sum::<usize>(),
2
);
assert_snapshot!(batches_to_sort_string(&partial_output), @r"
+---+
| a |
+---+
| 1 |
| 2 |
+---+
");
let final_input =
TestMemoryExec::try_new_exec(&[input_batches], Arc::clone(&schema), None)?;
let final_aggregate = Arc::new(
AggregateExec::try_new(
AggregateMode::Final,
group_by.as_final(),
vec![],
vec![],
final_input,
Arc::clone(&schema),
)?
.with_limit_options(Some(LimitOptions::new(2))),
);
let final_stream = final_aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(final_stream, StreamType::FinalHash(_)));
let stream: SendableRecordBatchStream = final_stream.into();
let final_output = collect(stream).await?;
assert_eq!(
final_output
.iter()
.map(RecordBatch::num_rows)
.sum::<usize>(),
2
);
assert_snapshot!(batches_to_sort_string(&final_output), @r"
+---+
| a |
+---+
| 1 |
| 2 |
+---+
");
Ok(())
}
fn single_test_aggregate() -> Result<AggregateExec> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let input_batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 1, 3])),
Arc::new(Float64Array::from(vec![10.0, 20.0, 40.0, 30.0])),
],
)?;
let input = TestMemoryExec::try_new_exec(
&[vec![input_batch]],
Arc::clone(&schema),
None,
)?;
let group_by =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("SUM(b)")
.build()?,
)];
AggregateExec::try_new(
AggregateMode::Single,
group_by,
aggregates,
vec![None],
input,
schema,
)
}
#[tokio::test]
async fn single_aggregate_planning() -> Result<()> {
let single = single_test_aggregate()?;
let task_ctx = new_migrated_hash_ctx(2);
let stream = single.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::SingleHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;
assert_eq!(output.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
assert_snapshot!(batches_to_sort_string(&output), @r"
+---+--------+
| a | SUM(b) |
+---+--------+
| 1 | 50.0 |
| 2 | 20.0 |
| 3 | 30.0 |
+---+--------+
");
Ok(())
}
#[tokio::test]
async fn single_aggregate_with_memory_limit_planning() -> Result<()> {
let single = single_test_aggregate()?;
let task_ctx = new_finite_memory_migrated_hash_ctx(2, 1024 * 1024)?;
let stream = single.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::SingleHash(_)));
Ok(())
}
fn partial_reduce_test_aggregate() -> Result<AggregateExec> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("SUM(b)")
.build()?,
)];
let empty_input =
TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema), None)?;
let partial = AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
aggregates.clone(),
vec![None],
empty_input,
Arc::clone(&schema),
)?;
let partial_schema = partial.schema();
let partial_state_batch = RecordBatch::try_new(
Arc::clone(&partial_schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 1, 3])),
Arc::new(Float64Array::from(vec![10.0, 20.0, 40.0, 30.0])),
],
)?;
let partial_reduce_input = TestMemoryExec::try_new_exec(
&[vec![partial_state_batch]],
Arc::clone(&partial_schema),
None,
)?;
AggregateExec::try_new(
AggregateMode::PartialReduce,
group_by,
aggregates,
vec![None],
partial_reduce_input,
partial_schema,
)
}
#[tokio::test]
async fn partial_reduce_aggregate_planning() -> Result<()> {
let partial_reduce = partial_reduce_test_aggregate()?;
let task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.set_bool("datafusion.execution.enable_migration_aggregate", true),
),
);
let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::PartialReduceHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;
assert_eq!(output.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
Ok(())
}
#[tokio::test]
async fn partial_reduce_aggregate_with_memory_limit_planning() -> Result<()> {
let partial_reduce = partial_reduce_test_aggregate()?;
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(1, 1.0)
.build_arc()?;
let task_ctx =
Arc::new(
TaskContext::default()
.with_session_config(SessionConfig::new().set_bool(
"datafusion.execution.enable_migration_aggregate",
true,
))
.with_runtime(runtime),
);
let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::GroupedHash(_)));
Ok(())
}
#[tokio::test]
async fn ordered_partial_aggregate_planning() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("sort_col", DataType::Int32, false),
Field::new("group_col", DataType::Int32, false),
Field::new("value_col", DataType::Int64, false),
]));
let input_batches = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 1, 1])),
Arc::new(Int32Array::from(vec![10, 11, 10])),
Arc::new(Int64Array::from(vec![1, 1, 1])),
],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 2])),
Arc::new(Int32Array::from(vec![20, 21])),
Arc::new(Int64Array::from(vec![1, 1])),
],
)?,
];
let ordering = LexOrdering::new([PhysicalSortExpr::new_default(Arc::new(
Column::new("sort_col", 0),
))])
.unwrap();
let input = TestMemoryExec::try_new(&[input_batches], Arc::clone(&schema), None)?
.try_with_sort_information(vec![ordering])?;
let input = Arc::new(TestMemoryExec::update_cache(&Arc::new(input)));
let group_by = PhysicalGroupBy::new_single(vec![
(col("sort_col", &schema)?, "sort_col".to_string()),
(col("group_col", &schema)?, "group_col".to_string()),
]);
let aggr_expr = vec![Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![col("value_col", &schema)?])
.schema(Arc::clone(&schema))
.alias("COUNT(value_col)")
.build()?,
)];
let aggregate = AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
input,
Arc::clone(&schema),
)?;
assert!(matches!(
aggregate.input_order_mode(),
InputOrderMode::PartiallySorted(_)
));
let task_ctx = new_migrated_hash_ctx(2);
let stream = aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::OrderedPartialAggregate(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;
assert_snapshot!(batches_to_sort_string(&output), @r"
+----------+-----------+-------------------------+
| sort_col | group_col | COUNT(value_col)[count] |
+----------+-----------+-------------------------+
| 1 | 10 | 2 |
| 1 | 11 | 1 |
| 2 | 20 | 1 |
| 2 | 21 | 1 |
+----------+-----------+-------------------------+
");
let finite_memory_task_ctx = new_finite_memory_migrated_hash_ctx(2, 1024 * 1024)?;
let stream = aggregate.execute_typed(0, &finite_memory_task_ctx)?;
assert!(matches!(stream, StreamType::OrderedPartialAggregate(_)));
Ok(())
}
#[tokio::test]
async fn ordered_final_aggregate_planning() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, false),
Field::new("value", DataType::Int64, false),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![col("value", &schema)?])
.schema(Arc::clone(&schema))
.alias("COUNT(value)")
.build()?,
)];
let empty_input =
TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema), None)?;
let partial_aggregate = AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
aggr_expr.clone(),
vec![None],
empty_input,
Arc::clone(&schema),
)?;
let partial_schema = partial_aggregate.schema();
let partial_state_batch = RecordBatch::try_new(
Arc::clone(&partial_schema),
vec![
Arc::new(Int32Array::from(vec![1, 1, 2, 3])),
Arc::new(Int64Array::from(vec![2, 3, 5, 7])),
],
)?;
let ordering = LexOrdering::new([PhysicalSortExpr::new_default(Arc::new(
Column::new("key", 0),
))])
.unwrap();
let final_input =
TestMemoryExec::try_new(&[vec![partial_state_batch]], partial_schema, None)?
.try_with_sort_information(vec![ordering])?;
let final_input = Arc::new(TestMemoryExec::update_cache(&Arc::new(final_input)));
let final_aggregate = AggregateExec::try_new(
AggregateMode::Final,
group_by.as_final(),
aggr_expr,
vec![None],
final_input,
Arc::clone(&schema),
)?;
assert_eq!(final_aggregate.input_order_mode(), &InputOrderMode::Sorted);
let task_ctx = new_migrated_hash_ctx(2);
let stream = final_aggregate.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::OrderedFinalAggregate(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;
assert_snapshot!(batches_to_sort_string(&output), @r"
+-----+--------------+
| key | COUNT(value) |
+-----+--------------+
| 1 | 5 |
| 2 | 5 |
| 3 | 7 |
+-----+--------------+
");
let finite_memory_task_ctx = new_finite_memory_migrated_hash_ctx(2, 1024 * 1024)?;
let stream = final_aggregate.execute_typed(0, &finite_memory_task_ctx)?;
assert!(matches!(stream, StreamType::OrderedFinalAggregate(_)));
Ok(())
}
#[tokio::test]
async fn ordered_partial_aggregate_partially_sorted_no_emit_panic() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("sort_col", DataType::Int32, false),
Field::new("group_col", DataType::Int32, false),
Field::new("value_col", DataType::Int64, false),
]));
let n = 256;
let batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1; n])),
Arc::new(Int32Array::from((0..n as i32).collect::<Vec<_>>())),
Arc::new(Int64Array::from(vec![1; n])),
],
)?;
let ordering = LexOrdering::new([PhysicalSortExpr::new_default(Arc::new(
Column::new("sort_col", 0),
))])
.unwrap();
let input = TestMemoryExec::try_new(&[vec![batch]], Arc::clone(&schema), None)?
.try_with_sort_information(vec![ordering])?;
let input = Arc::new(TestMemoryExec::update_cache(&Arc::new(input)));
let aggregate = AggregateExec::try_new(
AggregateMode::Partial,
PhysicalGroupBy::new_single(vec![
(col("sort_col", &schema)?, "sort_col".to_string()),
(col("group_col", &schema)?, "group_col".to_string()),
]),
vec![Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![col("value_col", &schema)?])
.schema(Arc::clone(&schema))
.alias("count_value")
.build()?,
)],
vec![None],
input,
Arc::clone(&schema),
)?;
assert!(matches!(
aggregate.input_order_mode(),
InputOrderMode::PartiallySorted(_)
));
let runtime = RuntimeEnvBuilder::default()
.with_memory_limit(4096, 1.0)
.build_arc()?;
let session_config = SessionConfig::new().with_batch_size(128).set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::UInt64(Some(u64::MAX)),
);
let task_ctx = Arc::new(
TaskContext::default()
.with_runtime(runtime)
.with_session_config(session_config),
);
let mut stream: SendableRecordBatchStream =
OrderedPartialAggregateStream::new(&aggregate, &task_ctx, 0)?.into_stream();
while let Some(result) = stream.next().await {
if let Err(e) = result {
if e.to_string().contains("Resources exhausted") {
break;
}
return Err(e);
}
}
Ok(())
}
#[tokio::test]
async fn test_drop_cancel_without_groups() -> Result<()> {
let task_ctx = Arc::new(TaskContext::default());
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Float64, true)]));
let groups = PhysicalGroupBy::default();
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("a", &schema)?])
.schema(Arc::clone(&schema))
.alias("AVG(a)")
.build()?,
)];
let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
let refs = blocking_exec.refs();
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups.clone(),
aggregates.clone(),
vec![None],
blocking_exec,
schema,
)?);
let fut = crate::collect(aggregate_exec, task_ctx);
let mut fut = fut.boxed();
assert_is_pending(&mut fut);
drop(fut);
assert_strong_count_converges_to_zero(refs).await;
Ok(())
}
#[tokio::test]
async fn test_drop_cancel_with_groups() -> Result<()> {
let task_ctx = Arc::new(TaskContext::default());
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Float64, true),
Field::new("b", DataType::Float64, true),
]));
let groups =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("AVG(b)")
.build()?,
)];
let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
let refs = blocking_exec.refs();
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups,
aggregates.clone(),
vec![None],
blocking_exec,
schema,
)?);
let fut = crate::collect(aggregate_exec, task_ctx);
let mut fut = fut.boxed();
assert_is_pending(&mut fut);
drop(fut);
assert_strong_count_converges_to_zero(refs).await;
Ok(())
}
#[tokio::test]
async fn run_first_last_multi_partitions() -> Result<()> {
for is_first_acc in [false, true] {
for spill in [false, true] {
first_last_multi_partitions(is_first_acc, spill, 5000).await?
}
}
Ok(())
}
fn test_first_value_agg_expr(
schema: &Schema,
sort_options: SortOptions,
) -> Result<Arc<AggregateFunctionExpr>> {
let order_bys = vec![PhysicalSortExpr {
expr: col("b", schema)?,
options: sort_options,
}];
let args = [col("b", schema)?];
AggregateExprBuilder::new(first_value_udaf(), args.to_vec())
.order_by(order_bys)
.schema(Arc::new(schema.clone()))
.alias(String::from("first_value(b) ORDER BY [b ASC NULLS LAST]"))
.build()
.map(Arc::new)
}
fn test_last_value_agg_expr(
schema: &Schema,
sort_options: SortOptions,
) -> Result<Arc<AggregateFunctionExpr>> {
let order_bys = vec![PhysicalSortExpr {
expr: col("b", schema)?,
options: sort_options,
}];
let args = [col("b", schema)?];
AggregateExprBuilder::new(last_value_udaf(), args.to_vec())
.order_by(order_bys)
.schema(Arc::new(schema.clone()))
.alias(String::from("last_value(b) ORDER BY [b ASC NULLS LAST]"))
.build()
.map(Arc::new)
}
fn first_value_agg_expr(
schema: &SchemaRef,
column: &str,
alias: &str,
human_display: Option<&str>,
human_display_alias: Option<&str>,
) -> Result<AggregateFunctionExpr> {
let mut builder =
AggregateExprBuilder::new(first_value_udaf(), vec![col(column, schema)?])
.order_by(vec![PhysicalSortExpr {
expr: col(column, schema)?,
options: SortOptions::new(false, false),
}])
.schema(Arc::clone(schema))
.alias(alias);
if let Some(human_display) = human_display {
builder = builder.human_display(human_display);
}
if let Some(human_display_alias) = human_display_alias {
builder = builder.human_display_alias(human_display_alias);
}
builder.build()
}
#[test]
fn test_reverse_expr_preserves_aliased_human_display() -> Result<()> {
let schema = create_test_schema()?;
let agg = first_value_agg_expr(
&schema,
"b",
"agg",
Some("first_value(b) ORDER BY [b ASC NULLS LAST]"),
Some("agg"),
)?;
let reversed = agg.reverse_expr().expect("expected reverse expr");
assert_eq!(reversed.name(), "agg");
assert_eq!(reversed.human_display_alias(), Some("agg"));
assert_eq!(
format_tree_aggregate_expr(&reversed),
"last_value(b) ORDER BY [b DESC NULLS FIRST] as agg"
);
assert_eq!(
reversed.human_display(),
Some("last_value(b) ORDER BY [b DESC NULLS FIRST]")
);
Ok(())
}
#[test]
fn test_reverse_expr_does_not_rewrite_column_names_in_human_display() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new(
"first_value_col",
DataType::Int32,
true,
)]));
let agg = first_value_agg_expr(
&schema,
"first_value_col",
"agg",
Some(
"first_value(first_value_col) ORDER BY [first_value_col ASC NULLS LAST]",
),
Some("agg"),
)?;
let reversed = agg.reverse_expr().expect("expected reverse expr");
assert_eq!(reversed.name(), "agg");
assert_eq!(
reversed.human_display(),
Some(
"last_value(first_value_col) ORDER BY [first_value_col DESC NULLS FIRST]"
)
);
assert_eq!(
format_tree_aggregate_expr(&reversed),
"last_value(first_value_col) ORDER BY [first_value_col DESC NULLS FIRST] as agg"
);
Ok(())
}
#[test]
fn test_empty_human_display_is_treated_as_absent() -> Result<()> {
let schema = create_test_schema()?;
let agg = first_value_agg_expr(&schema, "b", "agg", Some(""), None)?;
assert_eq!(agg.human_display(), None);
assert_eq!(format_tree_aggregate_expr(&agg), "agg");
Ok(())
}
#[test]
fn test_human_display_alias_must_match_name() -> Result<()> {
let schema = create_test_schema()?;
let error = first_value_agg_expr(
&schema,
"b",
"agg",
Some("first_value(b) ORDER BY [b ASC NULLS LAST]"),
Some("other_alias"),
)
.unwrap_err();
assert!(
error
.to_string()
.contains("aggregate human_display_alias must match")
);
Ok(())
}
#[test]
fn test_reverse_expr_preserves_non_aliased_display_path() -> Result<()> {
let schema = create_test_schema()?;
let agg = first_value_agg_expr(
&schema,
"b",
"first_value(b) ORDER BY [b ASC NULLS LAST]",
None,
None,
)?;
let reversed = agg.reverse_expr().expect("expected reverse expr");
assert_eq!(
reversed.name(),
"last_value(b) ORDER BY [b DESC NULLS FIRST]"
);
assert_eq!(reversed.human_display(), None);
Ok(())
}
async fn first_last_multi_partitions(
is_first_acc: bool,
spill: bool,
max_memory: usize,
) -> Result<()> {
let task_ctx = if spill {
new_spill_ctx(2, max_memory)
} else {
Arc::new(TaskContext::default())
};
let (schema, data) = some_data_v2();
let partition1 = data[0].clone();
let partition2 = data[1].clone();
let partition3 = data[2].clone();
let partition4 = data[3].clone();
let groups =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let sort_options = SortOptions {
descending: false,
nulls_first: false,
};
let aggregates: Vec<Arc<AggregateFunctionExpr>> = if is_first_acc {
vec![test_first_value_agg_expr(&schema, sort_options)?]
} else {
vec![test_last_value_agg_expr(&schema, sort_options)?]
};
let memory_exec = TestMemoryExec::try_new_exec(
&[
vec![partition1],
vec![partition2],
vec![partition3],
vec![partition4],
],
Arc::clone(&schema),
None,
)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups.clone(),
aggregates.clone(),
vec![None],
memory_exec,
Arc::clone(&schema),
)?);
let coalesce = Arc::new(CoalescePartitionsExec::new(aggregate_exec))
as Arc<dyn ExecutionPlan>;
let aggregate_final = Arc::new(AggregateExec::try_new(
AggregateMode::Final,
groups,
aggregates.clone(),
vec![None],
coalesce,
schema,
)?) as Arc<dyn ExecutionPlan>;
let result = crate::collect(aggregate_final, task_ctx).await?;
if is_first_acc {
allow_duplicates! {
assert_snapshot!(batches_to_string(&result), @r"
+---+--------------------------------------------+
| a | first_value(b) ORDER BY [b ASC NULLS LAST] |
+---+--------------------------------------------+
| 2 | 0.0 |
| 3 | 1.0 |
| 4 | 3.0 |
+---+--------------------------------------------+
");
}
} else {
allow_duplicates! {
assert_snapshot!(batches_to_string(&result), @r"
+---+-------------------------------------------+
| a | last_value(b) ORDER BY [b ASC NULLS LAST] |
+---+-------------------------------------------+
| 2 | 3.0 |
| 3 | 5.0 |
| 4 | 6.0 |
+---+-------------------------------------------+
");
}
};
Ok(())
}
#[tokio::test]
async fn test_get_finest_requirements() -> Result<()> {
let test_schema = create_test_schema()?;
let options = SortOptions {
descending: false,
nulls_first: false,
};
let col_a = &col("a", &test_schema)?;
let col_b = &col("b", &test_schema)?;
let col_c = &col("c", &test_schema)?;
let mut eq_properties = EquivalenceProperties::new(Arc::clone(&test_schema));
eq_properties.add_equal_conditions(Arc::clone(col_a), Arc::clone(col_b))?;
let order_by_exprs = vec![
vec![],
vec![PhysicalSortExpr {
expr: Arc::clone(col_a),
options,
}],
vec![
PhysicalSortExpr {
expr: Arc::clone(col_a),
options,
},
PhysicalSortExpr {
expr: Arc::clone(col_b),
options,
},
PhysicalSortExpr {
expr: Arc::clone(col_c),
options,
},
],
vec![
PhysicalSortExpr {
expr: Arc::clone(col_a),
options,
},
PhysicalSortExpr {
expr: Arc::clone(col_b),
options,
},
],
];
let common_requirement = vec![
PhysicalSortRequirement::new(Arc::clone(col_a), Some(options)),
PhysicalSortRequirement::new(Arc::clone(col_c), Some(options)),
];
let mut aggr_exprs = order_by_exprs
.into_iter()
.map(|order_by_expr| {
AggregateExprBuilder::new(array_agg_udaf(), vec![Arc::clone(col_a)])
.alias("a")
.order_by(order_by_expr)
.schema(Arc::clone(&test_schema))
.build()
.map(Arc::new)
.unwrap()
})
.collect::<Vec<_>>();
let group_by = PhysicalGroupBy::new_single(vec![]);
let result = get_finer_aggregate_exprs_requirement(
&mut aggr_exprs,
&group_by,
&eq_properties,
&AggregateMode::Partial,
)?;
assert_eq!(result, common_requirement);
Ok(())
}
#[test]
fn test_agg_exec_same_schema() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Float32, true),
Field::new("b", DataType::Float32, true),
]));
let col_a = col("a", &schema)?;
let option_desc = SortOptions {
descending: true,
nulls_first: true,
};
let groups = PhysicalGroupBy::new_single(vec![(col_a, "a".to_string())]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![
test_first_value_agg_expr(&schema, option_desc)?,
test_last_value_agg_expr(&schema, option_desc)?,
];
let blocking_exec = Arc::new(BlockingExec::new(Arc::clone(&schema), 1));
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups,
aggregates,
vec![None, None],
Arc::clone(&blocking_exec) as Arc<dyn ExecutionPlan>,
schema,
)?);
let new_agg = Arc::clone(&aggregate_exec).replace_children(
vec![blocking_exec],
ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
)?;
assert_eq!(new_agg.schema(), aggregate_exec.schema());
Ok(())
}
#[tokio::test]
async fn test_agg_exec_group_by_const() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Float32, true),
Field::new("b", DataType::Float32, true),
Field::new("const", DataType::Int32, false),
]));
let col_a = col("a", &schema)?;
let col_b = col("b", &schema)?;
let const_expr = Arc::new(Literal::new(ScalarValue::Int32(Some(1))));
let groups = PhysicalGroupBy::new(
vec![
(col_a, "a".to_string()),
(col_b, "b".to_string()),
(const_expr, "const".to_string()),
],
vec![
(
Arc::new(Literal::new(ScalarValue::Float32(None))),
"a".to_string(),
),
(
Arc::new(Literal::new(ScalarValue::Float32(None))),
"b".to_string(),
),
(
Arc::new(Literal::new(ScalarValue::Int32(None))),
"const".to_string(),
),
],
vec![
vec![false, true, true],
vec![true, false, true],
vec![true, true, false],
],
true,
);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![
AggregateExprBuilder::new(count_udaf(), vec![lit(1)])
.schema(Arc::clone(&schema))
.alias("1")
.build()
.map(Arc::new)?,
];
let input_batches = (0..4)
.map(|_| {
let a = Arc::new(Float32Array::from(vec![0.; 8192]));
let b = Arc::new(Float32Array::from(vec![0.; 8192]));
let c = Arc::new(Int32Array::from(vec![1; 8192]));
RecordBatch::try_new(Arc::clone(&schema), vec![a, b, c]).unwrap()
})
.collect();
let input =
TestMemoryExec::try_new_exec(&[input_batches], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
groups,
aggregates.clone(),
vec![None],
input,
schema,
)?);
let output =
collect(aggregate_exec.execute(0, Arc::new(TaskContext::default()))?).await?;
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&output), @r"
+-----+-----+-------+---------------+-------+
| a | b | const | __grouping_id | 1 |
+-----+-----+-------+---------------+-------+
| | | 1 | 6 | 32768 |
| | 0.0 | | 5 | 32768 |
| 0.0 | | | 3 | 32768 |
+-----+-----+-------+---------------+-------+
");
}
Ok(())
}
#[tokio::test]
async fn test_agg_exec_struct_of_dicts() -> Result<()> {
let batch = RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new(
"labels".to_string(),
DataType::Struct(
vec![
Field::new(
"a".to_string(),
DataType::Dictionary(
Box::new(DataType::Int32),
Box::new(DataType::Utf8),
),
true,
),
Field::new(
"b".to_string(),
DataType::Dictionary(
Box::new(DataType::Int32),
Box::new(DataType::Utf8),
),
true,
),
]
.into(),
),
false,
),
Field::new("value", DataType::UInt64, false),
])),
vec![
Arc::new(StructArray::from(vec![
(
Arc::new(Field::new(
"a".to_string(),
DataType::Dictionary(
Box::new(DataType::Int32),
Box::new(DataType::Utf8),
),
true,
)),
Arc::new(
vec![Some("a"), None, Some("a")]
.into_iter()
.collect::<DictionaryArray<Int32Type>>(),
) as ArrayRef,
),
(
Arc::new(Field::new(
"b".to_string(),
DataType::Dictionary(
Box::new(DataType::Int32),
Box::new(DataType::Utf8),
),
true,
)),
Arc::new(
vec![Some("b"), Some("c"), Some("b")]
.into_iter()
.collect::<DictionaryArray<Int32Type>>(),
) as ArrayRef,
),
])),
Arc::new(UInt64Array::from(vec![1, 1, 1])),
],
)
.expect("Failed to create RecordBatch");
let group_by = PhysicalGroupBy::new_single(vec![(
col("labels", &batch.schema())?,
"labels".to_string(),
)]);
let aggr_expr = vec![
AggregateExprBuilder::new(sum_udaf(), vec![col("value", &batch.schema())?])
.schema(Arc::clone(&batch.schema()))
.alias(String::from("SUM(value)"))
.build()
.map(Arc::new)?,
];
let input = TestMemoryExec::try_new_exec(
&[vec![batch.clone()]],
Arc::<Schema>::clone(&batch.schema()),
None,
)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::FinalPartitioned,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
batch.schema(),
)?);
let session_config = SessionConfig::default();
let ctx = TaskContext::default().with_session_config(session_config);
let output = collect(aggregate_exec.execute(0, Arc::new(ctx))?).await?;
allow_duplicates! {
assert_snapshot!(batches_to_string(&output), @r"
+--------------+------------+
| labels | SUM(value) |
+--------------+------------+
| {a: a, b: b} | 2 |
| {a: , b: c} | 1 |
+--------------+------------+
");
}
Ok(())
}
#[tokio::test]
async fn test_skip_aggregation_after_first_batch() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, true),
Field::new("val", DataType::Int32, true),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("val", &schema)?])
.schema(Arc::clone(&schema))
.alias(String::from("COUNT(val)"))
.build()
.map(Arc::new)?,
];
let input_data = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
];
let input =
TestMemoryExec::try_new_exec(&[input_data], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
schema,
)?);
let mut session_config = SessionConfig::default();
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::Int64(Some(2)),
);
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(0.1)),
);
let ctx = Arc::new(TaskContext::default().with_session_config(session_config));
let stream: SendableRecordBatchStream = Box::pin(
GroupedHashAggregateStream::new(aggregate_exec.as_ref(), &ctx, 0)?,
);
let output = collect(stream).await?;
allow_duplicates! {
assert_snapshot!(batches_to_string(&output), @r"
+-----+-------------------+
| key | COUNT(val)[count] |
+-----+-------------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
+-----+-------------------+
");
}
Ok(())
}
#[tokio::test]
async fn test_skip_aggregation_after_threshold() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, true),
Field::new("val", DataType::Int32, true),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("val", &schema)?])
.schema(Arc::clone(&schema))
.alias(String::from("COUNT(val)"))
.build()
.map(Arc::new)?,
];
let input_data = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
];
let input =
TestMemoryExec::try_new_exec(&[input_data], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
schema,
)?);
let mut session_config = SessionConfig::default();
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::Int64(Some(5)),
);
session_config = session_config.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(0.1)),
);
let ctx = Arc::new(TaskContext::default().with_session_config(session_config));
let stream: SendableRecordBatchStream = Box::pin(
GroupedHashAggregateStream::new(aggregate_exec.as_ref(), &ctx, 0)?,
);
let output = collect(stream).await?;
allow_duplicates! {
assert_snapshot!(batches_to_string(&output), @r"
+-----+-------------------+
| key | COUNT(val)[count] |
+-----+-------------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 2 |
| 4 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
+-----+-------------------+
");
}
Ok(())
}
#[tokio::test]
async fn test_partial_hash_stream_skip_aggregation_after_first_batch() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, true),
Field::new("val", DataType::Int32, true),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("val", &schema)?])
.schema(Arc::clone(&schema))
.alias(String::from("COUNT(val)"))
.build()
.map(Arc::new)?,
];
let input_data = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
];
let input =
TestMemoryExec::try_new_exec(&[input_data], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
schema,
)?);
let session_config = SessionConfig::default()
.set_bool("datafusion.execution.enable_migration_aggregate", true)
.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::Int64(Some(2)),
)
.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(0.1)),
);
let ctx = Arc::new(TaskContext::default().with_session_config(session_config));
let output = collect(aggregate_exec.execute(0, Arc::clone(&ctx))?).await?;
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&output), @r"
+-----+-------------------+
| key | COUNT(val)[count] |
+-----+-------------------+
| 1 | 1 |
| 2 | 1 |
| 2 | 1 |
| 3 | 1 |
| 3 | 1 |
| 4 | 1 |
+-----+-------------------+
");
}
let metrics = aggregate_exec.metrics().unwrap();
let skipped_rows = metrics
.sum_by_name("skipped_aggregation_rows")
.map(|m| m.as_usize())
.unwrap_or(0);
assert_eq!(skipped_rows, 3);
Ok(())
}
#[tokio::test]
async fn test_partial_hash_stream_skip_aggregation_after_threshold() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, true),
Field::new("val", DataType::Int32, true),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("val", &schema)?])
.schema(Arc::clone(&schema))
.alias(String::from("COUNT(val)"))
.build()
.map(Arc::new)?,
];
let input_data = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![2, 3, 4])),
Arc::new(Int32Array::from(vec![0, 0, 0])),
],
)
.unwrap(),
];
let input =
TestMemoryExec::try_new_exec(&[input_data], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
schema,
)?);
let session_config = SessionConfig::default()
.set_bool("datafusion.execution.enable_migration_aggregate", true)
.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::Int64(Some(5)),
)
.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(0.1)),
);
let ctx = Arc::new(TaskContext::default().with_session_config(session_config));
let output = collect(aggregate_exec.execute(0, Arc::clone(&ctx))?).await?;
allow_duplicates! {
assert_snapshot!(batches_to_sort_string(&output), @r"
+-----+-------------------+
| key | COUNT(val)[count] |
+-----+-------------------+
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
| 3 | 1 |
| 3 | 2 |
| 4 | 1 |
| 4 | 1 |
+-----+-------------------+
");
}
let metrics = aggregate_exec.metrics().unwrap();
let skipped_rows = metrics
.sum_by_name("skipped_aggregation_rows")
.map(|m| m.as_usize())
.unwrap_or(0);
assert_eq!(skipped_rows, 3);
Ok(())
}
#[tokio::test]
async fn test_skip_aggregation_disabled_at_threshold_one() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("key", DataType::Int32, true),
Field::new("val", DataType::Int32, true),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("key", &schema)?, "key".to_string())]);
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("val", &schema)?])
.schema(Arc::clone(&schema))
.alias(String::from("COUNT(val)"))
.build()
.map(Arc::new)?,
];
let input_data = vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])),
Arc::new(Int32Array::from(vec![0, 0, 0, 0, 0])),
],
)
.unwrap(),
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![6, 7, 8, 9, 10])),
Arc::new(Int32Array::from(vec![0, 0, 0, 0, 0])),
],
)
.unwrap(),
];
let input =
TestMemoryExec::try_new_exec(&[input_data], Arc::clone(&schema), None)?;
let aggregate_exec = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
group_by,
aggr_expr,
vec![None],
Arc::clone(&input) as Arc<dyn ExecutionPlan>,
schema,
)?);
let session_config = SessionConfig::default()
.set(
"datafusion.execution.skip_partial_aggregation_probe_rows_threshold",
&ScalarValue::Int64(Some(1)),
)
.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
&ScalarValue::Float64(Some(1.0)),
);
let ctx = TaskContext::default().with_session_config(session_config);
collect(aggregate_exec.execute(0, Arc::new(ctx))?).await?;
let metrics = aggregate_exec.metrics().unwrap();
let skipped_rows = metrics
.sum_by_name("skipped_aggregation_rows")
.map(|m| m.as_usize())
.unwrap_or(0);
assert_eq!(
skipped_rows, 0,
"threshold=1.0 should disable skip aggregation, but {skipped_rows} rows were skipped"
);
Ok(())
}
#[test]
fn group_exprs_nullable() -> Result<()> {
let input_schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Float32, false),
Field::new("b", DataType::Float32, false),
]));
let aggr_expr = vec![
AggregateExprBuilder::new(count_udaf(), vec![col("a", &input_schema)?])
.schema(Arc::clone(&input_schema))
.alias("COUNT(a)")
.build()
.map(Arc::new)?,
];
let grouping_set = PhysicalGroupBy::new(
vec![
(col("a", &input_schema)?, "a".to_string()),
(col("b", &input_schema)?, "b".to_string()),
],
vec![
(lit(ScalarValue::Float32(None)), "a".to_string()),
(lit(ScalarValue::Float32(None)), "b".to_string()),
],
vec![
vec![false, true], vec![false, false], ],
true,
);
let aggr_schema = create_schema(
&input_schema,
&grouping_set,
&aggr_expr,
AggregateMode::Final,
)?;
let expected_schema = Schema::new(vec![
Field::new("a", DataType::Float32, false),
Field::new("b", DataType::Float32, true),
Field::new("__grouping_id", DataType::UInt8, false),
Field::new("COUNT(a)", DataType::Int64, false),
]);
assert_eq!(aggr_schema, expected_schema);
Ok(())
}
async fn run_test_with_spill_pool_if_necessary(
pool_size: usize,
expect_spill: bool,
) -> Result<()> {
fn create_record_batch(
schema: &Arc<Schema>,
data: (Vec<u32>, Vec<f64>),
) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
Arc::clone(schema),
vec![
Arc::new(UInt32Array::from(data.0)),
Arc::new(Float64Array::from(data.1)),
],
)?)
}
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let group_keys = [2, 3, 4, 4].repeat(1_000);
let values = [1.0, 2.0, 3.0, 4.0].repeat(1_000);
let batches = vec![
create_record_batch(&schema, (group_keys.clone(), values.clone()))?,
create_record_batch(&schema, (group_keys, values))?,
];
let plan: Arc<dyn ExecutionPlan> =
TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
let grouping_set = PhysicalGroupBy::new(
vec![(col("a", &schema)?, "a".to_string())],
vec![],
vec![vec![false]],
false,
);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![
Arc::new(
AggregateExprBuilder::new(min_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("MIN(b)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("AVG(b)")
.build()?,
),
];
let single_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
grouping_set,
aggregates,
vec![None, None],
plan,
Arc::clone(&schema),
)?);
let batch_size = 2;
let memory_pool = Arc::new(FairSpillPool::new(pool_size));
let task_ctx = Arc::new(
TaskContext::default()
.with_session_config(SessionConfig::new().with_batch_size(batch_size))
.with_runtime(Arc::new(
RuntimeEnvBuilder::new()
.with_memory_pool(memory_pool)
.build()?,
)),
);
let result = collect(single_aggregate.execute(0, Arc::clone(&task_ctx))?).await?;
assert_spill_count_metric(expect_spill, single_aggregate);
allow_duplicates! {
assert_snapshot!(batches_to_string(&result), @r"
+---+--------+--------+
| a | MIN(b) | AVG(b) |
+---+--------+--------+
| 2 | 1.0 | 1.0 |
| 3 | 2.0 | 2.0 |
| 4 | 3.0 | 3.5 |
+---+--------+--------+
");
}
Ok(())
}
fn assert_spill_count_metric(
expect_spill: bool,
single_aggregate: Arc<AggregateExec>,
) {
if let Some(metrics_set) = single_aggregate.metrics() {
let mut spill_count = 0;
for metric in metrics_set.iter() {
if let MetricValue::SpillCount(count) = metric.value() {
spill_count = count.value();
break;
}
}
if expect_spill && spill_count == 0 {
panic!(
"Expected spill but SpillCount metric not found or SpillCount was 0."
);
} else if !expect_spill && spill_count > 0 {
panic!(
"Expected no spill but found SpillCount metric with value greater than 0."
);
}
} else {
panic!("No metrics returned from the operator; cannot verify spilling.");
}
}
#[tokio::test]
async fn test_aggregate_with_spill_if_necessary() -> Result<()> {
run_test_with_spill_pool_if_necessary(20_000, true).await?;
run_test_with_spill_pool_if_necessary(200_000, false).await?;
Ok(())
}
#[tokio::test]
async fn test_grouped_aggregation_respects_memory_limit() -> Result<()> {
fn create_record_batch(
schema: &Arc<Schema>,
data: (Vec<u32>, Vec<f64>),
) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
Arc::clone(schema),
vec![
Arc::new(UInt32Array::from(data.0)),
Arc::new(Float64Array::from(data.1)),
],
)?)
}
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let batches = vec![
create_record_batch(&schema, (vec![2, 3, 4, 4], vec![1.0, 2.0, 3.0, 4.0]))?,
create_record_batch(&schema, (vec![2, 3, 4, 4], vec![1.0, 2.0, 3.0, 4.0]))?,
];
let plan: Arc<dyn ExecutionPlan> =
TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
let proj = ProjectionExec::try_new(
vec![
ProjectionExpr::new(lit("0"), "l".to_string()),
ProjectionExpr::new_from_expression(col("a", &schema)?, &schema)?,
ProjectionExpr::new_from_expression(col("b", &schema)?, &schema)?,
],
plan,
)?;
let plan: Arc<dyn ExecutionPlan> = Arc::new(proj);
let schema = plan.schema();
let grouping_set = PhysicalGroupBy::new(
vec![
(col("l", &schema)?, "l".to_string()),
(col("a", &schema)?, "a".to_string()),
],
vec![],
vec![vec![false, false]],
false,
);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![
Arc::new(
AggregateExprBuilder::new(min_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("MIN(b)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("AVG(b)")
.build()?,
),
];
let single_aggregate = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
grouping_set,
aggregates,
vec![None, None],
plan,
Arc::clone(&schema),
)?);
let batch_size = 2;
let memory_pool = Arc::new(FairSpillPool::new(2000));
let task_ctx = Arc::new(
TaskContext::default()
.with_session_config(SessionConfig::new().with_batch_size(batch_size))
.with_runtime(Arc::new(
RuntimeEnvBuilder::new()
.with_memory_pool(memory_pool)
.build()?,
)),
);
let result = collect(single_aggregate.execute(0, Arc::clone(&task_ctx))?).await;
match result {
Ok(result) => {
assert_spill_count_metric(true, single_aggregate);
allow_duplicates! {
assert_snapshot!(batches_to_string(&result), @r"
+---+---+--------+--------+
| l | a | MIN(b) | AVG(b) |
+---+---+--------+--------+
| 0 | 2 | 1.0 | 1.0 |
| 0 | 3 | 2.0 | 2.0 |
| 0 | 4 | 3.0 | 3.5 |
+---+---+--------+--------+
");
}
}
Err(e) => assert!(matches!(e, DataFusionError::ResourcesExhausted(_))),
}
Ok(())
}
#[tokio::test]
async fn test_aggregate_statistics_edge_cases() -> Result<()> {
use datafusion_common::ColumnStatistics;
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, false),
Field::new("b", DataType::Float64, false),
]));
let absent_byte_stats = Statistics {
num_rows: Precision::Exact(100),
total_byte_size: Precision::Absent,
column_statistics: vec![
ColumnStatistics::new_unknown(),
ColumnStatistics::new_unknown(),
],
};
let agg = build_test_aggregate(
&schema,
absent_byte_stats,
PhysicalGroupBy::default(),
None,
)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(stats.total_byte_size, Precision::Absent);
let zero_row_stats = Statistics {
num_rows: Precision::Exact(0),
total_byte_size: Precision::Exact(0),
column_statistics: vec![
ColumnStatistics::new_unknown(),
ColumnStatistics::new_unknown(),
],
};
let agg_zero = build_test_aggregate(
&schema,
zero_row_stats,
PhysicalGroupBy::default(),
None,
)?;
let stats_zero =
StatisticsContext::new().compute(&agg_zero, &StatisticsArgs::new())?;
assert_eq!(stats_zero.total_byte_size, Precision::Absent);
let single_input =
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>;
let single_agg_zero = AggregateExec::try_new(
AggregateMode::Single,
PhysicalGroupBy::default(),
vec![count_a_aggregate(&schema)?],
vec![None],
single_input,
Arc::clone(&schema),
)?;
assert_eq!(
single_agg_zero
.properties()
.output_partitioning()
.partition_count(),
1
);
let single_stats_zero =
StatisticsContext::new().compute(&single_agg_zero, &StatisticsArgs::new())?;
assert_eq!(single_stats_zero.num_rows, Precision::Exact(1));
Ok(())
}
#[tokio::test]
async fn test_aggregate_statistics_empty_input_with_grouping_sets() -> Result<()> {
let schema = empty_grouping_sets_test_schema();
let grouped = build_test_aggregate(
&schema,
empty_input_statistics(),
simple_group_by(&schema, &["a"]),
None,
)?;
let stats = StatisticsContext::new().compute(&grouped, &StatisticsArgs::new())?;
assert_eq!(stats.num_rows, Precision::Exact(0));
let with_empty_set = build_test_aggregate(
&schema,
empty_input_statistics(),
grouping_sets_with_empty(&schema, 1)?,
None,
)?;
let stats =
StatisticsContext::new().compute(&with_empty_set, &StatisticsArgs::new())?;
assert_eq!(stats.num_rows, Precision::Exact(1));
let with_duplicate_empty_sets = build_test_aggregate(
&schema,
empty_input_statistics(),
grouping_sets_with_empty(&schema, 2)?,
None,
)?;
let stats = StatisticsContext::new()
.compute(&with_duplicate_empty_sets, &StatisticsArgs::new())?;
assert_eq!(stats.num_rows, Precision::Exact(2));
Ok(())
}
#[tokio::test]
async fn test_aggregate_statistics_empty_input_partial_mode_scaling() -> Result<()> {
let schema = empty_grouping_sets_test_schema();
let input = Arc::new(RepartitionExec::try_new(
Arc::new(StatisticsExec::new(
empty_input_statistics(),
(*schema).clone(),
)),
Partitioning::RoundRobinBatch(4),
)?) as Arc<dyn ExecutionPlan>;
let agg = AggregateExec::try_new(
AggregateMode::Partial,
grouping_sets_with_empty(&schema, 1)?,
vec![count_a_aggregate(&schema)?],
vec![None],
input,
Arc::clone(&schema),
)?;
assert_eq!(agg.properties().output_partitioning().partition_count(), 4);
let context = StatisticsContext::new();
assert_eq!(
context.compute(&agg, &StatisticsArgs::new())?.num_rows,
Precision::Exact(4)
);
let partition_statistics =
context.compute(&agg, &StatisticsArgs::new().with_partition(Some(0)))?;
assert_eq!(partition_statistics.num_rows, Precision::Inexact(1));
let group_column = &partition_statistics.column_statistics[0];
let typed_null = Precision::Inexact(ScalarValue::Int32(None));
assert_eq!(group_column.min_value, typed_null);
assert_eq!(group_column.max_value, typed_null);
assert_eq!(group_column.distinct_count, Precision::Inexact(0));
assert_eq!(group_column.null_count, Precision::Inexact(1));
Ok(())
}
#[tokio::test]
async fn test_aggregate_statistics_empty_input_nullifies_group_columns() -> Result<()>
{
let schema = empty_grouping_sets_test_schema();
let mut input_statistics = empty_input_statistics();
input_statistics.column_statistics[0] = ColumnStatistics {
null_count: Precision::Exact(0),
max_value: Precision::Exact(ScalarValue::Int32(Some(5))),
min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
sum_value: Precision::Absent,
distinct_count: Precision::Exact(1),
byte_size: Precision::Absent,
};
let agg = build_test_aggregate(
&schema,
input_statistics,
grouping_sets_with_empty(&schema, 1)?,
None,
)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(stats.num_rows, Precision::Exact(1));
let group_column = &stats.column_statistics[0];
let typed_null = Precision::Exact(ScalarValue::Int32(None));
assert_eq!(group_column.min_value, typed_null);
assert_eq!(group_column.max_value, typed_null);
assert_eq!(group_column.distinct_count, Precision::Exact(0));
assert_eq!(group_column.null_count, Precision::Exact(1));
Ok(())
}
fn empty_grouping_sets_test_schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, false),
Field::new("b", DataType::Float64, false),
]))
}
fn empty_input_statistics() -> Statistics {
Statistics {
num_rows: Precision::Exact(0),
total_byte_size: Precision::Exact(0),
column_statistics: vec![
ColumnStatistics::new_unknown(),
ColumnStatistics::new_unknown(),
],
}
}
fn grouping_sets_with_empty(
schema: &SchemaRef,
empty_sets: usize,
) -> Result<PhysicalGroupBy> {
let mut groups = vec![vec![false]];
groups.resize(1 + empty_sets, vec![true]);
Ok(PhysicalGroupBy::new(
vec![(col("a", schema)?, "a".to_string())],
vec![(lit(ScalarValue::Int32(None)), "a".to_string())],
groups,
true,
))
}
fn build_test_aggregate(
schema: &SchemaRef,
stats: Statistics,
group_by: PhysicalGroupBy,
limit: Option<LimitOptions>,
) -> Result<AggregateExec> {
build_test_aggregate_with_mode(
schema,
stats,
group_by,
limit,
AggregateMode::Final,
)
}
fn count_a_aggregate(schema: &SchemaRef) -> Result<Arc<AggregateFunctionExpr>> {
Ok(Arc::new(
AggregateExprBuilder::new(count_udaf(), vec![col("a", schema)?])
.schema(Arc::clone(schema))
.alias("COUNT(a)")
.build()?,
))
}
fn build_test_aggregate_with_mode(
schema: &SchemaRef,
stats: Statistics,
group_by: PhysicalGroupBy,
limit: Option<LimitOptions>,
mode: AggregateMode,
) -> Result<AggregateExec> {
let input = Arc::new(StatisticsExec::new(stats, (**schema).clone()))
as Arc<dyn ExecutionPlan>;
let mut agg = AggregateExec::try_new(
mode,
group_by,
vec![count_a_aggregate(schema)?],
vec![None],
input,
Arc::clone(schema),
)?;
if let Some(limit) = limit {
agg = agg.with_limit_options(Some(limit));
}
Ok(agg)
}
fn simple_group_by(schema: &SchemaRef, cols: &[&str]) -> PhysicalGroupBy {
if cols.is_empty() {
PhysicalGroupBy::default()
} else {
PhysicalGroupBy::new_single(
cols.iter()
.map(|name| {
(
col(name, schema).unwrap() as Arc<dyn PhysicalExpr>,
name.to_string(),
)
})
.collect(),
)
}
}
#[test]
fn test_aggregate_cardinality_estimation() -> Result<()> {
use datafusion_common::ColumnStatistics;
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
]));
struct TestCase {
name: &'static str,
input_rows: Precision<usize>,
col_a_stats: ColumnStatistics,
col_b_stats: ColumnStatistics,
group_by_cols: Vec<&'static str>,
limit_options: Option<LimitOptions>,
expected_num_rows: Precision<usize>,
}
let cases = vec![
TestCase {
name: "single group-by col with NDV tightens estimate",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(500),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(500),
},
TestCase {
name: "multi-col group-by multiplies NDVs",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics {
distinct_count: Precision::Exact(50),
..ColumnStatistics::new_unknown()
},
group_by_cols: vec!["a", "b"],
limit_options: None,
expected_num_rows: Precision::Inexact(5_000),
},
TestCase {
name: "NDV product capped by input rows",
input_rows: Precision::Exact(200),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics {
distinct_count: Precision::Exact(50),
..ColumnStatistics::new_unknown()
},
group_by_cols: vec!["a", "b"],
limit_options: None,
expected_num_rows: Precision::Inexact(200),
},
TestCase {
name: "null adjustment adds +1 per column",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(99),
null_count: Precision::Exact(10),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(100),
},
TestCase {
name: "null adjustment on multiple columns",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(99),
null_count: Precision::Exact(5),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics {
distinct_count: Precision::Exact(49),
null_count: Precision::Exact(3),
..ColumnStatistics::new_unknown()
},
group_by_cols: vec!["a", "b"],
limit_options: None,
expected_num_rows: Precision::Inexact(5_000),
},
TestCase {
name: "zero null_count means no adjustment",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
null_count: Precision::Exact(0),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(100),
},
TestCase {
name: "bail out when one group-by col lacks NDV",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a", "b"],
limit_options: None,
expected_num_rows: Precision::Inexact(1_000_000),
},
TestCase {
name: "bail out when all group-by cols lack NDV",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(1_000_000),
},
TestCase {
name: "TopK limit caps output rows",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(10),
},
TestCase {
name: "NDV + TopK limit: min(NDV, limit) when NDV < limit",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(5),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(5),
},
TestCase {
name: "NDV + TopK limit: min(NDV, limit) when limit < NDV",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(500),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(10),
},
TestCase {
name: "absent input rows without limit stays absent",
input_rows: Precision::Absent,
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Absent,
},
TestCase {
name: "absent input rows with TopK limit gives inexact(limit)",
input_rows: Precision::Absent,
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(10),
},
TestCase {
name: "no group-by cols (Final mode) returns Exact(1)",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec![],
limit_options: None,
expected_num_rows: Precision::Exact(1),
},
TestCase {
name: "one input row returns Exact(1)",
input_rows: Precision::Exact(1),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(1),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Exact(1),
},
TestCase {
name: "zero input rows returns Exact(0)",
input_rows: Precision::Exact(0),
col_a_stats: ColumnStatistics::new_unknown(),
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Exact(0),
},
TestCase {
name: "inexact NDV still used for estimation",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Inexact(200),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(200),
},
TestCase {
name: "inexact NDV combined with limit",
input_rows: Precision::Exact(1_000_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Inexact(200),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(10),
},
TestCase {
name: "all-null column contributes 1 to the product, not 0",
input_rows: Precision::Exact(1_000),
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(0),
null_count: Precision::Exact(1_000),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics {
distinct_count: Precision::Exact(50),
..ColumnStatistics::new_unknown()
},
group_by_cols: vec!["a", "b"],
limit_options: None,
expected_num_rows: Precision::Inexact(50),
},
TestCase {
name: "absent num_rows falls back to NDV estimate",
input_rows: Precision::Absent,
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: None,
expected_num_rows: Precision::Inexact(100),
},
TestCase {
name: "absent num_rows with NDV and limit returns min(ndv, limit)",
input_rows: Precision::Absent,
col_a_stats: ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
col_b_stats: ColumnStatistics::new_unknown(),
group_by_cols: vec!["a"],
limit_options: Some(LimitOptions::new(10)),
expected_num_rows: Precision::Inexact(10),
},
];
for case in cases {
let input_stats = Statistics {
num_rows: case.input_rows,
total_byte_size: Precision::Inexact(1_000_000),
column_statistics: vec![
case.col_a_stats.clone(),
case.col_b_stats.clone(),
],
};
let group_by = simple_group_by(&schema, &case.group_by_cols);
let agg =
build_test_aggregate(&schema, input_stats, group_by, case.limit_options)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(
stats.num_rows, case.expected_num_rows,
"FAILED: '{}' — expected {:?}, got {:?}",
case.name, case.expected_num_rows, stats.num_rows
);
}
Ok(())
}
#[test]
fn test_aggregate_stats_distinct_count_propagation() -> Result<()> {
use datafusion_common::ColumnStatistics;
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
]));
let input_stats = Statistics {
num_rows: Precision::Exact(1000),
total_byte_size: Precision::Inexact(10000),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Exact(100),
null_count: Precision::Exact(5),
..ColumnStatistics::new_unknown()
},
ColumnStatistics::new_unknown(),
],
};
let agg = build_test_aggregate(
&schema,
input_stats,
simple_group_by(&schema, &["a"]),
None,
)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(
stats.column_statistics[0].distinct_count,
Precision::Exact(100),
"distinct_count should be propagated from child for group-by columns"
);
Ok(())
}
#[test]
fn test_aggregate_stats_grouping_sets() -> Result<()> {
use datafusion_common::ColumnStatistics;
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
]));
let input_stats = Statistics {
num_rows: Precision::Exact(1_000_000),
total_byte_size: Precision::Inexact(1_000_000),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
ColumnStatistics {
distinct_count: Precision::Exact(50),
..ColumnStatistics::new_unknown()
},
],
};
let grouping_set = PhysicalGroupBy::new(
vec![
(col("a", &schema)? as Arc<dyn PhysicalExpr>, "a".to_string()),
(col("b", &schema)? as Arc<dyn PhysicalExpr>, "b".to_string()),
],
vec![
(lit(ScalarValue::Int32(None)), "a".to_string()),
(lit(ScalarValue::Int32(None)), "b".to_string()),
],
vec![
vec![false, true], vec![true, false], vec![false, false], ],
true,
);
let agg = build_test_aggregate(&schema, input_stats, grouping_set, None)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(
stats.num_rows,
Precision::Inexact(5_150),
"grouping sets should sum per-set NDV products"
);
Ok(())
}
#[tokio::test]
async fn test_aggregate_stats_duplicate_empty_grouping_sets() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
let duplicate_empty_grouping_sets =
PhysicalGroupBy::new(vec![], vec![], vec![vec![], vec![]], true);
let single_input =
Arc::new(EmptyExec::new(Arc::clone(&schema))) as Arc<dyn ExecutionPlan>;
let single_agg = AggregateExec::try_new(
AggregateMode::Single,
duplicate_empty_grouping_sets.clone(),
vec![count_a_aggregate(&schema)?],
vec![None],
single_input,
Arc::clone(&schema),
)?;
assert_eq!(
StatisticsContext::new()
.compute(&single_agg, &StatisticsArgs::new())?
.num_rows,
Precision::Exact(2)
);
let partial_input =
Arc::new(EmptyExec::new(Arc::clone(&schema)).with_partitions(2))
as Arc<dyn ExecutionPlan>;
let partial_agg = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
duplicate_empty_grouping_sets,
vec![count_a_aggregate(&schema)?],
vec![None],
partial_input,
Arc::clone(&schema),
)?);
assert_eq!(
partial_agg
.properties()
.output_partitioning()
.partition_count(),
2
);
let task_ctx = Arc::new(TaskContext::default());
for partition in 0..2 {
assert_eq!(
StatisticsContext::new()
.compute(
partial_agg.as_ref(),
&StatisticsArgs::new().with_partition(Some(partition)),
)?
.num_rows,
Precision::Exact(2)
);
let result =
collect(partial_agg.execute(partition, Arc::clone(&task_ctx))?).await?;
assert_eq!(result.iter().map(RecordBatch::num_rows).sum::<usize>(), 2);
}
assert_eq!(
StatisticsContext::new()
.compute(partial_agg.as_ref(), &StatisticsArgs::new())?
.num_rows,
Precision::Exact(4)
);
Ok(())
}
#[test]
fn test_aggregate_stats_non_column_expr_bails_out() -> Result<()> {
use datafusion_common::ColumnStatistics;
use datafusion_expr::Operator;
use datafusion_physical_expr::expressions::BinaryExpr;
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
]));
let input_stats = Statistics {
num_rows: Precision::Exact(1_000_000),
total_byte_size: Precision::Inexact(1_000_000),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Exact(100),
..ColumnStatistics::new_unknown()
},
ColumnStatistics {
distinct_count: Precision::Exact(50),
..ColumnStatistics::new_unknown()
},
],
};
let expr_a_plus_b: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
col("a", &schema)?,
Operator::Plus,
col("b", &schema)?,
));
let group_by =
PhysicalGroupBy::new_single(vec![(expr_a_plus_b, "a+b".to_string())]);
let agg = build_test_aggregate(&schema, input_stats, group_by, None)?;
let stats = StatisticsContext::new().compute(&agg, &StatisticsArgs::new())?;
assert_eq!(
stats.num_rows,
Precision::Inexact(1_000_000),
"non-column group-by expression should bail out to input_rows"
);
Ok(())
}
#[tokio::test]
async fn test_order_is_retained_when_spilling() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int64, false),
Field::new("b", DataType::Int64, false),
Field::new("c", DataType::Int64, false),
]));
let batches = vec![vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![2])),
Arc::new(Int64Array::from(vec![2])),
Arc::new(Int64Array::from(vec![1])),
],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![1])),
Arc::new(Int64Array::from(vec![1])),
Arc::new(Int64Array::from(vec![1])),
],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![0])),
Arc::new(Int64Array::from(vec![0])),
Arc::new(Int64Array::from(vec![1])),
],
)?,
]];
let scan = TestMemoryExec::try_new(&batches, Arc::clone(&schema), None)?;
let scan = scan.try_with_sort_information(vec![
LexOrdering::new([PhysicalSortExpr::new(
col("b", schema.as_ref())?,
SortOptions::default().desc(),
)])
.unwrap(),
])?;
let aggr = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
PhysicalGroupBy::new(
vec![
(col("b", schema.as_ref())?, "b".to_string()),
(col("c", schema.as_ref())?, "c".to_string()),
],
vec![],
vec![vec![false, false]],
false,
),
vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("c", schema.as_ref())?])
.schema(Arc::clone(&schema))
.alias("SUM(c)")
.build()?,
)],
vec![None],
Arc::new(scan) as Arc<dyn ExecutionPlan>,
Arc::clone(&schema),
)?);
let task_ctx = new_spill_ctx(1, 600);
let result = collect(aggr.execute(0, Arc::clone(&task_ctx))?).await?;
assert_spill_count_metric(true, aggr);
allow_duplicates! {
assert_snapshot!(batches_to_string(&result), @r"
+---+---+--------+
| b | c | SUM(c) |
+---+---+--------+
| 2 | 1 | 1 |
| 1 | 1 | 1 |
| 0 | 1 | 1 |
+---+---+--------+
");
}
Ok(())
}
#[tokio::test]
async fn test_sort_reservation_fails_during_spill() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("g", DataType::Int64, false),
Field::new("a", DataType::Float64, false),
Field::new("b", DataType::Float64, false),
Field::new("c", DataType::Float64, false),
Field::new("d", DataType::Float64, false),
Field::new("e", DataType::Float64, false),
]));
let batches = vec![vec![
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![1])),
Arc::new(Float64Array::from(vec![10.0])),
Arc::new(Float64Array::from(vec![20.0])),
Arc::new(Float64Array::from(vec![30.0])),
Arc::new(Float64Array::from(vec![40.0])),
Arc::new(Float64Array::from(vec![50.0])),
],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![2])),
Arc::new(Float64Array::from(vec![11.0])),
Arc::new(Float64Array::from(vec![21.0])),
Arc::new(Float64Array::from(vec![31.0])),
Arc::new(Float64Array::from(vec![41.0])),
Arc::new(Float64Array::from(vec![51.0])),
],
)?,
RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from(vec![3])),
Arc::new(Float64Array::from(vec![12.0])),
Arc::new(Float64Array::from(vec![22.0])),
Arc::new(Float64Array::from(vec![32.0])),
Arc::new(Float64Array::from(vec![42.0])),
Arc::new(Float64Array::from(vec![52.0])),
],
)?,
]];
let scan = TestMemoryExec::try_new(&batches, Arc::clone(&schema), None)?;
let aggr = Arc::new(AggregateExec::try_new(
AggregateMode::Single,
PhysicalGroupBy::new(
vec![(col("g", schema.as_ref())?, "g".to_string())],
vec![],
vec![vec![false]],
false,
),
vec![
Arc::new(
AggregateExprBuilder::new(
avg_udaf(),
vec![col("a", schema.as_ref())?],
)
.schema(Arc::clone(&schema))
.alias("AVG(a)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(
avg_udaf(),
vec![col("b", schema.as_ref())?],
)
.schema(Arc::clone(&schema))
.alias("AVG(b)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(
avg_udaf(),
vec![col("c", schema.as_ref())?],
)
.schema(Arc::clone(&schema))
.alias("AVG(c)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(
avg_udaf(),
vec![col("d", schema.as_ref())?],
)
.schema(Arc::clone(&schema))
.alias("AVG(d)")
.build()?,
),
Arc::new(
AggregateExprBuilder::new(
avg_udaf(),
vec![col("e", schema.as_ref())?],
)
.schema(Arc::clone(&schema))
.alias("AVG(e)")
.build()?,
),
],
vec![None, None, None, None, None],
Arc::new(scan) as Arc<dyn ExecutionPlan>,
Arc::clone(&schema),
)?);
let task_ctx = new_spill_ctx(1, 500);
let result = collect(aggr.execute(0, Arc::clone(&task_ctx))?).await;
match &result {
Ok(_) => panic!("Expected ResourcesExhausted error but query succeeded"),
Err(e) => {
let root = e.find_root();
assert!(
matches!(root, DataFusionError::ResourcesExhausted(_)),
"Expected ResourcesExhausted, got: {root}",
);
}
}
Ok(())
}
async fn evaluate_partial_reduce(
groups: PhysicalGroupBy,
aggregates: Vec<Arc<AggregateFunctionExpr>>,
partition_1_and_2_batches: [Vec<RecordBatch>; 2],
) -> Result<Vec<RecordBatch>> {
let schema = partition_1_and_2_batches
.iter()
.flatten()
.next()
.expect("Must have at least 1 batch")
.schema();
let [partition_1, partition_2] = partition_1_and_2_batches;
let input1 =
TestMemoryExec::try_new_exec(&[partition_1], Arc::clone(&schema), None)?;
let partial1 = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups.clone(),
aggregates.clone(),
vec![None; aggregates.len()],
input1,
Arc::clone(&schema),
)?);
let input2 =
TestMemoryExec::try_new_exec(&[partition_2], Arc::clone(&schema), None)?;
let partial2 = Arc::new(AggregateExec::try_new(
AggregateMode::Partial,
groups.clone(),
aggregates.clone(),
vec![None; aggregates.len()],
input2,
Arc::clone(&schema),
)?);
let task_ctx = Arc::new(TaskContext::default());
let partial_result1 =
crate::collect(Arc::clone(&partial1) as _, Arc::clone(&task_ctx)).await?;
let partial_result2 =
crate::collect(Arc::clone(&partial2) as _, Arc::clone(&task_ctx)).await?;
let partial_schema = partial1.schema();
let combined_input = TestMemoryExec::try_new_exec(
&[partial_result1, partial_result2],
Arc::clone(&partial_schema),
None,
)?;
let coalesced = Arc::new(CoalescePartitionsExec::new(combined_input));
let partial_reduce = Arc::new(AggregateExec::try_new(
AggregateMode::PartialReduce,
groups.clone(),
aggregates.clone(),
vec![None; aggregates.len()],
coalesced,
Arc::clone(&partial_schema),
)?);
assert_eq!(partial_reduce.schema(), partial_schema);
let reduce_result =
crate::collect(Arc::clone(&partial_reduce) as _, Arc::clone(&task_ctx))
.await?;
let final_input = TestMemoryExec::try_new_exec(
&[reduce_result],
Arc::clone(&partial_schema),
None,
)?;
let final_agg = Arc::new(AggregateExec::try_new(
AggregateMode::Final,
groups.clone(),
aggregates.clone(),
vec![None; aggregates.len()],
final_input,
Arc::clone(&partial_schema),
)?);
let result = crate::collect(final_agg, Arc::clone(&task_ctx)).await?;
Ok(result)
}
async fn run_partial_reduce_pipeline<F>(
build_aggregates: F,
) -> Result<Vec<RecordBatch>>
where
F: FnOnce(&Arc<Schema>) -> Result<Vec<Arc<AggregateFunctionExpr>>>,
{
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 3])),
Arc::new(Float64Array::from(vec![10.0, 20.0, 30.0])),
],
)?;
let batch2 = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 3])),
Arc::new(Float64Array::from(vec![40.0, 50.0, 60.0])),
],
)?;
let groups =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let aggregates = build_aggregates(&schema)?;
evaluate_partial_reduce(groups, aggregates, [vec![batch1], vec![batch2]]).await
}
#[tokio::test]
async fn test_partial_reduce_with_single_state_field_and_single_input_arg()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
Ok(vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", schema)?])
.schema(Arc::clone(schema))
.alias("SUM(b)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+--------+
| a | SUM(b) |
+---+--------+
| 1 | 50.0 |
| 2 | 70.0 |
| 3 | 90.0 |
+---+--------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_multiple_state_fields_and_single_input_arg()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
Ok(vec![Arc::new(
AggregateExprBuilder::new(avg_udaf(), vec![col("b", schema)?])
.schema(Arc::clone(schema))
.alias("AVG(b)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+--------+
| a | AVG(b) |
+---+--------+
| 1 | 25.0 |
| 2 | 35.0 |
| 3 | 45.0 |
+---+--------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_more_state_fields_than_input_args() -> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
Ok(vec![Arc::new(
AggregateExprBuilder::new(
approx_percentile_cont_udaf(),
vec![col("b", schema)?, lit(0.75f32)],
)
.schema(Arc::clone(schema))
.alias("approx_percentile_cont(b, 0.75)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+---------------------------------+
| a | approx_percentile_cont(b, 0.75) |
+---+---------------------------------+
| 1 | 40.0 |
| 2 | 50.0 |
| 3 | 60.0 |
+---+---------------------------------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_single_state_field_and_single_input_arg_using_unique_types()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![DataType::Float64],
vec![DataType::Int32],
DataType::Int64,
)));
Ok(vec![Arc::new(
AggregateExprBuilder::new(udaf, vec![col("b", schema)?])
.schema(Arc::clone(schema))
.alias("input_type_asserting(b)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+-------------------------+
| a | input_type_asserting(b) |
+---+-------------------------+
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
+---+-------------------------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_multiple_state_fields_and_single_input_arg_using_unique_types()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![DataType::Float64],
vec![DataType::Int32, DataType::Utf8],
DataType::Int64,
)));
Ok(vec![Arc::new(
AggregateExprBuilder::new(udaf, vec![col("b", schema)?])
.schema(Arc::clone(schema))
.alias("input_type_asserting(b)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+-------------------------+
| a | input_type_asserting(b) |
+---+-------------------------+
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
+---+-------------------------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_more_state_fields_than_input_args_using_unique_types()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![DataType::Float64, DataType::Date32],
vec![DataType::Int32, DataType::Utf8, DataType::Boolean],
DataType::Int64,
)));
Ok(vec![Arc::new(
AggregateExprBuilder::new(
udaf,
vec![col("b", schema)?, lit(ScalarValue::Date32(Some(1)))],
)
.schema(Arc::clone(schema))
.alias("input_type_asserting(b, lit)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+------------------------------+
| a | input_type_asserting(b, lit) |
+---+------------------------------+
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
+---+------------------------------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_5_input_args_and_2_state_fields_using_unique_types()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![
DataType::Float64,
DataType::Date32,
DataType::UInt16,
DataType::Boolean,
DataType::Int32,
],
vec![DataType::Utf8, DataType::Int64],
DataType::Float32,
)));
Ok(vec![Arc::new(
AggregateExprBuilder::new(
udaf,
vec![
col("b", schema)?,
lit(ScalarValue::Date32(Some(1))),
lit(ScalarValue::UInt16(Some(1))),
lit(ScalarValue::Boolean(Some(false))),
lit(ScalarValue::Int32(Some(1))),
],
)
.schema(Arc::clone(schema))
.alias("input_type_asserting(b, l1, l2, l3, l4)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+-----------------------------------------+
| a | input_type_asserting(b, l1, l2, l3, l4) |
+---+-----------------------------------------+
| 1 | 0.0 |
| 2 | 0.0 |
| 3 | 0.0 |
+---+-----------------------------------------+
");
Ok(())
}
#[tokio::test]
async fn test_partial_reduce_with_2_input_args_and_5_state_fields_using_unique_types()
-> Result<()> {
let result = run_partial_reduce_pipeline(|schema| {
let udaf = Arc::new(AggregateUDF::from(InputTypeAssertingUdaf::new(
vec![DataType::Float64, DataType::Date32],
vec![
DataType::Boolean,
DataType::Int32,
DataType::Utf8,
DataType::Int64,
DataType::UInt16,
],
DataType::Float32,
)));
Ok(vec![Arc::new(
AggregateExprBuilder::new(
udaf,
vec![col("b", schema)?, lit(ScalarValue::Date32(Some(1)))],
)
.schema(Arc::clone(schema))
.alias("input_type_asserting(b, lit)")
.build()?,
)])
})
.await?;
assert_snapshot!(batches_to_sort_string(&result), @r"
+---+------------------------------+
| a | input_type_asserting(b, lit) |
+---+------------------------------+
| 1 | 0.0 |
| 2 | 0.0 |
| 3 | 0.0 |
+---+------------------------------+
");
Ok(())
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct InputTypeAssertingUdaf {
signature: Signature,
input_types: Vec<DataType>,
state_types: Vec<DataType>,
output_type: DataType,
}
fn assert_data_types(
what: &str,
expected: &[DataType],
actual: &[DataType],
) -> Result<()> {
if actual != expected {
return internal_err!(
"InputTypeAssertingUdaf: {} expected types {:?} but got {:?} — a regression is leaking the wrong types into the accumulator contract",
what,
expected,
actual
);
}
Ok(())
}
fn zero_scalar_for(dt: &DataType) -> Result<ScalarValue> {
match dt {
DataType::Boolean => Ok(ScalarValue::Boolean(Some(false))),
DataType::Int32 => Ok(ScalarValue::Int32(Some(0))),
DataType::Int64 => Ok(ScalarValue::Int64(Some(0))),
DataType::UInt16 => Ok(ScalarValue::UInt16(Some(0))),
DataType::Float32 => Ok(ScalarValue::Float32(Some(0.0))),
DataType::Utf8 => Ok(ScalarValue::Utf8(Some(String::new()))),
other => internal_err!(
"InputTypeAssertingUdaf: no zero ScalarValue registered for {other:?} \
— extend `zero_scalar_for` when adding a new state/output type"
),
}
}
impl InputTypeAssertingUdaf {
fn new(
input_types: Vec<DataType>,
state_types: Vec<DataType>,
output_type: DataType,
) -> Self {
assert!(
all_pairwise_distinct(&input_types, &state_types, &output_type),
"InputTypeAssertingUdaf::new: input ({input_types:?}), state \
({state_types:?}), and output ({output_type:?}) types must be \
pairwise-disjoint to avoid accidental passes",
);
Self {
signature: Signature::exact(input_types.clone(), Volatility::Immutable),
input_types,
state_types,
output_type,
}
}
}
fn all_pairwise_distinct(
inputs: &[DataType],
states: &[DataType],
output: &DataType,
) -> bool {
let mut seen = HashSet::new();
for dt in inputs
.iter()
.chain(states.iter())
.chain(std::iter::once(output))
{
if !seen.insert(dt) {
return false;
}
}
true
}
impl AggregateUDFImpl for InputTypeAssertingUdaf {
fn name(&self) -> &str {
"input_type_asserting"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
assert_data_types("return_type(arg_types)", &self.input_types, arg_types)?;
Ok(self.output_type.clone())
}
fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
let actual: Vec<DataType> = args
.input_fields
.iter()
.map(|f| f.data_type().clone())
.collect();
assert_data_types(
"state_fields(args.input_fields)",
&self.input_types,
&actual,
)?;
Ok(self
.state_types
.iter()
.enumerate()
.map(|(i, dt)| {
Field::new(format!("{}[s{i}]", args.name), dt.clone(), true).into()
})
.collect())
}
fn accumulator(&self, acc_args: AccumulatorArgs) -> Result<Box<dyn Accumulator>> {
let actual: Vec<DataType> = acc_args
.expr_fields
.iter()
.map(|f| f.data_type().clone())
.collect();
assert_data_types(
"accumulator(acc_args.expr_fields)",
&self.input_types,
&actual,
)?;
Ok(Box::new(InputTypeAssertingAccumulator {
input_types: self.input_types.clone(),
state_types: self.state_types.clone(),
output_type: self.output_type.clone(),
}))
}
}
#[derive(Debug)]
struct InputTypeAssertingAccumulator {
input_types: Vec<DataType>,
state_types: Vec<DataType>,
output_type: DataType,
}
impl Accumulator for InputTypeAssertingAccumulator {
fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
let actual: Vec<DataType> =
values.iter().map(|a| a.data_type().clone()).collect();
assert_data_types("update_batch(values)", &self.input_types, &actual)
}
fn evaluate(&mut self) -> Result<ScalarValue> {
zero_scalar_for(&self.output_type)
}
fn size(&self) -> usize {
size_of_val(self)
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
self.state_types.iter().map(zero_scalar_for).collect()
}
fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> {
let actual: Vec<DataType> =
states.iter().map(|a| a.data_type().clone()).collect();
assert_data_types("merge_batch(states)", &self.state_types, &actual)
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct NoFirstEmitUdaf {
signature: Signature,
}
impl NoFirstEmitUdaf {
fn new() -> Self {
Self {
signature: Signature::exact(vec![DataType::Int32], Volatility::Immutable),
}
}
}
impl AggregateUDFImpl for NoFirstEmitUdaf {
fn name(&self) -> &str {
"no_first_emit"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
Ok(DataType::Int64)
}
fn state_fields(&self, args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
Ok(vec![Arc::new(Field::new(
format!("{}[count]", args.name),
DataType::Int64,
false,
))])
}
fn accumulator(
&self,
_acc_args: AccumulatorArgs,
) -> Result<Box<dyn Accumulator>> {
Ok(Box::new(NoFirstEmitAccumulator))
}
fn groups_accumulator_supported(&self, _args: AccumulatorArgs) -> bool {
true
}
fn create_groups_accumulator(
&self,
_args: AccumulatorArgs,
) -> Result<Box<dyn GroupsAccumulator>> {
Ok(Box::new(NoFirstEmitGroupsAccumulator { counts: vec![] }))
}
}
#[derive(Debug)]
struct NoFirstEmitAccumulator;
impl Accumulator for NoFirstEmitAccumulator {
fn update_batch(&mut self, _values: &[ArrayRef]) -> Result<()> {
Ok(())
}
fn evaluate(&mut self) -> Result<ScalarValue> {
Ok(ScalarValue::Int64(Some(0)))
}
fn size(&self) -> usize {
size_of_val(self)
}
fn state(&mut self) -> Result<Vec<ScalarValue>> {
Ok(vec![ScalarValue::Int64(Some(0))])
}
fn merge_batch(&mut self, _states: &[ArrayRef]) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
struct NoFirstEmitGroupsAccumulator {
counts: Vec<i64>,
}
impl NoFirstEmitGroupsAccumulator {
fn emit_counts(&mut self, emit_to: EmitTo) -> Result<ArrayRef> {
match emit_to {
EmitTo::All => {
let counts = std::mem::take(&mut self.counts);
Ok(Arc::new(Int64Array::from(counts)))
}
EmitTo::First(_) => internal_err!(
"partial grouped aggregate output must materialize with EmitTo::All before slicing"
),
}
}
}
impl GroupsAccumulator for NoFirstEmitGroupsAccumulator {
fn update_batch(
&mut self,
_values: &[ArrayRef],
group_indices: &[usize],
_opt_filter: Option<&BooleanArray>,
total_num_groups: usize,
) -> Result<()> {
self.counts.resize(total_num_groups, 0);
for group_index in group_indices {
self.counts[*group_index] += 1;
}
Ok(())
}
fn evaluate(&mut self, emit_to: EmitTo) -> Result<ArrayRef> {
self.emit_counts(emit_to)
}
fn state(&mut self, emit_to: EmitTo) -> Result<Vec<ArrayRef>> {
Ok(vec![self.emit_counts(emit_to)?])
}
fn convert_to_state(
&self,
values: &[ArrayRef],
opt_filter: Option<&BooleanArray>,
) -> Result<Vec<ArrayRef>> {
assert_eq!(values.len(), 1, "one argument to convert_to_state");
let counts = match opt_filter {
Some(filter) => filter
.iter()
.map(|value| i64::from(value.unwrap_or(false)))
.collect::<Vec<_>>(),
None => vec![1; values[0].len()],
};
Ok(vec![Arc::new(Int64Array::from(counts))])
}
fn merge_batch(
&mut self,
_values: &[ArrayRef],
_group_indices: &[usize],
_total_num_groups: usize,
) -> Result<()> {
Ok(())
}
fn size(&self) -> usize {
size_of_val(self) + self.counts.capacity() * size_of::<i64>()
}
}
#[test]
fn test_with_dynamic_filter() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
let child = Arc::new(EmptyExec::new(Arc::clone(&schema)));
let agg = AggregateExec::try_new(
AggregateMode::Partial,
PhysicalGroupBy::new_single(vec![]),
vec![Arc::new(
AggregateExprBuilder::new(min_udaf(), vec![col("a", &schema)?])
.schema(Arc::clone(&schema))
.alias("min_a")
.build()?,
)],
vec![None],
child,
Arc::clone(&schema),
)?;
let new_df = Arc::new(DynamicFilterPhysicalExpr::new(
vec![col("a", &schema)?],
lit(false),
));
let agg = agg.with_dynamic_filter_expr(Arc::clone(&new_df))?;
let produced = agg.dynamic_expressions_produced();
assert_eq!(produced.len(), 1);
assert_eq!(produced[0].expression_id(), new_df.expression_id());
let swapped = produced[0]
.downcast_ref::<DynamicFilterPhysicalExpr>()
.expect("produced expression should be a DynamicFilterPhysicalExpr")
.current()?;
assert_eq!(format!("{swapped}"), format!("{}", lit(false)));
let new_df_as_pexpr: Arc<dyn PhysicalExpr> =
Arc::<DynamicFilterPhysicalExpr>::clone(&new_df);
let remapped_pexpr =
new_df_as_pexpr.with_new_children(vec![col("a", &schema)?])?;
let Ok(remapped_df) = (remapped_pexpr as Arc<dyn std::any::Any + Send + Sync>)
.downcast::<DynamicFilterPhysicalExpr>()
else {
panic!("should be DynamicFilterPhysicalExpr after with_new_children");
};
let _agg = agg.with_dynamic_filter_expr(remapped_df)?;
Ok(())
}
#[test]
fn test_plan_contains_expression_id_recurses_plans_and_expressions() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
let empty: Arc<dyn ExecutionPlan> = Arc::new(EmptyExec::new(Arc::clone(&schema)));
let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new(
vec![col("a", &schema)?],
lit(true),
));
let expression_id = dynamic_filter
.expression_id()
.expect("dynamic filters always have an expression ID");
assert!(!plan_contains_expression_id(&empty, expression_id)?);
let dynamic_filter_expr: Arc<dyn PhysicalExpr> =
Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter);
let predicate: Arc<dyn PhysicalExpr> =
Arc::new(NotExpr::new(dynamic_filter_expr));
let filter: Arc<dyn ExecutionPlan> =
Arc::new(FilterExecBuilder::new(predicate, empty).build()?);
let projection: Arc<dyn ExecutionPlan> = Arc::new(ProjectionExec::try_new(
[ProjectionExpr::new_from_expression(
col("a", &schema)?,
&schema,
)?],
filter,
)?);
assert!(plan_contains_expression_id(&projection, expression_id)?);
Ok(())
}
#[test]
fn test_with_dynamic_filter_error_unsupported() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int64, false),
Field::new("b", DataType::Int64, false),
]));
let child = Arc::new(EmptyExec::new(Arc::clone(&schema)));
let agg = AggregateExec::try_new(
AggregateMode::Final,
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]),
vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("sum_b")
.build()?,
)],
vec![None],
child,
Arc::clone(&schema),
)?;
assert!(agg.dynamic_expressions_produced().is_empty());
let df = Arc::new(DynamicFilterPhysicalExpr::new(
vec![col("a", &schema)?],
lit(true),
));
assert!(agg.with_dynamic_filter_expr(df).is_err());
Ok(())
}
#[test]
fn test_with_dynamic_filter_error_column_mismatch() -> Result<()> {
let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
let child = Arc::new(EmptyExec::new(Arc::clone(&schema)));
let agg = AggregateExec::try_new(
AggregateMode::Partial,
PhysicalGroupBy::new_single(vec![]),
vec![Arc::new(
AggregateExprBuilder::new(min_udaf(), vec![col("a", &schema)?])
.schema(Arc::clone(&schema))
.alias("min_a")
.build()?,
)],
vec![None],
child,
Arc::clone(&schema),
)?;
let df = Arc::new(DynamicFilterPhysicalExpr::new(
vec![Arc::new(Column::new("bad", 99)) as _],
lit(true),
));
assert!(agg.with_dynamic_filter_expr(df).is_err());
Ok(())
}
}