{"record":{"id":"55046da1a0d73d8f","repo":"apache/druid","slug":"cannot-return-double-for-null-value-55046d","errorCode":null,"errorMessage":"Cannot return double for Null Value","messagePattern":"Cannot return double for Null Value","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericBufferAggregator.java","lineNumber":111,"sourceCode":"      throw new IllegalStateException(\"Cannot return float for Null Value\");\n    }\n    return delegate.getFloat(buf, position + Byte.BYTES);\n  }\n\n  @Override\n  public long getLong(ByteBuffer buf, int position)\n  {\n    if (buf.get(position) == TypeStrategies.IS_NULL_BYTE) {\n      throw new IllegalStateException(\"Cannot return long for Null Value\");\n    }\n    return delegate.getLong(buf, position + Byte.BYTES);\n  }\n\n  @Override\n  public double getDouble(ByteBuffer buf, int position)\n  {\n    if (buf.get(position) == TypeStrategies.IS_NULL_BYTE) {\n      throw new IllegalStateException(\"Cannot return double for Null Value\");\n    }\n    return delegate.getDouble(buf, position + Byte.BYTES);\n  }\n\n  @Override\n  public boolean isNull(ByteBuffer buf, int position)\n  {\n    return buf.get(position) == TypeStrategies.IS_NULL_BYTE || delegate.isNull(buf, position + Byte.BYTES);\n  }\n\n  @Override\n  public void relocate(int oldPosition, int newPosition, ByteBuffer oldBuffer, ByteBuffer newBuffer)\n  {\n    delegate.relocate(oldPosition + Byte.BYTES, newPosition + Byte.BYTES, oldBuffer, newBuffer);\n  }\n\n  @Override\n  public void close()","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericBufferAggregator.java#L93-L129","documentation":"NullableNumericBufferAggregator's getDouble() inspects the null-marker byte stored before each double value in the ByteBuffer and throws IllegalStateException if the stored value is null. This preserves Druid's explicit null semantics instead of coercing null to 0.0 or NaN.","triggerScenarios":"Calling getDouble(buf, position) where the null-flag byte at `position` equals TypeStrategies.IS_NULL_BYTE (group aggregated only null inputs).","commonSituations":"Double-typed group-by aggregations with all-null groups; buffer-alignment bugs (forgot Byte.BYTES offset when delegating); buffer corruptions from incorrect relocate()/copy logic moving value bytes without flag bytes.","solutions":["Query agg.isNull(buf, position) first and handle the null branch","Confirm all delegate calls offset by position + Byte.BYTES to account for the flag byte","Check relocate() copies both the marker byte and the value when buffers are reorganized","For legitimately null groups, handle null in the result-extraction layer rather than reading the primitive"],"exampleFix":"// before\ndouble v = agg.getDouble(buf, position);\n// after\ndouble v = agg.isNull(buf, position) ? Double.NaN /* or handle null */ : agg.getDouble(buf, position);","handlingStrategy":"validation","validationCode":"if (agg.isNull(buf, position)) {\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return agg.getDouble(buf, position);\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"Cannot return double for Null Value\")) {\n    return null;\n  }\n  throw e;\n}","preventionTips":["Check the null flag before every primitive buffer read","Keep relocate()/copy logic moving flag byte and value together","Verify buffer-size math includes the extra null-flag byte","Add tests exercising all-null groups and buffer reorganization paths"],"tags":["java","aggregation","bytebuffer","null-handling"],"backgroundTag":"internal-invariant-violation","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}