This is an automated email from the ASF dual-hosted git repository.

xiangfu0 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 a13a7ce49ab Fix group key generation when 
optimizeMaxInitialResultHolderCapacity shrinks the cardinality product (#19379)
a13a7ce49ab is described below

commit a13a7ce49ab64ef9f89ea6440c4c60c6f952ea7c
Author: Xiang Fu <[email protected]>
AuthorDate: Wed Sep 2 17:32:11 2026 -0700

    Fix group key generation when optimizeMaxInitialResultHolderCapacity 
shrinks the cardinality product (#19379)
    
    With optimizeMaxInitialResultHolderCapacity=true, 
DictionaryBasedGroupKeyGenerator used the
    IN/EQ-predicate-shrunk cardinality product both for holder type selection 
and for the group id
    upper bound. ArrayBasedHolder uses raw dictionary-id mixed-radix products 
as group ids, so any
    matching dictionary id beyond the shrunk bound threw 
ArrayIndexOutOfBoundsException, and
    resetting longOverflow could downgrade the holder to int/long raw keys that 
overflow for the
    full cardinalities, silently colliding distinct groups.
    
    Holder type selection now always uses the full cardinality product; the 
predicate-derived value
    only caps the dense group id upper bound, and ArrayBasedHolder falls back 
to IntMapBasedHolder
    when the bound shrinks below the product. Multi-value group-by expressions 
are excluded from
    the predicate-derived bound (every value inside a matching row becomes a 
group), enforced both
    in DefaultGroupByExecutor for all generators and inside 
DictionaryBasedGroupKeyGenerator.
---
 .../groupby/DefaultGroupByExecutor.java            |  18 ++-
 .../groupby/DictionaryBasedGroupKeyGenerator.java  |  69 ++++++----
 .../DictionaryBasedGroupKeyGeneratorTest.java      | 153 +++++++++++++++++++++
 3 files changed, 205 insertions(+), 35 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DefaultGroupByExecutor.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DefaultGroupByExecutor.java
index f4349c1171a..8a4015cd84d 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DefaultGroupByExecutor.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DefaultGroupByExecutor.java
@@ -97,10 +97,6 @@ public class DefaultGroupByExecutor implements 
GroupByExecutor {
     // Initialize group key generator
     int numGroupsLimit = queryContext.getNumGroupsLimit();
     int maxInitialResultHolderCapacity = 
queryContext.getMaxInitialResultHolderCapacity();
-    Map<ExpressionContext, Integer> groupByExpressionSizesFromPredicates = 
null;
-    if (queryContext.isOptimizeMaxInitialResultHolderCapacity()) {
-      groupByExpressionSizesFromPredicates = 
getGroupByExpressionSizesFromPredicates(queryContext);
-    }
     if (groupKeyGenerator != null) {
       _groupKeyGenerator = groupKeyGenerator;
     } else if (groupingSets) {
@@ -108,6 +104,9 @@ public class DefaultGroupByExecutor implements 
GroupByExecutor {
           new GroupingSetsGroupKeyGenerator(projectOperator, 
groupByExpressions, queryContext.getGroupingSets(),
               numGroupsLimit, _nullHandlingEnabled);
     } else {
+      Map<ExpressionContext, Integer> groupByExpressionSizesFromPredicates =
+          queryContext.isOptimizeMaxInitialResultHolderCapacity()
+              ? getGroupByExpressionSizesFromPredicates(queryContext, 
projectOperator) : null;
       // Null handling does not steer this choice: every generator below gives 
a null an id of its own, so the
       // encoding of the group-by columns decides on its own which one to use.
       if (hasNoDictionaryGroupByExpression) {
@@ -149,7 +148,11 @@ public class DefaultGroupByExecutor implements 
GroupByExecutor {
   /// 1. If the filter context is null or lacks GroupBy expressions, return 
null.
   /// 2. Ensure the top-level filter context consists solely of AND-type 
filters; other types for example OR we cannot
   ///    guarantee deterministic sizes for GroupBy expressions.
-  private Map<ExpressionContext, Integer> 
getGroupByExpressionSizesFromPredicates(QueryContext queryContext) {
+  /// 3. Skip multi-value GroupBy expressions: a row matching an IN/EQ 
predicate on a multi-value column contributes
+  ///    one group per value inside the row (not only the matching values), so 
the predicate size does not bound the
+  ///    number of distinct groups.
+  private Map<ExpressionContext, Integer> 
getGroupByExpressionSizesFromPredicates(QueryContext queryContext,
+      BaseProjectOperator<?> projectOperator) {
     FilterContext filterContext = queryContext.getFilter();
     if (filterContext == null || queryContext.getGroupByExpressions() == null) 
{
       return null;
@@ -183,11 +186,14 @@ public class DefaultGroupByExecutor implements 
GroupByExecutor {
         ));
 
     // Populate the group-by expressions with sizes from the predicate map
+    // NOTE: The merge function handles duplicate group-by expressions (e.g. 
GROUP BY c0, c0)
     return queryContext.getGroupByExpressions().stream()
         .filter(predicateSizeMap::containsKey)
+        .filter(expression -> 
projectOperator.getResultColumnContext(expression).isSingleValue())
         .collect(Collectors.toMap(
             expression -> expression,
-            expression -> predicateSizeMap.getOrDefault(expression, null)
+            expression -> predicateSizeMap.getOrDefault(expression, null),
+            Integer::min
         ));
   }
 
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGenerator.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGenerator.java
index 823f11ce971..32fdf9dc214 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGenerator.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGenerator.java
@@ -26,12 +26,10 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import it.unimi.dsi.fastutil.objects.ObjectIterator;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.function.ToIntFunction;
 import javax.annotation.Nullable;
-import org.apache.commons.lang3.tuple.Pair;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.core.common.BlockValSet;
 import org.apache.pinot.core.operator.BaseProjectOperator;
@@ -66,6 +64,11 @@ import org.roaringbitmap.RoaringBitmap;
 ///
 /// All the logic is maintained internally, and to the outside world, the 
group ids are always int type, and are
 /// bounded by the number of groups limit (globalGroupIdUpperBound is always 
smaller or equal to numGroupsLimit).
+///
+/// When IN/EQ predicates on the group-by expressions prove an upper bound of 
the number of distinct groups smaller
+/// than the cardinality product (see optimizeMaxInitialResultHolderCapacity), 
the bound shrinks
+/// globalGroupIdUpperBound but never the raw key space, so a map based holder 
is used even when the bound is below
+/// the ARRAY_BASED threshold.
 public class DictionaryBasedGroupKeyGenerator implements GroupKeyGenerator {
   // NOTE: map size = map capacity (power of 2) * load factor
   private static final int INITIAL_MAP_SIZE = (int) ((1 << 9) * 0.75f);
@@ -114,6 +117,9 @@ public class DictionaryBasedGroupKeyGenerator implements 
GroupKeyGenerator {
   private final int _globalGroupIdUpperBound;
   private final RawKeyHolder _rawKeyHolder;
 
+  /// @param groupByExpressionSizesFromPredicates IN/EQ predicate sizes for 
the group-by expressions, used to bound
+  ///        the number of distinct groups. Sizes for multi-value expressions 
are ignored: every value inside a
+  ///        matching row becomes a group, so the predicate size does not 
bound the group count.
   public DictionaryBasedGroupKeyGenerator(BaseProjectOperator<?> 
projectOperator,
       ExpressionContext[] groupByExpressions, int numGroupsLimit, int 
arrayBasedThreshold,
       boolean nullHandlingEnabled, @Nullable Map<ExpressionContext, Integer> 
groupByExpressionSizesFromPredicates) {
@@ -130,7 +136,6 @@ public class DictionaryBasedGroupKeyGenerator implements 
GroupKeyGenerator {
     // no need to intern dictionary values when there is only one group by 
expression because
     // only one call will be made to the dictionary to extract each raw value.
     _internedDictionaryValues = _numGroupByExpressions > 1 ? new 
Object[_numGroupByExpressions][] : null;
-    Map<ExpressionContext, Integer> cardinalityMap = new 
HashMap<>(_numGroupByExpressions);
     long cardinalityProduct = 1L;
     boolean longOverflow = false;
     for (int i = 0; i < _numGroupByExpressions; i++) {
@@ -158,7 +163,6 @@ public class DictionaryBasedGroupKeyGenerator implements 
GroupKeyGenerator {
         }
       }
       _cardinalities[i] = cardinality;
-      cardinalityMap.put(groupByExpression, cardinality);
       if (_internedDictionaryValues != null && cardinality < 
MAX_DICTIONARY_INTERN_TABLE_SIZE) {
         _internedDictionaryValues[i] = new Object[cardinality];
       }
@@ -180,35 +184,40 @@ public class DictionaryBasedGroupKeyGenerator implements 
GroupKeyGenerator {
       _nullReplacedSingleValueDictIds = null;
       _nullReplacedMultiValueDictIds = null;
     }
-    if (groupByExpressionSizesFromPredicates != null) {
-      Pair<Boolean, Long> optimizedCardinality = 
getOptimizedGroupByCardinality(groupByExpressionSizesFromPredicates,
-          cardinalityMap);
-      if (optimizedCardinality.getLeft() && optimizedCardinality.getRight() != 
null) {
-        longOverflow = false;
-        cardinalityProduct = Math.min(optimizedCardinality.getRight(), 
cardinalityProduct);
-      }
-    }
+    // An IN/EQ predicate on a group-by column bounds the number of distinct 
groups, so it can shrink the group id
+    // upper bound (and thus the result holder sizes). It must not influence 
the holder type selection though: raw
+    // keys are mixed-radix products over the FULL dictionary cardinalities 
regardless of the filter, so the
+    // int/long/array-map decision (raw key range) has to be based on the full 
cardinality product.
+    long optimizedGroupCountUpperBound = groupByExpressionSizesFromPredicates 
!= null
+        ? getOptimizedGroupByCardinality(groupByExpressionSizesFromPredicates) 
: Long.MAX_VALUE;
+    int cappedNumGroupsLimit = (int) Math.min(numGroupsLimit, 
optimizedGroupCountUpperBound);
     // NOTE: We need to clean up the thread-local map before using it in case 
RawKeyHolder.close() is not called
     //       for the previous segment
     // TODO: Ensure RawKeyHolder.close()
     if (longOverflow) {
       // ArrayMapBasedHolder
-      _globalGroupIdUpperBound = numGroupsLimit;
+      _globalGroupIdUpperBound = cappedNumGroupsLimit;
       Object2IntOpenHashMap<IntArray> groupIdMap = 
THREAD_LOCAL_INT_ARRAY_MAP.get();
       clearAndTrim(groupIdMap);
       _rawKeyHolder = new ArrayMapBasedHolder(groupIdMap);
     } else {
       if (cardinalityProduct > Integer.MAX_VALUE) {
         // LongMapBasedHolder
-        _globalGroupIdUpperBound = numGroupsLimit;
+        _globalGroupIdUpperBound = cappedNumGroupsLimit;
         Long2IntOpenHashMap groupIdMap = THREAD_LOCAL_LONG_MAP.get();
         clearAndTrim(groupIdMap);
         _rawKeyHolder = new LongMapBasedHolder(groupIdMap);
       } else {
-        _globalGroupIdUpperBound = Math.min((int) cardinalityProduct, 
numGroupsLimit);
+        _globalGroupIdUpperBound = (int) Math.min(cardinalityProduct, 
cappedNumGroupsLimit);
         // arrayBaseHolder fails with ArrayIndexOutOfBoundsException if 
numGroupsLimit < cardinalityProduct
-        // because array doesn't fit all (potentially unsorted) values
-        if (cardinalityProduct > arrayBasedThreshold || numGroupsLimit < 
cardinalityProduct) {
+        // because array doesn't fit all (potentially unsorted) values.
+        // ArrayBasedHolder uses the raw key directly as the group id, so it 
is only valid when the group id space
+        // covers the full cardinality product; when the predicate-based 
optimization proves a smaller upper bound,
+        // fall back to IntMapBasedHolder which maps the sparse raw keys onto 
dense group ids. This deliberately
+        // trades a hash lookup per row for smaller result holders, which is 
what the opt-in
+        // optimizeMaxInitialResultHolderCapacity query option asks for.
+        if (cardinalityProduct > arrayBasedThreshold || numGroupsLimit < 
cardinalityProduct
+            || optimizedGroupCountUpperBound < cardinalityProduct) {
           // IntMapBasedHolder
           IntGroupIdMap groupIdMap = THREAD_LOCAL_INT_MAP.get();
           groupIdMap.clearAndTrim();
@@ -220,20 +229,22 @@ public class DictionaryBasedGroupKeyGenerator implements 
GroupKeyGenerator {
     }
   }
 
-  private Pair<Boolean, Long> 
getOptimizedGroupByCardinality(Map<ExpressionContext, Integer> 
groupByExpressionSizes,
-      Map<ExpressionContext, Integer> columnCardinalityMap) {
-    long maxInitialResultHolderCapacity = 1L;
-    for (Map.Entry<ExpressionContext, Integer> entry : 
columnCardinalityMap.entrySet()) {
-      Integer cardinality = entry.getValue();
-      Integer size = groupByExpressionSizes.get(entry.getKey());
-      int minSize = size != null ? Math.min(size, cardinality) : cardinality;
-      if (maxInitialResultHolderCapacity > Long.MAX_VALUE / minSize) {
-        return Pair.of(false, null);
-      } else {
-        maxInitialResultHolderCapacity *= minSize;
+  /// Returns the upper bound of the number of distinct groups derived from 
the IN/EQ predicate sizes on the group-by
+  /// expressions, or [Long#MAX_VALUE] if the bound overflows long (i.e. no 
usable bound). Multi-value expressions
+  /// always contribute their full cardinality: every value inside a matching 
row becomes a group, so the predicate
+  /// size does not bound their group count. Iterating the expression array 
(rather than a deduplicated map) keeps
+  /// the bound comparable to the cardinality product when the same expression 
appears multiple times.
+  private long getOptimizedGroupByCardinality(Map<ExpressionContext, Integer> 
groupByExpressionSizes) {
+    long groupCountUpperBound = 1L;
+    for (int i = 0; i < _numGroupByExpressions; i++) {
+      Integer size = _isSingleValueColumn[i] ? 
groupByExpressionSizes.get(_groupByExpressions[i]) : null;
+      int minSize = size != null ? Math.min(size, _cardinalities[i]) : 
_cardinalities[i];
+      if (minSize <= 0 || groupCountUpperBound > Long.MAX_VALUE / minSize) {
+        return Long.MAX_VALUE;
       }
+      groupCountUpperBound *= minSize;
     }
-    return Pair.of(true, maxInitialResultHolderCapacity);
+    return groupCountUpperBound;
   }
 
   private static void clearAndTrim(Long2IntOpenHashMap map) {
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGeneratorTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGeneratorTest.java
index f77711f320e..05819441b80 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGeneratorTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/DictionaryBasedGroupKeyGeneratorTest.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
@@ -53,6 +54,7 @@ import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -165,6 +167,14 @@ public class DictionaryBasedGroupKeyGeneratorTest {
     _valueBlock = _projectOperator.nextBlock();
   }
 
+  // Clear the thread-local maps so that the holder type assertions on them 
are order-independent
+  @BeforeMethod
+  public void clearThreadLocalMaps() {
+    DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_MAP.get().clearAndTrim();
+    DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_LONG_MAP.get().clear();
+    DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_ARRAY_MAP.get().clear();
+  }
+
   @Test
   public void testArrayBasedSingleValue() {
     // Cardinality product (100) smaller than arrayBasedThreshold
@@ -480,6 +490,142 @@ public class DictionaryBasedGroupKeyGeneratorTest {
     dictionaryBasedGroupKeyGenerator.close();
   }
 
+  @Test
+  public void testOptimizedUpperBoundSmallerThanCardinalityProduct() {
+    // Regression test: with optimizeMaxInitialResultHolderCapacity, an EQ/IN 
predicate on the group-by column can
+    // shrink the group id upper bound below the cardinality product (here to 
1). ArrayBasedHolder uses the raw
+    // dictionary-id based key directly as the group id, so it must not be 
selected with the shrunk bound - doing so
+    // threw ArrayIndexOutOfBoundsException for any matching dictionary id 
beyond the bound. IntMapBasedHolder must be
+    // used instead to map the sparse raw keys onto dense group ids.
+    String[] groupByColumns = {"s1"};
+    ExpressionContext[] expressions = getExpressions(groupByColumns);
+    // Simulate WHERE s1 = <value> (predicate size 1)
+    DictionaryBasedGroupKeyGenerator dictionaryBasedGroupKeyGenerator =
+        new DictionaryBasedGroupKeyGenerator(_projectOperator, expressions,
+            Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT,
+            Server.DEFAULT_QUERY_EXECUTOR_MAX_INITIAL_RESULT_HOLDER_CAPACITY, 
false, Map.of(expressions[0], 1));
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getGlobalGroupKeyUpperBound(), 1, 
_errorMessage);
+
+    // The block contains 2 unique rows whose dictionary ids exceed the 
optimized upper bound; generating group keys
+    // must not throw
+    dictionaryBasedGroupKeyGenerator.generateKeysForBlock(_valueBlock, 
SV_GROUP_KEY_BUFFER);
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getCurrentGroupKeyUpperBound(), 
1, _errorMessage);
+    // Only 1 group can be generated; the other unique row exceeds the upper 
bound and gets INVALID_ID
+    for (int i = 0; i < NUM_GROUPS; i += 2) {
+      assertEquals(SV_GROUP_KEY_BUFFER[i], 0, _errorMessage);
+      assertEquals(SV_GROUP_KEY_BUFFER[i + 1], GroupKeyGenerator.INVALID_ID, 
_errorMessage);
+    }
+    testGetGroupKeys(dictionaryBasedGroupKeyGenerator.getGroupKeys(), 1);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_MAP.get().size(),
 1);
+    dictionaryBasedGroupKeyGenerator.close();
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_MAP.get().size(),
 0);
+  }
+
+  @Test
+  public void testOptimizedUpperBoundKeepsLongMapBasedHolder() {
+    // The predicate-based optimization must not downgrade the holder type: 
raw keys are mixed-radix products over the
+    // full cardinality product (10,000,000,000 here, beyond 
Integer.MAX_VALUE), so LongMapBasedHolder is required
+    // even though the predicates prove fewer groups. Downgrading to 
IntMapBasedHolder would overflow the int raw keys
+    // and silently produce wrong groups.
+    String[] groupByColumns = {"s1", "s2", "s3", "s4", "s5"};
+    ExpressionContext[] expressions = getExpressions(groupByColumns);
+    DictionaryBasedGroupKeyGenerator dictionaryBasedGroupKeyGenerator =
+        new DictionaryBasedGroupKeyGenerator(_projectOperator, expressions,
+            Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT,
+            Server.DEFAULT_QUERY_EXECUTOR_MAX_INITIAL_RESULT_HOLDER_CAPACITY, 
false, Map.of(expressions[0], 2));
+    // Optimized upper bound (2 * 100^4 = 200,000,000) is capped by 
numGroupsLimit
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getGlobalGroupKeyUpperBound(),
+        Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT, _errorMessage);
+
+    // Test group key generation
+    dictionaryBasedGroupKeyGenerator.generateKeysForBlock(_valueBlock, 
SV_GROUP_KEY_BUFFER);
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getCurrentGroupKeyUpperBound(), 
2, _errorMessage);
+    compareSingleValueBuffer();
+    testGetGroupKeys(dictionaryBasedGroupKeyGenerator.getGroupKeys(), 2);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_LONG_MAP.get().size(),
 2);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_MAP.get().size(),
 0);
+    dictionaryBasedGroupKeyGenerator.close();
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_LONG_MAP.get().size(),
 0);
+  }
+
+  @Test
+  public void testOptimizedUpperBoundKeepsArrayMapBasedHolder() {
+    // Same as above for the long overflow case: the full cardinality product 
(100^10) exceeds Long.MAX_VALUE, so
+    // ArrayMapBasedHolder is required even though the predicates prove fewer 
groups. Downgrading to a long raw key
+    // based holder would overflow the long raw keys and silently produce 
wrong groups.
+    String[] groupByColumns = {"s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", 
"s9", "s10"};
+    ExpressionContext[] expressions = getExpressions(groupByColumns);
+    DictionaryBasedGroupKeyGenerator dictionaryBasedGroupKeyGenerator =
+        new DictionaryBasedGroupKeyGenerator(_projectOperator, expressions,
+            Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT,
+            Server.DEFAULT_QUERY_EXECUTOR_MAX_INITIAL_RESULT_HOLDER_CAPACITY, 
false, Map.of(expressions[0], 2));
+    // Optimized upper bound (2 * 100^9) is capped by numGroupsLimit
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getGlobalGroupKeyUpperBound(),
+        Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT, _errorMessage);
+
+    // Test group key generation
+    dictionaryBasedGroupKeyGenerator.generateKeysForBlock(_valueBlock, 
SV_GROUP_KEY_BUFFER);
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getCurrentGroupKeyUpperBound(), 
2, _errorMessage);
+    compareSingleValueBuffer();
+    testGetGroupKeys(dictionaryBasedGroupKeyGenerator.getGroupKeys(), 2);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_ARRAY_MAP.get().size(),
 2);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_LONG_MAP.get().size(),
 0);
+    dictionaryBasedGroupKeyGenerator.close();
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_ARRAY_MAP.get().size(),
 0);
+  }
+
+  @Test
+  public void 
testOptimizedUpperBoundMatchingCardinalityProductKeepsArrayBasedHolder() {
+    // When the predicates do not prove fewer groups than the cardinality 
product, ArrayBasedHolder must still be
+    // selected (the optimization must not deoptimize the direct-addressed 
holder)
+    String[] groupByColumns = {"s1"};
+    ExpressionContext[] expressions = getExpressions(groupByColumns);
+    DictionaryBasedGroupKeyGenerator dictionaryBasedGroupKeyGenerator =
+        new DictionaryBasedGroupKeyGenerator(_projectOperator, expressions,
+            Server.DEFAULT_QUERY_EXECUTOR_NUM_GROUPS_LIMIT,
+            Server.DEFAULT_QUERY_EXECUTOR_MAX_INITIAL_RESULT_HOLDER_CAPACITY, 
false,
+            Map.of(expressions[0], UNIQUE_ROWS));
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getGlobalGroupKeyUpperBound(), 
UNIQUE_ROWS, _errorMessage);
+    // ArrayBasedHolder reports the full upper bound before generating any 
keys; the map-based holders report 0
+    
assertEquals(dictionaryBasedGroupKeyGenerator.getCurrentGroupKeyUpperBound(), 
UNIQUE_ROWS, _errorMessage);
+
+    // Test group key generation
+    dictionaryBasedGroupKeyGenerator.generateKeysForBlock(_valueBlock, 
SV_GROUP_KEY_BUFFER);
+    compareSingleValueBuffer();
+    testGetGroupKeys(dictionaryBasedGroupKeyGenerator.getGroupKeys(), 2);
+    
assertEquals(DictionaryBasedGroupKeyGenerator.THREAD_LOCAL_INT_MAP.get().size(),
 0);
+    dictionaryBasedGroupKeyGenerator.close();
+  }
+
+  @Test
+  public void testOptimizedUpperBoundIgnoresMultiValuePredicates() {
+    // A row matching an IN/EQ predicate on a multi-value column contributes 
one group per value inside the row, not
+    // only the matching values, so the predicate size must not shrink the 
group id upper bound for multi-value
+    // group-by expressions
+    String query = "SELECT COUNT(*) FROM testTable WHERE m1 = 1 GROUP BY m1";
+    QueryContext queryContext = 
QueryContextConverterUtils.getQueryContext(query);
+    ExpressionContext[] expressions = getExpressions(new String[]{"m1"});
+    // NOTE: Use the executors one after the other - concurrently alive 
generators could share a thread-local map
+    DefaultGroupByExecutor groupByExecutor = new 
DefaultGroupByExecutor(queryContext, expressions, _projectOperator);
+    int upperBound = 
groupByExecutor.getGroupKeyGenerator().getGlobalGroupKeyUpperBound();
+    groupByExecutor.process(_valueBlock);
+    int numGroups = groupByExecutor.getNumGroups();
+    // Precondition: with more groups than the EQ predicate size (1), a shrunk 
bound would actually drop groups. The
+    // block holds 2 rows with disjoint multi-value sets, so this holds for 
every random fixture.
+    assertTrue(numGroups > 1, _errorMessage);
+
+    QueryContext optimizedQueryContext = 
QueryContextConverterUtils.getQueryContext(query);
+    optimizedQueryContext.setOptimizeMaxInitialResultHolderCapacity(true);
+    DefaultGroupByExecutor optimizedGroupByExecutor =
+        new DefaultGroupByExecutor(optimizedQueryContext, expressions, 
_projectOperator);
+    
assertEquals(optimizedGroupByExecutor.getGroupKeyGenerator().getGlobalGroupKeyUpperBound(),
 upperBound,
+        _errorMessage);
+
+    // The block contains multi-value entries beyond the predicate values; all 
of them must become groups
+    optimizedGroupByExecutor.process(_valueBlock);
+    assertEquals(optimizedGroupByExecutor.getNumGroups(), numGroups, 
_errorMessage);
+  }
+
   private static ExpressionContext[] getExpressions(String[] columns) {
     int numColumns = columns.length;
     ExpressionContext[] expressions = new ExpressionContext[numColumns];
@@ -561,6 +707,11 @@ public class DictionaryBasedGroupKeyGeneratorTest {
         new DefaultGroupByExecutor(queryContext, expressions, 
_projectOperator);
     
assertEquals(defaultGroupByExecutor.getGroupKeyGenerator().getGlobalGroupKeyUpperBound(),
 expectedCapacity,
         _errorMessage);
+    // Generate group keys and aggregate over the block to ensure the 
optimized upper bound does not break key
+    // generation (regression: ArrayBasedHolder was selected with a shrunk 
upper bound and threw
+    // ArrayIndexOutOfBoundsException)
+    defaultGroupByExecutor.process(_valueBlock);
+    assertTrue(defaultGroupByExecutor.getNumGroups() <= expectedCapacity, 
_errorMessage);
   }
 
   @DataProvider(name = "groupByResultHolderCapacityDataProvider")
@@ -589,6 +740,8 @@ public class DictionaryBasedGroupKeyGeneratorTest {
         {"SELECT COUNT(s9), s1, s2 FROM testTable WHERE s1 IN (1, 2, 3) GROUP 
BY s1, s2 LIMIT 1000;", 300},
         // OR Predicate  -> (100 [s1] * 100 [s2]) = 1000 [Just cardinality 
cross product]
         {"SELECT COUNT(s9), s1, s2 FROM testTable WHERE s1 IN (1, 2, 3) OR s2 
> 1 GROUP BY s1, s2 LIMIT 20000;", 10000},
+        // Duplicate group-by expression -> (3 [s1] * 3 [s1]) = 9 (regression: 
used to throw on the duplicate map key)
+        {"SELECT COUNT(s9), s1 FROM testTable WHERE s1 IN (1, 2, 3) GROUP BY 
s1, s1 LIMIT 10;", 9},
     };
   }
 


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

Reply via email to