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 6e0a0a1b35a Support ARRAY_AGG without a data type (#19988)
6e0a0a1b35a is described below
commit 6e0a0a1b35a6e9b80595487e0a1e5412691d1480
Author: Xiang Fu <[email protected]>
AuthorDate: Wed Oct 28 18:36:31 2026 -0700
Support ARRAY_AGG without a data type (#19988)
---
.../apache/pinot/sql/parsers/CalciteSqlParser.java | 11 +++
.../pinot/sql/parsers/CalciteSqlParserTest.java | 14 +++
.../AggregateFunctionRewriteOptimizer.java | 44 +++++++++
.../AggregateFunctionRewriteOptimizerTest.java | 44 +++++++++
.../pinot/queries/ArrayAggMvQueriesTest.java | 19 ++++
.../apache/pinot/queries/ArrayAggQueriesTest.java | 43 +++++++++
.../pinot/queries/ArrayAggStarTreeQueriesTest.java | 42 +++++++--
.../PinotAggregateExchangeNodeInsertRule.java | 9 ++
.../PinotAggregateTypeArgumentRewriteRule.java | 85 ++++++++++-------
.../PinotApproximateAggregateRewriteRule.java | 22 ++---
.../pinot/calcite/rel/rules/PinotRuleUtils.java | 21 +++++
.../org/apache/pinot/query/QueryEnvironment.java | 8 +-
.../v2/opt/rules/AggregatePushdownRule.java | 9 ++
.../query/ApproximateAggregateRewriteTest.java | 32 +++++++
.../src/test/resources/queries/AggregatePlans.json | 105 +++++++++++++++++++++
.../src/test/resources/queries/UDFAggregates.json | 95 ++++++++++++++++++-
16 files changed, 536 insertions(+), 67 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
index 274b6be95cf..1312b2ac3a1 100644
---
a/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
+++
b/pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java
@@ -1308,6 +1308,17 @@ public class CalciteSqlParser {
operands.add(toExpression(childNode));
}
}
+ if (functionKind == SqlKind.ARRAY_AGG &&
functionNode.getFunctionQuantifier() != null
+ && "DISTINCT".equals(functionNode.getFunctionQuantifier().toString()))
{
+ Expression distinct = RequestUtils.getLiteralExpression(true);
+ int lastIndex = operands.size() - 1;
+ if ((operands.size() == 2 || operands.size() == 3) &&
operands.get(lastIndex).isSetLiteral()
+ && operands.get(lastIndex).getLiteral().isSetBoolValue()) {
+ operands.set(lastIndex, distinct);
+ } else if (operands.size() == 1 || operands.size() == 2) {
+ operands.add(distinct);
+ }
+ }
ParserUtils.validateFunction(canonicalName, operands);
Expression functionExpression =
RequestUtils.getFunctionExpression(canonicalName, operands);
if (negated) {
diff --git
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlParserTest.java
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlParserTest.java
index 2e3d93f30d5..0845d127e8f 100644
---
a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlParserTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlParserTest.java
@@ -64,6 +64,20 @@ public class CalciteSqlParserTest {
private static final String SINGLE_CHAR = "a";
private static final String QUERY_TEMPLATE = "SELECT %s FROM %s";
+ @Test
+ public void testArrayAggDistinctPreservesNonBooleanOperands() {
+ for (String[] arguments : new String[][]{
+ {"intCol, UPPER('long')", "intCol, UPPER('long'), true"},
+ {"intCol, longCol", "intCol, longCol, true"},
+ {"intCol, 'INT', strCol", "intCol, 'INT', strCol"},
+ {"intCol ORDER BY intCol", "intCol, intCol, true"}
+ }) {
+ assertEquals(CalciteSqlParser.compileToPinotQuery("SELECT
ARRAY_AGG(DISTINCT " + arguments[0] + ") FROM t")
+ .getSelectList(),
+ CalciteSqlParser.compileToPinotQuery("SELECT ARRAY_AGG(" +
arguments[1] + ") FROM t").getSelectList());
+ }
+ }
+
@AfterMethod
public void resetLegacyUnescaping() {
// Reset to default (false) after each test
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
index 398e5d53b9b..997bac50e18 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
@@ -37,6 +37,7 @@ import org.apache.pinot.spi.utils.CommonConstants;
/// Always applied:
/// - FIRST_WITH_TIME(dataColumn, timeColumn) -> FIRST_WITH_TIME(dataColumn,
timeColumn, 'dataType')
/// - LAST_WITH_TIME(dataColumn, timeColumn) -> LAST_WITH_TIME(dataColumn,
timeColumn, 'dataType')
+/// - ARRAY_AGG(column[, isDistinct]) -> ARRAY_AGG(column, 'dataType'[,
isDistinct])
/// - MODE(stringColumn[, 'MIN'|'MAX']) -> MODE(stringColumn, 'MIN'|'MAX',
'STRING')
///
/// Applied only when enabled through the `inferAnyValueType` query option:
@@ -112,6 +113,10 @@ public class AggregateFunctionRewriteOptimizer implements
StatementOptimizer {
}
return;
}
+ if (normalizedName.equals(AggregationFunctionType.ARRAYAGG.name()) &&
isArrayAggWithoutDataType(function)) {
+ addArrayAggDataTypeArgument(function, normalizedName, fieldSpec);
+ return;
+ }
if (normalizedName.equals(AggregationFunctionType.MODE.name()) &&
function.getOperandsSize() <= 2) {
addModeDataTypeArgument(function, fieldSpec);
return;
@@ -175,6 +180,45 @@ public class AggregateFunctionRewriteOptimizer implements
StatementOptimizer {
function.addToOperands(RequestUtils.getLiteralExpression(dataType));
}
+ /// Matches ARRAY_AGG(column) and ARRAY_AGG(column, isDistinct); a string
second argument is the explicit data type.
+ private static boolean isArrayAggWithoutDataType(Function function) {
+ int numOperands = function.getOperandsSize();
+ if (numOperands == 1) {
+ return true;
+ }
+ if (numOperands == 2) {
+ Expression secondOperand = function.getOperands().get(1);
+ return secondOperand.isSetLiteral() &&
secondOperand.getLiteral().isSetBoolValue();
+ }
+ return false;
+ }
+
+ /// Inserts the column's logical type, the element type for a multi-value
column, as the second argument. The
+ /// supported types match `PinotAggregateTypeArgumentRewriteRule` in the
multi-stage engine.
+ private static void addArrayAggDataTypeArgument(Function function, String
functionName, FieldSpec fieldSpec) {
+ switch (fieldSpec.getDataType()) {
+ case BOOLEAN:
+ case INT:
+ case LONG:
+ case TIMESTAMP:
+ case FLOAT:
+ case DOUBLE:
+ case BIG_DECIMAL:
+ case STRING:
+ case BYTES:
+ case UUID:
+ function.getOperands().add(1,
RequestUtils.getLiteralExpression(fieldSpec.getDataType().name()));
+ return;
+ case JSON:
+ // Same as the multi-stage engine, which types a JSON column as
VARCHAR.
+ function.getOperands().add(1,
RequestUtils.getLiteralExpression(FieldSpec.DataType.STRING.name()));
+ return;
+ default:
+ throw new BadQueryRequestException(
+ String.format("Unsupported data type for %s(expression): %s",
functionName, fieldSpec.getDataType()));
+ }
+ }
+
private static void rewriteToTypedVariant(Function function, String
functionName, FieldSpec.DataType operandType) {
// Rewrite MIN(stringCol) and MAX(stringCol) to MINSTRING / MAXSTRING
// Rewrite MIN(longCol) and MAX(longCol) to MINLONG / MAXLONG
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
index dc58ca7bac4..1cc042a0677 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
@@ -18,7 +18,9 @@
*/
package org.apache.pinot.core.query.optimizer.statement;
+import java.util.Map;
import org.apache.pinot.common.request.PinotQuery;
+import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.exception.BadQueryRequestException;
@@ -48,6 +50,9 @@ public class AggregateFunctionRewriteOptimizerTest {
.addMultiValueDimension("mvCol", DataType.INT)
.addMultiValueDimension("mvStringCol", DataType.STRING)
.addSingleValueDimension("timeCol", DataType.LONG)
+ .addComplex("mapCol", DataType.MAP, Map.of(
+ "key", new DimensionFieldSpec("key", DataType.STRING, true),
+ "value", new DimensionFieldSpec("value", DataType.INT, true)))
.build();
private static final String AUTO_REWRITE = "SET
autoRewriteAggregationType=true; ";
private static final String INFER_ANY_VALUE_TYPE = "SET
inferAnyValueType=true; ";
@@ -88,6 +93,45 @@ public class AggregateFunctionRewriteOptimizerTest {
+ "FROM testTable");
}
+ @DataProvider
+ public Object[][] arrayAggDataTypes() {
+ return new Object[][]{
+ {"intCol", "INT"}, {"longCol", "LONG"}, {"floatCol", "FLOAT"},
{"doubleCol", "DOUBLE"},
+ {"bigDecimalCol", "BIG_DECIMAL"}, {"stringCol", "STRING"}, {"jsonCol",
"STRING"}, {"boolCol", "BOOLEAN"},
+ {"timestampCol", "TIMESTAMP"}, {"bytesCol", "BYTES"}, {"uuidCol",
"UUID"}, {"mvCol", "INT"}
+ };
+ }
+
+ @Test(dataProvider = "arrayAggDataTypes")
+ public void testArrayAggInfersDataType(String column, String dataType) {
+ assertRewrite("SELECT ARRAY_AGG(" + column + "), arrayAgg(" + column + ",
true) FROM testTable",
+ "SELECT ARRAY_AGG(" + column + ", '" + dataType + "'), arrayAgg(" +
column + ", '" + dataType + "', true) "
+ + "FROM testTable");
+ }
+
+ @Test
+ public void testArrayAggDistinctKeyword() {
+ for (String arguments : new String[]{"intCol", "intCol, false", "intCol,
'INT'", "intCol, 'INT', true",
+ "intCol, 'INT', false"}) {
+ assertRewrite("SELECT ARRAY_AGG(DISTINCT " + arguments + ") FROM
testTable",
+ "SELECT ARRAY_AGG(intCol, 'INT', true) FROM testTable");
+ }
+ }
+
+ @Test
+ public void testArrayAggRejectsUnsupportedColumn() {
+ PinotQuery query = CalciteSqlParser.compileToPinotQuery("SELECT
ARRAY_AGG(mapCol) FROM testTable");
+ BadQueryRequestException exception =
expectThrows(BadQueryRequestException.class,
+ () -> new AggregateFunctionRewriteOptimizer().optimize(query, SCHEMA));
+ assertEquals(exception.getMessage(), "Unsupported data type for
ARRAYAGG(expression): MAP");
+ }
+
+ @Test
+ public void testArrayAggKeepsExplicitFormsAndColumnsWithoutSchemaType() {
+ assertUnchanged("SELECT ARRAY_AGG(intCol, 'LONG'), ARRAY_AGG(intCol,
'LONG', true), ARRAY_AGG(intCol, 1), "
+ + "ARRAY_AGG(missingCol), ARRAY_AGG(trim(stringCol), true) FROM
testTable");
+ }
+
@Test
public void testRewritesNestedAggregationsConsistently() {
// Aliases, transforms, FILTER, HAVING and ORDER BY wrap the aggregation
in another function.
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggMvQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggMvQueriesTest.java
index 118d27d143c..d21125a17e7 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggMvQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggMvQueriesTest.java
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.common.response.broker.ResultTable;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
import org.apache.pinot.core.operator.query.AggregationOperator;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
@@ -162,6 +163,24 @@ public class ArrayAggMvQueriesTest extends BaseQueriesTest
{
assertEquals(((String[]) resultTable.getRows().get(0)[4]).length, 2 *
NUM_RECORDS);
}
+ /// The data type argument can be omitted for a multi-value column; the
optimizer supplies its element type.
+ @Test
+ public void testArrayAggMvWithInferredDataType() {
+ String query = "SELECT ArrayAgg(intMV), ArrayAgg(longMV, true),
ArrayAgg(floatMV), ArrayAgg(doubleMV, true), "
+ + "ArrayAgg(stringMV) FROM testTableMv";
+ ResultTable resultTable = getBrokerResponseForOptimizedQuery(query,
SCHEMA).getResultTable();
+ assertEquals(resultTable.getDataSchema().getColumnDataTypes(), new
ColumnDataType[]{
+ ColumnDataType.INT_ARRAY, ColumnDataType.LONG_ARRAY,
ColumnDataType.FLOAT_ARRAY, ColumnDataType.DOUBLE_ARRAY,
+ ColumnDataType.STRING_ARRAY
+ });
+ Object[] row = resultTable.getRows().get(0);
+ assertEquals(((int[]) row[0]).length, 8 * NUM_RECORDS);
+ assertEquals(((long[]) row[1]).length, 2 * NUM_RECORDS);
+ assertEquals(((float[]) row[2]).length, 8 * NUM_RECORDS);
+ assertEquals(((double[]) row[3]).length, 2 * NUM_RECORDS);
+ assertEquals(((String[]) row[4]).length, 8 * NUM_RECORDS);
+ }
+
@AfterClass
public void tearDown()
throws IOException {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggQueriesTest.java
index a1749adeeea..674203533e9 100644
--- a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggQueriesTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggQueriesTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.queries;
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -29,6 +30,7 @@ import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.pinot.common.response.broker.ResultTable;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
import org.apache.pinot.common.utils.HashUtil;
import org.apache.pinot.core.operator.blocks.results.AggregationResultsBlock;
import org.apache.pinot.core.operator.query.AggregationOperator;
@@ -83,6 +85,11 @@ public class ArrayAggQueriesTest extends BaseQueriesTest {
.addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
.addSingleValueDimension(UUID_COLUMN, DataType.UUID)
.build();
+ // The JSON column aggregates to strings
+ private static final ColumnDataType[] EXPECTED_COLUMN_DATA_TYPES = {
+ ColumnDataType.INT_ARRAY, ColumnDataType.LONG_ARRAY,
ColumnDataType.FLOAT_ARRAY, ColumnDataType.DOUBLE_ARRAY,
+ ColumnDataType.STRING_ARRAY, ColumnDataType.STRING_ARRAY,
ColumnDataType.BYTES_ARRAY, ColumnDataType.UUID_ARRAY
+ };
private Set<Integer> _values;
private int[] _expectedCardinalityResults;
@@ -226,6 +233,42 @@ public class ArrayAggQueriesTest extends BaseQueriesTest {
assertEquals(((String[]) resultTable.getRows().get(0)[7]).length,
_expectedCardinalityResults[7]);
}
+ /// The data type argument can be omitted for a column; the optimizer
supplies it from the schema.
+ @Test
+ public void testArrayAggWithInferredDataType() {
+ String query = "SELECT ArrayAgg(intColumn), ArrayAgg(longColumn),
ArrayAgg(floatColumn), ArrayAgg(doubleColumn), "
+ + "ArrayAgg(stringColumn), ArrayAgg(jsonColumn),
ArrayAgg(bytesColumn), ArrayAgg(uuidColumn) FROM testTable";
+ ResultTable resultTable = getBrokerResponseForOptimizedQuery(query,
SCHEMA).getResultTable();
+ assertEquals(resultTable.getDataSchema().getColumnDataTypes(),
EXPECTED_COLUMN_DATA_TYPES);
+ Object[] row = resultTable.getRows().get(0);
+ for (int i = 0; i < 8; i++) {
+ assertEquals(Array.getLength(row[i]), 4 * NUM_RECORDS);
+ }
+ }
+
+ @Test
+ public void testArrayAggDistinctWithInferredDataType() {
+ String query = "SELECT ArrayAgg(intColumn, true), ArrayAgg(longColumn,
true), ArrayAgg(floatColumn, true), "
+ + "ArrayAgg(doubleColumn, true), ArrayAgg(stringColumn, true),
ArrayAgg(jsonColumn, true), "
+ + "ArrayAgg(bytesColumn, true), ArrayAgg(uuidColumn, true) FROM
testTable";
+ ResultTable resultTable = getBrokerResponseForOptimizedQuery(query,
SCHEMA).getResultTable();
+ assertEquals(resultTable.getDataSchema().getColumnDataTypes(),
EXPECTED_COLUMN_DATA_TYPES);
+ Object[] row = resultTable.getRows().get(0);
+ for (int i = 0; i < 8; i++) {
+ assertEquals(Array.getLength(row[i]), _expectedCardinalityResults[i]);
+ }
+ }
+
+ @Test
+ public void testArrayAggDistinctKeyword() {
+ for (String arguments : new String[]{"intColumn", "intColumn, false",
"intColumn, 'INT'",
+ "intColumn, 'INT', true", "intColumn, 'INT', false"}) {
+ ResultTable result = getBrokerResponseForOptimizedQuery(
+ "SELECT ARRAY_AGG(DISTINCT " + arguments + ") FROM testTable",
SCHEMA).getResultTable();
+ assertEquals(((int[]) result.getRows().get(0)[0]).length,
_expectedCardinalityResults[0]);
+ }
+ }
+
@AfterClass
public void tearDown()
throws IOException {
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggStarTreeQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggStarTreeQueriesTest.java
index ac3b9c4565a..6d64c2d1d10 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggStarTreeQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/ArrayAggStarTreeQueriesTest.java
@@ -64,6 +64,13 @@ public class ArrayAggStarTreeQueriesTest extends
BaseQueriesTest {
private static final String LONG_METRIC = "lm";
private static final String STRING_METRIC = "sm";
private static final String BYTES_METRIC = "bm";
+ private static final Schema SCHEMA = new
Schema.SchemaBuilder().setSchemaName(RAW_TABLE_NAME)
+ .addSingleValueDimension(DIMENSION, DataType.INT)
+ .addSingleValueDimension(INT_METRIC, DataType.INT)
+ .addSingleValueDimension(LONG_METRIC, DataType.LONG)
+ .addSingleValueDimension(STRING_METRIC, DataType.STRING)
+ .addSingleValueDimension(BYTES_METRIC, DataType.BYTES)
+ .build();
private static final Map<String, String> STAR_TREE_ON =
Map.of("useStarTree", "true");
private static final Map<String, String> STAR_TREE_OFF =
Map.of("useStarTree", "false");
@@ -95,13 +102,6 @@ public class ArrayAggStarTreeQueriesTest extends
BaseQueriesTest {
throws Exception {
FileUtils.deleteDirectory(INDEX_DIR);
- Schema schema = new Schema.SchemaBuilder().setSchemaName(RAW_TABLE_NAME)
- .addSingleValueDimension(DIMENSION, DataType.INT)
- .addSingleValueDimension(INT_METRIC, DataType.INT)
- .addSingleValueDimension(LONG_METRIC, DataType.LONG)
- .addSingleValueDimension(STRING_METRIC, DataType.STRING)
- .addSingleValueDimension(BYTES_METRIC, DataType.BYTES)
- .build();
// The LONG metric is raw-encoded: arrayAgg star-tree aggregation supports
both dictionary-encoded and raw
// single-value source columns, so the LONG tests double as raw-metric
build/query coverage.
TableConfig tableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME)
@@ -119,7 +119,7 @@ public class ArrayAggStarTreeQueriesTest extends
BaseQueriesTest {
rows.add(row);
}
- SegmentGeneratorConfig segmentGeneratorConfig = new
SegmentGeneratorConfig(tableConfig, schema);
+ SegmentGeneratorConfig segmentGeneratorConfig = new
SegmentGeneratorConfig(tableConfig, SCHEMA);
segmentGeneratorConfig.setSegmentName(SEGMENT_NAME);
segmentGeneratorConfig.setOutDir(INDEX_DIR.getPath());
SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
@@ -271,11 +271,33 @@ public class ArrayAggStarTreeQueriesTest extends
BaseQueriesTest {
"Non-distinct arrayAgg must not use the star-tree, so it scans the
same docs with the option on or off");
}
+ /// Without the data type argument, distinct arrayAgg is rewritten to the
explicit form with each column's type, which
+ /// the star-tree still serves.
+ @Test
+ public void testDistinctArrayAggWithInferredDataTypeUsesStarTree() {
+ String query = String.format(
+ "SELECT %s, arrayAgg(%s, true), arrayAgg(%s, true), arrayAgg(%s,
true), arrayAgg(%s, true) FROM %s "
+ + "GROUP BY %s ORDER BY %s", DIMENSION, INT_METRIC, LONG_METRIC,
STRING_METRIC, BYTES_METRIC,
+ RAW_TABLE_NAME, DIMENSION, DIMENSION);
+ String explicitQuery = String.format(
+ "SELECT %s, arrayAgg(%s, 'INT', true), arrayAgg(%s, 'LONG', true),
arrayAgg(%s, 'STRING', true), "
+ + "arrayAgg(%s, 'BYTES', true) FROM %s GROUP BY %s ORDER BY %s",
DIMENSION, INT_METRIC, LONG_METRIC,
+ STRING_METRIC, BYTES_METRIC, RAW_TABLE_NAME, DIMENSION, DIMENSION);
+ BrokerResponseNative starTree = getBrokerResponseForOptimizedQuery("SET
useStarTree=true; " + query, SCHEMA);
+ BrokerResponseNative rawScan = getBrokerResponse(explicitQuery,
STAR_TREE_OFF);
+ assertTrue(starTree.getNumDocsScanned() < rawScan.getNumDocsScanned(),
+ "Expected the star-tree to serve the distinct arrayAgg query without a
data type argument");
+ assertSameDistinctValues(starTree.getResultTable(),
rawScan.getResultTable());
+ }
+
/// Runs the query with the star-tree enabled and disabled and asserts each
arrayAgg cell (an unordered MV of
/// distinct values) is set-equal between the two, per group.
private void assertStarTreeMatchesRawScan(String query) {
- ResultTable starTree = getBrokerResponse(query,
STAR_TREE_ON).getResultTable();
- ResultTable rawScan = getBrokerResponse(query,
STAR_TREE_OFF).getResultTable();
+ assertSameDistinctValues(getBrokerResponse(query,
STAR_TREE_ON).getResultTable(),
+ getBrokerResponse(query, STAR_TREE_OFF).getResultTable());
+ }
+
+ private static void assertSameDistinctValues(ResultTable starTree,
ResultTable rawScan) {
List<Object[]> starTreeRows = starTree.getRows();
List<Object[]> rawScanRows = rawScan.getRows();
assertEquals(starTreeRows.size(), rawScanRows.size());
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateExchangeNodeInsertRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateExchangeNodeInsertRule.java
index 2411867f5e1..31aa9619630 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateExchangeNodeInsertRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateExchangeNodeInsertRule.java
@@ -578,6 +578,15 @@ public class PinotAggregateExchangeNodeInsertRule {
functionCategory = SqlFunctionCategory.USER_DEFINED_FUNCTION;
} else if (kind == SqlKind.LISTAGG) {
rexList.add(input.getCluster().getRexBuilder().makeLiteral(true));
+ } else if
(AggregationFunctionType.getNormalizedAggregationFunctionName(functionName)
+ .equals(AggregationFunctionType.ARRAYAGG.name())) {
+ // ARRAY_AGG(expression, 'dataType'[, isDistinct]) takes DISTINCT as
its isDistinct argument
+ RexNode isDistinct =
input.getCluster().getRexBuilder().makeLiteral(true);
+ if (rexList.size() == 3) {
+ rexList.set(2, isDistinct);
+ } else {
+ rexList.add(isDistinct);
+ }
}
}
SqlReturnTypeInference returnTypeInference = null;
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
index edbc553d07f..324d10fb5c9 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
@@ -31,9 +31,7 @@ import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.rel.core.Project;
-import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.logical.LogicalAggregate;
-import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexLiteral;
@@ -48,16 +46,17 @@ import org.apache.pinot.segment.spi.AggregationFunctionType;
/// Supplies the explicit data type argument from the type of the data
argument:
/// - FIRST_WITH_TIME(dataColumn, timeColumn) -> FIRST_WITH_TIME(dataColumn,
timeColumn, 'dataType')
/// - LAST_WITH_TIME(dataColumn, timeColumn) -> LAST_WITH_TIME(dataColumn,
timeColumn, 'dataType')
+/// - ARRAY_AGG(expression[, isDistinct]) -> ARRAY_AGG(expression,
'dataType'[, isDistinct])
/// - MODE(stringColumn) -> MODE(stringColumn, 'MIN', 'STRING')
/// - ANY_VALUE(expr) -> ANY_VALUE(expr, 'dataType'), except by
[#WITHOUT_ANY_VALUE]
///
/// The result type of the call is unchanged. The literals are projected
directly beneath the aggregate, appended to
/// the existing project when there is one, because
[PinotAggregateExchangeNodeInsertRule] and `AggregatePushdownRule`
-/// inline literal arguments only from the immediate project.
+/// inline literal arguments from the immediate project or the inputs of a
union.
///
-/// The servers require the data type argument of FIRST_WITH_TIME /
LAST_WITH_TIME, so the rule is required rather than
-/// an optimization: `QueryEnvironment` always applies [#WITHOUT_ANY_VALUE],
or [#INSTANCE] when the `inferAnyValueType`
-/// query option is true, and `skipPlannerRules` cannot turn it off.
+/// The servers require type arguments for FIRST_WITH_TIME, LAST_WITH_TIME and
ARRAY_AGG. `QueryEnvironment` always
+/// applies [#WITHOUT_ANY_VALUE], or [#INSTANCE] when `inferAnyValueType` is
true, regardless of `skipPlannerRules`.
+
public class PinotAggregateTypeArgumentRewriteRule extends RelOptRule {
private static final Set<ColumnDataType> ANY_VALUE_DATA_TYPES =
EnumSet.of(ColumnDataType.INT, ColumnDataType.LONG,
ColumnDataType.FLOAT, ColumnDataType.DOUBLE,
@@ -65,20 +64,27 @@ public class PinotAggregateTypeArgumentRewriteRule extends
RelOptRule {
ColumnDataType.BYTES, ColumnDataType.UUID);
public static final PinotAggregateTypeArgumentRewriteRule INSTANCE =
- new PinotAggregateTypeArgumentRewriteRule(true,
PinotRuleUtils.PINOT_REL_FACTORY, null);
+ new PinotAggregateTypeArgumentRewriteRule(true, false,
PinotRuleUtils.PINOT_REL_FACTORY, null);
/// Leaves ANY_VALUE without the data type argument.
public static final PinotAggregateTypeArgumentRewriteRule WITHOUT_ANY_VALUE =
- new PinotAggregateTypeArgumentRewriteRule(false,
PinotRuleUtils.PINOT_REL_FACTORY,
+ new PinotAggregateTypeArgumentRewriteRule(false, false,
PinotRuleUtils.PINOT_REL_FACTORY,
"PinotAggregateTypeArgumentRewriteRuleWithoutAnyValue");
+ /// Runs before CASE-to-FILTER without changing other aggregation functions.
+ public static final PinotAggregateTypeArgumentRewriteRule ARRAY_AGG_ONLY =
+ new PinotAggregateTypeArgumentRewriteRule(false, true,
PinotRuleUtils.PINOT_REL_FACTORY,
+ "PinotAggregateTypeArgumentRewriteRuleArrayAggOnly");
+
+ private final boolean _arrayAggOnly;
private final boolean _rewriteAnyValue;
// Matches with RelOptRule operands, like the other aggregate rewrite rules,
instead of a RelRule config.
@SuppressWarnings("deprecation")
- private PinotAggregateTypeArgumentRewriteRule(boolean rewriteAnyValue,
RelBuilderFactory factory,
- @Nullable String description) {
+ private PinotAggregateTypeArgumentRewriteRule(boolean rewriteAnyValue,
boolean arrayAggOnly,
+ RelBuilderFactory factory, @Nullable String description) {
super(operand(LogicalAggregate.class, any()), factory, description);
_rewriteAnyValue = rewriteAnyValue;
+ _arrayAggOnly = arrayAggOnly;
}
@Override
@@ -93,26 +99,9 @@ public class PinotAggregateTypeArgumentRewriteRule extends
RelOptRule {
public void onMatch(RelOptRuleCall call) {
Aggregate aggRel = call.rel(0);
RelNode input = aggRel.getInput();
- RelNode unboxedInput = PinotRuleUtils.unboxRel(input);
RexBuilder rexBuilder = aggRel.getCluster().getRexBuilder();
-
- List<RexNode> projects;
- RelNode projectInput;
- List<RelHint> hints;
- if (unboxedInput instanceof Project) {
- Project project = (Project) unboxedInput;
- projects = new ArrayList<>(project.getProjects());
- projectInput = project.getInput();
- hints = project.getHints();
- } else {
- int numFields = input.getRowType().getFieldCount();
- projects = new ArrayList<>(numFields + 1);
- for (int i = 0; i < numFields; i++) {
- projects.add(rexBuilder.makeInputRef(input, i));
- }
- projectInput = input;
- hints = List.of();
- }
+ int numInputFields = input.getRowType().getFieldCount();
+ List<RexNode> literals = new ArrayList<>();
// Appending keeps the existing field indexes, so the group set and the
other calls are unaffected.
List<AggregateCall> orgAggCalls = aggRel.getAggCallList();
@@ -125,16 +114,18 @@ public class PinotAggregateTypeArgumentRewriteRule
extends RelOptRule {
continue;
}
List<Integer> argList = new ArrayList<>(aggCall.getArgList());
+ int argumentPosition = isArrayAgg(aggCall) ? 1 : argList.size();
for (String literalArgument : literalArguments) {
- argList.add(literalIndexes.computeIfAbsent(literalArgument, literal ->
{
- projects.add(rexBuilder.makeLiteral(literal));
- return projects.size() - 1;
- }));
+ int literalIndex = literalIndexes.computeIfAbsent(literalArgument,
literal -> {
+ literals.add(rexBuilder.makeLiteral(literal));
+ return numInputFields + literals.size() - 1;
+ });
+ argList.add(argumentPosition++, literalIndex);
}
// Keeps the original result type, so the rewrite does not change the
result schema.
aggCalls.add(aggCall.withArgList(argList));
}
- RelNode newInput = LogicalProject.create(projectInput, hints, projects,
(List<String>) null, Set.of());
+ RelNode newInput = PinotRuleUtils.appendAggregateLiterals(input, literals);
call.transformTo(
aggRel.copy(aggRel.getTraitSet(), newInput, aggRel.getGroupSet(),
aggRel.getGroupSets(), aggCalls));
}
@@ -144,6 +135,23 @@ public class PinotAggregateTypeArgumentRewriteRule extends
RelOptRule {
private List<String> getLiteralArguments(AggregateCall aggCall, RelNode
input) {
List<Integer> argList = aggCall.getArgList();
String name =
AggregationFunctionType.getNormalizedAggregationFunctionName(aggCall.getAggregation().getName());
+ if (isArrayAgg(aggCall)) {
+ if (argList.size() != 1 && (argList.size() != 2
+ || getSqlTypeName(input, argList.get(1)) != SqlTypeName.BOOLEAN)) {
+ return null;
+ }
+ RelDataType dataType =
input.getRowType().getFieldList().get(argList.get(0)).getType();
+ if (dataType.getComponentType() != null) {
+ dataType = dataType.getComponentType();
+ }
+ if (dataType.getSqlTypeName() == SqlTypeName.DECIMAL) {
+ return
List.of(RelToPlanNodeConverter.convertToColumnDataType(aggCall.getType()).toDataType().name());
+ }
+ return List.of(getDataTypeArgument(name, dataType.getSqlTypeName()));
+ }
+ if (_arrayAggOnly) {
+ return null;
+ }
if (name.equals(AggregationFunctionType.MODE.name())) {
// BOOLEAN and TIMESTAMP already return their own type, cast from the
DOUBLE result.
return argList.size() == 1 &&
SqlTypeName.CHAR_TYPES.contains(getSqlTypeName(input, argList.get(0)))
@@ -202,7 +210,9 @@ public class PinotAggregateTypeArgumentRewriteRule extends
RelOptRule {
return "UUID";
default:
throw new IllegalArgumentException(
- String.format("Unsupported data type for %s(dataColumn,
timeColumn): %s", name, dataType));
+ String.format("Unsupported data type for %s%s: %s", name,
+ name.equals(AggregationFunctionType.ARRAYAGG.name()) ?
"(expression)" : "(dataColumn, timeColumn)",
+ dataType));
}
}
@@ -236,4 +246,9 @@ public class PinotAggregateTypeArgumentRewriteRule extends
RelOptRule {
relDataType.getSqlTypeName(), explicitDataType != null ?
explicitDataType : dataTypeArgument));
}
}
+
+ private static boolean isArrayAgg(AggregateCall aggCall) {
+ return
AggregationFunctionType.getNormalizedAggregationFunctionName(aggCall.getAggregation().getName())
+ .equals(AggregationFunctionType.ARRAYAGG.name());
+ }
}
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotApproximateAggregateRewriteRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotApproximateAggregateRewriteRule.java
index 21a23a5dc76..f0db5ca3b65 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotApproximateAggregateRewriteRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotApproximateAggregateRewriteRule.java
@@ -29,7 +29,6 @@ import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.core.AggregateCall;
import org.apache.calcite.rel.logical.LogicalAggregate;
-import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlAggFunction;
@@ -101,29 +100,22 @@ public class PinotApproximateAggregateRewriteRule extends
RelOptRule {
// indices of every call stay valid. With no parameters configured the
plan keeps the shape it had before, with no
// extra project.
//
- // Invariant: this project has to stay directly beneath the aggregate.
Both PinotAggregateExchangeNodeInsertRule
- // and AggregatePushdownRule inline the literal only through
findImmediateProjects(input), so anything that puts a
- // node between them would leave the leaf with a column reference where
the function expects a parameter string.
- // Nothing in the later phases does: AggregateProjectMergeRule needs an
all-input-ref project and the literals
- // block it, and ProjectMergeRule collapses into a single project that
stays adjacent.
+ // Keep literal arguments in the immediate project visible to the
aggregate split rules.
RelNode newInput = input;
int distinctCountParamsIndex = -1;
int percentileParamsIndex = -1;
if (!distinctCountParams.isEmpty() || !percentileParams.isEmpty()) {
RexBuilder rexBuilder = aggRel.getCluster().getRexBuilder();
- List<RexNode> projects = new ArrayList<>(numInputFields + 2);
- for (int i = 0; i < numInputFields; i++) {
- projects.add(rexBuilder.makeInputRef(input, i));
- }
+ List<RexNode> literals = new ArrayList<>(2);
if (!distinctCountParams.isEmpty()) {
- distinctCountParamsIndex = projects.size();
- projects.add(rexBuilder.makeLiteral(distinctCountParams));
+ distinctCountParamsIndex = numInputFields + literals.size();
+ literals.add(rexBuilder.makeLiteral(distinctCountParams));
}
if (!percentileParams.isEmpty()) {
- percentileParamsIndex = projects.size();
- projects.add(rexBuilder.makeLiteral(percentileParams));
+ percentileParamsIndex = numInputFields + literals.size();
+ literals.add(rexBuilder.makeLiteral(percentileParams));
}
- newInput = LogicalProject.create(input, List.of(), projects,
(List<String>) null);
+ newInput = PinotRuleUtils.appendAggregateLiterals(input, literals);
}
List<AggregateCall> rewrittenAggCalls = new
ArrayList<>(aggRel.getAggCallList().size());
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRuleUtils.java
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRuleUtils.java
index 6bbb888ebab..b71130b9c72 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRuleUtils.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotRuleUtils.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
+import java.util.Set;
import javax.annotation.Nullable;
import org.apache.calcite.plan.Contexts;
import org.apache.calcite.plan.RelTraitSet;
@@ -38,6 +39,8 @@ import org.apache.calcite.rel.core.TableScan;
import org.apache.calcite.rel.core.Window;
import org.apache.calcite.rel.logical.LogicalAsofJoin;
import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexLiteral;
@@ -85,6 +88,24 @@ public class PinotRuleUtils {
}
}
+ /// Appends literal arguments to the immediate project, preserving existing
column references.
+ public static RelNode appendAggregateLiterals(RelNode input, List<RexNode>
literals) {
+ RelNode rel = unboxRel(input);
+ if (rel instanceof Project) {
+ Project project = (Project) rel;
+ List<RexNode> projects = new ArrayList<>(project.getProjects());
+ projects.addAll(literals);
+ return LogicalProject.create(project.getInput(), project.getHints(),
projects, (List<String>) null, Set.of());
+ }
+ RexBuilder rexBuilder = input.getCluster().getRexBuilder();
+ List<RexNode> projects = new
ArrayList<>(input.getRowType().getFieldCount() + literals.size());
+ for (int i = 0; i < input.getRowType().getFieldCount(); i++) {
+ projects.add(rexBuilder.makeInputRef(input, i));
+ }
+ projects.addAll(literals);
+ return LogicalProject.create(input, List.of(), projects, (List<String>)
null, Set.of());
+ }
+
public static boolean isExchange(RelNode rel) {
return unboxRel(rel) instanceof Exchange;
}
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
index 8b50aea94f6..95e9b4f102e 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
@@ -610,10 +610,14 @@ public class QueryEnvironment {
// Run the Calcite CORE rules using 1 HepInstruction per rule. We use 1
HepInstruction per rule for simplicity:
// the rules used here can rest assured that they are the only ones
evaluated in a dedicated graph-traversal.
for (RelOptRule relOptRule : basicRules) {
+ // Supplies the data type argument before AggregateCaseToFilter, which
rewrites only one-argument calls: its
+ // ARRAY_AGG(expression) FILTER (WHERE condition) form of ARRAY_AGG(CASE
WHEN ...) fails at runtime.
+ if
(relOptRule.toString().equals(CommonConstants.Broker.PlannerRuleNames.AGGREGATE_CASE_TO_FILTER))
{
+
hepProgramBuilder.addRuleInstance(PinotAggregateTypeArgumentRewriteRule.ARRAY_AGG_ONLY);
+ }
hepProgramBuilder.addRuleInstance(relOptRule);
}
- // Required for FIRST_WITH_TIME / LAST_WITH_TIME without a data type and
MODE on strings, so the rule options
- // do not filter it.
+ // Required for FIRST_WITH_TIME, LAST_WITH_TIME, ARRAY_AGG and string
MODE, so the rule options do not filter it.
hepProgramBuilder.addRuleInstance(inferAnyValueType ?
PinotAggregateTypeArgumentRewriteRule.INSTANCE
: PinotAggregateTypeArgumentRewriteRule.WITHOUT_ANY_VALUE);
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/physical/v2/opt/rules/AggregatePushdownRule.java
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/physical/v2/opt/rules/AggregatePushdownRule.java
index ca30c851a01..a40402cd4e8 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/physical/v2/opt/rules/AggregatePushdownRule.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/physical/v2/opt/rules/AggregatePushdownRule.java
@@ -391,6 +391,15 @@ public class AggregatePushdownRule extends PRelOptRule {
functionCategory = SqlFunctionCategory.USER_DEFINED_FUNCTION;
} else if (kind == SqlKind.LISTAGG) {
rexList.add(input.getCluster().getRexBuilder().makeLiteral(true));
+ } else if
(AggregationFunctionType.getNormalizedAggregationFunctionName(functionName)
+ .equals(AggregationFunctionType.ARRAYAGG.name())) {
+ // ARRAY_AGG(expression, 'dataType'[, isDistinct]) takes DISTINCT as
its isDistinct argument
+ RexNode isDistinct =
input.getCluster().getRexBuilder().makeLiteral(true);
+ if (rexList.size() == 3) {
+ rexList.set(2, isDistinct);
+ } else {
+ rexList.add(isDistinct);
+ }
}
}
SqlReturnTypeInference returnTypeInference = null;
diff --git
a/pinot-query-planner/src/test/java/org/apache/pinot/query/ApproximateAggregateRewriteTest.java
b/pinot-query-planner/src/test/java/org/apache/pinot/query/ApproximateAggregateRewriteTest.java
index a220377a1a9..2dbaa1a864d 100644
---
a/pinot-query-planner/src/test/java/org/apache/pinot/query/ApproximateAggregateRewriteTest.java
+++
b/pinot-query-planner/src/test/java/org/apache/pinot/query/ApproximateAggregateRewriteTest.java
@@ -25,6 +25,7 @@ import org.apache.pinot.core.routing.RoutingManager;
import org.apache.pinot.query.routing.WorkerManager;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.sql.parsers.CalciteSqlParser;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
@@ -110,6 +111,37 @@ public class ApproximateAggregateRewriteTest extends
QueryEnvironmentTestBase {
assertTrue(subquery.contains("threshold=23"), subquery);
}
+ /// The parameters are appended to the project beneath the aggregate, so the
literals already in it, here the data
+ /// type argument of FIRST_WITH_TIME, are still inlined when ProjectMerge is
skipped.
+ @Test
+ public void testKeepsOtherLiteralsInlinedWithoutProjectMerge() {
+ for (boolean usePhysicalOptimizer : List.of(false, true)) {
+ QueryEnvironment env = newQueryEnvironment(true, "threshold=17",
"threshold=23", usePhysicalOptimizer);
+ for (String input : List.of("a", "(SELECT * FROM a UNION ALL SELECT *
FROM b UNION ALL SELECT * FROM c)")) {
+ String query = "SET skipPlannerRules='ProjectMerge'; EXPLAIN PLAN FOR
SELECT col2, "
+ + "FIRST_WITH_TIME(col3, ts), DISTINCTCOUNT(col1) FROM " + input +
" GROUP BY col2";
+ String plan = env.compile(query,
CalciteSqlParser.compileToSqlNodeAndOptions(query))
+ .explain(RANDOM_REQUEST_ID_GEN.nextLong(), null).getExplainPlan();
+ String leafAggregate = plan.lines().filter(line ->
line.contains("aggType=[LEAF]")).findFirst().orElse("");
+ for (String literal : List.of("_UTF-8'INT'", "_UTF-8'threshold=17'")) {
+ assertTrue(leafAggregate.contains(literal), "Expected " + literal +
" in the leaf aggregate of:\n" + plan);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testApproximateParamsKeepBranchSpecificUnionColumns() {
+ QueryEnvironment env = newQueryEnvironment(true, "threshold=17",
"threshold=23");
+ String query = "EXPLAIN PLAN FOR SELECT DISTINCTCOUNT(x), COVARPOP(x, y)
FROM "
+ + "(SELECT col3 x, 1.0 y FROM a UNION ALL SELECT col3, CAST(col7 AS
DOUBLE) FROM b)";
+ String plan = env.compile(query,
CalciteSqlParser.compileToSqlNodeAndOptions(query))
+ .explain(RANDOM_REQUEST_ID_GEN.nextLong(), null).getExplainPlan();
+ String leaf = plan.lines().filter(line ->
line.contains("aggType=[LEAF]")).findFirst().orElse("");
+ assertTrue(leaf.contains("_UTF-8'threshold=17'"), plan);
+ assertFalse(leaf.contains("1.0"), plan);
+ }
+
/// `AggregateCaseToFilterRule` turns `COUNT(DISTINCT CASE WHEN c THEN x
END)` into `COUNT(DISTINCT x) FILTER (WHERE
/// c)`, so the rows that fail `c` add nothing to the count. It only matches
a single-argument call, so the rewrite
/// has to run after it: with the parameters appended first, the CASE stays
and its NULL counts as one more value.
diff --git a/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
b/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
index d025fa29406..9e470212e4f 100644
--- a/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
+++ b/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
@@ -295,6 +295,111 @@
"\n"
]
},
+ {
+ "description": "ARRAY_AGG without a data type infers it from the
expression as the second argument",
+ "sql": "EXPLAIN PLAN FOR SELECT ARRAY_AGG(a.col1), ARRAY_AGG(a.col3,
true), ARRAY_AGG(a.col4), ARRAY_AGG(a.ts_timestamp, true), ARRAY_AGG(a.col5),
ARRAY_AGG(a.col7 + 1) FROM a",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'STRING')], agg#1=[ARRAYAGG($1, _UTF-8'INT', true)], agg#2=[ARRAYAGG($2,
_UTF-8'DOUBLE')], agg#3=[ARRAYAGG($3, _UTF-8'TIMESTAMP', true)],
agg#4=[ARRAYAGG($4, _UTF-8'BOOLEAN')], agg#5=[ARRAYAGG($5, _UTF-8'LONG')],
aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash])",
+ "\n PinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'STRING')], agg#1=[ARRAYAGG($1, _UTF-8'INT', true)], agg#2=[ARRAYAGG($3,
_UTF-8'DOUBLE')], agg#3=[ARRAYAGG($4, _UTF-8'TIMESTAMP', true)],
agg#4=[ARRAYAGG($5, _UTF-8'BOOLEAN')], agg#5=[ARRAYAGG($6, _UTF-8'LONG')],
aggType=[LEAF])",
+ "\n LogicalProject($f0=[$0], $f1=[$2], $f2=[true], $f3=[$3],
$f4=[$8], $f5=[$4], $f6=[+($6, 1)], $f7=[_UTF-8'STRING'], $f8=[_UTF-8'INT'],
$f9=[_UTF-8'DOUBLE'], $f10=[_UTF-8'TIMESTAMP'], $f11=[_UTF-8'BOOLEAN'],
$f12=[_UTF-8'LONG'])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG without a data type next to the explicit
forms in a grouped aggregate",
+ "sql": "EXPLAIN PLAN FOR SELECT a.col2, ARRAY_AGG(a.col3, true),
ARRAY_AGG(a.col3, 'LONG', true), ARRAY_AGG(a.col1, 'STRING') FROM a GROUP BY
a.col2",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{0}], agg#0=[ARRAYAGG($1,
_UTF-8'INT', true)], agg#1=[ARRAYAGG($2, _UTF-8'LONG', true)],
agg#2=[ARRAYAGG($3, _UTF-8'STRING')], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash[0]])",
+ "\n PinotLogicalAggregate(group=[{0}], agg#0=[ARRAYAGG($1,
_UTF-8'INT', true)], agg#1=[ARRAYAGG($1, _UTF-8'LONG', true)],
agg#2=[ARRAYAGG($4, _UTF-8'STRING')], aggType=[LEAF])",
+ "\n LogicalProject($f0=[$1], $f1=[$2], $f2=[true],
$f3=[_UTF-8'LONG'], $f4=[$0], $f5=[_UTF-8'STRING'], $f6=[_UTF-8'INT'])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG without a data type uses the element type of
a multi-value column",
+ "sql": "EXPLAIN PLAN FOR SELECT ARRAY_AGG(e.mcol1), ARRAY_AGG(e.mcol2,
true) FROM e",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'STRING')], agg#1=[ARRAYAGG($1, _UTF-8'LONG', true)], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash])",
+ "\n PinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'STRING')], agg#1=[ARRAYAGG($1, _UTF-8'LONG', true)], aggType=[LEAF])",
+ "\n LogicalProject($f0=[$7], $f1=[$8], $f2=[true],
$f3=[_UTF-8'STRING'], $f4=[_UTF-8'LONG'])",
+ "\n PinotLogicalTableScan(table=[[default, e]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG without a data type follows the planner's
array type for a DECIMAL",
+ "sql": "EXPLAIN PLAN FOR SELECT ARRAY_AGG(CAST(a.col3 AS DECIMAL(5,
0))), ARRAY_AGG(CAST(a.col7 AS DECIMAL(20, 0)), true), ARRAY_AGG(CAST(a.col7 AS
DECIMAL(39, 0))) FROM a",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'INT')], agg#1=[ARRAYAGG($1, _UTF-8'LONG', true)], agg#2=[ARRAYAGG($2,
_UTF-8'DOUBLE')], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash])",
+ "\n PinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'INT')], agg#1=[ARRAYAGG($1, _UTF-8'LONG', true)], agg#2=[ARRAYAGG($3,
_UTF-8'DOUBLE')], aggType=[LEAF])",
+ "\n LogicalProject($f0=[CAST($2):DECIMAL(5, 0) NOT NULL],
$f1=[CAST($6):DECIMAL(20, 0) NOT NULL], $f2=[true], $f3=[CAST($6):DECIMAL(39,
0) NOT NULL], $f4=[_UTF-8'INT'], $f5=[_UTF-8'LONG'], $f6=[_UTF-8'DOUBLE'])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG(DISTINCT ...) passes DISTINCT as a true
isDistinct argument, with or without a data type",
+ "sql": "EXPLAIN PLAN FOR SELECT a.col2, ARRAY_AGG(DISTINCT a.col3),
ARRAY_AGG(DISTINCT a.col3, false), ARRAY_AGG(DISTINCT a.col1, 'STRING') FROM a
GROUP BY a.col2",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{0}], agg#0=[ARRAYAGG($1,
_UTF-8'INT', true)], agg#1=[ARRAYAGG($2, _UTF-8'INT', true)],
agg#2=[ARRAYAGG($3, _UTF-8'STRING', true)], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash[0]])",
+ "\n PinotLogicalAggregate(group=[{0}], agg#0=[ARRAYAGG($1,
_UTF-8'INT', true)], agg#1=[ARRAYAGG($1, _UTF-8'INT', true)],
agg#2=[ARRAYAGG($3, _UTF-8'STRING', true)], aggType=[LEAF])",
+ "\n LogicalProject($f0=[$1], $f1=[$2], $f2=[false], $f3=[$0],
$f4=[_UTF-8'STRING'], $f5=[_UTF-8'INT'])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG(CASE ...) without a data type keeps the CASE
expression instead of a FILTER",
+ "sql": "EXPLAIN PLAN FOR SELECT ARRAY_AGG(CASE WHEN a.col3 > 1 THEN
a.col3 END) FROM a",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'INT')], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash])",
+ "\n PinotLogicalAggregate(group=[{}], agg#0=[ARRAYAGG($0,
_UTF-8'INT')], aggType=[LEAF])",
+ "\n LogicalProject($f0=[CASE(>($2, 1), $2, null:INTEGER)],
$f1=[_UTF-8'INT'])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "Inferred types above nested unions keep one union and
one literal project",
+ "sql": "EXPLAIN PLAN FOR SELECT FIRST_WITH_TIME(col3, ts),
ARRAY_AGG(col3) FROM (SELECT col3, ts FROM a UNION ALL SELECT col3, ts FROM b
UNION ALL SELECT col3, ts FROM c)",
+ "output": [
+ "Execution Plan",
+ "\nPinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0, $0,
_UTF-8'INT')], agg#1=[ARRAYAGG($1, _UTF-8'INT')], aggType=[FINAL])",
+ "\n PinotLogicalExchange(distribution=[hash])",
+ "\n PinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0,
$1, _UTF-8'INT')], agg#1=[ARRAYAGG($0, _UTF-8'INT')], aggType=[LEAF])",
+ "\n LogicalProject($f0=[$0], $f1=[$1], $f2=[_UTF-8'INT'],
$f3=[_UTF-8'INT'])",
+ "\n LogicalUnion(all=[true])",
+ "\n PinotLogicalExchange(distribution=[single])",
+ "\n LogicalProject(col3=[$2], ts=[$7])",
+ "\n PinotLogicalTableScan(table=[[default, a]])",
+ "\n PinotLogicalExchange(distribution=[single])",
+ "\n LogicalProject(col3=[$2], ts=[$7])",
+ "\n PinotLogicalTableScan(table=[[default, b]])",
+ "\n PinotLogicalExchange(distribution=[single])",
+ "\n LogicalProject(col3=[$2], ts=[$7])",
+ "\n PinotLogicalTableScan(table=[[default, c]])",
+ "\n"
+ ]
+ },
+ {
+ "description": "ARRAY_AGG without a data type rejects an unsupported
expression type",
+ "sql": "EXPLAIN PLAN FOR SELECT ARRAY_AGG(CAST(a.col1 AS DATE), true)
FROM a",
+ "expectedException": ".*Unsupported data type for .*\\(expression\\):
DATE.*"
+ },
{
"description": "MIN on STRING column rewritten to MINSTRING",
"sql": "SET autoRewriteAggregationType=true; EXPLAIN PLAN FOR SELECT
MIN(a.col1) FROM a",
diff --git a/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
b/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
index 68813c3d637..c76c7b4e17c 100644
--- a/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
+++ b/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
@@ -45,6 +45,14 @@
}
},
"queries": [
+ {"sql": "SELECT FIRSTWITHTIME(x, c) FROM (SELECT int_col x, 5 c FROM
{tbl} WHERE int_col = 2 UNION ALL SELECT int_col, 5 FROM {tbl} WHERE int_col =
2)", "outputs": [[2]]},
+ {"sql": "SELECT COVARPOP(x, c), MODE(s) FROM (SELECT int_col x, 50 c,
string_col s FROM {tbl} UNION ALL SELECT int_col, 50, string_col FROM {tbl})",
"outputs": [[0, "c"]]},
+ {"sql": "SELECT CARDINALITY(ARRAY_AGG(x)), DISTINCTCOUNT(x), COVARPOP(x,
y) FROM (SELECT int_col x, 1.0 y FROM {tbl} UNION ALL SELECT int_col,
CAST(long_col AS DOUBLE) FROM {tbl})", "outputs": [[14, 6, 2339.918367346939]]},
+ {"sql": "SET enableNullHandling=false; SELECT MODE(CASE WHEN int_col > 3
THEN string_col END) FROM {tbl}", "outputs": [["c"]]},
+ {"sql": "SET enableNullHandling=false; SELECT /*+
aggOptions(is_skip_leaf_stage_group_by='true') */ bool_col, MODE(CASE WHEN
int_col > 3 THEN string_col END) FROM {tbl} GROUP BY bool_col", "outputs":
[[true, "c"], [false, "c"]]},
+ {"sql": "SET inferAnyValueType=true; SELECT src, ANY_VALUE(x) FROM
(SELECT 'a' src, 1 x FROM {tbl} UNION ALL SELECT 'b', 2 FROM {tbl}) GROUP BY
src", "outputs": [["a", 1], ["b", 2]]},
+ {"sql": "SELECT LASTWITHTIME(x, t) FROM (SELECT int_col x,
CAST('2019-01-01 00:00:00' AS TIMESTAMP) t FROM {tbl} UNION ALL SELECT int_col,
timestamp_col FROM {tbl})", "outputs": [[101]]},
+ {"sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
src, k, MODE(s) FROM (SELECT 'a' src, int_col k, string_col s FROM {tbl} UNION
ALL SELECT 'b', int_col, string_col FROM {tbl}) WHERE k = 2 GROUP BY src, k",
"outputs": [["a", 2, "a"], ["b", 2, "a"]]},
{
"sql": "SELECT FIRSTWITHTIME(int_col, timestamp_col, 'INT'),
FIRSTWITHTIME(string_col, timestamp_col, 'STRING'), first_with_time(bool_col,
CAST(long_col AS TIMESTAMP), 'BOOLEAN') FROM {tbl}",
"outputs": [[2, "a", true]]
@@ -54,7 +62,7 @@
"outputs": [[false, 100, 100, 3], [true, 300, 300, 2]]
},
{
- "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */
string_col, FIRSTWITHTIME(float_col, timestamp_col, 'FLOAT'),
FIRSTWITHTIME(double_col, timestamp_col, 'DOUBLE'), first_with_time(long_col,
CAST(long_col AS TIMESTAMP), 'LONG') FROM {tbl} GROUP BY string_col",
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, FIRSTWITHTIME(float_col, timestamp_col, 'FLOAT'),
FIRSTWITHTIME(double_col, timestamp_col, 'DOUBLE'), first_with_time(long_col,
CAST(long_col AS TIMESTAMP), 'LONG') FROM {tbl} GROUP BY string_col",
"outputs": [["a", 300, 300, 2], ["b", 100, 100, 3], ["c", 1.75, 1.75,
101]]
},
{
@@ -66,7 +74,7 @@
"outputs": [[false, 1.01, 1.01, 150], [true, 1.75, 1.75, 175]]
},
{
- "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */
string_col, LASTWITHTIME(float_col, timestamp_col, 'FLOAT'),
LASTWITHTIME(double_col, timestamp_col, 'DOUBLE'), last_with_time(long_col,
CAST(long_col AS TIMESTAMP), 'LONG') FROM {tbl} GROUP BY string_col",
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, LASTWITHTIME(float_col, timestamp_col, 'FLOAT'),
LASTWITHTIME(double_col, timestamp_col, 'DOUBLE'), last_with_time(long_col,
CAST(long_col AS TIMESTAMP), 'LONG') FROM {tbl} GROUP BY string_col",
"outputs": [["a", 400, 400, 2], ["b", 1, 1, 100], ["c", 1.01, 1.01,
175]]
},
{
@@ -79,7 +87,7 @@
"outputs": [[false, 100, 100, 3], [true, 300, 300, 2]]
},
{
- "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */
string_col, FIRSTWITHTIME(float_col, timestamp_col), FIRSTWITHTIME(double_col,
timestamp_col), first_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM
{tbl} GROUP BY string_col",
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, FIRSTWITHTIME(float_col, timestamp_col), FIRSTWITHTIME(double_col,
timestamp_col), first_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM
{tbl} GROUP BY string_col",
"outputs": [["a", 300, 300, 2], ["b", 100, 100, 3], ["c", 1.75, 1.75,
101]]
},
{
@@ -91,7 +99,7 @@
"outputs": [[false, 1.01, 1.01, 150], [true, 1.75, 1.75, 175]]
},
{
- "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */
string_col, LASTWITHTIME(float_col, timestamp_col), LASTWITHTIME(double_col,
timestamp_col), last_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM
{tbl} GROUP BY string_col",
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, LASTWITHTIME(float_col, timestamp_col), LASTWITHTIME(double_col,
timestamp_col), last_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM
{tbl} GROUP BY string_col",
"outputs": [["a", 400, 400, 2], ["b", 1, 1, 100], ["c", 1.01, 1.01,
175]]
},
{
@@ -124,7 +132,7 @@
"outputs": [["a", "1E-307", "1E+307", "deadbeef", "fdeadbee"], ["b",
"1E-308", "1E+308", "efdeadbe", "beefdead"], ["c", "1E+328", "1E-317",
"deadbe02", "adbeefde"]]
},
{
- "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */
string_col, FIRST_WITH_TIME(decimal_col, timestamp_col),
LAST_WITH_TIME(decimal_col, timestamp_col), FIRST_WITH_TIME(bytes_col,
timestamp_col), LAST_WITH_TIME(bytes_col, timestamp_col) FROM {tbl} GROUP BY
string_col",
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, FIRST_WITH_TIME(decimal_col, timestamp_col),
LAST_WITH_TIME(decimal_col, timestamp_col), FIRST_WITH_TIME(bytes_col,
timestamp_col), LAST_WITH_TIME(bytes_col, timestamp_col) FROM {tbl} GROUP BY
string_col",
"outputs": [["a", "1E-307", "1E+307", "deadbeef", "fdeadbee"], ["b",
"1E-308", "1E+308", "efdeadbe", "beefdead"], ["c", "1E+328", "1E-317",
"deadbe02", "adbeefde"]]
},
{
@@ -392,5 +400,82 @@
"outputs": [["a", 1, 1, 2, 2], ["b", 2, 2, 2, 2], ["c", 3, 3, 3, 3]]
}
]
+ },
+ "pinot_array_agg_tests": {
+ "tables": {
+ "tbl": {
+ "schema": [
+ {"name": "int_col", "type": "INT"},
+ {"name": "long_col", "type": "LONG"},
+ {"name": "double_col", "type": "DOUBLE"},
+ {"name": "decimal_col", "type": "BIG_DECIMAL"},
+ {"name": "string_col", "type": "STRING"},
+ {"name": "json_col", "type": "JSON"},
+ {"name": "int_mv_col", "type": "INT", "isSingleValue": false},
+ {"name": "string_mv_col", "type": "STRING", "isSingleValue": false}
+ ],
+ "inputs": [
+ [1, 10, 1.5, "1.5", "a", "{\"k\":1}", [1, 2], ["x", "y"]],
+ [2, 20, 2.5, "2.5", "a", "{\"k\":2}", [2, 3], ["y"]],
+ [2, 20, 2.5, "2.5", "b", "{\"k\":2}", [3], ["z", "x"]],
+ [3, 30, 3.5, "3.5", "b", "{\"k\":3}", [4, 5], ["z"]]
+ ]
+ }
+ },
+ "queries": [
+ {
+ "comment": "The data type argument of ARRAY_AGG is inferred when
omitted",
+ "sql": "SELECT arraySortInt(ARRAY_AGG(int_col)),
arraySortInt(ARRAY_AGG(int_col, true)), arraySortString(ARRAY_AGG(string_col,
true)), arraySortString(ARRAY_AGG(json_col)), CARDINALITY(ARRAY_AGG(long_col,
true)), CARDINALITY(ARRAY_AGG(double_col)), CARDINALITY(ARRAY_AGG(decimal_col,
true)) FROM {tbl}",
+ "outputs": [[[1, 2, 2, 3], [1, 2, 3], ["a", "b"], ["{\"k\":1}",
"{\"k\":2}", "{\"k\":2}", "{\"k\":3}"], 3, 4, 3]]
+ },
+ {
+ "sql": "SELECT string_col, arraySortInt(ARRAY_AGG(int_col + 1, true)),
arraySortInt(ARRAY_AGG(int_mv_col, true)),
arraySortString(ARRAY_AGG(string_mv_col)) FROM {tbl} GROUP BY string_col",
+ "outputs": [["a", [2, 3], [1, 2, 3], ["x", "y", "y"]], ["b", [3, 4],
[3, 4, 5], ["x", "z", "z"]]]
+ },
+ {
+ "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_group_by='true') */
string_col, arraySortInt(ARRAY_AGG(int_col + 1, true)),
CARDINALITY(ARRAY_AGG(long_col)) FROM {tbl} GROUP BY string_col",
+ "outputs": [["a", [2, 3], 2], ["b", [3, 4], 2]]
+ },
+ {
+ "sql": "SELECT arraySortInt(ARRAY_AGG(CAST(int_col AS DECIMAL(10,
0)))), CARDINALITY(ARRAY_AGG(CAST(long_col AS DECIMAL(20, 0)), true)) FROM
{tbl}",
+ "outputs": [[[1, 2, 2, 3], 3]]
+ },
+ {
+ "sql": "SELECT arraySortInt(ARRAY_AGG(DISTINCT int_col)),
CARDINALITY(ARRAY_AGG(DISTINCT string_col)), CARDINALITY(ARRAY_AGG(DISTINCT
int_col, false)), CARDINALITY(ARRAY_AGG(DISTINCT int_col, 'INT')) FROM {tbl}",
+ "outputs": [[[1, 2, 3], 2, 3, 3]]
+ },
+ {
+ "sql": "SELECT string_col, arraySortString(ARRAY_AGG(DISTINCT
string_mv_col)) FROM {tbl} GROUP BY string_col",
+ "outputs": [["a", ["x", "y"]], ["b", ["x", "z"]]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(DISTINCT x, 'INT', true)),
CARDINALITY(ARRAY_AGG(DISTINCT x, 'INT')) FROM (SELECT int_col x FROM {tbl}
UNION ALL SELECT int_col FROM {tbl})",
+ "outputs": [[3, 3]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(x)), FIRSTWITHTIME(x, CAST(t AS
TIMESTAMP)) FROM (SELECT int_col x, long_col t FROM {tbl} UNION ALL SELECT
int_col, long_col FROM {tbl} UNION ALL SELECT int_col, long_col FROM {tbl})",
+ "outputs": [[12, 1]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(x)), FIRSTWITHTIME(x, CAST(t AS
TIMESTAMP)) FROM (SELECT int_col x, long_col t FROM {tbl} INTERSECT ALL SELECT
int_col, long_col FROM {tbl})",
+ "outputs": [[4, 1]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(x)) FROM (SELECT int_col x FROM
{tbl} EXCEPT ALL SELECT int_col FROM {tbl} WHERE int_col > 2)",
+ "outputs": [[3]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(long_col / ((int_col - 2) *
1.0))) FROM {tbl}",
+ "outputs": [[4]]
+ },
+ {
+ "sql": "SELECT CARDINALITY(ARRAY_AGG(CASE WHEN int_col > 1 THEN
int_col END)), CARDINALITY(ARRAY_AGG(CASE WHEN int_col > 1 THEN int_col END,
'INT')) FROM {tbl}",
+ "outputs": [[3, 3]]
+ },
+ {
+ "sql": "SELECT arraySortInt(ARRAY_AGG(int_col, 'INT')),
arraySortInt(ARRAY_AGG(int_col, 'INT', true)) FROM {tbl}",
+ "outputs": [[[1, 2, 2, 3], [1, 2, 3]]]
+ }
+ ]
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]