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 76e23b9c3bd Give the frequency sketch functions the query's null 
handling option (#19227)
76e23b9c3bd is described below

commit 76e23b9c3bd1a6aa551bb025a834f8c75f62835b
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Wed Aug 12 14:51:28 2026 -0700

    Give the frequency sketch functions the query's null handling option 
(#19227)
---
 .../aggregation/function/AggregationFunction.java  |   4 +-
 .../function/AggregationFunctionFactory.java       |   4 +-
 .../FrequentLongsSketchAggregationFunction.java    | 102 +++++++------
 .../FrequentStringsSketchAggregationFunction.java  | 104 ++++++++------
 .../AggregationFunctionNullContractTest.java       |   6 +-
 .../function/FrequentSketchNullHandlingTest.java   | 159 +++++++++++++++++++++
 6 files changed, 283 insertions(+), 96 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 45208aa6e02..b95b34a36f2 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
@@ -100,8 +100,8 @@ import org.apache.pinot.segment.spi.AggregationFunctionType;
 ///      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), the frequency sketches, `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.
+///      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
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 cc6efe8a17d..a5adab56302 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
@@ -516,9 +516,9 @@ public class AggregationFunctionFactory {
           case FUNNELEVENTSFUNCTIONEVAL:
             return new FunnelEventsFunctionEvalAggregationFunction(arguments);
           case FREQUENTSTRINGSSKETCH:
-            return new FrequentStringsSketchAggregationFunction(arguments);
+            return new FrequentStringsSketchAggregationFunction(arguments, 
nullHandlingEnabled);
           case FREQUENTLONGSSKETCH:
-            return new FrequentLongsSketchAggregationFunction(arguments);
+            return new FrequentLongsSketchAggregationFunction(arguments, 
nullHandlingEnabled);
           case DISTINCTCOUNTCPCSKETCH:
             return new DistinctCountCPCSketchAggregationFunction(arguments);
           case DISTINCTCOUNTRAWCPCSKETCH:
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentLongsSketchAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentLongsSketchAggregationFunction.java
index 8cf61c17507..c24e21d0be7 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentLongsSketchAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentLongsSketchAggregationFunction.java
@@ -61,13 +61,13 @@ import org.apache.pinot.spi.data.FieldSpec;
 ///
 ///   There is a variation of the function (**FREQUENT_STRINGS_SKETCH**) which 
accepts STRING type input columns.
 public class FrequentLongsSketchAggregationFunction
-    extends BaseSingleInputAggregationFunction<FrequentLongsSketch, 
Comparable<?>> {
+    extends NullableSingleInputAggregationFunction<FrequentLongsSketch, 
Comparable<?>> {
   protected static final int DEFAULT_MAX_MAP_SIZE = 256;
 
   protected int _maxMapSize;
 
-  public FrequentLongsSketchAggregationFunction(List<ExpressionContext> 
arguments) {
-    super(arguments.get(0));
+  public FrequentLongsSketchAggregationFunction(List<ExpressionContext> 
arguments, boolean nullHandlingEnabled) {
+    super(arguments.get(0), nullHandlingEnabled);
     int numArguments = arguments.size();
     Preconditions.checkArgument(numArguments == 1 || numArguments == 2,
         "Expecting 1 or 2 arguments for FrequentLongsSketch function: 
FREQUENTITEMSSKETCH(column, maxMapSize");
@@ -95,24 +95,34 @@ public class FrequentLongsSketchAggregationFunction
     BlockValSet valueSet = blockValSetMap.get(_expression);
     FieldSpec.DataType valueType = valueSet.getValueType();
 
-    FrequentLongsSketch sketch = getOrCreateSketch(aggregationResultHolder);
-
     switch (valueType) {
       case BYTES:
         // Assuming the column contains serialized data sketch
-        FrequentLongsSketch[] deserializedSketches =
-            
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-        sketch = getOrCreateSketch(aggregationResultHolder);
-
-        for (FrequentLongsSketch colSketch : deserializedSketches) {
-          sketch.merge(colSketch);
-        }
+        byte[][] bytesValues = valueSet.getBytesValuesSV();
+        // The sketch is created inside the range, so a block with no non-null 
row leaves the holder untouched and
+        // extractFinalResult sees the null that means nothing was aggregated
+        forEachNotNull(length, valueSet, (from, to) -> {
+          if (to == from) {
+            return;
+          }
+          FrequentLongsSketch sketch = 
getOrCreateSketch(aggregationResultHolder);
+          for (int i = from; i < to; i++) {
+            sketch.merge(deserializeSketch(bytesValues[i]));
+          }
+        });
         break;
       case INT:
       case LONG:
-        for (Long val : valueSet.getLongValuesSV()) {
-          sketch.update(val);
-        }
+        long[] longValues = valueSet.getLongValuesSV();
+        forEachNotNull(length, valueSet, (from, to) -> {
+          if (to == from) {
+            return;
+          }
+          FrequentLongsSketch sketch = 
getOrCreateSketch(aggregationResultHolder);
+          for (int i = from; i < to; i++) {
+            sketch.update(longValues[i]);
+          }
+        });
         break;
       default:
         throw new UnsupportedOperationException("Cannot aggregate on non 
int/long types");
@@ -128,20 +138,21 @@ public class FrequentLongsSketchAggregationFunction
     switch (valueType) {
       case BYTES:
         // serialized sketch
-        FrequentLongsSketch[] deserializedSketches =
-            
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-        for (int i = 0; i < length; i++) {
-          FrequentLongsSketch sketch = getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]);
-          sketch.merge(deserializedSketches[i]);
-        }
+        byte[][] bytesValues = valueSet.getBytesValuesSV();
+        forEachNotNull(length, valueSet, (from, to) -> {
+          for (int i = from; i < to; i++) {
+            getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]).merge(deserializeSketch(bytesValues[i]));
+          }
+        });
         break;
       case INT:
       case LONG:
         long[] values = valueSet.getLongValuesSV();
-        for (int i = 0; i < length; i++) {
-          FrequentLongsSketch sketch = getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]);
-          sketch.update(values[i]);
-        }
+        forEachNotNull(length, valueSet, (from, to) -> {
+          for (int i = from; i < to; i++) {
+            getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]).update(values[i]);
+          }
+        });
         break;
       default:
         throw new UnsupportedOperationException("Cannot aggregate on non 
int/long types");
@@ -157,24 +168,27 @@ public class FrequentLongsSketchAggregationFunction
     switch (valueType) {
       case BYTES:
         // serialized sketch
-        FrequentLongsSketch[] deserializedSketches =
-            
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-        for (int i = 0; i < length; i++) {
-          for (int groupKey : groupKeysArray[i]) {
-            FrequentLongsSketch sketch = 
getOrCreateSketch(groupByResultHolder, groupKey);
-            sketch.merge(deserializedSketches[i]);
+        byte[][] bytesValues = valueSet.getBytesValuesSV();
+        forEachNotNull(length, valueSet, (from, to) -> {
+          for (int i = from; i < to; i++) {
+            // Deserialized once per row, not once per group key the row 
belongs to
+            FrequentLongsSketch rowSketch = deserializeSketch(bytesValues[i]);
+            for (int groupKey : groupKeysArray[i]) {
+              getOrCreateSketch(groupByResultHolder, 
groupKey).merge(rowSketch);
+            }
           }
-        }
+        });
         break;
       case INT:
       case LONG:
         long[] values = valueSet.getLongValuesSV();
-        for (int i = 0; i < length; i++) {
-          for (int groupKey : groupKeysArray[i]) {
-            FrequentLongsSketch sketch = 
getOrCreateSketch(groupByResultHolder, groupKey);
-            sketch.update(values[i]);
+        forEachNotNull(length, valueSet, (from, to) -> {
+          for (int i = from; i < to; i++) {
+            for (int groupKey : groupKeysArray[i]) {
+              getOrCreateSketch(groupByResultHolder, 
groupKey).update(values[i]);
+            }
           }
-        }
+        });
         break;
       default:
         throw new UnsupportedOperationException("Cannot aggregate on non 
int/long types");
@@ -202,13 +216,9 @@ public class FrequentLongsSketchAggregationFunction
     return sketch;
   }
 
-  /// Deserializes the sketches from the bytes.
-  protected FrequentLongsSketch[] deserializeSketches(byte[][] 
serializedSketches) {
-    FrequentLongsSketch[] sketches = new 
FrequentLongsSketch[serializedSketches.length];
-    for (int i = 0; i < serializedSketches.length; i++) {
-      sketches[i] = 
FrequentLongsSketch.getInstance(MemorySegment.ofArray(serializedSketches[i]));
-    }
-    return sketches;
+  /// Deserializes a single serialized sketch, so a row that is skipped as 
null is never deserialized.
+  protected FrequentLongsSketch deserializeSketch(byte[] serializedSketch) {
+    return 
FrequentLongsSketch.getInstance(MemorySegment.ofArray(serializedSketch));
   }
 
   @Nullable
@@ -261,7 +271,9 @@ public class FrequentLongsSketchAggregationFunction
   @Nullable
   @Override
   public Comparable<?> extractFinalResult(@Nullable FrequentLongsSketch 
sketch) {
-    // A null intermediate result means nothing was aggregated, and there is 
no sketch to serialize
+    // A null intermediate result means nothing was aggregated, and there is 
no sketch to serialize. This function
+    // has never substituted an empty accumulator during extraction, so NULL 
is the answer in both modes and there is
+    // no disabled-mode identity to preserve here.
     return sketch != null ? new SerializedFrequentLongsSketch(sketch) : null;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentStringsSketchAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentStringsSketchAggregationFunction.java
index d164b58f954..e88cf12baad 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentStringsSketchAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FrequentStringsSketchAggregationFunction.java
@@ -62,13 +62,14 @@ import org.apache.pinot.spi.data.FieldSpec;
 ///
 ///   There is a variation of the function (**FREQUENT_LONGS_SKETCH**) which 
accept INT and LONG type input columns.
 public class FrequentStringsSketchAggregationFunction
-    extends BaseSingleInputAggregationFunction<FrequentItemsSketch<String>, 
Comparable<?>> {
+    extends 
NullableSingleInputAggregationFunction<FrequentItemsSketch<String>, 
Comparable<?>> {
   protected static final int DEFAULT_MAX_MAP_SIZE = 256;
 
   protected int _maxMapSize;
 
-  public FrequentStringsSketchAggregationFunction(List<ExpressionContext> 
arguments) {
-    super(arguments.get(0));
+  public FrequentStringsSketchAggregationFunction(List<ExpressionContext> 
arguments,
+      boolean nullHandlingEnabled) {
+    super(arguments.get(0), nullHandlingEnabled);
     int numArguments = arguments.size();
     Preconditions.checkArgument(numArguments == 1 || numArguments == 2,
         "Expecting 1 or 2 arguments for FrequentItemsSketch function: 
FREQUENTSTRINGSSKETCH(column, maxMapSize");
@@ -96,21 +97,31 @@ public class FrequentStringsSketchAggregationFunction
     BlockValSet valueSet = blockValSetMap.get(_expression);
     FieldSpec.DataType valueType = valueSet.getValueType();
 
-    FrequentItemsSketch<String> sketch = 
getOrCreateSketch(aggregationResultHolder);
-
     if (valueType == FieldSpec.DataType.BYTES) {
       // Assuming the column contains serialized data sketch
-      FrequentItemsSketch<String>[] deserializedSketches =
-          
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-      sketch = getOrCreateSketch(aggregationResultHolder);
-
-      for (FrequentItemsSketch<String> colSketch : deserializedSketches) {
-        sketch.merge(colSketch);
-      }
+      byte[][] bytesValues = valueSet.getBytesValuesSV();
+      // The sketch is created inside the range, so a block with no non-null 
row leaves the holder untouched and
+      // extractFinalResult sees the null that means nothing was aggregated
+      forEachNotNull(length, valueSet, (from, to) -> {
+        if (to == from) {
+          return;
+        }
+        FrequentItemsSketch<String> sketch = 
getOrCreateSketch(aggregationResultHolder);
+        for (int i = from; i < to; i++) {
+          sketch.merge(deserializeSketch(bytesValues[i]));
+        }
+      });
     } else {
-      for (String val : valueSet.getStringValuesSV()) {
-        sketch.update(val);
-      }
+      String[] values = valueSet.getStringValuesSV();
+      forEachNotNull(length, valueSet, (from, to) -> {
+        if (to == from) {
+          return;
+        }
+        FrequentItemsSketch<String> sketch = 
getOrCreateSketch(aggregationResultHolder);
+        for (int i = from; i < to; i++) {
+          sketch.update(values[i]);
+        }
+      });
     }
   }
 
@@ -122,18 +133,19 @@ public class FrequentStringsSketchAggregationFunction
 
     if (valueType == FieldSpec.DataType.BYTES) {
       // serialized sketch
-      FrequentItemsSketch<String>[] deserializedSketches =
-          
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-      for (int i = 0; i < length; i++) {
-        FrequentItemsSketch<String> sketch = 
getOrCreateSketch(groupByResultHolder, groupKeyArray[i]);
-        sketch.merge(deserializedSketches[i]);
-      }
+      byte[][] bytesValues = valueSet.getBytesValuesSV();
+      forEachNotNull(length, valueSet, (from, to) -> {
+        for (int i = from; i < to; i++) {
+          getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]).merge(deserializeSketch(bytesValues[i]));
+        }
+      });
     } else {
       String[] values = valueSet.getStringValuesSV();
-      for (int i = 0; i < length; i++) {
-        FrequentItemsSketch<String> sketch = 
getOrCreateSketch(groupByResultHolder, groupKeyArray[i]);
-        sketch.update(values[i]);
-      }
+      forEachNotNull(length, valueSet, (from, to) -> {
+        for (int i = from; i < to; i++) {
+          getOrCreateSketch(groupByResultHolder, 
groupKeyArray[i]).update(values[i]);
+        }
+      });
     }
   }
 
@@ -145,22 +157,25 @@ public class FrequentStringsSketchAggregationFunction
 
     if (valueType == FieldSpec.DataType.BYTES) {
       // serialized sketch
-      FrequentItemsSketch<String>[] deserializedSketches =
-          
deserializeSketches(blockValSetMap.get(_expression).getBytesValuesSV());
-      for (int i = 0; i < length; i++) {
-        for (int groupKey : groupKeysArray[i]) {
-          FrequentItemsSketch<String> sketch = 
getOrCreateSketch(groupByResultHolder, groupKey);
-          sketch.merge(deserializedSketches[i]);
+      byte[][] bytesValues = valueSet.getBytesValuesSV();
+      forEachNotNull(length, valueSet, (from, to) -> {
+        for (int i = from; i < to; i++) {
+          // Deserialized once per row, not once per group key the row belongs 
to
+          FrequentItemsSketch<String> rowSketch = 
deserializeSketch(bytesValues[i]);
+          for (int groupKey : groupKeysArray[i]) {
+            getOrCreateSketch(groupByResultHolder, groupKey).merge(rowSketch);
+          }
         }
-      }
+      });
     } else {
       String[] values = valueSet.getStringValuesSV();
-      for (int i = 0; i < length; i++) {
-        for (int groupKey : groupKeysArray[i]) {
-          FrequentItemsSketch<String> sketch = 
getOrCreateSketch(groupByResultHolder, groupKey);
-          sketch.update(values[i]);
+      forEachNotNull(length, valueSet, (from, to) -> {
+        for (int i = from; i < to; i++) {
+          for (int groupKey : groupKeysArray[i]) {
+            getOrCreateSketch(groupByResultHolder, groupKey).update(values[i]);
+          }
         }
-      }
+      });
     }
   }
 
@@ -185,14 +200,9 @@ public class FrequentStringsSketchAggregationFunction
     return sketch;
   }
 
-  /// Deserializes the sketches from the bytes.
-  protected FrequentItemsSketch<String>[] deserializeSketches(byte[][] 
serializedSketches) {
-    FrequentItemsSketch<String>[] sketches = new 
FrequentItemsSketch[serializedSketches.length];
-    for (int i = 0; i < serializedSketches.length; i++) {
-      sketches[i] =
-          
FrequentItemsSketch.getInstance(MemorySegment.ofArray(serializedSketches[i]), 
new ArrayOfStringsSerDe());
-    }
-    return sketches;
+  /// Deserializes a single serialized sketch, so a row that is skipped as 
null is never deserialized.
+  protected FrequentItemsSketch<String> deserializeSketch(byte[] 
serializedSketch) {
+    return 
FrequentItemsSketch.getInstance(MemorySegment.ofArray(serializedSketch), new 
ArrayOfStringsSerDe());
   }
 
   @Nullable
@@ -245,7 +255,9 @@ public class FrequentStringsSketchAggregationFunction
   @Nullable
   @Override
   public Comparable<?> extractFinalResult(@Nullable 
FrequentItemsSketch<String> sketch) {
-    // A null intermediate result means nothing was aggregated, and there is 
no sketch to serialize
+    // A null intermediate result means nothing was aggregated, and there is 
no sketch to serialize. This function
+    // has never substituted an empty accumulator during extraction, so NULL 
is the answer in both modes and there is
+    // no disabled-mode identity to preserve here.
     return sketch != null ? new SerializedFrequentStringsSketch(sketch) : null;
   }
 }
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 e511b0e7460..34ab0ea055f 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
@@ -253,7 +253,11 @@ public class AggregationFunctionNullContractTest {
       AggregationFunctionType.SUMLONG, AggregationFunctionType.SUMPRECISION, 
AggregationFunctionType.FIRSTWITHTIME,
       AggregationFunctionType.LASTWITHTIME, AggregationFunctionType.ARRAYAGG, 
AggregationFunctionType.LISTAGG,
       // Given the option so they can skip null rows; a row counts only when 
both input columns are non-null
-      AggregationFunctionType.COVARPOP, AggregationFunctionType.COVARSAMP
+      AggregationFunctionType.COVARPOP, AggregationFunctionType.COVARSAMP,
+      // Given the option so they can skip null rows. These two were in this 
set once before, on the strength of an
+      // identity comparison that reported every serializer-valued function as 
honouring it; they belong here now
+      // because they genuinely do.
+      AggregationFunctionType.FREQUENTSTRINGSSKETCH, 
AggregationFunctionType.FREQUENTLONGSSKETCH
   );
 
   /// Functions this test cannot drive with a one-column synthetic block, 
pinned so that a silent drop-out is always a
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/FrequentSketchNullHandlingTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/FrequentSketchNullHandlingTest.java
new file mode 100644
index 00000000000..7997e18b5b1
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/FrequentSketchNullHandlingTest.java
@@ -0,0 +1,159 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.datasketches.frequencies.FrequentLongsSketch;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.common.SyntheticBlockValSets;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import 
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.roaringbitmap.RoaringBitmap;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+
+/// Null handling and row bounding for `FREQUENTLONGSSKETCH`.
+///
+/// [AggregationFunctionNullContractTest] drives these functions through one 
synthetic block shape and only through
+/// `aggregate`, so the group-by paths and the row-bounding below are checked 
nowhere else.
+public class FrequentSketchNullHandlingTest {
+  private static final ExpressionContext COLUMN = 
ExpressionContext.forIdentifier("column");
+  private static final long[] VALUES = {10L, 20L, 30L, 40L};
+
+  private static FrequentLongsSketchAggregationFunction longs(boolean 
nullHandlingEnabled) {
+    return new FrequentLongsSketchAggregationFunction(List.of(COLUMN), 
nullHandlingEnabled);
+  }
+
+  private static Map<ExpressionContext, BlockValSet> block(RoaringBitmap 
nullBitmap, long[] values) {
+    return Map.of(COLUMN, SyntheticBlockValSets.Long.create(nullBitmap, 
values));
+  }
+
+  private static FrequentLongsSketch 
aggregate(FrequentLongsSketchAggregationFunction function, int length,
+      RoaringBitmap nullBitmap, long[] values) {
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    function.aggregate(length, resultHolder, block(nullBitmap, values));
+    return function.extractAggregationResult(resultHolder);
+  }
+
+  private static long estimate(FrequentLongsSketch sketch, long item) {
+    return sketch.getEstimate(item);
+  }
+
+  /// Only the rows that carry a value are counted.
+  @Test
+  public void testNullRowsAreSkipped() {
+    FrequentLongsSketch sketch = aggregate(longs(true), VALUES.length, 
RoaringBitmap.bitmapOf(1, 3), VALUES);
+
+    assertNotNull(sketch);
+    assertEquals(estimate(sketch, 10L), 1L);
+    assertEquals(estimate(sketch, 20L), 0L);
+    assertEquals(estimate(sketch, 30L), 1L);
+    assertEquals(estimate(sketch, 40L), 0L);
+  }
+
+  /// Aggregation must stop at `length`, not run to the end of the values 
array.
+  ///
+  /// `ProjectionBlockValSet` hands back the `DataBlockCache` array, and 
`DataBlockCache.initNewBlock` only clears the
+  /// cache when the new block is larger than the last one. A short block 
after a full one therefore sees a longer
+  /// array whose tail still holds the previous block's values, and anything 
that iterates the array rather than the
+  /// range counts that tail a second time.
+  @Test
+  public void testValuesBeyondLengthAreNotCounted() {
+    long[] oversized = {10L, 20L, 30L, 40L, 99L, 99L, 99L};
+
+    FrequentLongsSketch sketch = aggregate(longs(false), 4, null, oversized);
+
+    assertNotNull(sketch);
+    assertEquals(estimate(sketch, 10L), 1L);
+    assertEquals(estimate(sketch, 40L), 1L);
+    assertEquals(estimate(sketch, 99L), 0L);
+  }
+
+  @Test
+  public void testEveryRowNullYieldsNoIntermediateResult() {
+    RoaringBitmap allNull = new RoaringBitmap();
+    allNull.add(0L, VALUES.length);
+
+    assertNull(aggregate(longs(true), VALUES.length, allNull, VALUES));
+  }
+
+  /// A zero-length block still reaches the range callback, and must not mark 
the holder as aggregated.
+  @Test
+  public void testZeroLengthBlockLeavesTheHolderUntouched() {
+    assertNull(aggregate(longs(true), 0, null, VALUES));
+  }
+
+  /// The group-by path skips null rows per group, and a group whose every row 
is null is never created.
+  @Test
+  public void testGroupBySkipsNullRows() {
+    FrequentLongsSketchAggregationFunction function = longs(true);
+    GroupByResultHolder resultHolder = new ObjectGroupByResultHolder(4, 4);
+    // Rows 0 and 2 go to group 0, rows 1 and 3 to group 1; rows 1 and 3 are 
null, so group 1 gets nothing
+    function.aggregateGroupBySV(VALUES.length, new int[]{0, 1, 0, 1}, 
resultHolder,
+        block(RoaringBitmap.bitmapOf(1, 3), VALUES));
+
+    FrequentLongsSketch group0 = function.extractGroupByResult(resultHolder, 
0);
+    assertNotNull(group0);
+    assertEquals(estimate(group0, 10L), 1L);
+    assertEquals(estimate(group0, 30L), 1L);
+    assertNull(function.extractGroupByResult(resultHolder, 1));
+  }
+
+  /// With the option disabled the column default is counted, which is the 
answer this mode has always given, and the
+  /// final result stays `NULL` for an untouched accumulator.
+  @Test
+  public void testOptionDisabledCountsNullRowsAndStillRendersNull() {
+    RoaringBitmap allNull = new RoaringBitmap();
+    allNull.add(0L, VALUES.length);
+
+    FrequentLongsSketch sketch = aggregate(longs(false), VALUES.length, 
allNull, VALUES);
+    assertNotNull(sketch);
+    assertEquals(estimate(sketch, 10L), 1L);
+
+    assertNull(longs(false).extractFinalResult(null));
+    assertNull(longs(true).extractFinalResult(null));
+  }
+
+  /// A serialized sketch column is deserialized only for the rows that carry 
one.
+  @Test
+  public void testSerializedRowsAreDeserializedOnlyWhenNotNull() {
+    FrequentLongsSketch first = new FrequentLongsSketch(32);
+    first.update(10L);
+    FrequentLongsSketch second = new FrequentLongsSketch(32);
+    second.update(20L);
+    byte[][] serialized = {first.toByteArray(), second.toByteArray()};
+
+    FrequentLongsSketchAggregationFunction function = longs(true);
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    function.aggregate(2, resultHolder,
+        Map.of(COLUMN, 
SyntheticBlockValSets.Bytes.create(RoaringBitmap.bitmapOf(1), serialized)));
+
+    FrequentLongsSketch sketch = 
function.extractAggregationResult(resultHolder);
+    assertNotNull(sketch);
+    assertEquals(estimate(sketch, 10L), 1L);
+    assertEquals(estimate(sketch, 20L), 0L);
+  }
+}


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

Reply via email to