This is an automated email from the ASF dual-hosted git repository.
yqm 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 9f15763694f Handle 1. empty data in CountAggregator; 2. query's
required physical column is not in grouping, filtering, ordering, or
aggregating (#18307)
9f15763694f is described below
commit 9f15763694fb0ccede2a6f5d84975dac2103d9d1
Author: Cece Mei <[email protected]>
AuthorDate: Wed Jul 23 20:36:58 2025 -0700
Handle 1. empty data in CountAggregator; 2. query's required physical
column is not in grouping, filtering, ordering, or aggregating (#18307)
* add test for null values
* handle default value in NullableNumericAggregatorFactory
* revert style change
* revert style change, and add no data test in group by query
* test change
* use forceNullable rather than defaultValue
* minor change
* some change in test
* AggregateCombiner should care about isNullable
* isNullable should not be user facing in LongSumAggregatorFactory
* unit test failure
* review comments
* fix the unnest test failure
* style fix
* style fix
* javadoc
---
.../query/aggregation/CountAggregatorFactory.java | 7 +-
.../aggregation/LongSumAggregatorFactory.java | 34 ++++++-
.../NullableNumericAggregatorFactory.java | 31 ++++++-
.../druid/segment/AggregateProjectionMetadata.java | 64 ++++++++++---
.../druid/segment/projections/Projections.java | 20 ++++
.../query/groupby/GroupByQueryRunnerTest.java | 72 ++++++++++++---
.../groupby/GroupByTimeseriesQueryRunnerTest.java | 21 +++++
.../query/metadata/SegmentMetadataQueryTest.java | 18 ++--
.../timeseries/TimeseriesQueryRunnerTest.java | 102 +++++++++++++++++++++
.../java/org/apache/druid/segment/TestIndex.java | 23 +++--
10 files changed, 340 insertions(+), 52 deletions(-)
diff --git
a/processing/src/main/java/org/apache/druid/query/aggregation/CountAggregatorFactory.java
b/processing/src/main/java/org/apache/druid/query/aggregation/CountAggregatorFactory.java
index 7089789f90a..e0fa4c643a4 100644
---
a/processing/src/main/java/org/apache/druid/query/aggregation/CountAggregatorFactory.java
+++
b/processing/src/main/java/org/apache/druid/query/aggregation/CountAggregatorFactory.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.segment.ColumnInspector;
import org.apache.druid.segment.ColumnSelectorFactory;
import org.apache.druid.segment.column.ColumnType;
@@ -39,9 +40,7 @@ public class CountAggregatorFactory extends AggregatorFactory
private final String name;
@JsonCreator
- public CountAggregatorFactory(
- @JsonProperty("name") String name
- )
+ public CountAggregatorFactory(@JsonProperty("name") String name)
{
Preconditions.checkNotNull(name, "Must have a valid, non-null aggregator
name");
@@ -93,7 +92,7 @@ public class CountAggregatorFactory extends AggregatorFactory
@Override
public AggregatorFactory getCombiningFactory()
{
- return new LongSumAggregatorFactory(name, name);
+ return new LongSumAggregatorFactory(name, name, null,
ExprMacroTable.nil(), true);
}
@Override
diff --git
a/processing/src/main/java/org/apache/druid/query/aggregation/LongSumAggregatorFactory.java
b/processing/src/main/java/org/apache/druid/query/aggregation/LongSumAggregatorFactory.java
index 9d1c5fcdd84..8e70fac2536 100644
---
a/processing/src/main/java/org/apache/druid/query/aggregation/LongSumAggregatorFactory.java
+++
b/processing/src/main/java/org/apache/druid/query/aggregation/LongSumAggregatorFactory.java
@@ -31,11 +31,23 @@ import org.apache.druid.segment.vector.VectorValueSelector;
import javax.annotation.Nullable;
/**
+ * A null-aware aggregator factory. Note that the nullness is handled by
{@link NullableNumericAggregatorFactory}, the
+ * {@link LongSumAggregator}, {@link LongSumBufferAggregator}, and {@link
LongSumVectorAggregator} only aggregates
+ * non-null values, and returns 0 if no data has been aggregated.
+ * <p>
+ * If forceNotNullable is set to true, the aggregator factory will not allow
null values.
*/
public class LongSumAggregatorFactory extends SimpleLongAggregatorFactory
{
private final Supplier<byte[]> cacheKey;
+ private final boolean forceNotNullable;
+
+ public LongSumAggregatorFactory(String name, String fieldName)
+ {
+ this(name, fieldName, null, ExprMacroTable.nil(), false);
+ }
+
@JsonCreator
public LongSumAggregatorFactory(
@JsonProperty("name") String name,
@@ -43,6 +55,17 @@ public class LongSumAggregatorFactory extends
SimpleLongAggregatorFactory
@JsonProperty("expression") @Nullable String expression,
@JacksonInject ExprMacroTable macroTable
)
+ {
+ this(name, fieldName, expression, macroTable, false);
+ }
+
+ public LongSumAggregatorFactory(
+ String name,
+ String fieldName,
+ @Nullable String expression,
+ ExprMacroTable macroTable,
+ boolean forceNotNullable
+ )
{
super(macroTable, name, fieldName, expression);
this.cacheKey = AggregatorUtil.getSimpleAggregatorCacheKeySupplier(
@@ -50,11 +73,13 @@ public class LongSumAggregatorFactory extends
SimpleLongAggregatorFactory
fieldName,
fieldExpression
);
+ this.forceNotNullable = forceNotNullable;
}
- public LongSumAggregatorFactory(String name, String fieldName)
+ @Override
+ public boolean forceNotNullable()
{
- this(name, fieldName, null, ExprMacroTable.nil());
+ return forceNotNullable;
}
@Override
@@ -106,13 +131,13 @@ public class LongSumAggregatorFactory extends
SimpleLongAggregatorFactory
@Override
public AggregatorFactory withName(String newName)
{
- return new LongSumAggregatorFactory(newName, getFieldName(),
getExpression(), macroTable);
+ return new LongSumAggregatorFactory(newName, getFieldName(),
getExpression(), macroTable, forceNotNullable);
}
@Override
public AggregatorFactory getCombiningFactory()
{
- return new LongSumAggregatorFactory(name, name, null, macroTable);
+ return new LongSumAggregatorFactory(name, name, null, macroTable,
forceNotNullable);
}
@Override
@@ -128,6 +153,7 @@ public class LongSumAggregatorFactory extends
SimpleLongAggregatorFactory
"fieldName='" + fieldName + '\'' +
", expression='" + expression + '\'' +
", name='" + name + '\'' +
+ ", forceNotNullable='" + forceNotNullable + '\'' +
'}';
}
}
diff --git
a/processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericAggregatorFactory.java
b/processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericAggregatorFactory.java
index 3dfc2919e44..49dadfd3fea 100644
---
a/processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericAggregatorFactory.java
+++
b/processing/src/main/java/org/apache/druid/query/aggregation/NullableNumericAggregatorFactory.java
@@ -48,22 +48,36 @@ import org.apache.druid.segment.vector.VectorValueSelector;
public abstract class NullableNumericAggregatorFactory<T extends
BaseNullableColumnValueSelector>
extends AggregatorFactory
{
+ /**
+ * If true, this aggregator will not check for null inputs, instead directly
delegating to the underlying aggregator.
+ * Currently, this is only used by {@link
CountAggregatorFactory#getCombiningFactory}.
+ * This isn't entirely safe, as some selectors might throw an exception when
attempting to retrieve a primitive value from `null`.
+ */
+ public boolean forceNotNullable()
+ {
+ return false;
+ }
+
@Override
public final Aggregator factorize(ColumnSelectorFactory
columnSelectorFactory)
{
T selector = selector(columnSelectorFactory);
- BaseNullableColumnValueSelector nullSelector = makeNullSelector(selector,
columnSelectorFactory);
Aggregator aggregator = factorize(columnSelectorFactory, selector);
- return new NullableNumericAggregator(aggregator, nullSelector);
+ if (this.forceNotNullable()) {
+ return aggregator;
+ }
+ return new NullableNumericAggregator(aggregator,
makeNullSelector(selector, columnSelectorFactory));
}
@Override
public final BufferAggregator factorizeBuffered(ColumnSelectorFactory
columnSelectorFactory)
{
T selector = selector(columnSelectorFactory);
- BaseNullableColumnValueSelector nullSelector = makeNullSelector(selector,
columnSelectorFactory);
BufferAggregator aggregator = factorizeBuffered(columnSelectorFactory,
selector);
- return new NullableNumericBufferAggregator(aggregator, nullSelector);
+ if (this.forceNotNullable()) {
+ return aggregator;
+ }
+ return new NullableNumericBufferAggregator(aggregator,
makeNullSelector(selector, columnSelectorFactory));
}
@Override
@@ -72,6 +86,9 @@ public abstract class NullableNumericAggregatorFactory<T
extends BaseNullableCol
Preconditions.checkState(canVectorize(columnSelectorFactory), "Cannot
vectorize");
VectorValueSelector selector = vectorSelector(columnSelectorFactory);
VectorAggregator aggregator = factorizeVector(columnSelectorFactory,
selector);
+ if (this.forceNotNullable()) {
+ return aggregator;
+ }
return new NullableNumericVectorAggregator(aggregator, selector);
}
@@ -79,12 +96,18 @@ public abstract class NullableNumericAggregatorFactory<T
extends BaseNullableCol
public final AggregateCombiner makeNullableAggregateCombiner()
{
AggregateCombiner<?> combiner = makeAggregateCombiner();
+ if (this.forceNotNullable()) {
+ return combiner;
+ }
return new NullableNumericAggregateCombiner<>(combiner);
}
@Override
public final int getMaxIntermediateSizeWithNulls()
{
+ if (this.forceNotNullable()) {
+ return getMaxIntermediateSize();
+ }
return getMaxIntermediateSize() + Byte.BYTES;
}
diff --git
a/processing/src/main/java/org/apache/druid/segment/AggregateProjectionMetadata.java
b/processing/src/main/java/org/apache/druid/segment/AggregateProjectionMetadata.java
index c561757268a..83b762b629d 100644
---
a/processing/src/main/java/org/apache/druid/segment/AggregateProjectionMetadata.java
+++
b/processing/src/main/java/org/apache/druid/segment/AggregateProjectionMetadata.java
@@ -44,6 +44,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* Aggregate projection schema and row count information to store in {@link
Metadata} which itself is stored inside a
@@ -349,7 +350,8 @@ public class AggregateProjectionMetadata
if (combining != null) {
matchBuilder.remapColumn(queryAgg.getName(),
projectionAgg.getName())
.addReferencedPhysicalColumn(projectionAgg.getName())
- .addPreAggregatedAggregator(combining);
+ .addPreAggregatedAggregator(combining)
+ .addMatchedQueryColumns(queryAgg.requiredFields());
foundMatch = true;
break;
}
@@ -360,6 +362,40 @@ public class AggregateProjectionMetadata
return null;
}
}
+ // validate physical and virtual columns have all been accounted for
+ final Set<String> matchedQueryColumns =
matchBuilder.getMatchedQueryColumns();
+ if (queryCursorBuildSpec.getPhysicalColumns() != null) {
+ for (String queryColumn : queryCursorBuildSpec.getPhysicalColumns()) {
+ // time is special handled
+ if (ColumnHolder.TIME_COLUMN_NAME.equals(queryColumn)) {
+ continue;
+ }
+ if (!matchedQueryColumns.contains(queryColumn)) {
+ matchBuilder = matchRequiredColumn(
+ matchBuilder,
+ queryColumn,
+ queryCursorBuildSpec.getVirtualColumns(),
+ physicalColumnChecker
+ );
+ if (matchBuilder == null) {
+ return null;
+ }
+ }
+ }
+ for (VirtualColumn vc :
queryCursorBuildSpec.getVirtualColumns().getVirtualColumns()) {
+ if (!matchedQueryColumns.contains(vc.getOutputName())) {
+ matchBuilder = matchRequiredColumn(
+ matchBuilder,
+ vc.getOutputName(),
+ queryCursorBuildSpec.getVirtualColumns(),
+ physicalColumnChecker
+ );
+ if (matchBuilder == null) {
+ return null;
+ }
+ }
+ }
+ }
return matchBuilder.build(queryCursorBuildSpec);
}
@@ -395,10 +431,12 @@ public class AggregateProjectionMetadata
Projections.PhysicalColumnChecker physicalColumnChecker
)
{
- final VirtualColumn buildSpecVirtualColumn =
queryVirtualColumns.getVirtualColumn(column);
- if (buildSpecVirtualColumn != null) {
+ final VirtualColumn queryVirtualColumn =
queryVirtualColumns.getVirtualColumn(column);
+ if (queryVirtualColumn != null) {
+ matchBuilder.addMatchedQueryColumn(column)
+
.addMatchedQueryColumns(queryVirtualColumn.requiredColumns());
// check to see if we have an equivalent virtual column defined in the
projection, if so we can
- final VirtualColumn projectionEquivalent =
virtualColumns.findEquivalent(buildSpecVirtualColumn);
+ final VirtualColumn projectionEquivalent =
virtualColumns.findEquivalent(queryVirtualColumn);
if (projectionEquivalent != null) {
final String remapColumnName;
if (Objects.equals(projectionEquivalent.getOutputName(),
timeColumnName)) {
@@ -406,21 +444,18 @@ public class AggregateProjectionMetadata
} else {
remapColumnName = projectionEquivalent.getOutputName();
}
- if (!buildSpecVirtualColumn.getOutputName().equals(remapColumnName))
{
- matchBuilder.remapColumn(
- buildSpecVirtualColumn.getOutputName(),
- remapColumnName
- );
+ if (!queryVirtualColumn.getOutputName().equals(remapColumnName)) {
+ matchBuilder.remapColumn(queryVirtualColumn.getOutputName(),
remapColumnName);
}
return matchBuilder.addReferencedPhysicalColumn(remapColumnName);
}
- matchBuilder.addReferenceedVirtualColumn(buildSpecVirtualColumn);
- final List<String> requiredInputs =
buildSpecVirtualColumn.requiredColumns();
+ matchBuilder.addReferenceedVirtualColumn(queryVirtualColumn);
+ final List<String> requiredInputs =
queryVirtualColumn.requiredColumns();
if (requiredInputs.size() == 1 &&
ColumnHolder.TIME_COLUMN_NAME.equals(requiredInputs.get(0))) {
// special handle time granularity. in the future this should be
reworked to push this concept into the
// virtual column and underlying expression itself, but this will do
for now
- final Granularity virtualGranularity =
Granularities.fromVirtualColumn(buildSpecVirtualColumn);
+ final Granularity virtualGranularity =
Granularities.fromVirtualColumn(queryVirtualColumn);
if (virtualGranularity != null) {
if (virtualGranularity.isFinerThan(granularity)) {
return null;
@@ -434,7 +469,7 @@ public class AggregateProjectionMetadata
} else {
// anything else with __time requires none granularity
if (Granularities.NONE.equals(granularity)) {
- return matchBuilder;
+ return
matchBuilder.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
}
return null;
}
@@ -454,7 +489,8 @@ public class AggregateProjectionMetadata
}
} else {
if (physicalColumnChecker.check(name, column)) {
- return matchBuilder.addReferencedPhysicalColumn(column);
+ return matchBuilder.addMatchedQueryColumn(column)
+ .addReferencedPhysicalColumn(column);
}
return null;
}
diff --git
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
index 2a633aadf9d..db067bde85b 100644
---
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
+++
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
@@ -46,6 +46,7 @@ import org.apache.druid.segment.vector.VectorValueSelector;
import javax.annotation.Nullable;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -181,6 +182,7 @@ public class Projections
private final Set<VirtualColumn> referencedVirtualColumns;
private final Map<String, String> remapColumns;
private final List<AggregatorFactory> combiningFactories;
+ private final Set<String> matchedQueryColumns;
public ProjectionMatchBuilder()
{
@@ -188,6 +190,7 @@ public class Projections
this.referencedVirtualColumns = new HashSet<>();
this.remapColumns = new HashMap<>();
this.combiningFactories = new ArrayList<>();
+ this.matchedQueryColumns = new HashSet<>();
}
/**
@@ -235,6 +238,23 @@ public class Projections
return this;
}
+ public ProjectionMatchBuilder addMatchedQueryColumn(String queryColumn)
+ {
+ matchedQueryColumns.add(queryColumn);
+ return this;
+ }
+
+ public ProjectionMatchBuilder addMatchedQueryColumns(Collection<String>
queryColumns)
+ {
+ matchedQueryColumns.addAll(queryColumns);
+ return this;
+ }
+
+ public Set<String> getMatchedQueryColumns()
+ {
+ return matchedQueryColumns;
+ }
+
public ProjectionMatch build(CursorBuildSpec queryCursorBuildSpec)
{
return new ProjectionMatch(
diff --git
a/processing/src/test/java/org/apache/druid/query/groupby/GroupByQueryRunnerTest.java
b/processing/src/test/java/org/apache/druid/query/groupby/GroupByQueryRunnerTest.java
index 3477d3c7db7..f4e4d1dbd24 100644
---
a/processing/src/test/java/org/apache/druid/query/groupby/GroupByQueryRunnerTest.java
+++
b/processing/src/test/java/org/apache/druid/query/groupby/GroupByQueryRunnerTest.java
@@ -3399,26 +3399,70 @@ public class GroupByQueryRunnerTest extends
InitializedNullHandlingTest
@Test
public void testGroupByWithCardinality()
{
- GroupByQuery query = makeQueryBuilder()
+
+ GroupByQuery.Builder queryBuilder = makeQueryBuilder()
.setDataSource(QueryRunnerTestHelper.DATA_SOURCE)
.setQuerySegmentSpec(QueryRunnerTestHelper.FIRST_TO_THIRD)
.setAggregatorSpecs(QueryRunnerTestHelper.ROWS_COUNT,
QueryRunnerTestHelper.QUALITY_CARDINALITY)
- .setGranularity(QueryRunnerTestHelper.ALL_GRAN)
- .build();
+ .setGranularity(QueryRunnerTestHelper.ALL_GRAN);
+ GroupByQuery queryNoProjection =
queryBuilder.setContext(Map.of(QueryContexts.NO_PROJECTIONS, "true")).build();
+ GroupByQuery queryWithProjection = queryBuilder.setContext(Map.of(
+ QueryContexts.USE_PROJECTION,
+ "daily_countAndQualityCardinalityAndMaxLongNullable"
+ )).build();
+ // act
+ Iterable<ResultRow> resultsNoProjection =
GroupByQueryRunnerTestHelper.runQuery(factory, runner, queryNoProjection);
+ Iterable<ResultRow> resultsWithProjection =
GroupByQueryRunnerTestHelper.runQuery(
+ factory,
+ runner,
+ queryWithProjection
+ );
- List<ResultRow> expectedResults = Collections.singletonList(
- makeRow(
- query,
- "2011-04-01",
- "rows",
- 26L,
- "cardinality",
- QueryRunnerTestHelper.UNIQUES_9
- )
+ List<ResultRow> expectedResults = Collections.singletonList(makeRow(
+ queryBuilder.build(),
+ "2011-04-01",
+ "rows",
+ 26L,
+ "cardinality",
+ QueryRunnerTestHelper.UNIQUES_9
+ ));
+
+ TestHelper.assertExpectedObjects(expectedResults, resultsNoProjection,
"cardinality");
+ TestHelper.assertExpectedObjects(expectedResults, resultsWithProjection,
"cardinality");
+ }
+
+ @Test
+ public void testGroupByWithCardinalityNoData()
+ {
+
+ GroupByQuery.Builder queryBuilder = makeQueryBuilder()
+ .setDataSource(QueryRunnerTestHelper.DATA_SOURCE)
+ .setInterval("2011-01-21T00:00:00.000Z/P1D")
+ .setAggregatorSpecs(QueryRunnerTestHelper.ROWS_COUNT,
QueryRunnerTestHelper.QUALITY_CARDINALITY)
+ .setGranularity(QueryRunnerTestHelper.ALL_GRAN);
+ GroupByQuery queryNoProjection =
queryBuilder.setContext(Map.of(QueryContexts.NO_PROJECTIONS, "true")).build();
+ GroupByQuery queryWithProjection = queryBuilder.setContext(Map.of(
+ QueryContexts.USE_PROJECTION,
+ "daily_countAndQualityCardinalityAndMaxLongNullable"
+ )).build();
+
+ Iterable<ResultRow> resultsNoProjection =
GroupByQueryRunnerTestHelper.runQuery(factory, runner, queryNoProjection);
+ Iterable<ResultRow> resultsWithProjection =
GroupByQueryRunnerTestHelper.runQuery(
+ factory,
+ runner,
+ queryWithProjection
);
- Iterable<ResultRow> results =
GroupByQueryRunnerTestHelper.runQuery(factory, runner, query);
- TestHelper.assertExpectedObjects(expectedResults, results, "cardinality");
+ List<ResultRow> expectedResults = Collections.singletonList(makeRow(
+ queryBuilder.build(),
+ "2011-04-01",
+ "rows",
+ 0L,
+ "cardinality",
+ 0.0d
+ ));
+ TestHelper.assertExpectedObjects(expectedResults, resultsNoProjection, "no
data");
+ TestHelper.assertExpectedObjects(expectedResults, resultsWithProjection,
"no data");
}
@Test
diff --git
a/processing/src/test/java/org/apache/druid/query/groupby/GroupByTimeseriesQueryRunnerTest.java
b/processing/src/test/java/org/apache/druid/query/groupby/GroupByTimeseriesQueryRunnerTest.java
index b3d140d0479..6b2a9cb5120 100644
---
a/processing/src/test/java/org/apache/druid/query/groupby/GroupByTimeseriesQueryRunnerTest.java
+++
b/processing/src/test/java/org/apache/druid/query/groupby/GroupByTimeseriesQueryRunnerTest.java
@@ -280,6 +280,27 @@ public class GroupByTimeseriesQueryRunnerTest extends
TimeseriesQueryRunnerTest
Assert.assertEquals(1870.061F,
result.getValue().getFloatMetric(QueryRunnerTestHelper.FLOAT_MAX_INDEX_METRIC),
0);
}
+ @Override
+ public void testTimeseriesNullableLongMax()
+ {
+ // Skip this test because the timeseries test expects a null value to be
returned for the aggregator even if
+ // there's no data for a period in query, but group by doesn't return data
for periods that have no data.
+ }
+
+ @Override
+ public void testTimeseriesProjections()
+ {
+ // Skip this test because the timeseries test expects a null value to be
returned for the aggregator even if
+ // there's no data for a period in query, but group by doesn't return data
for periods that have no data.
+ }
+
+ @Override
+ public void testTimeseriesProjectionsCounts()
+ {
+ // Skip this test because the timeseries test expects a 0 value to be
returned for the count aggregator even if
+ // there's no data for a period in query, but group by doesn't return data
for periods that have no data.
+ }
+
@Override
public void testEmptyTimeseries()
{
diff --git
a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java
b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java
index 5fd5ea98d57..415b2db9572 100644
---
a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java
+++
b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java
@@ -103,8 +103,10 @@ public class SegmentMetadataQueryTest extends
InitializedNullHandlingTest
);
private static final ObjectMapper MAPPER = new DefaultObjectMapper();
private static final String DATASOURCE = "testDatasource";
- private static final AggregateProjectionMetadata.Schema PROJECTION_SCHEMA =
TestIndex.PROJECTIONS.get(0).toMetadataSchema();
- private static final int PROJECTION_ROWS = 279;
+ private static final AggregateProjectionMetadata.Schema PROJECTION1_SCHEMA =
TestIndex.PROJECTIONS.get(0).toMetadataSchema();
+ private static final AggregateProjectionMetadata.Schema PROJECTION2_SCHEMA =
TestIndex.PROJECTIONS.get(1).toMetadataSchema();
+ private static final int PROJECTION1_ROWS = 279;
+ private static final int PROJECTION2_ROWS = 93;
@SuppressWarnings("unchecked")
public static QueryRunner makeMMappedQueryRunner(
@@ -226,8 +228,10 @@ public class SegmentMetadataQueryTest extends
InitializedNullHandlingTest
expectedAggregators.put(agg.getName(), agg.getCombiningFactory());
}
final Map<String, AggregateProjectionMetadata> expectedProjections =
ImmutableMap.of(
- PROJECTION_SCHEMA.getName(),
- new AggregateProjectionMetadata(PROJECTION_SCHEMA, PROJECTION_ROWS)
+ PROJECTION1_SCHEMA.getName(),
+ new AggregateProjectionMetadata(PROJECTION1_SCHEMA, PROJECTION1_ROWS),
+ PROJECTION2_SCHEMA.getName(),
+ new AggregateProjectionMetadata(PROJECTION2_SCHEMA, PROJECTION2_ROWS)
);
expectedSegmentAnalysis1 = new SegmentAnalysis(
@@ -705,8 +709,10 @@ public class SegmentMetadataQueryTest extends
InitializedNullHandlingTest
expectedSegmentAnalysis1.getNumRows() +
expectedSegmentAnalysis2.getNumRows(),
expectedAggregators,
ImmutableMap.of(
- PROJECTION_SCHEMA.getName(),
- new AggregateProjectionMetadata(PROJECTION_SCHEMA, PROJECTION_ROWS
* 2)
+ PROJECTION1_SCHEMA.getName(),
+ new AggregateProjectionMetadata(PROJECTION1_SCHEMA,
PROJECTION1_ROWS * 2),
+ PROJECTION2_SCHEMA.getName(),
+ new AggregateProjectionMetadata(PROJECTION2_SCHEMA,
PROJECTION2_ROWS * 2)
),
null,
null,
diff --git
a/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryRunnerTest.java
b/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryRunnerTest.java
index d34f3fb84ce..16254aac72b 100644
---
a/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryRunnerTest.java
+++
b/processing/src/test/java/org/apache/druid/query/timeseries/TimeseriesQueryRunnerTest.java
@@ -52,6 +52,7 @@ import
org.apache.druid.query.aggregation.DoubleSumAggregatorFactory;
import org.apache.druid.query.aggregation.ExpressionLambdaAggregatorFactory;
import org.apache.druid.query.aggregation.FilteredAggregatorFactory;
import org.apache.druid.query.aggregation.FloatSumAggregatorFactory;
+import org.apache.druid.query.aggregation.LongMaxAggregatorFactory;
import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
import
org.apache.druid.query.aggregation.cardinality.CardinalityAggregatorFactory;
import
org.apache.druid.query.aggregation.firstlast.first.DoubleFirstAggregatorFactory;
@@ -87,6 +88,7 @@ import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -331,6 +333,99 @@ public class TimeseriesQueryRunnerTest extends
InitializedNullHandlingTest
Assert.assertEquals(lastResult.toString(), expectedLast,
lastResult.getTimestamp());
}
+ @Test
+ public void testTimeseriesProjections()
+ {
+
+ AggregatorFactory maxQuality = new LongMaxAggregatorFactory("maxQuality",
"qualityLong");
+ Druids.TimeseriesQueryBuilder queryBuilder =
Druids.newTimeseriesQueryBuilder()
+
.dataSource(QueryRunnerTestHelper.DATA_SOURCE)
+
.granularity(Granularities.DAY)
+
.intervals("2011-01-20/2011-01-22")
+
.aggregators(List.of(maxQuality));
+ // construct a query to ignore projection
+ Map<String, Object> contextNoProjection = makeContext();
+ contextNoProjection.put(QueryContexts.NO_PROJECTIONS, "true");
+ TimeseriesQuery queryNoProjection =
queryBuilder.context(contextNoProjection).build();
+ // construct a query to use projection
+ Map<String, Object> contextWithProjection = makeContext();
+ contextWithProjection.put(QueryContexts.USE_PROJECTION,
"daily_market_maxQuality");
+ TimeseriesQuery queryWithProjection =
queryBuilder.context(contextWithProjection).build();
+
+
+ List<Result<TimeseriesResultValue>> resultNoProjection =
runner.run(QueryPlus.wrap(queryNoProjection)).toList();
+ List<Result<TimeseriesResultValue>> resultWithProjection =
runner.run(QueryPlus.wrap(queryWithProjection)).toList();
+
+ List<Result<TimeseriesResultValue>> expectedResults = List.of(
+ new Result<>(DateTimes.of("2011-01-20"),
createTimeseriesResultValue("maxQuality", 1800L)),
+ new Result<>(DateTimes.of("2011-01-21"),
createTimeseriesResultValue("maxQuality", null))
+ );
+ Assert.assertEquals(expectedResults, resultNoProjection);
+ Assert.assertEquals(expectedResults, resultWithProjection);
+ }
+
+ @Test
+ public void testTimeseriesProjectionsCounts()
+ {
+
+ AggregatorFactory countAgg = new CountAggregatorFactory("count");
+ Druids.TimeseriesQueryBuilder queryBuilder =
Druids.newTimeseriesQueryBuilder()
+
.dataSource(QueryRunnerTestHelper.DATA_SOURCE)
+
.granularity(Granularities.DAY)
+
.intervals("2011-01-20/2011-01-22")
+
.aggregators(List.of(countAgg));
+ // construct a query to ignore projection
+ Map<String, Object> contextNoProjection = makeContext();
+ contextNoProjection.put(QueryContexts.NO_PROJECTIONS, "true");
+ TimeseriesQuery queryNoProjection =
queryBuilder.context(contextNoProjection).build();
+ // construct a query to use projection
+ Map<String, Object> contextWithProjection = makeContext();
+ contextWithProjection.put(QueryContexts.USE_PROJECTION,
"daily_countAndQualityCardinalityAndMaxLongNullable");
+ TimeseriesQuery queryWithProjection =
queryBuilder.context(contextWithProjection).build();
+
+
+ List<Result<TimeseriesResultValue>> resultNoProjection =
runner.run(QueryPlus.wrap(queryNoProjection)).toList();
+ List<Result<TimeseriesResultValue>> resultWithProjection =
runner.run(QueryPlus.wrap(queryWithProjection)).toList();
+
+ List<Result<TimeseriesResultValue>> expectedResults = List.of(
+ new Result<>(DateTimes.of("2011-01-20"),
createTimeseriesResultValue("count", 13L)),
+ new Result<>(DateTimes.of("2011-01-21"),
createTimeseriesResultValue("count", 0L))
+ );
+ Assert.assertEquals(expectedResults, resultNoProjection);
+ Assert.assertEquals(expectedResults, resultWithProjection);
+ }
+
+ @Test
+ public void testTimeseriesNullableLongMax()
+ {
+
+ AggregatorFactory longNullableMax = new
LongMaxAggregatorFactory("longNullableMax", "longNumericNull");
+ Druids.TimeseriesQueryBuilder queryBuilder =
Druids.newTimeseriesQueryBuilder()
+
.dataSource(QueryRunnerTestHelper.DATA_SOURCE)
+
.granularity(Granularities.DAY)
+
.intervals("2011-01-20/2011-01-22")
+
.aggregators(List.of(longNullableMax));
+ // construct a query to ignore projection
+ Map<String, Object> contextNoProjection = makeContext();
+ contextNoProjection.put(QueryContexts.NO_PROJECTIONS, "true");
+ TimeseriesQuery queryNoProjection =
queryBuilder.context(contextNoProjection).build();
+ // construct a query to use projection
+ Map<String, Object> contextWithProjection = makeContext();
+ contextWithProjection.put(QueryContexts.USE_PROJECTION,
"daily_countAndQualityCardinalityAndMaxLongNullable");
+ TimeseriesQuery queryWithProjection =
queryBuilder.context(contextWithProjection).build();
+
+
+ List<Result<TimeseriesResultValue>> resultNoProjection =
runner.run(QueryPlus.wrap(queryNoProjection)).toList();
+ List<Result<TimeseriesResultValue>> resultWithProjection =
runner.run(QueryPlus.wrap(queryWithProjection)).toList();
+
+ List<Result<TimeseriesResultValue>> expectedResults = List.of(
+ new Result<>(DateTimes.of("2011-01-20"),
createTimeseriesResultValue("longNullableMax", 80L)),
+ new Result<>(DateTimes.of("2011-01-21"),
createTimeseriesResultValue("longNullableMax", null))
+ );
+ Assert.assertEquals(expectedResults, resultNoProjection);
+ Assert.assertEquals(expectedResults, resultWithProjection);
+ }
+
@Test
public void testFullOnTimeseriesMaxMin()
{
@@ -3201,4 +3296,11 @@ public class TimeseriesQueryRunnerTest extends
InitializedNullHandlingTest
expectedException.expectMessage("Cannot vectorize!");
}
}
+
+ private static TimeseriesResultValue createTimeseriesResultValue(String key,
@Nullable Object val)
+ {
+ Map<String, Object> map = new HashMap<>();
+ map.put(key, val);
+ return new TimeseriesResultValue(map);
+ }
}
diff --git a/processing/src/test/java/org/apache/druid/segment/TestIndex.java
b/processing/src/test/java/org/apache/druid/segment/TestIndex.java
index c538c2b282d..712d5d95002 100644
--- a/processing/src/test/java/org/apache/druid/segment/TestIndex.java
+++ b/processing/src/test/java/org/apache/druid/segment/TestIndex.java
@@ -45,12 +45,14 @@ import
org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.java.util.common.parsers.JSONPathSpec;
import org.apache.druid.query.aggregation.AggregatorFactory;
+import org.apache.druid.query.aggregation.CountAggregatorFactory;
import org.apache.druid.query.aggregation.DoubleMaxAggregatorFactory;
import org.apache.druid.query.aggregation.DoubleMinAggregatorFactory;
import org.apache.druid.query.aggregation.DoubleSumAggregatorFactory;
import org.apache.druid.query.aggregation.FloatMaxAggregatorFactory;
import org.apache.druid.query.aggregation.FloatMinAggregatorFactory;
import org.apache.druid.query.aggregation.FloatSumAggregatorFactory;
+import org.apache.druid.query.aggregation.LongMaxAggregatorFactory;
import
org.apache.druid.query.aggregation.hyperloglog.HyperUniquesAggregatorFactory;
import org.apache.druid.query.aggregation.hyperloglog.HyperUniquesSerde;
import org.apache.druid.query.expression.TestExprMacroTable;
@@ -76,6 +78,8 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
+import static org.apache.druid.query.QueryRunnerTestHelper.QUALITY_CARDINALITY;
+
/**
*
*/
@@ -183,13 +187,20 @@ public class TestIndex
};
public static final ImmutableList<AggregateProjectionSpec> PROJECTIONS =
ImmutableList.of(
new AggregateProjectionSpec(
- "index_projection",
+ "daily_market_maxQuality",
+
VirtualColumns.create(Granularities.toVirtualColumn(Granularities.DAY,
"__gran")),
+ List.of(new LongDimensionSchema("__gran"), new
StringDimensionSchema("market")),
+ new AggregatorFactory[]{new LongMaxAggregatorFactory("maxQuality",
"qualityLong")}
+ ),
+ new AggregateProjectionSpec(
+ "daily_countAndQualityCardinalityAndMaxLongNullable",
VirtualColumns.create(Granularities.toVirtualColumn(Granularities.DAY,
"__gran")),
- Arrays.asList(
- new LongDimensionSchema("__gran"),
- new StringDimensionSchema("market")
- ),
- new AggregatorFactory[]{new DoubleMaxAggregatorFactory("maxQuality",
"qualityLong")}
+ List.of(new LongDimensionSchema("__gran")),
+ new AggregatorFactory[]{
+ new CountAggregatorFactory("count"),
+ QUALITY_CARDINALITY,
+ new LongMaxAggregatorFactory("longNullableMax",
"longNumericNull")
+ }
)
);
public static final IndexSpec INDEX_SPEC = IndexSpec.DEFAULT;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]