Skip to content

Fix ClassCastException for single-server colocated aggregations in the multi-stage engine#18835

Open
yashmayya wants to merge 1 commit into
apache:masterfrom
yashmayya:fix-mse-direct-agg-object-finalize
Open

Fix ClassCastException for single-server colocated aggregations in the multi-stage engine#18835
yashmayya wants to merge 1 commit into
apache:masterfrom
yashmayya:fix-mse-direct-agg-object-finalize

Conversation

@yashmayya

Copy link
Copy Markdown
Contributor

Problem

With the multi-stage engine and usePhysicalOptimizer=true, a no-GROUP-BY aggregate whose data is colocated on a single server is planned as a single-stage AGGREGATE_DIRECT leaf that returns final results (SERVER_RETURN_FINAL_RESULT). This happens, for example, when a query filters on a single partition value of a partitioned, strictReplicaGroup table so routing prunes to one server.

For aggregation functions whose intermediate type differs from their final type — e.g. DISTINCTCOUNTHLLPLUS (HyperLogLogPlusLONG), DISTINCTCOUNT (SetINT) — the leaf crashes while serializing its output:

SET useMultistageEngine=true; SET usePhysicalOptimizer=true;
SELECT SUM(a), DISTINCTCOUNTHLLPLUS(user_id) FILTER (WHERE user_id <> '') FROM tbl WHERE part_col = 'x'

Received 1 error from stage 1 on Server_...:
class com.clearspring.analytics.stream.cardinality.HyperLogLogPlus cannot be cast to class java.lang.Long

Root cause

AggregationResultsBlock is inconsistent between the two methods the multi-stage leaf relies on:

  • getDataSchema() reports the final column types (e.g. LONG) when isServerReturnFinalResult() is true.
  • getRows() returned the raw intermediate _results (the HyperLogLogPlus object) without finalizing. Only getDataTable() finalized via extractFinalResult().

LeafOperator.composeDirectMseBlock consumes getRows() + getDataSchema(). Because the schema already claims the final type, no type conversion is applied, and the intermediate object is left in a column typed as its final type. It then fails on MAILBOX_SEND when the block is serialized.

Fix

AggregationResultsBlock.getRows() now finalizes via extractFinalResult() when isServerReturnFinalResult() is true, so the rows are consistent with the schema it already advertises. This mirrors getDataTable() and the group-by path, where GroupByCombineOperator already finalizes the indexed table for server-return-final.

Scope

  • Only the v2 physical optimizer produces a no-GROUP-BY single-server DIRECT aggregate. The default (v1) multi-stage planner always splits no-GROUP-BY aggregates into LEAF + FINAL (intermediate OBJECT on the leaf, finalized in the FINAL stage), so it is not affected.
  • GROUP-BY DIRECT aggregates were already safe because GroupByCombineOperator finalizes the table.
  • This is a crash fix on an existing, option-gated path; prior behavior was a hard ClassCastException, so there is no behavior change for any query that previously succeeded.

Testing

Added DirectAggregateObjectIntermediate.json (a replicated table forces single-server colocation → AGGREGATE_DIRECT), covering:

  • SUM + DISTINCTCOUNTHLLPLUS ... FILTER (the reported shape),
  • plain DISTINCTCOUNTHLLPLUS and DISTINCTCOUNT,
  • a primitive-only case (intermediate type == final type) to confirm the finalize loop is a correct no-op,
  • a column-based-null-handling case with zero-match filters so a value finalizes to NULL.

The legacy planner passes and the physical optimizer fails before / passes after the fix. The full ResourceBasedQueriesTest (3589 cases) and the relevant pinot-core aggregation tests are green.

…tRows() for server-return-final

A no-group-by aggregate whose data is colocated on a single server (e.g. partition-pruned
to one partition on a strictReplicaGroup table) is planned by the v2 physical optimizer as a
single-stage AGGREGATE_DIRECT that returns final results (SERVER_RETURN_FINAL_RESULT). For
aggregations whose intermediate type differs from their final type (DISTINCTCOUNTHLLPLUS,
DISTINCTCOUNT, ...), the leaf crashed during serialization with e.g.
'HyperLogLogPlus cannot be cast to Long'.

AggregationResultsBlock.getDataSchema() reports the final column types when
isServerReturnFinalResult() is true, but getRows() returned the raw intermediate results
without finalizing (only getDataTable() finalized). The MSE LeafOperator consumes
getRows() + getDataSchema(), so an intermediate object was left in a column typed as its
final type and failed on MAILBOX_SEND serialization. getRows() now finalizes via
extractFinalResult() when isServerReturnFinalResult() is true, consistent with
getDataTable() and the group-by path (GroupByCombineOperator already finalizes the table).

The default (v1) MSE planner always splits no-group-by aggregates into LEAF+FINAL and is
unaffected; group-by DIRECT aggregates already finalize in the combine operator.
@yashmayya yashmayya added bug Something is not working as expected multi-stage Related to the multi-stage query engine labels Jun 22, 2026
public List<Object[]> getRows() {
return Collections.singletonList(_results.toArray());
if (!_queryContext.isServerReturnFinalResult()) {
return Collections.singletonList(_results.toArray());

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.

(nit, not introduced in this PR) We prefer List.of(x) over Collections.singletonList(x)

@codecov-commenter

codecov-commenter commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.81%. Comparing base (52b8fdc) to head (4887070).

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #18835      +/-   ##
============================================
+ Coverage     64.78%   64.81%   +0.02%     
  Complexity     1319     1319              
============================================
  Files          3392     3392              
  Lines        210988   210994       +6     
  Branches      33127    33128       +1     
============================================
+ Hits         136687   136748      +61     
+ Misses        63283    63229      -54     
+ Partials      11018    11017       -1     
Flag Coverage Δ
custom-integration1 100.00% <ø> (ø)
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-21 64.81% <100.00%> (+0.02%) ⬆️
temurin 64.81% <100.00%> (+0.02%) ⬆️
unittests 64.80% <100.00%> (+0.02%) ⬆️
unittests1 56.99% <100.00%> (+<0.01%) ⬆️
unittests2 37.21% <0.00%> (+0.02%) ⬆️

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.

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

Labels

bug Something is not working as expected multi-stage Related to the multi-stage query engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants