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 091eac79529 Fix HyperLogLogPlus max byte size and reject sp above 25 
(#20154)
091eac79529 is described below

commit 091eac79529cfe0499bce1e498259dedc858bd9b
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Wed Nov 4 16:47:32 2026 -0800

    Fix HyperLogLogPlus max byte size and reject sp above 25 (#20154)
---
 .../DistinctCountHLLPlusAggregationFunction.java   |  2 +
 ...stinctCountSmartHLLPlusAggregationFunction.java |  2 +
 .../function/AggregationFunctionFactoryTest.java   | 18 +++++
 .../DistinctCountHLLPlusValueAggregator.java       | 12 +--
 .../segment/local/utils/HyperLogLogPlusUtils.java  | 62 ++++++++++++---
 .../segment/local/utils/TableConfigUtils.java      | 13 ++++
 .../DistinctCountHLLPlusValueAggregatorTest.java   | 55 +++++++++++++
 .../local/aggregator/ValueAggregatorTest.java      |  4 +
 ...MutableSegmentImplIngestionAggregationTest.java | 47 ++++++++++++
 .../local/utils/HyperLogLogPlusUtilsTest.java      | 89 ++++++++++++++++++++++
 .../segment/local/utils/TableConfigUtilsTest.java  | 59 ++++++++++++++
 11 files changed, 349 insertions(+), 14 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLPlusAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLPlusAggregationFunction.java
index e54e59db08f..316d3d0bbe2 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLPlusAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountHLLPlusAggregationFunction.java
@@ -32,6 +32,7 @@ import 
org.apache.pinot.core.query.aggregation.AggregationResultHolder;
 import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
 import 
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.apache.pinot.segment.local.utils.HyperLogLogPlusUtils;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.segment.spi.Constants;
 import org.apache.pinot.segment.spi.index.reader.Dictionary;
@@ -59,6 +60,7 @@ public class DistinctCountHLLPlusAggregationFunction extends 
BaseSingleInputAggr
     } else if (arguments.size() == 3) {
       _p = arguments.get(1).getLiteral().getIntValue();
       _sp = arguments.get(2).getLiteral().getIntValue();
+      HyperLogLogPlusUtils.validateSp(_sp);
     } else {
       _p = CommonConstants.Helix.DEFAULT_HYPERLOGLOG_PLUS_P;
       _sp = CommonConstants.Helix.DEFAULT_HYPERLOGLOG_PLUS_SP;
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountSmartHLLPlusAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountSmartHLLPlusAggregationFunction.java
index cf30ff5f30b..62dfbea39ba 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountSmartHLLPlusAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/DistinctCountSmartHLLPlusAggregationFunction.java
@@ -32,6 +32,7 @@ import 
org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.core.common.BlockValSet;
 import org.apache.pinot.core.common.ObjectSerDeUtils;
 import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.segment.local.utils.HyperLogLogPlusUtils;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.segment.spi.index.reader.Dictionary;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
@@ -443,6 +444,7 @@ public class DistinctCountSmartHLLPlusAggregationFunction 
extends BaseDistinctCo
             break;
           case SP_KEY:
             _sp = Integer.parseInt(value);
+            HyperLogLogPlusUtils.validateSp(_sp);
             break;
           default:
             throw new IllegalArgumentException("Invalid parameter key: " + 
key);
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
index d10465d426c..c6652c9caca 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
@@ -18,12 +18,15 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.util.List;
 import org.apache.pinot.common.request.context.FunctionContext;
 import org.apache.pinot.common.request.context.RequestContextUtils;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.assertTrue;
 
 
@@ -491,6 +494,21 @@ public class AggregationFunctionFactoryTest {
     assertEquals(aggregationFunction.getResultColumnName(), 
function.toString());
   }
 
+  @Test
+  public void testDistinctCountHLLPlusSp() {
+    for (String functionName : List.of("distinctCountHLLPlus", 
"distinctCountRawHLLPlus", "distinctCountHLLPlusMV",
+        "distinctCountRawHLLPlusMV")) {
+      
AggregationFunctionFactory.getAggregationFunction(getFunction(functionName, 
"(column, 12, 25)"), false);
+      assertThrows(BadQueryRequestException.class,
+          () -> 
AggregationFunctionFactory.getAggregationFunction(getFunction(functionName, 
"(column, 12, 26)"),
+              false));
+    }
+    
AggregationFunctionFactory.getAggregationFunction(getFunction("distinctCountSmartHLLPlus",
 "(column, 'sp=25')"),
+        false);
+    assertThrows(BadQueryRequestException.class, () -> 
AggregationFunctionFactory.getAggregationFunction(
+        getFunction("distinctCountSmartHLLPlus", "(column, 'sp=26')"), false));
+  }
+
   private FunctionContext getFunction(String functionName) {
     return getFunction(functionName, ARGUMENT_COLUMN);
   }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregator.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregator.java
index 5c2855d3abb..71f90b9511e 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregator.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregator.java
@@ -37,7 +37,7 @@ public class DistinctCountHLLPlusValueAggregator implements 
ValueAggregator<Obje
   private final int _p;
   private final int _sp;
 
-  // Byte size won't change once we get the initial aggregated value
+  // Max of maxByteSize over the initial aggregated values. A merge keeps the 
p and sp of the value merged into
   private int _maxByteSize;
 
   public DistinctCountHLLPlusValueAggregator(List<ExpressionContext> 
arguments) {
@@ -48,6 +48,7 @@ public class DistinctCountHLLPlusValueAggregator implements 
ValueAggregator<Obje
     } else if (arguments.size() == 2) {
       _p = arguments.get(0).getLiteral().getIntValue();
       _sp = arguments.get(1).getLiteral().getIntValue();
+      HyperLogLogPlusUtils.validateSp(_sp);
     } else {
       _p = CommonConstants.Helix.DEFAULT_HYPERLOGLOG_PLUS_P;
       _sp = CommonConstants.Helix.DEFAULT_HYPERLOGLOG_PLUS_SP;
@@ -73,11 +74,11 @@ public class DistinctCountHLLPlusValueAggregator implements 
ValueAggregator<Obje
     if (rawValue instanceof byte[]) {
       byte[] bytes = (byte[]) rawValue;
       initialValue = deserializeAggregatedValue(bytes);
-      _maxByteSize = bytes.length;
+      _maxByteSize = Math.max(_maxByteSize, 
HyperLogLogPlusUtils.maxByteSize(bytes));
     } else {
       initialValue = new HyperLogLogPlus(_p, _sp);
       offerValue(initialValue, rawValue);
-      _maxByteSize = HyperLogLogPlusUtils.byteSize(_p, _sp);
+      _maxByteSize = Math.max(_maxByteSize, 
HyperLogLogPlusUtils.maxByteSize(_p, _sp));
     }
     return initialValue;
   }
@@ -125,14 +126,15 @@ public class DistinctCountHLLPlusValueAggregator 
implements ValueAggregator<Obje
 
   @Override
   public boolean isAggregatedValueFixedSize() {
-    return true;
+    // The sparse representation grows with the number of distinct values 
until it converts to the normal one
+    return _sp == 0;
   }
 
   @Override
   public int getMaxAggregatedValueByteSize() {
     // NOTE: For aggregated metrics, initial aggregated value might have not 
been generated. Returns the byte size
     //       based on p and sp.
-    return _maxByteSize > 0 ? _maxByteSize : HyperLogLogPlusUtils.byteSize(_p, 
_sp);
+    return _maxByteSize > 0 ? _maxByteSize : 
HyperLogLogPlusUtils.maxByteSize(_p, _sp);
   }
 
   @Override
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtils.java
index 6228fed3f52..f7761fa884d 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtils.java
@@ -18,22 +18,66 @@
  */
 package org.apache.pinot.segment.local.utils;
 
-import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
+import com.clearspring.analytics.stream.cardinality.RegisterSet;
+import com.google.common.base.Preconditions;
+import java.nio.ByteBuffer;
 
 
 public class HyperLogLogPlusUtils {
   private HyperLogLogPlusUtils() {
   }
 
-  /// Returns the byte size of the given HyperLogLog.
-  public static int byteSize(HyperLogLogPlus value) {
-    // 8 bytes header (p, sp, & register set size) & register set data
-    return value.sizeof() + 3 * Integer.BYTES;
+  /// Maximum supported `sp`. The sparse representation packs the `sp`-bit 
sparse index, a 6-bit run length and a flag
+  /// bit into an `int`, so a larger `sp` overflows and corrupts the registers 
once the sketch converts to the normal
+  /// representation, even though stream-lib accepts `sp` up to 32.
+  public static final int MAX_SP = 25;
+
+  private static final int MAX_VAR_INT_BYTES = 5;
+
+  /// Validates that `sp` does not exceed [#MAX_SP].
+  public static void validateSp(int sp) {
+    Preconditions.checkArgument(sp <= MAX_SP, "HyperLogLogPlus sp must be at 
most %s, got: %s", MAX_SP, sp);
+  }
+
+  /// Returns the maximum serialized size of a `HyperLogLogPlus` with the 
given `p` and `sp`: an upper bound of
+  /// `HyperLogLogPlus.getBytes().length` over every sketch with these 
parameters, not the length of any one of them.
+  ///
+  /// The serialized form is a 4-byte version, varints of `p`, `sp` and the 
format flag, then the format payload:
+  /// - Normal format: a varint of the register set byte size, then the 
register set. Its size depends only on `p`, so
+  ///   with `sp == 0` (sparse representation disabled) every sketch 
serializes to exactly the returned size.
+  /// - Sparse format (`sp > 0`, until the sketch converts to normal format): 
a varint of the sparse set length, then a
+  ///   delta varint of up to 5 bytes per entry. The sketch converts to normal 
format once the sparse set exceeds
+  ///   `0.75 * (1 << p)` entries, which bounds the entries a sparse sketch 
can serialize.
+  public static int maxByteSize(int p, int sp) {
+    int headerByteSize = Integer.BYTES + varIntSize(p) + varIntSize(sp) + 1;
+    int registerSetByteSize = RegisterSet.getSizeForCount(1 << p) * 
Integer.BYTES;
+    int normalByteSize = headerByteSize + varIntSize(registerSetByteSize) + 
registerSetByteSize;
+    if (sp == 0) {
+      return normalByteSize;
+    }
+    int sparseSetThreshold = (int) ((1 << p) * 0.75);
+    int sparseByteSize = headerByteSize + varIntSize(sparseSetThreshold) + 
sparseSetThreshold * MAX_VAR_INT_BYTES;
+    return Math.max(normalByteSize, sparseByteSize);
+  }
+
+  /// Returns the [#maxByteSize(int, int)] for the `p` and `sp` of the given 
serialized `HyperLogLogPlus`, read from its
+  /// header. Both the current format (a negative version followed by varints 
of `p` and `sp`) and the legacy format
+  /// without version (ints of `p` and `sp`) are supported. In the current 
format, each varint is a single byte because
+  /// `p` and `sp` are below 128: stream-lib rejects `sp` above 32, and `p` 
above 30 overflows the register count.
+  public static int maxByteSize(byte[] bytes) {
+    ByteBuffer buffer = ByteBuffer.wrap(bytes);
+    int version = buffer.getInt();
+    if (version < 0) {
+      int p = buffer.get();
+      int sp = buffer.get();
+      return maxByteSize(p, sp);
+    } else {
+      return maxByteSize(version, buffer.getInt());
+    }
   }
 
-  /// Returns the byte size of HyperLogLogPlus of a given p and sp.
-  public static int byteSize(int p, int sp) {
-    // 8 bytes header (p & sp) & register set data
-    return new HyperLogLogPlus(p, sp).sizeof() + 3 * Integer.BYTES;
+  /// Returns the number of bytes in the unsigned varint encoding of the given 
value.
+  private static int varIntSize(int value) {
+    return (Integer.SIZE - 1 - Integer.numberOfLeadingZeros(value | 1)) / 7 + 
1;
   }
 }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index 66f9355b3d4..f3b9fc3a809 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -53,6 +53,7 @@ import 
org.apache.pinot.segment.local.aggregator.ValueAggregator;
 import org.apache.pinot.segment.local.aggregator.ValueAggregatorFactory;
 import 
org.apache.pinot.segment.local.recordtransformer.SchemaConformingTransformer;
 import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
+import org.apache.pinot.segment.local.startree.StarTreeBuilderUtils;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.segment.spi.index.DictionaryIndexConfig;
 import org.apache.pinot.segment.spi.index.FieldIndexConfigs;
@@ -2320,6 +2321,18 @@ public final class TableConfigUtils {
             LOGGER.warn("StarTreeIndex config duplication: {} already matches 
existing function column pair: {}. ",
                 columnPair, storedType);
           }
+          // Create the value aggregator the same way the star-tree builder 
does, so that invalid function parameters
+          // fail the config validation instead of the segment creation
+          AggregationFunctionType storedFunctionType = 
storedType.getFunctionType();
+          Map<String, Object> functionParameters = 
aggregationConfig.getFunctionParameters();
+          try {
+            ValueAggregatorFactory.getValueAggregator(storedFunctionType,
+                
StarTreeBuilderUtils.expressionContextFromFunctionParameters(storedFunctionType,
+                    functionParameters != null ? functionParameters : 
Map.of()));
+          } catch (Exception e) {
+            throw new IllegalStateException("Invalid function parameters in 
StarTreeIndex config: " + aggregationConfig,
+                e);
+          }
           String column = columnPair.getColumn();
           if (!column.equals(AggregationFunctionColumnPair.STAR)) {
             aggregatedColumns.add(column);
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregatorTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregatorTest.java
new file mode 100644
index 00000000000..12efb475851
--- /dev/null
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/DistinctCountHLLPlusValueAggregatorTest.java
@@ -0,0 +1,55 @@
+/**
+ * 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.segment.local.aggregator;
+
+import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
+import java.util.List;
+import org.apache.pinot.segment.local.utils.HyperLogLogPlusUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+public class DistinctCountHLLPlusValueAggregatorTest {
+
+  @Test
+  public void testMaxAggregatedValueByteSizeForSerializedValues() {
+    // Default p and sp, which differ from the ones of the serialized values
+    DistinctCountHLLPlusValueAggregator aggregator = new 
DistinctCountHLLPlusValueAggregator(List.of());
+    int p = 12;
+    int sp = 20;
+
+    HyperLogLogPlus value = 
aggregator.getInitialAggregatedValue(serialize(aggregator, p, sp, 0, 10));
+    value = aggregator.applyRawValue(value, serialize(aggregator, p, sp, 10, 
2000));
+    aggregator.getInitialAggregatedValue(serialize(aggregator, p, sp, 0, 5));
+
+    int maxByteSize = aggregator.getMaxAggregatedValueByteSize();
+    assertEquals(maxByteSize, HyperLogLogPlusUtils.maxByteSize(p, sp));
+    assertTrue(aggregator.serializeAggregatedValue(value).length <= 
maxByteSize);
+  }
+
+  private static byte[] serialize(DistinctCountHLLPlusValueAggregator 
aggregator, int p, int sp, int start, int end) {
+    HyperLogLogPlus hllPlus = new HyperLogLogPlus(p, sp);
+    for (int i = start; i < end; i++) {
+      hllPlus.offer(i);
+    }
+    return aggregator.serializeAggregatedValue(hllPlus);
+  }
+}
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/ValueAggregatorTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/ValueAggregatorTest.java
index f98fac81a8c..238a80302a5 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/ValueAggregatorTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/aggregator/ValueAggregatorTest.java
@@ -54,6 +54,10 @@ public class ValueAggregatorTest {
         {AggregationFunctionType.DISTINCTCOUNTBITMAP, List.of(), false},
         {AggregationFunctionType.DISTINCTCOUNTHLL, List.of(), true},
         {AggregationFunctionType.DISTINCTCOUNTHLLPLUS, List.of(), true},
+        {AggregationFunctionType.DISTINCTCOUNTHLLPLUS, List.of(
+            ExpressionContext.forLiteral(Literal.intValue(12)),
+            ExpressionContext.forLiteral(Literal.intValue(20))
+        ), false},
         {AggregationFunctionType.DISTINCTCOUNTULL, List.of(), true},
         {AggregationFunctionType.DISTINCTCOUNTTHETASKETCH, List.of(), false},
         {AggregationFunctionType.DISTINCTCOUNTTUPLESKETCH, List.of(), true},
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplIngestionAggregationTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplIngestionAggregationTest.java
index eab8784a9e0..9d8799a1cad 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplIngestionAggregationTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/mutable/MutableSegmentImplIngestionAggregationTest.java
@@ -19,6 +19,7 @@
 package org.apache.pinot.segment.local.indexsegment.mutable;
 
 import com.clearspring.analytics.stream.cardinality.HyperLogLog;
+import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.ArrayList;
@@ -195,6 +196,33 @@ public class MutableSegmentImplIngestionAggregationTest {
     mutableSegmentImpl.destroy();
   }
 
+  @Test
+  public void testDistinctCountHLLPlus()
+      throws Exception {
+    String m1 = "hllPlus1";
+
+    Schema schema = getSchemaBuilder().addMetric(m1, DataType.BYTES).build();
+    MutableSegmentImpl mutableSegmentImpl =
+        MutableSegmentImplTestUtils.createMutableSegmentImpl(schema, 
Set.of(m1), VAR_LENGTH_SET, INVERTED_INDEX_SET,
+            List.of(new AggregationConfig(m1, "distinctCountHLLPlus(metric, 
12)")));
+
+    Map<String, HyperLogLogPlus> expectedValues = 
addRowsDistinctCountHLLPlus(998, mutableSegmentImpl);
+
+    int numDocsIndexed = mutableSegmentImpl.getNumDocsIndexed();
+    assertTrue(numDocsIndexed < NUM_ROWS);
+    assertEquals(numDocsIndexed, expectedValues.size());
+
+    GenericRow reuse = new GenericRow();
+    for (int docId = 0; docId < numDocsIndexed; docId++) {
+      GenericRow row = mutableSegmentImpl.getRecord(docId, reuse);
+      HyperLogLogPlus actualHLLPlus = HyperLogLogPlus.Builder.build((byte[]) 
row.getValue(m1));
+      HyperLogLogPlus expectedHLLPlus = expectedValues.get(buildKey(row));
+      assertEquals(actualHLLPlus.cardinality(), expectedHLLPlus.cardinality());
+    }
+
+    mutableSegmentImpl.destroy();
+  }
+
   @Test
   public void testCount()
       throws Exception {
@@ -276,6 +304,25 @@ public class MutableSegmentImplIngestionAggregationTest {
     return valueMap;
   }
 
+  private Map<String, HyperLogLogPlus> addRowsDistinctCountHLLPlus(long seed, 
MutableSegmentImpl mutableSegmentImpl)
+      throws Exception {
+    Map<String, HyperLogLogPlus> valueMap = new HashMap<>();
+
+    Random random = new Random(seed);
+    for (int i = 0; i < NUM_ROWS; i++) {
+      GenericRow row = getRow(random, 1);
+      String key = buildKey(row);
+
+      int metricValue = random.nextInt(5000000);
+      row.putValue(METRIC, metricValue);
+      mutableSegmentImpl.index(row, METADATA);
+
+      valueMap.computeIfAbsent(key, k -> new 
HyperLogLogPlus(12)).offer(metricValue);
+    }
+
+    return valueMap;
+  }
+
   private Map<String, BigDecimal> addRowsSumPrecision(long seed, 
MutableSegmentImpl mutableSegmentImpl)
       throws Exception {
     Map<String, BigDecimal> valueMap = new HashMap<>();
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtilsTest.java
new file mode 100644
index 00000000000..12e79cf09f0
--- /dev/null
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/HyperLogLogPlusUtilsTest.java
@@ -0,0 +1,89 @@
+/**
+ * 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.segment.local.utils;
+
+import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+
+
+public class HyperLogLogPlusUtilsTest {
+
+  @Test
+  public void testMaxByteSizeNormalFormat()
+      throws IOException {
+    for (int p = 4; p <= 16; p++) {
+      int maxByteSize = HyperLogLogPlusUtils.maxByteSize(p, 0);
+      HyperLogLogPlus hllPlus = new HyperLogLogPlus(p);
+      assertEquals(hllPlus.getBytes().length, maxByteSize);
+      for (int i = 0; i < 1000; i++) {
+        hllPlus.offer(i);
+      }
+      byte[] bytes = hllPlus.getBytes();
+      assertEquals(bytes.length, maxByteSize);
+      assertEquals(HyperLogLogPlusUtils.maxByteSize(bytes), maxByteSize);
+    }
+  }
+
+  @Test
+  public void testMaxByteSizeSparseFormat()
+      throws IOException {
+    for (int p = 4; p <= 16; p += 2) {
+      int m = 1 << p;
+      int sparseSetThreshold = (int) (m * 0.75);
+      int[] numValuesToFill = new int[]{0, 1, m / 8, m / 4, m / 2, 
sparseSetThreshold, sparseSetThreshold + 1, 2 * m};
+      for (int sp : new int[]{p, 20, HyperLogLogPlusUtils.MAX_SP}) {
+        int maxByteSize = HyperLogLogPlusUtils.maxByteSize(p, sp);
+        HyperLogLogPlus hllPlus = new HyperLogLogPlus(p, sp);
+        int numValues = 0;
+        for (int numValuesToReach : numValuesToFill) {
+          for (; numValues < numValuesToReach; numValues++) {
+            hllPlus.offer(numValues);
+          }
+          byte[] bytes = hllPlus.getBytes();
+          assertTrue(bytes.length <= maxByteSize,
+              String.format("p: %d, sp: %d, numValues: %d, serialized size: %d 
exceeds max byte size: %d", p, sp,
+                  numValues, bytes.length, maxByteSize));
+          assertEquals(HyperLogLogPlusUtils.maxByteSize(bytes), maxByteSize);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testMaxByteSizeLegacyFormat() {
+    for (int sp : new int[]{0, 20}) {
+      byte[] legacyHeader = ByteBuffer.allocate(2 * 
Integer.BYTES).putInt(12).putInt(sp).array();
+      assertEquals(HyperLogLogPlusUtils.maxByteSize(legacyHeader), 
HyperLogLogPlusUtils.maxByteSize(12, sp));
+    }
+  }
+
+  @Test
+  public void testValidateSp() {
+    HyperLogLogPlusUtils.validateSp(0);
+    HyperLogLogPlusUtils.validateSp(25);
+    assertThrows(IllegalArgumentException.class, () -> 
HyperLogLogPlusUtils.validateSp(26));
+    assertThrows(IllegalArgumentException.class, () -> 
HyperLogLogPlusUtils.validateSp(32));
+  }
+}
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index 2bc88730796..73edeac2f18 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -842,6 +842,40 @@ public class TableConfigUtilsTest {
       fail("Should not fail due to valid aggregation function", e);
     }
 
+    // distinctcounthllplus, expect sp above 25 to not validate
+    schema = new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addMetric("d1", 
DataType.BYTES).build();
+
+    aggregationConfigs = List.of(new AggregationConfig("d1", 
"DISTINCTCOUNTHLLPLUS(s1, 12, 26)"));
+    ingestionConfig.setAggregationConfigs(aggregationConfigs);
+    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName("myTable_REALTIME")
+        .setTimeColumnName("timeColumn")
+        .setIngestionConfig(ingestionConfig)
+        .setNoDictionaryColumns(List.of("d1"))
+        .build();
+
+    try {
+      TableConfigUtils.validateIngestionConfig(tableConfig, schema);
+      fail("Should fail due to sp above 25");
+    } catch (IllegalStateException e) {
+      // expected
+    }
+
+    // distinctcounthllplus, expect sp above 0 to not validate because the 
aggregated value is not of fixed size
+    aggregationConfigs = List.of(new AggregationConfig("d1", 
"DISTINCTCOUNTHLLPLUS(s1, 12, 20)"));
+    ingestionConfig.setAggregationConfigs(aggregationConfigs);
+    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName("myTable_REALTIME")
+        .setTimeColumnName("timeColumn")
+        .setIngestionConfig(ingestionConfig)
+        .setNoDictionaryColumns(List.of("d1"))
+        .build();
+
+    try {
+      TableConfigUtils.validateIngestionConfig(tableConfig, schema);
+      fail("Should fail due to sp above 0");
+    } catch (IllegalStateException e) {
+      assertTrue(e.getMessage().contains("must have fixed size aggregated 
value"), e.getMessage());
+    }
+
     // distinctcounthll, expect not specified log2m argument to default to 8
     schema = new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addMetric("d1", 
DataType.BYTES).build();
 
@@ -2569,6 +2603,31 @@ public class TableConfigUtilsTest {
     assertTrue(e.getMessage().contains("Duplicate function column pair"));
   }
 
+  @Test
+  public void testValidateStarTreeIndexFunctionParameters() {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addSingleValueDimension("myCol", DataType.STRING)
+        .build();
+
+    StarTreeIndexConfig starTreeIndexConfig = new 
StarTreeIndexConfig(List.of("myCol"), null, null, List.of(
+        new StarTreeAggregationConfig("myCol", "DISTINCTCOUNTHLLPLUS",
+            Map.of(Constants.HLLPLUS_ULL_P_KEY, 12, Constants.HLLPLUS_SP_KEY, 
25), null, null, null, null, null)), 1);
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setStarTreeIndexConfigs(List.of(starTreeIndexConfig))
+        .build();
+    TableConfigUtils.validate(tableConfig, schema);
+
+    starTreeIndexConfig = new StarTreeIndexConfig(List.of("myCol"), null, 
null, List.of(
+        new StarTreeAggregationConfig("myCol", "DISTINCTCOUNTHLLPLUS",
+            Map.of(Constants.HLLPLUS_ULL_P_KEY, 12, Constants.HLLPLUS_SP_KEY, 
26), null, null, null, null, null)), 1);
+    TableConfig invalidSpTableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setStarTreeIndexConfigs(List.of(starTreeIndexConfig))
+        .build();
+    IllegalStateException e =
+        expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validate(invalidSpTableConfig, schema));
+    assertTrue(e.getMessage().contains("Invalid function parameters in 
StarTreeIndex config"));
+  }
+
   @Test
   public void testValidateRetentionConfig() {
     Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)


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

Reply via email to