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 d22a30034d3 [perf] Reduce allocations in multi-stage grouping and 
result conversion (#19601)
d22a30034d3 is described below

commit d22a30034d385fc60a325db26c77fa17c80ca117
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Sep 21 12:58:39 2026 -0700

    [perf] Reduce allocations in multi-stage grouping and result conversion 
(#19601)
    
    * Reduce allocations in multi-stage group-by merging
    
    * Address allocation optimization review feedback
---
 .../aggregation/groupby/utils/DoubleToIdMap.java   |   5 +-
 .../aggregation/groupby/utils/FloatToIdMap.java    |   5 +-
 .../aggregation/groupby/utils/IntToIdMap.java      |   5 +-
 .../aggregation/groupby/utils/LongToIdMap.java     |   5 +-
 .../aggregation/groupby/utils/ObjectToIdMap.java   |   5 +-
 .../groupby/utils/ValueToIdMapTest.java            |  85 ++++++++++
 .../operator/MultistageGroupByExecutor.java        |  56 ++++---
 .../runtime/operator/groupby/GroupIdGenerator.java |   3 +-
 .../query/runtime/operator/utils/TypeUtils.java    |   8 +-
 .../operator/MultistageGroupByExecutorTest.java    | 173 +++++++++++++++++++++
 .../runtime/operator/utils/TypeUtilsTest.java      |  87 +++++++++++
 11 files changed, 404 insertions(+), 33 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/DoubleToIdMap.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/DoubleToIdMap.java
index 4ad1a2d17aa..74a3dac7acd 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/DoubleToIdMap.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/DoubleToIdMap.java
@@ -36,9 +36,10 @@ public class DoubleToIdMap implements ValueToIdMap {
   @Override
   public int put(double value) {
     int numValues = _valueToIdMap.size();
-    int id = _valueToIdMap.computeIfAbsent(value, k -> numValues);
-    if (id == numValues) {
+    int id = _valueToIdMap.putIfAbsent(value, numValues);
+    if (id == INVALID_KEY) {
       _idToValueMap.add(value);
+      return numValues;
     }
     return id;
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/FloatToIdMap.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/FloatToIdMap.java
index ad74990f69b..3050d0015bb 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/FloatToIdMap.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/FloatToIdMap.java
@@ -36,9 +36,10 @@ public class FloatToIdMap implements ValueToIdMap {
   @Override
   public int put(float value) {
     int numValues = _valueToIdMap.size();
-    int id = _valueToIdMap.computeIfAbsent(value, k -> numValues);
-    if (id == numValues) {
+    int id = _valueToIdMap.putIfAbsent(value, numValues);
+    if (id == INVALID_KEY) {
       _idToValueMap.add(value);
+      return numValues;
     }
     return id;
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/IntToIdMap.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/IntToIdMap.java
index 383012a17e8..9ec5bfbcf2d 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/IntToIdMap.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/IntToIdMap.java
@@ -36,9 +36,10 @@ public class IntToIdMap implements ValueToIdMap {
   @Override
   public int put(int value) {
     int numValues = _valueToIdMap.size();
-    int id = _valueToIdMap.computeIfAbsent(value, k -> numValues);
-    if (id == numValues) {
+    int id = _valueToIdMap.putIfAbsent(value, numValues);
+    if (id == INVALID_KEY) {
       _idToValueMap.add(value);
+      return numValues;
     }
     return id;
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/LongToIdMap.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/LongToIdMap.java
index 889c848828b..ced3687ad3c 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/LongToIdMap.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/LongToIdMap.java
@@ -36,9 +36,10 @@ public class LongToIdMap implements ValueToIdMap {
   @Override
   public int put(long value) {
     int numValues = _valueToIdMap.size();
-    int id = _valueToIdMap.computeIfAbsent(value, k -> numValues);
-    if (id == numValues) {
+    int id = _valueToIdMap.putIfAbsent(value, numValues);
+    if (id == INVALID_KEY) {
       _idToValueMap.add(value);
+      return numValues;
     }
     return id;
   }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/ObjectToIdMap.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/ObjectToIdMap.java
index b05722c94e4..218fc3aeac0 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/ObjectToIdMap.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/groupby/utils/ObjectToIdMap.java
@@ -36,9 +36,10 @@ public class ObjectToIdMap implements ValueToIdMap {
   @Override
   public int put(Object value) {
     int numValues = _valueToIdMap.size();
-    int id = _valueToIdMap.computeIntIfAbsent(value, k -> numValues);
-    if (id == numValues) {
+    int id = _valueToIdMap.putIfAbsent(value, numValues);
+    if (id == INVALID_KEY) {
       _idToValueMap.add(value);
+      return numValues;
     }
     return id;
   }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/utils/ValueToIdMapTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/utils/ValueToIdMapTest.java
new file mode 100644
index 00000000000..73406b4e2e4
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/groupby/utils/ValueToIdMapTest.java
@@ -0,0 +1,85 @@
+/**
+ * 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.groupby.utils;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.util.function.IntFunction;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.ByteArray;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+/// Verifies key identity and contiguous IDs across insertion, repetition, and 
map growth.
+public class ValueToIdMapTest {
+  @DataProvider(name = "maps")
+  public Object[][] maps() {
+    return new Object[][]{
+        {new IntToIdMap(), new Object[]{Integer.MIN_VALUE, -1, 0, 1, 
Integer.MAX_VALUE},
+            (IntFunction<Object>) Integer::valueOf},
+        {new LongToIdMap(), new Object[]{Long.MIN_VALUE, -1L, 0L, 1L, 
Long.MAX_VALUE, 9007199254740993L},
+            (IntFunction<Object>) Long::valueOf},
+        // fastutil preserves raw floating-point bits: signed zeros and 
different NaN payloads are distinct keys.
+        {new FloatToIdMap(), new Object[]{Float.NEGATIVE_INFINITY, 
-Float.MAX_VALUE, -0.0f, 0.0f, Float.MIN_VALUE,
+            Float.MAX_VALUE, Float.POSITIVE_INFINITY, Float.NaN, 
Float.intBitsToFloat(0x7fc00001)},
+            (IntFunction<Object>) Float::valueOf},
+        {new DoubleToIdMap(), new Object[]{Double.NEGATIVE_INFINITY, 
-Double.MAX_VALUE, -0.0d, 0.0d, Double.MIN_VALUE,
+            Double.MAX_VALUE, Double.POSITIVE_INFINITY, Double.NaN, 
Double.longBitsToDouble(0x7ff8000000000001L)},
+            (IntFunction<Object>) Double::valueOf},
+        {ValueToIdMapFactory.get(DataType.STRING), new Object[]{null, "", "a", 
"b"},
+            (IntFunction<Object>) String::valueOf},
+        {ValueToIdMapFactory.get(DataType.BYTES),
+            new Object[]{new ByteArray(new byte[0]), new ByteArray(new 
byte[]{1})},
+            (IntFunction<Object>) value -> new 
ByteArray(ByteBuffer.allocate(Integer.BYTES).putInt(value).array())},
+        {ValueToIdMapFactory.get(DataType.BIG_DECIMAL), new 
Object[]{BigDecimal.ZERO, new BigDecimal("1.0"),
+            new BigDecimal("1.00")}, (IntFunction<Object>) BigDecimal::valueOf}
+    };
+  }
+
+  @Test(dataProvider = "maps")
+  public void testKeys(ValueToIdMap map, Object[] values, IntFunction<Object> 
valueFactory) {
+    for (int i = 0; i < values.length; i++) {
+      assertEquals(map.getId(values[i]), ValueToIdMap.INVALID_KEY);
+      assertMapping(map, values[i], i);
+    }
+    // These values are disjoint from the initial keys and force several 
rehashes.
+    for (int i = 0; i < 4096; i++) {
+      Object value = valueFactory.apply(1000 + i);
+      assertEquals(map.getId(value), ValueToIdMap.INVALID_KEY);
+      assertMapping(map, value, values.length + i);
+    }
+    for (int i = 0; i < values.length; i++) {
+      assertMapping(map, values[i], i);
+    }
+    for (int i = 0; i < 4096; i++) {
+      assertMapping(map, valueFactory.apply(1000 + i), values.length + i);
+    }
+  }
+
+  private static void assertMapping(ValueToIdMap map, Object value, int 
expectedId) {
+    assertEquals(map.put(value), expectedId);
+    assertEquals(map.put(value), expectedId);
+    assertEquals(map.getId(value), expectedId);
+    assertEquals(map.get(expectedId), value);
+    assertEquals(map.getId(map.get(expectedId)), expectedId);
+  }
+}
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutor.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutor.java
index 02fd6baf9e3..5409aa7c463 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutor.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutor.java
@@ -372,7 +372,7 @@ public class MultistageGroupByExecutor {
     int[] groupByKeys = generateGroupByKeys(block);
     int numRows = groupByKeys.length;
     int numFunctions = _aggFunctions.length;
-    Object[][] intermediateResults = new Object[numFunctions][numRows];
+    Object[][] intermediateResults = new Object[numFunctions][];
     for (int i = 0; i < numFunctions; i++) {
       intermediateResults[i] = 
AggregateOperator.getIntermediateResults(_aggFunctions[i], block);
     }
@@ -446,16 +446,26 @@ public class MultistageGroupByExecutor {
   }
 
   private int[] generateGroupByKeys(DataBlock dataBlock) {
-    Object[] keys;
-    if (_groupKeyIds.length == 1) {
-      keys = DataBlockExtractUtils.extractKey(dataBlock, _groupKeyIds[0]);
-    } else {
-      keys = DataBlockExtractUtils.extractKeys(dataBlock, _groupKeyIds);
-    }
-    int numRows = keys.length;
+    int numRows = dataBlock.getNumberOfRows();
     int[] intKeys = new int[numRows];
-    for (int i = 0; i < numRows; i++) {
-      intKeys[i] = _groupIdGenerator.getGroupId(keys[i]);
+    int numKeys = _groupKeyIds.length;
+    if (numKeys == 1) {
+      Object[] keys = DataBlockExtractUtils.extractKey(dataBlock, 
_groupKeyIds[0]);
+      for (int i = 0; i < numRows; i++) {
+        intKeys[i] = _groupIdGenerator.getGroupId(keys[i]);
+      }
+    } else {
+      Object[][] columns = new Object[numKeys][];
+      for (int i = 0; i < numKeys; i++) {
+        columns[i] = DataBlockExtractUtils.extractKey(dataBlock, 
_groupKeyIds[i]);
+      }
+      Object[] key = new Object[numKeys];
+      for (int rowId = 0; rowId < numRows; rowId++) {
+        for (int i = 0; i < numKeys; i++) {
+          key[i] = columns[i][rowId];
+        }
+        intKeys[rowId] = _groupIdGenerator.getGroupId(key);
+      }
     }
     return intKeys;
   }
@@ -492,15 +502,25 @@ public class MultistageGroupByExecutor {
   }
 
   private int[] generateGroupByKeys(DataBlock dataBlock, int numMatchedRows, 
RoaringBitmap matchedBitmap) {
-    Object[] keys;
-    if (_groupKeyIds.length == 1) {
-      keys = DataBlockExtractUtils.extractKey(dataBlock, _groupKeyIds[0], 
numMatchedRows, matchedBitmap);
-    } else {
-      keys = DataBlockExtractUtils.extractKeys(dataBlock, _groupKeyIds, 
numMatchedRows, matchedBitmap);
-    }
     int[] intKeys = new int[numMatchedRows];
-    for (int i = 0; i < numMatchedRows; i++) {
-      intKeys[i] = _groupIdGenerator.getGroupId(keys[i]);
+    int numKeys = _groupKeyIds.length;
+    if (numKeys == 1) {
+      Object[] keys = DataBlockExtractUtils.extractKey(dataBlock, 
_groupKeyIds[0], numMatchedRows, matchedBitmap);
+      for (int i = 0; i < numMatchedRows; i++) {
+        intKeys[i] = _groupIdGenerator.getGroupId(keys[i]);
+      }
+    } else {
+      Object[][] columns = new Object[numKeys][];
+      for (int i = 0; i < numKeys; i++) {
+        columns[i] = DataBlockExtractUtils.extractKey(dataBlock, 
_groupKeyIds[i], numMatchedRows, matchedBitmap);
+      }
+      Object[] key = new Object[numKeys];
+      for (int rowId = 0; rowId < numMatchedRows; rowId++) {
+        for (int i = 0; i < numKeys; i++) {
+          key[i] = columns[i][rowId];
+        }
+        intKeys[rowId] = _groupIdGenerator.getGroupId(key);
+      }
     }
     return intKeys;
   }
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/groupby/GroupIdGenerator.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/groupby/GroupIdGenerator.java
index 65eef96b83d..ec3a20d27c6 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/groupby/GroupIdGenerator.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/groupby/GroupIdGenerator.java
@@ -28,7 +28,8 @@ public interface GroupIdGenerator {
   /// Returns the group id for the given key. When a new key is encountered, 
it assigns a new group id to it before
   /// reaching the groups limit, or returns [#INVALID_ID] when the limit is 
reached.
   /// For single key column, the input is a single Object. For multi key 
columns, the input is an Object\[\] containing
-  /// the values for each key column.
+  /// the values for each key column. The multi-column array is scratch 
storage and may be mutated immediately after
+  /// this method returns. Implementations must not retain the array.
   ///
   /// This method is called once per input row, so the implementation should 
be as fast as possible and reduce memory
   /// allocation.
diff --git 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/utils/TypeUtils.java
 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/utils/TypeUtils.java
index d95b60e0c22..f7a5ebe9c03 100644
--- 
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/utils/TypeUtils.java
+++ 
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/operator/utils/TypeUtils.java
@@ -39,13 +39,13 @@ public class TypeUtils {
   public static Object convert(Object value, ColumnDataType storedType) {
     switch (storedType) {
       case INT:
-        return ((Number) value).intValue();
+        return value instanceof Integer ? value : ((Number) value).intValue();
       case LONG:
-        return ((Number) value).longValue();
+        return value instanceof Long ? value : ((Number) value).longValue();
       case FLOAT:
-        return ((Number) value).floatValue();
+        return value instanceof Float ? value : ((Number) value).floatValue();
       case DOUBLE:
-        return ((Number) value).doubleValue();
+        return value instanceof Double ? value : ((Number) 
value).doubleValue();
       case BIG_DECIMAL:
         return value instanceof BigDecimal ? value : 
BigDecimal.valueOf(((Number) value).doubleValue());
       // For AggregationFunctions that return serialized custom object, e.g. 
DistinctCountRawHLLAggregationFunction
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutorTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutorTest.java
new file mode 100644
index 00000000000..6a9956e6be1
--- /dev/null
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/MultistageGroupByExecutorTest.java
@@ -0,0 +1,173 @@
+/**
+ * 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.query.runtime.operator;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
+import 
org.apache.pinot.core.query.aggregation.function.CountAggregationFunction;
+import org.apache.pinot.query.planner.plannode.AggregateNode.AggType;
+import org.apache.pinot.query.runtime.blocks.MseBlock;
+import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Exercises serialized composite keys across merging and filtered 
aggregation. Each test owns its executor and blocks.
+public class MultistageGroupByExecutorTest {
+  private static final DataSchema INPUT_SCHEMA = new DataSchema(new 
String[]{"weight", "count", "tv", "tag"},
+      new ColumnDataType[]{ColumnDataType.DOUBLE, ColumnDataType.LONG, 
ColumnDataType.INT, ColumnDataType.STRING});
+
+  @DataProvider
+  public Object[][] mergeModes() {
+    return new Object[][]{{2, false}, {2, true}, {3, false}, {3, true}};
+  }
+
+  @Test(dataProvider = "mergeModes")
+  public void testSerializedKeysAcrossBlocks(int numKeys, boolean 
leafReturnFinalResult) {
+    MultistageGroupByExecutor executor = newExecutor(numKeys, 
leafReturnFinalResult, 100);
+    MseBlock.Data first = OperatorTestUtil.block(INPUT_SCHEMA,
+        new Object[]{1.5, 2L, 1000, "Aa"},
+        new Object[]{null, 3L, 1000, "BB"},
+        new Object[]{1.5, 5L, null, "Aa"},
+        new Object[]{null, 7L, null, null},
+        new Object[]{0.0, 11L, 1000, "Aa"},
+        new Object[]{-0.0, 13L, 1000, "BB"},
+        new Object[]{Double.NaN, 17L, 1000, "Aa"},
+        new Object[]{Double.POSITIVE_INFINITY, 19L, 1000, 
null}).asSerialized();
+    executor.processBlock(first);
+    executor.processBlock(OperatorTestUtil.block(INPUT_SCHEMA).asSerialized());
+    executor.processBlock(OperatorTestUtil.block(INPUT_SCHEMA,
+        new Object[]{null, 23L, null, null},
+        new Object[]{Double.NaN, 29L, 1000, "Aa"},
+        new Object[]{1.5, 31L, 1000, "Aa"}).asSerialized());
+
+    List<Object[]> expected = List.of(
+        new Object[]{1000, 1.5, "Aa", 33L},
+        new Object[]{1000, null, "BB", 3L},
+        new Object[]{null, 1.5, "Aa", 5L},
+        new Object[]{null, null, null, 30L},
+        new Object[]{1000, 0.0, "Aa", 11L},
+        new Object[]{1000, -0.0, "BB", 13L},
+        new Object[]{1000, Double.NaN, "Aa", 46L},
+        new Object[]{1000, Double.POSITIVE_INFINITY, null, 19L});
+    assertEquals(asMap(executor.getResult(100), numKeys), 
expectedMap(expected, numKeys));
+    assertEquals(executor.getNumGroups(), 8);
+  }
+
+  @Test(dataProvider = "mergeModes")
+  public void testExistingKeysStillMergeAtGroupLimit(int numKeys, boolean 
leafReturnFinalResult) {
+    MultistageGroupByExecutor executor = newExecutor(numKeys, 
leafReturnFinalResult, 2);
+    executor.processBlock(OperatorTestUtil.block(INPUT_SCHEMA,
+        new Object[]{1.5, 2L, 1000, "Aa"},
+        new Object[]{null, 3L, null, null},
+        new Object[]{2.5, 100L, 2000, "BB"},
+        new Object[]{1.5, 5L, 1000, "Aa"},
+        new Object[]{null, 7L, null, null}).asSerialized());
+    executor.processBlock(OperatorTestUtil.block(INPUT_SCHEMA,
+        new Object[]{1.5, 100L, null, "Aa"},
+        new Object[]{1.5, 11L, 1000, "Aa"}).asSerialized());
+
+    assertEquals(asMap(executor.getResult(100), numKeys), expectedMap(List.of(
+        new Object[]{1000, 1.5, "Aa", 18L}, new Object[]{null, null, null, 
10L}), numKeys));
+    assertTrue(executor.isNumGroupsLimitReached());
+  }
+
+  @DataProvider
+  public Object[][] filteredModes() {
+    return new Object[][]{{2, false}, {2, true}, {3, false}, {3, true}};
+  }
+
+  @Test(dataProvider = "filteredModes")
+  public void testFilteredSerializedKeysAcrossBlocks(int numKeys, boolean 
skipEmptyGroups) {
+    DataSchema inputSchema = new DataSchema(new String[]{"weight", "count", 
"tv", "tag", "filter"},
+        new ColumnDataType[]{ColumnDataType.DOUBLE, ColumnDataType.LONG, 
ColumnDataType.INT, ColumnDataType.STRING,
+            ColumnDataType.BOOLEAN});
+    MultistageGroupByExecutor executor = newExecutor(numKeys, false, 100, 4, 
skipEmptyGroups);
+    executor.processBlock(OperatorTestUtil.block(inputSchema,
+        new Object[]{1.5, 2L, 1000, "Aa", 1},
+        new Object[]{9.5, 3L, 9000, "unmatched", 0},
+        new Object[]{null, 5L, null, null, 1},
+        new Object[]{1.5, 7L, 1000, "Aa", 0},
+        new Object[]{2.5, 11L, 2000, "BB", 1}).asSerialized());
+    executor.processBlock(OperatorTestUtil.block(inputSchema).asSerialized());
+    executor.processBlock(OperatorTestUtil.block(inputSchema,
+        new Object[]{9.5, 13L, 9000, "unmatched", 0}).asSerialized());
+    executor.processBlock(OperatorTestUtil.block(inputSchema,
+        new Object[]{null, 17L, null, null, 1},
+        new Object[]{2.5, 19L, 2000, "BB", 0},
+        new Object[]{1.5, 23L, 1000, "Aa", 1}).asSerialized());
+
+    Map<List<Object>, Long> expected = expectedMap(List.of(
+        new Object[]{1000, 1.5, "Aa", 2L},
+        new Object[]{null, null, null, 2L},
+        new Object[]{2000, 2.5, "BB", 1L}), numKeys);
+    if (!skipEmptyGroups) {
+      expected.put(Arrays.asList(Arrays.copyOf(new Object[]{9000, 9.5, 
"unmatched"}, numKeys)), 0L);
+    }
+    assertEquals(asMap(executor.getResult(100), numKeys), expected);
+    assertEquals(executor.getNumGroups(), expected.size());
+  }
+
+  private static MultistageGroupByExecutor newExecutor(int numKeys, boolean 
leafReturnFinalResult, int groupLimit) {
+    return newExecutor(numKeys, leafReturnFinalResult, groupLimit, -1, false);
+  }
+
+  private static MultistageGroupByExecutor newExecutor(int numKeys, boolean 
leafReturnFinalResult, int groupLimit,
+      int filterArgId, boolean skipEmptyGroups) {
+    int[] groupKeys = numKeys == 2 ? new int[]{2, 0} : new int[]{2, 0, 3};
+    DataSchema resultSchema = numKeys == 2
+        ? new DataSchema(new String[]{"tv", "weight", "count"},
+            new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.DOUBLE, 
ColumnDataType.LONG})
+        : new DataSchema(new String[]{"tv", "weight", "tag", "count"},
+            new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.DOUBLE, 
ColumnDataType.STRING,
+                ColumnDataType.LONG});
+    AggregationFunction<?, ?>[] functions = {
+        new 
CountAggregationFunction(List.of(ExpressionContext.forIdentifier("$1")), true)};
+    return new MultistageGroupByExecutor(groupKeys, functions, new 
int[]{filterArgId}, filterArgId,
+        filterArgId < 0 ? AggType.FINAL : AggType.DIRECT, 
leafReturnFinalResult, resultSchema,
+        Map.of(QueryOptionKey.NUM_GROUPS_LIMIT, Integer.toString(groupLimit),
+            QueryOptionKey.FILTERED_AGGREGATIONS_SKIP_EMPTY_GROUPS, 
Boolean.toString(skipEmptyGroups)), null);
+  }
+
+  private static Map<List<Object>, Long> expectedMap(List<Object[]> rows, int 
numKeys) {
+    Map<List<Object>, Long> result = new HashMap<>();
+    for (Object[] row : rows) {
+      result.put(Arrays.asList(Arrays.copyOf(row, numKeys)), (Long) row[3]);
+    }
+    return result;
+  }
+
+  private static Map<List<Object>, Long> asMap(List<Object[]> rows, int 
numKeys) {
+    Map<List<Object>, Long> result = new HashMap<>();
+    for (Object[] row : rows) {
+      assertNull(result.put(Arrays.asList(Arrays.copyOf(row, numKeys)), (Long) 
row[numKeys]), "Duplicate output group");
+    }
+    return result;
+  }
+}
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/utils/TypeUtilsTest.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/utils/TypeUtilsTest.java
new file mode 100644
index 00000000000..653f08c699a
--- /dev/null
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/operator/utils/TypeUtilsTest.java
@@ -0,0 +1,87 @@
+/**
+ * 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.query.runtime.operator.utils;
+
+import java.math.BigDecimal;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertThrows;
+
+
+/// Verifies identity conversion and numeric coercions at the single-stage to 
multi-stage boundary.
+public class TypeUtilsTest {
+  @DataProvider(name = "numericValues")
+  public Object[][] numericValues() {
+    return new Object[][]{
+        {Integer.valueOf(1024)}, {Long.valueOf(9007199254740993L)}, 
{Float.valueOf(-0.0f)},
+        {Double.valueOf(-0.0d)}, {Float.intBitsToFloat(0x7fc00001)},
+        {Double.longBitsToDouble(0x7ff8000000000001L)}, 
{Double.POSITIVE_INFINITY},
+        {Double.NEGATIVE_INFINITY}, {Double.MIN_VALUE}, {new 
BigDecimal("123456789.125")},
+        {Byte.valueOf((byte) -1)}, {Short.valueOf((short) 1024)}
+    };
+  }
+
+  @Test(dataProvider = "numericValues")
+  public void testNumericConversions(Number value) {
+    Object converted = TypeUtils.convert(value, ColumnDataType.INT);
+    assertEquals(converted, Integer.valueOf(value.intValue()));
+    if (value instanceof Integer) {
+      assertSame(converted, value);
+    }
+    converted = TypeUtils.convert(value, ColumnDataType.LONG);
+    assertEquals(converted, Long.valueOf(value.longValue()));
+    if (value instanceof Long) {
+      assertSame(converted, value);
+    }
+    converted = TypeUtils.convert(value, ColumnDataType.FLOAT);
+    assertEquals(Float.floatToRawIntBits((Float) converted), 
Float.floatToRawIntBits(value.floatValue()));
+    if (value instanceof Float) {
+      assertSame(converted, value);
+    }
+    converted = TypeUtils.convert(value, ColumnDataType.DOUBLE);
+    assertEquals(Double.doubleToRawLongBits((Double) converted), 
Double.doubleToRawLongBits(value.doubleValue()));
+    if (value instanceof Double) {
+      assertSame(converted, value);
+    }
+  }
+
+  @Test
+  public void testConvertRowWithNullAndMixedTypes() {
+    Object[] row = {Integer.valueOf(1024), Double.valueOf(-0.0d), null, 
Long.valueOf(9007199254740993L),
+        Double.valueOf(9.75d), Integer.valueOf(1024)};
+    Object[] original = row.clone();
+    TypeUtils.convertRow(row, new ColumnDataType[]{ColumnDataType.INT, 
ColumnDataType.DOUBLE, ColumnDataType.FLOAT,
+        ColumnDataType.LONG, ColumnDataType.INT, ColumnDataType.DOUBLE});
+    assertSame(row[0], original[0]);
+    assertSame(row[1], original[1]);
+    assertNull(row[2]);
+    assertSame(row[3], original[3]);
+    assertEquals(row[4], Integer.valueOf(9));
+    assertEquals(row[5], Double.valueOf(1024d));
+    for (ColumnDataType type : new ColumnDataType[]{ColumnDataType.INT, 
ColumnDataType.LONG, ColumnDataType.FLOAT,
+        ColumnDataType.DOUBLE}) {
+      assertThrows(NullPointerException.class, () -> TypeUtils.convert(null, 
type));
+    }
+  }
+}


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

Reply via email to