This is an automated email from the ASF dual-hosted git repository.
gortiz 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 29f91c6234d Add string type support to MIN/MAX aggregation functions
(#16497)
29f91c6234d is described below
commit 29f91c6234d2fee30543496235efd17b47db457b
Author: Arunkumar Saravanan <[email protected]>
AuthorDate: Mon Aug 11 16:40:14 2025 +0530
Add string type support to MIN/MAX aggregation functions (#16497)
---
.../blocks/results/AggregationResultsBlock.java | 3 +
.../function/AggregationFunctionFactory.java | 4 +
.../function/AggregationFunctionUtils.java | 2 +
.../function/MaxStringAggregationFunction.java | 164 +++++++++++
.../function/MinStringAggregationFunction.java | 163 +++++++++++
.../function/AggregationFunctionFactoryTest.java | 12 +
.../function/MaxStringAggregationFunctionTest.java | 312 +++++++++++++++++++++
.../function/MinStringAggregationFunctionTest.java | 306 ++++++++++++++++++++
.../pinot/segment/spi/AggregationFunctionType.java | 2 +
9 files changed, 968 insertions(+)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
index 58991924c28..91b4f14e3c2 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
@@ -197,6 +197,9 @@ public class AggregationResultsBlock extends
BaseResultsBlock {
case DOUBLE:
dataTableBuilder.setColumn(index, (double) result);
break;
+ case STRING:
+ dataTableBuilder.setColumn(index, result.toString());
+ break;
default:
throw new IllegalStateException("Illegal column data type in
intermediate result: " + columnDataType);
}
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 8685be9a1ed..d05316b5480 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
@@ -216,6 +216,10 @@ public class AggregationFunctionFactory {
return new MinAggregationFunction(arguments, nullHandlingEnabled);
case MAX:
return new MaxAggregationFunction(arguments, nullHandlingEnabled);
+ case MINSTRING:
+ return new MinStringAggregationFunction(arguments,
nullHandlingEnabled);
+ case MAXSTRING:
+ return new MaxStringAggregationFunction(arguments,
nullHandlingEnabled);
case SUM:
case SUM0:
return new SumAggregationFunction(arguments, nullHandlingEnabled);
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
index 5a74894116e..c9edf0bf00c 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
@@ -152,6 +152,8 @@ public class AggregationFunctionUtils {
return dataTable.getLong(rowId, colId);
case DOUBLE:
return dataTable.getDouble(rowId, colId);
+ case STRING:
+ return dataTable.getString(rowId, colId);
case OBJECT:
CustomObject customObject = dataTable.getCustomObject(rowId, colId);
return customObject != null ?
aggregationFunction.deserializeIntermediateResult(customObject) : null;
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
new file mode 100644
index 00000000000..37eb822f213
--- /dev/null
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
@@ -0,0 +1,164 @@
+/**
+ * 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 javax.annotation.Nullable;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.BlockValSet;
+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.spi.AggregationFunctionType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+
+
+public class MaxStringAggregationFunction extends
NullableSingleInputAggregationFunction<String, String> {
+
+ public MaxStringAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, "MAXSTRING"), nullHandlingEnabled);
+ }
+
+ @Override
+ public AggregationFunctionType getType() {
+ return AggregationFunctionType.MAXSTRING;
+ }
+
+ @Override
+ public AggregationResultHolder createAggregationResultHolder() {
+ return new ObjectAggregationResultHolder();
+ }
+
+ @Override
+ public GroupByResultHolder createGroupByResultHolder(int initialCapacity,
int maxCapacity) {
+ return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
+ }
+
+ @Override
+ public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ String maxValue = foldNotNull(length, blockValSet, null, (acum, from, to)
-> {
+ String innerMax = stringValues[from];
+ for (int i = from + 1; i < to; i++) {
+ if (stringValues[i].compareTo(innerMax) > 0) {
+ innerMax = stringValues[i];
+ }
+ }
+ return acum == null ? innerMax : (acum.compareTo(innerMax) > 0 ? acum :
innerMax);
+ });
+
+ String currentMax = aggregationResultHolder.getResult();
+ if (currentMax == null || (maxValue != null &&
maxValue.compareTo(currentMax) > 0)) {
+ aggregationResultHolder.setValue(maxValue);
+ }
+ }
+
+ @Override
+ public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ String value = stringValues[i];
+ int groupKey = groupKeyArray[i];
+ String currentMax = groupByResultHolder.getResult(groupKey);
+ if (currentMax == null || value.compareTo(currentMax) > 0) {
+ groupByResultHolder.setValueForKey(groupKey, value);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ String value = stringValues[i];
+ for (int groupKey : groupKeysArray[i]) {
+ String currentMax = groupByResultHolder.getResult(groupKey);
+ if (currentMax == null || value.compareTo(currentMax) > 0) {
+ groupByResultHolder.setValueForKey(groupKey, value);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public String extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
+ return aggregationResultHolder.getResult();
+ }
+
+ @Override
+ public String extractGroupByResult(GroupByResultHolder groupByResultHolder,
int groupKey) {
+ return groupByResultHolder.getResult(groupKey);
+ }
+
+ @Override
+ public String merge(@Nullable String intermediateResult1, @Nullable String
intermediateResult2) {
+ if (intermediateResult1 == null) {
+ return intermediateResult2;
+ }
+ if (intermediateResult2 == null) {
+ return intermediateResult1;
+ }
+ return intermediateResult1.compareTo(intermediateResult2) > 0 ?
intermediateResult1 : intermediateResult2;
+ }
+
+ @Override
+ public ColumnDataType getIntermediateResultColumnType() {
+ return ColumnDataType.STRING;
+ }
+
+ @Override
+ public ColumnDataType getFinalResultColumnType() {
+ return ColumnDataType.STRING;
+ }
+
+ @Override
+ public String extractFinalResult(String intermediateResult) {
+ return intermediateResult;
+ }
+
+ @Override
+ public String mergeFinalResult(String finalResult1, String finalResult2) {
+ return merge(finalResult1, finalResult2);
+ }
+}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
new file mode 100644
index 00000000000..bb12dd4b9e3
--- /dev/null
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
@@ -0,0 +1,163 @@
+/**
+ * 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 javax.annotation.Nullable;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.BlockValSet;
+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.spi.AggregationFunctionType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+
+
+public class MinStringAggregationFunction extends
NullableSingleInputAggregationFunction<String, String> {
+
+ public MinStringAggregationFunction(List<ExpressionContext> arguments,
boolean nullHandlingEnabled) {
+ super(verifySingleArgument(arguments, "MINSTRING"), nullHandlingEnabled);
+ }
+
+ @Override
+ public AggregationFunctionType getType() {
+ return AggregationFunctionType.MINSTRING;
+ }
+
+ @Override
+ public AggregationResultHolder createAggregationResultHolder() {
+ return new ObjectAggregationResultHolder();
+ }
+
+ @Override
+ public GroupByResultHolder createGroupByResultHolder(int initialCapacity,
int maxCapacity) {
+ return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
+ }
+
+ @Override
+ public void aggregate(int length, AggregationResultHolder
aggregationResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MINSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ String minValue = foldNotNull(length, blockValSet, null, (acum, from, to)
-> {
+ String innerMin = stringValues[from];
+ for (int i = from + 1; i < to; i++) {
+ if (stringValues[i].compareTo(innerMin) < 0) {
+ innerMin = stringValues[i];
+ }
+ }
+ return acum == null ? innerMin : (acum.compareTo(innerMin) < 0 ? acum :
innerMin);
+ });
+ String currentMin = aggregationResultHolder.getResult();
+ if (currentMin == null || (minValue != null &&
minValue.compareTo(currentMin) < 0)) {
+ aggregationResultHolder.setValue(minValue);
+ }
+ }
+
+ @Override
+ public void aggregateGroupBySV(int length, int[] groupKeyArray,
GroupByResultHolder groupByResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MINSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ String value = stringValues[i];
+ int groupKey = groupKeyArray[i];
+ String currentMin = groupByResultHolder.getResult(groupKey);
+ if (currentMin == null || value.compareTo(currentMin) < 0) {
+ groupByResultHolder.setValueForKey(groupKey, value);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void aggregateGroupByMV(int length, int[][] groupKeysArray,
GroupByResultHolder groupByResultHolder,
+ Map<ExpressionContext, BlockValSet> blockValSetMap) {
+ BlockValSet blockValSet = blockValSetMap.get(_expression);
+ if (blockValSet.getValueType().isNumeric()) {
+ throw new BadQueryRequestException("Cannot compute MINSTRING for numeric
column: "
+ + blockValSet.getValueType());
+ }
+ String[] stringValues = blockValSet.getStringValuesSV();
+ forEachNotNull(length, blockValSet, (from, to) -> {
+ for (int i = from; i < to; i++) {
+ String value = stringValues[i];
+ for (int groupKey : groupKeysArray[i]) {
+ String currentMin = groupByResultHolder.getResult(groupKey);
+ if (currentMin == null || value.compareTo(currentMin) < 0) {
+ groupByResultHolder.setValueForKey(groupKey, value);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public String extractAggregationResult(AggregationResultHolder
aggregationResultHolder) {
+ return aggregationResultHolder.getResult();
+ }
+
+ @Override
+ public String extractGroupByResult(GroupByResultHolder groupByResultHolder,
int groupKey) {
+ return groupByResultHolder.getResult(groupKey);
+ }
+
+ @Override
+ public String merge(@Nullable String intermediateResult1, @Nullable String
intermediateResult2) {
+ if (intermediateResult1 == null) {
+ return intermediateResult2;
+ }
+ if (intermediateResult2 == null) {
+ return intermediateResult1;
+ }
+ return intermediateResult1.compareTo(intermediateResult2) < 0 ?
intermediateResult1 : intermediateResult2;
+ }
+
+ @Override
+ public ColumnDataType getIntermediateResultColumnType() {
+ return ColumnDataType.STRING;
+ }
+
+ @Override
+ public ColumnDataType getFinalResultColumnType() {
+ return ColumnDataType.STRING;
+ }
+
+ @Override
+ public String extractFinalResult(String intermediateResult) {
+ return intermediateResult;
+ }
+
+ @Override
+ public String mergeFinalResult(String finalResult1, String finalResult2) {
+ return merge(finalResult1, finalResult2);
+ }
+}
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 0caf40536bc..2ec20535f2b 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
@@ -52,6 +52,18 @@ public class AggregationFunctionFactoryTest {
assertEquals(aggregationFunction.getType(), AggregationFunctionType.MAX);
assertEquals(aggregationFunction.getResultColumnName(),
function.toString());
+ function = getFunction("MiNsTrInG");
+ aggregationFunction =
AggregationFunctionFactory.getAggregationFunction(function, false);
+ assertTrue(aggregationFunction instanceof MinStringAggregationFunction);
+ assertEquals(aggregationFunction.getType(),
AggregationFunctionType.MINSTRING);
+ assertEquals(aggregationFunction.getResultColumnName(),
function.toString());
+
+ function = getFunction("MaXsTrInG");
+ aggregationFunction =
AggregationFunctionFactory.getAggregationFunction(function, false);
+ assertTrue(aggregationFunction instanceof MaxStringAggregationFunction);
+ assertEquals(aggregationFunction.getType(),
AggregationFunctionType.MAXSTRING);
+ assertEquals(aggregationFunction.getResultColumnName(),
function.toString());
+
function = getFunction("SuM");
aggregationFunction =
AggregationFunctionFactory.getAggregationFunction(function, false);
assertTrue(aggregationFunction instanceof SumAggregationFunction);
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
new file mode 100644
index 00000000000..d0cf29d825c
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
@@ -0,0 +1,312 @@
+/**
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.RequestContextUtils;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import org.apache.pinot.queries.FluentQueryTest;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+
+public class MaxStringAggregationFunctionTest extends
AbstractAggregationFunctionTest {
+
+ /**
+ * Helper method to create a FluentQueryTest builder for a table with a
single String field.
+ * This is used to simulate the DataTypeScenario concept from numeric
aggregation tests,
+ * but fixed for the STRING data type.
+ */
+ protected FluentQueryTest.DeclaringTable getDeclaringTable(boolean
enableColumnBasedNullHandling) {
+ return FluentQueryTest.withBaseDir(_baseDir)
+ .givenTable(
+ new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+
.setEnableColumnBasedNullHandling(enableColumnBasedNullHandling)
+ .addSingleValueDimension("myField", FieldSpec.DataType.STRING)
+ .build(), SINGLE_FIELD_TABLE_CONFIG);
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MaxStringAggregationFunction function = new
MaxStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ AggregationResultHolder resultHolder =
function.createAggregationResultHolder();
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+
+ try {
+ function.aggregate(10, resultHolder, blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MAXSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateGroupBySVMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MaxStringAggregationFunction function = new
MaxStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ GroupByResultHolder groupByResultHolder =
function.createGroupByResultHolder(10, 20);
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+
+ try {
+ function.aggregateGroupBySV(10, new int[10], groupByResultHolder,
blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MAXSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateGroupByMVMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MaxStringAggregationFunction function = new
MaxStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ GroupByResultHolder groupByResultHolder =
function.createGroupByResultHolder(10, 20);
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+
+ try {
+ function.aggregateGroupByMV(10, new int[10][], groupByResultHolder,
blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MAXSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testFunctionBasics() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MaxStringAggregationFunction function = new
MaxStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ // Test function type
+ assertEquals(function.getType(), AggregationFunctionType.MAXSTRING);
+
+ // Test string comparisons
+ assertEquals(function.merge("apple", "banana"), "banana");
+ assertEquals(function.merge("banana", "apple"), "banana");
+ assertEquals(function.merge("", "apple"), "apple");
+ assertEquals(function.merge("apple", ""), "apple");
+
+ // Test null handling
+ assertEquals(function.merge("apple", null), "apple");
+ assertEquals(function.merge(null, "apple"), "apple");
+ assertNull(function.merge(null, null));
+ assertEquals(function.merge("apple", "null"), "null");
+
+ // Test final result merging
+ assertEquals(function.mergeFinalResult("apple", "banana"), "banana");
+ }
+
+ @Test
+ void aggregationAllNullsWithNullHandlingDisabled() {
+ // For MAXSTRING, when null handling is disabled, and all values are null,
+ // the result should be 'null' as there's no valid string to compare.
+ // This differs from numeric MAX/MIN which might return an initial default
value.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select maxstring(myField) from testTable")
+ .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string
literal for the result
+ }
+
+ @Test
+ void aggregationAllNullsWithNullHandlingEnabled() {
+ // When null handling is enabled, and all values are null, the result
should also be 'null'.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select maxstring(myField) from testTable")
+ .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string
literal for the result
+ }
+
+ @Test
+ void aggregationGroupBySVAllNullsWithNullHandlingDisabled() {
+ // For group by, if all values in a group are null and null handling is
disabled,
+ // the group's result for MAXSTRING should be 'null'.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select 'literal', maxstring(myField) from testTable group
by 'literal'")
+ // Expected "null" as a string literal for the aggregated column
+ .thenResultIs("STRING | STRING", "literal | \"null\"");
+ }
+
+ @Test
+ void aggregationGroupBySVAllNullsWithNullHandlingEnabled() {
+ // For group by, if all values in a group are null and null handling is
enabled,
+ // the group's result for MAXSTRING should be 'null'.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select 'literal', maxstring(myField) from testTable group
by 'literal'")
+ .thenResultIs("STRING | STRING", "literal | \"null\"");
+ }
+
+ @Test
+ void aggregationWithNullHandlingDisabled() {
+ // With null handling disabled, null values are effectively skipped, and
the maximum non-null
+ // string should be found. The updated function handles this correctly.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "cat",
+ "null",
+ "apple"
+ ).andOnSecondInstance("myField",
+ "null",
+ "zebra",
+ "null"
+ ).whenQuery("select maxstring(myField) from testTable")
+ .thenResultIs("STRING", "zebra"); // Max of {"cat", "apple", "zebra"}
is "zebra"
+ }
+
+ @Test
+ void aggregationWithNullHandlingEnabled() {
+ // With null handling enabled, null values are explicitly ignored, and the
maximum non-null
+ // string should be found. The updated function handles this correctly.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "cat",
+ "null",
+ "apple"
+ ).andOnSecondInstance("myField",
+ "null",
+ "zebra",
+ "null"
+ ).whenQuery("select maxstring(myField) from testTable")
+ .thenResultIs("STRING", "zebra"); // Max of {"cat", "apple", "zebra"}
is "zebra"
+ }
+
+ @Test
+ void aggregationGroupBySVWithNullHandlingDisabled() {
+ // Group By on a single value (SV) column with mixed nulls and non-nulls.
+ // Null handling disabled: nulls are ignored if there's at least one
non-null value in the group.
+ // The updated function should now correctly find the max among non-nulls.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "alpha", // Grouped with 'literal'
+ "null", // Grouped with 'literal'
+ "gamma" // Grouped with 'literal'
+ ).andOnSecondInstance("myField",
+ "null", // Grouped with 'literal'
+ "beta", // Grouped with 'literal'
+ "null" // Grouped with 'literal'
+ ).whenQuery("select 'literal', maxstring(myField) from testTable group
by 'literal'")
+ .thenResultIs("STRING | STRING",
+ "literal | \"null\""); // Max of {"alpha", null, "gamma", "beta"}
is "null" when null handling is disabled
+ }
+
+ @Test
+ void aggregationGroupBySVWithNullHandlingEnabled() {
+ // Group By on a single value (SV) column with mixed nulls and non-nulls.
+ // Null handling enabled: nulls are ignored.
+ // The updated function should now correctly find the max among non-nulls.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "alpha", // Grouped with 'literal'
+ "null", // Grouped with 'literal'
+ "gamma" // Grouped with 'literal'
+ ).andOnSecondInstance("myField",
+ "null", // Grouped with 'literal'
+ "beta", // Grouped with 'literal'
+ "null" // Grouped with 'literal'
+ ).whenQueryWithNullHandlingEnabled("select 'literal',
maxstring(myField) from testTable group by 'literal'")
+ .thenResultIs("STRING | STRING", "literal | gamma"); // Max of
{"alpha", "gamma", "beta"} is "gamma"
+ }
+
+ @Test
+ void aggregationGroupByMV() {
+ FluentQueryTest.withBaseDir(_baseDir)
+ .givenTable(
+ new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true) // Set at schema level
for general behavior
+ .addMultiValueDimension("tags", FieldSpec.DataType.STRING) //
Dimension for tags
+ .addDimensionField("value", FieldSpec.DataType.STRING)
+ .build(), SINGLE_FIELD_TABLE_CONFIG)
+ .onFirstInstance(
+ new Object[]{"tag1;tag2", "banana"}, // Row 1: tag1 -> "banana",
tag2 -> "banana"
+ new Object[]{"tag2;tag3", null} // Row 2: tag2 -> null, tag3
-> null
+ )
+ .andOnSecondInstance(
+ new Object[]{"tag1;tag2", "apple"}, // Row 3: tag1 -> "apple",
tag2 -> "apple"
+ new Object[]{"tag2;tag3", "cherry"} // Row 4: tag2 -> "cherry",
tag3 -> "cherry"
+ )
+ // Query without explicit null handling enabled via query option (uses
table schema setting or default)
+ .whenQuery("select tags, MAXSTRING(value) from testTable group by tags
order by tags")
+ .thenResultIs(
+ "STRING | STRING",
+ "tag1 | banana", // Values for tag1: "banana", "apple". Max is
"banana".
+ "tag2 | \"null\"",
+ // Values for tag2: "banana", "apple", null, "cherry". Max is
"null" (nulls ignored). This is because
+ // when null handling is disabled, the null value is read as
"null" and we need to honor that
+ "tag3 | \"null\"" // Values for tag3: null, "cherry". Max is
"null".
+ )
+ // Query with explicit null handling enabled via query option
+ .whenQueryWithNullHandlingEnabled("select tags, MAXSTRING(value) from
testTable "
+ + "group by tags order by tags")
+ .thenResultIs(
+ "STRING | STRING",
+ "tag1 | banana", // Values for tag1: "banana", "apple". Max is
"banana".
+ "tag2 | cherry", // Values for tag2: "banana", "apple",
"cherry". Max is "cherry".
+ "tag3 | cherry" // Values for tag3: "cherry". Max is "cherry".
+ );
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
new file mode 100644
index 00000000000..89a3bd17d00
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
@@ -0,0 +1,306 @@
+/**
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.RequestContextUtils;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import org.apache.pinot.queries.FluentQueryTest;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+
+public class MinStringAggregationFunctionTest extends
AbstractAggregationFunctionTest {
+
+ /**
+ * Helper method to create a FluentQueryTest builder for a table with a
single String field.
+ * This is used to simulate the DataTypeScenario concept from numeric
aggregation tests,
+ * but fixed for the STRING data type.
+ */
+ protected FluentQueryTest.DeclaringTable getDeclaringTable(boolean
enableColumnBasedNullHandling) {
+ return FluentQueryTest.withBaseDir(_baseDir)
+ .givenTable(
+ new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+
.setEnableColumnBasedNullHandling(enableColumnBasedNullHandling)
+ .addSingleValueDimension("myField", FieldSpec.DataType.STRING)
+ .build(), SINGLE_FIELD_TABLE_CONFIG);
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MinStringAggregationFunction function = new
MinStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ AggregationResultHolder resultHolder =
function.createAggregationResultHolder();
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+
+ try {
+ function.aggregate(10, resultHolder, blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MINSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateGroupBySVMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MinStringAggregationFunction function = new
MinStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ GroupByResultHolder groupByResultHolder =
function.createGroupByResultHolder(10, 20);
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+ try {
+ function.aggregateGroupBySV(10, new int[10], groupByResultHolder,
blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MINSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testNumericColumnExceptioninAggregateGroupByMVMethod() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MinStringAggregationFunction function = new
MinStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ GroupByResultHolder groupByResultHolder =
function.createGroupByResultHolder(10, 20);
+ Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+ BlockValSet mockBlockValSet = mock(BlockValSet.class);
+ when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+ blockValSetMap.put(expression, mockBlockValSet);
+ try {
+ function.aggregateGroupByMV(10, new int[10][], groupByResultHolder,
blockValSetMap);
+ fail("Should throw BadQueryRequestException");
+ } catch (BadQueryRequestException e) {
+ assertTrue(e.getMessage().contains("Cannot compute MINSTRING for numeric
column"));
+ }
+ }
+
+ @Test
+ public void testFunctionBasics() {
+ ExpressionContext expression = RequestContextUtils.getExpression("column");
+ MinStringAggregationFunction function = new
MinStringAggregationFunction(Collections.singletonList(expression),
+ false);
+
+ // Test function type
+ assertEquals(function.getType(), AggregationFunctionType.MINSTRING);
+
+ // Test string comparisons
+ assertEquals(function.merge("apple", "banana"), "apple");
+ assertEquals(function.merge("banana", "apple"), "apple");
+ assertEquals(function.merge("", "apple"), "");
+ assertEquals(function.merge("apple", ""), "");
+
+ // Test null handling
+ assertEquals(function.merge("apple", null), "apple");
+ assertEquals(function.merge(null, "apple"), "apple");
+ assertNull(function.merge(null, null));
+
+ // Test final result merging
+ assertEquals(function.mergeFinalResult("apple", "banana"), "apple");
+ }
+
+ @Test
+ void aggregationAllNullsWithNullHandlingDisabled() {
+ // For MINSTRING, when null handling is disabled, and all values are null,
+ // the result should be 'null' as there's no valid string to compare.
+ // This differs from numeric MAX/MIN which might return an initial default
value.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select minstring(myField) from testTable")
+ .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string
literal for the result
+ }
+
+ @Test
+ void aggregationAllNullsWithNullHandlingEnabled() {
+ // When null handling is enabled, and all values are null, the result
should also be 'null'.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select minstring(myField) from testTable")
+ .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string
literal for the result
+ }
+
+ @Test
+ void aggregationGroupBySVAllNullsWithNullHandlingDisabled() {
+ // For group by, if all values in a group are null and null handling is
disabled,
+ // the group's result for MINSTRING should be 'null'.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select 'literal', minstring(myField) from testTable group
by 'literal'")
+ // Expected "null" as a string literal for the aggregated column
+ .thenResultIs("STRING | STRING", "literal | \"null\"");
+ }
+
+ @Test
+ void aggregationGroupBySVAllNullsWithNullHandlingEnabled() {
+ // For group by, if all values in a group are null and null handling is
enabled,
+ // the group's result for MINSTRING should be 'null'.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "null",
+ "null"
+ ).andOnSecondInstance("myField",
+ "null"
+ ).whenQuery("select 'literal', minstring(myField) from testTable group
by 'literal'")
+ .thenResultIs("STRING | STRING", "literal | \"null\"");
+ }
+
+ @Test
+ void aggregationWithNullHandlingDisabled() {
+ // With null handling disabled, null values are effectively skipped, and
the minimum non-null
+ // string should be found. The updated function handles this correctly.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "cat",
+ "null",
+ "apple"
+ ).andOnSecondInstance("myField",
+ "null",
+ "zebra",
+ "null"
+ ).whenQuery("select minstring(myField) from testTable")
+ .thenResultIs("STRING", "apple"); // Min of {"cat", "apple", "zebra"}
is "apple"
+ }
+
+ @Test
+ void aggregationWithNullHandlingEnabled() {
+ // With null handling enabled, null values are explicitly ignored, and the
minimum non-null
+ // string should be found. The updated function handles this correctly.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "cat",
+ "null",
+ "apple"
+ ).andOnSecondInstance("myField",
+ "null",
+ "zebra",
+ "null"
+ ).whenQuery("select minstring(myField) from testTable")
+ .thenResultIs("STRING", "apple"); // Min of {"cat", "apple", "zebra"}
is "apple"
+ }
+
+ @Test
+ void aggregationGroupBySVWithNullHandlingDisabled() {
+ // Group By on a single value (SV) column with mixed nulls and non-nulls.
+ // Null handling disabled: nulls are ignored if there's at least one
non-null value in the group.
+ // The updated function should now correctly find the min among non-nulls.
+ getDeclaringTable(false) // nullHandlingEnabled = false
+ .onFirstInstance("myField",
+ "alpha", // Grouped with 'literal'
+ "null", // Grouped with 'literal'
+ "gamma" // Grouped with 'literal'
+ ).andOnSecondInstance("myField",
+ "null", // Grouped with 'literal'
+ "beta", // Grouped with 'literal'
+ "null" // Grouped with 'literal'
+ ).whenQuery("select 'literal', minstring(myField) from testTable group
by 'literal'")
+ .thenResultIs("STRING | STRING", "literal | alpha"); // Min of
{"alpha", "gamma", "beta"} is "alpha"
+ }
+
+ @Test
+ void aggregationGroupBySVWithNullHandlingEnabled() {
+ // Group By on a single value (SV) column with mixed nulls and non-nulls.
+ // Null handling enabled: nulls are ignored.
+ // The updated function should now correctly find the min among non-nulls.
+ getDeclaringTable(true) // nullHandlingEnabled = true
+ .onFirstInstance("myField",
+ "alpha", // Grouped with 'literal'
+ "null", // Grouped with 'literal'
+ "gamma" // Grouped with 'literal'
+ ).andOnSecondInstance("myField",
+ "null", // Grouped with 'literal'
+ "beta", // Grouped with 'literal'
+ "null" // Grouped with 'literal'
+ ).whenQuery("select 'literal', minstring(myField) from testTable group
by 'literal'")
+ .thenResultIs("STRING | STRING", "literal | alpha"); // Min of
{"alpha", "gamma", "beta"} is "alpha"
+ }
+
+ @Test
+ void aggregationGroupByMV() {
+ FluentQueryTest.withBaseDir(_baseDir)
+ .givenTable(
+ new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true) // Set at schema level
for general behavior
+ .addMultiValueDimension("tags", FieldSpec.DataType.STRING) //
Dimension for tags
+ .addDimensionField("value", FieldSpec.DataType.STRING)
+ .build(), SINGLE_FIELD_TABLE_CONFIG)
+ .onFirstInstance(
+ new Object[]{"tag1;tag2", "banana"}, // Row 1: tag1 -> "banana",
tag2 -> "banana"
+ new Object[]{"tag2;tag3", null} // Row 2: tag2 -> null, tag3
-> null
+ )
+ .andOnSecondInstance(
+ new Object[]{"tag1;tag2", "apple"}, // Row 3: tag1 -> "apple",
tag2 -> "apple"
+ new Object[]{"tag2;tag3", "cherry"} // Row 4: tag2 -> "cherry",
tag3 -> "cherry"
+ )
+ // Query without explicit null handling enabled via query option (uses
table schema setting or default)
+ .whenQuery("select tags, MINSTRING(value) from testTable group by tags
order by tags")
+ .thenResultIs(
+ "STRING | STRING",
+ "tag1 | apple", // Values for tag1: "banana", "apple". Min is
"apple".
+ "tag2 | apple", // Values for tag2: "banana", "apple",
"cherry". Min is "apple".
+ "tag3 | cherry" // Values for tag3: null, "cherry". Min is
"cherry".
+ )
+ // Query with explicit null handling enabled via query option
+ .whenQueryWithNullHandlingEnabled("select tags, MINSTRING(value) from
testTable "
+ + "group by tags order by tags")
+ .thenResultIs(
+ "STRING | STRING",
+ "tag1 | apple", // Values for tag1: "banana", "apple". Min is
"apple".
+ "tag2 | apple", // Values for tag2: "banana", "apple",
"cherry". Min is "apple".
+ "tag3 | cherry" // Values for tag3: null, "cherry". Min is
"cherry".
+ );
+ }
+}
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
index cace39ff1a1..ede51582739 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
@@ -53,6 +53,8 @@ public enum AggregationFunctionType {
// TODO: min/max only supports NUMERIC in Pinot, where Calcite supports
COMPARABLE_ORDERED
MIN("min", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
MAX("max", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
+ MINSTRING("minString", SqlTypeName.VARCHAR, SqlTypeName.VARCHAR),
+ MAXSTRING("maxString", SqlTypeName.VARCHAR, SqlTypeName.VARCHAR),
SUM("sum", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
SUM0("$sum0", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
SUMPRECISION("sumPrecision", ReturnTypes.explicit(SqlTypeName.DECIMAL),
OperandTypes.ANY, SqlTypeName.OTHER),
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]