This is an automated email from the ASF dual-hosted git repository.
slfan1989 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/auron.git
The following commit(s) were added to refs/heads/master by this push:
new 14d17f7ce [AURON #2453] Fix filtered aggregation for groups with no
matching rows (#2454)
14d17f7ce is described below
commit 14d17f7ce5fa8084f8f4af73fccc89180bb0161f
Author: linfeng <[email protected]>
AuthorDate: Sun Aug 9 08:42:34 2026 +0800
[AURON #2453] Fix filtered aggregation for groups with no matching rows
(#2454)
# Which issue does this PR close?
Closes #2453
# Rationale for this change
A grouped aggregate may have no rows matching its `FILTER` clause. In
this case, the group exists but its accumulator may not be initialized,
causing an index-out-of-bounds error while producing the result.
# What changes are included in this PR?
- Ensure accumulator slots are initialized for all discovered groups
before applying aggregate filters.
- Add regression coverage for filtered `SUM` and `COUNT`.
# Are there any user-facing changes?
Yes, bug fix only.
# How was this patch tested?
Yes, added regression tests for filtered `SUM` and `COUNT`.
# Was this patch authored or co-authored using generative AI tooling?
- [x] Yes
- [ ] No
If yes, include: `Generated-by: gpt-5`
ASF guidance: https://www.apache.org/legal/generative-tooling.html
---
native-engine/datafusion-ext-plans/src/agg/acc.rs | 19 ++++++++++++
.../datafusion-ext-plans/src/agg/agg_ctx.rs | 4 +++
native-engine/datafusion-ext-plans/src/agg_exec.rs | 34 ++++++++++++++++------
.../scala/org/apache/auron/AuronQuerySuite.scala | 8 +++++
4 files changed, 56 insertions(+), 9 deletions(-)
diff --git a/native-engine/datafusion-ext-plans/src/agg/acc.rs
b/native-engine/datafusion-ext-plans/src/agg/acc.rs
index bea1c05ec..0857e161b 100644
--- a/native-engine/datafusion-ext-plans/src/agg/acc.rs
+++ b/native-engine/datafusion-ext-plans/src/agg/acc.rs
@@ -88,6 +88,25 @@ impl AccTable {
self.cols.iter_mut().for_each(|c| c.resize(num_records));
}
+ pub fn ensure_size(&mut self, idx: IdxSelection<'_>) {
+ let num_records = match idx {
+ IdxSelection::Single(idx) => idx + 1,
+ IdxSelection::Indices(indices) => {
+ indices.iter().copied().max().map_or(0, |idx| idx + 1)
+ }
+ IdxSelection::IndicesU32(indices) => indices
+ .iter()
+ .copied()
+ .max()
+ .map_or(0, |idx| idx as usize + 1),
+ IdxSelection::Range(_, end) => end,
+ };
+ self.cols
+ .iter_mut()
+ .filter(|col| col.num_records() < num_records)
+ .for_each(|col| col.resize(num_records));
+ }
+
pub fn shrink_to_fit(&mut self) {
self.cols.iter_mut().for_each(|c| c.shrink_to_fit());
}
diff --git a/native-engine/datafusion-ext-plans/src/agg/agg_ctx.rs
b/native-engine/datafusion-ext-plans/src/agg/agg_ctx.rs
index f2dfbc240..73ce4a26b 100644
--- a/native-engine/datafusion-ext-plans/src/agg/agg_ctx.rs
+++ b/native-engine/datafusion-ext-plans/src/agg/agg_ctx.rs
@@ -274,6 +274,10 @@ impl AggContext {
// arrow-ffi with sliced batch is buggy in older arrow-java, so we use
unsliced
// batch with explicit offsets
+ // Every group needs an accumulator slot even when FILTER excludes all
of its
+ // rows.
+ acc_table.ensure_size(acc_idx);
+
// partial update
if self.need_partial_update {
let agg_exprs_batch =
self.agg_expr_evaluator.filter_project(&batch)?;
diff --git a/native-engine/datafusion-ext-plans/src/agg_exec.rs
b/native-engine/datafusion-ext-plans/src/agg_exec.rs
index d75d304f0..7ffaf295f 100644
--- a/native-engine/datafusion-ext-plans/src/agg_exec.rs
+++ b/native-engine/datafusion-ext-plans/src/agg_exec.rs
@@ -436,6 +436,7 @@ mod test {
AggMode::{Final, Partial},
GroupingExpr,
agg::create_agg,
+ count::AggCount,
sum::AggSum,
},
agg_exec::AggExec,
@@ -738,15 +739,28 @@ mod test {
field_name: "grp".to_string(),
expr: phys_expr::col("grp", &schema)?,
}],
- vec![AggExpr {
- field_name: "sum_filtered".to_string(),
- mode: Partial,
- filter: Some(filter_expr),
- agg: Arc::new(AggSum::try_new(
- phys_expr::col("val", &schema)?,
- DataType::Int64,
- )?),
- }],
+ vec![
+ AggExpr {
+ field_name: "sum_filtered".to_string(),
+ mode: Partial,
+ filter: Some(filter_expr),
+ agg: Arc::new(AggSum::try_new(
+ phys_expr::col("val", &schema)?,
+ DataType::Int64,
+ )?),
+ },
+ AggExpr {
+ field_name: "count_filtered".to_string(),
+ mode: Partial,
+ filter:
Some(Arc::new(phys_expr::Literal::new(ScalarValue::Boolean(
+ Some(false),
+ )))),
+ agg: Arc::new(AggCount::try_new(
+ vec![phys_expr::col("val", &schema)?],
+ DataType::Int64,
+ )?),
+ },
+ ],
false,
input,
)?);
@@ -756,11 +770,13 @@ mod test {
let grp_result = result.column(0).as_string::<i32>();
let sum_result = result.column(1).as_primitive::<Int64Type>();
+ let count_result = result.column(2).as_primitive::<Int64Type>();
assert_eq!(grp_result.len(), 2);
let mut found = std::collections::HashMap::new();
for i in 0..grp_result.len() {
found.insert(grp_result.value(i), sum_result.value(i));
+ assert_eq!(count_result.value(i), 0);
}
assert_eq!(found["a"], 4); // 1 + 3
assert_eq!(found["b"], 5); // 5 (NULL val contributes 0)
diff --git
a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
index 56119a7cb..e080baaa6 100644
---
a/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
+++
b/spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala
@@ -974,6 +974,14 @@ class AuronQuerySuite extends AuronQueryTest with
BaseAuronSQLSuite with AuronSQ
|GROUP BY category
|ORDER BY category""".stripMargin)
+ // A group with no matching rows keeps the aggregate's empty-input state.
+ checkSparkAnswerAndOperator("""SELECT category,
+ | SUM(amount) FILTER (WHERE amount > 450) AS high_amount,
+ | SUM(amount) FILTER (WHERE amount < 150) AS low_amount
+ |FROM t_filter_agg_2289
+ |GROUP BY category
+ |ORDER BY category""".stripMargin)
+
// Multiple aggregates with different FILTER predicates
checkSparkAnswerAndOperator("""SELECT
| SUM(amount) FILTER (WHERE is_vip = true) AS sum_vip,