This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 3c92923b82d Give the value-accumulating aggregations the query's null
handling option, and add their missing multi-value paths (#19326)
3c92923b82d is described below
commit 3c92923b82d0ee75ecaa863d07bf7f9356ec2e35
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Aug 21 12:17:05 2026 -0700
Give the value-accumulating aggregations the query's null handling option,
and add their missing multi-value paths (#19326)
---
.../function/AggregationFunctionFactory.java | 19 +-
.../function/FourthMomentAggregationFunction.java | 87 ++--
.../function/HistogramAggregationFunction.java | 452 ++++++++++++++---
.../function/IdSetAggregationFunction.java | 537 ++++++++++++++-------
.../function/StUnionAggregationFunction.java | 148 +++++-
.../array/SumArrayDoubleAggregationFunction.java | 63 ++-
.../array/SumArrayLongAggregationFunction.java | 64 ++-
.../pinot/core/common/SyntheticBlockValSets.java | 232 +++++++++
.../AggregationFunctionNullContractTest.java | 13 +-
.../function/StUnionAggregationFunctionTest.java | 4 +-
.../function/ValueAggregationNullHandlingTest.java | 453 +++++++++++++++++
11 files changed, 1704 insertions(+), 368 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
index 4354ae48282..519333ab345 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
@@ -299,9 +299,9 @@ public class AggregationFunctionFactory {
return new ListAggFunction(arguments.get(0), separator,
nullHandlingEnabled);
}
case SUMARRAYLONG:
- return new SumArrayLongAggregationFunction(arguments);
+ return new SumArrayLongAggregationFunction(arguments,
nullHandlingEnabled);
case SUMARRAYDOUBLE:
- return new SumArrayDoubleAggregationFunction(arguments);
+ return new SumArrayDoubleAggregationFunction(arguments,
nullHandlingEnabled);
case ARRAYAGG: {
Preconditions.checkArgument(numArguments >= 2,
"ARRAY_AGG expects 2 or 3 arguments, got: %s. The function can
be used as "
@@ -420,7 +420,7 @@ public class AggregationFunctionFactory {
case DISTINCTAVG:
return new DistinctAvgAggregationFunction(arguments,
nullHandlingEnabled);
case IDSET:
- return new IdSetAggregationFunction(arguments);
+ return new IdSetAggregationFunction(arguments,
nullHandlingEnabled);
case COUNTMV:
return new CountMVAggregationFunction(arguments,
nullHandlingEnabled);
case MINMV:
@@ -454,9 +454,9 @@ public class AggregationFunctionFactory {
case DISTINCTAVGMV:
return new DistinctAvgMVAggregationFunction(arguments,
nullHandlingEnabled);
case STUNION:
- return new StUnionAggregationFunction(arguments);
+ return new StUnionAggregationFunction(arguments,
nullHandlingEnabled);
case HISTOGRAM:
- return new HistogramAggregationFunction(arguments);
+ return new HistogramAggregationFunction(arguments,
nullHandlingEnabled);
case COVARPOP:
return new CovarianceAggregationFunction(arguments, false,
nullHandlingEnabled);
case COVARSAMP:
@@ -474,11 +474,14 @@ public class AggregationFunctionFactory {
case STDDEVSAMP:
return new VarianceAggregationFunction(arguments, true, true,
nullHandlingEnabled);
case SKEWNESS:
- return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.SKEWNESS);
+ return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.SKEWNESS,
+ nullHandlingEnabled);
case KURTOSIS:
- return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.KURTOSIS);
+ return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.KURTOSIS,
+ nullHandlingEnabled);
case FOURTHMOMENT:
- return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.MOMENT);
+ return new FourthMomentAggregationFunction(arguments,
FourthMomentAggregationFunction.Type.MOMENT,
+ nullHandlingEnabled);
case DISTINCTCOUNTTUPLESKETCH:
// mode actually doesn't matter here because we only care about
keys, not values
return new
DistinctCountIntegerTupleSketchAggregationFunction(arguments,
IntegerSummary.Mode.Sum,
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FourthMomentAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FourthMomentAggregationFunction.java
index d4a7dd871dc..64f4162fccd 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FourthMomentAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FourthMomentAggregationFunction.java
@@ -35,7 +35,8 @@ import
org.apache.pinot.segment.local.customobject.PinotFourthMoment;
import org.apache.pinot.segment.spi.AggregationFunctionType;
-public class FourthMomentAggregationFunction extends
BaseSingleInputAggregationFunction<PinotFourthMoment, Double> {
+public class FourthMomentAggregationFunction
+ extends NullableSingleInputAggregationFunction<PinotFourthMoment, Double> {
private final Type _type;
@@ -43,8 +44,9 @@ public class FourthMomentAggregationFunction extends
BaseSingleInputAggregationF
KURTOSIS, SKEWNESS, MOMENT
}
- public FourthMomentAggregationFunction(List<ExpressionContext> arguments,
Type type) {
- super(verifySingleArgument(arguments, type.name()));
+ public FourthMomentAggregationFunction(List<ExpressionContext> arguments,
Type type,
+ boolean nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, type.name()), nullHandlingEnabled);
_type = type;
}
@@ -77,65 +79,75 @@ public class FourthMomentAggregationFunction extends
BaseSingleInputAggregationF
Map<ExpressionContext, BlockValSet> blockValSetMap) {
double[] values =
StatisticalAggregationFunctionUtils.getValSet(blockValSetMap, _expression);
- PinotFourthMoment m4 = aggregationResultHolder.getResult();
- if (m4 == null) {
- m4 = new PinotFourthMoment();
- aggregationResultHolder.setValue(m4);
- }
-
- for (int i = 0; i < length; i++) {
- m4.increment(values[i]);
- }
+ // The moment is created inside the range, so a block with no non-null row
leaves the holder untouched and
+ // extractFinalResult sees the null that means nothing was aggregated
+ forEachNotNull(length, blockValSetMap.get(_expression), (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ PinotFourthMoment m4 = aggregationResultHolder.getResult();
+ if (m4 == null) {
+ m4 = new PinotFourthMoment();
+ aggregationResultHolder.setValue(m4);
+ }
+ for (int i = from; i < to; i++) {
+ m4.increment(values[i]);
+ }
+ });
}
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
double[] values =
StatisticalAggregationFunctionUtils.getValSet(blockValSetMap, _expression);
- for (int i = 0; i < length; i++) {
- PinotFourthMoment m4 = groupByResultHolder.getResult(groupKeyArray[i]);
- if (m4 == null) {
- m4 = new PinotFourthMoment();
- groupByResultHolder.setValueForKey(groupKeyArray[i], m4);
+ forEachNotNull(length, blockValSetMap.get(_expression), (from, to) -> {
+ for (int i = from; i < to; i++) {
+ PinotFourthMoment m4 = groupByResultHolder.getResult(groupKeyArray[i]);
+ if (m4 == null) {
+ m4 = new PinotFourthMoment();
+ groupByResultHolder.setValueForKey(groupKeyArray[i], m4);
+ }
+ m4.increment(values[i]);
}
- m4.increment(values[i]);
- }
+ });
}
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
double[] values =
StatisticalAggregationFunctionUtils.getValSet(blockValSetMap, _expression);
- for (int i = 0; i < length; i++) {
- for (int groupKey : groupKeysArray[i]) {
- PinotFourthMoment m4 = groupByResultHolder.getResult(groupKey);
- if (m4 == null) {
- m4 = new PinotFourthMoment();
- groupByResultHolder.setValueForKey(groupKey, m4);
+ forEachNotNull(length, blockValSetMap.get(_expression), (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int groupKey : groupKeysArray[i]) {
+ PinotFourthMoment m4 = groupByResultHolder.getResult(groupKey);
+ if (m4 == null) {
+ m4 = new PinotFourthMoment();
+ groupByResultHolder.setValueForKey(groupKey, m4);
+ }
+ m4.increment(values[i]);
}
- m4.increment(values[i]);
}
- }
+ });
}
+ @Nullable
@Override
public PinotFourthMoment extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
PinotFourthMoment m4 = aggregationResultHolder.getResult();
- if (m4 == null) {
- return new PinotFourthMoment();
- } else {
+ if (m4 != null) {
return m4;
}
+ // With the option disabled an untouched holder still renders the empty
accumulator, which is the
+ // intermediate this mode has always emitted; with it enabled the null is
the signal that nothing was
+ // aggregated.
+ return _nullHandlingEnabled ? null : new PinotFourthMoment();
}
+ @Nullable
@Override
public PinotFourthMoment extractGroupByResult(GroupByResultHolder
groupByResultHolder, int groupKey) {
PinotFourthMoment m4 = groupByResultHolder.getResult(groupKey);
- if (m4 == null) {
- return new PinotFourthMoment();
- } else {
- return m4;
- }
+ return m4 != null ? m4 : (_nullHandlingEnabled ? null : new
PinotFourthMoment());
}
@Override
@@ -169,7 +181,10 @@ public class FourthMomentAggregationFunction extends
BaseSingleInputAggregationF
@Override
public Double extractFinalResult(@Nullable PinotFourthMoment m4) {
if (m4 == null) {
- return null;
+ // A null intermediate result means nothing was aggregated. With null
handling enabled the skewness of nothing
+ // is NULL; with it disabled it is what an untouched moment renders to,
which Commons Math reports as NaN below
+ // the three samples skewness needs and the four kurtosis needs.
+ return _nullHandlingEnabled ? null : Double.NaN;
}
switch (_type) {
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/HistogramAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/HistogramAggregationFunction.java
index b4ca9bacc91..80830edcd92 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/HistogramAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/HistogramAggregationFunction.java
@@ -20,6 +20,7 @@ package org.apache.pinot.core.query.aggregation.function;
import com.google.common.base.Preconditions;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -41,7 +42,8 @@ import org.apache.pinot.spi.utils.ArrayCopyUtils;
/// usage example:
/// `Histogram(columnName, ARRAY\[0,1,10,100\])` to specify bins \[0,1),
\[1,10), \[10,1000\] or
/// `Histogram(columnName, 0, 1000, 10)` to specify 10 equal-length bins
\[0,100), \[100,200), ..., \[900,1000\]
-public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunction<DoubleArrayList, DoubleArrayList> {
+public class HistogramAggregationFunction
+ extends NullableSingleInputAggregationFunction<DoubleArrayList,
DoubleArrayList> {
private static final String ARRAY_CONSTRUCTOR = "arrayvalueconstructor";
private static final int INVALID_BIN = -1;
@@ -51,8 +53,8 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
double _upper;
double _binLength;
- public HistogramAggregationFunction(List<ExpressionContext> arguments) {
- super(arguments.get(0));
+ public HistogramAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(arguments.get(0), nullHandlingEnabled);
int numArguments = arguments.size();
Preconditions.checkArgument(numArguments == 4 || numArguments == 2,
"Histogram expects 2 or 4 arguments, got: %s;"
+ " usage example: `Histogram(columnName, ARRAY[0,1,10,100])` to
specify bins [0,1), [1,10), [10,1000] or "
@@ -150,6 +152,14 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
///
/// @param val input value
/// @return bin id
+ /// Counts one value into the supplied histogram, ignoring values that fall
outside every bin.
+ private void increment(double[] histogram, double value) {
+ int binId = getBinId(value);
+ if (binId != INVALID_BIN) {
+ histogram[binId] += 1;
+ }
+ }
+
private int getBinId(double val) {
if (val > _upper || val < _lower) {
return INVALID_BIN;
@@ -191,14 +201,17 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
}
+ @Nullable
@Override
public DoubleArrayList extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
- DoubleArrayList aggregationResultHolderResult =
aggregationResultHolder.getResult();
- if (aggregationResultHolderResult == null) {
- return DoubleVectorOpUtils.createAndInitialize(getNumBins());
- } else {
- return aggregationResultHolderResult;
+ DoubleArrayList histogram = aggregationResultHolder.getResult();
+ if (histogram != null) {
+ return histogram;
}
+ // With the option disabled an untouched holder still renders the empty
accumulator, which is the
+ // intermediate this mode has always emitted; with it enabled the null is
the signal that nothing was
+ // aggregated.
+ return _nullHandlingEnabled ? null :
DoubleVectorOpUtils.createAndInitialize(getNumBins());
}
@Nullable
@@ -237,6 +250,11 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
@Nullable
@Override
public DoubleArrayList extractFinalResult(@Nullable DoubleArrayList
doubleArrayList) {
+ if (doubleArrayList == null) {
+ // A null intermediate result means nothing was aggregated. With null
handling enabled the histogram of nothing
+ // is NULL; with it disabled it is the all-zero histogram, which is the
answer this mode has always given.
+ return _nullHandlingEnabled ? null :
DoubleVectorOpUtils.createAndInitialize(getNumBins());
+ }
return doubleArrayList;
}
@@ -244,50 +262,153 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
- Preconditions.checkState(blockValSet.isSingleValue(), "Histogram currently
only supports single-valued column");
+ if (blockValSet.isSingleValue()) {
+ aggregateSVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ }
+ }
+
+ private void aggregateSVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
switch (blockValSet.getValueType().getStoredType()) {
case INT: {
int[] values = blockValSet.getIntValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- double value = values[i];
- for (int groupKey : groupKeysArray[i]) {
- setGroupByResult(groupKey, groupByResultHolder, value);
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ int value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- }
+ });
break;
}
case LONG: {
long[] values = blockValSet.getLongValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- double value = values[i];
- for (int groupKey : groupKeysArray[i]) {
- setGroupByResult(groupKey, groupByResultHolder, value);
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ long value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- }
+ });
break;
}
case FLOAT: {
float[] values = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- double value = values[i];
- for (int groupKey : groupKeysArray[i]) {
- setGroupByResult(groupKey, groupByResultHolder, value);
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ float value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- }
+ });
break;
}
case DOUBLE: {
double[] values = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- double value = values[i];
- for (int groupKey : groupKeysArray[i]) {
- setGroupByResult(groupKey, groupByResultHolder, value);
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ double value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- }
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[] values = blockValSet.getBigDecimalValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ double value = values[i].doubleValue();
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
+ }
+ });
break;
}
default:
- throw new IllegalStateException("Cannot compute histogram for
non-numeric type: " + blockValSet.getValueType());
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
+ }
+ }
+
+ private void aggregateMVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ switch (blockValSet.getValueType().getStoredType()) {
+ case INT: {
+ int[][] values = blockValSet.getIntValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (int value : values[i]) {
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
+ }
+ }
+ });
+ break;
+ }
+ case LONG: {
+ long[][] values = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (long value : values[i]) {
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
+ }
+ }
+ });
+ break;
+ }
+ case FLOAT: {
+ float[][] values = blockValSet.getFloatValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (float value : values[i]) {
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
+ }
+ }
+ });
+ break;
+ }
+ case DOUBLE: {
+ double[][] values = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (double value : values[i]) {
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
+ }
+ }
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[][] values = blockValSet.getBigDecimalValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (BigDecimal value : values[i]) {
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder,
value.doubleValue());
+ }
+ }
+ }
+ });
+ break;
+ }
+ default:
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
}
}
@@ -295,37 +416,133 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.isSingleValue()) {
+ aggregateSVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
+ }
+ }
+
+ private void aggregateSVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
switch (blockValSet.getValueType().getStoredType()) {
case INT: {
int[] values = blockValSet.getIntValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- setGroupByResult(groupKeyArray[i], groupByResultHolder, values[i]);
- }
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ int value = values[i];
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ });
break;
}
case LONG: {
long[] values = blockValSet.getLongValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- setGroupByResult(groupKeyArray[i], groupByResultHolder, values[i]);
- }
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ long value = values[i];
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ });
break;
}
case FLOAT: {
float[] values = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- setGroupByResult(groupKeyArray[i], groupByResultHolder, values[i]);
- }
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ float value = values[i];
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ });
break;
}
case DOUBLE: {
double[] values = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- setGroupByResult(groupKeyArray[i], groupByResultHolder, values[i]);
- }
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ double value = values[i];
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[] values = blockValSet.getBigDecimalValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ double value = values[i].doubleValue();
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ });
break;
}
default:
- throw new IllegalStateException("Cannot compute histogram for
non-numeric type: " + blockValSet.getValueType());
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
+ }
+ }
+
+ private void aggregateMVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ switch (blockValSet.getValueType().getStoredType()) {
+ case INT: {
+ int[][] values = blockValSet.getIntValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (int value : values[i]) {
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ }
+ });
+ break;
+ }
+ case LONG: {
+ long[][] values = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (long value : values[i]) {
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ }
+ });
+ break;
+ }
+ case FLOAT: {
+ float[][] values = blockValSet.getFloatValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (float value : values[i]) {
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ }
+ });
+ break;
+ }
+ case DOUBLE: {
+ double[][] values = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (double value : values[i]) {
+ setGroupByResult(groupKeyArray[i], groupByResultHolder, value);
+ }
+ }
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[][] values = blockValSet.getBigDecimalValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (BigDecimal value : values[i]) {
+ setGroupByResult(groupKeyArray[i], groupByResultHolder,
value.doubleValue());
+ }
+ }
+ });
+ break;
+ }
+ default:
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
}
}
@@ -345,56 +562,149 @@ public class HistogramAggregationFunction extends
BaseSingleInputAggregationFunc
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
- //TODO: Add MV support for histogram
- Preconditions.checkState(blockValSet.isSingleValue(), "Histogram currently
only supports single-valued column");
- double[] histogram = new double[this.getNumBins()];
+ if (blockValSet.isSingleValue()) {
+ aggregateSV(length, aggregationResultHolder, blockValSet);
+ } else {
+ aggregateMV(length, aggregationResultHolder, blockValSet);
+ }
+ }
+
+ private void aggregateSV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ double[] histogram = new double[getNumBins()];
+ int numRows;
switch (blockValSet.getValueType().getStoredType()) {
case INT: {
int[] values = blockValSet.getIntValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- int binId = this.getBinId(values[i]);
- if (binId != INVALID_BIN) {
- histogram[binId] += 1;
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ increment(histogram, values[i]);
}
- }
- setAggregationResult(aggregationResultHolder, histogram);
+ return acum + to - from;
+ });
break;
}
case LONG: {
long[] values = blockValSet.getLongValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- int binId = this.getBinId(values[i]);
- if (binId != INVALID_BIN) {
- histogram[binId] += 1;
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ increment(histogram, values[i]);
}
- }
- setAggregationResult(aggregationResultHolder, histogram);
+ return acum + to - from;
+ });
break;
}
case FLOAT: {
float[] values = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- int binId = this.getBinId(values[i]);
- if (binId != INVALID_BIN) {
- histogram[binId] += 1;
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ increment(histogram, values[i]);
}
- }
- setAggregationResult(aggregationResultHolder, histogram);
+ return acum + to - from;
+ });
break;
}
case DOUBLE: {
double[] values = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length && i < values.length; i++) {
- int binId = this.getBinId(values[i]);
- if (binId != INVALID_BIN) {
- histogram[binId] += 1;
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ increment(histogram, values[i]);
}
- }
- setAggregationResult(aggregationResultHolder, histogram);
+ return acum + to - from;
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[] values = blockValSet.getBigDecimalValuesSV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ increment(histogram, values[i].doubleValue());
+ }
+ return acum + to - from;
+ });
break;
}
default:
- throw new IllegalStateException("Cannot compute histogram for
non-numeric type: " + blockValSet.getValueType());
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
+ }
+ // The histogram is published only when a row reached it, so a block with
no non-null row leaves the holder
+ // untouched and extractFinalResult sees the null that means nothing was
aggregated. It is published once rather
+ // than per range, because the buffer accumulates across ranges and adding
it again would recount earlier rows.
+ if (numRows > 0) {
+ setAggregationResult(aggregationResultHolder, histogram);
+ }
+ }
+
+ private void aggregateMV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ double[] histogram = new double[getNumBins()];
+ int numRows;
+ switch (blockValSet.getValueType().getStoredType()) {
+ case INT: {
+ int[][] values = blockValSet.getIntValuesMV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (int value : values[i]) {
+ increment(histogram, value);
+ }
+ }
+ return acum + to - from;
+ });
+ break;
+ }
+ case LONG: {
+ long[][] values = blockValSet.getLongValuesMV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (long value : values[i]) {
+ increment(histogram, value);
+ }
+ }
+ return acum + to - from;
+ });
+ break;
+ }
+ case FLOAT: {
+ float[][] values = blockValSet.getFloatValuesMV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (float value : values[i]) {
+ increment(histogram, value);
+ }
+ }
+ return acum + to - from;
+ });
+ break;
+ }
+ case DOUBLE: {
+ double[][] values = blockValSet.getDoubleValuesMV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (double value : values[i]) {
+ increment(histogram, value);
+ }
+ }
+ return acum + to - from;
+ });
+ break;
+ }
+ case BIG_DECIMAL: {
+ BigDecimal[][] values = blockValSet.getBigDecimalValuesMV();
+ numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ for (int i = from; i < to && i < values.length; i++) {
+ for (BigDecimal value : values[i]) {
+ increment(histogram, value.doubleValue());
+ }
+ }
+ return acum + to - from;
+ });
+ break;
+ }
+ default:
+ throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ + blockValSet.getValueType());
+ }
+ if (numRows > 0) {
+ setAggregationResult(aggregationResultHolder, histogram);
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/IdSetAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/IdSetAggregationFunction.java
index 30cacf08e95..2e93c26cc97 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/IdSetAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/IdSetAggregationFunction.java
@@ -55,7 +55,7 @@ import org.apache.pinot.spi.data.FieldSpec.DataType;
/// - fpp: Desired false positive probability for the BloomFilter, must be
positive and less than 1.0. (Default 0.03)
///
/// Example: IDSET(col,
'sizeThresholdInBytes=1000;expectedInsertions=10000;fpp=0.03')
-public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction<IdSet, String> {
+public class IdSetAggregationFunction extends
NullableSingleInputAggregationFunction<IdSet, String> {
private static final char PARAMETER_DELIMITER = ';';
private static final char PARAMETER_KEY_VALUE_SEPARATOR = '=';
private static final String UPPER_CASE_SIZE_THRESHOLD_IN_BYTES =
"SIZETHRESHOLDINBYTES";
@@ -66,8 +66,8 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
private final int _expectedInsertions;
private final double _fpp;
- public IdSetAggregationFunction(List<ExpressionContext> arguments) {
- super(arguments.get(0));
+ public IdSetAggregationFunction(List<ExpressionContext> arguments, boolean
nullHandlingEnabled) {
+ super(arguments.get(0), nullHandlingEnabled);
if (arguments.size() == 1) {
_sizeThresholdInBytes = IdSets.DEFAULT_SIZE_THRESHOLD_IN_BYTES;
_expectedInsertions = IdSets.DEFAULT_EXPECTED_INSERTIONS;
@@ -127,94 +127,182 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
- DataType storedType = blockValSet.getValueType().getStoredType();
- IdSet idSet = getIdSet(aggregationResultHolder, storedType);
if (blockValSet.isSingleValue()) {
- switch (storedType) {
- case INT:
- int[] intValuesSV = blockValSet.getIntValuesSV();
- for (int i = 0; i < length; i++) {
+ aggregateSV(length, aggregationResultHolder, blockValSet);
+ } else {
+ aggregateMV(length, aggregationResultHolder, blockValSet);
+ }
+ }
+
+ private void aggregateSV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[] intValuesSV = blockValSet.getIntValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(intValuesSV[i]);
}
- break;
- case LONG:
- long[] longValuesSV = blockValSet.getLongValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[] longValuesSV = blockValSet.getLongValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(longValuesSV[i]);
}
- break;
- case FLOAT:
- float[] floatValuesSV = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[] floatValuesSV = blockValSet.getFloatValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(floatValuesSV[i]);
}
- break;
- case DOUBLE:
- double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(doubleValuesSV[i]);
}
- break;
- case STRING:
- String[] stringValuesSV = blockValSet.getStringValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[] stringValuesSV = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(stringValuesSV[i]);
}
- break;
- case BYTES:
- byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case BYTES:
+ byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
idSet.add(bytesValuesSV[i]);
}
- break;
- default:
- throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
- }
- } else {
- switch (storedType) {
- case INT:
- int[][] intValuesMV = blockValSet.getIntValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
+ }
+ }
+
+ private void aggregateMV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[][] intValuesMV = blockValSet.getIntValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
for (int intValue : intValuesMV[i]) {
idSet.add(intValue);
}
}
- break;
- case LONG:
- long[][] longValuesMV = blockValSet.getLongValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[][] longValuesMV = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
for (long longValue : longValuesMV[i]) {
idSet.add(longValue);
}
}
- break;
- case FLOAT:
- float[][] floatValuesMV = blockValSet.getFloatValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[][] floatValuesMV = blockValSet.getFloatValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
for (float floatValue : floatValuesMV[i]) {
idSet.add(floatValue);
}
}
- break;
- case DOUBLE:
- double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
for (double doubleValue : doubleValuesMV[i]) {
idSet.add(doubleValue);
}
}
- break;
- case STRING:
- String[][] stringValuesMV = blockValSet.getStringValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[][] stringValuesMV = blockValSet.getStringValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
for (String stringValue : stringValuesMV[i]) {
idSet.add(stringValue);
}
}
- break;
- default:
- throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
- }
+ });
+ break;
+ case BYTES:
+ byte[][][] bytesValuesMV = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ IdSet idSet = getIdSet(aggregationResultHolder, storedType);
+ for (int i = from; i < to; i++) {
+ for (byte[] bytesValue : bytesValuesMV[i]) {
+ idSet.add(bytesValue);
+ }
+ }
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
}
}
@@ -222,98 +310,142 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
- DataType storedType = blockValSet.getValueType().getStoredType();
if (blockValSet.isSingleValue()) {
- switch (storedType) {
- case INT:
- int[] intValuesSV = blockValSet.getIntValuesSV();
- for (int i = 0; i < length; i++) {
+ aggregateSVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
+ }
+ }
+
+ private void aggregateSVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[] intValuesSV = blockValSet.getIntValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.INT).add(intValuesSV[i]);
}
- break;
- case LONG:
- long[] longValuesSV = blockValSet.getLongValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[] longValuesSV = blockValSet.getLongValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.LONG).add(longValuesSV[i]);
}
- break;
- case FLOAT:
- float[] floatValuesSV = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[] floatValuesSV = blockValSet.getFloatValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.FLOAT).add(floatValuesSV[i]);
}
- break;
- case DOUBLE:
- double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.DOUBLE).add(doubleValuesSV[i]);
}
- break;
- case STRING:
- String[] stringValuesSV = blockValSet.getStringValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[] stringValuesSV = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.STRING).add(stringValuesSV[i]);
}
- break;
- case BYTES:
- byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case BYTES:
+ byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.BYTES).add(bytesValuesSV[i]);
}
- break;
- default:
- throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
- }
- } else {
- switch (storedType) {
- case INT:
- int[][] intValuesMV = blockValSet.getIntValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
+ }
+ }
+
+ private void aggregateMVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[][] intValuesMV = blockValSet.getIntValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.INT);
for (int intValue : intValuesMV[i]) {
idSet.add(intValue);
}
}
- break;
- case LONG:
- long[][] longValuesMV = blockValSet.getLongValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[][] longValuesMV = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.LONG);
for (long longValue : longValuesMV[i]) {
idSet.add(longValue);
}
}
- break;
- case FLOAT:
- float[][] floatValuesMV = blockValSet.getFloatValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[][] floatValuesMV = blockValSet.getFloatValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.FLOAT);
for (float floatValue : floatValuesMV[i]) {
idSet.add(floatValue);
}
}
- break;
- case DOUBLE:
- double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.DOUBLE);
for (double doubleValue : doubleValuesMV[i]) {
idSet.add(doubleValue);
}
}
- break;
- case STRING:
- String[][] stringValuesMV = blockValSet.getStringValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[][] stringValuesMV = blockValSet.getStringValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.STRING);
for (String stringValue : stringValuesMV[i]) {
idSet.add(stringValue);
}
}
- break;
- default:
- throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
- }
+ });
+ break;
+ case BYTES:
+ byte[][][] bytesValuesMV = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ IdSet idSet = getIdSet(groupByResultHolder, groupKeyArray[i],
DataType.BYTES);
+ for (byte[] bytesValue : bytesValuesMV[i]) {
+ idSet.add(bytesValue);
+ }
+ }
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
}
}
@@ -321,71 +453,96 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
- DataType storedType = blockValSet.getValueType().getStoredType();
if (blockValSet.isSingleValue()) {
- switch (storedType) {
- case INT:
- int[] intValuesSV = blockValSet.getIntValuesSV();
- for (int i = 0; i < length; i++) {
+ aggregateSVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ }
+ }
+
+ private void aggregateSVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[] intValuesSV = blockValSet.getIntValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
int intValue = intValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.INT).add(intValue);
}
}
- break;
- case LONG:
- long[] longValuesSV = blockValSet.getLongValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[] longValuesSV = blockValSet.getLongValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
long longValue = longValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.LONG).add(longValue);
}
}
- break;
- case FLOAT:
- float[] floatValuesSV = blockValSet.getFloatValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[] floatValuesSV = blockValSet.getFloatValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
float floatValue = floatValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.FLOAT).add(floatValue);
}
}
- break;
- case DOUBLE:
- double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[] doubleValuesSV = blockValSet.getDoubleValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
double doubleValue = doubleValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.DOUBLE).add(doubleValue);
}
}
- break;
- case STRING:
- String[] stringValuesSV = blockValSet.getStringValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[] stringValuesSV = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
String stringValue = stringValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.STRING).add(stringValue);
}
}
- break;
- case BYTES:
- byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case BYTES:
+ byte[][] bytesValuesSV = blockValSet.getBytesValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
byte[] bytesValue = bytesValuesSV[i];
for (int groupKey : groupKeysArray[i]) {
getIdSet(groupByResultHolder, groupKey,
DataType.BYTES).add(bytesValue);
}
}
- break;
- default:
- throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
- }
- } else {
- switch (storedType) {
- case INT:
- int[][] intValuesMV = blockValSet.getIntValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal SV data type for ID_SET
aggregation function: " + storedType);
+ }
+ }
+
+ private void aggregateMVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ DataType storedType = blockValSet.getValueType().getStoredType();
+ switch (storedType) {
+ case INT:
+ int[][] intValuesMV = blockValSet.getIntValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
int[] intValues = intValuesMV[i];
for (int groupKey : groupKeysArray[i]) {
IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.INT);
@@ -394,10 +551,12 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
}
}
}
- break;
- case LONG:
- long[][] longValuesMV = blockValSet.getLongValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case LONG:
+ long[][] longValuesMV = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
long[] longValues = longValuesMV[i];
for (int groupKey : groupKeysArray[i]) {
IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.LONG);
@@ -406,10 +565,12 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
}
}
}
- break;
- case FLOAT:
- float[][] floatValuesMV = blockValSet.getFloatValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case FLOAT:
+ float[][] floatValuesMV = blockValSet.getFloatValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
float[] floatValues = floatValuesMV[i];
for (int groupKey : groupKeysArray[i]) {
IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.FLOAT);
@@ -418,10 +579,12 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
}
}
}
- break;
- case DOUBLE:
- double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case DOUBLE:
+ double[][] doubleValuesMV = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
double[] doubleValues = doubleValuesMV[i];
for (int groupKey : groupKeysArray[i]) {
IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.DOUBLE);
@@ -430,10 +593,12 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
}
}
}
- break;
- case STRING:
- String[][] stringValuesMV = blockValSet.getStringValuesMV();
- for (int i = 0; i < length; i++) {
+ });
+ break;
+ case STRING:
+ String[][] stringValuesMV = blockValSet.getStringValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
String[] stringValues = stringValuesMV[i];
for (int groupKey : groupKeysArray[i]) {
IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.STRING);
@@ -442,23 +607,45 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
}
}
}
- break;
- default:
- throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
- }
+ });
+ break;
+ case BYTES:
+ byte[][][] bytesValuesMV = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ byte[][] bytesValues = bytesValuesMV[i];
+ for (int groupKey : groupKeysArray[i]) {
+ IdSet idSet = getIdSet(groupByResultHolder, groupKey,
DataType.BYTES);
+ for (byte[] bytesValue : bytesValues) {
+ idSet.add(bytesValue);
+ }
+ }
+ }
+ });
+ break;
+ default:
+ throw new IllegalStateException("Illegal MV data type for ID_SET
aggregation function: " + storedType);
}
}
@Override
+ @Nullable
public IdSet extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
IdSet idSet = aggregationResultHolder.getResult();
- return idSet != null ? idSet : IdSets.emptyIdSet();
+ if (idSet != null) {
+ return idSet;
+ }
+ // With the option disabled an untouched holder still renders the empty
accumulator, which is the
+ // intermediate this mode has always emitted; with it enabled the null is
the signal that nothing was
+ // aggregated.
+ return _nullHandlingEnabled ? null : IdSets.emptyIdSet();
}
@Override
+ @Nullable
public IdSet extractGroupByResult(GroupByResultHolder groupByResultHolder,
int groupKey) {
IdSet idSet = groupByResultHolder.getResult(groupKey);
- return idSet != null ? idSet : IdSets.emptyIdSet();
+ return idSet != null ? idSet : (_nullHandlingEnabled ? null :
IdSets.emptyIdSet());
}
@Override
@@ -490,12 +677,18 @@ public class IdSetAggregationFunction extends
BaseSingleInputAggregationFunction
@Nullable
@Override
public String extractFinalResult(@Nullable IdSet intermediateResult) {
- // A null intermediate result means nothing was aggregated, and there is
no id set to serialize
- if (intermediateResult == null) {
- return null;
+ // A null intermediate result means nothing was aggregated. With null
handling enabled there is no id set to
+ // serialize and the answer is NULL; with it disabled it is the serialized
empty id set, which is the answer this
+ // mode has always given.
+ IdSet idSet = intermediateResult;
+ if (idSet == null) {
+ if (_nullHandlingEnabled) {
+ return null;
+ }
+ idSet = IdSets.emptyIdSet();
}
try {
- return intermediateResult.toBase64String();
+ return idSet.toBase64String();
} catch (IOException e) {
throw new RuntimeException("Caught exception while serializing IdSet",
e);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunction.java
index 039fa8912b3..5ad959a33fe 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunction.java
@@ -39,10 +39,10 @@ import org.locationtech.jts.geom.util.GeometryCombiner;
import org.locationtech.jts.operation.union.UnaryUnionOp;
-public class StUnionAggregationFunction extends
BaseSingleInputAggregationFunction<Geometry, ByteArray> {
+public class StUnionAggregationFunction extends
NullableSingleInputAggregationFunction<Geometry, ByteArray> {
- public StUnionAggregationFunction(List<ExpressionContext> arguments) {
- super(verifySingleArgument(arguments, "ST_UNION"));
+ public StUnionAggregationFunction(List<ExpressionContext> arguments, boolean
nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, "ST_UNION"), nullHandlingEnabled);
}
@Override
@@ -63,49 +63,142 @@ public class StUnionAggregationFunction extends
BaseSingleInputAggregationFuncti
@Override
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- byte[][] bytesArray = blockValSetMap.get(_expression).getBytesValuesSV();
- Geometry geometry = aggregationResultHolder.getResult();
- for (int i = 0; i < length; i++) {
- geometry = union(geometry,
GeometrySerializer.deserialize(bytesArray[i]));
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.isSingleValue()) {
+ aggregateSV(length, aggregationResultHolder, blockValSet);
+ } else {
+ aggregateMV(length, aggregationResultHolder, blockValSet);
}
- aggregationResultHolder.setValue(geometry);
+ }
+
+ private void aggregateSV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ byte[][] bytesArray = blockValSet.getBytesValuesSV();
+ // The holder is written only from inside the range, so a block with no
non-null row leaves it untouched and
+ // extractFinalResult sees the null that means nothing was aggregated
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ Geometry geometry = aggregationResultHolder.getResult();
+ for (int i = from; i < to; i++) {
+ geometry = union(geometry,
GeometrySerializer.deserialize(bytesArray[i]));
+ }
+ aggregationResultHolder.setValue(geometry);
+ });
+ }
+
+ /// Every geometry of a multi-value row is folded into the same union, so a
row contributes once per value.
+ private void aggregateMV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
+ byte[][][] bytesArrays = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ Geometry geometry = aggregationResultHolder.getResult();
+ for (int i = from; i < to; i++) {
+ for (byte[] bytes : bytesArrays[i]) {
+ geometry = union(geometry, GeometrySerializer.deserialize(bytes));
+ }
+ }
+ aggregationResultHolder.setValue(geometry);
+ });
}
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- byte[][] bytesArray = blockValSetMap.get(_expression).getBytesValuesSV();
- for (int i = 0; i < length; i++) {
- int groupKey = groupKeyArray[i];
- Geometry value = GeometrySerializer.deserialize(bytesArray[i]);
- Geometry geometry = groupByResultHolder.getResult(groupKey);
- groupByResultHolder.setValueForKey(groupKey, union(geometry, value));
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.isSingleValue()) {
+ aggregateSVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupBySV(length, groupKeyArray, groupByResultHolder,
blockValSet);
}
}
+ private void aggregateSVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ byte[][] bytesArray = blockValSet.getBytesValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int groupKey = groupKeyArray[i];
+ Geometry value = GeometrySerializer.deserialize(bytesArray[i]);
+ groupByResultHolder.setValueForKey(groupKey,
union(groupByResultHolder.getResult(groupKey), value));
+ }
+ });
+ }
+
+ private void aggregateMVGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ byte[][][] bytesArrays = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int groupKey = groupKeyArray[i];
+ for (byte[] bytes : bytesArrays[i]) {
+ Geometry value = GeometrySerializer.deserialize(bytes);
+ groupByResultHolder.setValueForKey(groupKey,
union(groupByResultHolder.getResult(groupKey), value));
+ }
+ }
+ });
+ }
+
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- byte[][] bytesArray = blockValSetMap.get(_expression).getBytesValuesSV();
- for (int i = 0; i < length; i++) {
- Geometry value = GeometrySerializer.deserialize(bytesArray[i]);
- for (int groupKey : groupKeysArray[i]) {
- Geometry geometry = groupByResultHolder.getResult(groupKey);
- groupByResultHolder.setValueForKey(groupKey, union(geometry, value));
- }
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.isSingleValue()) {
+ aggregateSVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ } else {
+ aggregateMVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
}
}
+ private void aggregateSVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ byte[][] bytesArray = blockValSet.getBytesValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ // Deserialized once per row, not once per group key the row belongs to
+ Geometry value = GeometrySerializer.deserialize(bytesArray[i]);
+ for (int groupKey : groupKeysArray[i]) {
+ groupByResultHolder.setValueForKey(groupKey,
union(groupByResultHolder.getResult(groupKey), value));
+ }
+ }
+ });
+ }
+
+ private void aggregateMVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
+ byte[][][] bytesArrays = blockValSet.getBytesValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (byte[] bytes : bytesArrays[i]) {
+ Geometry value = GeometrySerializer.deserialize(bytes);
+ for (int groupKey : groupKeysArray[i]) {
+ groupByResultHolder.setValueForKey(groupKey,
union(groupByResultHolder.getResult(groupKey), value));
+ }
+ }
+ }
+ });
+ }
+
+ @Nullable
@Override
public Geometry extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
Geometry geometry = aggregationResultHolder.getResult();
- return geometry == null ? GeometryUtils.EMPTY_POINT : geometry;
+ if (geometry != null) {
+ return geometry;
+ }
+ // With the option disabled an untouched holder still renders the empty
accumulator, which is the
+ // intermediate this mode has always emitted; with it enabled the null is
the signal that nothing was
+ // aggregated.
+ return _nullHandlingEnabled ? null : GeometryUtils.EMPTY_POINT;
}
+ @Nullable
@Override
public Geometry extractGroupByResult(GroupByResultHolder
groupByResultHolder, int groupKey) {
Geometry geometry = groupByResultHolder.getResult(groupKey);
- return geometry == null ? GeometryUtils.EMPTY_POINT : geometry;
+ return geometry != null ? geometry : (_nullHandlingEnabled ? null :
GeometryUtils.EMPTY_POINT);
}
@Override
@@ -137,8 +230,13 @@ public class StUnionAggregationFunction extends
BaseSingleInputAggregationFuncti
@Nullable
@Override
public ByteArray extractFinalResult(@Nullable Geometry geometry) {
- // A null intermediate result means nothing was aggregated; the union of
no geometries is NULL, matching ST_Union
- return geometry != null ? new
ByteArray(GeometrySerializer.serialize(geometry)) : null;
+ if (geometry == null) {
+ // A null intermediate result means nothing was aggregated. With null
handling enabled the union of no
+ // geometries is NULL, matching ST_Union; with it disabled it is the
empty point, which is the answer this mode
+ // has always given.
+ return _nullHandlingEnabled ? null : new
ByteArray(GeometrySerializer.serialize(GeometryUtils.EMPTY_POINT));
+ }
+ return new ByteArray(GeometrySerializer.serialize(geometry));
}
/// Returns the union of the supplied geometries.
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayDoubleAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayDoubleAggregationFunction.java
index a10997e448b..f19468d3a49 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayDoubleAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayDoubleAggregationFunction.java
@@ -29,17 +29,17 @@ import org.apache.pinot.core.common.BlockValSet;
import org.apache.pinot.core.common.ObjectSerDeUtils;
import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
-import
org.apache.pinot.core.query.aggregation.function.BaseSingleInputAggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.NullableSingleInputAggregationFunction;
import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
import
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
import org.apache.pinot.segment.spi.AggregationFunctionType;
public class SumArrayDoubleAggregationFunction
- extends BaseSingleInputAggregationFunction<DoubleArrayList,
DoubleArrayList> {
+ extends NullableSingleInputAggregationFunction<DoubleArrayList,
DoubleArrayList> {
- public SumArrayDoubleAggregationFunction(List<ExpressionContext> arguments) {
- super(verifySingleArgument(arguments, "SUM_ARRAY"));
+ public SumArrayDoubleAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, "SUM_ARRAY"), nullHandlingEnabled);
}
@Override
@@ -60,39 +60,50 @@ public class SumArrayDoubleAggregationFunction
@Override
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- double[][] values = blockValSetMap.get(_expression).getDoubleValuesMV();
- if (aggregationResultHolder.getResult() == null) {
- aggregationResultHolder.setValue(new DoubleArrayList());
- }
- DoubleArrayList result = aggregationResultHolder.getResult();
- for (int i = 0; i < length; i++) {
- double[] value = values[i];
- aggregateMerge(value, result);
- }
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ double[][] values = blockValSet.getDoubleValuesMV();
+ // The accumulator is created inside the range, so a block with no
non-null row leaves the holder untouched and
+ // extractFinalResult sees the null that means nothing was aggregated
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ DoubleArrayList result = aggregationResultHolder.getResult();
+ if (result == null) {
+ result = new DoubleArrayList();
+ aggregationResultHolder.setValue(result);
+ }
+ for (int i = from; i < to; i++) {
+ aggregateMerge(values[i], result);
+ }
+ });
}
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- double[][] valuesArray =
blockValSetMap.get(_expression).getDoubleValuesMV();
- for (int i = 0; i < length; i++) {
- double[] values = valuesArray[i];
- int groupKey = groupKeyArray[i];
- setGroupByResult(groupByResultHolder, values, groupKey);
- }
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ double[][] valuesArray = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ setGroupByResult(groupByResultHolder, valuesArray[i],
groupKeyArray[i]);
+ }
+ });
}
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- double[][] valuesArray =
blockValSetMap.get(_expression).getDoubleValuesMV();
- for (int i = 0; i < length; i++) {
- double[] values = valuesArray[i];
- int[] groupKeys = groupKeysArray[i];
- for (int groupKey : groupKeys) {
- setGroupByResult(groupByResultHolder, values, groupKey);
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ double[][] valuesArray = blockValSet.getDoubleValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ double[] values = valuesArray[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupByResultHolder, values, groupKey);
+ }
}
- }
+ });
}
private void setGroupByResult(GroupByResultHolder groupByResultHolder,
double[] values, int groupKey) {
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayLongAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayLongAggregationFunction.java
index 78035b9fa7f..22c2d84a4c8 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayLongAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/array/SumArrayLongAggregationFunction.java
@@ -29,16 +29,17 @@ import org.apache.pinot.core.common.BlockValSet;
import org.apache.pinot.core.common.ObjectSerDeUtils;
import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
-import
org.apache.pinot.core.query.aggregation.function.BaseSingleInputAggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.NullableSingleInputAggregationFunction;
import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
import
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
import org.apache.pinot.segment.spi.AggregationFunctionType;
-public class SumArrayLongAggregationFunction extends
BaseSingleInputAggregationFunction<LongArrayList, LongArrayList> {
+public class SumArrayLongAggregationFunction
+ extends NullableSingleInputAggregationFunction<LongArrayList,
LongArrayList> {
- public SumArrayLongAggregationFunction(List<ExpressionContext> arguments) {
- super(verifySingleArgument(arguments, "SUM_ARRAY"));
+ public SumArrayLongAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, "SUM_ARRAY"), nullHandlingEnabled);
}
@Override
@@ -59,39 +60,50 @@ public class SumArrayLongAggregationFunction extends
BaseSingleInputAggregationF
@Override
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[][] values = blockValSetMap.get(_expression).getLongValuesMV();
- if (aggregationResultHolder.getResult() == null) {
- aggregationResultHolder.setValue(new LongArrayList());
- }
- LongArrayList result = aggregationResultHolder.getResult();
- for (int i = 0; i < length; i++) {
- long[] value = values[i];
- aggregateMerge(value, result);
- }
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ long[][] values = blockValSet.getLongValuesMV();
+ // The accumulator is created inside the range, so a block with no
non-null row leaves the holder untouched and
+ // extractFinalResult sees the null that means nothing was aggregated
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ if (to == from) {
+ return;
+ }
+ LongArrayList result = aggregationResultHolder.getResult();
+ if (result == null) {
+ result = new LongArrayList();
+ aggregationResultHolder.setValue(result);
+ }
+ for (int i = from; i < to; i++) {
+ aggregateMerge(values[i], result);
+ }
+ });
}
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[][] valuesArray = blockValSetMap.get(_expression).getLongValuesMV();
- for (int i = 0; i < length; i++) {
- long[] values = valuesArray[i];
- int groupKey = groupKeyArray[i];
- setGroupByResult(groupByResultHolder, values, groupKey);
- }
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ long[][] valuesArray = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ setGroupByResult(groupByResultHolder, valuesArray[i],
groupKeyArray[i]);
+ }
+ });
}
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[][] valuesArray = blockValSetMap.get(_expression).getLongValuesMV();
- for (int i = 0; i < length; i++) {
- long[] values = valuesArray[i];
- int[] groupKeys = groupKeysArray[i];
- for (int groupKey : groupKeys) {
- setGroupByResult(groupByResultHolder, values, groupKey);
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ long[][] valuesArray = blockValSet.getLongValuesMV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ long[] values = valuesArray[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupByResultHolder, values, groupKey);
+ }
}
- }
+ });
}
private void setGroupByResult(GroupByResultHolder groupByResultHolder,
long[] values, int groupKey) {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/common/SyntheticBlockValSets.java
b/pinot-core/src/test/java/org/apache/pinot/core/common/SyntheticBlockValSets.java
index 76164c81a2f..ddb6dc6c314 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/common/SyntheticBlockValSets.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/common/SyntheticBlockValSets.java
@@ -238,6 +238,44 @@ public class SyntheticBlockValSets {
}
}
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded multi-value
int values.
+ public static class IntMV extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final int[][] _values;
+
+ private IntMV(@Nullable RoaringBitmap nullBitmap, int[][] values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static IntMV create(@Nullable RoaringBitmap nullBitmap, int[][]
values) {
+ return new IntMV(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.INT;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return false;
+ }
+
+ @Override
+ public int[][] getIntValuesMV() {
+ return _values;
+ }
+ }
+
/// A simple [BlockValSet] for nullable, not dictionary-encoded long values.
public static class Long extends Base {
@@ -332,6 +370,82 @@ public class SyntheticBlockValSets {
}
}
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded float values.
+ public static class Float extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final float[] _values;
+
+ private Float(@Nullable RoaringBitmap nullBitmap, float[] values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static Float create(@Nullable RoaringBitmap nullBitmap, float[]
values) {
+ return new Float(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.FLOAT;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return true;
+ }
+
+ @Override
+ public float[] getFloatValuesSV() {
+ return _values;
+ }
+ }
+
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded multi-value
float values.
+ public static class FloatMV extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final float[][] _values;
+
+ private FloatMV(@Nullable RoaringBitmap nullBitmap, float[][] values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static FloatMV create(@Nullable RoaringBitmap nullBitmap, float[][]
values) {
+ return new FloatMV(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.FLOAT;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return false;
+ }
+
+ @Override
+ public float[][] getFloatValuesMV() {
+ return _values;
+ }
+ }
+
/// A simple [BlockValSet] for nullable, not dictionary-encoded double
values.
public static class Double extends Base {
@@ -388,6 +502,124 @@ public class SyntheticBlockValSets {
}
}
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded multi-value
double values.
+ public static class DoubleMV extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final double[][] _values;
+
+ private DoubleMV(@Nullable RoaringBitmap nullBitmap, double[][] values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static DoubleMV create(@Nullable RoaringBitmap nullBitmap,
double[][] values) {
+ return new DoubleMV(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.DOUBLE;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return false;
+ }
+
+ @Override
+ public double[][] getDoubleValuesMV() {
+ return _values;
+ }
+ }
+
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded BigDecimal
values.
+ ///
+ /// Named `BigDec` rather than `BigDecimal` for the same reason [Str] is not
named `String`: a nested class of that
+ /// name shadows [java.math.BigDecimal] across the whole enclosing class,
which silently changes the signature of
+ /// every `getBigDecimal*` method declared here so that it no longer
implements [BlockValSet].
+ public static class BigDec extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final BigDecimal[] _values;
+
+ private BigDec(@Nullable RoaringBitmap nullBitmap, BigDecimal[] values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static BigDec create(@Nullable RoaringBitmap nullBitmap,
BigDecimal[] values) {
+ return new BigDec(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.BIG_DECIMAL;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return true;
+ }
+
+ @Override
+ public BigDecimal[] getBigDecimalValuesSV() {
+ return _values;
+ }
+ }
+
+ /// A simple [BlockValSet] for nullable, not dictionary-encoded multi-value
BigDecimal values.
+ public static class BigDecMV extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final BigDecimal[][] _values;
+
+ private BigDecMV(@Nullable RoaringBitmap nullBitmap, BigDecimal[][]
values) {
+ _nullBitmap = nullBitmap;
+ _values = values;
+ }
+
+ public static BigDecMV create(@Nullable RoaringBitmap nullBitmap,
BigDecimal[][] values) {
+ return new BigDecMV(nullBitmap, values);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return DataType.BIG_DECIMAL;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return false;
+ }
+
+ @Override
+ public BigDecimal[][] getBigDecimalValuesMV() {
+ return _values;
+ }
+ }
+
/// A simple [BlockValSet] for nullable, not dictionary-encoded string
values.
///
/// Named `Str` rather than `String`: a nested class called `String` shadows
`java.lang.String` across
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
index 2a1d77138ef..4e282e463a6 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionNullContractTest.java
@@ -203,7 +203,13 @@ public class AggregationFunctionNullContractTest {
AggregationFunctionType.AVG, AggregationFunctionType.MINMAXRANGE,
AggregationFunctionType.VARPOP,
AggregationFunctionType.STDDEVPOP, AggregationFunctionType.PERCENTILE,
AggregationFunctionType.PERCENTILEEST,
AggregationFunctionType.PERCENTILETDIGEST,
- AggregationFunctionType.PERCENTILEKLL,
AggregationFunctionType.PERCENTILESMARTTDIGEST}) {
+ AggregationFunctionType.PERCENTILEKLL,
AggregationFunctionType.PERCENTILESMARTTDIGEST,
+ // These carry a legacy sentinel with the option disabled - NaN, the
empty point, an all-zero histogram, an
+ // empty id set - so only the enabled answer is NULL, and only the
enabled answer is checked here
+ AggregationFunctionType.SKEWNESS, AggregationFunctionType.KURTOSIS,
AggregationFunctionType.STUNION,
+ AggregationFunctionType.HISTOGRAM, AggregationFunctionType.IDSET,
+ // These two answer NULL in both modes, which is why neither needs a
mode-aware branch
+ AggregationFunctionType.SUMARRAYLONG,
AggregationFunctionType.SUMARRAYDOUBLE}) {
// Built through the shared argument shapes: the percentile families
disagree on whether the percentile is a
// name suffix or an argument, and only some accept both
AggregationFunction function = tryCreate(type, true);
@@ -241,7 +247,9 @@ public class AggregationFunctionNullContractTest {
AggregationFunctionType.PERCENTILETDIGEST,
AggregationFunctionType.PERCENTILERAWTDIGEST,
AggregationFunctionType.PERCENTILESMARTTDIGEST,
AggregationFunctionType.PERCENTILEKLL,
AggregationFunctionType.PERCENTILERAWKLL,
AggregationFunctionType.VARPOP, AggregationFunctionType.VARSAMP,
- AggregationFunctionType.STDDEVPOP, AggregationFunctionType.STDDEVSAMP,
AggregationFunctionType.MINMV,
+ AggregationFunctionType.STDDEVPOP, AggregationFunctionType.STDDEVSAMP,
+ // Both are the same PinotFourthMoment accumulator, so threading the
option into it moves the pair
+ AggregationFunctionType.SKEWNESS, AggregationFunctionType.KURTOSIS,
AggregationFunctionType.MINMV,
AggregationFunctionType.MAXMV, AggregationFunctionType.SUMMV,
AggregationFunctionType.AVGMV,
AggregationFunctionType.MINMAXRANGEMV,
AggregationFunctionType.DISTINCTCOUNTMV,
AggregationFunctionType.DISTINCTSUMMV,
AggregationFunctionType.DISTINCTAVGMV,
@@ -252,6 +260,7 @@ public class AggregationFunctionNullContractTest {
AggregationFunctionType.MINLONG, AggregationFunctionType.MAXLONG,
AggregationFunctionType.SUMINT,
AggregationFunctionType.SUMLONG, AggregationFunctionType.SUMPRECISION,
AggregationFunctionType.FIRSTWITHTIME,
AggregationFunctionType.LASTWITHTIME, AggregationFunctionType.ARRAYAGG,
AggregationFunctionType.LISTAGG,
+ AggregationFunctionType.IDSET, AggregationFunctionType.HISTOGRAM,
// Given the option so they can skip null rows; a row counts only when
both input columns are non-null
AggregationFunctionType.COVARPOP, AggregationFunctionType.COVARSAMP,
// Given the option so they can skip null rows. These two were in this
set once before, on the strength of an
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunctionTest.java
index 1e6371906d1..a114799ec2e 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunctionTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/StUnionAggregationFunctionTest.java
@@ -40,7 +40,7 @@ public class StUnionAggregationFunctionTest {
public void testMergeHandlesGeometryCollectionInputs()
throws Exception {
ExpressionContext expression =
ExpressionContext.forIdentifier("geometryColumn");
- StUnionAggregationFunction aggregationFunction = new
StUnionAggregationFunction(List.of(expression));
+ StUnionAggregationFunction aggregationFunction = new
StUnionAggregationFunction(List.of(expression), false);
Geometry polygon = GeometryUtils.GEOMETRY_WKT_READER.read("POLYGON((0 0, 0
5, 5 5, 5 0, 0 0))");
Geometry line = GeometryUtils.GEOMETRY_WKT_READER.read("LINESTRING(20 20,
25 25)");
Geometry geometryCollection =
@@ -57,7 +57,7 @@ public class StUnionAggregationFunctionTest {
public void testAggregateHandlesMixedDimensionSequence()
throws Exception {
ExpressionContext expression =
ExpressionContext.forIdentifier("geometryColumn");
- StUnionAggregationFunction aggregationFunction = new
StUnionAggregationFunction(List.of(expression));
+ StUnionAggregationFunction aggregationFunction = new
StUnionAggregationFunction(List.of(expression), false);
Geometry polygon = GeometryUtils.GEOMETRY_WKT_READER.read("POLYGON((0 0, 0
5, 5 5, 5 0, 0 0))");
Geometry line = GeometryUtils.GEOMETRY_WKT_READER.read("LINESTRING(10 10,
15 15)");
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/ValueAggregationNullHandlingTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/ValueAggregationNullHandlingTest.java
new file mode 100644
index 00000000000..54d2d187ad5
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/ValueAggregationNullHandlingTest.java
@@ -0,0 +1,453 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.common.SyntheticBlockValSets;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import
org.apache.pinot.core.query.aggregation.function.array.SumArrayDoubleAggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.array.SumArrayLongAggregationFunction;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.apache.pinot.core.query.utils.idset.IdSet;
+import org.apache.pinot.core.query.utils.idset.IdSets;
+import org.apache.pinot.segment.local.customobject.PinotFourthMoment;
+import org.apache.pinot.segment.local.utils.GeometrySerializer;
+import org.apache.pinot.segment.local.utils.GeometryUtils;
+import org.apache.pinot.spi.utils.ByteArray;
+import org.locationtech.jts.geom.Geometry;
+import org.roaringbitmap.RoaringBitmap;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Null handling, multi-value columns and the empty-input answer for the
value-accumulating aggregations:
+/// `SKEWNESS`, `KURTOSIS`, `STUNION`, `SUMARRAYLONG`, `SUMARRAYDOUBLE`,
`HISTOGRAM` and `IDSET`.
+///
+/// [AggregationFunctionNullContractTest] drives one synthetic single-value
block through `aggregate` only, and it
+/// cannot construct most of these from its shared argument shapes at all, so
the group-by paths, the multi-value
+/// paths and the per-mode empty answers are checked nowhere else.
+///
+/// Each of these functions renders "nothing aggregated" differently with the
option disabled — `NaN`, the empty
+/// point, an all-zero histogram, an empty id set, `NULL` — and that sentinel
is a backward-compatibility
+/// constraint, so both modes are asserted rather than only the SQL one.
+public class ValueAggregationNullHandlingTest {
+ private static final ExpressionContext COLUMN =
ExpressionContext.forIdentifier("column");
+ private static final RoaringBitmap ROW1_NULL = RoaringBitmap.bitmapOf(1);
+
+ private static RoaringBitmap allNull(int length) {
+ RoaringBitmap bitmap = new RoaringBitmap();
+ bitmap.add(0L, length);
+ return bitmap;
+ }
+
+ private static Map<ExpressionContext, BlockValSet> block(BlockValSet
blockValSet) {
+ return Map.of(COLUMN, blockValSet);
+ }
+
+ // ---------- SKEWNESS / KURTOSIS ----------
+
+ private static FourthMomentAggregationFunction skewness(boolean
nullHandlingEnabled) {
+ return new FourthMomentAggregationFunction(List.of(COLUMN),
FourthMomentAggregationFunction.Type.SKEWNESS,
+ nullHandlingEnabled);
+ }
+
+ private static PinotFourthMoment
aggregateDoubles(FourthMomentAggregationFunction function, RoaringBitmap
nullBitmap,
+ double[] values) {
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(values.length, holder,
block(SyntheticBlockValSets.Double.create(nullBitmap, values)));
+ return function.extractAggregationResult(holder);
+ }
+
+ /// A null row contributes nothing, so the skewness equals that of the same
block with the null row removed.
+ @Test
+ public void testSkewnessSkipsNullRows() {
+ double[] withNull = {1.0d, 999.0d, 2.0d, 4.0d, 8.0d};
+ double[] without = {1.0d, 2.0d, 4.0d, 8.0d};
+
+ Double skipped = skewness(true).extractFinalResult(
+ aggregateDoubles(skewness(true), RoaringBitmap.bitmapOf(1), withNull));
+ Double expected =
skewness(true).extractFinalResult(aggregateDoubles(skewness(true), null,
without));
+
+ assertNotNull(skipped);
+ assertEquals(skipped, expected);
+ }
+
+ /// With the option enabled the skewness of nothing is NULL; with it
disabled it is the NaN an untouched moment
+ /// renders to, which is the answer that mode has always given.
+ ///
+ /// The two modes need different inputs to reach "nothing aggregated". With
the option enabled an all-null block
+ /// does it, because every row is skipped. With it disabled the null bitmap
is ignored and those same rows are
+ /// aggregated as values, so only a holder that was never touched gets there.
+ @Test
+ public void testSkewnessEmptyAnswerDiffersByMode() {
+ double[] values = {1.0d, 2.0d, 4.0d};
+
+
assertNull(skewness(true).extractFinalResult(aggregateDoubles(skewness(true),
allNull(3), values)));
+
+ FourthMomentAggregationFunction disabled = skewness(false);
+ Double result = disabled.extractFinalResult(
+
disabled.extractAggregationResult(disabled.createAggregationResultHolder()));
+ assertNotNull(result);
+ assertTrue(result.isNaN(), "expected NaN, got " + result);
+ }
+
+ /// With the option disabled a null row is read as the column default and
still aggregated, which is the answer
+ /// that mode has always given.
+ @Test
+ public void testSkewnessCountsNullRowsWhenOptionDisabled() {
+ double[] values = {1.0d, 2.0d, 4.0d};
+
+ Double withBitmap =
skewness(false).extractFinalResult(aggregateDoubles(skewness(false),
allNull(3), values));
+ Double withoutBitmap =
skewness(false).extractFinalResult(aggregateDoubles(skewness(false), null,
values));
+
+ assertNotNull(withBitmap);
+ assertEquals(withBitmap, withoutBitmap);
+ }
+
+ // ---------- STUNION ----------
+
+ private static StUnionAggregationFunction stUnion(boolean
nullHandlingEnabled) {
+ return new StUnionAggregationFunction(List.of(COLUMN),
nullHandlingEnabled);
+ }
+
+ private static Geometry point(double x, double y) {
+ return GeometryUtils.GEOMETRY_FACTORY.createPoint(new
org.locationtech.jts.geom.Coordinate(x, y));
+ }
+
+ private static byte[] serialized(double x, double y) {
+ return GeometrySerializer.serialize(point(x, y));
+ }
+
+ /// Only the geometries of the rows that carry one are unioned.
+ @Test
+ public void testStUnionSkipsNullRows() {
+ byte[][] values = {serialized(0, 0), serialized(5, 5)};
+
+ StUnionAggregationFunction function = stUnion(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(2, holder,
block(SyntheticBlockValSets.Bytes.create(ROW1_NULL, values)));
+
+ assertEquals(function.extractAggregationResult(holder), point(0, 0));
+ }
+
+ /// With the option enabled the union of nothing is NULL; with it disabled
it is the empty point.
+ @Test
+ public void testStUnionEmptyAnswerDiffersByMode() {
+ byte[][] values = {serialized(0, 0), serialized(5, 5)};
+
+ StUnionAggregationFunction enabled = stUnion(true);
+ AggregationResultHolder enabledHolder =
enabled.createAggregationResultHolder();
+ enabled.aggregate(2, enabledHolder,
block(SyntheticBlockValSets.Bytes.create(allNull(2), values)));
+
assertNull(enabled.extractFinalResult(enabled.extractAggregationResult(enabledHolder)));
+
+ StUnionAggregationFunction disabled = stUnion(false);
+ assertEquals(disabled.extractFinalResult(
+
disabled.extractAggregationResult(disabled.createAggregationResultHolder())),
+ new
ByteArray(GeometrySerializer.serialize(GeometryUtils.EMPTY_POINT)));
+ }
+
+ /// Every geometry of a multi-value row is folded into the union.
+ @Test
+ public void testStUnionMVColumnUnionsEveryGeometry() {
+ byte[][][] rows = {
+ {serialized(0, 0), serialized(1, 1)},
+ {serialized(5, 5)}
+ };
+
+ StUnionAggregationFunction function = stUnion(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(2, holder,
block(SyntheticBlockValSets.BytesMV.create(ROW1_NULL, rows)));
+
+ // Row 1 is null, so only the two geometries of row 0 are unioned
+ Geometry result = function.extractAggregationResult(holder);
+ assertNotNull(result);
+ assertEquals(result.getNumGeometries(), 2);
+ assertTrue(result.covers(point(0, 0)));
+ assertTrue(result.covers(point(1, 1)));
+ }
+
+ /// A row's geometries land in that row's group, and a group whose only row
is null is never created.
+ @Test
+ public void testStUnionMVColumnGroupBySV() {
+ byte[][][] rows = {
+ {serialized(0, 0), serialized(1, 1)},
+ {serialized(5, 5)}
+ };
+
+ StUnionAggregationFunction function = stUnion(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupBySV(2, new int[]{0, 1}, holder,
+ block(SyntheticBlockValSets.BytesMV.create(ROW1_NULL, rows)));
+
+ Geometry group0 = function.extractGroupByResult(holder, 0);
+ assertNotNull(group0);
+ assertEquals(group0.getNumGeometries(), 2);
+ assertNull(function.extractGroupByResult(holder, 1));
+ }
+
+ // ---------- SUMARRAYLONG / SUMARRAYDOUBLE ----------
+
+ /// A null row contributes none of its array elements.
+ @Test
+ public void testSumArrayLongSkipsNullRows() {
+ long[][] rows = {{1L, 2L}, {100L, 100L}, {10L, 20L}};
+
+ SumArrayLongAggregationFunction function = new
SumArrayLongAggregationFunction(List.of(COLUMN), true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(3, holder,
block(SyntheticBlockValSets.LongMV.create(ROW1_NULL, rows)));
+
+
assertEquals(function.extractFinalResult(function.extractAggregationResult(holder)),
+ new LongArrayList(new long[]{11L, 22L}));
+ }
+
+ @Test
+ public void testSumArrayDoubleSkipsNullRows() {
+ double[][] rows = {{1.5d, 2.5d}, {100d, 100d}, {10d, 20d}};
+
+ SumArrayDoubleAggregationFunction function = new
SumArrayDoubleAggregationFunction(List.of(COLUMN), true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(3, holder,
block(SyntheticBlockValSets.DoubleMV.create(ROW1_NULL, rows)));
+
+
assertEquals(function.extractFinalResult(function.extractAggregationResult(holder)),
+ new DoubleArrayList(new double[]{11.5d, 22.5d}));
+ }
+
+ /// Unlike the others here, the sum of no arrays is NULL in both modes,
which is why neither needs a mode-aware
+ /// branch in extractFinalResult.
+ @Test
+ public void testSumArrayEmptyAnswerIsNullInBothModes() {
+ long[][] rows = {{1L}, {2L}};
+ for (boolean nullHandlingEnabled : new boolean[]{true, false}) {
+ SumArrayLongAggregationFunction function =
+ new SumArrayLongAggregationFunction(List.of(COLUMN),
nullHandlingEnabled);
+ assertNull(function.extractFinalResult(
+
function.extractAggregationResult(function.createAggregationResultHolder())),
+ "SUM_ARRAY over nothing must be NULL with nullHandlingEnabled=" +
nullHandlingEnabled);
+ if (nullHandlingEnabled) {
+ AggregationResultHolder holder =
function.createAggregationResultHolder();
+ function.aggregate(2, holder,
block(SyntheticBlockValSets.LongMV.create(allNull(2), rows)));
+
assertNull(function.extractFinalResult(function.extractAggregationResult(holder)));
+ }
+ }
+ }
+
+ /// A zero-length block still reaches the range callback, and must not
create the accumulator: doing so would
+ /// turn the empty answer into an empty array instead of NULL.
+ @Test
+ public void testSumArrayZeroLengthBlockLeavesTheHolderUntouched() {
+ long[][] rows = {{1L}, {2L}};
+
+ for (boolean nullHandlingEnabled : new boolean[]{true, false}) {
+ SumArrayLongAggregationFunction function =
+ new SumArrayLongAggregationFunction(List.of(COLUMN),
nullHandlingEnabled);
+ AggregationResultHolder holder =
function.createAggregationResultHolder();
+ function.aggregate(0, holder,
block(SyntheticBlockValSets.LongMV.create(null, rows)));
+ assertNull(function.extractAggregationResult(holder),
+ "a zero-length block must leave the holder untouched with
nullHandlingEnabled=" + nullHandlingEnabled);
+ }
+ }
+
+ // ---------- HISTOGRAM ----------
+
+ /// Two equal-length bins over [0, 10): [0, 5) and [5, 10].
+ private static HistogramAggregationFunction histogram(boolean
nullHandlingEnabled) {
+ return new HistogramAggregationFunction(List.of(COLUMN,
+ ExpressionContext.forLiteral(Literal.doubleValue(0)),
+ ExpressionContext.forLiteral(Literal.doubleValue(10)),
+ ExpressionContext.forLiteral(Literal.intValue(2))),
nullHandlingEnabled);
+ }
+
+ private static DoubleArrayList histogramOf(HistogramAggregationFunction
function, BlockValSet blockValSet,
+ int length) {
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(length, holder, block(blockValSet));
+ return
function.extractFinalResult(function.extractAggregationResult(holder));
+ }
+
+ /// A null row is counted into no bin.
+ @Test
+ public void testHistogramSkipsNullRows() {
+ double[] values = {1.0d, 7.0d, 6.0d};
+
+ assertEquals(histogramOf(histogram(true),
SyntheticBlockValSets.Double.create(ROW1_NULL, values), 3),
+ new DoubleArrayList(new double[]{1.0d, 1.0d}));
+ }
+
+ /// With the option enabled the histogram of nothing is NULL; with it
disabled it is the all-zero histogram.
+ @Test
+ public void testHistogramEmptyAnswerDiffersByMode() {
+ double[] values = {1.0d, 7.0d, 6.0d};
+
+ assertNull(histogramOf(histogram(true),
SyntheticBlockValSets.Double.create(allNull(3), values), 3));
+ assertEquals(histogramOf(histogram(false),
SyntheticBlockValSets.Double.create(allNull(3), values), 0),
+ new DoubleArrayList(new double[]{0.0d, 0.0d}));
+ }
+
+ /// Every value of a multi-value row is counted, which is a path this
function did not support before.
+ @Test
+ public void testHistogramMVColumnCountsEveryValue() {
+ double[][] rows = {{1.0d, 2.0d}, {7.0d}, {6.0d, 9.0d}};
+
+ assertEquals(histogramOf(histogram(true),
SyntheticBlockValSets.DoubleMV.create(ROW1_NULL, rows), 3),
+ new DoubleArrayList(new double[]{2.0d, 2.0d}));
+ }
+
+ /// BIG_DECIMAL is numeric and now lands in the same bins as the other
numeric types, single- and multi-value.
+ @Test
+ public void testHistogramBigDecimalColumn() {
+ BigDecimal[] sv = {new BigDecimal("1.5"), new BigDecimal("99"), new
BigDecimal("6.5")};
+ assertEquals(histogramOf(histogram(true),
SyntheticBlockValSets.BigDec.create(ROW1_NULL, sv), 3),
+ new DoubleArrayList(new double[]{1.0d, 1.0d}));
+
+ BigDecimal[][] mv = {{new BigDecimal("1.5"), new BigDecimal("2.5")}, {new
BigDecimal("99")}};
+ assertEquals(histogramOf(histogram(true),
SyntheticBlockValSets.BigDecMV.create(ROW1_NULL, mv), 2),
+ new DoubleArrayList(new double[]{2.0d, 0.0d}));
+ }
+
+ /// A row's values land in that row's group, and a group whose only row is
null is never created.
+ @Test
+ public void testHistogramMVColumnGroupBySV() {
+ double[][] rows = {{1.0d, 2.0d}, {7.0d}, {6.0d}};
+
+ HistogramAggregationFunction function = histogram(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupBySV(3, new int[]{0, 1, 0}, holder,
+ block(SyntheticBlockValSets.DoubleMV.create(ROW1_NULL, rows)));
+
+ assertEquals(function.extractGroupByResult(holder, 0), new
DoubleArrayList(new double[]{2.0d, 1.0d}));
+ assertNull(function.extractGroupByResult(holder, 1));
+ }
+
+ /// A null row is skipped for every group key it would have fed.
+ @Test
+ public void testHistogramMVColumnGroupByMV() {
+ double[][] rows = {{1.0d, 2.0d}, {7.0d}};
+
+ HistogramAggregationFunction function = histogram(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupByMV(2, new int[][]{{0, 1}, {0, 1}}, holder,
+ block(SyntheticBlockValSets.DoubleMV.create(ROW1_NULL, rows)));
+
+ assertEquals(function.extractGroupByResult(holder, 0), new
DoubleArrayList(new double[]{2.0d, 0.0d}));
+ assertEquals(function.extractGroupByResult(holder, 1), new
DoubleArrayList(new double[]{2.0d, 0.0d}));
+ }
+
+ // ---------- IDSET ----------
+
+ private static IdSetAggregationFunction idSet(boolean nullHandlingEnabled) {
+ return new IdSetAggregationFunction(List.of(COLUMN), nullHandlingEnabled);
+ }
+
+ /// Only the ids of the rows that carry one are collected.
+ @Test
+ public void testIdSetSkipsNullRows() {
+ int[] values = {10, 20, 30};
+
+ IdSetAggregationFunction function = idSet(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(3, holder,
block(SyntheticBlockValSets.Int.create(ROW1_NULL, values)));
+
+ IdSet result = function.extractAggregationResult(holder);
+ assertNotNull(result);
+ assertTrue(result.contains(10));
+ assertTrue(result.contains(30));
+ assertFalse(result.contains(20));
+ }
+
+ /// With the option enabled the id set of nothing is NULL; with it disabled
it is the empty id set.
+ @Test
+ public void testIdSetEmptyAnswerDiffersByMode() {
+ int[] values = {10, 20};
+
+ IdSetAggregationFunction enabled = idSet(true);
+ AggregationResultHolder holder = enabled.createAggregationResultHolder();
+ enabled.aggregate(2, holder,
block(SyntheticBlockValSets.Int.create(allNull(2), values)));
+ assertNull(enabled.extractAggregationResult(holder));
+ assertNull(enabled.extractFinalResult(null));
+
+ IdSetAggregationFunction disabled = idSet(false);
+ IdSet empty =
disabled.extractAggregationResult(disabled.createAggregationResultHolder());
+ assertNotNull(empty);
+ assertEquals(empty.getType(), IdSets.emptyIdSet().getType());
+ }
+
+ /// Every id of a multi-value row is collected.
+ @Test
+ public void testIdSetMVColumnCollectsEveryValue() {
+ int[][] rows = {{10, 11}, {99}, {30}};
+
+ IdSetAggregationFunction function = idSet(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(3, holder,
block(SyntheticBlockValSets.IntMV.create(ROW1_NULL, rows)));
+
+ IdSet result = function.extractAggregationResult(holder);
+ assertNotNull(result);
+ assertTrue(result.contains(10));
+ assertTrue(result.contains(11));
+ assertTrue(result.contains(30));
+ assertFalse(result.contains(99));
+ }
+
+ /// A multi-value BYTES column was rejected before, although IdSets supports
BYTES and the single-value case
+ /// already worked.
+ @Test
+ public void testIdSetMVBytesColumn() {
+ byte[][][] rows = {{{1, 2}, {3, 4}}, {{9, 9}}};
+
+ IdSetAggregationFunction function = idSet(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(2, holder,
block(SyntheticBlockValSets.BytesMV.create(ROW1_NULL, rows)));
+
+ IdSet result = function.extractAggregationResult(holder);
+ assertNotNull(result);
+ assertTrue(result.contains(new byte[]{1, 2}));
+ assertTrue(result.contains(new byte[]{3, 4}));
+ assertFalse(result.contains(new byte[]{9, 9}));
+ }
+
+ /// A row's ids land in that row's group, and a group whose only row is null
is never created.
+ @Test
+ public void testIdSetMVColumnGroupBySV() {
+ int[][] rows = {{10, 11}, {99}, {30}};
+
+ IdSetAggregationFunction function = idSet(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupBySV(3, new int[]{0, 1, 0}, holder,
+ block(SyntheticBlockValSets.IntMV.create(ROW1_NULL, rows)));
+
+ IdSet group0 = function.extractGroupByResult(holder, 0);
+ assertNotNull(group0);
+ assertTrue(group0.contains(10));
+ assertTrue(group0.contains(30));
+ assertNull(function.extractGroupByResult(holder, 1));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]