Skip to content

Commit 0fe52b0

Browse files
xiedeyantuiwanttobepowerful
authored andcommitted
[CALCITE-5347] Add 'SELECT ... BY', a syntax extension that is shorthand for GROUP BY and ORDER BY
1 parent 5ec06f2 commit 0fe52b0

File tree

13 files changed

+459
-16
lines changed

13 files changed

+459
-16
lines changed

babel/src/main/codegen/config.fmpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ data: {
119119
"BOOLEAN"
120120
"BOTH"
121121
"BREADTH"
122-
"BY"
122+
# "BY"
123123
# "CALL"
124124
"CALLED"
125125
"CARDINALITY"
@@ -618,6 +618,7 @@ data: {
618618
includeParsingStringLiteralAsArrayLiteral: true
619619
includeIntervalWithoutQualifier: true
620620
includeStarExclude: true
621+
includeSelectBy: true
621622
}
622623
}
623624

babel/src/test/java/org/apache/calcite/test/BabelTest.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,27 @@
1717
package org.apache.calcite.test;
1818

1919
import org.apache.calcite.config.CalciteConnectionProperty;
20+
import org.apache.calcite.rel.RelNode;
21+
import org.apache.calcite.rel.rel2sql.RelToSqlConverter;
2022
import org.apache.calcite.rel.type.DelegatingTypeSystem;
2123
import org.apache.calcite.rel.type.RelDataTypeField;
2224
import org.apache.calcite.rel.type.TimeFrameSet;
25+
import org.apache.calcite.schema.SchemaPlus;
26+
import org.apache.calcite.sql.SqlDialect;
27+
import org.apache.calcite.sql.SqlNode;
2328
import org.apache.calcite.sql.SqlOperatorTable;
29+
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
2430
import org.apache.calcite.sql.fun.SqlLibrary;
2531
import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
32+
import org.apache.calcite.sql.parser.SqlParser;
2633
import org.apache.calcite.sql.parser.SqlParserFixture;
2734
import org.apache.calcite.sql.parser.babel.SqlBabelParserImpl;
2835
import org.apache.calcite.sql.validate.SqlConformanceEnum;
36+
import org.apache.calcite.tools.FrameworkConfig;
37+
import org.apache.calcite.tools.Frameworks;
38+
import org.apache.calcite.tools.Planner;
39+
import org.apache.calcite.tools.Programs;
40+
import org.apache.calcite.util.TestUtil;
2941

3042
import com.google.common.collect.ImmutableList;
3143

@@ -43,6 +55,8 @@
4355
import java.util.function.UnaryOperator;
4456
import java.util.stream.Collectors;
4557

58+
import static org.apache.calcite.test.Matchers.isLinux;
59+
4660
import static org.hamcrest.CoreMatchers.is;
4761
import static org.hamcrest.MatcherAssert.assertThat;
4862

@@ -341,6 +355,133 @@ names, is(
341355
.type("RecordType(VARCHAR(10) NOT NULL NAME) NOT NULL");
342356
}
343357

358+
/** Test case for
359+
* <a href="https://issues.apache.org/jira/browse/CALCITE-5347">[CALCITE-5347]
360+
* Support parse "SELECT ... BY" in Babel parser</a>. */
361+
@Test void testByClause() {
362+
final SqlValidatorFixture v = Fixtures.forValidator()
363+
.withParserConfig(c -> c.withParserFactory(SqlBabelParserImpl.FACTORY))
364+
.withConformance(SqlConformanceEnum.BABEL);
365+
366+
// Test basic BY clause: SELECT a BY b is sugar for SELECT b, a GROUP BY b ORDER BY b
367+
v.withSql("select ename, empno by deptno from emp").ok();
368+
// Test BY clause with alias
369+
v.withSql("select ename, empno by deptno as dept from emp").ok();
370+
// Test BY clause with DESC modifier
371+
v.withSql("select ename, empno by deptno DESC from emp").ok();
372+
// Test BY clause with multiple columns
373+
v.withSql("select ename, empno by deptno, job from emp").ok();
374+
// Test complex BY clause example from the feature proposal
375+
v.withSql("select e.ename, e.empno by d.name as dept DESC, e.job as title "
376+
+ "from emp as e join dept as d on e.deptno = d.deptno where d.name = 'SALES'")
377+
.ok();
378+
379+
// Test SELECT BY cannot be used with GROUP BY
380+
v.withSql("select ename by deptno from emp ^group by empno^")
381+
.fails("SELECT BY cannot be used with GROUP BY");
382+
// Test SELECT BY cannot be used with ORDER BY
383+
v.withSql("select ename by deptno from emp ^order by empno^")
384+
.fails("SELECT BY cannot be used with ORDER BY");
385+
}
386+
387+
/** Test case of
388+
* <a href="https://issues.apache.org/jira/browse/CALCITE-5347">[CALCITE-5347]
389+
* Add 'SELECT ... BY', a syntax extension that is shorthand for GROUP BY and ORDER BY</a>. */
390+
@Test void testByClauseConversion() {
391+
// Test basic BY clause: SELECT a BY b is sugar for SELECT b, a GROUP BY b ORDER BY b
392+
final String sql = "select ename, empno by deptno from emp";
393+
final String expected = "SELECT \"DEPTNO\","
394+
+ " ANY_VALUE(\"ENAME\") AS \"ENAME\","
395+
+ " ANY_VALUE(\"EMPNO\") AS \"EMPNO\"\n"
396+
+ "FROM \"SCOTT\".\"EMP\"\n"
397+
+ "GROUP BY \"DEPTNO\"\n"
398+
+ "ORDER BY \"DEPTNO\"";
399+
checkSqlConversion(sql, expected);
400+
401+
// Test BY clause with alias
402+
final String sql2 = "select ename, empno by deptno as dept from emp";
403+
final String expected2 = "SELECT \"DEPTNO\" AS \"DEPT\","
404+
+ " ANY_VALUE(\"ENAME\") AS \"ENAME\","
405+
+ " ANY_VALUE(\"EMPNO\") AS \"EMPNO\"\n"
406+
+ "FROM \"SCOTT\".\"EMP\"\n"
407+
+ "GROUP BY \"DEPTNO\"\n"
408+
+ "ORDER BY \"DEPTNO\"";
409+
checkSqlConversion(sql2, expected2);
410+
411+
// Test BY clause with DESC modifier
412+
final String sql3 = "select ename, empno by deptno DESC from emp";
413+
final String expected3 = "SELECT \"DEPTNO\","
414+
+ " ANY_VALUE(\"ENAME\") AS \"ENAME\","
415+
+ " ANY_VALUE(\"EMPNO\") AS \"EMPNO\"\n"
416+
+ "FROM \"SCOTT\".\"EMP\"\n"
417+
+ "GROUP BY \"DEPTNO\"\n"
418+
+ "ORDER BY \"DEPTNO\" DESC";
419+
checkSqlConversion(sql3, expected3);
420+
421+
// Test BY clause with multiple columns
422+
final String sql4 = "select ename, empno by deptno, job from emp";
423+
final String expected4 = "SELECT \"DEPTNO\", \"JOB\","
424+
+ " ANY_VALUE(\"ENAME\") AS \"ENAME\","
425+
+ " ANY_VALUE(\"EMPNO\") AS \"EMPNO\"\n"
426+
+ "FROM \"SCOTT\".\"EMP\"\n"
427+
+ "GROUP BY \"DEPTNO\", \"JOB\"\n"
428+
+ "ORDER BY \"DEPTNO\", \"JOB\"";
429+
checkSqlConversion(sql4, expected4);
430+
431+
// Test complex BY clause example from the feature proposal
432+
final String sql5 = "SELECT e.ename, e.empno BY d.dname AS dept DESC, e.job AS title\n"
433+
+ "FROM emp AS e\n"
434+
+ " JOIN dept AS d ON e.deptno = d.deptno\n"
435+
+ "WHERE d.loc = 'CHICAGO'";
436+
final String expected5 = "SELECT \"DEPT\".\"DNAME\" AS \"DEPT\","
437+
+ " \"EMP\".\"JOB\" AS \"TITLE\","
438+
+ " ANY_VALUE(\"EMP\".\"ENAME\") AS \"ENAME\","
439+
+ " ANY_VALUE(\"EMP\".\"EMPNO\") AS \"EMPNO\"\n"
440+
+ "FROM \"SCOTT\".\"EMP\"\n"
441+
+ "INNER JOIN \"SCOTT\".\"DEPT\" ON \"EMP\".\"DEPTNO\" = \"DEPT\".\"DEPTNO\"\n"
442+
+ "WHERE \"DEPT\".\"LOC\" = 'CHICAGO'\n"
443+
+ "GROUP BY \"DEPT\".\"DNAME\", \"EMP\".\"JOB\"\n"
444+
+ "ORDER BY \"DEPT\".\"DNAME\" DESC, \"EMP\".\"JOB\"";
445+
checkSqlConversion(sql5, expected5);
446+
}
447+
448+
private void checkSqlConversion(String sql, String expected) {
449+
try {
450+
final SqlParser.Config parserConfig = SqlParser.config()
451+
.withParserFactory(SqlBabelParserImpl.FACTORY);
452+
453+
final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
454+
final SchemaPlus defaultSchema =
455+
CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.JDBC_SCOTT);
456+
457+
final FrameworkConfig config = Frameworks.newConfigBuilder()
458+
.parserConfig(parserConfig)
459+
.defaultSchema(defaultSchema)
460+
.programs(Programs.standard())
461+
.build();
462+
463+
final Planner planner = Frameworks.getPlanner(config);
464+
final SqlNode parse = planner.parse(sql);
465+
final SqlNode validate = planner.validate(parse);
466+
RelNode rel = planner.rel(validate).project();
467+
468+
final SqlDialect dialect = CalciteSqlDialect.DEFAULT;
469+
final RelToSqlConverter converter = new RelToSqlConverter(dialect);
470+
final SqlNode sqlNode = converter.visitRoot(rel).asStatement();
471+
final String actual = sqlNode.toSqlString(c ->
472+
c.withDialect(dialect)
473+
.withAlwaysUseParentheses(false)
474+
.withSelectListItemsOnSeparateLines(false)
475+
.withUpdateSetListNewline(false)
476+
.withIndentation(0))
477+
.getSql();
478+
479+
assertThat(actual, isLinux(expected));
480+
} catch (Exception e) {
481+
throw TestUtil.rethrow(e);
482+
}
483+
}
484+
344485
private void checkSqlResult(String funLibrary, String query, String result) {
345486
CalciteAssert.that()
346487
.with(CalciteConnectionProperty.PARSER_FACTORY,

babel/src/test/resources/sql/select.iq

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,78 @@ from emp e join dept d on e.deptno = d.deptno limit 1;
160160

161161
!ok
162162

163+
# Test basic BY clause: SELECT a BY b is sugar for SELECT b, a GROUP BY b ORDER BY b
164+
select ename, empno by deptno from emp;
165+
+--------+--------+-------+
166+
| DEPTNO | ENAME | EMPNO |
167+
+--------+--------+-------+
168+
| 10 | MILLER | 7934 |
169+
| 20 | SMITH | 7902 |
170+
| 30 | WARD | 7900 |
171+
+--------+--------+-------+
172+
(3 rows)
173+
174+
!ok
175+
176+
# Test BY clause with alias
177+
select ename, empno by deptno as dept from emp;
178+
+------+--------+-------+
179+
| DEPT | ENAME | EMPNO |
180+
+------+--------+-------+
181+
| 10 | MILLER | 7934 |
182+
| 20 | SMITH | 7902 |
183+
| 30 | WARD | 7900 |
184+
+------+--------+-------+
185+
(3 rows)
186+
187+
!ok
188+
189+
# Test BY clause with DESC modifier
190+
select ename, empno by deptno DESC from emp;
191+
+--------+--------+-------+
192+
| DEPTNO | ENAME | EMPNO |
193+
+--------+--------+-------+
194+
| 30 | WARD | 7900 |
195+
| 20 | SMITH | 7902 |
196+
| 10 | MILLER | 7934 |
197+
+--------+--------+-------+
198+
(3 rows)
199+
200+
!ok
201+
202+
# Test BY clause with multiple columns
203+
select ename, empno by deptno, job from emp;
204+
+--------+-----------+--------+-------+
205+
| DEPTNO | JOB | ENAME | EMPNO |
206+
+--------+-----------+--------+-------+
207+
| 10 | CLERK | MILLER | 7934 |
208+
| 10 | MANAGER | CLARK | 7782 |
209+
| 10 | PRESIDENT | KING | 7839 |
210+
| 20 | ANALYST | SCOTT | 7902 |
211+
| 20 | CLERK | SMITH | 7876 |
212+
| 20 | MANAGER | JONES | 7566 |
213+
| 30 | CLERK | JAMES | 7900 |
214+
| 30 | MANAGER | BLAKE | 7698 |
215+
| 30 | SALESMAN | WARD | 7844 |
216+
+--------+-----------+--------+-------+
217+
(9 rows)
218+
219+
!ok
220+
221+
# Test complex BY clause example from the feature proposal
222+
SELECT e.ename, e.empno BY d.dname AS dept DESC, e.job AS title
223+
FROM emp AS e
224+
JOIN dept AS d ON e.deptno = d.deptno
225+
WHERE d.loc = 'CHICAGO';
226+
+-------+----------+-------+-------+
227+
| DEPT | TITLE | ENAME | EMPNO |
228+
+-------+----------+-------+-------+
229+
| SALES | CLERK | JAMES | 7900 |
230+
| SALES | MANAGER | BLAKE | 7698 |
231+
| SALES | SALESMAN | WARD | 7844 |
232+
+-------+----------+-------+-------+
233+
(3 rows)
234+
235+
!ok
236+
163237
# End select.iq

core/src/main/codegen/templates/Parser.jj

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import org.apache.calcite.sql.SqlPrefixOperator;
9090
import org.apache.calcite.sql.SqlRowTypeNameSpec;
9191
import org.apache.calcite.sql.SqlSampleSpec;
9292
import org.apache.calcite.sql.SqlSelect;
93+
import org.apache.calcite.sql.SqlByRewriter;
9394
import org.apache.calcite.sql.SqlSelectKeyword;
9495
import org.apache.calcite.sql.SqlStarExclude;
9596
import org.apache.calcite.sql.SqlSetOption;
@@ -724,6 +725,12 @@ SqlNode OrderByLimitOpt(SqlNode e) :
724725
]
725726
{
726727
if (orderBy != null || offsetFetch[0] != null || offsetFetch[1] != null) {
728+
if (orderBy != null
729+
&& e instanceof SqlSelect
730+
&& ((SqlSelect) e).hasByClause()) {
731+
throw SqlUtil.newContextException(orderBy.getParserPosition(),
732+
RESOURCE.selectByCannotWithOrderBy());
733+
}
727734
return new SqlOrderBy(getPos(), e,
728735
Util.first(orderBy, SqlNodeList.EMPTY),
729736
offsetFetch[0], offsetFetch[1]);
@@ -1343,6 +1350,7 @@ SqlSelect SqlSelect() :
13431350
final SqlNode having;
13441351
final SqlNodeList windowDecls;
13451352
final SqlNode qualify;
1353+
final SqlNodeList by;
13461354
final List<SqlNode> hints = new ArrayList<SqlNode>();
13471355
final Span s;
13481356
}
@@ -1363,6 +1371,11 @@ SqlSelect SqlSelect() :
13631371
}
13641372
AddSelectItem(selectList)
13651373
( <COMMA> AddSelectItem(selectList) )*
1374+
<#if parser.includeSelectBy!false>
1375+
( by = SqlSelectBy() | { by = null; } )
1376+
<#else>
1377+
{ by = null; }
1378+
</#if>
13661379
(
13671380
<FROM> fromClause = FromClause()
13681381
( where = Where() | { where = null; } )
@@ -1381,10 +1394,12 @@ SqlSelect SqlSelect() :
13811394
}
13821395
)
13831396
{
1384-
return new SqlSelect(s.end(this), keywordList,
1397+
final SqlSelect select = new SqlSelect(s.end(this), keywordList,
13851398
new SqlNodeList(selectList, Span.of(selectList).pos()),
13861399
fromClause, where, groupBy, having, windowDecls, qualify,
13871400
null, null, null, new SqlNodeList(hints, getPos()));
1401+
SqlByRewriter.rewrite(select, by);
1402+
return select;
13881403
}
13891404
}
13901405

@@ -2969,26 +2984,65 @@ SqlNodeList OrderBy(boolean accept) :
29692984
throw SqlUtil.newContextException(s.pos(), RESOURCE.illegalOrderBy());
29702985
}
29712986
}
2972-
<BY> AddOrderItem(list)
2973-
(
2974-
// NOTE jvs 6-Feb-2004: See comments at top of file for why
2975-
// hint is necessary here.
2976-
LOOKAHEAD(2) <COMMA> AddOrderItem(list)
2977-
)*
2987+
<BY> OrderItemList(list)
2988+
{
2989+
return new SqlNodeList(list, s.addAll(list).pos());
2990+
}
2991+
}
2992+
2993+
<#if parser.includeSelectBy!false>
2994+
/**
2995+
* Parses a BY clause for SELECT (syntactic sugar for GROUP BY ... ORDER BY).
2996+
*/
2997+
SqlNodeList SqlSelectBy() :
2998+
{
2999+
final List<SqlNode> list = new ArrayList<SqlNode>();
3000+
final Span s;
3001+
}
3002+
{
3003+
<BY> { s = span(); }
3004+
OrderItemList(list)
29783005
{
29793006
return new SqlNodeList(list, s.addAll(list).pos());
29803007
}
29813008
}
3009+
</#if>
3010+
3011+
3012+
/**
3013+
* Parses a list of ORDER BY items.
3014+
*/
3015+
void OrderItemList(List<SqlNode> list) :
3016+
{
3017+
}
3018+
{
3019+
AddOrderItem(list)
3020+
(
3021+
LOOKAHEAD(2) <COMMA> AddOrderItem(list)
3022+
)*
3023+
}
29823024

29833025
/**
29843026
* Parses one item in an ORDER BY clause, and adds it to a list.
29853027
*/
29863028
void AddOrderItem(List<SqlNode> list) :
29873029
{
29883030
SqlNode e;
3031+
final SqlIdentifier id;
29893032
}
29903033
{
29913034
e = Expression(ExprContext.ACCEPT_SUB_QUERY)
3035+
(
3036+
[ <AS>
3037+
(
3038+
id = SimpleIdentifier()
3039+
|
3040+
LOOKAHEAD(1)
3041+
id = SimpleIdentifierFromStringLiteral()
3042+
)
3043+
{ e = SqlStdOperatorTable.AS.createCall(span().end(e), e, id); }
3044+
]
3045+
)
29923046
(
29933047
<ASC>
29943048
| <DESC> {

0 commit comments

Comments
 (0)