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

  1. Ensure the plan has exactly one root: merge extra roots or split them into separate QueryDefinitions
  2. Remove cycles in input-stage references so at least one stage is never consumed
  3. 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

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


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/4a970c37e735b2c2. Report an issue: GitHub.