apache/druid · error · IllegalArgumentException
Must have a single final stage, but found [%d] candidates
Error message
Must have a single final stage, but found [%d] candidates
What it means
A QueryDefinition must have exactly one final stage (a stage no other stage consumes). create computes finalStageCandidates = total stages - non-final stages and throws IAE if the count is not exactly 1, because the query would be ambiguous (multiple roots or none).
Source
Thrown at multi-stage-query/src/main/java/org/apache/druid/msq/kernel/QueryDefinition.java:110
}
for (final StageId nonFinalStageId : nonFinalStages) {
if (!stageMap.containsKey(nonFinalStageId)) {
throw new ISE("Stage [%s] is missing a definition", nonFinalStageId);
}
}
final int finalStageCandidates = stageMap.size() - nonFinalStages.size();
if (finalStageCandidates == 1) {
return new QueryDefinition(
stageMap,
Iterables.getOnlyElement(Sets.difference(stageMap.keySet(), nonFinalStages)),
context == null ? QueryContext.empty() : context
);
} else {
throw new IAE("Must have a single final stage, but found [%d] candidates", finalStageCandidates);
}
}
public static QueryDefinitionBuilder builder(final String queryId)
{
return new QueryDefinitionBuilder(queryId);
}
public static QueryDefinitionBuilder builder(final QueryDefinition queryDef)
{
return new QueryDefinitionBuilder(queryDef.getQueryId()).addAll(queryDef);
}
public String getQueryId()
{
return finalStage.getQueryId();
}
View on GitHub (pinned to 9b90983fd2)
Solutions
- Ensure the plan has exactly one root: merge extra roots or split them into separate QueryDefinitions
- Remove cycles in input-stage references so at least one stage is never consumed
- Fix builder/rewrite logic so only one stage remains unconsumed before calling create
Example fix
// before: two roots builder.add(scanDef); builder.add(scanDef2); // after: union the roots or keep only one per QueryDefinition QueryDefinition q1 = ...singleRootPlan...; QueryDefinition q2 = ...singleRootPlan...;
Defensive patterns
Strategy: validation
Validate before calling
Set<StageId> nonFinal = new HashSet<>();
for (StageDefinition s : stageDefinitions) {
for (int n : s.getInputStageNumbers()) nonFinal.add(new StageId(s.getId().getQueryId(), n));
}
long finals = stageDefinitions.size() - nonFinal.size();
if (finals != 1) {
throw new IllegalArgumentException("Plan must have exactly 1 final stage, found " + finals);
} Try / catch
try {
return QueryDefinition.create(stageDefinitions, context);
} catch (IllegalArgumentException e) {
throw new PlanValidationException("Plan must have a single root/final stage", e);
} Prevention
- Ensure the stage graph is acyclic so at least one stage is never consumed
- Split independent subqueries into separate QueryDefinitions
- After plan rewrites, assert exactly one unconsumed stage remains
When it happens
Trigger: QueryDefinition.create where the stage graph has zero final stages (cycles / all stages consumed) or two or more final stages (disconnected subqueries or an extra unused root stage).
Common situations: Assembling multiple independent queries into one QueryDefinition; plan-rewriting bugs leaving orphan root stages; cycles introduced during plan merges making every stage non-final.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot bucket with %s partitioning
- ClusterBy key must be sortable
- Partition count must be 1 when adjustable is true, but was [
- Cannot bucket with %s partitioning (clusterBy = %s)
- Cannot accept duplicate stage numbers
AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07).
Data as JSON: /api/errors/4a970c37e735b2c2.
Report an issue: GitHub.