{"record":{"id":"d07dea6c339c4690","repo":"apache/druid","slug":"expected-d-children-got-d","errorCode":null,"errorMessage":"Expected [%d] children, got [%d]","messagePattern":"Expected \\[(.+?)\\] children, got \\[(.+?)\\]","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/query/UnionDataSource.java","lineNumber":98,"sourceCode":"   */\n  public boolean isTableBased()\n  {\n    return dataSources.stream()\n                      .allMatch(dataSource -> dataSource instanceof TableDataSource\n                                              || dataSource instanceof RestrictedDataSource);\n  }\n\n  @Override\n  public List<DataSource> getChildren()\n  {\n    return ImmutableList.copyOf(dataSources);\n  }\n\n  @Override\n  public DataSource withChildren(List<DataSource> children)\n  {\n    if (children.size() != dataSources.size()) {\n      throw new IAE(\"Expected [%d] children, got [%d]\", dataSources.size(), children.size());\n    }\n\n    return new UnionDataSource(children);\n  }\n\n  @Override\n  public boolean isCacheable(boolean isBroker)\n  {\n    // Disables result-level caching for 'union' datasources, which doesn't work currently.\n    // See https://github.com/apache/druid/issues/8713 for reference.\n    //\n    // Note that per-segment caching is still effective, since at the time the per-segment cache evaluates a query\n    // for cacheability, it would have already been rewritten to a query on a single table.\n    return false;\n  }\n\n  @Override\n  public boolean isGlobal()","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/query/UnionDataSource.java#L80-L116","documentation":"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.","triggerScenarios":"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.","commonSituations":"Planner/rewrite passes that rebuild datasource trees generically; code assuming all datasources have one child; unions composed of a dynamic number of tables.","solutions":["Pass children.size() == dataSources.size(), mapping each input to its replacement","In rewrite code, keep a 1:1 correspondence between original inputs and replacements","Use DataSourceAnalysis or children inspection to determine arity before calling"],"exampleFix":"// before\nDataSource rewritten = union.withChildren(collectAllRewrittenChildren(query));\n// after\nList<DataSource> kids = union.getChildren().stream().map(this::rewrite).collect(Collectors.toList());\nDataSource rewritten = union.withChildren(kids);","handlingStrategy":"validation","validationCode":"if (children.size() != unionDs.getChildren().size()) {\n  throw new IllegalStateException(\"children must match union input count\");\n}","typeGuard":"boolean arityMatches(UnionDataSource u, List<DataSource> children) {\n  return u.getChildren().size() == children.size();\n}","tryCatchPattern":"try {\n  DataSource rewritten = unionDs.withChildren(children);\n} catch (IllegalArgumentException e) {\n  // rebuild children 1:1 from unionDs.getChildren()\n}","preventionTips":["Always derive children from ds.getChildren() before replacing","Never merge/drop branches inside a rewrite pass that later calls withChildren"],"tags":["druid","datasource","arity-mismatch"],"backgroundTag":"invalid-argument-value","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}