apache/druid · error · IllegalArgumentException

Expected [%d] children, got [%d]

Error message

Expected [%d] children, got [%d]

What it means

UnionDataSource.withChildren replaces each input datasource, so the children list must have exactly as many entries as the union's original dataSources. An arity mismatch would silently change the union's shape, so Druid throws IAE.

Source

Thrown at processing/src/main/java/org/apache/druid/query/UnionDataSource.java:98

   */
  public boolean isTableBased()
  {
    return dataSources.stream()
                      .allMatch(dataSource -> dataSource instanceof TableDataSource
                                              || dataSource instanceof RestrictedDataSource);
  }

  @Override
  public List<DataSource> getChildren()
  {
    return ImmutableList.copyOf(dataSources);
  }

  @Override
  public DataSource withChildren(List<DataSource> children)
  {
    if (children.size() != dataSources.size()) {
      throw new IAE("Expected [%d] children, got [%d]", dataSources.size(), children.size());
    }

    return new UnionDataSource(children);
  }

  @Override
  public boolean isCacheable(boolean isBroker)
  {
    // Disables result-level caching for 'union' datasources, which doesn't work currently.
    // See https://github.com/apache/druid/issues/8713 for reference.
    //
    // Note that per-segment caching is still effective, since at the time the per-segment cache evaluates a query
    // for cacheability, it would have already been rewritten to a query on a single table.
    return false;
  }

  @Override
  public boolean isGlobal()

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Pass children.size() == dataSources.size(), mapping each input to its replacement
  2. In rewrite code, keep a 1:1 correspondence between original inputs and replacements
  3. Use DataSourceAnalysis or children inspection to determine arity before calling

Example fix

// before
DataSource rewritten = union.withChildren(collectAllRewrittenChildren(query));
// after
List<DataSource> kids = union.getChildren().stream().map(this::rewrite).collect(Collectors.toList());
DataSource rewritten = union.withChildren(kids);
Defensive patterns

Strategy: validation

Validate before calling

if (children.size() != unionDs.getChildren().size()) {
  throw new IllegalStateException("children must match union input count");
}

Type guard

boolean arityMatches(UnionDataSource u, List<DataSource> children) {
  return u.getChildren().size() == children.size();
}

Try / catch

try {
  DataSource rewritten = unionDs.withChildren(children);
} catch (IllegalArgumentException e) {
  // rebuild children 1:1 from unionDs.getChildren()
}

Prevention

When it happens

Trigger: Calling withChildren with a different number of datasources than the union contains — e.g. rewrite code that flattens or drops one branch before calling withChildren, or passing children of the whole query instead of this datasource.

Common situations: Planner/rewrite passes that rebuild datasource trees generically; code assuming all datasources have one child; unions composed of a dynamic number of tables.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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