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 c9b35852cd9 MSQ: Write summary row for GROUP BY (). (#16326)
c9b35852cd9 is described below

commit c9b35852cd96848dee74b0dad527da19bc81332f
Author: Gian Merlino <[email protected]>
AuthorDate: Thu Feb 6 08:25:53 2025 -0800

    MSQ: Write summary row for GROUP BY (). (#16326)
    
    
    
    This allows us to remove "msqIncompatible()" from a couple of tests that 
involve GROUP BY () summary rows.
    
    To handle the case where the SQL layer drops a dimension like GROUP BY 
'constant', this patch also adds a "hasDroppedDimensions" context flag to the 
groupBy query.
---
 .../groupby/GroupByPostShuffleFrameProcessor.java  |  38 +++++
 .../msq/querykit/groupby/GroupByQueryKit.java      |   2 +-
 .../druid/msq/test/CalciteSelectQueryMSQTest.java  |  15 --
 .../apache/druid/query/groupby/GroupByQuery.java   |  15 ++
 .../apache/druid/query/groupby/GroupingEngine.java |   7 +-
 .../timeseries/TimeseriesQueryQueryToolChest.java  |  68 +++++----
 .../TimeseriesQueryQueryToolChestTest.java         |  28 +++-
 .../apache/druid/sql/calcite/rel/DruidQuery.java   |  10 +-
 .../apache/druid/sql/calcite/CalciteQueryTest.java | 153 +++++++++++++++++++--
 9 files changed, 279 insertions(+), 57 deletions(-)

diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java
index e9783b8366c..f2c476047d1 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java
@@ -25,6 +25,7 @@ import org.apache.druid.frame.Frame;
 import org.apache.druid.frame.channel.FrameWithPartition;
 import org.apache.druid.frame.channel.ReadableFrameChannel;
 import org.apache.druid.frame.channel.WritableFrameChannel;
+import org.apache.druid.frame.key.ClusterBy;
 import org.apache.druid.frame.processor.FrameProcessor;
 import org.apache.druid.frame.processor.FrameProcessors;
 import org.apache.druid.frame.processor.FrameRowTooLargeException;
@@ -44,6 +45,7 @@ import 
org.apache.druid.query.groupby.epinephelinae.RowBasedGrouperHelper;
 import org.apache.druid.query.groupby.having.AlwaysHavingSpec;
 import org.apache.druid.query.groupby.having.DimFilterHavingSpec;
 import org.apache.druid.query.groupby.having.HavingSpec;
+import org.apache.druid.query.timeseries.TimeseriesQueryQueryToolChest;
 import org.apache.druid.segment.ColumnSelectorFactory;
 import org.apache.druid.segment.ColumnValueSelector;
 import org.apache.druid.segment.Cursor;
@@ -83,6 +85,7 @@ public class GroupByPostShuffleFrameProcessor implements 
FrameProcessor<Object>
   private Supplier<ResultRow> rowSupplierFromFrameCursor;
   private ResultRow outputRow = null;
   private FrameWriter frameWriter = null;
+  private long outputRows = 0L;
 
   public GroupByPostShuffleFrameProcessor(
       final GroupByQuery query,
@@ -139,6 +142,7 @@ public class GroupByPostShuffleFrameProcessor implements 
FrameProcessor<Object>
         }
 
         writeCurrentFrameIfNeeded();
+        writeEmptyAggregationsFrameIfNeeded();
         return ReturnOrAwait.returnObject(Unit.instance());
       } else {
         final Frame frame = inputChannel.read();
@@ -263,6 +267,40 @@ public class GroupByPostShuffleFrameProcessor implements 
FrameProcessor<Object>
       outputChannel.write(new FrameWithPartition(frame, 
FrameWithPartition.NO_PARTITION));
       frameWriter.close();
       frameWriter = null;
+      outputRows += frame.numRows();
+    }
+  }
+
+  /**
+   * Generate a frame containing a single row with default values of all 
aggregations if needed. This method uses
+   * {@link GroupingEngine#summaryRowPreconditions(GroupByQuery)} to determine 
if such an operation is needed.
+   *
+   * Note that in cases where {@link 
GroupingEngine#summaryRowPreconditions(GroupByQuery)} returns true, the
+   * preceding {@link GroupByPreShuffleFrameProcessorFactory} stage would use 
an empty {@link ClusterBy}. Therefore,
+   * there would only be a single output partition of the prior stage, and 
therefore a single instance of
+   * this processor. This ensures that only a single null-aggregations row is 
generated for the entire stage.
+   */
+  private void writeEmptyAggregationsFrameIfNeeded() throws IOException
+  {
+    if (outputRows == 0 && GroupingEngine.summaryRowPreconditions(query)) {
+      final int resultRowSize = query.getResultRowSignature().size();
+      this.outputRow = ResultRow.create(resultRowSize);
+      final Object[] emptyResultArray = 
TimeseriesQueryQueryToolChest.getEmptyAggregations(query.getAggregatorSpecs());
+      if (query.getResultRowHasTimestamp()) {
+        // Can happen if the query has granularity "ALL" but no intervals. In 
this case nothing is matched and the
+        // __time column will be ignored, but it's there in the result row 
signature anyway, so we need to populate it.
+        outputRow.set(0, 0L);
+      }
+      System.arraycopy(
+          emptyResultArray,
+          0,
+          outputRow.getArray(),
+          query.getResultRowAggregatorStart(),
+          emptyResultArray.length
+      );
+      setUpFrameWriterIfNeeded();
+      writeOutputRow();
+      writeCurrentFrameIfNeeded();
     }
   }
 
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
index 42f39fb78e5..d8be6ca423e 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByQueryKit.java
@@ -148,7 +148,7 @@ public class GroupByQueryKit implements 
QueryKit<GroupByQuery>
 
       partitionBoost = true;
     } else {
-      shuffleSpecFactoryPreAggregation = doLimitOrOffset
+      shuffleSpecFactoryPreAggregation = doLimitOrOffset || 
intermediateClusterBy.isEmpty()
                                          ? 
ShuffleSpecFactories.singlePartition()
                                          : resultShuffleSpecFactory;
 
diff --git 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteSelectQueryMSQTest.java
 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteSelectQueryMSQTest.java
index 566778ee7c9..33aa39e21a5 100644
--- 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteSelectQueryMSQTest.java
+++ 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/CalciteSelectQueryMSQTest.java
@@ -181,19 +181,4 @@ public class CalciteSelectQueryMSQTest extends 
CalciteQueryTest
         )
         .run();
   }
-
-  @Override
-  @Test
-  public void testFilterParseLongNullable()
-  {
-    // this isn't really correct in default value mode, the result should be 
ImmutableList.of(new Object[]{0L})
-    // but MSQ is missing default aggregator values in empty group results. 
this override can be removed when this
-    // is fixed
-    testBuilder().queryContext(QUERY_CONTEXT_DEFAULT)
-                 .sql("select count(*) from druid.foo where parse_long(dim1, 
10) is null")
-                 .expectedResults(
-                     ImmutableList.of(new Object[]{4L})
-                 )
-                 .run();
-  }
 }
diff --git 
a/processing/src/main/java/org/apache/druid/query/groupby/GroupByQuery.java 
b/processing/src/main/java/org/apache/druid/query/groupby/GroupByQuery.java
index 79282735f38..7a72002e1ed 100644
--- a/processing/src/main/java/org/apache/druid/query/groupby/GroupByQuery.java
+++ b/processing/src/main/java/org/apache/druid/query/groupby/GroupByQuery.java
@@ -91,6 +91,13 @@ public class GroupByQuery extends BaseQuery<ResultRow>
   public static final String CTX_TIMESTAMP_RESULT_FIELD = 
"timestampResultField";
   public static final String CTX_TIMESTAMP_RESULT_FIELD_GRANULARITY = 
"timestampResultFieldGranularity";
   public static final String CTX_TIMESTAMP_RESULT_FIELD_INDEX = 
"timestampResultFieldInOriginalDimensions";
+
+  /**
+   * Context key for whether this query has any "dropped" dimensions. This is 
set true for queries like
+   * "GROUP BY 'constant'", and enables {@link 
GroupingEngine#summaryRowPreconditions(GroupByQuery)} to correctly
+   * determine whether to include a summary row.
+   */
+  public static final String CTX_HAS_DROPPED_DIMENSIONS = 
"hasDroppedDimensions";
   private static final String CTX_KEY_FUDGE_TIMESTAMP = "fudgeTimestamp";
 
   private static final Comparator<ResultRow> NON_GRANULAR_TIME_COMP =
@@ -465,6 +472,14 @@ public class GroupByQuery extends BaseQuery<ResultRow>
     return 
context().getBoolean(GroupByQueryConfig.CTX_KEY_APPLY_LIMIT_PUSH_DOWN, true);
   }
 
+  /**
+   * See {@link #CTX_HAS_DROPPED_DIMENSIONS}.
+   */
+  public boolean hasDroppedDimensions()
+  {
+    return context().getBoolean(CTX_HAS_DROPPED_DIMENSIONS, false);
+  }
+
   @Override
   public Ordering getResultOrdering()
   {
diff --git 
a/processing/src/main/java/org/apache/druid/query/groupby/GroupingEngine.java 
b/processing/src/main/java/org/apache/druid/query/groupby/GroupingEngine.java
index acb07c2298f..91629706788 100644
--- 
a/processing/src/main/java/org/apache/druid/query/groupby/GroupingEngine.java
+++ 
b/processing/src/main/java/org/apache/druid/query/groupby/GroupingEngine.java
@@ -983,7 +983,10 @@ public class GroupingEngine
         }));
   }
 
-  private static boolean summaryRowPreconditions(GroupByQuery query)
+  /**
+   * Whether a query should include a summary row. True for queries that 
correspond to SQL GROUP BY ().
+   */
+  public static boolean summaryRowPreconditions(GroupByQuery query)
   {
     LimitSpec limit = query.getLimitSpec();
     if (limit instanceof DefaultLimitSpec) {
@@ -992,7 +995,7 @@ public class GroupingEngine
         return false;
       }
     }
-    if (!query.getDimensions().isEmpty()) {
+    if (!query.getDimensions().isEmpty() || query.hasDroppedDimensions()) {
       return false;
     }
     if (query.getGranularity().isFinerThan(Granularities.ALL)) {
diff --git 
a/processing/src/main/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChest.java
 
b/processing/src/main/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChest.java
index 91fddf49eef..41ecbf1ddd2 100644
--- 
a/processing/src/main/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChest.java
+++ 
b/processing/src/main/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChest.java
@@ -160,8 +160,9 @@ public class TimeseriesQueryQueryToolChest extends 
QueryToolChest<Result<Timeser
         // Usally it is NOT Okay to materialize results via toList(), but 
Granularity is ALL thus
         // we have only one record.
         final List<Result<TimeseriesResultValue>> val = baseResults.toList();
-        finalSequence = val.isEmpty() ? 
Sequences.simple(Collections.singletonList(
-            getNullTimeseriesResultValue(query))) : Sequences.simple(val);
+        finalSequence = val.isEmpty()
+                        ? 
Sequences.simple(Collections.singletonList(getEmptyTimeseriesResultValue(query)))
+                        : Sequences.simple(val);
       } else {
         finalSequence = baseResults;
       }
@@ -227,32 +228,17 @@ public class TimeseriesQueryQueryToolChest extends 
QueryToolChest<Result<Timeser
     return ResultGranularTimestampComparator.create(query.getGranularity(), 
((TimeseriesQuery) query).isDescending());
   }
 
-  private Result<TimeseriesResultValue> 
getNullTimeseriesResultValue(TimeseriesQuery query)
+  /**
+   * Returns a {@link TimeseriesResultValue} that corresponds to an empty-set 
aggregation, which is used in situations
+   * where we want to return a single result representing "nothing was 
aggregated".
+   */
+  Result<TimeseriesResultValue> getEmptyTimeseriesResultValue(TimeseriesQuery 
query)
   {
-    List<AggregatorFactory> aggregatorSpecs = query.getAggregatorSpecs();
-    Aggregator[] aggregators = new Aggregator[aggregatorSpecs.size()];
-    String[] aggregatorNames = new String[aggregatorSpecs.size()];
-    RowSignature aggregatorsSignature =
-        RowSignature.builder().addAggregators(aggregatorSpecs, 
RowSignature.Finalization.UNKNOWN).build();
-    for (int i = 0; i < aggregatorSpecs.size(); i++) {
-      aggregators[i] =
-          aggregatorSpecs.get(i)
-                         .factorize(
-                             RowBasedColumnSelectorFactory.create(
-                                 RowAdapters.standardRow(),
-                                 () -> new MapBasedRow(null, null),
-                                 aggregatorsSignature,
-                                 false,
-                                 false
-                             )
-                         );
-      aggregatorNames[i] = aggregatorSpecs.get(i).getName();
-    }
+    final Object[] resultArray = 
getEmptyAggregations(query.getAggregatorSpecs());
     final DateTime start = query.getIntervals().isEmpty() ? DateTimes.EPOCH : 
query.getIntervals().get(0).getStart();
     TimeseriesResultBuilder bob = new TimeseriesResultBuilder(start);
-    for (int i = 0; i < aggregatorSpecs.size(); i++) {
-      bob.addMetric(aggregatorNames[i], aggregators[i].get());
-      aggregators[i].close();
+    for (int i = 0; i < query.getAggregatorSpecs().size(); i++) {
+      bob.addMetric(query.getAggregatorSpecs().get(i).getName(), 
resultArray[i]);
     }
     return bob.build();
   }
@@ -545,4 +531,36 @@ public class TimeseriesQueryQueryToolChest extends 
QueryToolChest<Result<Timeser
       );
     };
   }
+
+  /**
+   * Returns a set of values that corresponds to an empty-set aggregation, 
which is used in situations
+   * where we want to return a single result representing "nothing was 
aggregated". The returned array has
+   * one element per {@link AggregatorFactory}.
+   */
+  public static Object[] getEmptyAggregations(List<AggregatorFactory> 
aggregatorSpecs)
+  {
+    final Aggregator[] aggregators = new Aggregator[aggregatorSpecs.size()];
+    final RowSignature aggregatorsSignature =
+        RowSignature.builder().addAggregators(aggregatorSpecs, 
RowSignature.Finalization.UNKNOWN).build();
+    for (int i = 0; i < aggregatorSpecs.size(); i++) {
+      aggregators[i] =
+          aggregatorSpecs.get(i)
+                         .factorize(
+                             RowBasedColumnSelectorFactory.create(
+                                 RowAdapters.standardRow(),
+                                 () -> new MapBasedRow(null, null),
+                                 aggregatorsSignature,
+                                 false,
+                                 false
+                             )
+                         );
+    }
+
+    final Object[] retVal = new Object[aggregatorSpecs.size()];
+    for (int i = 0; i < aggregatorSpecs.size(); i++) {
+      retVal[i] = aggregators[i].get();
+      aggregators[i].close();
+    }
+    return retVal;
+  }
 }
diff --git 
a/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChestTest.java
 
b/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChestTest.java
index f5e46f04c00..1ae5964d17e 100644
--- 
a/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChestTest.java
+++ 
b/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryQueryToolChestTest.java
@@ -44,15 +44,18 @@ import org.apache.druid.segment.TestHelper;
 import org.apache.druid.segment.VirtualColumns;
 import org.apache.druid.segment.column.ColumnType;
 import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.testing.InitializedNullHandlingTest;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 @RunWith(Parameterized.class)
-public class TimeseriesQueryQueryToolChestTest
+public class TimeseriesQueryQueryToolChestTest extends 
InitializedNullHandlingTest
 {
   private static final String TIMESTAMP_RESULT_FIELD_NAME = "d0";
   private static final TimeseriesQueryQueryToolChest TOOL_CHEST = new 
TimeseriesQueryQueryToolChest(null);
@@ -449,4 +452,27 @@ public class TimeseriesQueryQueryToolChestTest
         )
     );
   }
+
+  @Test
+  public void testGetEmptyTimeseriesResultValue()
+  {
+    final TimeseriesQuery query =
+        Druids.newTimeseriesQueryBuilder()
+              .intervals("2000/P1D")
+              .dataSource("foo")
+              .aggregators(new CountAggregatorFactory("a0"), new 
LongSumAggregatorFactory("a1", "nofield"))
+              .build();
+
+    final Map<String, Object> resultMap = new HashMap<>();
+    resultMap.put("a0", 0L);
+    resultMap.put("a1", null);
+
+    Assert.assertEquals(
+        new Result<>(
+            DateTimes.of("2000"),
+            new TimeseriesResultValue(resultMap)
+        ),
+        TOOL_CHEST.getEmptyTimeseriesResultValue(query)
+    );
+  }
 }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/rel/DruidQuery.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/DruidQuery.java
index 72f72cd6d93..0330b9db909 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/rel/DruidQuery.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rel/DruidQuery.java
@@ -1428,7 +1428,7 @@ public class DruidQuery
         if (queryGranularity != null) {
           // group by more than one timestamp_floor
           // eg: group by timestamp_floor(__time to 
DAY),timestamp_floor(__time, to HOUR)
-          queryGranularity = null;
+          theContext.clear();
           break;
         }
         queryGranularity = granularity;
@@ -1449,7 +1449,13 @@ public class DruidQuery
         }
       }
     }
-    if (queryGranularity == null) {
+
+    if (grouping.getDimensions().isEmpty() && 
grouping.hasGroupingDimensionsDropped()) {
+      // GROUP BY ().
+      theContext.put(GroupByQuery.CTX_HAS_DROPPED_DIMENSIONS, 
grouping.hasGroupingDimensionsDropped());
+    }
+
+    if (theContext.isEmpty()) {
       return query;
     }
     return query.withOverriddenContext(theContext);
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 125acc578f3..90a64d5fc70 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -3857,7 +3857,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testGroupingWithNullPlusNonNullInFilter()
   {
-    msqIncompatible();
     testQuery(
         "SELECT COUNT(*) FROM foo WHERE dim1 IN (NULL, 'abc')",
         ImmutableList.of(
@@ -3877,7 +3876,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testGroupingWithNotNullPlusNonNullInFilter()
   {
-    msqIncompatible();
     testQuery(
         "SELECT COUNT(*) FROM foo WHERE dim1 NOT IN (NULL, 'abc')",
         ImmutableList.of(
@@ -3902,7 +3900,7 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testGroupByNothingWithLiterallyFalseFilter()
   {
-    msqIncompatible();
+    // Result of MAX(cnt) when nothing matches the filter.
     testQuery(
         "SELECT COUNT(*), MAX(cnt) FROM druid.foo WHERE 1 = 0",
         ImmutableList.of(
@@ -3928,7 +3926,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testGroupByNothingWithImpossibleTimeFilter()
   {
-    msqIncompatible();
     // Regression test for https://github.com/apache/druid/issues/7671
 
     testQuery(
@@ -4331,7 +4328,6 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testCountStarWithLongColumnFiltersOnFloatLiterals()
   {
-    msqIncompatible();
     testQuery(
         "SELECT COUNT(*) FROM druid.foo WHERE cnt > 1.1 and cnt < 100000001.0",
         ImmutableList.of(
@@ -9762,8 +9758,7 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
   @Test
   public void testTimeseriesEmptyResultsAggregatorDefaultValues()
   {
-    msqIncompatible();
-    // timeseries with all granularity have a single group, so should return 
default results for given aggregators
+    // timeseries with all granularity has a single group, so should return 
default results for given aggregators.
     testQuery(
         "SELECT\n"
         + " count(*),\n"
@@ -9848,13 +9843,149 @@ public class CalciteQueryTest extends 
BaseCalciteQueryTest
   }
 
   @Test
-  public void testTimeseriesEmptyResultsAggregatorDefaultValuesNonVectorized()
+  public void testEmptyResultsAggregatorWithHavingTrue()
   {
-    // Empty-dataset aggregation queries in MSQ return an empty row, rather 
than a single row as SQL requires.
-    msqIncompatible();
+    // GROUP BY () that matches nothing should return an empty result row with 
default aggregator values.
+    // Adding a HAVING retains the row, if the HAVING matches the default 
aggregators.
+    testQuery(
+        "SELECT\n"
+        + " COUNT(*)\n"
+        + "FROM druid.numfoo\n"
+        + "WHERE __time >= TIMESTAMP '4000-01-01 00:00:00' AND __time < 
TIMESTAMP '4001-01-01 00:00:00'\n"
+        + "HAVING COUNT(*) = 0",
+        ImmutableList.of(
+            GroupByQuery.builder()
+                        .setDataSource(CalciteTests.DATASOURCE3)
+                        
.setInterval(querySegmentSpec(Intervals.of("4000/P1Y")))
+                        .setGranularity(Granularities.ALL)
+                        .setAggregatorSpecs(new CountAggregatorFactory("a0"))
+                        .setHavingSpec(having(equality("a0", 0L, 
ColumnType.LONG)))
+                        .setContext(QUERY_CONTEXT_DEFAULT)
+                        .build()
+        ),
+        ImmutableList.of(
+            new Object[]{0L}
+        )
+    );
+  }
 
+  @Test
+  public void testEmptyResultsAggregatorWithHavingFalse()
+  {
+    // GROUP BY () that matches nothing should return an empty result row with 
default aggregator values.
+    // Adding a HAVING omits the row, if the HAVING does not match the default 
aggregators.
+    testQuery(
+        "SELECT\n"
+        + " COUNT(*)\n"
+        + "FROM druid.numfoo\n"
+        + "WHERE __time >= TIMESTAMP '4000-01-01 00:00:00' AND __time < 
TIMESTAMP '4001-01-01 00:00:00'\n"
+        + "HAVING COUNT(*) = 1",
+        ImmutableList.of(
+            GroupByQuery.builder()
+                        .setDataSource(CalciteTests.DATASOURCE3)
+                        
.setInterval(querySegmentSpec(Intervals.of("4000/P1Y")))
+                        .setGranularity(Granularities.ALL)
+                        .setAggregatorSpecs(new CountAggregatorFactory("a0"))
+                        .setHavingSpec(having(equality("a0", 1L, 
ColumnType.LONG)))
+                        .setContext(QUERY_CONTEXT_DEFAULT)
+                        .build()
+        ),
+        ImmutableList.of()
+    );
+  }
+
+  @Test
+  public void 
testTimeseriesEmptyResultsAggregatorDefaultValuesTimeFilterMatchesNothing()
+  {
+    // timeseries with all granularity has a single group, so should return 
default results for given aggregators.
+    testQuery(
+        "SELECT\n"
+        + " count(*),\n"
+        + " COUNT(DISTINCT dim1),\n"
+        + " APPROX_COUNT_DISTINCT(distinct dim1),\n"
+        + " sum(dbl1),\n"
+        + " max(dbl1),\n"
+        + " min(dbl1),\n"
+        + " sum(l1),\n"
+        + " max(l1),\n"
+        + " min(l1),\n"
+        + " avg(l1),\n"
+        + " avg(dbl1)\n"
+        + "FROM druid.numfoo\n"
+        + "WHERE __time >= TIMESTAMP '4000-01-01 00:00:00' AND __time < 
TIMESTAMP '4001-01-01 00:00:00'",
+        ImmutableList.of(
+            Druids.newTimeseriesQueryBuilder()
+                  .dataSource(CalciteTests.DATASOURCE3)
+                  .intervals(querySegmentSpec(Intervals.of("4000/P1Y")))
+                  .granularity(Granularities.ALL)
+                  .aggregators(
+                      aggregators(
+                          new CountAggregatorFactory("a0"),
+                          new CardinalityAggregatorFactory(
+                              "a1",
+                              null,
+                              
ImmutableList.of(DefaultDimensionSpec.of("dim1")),
+                              false,
+                              true
+                          ),
+                          new CardinalityAggregatorFactory(
+                              "a2",
+                              null,
+                              
ImmutableList.of(DefaultDimensionSpec.of("dim1")),
+                              false,
+                              true
+                          ),
+                          new DoubleSumAggregatorFactory("a3", "dbl1"),
+                          new DoubleMaxAggregatorFactory("a4", "dbl1"),
+                          new DoubleMinAggregatorFactory("a5", "dbl1"),
+                          new LongSumAggregatorFactory("a6", "l1"),
+                          new LongMaxAggregatorFactory("a7", "l1"),
+                          new LongMinAggregatorFactory("a8", "l1"),
+                          new DoubleSumAggregatorFactory("a9:sum", "l1"),
+                          new FilteredAggregatorFactory(
+                              new CountAggregatorFactory("a9:count"),
+                              notNull("l1")
+                          ),
+                          new DoubleSumAggregatorFactory("a10:sum", "dbl1"),
+                          new FilteredAggregatorFactory(
+                              new CountAggregatorFactory("a10:count"),
+                              notNull("dbl1")
+                          )
+                      )
+                  )
+                  .postAggregators(
+                      new ArithmeticPostAggregator(
+                          "a9",
+                          "quotient",
+                          ImmutableList.of(
+                              new FieldAccessPostAggregator(null, "a9:sum"),
+                              new FieldAccessPostAggregator(null, "a9:count")
+                          )
+                      ),
+                      new ArithmeticPostAggregator(
+                          "a10",
+                          "quotient",
+                          ImmutableList.of(
+                              new FieldAccessPostAggregator(null, "a10:sum"),
+                              new FieldAccessPostAggregator(null, "a10:count")
+                          )
+                      )
+                  )
+                  .context(QUERY_CONTEXT_DEFAULT)
+                  .build()
+        ),
+        ImmutableList.of(new Object[]{0L, 0L, 0L, null, null, null, null, 
null, null, null, null})
+    );
+  }
+
+  @Test
+  public void testTimeseriesEmptyResultsAggregatorDefaultValuesNonVectorized()
+  {
+    // This test is like testTimeseriesEmptyResultsAggregatorDefaultValues, 
but includes some non-vectorizable
+    // aggregators.
     cannotVectorize();
-    // timeseries with all granularity have a single group, so should return 
default results for given aggregators
+
+    // timeseries with all granularity has a single group, so should return 
default results for given aggregators.
     testQuery(
         "SELECT\n"
         + " ANY_VALUE(dim1, 1024),\n"


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

Reply via email to