Skip to content

Fix GROUP BY DISTINCTCOUNT ClassCastException in mergeDataTablesOnly#18842

Draft
navina wants to merge 2 commits into
apache:masterfrom
navina:fix-merge-only-reducer-distinctcount
Draft

Fix GROUP BY DISTINCTCOUNT ClassCastException in mergeDataTablesOnly#18842
navina wants to merge 2 commits into
apache:masterfrom
navina:fix-merge-only-reducer-distinctcount

Conversation

@navina

@navina navina commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Background

BrokerReduceService exposes two reduce paths:

  • reduceResult(...) — the normal path: merge per-server results and finalize.
  • mergeDataTablesOnly(...) — merge-only: produce an intermediate DataTable that can be re-merged later via the normal reduce path. Used by callers that need to combine partial scatters without finalizing.

The merge-only path's contract is that aggregate values stay as intermediates end-to-end.

What's wrong

OSS's single-stage broker request handler auto-sets SERVER_RETURN_FINAL_RESULT=true when the routing table targets exactly one server (BaseSingleStageBrokerRequestHandler.java:844). That optimization causes servers to return finalized scalars (e.g. Integer for DISTINCTCOUNT, Long for DISTINCTCOUNTHLL) instead of intermediate sketches — which directly contradicts the merge-only contract: the input is already final-typed, not intermediate.

Two ways this manifested in GroupByDataTableReducer.mergeDataTablesOnly:

  1. No guard for the single-server-final-result mode. When _queryContext.isServerReturnFinalResult() is true, the input DataTables hold final values, so merge-only cannot honor its contract. The previous code silently proceeded and produced a malformed intermediate.

  2. getIndexedTable over-finalized internally. Even when servers correctly returned intermediates, the shared helper unconditionally called indexedTable.finish(true, true). For a GROUP BY query with an OBJECT-typed intermediate aggregate (DISTINCTCOUNT, DISTINCTCOUNTHLL, etc.), storeFinalResult=true:

    • mutated DataSchema._columnDataTypes from OBJECT to the final-result type in place (IndexedTable.java:170-174), and
    • replaced each row's value with extractFinalResult(value) (Set → Integer for DISTINCTCOUNT) at IndexedTable.java:195.

    The subsequent buildIntermediateDataTable read DataSchema's lazily-populated _storedColumnDataTypes (populated earlier from the pre-finalize OBJECT schema and never invalidated by finish), so the OBJECT branch fired and handed the now-Integer value into BaseDistinctAggregateAggregationFunction.serializeIntermediateResult(Set), which threw ClassCastException.

Fix

  • Reject the single-server final-result mode explicitly in mergeDataTablesOnly: throw UnsupportedOperationException with a clear message naming SERVER_RETURN_FINAL_RESULT. Callers expecting an intermediate must either disable this option or use reduceResult instead.
  • Move finish() out of getIndexedTable and let each caller pick the right mode:
    • reduceResult keeps finish(true, true) — downstream consumers expect final scalars.
    • mergeDataTablesOnly calls finish(true, false) so aggregate values stay as intermediates and round-trip through buildIntermediateDataTable correctly.

Tests

Adds testGroupByDistinctCountObjectRoundTrip in MergeDataTablesOnlyTest: servers emit intermediate OBJECT-encoded Sets, merge produces a re-injectable intermediate DataTable, and reducing that intermediate matches a direct reduce of the same per-server inputs.

Release Notes

Fixes a ClassCastException in mergeDataTablesOnly when merging GROUP BY results that include OBJECT-typed intermediate aggregates (e.g. DISTINCTCOUNT, DISTINCTCOUNTHLL). mergeDataTablesOnly now also throws UnsupportedOperationException up front when called against a query whose servers will return final results (e.g. the single-server SERVER_RETURN_FINAL_RESULT auto-set), instead of silently producing a malformed intermediate.

mergeDataTablesOnly reused getIndexedTable, which unconditionally called
indexedTable.finish(true, true). For a GROUP BY query with an OBJECT-typed
intermediate aggregate (DISTINCTCOUNT, DISTINCTCOUNTHLL, etc.), that
storeFinalResult=true call:
- mutated DataSchema._columnDataTypes from OBJECT to the final-result type
  in place (IndexedTable.java:170-174), and
- replaced each row's value with extractFinalResult(value) (Set → Integer
  for DISTINCTCOUNT) at IndexedTable.java:195.

The subsequent buildIntermediateDataTable read DataSchema's cached
_storedColumnDataTypes — populated earlier from the pre-finalize OBJECT
schema and never invalidated by finish — so the OBJECT branch fired and
handed the now-Integer value into
BaseDistinctAggregateAggregationFunction.serializeIntermediateResult(Set),
which threw ClassCastException.

Fix: move finish() out of getIndexedTable and let each caller pick the
right mode. reduceResult keeps finish(true, true) (downstream consumers
expect final scalars). mergeDataTablesOnly calls finish(true, false) so
aggregate values stay as intermediates and round-trip through
buildIntermediateDataTable correctly.

Adds a testGroupByDistinctCountObjectRoundTrip regression: server emits
intermediate OBJECT-encoded Sets, merge produces a re-injectable
intermediate DataTable, and reducing that intermediate matches a direct
reduce of the same servers.
@navina navina force-pushed the fix-merge-only-reducer-distinctcount branch from 02d46d8 to af6f235 Compare June 23, 2026 19:22
@codecov-commenter

codecov-commenter commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.77%. Comparing base (302783f) to head (27b3b99).

Additional details and impacted files
@@            Coverage Diff            @@
##             master   #18842   +/-   ##
=========================================
  Coverage     64.77%   64.77%           
  Complexity     1322     1322           
=========================================
  Files          3393     3393           
  Lines        211022   211023    +1     
  Branches      33135    33135           
=========================================
+ Hits         136687   136695    +8     
+ Misses        63322    63310   -12     
- Partials      11013    11018    +5     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-21 64.77% <100.00%> (+<0.01%) ⬆️
temurin 64.77% <100.00%> (+<0.01%) ⬆️
unittests 64.77% <100.00%> (+<0.01%) ⬆️
unittests1 56.98% <100.00%> (-0.01%) ⬇️
unittests2 37.18% <0.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Previous run hit a Maven local-repo lock-contention failure in Unit Test Set 2
(annotation-processor classpath resolution timed out at 900s on hk2 / immutables
artifacts). Unrelated to the code change in this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants