This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 379f41d3be465992328d5659ea62b8355e0399d1 Author: Julian Hyde <[email protected]> AuthorDate: Sat May 27 10:50:28 2023 -0700 [CALCITE-5717] RelBuilder.project of literals on a single-row Aggregate should create a Values If RelBuilder.project is applied to an Aggregate with no group keys (which always returns one row) and all of the projected expressions are literals, then the result should be a Values with one row. For example, SELECT 42 FROM myTable GROUP BY () becomes VALUES 42 (Both queries always return one row, even if myTable is empty.) Close apache/calcite#3209 --- .../java/org/apache/calcite/tools/RelBuilder.java | 28 ++++++++++++++++++---- .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 27 ++++++++++++++++++--- .../org/apache/calcite/test/RelBuilderTest.java | 22 +++++++++++++++-- .../apache/calcite/test/SqlToRelConverterTest.java | 4 ++++ .../apache/calcite/test/SqlToRelConverterTest.xml | 5 +--- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java index f5fa384af4..d745488cb3 100644 --- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java @@ -2097,17 +2097,19 @@ public class RelBuilder { } // If the expressions are all literals, and the input is a Values with N - // rows, replace with a Values with same tuple N times. + // rows (or an Aggregate with 1 row), replace with a Values with same tuple + // N times. + final int rowCount; if (config.simplifyValues() - && frame.rel instanceof Values - && nodeList.stream().allMatch(e -> e instanceof RexLiteral)) { - final Values values = (Values) build(); + && nodeList.stream().allMatch(e -> e instanceof RexLiteral) + && (rowCount = fixedRowCount(frame)) >= 0) { + RelNode unused = build(); final RelDataTypeFactory.Builder typeBuilder = getTypeFactory().builder(); Pair.forEach(fieldNameList, nodeList, (name, expr) -> typeBuilder.add(requireNonNull(name, "name"), expr.getType())); @SuppressWarnings({"unchecked", "rawtypes"}) final List<RexLiteral> tuple = (List<RexLiteral>) (List) nodeList; - return values(Collections.nCopies(values.tuples.size(), tuple), + return values(Collections.nCopies(rowCount, tuple), typeBuilder.build()); } @@ -2122,6 +2124,22 @@ public class RelBuilder { return this; } + /** If current frame will return a known, constant number of + * rows, returns that number; otherwise returns -1. */ + private static int fixedRowCount(Frame frame) { + if (frame.rel instanceof Values) { + return ((Values) frame.rel).tuples.size(); + } + if (frame.rel instanceof Aggregate) { + final Aggregate aggregate = (Aggregate) frame.rel; + if (aggregate.getGroupSet().isEmpty() + && aggregate.getGroupType() == Aggregate.Group.SIMPLE) { + return 1; + } + } + return -1; + } + /** Creates a {@link Project} of the given * expressions and field names, and optionally optimizing. * diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 3c2d2adc65..53fde5cdf9 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -694,9 +694,30 @@ class RelToSqlConverterTest { relFn(fn).ok("SELECT \"t\".\"V\" AS \"l_v\"\n" + "FROM (VALUES (1, 2)) AS \"t\" (\"K\", \"V\")\n" + "INNER JOIN " - + "(SELECT 1 AS \"K\"\n" - + "FROM (SELECT COUNT(\"ENAME\") AS \"DUMMY\"\n" - + "FROM \"scott\".\"EMP\") AS \"t0\") AS \"t1\" ON \"t\".\"K\" = \"t1\".\"K\""); + + "(VALUES (1)) AS \"t0\" (\"K\") ON \"t\".\"K\" = \"t0\".\"K\""); + } + + /** As {@link #testTrimmedAggregateUnderProject()} + * but the "COUNT(*) AS DUMMY" field is used. */ + @Test public void testTrimmedAggregateUnderProject2() { + final Function<RelBuilder, RelNode> fn = b -> b + .values(new String[]{"K", "V"}, 1, 2) + .scan("EMP") + .aggregate(b.groupKey(), + b.aggregateCall(SqlStdOperatorTable.COUNT, b.field(1)) + .as("DUMMY")) + .project(b.alias(b.field("DUMMY"), "K")) + .join(JoinRelType.INNER, + b.call(SqlStdOperatorTable.EQUALS, + b.field(2, 0, "K"), + b.field(2, 1, "K"))) + .project(b.alias(b.field(1), "l_v")) + .build(); + // RelFieldTrimmer maybe build the RelNode. + relFn(fn).ok("SELECT \"t\".\"V\" AS \"l_v\"\n" + + "FROM (VALUES (1, 2)) AS \"t\" (\"K\", \"V\")\n" + + "INNER JOIN (SELECT COUNT(\"ENAME\") AS \"DUMMY\"\n" + + "FROM \"scott\".\"EMP\") AS \"t0\" ON \"t\".\"K\" = \"t0\".\"DUMMY\""); } /** Tests GROUP BY ROLLUP of two columns. The SQL for MySQL has diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java index 727fe5ea7b..3000e84d71 100644 --- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java @@ -1356,13 +1356,31 @@ public class RelBuilderTest { RelNode root = builder.scan("EMP") .aggregate(builder.groupKey(), builder.count().as("C")) + .filter( + builder.greaterThan(builder.field("C"), builder.literal(5))) .project(builder.literal(4), builder.literal(2), builder.field(0)) .aggregate(builder.groupKey(builder.field(0), builder.field(1))) .build(); final String expected = "" + "LogicalProject($f0=[4], $f1=[2])\n" - + " LogicalAggregate(group=[{}], C=[COUNT()])\n" - + " LogicalTableScan(table=[[scott, EMP]])\n"; + + " LogicalFilter(condition=[>($0, 5)])\n" + + " LogicalAggregate(group=[{}], C=[COUNT()])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(root, hasTree(expected)); + } + + /** Unlike {@link #testAggregate5()}, where the input to the Aggregate has at + * most one row (zero or one rows), the input is known to be exactly one row, + * and therefore can be converted to {@code Values}. */ + @Test void testAggregate5b() { + final RelBuilder builder = RelBuilder.create(config().build()); + RelNode root = + builder.scan("EMP") + .aggregate(builder.groupKey(), builder.count().as("C")) + .project(builder.literal(4), builder.literal(2), builder.field(0)) + .aggregate(builder.groupKey(builder.field(0), builder.field(1))) + .build(); + final String expected = "LogicalValues(tuples=[[{ 4, 2 }]])\n"; assertThat(root, hasTree(expected)); } diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java index 95b6274a31..4e2c029fb9 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java @@ -426,6 +426,10 @@ class SqlToRelConverterTest extends SqlToRelTestBase { sql("select sum(deptno) from emp group by ()").ok(); } + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-5717">[CALCITE-5717] + * RelBuilder.project should generate a Values if all expressions are literals + * and the input is an Aggregate that returns exactly one row</a>. */ @Test void testGroupEmptyYieldLiteral() { // Expected plan is "VALUES 42". The result is one row even if EMP is empty. sql("select 42 from emp group by ()").ok(); diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml index 6902fe5f81..b48f9015ad 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -2350,10 +2350,7 @@ LogicalAggregate(group=[{}], EXPR$0=[SUM($0)]) </Resource> <Resource name="plan"> <![CDATA[ -LogicalProject(EXPR$0=[42]) - LogicalAggregate(group=[{}]) - LogicalProject($f0=[0]) - LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +LogicalValues(tuples=[[{ 42 }]]) ]]> </Resource> </TestCase>
