Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 40 additions & 17 deletions datafusion/substrait/src/logical_plan/consumer/rel/aggregate_rel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@

use crate::logical_plan::consumer::{NameTracker, SubstraitConsumer};
use crate::logical_plan::consumer::{from_substrait_agg_func, from_substrait_sorts};
use crate::logical_plan::grouping_set::{
grouping_id_column, grouping_set_columns, grouping_set_ids, grouping_sets_of,
index_from_grouping_id, unique_grouping_set_index_name,
};
use datafusion::common::{Column, DFSchemaRef, internal_err, not_impl_err};
use datafusion::logical_expr::builder::project;
use datafusion::logical_expr::{
Aggregate, Expr, GroupingSet, LogicalPlan, LogicalPlanBuilder,
Aggregate, Expr, ExprSchemable, GroupingSet, LogicalPlan, LogicalPlanBuilder,
};
use substrait::proto::AggregateRel;
use substrait::proto::aggregate_function::AggregationInvocation;
Expand Down Expand Up @@ -125,33 +129,44 @@ pub async fn from_aggregate_rel(
.map(|e| name_tracker.get_uniquely_named_expr(e))
.collect::<Result<Vec<Expr>, _>>()?;

let set_ids = (agg.groupings.len() > 1)
.then(|| {
grouping_set_ids(
&grouping_set_columns(&group_exprs)?,
grouping_sets_of(&group_exprs)?,
)
})
.transpose()?;
let plan = input.aggregate(group_exprs, aggr_exprs)?.build()?;
if agg.groupings.len() > 1 {
reorder_grouping_set_output(plan, agg.measures.len())
} else {
Ok(plan)
match set_ids {
Some(set_ids) => grouping_set_output(plan, agg.measures.len(), &set_ids),
None => Ok(plan),
}
} else {
not_impl_err!("Aggregate without an input is not valid")
}
}

/// Reorders DataFusion's `[groups, grouping_id, measures]` aggregate schema to
/// Substrait's direct output order of `[groups, measures, grouping_id]`.
fn reorder_grouping_set_output(
/// Shapes DataFusion's `[groups, grouping_id, measures]` aggregate schema into
/// the direct output Substrait gives a multi-set aggregate:
/// `[groups, measures, grouping set index]`.
///
/// The trailing column is not DataFusion's `__grouping_id`. Substrait defines it
/// as "the zero-based index of the grouping set that yielded the record", while
/// `__grouping_id` packs a bitmask of the columns the set leaves out together
/// with an ordinal that separates repeated sets. Both identify the set that
/// produced a row, so the column is replaced here by an expression mapping one
/// to the other.
///
/// [Aggregate Operation]: https://substrait.io/relations/logical_relations/#aggregate-operation
fn grouping_set_output(
plan: LogicalPlan,
measure_count: usize,
set_ids: &[u64],
) -> datafusion::common::Result<LogicalPlan> {
let exprs: Vec<Expr> = {
let schema = plan.schema();
let Some(grouping_id_index) =
schema.index_of_column_by_name(None, Aggregate::INTERNAL_GROUPING_ID)
else {
return internal_err!(
"Grouping set aggregate schema is missing {}",
Aggregate::INTERNAL_GROUPING_ID
);
};
let (grouping_id_index, grouping_id) = grouping_id_column(schema)?;
if grouping_id_index + measure_count + 1 != schema.fields().len() {
return internal_err!(
"Grouping set aggregate schema has {} fields after {}, expected {} measures",
Expand All @@ -160,11 +175,19 @@ fn reorder_grouping_set_output(
measure_count
);
}
let grouping_id = Expr::Column(grouping_id);
let grouping_id_type = grouping_id.get_type(schema)?;
// A real column already named like the preferred alias - most likely
// a user column literally called `grouping_set_index` - would
// otherwise collide with it as a duplicate unqualified field.
let index_name = unique_grouping_set_index_name(schema);
let set_index = index_from_grouping_id(&grouping_id, &grouping_id_type, set_ids)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixed grouping_set_index alias can collide with a real user column of the same name on both the consumer and producer paths. Please generate a collision-free synthetic name and use that same chosen name for the producer reference, with regressions for both boundaries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: added unique_grouping_set_index_name (shared helper in grouping_set.rs) that falls back to a free name when the schema already has one named grouping_set_index; used by both the producer and the consumer. Added a regression with a real column of that name.

.alias(index_name);

(0..grouping_id_index)
.chain(grouping_id_index + 1..schema.fields().len())
.chain(std::iter::once(grouping_id_index))
.map(|index| Expr::Column(Column::from(schema.qualified_field(index))))
.chain(std::iter::once(set_index))
.collect()
};
project(plan, exprs)
Expand Down
Loading
Loading