From 9cf4c73e7b4ccad03b4aefd17bd9bd5af7760fe9 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Sun, 7 Feb 2021 21:30:10 +0800 Subject: [PATCH 1/5] Add JavaCC based new parser --- .gitignore | 9 + pom.xml | 22 + .../ClickHousePreparedStatementImpl.java | 24 +- .../clickhouse/ClickHouseStatementImpl.java | 53 +- .../clickhouse/PreparedStatementParser.java | 43 +- .../jdbc/parser/ClickHouseSqlStatement.java | 237 +++++ .../jdbc/parser/ClickHouseSqlUtils.java | 73 ++ .../clickhouse/jdbc/parser/LanguageType.java | 9 + .../clickhouse/jdbc/parser/OperationType.java | 5 + .../clickhouse/jdbc/parser/ParseHandler.java | 25 + .../clickhouse/jdbc/parser/StatementType.java | 47 + .../ClickHouseConnectionSettings.java | 5 +- .../settings/ClickHouseProperties.java | 17 + src/main/javacc/ClickHouseSqlParser.jj | 940 ++++++++++++++++++ .../jdbc/parser/ClickHouseSqlParserTest.java | 541 ++++++++++ .../jdbc/parser/ClickHouseSqlUtilsTest.java | 66 ++ .../sqls/simple-query-from-telegram.sql | 36 + 17 files changed, 2121 insertions(+), 31 deletions(-) create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtils.java create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/LanguageType.java create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/OperationType.java create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java create mode 100644 src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java create mode 100644 src/main/javacc/ClickHouseSqlParser.jj create mode 100644 src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java create mode 100644 src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtilsTest.java create mode 100644 src/test/resources/sqls/simple-query-from-telegram.sql diff --git a/.gitignore b/.gitignore index f85c8c0da..a3764496c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,12 @@ out/ .antlr/ log/ target/ + +# Generated files +src/main/java/ru/yandex/clickhouse/jdbc/parser/*CharStream.java +src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParser*.java +src/main/java/ru/yandex/clickhouse/jdbc/parser/Token*.java +src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseException.java + +# Shell scripts +*.sh diff --git a/pom.xml b/pom.xml index 2efed71a0..99d86ee8f 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ 3.0.0-M1 1.6.8 1.6 + 4.1.4 3.8.1 3.2.1 3.2.0 @@ -288,6 +289,27 @@ false + + com.helger.maven + ph-javacc-maven-plugin + ${javacc-plugin.version} + + + jjc + generate-sources + + javacc + + + ${jdk.version} + true + ru.yandex.clickhouse.jdbc.parser + src/main/javacc + src/main/java + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java index bd4cf4cb6..beef85d8b 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java @@ -36,6 +36,9 @@ import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; +import ru.yandex.clickhouse.jdbc.parser.StatementType; import ru.yandex.clickhouse.response.ClickHouseResponse; import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.settings.ClickHouseQueryParam; @@ -43,7 +46,6 @@ import ru.yandex.clickhouse.util.ClickHouseValueFormatter; import ru.yandex.clickhouse.util.guava.StreamUtils; - public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl implements ClickHousePreparedStatement { static final String PARAM_MARKER = "?"; @@ -53,6 +55,7 @@ public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl imp private final TimeZone dateTimeZone; private final TimeZone dateTimeTimeZone; + private final ClickHouseSqlStatement parsedSql; private final String sql; private final List sqlParts; private final ClickHousePreparedStatementParameter[] binds; @@ -65,6 +68,7 @@ public ClickHousePreparedStatementImpl(CloseableHttpClient client, TimeZone serverTimeZone, int resultSetType) throws SQLException { super(client, connection, properties, resultSetType); + this.parsedSql = ClickHouseSqlParser.parseSingleStatement(sql, properties); this.sql = sql; PreparedStatementParser parser = PreparedStatementParser.parse(sql); this.parameterList = parser.getParameters(); @@ -347,14 +351,22 @@ public int[] executeBatch() throws SQLException { @Override public int[] executeBatch(Map additionalDBParams) throws SQLException { - Matcher matcher = VALUES.matcher(sql); - if (!matcher.find()) { + int valuePosition = -1; + if (parsedSql.getStatementType() == StatementType.INSERT && parsedSql.hasValues()) { + valuePosition = parsedSql.getStartPosition(ClickHouseSqlStatement.KEYWORD_VALUES); + } else { + Matcher matcher = VALUES.matcher(sql); + if (matcher.find()) { + valuePosition = matcher.start(); + } + } + + if (valuePosition < 0) { throw new SQLSyntaxErrorException( "Query must be like 'INSERT INTO [db.]table [(c1, c2, c3)] VALUES (?, ?, ?)'. " + "Got: " + sql ); } - int valuePosition = matcher.start(); String insertSql = sql.substring(0, valuePosition); BatchHttpEntity entity = new BatchHttpEntity(batchRows); sendStream(entity, insertSql, additionalDBParams); @@ -429,7 +441,9 @@ public ResultSetMetaData getMetaData() throws SQLException { if (currentResult != null) { return currentResult.getMetaData(); } - if (!isSelect(sql)) { + + ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); + if ((parsedStmt.isRecognized() && !parsedStmt.isQuery()) || !isSelect(sql)) { return null; } ResultSet myRs = executeQuery(Collections.singletonMap( diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java index 89c10a556..4f9578399 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java @@ -11,6 +11,7 @@ import java.sql.SQLWarning; import java.util.ArrayList; import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -38,6 +39,9 @@ import ru.yandex.clickhouse.domain.ClickHouseFormat; import ru.yandex.clickhouse.except.ClickHouseException; import ru.yandex.clickhouse.except.ClickHouseExceptionSpecifier; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; +import ru.yandex.clickhouse.jdbc.parser.StatementType; import ru.yandex.clickhouse.response.ClickHouseLZ4Stream; import ru.yandex.clickhouse.response.ClickHouseResponse; import ru.yandex.clickhouse.response.ClickHouseResponseSummary; @@ -91,10 +95,11 @@ public class ClickHouseStatementImpl extends ConfigurableApi positions = new HashMap<>(); + String dbName = extractDBName(sql); + String tableName = extractTableName(sql); + if (extractWithTotals(sql)) { + positions.put(ClickHouseSqlStatement.KEYWORD_TOTALS, 1); + } + parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT, + null, dbName, tableName, null, null, positions); + } currentUpdateCount = -1; currentResult = createResultSet(properties.isCompress() ? new ClickHouseLZ4Stream(is) : is, properties.getBufferSize(), - extractDBName(sql), - extractTableName(sql), - extractWithTotals(sql), + parsedStmt.getDatabaseOrDefault(properties.getDatabase()), + parsedStmt.getTable(), + parsedStmt.hasWithTotals(), this, getConnection().getTimeZone(), properties @@ -177,7 +193,7 @@ public ClickHouseResponse executeQueryClickhouseResponse(String sql, Map additionalDBParams, Map additionalRequestParams) throws SQLException { InputStream is = getInputStream( - addFormatIfAbsent(sql, ClickHouseFormat.JSONCompact), + addFormatIfAbsent(sql, properties, ClickHouseFormat.JSONCompact), additionalDBParams, null, additionalRequestParams @@ -207,13 +223,14 @@ public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(Stri @Override public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(String sql, Map additionalDBParams, Map additionalRequestParams) throws SQLException { InputStream is = getInputStream( - addFormatIfAbsent(sql, ClickHouseFormat.RowBinary), + addFormatIfAbsent(sql, properties, ClickHouseFormat.RowBinary), additionalDBParams, null, additionalRequestParams ); + ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); try { - if (isSelect(sql)) { + if (parsedStmt.isQuery() || (!parsedStmt.isRecognized() && isSelect(sql))) { currentUpdateCount = -1; currentRowBinaryResult = new ClickHouseRowBinaryInputStream(properties.isCompress() ? new ClickHouseLZ4Stream(is) : is, getConnection().getTimeZone(), properties); @@ -245,8 +262,7 @@ public int executeUpdate(String sql) throws SQLException { @Override public boolean execute(String sql) throws SQLException { // currentResult is stored here. InputString and currentResult will be closed on this.close() - executeQuery(sql); - return isSelect(sql); + return executeQuery(sql) != null; } @Override @@ -471,20 +487,26 @@ public ClickHouseResponseSummary getResponseSummary() { return currentSummary; } + @Deprecated static String clickhousifySql(String sql) { - return addFormatIfAbsent(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes); + return clickhousifySql(sql, null); + } + + static String clickhousifySql(String sql, ClickHouseProperties properties) { + return addFormatIfAbsent(sql, properties, ClickHouseFormat.TabSeparatedWithNamesAndTypes); } /** * Adding FORMAT TabSeparatedWithNamesAndTypes if not added * adds format only to select queries */ - private static String addFormatIfAbsent(final String sql, ClickHouseFormat format) { + private static String addFormatIfAbsent(final String sql, ClickHouseProperties properties, ClickHouseFormat format) { String cleanSQL = sql.trim(); - if (!isSelect(cleanSQL)) { + ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(cleanSQL, properties); + if ((parsedStmt.isRecognized() && !parsedStmt.isQuery()) || !isSelect(cleanSQL)) { return cleanSQL; } - if (ClickHouseFormat.containsFormat(cleanSQL)) { + if ((parsedStmt.isRecognized() && parsedStmt.hasFormat()) || ClickHouseFormat.containsFormat(cleanSQL)) { return cleanSQL; } StringBuilder sb = new StringBuilder(); @@ -498,6 +520,7 @@ private static String addFormatIfAbsent(final String sql, ClickHouseFormat forma return sb.toString(); } + @Deprecated static boolean isSelect(String sql) { for (int i = 0; i < sql.length(); i++) { String nextTwo = sql.substring(i, Math.min(i + 2, sql.length())); @@ -572,7 +595,7 @@ private InputStream getInputStream( List externalData, Map additionalRequestParams ) throws ClickHouseException { - sql = clickhousifySql(sql); + sql = clickhousifySql(sql, properties); log.debug("Executing SQL: {}", sql); additionalClickHouseDBParams = addQueryIdTo( diff --git a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java index 8225b5456..b3723848e 100644 --- a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java +++ b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java @@ -6,6 +6,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser; +import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; +import ru.yandex.clickhouse.jdbc.parser.StatementType; +import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.util.apache.StringUtils; /** @@ -30,12 +34,17 @@ private PreparedStatementParser() { valuesMode = false; } + @Deprecated static PreparedStatementParser parse(String sql) { + return parse(sql, null); + } + + static PreparedStatementParser parse(String sql, ClickHouseProperties properties) { if (StringUtils.isBlank(sql)) { throw new IllegalArgumentException("SQL may not be blank"); } PreparedStatementParser parser = new PreparedStatementParser(); - parser.parseSQL(sql); + parser.parseSQL(sql, properties); return parser; } @@ -57,7 +66,7 @@ private void reset() { valuesMode = false; } - private void parseSQL(String sql) { + private void parseSQL(String sql, ClickHouseProperties properties) { reset(); List currentParamList = new ArrayList(); boolean afterBackSlash = false; @@ -66,21 +75,35 @@ private void parseSQL(String sql) { boolean inSingleLineComment = false; boolean inMultiLineComment = false; boolean whiteSpace = false; - Matcher matcher = VALUES.matcher(sql); - if (matcher.find()) { - valuesMode = true; + ClickHouseSqlStatement parsedSql = ClickHouseSqlParser.parseSingleStatement(sql, properties); + int endPosition = 0; + if (parsedSql.getStatementType() == StatementType.INSERT) { + endPosition = parsedSql.getEndPosition(ClickHouseSqlStatement.KEYWORD_VALUES) - 1; + if (endPosition > 0) { + valuesMode = true; + } else { + endPosition = 0; + } + } else { + Matcher matcher = VALUES.matcher(sql); + if (matcher.find()) { + valuesMode = true; + endPosition = matcher.end() - 1; + } } + int currentParensLevel = 0; int quotedStart = 0; int partStart = 0; - for (int i = valuesMode ? matcher.end() - 1 : 0, idxStart = i, idxEnd = i ; i < sql.length(); i++) { + int sqlLength = sql.length(); + for (int i = valuesMode ? endPosition : 0, idxStart = i, idxEnd = i ; i < sqlLength; i++) { char c = sql.charAt(i); if (inSingleLineComment) { if (c == '\n') { inSingleLineComment = false; } } else if (inMultiLineComment) { - if (c == '*' && sql.length() > i + 1 && sql.charAt(i + 1) == '/') { + if (c == '*' && sqlLength > i + 1 && sql.charAt(i + 1) == '/') { inMultiLineComment = false; i++; } @@ -109,10 +132,10 @@ private void parseSQL(String sql) { partStart = i + 1; currentParamList.add(ClickHousePreparedStatementImpl.PARAM_MARKER); } - } else if (c == '-' && sql.length() > i + 1 && sql.charAt(i + 1) == '-') { + } else if (c == '-' && sqlLength > i + 1 && sql.charAt(i + 1) == '-') { inSingleLineComment = true; i++; - } else if (c == '/' && sql.length() > i + 1 && sql.charAt(i + 1) == '*') { + } else if (c == '/' && sqlLength > i + 1 && sql.charAt(i + 1) == '*') { inMultiLineComment = true; i++; } else if (c == ',') { @@ -158,7 +181,7 @@ private void parseSQL(String sql) { if (!valuesMode && !currentParamList.isEmpty()) { parameters.add(currentParamList); } - String lastPart = sql.substring(partStart, sql.length()); + String lastPart = sql.substring(partStart, sqlLength); parts.add(lastPart); } diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java new file mode 100644 index 000000000..846bd47c8 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java @@ -0,0 +1,237 @@ +package ru.yandex.clickhouse.jdbc.parser; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class ClickHouseSqlStatement { + public static final String DEFAULT_DATABASE = "system"; + public static final String DEFAULT_TABLE = "unknown"; + public static final List DEFAULT_PARAMETERS = Collections.emptyList(); + public static final Map DEFAULT_POSITIONS = Collections.emptyMap(); + public static final Map DEFAULT_VARIABLES = Collections.emptyMap(); + + public static final String KEYWORD_FORMAT = "FORMAT"; + public static final String KEYWORD_TOTALS = "TOTALS"; + public static final String KEYWORD_VALUES = "VALUES"; + + private final String sql; + private final StatementType stmtType; + private final String cluster; + private final String database; + private final String table; + private final String format; + private final String outfile; + private final Map positions; + + public ClickHouseSqlStatement(String sql) { + this(sql, StatementType.UNKNOWN, null, null, null, null, null, null); + } + + public ClickHouseSqlStatement(String sql, StatementType stmtType) { + this(sql, stmtType, null, null, null, null, null, null); + } + + public ClickHouseSqlStatement(String sql, StatementType stmtType, String cluster, String database, String table, + String format, String outfile, Map positions) { + this.sql = sql; + this.stmtType = stmtType; + + this.cluster = cluster; + this.database = database; + this.table = table == null || table.isEmpty() ? DEFAULT_TABLE : table; + this.format = format; + this.outfile = outfile; + + if (positions != null && positions.size() > 0) { + Map p = new HashMap<>(); + for (Entry e : positions.entrySet()) { + String keyword = e.getKey(); + Integer position = e.getValue(); + + if (keyword != null && position != null) { + p.put(keyword.toUpperCase(), position); + } + } + this.positions = Collections.unmodifiableMap(p); + } else { + this.positions = DEFAULT_POSITIONS; + } + } + + public String getSQL() { + return this.sql; + } + + public boolean isRecognized() { + return stmtType != StatementType.UNKNOWN; + } + + public boolean isDDL() { + return this.stmtType.getLanguageType() == LanguageType.DDL; + } + + public boolean isDML() { + return this.stmtType.getLanguageType() == LanguageType.DML; + } + + public boolean isQuery() { + return this.stmtType.getOperationType() == OperationType.READ && !this.hasOutfile(); + } + + public boolean isMutation() { + return this.stmtType.getOperationType() == OperationType.WRITE || this.hasOutfile(); + } + + public boolean isIdemponent() { + return this.isQuery(); + } + + public LanguageType getLanguageType() { + return this.stmtType.getLanguageType(); + } + + public OperationType getOperationType() { + return this.stmtType.getOperationType(); + } + + public StatementType getStatementType() { + return this.stmtType; + } + + public String getCluster() { + return this.cluster; + } + + public String getDatabase() { + return this.database; + } + + public String getDatabaseOrDefault(String database) { + return this.database == null ? (database == null ? DEFAULT_DATABASE : database) : this.database; + } + + public String getTable() { + return this.table; + } + + public String getFormat() { + return this.format; + } + + public String getOutfile() { + return this.outfile; + } + + public boolean hasFormat() { + return this.format != null && !this.format.isEmpty(); + } + + public boolean hasOutfile() { + return this.outfile != null && !this.outfile.isEmpty(); + } + + public boolean hasWithTotals() { + return this.positions.containsKey(KEYWORD_TOTALS); + } + + public boolean hasValues() { + return this.positions.containsKey(KEYWORD_VALUES); + } + + public int getStartPosition(String keyword) { + int position = -1; + + if (!this.positions.isEmpty() && keyword != null) { + Integer p = this.positions.get(keyword.toUpperCase()); + if (p != null) { + position = p.intValue(); + } + } + + return position; + } + + public int getEndPosition(String keyword) { + int position = getStartPosition(keyword); + + return position != -1 && keyword != null ? position + keyword.length() : position; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append('[').append(stmtType.name()).append(']').append(" cluster=").append(cluster).append(", database=") + .append(database).append(", table=").append(table).append(", format=").append(format) + .append(", positions=").append(positions).append("\nSQL:\n").append(sql); + + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((cluster == null) ? 0 : cluster.hashCode()); + result = prime * result + ((database == null) ? 0 : database.hashCode()); + result = prime * result + ((format == null) ? 0 : format.hashCode()); + result = prime * result + ((outfile == null) ? 0 : outfile.hashCode()); + result = prime * result + ((positions == null) ? 0 : positions.hashCode()); + result = prime * result + ((sql == null) ? 0 : sql.hashCode()); + result = prime * result + ((stmtType == null) ? 0 : stmtType.hashCode()); + result = prime * result + ((table == null) ? 0 : table.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClickHouseSqlStatement other = (ClickHouseSqlStatement) obj; + if (cluster == null) { + if (other.cluster != null) + return false; + } else if (!cluster.equals(other.cluster)) + return false; + if (database == null) { + if (other.database != null) + return false; + } else if (!database.equals(other.database)) + return false; + if (format == null) { + if (other.format != null) + return false; + } else if (!format.equals(other.format)) + return false; + if (outfile == null) { + if (other.outfile != null) + return false; + } else if (!outfile.equals(other.outfile)) + return false; + if (positions == null) { + if (other.positions != null) + return false; + } else if (!positions.equals(other.positions)) + return false; + if (sql == null) { + if (other.sql != null) + return false; + } else if (!sql.equals(other.sql)) + return false; + if (stmtType != other.stmtType) + return false; + if (table == null) { + if (other.table != null) + return false; + } else if (!table.equals(other.table)) + return false; + return true; + } +} diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtils.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtils.java new file mode 100644 index 000000000..a7be146de --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtils.java @@ -0,0 +1,73 @@ +package ru.yandex.clickhouse.jdbc.parser; + +public final class ClickHouseSqlUtils { + public static boolean isQuote(char ch) { + return ch == '"' || ch == '\'' || ch == '`'; + } + + /** + * Escape quotes in given string. + * + * @param str string + * @param quote quote to escape + * @return escaped string + */ + public static String escape(String str, char quote) { + if (str == null) { + return str; + } + + int len = str.length(); + StringBuilder sb = new StringBuilder(len + 10).append(quote); + + for (int i = 0; i < len; i++) { + char ch = str.charAt(i); + if (ch == quote || ch == '\\') { + sb.append('\\'); + } + sb.append(ch); + } + + return sb.append(quote).toString(); + } + + /** + * Unescape quoted string. + * + * @param str quoted string + * @return unescaped string + */ + public static String unescape(String str) { + if (str == null || str.isEmpty()) { + return str; + } + + int len = str.length(); + char quote = str.charAt(0); + if (!isQuote(quote) || quote != str.charAt(len - 1)) { // not a quoted string + return str; + } + + StringBuilder sb = new StringBuilder(len = len - 1); + for (int i = 1; i < len; i++) { + char ch = str.charAt(i); + + if (++i >= len) { + sb.append(ch); + } else { + char nextChar = str.charAt(i); + if (ch == '\\' || (ch == quote && nextChar == quote)) { + sb.append(nextChar); + } else { + sb.append(ch); + i--; + } + } + } + + return sb.toString(); + } + + private ClickHouseSqlUtils() { + } +} diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/LanguageType.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/LanguageType.java new file mode 100644 index 000000000..c3fa2cfa6 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/LanguageType.java @@ -0,0 +1,9 @@ +package ru.yandex.clickhouse.jdbc.parser; + +public enum LanguageType { + UNKNOWN, // unknown language + DCL, // data control language + DDL, // data definition language + DML, // data manipulation language + TCL // transaction control language +} diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/OperationType.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/OperationType.java new file mode 100644 index 000000000..4c5a2222f --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/OperationType.java @@ -0,0 +1,5 @@ +package ru.yandex.clickhouse.jdbc.parser; + +public enum OperationType { + UNKNOWN, READ, WRITE +} diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java new file mode 100644 index 000000000..985128cf8 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java @@ -0,0 +1,25 @@ +package ru.yandex.clickhouse.jdbc.parser; + +import java.util.List; + +public interface ParseHandler { + /** + * Handle macro like "#include('/tmp/template.sql')". + * + * @param name name of the macro + * @param parameters parameters + * @return output of the macro, could be null or empty string + */ + String handleMacro(String name, List parameters); + + /** + * Handle parameter. + * + * @param String cluster cluster + * @param String database database + * @param String table table + * @param int index(starts from 1 not 0) + * @return parameter value + */ + String handleParameter(String cluster, String database, String table, int columnIndex); +} diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java new file mode 100644 index 000000000..0a58e0c38 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java @@ -0,0 +1,47 @@ +package ru.yandex.clickhouse.jdbc.parser; + +public enum StatementType { + UNKNOWN(LanguageType.UNKNOWN, OperationType.UNKNOWN), // unknown statement + ALTER(LanguageType.DDL, OperationType.UNKNOWN), // alter statement + ALTER_DELETE(LanguageType.DDL, OperationType.WRITE), // delete statement + ALTER_UPDATE(LanguageType.DDL, OperationType.WRITE), // update statement + ATTACH(LanguageType.DDL, OperationType.UNKNOWN), // attach statement + CHECK(LanguageType.DDL, OperationType.UNKNOWN), // check statement + CREATE(LanguageType.DDL, OperationType.UNKNOWN), // create statement + DELETE(LanguageType.DML, OperationType.WRITE), // the upcoming light-weight delete statement + DESCRIBE(LanguageType.DDL, OperationType.READ), // describe/desc statement + DETACH(LanguageType.DDL, OperationType.UNKNOWN), // detach statement + DROP(LanguageType.DDL, OperationType.UNKNOWN), // drop statement + EXISTS(LanguageType.DML, OperationType.READ), // exists statement + EXPLAIN(LanguageType.DDL, OperationType.READ), // explain statement + GRANT(LanguageType.DCL, OperationType.UNKNOWN), // grant statement + INSERT(LanguageType.DML, OperationType.WRITE), // insert statement + KILL(LanguageType.DCL, OperationType.UNKNOWN), // kill statement + OPTIMIZE(LanguageType.DDL, OperationType.UNKNOWN), // optimize statement + RENAME(LanguageType.DDL, OperationType.UNKNOWN), // rename statement + REVOKE(LanguageType.DCL, OperationType.UNKNOWN), // revoke statement + SELECT(LanguageType.DML, OperationType.READ), // select statement + SET(LanguageType.DCL, OperationType.UNKNOWN), // set statement + SHOW(LanguageType.DDL, OperationType.READ), // show statement + SYSTEM(LanguageType.DDL, OperationType.UNKNOWN), // system statement + TRUNCATE(LanguageType.DDL, OperationType.UNKNOWN), // truncate statement + UPDATE(LanguageType.DML, OperationType.WRITE), // the upcoming light-weight update statement + USE(LanguageType.DDL, OperationType.UNKNOWN), // use statement + WATCH(LanguageType.DDL, OperationType.UNKNOWN); // watch statement + + private LanguageType langType; + private OperationType opType; + + StatementType(LanguageType langType, OperationType operationType) { + this.langType = langType; + this.opType = operationType; + } + + LanguageType getLanguageType() { + return this.langType; + } + + OperationType getOperationType() { + return this.opType; + } +} diff --git a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java index 202574fed..1d7e17b32 100644 --- a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java +++ b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java @@ -27,6 +27,7 @@ public enum ClickHouseConnectionSettings implements DriverPropertyCreator { + " ClickHouse rejects request execution if its time exceeds max_execution_time"), + @Deprecated KEEP_ALIVE_TIMEOUT("keepAliveTimeout", 30 * 1000, ""), /** @@ -47,7 +48,9 @@ public enum ClickHouseConnectionSettings implements DriverPropertyCreator { USE_SERVER_TIME_ZONE_FOR_DATES("use_server_time_zone_for_dates", false, "Whether to use timezone from server on Date parsing in getDate(). " + "If false, Date returned is a wrapper of a timestamp at start of the day in client timezone. " + - "If true - at start of the day in server or use_timezone timezone.") + "If true - at start of the day in server or use_timezone timezone."), + @Deprecated + USE_NEW_PARSER("use_new_parser", true, "Whether to use JavaCC based SQL parser or not.") ; private final String key; diff --git a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java index b368917e9..22cef5986 100644 --- a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java +++ b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java @@ -96,6 +96,8 @@ public class ClickHouseProperties { private Boolean anyJoinDistinctRightTableKeys; private Boolean sendProgressInHttpHeaders; private Boolean waitEndOfQuery; + @Deprecated + private boolean useNewParser; public ClickHouseProperties() { this(new Properties()); @@ -125,6 +127,7 @@ public ClickHouseProperties(Properties info) { this.useTimeZone = (String)getSetting(info, ClickHouseConnectionSettings.USE_TIME_ZONE); this.useServerTimeZoneForDates = (Boolean)getSetting(info, ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE_FOR_DATES); this.useObjectsInArrays = (Boolean)getSetting(info, ClickHouseConnectionSettings.USE_OBJECTS_IN_ARRAYS); + this.useNewParser = (Boolean)getSetting(info, ClickHouseConnectionSettings.USE_NEW_PARSER); this.maxParallelReplicas = getSetting(info, ClickHouseQueryParam.MAX_PARALLEL_REPLICAS); this.maxPartitionsPerInsertBlock = getSetting(info, ClickHouseQueryParam.MAX_PARTITIONS_PER_INSERT_BLOCK); @@ -191,6 +194,7 @@ public Properties asProperties() { ret.put(ClickHouseConnectionSettings.USE_TIME_ZONE.getKey(), String.valueOf(useTimeZone)); ret.put(ClickHouseConnectionSettings.USE_SERVER_TIME_ZONE_FOR_DATES.getKey(), String.valueOf(useServerTimeZoneForDates)); ret.put(ClickHouseConnectionSettings.USE_OBJECTS_IN_ARRAYS.getKey(), String.valueOf(useObjectsInArrays)); + ret.put(ClickHouseConnectionSettings.USE_NEW_PARSER.getKey(), String.valueOf(useNewParser)); ret.put(ClickHouseQueryParam.MAX_PARALLEL_REPLICAS.getKey(), maxParallelReplicas); ret.put(ClickHouseQueryParam.MAX_PARTITIONS_PER_INSERT_BLOCK.getKey(), maxPartitionsPerInsertBlock); @@ -260,6 +264,7 @@ public ClickHouseProperties(ClickHouseProperties properties) { setUseTimeZone(properties.useTimeZone); setUseServerTimeZoneForDates(properties.useServerTimeZoneForDates); setUseObjectsInArrays(properties.useObjectsInArrays); + setUseNewParser(properties.useNewParser); setMaxParallelReplicas(properties.maxParallelReplicas); setMaxPartitionsPerInsertBlock(properties.maxPartitionsPerInsertBlock); setTotalsMode(properties.totalsMode); @@ -554,10 +559,12 @@ public void setDataTransferTimeout(int dataTransferTimeout) { this.dataTransferTimeout = dataTransferTimeout; } + @Deprecated public int getKeepAliveTimeout() { return keepAliveTimeout; } + @Deprecated public void setKeepAliveTimeout(int keepAliveTimeout) { this.keepAliveTimeout = keepAliveTimeout; } @@ -665,6 +672,16 @@ public void setUseObjectsInArrays(boolean useObjectsInArrays) { this.useObjectsInArrays = useObjectsInArrays; } + @Deprecated + public boolean isUseNewParser() { + return useNewParser; + } + + @Deprecated + public void setUseNewParser(boolean useNewParser) { + this.useNewParser = useNewParser; + } + public boolean isUseServerTimeZoneForDates() { return useServerTimeZoneForDates; } diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj new file mode 100644 index 000000000..0e3151315 --- /dev/null +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -0,0 +1,940 @@ +/** + * This ugly grammar defines a loose parser for ClickHouse. It cannot be used to validate SQL + * on behalf of server, but only for the following purposes: + * 1) split given SQL into multiple statements + * 2) recognize type of each statement(DDL/DML/DCL/TCL, query or mutation etc.) + * 3) extract cluster, database, table, format, outfile, macros and parameters from a statement + * 4) check if specific keywords like "WITH TOTALS" or so exist in the statement or not + * + * The ANTLR4 grammar at https://github.com/ClickHouse/ClickHouse/blob/master/src/Parsers/New is incomplete. + * Also using it will introduce 300KB runtime and we'll have to deal with many parsing errors, + * which is too much for a JDBC driver. On the other hand, if we write a parser from scratch, + * we'll end up with one like Druid, which is more complex the JDBC driver itself. + * + * JavaCC is something in the middle that fits our need - no runtime and easy to maintain/extend. + */ +options { + // DEBUG_LOOKAHEAD = true; + // DEBUG_PARSER = true; + // DEBUG_TOKEN_MANAGER = true; + + ERROR_REPORTING = false; + UNICODE_INPUT = true; + COMMON_TOKEN_ACTION = true; +} + +PARSER_BEGIN(ClickHouseSqlParser) + +package ru.yandex.clickhouse.jdbc.parser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ru.yandex.clickhouse.settings.ClickHouseProperties; + +public class ClickHouseSqlParser { + private static final boolean DEBUG = false; + + private static final Logger log = LoggerFactory.getLogger(ClickHouseSqlParser.class); + + private final List statements = new ArrayList<>(); + + private ClickHouseProperties properties; + private ParseHandler handler; + + private boolean tokenIn(int tokenIndex, int... tokens) { + boolean matched = false; + + int t = getToken(tokenIndex).kind; + if (tokens != null) { + for (int i : tokens) { + if (t == i) { + matched = true; + break; + } + } + } + + return matched; + } + + /** + * @deprecated This method will be removed in the near future. + *

+ * Use {@link Utils#parse(String)} instead. + */ + public static ClickHouseSqlStatement parseSingleStatement(String sql, ClickHouseProperties properties) { + return parseSingleStatement(sql, properties, null); + } + + /** + * Parse given SQL. + * + * @deprecated This method will be removed in the near future. + *

+ * Use {@link Utils#parse(String, ParseHandler)} instead. + * + * @param sql SQL query + * @param properties properties + * @param handler parse handler + * @return parsed SQL statement + */ + public static ClickHouseSqlStatement parseSingleStatement( + String sql, ClickHouseProperties properties, ParseHandler handler) { + ClickHouseSqlStatement[] stmts = parse(sql, properties, handler); + + return stmts.length == 1 ? stmts[0] : new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null); + } + + @Deprecated + public static ClickHouseSqlStatement[] parse(String sql) { + return parse(sql, null, null); + } + + public static ClickHouseSqlStatement[] parse(String sql, ClickHouseProperties properties) { + return parse(sql, properties, null); + } + + public static ClickHouseSqlStatement[] parse(String sql, ClickHouseProperties properties, ParseHandler handler) { + if (properties == null) { + properties = new ClickHouseProperties(); + } + + ClickHouseSqlStatement[] stmts = new ClickHouseSqlStatement[0]; + + if (sql == null || sql.isEmpty()) { + return stmts; + } + + if (!properties.isUseNewParser()) { + return new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) + }; + } + + ClickHouseSqlParser p = new ClickHouseSqlParser(sql, properties, handler); + try { + p.sql(); + List list = p.statements; + stmts = list.toArray(new ClickHouseSqlStatement[list.size()]); + } catch (Exception e) { + if (DEBUG) { + throw new IllegalArgumentException(e); + } else { + log.warn("Failed to parse the given SQL. If you believe the SQL is valid, please feel free to open an issue on Github with the following SQL and exception attached.\n{}", sql, e); + stmts = new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }; + } + } + + return stmts; + } + + public ClickHouseSqlParser(String sql, ClickHouseProperties properties, ParseHandler handler) { + this(new StringReader(sql)); + + this.properties = properties; + this.handler = handler; + } + + public void addStatement() { + if (token_source.isValid()) { + statements.add(token_source.build()); + } else { + token_source.reset(); + } + } +} + +PARSER_END(ClickHouseSqlParser) + +TOKEN_MGR_DECLS: { + private int validTokens = 0; // whitespaces and comments are invalid + + final StringBuilder builder = new StringBuilder(); + + StatementType stmtType = StatementType.UNKNOWN; + String cluster = null; + String database = null; + String table = null; + String format = null; + String outfile = null; + + final List parameters = new ArrayList<>(); + final Map positions = new HashMap<>(); + + public void CommonTokenAction(Token t) { + if (t.kind != ClickHouseSqlParserConstants.SEMICOLON) { + builder.append(t.image); + + if (t.kind != ClickHouseSqlParserConstants.EOF) { + validTokens++; + } + } + } + + void processMacro(String name, List params, ParseHandler handler) { + StringBuilder m = new StringBuilder(); + m.append('#').append(name); + + int startPos = builder.lastIndexOf(m.toString()); + int endPos = params.size() > 0 ? builder.indexOf(")", startPos) + 1 : startPos + m.length(); + + builder.delete(startPos, endPos); + if (handler != null) { + String replacement = handler.handleMacro(name, params); + if (replacement != null && !replacement.isEmpty()) { + builder.insert(startPos, replacement); + } + } + } + + void processParameter(String str, ParseHandler handler) { + int pos = builder.lastIndexOf(str); + parameters.add(pos); + + if (handler != null) { + String replacement = handler.handleParameter(cluster, database, table, parameters.size()); + if (replacement != null && !replacement.isEmpty()) { + builder.deleteCharAt(pos); + builder.insert(pos, replacement); + } + } + } + + void append(StringBuilder str) { + builder.append(str.toString()); + } + + void reset() { + builder.setLength(validTokens = 0); + + stmtType = StatementType.UNKNOWN; + cluster = null; + database = null; + table = null; + format = null; + outfile = null; + parameters.clear(); + positions.clear(); + } + + ClickHouseSqlStatement build() { + ClickHouseSqlStatement s = new ClickHouseSqlStatement( + builder.toString(), stmtType, cluster, database, table, format, outfile, positions); + + // reset variables + reset(); + + return s; + } + + boolean isValid() { + return validTokens > 0; + } + + void setPosition(String keyword) { + if (keyword == null || keyword.isEmpty()) { + return; + } + + this.positions.put(keyword, builder.lastIndexOf(keyword)); + } +} + +SKIP: { + + { append(image); } +} + +SPECIAL_TOKEN: { + + { append(image); } + | + { append(image); } +} + +// top-level statements +ClickHouseSqlStatement[] sql(): {} { + stmts() + { addStatement(); } + ( + (LOOKAHEAD(2) )+ + (stmts())? + { addStatement(); } + )* + + { return statements.toArray(new ClickHouseSqlStatement[statements.size()]); } +} + +void stmts(): { Token t; } { + LOOKAHEAD(2) stmt() + | LOOKAHEAD(2) anyExprList() // in case there's anything new +} + +void stmt(): {} { + alterStmt() { if (token_source.stmtType == StatementType.UNKNOWN) token_source.stmtType = StatementType.ALTER; } + | attachStmt() { token_source.stmtType = StatementType.ATTACH; } + | checkStmt() { token_source.stmtType = StatementType.CHECK; } + | createStmt() { token_source.stmtType = StatementType.CREATE; } + | deleteStmt() { token_source.stmtType = StatementType.DELETE; } + | describeStmt() { token_source.stmtType = StatementType.DESCRIBE; } + | detachStmt() { token_source.stmtType = StatementType.DETACH; } + | dropStmt() { token_source.stmtType = StatementType.DROP; } + | existsStmt() { token_source.stmtType = StatementType.EXISTS; } + | explainStmt() { token_source.stmtType = StatementType.EXPLAIN; } + | insertStmt() { token_source.stmtType = StatementType.INSERT; } + | grantStmt() { token_source.stmtType = StatementType.GRANT; } + | killStmt() { token_source.stmtType = StatementType.KILL; } + | optimizeStmt() { token_source.stmtType = StatementType.OPTIMIZE; } + | renameStmt() { token_source.stmtType = StatementType.RENAME; } + | revokeStmt() { token_source.stmtType = StatementType.REVOKE; } + | selectStmt() { token_source.stmtType = StatementType.SELECT; } + | setStmt() { token_source.stmtType = StatementType.SET; } + | showStmt() { token_source.stmtType = StatementType.SHOW; } + | systemStmt() { token_source.stmtType = StatementType.SYSTEM; } + | truncateStmt() { token_source.stmtType = StatementType.TRUNCATE; } + | updateStmt() { token_source.stmtType = StatementType.UPDATE; } + | useStmt() { token_source.stmtType = StatementType.USE; } + | watchStmt() { token_source.stmtType = StatementType.WATCH; } +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/alter/ +void alterStmt(): {} { + + ( + LOOKAHEAD(2) + tableIdentifier(true) (clusterClause())? ( + { token_source.stmtType = StatementType.ALTER_UPDATE; } + | { token_source.stmtType = StatementType.ALTER_DELETE; } + ) + )? anyExprList() +} + +void clusterClause(): { Token t; } { + (LOOKAHEAD(2) t = anyIdentifier() | t = ) + { token_source.cluster = ClickHouseSqlUtils.unescape(t.image); } +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/attach/ +void attachStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/check-table/ +void checkStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/create/ +void createStmt(): {} { // not interested + anyExprList() +} + +// upcoming lightweight mutation - see https://github.com/ClickHouse/ClickHouse/issues/19627 +void deleteStmt(): {} { // not interested + tableIdentifier(true) ( anyExprList())? +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/describe-table/ +void describeStmt(): {} { + ( | ) + { token_source.table = "columns"; } + (LOOKAHEAD(2)
)? + (LOOKAHEAD(2) tableIdentifier(true) | anyExprList()) +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/detach/ +void detachStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/drop/ +void dropStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/exists/ +void existsStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/explain/ +void explainStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/grant/ +void grantStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/insert-into/ +void insertStmt(): {} { + + ( + LOOKAHEAD({ getToken(1).kind == FUNCTION }) functionExpr() + | (LOOKAHEAD(2)
)? tableIdentifier(true) + ) + (LOOKAHEAD(2) columnExprList() )? + dataClause() +} + +void dataClause(): { Token t; } { + LOOKAHEAD(2) anyIdentifier() + | LOOKAHEAD(2) t = { token_source.setPosition(t.image); } + columnExprList() + ( + LOOKAHEAD(2) + ()? + columnExprList() + )* + | anyExprList() // not interested +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/kill/ +void killStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/optimize/ +void optimizeStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/rename/ +void renameStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/revoke/ +void revokeStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/select/ +void selectStmt(): {} { + // FIXME with (select 1), (select 2), 3 select * + ( (LOOKAHEAD(2) columnExprList() | anyExprList() ))? +
)? tableIdentifier(true)) + ) + ) + { token_source.database = "system"; } + (LOOKAHEAD(2) anyExprList())? +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/system/ +void systemStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/truncate/ +void truncateStmt(): {} { + (LOOKAHEAD(2) )? (LOOKAHEAD(2)
)? (LOOKAHEAD(2) )? + tableIdentifier(true) (clusterClause())? +} + +// upcoming lightweight mutation - see https://github.com/ClickHouse/ClickHouse/issues/19627 +void updateStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/use/ +void useStmt(): {} { + databaseIdentifier(true) +} + +// Experimental LIVE VIEW feature +void watchStmt(): {} { // not interested + anyExprList() +} + +// columns +void columnExprList(): {} { + columnsExpr() ( columnsExpr())* +} + +void columnsExpr(): {} { + LOOKAHEAD(allColumnsExpr()) allColumnsExpr() + ( + LOOKAHEAD(2) ( | | ) anyExprList() + )* + | nestedExpr() + ( + ( | )+ + | (LOOKAHEAD(2) anyExprList() )+ + | LOOKAHEAD(2) ()? + | LOOKAHEAD(2) ()? nestedExpr() nestedExpr() + | LOOKAHEAD(2) (LOOKAHEAD(2) ( | | operator()) nestedExpr())+ + | LOOKAHEAD(2) ()? anyExprList() + | LOOKAHEAD(2) nestedExpr() nestedExpr() + | LOOKAHEAD(2) aliasExpr() + )? +} + +void allColumnsExpr(): {} { + | anyIdentifier() (LOOKAHEAD(2) anyIdentifier() )? +} + +void nestedExpr(): {} { + LOOKAHEAD(2) ( | ) nestedExpr() + | LOOKAHEAD(2) (LOOKAHEAD(2) nestedExpr())? ( nestedExpr() nestedExpr())+ ( nestedExpr())? + | LOOKAHEAD(2) nestedExpr() interval() + | columnExpr() + ( + ( | )+ + |(LOOKAHEAD(2) anyExprList() )+ + | LOOKAHEAD(2) ()? + | LOOKAHEAD(2) ()? nestedExpr() nestedExpr() + | LOOKAHEAD(2) (LOOKAHEAD(2) ( | | operator()) nestedExpr())+ + | LOOKAHEAD(2) ()? anyExprList() + | LOOKAHEAD(2) nestedExpr() nestedExpr() + )? +} + +void functionExpr(): {} { + anyIdentifier() (anyExprList())? +} + +void columnExpr(): { Token t; } { + t = { token_source.processParameter(t.image, handler); } + | (LOOKAHEAD(2) anyExprList())? + | (LOOKAHEAD(2) anyExprList())? + | anyExprList() + | (LOOKAHEAD(2) macro())+ + | LOOKAHEAD(2, { !(tokenIn(1, INF, NAN, NULL) && tokenIn(2, DOT)) }) literal() + | LOOKAHEAD(2, { getToken(2).kind == LPAREN }) functionExpr() + | anyIdentifier() (LOOKAHEAD(2) anyIdentifier())* +} + +// interested parts +void formatPart(): { Token t; } { + (LOOKAHEAD(2) t = { token_source.format = t.image; })? +} + +void outfilePart(): { Token t; } { + (LOOKAHEAD(2) t = { token_source.outfile = t.image; })? +} + +void withTotalPart(): { Token t; } { + (LOOKAHEAD(2) t = { token_source.setPosition(t.image); })? +} + +// expressions +void anyExprList(): {} { + anyExpr() (LOOKAHEAD(2) | anyExpr())* +} + +void anyExpr(): {} { + anyNestedExpr() (LOOKAHEAD(2) ( | | operator()) anyNestedExpr())* +} + +void anyNestedExpr(): {} { + LOOKAHEAD(2) formatPart() + | LOOKAHEAD(2) withTotalPart() + | LOOKAHEAD(2) outfilePart() + | (LOOKAHEAD(2) anyColumnExpr() ( | )*)+ +} + +void anyColumnExpr(): { Token t; } { + + | t = { token_source.processParameter(t.image, handler); } + | (LOOKAHEAD(2) anyExprList())? + | (LOOKAHEAD(2) anyExprList())? + | (LOOKAHEAD(2) anyExprList())? + | (LOOKAHEAD(2) macro())+ + | LOOKAHEAD(2, { !(tokenIn(1, INF, NAN, NULL) && tokenIn(2, DOT)) }) literal() + | nestedIdentifier() +} + +Token aliasExpr(): { Token t = null; } { + ( + LOOKAHEAD(2) t = anyIdentifier() + | LOOKAHEAD(2) formatPart() + | LOOKAHEAD(2) outfilePart() + | t = identifier() + ) + { return t; } +} + +void nestedIdentifier(): {} { + anyIdentifier() (LOOKAHEAD(2) ( | anyIdentifier()))* +} + +void tableIdentifier(boolean record): { Token t; } { + ( + (LOOKAHEAD(2) databaseIdentifier(record) )? t = anyIdentifier() + (LOOKAHEAD(2) anyExprList() )? + ) + { + if (record && t != null && token_source.table == null) { + token_source.table = ClickHouseSqlUtils.unescape(t.image); + } + } +} + +void databaseIdentifier(boolean record): { Token t; } { + t = anyIdentifier() { if (record) token_source.database = ClickHouseSqlUtils.unescape(t.image); } +} + +void settingExprList(): {} { + settingExpr() ( settingExpr())* +} + +void settingExpr(): {} { + identifier() literal() +} + +// basics +Token anyIdentifier(): { Token t; } { + ( + t = + | t = + | t = variable() + | t = + | t = anyKeyword() + ) + { return t; } +} + +Token identifier(): { Token t; } { + ( + t = + | t = + | t = variable() + | t = + | t = keyword() + ) + { return t; } +} + +void interval(): {} { + | | | | | | | +} + +Token literal(): { Token t; } { + ( + t = dateLiteral() + | t = numberLiteral() + | t = + | t = + ) + { return t; } +} + +Token dateLiteral(): { Token t; String prefix; } { + (t = | t = ) { prefix = t.image; } + t = + { return Token.newToken(0, prefix + " " + t.image); } +} + +Token numberLiteral(): { Token t = null; StringBuilder sb = new StringBuilder(); } { + (t = | t = )? { if (t != null) sb.append(t.image); } + ( + LOOKAHEAD(2) + t = | t = | t = | t = | t = + ) + { return Token.newToken(0, sb.append(t.image).toString()); } +} + +void operator(): {} { + ( | | | | | + | | | | | | | | ) +} + +void macro(): { + Token t; + String name; + List params = new ArrayList<>(); +} { + ( + + (t = anyKeyword() | t = ) { name = t.image; } + ( + LOOKAHEAD(2) + t = { params.add(ClickHouseSqlUtils.unescape(t.image)); } + ( t = { params.add(ClickHouseSqlUtils.unescape(t.image)); })* + + )? + ) + { token_source.processMacro(name, params, handler); } +} + +Token variable(): { Token t; } { + ( (t = anyKeyword() | t = )) + { + return Token.newToken(0, "@@" + t.image); + } +} + +Token anyKeyword(): { Token t; } { + ( + // leading keywords(except with) + t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t =
| t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + // interval + | t = | t = | t = | t = | t = | t = | t = | t = + // values + | t = | t = | t = + ) + { return t; } +} + +Token keyword(): { Token t; } { + ( + // leading keywords(except with) + t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t =
| t = | t = + | t = | t = | t = | t = | t = | t = + // interval + | t = | t = | t = | t = | t = | t = | t = | t = + // values + | t = | t = | t = + ) + { return t; } +} + +// keywords +TOKEN: { + > + | > + | > + | > + | > + | > + | > + | > + |

> + | > + |

> + | > + | > + | > + |

> + | > + | > + |

> + | > + | > + | > + |

> + |

> + | > + | > + | > + | > + | > + | > + + | > + | > + | > + | > + | > + | > + | > + | > + + | > + | > + | > +} + +// letters +TOKEN: { + <#A: ["a", "A"]> + | <#B: ["b", "B"]> + | <#C: ["c", "C"]> + | <#D: ["d", "D"]> + | <#E: ["e", "E"]> + | <#F: ["f", "F"]> + | <#G: ["g", "G"]> + | <#H: ["h", "H"]> + | <#I: ["i", "I"]> + | <#J: ["j", "J"]> + | <#K: ["k", "K"]> + | <#L: ["l", "L"]> + | <#M: ["m", "M"]> + | <#N: ["n", "N"]> + | <#O: ["o", "O"]> + | <#P: ["p", "P"]> + | <#Q: ["q", "Q"]> + | <#R: ["r", "R"]> + | <#S: ["s", "S"]> + | <#T: ["t", "T"]> + | <#U: ["u", "U"]> + | <#V: ["v", "V"]> + | <#W: ["w", "W"]> + | <#X: ["x", "X"]> + | <#Y: ["y", "Y"]> + | <#Z: ["z", "Z"]> + + | <#LETTER: ["a"-"z", "A"-"Z"]> +} + +// numbers +TOKEN: { + <#ZERO: "0"> + | <#DEC_DIGIT: ["0"-"9"]> // including octal digit + | <#HEX_DIGIT: ["0"-"9", "a"-"f", "A"-"F"]> +} + +// symbols +TOKEN: { + "> + | + | + | + | + | + | + | + | + | + | + | + | ="> + | "> + | + | + | + | + | + | "> + | + | + | + | + | + | + | + | + | + | + | +} + +// string literal +TOKEN: { + ( ~[] | ~["'", "\\"] | "''")* > +} + +TOKEN: { + | | ) ( | | | )*> + | ( ~[] | ~["`", "\\"] | "``")* > + | ( ~[] | ~["\"", "\\"] | "\"\"")* > +} + +TOKEN: { + ( )*> + | ()? (

| ) ( | )? + | (

| ) ( | )? + | ()? ( ( | )? )? + // | ( ( | )? )? + | ( | )? > +} +TOKEN: { )+> } +TOKEN: { ()+> } diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java new file mode 100644 index 000000000..449aebc32 --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -0,0 +1,541 @@ +package ru.yandex.clickhouse.jdbc.parser; + +import org.testng.annotations.Test; + +import ru.yandex.clickhouse.settings.ClickHouseProperties; + +import static org.testng.Assert.assertEquals; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ClickHouseSqlParserTest { + private String loadSql(String file) { + InputStream inputStream = ClickHouseSqlParserTest.class.getResourceAsStream("/sqls/" + file); + + StringBuilder sql = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = br.readLine()) != null) { + sql.append(line).append("\n"); + } + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + + return sql.toString(); + } + + private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql) { + checkSingleStatement(stmts, sql, StatementType.UNKNOWN, ClickHouseSqlStatement.DEFAULT_DATABASE, + ClickHouseSqlStatement.DEFAULT_TABLE); + } + + private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql, StatementType stmtType) { + checkSingleStatement(stmts, sql, stmtType, ClickHouseSqlStatement.DEFAULT_DATABASE, + ClickHouseSqlStatement.DEFAULT_TABLE); + } + + private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql, StatementType stmtType, + String database, String table) { + assertEquals(stmts.length, 1); + + ClickHouseSqlStatement s = stmts[0]; + assertEquals(s.getSQL(), sql); + assertEquals(s.getStatementType(), stmtType); + assertEquals(s.getDatabaseOrDefault(null), database); + assertEquals(s.getTable(), table); + } + + @Test + public void testParseNonSql() throws ParseException { + String sql; + + assertEquals(ClickHouseSqlParser.parse(sql = null), new ClickHouseSqlStatement[0]); + assertEquals(ClickHouseSqlParser.parse(sql = ""), new ClickHouseSqlStatement[0]); + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "invalid sql"), sql); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "-- some comments"), sql); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "/*********\r\n\r\t some ***** comments*/"), sql); + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select"), sql, StatementType.UNKNOWN); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select ()"), sql, StatementType.UNKNOWN); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (()"), sql, StatementType.UNKNOWN); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[]"), sql, StatementType.UNKNOWN); + // checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 select"), sql, + // StatementType.UNKNOWN); + } + + @Test + public void testAlterStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser + .parse(sql = "ALTER TABLE test_db.test_table UPDATE a = 1, \"b\" = '2', `c`=3.3 WHERE d=123 and e=456"), + sql, StatementType.ALTER_UPDATE, "test_db", "test_table"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "ALTER TABLE tTt on cluster 'cc' delete WHERE d=123 and e=456"), sql, + StatementType.ALTER_DELETE, "system", "tTt"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "ALTER USER user DEFAULT ROLE role1, role2"), sql, + StatementType.ALTER); + } + + @Test + public void testAttachStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "ATTACH TABLE IF NOT EXISTS t.t ON CLUSTER cluster"), sql, + StatementType.ATTACH); + } + + @Test + public void testCheckStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "check table a"), sql, StatementType.CHECK); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "check table a.a"), sql, StatementType.CHECK); + } + + @Test + public void testCreateStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "create table a(a String) engine=Memory"), sql, + StatementType.CREATE); + } + + @Test + public void testDeleteStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "delete from a"), sql, StatementType.DELETE, "system", + "a"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "delete from c.a where upper(a)=upper(lower(b))"), sql, + StatementType.DELETE, "c", "a"); + } + + @Test + public void testDescribeStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table a"), sql, StatementType.DESCRIBE, "system", + "columns"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "describe table a.a"), sql, StatementType.DESCRIBE, "a", + "columns"); + } + + @Test + public void testDetachStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "detach TABLE t"), sql, StatementType.DETACH); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "detach TABLE if exists t.t on cluster 'cc'"), sql, + StatementType.DETACH); + } + + @Test + public void testDropStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "drop TEMPORARY table t"), sql, StatementType.DROP); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "drop TABLE if exists t.t on cluster 'cc'"), sql, + StatementType.DROP); + } + + @Test + public void testExistsStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS TEMPORARY TABLE a"), sql, StatementType.EXISTS); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS TABLE a.a"), sql, StatementType.EXISTS); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS DICTIONARY c"), sql, StatementType.EXISTS); + } + + @Test + public void testExplainStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse( + sql = "EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV"), + sql, StatementType.EXPLAIN); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXPLAIN AST SELECT 1"), sql, StatementType.EXPLAIN); + checkSingleStatement(ClickHouseSqlParser.parse( + sql = "EXPLAIN SYNTAX SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c"), + sql, StatementType.EXPLAIN); + } + + @Test + public void testGrantStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "GRANT SELECT(x,y) ON db.table TO john WITH GRANT OPTION"), + sql, StatementType.GRANT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "GRANT INSERT(x,y) ON db.table TO john"), sql, + StatementType.GRANT); + } + + @Test + public void testInsertStatement() throws ParseException { + String sql; + + ClickHouseSqlStatement s = ClickHouseSqlParser.parse(sql = "insert into table test(a,b) Values (1,2)")[0]; + assertEquals(sql.substring(s.getStartPosition("values"), s.getEndPosition("VALUES")), "Values"); + assertEquals(sql.substring(0, s.getEndPosition("values")) + " (1,2)", sql); + + Pattern values = Pattern.compile("(?i)VALUES[\\s]*\\("); + int valuePosition = -1; + Matcher matcher = values.matcher(sql); + if (matcher.find()) { + valuePosition = matcher.start(); + } + assertEquals(s.getStartPosition("values"), valuePosition); + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') values(1)"), sql, + StatementType.INSERT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') values(1)(2)"), sql, + StatementType.INSERT); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') select * from number(10)"), sql, + StatementType.INSERT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into test2(a,b) values('values(',',')"), sql, + StatementType.INSERT, "system", "test2"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "INSERT INTO table t(a, b, c) values('1', ',', 'ccc')"), + sql, StatementType.INSERT, "system", "t"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "INSERT INTO table t(a, b, c) values('1', 2, 'ccc') (3,2,1)"), sql, + StatementType.INSERT, "system", "t"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "INSERT INTO table s.t select * from ttt"), sql, + StatementType.INSERT, "s", "t"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "INSERT INTO insert_select_testtable (* EXCEPT(b)) Values (2, 2)"), sql, + StatementType.INSERT, "system", "insert_select_testtable"); + + } + + @Test + public void testKillStatement() { + String sql; + + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'"), sql, + StatementType.KILL); + checkSingleStatement(ClickHouseSqlParser.parse( + sql = "KILL MUTATION WHERE database = 'default' AND table = 'table' AND mutation_id = 'mutation_3.txt' SYNC"), + sql, StatementType.KILL); + } + + @Test + public void testOptimizeStatement() { + String sql; + + checkSingleStatement( + ClickHouseSqlParser + .parse(sql = "OPTIMIZE TABLE a ON CLUSTER cluster PARTITION ID 'partition_id' FINAL"), + sql, StatementType.OPTIMIZE); + } + + @Test + public void testRenameStatement() { + String sql; + + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "RENAME TABLE table1 TO table2, table3 TO table4 ON CLUSTER cluster"), + sql, StatementType.RENAME); + checkSingleStatement(ClickHouseSqlParser.parse( + sql = "RENAME TABLE db1.table1 TO db2.table2, db2.table3 to db2.table4, db3.table5 to db2.table6 ON CLUSTER 'c'"), + sql, StatementType.RENAME); + } + + @Test + public void testRevokeStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "REVOKE SELECT ON accounts.* FROM john"), sql, + StatementType.REVOKE); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "REVOKE SELECT(wage) ON accounts.staff FROM mira"), sql, + StatementType.REVOKE); + } + + @Test + public void testSelectStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (())"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select []"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[]]"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select *"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select timezone()"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select @@version, $version"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select * from jdbc('db', 'schema', 'select 1')"), sql, + StatementType.SELECT, "system", "jdbc"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "select 1 as a1, a.a as a2, aa(a1, a2) a3, length(a3) as a4 from x"), + sql, StatementType.SELECT, "system", "x"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "select x.* from (select [1,2] a, (1,2,3) b, a[1], b.2) x"), sql, + StatementType.SELECT, "system", "x"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (3, [[1,2],[3,4]]) as a, (a.2)[2][1]"), sql, + StatementType.SELECT); + checkSingleStatement( + ClickHouseSqlParser + .parse(sql = "select 1,1.1,'\"''`a' a, \"'`\"\"a\" as b, (1 + `a`a) c, null, inf i, nan as n"), + sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as select"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1, 2 a, 3 as b, 1+1-2*3/4, *, c.* from c a"), sql, + StatementType.SELECT, "system", "c"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as select"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse( + sql = " -- cc\nselect 1 as `a.b`, a, 1+1, b from \"a\".`b` inner join a on a.abb/* \n\r\n1*/\n=2 and a.abb = c.a and a=1 and (k is null and j not in(1,2))"), + sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT idx, s FROM test.mymetadata WHERE idx = ?"), sql, + StatementType.SELECT, "test", "mymetadata"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "WITH 2 AS two SELECT two * two"), sql, + StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse( + sql = "SELECT i, array(toUnixTimestamp(dt_server[1])), array(toUnixTimestamp(dt_berlin[1])), array(toUnixTimestamp(dt_lax[1])) FROM test.fun_with_timezones_array"), + sql, StatementType.SELECT, "test", "fun_with_timezones_array"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT SUM(x) FROM t WHERE y = ? GROUP BY ?"), sql, + StatementType.SELECT, "system", "t"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = loadSql("simple-query-from-telegram.sql")), sql, + StatementType.SELECT, "system", "wrd"); + } + + @Test + public void testSetStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SET profile = 'my-profile', mutations_sync=1"), sql, + StatementType.SET); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SET DEFAULT ROLE role1, role2, role3 TO user"), sql, + StatementType.SET); + } + + @Test + public void testShowStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SHOW DATABASES LIKE '%de%'"), sql, StatementType.SHOW, + "system", "databases"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "show tables from db"), sql, StatementType.SHOW, "system", + "tables"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "show dictionaries from db"), sql, StatementType.SHOW, + "system", "dictionaries"); + } + + @Test + public void testSystemStatement() { + String sql; + + checkSingleStatement( + ClickHouseSqlParser + .parse(sql = "SYSTEM DROP REPLICA 'replica_name' FROM ZKPATH '/path/to/table/in/zk'"), + sql, StatementType.SYSTEM); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "SYSTEM RESTART REPLICA db.replicated_merge_tree_family_table_name"), + sql, StatementType.SYSTEM); + } + + @Test + public void testTruncateStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "truncate table a.b"), sql, StatementType.TRUNCATE, "a", + "b"); + } + + @Test + public void testUpdateStatement() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "update a set a='1'"), sql, StatementType.UPDATE); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "update a.a set `a`=2 where upper(a)=upper(lower(b))"), + sql, StatementType.UPDATE); + } + + @Test + public void testUseStatement() throws ParseException { + String sql; + checkSingleStatement(ClickHouseSqlParser.parse(sql = "use system"), sql, StatementType.USE); + } + + @Test + public void testWatchStatement() throws ParseException { + String sql; + checkSingleStatement(ClickHouseSqlParser.parse(sql = "watch system.processes"), sql, StatementType.WATCH); + } + + @Test + public void testMultipleStatements() throws ParseException { + assertEquals(ClickHouseSqlParser.parse("use ab;;;select 1; ;\t;\r;\n"), + new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement("use ab", StatementType.USE, null, "ab", null, null, null, null), + new ClickHouseSqlStatement("select 1", StatementType.SELECT) }); + assertEquals(ClickHouseSqlParser.parse("select * from \"a;1\".`b;c`;;;select 1 as `a ; a`; ;\t;\r;\n"), + new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement("select * from \"a;1\".`b;c`", StatementType.SELECT, null, "a;1", + "b;c", null, null, null), + new ClickHouseSqlStatement("select 1 as `a ; a`", StatementType.SELECT) }); + } + + @Test + public void testAlias() throws ParseException { + String sql; + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as c, 2 b"), sql, StatementType.SELECT); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from a.b c"), sql, StatementType.SELECT, "a", + "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 select from a.b c"), sql, StatementType.SELECT, + "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from (select 2) b"), sql, StatementType.SELECT, + "system", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from (select 2) as from"), sql, + StatementType.SELECT, "system", "from"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from a.b c1, b.a c2"), sql, StatementType.SELECT, + "a", "b"); + } + + @Test + public void testExpression() throws ParseException { + String sql; + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[[1,2],[3,4],[5,6]]] a, a[1][1][2] from a.b"), + sql, StatementType.SELECT, "a", "b"); + checkSingleStatement( + ClickHouseSqlParser.parse( + sql = "select [[[[]]]], a[1][2][3], ([[1]] || [[2]])[2][1] ,func(1,2) [1] [2] [ 3 ] from a.b"), + sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select c.c1, c.c2 c, c.c3 as cc, c.c4.1.2 from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select - (select (1,).1) from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1.1e1,(1) . 1 , ((1,2)).1 .2 . 3 from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select a.b.c1, c1, b.c1 from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "select date'2020-02-04', timestamp '2020-02-04' from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "select count (), sum(c1), fake(a1, count(), (1+1)) from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select {}, {'a':'b', 'c':'1'} from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [], [1,2], [ [1,2], [3,4] ] from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1+1-1*1/1 from a.b"), sql, StatementType.SELECT, + "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (1+(1-1)*1/1)-1 from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (1+(1+(-1))*1/1)-(select (1,).1) from a.b"), sql, + StatementType.SELECT, "a", "b"); + } + + @Test + public void testFormat() throws ParseException { + String sql = "select 1 as format, format csv"; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasFormat(), false); + assertEquals(stmts[0].getFormat(), null); + + sql = "select 1 format csv"; + stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasFormat(), true); + assertEquals(stmts[0].getFormat(), "csv"); + + sql = "select 1 a, a.a b, a.a.a c, e.* except(e1), e.e.* except(e2), 'aaa' format, format csv from numbers(2) FORMAT CSVWithNames"; + stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasFormat(), true); + assertEquals(stmts[0].getFormat(), "CSVWithNames"); + } + + @Test + public void testOutfile() throws ParseException { + String sql = "select 1 into outfile '1.txt'"; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasOutfile(), true); + assertEquals(stmts[0].getOutfile(), "'1.txt'"); + + sql = "insert into outfile values(1,2,3)"; + stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasOutfile(), false); + assertEquals(stmts[0].getOutfile(), null); + } + + @Test + public void testWithTotals() throws ParseException { + String sql = "select 1 as with totals"; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasWithTotals(), false); + + sql = "select 1 with totals"; + stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasWithTotals(), true); + } + + @Test + public void testParameterHandling() throws ParseException { + String sql = "insert into table d.t(a1, a2, a3) values(?,?,?)"; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + + stmts = ClickHouseSqlParser.parse(sql, new ClickHouseProperties(), new ParseHandler() { + @Override + public String handleMacro(String name, List parameters) { + return null; + } + + @Override + public String handleParameter(String cluster, String database, String table, int columnIndex) { + return String.valueOf(columnIndex); + } + }); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), "insert into table d.t(a1, a2, a3) values(1,2,3)"); + } + + @Test + public void testMacroHandling() throws ParseException { + String sql = "select #listOfColumns #ignored from (#subQuery('1','2','3'))"; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), "select from ()"); + + stmts = ClickHouseSqlParser.parse(sql, new ClickHouseProperties(), new ParseHandler() { + @Override + public String handleMacro(String name, List parameters) { + if ("listOfColumns".equals(name)) { + return "a, b"; + } else if ("subQuery".equals(name)) { + return "select " + String.join("||", parameters); + } else { + return null; + } + } + + @Override + public String handleParameter(String cluster, String database, String table, int columnIndex) { + return null; + } + }); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), "select a, b from (select 1||2||3)"); + } +} diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtilsTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtilsTest.java new file mode 100644 index 000000000..a0bf980e1 --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlUtilsTest.java @@ -0,0 +1,66 @@ +package ru.yandex.clickhouse.jdbc.parser; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ClickHouseSqlUtilsTest { + @Test + public void testIsQuote() { + Assert.assertFalse(ClickHouseSqlUtils.isQuote('\0')); + + Assert.assertTrue(ClickHouseSqlUtils.isQuote('"')); + Assert.assertTrue(ClickHouseSqlUtils.isQuote('\'')); + Assert.assertTrue(ClickHouseSqlUtils.isQuote('`')); + } + + @Test + public void testEscape() { + char[] quotes = new char[] { '"', '\'', '`' }; + String str; + for (int i = 0; i < quotes.length; i++) { + char quote = quotes[i]; + Assert.assertEquals(ClickHouseSqlUtils.escape(str = null, quote), str); + Assert.assertEquals(ClickHouseSqlUtils.escape(str = "", quote), + String.valueOf(quote) + String.valueOf(quote)); + Assert.assertEquals(ClickHouseSqlUtils.escape(str = "\\any \\string\\", quote), + String.valueOf(quote) + "\\\\any \\\\string\\\\" + String.valueOf(quote)); + Assert.assertEquals( + ClickHouseSqlUtils.escape(str = String.valueOf(quote) + "any " + String.valueOf(quote) + "string", + quote), + String.valueOf(quote) + "\\" + String.valueOf(quote) + "any \\" + String.valueOf(quote) + "string" + + String.valueOf(quote)); + Assert.assertEquals(ClickHouseSqlUtils.escape(str = "\\any \\string\\" + String.valueOf(quote), quote), + String.valueOf(quote) + "\\\\any \\\\string\\\\\\" + String.valueOf(quote) + String.valueOf(quote)); + Assert.assertEquals( + ClickHouseSqlUtils.escape(str = String.valueOf(quote) + "\\any \\" + String.valueOf(quote) + + "string\\" + String.valueOf(quote), quote), + String.valueOf(quote) + "\\" + String.valueOf(quote) + "\\\\any \\\\\\" + String.valueOf(quote) + + "string" + "\\\\\\" + String.valueOf(quote) + String.valueOf(quote)); + } + } + + @Test + public void testUnescape() { + String str; + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = null), str); + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = ""), str); + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = "\\any \\string\\"), str); + char[] quotes = new char[] { '"', '\'', '`' }; + for (int i = 0; i < quotes.length; i++) { + char quote = quotes[i]; + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = String.valueOf(quote) + "1" + String.valueOf(quote)), + "1"); + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = String.valueOf(quote) + "\\any \\string\\"), str); + Assert.assertEquals(ClickHouseSqlUtils.unescape(str = "\\any \\string\\" + String.valueOf(quote)), str); + Assert.assertEquals( + ClickHouseSqlUtils.unescape(str = String.valueOf(quote) + "\\any" + String.valueOf(quote) + + String.valueOf(quote) + "\\string\\" + String.valueOf(quote)), + "any" + String.valueOf(quote) + "string\\"); + Assert.assertEquals( + ClickHouseSqlUtils.unescape(str = String.valueOf(quote) + String.valueOf(quote) + "\\" + + String.valueOf(quote) + "any" + String.valueOf(quote) + String.valueOf(quote) + + "\\string\\" + String.valueOf(quote)), + String.valueOf(quote) + String.valueOf(quote) + "any" + String.valueOf(quote) + "string\\"); + } + } +} diff --git a/src/test/resources/sqls/simple-query-from-telegram.sql b/src/test/resources/sqls/simple-query-from-telegram.sql new file mode 100644 index 000000000..79b7097db --- /dev/null +++ b/src/test/resources/sqls/simple-query-from-telegram.sql @@ -0,0 +1,36 @@ +select + JSONExtractRaw(abcedfg.fields, 'someDateField___e') as abc_someDateField___e, + some_word as sw_someWord, + JSONExtractString(abcedfg.fields, 'field') as abc_field, + some_more_words as sw_moreWords , + last_word as sw_lastWord, + JSONExtractInt(abcedfg.fields, 'countOfWords') as abc_countOfWords, + abcedfg.id as abc_id, + JSONExtractString(abcedfg.fields, 'somePlace') as abc_somePlace, + JSONExtractString(abcedfg.fields, 'place') as abc_place, + JSONExtractInt(abcedfg.fields, 'countOfPlaces') as abc_countOfPlaces, + abcedfg.name as abc_name, + (some_more_words * 100 / (even_more_words * (? / 28))) - 100 as sw_wordsPercentChange, + some_unique_words as sw_uniqueWords +from ( + select + abcedfg_id, + sum(if(toDate(sample_date) >= toDate(?, 'UTC'), 1, 0)) some_more_words, + count(distinct if(toDate(sample_date) >= toDate(?, 'UTC'), wrd.word_id, null)) some_unique_words, + sum(if(toDate(sample_date) < toDate(?, 'UTC'), 1, 0)) even_more_words, + min(toDate(sample_date, 'UTC')) some_word, + max(toDate(sample_date, 'UTC')) last_word + from a1234_test.sample wrd + join a1234_test.abcedfg_list_item itm on itm.abcedfg_id = wrd.abcedfg_id + where toDate(sample_date, 'UTC') between + addDays(toDate(?, 'UTC'), -28) + and toDate(?, 'UTC') + and wrd.sample_type_id IN (?) + and itm.abcedfg_list_id IN (?) + and 1 + group by abcedfg_id +) as wrd +join a1234_test.abcedfg abc on abc.id = wrd.abcedfg_id +order by sw_moreWords desc + limit ? offset ? +FORMAT CSVWithNames From 1347cecf2c0131ef2181dc485261d0399e819739 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Mon, 8 Feb 2021 20:06:01 +0800 Subject: [PATCH 2/5] Enhance parser and add more test cases --- .../ClickHousePreparedStatementImpl.java | 2 +- .../clickhouse/ClickHouseStatementImpl.java | 6 +- .../jdbc/parser/ClickHouseSqlStatement.java | 3 +- src/main/javacc/ClickHouseSqlParser.jj | 52 +++++----- .../ClickHousePreparedStatementTest.java | 18 ++++ .../ClickHouseStatementImplTest.java | 10 ++ .../jdbc/parser/ClickHouseSqlParserTest.java | 96 ++++++++++++++++++- .../resources/sqls/issue-441_with-totals.sql | 13 +++ ...legram.sql => issue-555_custom-format.sql} | 0 9 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 src/test/resources/sqls/issue-441_with-totals.sql rename src/test/resources/sqls/{simple-query-from-telegram.sql => issue-555_custom-format.sql} (100%) diff --git a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java index beef85d8b..2681d0726 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java @@ -443,7 +443,7 @@ public ResultSetMetaData getMetaData() throws SQLException { } ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); - if ((parsedStmt.isRecognized() && !parsedStmt.isQuery()) || !isSelect(sql)) { + if (!parsedStmt.isQuery() || (!parsedStmt.isRecognized() && !isSelect(sql))) { return null; } ResultSet myRs = executeQuery(Collections.singletonMap( diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java index 4f9578399..54078f982 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java @@ -144,7 +144,7 @@ public ResultSet executeQuery(String sql, ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); try { - if (parsedStmt.isQuery() || isSelect(sql)) { + if (parsedStmt.isQuery() || (!parsedStmt.isRecognized() && isSelect(sql))) { if (!parsedStmt.isRecognized()) { Map positions = new HashMap<>(); String dbName = extractDBName(sql); @@ -503,10 +503,10 @@ static String clickhousifySql(String sql, ClickHouseProperties properties) { private static String addFormatIfAbsent(final String sql, ClickHouseProperties properties, ClickHouseFormat format) { String cleanSQL = sql.trim(); ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(cleanSQL, properties); - if ((parsedStmt.isRecognized() && !parsedStmt.isQuery()) || !isSelect(cleanSQL)) { + if (!parsedStmt.isQuery() || (!parsedStmt.isRecognized() && !isSelect(cleanSQL))) { return cleanSQL; } - if ((parsedStmt.isRecognized() && parsedStmt.hasFormat()) || ClickHouseFormat.containsFormat(cleanSQL)) { + if (parsedStmt.hasFormat() || (!parsedStmt.isRecognized() && ClickHouseFormat.containsFormat(cleanSQL))) { return cleanSQL; } StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java index 846bd47c8..93e0a0853 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java @@ -166,7 +166,8 @@ public String toString() { sb.append('[').append(stmtType.name()).append(']').append(" cluster=").append(cluster).append(", database=") .append(database).append(", table=").append(table).append(", format=").append(format) - .append(", positions=").append(positions).append("\nSQL:\n").append(sql); + .append(", outfile=").append(outfile).append(", positions=").append(positions).append("\nSQL:\n") + .append(sql); return sb.toString(); } diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj index 0e3151315..4e1754019 100644 --- a/src/main/javacc/ClickHouseSqlParser.jj +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -107,18 +107,13 @@ public class ClickHouseSqlParser { properties = new ClickHouseProperties(); } - ClickHouseSqlStatement[] stmts = new ClickHouseSqlStatement[0]; + ClickHouseSqlStatement[] stmts = new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }; - if (sql == null || sql.isEmpty()) { + if (!properties.isUseNewParser() || sql == null || sql.isEmpty()) { return stmts; } - if (!properties.isUseNewParser()) { - return new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) - }; - } - ClickHouseSqlParser p = new ClickHouseSqlParser(sql, properties, handler); try { p.sql(); @@ -344,10 +339,8 @@ void deleteStmt(): {} { // not interested // https://clickhouse.tech/docs/en/sql-reference/statements/describe-table/ void describeStmt(): {} { - ( | ) - { token_source.table = "columns"; } - (LOOKAHEAD(2)

)? - (LOOKAHEAD(2) tableIdentifier(true) | anyExprList()) + ( | ) { token_source.table = "columns"; } + (LOOKAHEAD({ getToken(1).kind == TABLE })
)? (LOOKAHEAD(2) tableIdentifier(true) | anyExprList()) } // https://clickhouse.tech/docs/en/sql-reference/statements/detach/ @@ -567,14 +560,14 @@ void anyExprList(): {} { } void anyExpr(): {} { - anyNestedExpr() (LOOKAHEAD(2) ( | | operator()) anyNestedExpr())* + anyNestedExpr() (LOOKAHEAD(2) ( | | | | operator())? anyNestedExpr())* } void anyNestedExpr(): {} { LOOKAHEAD(2) formatPart() | LOOKAHEAD(2) withTotalPart() | LOOKAHEAD(2) outfilePart() - | (LOOKAHEAD(2) anyColumnExpr() ( | )*)+ + | LOOKAHEAD(2) anyColumnExpr() ( | )* } void anyColumnExpr(): { Token t; } { @@ -716,14 +709,14 @@ Token anyKeyword(): { Token t; } { | t = | t = | t = | t =
| t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t =
| t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = // interval | t = | t = | t = | t = | t = | t = | t = | t = // values @@ -781,8 +774,10 @@ TOKEN: { | > | > + | > | > |

> + | > | > | > | > @@ -800,18 +795,30 @@ TOKEN: { | > | > | > + | > | > + | > + |

> + | > | > | > + | > | > | > | > + | > + | > + | > | > | > | > + | > | > + | > |

> + | > |

> + | > |

> |

> | > @@ -820,6 +827,7 @@ TOKEN: { |

> |

> | > + | > | > | > | > diff --git a/src/test/java/ru/yandex/clickhouse/integration/ClickHousePreparedStatementTest.java b/src/test/java/ru/yandex/clickhouse/integration/ClickHousePreparedStatementTest.java index c23647cc3..f6c982010 100644 --- a/src/test/java/ru/yandex/clickhouse/integration/ClickHousePreparedStatementTest.java +++ b/src/test/java/ru/yandex/clickhouse/integration/ClickHousePreparedStatementTest.java @@ -213,6 +213,24 @@ public void testInsertUUIDBatch() throws SQLException { Assert.assertEquals(uuid, UUID.fromString("bef35f40-3b03-45b0-b1bd-8ec6593dcaaa")); } + @Test + public void testInsertStringContainsKeyword() throws SQLException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.keyword_insert"); + connection.createStatement().execute( + "CREATE TABLE test.keyword_insert(a String,b String)ENGINE = MergeTree() ORDER BY a SETTINGS index_granularity = 8192" + ); + + PreparedStatement stmt = connection.prepareStatement("insert into test.keyword_insert(a,b) values('values(',',')"); + stmt.execute(); + + Statement select = connection.createStatement(); + ResultSet rs = select.executeQuery("select * from test.keyword_insert"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getString(1), "values("); + Assert.assertEquals(rs.getString(2), ","); + Assert.assertFalse(rs.next()); + } + @Test public void testInsertNullString() throws SQLException { connection.createStatement().execute("DROP TABLE IF EXISTS test.null_insert"); diff --git a/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java b/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java index 6700534b3..77e84e530 100644 --- a/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java +++ b/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java @@ -167,6 +167,16 @@ public void testResultSetWithExtremes() throws SQLException { } } + @Test + public void testSelectOne() throws SQLException { + try (Statement stmt = connection.createStatement()) { + ResultSet rs = stmt.executeQuery("select\n1"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt(1), 1); + Assert.assertFalse(rs.next()); + } + } + @Test public void testSelectManyRows() throws SQLException { Statement stmt = connection.createStatement(); diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java index 449aebc32..fccb558fc 100644 --- a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -56,8 +57,10 @@ private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql, St public void testParseNonSql() throws ParseException { String sql; - assertEquals(ClickHouseSqlParser.parse(sql = null), new ClickHouseSqlStatement[0]); - assertEquals(ClickHouseSqlParser.parse(sql = ""), new ClickHouseSqlStatement[0]); + assertEquals(ClickHouseSqlParser.parse(sql = null), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); + assertEquals(ClickHouseSqlParser.parse(sql = ""), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); checkSingleStatement(ClickHouseSqlParser.parse(sql = "invalid sql"), sql); checkSingleStatement(ClickHouseSqlParser.parse(sql = "-- some comments"), sql); @@ -123,10 +126,14 @@ public void testDeleteStatement() { public void testDescribeStatement() { String sql; + checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc a"), sql, StatementType.DESCRIBE, "system", + "columns"); checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table a"), sql, StatementType.DESCRIBE, "system", "columns"); checkSingleStatement(ClickHouseSqlParser.parse(sql = "describe table a.a"), sql, StatementType.DESCRIBE, "a", "columns"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table table"), sql, StatementType.DESCRIBE, "system", + "columns"); } @Test @@ -265,6 +272,15 @@ public void testRevokeStatement() { public void testSelectStatement() { String sql; + assertEquals(ClickHouseSqlParser.parse(sql = "select\n1"), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); + assertEquals(ClickHouseSqlParser.parse(sql = "select\r\n1"), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); + + assertEquals(ClickHouseSqlParser.parse(sql = "select 314 limit 5\nFORMAT JSONCompact;"), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("select 314 limit 5\nFORMAT JSONCompact", + StatementType.SELECT, null, null, "unknown", "JSONCompact", null, null) }); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (())"), sql, StatementType.SELECT); checkSingleStatement(ClickHouseSqlParser.parse(sql = "select []"), sql, StatementType.SELECT); checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[]]"), sql, StatementType.SELECT); @@ -283,7 +299,7 @@ public void testSelectStatement() { StatementType.SELECT); checkSingleStatement( ClickHouseSqlParser - .parse(sql = "select 1,1.1,'\"''`a' a, \"'`\"\"a\" as b, (1 + `a`a) c, null, inf i, nan as n"), + .parse(sql = "select 1,1.1,'\"''`a' a, \"'`\"\"a\" as b, (1 + `a`.a) c, null, inf i, nan as n"), sql, StatementType.SELECT); checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as select"), sql, StatementType.SELECT); checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1, 2 a, 3 as b, 1+1-2*3/4, *, c.* from c a"), sql, @@ -301,8 +317,17 @@ public void testSelectStatement() { sql, StatementType.SELECT, "test", "fun_with_timezones_array"); checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT SUM(x) FROM t WHERE y = ? GROUP BY ?"), sql, StatementType.SELECT, "system", "t"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = loadSql("simple-query-from-telegram.sql")), sql, - StatementType.SELECT, "system", "wrd"); + + assertEquals(ClickHouseSqlParser.parse(sql = loadSql("issue-441_with-totals.sql")), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, + "unknown", null, null, new HashMap() { + { + put("TOTALS", 208); + } + }) }); + assertEquals(ClickHouseSqlParser.parse(sql = loadSql("issue-555_custom-format.sql")), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", + "CSVWithNames", null, null) }); } @Test @@ -401,6 +426,10 @@ public void testAlias() throws ParseException { @Test public void testExpression() throws ParseException { String sql; + + checkSingleStatement( + ClickHouseSqlParser.parse(sql = "select 1 and `a`.\"b\" c1, c1 or (c2 and c3), c4 ? c5 : c6 from a.b"), + sql, StatementType.SELECT, "a", "b"); checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[[1,2],[3,4],[5,6]]] a, a[1][1][2] from a.b"), sql, StatementType.SELECT, "a", "b"); checkSingleStatement( @@ -538,4 +567,61 @@ public String handleParameter(String cluster, String database, String table, int assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), "select a, b from (select 1||2||3)"); } + + @Test + public void testExtractDBAndTableName() { + String sql; + + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from table"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from table a"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from\ntable a"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1\nfrom\ntable a"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1\nFrom\ntable a"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from db.table a"), sql, StatementType.SELECT, + "db", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from \"db.table\" a"), sql, + StatementType.SELECT, "system", "db.table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from `db.table` a"), sql, StatementType.SELECT, + "system", "db.table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "from `db.table` a"), sql, StatementType.UNKNOWN, "system", + "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " from `db.table` a"), sql, StatementType.UNKNOWN, + "system", "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "ELECT from `db.table` a"), sql, StatementType.UNKNOWN, + "system", "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SHOW tables"), sql, StatementType.SHOW, "system", + "tables"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table1"), sql, StatementType.DESCRIBE, "system", + "columns"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "DESC table1"), sql, StatementType.DESCRIBE, "system", + "columns"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 'from db.table a' from tab"), sql, + StatementType.SELECT, "system", "tab"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT"), sql, StatementType.UNKNOWN, "system", + "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "S"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = ""), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, + "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT fromUnixTimestamp64Milli(time) as x from table"), + sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from table"), sql, + StatementType.SELECT, "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = "/*qq*/ SELECT fromUnixTimestamp64Milli(time)from table"), + sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECTfromUnixTimestamp64Milli(time)from table"), sql, + StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from \".inner.a\""), + sql, StatementType.SELECT, "system", ".inner.a"); + checkSingleStatement( + ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from db.`.inner.a`"), sql, + StatementType.SELECT, "db", ".inner.a"); + } } diff --git a/src/test/resources/sqls/issue-441_with-totals.sql b/src/test/resources/sqls/issue-441_with-totals.sql new file mode 100644 index 000000000..2169ce645 --- /dev/null +++ b/src/test/resources/sqls/issue-441_with-totals.sql @@ -0,0 +1,13 @@ +WITH 2 AS factor +SELECT + number % 2 AS odd_even, + count(*) AS count, + sum(factor * number) AS output +FROM +( + SELECT number + FROM system.numbers + LIMIT 100 +) +GROUP BY number % 2 + WITH TOTALS \ No newline at end of file diff --git a/src/test/resources/sqls/simple-query-from-telegram.sql b/src/test/resources/sqls/issue-555_custom-format.sql similarity index 100% rename from src/test/resources/sqls/simple-query-from-telegram.sql rename to src/test/resources/sqls/issue-555_custom-format.sql From 6f618bf804bf379579b4a2c8c1e612c135b11490 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Mon, 8 Feb 2021 20:57:07 +0800 Subject: [PATCH 3/5] Fix javadoc errors --- .../clickhouse/jdbc/parser/ParseHandler.java | 8 +- src/main/javacc/ClickHouseSqlParser.jj | 15 +- .../jdbc/parser/ClickHouseSqlParserTest.java | 411 ++++++++---------- 3 files changed, 188 insertions(+), 246 deletions(-) diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java index 985128cf8..8d586db81 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java @@ -15,10 +15,10 @@ public interface ParseHandler { /** * Handle parameter. * - * @param String cluster cluster - * @param String database database - * @param String table table - * @param int index(starts from 1 not 0) + * @param cluster cluster + * @param database database + * @param table table + * @param columnIndex columnIndex(starts from 1 not 0) * @return parameter value */ String handleParameter(String cluster, String database, String table, int columnIndex); diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj index 4e1754019..c1454244b 100644 --- a/src/main/javacc/ClickHouseSqlParser.jj +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -66,9 +66,15 @@ public class ClickHouseSqlParser { } /** + * Parse given SQL. + * * @deprecated This method will be removed in the near future. *

- * Use {@link Utils#parse(String)} instead. + * Use {@link #parse(String, ClickHouseProperties)} instead. + * + * @param sql SQL query + * @param properties properties + * @return parsed SQL statement */ public static ClickHouseSqlStatement parseSingleStatement(String sql, ClickHouseProperties properties) { return parseSingleStatement(sql, properties, null); @@ -79,7 +85,7 @@ public class ClickHouseSqlParser { * * @deprecated This method will be removed in the near future. *

- * Use {@link Utils#parse(String, ParseHandler)} instead. + * Use {@link #parse(String, ClickHouseProperties, ParseHandler)} instead. * * @param sql SQL query * @param properties properties @@ -93,11 +99,6 @@ public class ClickHouseSqlParser { return stmts.length == 1 ? stmts[0] : new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null); } - @Deprecated - public static ClickHouseSqlStatement[] parse(String sql) { - return parse(sql, null, null); - } - public static ClickHouseSqlStatement[] parse(String sql, ClickHouseProperties properties) { return parse(sql, properties, null); } diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java index fccb558fc..70ca0d7c2 100644 --- a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -16,6 +16,10 @@ import java.util.regex.Pattern; public class ClickHouseSqlParserTest { + private ClickHouseSqlStatement[] parse(String sql) { + return ClickHouseSqlParser.parse(sql, new ClickHouseProperties()); + } + private String loadSql(String file) { InputStream inputStream = ClickHouseSqlParserTest.class.getResourceAsStream("/sqls/" + file); @@ -57,20 +61,20 @@ private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql, St public void testParseNonSql() throws ParseException { String sql; - assertEquals(ClickHouseSqlParser.parse(sql = null), new ClickHouseSqlStatement[] { + assertEquals(parse(sql = null), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); - assertEquals(ClickHouseSqlParser.parse(sql = ""), new ClickHouseSqlStatement[] { + assertEquals(parse(sql = ""), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "invalid sql"), sql); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "-- some comments"), sql); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "/*********\r\n\r\t some ***** comments*/"), sql); + checkSingleStatement(parse(sql = "invalid sql"), sql); + checkSingleStatement(parse(sql = "-- some comments"), sql); + checkSingleStatement(parse(sql = "/*********\r\n\r\t some ***** comments*/"), sql); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select"), sql, StatementType.UNKNOWN); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select ()"), sql, StatementType.UNKNOWN); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (()"), sql, StatementType.UNKNOWN); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[]"), sql, StatementType.UNKNOWN); - // checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 select"), sql, + checkSingleStatement(parse(sql = "select"), sql, StatementType.UNKNOWN); + checkSingleStatement(parse(sql = "select ()"), sql, StatementType.UNKNOWN); + checkSingleStatement(parse(sql = "select (()"), sql, StatementType.UNKNOWN); + checkSingleStatement(parse(sql = "select [[]"), sql, StatementType.UNKNOWN); + // checkSingleStatement(parse(sql = "select 1 select"), sql, // StatementType.UNKNOWN); } @@ -78,21 +82,19 @@ public void testParseNonSql() throws ParseException { public void testAlterStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser - .parse(sql = "ALTER TABLE test_db.test_table UPDATE a = 1, \"b\" = '2', `c`=3.3 WHERE d=123 and e=456"), - sql, StatementType.ALTER_UPDATE, "test_db", "test_table"); checkSingleStatement( - ClickHouseSqlParser.parse(sql = "ALTER TABLE tTt on cluster 'cc' delete WHERE d=123 and e=456"), sql, + parse(sql = "ALTER TABLE test_db.test_table UPDATE a = 1, \"b\" = '2', `c`=3.3 WHERE d=123 and e=456"), + sql, StatementType.ALTER_UPDATE, "test_db", "test_table"); + checkSingleStatement(parse(sql = "ALTER TABLE tTt on cluster 'cc' delete WHERE d=123 and e=456"), sql, StatementType.ALTER_DELETE, "system", "tTt"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "ALTER USER user DEFAULT ROLE role1, role2"), sql, - StatementType.ALTER); + checkSingleStatement(parse(sql = "ALTER USER user DEFAULT ROLE role1, role2"), sql, StatementType.ALTER); } @Test public void testAttachStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "ATTACH TABLE IF NOT EXISTS t.t ON CLUSTER cluster"), sql, + checkSingleStatement(parse(sql = "ATTACH TABLE IF NOT EXISTS t.t ON CLUSTER cluster"), sql, StatementType.ATTACH); } @@ -100,78 +102,70 @@ public void testAttachStatement() { public void testCheckStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "check table a"), sql, StatementType.CHECK); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "check table a.a"), sql, StatementType.CHECK); + checkSingleStatement(parse(sql = "check table a"), sql, StatementType.CHECK); + checkSingleStatement(parse(sql = "check table a.a"), sql, StatementType.CHECK); } @Test public void testCreateStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "create table a(a String) engine=Memory"), sql, - StatementType.CREATE); + checkSingleStatement(parse(sql = "create table a(a String) engine=Memory"), sql, StatementType.CREATE); } @Test public void testDeleteStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "delete from a"), sql, StatementType.DELETE, "system", - "a"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "delete from c.a where upper(a)=upper(lower(b))"), sql, - StatementType.DELETE, "c", "a"); + checkSingleStatement(parse(sql = "delete from a"), sql, StatementType.DELETE, "system", "a"); + checkSingleStatement(parse(sql = "delete from c.a where upper(a)=upper(lower(b))"), sql, StatementType.DELETE, + "c", "a"); } @Test public void testDescribeStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc a"), sql, StatementType.DESCRIBE, "system", - "columns"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table a"), sql, StatementType.DESCRIBE, "system", - "columns"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "describe table a.a"), sql, StatementType.DESCRIBE, "a", - "columns"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table table"), sql, StatementType.DESCRIBE, "system", - "columns"); + checkSingleStatement(parse(sql = "desc a"), sql, StatementType.DESCRIBE, "system", "columns"); + checkSingleStatement(parse(sql = "desc table a"), sql, StatementType.DESCRIBE, "system", "columns"); + checkSingleStatement(parse(sql = "describe table a.a"), sql, StatementType.DESCRIBE, "a", "columns"); + checkSingleStatement(parse(sql = "desc table table"), sql, StatementType.DESCRIBE, "system", "columns"); } @Test public void testDetachStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "detach TABLE t"), sql, StatementType.DETACH); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "detach TABLE if exists t.t on cluster 'cc'"), sql, - StatementType.DETACH); + checkSingleStatement(parse(sql = "detach TABLE t"), sql, StatementType.DETACH); + checkSingleStatement(parse(sql = "detach TABLE if exists t.t on cluster 'cc'"), sql, StatementType.DETACH); } @Test public void testDropStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "drop TEMPORARY table t"), sql, StatementType.DROP); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "drop TABLE if exists t.t on cluster 'cc'"), sql, - StatementType.DROP); + checkSingleStatement(parse(sql = "drop TEMPORARY table t"), sql, StatementType.DROP); + checkSingleStatement(parse(sql = "drop TABLE if exists t.t on cluster 'cc'"), sql, StatementType.DROP); } @Test public void testExistsStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS TEMPORARY TABLE a"), sql, StatementType.EXISTS); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS TABLE a.a"), sql, StatementType.EXISTS); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXISTS DICTIONARY c"), sql, StatementType.EXISTS); + checkSingleStatement(parse(sql = "EXISTS TEMPORARY TABLE a"), sql, StatementType.EXISTS); + checkSingleStatement(parse(sql = "EXISTS TABLE a.a"), sql, StatementType.EXISTS); + checkSingleStatement(parse(sql = "EXISTS DICTIONARY c"), sql, StatementType.EXISTS); } @Test public void testExplainStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse( sql = "EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV"), sql, StatementType.EXPLAIN); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "EXPLAIN AST SELECT 1"), sql, StatementType.EXPLAIN); - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse(sql = "EXPLAIN AST SELECT 1"), sql, StatementType.EXPLAIN); + checkSingleStatement(parse( sql = "EXPLAIN SYNTAX SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c"), sql, StatementType.EXPLAIN); } @@ -180,17 +174,16 @@ public void testExplainStatement() { public void testGrantStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "GRANT SELECT(x,y) ON db.table TO john WITH GRANT OPTION"), - sql, StatementType.GRANT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "GRANT INSERT(x,y) ON db.table TO john"), sql, + checkSingleStatement(parse(sql = "GRANT SELECT(x,y) ON db.table TO john WITH GRANT OPTION"), sql, StatementType.GRANT); + checkSingleStatement(parse(sql = "GRANT INSERT(x,y) ON db.table TO john"), sql, StatementType.GRANT); } @Test public void testInsertStatement() throws ParseException { String sql; - ClickHouseSqlStatement s = ClickHouseSqlParser.parse(sql = "insert into table test(a,b) Values (1,2)")[0]; + ClickHouseSqlStatement s = parse(sql = "insert into table test(a,b) Values (1,2)")[0]; assertEquals(sql.substring(s.getStartPosition("values"), s.getEndPosition("VALUES")), "Values"); assertEquals(sql.substring(0, s.getEndPosition("values")) + " (1,2)", sql); @@ -202,24 +195,20 @@ public void testInsertStatement() throws ParseException { } assertEquals(s.getStartPosition("values"), valuePosition); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') values(1)"), sql, + checkSingleStatement(parse(sql = "insert into function null('a UInt8') values(1)"), sql, StatementType.INSERT); + checkSingleStatement(parse(sql = "insert into function null('a UInt8') values(1)(2)"), sql, StatementType.INSERT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') values(1)(2)"), sql, + checkSingleStatement(parse(sql = "insert into function null('a UInt8') select * from number(10)"), sql, StatementType.INSERT); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "insert into function null('a UInt8') select * from number(10)"), sql, - StatementType.INSERT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "insert into test2(a,b) values('values(',',')"), sql, - StatementType.INSERT, "system", "test2"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "INSERT INTO table t(a, b, c) values('1', ',', 'ccc')"), - sql, StatementType.INSERT, "system", "t"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "INSERT INTO table t(a, b, c) values('1', 2, 'ccc') (3,2,1)"), sql, + checkSingleStatement(parse(sql = "insert into test2(a,b) values('values(',',')"), sql, StatementType.INSERT, + "system", "test2"); + checkSingleStatement(parse(sql = "INSERT INTO table t(a, b, c) values('1', ',', 'ccc')"), sql, StatementType.INSERT, "system", "t"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "INSERT INTO table s.t select * from ttt"), sql, - StatementType.INSERT, "s", "t"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "INSERT INTO insert_select_testtable (* EXCEPT(b)) Values (2, 2)"), sql, + checkSingleStatement(parse(sql = "INSERT INTO table t(a, b, c) values('1', 2, 'ccc') (3,2,1)"), sql, + StatementType.INSERT, "system", "t"); + checkSingleStatement(parse(sql = "INSERT INTO table s.t select * from ttt"), sql, StatementType.INSERT, "s", + "t"); + checkSingleStatement(parse(sql = "INSERT INTO insert_select_testtable (* EXCEPT(b)) Values (2, 2)"), sql, StatementType.INSERT, "system", "insert_select_testtable"); } @@ -228,10 +217,9 @@ public void testInsertStatement() throws ParseException { public void testKillStatement() { String sql; - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'"), sql, + checkSingleStatement(parse(sql = "KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'"), sql, StatementType.KILL); - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse( sql = "KILL MUTATION WHERE database = 'default' AND table = 'table' AND mutation_id = 'mutation_3.txt' SYNC"), sql, StatementType.KILL); } @@ -240,20 +228,17 @@ public void testKillStatement() { public void testOptimizeStatement() { String sql; - checkSingleStatement( - ClickHouseSqlParser - .parse(sql = "OPTIMIZE TABLE a ON CLUSTER cluster PARTITION ID 'partition_id' FINAL"), - sql, StatementType.OPTIMIZE); + checkSingleStatement(parse(sql = "OPTIMIZE TABLE a ON CLUSTER cluster PARTITION ID 'partition_id' FINAL"), sql, + StatementType.OPTIMIZE); } @Test public void testRenameStatement() { String sql; - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "RENAME TABLE table1 TO table2, table3 TO table4 ON CLUSTER cluster"), - sql, StatementType.RENAME); - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse(sql = "RENAME TABLE table1 TO table2, table3 TO table4 ON CLUSTER cluster"), sql, + StatementType.RENAME); + checkSingleStatement(parse( sql = "RENAME TABLE db1.table1 TO db2.table2, db2.table3 to db2.table4, db3.table5 to db2.table6 ON CLUSTER 'c'"), sql, StatementType.RENAME); } @@ -262,145 +247,129 @@ public void testRenameStatement() { public void testRevokeStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "REVOKE SELECT ON accounts.* FROM john"), sql, - StatementType.REVOKE); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "REVOKE SELECT(wage) ON accounts.staff FROM mira"), sql, - StatementType.REVOKE); + checkSingleStatement(parse(sql = "REVOKE SELECT ON accounts.* FROM john"), sql, StatementType.REVOKE); + checkSingleStatement(parse(sql = "REVOKE SELECT(wage) ON accounts.staff FROM mira"), sql, StatementType.REVOKE); } @Test public void testSelectStatement() { String sql; - assertEquals(ClickHouseSqlParser.parse(sql = "select\n1"), new ClickHouseSqlStatement[] { + assertEquals(parse(sql = "select\n1"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); - assertEquals(ClickHouseSqlParser.parse(sql = "select\r\n1"), new ClickHouseSqlStatement[] { + assertEquals(parse(sql = "select\r\n1"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); - assertEquals(ClickHouseSqlParser.parse(sql = "select 314 limit 5\nFORMAT JSONCompact;"), + assertEquals(parse(sql = "select 314 limit 5\nFORMAT JSONCompact;"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("select 314 limit 5\nFORMAT JSONCompact", StatementType.SELECT, null, null, "unknown", "JSONCompact", null, null) }); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (())"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select []"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[]]"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select *"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select timezone()"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select @@version, $version"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select * from jdbc('db', 'schema', 'select 1')"), sql, - StatementType.SELECT, "system", "jdbc"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "select 1 as a1, a.a as a2, aa(a1, a2) a3, length(a3) as a4 from x"), - sql, StatementType.SELECT, "system", "x"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "select x.* from (select [1,2] a, (1,2,3) b, a[1], b.2) x"), sql, + checkSingleStatement(parse(sql = "select (())"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select []"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select [[]]"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select *"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select timezone()"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select @@version, $version"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select * from jdbc('db', 'schema', 'select 1')"), sql, StatementType.SELECT, + "system", "jdbc"); + checkSingleStatement(parse(sql = "select 1 as a1, a.a as a2, aa(a1, a2) a3, length(a3) as a4 from x"), sql, StatementType.SELECT, "system", "x"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (3, [[1,2],[3,4]]) as a, (a.2)[2][1]"), sql, - StatementType.SELECT); + checkSingleStatement(parse(sql = "select x.* from (select [1,2] a, (1,2,3) b, a[1], b.2) x"), sql, + StatementType.SELECT, "system", "x"); + checkSingleStatement(parse(sql = "select (3, [[1,2],[3,4]]) as a, (a.2)[2][1]"), sql, StatementType.SELECT); checkSingleStatement( - ClickHouseSqlParser - .parse(sql = "select 1,1.1,'\"''`a' a, \"'`\"\"a\" as b, (1 + `a`.a) c, null, inf i, nan as n"), - sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as select"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1, 2 a, 3 as b, 1+1-2*3/4, *, c.* from c a"), sql, + parse(sql = "select 1,1.1,'\"''`a' a, \"'`\"\"a\" as b, (1 + `a`.a) c, null, inf i, nan as n"), sql, + StatementType.SELECT); + checkSingleStatement(parse(sql = "select 1 as select"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select 1, 2 a, 3 as b, 1+1-2*3/4, *, c.* from c a"), sql, StatementType.SELECT, "system", "c"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as select"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse(sql = "select 1 as select"), sql, StatementType.SELECT); + checkSingleStatement(parse( sql = " -- cc\nselect 1 as `a.b`, a, 1+1, b from \"a\".`b` inner join a on a.abb/* \n\r\n1*/\n=2 and a.abb = c.a and a=1 and (k is null and j not in(1,2))"), sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT idx, s FROM test.mymetadata WHERE idx = ?"), sql, - StatementType.SELECT, "test", "mymetadata"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "WITH 2 AS two SELECT two * two"), sql, - StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse( + checkSingleStatement(parse(sql = "SELECT idx, s FROM test.mymetadata WHERE idx = ?"), sql, StatementType.SELECT, + "test", "mymetadata"); + checkSingleStatement(parse(sql = "WITH 2 AS two SELECT two * two"), sql, StatementType.SELECT); + checkSingleStatement(parse( sql = "SELECT i, array(toUnixTimestamp(dt_server[1])), array(toUnixTimestamp(dt_berlin[1])), array(toUnixTimestamp(dt_lax[1])) FROM test.fun_with_timezones_array"), sql, StatementType.SELECT, "test", "fun_with_timezones_array"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT SUM(x) FROM t WHERE y = ? GROUP BY ?"), sql, - StatementType.SELECT, "system", "t"); + checkSingleStatement(parse(sql = "SELECT SUM(x) FROM t WHERE y = ? GROUP BY ?"), sql, StatementType.SELECT, + "system", "t"); - assertEquals(ClickHouseSqlParser.parse(sql = loadSql("issue-441_with-totals.sql")), + assertEquals(parse(sql = loadSql("issue-441_with-totals.sql")), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, new HashMap() { { put("TOTALS", 208); } }) }); - assertEquals(ClickHouseSqlParser.parse(sql = loadSql("issue-555_custom-format.sql")), - new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", - "CSVWithNames", null, null) }); + assertEquals(parse(sql = loadSql("issue-555_custom-format.sql")), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", "CSVWithNames", null, null) }); } @Test public void testSetStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SET profile = 'my-profile', mutations_sync=1"), sql, - StatementType.SET); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SET DEFAULT ROLE role1, role2, role3 TO user"), sql, - StatementType.SET); + checkSingleStatement(parse(sql = "SET profile = 'my-profile', mutations_sync=1"), sql, StatementType.SET); + checkSingleStatement(parse(sql = "SET DEFAULT ROLE role1, role2, role3 TO user"), sql, StatementType.SET); } @Test public void testShowStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SHOW DATABASES LIKE '%de%'"), sql, StatementType.SHOW, - "system", "databases"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "show tables from db"), sql, StatementType.SHOW, "system", - "tables"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "show dictionaries from db"), sql, StatementType.SHOW, - "system", "dictionaries"); + checkSingleStatement(parse(sql = "SHOW DATABASES LIKE '%de%'"), sql, StatementType.SHOW, "system", "databases"); + checkSingleStatement(parse(sql = "show tables from db"), sql, StatementType.SHOW, "system", "tables"); + checkSingleStatement(parse(sql = "show dictionaries from db"), sql, StatementType.SHOW, "system", + "dictionaries"); } @Test public void testSystemStatement() { String sql; - checkSingleStatement( - ClickHouseSqlParser - .parse(sql = "SYSTEM DROP REPLICA 'replica_name' FROM ZKPATH '/path/to/table/in/zk'"), - sql, StatementType.SYSTEM); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "SYSTEM RESTART REPLICA db.replicated_merge_tree_family_table_name"), - sql, StatementType.SYSTEM); + checkSingleStatement(parse(sql = "SYSTEM DROP REPLICA 'replica_name' FROM ZKPATH '/path/to/table/in/zk'"), sql, + StatementType.SYSTEM); + checkSingleStatement(parse(sql = "SYSTEM RESTART REPLICA db.replicated_merge_tree_family_table_name"), sql, + StatementType.SYSTEM); } @Test public void testTruncateStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "truncate table a.b"), sql, StatementType.TRUNCATE, "a", - "b"); + checkSingleStatement(parse(sql = "truncate table a.b"), sql, StatementType.TRUNCATE, "a", "b"); } @Test public void testUpdateStatement() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "update a set a='1'"), sql, StatementType.UPDATE); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "update a.a set `a`=2 where upper(a)=upper(lower(b))"), - sql, StatementType.UPDATE); + checkSingleStatement(parse(sql = "update a set a='1'"), sql, StatementType.UPDATE); + checkSingleStatement(parse(sql = "update a.a set `a`=2 where upper(a)=upper(lower(b))"), sql, + StatementType.UPDATE); } @Test public void testUseStatement() throws ParseException { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "use system"), sql, StatementType.USE); + checkSingleStatement(parse(sql = "use system"), sql, StatementType.USE); } @Test public void testWatchStatement() throws ParseException { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "watch system.processes"), sql, StatementType.WATCH); + checkSingleStatement(parse(sql = "watch system.processes"), sql, StatementType.WATCH); } @Test public void testMultipleStatements() throws ParseException { - assertEquals(ClickHouseSqlParser.parse("use ab;;;select 1; ;\t;\r;\n"), + assertEquals(parse("use ab;;;select 1; ;\t;\r;\n"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("use ab", StatementType.USE, null, "ab", null, null, null, null), new ClickHouseSqlStatement("select 1", StatementType.SELECT) }); - assertEquals(ClickHouseSqlParser.parse("select * from \"a;1\".`b;c`;;;select 1 as `a ; a`; ;\t;\r;\n"), + assertEquals(parse("select * from \"a;1\".`b;c`;;;select 1 as `a ; a`; ;\t;\r;\n"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("select * from \"a;1\".`b;c`", StatementType.SELECT, null, "a;1", "b;c", null, null, null), @@ -410,76 +379,64 @@ public void testMultipleStatements() throws ParseException { @Test public void testAlias() throws ParseException { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 as c, 2 b"), sql, StatementType.SELECT); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from a.b c"), sql, StatementType.SELECT, "a", - "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 select from a.b c"), sql, StatementType.SELECT, - "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from (select 2) b"), sql, StatementType.SELECT, - "system", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from (select 2) as from"), sql, - StatementType.SELECT, "system", "from"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1 from a.b c1, b.a c2"), sql, StatementType.SELECT, - "a", "b"); + checkSingleStatement(parse(sql = "select 1 as c, 2 b"), sql, StatementType.SELECT); + checkSingleStatement(parse(sql = "select 1 from a.b c"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select 1 select from a.b c"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select 1 from (select 2) b"), sql, StatementType.SELECT, "system", "b"); + checkSingleStatement(parse(sql = "select 1 from (select 2) as from"), sql, StatementType.SELECT, "system", + "from"); + checkSingleStatement(parse(sql = "select 1 from a.b c1, b.a c2"), sql, StatementType.SELECT, "a", "b"); } @Test public void testExpression() throws ParseException { String sql; - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "select 1 and `a`.\"b\" c1, c1 or (c2 and c3), c4 ? c5 : c6 from a.b"), - sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [[[1,2],[3,4],[5,6]]] a, a[1][1][2] from a.b"), - sql, StatementType.SELECT, "a", "b"); - checkSingleStatement( - ClickHouseSqlParser.parse( - sql = "select [[[[]]]], a[1][2][3], ([[1]] || [[2]])[2][1] ,func(1,2) [1] [2] [ 3 ] from a.b"), - sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select c.c1, c.c2 c, c.c3 as cc, c.c4.1.2 from a.b"), sql, + checkSingleStatement(parse(sql = "select 1 and `a`.\"b\" c1, c1 or (c2 and c3), c4 ? c5 : c6 from a.b"), sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select - (select (1,).1) from a.b"), sql, - StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1.1e1,(1) . 1 , ((1,2)).1 .2 . 3 from a.b"), sql, - StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select a.b.c1, c1, b.c1 from a.b"), sql, + checkSingleStatement(parse(sql = "select [[[1,2],[3,4],[5,6]]] a, a[1][1][2] from a.b"), sql, StatementType.SELECT, "a", "b"); checkSingleStatement( - ClickHouseSqlParser.parse(sql = "select date'2020-02-04', timestamp '2020-02-04' from a.b"), sql, - StatementType.SELECT, "a", "b"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = "select count (), sum(c1), fake(a1, count(), (1+1)) from a.b"), sql, - StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select {}, {'a':'b', 'c':'1'} from a.b"), sql, - StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select [], [1,2], [ [1,2], [3,4] ] from a.b"), sql, + parse(sql = "select [[[[]]]], a[1][2][3], ([[1]] || [[2]])[2][1] ,func(1,2) [1] [2] [ 3 ] from a.b"), + sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select c.c1, c.c2 c, c.c3 as cc, c.c4.1.2 from a.b"), sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select 1+1-1*1/1 from a.b"), sql, StatementType.SELECT, + checkSingleStatement(parse(sql = "select - (select (1,).1) from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select 1.1e1,(1) . 1 , ((1,2)).1 .2 . 3 from a.b"), sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (1+(1-1)*1/1)-1 from a.b"), sql, + checkSingleStatement(parse(sql = "select a.b.c1, c1, b.c1 from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select date'2020-02-04', timestamp '2020-02-04' from a.b"), sql, StatementType.SELECT, "a", "b"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "select (1+(1+(-1))*1/1)-(select (1,).1) from a.b"), sql, + checkSingleStatement(parse(sql = "select count (), sum(c1), fake(a1, count(), (1+1)) from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select {}, {'a':'b', 'c':'1'} from a.b"), sql, StatementType.SELECT, "a", + "b"); + checkSingleStatement(parse(sql = "select [], [1,2], [ [1,2], [3,4] ] from a.b"), sql, StatementType.SELECT, "a", + "b"); + checkSingleStatement(parse(sql = "select 1+1-1*1/1 from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select (1+(1-1)*1/1)-1 from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select (1+(1+(-1))*1/1)-(select (1,).1) from a.b"), sql, StatementType.SELECT, + "a", "b"); } @Test public void testFormat() throws ParseException { String sql = "select 1 as format, format csv"; - ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + ClickHouseSqlStatement[] stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasFormat(), false); assertEquals(stmts[0].getFormat(), null); sql = "select 1 format csv"; - stmts = ClickHouseSqlParser.parse(sql); + stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasFormat(), true); assertEquals(stmts[0].getFormat(), "csv"); sql = "select 1 a, a.a b, a.a.a c, e.* except(e1), e.e.* except(e2), 'aaa' format, format csv from numbers(2) FORMAT CSVWithNames"; - stmts = ClickHouseSqlParser.parse(sql); + stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasFormat(), true); @@ -489,14 +446,14 @@ public void testFormat() throws ParseException { @Test public void testOutfile() throws ParseException { String sql = "select 1 into outfile '1.txt'"; - ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + ClickHouseSqlStatement[] stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasOutfile(), true); assertEquals(stmts[0].getOutfile(), "'1.txt'"); sql = "insert into outfile values(1,2,3)"; - stmts = ClickHouseSqlParser.parse(sql); + stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasOutfile(), false); @@ -506,13 +463,13 @@ public void testOutfile() throws ParseException { @Test public void testWithTotals() throws ParseException { String sql = "select 1 as with totals"; - ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + ClickHouseSqlStatement[] stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasWithTotals(), false); sql = "select 1 with totals"; - stmts = ClickHouseSqlParser.parse(sql); + stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); assertEquals(stmts[0].hasWithTotals(), true); @@ -521,7 +478,7 @@ public void testWithTotals() throws ParseException { @Test public void testParameterHandling() throws ParseException { String sql = "insert into table d.t(a1, a2, a3) values(?,?,?)"; - ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + ClickHouseSqlStatement[] stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), sql); @@ -543,7 +500,7 @@ public String handleParameter(String cluster, String database, String table, int @Test public void testMacroHandling() throws ParseException { String sql = "select #listOfColumns #ignored from (#subQuery('1','2','3'))"; - ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql); + ClickHouseSqlStatement[] stmts = parse(sql); assertEquals(stmts.length, 1); assertEquals(stmts[0].getSQL(), "select from ()"); @@ -572,56 +529,40 @@ public String handleParameter(String cluster, String database, String table, int public void testExtractDBAndTableName() { String sql; - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from table"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from table a"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from\ntable a"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1\nfrom\ntable a"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1\nFrom\ntable a"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from db.table a"), sql, StatementType.SELECT, - "db", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from \"db.table\" a"), sql, - StatementType.SELECT, "system", "db.table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 1 from `db.table` a"), sql, StatementType.SELECT, - "system", "db.table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "from `db.table` a"), sql, StatementType.UNKNOWN, "system", - "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " from `db.table` a"), sql, StatementType.UNKNOWN, - "system", "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "ELECT from `db.table` a"), sql, StatementType.UNKNOWN, - "system", "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SHOW tables"), sql, StatementType.SHOW, "system", - "tables"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "desc table1"), sql, StatementType.DESCRIBE, "system", - "columns"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "DESC table1"), sql, StatementType.DESCRIBE, "system", - "columns"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT 'from db.table a' from tab"), sql, - StatementType.SELECT, "system", "tab"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT"), sql, StatementType.UNKNOWN, "system", - "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "S"), sql, StatementType.UNKNOWN, "system", "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = ""), sql, StatementType.UNKNOWN, "system", "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, - "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, + checkSingleStatement(parse(sql = "SELECT 1 from table"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT 1 from table a"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT 1 from\ntable a"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT 1\nfrom\ntable a"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT 1\nFrom\ntable a"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT 1 from db.table a"), sql, StatementType.SELECT, "db", "table"); + checkSingleStatement(parse(sql = " SELECT 1 from \"db.table\" a"), sql, StatementType.SELECT, "system", + "db.table"); + checkSingleStatement(parse(sql = "SELECT 1 from `db.table` a"), sql, StatementType.SELECT, "system", + "db.table"); + checkSingleStatement(parse(sql = "from `db.table` a"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = " from `db.table` a"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = "ELECT from `db.table` a"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = "SHOW tables"), sql, StatementType.SHOW, "system", "tables"); + checkSingleStatement(parse(sql = "desc table1"), sql, StatementType.DESCRIBE, "system", "columns"); + checkSingleStatement(parse(sql = "DESC table1"), sql, StatementType.DESCRIBE, "system", "columns"); + checkSingleStatement(parse(sql = "SELECT 'from db.table a' from tab"), sql, StatementType.SELECT, "system", + "tab"); + checkSingleStatement(parse(sql = "SELECT"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = "S"), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = ""), sql, StatementType.UNKNOWN, "system", "unknown"); + checkSingleStatement(parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = " SELECT 1 from table from"), sql, StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = "SELECT fromUnixTimestamp64Milli(time) as x from table"), sql, + StatementType.SELECT, "system", "table"); + checkSingleStatement(parse(sql = " SELECT fromUnixTimestamp64Milli(time)from table"), sql, StatementType.SELECT, "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "SELECT fromUnixTimestamp64Milli(time) as x from table"), - sql, StatementType.SELECT, "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from table"), sql, + checkSingleStatement(parse(sql = "/*qq*/ SELECT fromUnixTimestamp64Milli(time)from table"), sql, StatementType.SELECT, "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = "/*qq*/ SELECT fromUnixTimestamp64Milli(time)from table"), - sql, StatementType.SELECT, "system", "table"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECTfromUnixTimestamp64Milli(time)from table"), sql, - StatementType.UNKNOWN, "system", "unknown"); - checkSingleStatement(ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from \".inner.a\""), - sql, StatementType.SELECT, "system", ".inner.a"); - checkSingleStatement( - ClickHouseSqlParser.parse(sql = " SELECT fromUnixTimestamp64Milli(time)from db.`.inner.a`"), sql, + checkSingleStatement(parse(sql = " SELECTfromUnixTimestamp64Milli(time)from table"), sql, StatementType.UNKNOWN, + "system", "unknown"); + checkSingleStatement(parse(sql = " SELECT fromUnixTimestamp64Milli(time)from \".inner.a\""), sql, + StatementType.SELECT, "system", ".inner.a"); + checkSingleStatement(parse(sql = " SELECT fromUnixTimestamp64Milli(time)from db.`.inner.a`"), sql, StatementType.SELECT, "db", ".inner.a"); } } From b0eea78839cefaf82d25705425a4d5f24e58394e Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Fri, 12 Feb 2021 17:26:47 +0800 Subject: [PATCH 4/5] Enhance the parser so that it can deal with test cases from ClickHouse repo --- .../clickhouse/ClickHouseStatementImpl.java | 3 +- .../jdbc/parser/ClickHouseSqlStatement.java | 5 +- src/main/javacc/ClickHouseSqlParser.jj | 218 +++++++++++++----- .../jdbc/parser/ClickHouseSqlParserTest.java | 68 +++++- src/test/resources/sqls/with-clause.sql | 16 ++ 5 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 src/test/resources/sqls/with-clause.sql diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java index 54078f982..70d949edd 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java @@ -13,6 +13,7 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; @@ -584,7 +585,7 @@ private String extractDBAndTableName(String sql) { private boolean extractWithTotals(String sql) { if (Utils.startsWithIgnoreCase(sql, "select")) { String withoutStrings = Utils.retainUnquoted(sql, '\''); - return withoutStrings.toLowerCase().contains(" with totals"); + return withoutStrings.toLowerCase(Locale.ROOT).contains(" with totals"); } return false; } diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java index 93e0a0853..5da932589 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -52,7 +53,7 @@ public ClickHouseSqlStatement(String sql, StatementType stmtType, String cluster Integer position = e.getValue(); if (keyword != null && position != null) { - p.put(keyword.toUpperCase(), position); + p.put(keyword.toUpperCase(Locale.ROOT), position); } } this.positions = Collections.unmodifiableMap(p); @@ -145,7 +146,7 @@ public int getStartPosition(String keyword) { int position = -1; if (!this.positions.isEmpty() && keyword != null) { - Integer p = this.positions.get(keyword.toUpperCase()); + Integer p = this.positions.get(keyword.toUpperCase(Locale.ROOT)); if (p != null) { position = p.intValue(); } diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj index c1454244b..05f3f8854 100644 --- a/src/main/javacc/ClickHouseSqlParser.jj +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -65,6 +65,11 @@ public class ClickHouseSqlParser { return matched; } + // FIXME ugly workaround but performs better than adding another lexical state for ... + private boolean noAndWithinBetween() { + return !(getToken(1).kind == AND && token_source.parentToken == BETWEEN); + } + /** * Parse given SQL. * @@ -141,7 +146,11 @@ public class ClickHouseSqlParser { public void addStatement() { if (token_source.isValid()) { - statements.add(token_source.build()); + ClickHouseSqlStatement sqlStmt = token_source.build(); + // FIXME remove the restriction once we can hanlde insertion with format well + if (statements.size() == 0 || sqlStmt.isRecognized()) { + statements.add(sqlStmt); + } } else { token_source.reset(); } @@ -151,7 +160,13 @@ public class ClickHouseSqlParser { PARSER_END(ClickHouseSqlParser) TOKEN_MGR_DECLS: { - private int validTokens = 0; // whitespaces and comments are invalid + // whitespaces and comments are invalid + private int validTokens = 0; + // see http://www.engr.mun.ca/~theo/JavaCC-FAQ/javacc-faq-moz.htm#tth_sEc3.17 + private int commentNestingDepth = 0; + + final java.util.Deque stack = new java.util.LinkedList<>(); + int parentToken = -1; final StringBuilder builder = new StringBuilder(); @@ -175,6 +190,22 @@ TOKEN_MGR_DECLS: { } } + void enterToken(int tokenKind) { + if (tokenKind < 0) { + return; + } + + stack.push(parentToken = tokenKind); + } + + void leaveToken(int tokenKind) { + if (parentToken == tokenKind) { + stack.pop(); + } + + parentToken = stack.isEmpty() ? -1 : stack.getLast(); + } + void processMacro(String name, List params, ParseHandler handler) { StringBuilder m = new StringBuilder(); m.append('#').append(name); @@ -209,6 +240,9 @@ TOKEN_MGR_DECLS: { } void reset() { + stack.clear(); + parentToken = -1; + builder.setLength(validTokens = 0); stmtType = StatementType.UNKNOWN; @@ -245,15 +279,18 @@ TOKEN_MGR_DECLS: { } SKIP: { - + { append(image); } + | { append(image); } + | "/*" { commentNestingDepth = 1; append(image); }: MULTI_LINE_COMMENT } -SPECIAL_TOKEN: { - - { append(image); } - | - { append(image); } + SKIP: { + "/*" { commentNestingDepth += 1; append(image); } + | "*/" { SwitchTo(--commentNestingDepth == 0 ? DEFAULT : MULTI_LINE_COMMENT); append(image); } + | < ~[] > { append(image); } } // top-level statements @@ -306,11 +343,12 @@ void alterStmt(): {} { ( LOOKAHEAD(2) -

tableIdentifier(true) (clusterClause())? ( - { token_source.stmtType = StatementType.ALTER_UPDATE; } +
tableIdentifier(true) (LOOKAHEAD(2) clusterClause())? ( + LOOKAHEAD({ !tokenIn(1, UPDATE, DELETE) }) anyIdentifier() + | { token_source.stmtType = StatementType.ALTER_UPDATE; } | { token_source.stmtType = StatementType.ALTER_DELETE; } ) - )? anyExprList() + )? (anyExprList())? } void clusterClause(): { Token t; } { @@ -381,15 +419,23 @@ void insertStmt(): {} { } void dataClause(): { Token t; } { - LOOKAHEAD(2) anyIdentifier() - | LOOKAHEAD(2) t = { token_source.setPosition(t.image); } - columnExprList() - ( - LOOKAHEAD(2) - ()? + try { + LOOKAHEAD(2) anyIdentifier() (LOOKAHEAD(2) anyExprList())? + | LOOKAHEAD(2) t = { token_source.setPosition(t.image); } columnExprList() - )* - | anyExprList() // not interested + ( + LOOKAHEAD(2) + ()? + columnExprList() + )* + | anyExprList() // not interested + } catch (ParseException e) { + // FIXME introduce a lexical state in next release with consideration of delimiter from the context + Token nextToken; + do { + nextToken = getNextToken(); + } while(nextToken.kind != SEMICOLON && nextToken.kind != EOF); + } } // https://clickhouse.tech/docs/en/sql-reference/statements/kill/ @@ -415,19 +461,26 @@ void revokeStmt(): {} { // not interested // https://clickhouse.tech/docs/en/sql-reference/statements/select/ void selectStmt(): {} { // FIXME with (select 1), (select 2), 3 select * - ( (LOOKAHEAD(2) columnExprList() | anyExprList() ))? + (withClause())? | t = | t = | t = | t = | t = | t = | t = // others - | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t =
| t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t =
| t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = // interval | t = | t = | t = | t = | t = | t = | t = | t = // values @@ -802,6 +905,7 @@ TOKEN: { |

> | > | > + | > | > | > | > @@ -809,8 +913,10 @@ TOKEN: { | > | > | > + | > | > | > + | > | > | > | > @@ -922,7 +1028,7 @@ TOKEN: { | | | - | + | <#UNDERSCORE: "_"> } // string literal @@ -931,18 +1037,26 @@ TOKEN: { } TOKEN: { - | | ) ( | | | )*> + | | ) ( | | | )* + | ()+ ( + ( | )* + | ( + | | | | | | | | | | | | + | |

| | | | | | | | | | + | + ) ( | | )* + )> | ( ~[] | ~["`", "\\"] | "``")* > | ( ~[] | ~["\"", "\\"] | "\"\"")* > } TOKEN: { - ( )*> - | ()? (

| ) ( | )? | (

| ) ( | )? | ()? ( ( | )? )? - // | ( ( | )? )? + | ( ( | )? )? | ( | )? > } TOKEN: { )+> } diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java index 70ca0d7c2..6b2ddd38b 100644 --- a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -7,9 +7,12 @@ import static org.testng.Assert.assertEquals; import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; @@ -82,6 +85,8 @@ public void testParseNonSql() throws ParseException { public void testAlterStatement() { String sql; + checkSingleStatement(parse(sql = "ALTER TABLE alter_test ADD COLUMN Added0 UInt32"), sql, StatementType.ALTER, + "system", "alter_test"); checkSingleStatement( parse(sql = "ALTER TABLE test_db.test_table UPDATE a = 1, \"b\" = '2', `c`=3.3 WHERE d=123 and e=456"), sql, StatementType.ALTER_UPDATE, "test_db", "test_table"); @@ -305,6 +310,8 @@ public void testSelectStatement() { }) }); assertEquals(parse(sql = loadSql("issue-555_custom-format.sql")), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", "CSVWithNames", null, null) }); + assertEquals(parse(sql = loadSql("with-clause.sql")), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); } @Test @@ -363,6 +370,19 @@ public void testWatchStatement() throws ParseException { checkSingleStatement(parse(sql = "watch system.processes"), sql, StatementType.WATCH); } + @Test + public void testComments() throws ParseException { + String sql; + checkSingleStatement(parse(sql = "select\n--something\n//else\n1/*2*/ from a.b"), sql, StatementType.SELECT, + "a", "b"); + + checkSingleStatement(parse(sql = "select 1/*/**/*/ from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select 1/*/1/**/*2*/ from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "SELECT /*/**/*/ 1 from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "SELECT /*a/*b*/c*/ 1 from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "SELECT /*ab/*cd*/ef*/ 1 from a.b"), sql, StatementType.SELECT, "a", "b"); + } + @Test public void testMultipleStatements() throws ParseException { assertEquals(parse("use ab;;;select 1; ;\t;\r;\n"), @@ -391,7 +411,14 @@ public void testAlias() throws ParseException { @Test public void testExpression() throws ParseException { String sql; - + checkSingleStatement(parse(sql = "SELECT a._ from a.b"), sql, StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "SELECT 2 BETWEEN 1 + 1 AND 3 - 1 from a.b"), sql, StatementType.SELECT, "a", + "b"); + checkSingleStatement(parse(sql = "SELECT CASE WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END from a.b"), sql, + StatementType.SELECT, "a", "b"); + checkSingleStatement(parse(sql = "select (1,2) a1, a1.1, a1 .1, a1 . 1 from a.b"), sql, StatementType.SELECT, + "a", "b"); + checkSingleStatement(parse(sql = "select -.0, +.0, -a from a.b"), sql, StatementType.SELECT, "a", "b"); checkSingleStatement(parse(sql = "select 1 and `a`.\"b\" c1, c1 or (c2 and c3), c4 ? c5 : c6 from a.b"), sql, StatementType.SELECT, "a", "b"); checkSingleStatement(parse(sql = "select [[[1,2],[3,4],[5,6]]] a, a[1][1][2] from a.b"), sql, @@ -565,4 +592,43 @@ public void testExtractDBAndTableName() { checkSingleStatement(parse(sql = " SELECT fromUnixTimestamp64Milli(time)from db.`.inner.a`"), sql, StatementType.SELECT, "db", ".inner.a"); } + + static void parseAllSqlFiles(File f) throws IOException { + if (f.isDirectory()) { + File[] files = f.listFiles(); + for (File file : files) { + parseAllSqlFiles(file); + } + } else if (f.getName().endsWith(".sql")) { + StringBuilder sql = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + sql.append(line).append("\n"); + } + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + + ClickHouseSqlParser p = new ClickHouseSqlParser(sql.toString(), null, null); + try { + p.sql(); + } catch (ParseException e) { + System.out.println(f.getAbsolutePath() + " -> " + e.getMessage()); + } catch (TokenMgrException e) { + System.out.println(f.getAbsolutePath() + " -> " + e.getMessage()); + } + } + } + + // TODO: add a sub-module points to ClickHouse/tests/queries? + public static void main(String[] args) throws Exception { + String chTestQueryDir = "D:/Sources/Github/ch/queries"; + if (args != null && args.length > 0) { + chTestQueryDir = args[0]; + } + chTestQueryDir = System.getProperty("chTestQueryDir", chTestQueryDir); + parseAllSqlFiles(new File(chTestQueryDir)); + } } diff --git a/src/test/resources/sqls/with-clause.sql b/src/test/resources/sqls/with-clause.sql new file mode 100644 index 000000000..1b3abb3c7 --- /dev/null +++ b/src/test/resources/sqls/with-clause.sql @@ -0,0 +1,16 @@ +WITH ( + ( + SELECT query_start_time_microseconds + FROM system.query_log + WHERE current_database = currentDatabase() + ORDER BY query_start_time DESC + LIMIT 1 + ) AS time_with_microseconds, + ( + SELECT query_start_time + FROM system.query_log + WHERE current_database = currentDatabase() + ORDER BY query_start_time DESC + LIMIT 1 + ) AS t) +SELECT if(dateDiff('second', toDateTime(time_with_microseconds), toDateTime(t)) = 0, 'ok', 'fail') \ No newline at end of file From 91c0059d610765152f62fbff93ba2a33fd6c60d6 Mon Sep 17 00:00:00 2001 From: Zhichun Wu Date: Sat, 13 Feb 2021 22:56:19 +0800 Subject: [PATCH 5/5] Enhance parser to recognize idempotent operations --- .../ClickHousePreparedStatementImpl.java | 13 +- .../clickhouse/ClickHouseStatementImpl.java | 126 +++++++++++++---- .../clickhouse/PreparedStatementParser.java | 24 ++-- .../jdbc/parser/ClickHouseSqlStatement.java | 61 +++++++- .../clickhouse/jdbc/parser/ParseHandler.java | 30 +++- .../clickhouse/jdbc/parser/StatementType.java | 62 ++++---- src/main/javacc/ClickHouseSqlParser.jj | 133 +++++++++++++----- .../jdbc/parser/ClickHouseSqlParserTest.java | 46 +++--- .../util/ClickHouseHttpClientBuilderTest.java | 4 +- 9 files changed, 348 insertions(+), 151 deletions(-) diff --git a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java index 2681d0726..a0e64175e 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java @@ -36,7 +36,6 @@ import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; -import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser; import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; import ru.yandex.clickhouse.jdbc.parser.StatementType; import ru.yandex.clickhouse.response.ClickHouseResponse; @@ -55,7 +54,6 @@ public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl imp private final TimeZone dateTimeZone; private final TimeZone dateTimeTimeZone; - private final ClickHouseSqlStatement parsedSql; private final String sql; private final List sqlParts; private final ClickHousePreparedStatementParameter[] binds; @@ -68,9 +66,11 @@ public ClickHousePreparedStatementImpl(CloseableHttpClient client, TimeZone serverTimeZone, int resultSetType) throws SQLException { super(client, connection, properties, resultSetType); - this.parsedSql = ClickHouseSqlParser.parseSingleStatement(sql, properties); + parseSingleStatement(sql); + this.sql = sql; - PreparedStatementParser parser = PreparedStatementParser.parse(sql); + PreparedStatementParser parser = PreparedStatementParser.parse(sql, + parsedStmt.getEndPosition(ClickHouseSqlStatement.KEYWORD_VALUES)); this.parameterList = parser.getParameters(); this.insertBatchMode = parser.isValuesMode(); this.sqlParts = parser.getParts(); @@ -352,8 +352,8 @@ public int[] executeBatch() throws SQLException { @Override public int[] executeBatch(Map additionalDBParams) throws SQLException { int valuePosition = -1; - if (parsedSql.getStatementType() == StatementType.INSERT && parsedSql.hasValues()) { - valuePosition = parsedSql.getStartPosition(ClickHouseSqlStatement.KEYWORD_VALUES); + if (parsedStmt.getStatementType() == StatementType.INSERT && parsedStmt.hasValues()) { + valuePosition = parsedStmt.getStartPosition(ClickHouseSqlStatement.KEYWORD_VALUES); } else { Matcher matcher = VALUES.matcher(sql); if (matcher.find()) { @@ -442,7 +442,6 @@ public ResultSetMetaData getMetaData() throws SQLException { return currentResult.getMetaData(); } - ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); if (!parsedStmt.isQuery() || (!parsedStmt.isRecognized() && !isSelect(sql))) { return null; } diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java index 70d949edd..947c877f4 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java @@ -89,6 +89,8 @@ public class ClickHouseStatementImpl extends ConfigurableApi positions = new HashMap<>(); + positions.putAll(parsedStmt.getPositions()); + positions.put(ClickHouseSqlStatement.KEYWORD_FORMAT, sql.length()); + + sql = new StringBuilder(parsedStmt.getSQL()).append("\nFORMAT ").append(format).append(';') + .toString(); + parsedStmt = new ClickHouseSqlStatement(sql, parsedStmt.getStatementType(), + parsedStmt.getCluster(), parsedStmt.getDatabase(), parsedStmt.getTable(), + format, parsedStmt.getOutfile(), parsedStmt.getParameters(), positions); + } + } + public ClickHouseStatementImpl(CloseableHttpClient client, ClickHouseConnection connection, ClickHouseProperties properties, int resultSetType) { super(null); @@ -141,21 +180,23 @@ public ResultSet executeQuery(String sql, } additionalDBParams.put(ClickHouseQueryParam.EXTREMES, "0"); - InputStream is = getInputStream(sql, additionalDBParams, externalData, additionalRequestParams); + parseSingleStatement(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes); + if (!parsedStmt.isRecognized() && isSelect(sql)) { + Map positions = new HashMap<>(); + String dbName = extractDBName(sql); + String tableName = extractTableName(sql); + if (extractWithTotals(sql)) { + positions.put(ClickHouseSqlStatement.KEYWORD_TOTALS, 1); + } + parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT, + null, dbName, tableName, null, null, null, positions); + // httpContext.setAttribute("is_idempotent", Boolean.TRUE); + } - ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); + InputStream is = getInputStream(sql, additionalDBParams, externalData, additionalRequestParams); + try { - if (parsedStmt.isQuery() || (!parsedStmt.isRecognized() && isSelect(sql))) { - if (!parsedStmt.isRecognized()) { - Map positions = new HashMap<>(); - String dbName = extractDBName(sql); - String tableName = extractTableName(sql); - if (extractWithTotals(sql)) { - positions.put(ClickHouseSqlStatement.KEYWORD_TOTALS, 1); - } - parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT, - null, dbName, tableName, null, null, positions); - } + if (parsedStmt.isQuery()) { currentUpdateCount = -1; currentResult = createResultSet(properties.isCompress() ? new ClickHouseLZ4Stream(is) : is, properties.getBufferSize(), @@ -193,8 +234,15 @@ public ClickHouseResponse executeQueryClickhouseResponse(String sql, Map additionalDBParams, Map additionalRequestParams) throws SQLException { + parseSingleStatement(sql, ClickHouseFormat.JSONCompact); + if (parsedStmt.isRecognized()) { + sql = parsedStmt.getSQL(); + } else { + sql = addFormatIfAbsent(sql, ClickHouseFormat.JSONCompact); + } + InputStream is = getInputStream( - addFormatIfAbsent(sql, properties, ClickHouseFormat.JSONCompact), + sql, additionalDBParams, null, additionalRequestParams @@ -223,15 +271,27 @@ public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(Stri @Override public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(String sql, Map additionalDBParams, Map additionalRequestParams) throws SQLException { + parseSingleStatement(sql, ClickHouseFormat.RowBinary); + if (parsedStmt.isRecognized()) { + sql = parsedStmt.getSQL(); + } else { + sql = addFormatIfAbsent(sql, ClickHouseFormat.RowBinary); + if (isSelect(sql)) { + parsedStmt = new ClickHouseSqlStatement(sql, StatementType.SELECT); + // httpContext.setAttribute("is_idempotent", Boolean.TRUE); + } else { + parsedStmt = new ClickHouseSqlStatement(sql, StatementType.UNKNOWN); + } + } + InputStream is = getInputStream( - addFormatIfAbsent(sql, properties, ClickHouseFormat.RowBinary), + sql, additionalDBParams, null, additionalRequestParams ); - ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(sql, properties); try { - if (parsedStmt.isQuery() || (!parsedStmt.isRecognized() && isSelect(sql))) { + if (parsedStmt.isQuery()) { currentUpdateCount = -1; currentRowBinaryResult = new ClickHouseRowBinaryInputStream(properties.isCompress() ? new ClickHouseLZ4Stream(is) : is, getConnection().getTimeZone(), properties); @@ -249,6 +309,8 @@ public ClickHouseRowBinaryInputStream executeQueryClickhouseRowBinaryStream(Stri @Override public int executeUpdate(String sql) throws SQLException { + parseSingleStatement(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes); + InputStream is = null; try { is = getInputStream(sql, null, null, null); @@ -490,24 +552,20 @@ public ClickHouseResponseSummary getResponseSummary() { @Deprecated static String clickhousifySql(String sql) { - return clickhousifySql(sql, null); - } - - static String clickhousifySql(String sql, ClickHouseProperties properties) { - return addFormatIfAbsent(sql, properties, ClickHouseFormat.TabSeparatedWithNamesAndTypes); + return addFormatIfAbsent(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes); } /** * Adding FORMAT TabSeparatedWithNamesAndTypes if not added * adds format only to select queries */ - private static String addFormatIfAbsent(final String sql, ClickHouseProperties properties, ClickHouseFormat format) { + @Deprecated + private static String addFormatIfAbsent(final String sql, ClickHouseFormat format) { String cleanSQL = sql.trim(); - ClickHouseSqlStatement parsedStmt = ClickHouseSqlParser.parseSingleStatement(cleanSQL, properties); - if (!parsedStmt.isQuery() || (!parsedStmt.isRecognized() && !isSelect(cleanSQL))) { + if (!isSelect(cleanSQL)) { return cleanSQL; } - if (parsedStmt.hasFormat() || (!parsedStmt.isRecognized() && ClickHouseFormat.containsFormat(cleanSQL))) { + if (ClickHouseFormat.containsFormat(cleanSQL)) { return cleanSQL; } StringBuilder sb = new StringBuilder(); @@ -542,6 +600,7 @@ static boolean isSelect(String sql) { return false; } + @Deprecated private String extractTableName(String sql) { String s = extractDBAndTableName(sql); if (s.contains(".")) { @@ -551,6 +610,7 @@ private String extractTableName(String sql) { } } + @Deprecated private String extractDBName(String sql) { String s = extractDBAndTableName(sql); if (s.contains(".")) { @@ -560,6 +620,7 @@ private String extractDBName(String sql) { } } + @Deprecated private String extractDBAndTableName(String sql) { if (Utils.startsWithIgnoreCase(sql, "select")) { String withoutStrings = Utils.retainUnquoted(sql, '\''); @@ -582,6 +643,7 @@ private String extractDBAndTableName(String sql) { return "system.unknown"; } + @Deprecated private boolean extractWithTotals(String sql) { if (Utils.startsWithIgnoreCase(sql, "select")) { String withoutStrings = Utils.retainUnquoted(sql, '\''); @@ -596,7 +658,16 @@ private InputStream getInputStream( List externalData, Map additionalRequestParams ) throws ClickHouseException { - sql = clickhousifySql(sql, properties); + boolean ignoreDatabase = false; + if (parsedStmt.isRecognized()) { + sql = parsedStmt.getSQL(); + // TODO consider more scenarios like drop, show etc. + ignoreDatabase = parsedStmt.getStatementType() == StatementType.CREATE + && parsedStmt.containsKeyword(ClickHouseSqlStatement.KEYWORD_DATABASE); + } else { + sql = clickhousifySql(sql); + ignoreDatabase = sql.trim().regionMatches(true, 0, databaseKeyword, 0, databaseKeyword.length()); + } log.debug("Executing SQL: {}", sql); additionalClickHouseDBParams = addQueryIdTo( @@ -604,7 +675,6 @@ private InputStream getInputStream( ? new EnumMap(ClickHouseQueryParam.class) : additionalClickHouseDBParams); - boolean ignoreDatabase = sql.trim().regionMatches(true, 0, databaseKeyword, 0, databaseKeyword.length()); URI uri; if (externalData == null || externalData.isEmpty()) { uri = buildRequestUri( diff --git a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java index b3723848e..f3c17ff5c 100644 --- a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java +++ b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java @@ -6,10 +6,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlParser; -import ru.yandex.clickhouse.jdbc.parser.ClickHouseSqlStatement; -import ru.yandex.clickhouse.jdbc.parser.StatementType; -import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.util.apache.StringUtils; /** @@ -36,15 +32,16 @@ private PreparedStatementParser() { @Deprecated static PreparedStatementParser parse(String sql) { - return parse(sql, null); + return parse(sql, -1); } - static PreparedStatementParser parse(String sql, ClickHouseProperties properties) { + @Deprecated + static PreparedStatementParser parse(String sql, int valuesEndPosition) { if (StringUtils.isBlank(sql)) { throw new IllegalArgumentException("SQL may not be blank"); } PreparedStatementParser parser = new PreparedStatementParser(); - parser.parseSQL(sql, properties); + parser.parseSQL(sql, valuesEndPosition); return parser; } @@ -66,7 +63,7 @@ private void reset() { valuesMode = false; } - private void parseSQL(String sql, ClickHouseProperties properties) { + private void parseSQL(String sql, int valuesEndPosition) { reset(); List currentParamList = new ArrayList(); boolean afterBackSlash = false; @@ -75,15 +72,10 @@ private void parseSQL(String sql, ClickHouseProperties properties) { boolean inSingleLineComment = false; boolean inMultiLineComment = false; boolean whiteSpace = false; - ClickHouseSqlStatement parsedSql = ClickHouseSqlParser.parseSingleStatement(sql, properties); int endPosition = 0; - if (parsedSql.getStatementType() == StatementType.INSERT) { - endPosition = parsedSql.getEndPosition(ClickHouseSqlStatement.KEYWORD_VALUES) - 1; - if (endPosition > 0) { - valuesMode = true; - } else { - endPosition = 0; - } + if (valuesEndPosition > 0) { + valuesMode = true; + endPosition = valuesEndPosition; } else { Matcher matcher = VALUES.matcher(sql); if (matcher.find()) { diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java index 5da932589..f21d13f69 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java @@ -12,9 +12,11 @@ public class ClickHouseSqlStatement { public static final String DEFAULT_TABLE = "unknown"; public static final List DEFAULT_PARAMETERS = Collections.emptyList(); public static final Map DEFAULT_POSITIONS = Collections.emptyMap(); - public static final Map DEFAULT_VARIABLES = Collections.emptyMap(); + public static final String KEYWORD_DATABASE = "DATABASE"; + public static final String KEYWORD_EXISTS = "EXISTS"; public static final String KEYWORD_FORMAT = "FORMAT"; + public static final String KEYWORD_REPLACE = "REPLACE"; public static final String KEYWORD_TOTALS = "TOTALS"; public static final String KEYWORD_VALUES = "VALUES"; @@ -25,18 +27,19 @@ public class ClickHouseSqlStatement { private final String table; private final String format; private final String outfile; + private final List parameters; private final Map positions; public ClickHouseSqlStatement(String sql) { - this(sql, StatementType.UNKNOWN, null, null, null, null, null, null); + this(sql, StatementType.UNKNOWN, null, null, null, null, null, null, null); } public ClickHouseSqlStatement(String sql, StatementType stmtType) { - this(sql, stmtType, null, null, null, null, null, null); + this(sql, stmtType, null, null, null, null, null, null, null); } public ClickHouseSqlStatement(String sql, StatementType stmtType, String cluster, String database, String table, - String format, String outfile, Map positions) { + String format, String outfile, List parameters, Map positions) { this.sql = sql; this.stmtType = stmtType; @@ -46,6 +49,12 @@ public ClickHouseSqlStatement(String sql, StatementType stmtType, String cluster this.format = format; this.outfile = outfile; + if (parameters != null && parameters.size() > 0) { + this.parameters = Collections.unmodifiableList(parameters); + } else { + this.parameters = DEFAULT_PARAMETERS; + } + if (positions != null && positions.size() > 0) { Map p = new HashMap<>(); for (Entry e : positions.entrySet()) { @@ -87,7 +96,23 @@ public boolean isMutation() { } public boolean isIdemponent() { - return this.isQuery(); + boolean result = this.stmtType.isIdempotent() && !this.hasOutfile(); + + if (!result) { // try harder + switch (this.stmtType) { + case ATTACH: + case CREATE: + case DETACH: + case DROP: + result = positions.containsKey(KEYWORD_EXISTS) || positions.containsKey(KEYWORD_REPLACE); + break; + + default: + break; + } + } + + return result; } public LanguageType getLanguageType() { @@ -126,6 +151,14 @@ public String getOutfile() { return this.outfile; } + public boolean containsKeyword(String keyword) { + if (keyword == null || keyword.isEmpty()) { + return false; + } + + return positions.containsKey(keyword.toUpperCase(Locale.ROOT)); + } + public boolean hasFormat() { return this.format != null && !this.format.isEmpty(); } @@ -142,6 +175,10 @@ public boolean hasValues() { return this.positions.containsKey(KEYWORD_VALUES); } + public List getParameters() { + return this.parameters; + } + public int getStartPosition(String keyword) { int position = -1; @@ -161,14 +198,18 @@ public int getEndPosition(String keyword) { return position != -1 && keyword != null ? position + keyword.length() : position; } + public Map getPositions() { + return this.positions; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('[').append(stmtType.name()).append(']').append(" cluster=").append(cluster).append(", database=") .append(database).append(", table=").append(table).append(", format=").append(format) - .append(", outfile=").append(outfile).append(", positions=").append(positions).append("\nSQL:\n") - .append(sql); + .append(", outfile=").append(outfile).append(", parameters=").append(parameters).append(", positions=") + .append(positions).append("\nSQL:\n").append(sql); return sb.toString(); } @@ -181,6 +222,7 @@ public int hashCode() { result = prime * result + ((database == null) ? 0 : database.hashCode()); result = prime * result + ((format == null) ? 0 : format.hashCode()); result = prime * result + ((outfile == null) ? 0 : outfile.hashCode()); + result = prime * result + ((parameters == null) ? 0 : parameters.hashCode()); result = prime * result + ((positions == null) ? 0 : positions.hashCode()); result = prime * result + ((sql == null) ? 0 : sql.hashCode()); result = prime * result + ((stmtType == null) ? 0 : stmtType.hashCode()); @@ -217,6 +259,11 @@ public boolean equals(Object obj) { return false; } else if (!outfile.equals(other.outfile)) return false; + if (parameters == null) { + if (other.parameters != null) + return false; + } else if (!parameters.equals(other.parameters)) + return false; if (positions == null) { if (other.positions != null) return false; diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java index 8d586db81..d9d244487 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java @@ -1,8 +1,9 @@ package ru.yandex.clickhouse.jdbc.parser; import java.util.List; +import java.util.Map; -public interface ParseHandler { +public abstract class ParseHandler { /** * Handle macro like "#include('/tmp/template.sql')". * @@ -10,7 +11,9 @@ public interface ParseHandler { * @param parameters parameters * @return output of the macro, could be null or empty string */ - String handleMacro(String name, List parameters); + public String handleMacro(String name, List parameters) { + return null; + } /** * Handle parameter. @@ -21,5 +24,26 @@ public interface ParseHandler { * @param columnIndex columnIndex(starts from 1 not 0) * @return parameter value */ - String handleParameter(String cluster, String database, String table, int columnIndex); + public String handleParameter(String cluster, String database, String table, int columnIndex) { + return null; + } + + /** + * Hanlde statemenet. + * + * @param sql sql statement + * @param stmtType statement type + * @param cluster cluster + * @param database database + * @param table table + * @param format format + * @param outfile outfile + * @param parameters positions of parameters + * @param positions keyword positions + * @return sql statement, or null means no change + */ + public ClickHouseSqlStatement handleStatement(String sql, StatementType stmtType, String cluster, String database, + String table, String format, String outfile, List parameters, Map positions) { + return null; + } } diff --git a/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java b/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java index 0a58e0c38..3797026d9 100644 --- a/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java @@ -1,40 +1,42 @@ package ru.yandex.clickhouse.jdbc.parser; public enum StatementType { - UNKNOWN(LanguageType.UNKNOWN, OperationType.UNKNOWN), // unknown statement - ALTER(LanguageType.DDL, OperationType.UNKNOWN), // alter statement - ALTER_DELETE(LanguageType.DDL, OperationType.WRITE), // delete statement - ALTER_UPDATE(LanguageType.DDL, OperationType.WRITE), // update statement - ATTACH(LanguageType.DDL, OperationType.UNKNOWN), // attach statement - CHECK(LanguageType.DDL, OperationType.UNKNOWN), // check statement - CREATE(LanguageType.DDL, OperationType.UNKNOWN), // create statement - DELETE(LanguageType.DML, OperationType.WRITE), // the upcoming light-weight delete statement - DESCRIBE(LanguageType.DDL, OperationType.READ), // describe/desc statement - DETACH(LanguageType.DDL, OperationType.UNKNOWN), // detach statement - DROP(LanguageType.DDL, OperationType.UNKNOWN), // drop statement - EXISTS(LanguageType.DML, OperationType.READ), // exists statement - EXPLAIN(LanguageType.DDL, OperationType.READ), // explain statement - GRANT(LanguageType.DCL, OperationType.UNKNOWN), // grant statement - INSERT(LanguageType.DML, OperationType.WRITE), // insert statement - KILL(LanguageType.DCL, OperationType.UNKNOWN), // kill statement - OPTIMIZE(LanguageType.DDL, OperationType.UNKNOWN), // optimize statement - RENAME(LanguageType.DDL, OperationType.UNKNOWN), // rename statement - REVOKE(LanguageType.DCL, OperationType.UNKNOWN), // revoke statement - SELECT(LanguageType.DML, OperationType.READ), // select statement - SET(LanguageType.DCL, OperationType.UNKNOWN), // set statement - SHOW(LanguageType.DDL, OperationType.READ), // show statement - SYSTEM(LanguageType.DDL, OperationType.UNKNOWN), // system statement - TRUNCATE(LanguageType.DDL, OperationType.UNKNOWN), // truncate statement - UPDATE(LanguageType.DML, OperationType.WRITE), // the upcoming light-weight update statement - USE(LanguageType.DDL, OperationType.UNKNOWN), // use statement - WATCH(LanguageType.DDL, OperationType.UNKNOWN); // watch statement + UNKNOWN(LanguageType.UNKNOWN, OperationType.UNKNOWN, false), // unknown statement + ALTER(LanguageType.DDL, OperationType.UNKNOWN, false), // alter statement + ALTER_DELETE(LanguageType.DDL, OperationType.WRITE, false), // delete statement + ALTER_UPDATE(LanguageType.DDL, OperationType.WRITE, false), // update statement + ATTACH(LanguageType.DDL, OperationType.UNKNOWN, false), // attach statement + CHECK(LanguageType.DDL, OperationType.UNKNOWN, true), // check statement + CREATE(LanguageType.DDL, OperationType.UNKNOWN, false), // create statement + DELETE(LanguageType.DML, OperationType.WRITE, false), // the upcoming light-weight delete statement + DESCRIBE(LanguageType.DDL, OperationType.READ, true), // describe/desc statement + DETACH(LanguageType.DDL, OperationType.UNKNOWN, false), // detach statement + DROP(LanguageType.DDL, OperationType.UNKNOWN, false), // drop statement + EXISTS(LanguageType.DML, OperationType.READ, true), // exists statement + EXPLAIN(LanguageType.DDL, OperationType.READ, true), // explain statement + GRANT(LanguageType.DCL, OperationType.UNKNOWN, true), // grant statement + INSERT(LanguageType.DML, OperationType.WRITE, false), // insert statement + KILL(LanguageType.DCL, OperationType.UNKNOWN, false), // kill statement + OPTIMIZE(LanguageType.DDL, OperationType.UNKNOWN, false), // optimize statement + RENAME(LanguageType.DDL, OperationType.UNKNOWN, false), // rename statement + REVOKE(LanguageType.DCL, OperationType.UNKNOWN, true), // revoke statement + SELECT(LanguageType.DML, OperationType.READ, true), // select statement + SET(LanguageType.DCL, OperationType.UNKNOWN, true), // set statement + SHOW(LanguageType.DDL, OperationType.READ, true), // show statement + SYSTEM(LanguageType.DDL, OperationType.UNKNOWN, false), // system statement + TRUNCATE(LanguageType.DDL, OperationType.UNKNOWN, true), // truncate statement + UPDATE(LanguageType.DML, OperationType.WRITE, false), // the upcoming light-weight update statement + USE(LanguageType.DDL, OperationType.UNKNOWN, true), // use statement + WATCH(LanguageType.DDL, OperationType.UNKNOWN, true); // watch statement private LanguageType langType; private OperationType opType; + private boolean idempotent; - StatementType(LanguageType langType, OperationType operationType) { + StatementType(LanguageType langType, OperationType operationType, boolean idempotent) { this.langType = langType; this.opType = operationType; + this.idempotent = idempotent; } LanguageType getLanguageType() { @@ -44,4 +46,8 @@ LanguageType getLanguageType() { OperationType getOperationType() { return this.opType; } + + boolean isIdempotent() { + return this.idempotent; + } } diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj index 05f3f8854..0810dfecd 100644 --- a/src/main/javacc/ClickHouseSqlParser.jj +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -9,7 +9,7 @@ * The ANTLR4 grammar at https://github.com/ClickHouse/ClickHouse/blob/master/src/Parsers/New is incomplete. * Also using it will introduce 300KB runtime and we'll have to deal with many parsing errors, * which is too much for a JDBC driver. On the other hand, if we write a parser from scratch, - * we'll end up with one like Druid, which is more complex the JDBC driver itself. + * we'll end up with one like Druid, which is more complex than the JDBC driver itself. * * JavaCC is something in the middle that fits our need - no runtime and easy to maintain/extend. */ @@ -101,7 +101,7 @@ public class ClickHouseSqlParser { String sql, ClickHouseProperties properties, ParseHandler handler) { ClickHouseSqlStatement[] stmts = parse(sql, properties, handler); - return stmts.length == 1 ? stmts[0] : new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null); + return stmts.length == 1 ? stmts[0] : new ClickHouseSqlStatement(sql, StatementType.UNKNOWN); } public static ClickHouseSqlStatement[] parse(String sql, ClickHouseProperties properties) { @@ -114,7 +114,7 @@ public class ClickHouseSqlParser { } ClickHouseSqlStatement[] stmts = new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }; + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }; if (!properties.isUseNewParser() || sql == null || sql.isEmpty()) { return stmts; @@ -122,15 +122,12 @@ public class ClickHouseSqlParser { ClickHouseSqlParser p = new ClickHouseSqlParser(sql, properties, handler); try { - p.sql(); - List list = p.statements; - stmts = list.toArray(new ClickHouseSqlStatement[list.size()]); + stmts = p.sql(); } catch (Exception e) { if (DEBUG) { throw new IllegalArgumentException(e); } else { log.warn("Failed to parse the given SQL. If you believe the SQL is valid, please feel free to open an issue on Github with the following SQL and exception attached.\n{}", sql, e); - stmts = new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }; } } @@ -146,7 +143,7 @@ public class ClickHouseSqlParser { public void addStatement() { if (token_source.isValid()) { - ClickHouseSqlStatement sqlStmt = token_source.build(); + ClickHouseSqlStatement sqlStmt = token_source.build(handler); // FIXME remove the restriction once we can hanlde insertion with format well if (statements.size() == 0 || sqlStmt.isRecognized()) { statements.add(sqlStmt); @@ -255,9 +252,18 @@ TOKEN_MGR_DECLS: { positions.clear(); } - ClickHouseSqlStatement build() { - ClickHouseSqlStatement s = new ClickHouseSqlStatement( - builder.toString(), stmtType, cluster, database, table, format, outfile, positions); + ClickHouseSqlStatement build(ParseHandler handler) { + String sqlStmt = builder.toString(); + ClickHouseSqlStatement s = null; + if (handler != null) { + s = handler.handleStatement( + sqlStmt, stmtType, cluster, database, table, format, outfile, parameters, positions); + } + + if (s == null) { + s = new ClickHouseSqlStatement( + sqlStmt, stmtType, cluster, database, table, format, outfile, parameters, positions); + } // reset variables reset(); @@ -357,8 +363,22 @@ void clusterClause(): { Token t; } { } // https://clickhouse.tech/docs/en/sql-reference/statements/attach/ -void attachStmt(): {} { // not interested - anyExprList() +void attachStmt(): { Token t; } { + ( + LOOKAHEAD(2) + ( + t = { token_source.setPosition(t.image); } + | + | ( t = { token_source.setPosition(t.image); }) ( + ()?

| ( | )? + ) + ) + ( + LOOKAHEAD(2) + t = { token_source.setPosition(t.image); } + )? + )? + anyExprList() // not interested } // https://clickhouse.tech/docs/en/sql-reference/statements/check-table/ @@ -367,12 +387,26 @@ void checkStmt(): {} { // not interested } // https://clickhouse.tech/docs/en/sql-reference/statements/create/ -void createStmt(): {} { // not interested - anyExprList() +void createStmt(): { Token t; } { + ( + LOOKAHEAD(2) + ( + t = { token_source.setPosition(t.image); } + | ( t = { token_source.setPosition(t.image); })? ( + ()?
| ( | )? + ) + | | | | ()? | | ()? + ) + ( + LOOKAHEAD(2) + t = { token_source.setPosition(t.image); } + )? + )? + anyExprList() // not interested } // upcoming lightweight mutation - see https://github.com/ClickHouse/ClickHouse/issues/19627 -void deleteStmt(): {} { // not interested +void deleteStmt(): {} { tableIdentifier(true) ( anyExprList())? } @@ -383,13 +417,36 @@ void describeStmt(): {} { } // https://clickhouse.tech/docs/en/sql-reference/statements/detach/ -void detachStmt(): {} { // not interested - anyExprList() +void detachStmt(): { Token t; } { + ( + LOOKAHEAD(2) + ( + t = { token_source.setPosition(t.image); } + | ()?
| | + ) + ( + LOOKAHEAD(2) + t = { token_source.setPosition(t.image); } + )? + )? + anyExprList() // not interested } // https://clickhouse.tech/docs/en/sql-reference/statements/drop/ -void dropStmt(): {} { // not interested - anyExprList() +void dropStmt(): { Token t; } { + ( + LOOKAHEAD(2) + ( + t = { token_source.setPosition(t.image); } + | ()?
| | | | + | ()? | | ()? + ) + ( + LOOKAHEAD(2) + t = { token_source.setPosition(t.image); } + )? + )? + anyExprList() // not interested } // https://clickhouse.tech/docs/en/sql-reference/statements/exists/ @@ -594,10 +651,9 @@ void nestedExpr(): {} { | columnExpr() ( ( - LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) | )+ - |(LOOKAHEAD(2) anyExprList() )+ + | (LOOKAHEAD(2) anyExprList() )+ | LOOKAHEAD(2) ()? | LOOKAHEAD(2) ()? betweenExpr() | LOOKAHEAD(2) ()? ( | ) nestedExpr() @@ -656,7 +712,9 @@ void anyExprList(): {} { void anyExpr(): {} { anyNestedExpr() ( LOOKAHEAD(2) - ( | | | | | operator())? anyNestedExpr() + ( + | | | | | operator() + )? anyNestedExpr() )* } @@ -816,10 +874,11 @@ Token anyKeyword(): { Token t; } { | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t =
| t = | t = - | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t =
| t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = // interval | t = | t = | t = | t = | t = | t = | t = | t = @@ -837,11 +896,12 @@ Token keyword(): { Token t; } { | t = | t = | t = | t =
| t = | t = - | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = + | t =
| t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = // interval | t = | t = | t = | t = | t = | t = | t = | t = // values @@ -915,14 +975,21 @@ TOKEN: { | > | > | > + | > + | > | > | > | > | > | > | > + | > | > + | > + | > |

> + | > + | > | > |

> | > @@ -935,8 +1002,10 @@ TOKEN: { |

> | > | > + | > | > | > + | > | > | > | > diff --git a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java index 6b2ddd38b..9e2768413 100644 --- a/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -64,10 +64,10 @@ private void checkSingleStatement(ClickHouseSqlStatement[] stmts, String sql, St public void testParseNonSql() throws ParseException { String sql; - assertEquals(parse(sql = null), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); - assertEquals(parse(sql = ""), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.UNKNOWN, null, null, null, null, null, null) }); + assertEquals(parse(sql = null), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }); + assertEquals(parse(sql = ""), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }); checkSingleStatement(parse(sql = "invalid sql"), sql); checkSingleStatement(parse(sql = "-- some comments"), sql); @@ -261,13 +261,13 @@ public void testSelectStatement() { String sql; assertEquals(parse(sql = "select\n1"), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null, null) }); assertEquals(parse(sql = "select\r\n1"), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null, null) }); assertEquals(parse(sql = "select 314 limit 5\nFORMAT JSONCompact;"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("select 314 limit 5\nFORMAT JSONCompact", - StatementType.SELECT, null, null, "unknown", "JSONCompact", null, null) }); + StatementType.SELECT, null, null, "unknown", "JSONCompact", null, null, null) }); checkSingleStatement(parse(sql = "select (())"), sql, StatementType.SELECT); checkSingleStatement(parse(sql = "select []"), sql, StatementType.SELECT); @@ -303,15 +303,16 @@ public void testSelectStatement() { assertEquals(parse(sql = loadSql("issue-441_with-totals.sql")), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, - "unknown", null, null, new HashMap() { + "unknown", null, null, null, new HashMap() { { put("TOTALS", 208); } }) }); - assertEquals(parse(sql = loadSql("issue-555_custom-format.sql")), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", "CSVWithNames", null, null) }); + assertEquals(parse(sql = loadSql("issue-555_custom-format.sql")), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "wrd", + "CSVWithNames", null, null, null) }); assertEquals(parse(sql = loadSql("with-clause.sql")), new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null) }); + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null, null) }); } @Test @@ -385,14 +386,13 @@ public void testComments() throws ParseException { @Test public void testMultipleStatements() throws ParseException { - assertEquals(parse("use ab;;;select 1; ;\t;\r;\n"), - new ClickHouseSqlStatement[] { - new ClickHouseSqlStatement("use ab", StatementType.USE, null, "ab", null, null, null, null), - new ClickHouseSqlStatement("select 1", StatementType.SELECT) }); + assertEquals(parse("use ab;;;select 1; ;\t;\r;\n"), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement("use ab", StatementType.USE, null, "ab", null, null, null, null, null), + new ClickHouseSqlStatement("select 1", StatementType.SELECT) }); assertEquals(parse("select * from \"a;1\".`b;c`;;;select 1 as `a ; a`; ;\t;\r;\n"), new ClickHouseSqlStatement[] { new ClickHouseSqlStatement("select * from \"a;1\".`b;c`", StatementType.SELECT, null, "a;1", - "b;c", null, null, null), + "b;c", null, null, null, null), new ClickHouseSqlStatement("select 1 as `a ; a`", StatementType.SELECT) }); } @@ -510,11 +510,6 @@ public void testParameterHandling() throws ParseException { assertEquals(stmts[0].getSQL(), sql); stmts = ClickHouseSqlParser.parse(sql, new ClickHouseProperties(), new ParseHandler() { - @Override - public String handleMacro(String name, List parameters) { - return null; - } - @Override public String handleParameter(String cluster, String database, String table, int columnIndex) { return String.valueOf(columnIndex); @@ -537,19 +532,14 @@ public String handleMacro(String name, List parameters) { if ("listOfColumns".equals(name)) { return "a, b"; } else if ("subQuery".equals(name)) { - return "select " + String.join("||", parameters); + return "select " + String.join("+", parameters); } else { return null; } } - - @Override - public String handleParameter(String cluster, String database, String table, int columnIndex) { - return null; - } }); assertEquals(stmts.length, 1); - assertEquals(stmts[0].getSQL(), "select a, b from (select 1||2||3)"); + assertEquals(stmts[0].getSQL(), "select a, b from (select 1+2+3)"); } @Test diff --git a/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java b/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java index e7eee897f..41502c0ee 100644 --- a/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java +++ b/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java @@ -184,7 +184,7 @@ public void testWithoutRetry() throws Exception { CloseableHttpClient client = builder.buildClient(); HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%201"); - shutDownServerWithDelay(server, 100); + shutDownServerWithDelay(server, 500); try { client.execute(post); @@ -205,7 +205,7 @@ public void testWithRetry() throws Exception { context.setAttribute("is_idempotent", Boolean.TRUE); HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%202"); - shutDownServerWithDelay(server, 100); + shutDownServerWithDelay(server, 500); try { client.execute(post, context);