apache/druid · error · IllegalArgumentException

Must have a single final stage, but found

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).

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.

Appendix: 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)