This is an automated email from the ASF dual-hosted git repository.
jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/master by this push:
new 986a2d5 [CALCITE-2453] Parse list of SQL statements separated with a
semicolon (Chunwei Lei, charbel yazbeck)
986a2d5 is described below
commit 986a2d579c8f9b9f08aa9bbbfe11efc4e7bb0809
Author: cyazbeck <[email protected]>
AuthorDate: Wed Aug 8 12:41:13 2018 +0300
[CALCITE-2453] Parse list of SQL statements separated with a semicolon
(Chunwei Lei, charbel yazbeck)
Close apache/calcite#1177
---
core/src/main/codegen/templates/Parser.jj | 62 ++++--
.../calcite/sql/parser/SqlAbstractParserImpl.java | 10 +
.../org/apache/calcite/sql/parser/SqlParser.java | 36 +++-
.../apache/calcite/sql/parser/SqlParserTest.java | 236 ++++++++++++++++++++-
site/_docs/reference.md | 3 +
5 files changed, 312 insertions(+), 35 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index e83c946..6499e4b 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -166,8 +166,7 @@ public class ${parser.class} extends SqlAbstractParserImpl
}
};
- public SqlParseException normalizeException(Throwable ex)
- {
+ public SqlParseException normalizeException(Throwable ex) {
try {
if (ex instanceof ParseException) {
ex = cleanupParseException((ParseException) ex);
@@ -178,8 +177,7 @@ public class ${parser.class} extends SqlAbstractParserImpl
}
}
- public Metadata getMetadata()
- {
+ public Metadata getMetadata() {
synchronized (${parser.class}.class) {
if (metadata == null) {
metadata = new MetadataImpl(
@@ -189,48 +187,44 @@ public class ${parser.class} extends SqlAbstractParserImpl
}
}
- public void setTabSize(int tabSize)
- {
+ public void setTabSize(int tabSize) {
jj_input_stream.setTabSize(tabSize);
}
- public void switchTo(String stateName)
- {
+ public void switchTo(String stateName) {
int state = Arrays.asList(${parser.class}TokenManager.lexStateNames)
.indexOf(stateName);
token_source.SwitchTo(state);
}
- public void setQuotedCasing(Casing quotedCasing)
- {
+ public void setQuotedCasing(Casing quotedCasing) {
this.quotedCasing = quotedCasing;
}
- public void setUnquotedCasing(Casing unquotedCasing)
- {
+ public void setUnquotedCasing(Casing unquotedCasing) {
this.unquotedCasing = unquotedCasing;
}
- public void setIdentifierMaxLength(int identifierMaxLength)
- {
+ public void setIdentifierMaxLength(int identifierMaxLength) {
this.identifierMaxLength = identifierMaxLength;
}
- public void setConformance(SqlConformance conformance)
- {
+ public void setConformance(SqlConformance conformance) {
this.conformance = conformance;
}
- public SqlNode parseSqlExpressionEof() throws Exception
- {
+ public SqlNode parseSqlExpressionEof() throws Exception {
return SqlExpressionEof();
}
- public SqlNode parseSqlStmtEof() throws Exception
- {
+ public SqlNode parseSqlStmtEof() throws Exception {
return SqlStmtEof();
}
+ public SqlNodeList parseSqlStmtList() throws Exception {
+ return SqlStmtList();
+ }
+
private SqlNode extend(SqlNode table, SqlNodeList extendList) {
return SqlStdOperatorTable.EXTEND.createCall(
Span.of(table, extendList).pos(), table, extendList);
@@ -943,6 +937,34 @@ SqlNode SqlQueryEof() :
}
/**
+ * Parses a list of SQL statements separated by semicolon.
+ * The semicolon is required between statements, but is
+ * optional at the end.
+ */
+SqlNodeList SqlStmtList() :
+{
+ final List<SqlNode> stmtList = new ArrayList<SqlNode>();
+ SqlNode stmt;
+}
+{
+ stmt = SqlStmt() {
+ stmtList.add(stmt);
+ }
+ (
+ <SEMICOLON>
+ [
+ stmt = SqlStmt() {
+ stmtList.add(stmt);
+ }
+ ]
+ )*
+ <EOF>
+ {
+ return new SqlNodeList(stmtList, Span.of(stmtList).pos());
+ }
+}
+
+/**
* Parses an SQL statement.
*/
SqlNode SqlStmt() :
diff --git
a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
index 4645b43..aabb9d8 100644
---
a/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
+++
b/core/src/main/java/org/apache/calcite/sql/parser/SqlAbstractParserImpl.java
@@ -22,6 +22,7 @@ import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlSyntax;
import org.apache.calcite.sql.SqlUnresolvedFunction;
@@ -451,6 +452,15 @@ public abstract class SqlAbstractParserImpl {
public abstract SqlNode parseSqlStmtEof() throws Exception;
/**
+ * Parses a list of SQL statements separated by semicolon and constructs a
+ * parse tree. The semicolon is required between statements, but is
+ * optional at the end.
+ *
+ * @return constructed list of SQL statements.
+ */
+ public abstract SqlNodeList parseSqlStmtList() throws Exception;
+
+ /**
* Sets the tab stop size.
*
* @param tabSize Tab stop size
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
index 50772b4..61e0a01 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParser.java
@@ -21,6 +21,7 @@ import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.config.Lex;
import org.apache.calcite.runtime.CalciteContextException;
import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
@@ -135,6 +136,17 @@ public class SqlParser {
}
}
+ /** Normalizes a SQL exception. */
+ private SqlParseException handleException(Throwable ex) {
+ if (ex instanceof CalciteContextException) {
+ final String originalSql = parser.getOriginalSql();
+ if (originalSql != null) {
+ ((CalciteContextException) ex).setOriginalStatement(originalSql);
+ }
+ }
+ return parser.normalizeException(ex);
+ }
+
/**
* Parses a <code>SELECT</code> statement.
*
@@ -147,13 +159,7 @@ public class SqlParser {
try {
return parser.parseSqlStmtEof();
} catch (Throwable ex) {
- if (ex instanceof CalciteContextException) {
- final String originalSql = parser.getOriginalSql();
- if (originalSql != null) {
- ((CalciteContextException) ex).setOriginalStatement(originalSql);
- }
- }
- throw parser.normalizeException(ex);
+ throw handleException(ex);
}
}
@@ -182,6 +188,22 @@ public class SqlParser {
}
/**
+ * Parses a list of SQL statements separated by semicolon.
+ * The semicolon is required between statements, but is
+ * optional at the end.
+ *
+ * @return list of SqlNodeList representing the list of SQL statements
+ * @throws SqlParseException if there is a parse error
+ */
+ public SqlNodeList parseStmtList() throws SqlParseException {
+ try {
+ return parser.parseSqlStmtList();
+ } catch (Throwable ex) {
+ throw handleException(ex);
+ }
+ }
+
+ /**
* Get the parser metadata.
*
* @return {@link SqlAbstractParserImpl.Metadata} implementation of
diff --git
a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 93c5fa8..88046f4 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -22,6 +22,7 @@ import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSetOption;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
@@ -45,6 +46,7 @@ import org.hamcrest.BaseMatcher;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
+import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
@@ -62,6 +64,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
+import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -593,6 +596,12 @@ public class SqlParserTest {
return new Sql(sql);
}
+ /** Creates an instance of helper class {@link SqlList} to test parsing a
+ * list of statements. */
+ protected SqlList sqlList(String sql) {
+ return new SqlList(sql);
+ }
+
/**
* Implementors of custom parsing logic who want to reuse this test should
* override this method with the factory for their extension parser.
@@ -1167,6 +1176,110 @@ public class SqlParserTest {
}
}
+ /** Parses a list of statements (that contains only one statement). */
+ @Test public void testStmtListWithSelect() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ sqlList("select * from emp, dept")
+ .ok(expected);
+ }
+
+ @Test public void testStmtListWithSelectAndSemicolon() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ sqlList("select * from emp, dept;")
+ .ok(expected);
+ }
+
+ @Test public void testStmtListWithTwoSelect() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ sqlList("select * from emp, dept ; select * from emp, dept")
+ .ok(expected, expected);
+ }
+
+ @Test public void testStmtListWithTwoSelectSemicolon() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ sqlList("select * from emp, dept ; select * from emp, dept;")
+ .ok(expected, expected);
+ }
+
+ @Test public void testStmtListWithSelectDelete() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ final String expected1 = "DELETE FROM `EMP`";
+ sqlList("select * from emp, dept; delete from emp")
+ .ok(expected, expected1);
+ }
+
+ @Test public void testStmtListWithSelectDeleteUpdate() {
+ final String sql = "select * from emp, dept; "
+ + "delete from emp; "
+ + "update emps set empno = empno + 1";
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ final String expected1 = "DELETE FROM `EMP`";
+ final String expected2 = "UPDATE `EMPS` SET `EMPNO` = (`EMPNO` + 1)";
+ sqlList(sql).ok(expected, expected1, expected2);
+ }
+
+ @Test public void testStmtListWithSemiColonInComment() {
+ final String sql = ""
+ + "select * from emp, dept; // comment with semicolon ; values 1\n"
+ + "values 2";
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`,\n"
+ + "`DEPT`";
+ final String expected1 = "VALUES (ROW(2))";
+ sqlList(sql).ok(expected, expected1);
+ }
+
+ @Test public void testStmtListWithSemiColonInWhere() {
+ final String expected = "SELECT *\n"
+ + "FROM `EMP`\n"
+ + "WHERE (`NAME` LIKE 'toto;')";
+ final String expected1 = "DELETE FROM `EMP`";
+ sqlList("select * from emp where name like 'toto;'; delete from emp")
+ .ok(expected, expected1);
+ }
+
+ @Test public void testStmtListWithInsertSelectInsert() {
+ final String sql = "insert into dept (name, deptno) values ('a', 123); "
+ + "select * from emp where name like 'toto;'; "
+ + "insert into dept (name, deptno) values ('b', 123);";
+ final String expected = "INSERT INTO `DEPT` (`NAME`, `DEPTNO`)\n"
+ + "VALUES (ROW('a', 123))";
+ final String expected1 = "SELECT *\n"
+ + "FROM `EMP`\n"
+ + "WHERE (`NAME` LIKE 'toto;')";
+ final String expected2 = "INSERT INTO `DEPT` (`NAME`, `DEPTNO`)\n"
+ + "VALUES (ROW('b', 123))";
+ sqlList(sql).ok(expected, expected1, expected2);
+ }
+
+ /** Should fail since the first statement lacks semicolon */
+ @Test public void testStmtListWithoutSemiColon1() {
+ sqlList("select * from emp where name like 'toto' "
+ + "^delete^ from emp")
+ .fails("(?s).*Encountered \"delete\" at .*");
+ }
+
+ /** Should fail since the third statement lacks semicolon */
+ @Test public void testStmtListWithoutSemiColon2() {
+ sqlList("select * from emp where name like 'toto'; "
+ + "delete from emp; "
+ + "insert into dept (name, deptno) values ('a', 123) "
+ + "^select^ * from dept")
+ .fails("(?s).*Encountered \"select\" at .*");
+ }
+
@Test public void testIsDistinctFrom() {
check(
"select x is distinct from y from t",
@@ -8634,11 +8747,13 @@ public class SqlParserTest {
* Callback to control how test actions are performed.
*/
protected interface Tester {
+ void checkList(String sql, List<String> expected);
+
void check(String sql, String expected);
void checkExp(String sql, String expected);
- void checkFails(String sql, String expectedMsgPattern);
+ void checkFails(String sql, boolean list, String expectedMsgPattern);
void checkExpFails(String sql, String expectedMsgPattern);
@@ -8651,11 +8766,9 @@ public class SqlParserTest {
* Default implementation of {@link Tester}.
*/
protected class TesterImpl implements Tester {
- public void check(
- String sql,
+ private void check(
+ SqlNode sqlNode,
String expected) {
- final SqlNode sqlNode = parseStmtAndHandleEx(sql);
-
// no dialect, always parenthesize
String actual = sqlNode.toSqlString(null, true).getSql();
if (LINUXIFY.get()[0]) {
@@ -8664,6 +8777,25 @@ public class SqlParserTest {
TestUtil.assertEqualsVerbose(expected, actual);
}
+ @Override public void checkList(
+ String sql,
+ List<String> expected) {
+ final SqlNodeList sqlNodeList = parseStmtsAndHandleEx(sql);
+ assertThat(sqlNodeList.size(), is(expected.size()));
+
+ for (int i = 0; i < sqlNodeList.size(); i++) {
+ SqlNode sqlNode = sqlNodeList.get(i);
+ check(sqlNode, expected.get(i));
+ }
+ }
+
+ public void check(
+ String sql,
+ String expected) {
+ final SqlNode sqlNode = parseStmtAndHandleEx(sql);
+ check(sqlNode, expected);
+ }
+
protected SqlNode parseStmtAndHandleEx(String sql) {
final SqlNode sqlNode;
try {
@@ -8674,6 +8806,17 @@ public class SqlParserTest {
return sqlNode;
}
+ /** Parses a list of statements. */
+ protected SqlNodeList parseStmtsAndHandleEx(String sql) {
+ final SqlNodeList sqlNodeList;
+ try {
+ sqlNodeList = getSqlParser(sql).parseStmtList();
+ } catch (SqlParseException e) {
+ throw new RuntimeException("Error while parsing SQL: " + sql, e);
+ }
+ return sqlNodeList;
+ }
+
public void checkExp(
String sql,
String expected) {
@@ -8697,11 +8840,17 @@ public class SqlParserTest {
public void checkFails(
String sql,
+ boolean list,
String expectedMsgPattern) {
SqlParserUtil.StringAndPos sap = SqlParserUtil.findPos(sql);
Throwable thrown = null;
try {
- final SqlNode sqlNode = getSqlParser(sap.sql).parseStmt();
+ final SqlNode sqlNode;
+ if (list) {
+ sqlNode = getSqlParser(sap.sql).parseStmtList();
+ } else {
+ sqlNode = getSqlParser(sap.sql).parseStmt();
+ }
Util.discard(sqlNode);
} catch (Throwable ex) {
thrown = ex;
@@ -8754,6 +8903,54 @@ public class SqlParserTest {
* unparsing a query are consistent with the original query.
*/
public class UnparsingTesterImpl extends TesterImpl {
+
+ private String toSqlString(SqlNodeList sqlNodeList) {
+ List<String> sqls = sqlNodeList.getList().stream()
+ .map(it -> it.toSqlString(CalciteSqlDialect.DEFAULT, false).getSql())
+ .collect(Collectors.toList());
+ return String.join(";", sqls);
+ }
+
+ private void checkList(SqlNodeList sqlNodeList, List<String> expected) {
+ Assert.assertEquals(expected.size(), sqlNodeList.size());
+
+ for (int i = 0; i < sqlNodeList.size(); i++) {
+ SqlNode sqlNode = sqlNodeList.get(i);
+ // Unparse with no dialect, always parenthesize.
+ final String actual = sqlNode.toSqlString(null, true).getSql();
+ assertEquals(expected.get(i), linux(actual));
+ }
+ }
+
+ @Override public void checkList(String sql, List<String> expected) {
+ SqlNodeList sqlNodeList = parseStmtsAndHandleEx(sql);
+
+ checkList(sqlNodeList, expected);
+
+ // Unparse again in Calcite dialect (which we can parse), and
+ // minimal parentheses.
+ final String sql1 = toSqlString(sqlNodeList);
+
+ // Parse and unparse again.
+ SqlNodeList sqlNodeList2;
+ final Quoting q = quoting;
+ try {
+ quoting = Quoting.DOUBLE_QUOTE;
+ sqlNodeList2 = parseStmtsAndHandleEx(sql1);
+ } finally {
+ quoting = q;
+ }
+ final String sql2 = toSqlString(sqlNodeList2);
+
+ // Should be the same as we started with.
+ assertEquals(sql1, sql2);
+
+ // Now unparse again in the null dialect.
+ // If the unparser is not including sufficient parens to override
+ // precedence, the problem will show up here.
+ checkList(sqlNodeList2, expected);
+ }
+
@Override public void check(String sql, String expected) {
SqlNode sqlNode = parseStmtAndHandleEx(sql);
@@ -8822,7 +9019,8 @@ public class SqlParserTest {
assertEquals(expected, linux(actual2));
}
- @Override public void checkFails(String sql, String expectedMsgPattern) {
+ @Override public void checkFails(String sql,
+ boolean list, String expectedMsgPattern) {
// Do nothing. We're not interested in unparsing invalid SQL
}
@@ -8866,7 +9064,7 @@ public class SqlParserTest {
if (expression) {
getTester().checkExpFails(sql, expectedMsgPattern);
} else {
- getTester().checkFails(sql, expectedMsgPattern);
+ getTester().checkFails(sql, false, expectedMsgPattern);
}
return this;
}
@@ -8889,6 +9087,28 @@ public class SqlParserTest {
}
}
+ /** Helper class for building fluent code,
+ * similar to {@link Sql}, but used to manipulate
+ * a list of statements, such as
+ * {@code sqlList("select * from a;").ok();}. */
+ protected class SqlList {
+ private final String sql;
+
+ SqlList(String sql) {
+ this.sql = sql;
+ }
+
+ public SqlList ok(String... expected) {
+ getTester().checkList(sql, ImmutableList.copyOf(expected));
+ return this;
+ }
+
+ public SqlList fails(String expectedMsgPattern) {
+ getTester().checkFails(sql, true, expectedMsgPattern);
+ return this;
+ }
+ }
+
/** Runs tests on period operators such as OVERLAPS, IMMEDIATELY PRECEDES. */
private class Checker {
final String op;
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 8ac4581..1317803 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -96,6 +96,9 @@ statement:
| delete
| query
+statementList:
+ statement [ ';' statement ]* [ ';' ]
+
setStatement:
[ ALTER ( SYSTEM | SESSION ) ] SET identifier '=' expression