[CALCITE-1306] Allow GROUP BY and HAVING to reference SELECT expressions by ordinal and alias (Rajeshbabu Chintaguntla)
Close apache/calcite#413 Project: http://git-wip-us.apache.org/repos/asf/calcite/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/e046be23 Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/e046be23 Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/e046be23 Branch: refs/heads/master Commit: e046be23d7364e16648ade5240ad948a04cc814b Parents: 5ee895d Author: Rajeshbabu Chintaguntla <[email protected]> Authored: Thu Mar 30 08:25:19 2017 +0530 Committer: Julian Hyde <[email protected]> Committed: Tue May 9 16:24:25 2017 -0700 ---------------------------------------------------------------------- .../calcite/sql/validate/GroupByScope.java | 66 ++++++ .../sql/validate/SqlAbstractConformance.java | 12 ++ .../calcite/sql/validate/SqlConformance.java | 31 +++ .../sql/validate/SqlConformanceEnum.java | 27 +++ .../sql/validate/SqlDelegatingConformance.java | 13 ++ .../calcite/sql/validate/SqlValidatorImpl.java | 148 +++++++++++++- .../java/org/apache/calcite/test/JdbcTest.java | 2 +- .../calcite/test/SqlToRelConverterTest.java | 34 ++++ .../apache/calcite/test/SqlValidatorTest.java | 204 ++++++++++++++++++- .../calcite/test/SqlValidatorTestCase.java | 3 + .../calcite/test/SqlToRelConverterTest.xml | 77 +++++++ 11 files changed, 608 insertions(+), 9 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/GroupByScope.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/GroupByScope.java b/core/src/main/java/org/apache/calcite/sql/validate/GroupByScope.java new file mode 100644 index 0000000..964fea9 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/sql/validate/GroupByScope.java @@ -0,0 +1,66 @@ +/* + * 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.calcite.sql.validate; + +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlNodeList; +import org.apache.calcite.sql.SqlSelect; + +/** + * Represents the name-resolution context for expressions in an GROUP BY clause. + * + * <p>In some dialects of SQL, the GROUP BY clause can reference column aliases + * in the SELECT clause. For example, the query</p> + * + * <blockquote><code>SELECT empno AS x<br> + * FROM emp<br> + * GROUP BY x</code></blockquote> + * + * <p>is valid.</p> + */ +public class GroupByScope extends DelegatingScope { + //~ Instance fields -------------------------------------------------------- + + private final SqlNodeList groupByList; + private final SqlSelect select; + + //~ Constructors ----------------------------------------------------------- + + GroupByScope( + SqlValidatorScope parent, + SqlNodeList groupByList, + SqlSelect select) { + super(parent); + this.groupByList = groupByList; + this.select = select; + } + + //~ Methods ---------------------------------------------------------------- + + public SqlNode getNode() { + return groupByList; + } + + public void validateExpr(SqlNode expr) { + SqlNode expanded = validator.expandGroupByOrHavingExpr(expr, this, select, false); + + // expression needs to be valid in parent scope too + parent.validateExpr(expanded); + } +} + +// End GroupByScope.java http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java index ddb4d7e..0c27dca 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java @@ -23,6 +23,18 @@ package org.apache.calcite.sql.validate; * and behaves the same as in {@link SqlConformanceEnum#DEFAULT}. */ public abstract class SqlAbstractConformance implements SqlConformance { + public boolean isGroupByAlias() { + return SqlConformanceEnum.DEFAULT.isGroupByAlias(); + } + + public boolean isGroupByOrdinal() { + return SqlConformanceEnum.DEFAULT.isGroupByOrdinal(); + } + + public boolean isHavingAlias() { + return SqlConformanceEnum.DEFAULT.isHavingAlias(); + } + public boolean isSortByOrdinal() { return SqlConformanceEnum.DEFAULT.isSortByOrdinal(); } http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java index 88af704..9d18268 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java @@ -62,6 +62,36 @@ public interface SqlConformance { SqlConformanceEnum PRAGMATIC_2003 = SqlConformanceEnum.PRAGMATIC_2003; /** + * Whether to allow aliases from the {@code SELECT} clause to be used as + * column names in the {@code GROUP BY} clause. + * + * <p>Among the built-in conformance levels, true in + * {@link SqlConformanceEnum#LENIENT}; + * false otherwise. + */ + boolean isGroupByAlias(); + + /** + * Whether {@code GROUP BY 2} is interpreted to mean 'group by the 2nd column + * in the select list'. + * + * <p>Among the built-in conformance levels, true in + * {@link SqlConformanceEnum#LENIENT}; + * false otherwise. + */ + boolean isGroupByOrdinal(); + + /** + * Whether to allow aliases from the {@code SELECT} clause to be used as + * column names in the {@code HAVING} clause. + * + * <p>Among the built-in conformance levels, true in + * {@link SqlConformanceEnum#LENIENT}; + * false otherwise. + */ + boolean isHavingAlias(); + + /** * Whether '{@code ORDER BY 2}' is interpreted to mean 'sort by the 2nd * column in the select list'. * @@ -208,6 +238,7 @@ public interface SqlConformance { * false otherwise. */ boolean allowNiladicParentheses(); + } // End SqlConformance.java http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java index 1ff0a10..226ec21 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java @@ -62,6 +62,33 @@ public enum SqlConformanceEnum implements SqlConformance { * consistent with Microsoft SQL Server version 2008. */ SQL_SERVER_2008; + public boolean isGroupByAlias() { + switch (this) { + case LENIENT: + return true; + default: + return false; + } + } + + public boolean isGroupByOrdinal() { + switch (this) { + case LENIENT: + return true; + default: + return false; + } + } + + public boolean isHavingAlias() { + switch (this) { + case LENIENT: + return true; + default: + return false; + } + } + public boolean isSortByOrdinal() { switch (this) { case DEFAULT: http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java index e1b4b4c..680cc60 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java @@ -29,6 +29,18 @@ public class SqlDelegatingConformance extends SqlAbstractConformance { this.delegate = delegate; } + @Override public boolean isGroupByAlias() { + return delegate.isGroupByAlias(); + } + + @Override public boolean isGroupByOrdinal() { + return delegate.isGroupByOrdinal(); + } + + @Override public boolean isHavingAlias() { + return delegate.isGroupByAlias(); + } + @Override public boolean isSortByOrdinal() { return delegate.isSortByOrdinal(); } @@ -60,6 +72,7 @@ public class SqlDelegatingConformance extends SqlAbstractConformance { @Override public boolean allowNiladicParentheses() { return delegate.allowNiladicParentheses(); } + } // End SqlDelegatingConformance.java http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 28c4047..499cdc9 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -181,6 +181,12 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { new IdentityHashMap<>(); /** + * Maps a {@link SqlSelect} node to the scope used by its GROUP BY clause. + */ + private final Map<SqlSelect, SqlValidatorScope> groupByScopes = + new IdentityHashMap<>(); + + /** * Maps a {@link SqlSelect} node to the scope used by its SELECT and HAVING * clauses. */ @@ -968,7 +974,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { public SqlValidatorScope getGroupScope(SqlSelect select) { // Yes, it's the same as getWhereScope - return whereScopes.get(select); + return groupByScopes.get(select); } public SqlValidatorScope getFromScope(SqlSelect select) { @@ -2332,7 +2338,12 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { } else { selectScopes.put(select, selectScope); } - registerSubQueries(selectScope, select.getGroup()); + if (select.getGroup() != null) { + GroupByScope groupByScope = + new GroupByScope(selectScope, select.getGroup(), select); + groupByScopes.put(select, groupByScope); + registerSubQueries(groupByScope, select.getGroup()); + } registerOperandSubQueries( aggScope, select, @@ -3472,6 +3483,17 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { } /** + * Validates an item in the GROUP BY clause of a SELECT statement. + * + * @param select Select statement + * @param groupByItem GROUP BY clause item + */ + private void validateGroupByItem(SqlSelect select, SqlNode groupByItem) { + final SqlValidatorScope groupByScope = getGroupScope(select); + groupByScope.validateExpr(groupByItem); + } + + /** * Validates an item in the ORDER BY clause of a SELECT statement. * * @param select Select statement @@ -3519,11 +3541,14 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { // expand the expression in group list. List<SqlNode> expandedList = new ArrayList<>(); for (SqlNode groupItem : groupList) { - SqlNode expandedItem = expand(groupItem, groupScope); + SqlNode expandedItem = expandGroupByOrHavingExpr(groupItem, groupScope, select, false); expandedList.add(expandedItem); } groupList = new SqlNodeList(expandedList, groupList.getParserPosition()); select.setGroupBy(groupList); + for (SqlNode groupItem : expandedList) { + validateGroupByItem(select, groupItem); + } // Nodes in the GROUP BY clause are expressions except if they are calls // to the GROUPING SETS, ROLLUP or CUBE operators; this operators are not @@ -3620,12 +3645,19 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { // For example, in "SELECT empno FROM emp WHERE empno = 10 GROUP BY // deptno HAVING empno = 10", the reference to 'empno' in the HAVING // clause is illegal. - final SqlNode having = select.getHaving(); + SqlNode having = select.getHaving(); if (having == null) { return; } final AggregatingScope havingScope = (AggregatingScope) getSelectScope(select); + if (getConformance().isHavingAlias()) { + SqlNode newExpr = expandGroupByOrHavingExpr(having, havingScope, select, true); + if (having != newExpr) { + having = newExpr; + select.setHaving(newExpr); + } + } havingScope.checkAggregateExpr(having, true); inferUnknownTypes( booleanType, @@ -4699,6 +4731,16 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { return newExpr; } + public SqlNode expandGroupByOrHavingExpr(SqlNode expr, SqlValidatorScope scope, SqlSelect select, + boolean havingExpression) { + final Expander expander = new ExtendedExpander(this, scope, select, expr, havingExpression); + SqlNode newExpr = expr.accept(expander); + if (expr != newExpr) { + setOriginal(newExpr, expr); + } + return newExpr; + } + public boolean isSystemField(RelDataTypeField field) { return false; } @@ -5069,7 +5111,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { * identifiers. */ private static class Expander extends SqlScopedShuttle { - private final SqlValidatorImpl validator; + protected final SqlValidatorImpl validator; Expander(SqlValidatorImpl validator, SqlValidatorScope scope) { super(scope); @@ -5229,6 +5271,102 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { } } + /** + * Shuttle which walks over an expression in the GROUP BY/HAVING clause, replacing + * usages of aliases or ordinals with the underlying expression. + */ + static class ExtendedExpander extends Expander { + final SqlSelect select; + final SqlNode root; + final boolean havingExpr; + + ExtendedExpander(SqlValidatorImpl validator, SqlValidatorScope scope, + SqlSelect select, SqlNode root, boolean havingExpr) { + super(validator, scope); + this.select = select; + this.root = root; + this.havingExpr = havingExpr; + } + + @Override public SqlNode visit(SqlIdentifier id) { + if (id.isSimple() + && (havingExpr + ? validator.getConformance().isHavingAlias() + : validator.getConformance().isGroupByAlias())) { + String name = id.getSimple(); + SqlNode expr = null; + final SqlNameMatcher nameMatcher = + validator.catalogReader.nameMatcher(); + int n = 0; + for (SqlNode s : select.getSelectList()) { + final String alias = SqlValidatorUtil.getAlias(s, -1); + if (alias != null && nameMatcher.matches(alias, name)) { + expr = s; + n++; + } + } + if (n == 0) { + return super.visit(id); + } else if (n > 1) { + // More than one column has this alias. + throw validator.newValidationError(id, + RESOURCE.columnAmbiguous(name)); + } + if (havingExpr && validator.isAggregate(root)) { + return super.visit(id); + } + expr = stripAs(expr); + if (expr instanceof SqlIdentifier) { + expr = getScope().fullyQualify((SqlIdentifier) expr).identifier; + } + return expr; + } + return super.visit(id); + } + + public SqlNode visit(SqlLiteral literal) { + if (havingExpr || !validator.getConformance().isGroupByOrdinal()) { + return super.visit(literal); + } + boolean isOrdinalLiteral = literal == root; + switch (root.getKind()) { + case GROUPING_SETS: + case ROLLUP: + case CUBE: + if (root instanceof SqlBasicCall) { + List<SqlNode> operandList = ((SqlBasicCall) root).getOperandList(); + for (SqlNode node : operandList) { + if (node.equals(literal)) { + isOrdinalLiteral = true; + break; + } + } + } + break; + } + if (isOrdinalLiteral) { + switch (literal.getTypeName()) { + case DECIMAL: + case DOUBLE: + final int intValue = literal.intValue(false); + if (intValue >= 0) { + if (intValue < 1 || intValue > select.getSelectList().size()) { + throw validator.newValidationError(literal, + RESOURCE.orderByOrdinalOutOfRange()); + } + + // SQL ordinals are 1-based, but Sort's are 0-based + int ordinal = intValue - 1; + return select.getSelectList().get(ordinal); + } + break; + } + } + + return super.visit(literal); + } + } + /** Information about an identifier in a particular scope. */ protected static class IdInfo { public final SqlValidatorScope scope; http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/test/java/org/apache/calcite/test/JdbcTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java index 62a526b..fb576dc 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -6168,7 +6168,7 @@ public class JdbcTest { /** Tests case-insensitive resolution of sub-query columns. * * <p>Test case for - * <a href="https://issues.apache.org/jira/browse/CALCITE-555">[CALCITE-555] + * <a href="https://issues.apache.org/jira/browse/CALCITE-550">[CALCITE-550] * Case-insensitive matching of sub-query columns fails</a>. */ @Test public void testLexCaseInsensitiveSubQueryField() { CalciteAssert.that() http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java ---------------------------------------------------------------------- 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 01b1897..4bdd791 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java @@ -237,6 +237,40 @@ public class SqlToRelConverterTest extends SqlToRelTestBase { sql("select deptno from emp group by deptno").ok(); } + @Test public void testGroupByAlias() { + sql("select empno as d from emp group by d") + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + + @Test public void testGroupByAliasOfSubExpressionsInProject() { + final String sql = "select deptno+empno as d, deptno+empno+mgr\n" + + "from emp group by d,mgr"; + sql(sql) + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + + @Test public void testGroupByAliasEqualToColumnName() { + sql("select empno, ename as deptno from emp group by empno, deptno") + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + + @Test public void testGroupByOrdinal() { + sql("select empno from emp group by 1") + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + + @Test public void testGroupByContainsLiterals() { + final String sql = "select count(*) from (\n" + + " select 1 from emp group by substring(ename from 2 for 3))"; + sql(sql) + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + + @Test public void testAliasInHaving() { + sql("select count(empno) as e from emp having e > 1") + .conformance(SqlConformanceEnum.LENIENT).ok(); + } + @Test public void testGroupJustOneAgg() { // just one agg final String sql = http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index ae1e1f8..c902669 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -6116,6 +6116,197 @@ public class SqlValidatorTest extends SqlValidatorTestCase { } /** + * Tests validation of the aliases in GROUP BY. + * + * <p>Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-1306">[CALCITE-1306] + * Allow GROUP BY and HAVING to reference SELECT expressions by ordinal and + * alias</a>. + * + * @see SqlConformance#isGroupByAlias() + */ + @Test public void testAliasInGroupBy() { + final SqlTester lenient = + tester.withConformance(SqlConformanceEnum.LENIENT); + final SqlTester strict = + tester.withConformance(SqlConformanceEnum.STRICT_2003); + + // Group by + sql("select empno as e from emp group by ^e^") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select empno as e from emp group by ^e^") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select emp.empno as e from emp group by ^e^") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select e.empno from emp as e group by e.empno") + .tester(strict).ok() + .tester(lenient).ok(); + sql("select e.empno as eno from emp as e group by ^eno^") + .tester(strict).fails("Column 'ENO' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select deptno as dno from emp group by cube(^dno^)") + .tester(strict).fails("Column 'DNO' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select deptno as dno, ename name, sum(sal) from emp\n" + + "group by grouping sets ((^dno^), (name, deptno))") + .tester(strict).fails("Column 'DNO' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select ename as deptno from emp as e join dept as d on " + + "e.deptno = d.deptno group by ^deptno^") + .tester(lenient).sansCarets().ok(); + sql("select t.e, count(*) from (select empno as e from emp) t group by e") + .tester(strict).ok() + .tester(lenient).ok(); + + // The following 2 tests have the same SQL but fail for different reasons. + sql("select t.e, count(*) as c from " + + " (select empno as e from emp) t group by e,^c^") + .tester(strict).fails("Column 'C' not found in any table"); + sql("select t.e, ^count(*)^ as c from " + + " (select empno as e from emp) t group by e,c") + .tester(lenient).fails(ERR_AGG_IN_GROUP_BY); + + sql("select t.e, e + ^count(*)^ as c from " + + " (select empno as e from emp) t group by e,c") + .tester(lenient).fails(ERR_AGG_IN_GROUP_BY); + sql("select t.e, e + ^count(*)^ as c from " + + " (select empno as e from emp) t group by e,2") + .tester(lenient).fails(ERR_AGG_IN_GROUP_BY) + .tester(strict).sansCarets().ok(); + + sql("select deptno,(select empno + 1 from emp) eno\n" + + "from dept group by deptno,^eno^") + .tester(strict).fails("Column 'ENO' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select empno as e, deptno as e\n" + + "from emp group by ^e^") + .tester(lenient).fails("Column 'E' is ambiguous"); + sql("select empno, ^count(*)^ c from emp group by empno, c") + .tester(lenient).fails(ERR_AGG_IN_GROUP_BY); + sql("select deptno + empno as d, deptno + empno + mgr from emp" + + " group by d,mgr") + .tester(lenient).sansCarets().ok(); + // When alias is equal to one or more columns in the query then giving + // priority to alias. But Postgres may throw ambiguous column error or give + // priority to column name. + sql("select count(*) from (\n" + + " select ename AS deptno FROM emp GROUP BY deptno) t") + .tester(lenient).sansCarets().ok(); + sql("select count(*) from " + + "(select ename AS deptno FROM emp, dept GROUP BY deptno) t") + .tester(lenient).sansCarets().ok(); + sql("select empno + deptno AS \"z\" FROM emp GROUP BY \"Z\"") + .tester(lenient.withCaseSensitive(false)).sansCarets().ok(); + sql("select empno + deptno as c, ^c^ + mgr as d from emp group by c, d") + .tester(lenient).fails("Column 'C' not found in any table"); + // Group by alias with strict conformance should fail. + sql("select empno as e from emp group by ^e^") + .tester(strict).fails("Column 'E' not found in any table"); + } + + /** + * Tests validation of ordinals in GROUP BY. + * + * @see SqlConformance#isGroupByOrdinal() + */ + @Test public void testOrdinalInGroupBy() { + final SqlTester lenient = + tester.withConformance(SqlConformanceEnum.LENIENT); + final SqlTester strict = + tester.withConformance(SqlConformanceEnum.STRICT_2003); + + sql("select ^empno^,deptno from emp group by 1, deptno") + .tester(strict).fails("Expression 'EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select ^emp.empno^ as e from emp group by 1") + .tester(strict).fails("Expression 'EMP.EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select 2 + ^emp.empno^ + 3 as e from emp group by 1") + .tester(strict).fails("Expression 'EMP.EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select ^e.empno^ from emp as e group by 1") + .tester(strict).fails("Expression 'E.EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select e.empno from emp as e group by 1, empno") + .tester(strict).ok() + .tester(lenient).sansCarets().ok(); + sql("select ^e.empno^ as eno from emp as e group by 1") + .tester(strict).fails("Expression 'E.EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select ^deptno^ as dno from emp group by cube(1)") + .tester(strict).fails("Expression 'DEPTNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select 1 as dno from emp group by cube(1)") + .tester(strict).ok() + .tester(lenient).sansCarets().ok(); + sql("select deptno as dno, ename name, sum(sal) from emp\n" + + "group by grouping sets ((1), (^name^, deptno))") + .tester(strict).fails("Column 'NAME' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select ^e.deptno^ from emp as e\n" + + "join dept as d on e.deptno = d.deptno group by 1") + .tester(strict).fails("Expression 'E.DEPTNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select ^deptno^,(select empno from emp) eno from dept" + + " group by 1,2") + .tester(strict).fails("Expression 'DEPTNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + sql("select count(*) from (select 1 from emp" + + " group by substring(ename from 2 for 3))") + .tester(strict).ok() + .tester(lenient).sansCarets().ok(); + sql("select deptno from emp group by deptno, ^100^") + .tester(lenient).fails("Ordinal out of range") + .tester(strict).sansCarets().ok(); + // Calcite considers integers in GROUP BY to be constants, so test passes. + // Postgres considers them ordinals and throws out of range position error. + sql("select deptno from emp group by ^100^, deptno") + .tester(lenient).fails("Ordinal out of range") + .tester(strict).sansCarets().ok(); + } + + /** + * Tests validation of the aliases in HAVING. + * + * @see SqlConformance#isHavingAlias() + */ + @Test public void testAliasInHaving() { + final SqlTester lenient = + tester.withConformance(SqlConformanceEnum.LENIENT); + final SqlTester strict = + tester.withConformance(SqlConformanceEnum.STRICT_2003); + + sql("select count(empno) as e from emp having ^e^ > 10") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select emp.empno as e from emp group by ^e^ having e > 10") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select emp.empno as e from emp group by empno having ^e^ > 10") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + sql("select e.empno from emp as e group by 1 having ^e.empno^ > 10") + .tester(strict).fails("Expression 'E.EMPNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + // When alias is equal to one or more columns in the query then giving + // priority to alias, but PostgreSQL throws ambiguous column error or gives + // priority to column name. + sql("select count(empno) as deptno from emp having ^deptno^ > 10") + .tester(strict).fails("Expression 'DEPTNO' is not being grouped") + .tester(lenient).sansCarets().ok(); + // Alias in aggregate is not allowed. + sql("select empno as e from emp having max(^e^) > 10") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).fails("Column 'E' not found in any table"); + sql("select count(empno) as e from emp having ^e^ > 10") + .tester(strict).fails("Column 'E' not found in any table") + .tester(lenient).sansCarets().ok(); + } + + /** * Tests validation of the ORDER BY clause when DISTINCT is present. */ @Test public void testOrderDistinct() { @@ -7247,9 +7438,16 @@ public class SqlValidatorTest extends SqlValidatorTestCase { check("SELECT deptno FROM emp GROUP BY deptno HAVING deptno > 55"); check("SELECT DISTINCT deptno, 33 FROM emp\n" + "GROUP BY deptno HAVING deptno > 55"); - checkFails( - "SELECT DISTINCT deptno, 33 FROM emp HAVING ^deptno^ > 55", - "Expression 'DEPTNO' is not being grouped"); + sql("SELECT DISTINCT deptno, 33 FROM emp HAVING ^deptno^ > 55") + .fails("Expression 'DEPTNO' is not being grouped"); + // same query under a different conformance finds a different error first + sql("SELECT DISTINCT ^deptno^, 33 FROM emp HAVING deptno > 55") + .tester(tester.withConformance(SqlConformanceEnum.LENIENT)) + .fails("Expression 'DEPTNO' is not being grouped"); + sql("SELECT DISTINCT 33 FROM emp HAVING ^deptno^ > 55") + .fails("Expression 'DEPTNO' is not being grouped") + .tester(tester.withConformance(SqlConformanceEnum.LENIENT)) + .fails("Expression 'DEPTNO' is not being grouped"); check("SELECT DISTINCT * from emp"); checkFails( "SELECT DISTINCT ^*^ from emp GROUP BY deptno", http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java index cc66e3f..b5db2c3 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java @@ -581,6 +581,9 @@ public class SqlValidatorTestCase { return this; } + /** Removes the carets from the SQL string. Useful if you want to run + * a test once at a conformance level where it fails, then run it again + * at a conformance level where it succeeds. */ public Sql sansCarets() { return new Sql(tester, sql.replace("^", "")); } http://git-wip-us.apache.org/repos/asf/calcite/blob/e046be23/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml ---------------------------------------------------------------------- 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 13257e2..4cb6f9d 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -39,6 +39,83 @@ LogicalAggregate(group=[{0}]) <![CDATA[select deptno from emp group by deptno]]> </Resource> </TestCase> + <TestCase name="testGroupByAlias"> + <Resource name="plan"> + <![CDATA[ +LogicalAggregate(group=[{0}]) + LogicalProject(D=[$0]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select empno as d from emp group by d]]> + </Resource> + </TestCase> + <TestCase name="testGroupByAliasEqualToColumnName"> + <Resource name="plan"> + <![CDATA[ +LogicalAggregate(group=[{0, 1}]) + LogicalProject(EMPNO=[$0], DEPTNO=[$1]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select empno, ename as deptno from emp group by empno, deptno]]> + </Resource> + </TestCase> + <TestCase name="testGroupByAliasOfSubExpressionsInProject"> + <Resource name="plan"> + <![CDATA[ +LogicalProject(D=[$0], EXPR$1=[+($0, $1)]) + LogicalAggregate(group=[{0, 1}]) + LogicalProject(D=[+($7, $0)], MGR=[$3]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select deptno+empno as d, deptno+empno+mgr from emp group by d,mgr]]> + </Resource> + </TestCase> + <TestCase name="testGroupByOrdinal"> + <Resource name="plan"> + <![CDATA[ +LogicalAggregate(group=[{0}]) + LogicalProject(EMPNO=[$0]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select empno from emp group by 1]]> + </Resource> + </TestCase> + <TestCase name="testGroupByContainsLiterals"> + <Resource name="plan"> + <![CDATA[ +LogicalAggregate(group=[{}], EXPR$0=[COUNT()]) + LogicalProject($f0=[0]) + LogicalProject(EXPR$0=[1]) + LogicalAggregate(group=[{0}]) + LogicalProject($f0=[SUBSTRING($1, 2, 3)]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select count(*) from (select 1 from emp group by substring(ename from 2 for 3))]]> + </Resource> + </TestCase> + <TestCase name="testAliasInHaving"> + <Resource name="plan"> + <![CDATA[ +LogicalFilter(condition=[>($0, 1)]) + LogicalAggregate(group=[{}], E=[COUNT()]) + LogicalProject(EMPNO=[$0]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + <Resource name="sql"> + <![CDATA[select count(empno) as e from emp having e > 1]]> + </Resource> + </TestCase> <TestCase name="testGroupJustOneAgg"> <Resource name="plan"> <