Skip to content

Commit

Permalink
[CALCITE-5962] Support parse Spark-style syntax LEFT ANTI JOIN in Bab…
Browse files Browse the repository at this point in the history
…el parser
  • Loading branch information
jiefei30 authored and JiajunBernoulli committed Sep 28, 2023
1 parent 436ae7d commit be21466
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 5 deletions.
3 changes: 3 additions & 0 deletions babel/src/main/codegen/config.fmpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ data: {
# List of new keywords. Example: "DATABASES", "TABLES". If the keyword is
# not a reserved keyword, add it to the 'nonReservedKeywords' section.
keywords: [
"ANTI"
"DISCARD"
"IF"
"PLANS"
Expand All @@ -56,6 +57,7 @@ data: {
# items in this list become non-reserved
nonReservedKeywordsToAdd: [
# not in core, added in babel
"ANTI"
"DISCARD"
"IF"
"PLANS"
Expand Down Expand Up @@ -545,6 +547,7 @@ data: {
# List of additional join types. Each is a method with no arguments.
# Example: "LeftSemiJoin".
joinTypes: [
"LeftAntiJoin",
"LeftSemiJoin"
]

Expand Down
7 changes: 7 additions & 0 deletions babel/src/main/codegen/includes/parserImpls.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ JoinType LeftSemiJoin() :
<LEFT> <SEMI> <JOIN> { return JoinType.LEFT_SEMI_JOIN; }
}

JoinType LeftAntiJoin() :
{
}
{
<LEFT> <ANTI> <JOIN> { return JoinType.LEFT_ANTI_JOIN; }
}

SqlNode DateaddFunctionCall() :
{
final Span s;
Expand Down
34 changes: 34 additions & 0 deletions babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.dialect.MysqlSqlDialect;
import org.apache.calcite.sql.dialect.PostgresqlSqlDialect;
import org.apache.calcite.sql.dialect.SparkSqlDialect;
import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.SqlParserFixture;
Expand Down Expand Up @@ -430,6 +431,39 @@ private void checkParseInfixCast(String sqlType) {
f.sql("DISCARD TEMP").same();
}

@Test void testSparkLeftAntiJoin() {
final SqlParserFixture f = fixture().withDialect(SparkSqlDialect.DEFAULT);
final String sql = "select a.cid, a.cname, count(1) as amount\n"
+ "from geo.area1 as a\n"
+ "left anti join (select distinct cid, cname\n"
+ "from geo.area2\n"
+ "where cname = 'cityA') as b on a.cid = b.cid\n"
+ "group by a.cid, a.cname";
final String expected = "SELECT A.CID, A.CNAME, COUNT(1) AMOUNT\n"
+ "FROM GEO.AREA1 A\n"
+ "LEFT ANTI JOIN (SELECT DISTINCT CID, CNAME\n"
+ "FROM GEO.AREA2\n"
+ "WHERE (CNAME = 'cityA')) B ON (A.CID = B.CID)\n"
+ "GROUP BY A.CID, A.CNAME";
f.sql(sql).ok(expected);
}

@Test void testLeftAntiJoin() {
final String sql = "select a.cid, a.cname, count(1) as amount\n"
+ "from geo.area1 as a\n"
+ "left anti join (select distinct cid, cname\n"
+ "from geo.area2\n"
+ "where cname = 'cityA') as b on a.cid = b.cid\n"
+ "group by a.cid, a.cname";
final String expected = "SELECT `A`.`CID`, `A`.`CNAME`, COUNT(1) AS `AMOUNT`\n"
+ "FROM `GEO`.`AREA1` AS `A`\n"
+ "LEFT ANTI JOIN (SELECT DISTINCT `CID`, `CNAME`\n"
+ "FROM `GEO`.`AREA2`\n"
+ "WHERE (`CNAME` = 'cityA')) AS `B` ON (`A`.`CID` = `B`.`CID`)\n"
+ "GROUP BY `A`.`CID`, `A`.`CNAME`";
sql(sql).ok(expected);
}

/** Similar to {@link #testHoist()} but using custom parser. */
@Test void testHoistMySql() {
// SQL contains back-ticks, which require MySQL's quoting,
Expand Down
15 changes: 15 additions & 0 deletions babel/src/test/java/org/apache/calcite/test/BabelTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,21 @@ private void checkInfixCast(Statement statement, String typeName, int sqlType)
.type("RecordType(INTEGER NOT NULL DEPTNO) NOT NULL");
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-5962">[CALCITE-5962]
* Support parse Spark-style syntax "LEFT ANTI JOIN" in Babel parser</a>. */
@Test public void testLeftAntiJoin() {
final SqlValidatorFixture v = Fixtures.forValidator()
.withParserConfig(c -> c.withParserFactory(SqlBabelParserImpl.FACTORY))
.withConformance(SqlConformanceEnum.BABEL);

v.withSql("SELECT * FROM dept LEFT ANTI JOIN emp ON emp.deptno = dept.deptno")
.type("RecordType(INTEGER NOT NULL DEPTNO, VARCHAR(10) NOT NULL NAME) NOT NULL");

v.withSql("SELECT name FROM dept LEFT ANTI JOIN emp ON emp.deptno = dept.deptno")
.type("RecordType(VARCHAR(10) NOT NULL NAME) NOT NULL");
}

private void checkSqlResult(String funLibrary, String query, String result) {
CalciteAssert.that()
.with(CalciteConnectionProperty.PARSER_FACTORY,
Expand Down
22 changes: 22 additions & 0 deletions babel/src/test/resources/sql/select.iq
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno where dept.deptno = 100
Table 'DEPT' not found
!error

# LEFT ANTI JOIN (Spark only)
# Only LHS columns are referenced in SELECT clause with LEFT ANTI JOIN syntax
SELECT *
FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno;

SELECT "EMP"."EMPNO", "EMP"."ENAME", "EMP"."JOB", "EMP"."MGR", "EMP"."HIREDATE", "EMP"."SAL", "EMP"."COMM", "EMP"."DEPTNO"
FROM "scott"."EMP" AS "EMP"
LEFT ANTI JOIN "scott"."DEPT" AS "DEPT" ON "EMP"."DEPTNO" = "DEPT"."DEPTNO"
!explain-validated-on spark

# Can not reference RHS columns in SELECT clause with LEFT ANTI JOIN syntax
SELECT emp.ename
FROM dept LEFT ANTI JOIN emp ON dept.deptno = emp.deptno;
Table 'EMP' not found
!error

# Can not reference RHS columns in WHERE clause with LEFT ANTI JOIN syntax
SELECT *
FROM dept LEFT ANTI JOIN emp ON dept.deptno = emp.deptno where emp.empno = 30;
Table 'EMP' not found
!error

# Test CONNECT BY (Oracle only)
!if (false) {
SELECT *
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -1988,8 +1988,8 @@ SqlLiteral JoinType() :
}
{
(
LOOKAHEAD(3) // required for "LEFT SEMI JOIN" in Babel
<#list (parser.joinTypes!default.parser.joinTypes) as method>
LOOKAHEAD(3) // required for "LEFT SEMI JOIN" and "LEFT ANTI JOIN" in Babel
joinType = ${method}()
|
</#list>
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/JoinType.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public enum JoinType implements Symbolizable {
*/
LEFT_SEMI_JOIN,

/**
* Left anti join.
*
* <p>Not used by Calcite; only in Babel's Spark dialect.
*/
LEFT_ANTI_JOIN,

/**
* Comma join: the good old-fashioned SQL <code>FROM</code> clause,
* where table expressions are specified with commas between them, and
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/SqlJoin.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ private SqlJoinOperator(String name, int prec) {
writer.sep(join.isNatural() ? "NATURAL LEFT SEMI JOIN"
: "LEFT SEMI JOIN");
break;
case LEFT_ANTI_JOIN:
writer.sep(join.isNatural() ? "NATURAL LEFT ANTI JOIN"
: "LEFT ANTI JOIN");
break;
case RIGHT:
writer.sep(join.isNatural() ? "NATURAL RIGHT JOIN" : "RIGHT JOIN");
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class JoinNamespace extends AbstractNamespace {
leftType = typeFactory.createTypeWithNullability(leftType, true);
rightType = typeFactory.createTypeWithNullability(rightType, true);
break;
// LEFT SEMI JOIN can only come from Babel.
// LEFT SEMI JOIN and LEFT ANTI JOIN can only come from Babel.
case LEFT_SEMI_JOIN:
case LEFT_ANTI_JOIN:
return typeFactory.createJoinType(leftType);
default:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import org.checkerframework.checker.nullness.qual.Nullable;

import static org.apache.calcite.sql.JoinType.LEFT_ANTI_JOIN;
import static org.apache.calcite.sql.JoinType.LEFT_SEMI_JOIN;
import static org.apache.calcite.sql.SqlUtil.stripAs;

Expand Down Expand Up @@ -70,8 +71,9 @@ public class JoinScope extends ListScope {
boolean nullable) {
super.addChild(ns, alias, nullable);

// LEFT SEMI JOIN can only come from Babel.
if (join.getJoinType() == LEFT_SEMI_JOIN
// LEFT SEMI JOIN and LEFT ANTI JOIN can only come from Babel.
if ((join.getJoinType() == LEFT_SEMI_JOIN
|| join.getJoinType() == LEFT_ANTI_JOIN)
&& stripAs(join.getRight()) == ns.getNode()) {
// Ignore the right hand side.
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3619,10 +3619,11 @@ protected void validateJoin(SqlJoin join, SqlValidatorScope scope) {
// Which join types require/allow a ON/USING condition, or allow
// a NATURAL keyword?
switch (joinType) {
case LEFT_ANTI_JOIN:
case LEFT_SEMI_JOIN:
if (!this.config.conformance().isLiberal()) {
throw newValidationError(join.getJoinTypeNode(),
RESOURCE.dialectDoesNotSupportFeature("LEFT SEMI JOIN"));
RESOURCE.dialectDoesNotSupportFeature(joinType.name()));
}
// fall through
case INNER:
Expand Down

0 comments on commit be21466

Please sign in to comment.