Skip to content

Commit add0efd

Browse files
committed
feat: decompose shared float64 avg aggregates
1 parent c149764 commit add0efd

14 files changed

Lines changed: 997 additions & 244 deletions

File tree

datafusion/expr/src/udaf.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use crate::function::{
4040
AccumulatorArgs, AggregateFunctionSimplification, StateFieldsArgs,
4141
};
4242
use crate::groups_accumulator::GroupsAccumulator;
43+
use crate::simplify::SimplifyContext;
4344
use crate::udf_eq::UdfEq;
4445
use crate::utils::AggregateOrderSensitivity;
4546
use crate::utils::format_state_name;
@@ -312,6 +313,17 @@ impl AggregateUDF {
312313
self.inner.simplify()
313314
}
314315

316+
/// Returns this aggregate function's candidate decomposition, if any.
317+
///
318+
/// See [`AggregateUDFImpl::decompose`] for more details.
319+
pub fn decompose(
320+
&self,
321+
aggregate_function: &AggregateFunction,
322+
info: &SimplifyContext,
323+
) -> Result<Option<Expr>> {
324+
self.inner.decompose(aggregate_function, info)
325+
}
326+
315327
/// Rewrite aggregate to have simpler arguments
316328
///
317329
/// See [`AggregateUDFImpl::simplify_expr_op_literal`] for more details
@@ -751,6 +763,25 @@ pub trait AggregateUDFImpl: Debug + DynEq + DynHash + Send + Sync + Any {
751763
None
752764
}
753765

766+
/// Returns an optional candidate decomposition into simpler aggregates.
767+
///
768+
/// Unlike [`Self::simplify`], the optimizer only applies this rewrite when
769+
/// at least one aggregate in the returned expression can be shared with
770+
/// another aggregate in the same plan node. This makes the hook suitable
771+
/// for rewrites such as `AVG(x)` into `SUM(x) / COUNT(x)`, which may be
772+
/// slower when neither component can be reused.
773+
///
774+
/// A returned candidate expression must have the same data type and
775+
/// nullability as the original aggregate expression. Return `None` when
776+
/// this aggregate cannot be decomposed.
777+
fn decompose(
778+
&self,
779+
_aggregate_function: &AggregateFunction,
780+
_info: &SimplifyContext,
781+
) -> Result<Option<Expr>> {
782+
Ok(None)
783+
}
784+
754785
/// Rewrite the aggregate to have simpler arguments
755786
///
756787
/// This query pattern is not common in most real workloads, and most
@@ -1635,6 +1666,14 @@ impl AggregateUDFImpl for AliasedAggregateUDFImpl {
16351666
self.inner.simplify()
16361667
}
16371668

1669+
fn decompose(
1670+
&self,
1671+
aggregate_function: &AggregateFunction,
1672+
info: &SimplifyContext,
1673+
) -> Result<Option<Expr>> {
1674+
self.inner.decompose(aggregate_function, info)
1675+
}
1676+
16381677
fn simplify_expr_op_literal(
16391678
&self,
16401679
agg_function: &AggregateFunction,
@@ -1715,7 +1754,9 @@ pub enum SetMonotonicity {
17151754

17161755
#[cfg(test)]
17171756
mod test {
1718-
use crate::{AggregateUDF, AggregateUDFImpl};
1757+
use crate::expr::AggregateFunction;
1758+
use crate::simplify::SimplifyContext;
1759+
use crate::{AggregateUDF, AggregateUDFImpl, Expr, col};
17191760
use arrow::datatypes::{DataType, FieldRef};
17201761
use datafusion_common::Result;
17211762
use datafusion_expr_common::accumulator::Accumulator;
@@ -1762,6 +1803,13 @@ mod test {
17621803
fn state_fields(&self, _args: StateFieldsArgs) -> Result<Vec<FieldRef>> {
17631804
unimplemented!()
17641805
}
1806+
fn decompose(
1807+
&self,
1808+
_aggregate_function: &AggregateFunction,
1809+
_info: &SimplifyContext,
1810+
) -> Result<Option<Expr>> {
1811+
Ok(Some(col("decomposed")))
1812+
}
17651813
}
17661814

17671815
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -1824,6 +1872,34 @@ mod test {
18241872
assert!(!(a1 == b1));
18251873
}
18261874

1875+
#[test]
1876+
fn test_decompose_forwarded_through_aliases() -> Result<()> {
1877+
let udf = AggregateUDF::from(AMeanUdf::new()).with_aliases(["alias"]);
1878+
let Expr::AggregateFunction(aggregate_function) = udf.call(vec![col("a")]) else {
1879+
panic!("expected aggregate function")
1880+
};
1881+
1882+
assert_eq!(
1883+
udf.decompose(&aggregate_function, &SimplifyContext::default())?,
1884+
Some(col("decomposed"))
1885+
);
1886+
Ok(())
1887+
}
1888+
1889+
#[test]
1890+
fn test_default_decompose_returns_none() -> Result<()> {
1891+
let udf = AggregateUDF::from(BMeanUdf::new());
1892+
let Expr::AggregateFunction(aggregate_function) = udf.call(vec![col("a")]) else {
1893+
panic!("expected aggregate function")
1894+
};
1895+
1896+
assert_eq!(
1897+
udf.decompose(&aggregate_function, &SimplifyContext::default())?,
1898+
None
1899+
);
1900+
Ok(())
1901+
}
1902+
18271903
fn hash<T: Hash>(value: T) -> u64 {
18281904
let hasher = &mut DefaultHasher::new();
18291905
value.hash(hasher);

datafusion/expr/src/utils.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,110 @@ pub use datafusion_functions_aggregate_common::order::AggregateOrderSensitivity;
5050
/// `COUNT(<constant>)` expressions
5151
pub use datafusion_common::utils::expr::COUNT_STAR_EXPANSION;
5252

53+
/// Tracks expression names and generates aliases that do not conflict with
54+
/// names already in an output schema.
55+
///
56+
/// In addition to duplicate schema names, this detects ambiguity between a
57+
/// qualified field such as `t.a` and an unqualified field named `a`.
58+
#[derive(Default)]
59+
pub struct NameTracker {
60+
/// Tracks seen schema names (from expr.schema_name()).
61+
/// Used to detect duplicates that would fail validate_unique_names.
62+
seen_schema_names: HashSet<String>,
63+
/// Tracks column names that have been seen with a qualifier.
64+
/// Used to detect ambiguous references (qualified + unqualified with same name).
65+
qualified_names: HashSet<String>,
66+
/// Tracks column names that have been seen without a qualifier.
67+
/// Used to detect ambiguous references.
68+
unqualified_names: HashSet<String>,
69+
}
70+
71+
impl NameTracker {
72+
pub fn new() -> Self {
73+
Self::default()
74+
}
75+
76+
/// Reserve the names of `exprs` without changing the expressions.
77+
///
78+
/// This is useful when existing output expressions must retain their names
79+
/// and subsequently generated expressions need to avoid them.
80+
pub fn reserve(&mut self, exprs: &[Expr]) {
81+
for expr in exprs {
82+
self.insert(expr);
83+
}
84+
}
85+
86+
fn would_conflict(&self, expr: &Expr) -> bool {
87+
let (qualifier, name) = expr.qualified_name();
88+
let schema_name = expr.schema_name().to_string();
89+
self.would_conflict_inner((qualifier, &name), &schema_name)
90+
}
91+
92+
fn would_conflict_inner(
93+
&self,
94+
qualified_name: (Option<TableReference>, &str),
95+
schema_name: &str,
96+
) -> bool {
97+
// Check for duplicate schema_name (would fail validate_unique_names)
98+
if self.seen_schema_names.contains(schema_name) {
99+
return true;
100+
}
101+
102+
// Check for ambiguous reference (would fail DFSchema::check_names)
103+
// This happens when a qualified field and unqualified field have the same name
104+
let (qualifier, name) = qualified_name;
105+
match qualifier {
106+
Some(_) => {
107+
// Adding a qualified name - conflicts if unqualified version exists
108+
self.unqualified_names.contains(name)
109+
}
110+
None => {
111+
// Adding an unqualified name - conflicts if qualified version exists
112+
self.qualified_names.contains(name)
113+
}
114+
}
115+
}
116+
117+
fn insert(&mut self, expr: &Expr) {
118+
let schema_name = expr.schema_name().to_string();
119+
self.seen_schema_names.insert(schema_name);
120+
121+
let (qualifier, name) = expr.qualified_name();
122+
match qualifier {
123+
Some(_) => {
124+
self.qualified_names.insert(name);
125+
}
126+
None => {
127+
self.unqualified_names.insert(name);
128+
}
129+
}
130+
}
131+
132+
/// Return `expr` unchanged if its name is available, or alias it to a
133+
/// unique name otherwise.
134+
pub fn get_uniquely_named_expr(&mut self, expr: Expr) -> Result<Expr> {
135+
if !self.would_conflict(&expr) {
136+
self.insert(&expr);
137+
return Ok(expr);
138+
}
139+
140+
// Name collision - need to generate a unique alias
141+
let schema_name = expr.schema_name().to_string();
142+
let mut counter = 0;
143+
let candidate_name = loop {
144+
let candidate_name = format!("{schema_name}__temp__{counter}");
145+
// .alias always produces an unqualified name so check for conflicts accordingly.
146+
if !self.would_conflict_inner((None, &candidate_name), &candidate_name) {
147+
break candidate_name;
148+
}
149+
counter += 1;
150+
};
151+
let candidate_expr = expr.alias(&candidate_name);
152+
self.insert(&candidate_expr);
153+
Ok(candidate_expr)
154+
}
155+
}
156+
53157
/// Count the number of distinct exprs in a list of group by expressions. If the
54158
/// first element is a `GroupingSet` expression then it must be the only expr.
55159
pub fn grouping_set_expr_count(group_expr: &[Expr]) -> Result<usize> {
@@ -1515,6 +1619,131 @@ mod tests {
15151619
use arrow::datatypes::{UnionFields, UnionMode};
15161620
use datafusion_expr_common::signature::Volatility;
15171621

1622+
#[test]
1623+
fn name_tracker_unique_names_pass_through() -> Result<()> {
1624+
let mut tracker = NameTracker::new();
1625+
1626+
// First expression should pass through unchanged
1627+
let expr1 = col("a");
1628+
let result1 = tracker.get_uniquely_named_expr(expr1.clone())?;
1629+
assert_eq!(result1, col("a"));
1630+
1631+
// Different name should also pass through unchanged
1632+
let expr2 = col("b");
1633+
let result2 = tracker.get_uniquely_named_expr(expr2)?;
1634+
assert_eq!(result2, col("b"));
1635+
1636+
Ok(())
1637+
}
1638+
1639+
#[test]
1640+
fn name_tracker_duplicate_schema_name_gets_alias() -> Result<()> {
1641+
let mut tracker = NameTracker::new();
1642+
1643+
// First expression with name "a"
1644+
let expr1 = col("a");
1645+
let result1 = tracker.get_uniquely_named_expr(expr1)?;
1646+
assert_eq!(result1, col("a"));
1647+
1648+
// Second expression with same name "a" should get aliased
1649+
let expr2 = col("a");
1650+
let result2 = tracker.get_uniquely_named_expr(expr2)?;
1651+
assert_eq!(result2, col("a").alias("a__temp__0"));
1652+
1653+
// Third expression with same name "a" should get a different alias
1654+
let expr3 = col("a");
1655+
let result3 = tracker.get_uniquely_named_expr(expr3)?;
1656+
assert_eq!(result3, col("a").alias("a__temp__1"));
1657+
1658+
Ok(())
1659+
}
1660+
1661+
#[test]
1662+
fn name_tracker_qualified_then_unqualified_conflicts() -> Result<()> {
1663+
let mut tracker = NameTracker::new();
1664+
1665+
// First: qualified column "table.a"
1666+
let qualified_col = Expr::Column(Column::new(Some("table"), "a"));
1667+
let result1 = tracker.get_uniquely_named_expr(qualified_col)?;
1668+
assert_eq!(result1, Expr::Column(Column::new(Some("table"), "a")));
1669+
1670+
// Second: unqualified column "a" - should conflict (ambiguous reference)
1671+
let unqualified_col = col("a");
1672+
let result2 = tracker.get_uniquely_named_expr(unqualified_col)?;
1673+
// Should be aliased to avoid ambiguous reference
1674+
assert_eq!(result2, col("a").alias("a__temp__0"));
1675+
1676+
Ok(())
1677+
}
1678+
1679+
#[test]
1680+
fn name_tracker_unqualified_then_qualified_conflicts() -> Result<()> {
1681+
let mut tracker = NameTracker::new();
1682+
1683+
// First: unqualified column "a"
1684+
let unqualified_col = col("a");
1685+
let result1 = tracker.get_uniquely_named_expr(unqualified_col)?;
1686+
assert_eq!(result1, col("a"));
1687+
1688+
// Second: qualified column "table.a" - should conflict (ambiguous reference)
1689+
let qualified_col = Expr::Column(Column::new(Some("table"), "a"));
1690+
let result2 = tracker.get_uniquely_named_expr(qualified_col)?;
1691+
// Should be aliased to avoid ambiguous reference
1692+
assert_eq!(
1693+
result2,
1694+
Expr::Column(Column::new(Some("table"), "a")).alias("table.a__temp__0")
1695+
);
1696+
1697+
Ok(())
1698+
}
1699+
1700+
#[test]
1701+
fn name_tracker_different_qualifiers_no_conflict() -> Result<()> {
1702+
let mut tracker = NameTracker::new();
1703+
1704+
// First: qualified column "table1.a"
1705+
let col1 = Expr::Column(Column::new(Some("table1"), "a"));
1706+
let result1 = tracker.get_uniquely_named_expr(col1.clone())?;
1707+
assert_eq!(result1, col1);
1708+
1709+
// Second: qualified column "table2.a" - different qualifier, different schema_name
1710+
// so should NOT conflict
1711+
let col2 = Expr::Column(Column::new(Some("table2"), "a"));
1712+
let result2 = tracker.get_uniquely_named_expr(col2.clone())?;
1713+
assert_eq!(result2, col2);
1714+
1715+
Ok(())
1716+
}
1717+
1718+
#[test]
1719+
fn name_tracker_aliased_expressions() -> Result<()> {
1720+
let mut tracker = NameTracker::new();
1721+
1722+
// First: col("x").alias("result")
1723+
let expr1 = col("x").alias("result");
1724+
let result1 = tracker.get_uniquely_named_expr(expr1.clone())?;
1725+
assert_eq!(result1, col("x").alias("result"));
1726+
1727+
// Second: col("y").alias("result") - same alias name, should conflict
1728+
let expr2 = col("y").alias("result");
1729+
let result2 = tracker.get_uniquely_named_expr(expr2)?;
1730+
assert_eq!(result2, col("y").alias("result").alias("result__temp__0"));
1731+
1732+
Ok(())
1733+
}
1734+
1735+
#[test]
1736+
fn name_tracker_avoids_reserved_qualified_name() -> Result<()> {
1737+
let mut tracker = NameTracker::new();
1738+
tracker.reserve(&[Expr::Column(Column::new(Some("t"), "a"))]);
1739+
1740+
assert_eq!(
1741+
tracker.get_uniquely_named_expr(col("a"))?,
1742+
col("a").alias("a__temp__0")
1743+
);
1744+
Ok(())
1745+
}
1746+
15181747
#[test]
15191748
fn test_group_window_expr_by_sort_keys_empty_case() -> Result<()> {
15201749
let result = group_window_expr_by_sort_keys(vec![])?;

0 commit comments

Comments
 (0)