Skip to content

feat: add grouping equivalence properties - #24698

Merged
jayzhan211 merged 5 commits into
apache:mainfrom
xavlee:feat/issue-24438-group-contiguous-property
Nov 3, 2026
Merged

jayzhan211 merged 5 commits into
apache:mainfrom
xavlee:feat/issue-24438-group-contiguous-property

Conversation

@xavlee

@xavlee xavlee commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR relate to?

Rationale for this change

Aggregation can recognize completed groups when all rows with the same grouping-key tuple occur in one contiguous run within a partition. Sorted input provides this guarantee, and the runs can also appear in an arbitrary order:

AAABBBCCCC -> ordered and grouped
CCCAAABBB  -> grouped; each group can be completed when its run ends
AAABBBAACC -> A appears in separate runs

This PR represents that guarantee in EquivalenceProperties, allowing execution-plan producers to declare grouping tuples and consumers to check whether an input satisfies a grouping requirement.

What changes are included in this PR?

Semantics and propagation

A grouping assertion describes a complete expression tuple within every output partition. [a, b] and [b, a] identify the same groups. The guarantee applies to the complete pair; a guarantee for a subset such as [a] requires its own proof.

Grouping assertions are correctness guarantees supplied by producers. DataFusion trusts the asserted row layout. Producers can declare derived tuples such as [bhandle, date_bin(timestamp)] directly.

  • Store grouping tuples alongside ordering, equality, and constant information in EquivalenceProperties.
  • Normalize tuples using expression equivalences and constants, removing duplicate expressions.
  • Check grouping requirements against declared tuples and grouping implied by lexicographical ordering prefixes.
  • Project a tuple when every expression can be represented in the output, including explicitly declared derived expressions.
  • Remap grouping expressions during schema replacement.
  • Preserve a declared grouping tuple through UnionExec when every input satisfies it, explicitly or through ordering.
  • Clear explicit grouping assertions at aggregation, sorting, repartitioning, and partition-merging boundaries where execution can invalidate them.

Public API additions

  • GroupingEquivalenceClass represents complete grouping tuples and supports construction, insertion, lookup, clearing, schema rewriting, iteration, and display.
  • EquivalenceProperties::{geq_class, normalized_geq_class, add_grouping, add_groupings, clear_groupings, grouping_satisfy} expose grouping metadata and its analysis.
  • TestMemoryExec::try_with_grouping_information attaches grouping assertions in physical-plan tests and examples.

Execution-plan producers attach these assertions through the EquivalenceProperties held by PlanProperties. The follow-up #24497 consumes them to derive GroupClusteringMode::Full for eligible aggregates.

Stack

  1. #24737 — test: cover unsorted contiguous groups in one partition (merged)
  2. #24697 — refactor: use group clustering terminology in aggregate execution (merged)
  3. #24698 — feat: add grouping equivalence properties ← this PR
  4. #24497 — feat: stream aggregates over grouped input

What is the testing strategy for this PR?

Tests cover complete-tuple semantics, equivalent permutations, normalization through equivalences and constants, ordering-derived grouping, complete and incomplete projection mappings, explicitly declared derived expressions, operator boundaries, and UNION propagation with selective tuple retention, mixed grouping/ordering proofs, schema rewrites, and input permutations.

aggregate_clears_explicit_grouping covers partial and single aggregation with reordered and aliased grouping keys. Both cases fail without the aggregate boundary clear and pass with it.

Local validation passed the extended workspace suite, including 2,997 physical-plan tests and 546 SQL logic files, and all-target/all-feature Clippy with warnings denied.

Are there any user-facing changes?

Yes. The additive Rust APIs above let plan producers describe contiguous grouping tuples and let consumers evaluate grouping requirements. The streaming aggregate optimization follows in #24497.

Review this layer

View this PR's diff

@codecov-commenter

codecov-commenter commented Aug 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.05028% with 150 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.44%. Comparing base (ebdb657) to head (c2fe167).

Files with missing lines Patch % Lines
datafusion/physical-expr/src/equivalence/mod.rs 58.97% 3 Missing and 61 partials ⚠️
...tafusion/physical-expr/src/equivalence/grouping.rs 82.17% 13 Missing and 5 partials ⚠️
datafusion/physical-plan/src/test.rs 60.46% 16 Missing and 1 partial ⚠️
datafusion/physical-plan/src/aggregates/mod.rs 82.19% 5 Missing and 8 partials ⚠️
...on/physical-expr/src/equivalence/properties/mod.rs 90.00% 7 Missing and 5 partials ⚠️
datafusion/physical-plan/src/repartition/mod.rs 78.57% 0 Missing and 6 partials ⚠️
...atafusion/physical-plan/src/coalesce_partitions.rs 84.61% 0 Missing and 4 partials ⚠️
datafusion/physical-expr/src/expressions/cast.rs 81.25% 0 Missing and 3 partials ⚠️
...n/physical-plan/src/sorts/sort_preserving_merge.rs 85.00% 0 Missing and 3 partials ⚠️
datafusion/functions/src/datetime/date_bin.rs 84.61% 0 Missing and 2 partials ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24698      +/-   ##
==========================================
- Coverage   82.45%   82.44%   -0.01%     
==========================================
  Files        1140     1141       +1     
  Lines      436434   437085     +651     
  Branches   436434   437085     +651     
==========================================
+ Hits       359840   360355     +515     
- Misses      54840    54879      +39     
- Partials    21754    21851      +97     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch from b2ceb1a to 6bd697f Compare August 26, 2026 19:20
@github-actions github-actions Bot added datasource Changes to the datasource crate physical-plan Changes to the physical-plan crate labels Aug 26, 2026
@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch 5 times, most recently from 1e7d83a to 72d66b3 Compare August 28, 2026 19:45
@github-actions github-actions Bot added the ffi Changes to the ffi crate label Aug 28, 2026
@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch 9 times, most recently from fa946cb to db683f4 Compare September 4, 2026 19:21
@github-actions github-actions Bot added logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates optimizer Optimizer rules functions Changes to functions implementation and removed datasource Changes to the datasource crate labels Sep 4, 2026
@xavlee xavlee changed the title feat: add narrow group-contiguous source property feat: add grouped equivalence properties Sep 4, 2026
@xavlee xavlee changed the title feat: add grouped equivalence properties feat: add Grouped equivalence properties Sep 4, 2026
@github-actions github-actions Bot removed optimizer Optimizer rules core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation ffi Changes to the ffi crate labels Oct 27, 2026
@xavlee

xavlee commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Hey @jayzhan211 , this is ready for another look when you get the chance 🙇

self.oeq_cache.clear();
}

/// Clears grouping information invalidated by an operation that changes

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.

@xavlee thanks for the updates, both earlier points look good now. One remaining gap: an operator that reorders rows and calls only clear_orderings() still forwards its input's groupings. That's the same thing AggregateExec hit in the previous revision, and it's the pattern custom operators downstream already follow. Could clear_orderings() also clear groupings? Then every operator that already drops orderings stays correct without having to know about groupings:

     pub fn clear_orderings(&mut self) {
         self.oeq_class.clear();
         self.oeq_cache.clear();
+        self.clear_groupings();
     }
         eq_properties.clear_orderings();
-        if input_partitions > 1 {
-            eq_properties.clear_groupings();
-        }
         eq_properties.clear_per_partition_constants();

I ran this locally. The only test that changes is the single-partition half of grouping_is_cleared_only_when_partitions_are_merged, which now expects the grouping to be cleared, the same as orderings. With this change aggregate_clears_explicit_grouping no longer exercises the explicit clear in AggregateExec, so I'd add a clustered case too. It passes with the explicit clear and fails without it:

#[test]
fn clustered_aggregate_clears_explicit_grouping() -> Result<()> {
    let schema = Arc::new(Schema::new(vec![
        Field::new("a", DataType::Int32, true),
        Field::new("c", DataType::Int32, true),
        Field::new("value", DataType::Int64, true),
    ]));
    let a = col("a", &schema)?;
    let c = col("c", &schema)?;
    let input = TestMemoryExec::try_new(&[vec![]], Arc::clone(&schema), None)?
        .try_with_sort_information(vec![
            [PhysicalSortExpr::new_default(Arc::clone(&a))].into(),
        ])?
        .try_with_grouping_information(vec![vec![Arc::clone(&c)]])?;
    let aggregate = AggregateExec::try_new(
        AggregateMode::Single,
        PhysicalGroupBy::new_single(vec![
            (a, "group_a".to_string()),
            (c, "group_c".to_string()),
        ]),
        vec![Arc::new(
            AggregateExprBuilder::new(sum_udaf(), vec![col("value", &schema)?])
                .schema(Arc::clone(&schema))
                .alias("SUM(value)")
                .build()?,
        )],
        vec![None],
        Arc::new(input),
        schema,
    )?;

    assert!(matches!(
        aggregate.group_clustering_mode(),
        GroupClusteringMode::Partial(_)
    ));
    let properties = aggregate.properties().equivalence_properties();
    assert!(!properties.oeq_class().is_empty());
    assert!(properties.geq_class().is_empty());
    assert!(!properties.grouping_satisfy([col("group_c", &aggregate.schema())?])?);
    Ok(())
}

If you'd rather keep clear_orderings() as it is, could you note in its doc comment and in add_groupings that operators which reorder rows must also call clear_groupings()? A follow-up PR is fine for either option.

@xavlee xavlee Oct 28, 2026 •

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.

Thanks @jayzhan211. Assuming we have no existing operators that would change ordering but somehow maintain groupings, the conservative route to clear groupings in clear orderings makes sense to me.
I added this as a follow-up PR here: #26852

@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch from 2c5ab1f to 59f1596 Compare October 28, 2026 17:35
@xavlee

xavlee commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @jayzhan211, I came across another finding- we may also want to propagate grouping properties through UnionExec.

Currently, we construct a new EquivalenceProperties and only add back constants and orderings. However, since UnionExec maintains isolated partitions, grouping properties should also be maintained. On the other hand, InterleaveExec should not propagate groupings since it merges partitions, potentially breaking contiguity.

Please let me know if this makes sense, and if I should add this as part of this PR or in another followup 🙇

@alamb

alamb commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@mhilton is this a PR you think would help us at Influx?

// explicit grouping assertions do not necessarily survive even when
// the requested ordering is already satisfied. Groupings implied by
// the output ordering remain discoverable through `oeq_class`.
self.clear_groupings();

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.

It might be that the ordering maintains the grouping, if every column in the grouping is included in the ordering, and therefore the groupings could filtered rather than just cleared. I suspect however that this is unnecessary as ordering is the stronger signal and will be checked first.

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.

Thanks for the review @mhilton. That's the intent here 🙂 grouping implied by the resulting ordering remains discoverable when determining a ClusteringMode, so we don't need to retain the grouping assertions here.

@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch from 59f1596 to aaae646 Compare October 30, 2026 18:43
@jayzhan211
jayzhan211 self-requested a review November 2, 2026 13:54
@jayzhan211

Copy link
Copy Markdown
Contributor

@xavlee Yes, that makes sense. UnionExec turns each child partition into its own output partition, so a tuple holds on the output whenever it holds on every input, and InterleaveExec is right to drop it. It's a small change, so could you add it to this PR? This rule passes the existing union tests locally. It keeps a tuple from either side when both sides satisfy it, so an ordering on one side counts too:

+    // Each union output partition is one input partition, so a grouping holds
+    // when every input satisfies it, explicitly or through an ordering.
+    let mut groupings = vec![];
+    for grouping in lhs.geq_class().iter().chain(rhs.geq_class().iter()) {
+        if lhs.grouping_satisfy(grouping.iter().cloned())?
+            && rhs.grouping_satisfy(grouping.iter().cloned())?
+        {
+            groupings.push(grouping.clone());
+        }
+    }
+
     let mut eq_properties = EquivalenceProperties::new(lhs.schema);
     eq_properties.add_constants(constants)?;
     eq_properties.add_orderings(orderings);
+    eq_properties.add_groupings(groupings);

Please add tests for these cases: grouped on [a, b] with grouped keeps it, grouped with an unannotated input drops it, and sorted on [a, b] with grouped keeps it.

@xavlee
xavlee force-pushed the feat/issue-24438-group-contiguous-property branch from aaae646 to 6f84735 Compare November 2, 2026 18:16
@xavlee

xavlee commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@jayzhan211 I updated the PR with the union change/tests. Please re-review when you get the chance 🙇

@jayzhan211
jayzhan211 added this pull request to the merge queue Nov 3, 2026
@jayzhan211

Copy link
Copy Markdown
Contributor

Thanks @xavlee @gene-bordegaray @mhilton 🚀 !

Merged via the queue into apache:main with commit 320a7d2 Nov 3, 2026
44 of 45 checks passed
Tristan1900 added a commit to DataDog/datafusion that referenced this pull request Nov 3, 2026
Backport the Apache DataFusion apache#24438 stack onto DataDog branch-55:
- apache#24737: unsorted contiguous-group regression coverage
- apache#24697: group-clustering terminology and aggregate APIs
- apache#24698: grouping equivalence properties and propagation
- apache#24497: incremental aggregation over explicitly grouped input
- apache#26852: invalidate grouping assertions when clearing orderings

Adapt the changes to DF55's existing aggregate streams. Single-stage
clustered aggregation continues to use GroupedHashAggregateStream;
partial and final aggregation retain the existing specialized streams.
Do not import the unrelated DF56 aggregate-stream migration.

Include the constant-key, grouping-set, and PartialReduce guards. Extend
blocked-input tests across single/final and migrated/legacy execution,
and retain sort-option and metadata-invalidation coverage. Update DF55
plan snapshots, the wrapped memory-limit error assertion, and the API
upgrade guide.

Validation: 503 SQL test files passed, 3347 focused expression/plan unit
tests passed, and all-targets/all-features clippy passed. Extended tests
exposed a dictionary sort-fuzzer failure also reproduced on the unchanged
branch-55 base (root seed 1793724857; dataset 15872832093508421016; query
12465732596605393044; config 11717754459111916353).

Upstream-Commit: 59d74ca
Upstream-Commit: d93699c
Upstream-Commit: 320a7d2
Upstream-Commit: 64a5955
Upstream-Commit: 3c3898e
Upstream-Commit: 86f93b6
Co-authored-by: xavlee <52435309+xavlee@users.noreply.github.com>
Tristan1900 pushed a commit to DataDog/datafusion that referenced this pull request Nov 3, 2026
Backport apache#24698

Backport grouping metadata, projection and union propagation, and explicit
invalidation at aggregate and partition boundaries. Update the DF55 FFI
debug expectation for the additional metadata fields.

Upstream-Commit: 320a7d2
@alamb

alamb commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

epic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants