{"record":{"id":"7ef81ba0f73a4154","repo":"apache/druid","slug":"join-clause-cannot-have-null-or-empty-prefix","errorCode":null,"errorMessage":"Join clause cannot have null or empty prefix","messagePattern":"Join clause cannot have null or empty prefix","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/segment/join/JoinPrefixUtils.java","lineNumber":44,"sourceCode":"import java.util.Comparator;\nimport java.util.List;\n\n/**\n * Utility class for working with prefixes in join operations\n */\npublic class JoinPrefixUtils\n{\n  private static final Comparator<String> DESCENDING_LENGTH_STRING_COMPARATOR = (s1, s2) ->\n      Integer.compare(s2.length(), s1.length());\n\n  /**\n   * Checks that \"prefix\" is a valid prefix for a join clause (see {@link JoinableClause#getPrefix()}) and, if so,\n   * returns it. Otherwise, throws an exception.\n   */\n  public static String validatePrefix(@Nullable final String prefix)\n  {\n    if (prefix == null || prefix.isEmpty()) {\n      throw new IAE(\"Join clause cannot have null or empty prefix\");\n    } else if (isPrefixedBy(ColumnHolder.TIME_COLUMN_NAME, prefix) || ColumnHolder.TIME_COLUMN_NAME.equals(prefix)) {\n      throw new IAE(\n          \"Join clause cannot have prefix[%s], since it would shadow %s\",\n          prefix,\n          ColumnHolder.TIME_COLUMN_NAME\n      );\n    } else {\n      return prefix;\n    }\n  }\n\n  public static boolean isPrefixedBy(final String columnName, final String prefix)\n  {\n    return columnName.length() > prefix.length() && columnName.startsWith(prefix);\n  }\n\n  /**\n   * Removes the prefix on {@code columnName}. Must only be called if the column name is actually prefixed; i.e.,","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/segment/join/JoinPrefixUtils.java#L26-L62","documentation":"JoinPrefixUtils.validatePrefix checks that a join clause prefix is usable: a null or empty prefix cannot namespace the joinable's columns, so it throws this IAE. Prefixes exist to qualify joined column names and prevent collisions.","triggerScenarios":"Creating a JoinableClause or validating a join in the query planner with prefix null or \"\", e.g. building joins from parsed SQL/subquery references without an explicit prefix.","commonSituations":"Programmatic native query construction omitting the prefix field; planner bugs where subquery join prefixes are not assigned; hand-written native JSON with \"prefix\": \"\".","solutions":["Set a non-empty prefix on the JoinableClause (or use the joinable's name via the prefix field in the native join)","In native JSON joins, populate \"prefix\" (e.g. \"j0.\", \"j1.\") for right-side joinables","If generating queries programmatically, default the prefix to a unique identifier per clause"],"exampleFix":"// before\n{\"rightSubQuery\": ..., \"prefix\": null, ...}\n// after\n{\"rightSubQuery\": ..., \"prefix\": \"j0.\", ...}","handlingStrategy":"validation","validationCode":"if (prefix == null || prefix.isEmpty()) throw new IllegalArgumentException(\"Join prefix required\");","typeGuard":null,"tryCatchPattern":"try { JoinPrefixUtils.validatePrefix(prefix); } catch (IAE e) { if (e.getMessage().contains(\"null or empty\")) { /* set prefix */ } throw e; }","preventionTips":["Always populate prefix in native join queries","Default programmatic prefixes to j0., j1., ..."],"tags":["join","query","prefix"],"backgroundTag":"missing-required-argument","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}