Skip to content

Commit d6acab2

Browse files
authored
Merge pull request #563 from zhicwu/new-sql-parser
Add JavaCC based new parser
2 parents 41f3e34 + 91c0059 commit d6acab2

22 files changed

+2593
-31
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,12 @@ out/
2626
.antlr/
2727
log/
2828
target/
29+
30+
# Generated files
31+
src/main/java/ru/yandex/clickhouse/jdbc/parser/*CharStream.java
32+
src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParser*.java
33+
src/main/java/ru/yandex/clickhouse/jdbc/parser/Token*.java
34+
src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseException.java
35+
36+
# Shell scripts
37+
*.sh

pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<deploy-plugin.version>3.0.0-M1</deploy-plugin.version>
7171
<staging-plugin.version>1.6.8</staging-plugin.version>
7272
<gpg-plugin.version>1.6</gpg-plugin.version>
73+
<javacc-plugin.version>4.1.4</javacc-plugin.version>
7374
<compiler-plugin.version>3.8.1</compiler-plugin.version>
7475
<source-plugin.version>3.2.1</source-plugin.version>
7576
<jar-plugin.version>3.2.0</jar-plugin.version>
@@ -288,6 +289,27 @@
288289
<useSystemClassLoader>false</useSystemClassLoader>
289290
</configuration>
290291
</plugin>
292+
<plugin>
293+
<groupId>com.helger.maven</groupId>
294+
<artifactId>ph-javacc-maven-plugin</artifactId>
295+
<version>${javacc-plugin.version}</version>
296+
<executions>
297+
<execution>
298+
<id>jjc</id>
299+
<phase>generate-sources</phase>
300+
<goals>
301+
<goal>javacc</goal>
302+
</goals>
303+
<configuration>
304+
<jdkVersion>${jdk.version}</jdkVersion>
305+
<javadocFriendlyComments>true</javadocFriendlyComments>
306+
<packageName>ru.yandex.clickhouse.jdbc.parser</packageName>
307+
<sourceDirectory>src/main/javacc</sourceDirectory>
308+
<outputDirectory>src/main/java</outputDirectory>
309+
</configuration>
310+
</execution>
311+
</executions>
312+
</plugin>
291313
<plugin>
292314
<groupId>org.apache.maven.plugins</groupId>
293315
<artifactId>maven-compiler-plugin</artifactId>

src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@
3636
import org.apache.http.entity.AbstractHttpEntity;
3737
import org.apache.http.impl.client.CloseableHttpClient;
3838

39+
import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement;
40+
import ru.yandex.clickhouse.jdbc.parser.StatementType;
3941
import ru.yandex.clickhouse.response.ClickHouseResponse;
4042
import ru.yandex.clickhouse.settings.ClickHouseProperties;
4143
import ru.yandex.clickhouse.settings.ClickHouseQueryParam;
4244
import ru.yandex.clickhouse.util.ClickHouseArrayUtil;
4345
import ru.yandex.clickhouse.util.ClickHouseValueFormatter;
4446
import ru.yandex.clickhouse.util.guava.StreamUtils;
4547

46-
4748
public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl implements ClickHousePreparedStatement {
4849

4950
static final String PARAM_MARKER = "?";
@@ -65,8 +66,11 @@ public ClickHousePreparedStatementImpl(CloseableHttpClient client,
6566
TimeZone serverTimeZone, int resultSetType) throws SQLException
6667
{
6768
super(client, connection, properties, resultSetType);
69+
parseSingleStatement(sql);
70+
6871
this.sql = sql;
69-
PreparedStatementParser parser = PreparedStatementParser.parse(sql);
72+
PreparedStatementParser parser = PreparedStatementParser.parse(sql,
73+
parsedStmt.getEndPosition(ClickHouseSqlStatement.KEYWORD_VALUES));
7074
this.parameterList = parser.getParameters();
7175
this.insertBatchMode = parser.isValuesMode();
7276
this.sqlParts = parser.getParts();
@@ -347,14 +351,22 @@ public int[] executeBatch() throws SQLException {
347351

348352
@Override
349353
public int[] executeBatch(Map<ClickHouseQueryParam, String> additionalDBParams) throws SQLException {
350-
Matcher matcher = VALUES.matcher(sql);
351-
if (!matcher.find()) {
354+
int valuePosition = -1;
355+
if (parsedStmt.getStatementType() == StatementType.INSERT && parsedStmt.hasValues()) {
356+
valuePosition = parsedStmt.getStartPosition(ClickHouseSqlStatement.KEYWORD_VALUES);
357+
} else {
358+
Matcher matcher = VALUES.matcher(sql);
359+
if (matcher.find()) {
360+
valuePosition = matcher.start();
361+
}
362+
}
363+
364+
if (valuePosition < 0) {
352365
throw new SQLSyntaxErrorException(
353366
"Query must be like 'INSERT INTO [db.]table [(c1, c2, c3)] VALUES (?, ?, ?)'. " +
354367
"Got: " + sql
355368
);
356369
}
357-
int valuePosition = matcher.start();
358370
String insertSql = sql.substring(0, valuePosition);
359371
BatchHttpEntity entity = new BatchHttpEntity(batchRows);
360372
sendStream(entity, insertSql, additionalDBParams);
@@ -429,7 +441,8 @@ public ResultSetMetaData getMetaData() throws SQLException {
429441
if (currentResult != null) {
430442
return currentResult.getMetaData();
431443
}
432-
if (!isSelect(sql)) {
444+
445+
if (!parsedStmt.isQuery() || (!parsedStmt.isRecognized() && !isSelect(sql))) {
433446
return null;
434447
}
435448
ResultSet myRs = executeQuery(Collections.singletonMap(

src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import java.sql.SQLWarning;
1212
import java.util.ArrayList;
1313
import java.util.EnumMap;
14+
import java.util.HashMap;
1415
import java.util.List;
16+
import java.util.Locale;
1517
import java.util.Map;
1618
import java.util.TimeZone;
1719
import java.util.UUID;
@@ -38,6 +40,9 @@
3840
import ru.yandex.clickhouse.domain.ClickHouseFormat;
3941
import ru.yandex.clickhouse.except.ClickHouseException;
4042
import ru.yandex.clickhouse.except.ClickHouseExceptionSpecifier;
43+
import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser;
44+
import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement;
45+
import ru.yandex.clickhouse.jdbc.parser.StatementType;
4146
import ru.yandex.clickhouse.response.ClickHouseLZ4Stream;
4247
import ru.yandex.clickhouse.response.ClickHouseResponse;
4348
import ru.yandex.clickhouse.response.ClickHouseResponseSummary;
@@ -84,16 +89,56 @@ public class ClickHouseStatementImpl extends ConfigurableApi<ClickHouseStatement
8489

8590
private volatile String queryId;
8691

92+
protected ClickHouseSqlStatement parsedStmt;
93+
8794
/**
8895
* Current database name may be changed by {@link java.sql.Connection#setCatalog(String)}
8996
* between creation of this object and query execution, but javadoc does not allow
9097
* {@code setCatalog} influence on already created statements.
9198
*/
9299
private final String initialDatabase;
93100

101+
@Deprecated
94102
private static final String[] selectKeywords = new String[]{"SELECT", "WITH", "SHOW", "DESC", "EXISTS", "EXPLAIN"};
103+
@Deprecated
95104
private static final String databaseKeyword = "CREATE DATABASE";
96105

106+
@Deprecated
107+
protected void parseSingleStatement(String sql) throws SQLException {
108+
this.parsedStmt = null;
109+
ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql, properties);
110+
111+
if (stmts.length == 1) {
112+
this.parsedStmt = stmts[0];
113+
} else {
114+
this.parsedStmt = new ClickHouseSqlStatement(sql, StatementType.UNKNOWN);
115+
// throw new SQLException("Multiple statements are not supported.");
116+
}
117+
118+
if (this.parsedStmt.isIdemponent()) {
119+
httpContext.setAttribute("is_idempotent", Boolean.TRUE);
120+
} else {
121+
httpContext.removeAttribute("is_idempotent");
122+
}
123+
}
124+
125+
@Deprecated
126+
private void parseSingleStatement(String sql, ClickHouseFormat preferredFormat) throws SQLException {
127+
parseSingleStatement(sql);
128+
129+
if (parsedStmt.isQuery() && !parsedStmt.hasFormat()) {
130+
String format = preferredFormat.name();
131+
Map<String, Integer> positions = new HashMap<>();
132+
positions.putAll(parsedStmt.getPositions());
133+
positions.put(ClickHouseSqlStatement.KEYWORD_FORMAT, sql.length());
134+
135+
sql = new StringBuilder(parsedStmt.getSQL()).append("\nFORMAT ").append(format).append(';')
136+
.toString();
137+
parsedStmt = new ClickHouseSqlStatement(sql, parsedStmt.getStatementType(),
138+
parsedStmt.getCluster(), parsedStmt.getDatabase(), parsedStmt.getTable(),
139+
format, parsedStmt.getOutfile(), parsedStmt.getParameters(), positions);
140+
}
141+
}
97142

98143
public ClickHouseStatementImpl(CloseableHttpClient client, ClickHouseConnection connection,
99144
ClickHouseProperties properties, int resultSetType) {
@@ -135,16 +180,29 @@ public ResultSet executeQuery(String sql,
135180
}
136181
additionalDBParams.put(ClickHouseQueryParam.EXTREMES, "0");
137182

138-
InputStream is = getInputStream(sql, additionalDBParams, externalData, additionalRequestParams);
183+
parseSingleStatement(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes);
184+
if (!parsedStmt.isRecognized() && isSelect(sql)) {
185+
Map<String, Integer> positions = new HashMap<>();
186+
String dbName = extractDBName(sql);
187+
String tableName = extractTableName(sql);
188+
if (extractWithTotals(sql)) {
189+
positions.put(ClickHouseSqlStatement.KEYWORD_TOTALS, 1);
190+
}
191+
parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT,
192+
null, dbName, tableName, null, null, null, positions);
193+
// httpContext.setAttribute("is_idempotent", Boolean.TRUE);
194+
}
139195

196+
InputStream is = getInputStream(sql, additionalDBParams, externalData, additionalRequestParams);
197+
140198
try {
141-
if (isSelect(sql)) {
199+
if (parsedStmt.isQuery()) {
142200
currentUpdateCount = -1;
143201
currentResult = createResultSet(properties.isCompress()
144202
? new ClickHouseLZ4Stream(is) : is, properties.getBufferSize(),
145-
extractDBName(sql),
146-
extractTableName(sql),
147-
extractWithTotals(sql),
203+
parsedStmt.getDatabaseOrDefault(properties.getDatabase()),
204+
parsedStmt.getTable(),
205+
parsedStmt.hasWithTotals(),
148206
this,
149207
getConnection().getTimeZone(),
150208
properties
@@ -176,8 +234,15 @@ public ClickHouseResponse executeQueryClickhouseResponse(String sql, Map<ClickHo
176234
public ClickHouseResponse executeQueryClickhouseResponse(String sql,
177235
Map<ClickHouseQueryParam, String> additionalDBParams,
178236
Map<String, String> additionalRequestParams) throws SQLException {
237+
parseSingleStatement(sql, ClickHouseFormat.JSONCompact);
238+
if (parsedStmt.isRecognized()) {
239+
sql = parsedStmt.getSQL();
240+
} else {
241+
sql = addFormatIfAbsent(sql, ClickHouseFormat.JSONCompact);
242+
}
243+
179244
InputStream is = getInputStream(
180-
addFormatIfAbsent(sql, ClickHouseFormat.JSONCompact),
245+
sql,
181246
additionalDBParams,
182247
null,
183248
additionalRequestParams
@@ -206,14 +271,27 @@ public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(Stri
206271

207272
@Override
208273
public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(String sql, Map<ClickHouseQueryParam, String> additionalDBParams, Map<String, String> additionalRequestParams) throws SQLException {
274+
parseSingleStatement(sql, ClickHouseFormat.RowBinary);
275+
if (parsedStmt.isRecognized()) {
276+
sql = parsedStmt.getSQL();
277+
} else {
278+
sql = addFormatIfAbsent(sql, ClickHouseFormat.RowBinary);
279+
if (isSelect(sql)) {
280+
parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT);
281+
// httpContext.setAttribute("is_idempotent", Boolean.TRUE);
282+
} else {
283+
parsedStmt = new ClickHouseSqlStatement(sql, StatementType.UNKNOWN);
284+
}
285+
}
286+
209287
InputStream is = getInputStream(
210-
addFormatIfAbsent(sql, ClickHouseFormat.RowBinary),
288+
sql,
211289
additionalDBParams,
212290
null,
213291
additionalRequestParams
214292
);
215293
try {
216-
if (isSelect(sql)) {
294+
if (parsedStmt.isQuery()) {
217295
currentUpdateCount = -1;
218296
currentRowBinaryResult = new ClickHouseRowBinaryInputStream(properties.isCompress()
219297
? new ClickHouseLZ4Stream(is) : is, getConnection().getTimeZone(), properties);
@@ -231,6 +309,8 @@ public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(Stri
231309

232310
@Override
233311
public int executeUpdate(String sql) throws SQLException {
312+
parseSingleStatement(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes);
313+
234314
InputStream is = null;
235315
try {
236316
is = getInputStream(sql, null, null, null);
@@ -245,8 +325,7 @@ public int executeUpdate(String sql) throws SQLException {
245325
@Override
246326
public boolean execute(String sql) throws SQLException {
247327
// currentResult is stored here. InputString and currentResult will be closed on this.close()
248-
executeQuery(sql);
249-
return isSelect(sql);
328+
return executeQuery(sql) != null;
250329
}
251330

252331
@Override
@@ -471,6 +550,7 @@ public ClickHouseResponseSummary getResponseSummary() {
471550
return currentSummary;
472551
}
473552

553+
@Deprecated
474554
static String clickhousifySql(String sql) {
475555
return addFormatIfAbsent(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes);
476556
}
@@ -479,6 +559,7 @@ static String clickhousifySql(String sql) {
479559
* Adding FORMAT TabSeparatedWithNamesAndTypes if not added
480560
* adds format only to select queries
481561
*/
562+
@Deprecated
482563
private static String addFormatIfAbsent(final String sql, ClickHouseFormat format) {
483564
String cleanSQL = sql.trim();
484565
if (!isSelect(cleanSQL)) {
@@ -498,6 +579,7 @@ private static String addFormatIfAbsent(final String sql, ClickHouseFormat forma
498579
return sb.toString();
499580
}
500581

582+
@Deprecated
501583
static boolean isSelect(String sql) {
502584
for (int i = 0; i < sql.length(); i++) {
503585
String nextTwo = sql.substring(i, Math.min(i + 2, sql.length()));
@@ -518,6 +600,7 @@ static boolean isSelect(String sql) {
518600
return false;
519601
}
520602

603+
@Deprecated
521604
private String extractTableName(String sql) {
522605
String s = extractDBAndTableName(sql);
523606
if (s.contains(".")) {
@@ -527,6 +610,7 @@ private String extractTableName(String sql) {
527610
}
528611
}
529612

613+
@Deprecated
530614
private String extractDBName(String sql) {
531615
String s = extractDBAndTableName(sql);
532616
if (s.contains(".")) {
@@ -536,6 +620,7 @@ private String extractDBName(String sql) {
536620
}
537621
}
538622

623+
@Deprecated
539624
private String extractDBAndTableName(String sql) {
540625
if (Utils.startsWithIgnoreCase(sql, "select")) {
541626
String withoutStrings = Utils.retainUnquoted(sql, '\'');
@@ -558,10 +643,11 @@ private String extractDBAndTableName(String sql) {
558643
return "system.unknown";
559644
}
560645

646+
@Deprecated
561647
private boolean extractWithTotals(String sql) {
562648
if (Utils.startsWithIgnoreCase(sql, "select")) {
563649
String withoutStrings = Utils.retainUnquoted(sql, '\'');
564-
return withoutStrings.toLowerCase().contains(" with totals");
650+
return withoutStrings.toLowerCase(Locale.ROOT).contains(" with totals");
565651
}
566652
return false;
567653
}
@@ -572,15 +658,23 @@ private InputStream getInputStream(
572658
List<ClickHouseExternalData> externalData,
573659
Map<String, String> additionalRequestParams
574660
) throws ClickHouseException {
575-
sql = clickhousifySql(sql);
661+
boolean ignoreDatabase = false;
662+
if (parsedStmt.isRecognized()) {
663+
sql = parsedStmt.getSQL();
664+
// TODO consider more scenarios like drop, show etc.
665+
ignoreDatabase = parsedStmt.getStatementType() == StatementType.CREATE
666+
&& parsedStmt.containsKeyword(ClickHouseSqlStatement.KEYWORD_DATABASE);
667+
} else {
668+
sql = clickhousifySql(sql);
669+
ignoreDatabase = sql.trim().regionMatches(true, 0, databaseKeyword, 0, databaseKeyword.length());
670+
}
576671
log.debug("Executing SQL: {}", sql);
577672

578673
additionalClickHouseDBParams = addQueryIdTo(
579674
additionalClickHouseDBParams == null
580675
? new EnumMap<ClickHouseQueryParam, String>(ClickHouseQueryParam.class)
581676
: additionalClickHouseDBParams);
582677

583-
boolean ignoreDatabase = sql.trim().regionMatches(true, 0, databaseKeyword, 0, databaseKeyword.length());
584678
URI uri;
585679
if (externalData == null || externalData.isEmpty()) {
586680
uri = buildRequestUri(

0 commit comments

Comments
 (0)