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 59c2883cf02 Give the funnel functions the query's null handling option
(#19332)
59c2883cf02 is described below
commit 59c2883cf021755684c9d4bc62d2e1179e150490
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Aug 21 16:08:28 2026 -0700
Give the funnel functions the query's null handling option (#19332)
---
.../aggregation/function/AggregationFunction.java | 62 +++---
.../function/AggregationFunctionFactory.java | 12 +-
.../function/HistogramAggregationFunction.java | 222 ++++++++++----------
.../function/funnel/AggregationStrategy.java | 158 +++++++++-----
.../function/funnel/BitmapAggregationStrategy.java | 4 +-
.../FunnelCountAggregationFunctionFactory.java | 12 +-
.../FunnelCountSortedAggregationFunction.java | 5 +-
.../function/funnel/SortedAggregationStrategy.java | 4 +-
.../funnel/ThetaSketchAggregationStrategy.java | 4 +-
.../window/FunnelBaseAggregationFunction.java | 134 +++++++-----
.../FunnelCompleteCountAggregationFunction.java | 4 +-
...unnelEventsFunctionEvalAggregationFunction.java | 151 ++++++++-----
.../window/FunnelMatchStepAggregationFunction.java | 4 +-
.../window/FunnelMaxStepAggregationFunction.java | 4 +-
...FunnelStepDurationStatsAggregationFunction.java | 4 +-
.../pinot/core/common/SyntheticBlockValSets.java | 69 +++++-
.../AggregationFunctionNullContractTest.java | 93 ++++++--
.../function/funnel/FunnelNullHandlingTest.java | 233 +++++++++++++++++++++
18 files changed, 840 insertions(+), 339 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunction.java
index b95b34a36f2..79ca2736e8a 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunction.java
@@ -64,9 +64,9 @@ import org.apache.pinot.segment.spi.AggregationFunctionType;
/// matching value was null. It carries no per-function meaning, which is
what makes it correct for the aggregation
/// methods to skip null rows outright rather than fold them in.
/// `null` is not the only way that state is represented, though: an empty
accumulator — an empty set, value list,
-/// digest, sketch or map — means the same thing, and still arrives. A peer
that serialized one sends it, and so do
-/// the functions in the first deviation below, which never receive the
option and substitute one rather than
-/// returning `null`. So [#extractFinalResult] accepts both.
+/// digest, sketch or map — means the same thing, and still arrives. A peer
that serialized one sends it, and so
+/// does any function whose disabled-mode answer is an empty accumulator
rather than a constant, since it
+/// substitutes one in that mode to keep the answer it has always given. So
[#extractFinalResult] accepts both.
/// `null` is the representation to produce where there is a choice, because
it is the only one always available:
/// `MAXSTRING`, `MINSTRING` and `ANYVALUE` are object-backed and have no
empty value to substitute.
/// - [#extractFinalResult] decides what that means for this aggregation, and
is the only method that does. `COUNT`
@@ -95,37 +95,31 @@ import org.apache.pinot.segment.spi.AggregationFunctionType;
/// these is therefore a behavioural change and not a documentation fix,
because it widens what the rest of the
/// engine must carry.
///
-/// TODO: Known deviations from the above.
-/// 1. Several aggregation functions never receive the query's null handling
option at all, so they cannot skip
-/// null rows and fold the column's default value into the aggregate
whatever the query asked for: the
-/// sketch-backed distinct counts (`DISTINCTCOUNTBITMAP`,
`DISTINCTCOUNTHLL`, `DISTINCTCOUNTTHETASKETCH`,
-/// `DISTINCTCOUNTCPCSKETCH`, `FASTHLL`,
`SEGMENTPARTITIONEDDISTINCTCOUNT` and the raw and smart variants of
-/// each), `HISTOGRAM`, `IDSET`, `STUNION`, the array sums, and the
funnel family. Whether a function takes the
-/// option is visible at its construction site, which is the reliable way
to tell.
-/// The family names are not a safe shorthand for this, in either
direction. The exact distinct functions
-/// (`DISTINCTCOUNT`, `DISTINCTSUM`, `DISTINCTAVG`,
`DISTINCTCOUNTOFFHEAP`) do take the option and skip null
-/// rows through their shared base, as do the variance,
standard-deviation and covariance functions and the
-/// first/last-with-time functions, so "the distinct-count family" and
"the statistical functions" both include
-/// members that honour the option and members that cannot.
-/// These same functions also substitute an empty accumulator in
[#extractAggregationResult],
-/// [#extractGroupByResult] or both, rather than returning `null` and
letting [#extractFinalResult] render the
-/// disabled-mode value. That is not a separate defect: without the
option [#extractFinalResult] cannot tell the
-/// two modes apart, so it has nothing to decide with and the
substitution is the only thing holding the answer.
-/// Which of the two paths substitutes is inconsistent across them and
sometimes within one, so such a function
-/// can answer differently depending on whether the query groups — a
filtered aggregation makes that observable,
-/// since one group key space is shared by every aggregation in a query
and a group created by one of them can
-/// hold no rows for another.
-/// Conforming them alongside the option is more than moving a branch.
The value to preserve is often not a
-/// constant, and the function that substitutes is frequently not the one
that renders: the raw variants
-/// delegate extraction to the plain function and serialize what it
returns, so conforming the plain function
-/// alone changes the raw variant's answer from a serialized empty sketch
to `NULL` without touching the raw
-/// variant at all.
-/// 2. The multi-stage engine constructs every aggregation function with
null handling enabled and never consults the
-/// query's null handling option, so a query that disables it still gets
enabled-mode semantics there. The two
-/// engines can therefore answer the same query differently: with null
handling disabled, `SUM` over a query
-/// whose segments are all pruned is `NULL` on the multi-stage engine and
`0` on the single-stage engine. This
-/// may be intended, the multi-stage engine being the SQL-conformant one,
but it means the mode described above
-/// is not actually per-query everywhere.
+/// ### Multi-input functions
+///
+/// A function that reads more than one column decides for itself which of
them a `null` disqualifies the row on,
+/// because that follows from what each column is for rather than from a
single rule. The covariances skip a row
+/// unless both value columns are non-null. A funnel skips a row whose
timestamp or correlation key is null, since
+/// neither an event with no position in the window nor a row belonging to no
key can contribute, but it does not
+/// skip a row for a null step: a step expression is a predicate, and a
predicate over a null operand is UNKNOWN,
+/// which SQL treats as not satisfied wherever a boolean is consumed, so a
null step already means that step did not
+/// match.
+///
+/// ### Functions outside this contract
+///
+/// Every user-facing aggregation receives the option. Three do not, and none
of them is an aggregate a query can
+/// ask for by name: `ParentExprMinMax` and `ChildExprMinMax` are produced by
the query rewriter rather than written
+/// by users, and `TimeSeriesAggregationFunction` is built from time-series
plan context rather than from a
+/// [org.apache.pinot.common.request.context.FunctionContext]. Whether a
function takes the option is visible at its
+/// construction site in [AggregationFunctionFactory], which is the reliable
way to tell.
+///
+/// TODO: Known deviation from the above.
+/// The multi-stage engine constructs every aggregation function with null
handling enabled and never consults the
+/// query's null handling option, so a query that disables it still gets
enabled-mode semantics there. The two
+/// engines can therefore answer the same query differently: with null
handling disabled, `SUM` over a query
+/// whose segments are all pruned is `NULL` on the multi-stage engine and
`0` on the single-stage engine. This
+/// may be intended, the multi-stage engine being the SQL-conformant one,
but it means the mode described above
+/// is not actually per-query everywhere.
///
/// @param <IntermediateResult> Intermediate result generated from segment
/// @param <FinalResult> Final result used in broker response
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 87b97a297e7..4c4165edbdb 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
@@ -511,17 +511,17 @@ public class AggregationFunctionFactory {
throw new IllegalArgumentException(
"Aggregation function: " + functionType + " is only supported
in selection without alias.");
case FUNNELCOUNT:
- return new FunnelCountAggregationFunctionFactory(arguments).get();
+ return new FunnelCountAggregationFunctionFactory(arguments,
nullHandlingEnabled).get();
case FUNNELMAXSTEP:
- return new FunnelMaxStepAggregationFunction(arguments);
+ return new FunnelMaxStepAggregationFunction(arguments,
nullHandlingEnabled);
case FUNNELMATCHSTEP:
- return new FunnelMatchStepAggregationFunction(arguments);
+ return new FunnelMatchStepAggregationFunction(arguments,
nullHandlingEnabled);
case FUNNELCOMPLETECOUNT:
- return new FunnelCompleteCountAggregationFunction(arguments);
+ return new FunnelCompleteCountAggregationFunction(arguments,
nullHandlingEnabled);
case FUNNELSTEPDURATIONSTATS:
- return new FunnelStepDurationStatsAggregationFunction(arguments);
+ return new FunnelStepDurationStatsAggregationFunction(arguments,
nullHandlingEnabled);
case FUNNELEVENTSFUNCTIONEVAL:
- return new FunnelEventsFunctionEvalAggregationFunction(arguments);
+ return new FunnelEventsFunctionEvalAggregationFunction(arguments,
nullHandlingEnabled);
case FREQUENTSTRINGSSKETCH:
return new FrequentStringsSketchAggregationFunction(arguments,
nullHandlingEnabled);
case FREQUENTLONGSSKETCH:
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 80830edcd92..6ca0f29891b 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
@@ -259,76 +259,67 @@ public class HistogramAggregationFunction
}
@Override
- public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
if (blockValSet.isSingleValue()) {
- aggregateSVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ aggregateSV(length, aggregationResultHolder, blockValSet);
} else {
- aggregateMVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
+ aggregateMV(length, aggregationResultHolder, blockValSet);
}
}
- private void aggregateSVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
- BlockValSet 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();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, values[i]);
}
+ return acum + to - from;
});
break;
}
case LONG: {
long[] values = blockValSet.getLongValuesSV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, values[i]);
}
+ return acum + to - from;
});
break;
}
case FLOAT: {
float[] values = blockValSet.getFloatValuesSV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, values[i]);
}
+ return acum + to - from;
});
break;
}
case DOUBLE: {
double[] values = blockValSet.getDoubleValuesSV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, values[i]);
}
+ return acum + to - from;
});
break;
}
case BIG_DECIMAL: {
BigDecimal[] values = blockValSet.getBigDecimalValuesSV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, values[i].doubleValue());
}
+ return acum + to - from;
});
break;
}
@@ -336,73 +327,75 @@ public class HistogramAggregationFunction
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 aggregateMVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
- BlockValSet blockValSet) {
+ 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();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, value);
}
}
+ return acum + to - from;
});
break;
}
case LONG: {
long[][] values = blockValSet.getLongValuesMV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, value);
}
}
+ return acum + to - from;
});
break;
}
case FLOAT: {
float[][] values = blockValSet.getFloatValuesMV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, value);
}
}
+ return acum + to - from;
});
break;
}
case DOUBLE: {
double[][] values = blockValSet.getDoubleValuesMV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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);
- }
+ increment(histogram, value);
}
}
+ return acum + to - from;
});
break;
}
case BIG_DECIMAL: {
BigDecimal[][] values = blockValSet.getBigDecimalValuesMV();
- forEachNotNull(length, blockValSet, (from, to) -> {
+ numRows = foldNotNull(length, blockValSet, 0, (acum, 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());
- }
+ increment(histogram, value.doubleValue());
}
}
+ return acum + to - from;
});
break;
}
@@ -410,6 +403,9 @@ public class HistogramAggregationFunction
throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ blockValSet.getValueType());
}
+ if (numRows > 0) {
+ setAggregationResult(aggregationResultHolder, histogram);
+ }
}
@Override
@@ -546,80 +542,77 @@ public class HistogramAggregationFunction
}
}
- protected void setGroupByResult(int groupKey, GroupByResultHolder
groupByResultHolder, double val) {
- int binID = getBinId(val);
- DoubleArrayList byResultHolderResult =
groupByResultHolder.getResult(groupKey);
- if (byResultHolderResult == null) {
- byResultHolderResult =
DoubleVectorOpUtils.createAndInitialize(getNumBins());
- groupByResultHolder.setValueForKey(groupKey, byResultHolderResult);
- }
- if (binID != INVALID_BIN) {
- DoubleVectorOpUtils.incrementElementByOne(byResultHolderResult, binID);
- }
- }
-
@Override
- public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
+ public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
BlockValSet blockValSet = blockValSetMap.get(_expression);
if (blockValSet.isSingleValue()) {
- aggregateSV(length, aggregationResultHolder, blockValSet);
+ aggregateSVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
} else {
- aggregateMV(length, aggregationResultHolder, blockValSet);
+ aggregateMVGroupByMV(length, groupKeysArray, groupByResultHolder,
blockValSet);
}
}
- private void aggregateSV(int length, AggregationResultHolder
aggregationResultHolder, BlockValSet blockValSet) {
- double[] histogram = new double[getNumBins()];
- int numRows;
+ private void aggregateSVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
switch (blockValSet.getValueType().getStoredType()) {
case INT: {
int[] values = blockValSet.getIntValuesSV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
- increment(histogram, values[i]);
+ int value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- return acum + to - from;
});
break;
}
case LONG: {
long[] values = blockValSet.getLongValuesSV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
- increment(histogram, values[i]);
+ long value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- return acum + to - from;
});
break;
}
case FLOAT: {
float[] values = blockValSet.getFloatValuesSV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
- increment(histogram, values[i]);
+ float value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- return acum + to - from;
});
break;
}
case DOUBLE: {
double[] values = blockValSet.getDoubleValuesSV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
- increment(histogram, values[i]);
+ double value = values[i];
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- return acum + to - from;
});
break;
}
case BIG_DECIMAL: {
BigDecimal[] values = blockValSet.getBigDecimalValuesSV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
- increment(histogram, values[i].doubleValue());
+ double value = values[i].doubleValue();
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
- return acum + to - from;
});
break;
}
@@ -627,75 +620,73 @@ public class HistogramAggregationFunction
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;
+ private void aggregateMVGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ BlockValSet blockValSet) {
switch (blockValSet.getValueType().getStoredType()) {
case INT: {
int[][] values = blockValSet.getIntValuesMV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
for (int value : values[i]) {
- increment(histogram, value);
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
}
- return acum + to - from;
});
break;
}
case LONG: {
long[][] values = blockValSet.getLongValuesMV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
for (long value : values[i]) {
- increment(histogram, value);
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
}
- return acum + to - from;
});
break;
}
case FLOAT: {
float[][] values = blockValSet.getFloatValuesMV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
for (float value : values[i]) {
- increment(histogram, value);
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
}
- return acum + to - from;
});
break;
}
case DOUBLE: {
double[][] values = blockValSet.getDoubleValuesMV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
for (double value : values[i]) {
- increment(histogram, value);
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder, value);
+ }
}
}
- return acum + to - from;
});
break;
}
case BIG_DECIMAL: {
BigDecimal[][] values = blockValSet.getBigDecimalValuesMV();
- numRows = foldNotNull(length, blockValSet, 0, (acum, from, to) -> {
+ forEachNotNull(length, blockValSet, (from, to) -> {
for (int i = from; i < to && i < values.length; i++) {
for (BigDecimal value : values[i]) {
- increment(histogram, value.doubleValue());
+ for (int groupKey : groupKeysArray[i]) {
+ setGroupByResult(groupKey, groupByResultHolder,
value.doubleValue());
+ }
}
}
- return acum + to - from;
});
break;
}
@@ -703,8 +694,17 @@ public class HistogramAggregationFunction
throw new IllegalStateException("Cannot compute histogram for
non-numeric type: "
+ blockValSet.getValueType());
}
- if (numRows > 0) {
- setAggregationResult(aggregationResultHolder, histogram);
+ }
+
+ protected void setGroupByResult(int groupKey, GroupByResultHolder
groupByResultHolder, double val) {
+ int binID = getBinId(val);
+ DoubleArrayList byResultHolderResult =
groupByResultHolder.getResult(groupKey);
+ if (byResultHolderResult == null) {
+ byResultHolderResult =
DoubleVectorOpUtils.createAndInitialize(getNumBins());
+ groupByResultHolder.setValueForKey(groupKey, byResultHolderResult);
+ }
+ if (binID != INVALID_BIN) {
+ DoubleVectorOpUtils.incrementElementByOne(byResultHolderResult, binID);
}
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/AggregationStrategy.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/AggregationStrategy.java
index 1de0f923b89..5cf401c333b 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/AggregationStrategy.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/AggregationStrategy.java
@@ -21,12 +21,15 @@ package
org.apache.pinot.core.query.aggregation.function.funnel;
import com.google.common.base.Preconditions;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.RoaringBitmapUtils;
import org.apache.pinot.core.common.BlockValSet;
import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.roaringbitmap.RoaringBitmap;
/// Interface for within segment aggregation strategy.
@@ -49,7 +52,11 @@ public abstract class AggregationStrategy<A> {
private final List<ExpressionContext> _correlateByExpressions;
private final ExpressionContext _primaryCorrelationCol;
- public AggregationStrategy(List<ExpressionContext> stepExpressions,
List<ExpressionContext> correlateByExpressions) {
+ protected final boolean _nullHandlingEnabled;
+
+ public AggregationStrategy(List<ExpressionContext> stepExpressions,
List<ExpressionContext> correlateByExpressions,
+ boolean nullHandlingEnabled) {
+ _nullHandlingEnabled = nullHandlingEnabled;
_stepExpressions = stepExpressions;
_correlateByExpressions = correlateByExpressions;
_primaryCorrelationCol = _correlateByExpressions.get(0);
@@ -117,13 +124,15 @@ public abstract class AggregationStrategy<A> {
final Dictionary dictionary = getPrimaryDictionary(blockValSetMap);
final int[] correlationIds = getPrimaryCorrelationIds(blockValSetMap);
final A aggResult = getAggregationResult(dictionary,
aggregationResultHolder);
- for (int i = 0; i < length; i++) {
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- add(dictionary, aggResult, n, correlationIds[i]);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ add(dictionary, aggResult, n, correlationIds[i]);
+ }
}
}
- }
+ });
}
private void aggregateMultiKey(int length, AggregationResultHolder
aggregationResultHolder,
@@ -132,16 +141,18 @@ public abstract class AggregationStrategy<A> {
final int[][] allCorrelationIds = getAllCorrelationDictIds(blockValSetMap);
final A aggResult = getAggregationResultMultiKey(dictionaries,
aggregationResultHolder);
final int[] rowDictIds = new int[_numCorrelateByKeys];
- for (int i = 0; i < length; i++) {
- for (int k = 0; k < _numCorrelateByKeys; k++) {
- rowDictIds[k] = allCorrelationIds[k][i];
- }
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int k = 0; k < _numCorrelateByKeys; k++) {
+ rowDictIds[k] = allCorrelationIds[k][i];
+ }
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ }
}
}
- }
+ });
}
/// Performs aggregation on the given group key array and block value sets
(aggregation group-by on single-value
@@ -160,15 +171,17 @@ public abstract class AggregationStrategy<A> {
Map<ExpressionContext, BlockValSet> blockValSetMap, int[][] steps) {
final Dictionary dictionary = getPrimaryDictionary(blockValSetMap);
final int[] correlationIds = getPrimaryCorrelationIds(blockValSetMap);
- for (int i = 0; i < length; i++) {
- final int groupKey = groupKeyArray[i];
- final A aggResult = getAggregationResultGroupBy(dictionary,
groupByResultHolder, groupKey);
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- add(dictionary, aggResult, n, correlationIds[i]);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ final int groupKey = groupKeyArray[i];
+ final A aggResult = getAggregationResultGroupBy(dictionary,
groupByResultHolder, groupKey);
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ add(dictionary, aggResult, n, correlationIds[i]);
+ }
}
}
- }
+ });
}
private void aggregateGroupBySVMultiKey(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
@@ -176,18 +189,20 @@ public abstract class AggregationStrategy<A> {
final Dictionary[] dictionaries = getAllDictionaries(blockValSetMap);
final int[][] allCorrelationIds = getAllCorrelationDictIds(blockValSetMap);
final int[] rowDictIds = new int[_numCorrelateByKeys];
- for (int i = 0; i < length; i++) {
- for (int k = 0; k < _numCorrelateByKeys; k++) {
- rowDictIds[k] = allCorrelationIds[k][i];
- }
- final int groupKey = groupKeyArray[i];
- final A aggResult = getAggregationResultGroupByMultiKey(dictionaries,
groupByResultHolder, groupKey);
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int k = 0; k < _numCorrelateByKeys; k++) {
+ rowDictIds[k] = allCorrelationIds[k][i];
+ }
+ final int groupKey = groupKeyArray[i];
+ final A aggResult = getAggregationResultGroupByMultiKey(dictionaries,
groupByResultHolder, groupKey);
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ }
}
}
- }
+ });
}
/// Performs aggregation on the given group keys array and block value sets
(aggregation group-by on multi-value
@@ -206,16 +221,18 @@ public abstract class AggregationStrategy<A> {
GroupByResultHolder groupByResultHolder, Map<ExpressionContext,
BlockValSet> blockValSetMap, int[][] steps) {
final Dictionary dictionary = getPrimaryDictionary(blockValSetMap);
final int[] correlationIds = getPrimaryCorrelationIds(blockValSetMap);
- for (int i = 0; i < length; i++) {
- for (int groupKey : groupKeysArray[i]) {
- final A aggResult = getAggregationResultGroupBy(dictionary,
groupByResultHolder, groupKey);
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- add(dictionary, aggResult, n, correlationIds[i]);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int groupKey : groupKeysArray[i]) {
+ final A aggResult = getAggregationResultGroupBy(dictionary,
groupByResultHolder, groupKey);
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ add(dictionary, aggResult, n, correlationIds[i]);
+ }
}
}
}
- }
+ });
}
private void aggregateGroupByMVMultiKey(int length, int[][] groupKeysArray,
@@ -223,19 +240,21 @@ public abstract class AggregationStrategy<A> {
final Dictionary[] dictionaries = getAllDictionaries(blockValSetMap);
final int[][] allCorrelationIds = getAllCorrelationDictIds(blockValSetMap);
final int[] rowDictIds = new int[_numCorrelateByKeys];
- for (int i = 0; i < length; i++) {
- for (int k = 0; k < _numCorrelateByKeys; k++) {
- rowDictIds[k] = allCorrelationIds[k][i];
- }
- for (int groupKey : groupKeysArray[i]) {
- final A aggResult = getAggregationResultGroupByMultiKey(dictionaries,
groupByResultHolder, groupKey);
- for (int n = 0; n < _numSteps; n++) {
- if (steps[n][i] > 0) {
- addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ forEachNotNullCorrelation(length, blockValSetMap, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ for (int k = 0; k < _numCorrelateByKeys; k++) {
+ rowDictIds[k] = allCorrelationIds[k][i];
+ }
+ for (int groupKey : groupKeysArray[i]) {
+ final A aggResult =
getAggregationResultGroupByMultiKey(dictionaries, groupByResultHolder,
groupKey);
+ for (int n = 0; n < _numSteps; n++) {
+ if (steps[n][i] > 0) {
+ addMultiKey(aggResult, n, dictionaries, rowDictIds);
+ }
}
}
}
- }
+ });
}
/// Adds a correlation id to the aggregation counter for a given step in the
funnel.
@@ -282,6 +301,49 @@ public abstract class AggregationStrategy<A> {
return allIds;
}
+ /// Runs the consumer over each range of rows whose correlation key is
entirely non-null, or over the whole block
+ /// when the option is disabled.
+ ///
+ /// Only the correlation key is consulted. A step expression is a predicate,
and a predicate over a null operand is
+ /// UNKNOWN, which SQL treats as not satisfied wherever a boolean is
consumed, so a null step already means that
+ /// step did not match. The correlation key is what the funnel counts
distinct values of, and a row whose key is
+ /// null belongs to no key at all; its dictionary id would otherwise be the
default's, counted as a real one. With
+ /// a composite key a null in any component leaves the whole key undefined,
so the row is skipped.
+ private void forEachNotNullCorrelation(int length, Map<ExpressionContext,
BlockValSet> blockValSetMap,
+ RoaringBitmapUtils.BatchConsumer consumer) {
+ RoaringBitmap nullBitmap = correlationNullBitmap(blockValSetMap);
+ if (nullBitmap == null) {
+ consumer.consume(0, length);
+ return;
+ }
+ // Skip if the entire block is null
+ if (!nullBitmap.contains(0, length)) {
+ RoaringBitmapUtils.forEachUnset(length, nullBitmap.getIntIterator(),
consumer);
+ }
+ }
+
+ /// Returns the union of the correlation columns' null bitmaps, or `null`
when no row is null.
+ @Nullable
+ private RoaringBitmap correlationNullBitmap(Map<ExpressionContext,
BlockValSet> blockValSetMap) {
+ if (!_nullHandlingEnabled) {
+ return null;
+ }
+ RoaringBitmap merged = null;
+ for (ExpressionContext correlateByExpression : _correlateByExpressions) {
+ RoaringBitmap nullBitmap =
blockValSetMap.get(correlateByExpression).getNullBitmap();
+ if (nullBitmap == null) {
+ continue;
+ }
+ // Copied before merging: the bitmap belongs to the block and must not
be mutated
+ if (merged == null) {
+ merged = nullBitmap.clone();
+ } else {
+ merged.or(nullBitmap);
+ }
+ }
+ return merged;
+ }
+
private int[][] getSteps(Map<ExpressionContext, BlockValSet> blockValSetMap)
{
final int[][] steps = new int[_numSteps][];
for (int n = 0; n < _numSteps; n++) {
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/BitmapAggregationStrategy.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/BitmapAggregationStrategy.java
index da648e13f39..3f76e8cdbe9 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/BitmapAggregationStrategy.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/BitmapAggregationStrategy.java
@@ -26,8 +26,8 @@ import org.apache.pinot.segment.spi.index.reader.Dictionary;
/// Aggregation strategy leveraging roaring bitmap algebra
(unions/intersections).
class BitmapAggregationStrategy extends AggregationStrategy<DictIdsWrapper> {
public BitmapAggregationStrategy(List<ExpressionContext> stepExpressions,
- List<ExpressionContext> correlateByExpressions) {
- super(stepExpressions, correlateByExpressions);
+ List<ExpressionContext> correlateByExpressions, boolean
nullHandlingEnabled) {
+ super(stepExpressions, correlateByExpressions, nullHandlingEnabled);
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountAggregationFunctionFactory.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountAggregationFunctionFactory.java
index 30b84f40b00..d59a610c94d 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountAggregationFunctionFactory.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountAggregationFunctionFactory.java
@@ -62,7 +62,10 @@ public class FunnelCountAggregationFunctionFactory
implements Supplier<Aggregati
final boolean _thetaSketchSetting;
final boolean _setSetting;
- public FunnelCountAggregationFunctionFactory(List<ExpressionContext>
expressions) {
+ private final boolean _nullHandlingEnabled;
+
+ public FunnelCountAggregationFunctionFactory(List<ExpressionContext>
expressions, boolean nullHandlingEnabled) {
+ _nullHandlingEnabled = nullHandlingEnabled;
_expressions = expressions;
Option.validate(expressions);
_correlateByExpressions =
Option.CORRELATE_BY.getInputExpressions(expressions);
@@ -120,7 +123,7 @@ public class FunnelCountAggregationFunctionFactory
implements Supplier<Aggregati
MergeStrategy<List<Long>> mergeStrategy) {
if (_sortingSetting) {
return new FunnelCountSortedAggregationFunction<>(_expressions,
_stepExpressions, _correlateByExpressions,
- aggregationStrategy, resultExtractionStrategy, mergeStrategy);
+ aggregationStrategy, resultExtractionStrategy, mergeStrategy,
_nullHandlingEnabled);
} else {
return new FunnelCountAggregationFunction<>(_expressions,
_stepExpressions, _correlateByExpressions,
aggregationStrategy, resultExtractionStrategy, mergeStrategy);
@@ -128,11 +131,12 @@ public class FunnelCountAggregationFunctionFactory
implements Supplier<Aggregati
}
AggregationStrategy<UpdatableThetaSketch[]> thetaSketchAggregationStrategy()
{
- return new ThetaSketchAggregationStrategy(_stepExpressions,
_correlateByExpressions, _nominalEntries);
+ return new ThetaSketchAggregationStrategy(_stepExpressions,
_correlateByExpressions, _nominalEntries,
+ _nullHandlingEnabled);
}
AggregationStrategy<DictIdsWrapper> bitmapAggregationStrategy() {
- return new BitmapAggregationStrategy(_stepExpressions,
_correlateByExpressions);
+ return new BitmapAggregationStrategy(_stepExpressions,
_correlateByExpressions, _nullHandlingEnabled);
}
MergeStrategy<List<ThetaSketch>> thetaSketchMergeStrategy() {
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountSortedAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountSortedAggregationFunction.java
index 4a766e0ad4f..df4fc17b9a3 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountSortedAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelCountSortedAggregationFunction.java
@@ -54,10 +54,11 @@ public class FunnelCountSortedAggregationFunction<A>
extends FunnelCountAggregat
public FunnelCountSortedAggregationFunction(List<ExpressionContext>
expressions,
List<ExpressionContext> stepExpressions, List<ExpressionContext>
correlateByExpressions,
AggregationStrategy<A> aggregationStrategy, ResultExtractionStrategy<A,
List<Long>> resultExtractionStrategy,
- MergeStrategy<List<Long>> mergeStrategy) {
+ MergeStrategy<List<Long>> mergeStrategy, boolean nullHandlingEnabled) {
super(expressions, stepExpressions, correlateByExpressions,
aggregationStrategy, resultExtractionStrategy,
mergeStrategy);
- _sortedAggregationStrategy = new
SortedAggregationStrategy(stepExpressions, correlateByExpressions);
+ _sortedAggregationStrategy =
+ new SortedAggregationStrategy(stepExpressions, correlateByExpressions,
nullHandlingEnabled);
_sortedResultExtractionStrategy = SortedAggregationResult::extractResult;
_primaryCorrelationCol = correlateByExpressions.get(0);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/SortedAggregationStrategy.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/SortedAggregationStrategy.java
index 154e84837fa..7e423a10949 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/SortedAggregationStrategy.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/SortedAggregationStrategy.java
@@ -28,8 +28,8 @@ import org.apache.pinot.segment.spi.index.reader.Dictionary;
/// keys are handled within each primary-key group by
[SortedAggregationResult].
class SortedAggregationStrategy extends
AggregationStrategy<SortedAggregationResult> {
public SortedAggregationStrategy(List<ExpressionContext> stepExpressions,
- List<ExpressionContext> correlateByExpressions) {
- super(stepExpressions, correlateByExpressions);
+ List<ExpressionContext> correlateByExpressions, boolean
nullHandlingEnabled) {
+ super(stepExpressions, correlateByExpressions, nullHandlingEnabled);
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/ThetaSketchAggregationStrategy.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/ThetaSketchAggregationStrategy.java
index 406c9db6617..23cc4043e60 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/ThetaSketchAggregationStrategy.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/ThetaSketchAggregationStrategy.java
@@ -30,8 +30,8 @@ class ThetaSketchAggregationStrategy extends
AggregationStrategy<UpdatableThetaS
final UpdatableThetaSketchBuilder _updateSketchBuilder;
public ThetaSketchAggregationStrategy(List<ExpressionContext>
stepExpressions,
- List<ExpressionContext> correlateByExpressions, int nominalEntries) {
- super(stepExpressions, correlateByExpressions);
+ List<ExpressionContext> correlateByExpressions, int nominalEntries,
boolean nullHandlingEnabled) {
+ super(stepExpressions, correlateByExpressions, nullHandlingEnabled);
_updateSketchBuilder = new
UpdatableThetaSketchBuilder().setNominalEntries(nominalEntries);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelBaseAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelBaseAggregationFunction.java
index f63350b0305..243cf4a71d8 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelBaseAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelBaseAggregationFunction.java
@@ -30,6 +30,7 @@ import javax.annotation.Nullable;
import org.apache.pinot.common.CustomObject;
import org.apache.pinot.common.request.context.ExpressionContext;
import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.common.utils.RoaringBitmapUtils;
import org.apache.pinot.core.common.BlockValSet;
import org.apache.pinot.core.common.ObjectSerDeUtils;
import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
@@ -39,10 +40,12 @@ import
org.apache.pinot.core.query.aggregation.function.funnel.FunnelStepEvent;
import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
import
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
import org.apache.pinot.spi.query.QueryThreadContext;
+import org.roaringbitmap.RoaringBitmap;
public abstract class FunnelBaseAggregationFunction<F extends Comparable>
implements AggregationFunction<PriorityQueue<FunnelStepEvent>, F> {
+ protected final boolean _nullHandlingEnabled;
protected final ExpressionContext _timestampExpression;
protected final long _windowSize;
protected final List<ExpressionContext> _stepExpressions;
@@ -51,7 +54,8 @@ public abstract class FunnelBaseAggregationFunction<F extends
Comparable>
protected long _maxStepDuration = 0L;
protected final Map<String, String> _extraArguments = new HashMap<>();
- public FunnelBaseAggregationFunction(List<ExpressionContext> arguments) {
+ public FunnelBaseAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ _nullHandlingEnabled = nullHandlingEnabled;
int numArguments = arguments.size();
Preconditions.checkArgument(numArguments > 3,
"FUNNEL_AGG_FUNC expects >= 4 arguments, got: %s. The function can be
used as "
@@ -121,91 +125,121 @@ public abstract class FunnelBaseAggregationFunction<F
extends Comparable>
return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
}
+ /// Runs the consumer over each range of rows whose timestamp is not null,
or over the whole block when the
+ /// option is disabled.
+ ///
+ /// Only the timestamp is consulted. A step expression is a predicate, and a
predicate over a null operand is
+ /// UNKNOWN, which SQL treats as not satisfied wherever a boolean is
consumed, so a null step already means that
+ /// step did not match and the row still belongs to the funnel. A null
timestamp is different: the event has no
+ /// position in the window, and an aggregate ignores a row whose input is
null.
+ private void forEachNotNullTimestamp(int length, BlockValSet
timestampBlockValSet,
+ RoaringBitmapUtils.BatchConsumer consumer) {
+ RoaringBitmap nullBitmap = _nullHandlingEnabled ?
timestampBlockValSet.getNullBitmap() : null;
+ if (nullBitmap == null) {
+ consumer.consume(0, length);
+ return;
+ }
+ // Skip if the entire block is null
+ if (!nullBitmap.contains(0, length)) {
+ RoaringBitmapUtils.forEachUnset(length, nullBitmap.getIntIterator(),
consumer);
+ }
+ }
+
@Override
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
- PriorityQueue<FunnelStepEvent> stepEvents =
aggregationResultHolder.getResult();
- if (stepEvents == null) {
- stepEvents = new PriorityQueue<>();
- aggregationResultHolder.setValue(stepEvents);
+ PriorityQueue<FunnelStepEvent> existing =
aggregationResultHolder.getResult();
+ if (existing == null) {
+ existing = new PriorityQueue<>();
+ aggregationResultHolder.setValue(existing);
}
- for (int i = 0; i < length; i++) {
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
- stepFound = true;
- break;
+ PriorityQueue<FunnelStepEvent> stepEvents = existing;
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
}
}
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
- }
- }
+ });
}
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
- for (int i = 0; i < length; i++) {
- int groupKey = groupKeyArray[i];
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int groupKey = groupKeyArray[i];
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
- stepFound = true;
- break;
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
}
}
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
- }
- }
+ });
}
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
- for (int i = 0; i < length; i++) {
- int[] groupKeys = groupKeysArray[i];
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int[] groupKeys = groupKeysArray[i];
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ for (int groupKey : groupKeys) {
+ PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
+ }
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
for (int groupKey : groupKeys) {
PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], j));
+ stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
}
- stepFound = true;
- break;
- }
- }
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- for (int groupKey : groupKeys) {
- PriorityQueue<FunnelStepEvent> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- stepEvents.add(new FunnelStepEvent(timestampBlock[i], -1));
}
}
- }
+ });
}
private static PriorityQueue<FunnelStepEvent>
getFunnelStepEvents(GroupByResultHolder groupByResultHolder,
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelCompleteCountAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelCompleteCountAggregationFunction.java
index f76e678cb14..02a0392d850 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelCompleteCountAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelCompleteCountAggregationFunction.java
@@ -31,8 +31,8 @@ import org.apache.pinot.spi.query.QueryThreadContext;
public class FunnelCompleteCountAggregationFunction extends
FunnelBaseAggregationFunction<Integer> {
- public FunnelCompleteCountAggregationFunction(List<ExpressionContext>
arguments) {
- super(arguments);
+ public FunnelCompleteCountAggregationFunction(List<ExpressionContext>
arguments, boolean nullHandlingEnabled) {
+ super(arguments, nullHandlingEnabled);
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelEventsFunctionEvalAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelEventsFunctionEvalAggregationFunction.java
index f319a29b712..408d4fd75cb 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelEventsFunctionEvalAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelEventsFunctionEvalAggregationFunction.java
@@ -31,6 +31,7 @@ import javax.annotation.Nullable;
import org.apache.pinot.common.CustomObject;
import org.apache.pinot.common.request.context.ExpressionContext;
import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.RoaringBitmapUtils;
import org.apache.pinot.core.common.BlockValSet;
import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
@@ -41,12 +42,14 @@ 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;
import org.apache.pinot.spi.query.QueryThreadContext;
+import org.roaringbitmap.RoaringBitmap;
public class FunnelEventsFunctionEvalAggregationFunction
implements
AggregationFunction<PriorityQueue<FunnelStepEventWithExtraFields>,
ObjectArrayList<String>> {
private final static int INTERMEDIATE_RESULT_SERDE_VERSION = 0;
+ protected final boolean _nullHandlingEnabled;
protected final ExpressionContext _timestampExpression;
protected final long _windowSize;
protected final List<ExpressionContext> _stepExpressions;
@@ -56,7 +59,9 @@ public class FunnelEventsFunctionEvalAggregationFunction
protected final List<ExpressionContext> _extraExpressions;
protected long _maxStepDuration = 0L;
- public FunnelEventsFunctionEvalAggregationFunction(List<ExpressionContext>
arguments) {
+ public FunnelEventsFunctionEvalAggregationFunction(List<ExpressionContext>
arguments,
+ boolean nullHandlingEnabled) {
+ _nullHandlingEnabled = nullHandlingEnabled;
int numArguments = arguments.size();
Preconditions.checkArgument(numArguments > 3,
"FUNNEL_EVENTS_FUNCTION_EVAL expects >= 4 arguments, got: %s. The
function can be used as "
@@ -140,36 +145,67 @@ public class FunnelEventsFunctionEvalAggregationFunction
return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
}
+ /// Runs the consumer over each range of rows whose timestamp is not null,
or over the whole block when the
+ /// option is disabled.
+ ///
+ /// Only the timestamp is consulted, of the three kinds of column this
function reads.
+ ///
+ /// A step expression is a predicate, and a predicate over a null operand is
UNKNOWN, which SQL treats as not
+ /// satisfied wherever a boolean is consumed, so a null step already means
that step did not match and the row
+ /// still belongs to the funnel. A null timestamp is different: the event
has no position in the window, and an
+ /// aggregate ignores a row whose input is null.
+ ///
+ /// An extra field is neither. It is payload carried alongside a matched
event, so a null one does not make the
+ /// event invalid and dropping the row would lose an event that really
happened. It is therefore not gated on,
+ /// with one known limitation: the value is read positionally and a null row
yields the column default, so a null
+ /// extra field renders as `0` or the empty string rather than as NULL.
+ private void forEachNotNullTimestamp(int length, BlockValSet
timestampBlockValSet,
+ RoaringBitmapUtils.BatchConsumer consumer) {
+ RoaringBitmap nullBitmap = _nullHandlingEnabled ?
timestampBlockValSet.getNullBitmap() : null;
+ if (nullBitmap == null) {
+ consumer.consume(0, length);
+ return;
+ }
+ // Skip if the entire block is null
+ if (!nullBitmap.contains(0, length)) {
+ RoaringBitmapUtils.forEachUnset(length, nullBitmap.getIntIterator(),
consumer);
+ }
+ }
+
@Override
public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
- PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
aggregationResultHolder.getResult();
- if (stepEvents == null) {
- stepEvents = new PriorityQueue<>();
- aggregationResultHolder.setValue(stepEvents);
+ PriorityQueue<FunnelStepEventWithExtraFields> existing =
aggregationResultHolder.getResult();
+ if (existing == null) {
+ existing = new PriorityQueue<>();
+ aggregationResultHolder.setValue(existing);
}
+ PriorityQueue<FunnelStepEventWithExtraFields> stepEvents = existing;
List<Object> extraFieldsBlocks = getExtraFieldsBlocks(blockValSetMap);
- for (int i = 0; i < length; i++) {
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ List<Object> extraFields = extractExtraFields(extraFieldsBlocks,
i);
+ stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
List<Object> extraFields = extractExtraFields(extraFieldsBlocks, i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
- stepFound = true;
- break;
+ stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
}
}
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- List<Object> extraFields = extractExtraFields(extraFieldsBlocks, i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
- }
- }
+ });
}
private List<Object> getExtraFieldsBlocks(Map<ExpressionContext,
BlockValSet> blockValSetMap) {
@@ -232,66 +268,75 @@ public class FunnelEventsFunctionEvalAggregationFunction
@Override
public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
List<Object> extraFieldsBlocks = getExtraFieldsBlocks(blockValSetMap);
- for (int i = 0; i < length; i++) {
- int groupKey = groupKeyArray[i];
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int groupKey = groupKeyArray[i];
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
+ getFunnelStepEvents(groupByResultHolder, groupKey);
+ List<Object> extraFields = extractExtraFields(extraFieldsBlocks,
i);
+ stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
List<Object> extraFields = extractExtraFields(extraFieldsBlocks, i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
- stepFound = true;
- break;
+ stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
}
}
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- List<Object> extraFields = extractExtraFields(extraFieldsBlocks, i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
- }
- }
+ });
}
@Override
public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
Map<ExpressionContext, BlockValSet> blockValSetMap) {
- long[] timestampBlock =
blockValSetMap.get(_timestampExpression).getLongValuesSV();
+ BlockValSet timestampBlockValSet =
blockValSetMap.get(_timestampExpression);
+ long[] timestampBlock = timestampBlockValSet.getLongValuesSV();
List<int[]> stepBlocks = new ArrayList<>(_numSteps);
for (ExpressionContext stepExpression : _stepExpressions) {
stepBlocks.add(blockValSetMap.get(stepExpression).getIntValuesSV());
}
List<Object> extraFieldsBlocks = getExtraFieldsBlocks(blockValSetMap);
- for (int i = 0; i < length; i++) {
- int[] groupKeys = groupKeysArray[i];
- boolean stepFound = false;
- for (int j = 0; j < _numSteps; j++) {
- if (stepBlocks.get(j)[i] == 1) {
+ forEachNotNullTimestamp(length, timestampBlockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ int[] groupKeys = groupKeysArray[i];
+ boolean stepFound = false;
+ for (int j = 0; j < _numSteps; j++) {
+ if (stepBlocks.get(j)[i] == 1) {
+ for (int groupKey : groupKeys) {
+ PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
+ getFunnelStepEvents(groupByResultHolder, groupKey);
+ List<Object> extraFields = extractExtraFields(extraFieldsBlocks,
i);
+ stepEvents.add(
+ new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
+ }
+ stepFound = true;
+ break;
+ }
+ }
+ // If the mode is KEEP_ALL and no step is found, add a dummy step
event with step -1
+ if (_modes.hasKeepAll() && !stepFound) {
for (int groupKey : groupKeys) {
PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
List<Object> extraFields = extractExtraFields(extraFieldsBlocks,
i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], j), extraFields));
+ stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
}
- stepFound = true;
- break;
- }
- }
- // If the mode is KEEP_ALL and no step is found, add a dummy step event
with step -1
- if (_modes.hasKeepAll() && !stepFound) {
- for (int groupKey : groupKeys) {
- PriorityQueue<FunnelStepEventWithExtraFields> stepEvents =
getFunnelStepEvents(groupByResultHolder, groupKey);
- List<Object> extraFields = extractExtraFields(extraFieldsBlocks, i);
- stepEvents.add(new FunnelStepEventWithExtraFields(new
FunnelStepEvent(timestampBlock[i], -1), extraFields));
}
}
- }
+ });
}
private static PriorityQueue<FunnelStepEventWithExtraFields>
getFunnelStepEvents(
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMatchStepAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMatchStepAggregationFunction.java
index 9710f32ecd7..0c08dee146d 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMatchStepAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMatchStepAggregationFunction.java
@@ -32,8 +32,8 @@ import org.apache.pinot.spi.query.QueryThreadContext;
public class FunnelMatchStepAggregationFunction extends
FunnelBaseAggregationFunction<IntArrayList> {
- public FunnelMatchStepAggregationFunction(List<ExpressionContext> arguments)
{
- super(arguments);
+ public FunnelMatchStepAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(arguments, nullHandlingEnabled);
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMaxStepAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMaxStepAggregationFunction.java
index 23ae788daa9..7cac01529c9 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMaxStepAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelMaxStepAggregationFunction.java
@@ -31,8 +31,8 @@ import org.apache.pinot.spi.query.QueryThreadContext;
public class FunnelMaxStepAggregationFunction extends
FunnelBaseAggregationFunction<Integer> {
- public FunnelMaxStepAggregationFunction(List<ExpressionContext> arguments) {
- super(arguments);
+ public FunnelMaxStepAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(arguments, nullHandlingEnabled);
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelStepDurationStatsAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelStepDurationStatsAggregationFunction.java
index d6ce01f983b..b162aabba43 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelStepDurationStatsAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/funnel/window/FunnelStepDurationStatsAggregationFunction.java
@@ -49,8 +49,8 @@ public class FunnelStepDurationStatsAggregationFunction
extends FunnelBaseAggreg
private final List<String> _durationFunctions = new ArrayList<>();
private boolean _canSkipNonMatchedFunnel = true;
- public FunnelStepDurationStatsAggregationFunction(List<ExpressionContext>
arguments) {
- super(arguments);
+ public FunnelStepDurationStatsAggregationFunction(List<ExpressionContext>
arguments, boolean nullHandlingEnabled) {
+ super(arguments, nullHandlingEnabled);
if (_extraArguments.get("DURATIONFUNCTIONS") != null) {
String[] durationFunctions =
_extraArguments.get("DURATIONFUNCTIONS").split(",");
for (String durationFunction : durationFunctions) {
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 ddb6dc6c314..dfb4824aea9 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
@@ -32,6 +32,22 @@ import org.roaringbitmap.RoaringBitmap;
/// Synthetic [BlockValSet] for testing and benchmarking.
+///
+/// There is one fixture per stored type, single- and multi-value:
`Int`/`IntMV`, `Long`/`LongMV`, `Float`/`FloatMV`,
+/// `Double`/`DoubleMV`, `BigDec`/`BigDecMV`, `Str`/`StrMV` and
`Bytes`/`BytesMV`, plus `DictIds`/`DictIdsMV` for
+/// dictionary-encoded columns. Each takes an optional null bitmap and the raw
values.
+///
+/// Two of them are named for a JDK type they must not shadow. A nested class
called `String` or `BigDecimal` hides
+/// [String] or [BigDecimal] across the whole of this class, which silently
changes the signature of every `getString*`
+/// or `getBigDecimal*` method declared here so that it no longer implements
[BlockValSet]. Hence `Str` and `BigDec`;
+/// keep any future fixture clear of the same collision.
+///
+/// **The null bitmap is independent of the values.** A row marked null still
holds whatever the values array puts
+/// at that index, and these fixtures have no notion of a column's
`defaultNullValue`. That matches what a caller
+/// with null handling enabled sees, since it reads the bitmap and skips those
rows. It does **not** match a real
+/// segment with null handling disabled, where a null row reads as the column
default rather than as a neighbouring
+/// value. So a test that wants "nothing was aggregated" in the disabled mode
cannot get there with an all-null
+/// bitmap - the values are still aggregated - and needs a holder that was
never touched instead.
public class SyntheticBlockValSets {
private SyntheticBlockValSets() {
}
@@ -142,6 +158,55 @@ public class SyntheticBlockValSets {
/// A [BlockValSet] for a dictionary-encoded multi-value column, which
exposes dictionary ids rather than values.
///
/// Functions that collect dictionary ids take a different path from the one
that reads values, and resolve the ids
+ /// A simple [BlockValSet] for nullable, dictionary-encoded single-value
columns.
+ public static class DictIds extends Base {
+
+ @Nullable
+ final RoaringBitmap _nullBitmap;
+ final int[] _dictIds;
+ final Dictionary _dictionary;
+ final DataType _valueType;
+
+ private DictIds(@Nullable RoaringBitmap nullBitmap, int[] dictIds,
Dictionary dictionary, DataType valueType) {
+ _nullBitmap = nullBitmap;
+ _dictIds = dictIds;
+ _dictionary = dictionary;
+ _valueType = valueType;
+ }
+
+ public static DictIds create(@Nullable RoaringBitmap nullBitmap, int[]
dictIds, Dictionary dictionary,
+ DataType valueType) {
+ return new DictIds(nullBitmap, dictIds, dictionary, valueType);
+ }
+
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap() {
+ return _nullBitmap;
+ }
+
+ @Override
+ public DataType getValueType() {
+ return _valueType;
+ }
+
+ @Override
+ public boolean isSingleValue() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Dictionary getDictionary() {
+ return _dictionary;
+ }
+
+ @Override
+ public int[] getDictionaryIdsSV() {
+ return _dictIds;
+ }
+ }
+
/// against the dictionary only when the result is extracted.
public static class DictIdsMV extends Base {
@@ -542,9 +607,7 @@ public class SyntheticBlockValSets {
/// 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].
+ /// Named `BigDec` rather than `BigDecimal` to avoid shadowing [BigDecimal];
see the class comment.
public static class BigDec extends Base {
@Nullable
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 4e282e463a6..1a8df4c13da 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
@@ -18,6 +18,10 @@
*/
package org.apache.pinot.core.query.aggregation.function;
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -191,6 +195,27 @@ public class AggregationFunctionNullContractTest {
assertEquals(create("DISTINCTCOUNTBITMAP", "(column)",
true).extractFinalResult(null), 0);
}
+ /// A funnel over no events completed no steps, so every one of them answers
zero rather than `NULL`.
+ ///
+ /// They belong with the counting functions above rather than the value
functions below: the answer is a count, or
+ /// a per-step vector of counts, and zero is the meaningful value for it.
Pinned because the whole family renders
+ /// this from an initial accumulator rather than from a branch in
`extractFinalResult`, so it would move quietly.
+ @Test
+ public void testFunnelFunctionsReturnZeroWhenNothingAggregated() {
+ String steps = "(column, '1000', 2, column2 = 'a', column2 = 'b')";
+ assertEquals(create("FUNNELMAXSTEP", steps,
true).extractFinalResult(null), 0);
+ assertEquals(create("FUNNELCOMPLETECOUNT", steps,
true).extractFinalResult(null), 0);
+ assertEquals(create("FUNNELSTEPDURATIONSTATS",
+ "(column, '1000', 2, column2 = 'a', column2 = 'b',
'durationFunctions=count')", true)
+ .extractFinalResult(null), new DoubleArrayList());
+ assertEquals(create("FUNNELMATCHSTEP", steps,
true).extractFinalResult(null), new IntArrayList(new int[]{0, 0}));
+ assertEquals(create("FUNNELEVENTSFUNCTIONEVAL",
+ "(column, '1000', 2, column2 = 'a', column2 = 'b', 2, column,
column2)", true)
+ .extractFinalResult(null), new ObjectArrayList<String>());
+ assertEquals(create("FUNNELCOUNT", "(STEPS(column2 = 'a', column2 = 'b'),
CORRELATE_BY(column))", true)
+ .extractFinalResult(null), new LongArrayList(new long[]{0L, 0L}));
+ }
+
/// The functions that return SQL `NULL` when nothing was aggregated.
///
/// Both representations of that state are checked. A `null` intermediate
result is the obvious one, but several of
@@ -206,8 +231,8 @@ public class AggregationFunctionNullContractTest {
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,
+ AggregationFunctionType.IDSET, AggregationFunctionType.HISTOGRAM,
AggregationFunctionType.SKEWNESS,
+ AggregationFunctionType.KURTOSIS, AggregationFunctionType.STUNION,
// 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
@@ -229,9 +254,13 @@ public class AggregationFunctionNullContractTest {
/// which functions the option actually reaches.
///
/// The raw variants reach the option through the function they delegate to,
so threading it into one of those
- /// changes the raw variant alongside it. Absence is as meaningful as
presence: a function missing from here either
- /// never receives the option, which is the first known deviation on
[AggregationFunction], or does not skip null
- /// rows.
+ /// changes the raw variant alongside it.
+ ///
+ /// Absence no longer means a function was left out of the contract. Every
user-facing aggregation now receives the
+ /// option, so a function missing from here is one this harness cannot drive
— see
+ /// [#NOT_EXERCISABLE_BY_SYNTHETIC_BLOCK], which lists where each of those
is covered instead — or one that
+ /// genuinely answers the same in both modes, as the counting functions do
when the empty answer is `0` either
+ /// way.
///
/// Two bounds on how much this set proves. It only covers what the harness
can drive — see
/// [#NOT_EXERCISABLE_BY_SYNTHETIC_BLOCK] — and it compares the **rendered**
answers, because several functions
@@ -278,22 +307,31 @@ public class AggregationFunctionNullContractTest {
AggregationFunctionType.DISTINCTCOUNTRAWTHETASKETCH,
AggregationFunctionType.DISTINCTCOUNTCPCSKETCH,
AggregationFunctionType.DISTINCTCOUNTRAWCPCSKETCH,
AggregationFunctionType.DISTINCTCOUNTBITMAPMV,
AggregationFunctionType.DISTINCTCOUNTHLLMV,
AggregationFunctionType.DISTINCTCOUNTRAWHLLMV,
- AggregationFunctionType.DISTINCTCOUNTHLLPLUSMV,
AggregationFunctionType.DISTINCTCOUNTRAWHLLPLUSMV
+ AggregationFunctionType.DISTINCTCOUNTHLLPLUSMV,
AggregationFunctionType.DISTINCTCOUNTRAWHLLPLUSMV,
+ // Reached once the multi-value shapes were added below; the array sums
take only an array column
+ AggregationFunctionType.SUMARRAYLONG,
AggregationFunctionType.SUMARRAYDOUBLE
);
/// Functions this test cannot drive with a one-column synthetic block,
pinned so that a silent drop-out is always a
/// reviewed decision. Derived from a run rather than predicted.
///
- /// The block implements only single-value `long` and `double`, so a
function lands here when it reads another value
- /// type, needs a multi-value block, or takes more than the one input column
the shared argument shapes supply. The
- /// exclusion is scoped to this test: the rest of the contract is still
checked against these functions, since the
- /// other cases construct them and call
[AggregationFunction#extractFinalResult] without aggregating first.
+ /// [#BLOCK_SHAPES] supplies one column at a time — `double`, `long`, `int`,
`String` or `byte[]`, single-value,
+ /// plus multi-value `long`, `double` and `int` — and gives every input
expression the same shape. A function
+ /// lands here when it needs a value type outside that list, a payload the
shape cannot fabricate (a serialized
+ /// sketch or geometry rather than an empty `byte[]`), a dictionary, or two
input columns of different types. That
+ /// last one is what rules out the funnels: their timestamp is a `long` and
their steps are `int` predicates, and
+ /// no single shape is both.
+ ///
+ /// The exclusion is scoped to this one check. The rest of the contract is
still enforced against these functions
+ /// here, since the other cases construct them and call
[AggregationFunction#extractFinalResult] without
+ /// aggregating first, and their null-row skipping is covered by a test
built for the shape each one needs:
+ /// `DistinctCountSketchNullHandlingTest`, `FrequentSketchNullHandlingTest`,
`ValueAggregationNullHandlingTest`
+ /// and `FunnelNullHandlingTest`. So membership here means "checked
elsewhere", not "unchecked".
private static final Set<AggregationFunctionType>
NOT_EXERCISABLE_BY_SYNTHETIC_BLOCK = Set.of(
AggregationFunctionType.FASTHLL,
AggregationFunctionType.DISTINCTCOUNTTUPLESKETCH,
AggregationFunctionType.DISTINCTCOUNTRAWINTEGERSUMTUPLESKETCH,
AggregationFunctionType.SUMVALUESINTEGERSUMTUPLESKETCH,
AggregationFunctionType.AVGVALUEINTEGERSUMTUPLESKETCH,
AggregationFunctionType.STUNION, AggregationFunctionType.BOOLAND,
AggregationFunctionType.BOOLOR,
- AggregationFunctionType.SUMARRAYLONG,
AggregationFunctionType.SUMARRAYDOUBLE,
AggregationFunctionType.FUNNELMAXSTEP,
AggregationFunctionType.FUNNELCOMPLETECOUNT,
AggregationFunctionType.FUNNELSTEPDURATIONSTATS,
AggregationFunctionType.FUNNELMATCHSTEP,
AggregationFunctionType.FUNNELEVENTSFUNCTIONEVAL,
AggregationFunctionType.FUNNELCOUNT
@@ -391,12 +429,39 @@ public class AggregationFunctionNullContractTest {
///
/// Probed rather than pinned per function: which value type an accumulator
reads is an implementation detail that
/// changes, and a hard-coded mapping silently drops a function out of the
census when it feeds the wrong width.
+ private static int[][] mvInts() {
+ int[][] rows = new int[NUM_DOCS][];
+ for (int i = 0; i < NUM_DOCS; i++) {
+ rows[i] = new int[]{0};
+ }
+ return rows;
+ }
+
+ private static long[][] mvLongs() {
+ long[][] rows = new long[NUM_DOCS][];
+ for (int i = 0; i < NUM_DOCS; i++) {
+ rows[i] = new long[]{0L};
+ }
+ return rows;
+ }
+
+ private static double[][] mvDoubles() {
+ double[][] rows = new double[NUM_DOCS][];
+ for (int i = 0; i < NUM_DOCS; i++) {
+ rows[i] = new double[]{0.0};
+ }
+ return rows;
+ }
+
private static final List<Supplier<BlockValSet>> BLOCK_SHAPES = List.of(
- () -> SyntheticBlockValSets.Double.create(NUM_DOCS, allNullBitmap(), ()
-> 0.0),
- () -> SyntheticBlockValSets.Long.create(NUM_DOCS, allNullBitmap(), () ->
0L),
() -> SyntheticBlockValSets.Int.create(NUM_DOCS, allNullBitmap(), () ->
0),
+ () -> SyntheticBlockValSets.Long.create(NUM_DOCS, allNullBitmap(), () ->
0L),
+ () -> SyntheticBlockValSets.Double.create(NUM_DOCS, allNullBitmap(), ()
-> 0.0),
() -> SyntheticBlockValSets.Str.create(NUM_DOCS, allNullBitmap(), () ->
""),
- () -> SyntheticBlockValSets.Bytes.create(NUM_DOCS, allNullBitmap(), ()
-> new byte[0])
+ () -> SyntheticBlockValSets.Bytes.create(NUM_DOCS, allNullBitmap(), ()
-> new byte[0]),
+ () -> SyntheticBlockValSets.IntMV.create(allNullBitmap(), mvInts()),
+ () -> SyntheticBlockValSets.LongMV.create(allNullBitmap(), mvLongs()),
+ () -> SyntheticBlockValSets.DoubleMV.create(allNullBitmap(), mvDoubles())
);
private static RoaringBitmap allNullBitmap() {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelNullHandlingTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelNullHandlingTest.java
new file mode 100644
index 00000000000..fe1cb727cae
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/funnel/FunnelNullHandlingTest.java
@@ -0,0 +1,233 @@
+/**
+ * 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.funnel;
+
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.FunctionContext;
+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.AggregationFunction;
+import
org.apache.pinot.core.query.aggregation.function.funnel.window.FunnelMaxStepAggregationFunction;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.roaringbitmap.RoaringBitmap;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+
+/// Null handling for the window funnel functions.
+///
+/// A funnel reads more than one column per row, so it never received the
option through
+///
[org.apache.pinot.core.query.aggregation.function.NullableSingleInputAggregationFunction]
and is invisible to
+///
[org.apache.pinot.core.query.aggregation.function.AggregationFunctionNullContractTest],
whose synthetic block
+/// supplies a single column.
+///
+/// Only the timestamp is consulted. A step expression is a predicate, and a
predicate over a null operand is
+/// UNKNOWN, which SQL treats as not satisfied wherever a boolean is consumed,
so a null step already means that
+/// step did not match. A null timestamp is different: the event has no
position in the window, and today it is read
+/// as the column default, which places a real step event at a fabricated time.
+public class FunnelNullHandlingTest {
+ private static final ExpressionContext TIMESTAMP =
ExpressionContext.forIdentifier("ts");
+ private static final ExpressionContext STEP_0 =
ExpressionContext.forIdentifier("step0");
+ private static final ExpressionContext STEP_1 =
ExpressionContext.forIdentifier("step1");
+
+ /// `funnelMaxStep(ts, '1000', 2, step0, step1)`, optionally with extra
arguments such as `MODE=KEEP_ALL`.
+ private static FunnelMaxStepAggregationFunction maxStep(boolean
nullHandlingEnabled, String... extraArguments) {
+ List<ExpressionContext> arguments = new ArrayList<>(List.of(TIMESTAMP,
+ ExpressionContext.forLiteral(Literal.longValue(1000)),
+ ExpressionContext.forLiteral(Literal.intValue(2)), STEP_0, STEP_1));
+ for (String extraArgument : extraArguments) {
+
arguments.add(ExpressionContext.forLiteral(Literal.stringValue(extraArgument)));
+ }
+ return new FunnelMaxStepAggregationFunction(arguments,
nullHandlingEnabled);
+ }
+
+ /// Timestamps carry the null bitmap; the two step columns are the
already-evaluated predicate results.
+ private static Map<ExpressionContext, BlockValSet> block(RoaringBitmap
timestampNulls, long[] timestamps,
+ int[] step0, int[] step1) {
+ return Map.of(
+ TIMESTAMP, SyntheticBlockValSets.Long.create(timestampNulls,
timestamps),
+ STEP_0, SyntheticBlockValSets.Int.create(null, step0),
+ STEP_1, SyntheticBlockValSets.Int.create(null, step1));
+ }
+
+ /// A row whose timestamp is null contributes no step event, so the funnel
that row would have completed does not
+ /// complete.
+ @Test
+ public void testNullTimestampRowContributesNoEvent() {
+ long[] timestamps = {100L, 200L};
+ int[] step0 = {1, 0};
+ int[] step1 = {0, 1};
+
+ FunnelMaxStepAggregationFunction enabled = maxStep(true);
+ AggregationResultHolder holder = enabled.createAggregationResultHolder();
+ enabled.aggregate(2, holder, block(RoaringBitmap.bitmapOf(1), timestamps,
step0, step1));
+
assertEquals(enabled.extractFinalResult(enabled.extractAggregationResult(holder)),
Integer.valueOf(1));
+
+ // With the option disabled the null bitmap is ignored and row 1 still
completes the funnel, which is the answer
+ // this mode has always given
+ FunnelMaxStepAggregationFunction disabled = maxStep(false);
+ AggregationResultHolder disabledHolder =
disabled.createAggregationResultHolder();
+ disabled.aggregate(2, disabledHolder, block(RoaringBitmap.bitmapOf(1),
timestamps, step0, step1));
+
assertEquals(disabled.extractFinalResult(disabled.extractAggregationResult(disabledHolder)),
Integer.valueOf(2));
+ }
+
+ /// Every row null leaves the queue empty rather than filling it with events
at fabricated timestamps.
+ @Test
+ public void testEveryRowNullYieldsNoEvents() {
+ long[] timestamps = {100L, 200L};
+ RoaringBitmap allNull = new RoaringBitmap();
+ allNull.add(0L, 2L);
+
+ FunnelMaxStepAggregationFunction function = maxStep(true);
+ AggregationResultHolder holder = function.createAggregationResultHolder();
+ function.aggregate(2, holder, block(allNull, timestamps, new int[]{1, 0},
new int[]{0, 1}));
+
+ PriorityQueue<FunnelStepEvent> events =
function.extractAggregationResult(holder);
+ assertNotNull(events, "the holder is created before the range, so it stays
non-null");
+ assertEquals(events.size(), 0);
+ assertEquals(function.extractFinalResult(events), Integer.valueOf(0));
+ }
+
+ /// In KEEP_ALL mode a row that matches no step still produces a dummy event
- but only if it has a timestamp to
+ /// place it at.
+ @Test
+ public void testKeepAllDoesNotFabricateAnEventForANullTimestamp() {
+ long[] timestamps = {100L, 200L};
+ int[] noMatch = {0, 0};
+
+ FunnelMaxStepAggregationFunction enabled = maxStep(true, "MODE=KEEP_ALL");
+ AggregationResultHolder holder = enabled.createAggregationResultHolder();
+ enabled.aggregate(2, holder, block(RoaringBitmap.bitmapOf(1), timestamps,
new int[]{1, 0}, noMatch));
+ assertEquals(enabled.extractAggregationResult(holder).size(), 1);
+
+ FunnelMaxStepAggregationFunction disabled = maxStep(false,
"MODE=KEEP_ALL");
+ AggregationResultHolder disabledHolder =
disabled.createAggregationResultHolder();
+ disabled.aggregate(2, disabledHolder, block(RoaringBitmap.bitmapOf(1),
timestamps, new int[]{1, 0}, noMatch));
+ assertEquals(disabled.extractAggregationResult(disabledHolder).size(), 2);
+ }
+
+ /// The group-by path skips the null row for its own group only.
+ @Test
+ public void testGroupBySVSkipsTheNullRow() {
+ long[] timestamps = {100L, 200L};
+
+ FunnelMaxStepAggregationFunction function = maxStep(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupBySV(2, new int[]{0, 1}, holder,
+ block(RoaringBitmap.bitmapOf(1), timestamps, new int[]{1, 1}, new
int[]{0, 0}));
+
+ PriorityQueue<FunnelStepEvent> group0 =
function.extractGroupByResult(holder, 0);
+ assertNotNull(group0);
+ assertEquals(group0.size(), 1);
+ assertNull(function.extractGroupByResult(holder, 1));
+ }
+
+ /// A null row is skipped for every group key it would have fed.
+ @Test
+ public void testGroupByMVSkipsTheNullRowForAllKeys() {
+ long[] timestamps = {100L, 200L};
+
+ FunnelMaxStepAggregationFunction function = maxStep(true);
+ GroupByResultHolder holder = new ObjectGroupByResultHolder(2, 2);
+ function.aggregateGroupByMV(2, new int[][]{{0}, {0, 1}}, holder,
+ block(RoaringBitmap.bitmapOf(1), timestamps, new int[]{1, 1}, new
int[]{0, 0}));
+
+ PriorityQueue<FunnelStepEvent> group0 =
function.extractGroupByResult(holder, 0);
+ assertNotNull(group0);
+ assertEquals(group0.size(), 1);
+ assertNull(function.extractGroupByResult(holder, 1));
+ }
+
+ // ---------- FUNNELCOUNT ----------
+
+ private static final ExpressionContext USER =
ExpressionContext.forIdentifier("userId");
+
+ /// `funnelCount(steps(step0, step1), correlateby(userId))`, built through
the same factory
+ /// AggregationFunctionFactory uses.
+ private static AggregationFunction<?, LongArrayList> funnelCount(boolean
nullHandlingEnabled) {
+ List<ExpressionContext> arguments = List.of(
+ ExpressionContext.forFunction(
+ new FunctionContext(FunctionContext.Type.TRANSFORM, "steps",
List.of(STEP_0, STEP_1))),
+ ExpressionContext.forFunction(
+ new FunctionContext(FunctionContext.Type.TRANSFORM, "correlateby",
List.of(USER))));
+ return new FunnelCountAggregationFunctionFactory(arguments,
nullHandlingEnabled).get();
+ }
+
+ /// Two users, ids 0 and 1, so a dictionary id doubles as the user it stands
for.
+ private static Dictionary userDictionary() {
+ Dictionary dictionary = mock(Dictionary.class);
+ when(dictionary.length()).thenReturn(2);
+ when(dictionary.getValueType()).thenReturn(DataType.INT);
+ when(dictionary.getIntValue(anyInt())).thenAnswer(invocation ->
invocation.getArgument(0));
+ return dictionary;
+ }
+
+ private static Map<ExpressionContext, BlockValSet>
funnelCountBlock(RoaringBitmap userNulls, int[] userDictIds,
+ int[] step0, int[] step1) {
+ return Map.of(
+ USER, SyntheticBlockValSets.DictIds.create(userNulls, userDictIds,
userDictionary(), DataType.INT),
+ STEP_0, SyntheticBlockValSets.Int.create(null, step0),
+ STEP_1, SyntheticBlockValSets.Int.create(null, step1));
+ }
+
+ /// A row whose correlation key is null belongs to no user, so it must not
be counted as one.
+ ///
+ /// Its dictionary id is the default's, which is a real user's id, so
without the null check the funnel credits
+ /// that user with a step they never took.
+ @Test
+ public void testFunnelCountSkipsRowsWithANullCorrelationKey() {
+ // Row 0 is user 0 reaching step 0. Row 1 is null, and its dictionary id
happens to be user 1's.
+ int[] userDictIds = {0, 1};
+ int[] step0 = {1, 1};
+ int[] step1 = {0, 0};
+
+ AggregationFunction<?, LongArrayList> enabled = funnelCount(true);
+ AggregationResultHolder holder = enabled.createAggregationResultHolder();
+ enabled.aggregate(2, holder, funnelCountBlock(RoaringBitmap.bitmapOf(1),
userDictIds, step0, step1));
+ assertEquals(extractCounts(enabled, holder), List.of(1L, 0L));
+
+ AggregationFunction<?, LongArrayList> disabled = funnelCount(false);
+ AggregationResultHolder disabledHolder =
disabled.createAggregationResultHolder();
+ disabled.aggregate(2, disabledHolder,
funnelCountBlock(RoaringBitmap.bitmapOf(1), userDictIds, step0, step1));
+ assertEquals(extractCounts(disabled, disabledHolder), List.of(2L, 0L));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<Long> extractCounts(AggregationFunction<?,
LongArrayList> function,
+ AggregationResultHolder holder) {
+ AggregationFunction<Object, LongArrayList> typed =
(AggregationFunction<Object, LongArrayList>) function;
+ return typed.extractFinalResult(typed.extractAggregationResult(holder));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]