This is an automated email from the ASF dual-hosted git repository.

karan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 2208a8c3b24 HavingSpecMetricComparator: Return -1 when finalized 
metric is null. (#17911)
2208a8c3b24 is described below

commit 2208a8c3b24a5f375b139d3e38f233ba40088f58
Author: Gian Merlino <[email protected]>
AuthorDate: Tue Apr 15 03:07:13 2025 -0700

    HavingSpecMetricComparator: Return -1 when finalized metric is null. 
(#17911)
    
    * HavingSpecMetricComparator: Return -1 when finalized metric is null.
    
    This encodes an assumption around nulls-first ordering, rather than
    branching into the "custom type" code path. Fixes a bug where equalTo,
    greaterThan, and lessThan having specs would throw null-pointer exceptions
    when used with first/last aggregators that return nulls.
    
    This patch also switches error handling to DruidException and adds
    defensive checks.
    
    * Add coverage.
---
 .../groupby/having/HavingSpecMetricComparator.java | 27 ++++++++---
 .../having/HavingSpecMetricComparatorTest.java     | 53 +++++++++++++++++++++-
 .../druid/query/groupby/having/HavingSpecTest.java | 20 ++++++++
 3 files changed, 93 insertions(+), 7 deletions(-)

diff --git 
a/processing/src/main/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparator.java
 
b/processing/src/main/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparator.java
index e3f786e1f8a..9a4b3597c49 100644
--- 
a/processing/src/main/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparator.java
+++ 
b/processing/src/main/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparator.java
@@ -22,9 +22,11 @@ package org.apache.druid.query.groupby.having;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.primitives.Doubles;
 import com.google.common.primitives.Longs;
-import org.apache.druid.java.util.common.ISE;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.error.InvalidInput;
 import org.apache.druid.query.aggregation.AggregatorFactory;
 
+import javax.annotation.Nullable;
 import java.math.BigDecimal;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -35,8 +37,17 @@ class HavingSpecMetricComparator
 {
   static final Pattern LONG_PAT = Pattern.compile("[-|+]?\\d+");
 
-  static int compare(String aggregationName, Number value, Map<String, 
AggregatorFactory> aggregators, Object metricValueObj)
+  static int compare(
+      String aggregationName,
+      Number value,
+      @Nullable Map<String, AggregatorFactory> aggregators,
+      Object metricValueObj
+  )
   {
+    if (value == null) {
+      throw DruidException.defensive("Unexpected null match value");
+    }
+
     if (metricValueObj != null) {
       if (aggregators != null && aggregators.containsKey(aggregationName)) {
         metricValueObj = 
aggregators.get(aggregationName).finalizeComputation(metricValueObj);
@@ -52,7 +63,7 @@ class HavingSpecMetricComparator
           // Invert the comparison since we're providing n, value backwards.
           return -compareDoubleToLong(value.doubleValue(), n);
         } else {
-          throw new ISE("Number was[%s]?!?", value.getClass().getName());
+          throw DruidException.defensive("Number was[%s]?!?", 
value.getClass().getName());
         }
       } else if (metricValueObj instanceof Double || metricValueObj instanceof 
Float) {
         // Cast floats to doubles, it's fine since doubles can represent all 
float values.
@@ -63,7 +74,7 @@ class HavingSpecMetricComparator
         } else if (value instanceof Double || value instanceof Float) {
           return Doubles.compare(n, value.doubleValue());
         } else {
-          throw new ISE("Number was[%s]?!?", value.getClass().getName());
+          throw DruidException.defensive("Number was[%s]?!?", 
value.getClass().getName());
         }
       } else if (metricValueObj instanceof String) {
         String metricValueStr = (String) metricValueObj;
@@ -74,6 +85,10 @@ class HavingSpecMetricComparator
           double d = Double.parseDouble(metricValueStr);
           return Double.compare(d, value.doubleValue());
         }
+      } else if (metricValueObj == null) {
+        // Finalized value was null. Use nulls-first ordering. The matchable 
"value" cannot be null, so the result
+        // must be -1.
+        return -1;
       } else if (aggregators != null && 
aggregators.containsKey(aggregationName)) {
         // Use custom comparator in case of custom aggregation types
         AggregatorFactory aggregatorFactory = aggregators.get(aggregationName);
@@ -83,10 +98,10 @@ class HavingSpecMetricComparator
                                     aggregatorFactory.deserialize(value)
                                 );
       } else {
-        throw new ISE("Unknown type of metric value: %s", metricValueObj);
+        throw InvalidInput.exception("Unknown type of metric value[%s]", 
metricValueObj);
       }
     } else {
-      return Double.compare(0, value.doubleValue());
+      throw DruidException.defensive("Unexpected null metric value");
     }
   }
 
diff --git 
a/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparatorTest.java
 
b/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparatorTest.java
index 46ba9758395..d3fe65a0d3f 100644
--- 
a/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparatorTest.java
+++ 
b/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecMetricComparatorTest.java
@@ -19,10 +19,14 @@
 
 package org.apache.druid.query.groupby.having;
 
+import org.apache.druid.error.DruidException;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.math.BigDecimal;
+
 /**
+ *
  */
 public class HavingSpecMetricComparatorTest
 {
@@ -99,6 +103,53 @@ public class HavingSpecMetricComparatorTest
     Assert.assertEquals(0, HavingSpecMetricComparator.compareDoubleToLong(0D, 
0));
     Assert.assertEquals(0, HavingSpecMetricComparator.compareDoubleToLong(-0D, 
0));
     Assert.assertEquals(1, 
HavingSpecMetricComparator.compareDoubleToLong((double) Long.MAX_VALUE + 1, 
Long.MAX_VALUE));
-    Assert.assertEquals(-1, 
HavingSpecMetricComparator.compareDoubleToLong((double) Long.MIN_VALUE - 1, 
Long.MIN_VALUE));
+    Assert.assertEquals(
+        -1,
+        HavingSpecMetricComparator.compareDoubleToLong((double) Long.MIN_VALUE 
- 1, Long.MIN_VALUE)
+    );
+  }
+
+  @Test
+  public void testNullValue()
+  {
+    final DruidException e = Assert.assertThrows(
+        DruidException.class,
+        () -> HavingSpecMetricComparator.compare("metric", null, null, 10)
+    );
+
+    Assert.assertEquals(DruidException.Category.DEFENSIVE, e.getCategory());
+  }
+
+  @Test
+  public void testNullMetricValue()
+  {
+    final DruidException e = Assert.assertThrows(
+        DruidException.class,
+        () -> HavingSpecMetricComparator.compare("metric", 10, null, null)
+    );
+
+    Assert.assertEquals(DruidException.Category.DEFENSIVE, e.getCategory());
+  }
+
+  @Test
+  public void testUnsupportedNumberTypeLongValue()
+  {
+    final DruidException e = Assert.assertThrows(
+        DruidException.class,
+        () -> HavingSpecMetricComparator.compare("metric", 
BigDecimal.valueOf(10), null, 10)
+    );
+
+    Assert.assertEquals(DruidException.Category.DEFENSIVE, e.getCategory());
+  }
+
+  @Test
+  public void testUnsupportedNumberTypeDoubleValue()
+  {
+    final DruidException e = Assert.assertThrows(
+        DruidException.class,
+        () -> HavingSpecMetricComparator.compare("metric", 
BigDecimal.valueOf(10), null, 10d)
+    );
+
+    Assert.assertEquals(DruidException.Category.DEFENSIVE, e.getCategory());
   }
 }
diff --git 
a/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecTest.java
 
b/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecTest.java
index fbf42ca3765..25b9e884a67 100644
--- 
a/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecTest.java
+++ 
b/processing/src/test/java/org/apache/druid/query/groupby/having/HavingSpecTest.java
@@ -24,6 +24,9 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.query.aggregation.SerializablePairLongDouble;
+import 
org.apache.druid.query.aggregation.firstlast.last.DoubleLastAggregatorFactory;
 import org.apache.druid.query.cache.CacheKeyBuilder;
 import org.apache.druid.query.groupby.GroupByQuery;
 import org.apache.druid.query.groupby.ResultRow;
@@ -208,6 +211,23 @@ public class HavingSpecTest
     Assert.assertFalse(spec.eval(getTestRow(Long.MAX_VALUE)));
   }
 
+  @Test
+  public void testEqualHavingSpecWithAggregator()
+  {
+    EqualToHavingSpec spec = new EqualToHavingSpec("metric", 1.0d);
+    spec.setQuery(
+        GroupByQuery.builder()
+                    .setDataSource("dummy")
+                    .setInterval("1000/3000")
+                    .setGranularity(Granularities.ALL)
+                    .setAggregatorSpecs(new 
DoubleLastAggregatorFactory("metric", "metric", null))
+                    .build()
+    );
+    Assert.assertTrue(spec.eval(getTestRow(new SerializablePairLongDouble(0L, 
1d))));
+    Assert.assertFalse(spec.eval(getTestRow(new SerializablePairLongDouble(0L, 
1.1d))));
+    Assert.assertFalse(spec.eval(getTestRow(new SerializablePairLongDouble(0L, 
null))));
+  }
+
   private static class CountingHavingSpec implements HavingSpec
   {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to