{"record":{"id":"d8fcbe803d87a373","repo":"apache/druid","slug":"unable-to-compare-unexpected-types-s","errorCode":null,"errorMessage":"Unable to compare unexpected types [%s]","messagePattern":"Unable to compare unexpected types \\[(.+?)\\]","errorType":"exception","errorClass":"RE","httpStatus":null,"severity":"error","filePath":"extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java","lineNumber":71,"sourceCode":"  private static final int DEFAULT_NUM_ENTRIES = 1500;\n\n  public static final Comparator COMPARATOR = Comparator.nullsFirst((o1, o2) -> {\n    if (o1 instanceof ByteBuffer && o2 instanceof ByteBuffer) {\n      ByteBuffer buf1 = (ByteBuffer) o1;\n      ByteBuffer buf2 = (ByteBuffer) o2;\n      return Integer.compare(\n          BloomKFilter.getNumSetBits(buf1, buf1.position()),\n          BloomKFilter.getNumSetBits(buf2, buf2.position())\n      );\n    } else if (o1 instanceof BloomKFilter && o2 instanceof BloomKFilter) {\n      BloomKFilter f1 = (BloomKFilter) o1;\n      BloomKFilter f2 = (BloomKFilter) o2;\n      return Integer.compare(\n          f1.getNumSetBits(),\n          f2.getNumSetBits()\n      );\n    } else {\n      throw new RE(\"Unable to compare unexpected types [%s]\", o1.getClass().getName());\n    }\n  });\n\n  private final String name;\n  private final DimensionSpec field;\n  private final int maxNumEntries;\n\n  @JsonCreator\n  public BloomFilterAggregatorFactory(\n      @JsonProperty(\"name\") String name,\n      @JsonProperty(\"field\") final DimensionSpec field,\n      @JsonProperty(\"maxNumEntries\") @Nullable Integer maxNumEntries\n  )\n  {\n    this.name = name;\n    this.field = field;\n    this.maxNumEntries = maxNumEntries != null ? maxNumEntries : DEFAULT_NUM_ENTRIES;\n  }","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/extensions-core/druid-bloom-filter/src/main/java/org/apache/druid/query/aggregation/bloom/BloomFilterAggregatorFactory.java#L53-L89","documentation":"The static COMPARATOR in BloomFilterAggregatorFactory can only compare pairs of ByteBuffer or pairs of BloomKFilter objects, ranking them by number of set bits. If it is handed any other type (or a mixed pair), it throws this RuntimeException with the actual class name of o1. This guards the query machinery against feeding unsupported intermediate objects into sorting/merging of bloom filter aggregation results.","triggerScenarios":"Calling getComparator()-based comparisons (e.g. in merge/sort buffers or GroupByMerge query machinery) with objects that are neither ByteBuffer nor BloomKFilter, or comparing one of each; typically caused by a corrupted intermediate result, a custom aggregator subclass returning the wrong type, or deserialized results not passing through BloomFilterAggregatorFactory.deserialize().","commonSituations":"Distributed/group-by queries where intermediate results were serialized differently than expected; custom query tooling invoking the comparator directly with byte[] or String results (the native JSON result is base64 String, which deserialize() must convert to ByteBuffer first); version skew between nodes producing different intermediate representations.","solutions":["Pass the object through the aggregator factory's deserialize(Object) method (base64 String -> ByteBuffer) before comparing","Ensure only ByteBuffer (post-deserialize) or BloomKFilter objects are given to this comparator; convert byte[] with ByteBuffer.wrap","Check all Druid cluster nodes run the same druid-bloom-filter extension version","If using the comparator directly, instanceof-check inputs first and reject/convert unknown types"],"exampleFix":"// before\nObject a = \"<base64 string from JSON result>\";\nint cmp = factory.getComparator().compare(a, b); // throws RE\n// after\nObject a = factory.deserialize(\"<base64 string from JSON result>\"); // -> ByteBuffer\nint cmp = factory.getComparator().compare(a, factory.deserialize(b));","handlingStrategy":"type-guard","validationCode":"// Java: ensure inputs are comparable before invoking COMPARATOR\nstatic boolean comparable(Object o1, Object o2) {\n  boolean b1 = o1 instanceof ByteBuffer || o1 instanceof BloomKFilter;\n  boolean b2 = o2 instanceof ByteBuffer || o2 instanceof BloomKFilter;\n  return b1 && b2 && o1.getClass().equals(o2.getClass());\n}","typeGuard":"if (!(o instanceof ByteBuffer) && !(o instanceof BloomKFilter)) { o = factory.deserialize(o); }","tryCatchPattern":"try {\n  comparator.compare(a, b);\n} catch (RuntimeException e) {\n  if (e.getMessage().startsWith(\"Unable to compare unexpected types\")) {\n    a = factory.deserialize(a); b = factory.deserialize(b); // retry after conversion\n  } else throw e;\n}","preventionTips":["Always route result objects through BloomFilterAggregatorFactory.deserialize() before sorting/comparing","Never compare raw JSON (base64 String) results with this comparator","Keep extension versions consistent across all Druid nodes"],"tags":["druid","bloom-filter","type-mismatch","comparator"],"backgroundTag":"type-mismatch","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"}