diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df121fa14..d13b22284 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: version: description: "Release version" required: true - default: "0.2.5-SNAPSHOT" + default: "0.2.6-SNAPSHOT" jobs: release: @@ -20,11 +20,12 @@ jobs: uses: actions/setup-java@v1 with: java-version: 1.8 + - run: sed -i -e 's|^\( \).*\(\)$|\1${{ github.event.inputs.version }}\2|' pom.xml - name: Release Maven package uses: samuelmeuli/action-maven-publish@v1 with: maven_profiles: release - maven_args: -Drevision=${{ github.event.inputs.version }} --batch-mode + maven_args: --batch-mode gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} nexus_username: ${{ secrets.SONATYPE_USER }} 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/CHANGELOG b/CHANGELOG index a785614b9..f6ffc6e73 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,19 @@ +0.2.6 + * add new feature for sending compressed files/streams + * introduce an experimental SQL parser to fix parsing related issues - set connection setting use_new_parser to false to disable + * restore String[] getColumnNames() method for backward compatibility + * retry idempotent operation up to 3 times when server closed connection - set connection setting maxRetries to zero to disable + * return inserted rows(not accurate) when query parameter send_progress_in_http_headers is set to true + * set socket timeout in ClickHouseConnectionImpl.isValid() + * upgrade to lz4-java and improve performance of LZ4 stream + * use HTTP Basic Auth for credentials instead of query parameters + * use static version instead of property-based revision in pom.xml 0.2.5 * bump dependencies and include lz4 in shaded jar * new API: ClickHouseRowBinaryStream.writeUInt64Array(UnsignedLong[]) * support column comments * support explain queries + * fix keep-alive timeout issue by reusing validated connection * fix ResultSet.findColumn(String) issue * fix the issue of not being able to use NULL constant in PreparedStatement * fix toLowerCase issue for Turkish diff --git a/README.md b/README.md index 9d25947c8..ff31548bc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It has support of a minimal subset of features to be usable. ru.yandex.clickhouse clickhouse-jdbc - 0.2.5 + 0.2.6 ``` @@ -35,7 +35,7 @@ sth .write() // Write API entrypoint .table("default.my_table") // where to write data .option("format_csv_delimiter", ";") // specific param - .data(new File("/path/to/file.csv"), ClickHouseFormat.CSV) // specify input + .data(new File("/path/to/file.csv.gz"), ClickHouseFormat.CSV, ClickHouseCompression.gzip) // specify input .send(); ``` #### Configurable send @@ -46,6 +46,7 @@ sth .write() .sql("INSERT INTO default.my_table (a,b,c)") .data(new MyCustomInputStream(), ClickHouseFormat.JSONEachRow) + .dataCompression(ClickHouseCompression.brotli) .addDbParam(ClickHouseQueryParam.MAX_PARALLEL_REPLICAS, 2) .send(); ``` diff --git a/pom.xml b/pom.xml index 7ad1be16a..99d86ee8f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.yandex.clickhouse clickhouse-jdbc - ${revision} + 0.2.6 jar clickhouse-jdbc @@ -52,12 +52,11 @@ - 0.2.5 1.7.30 2021 UTF-8 4.5.13 - 1.3.0 + 1.7.1 2.9.10 2.9.10.8 29.0-jre @@ -66,10 +65,12 @@ 1.15.1 6.14.3 1.10.19 + 2.27.2 3.2.0 3.0.0-M1 1.6.8 1.6 + 4.1.4 3.8.1 3.2.1 3.2.0 @@ -98,8 +99,8 @@ ${httpclient.version} - net.jpountz.lz4 - lz4 + org.lz4 + lz4-java ${lz4.version} @@ -152,6 +153,12 @@ ${mockito.version} test + + com.github.tomakehurst + wiremock-jre8 + ${wiremock.version} + test + @@ -282,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/ClickHouseConnectionImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java index 2a7698355..e767bd445 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseConnectionImpl.java @@ -111,7 +111,13 @@ public ClickHouseStatement createStatement() throws SQLException { } public ClickHouseStatement createStatement(int resultSetType) throws SQLException { - return LogProxy.wrap(ClickHouseStatement.class, new ClickHouseStatementImpl(httpclient, this, properties, resultSetType)); + return LogProxy.wrap( + ClickHouseStatement.class, + new ClickHouseStatementImpl( + httpclient, + this, + properties, + resultSetType)); } @Deprecated @@ -126,15 +132,37 @@ public TimeZone getTimeZone() { } private ClickHouseStatement createClickHouseStatement(CloseableHttpClient httpClient) throws SQLException { - return LogProxy.wrap(ClickHouseStatement.class, new ClickHouseStatementImpl(httpClient, this, properties, DEFAULT_RESULTSET_TYPE)); + return LogProxy.wrap( + ClickHouseStatement.class, + new ClickHouseStatementImpl( + httpClient, + this, + properties, + DEFAULT_RESULTSET_TYPE)); } public PreparedStatement createPreparedStatement(String sql, int resultSetType) throws SQLException { - return LogProxy.wrap(PreparedStatement.class, new ClickHousePreparedStatementImpl(httpclient, this, properties, sql, getTimeZone(), resultSetType)); + return LogProxy.wrap( + PreparedStatement.class, + new ClickHousePreparedStatementImpl( + httpclient, + this, + properties, + sql, + getTimeZone(), + resultSetType)); } public ClickHousePreparedStatement createClickHousePreparedStatement(String sql, int resultSetType) throws SQLException { - return LogProxy.wrap(ClickHousePreparedStatement.class, new ClickHousePreparedStatementImpl(httpclient, this, properties, sql, getTimeZone(), resultSetType)); + return LogProxy.wrap( + ClickHousePreparedStatement.class, + new ClickHousePreparedStatementImpl( + httpclient, + this, + properties, + sql, + getTimeZone(), + resultSetType)); } @@ -385,8 +413,10 @@ public boolean isValid(int timeout) throws SQLException { closeableHttpClient = this.httpclient; } else { ClickHouseProperties properties = new ClickHouseProperties(this.properties); - properties.setConnectionTimeout((int) TimeUnit.SECONDS.toMillis(timeout)); + int timeoutMs = (int) TimeUnit.SECONDS.toMillis(timeout); + properties.setConnectionTimeout(timeoutMs); properties.setMaxExecutionTime(timeout); + properties.setSocketTimeout(timeoutMs); closeableHttpClient = new ClickHouseHttpClientBuilder(properties).buildClient(); isAnotherHttpClient = true; } @@ -406,12 +436,13 @@ public boolean isValid(int timeout) throws SQLException { return false; } finally { - if (isAnotherHttpClient) + if (isAnotherHttpClient) { try { closeableHttpClient.close(); } catch (IOException e) { log.warn("Can't close a http client", e); } + } } } @@ -460,22 +491,27 @@ public boolean isWrapperFor(Class iface) throws SQLException { return iface.isAssignableFrom(getClass()); } + @Override public void setSchema(String schema) throws SQLException { properties.setDatabase(schema); } + @Override public String getSchema() throws SQLException { return properties.getDatabase(); } + @Override public void abort(Executor executor) throws SQLException { this.close(); } + @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { } + @Override public int getNetworkTimeout() throws SQLException { return 0; } diff --git a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java index e5e1fae0c..a0e64175e 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHousePreparedStatementImpl.java @@ -1,26 +1,49 @@ package ru.yandex.clickhouse; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import ru.yandex.clickhouse.response.ClickHouseResponse; -import ru.yandex.clickhouse.settings.ClickHouseProperties; -import ru.yandex.clickhouse.settings.ClickHouseQueryParam; -import ru.yandex.clickhouse.util.ClickHouseArrayUtil; -import ru.yandex.clickhouse.util.ClickHouseValueFormatter; -import ru.yandex.clickhouse.util.guava.StreamUtils; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.math.BigDecimal; import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Date; -import java.sql.*; -import java.util.*; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLSyntaxErrorException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; + +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; +import ru.yandex.clickhouse.util.ClickHouseArrayUtil; +import ru.yandex.clickhouse.util.ClickHouseValueFormatter; +import ru.yandex.clickhouse.util.guava.StreamUtils; public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl implements ClickHousePreparedStatement { @@ -36,15 +59,18 @@ public class ClickHousePreparedStatementImpl extends ClickHouseStatementImpl imp private final ClickHousePreparedStatementParameter[] binds; private final List> parameterList; private final boolean insertBatchMode; - private List batchRows = new ArrayList(); + private List batchRows = new ArrayList<>(); - public ClickHousePreparedStatementImpl(CloseableHttpClient client, ClickHouseConnection connection, - ClickHouseProperties properties, String sql, TimeZone serverTimeZone, - int resultSetType) throws SQLException + public ClickHousePreparedStatementImpl(CloseableHttpClient client, + ClickHouseConnection connection, ClickHouseProperties properties, String sql, + TimeZone serverTimeZone, int resultSetType) throws SQLException { super(client, connection, properties, resultSetType); + 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(); @@ -295,7 +321,7 @@ public void addBatch() throws SQLException { private List buildBatch() throws SQLException { checkBinded(); - List newBatches = new ArrayList(parameterList.size()); + List newBatches = new ArrayList<>(parameterList.size()); StringBuilder sb = new StringBuilder(); for (int i = 0, p = 0; i < parameterList.size(); i++) { List pList = parameterList.get(i); @@ -325,20 +351,28 @@ 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 (parsedStmt.getStatementType() == StatementType.INSERT && parsedStmt.hasValues()) { + valuePosition = parsedStmt.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); int[] result = new int[batchRows.size()]; Arrays.fill(result, 1); - batchRows = new ArrayList(); + batchRows = new ArrayList<>(); return result; } @@ -407,7 +441,8 @@ public ResultSetMetaData getMetaData() throws SQLException { if (currentResult != null) { return currentResult.getMetaData(); } - if (!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/ClickHouseStatement.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java index 262257a3d..1773698ae 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatement.java @@ -1,6 +1,7 @@ package ru.yandex.clickhouse; import ru.yandex.clickhouse.response.ClickHouseResponse; +import ru.yandex.clickhouse.response.ClickHouseResponseSummary; import ru.yandex.clickhouse.settings.ClickHouseQueryParam; import ru.yandex.clickhouse.util.ClickHouseRowBinaryInputStream; import ru.yandex.clickhouse.util.ClickHouseStreamCallback; @@ -105,4 +106,6 @@ ResultSet executeQuery(String sql, * Returns extended write-API */ Writer write(); + + ClickHouseResponseSummary getResponseSummary(); } diff --git a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java index 3df0ea2cb..947c877f4 100644 --- a/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java +++ b/src/main/java/ru/yandex/clickhouse/ClickHouseStatementImpl.java @@ -1,11 +1,30 @@ package ru.yandex.clickhouse; -import com.google.common.base.Strings; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.util.ArrayList; +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; + +import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -15,35 +34,37 @@ import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.google.common.base.Strings; + import ru.yandex.clickhouse.domain.ClickHouseFormat; import ru.yandex.clickhouse.except.ClickHouseException; import ru.yandex.clickhouse.except.ClickHouseExceptionSpecifier; -import ru.yandex.clickhouse.response.*; +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; +import ru.yandex.clickhouse.response.ClickHouseResultSet; +import ru.yandex.clickhouse.response.ClickHouseScrollableResultSet; +import ru.yandex.clickhouse.response.FastByteArrayOutputStream; import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.settings.ClickHouseQueryParam; +import ru.yandex.clickhouse.util.ClickHouseHttpClientBuilder; import ru.yandex.clickhouse.util.ClickHouseRowBinaryInputStream; import ru.yandex.clickhouse.util.ClickHouseStreamCallback; import ru.yandex.clickhouse.util.Utils; import ru.yandex.clickhouse.util.guava.StreamUtils; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLWarning; -import java.util.*; - - -public class ClickHouseStatementImpl implements ClickHouseStatement { +public class ClickHouseStatementImpl extends ConfigurableApi implements ClickHouseStatement { private static final Logger log = LoggerFactory.getLogger(ClickHouseStatementImpl.class); private final CloseableHttpClient client; + private final HttpClientContext httpContext; + protected ClickHouseProperties properties; private ClickHouseConnection connection; @@ -52,6 +73,8 @@ public class ClickHouseStatementImpl implements ClickHouseStatement { private ClickHouseRowBinaryInputStream currentRowBinaryResult; + private ClickHouseResponseSummary currentSummary; + private int currentUpdateCount = -1; private int queryTimeout; @@ -66,6 +89,8 @@ public class ClickHouseStatementImpl implements ClickHouseStatement { private volatile String queryId; + protected ClickHouseSqlStatement parsedStmt; + /** * Current database name may be changed by {@link java.sql.Connection#setCatalog(String)} * between creation of this object and query execution, but javadoc does not allow @@ -73,12 +98,53 @@ public class ClickHouseStatementImpl implements ClickHouseStatement { */ private final String initialDatabase; + @Deprecated private static final String[] selectKeywords = new String[]{"SELECT", "WITH", "SHOW", "DESC", "EXISTS", "EXPLAIN"}; + @Deprecated private static final String databaseKeyword = "CREATE DATABASE"; + @Deprecated + protected void parseSingleStatement(String sql) throws SQLException { + this.parsedStmt = null; + ClickHouseSqlStatement[] stmts = ClickHouseSqlParser.parse(sql, properties); + + if (stmts.length == 1) { + this.parsedStmt = stmts[0]; + } else { + this.parsedStmt = new ClickHouseSqlStatement(sql, StatementType.UNKNOWN); + // throw new SQLException("Multiple statements are not supported."); + } + + if (this.parsedStmt.isIdemponent()) { + httpContext.setAttribute("is_idempotent", Boolean.TRUE); + } else { + httpContext.removeAttribute("is_idempotent"); + } + } + + @Deprecated + private void parseSingleStatement(String sql, ClickHouseFormat preferredFormat) throws SQLException { + parseSingleStatement(sql); + + if (parsedStmt.isQuery() && !parsedStmt.hasFormat()) { + String format = preferredFormat.name(); + Map 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); this.client = client; + this.httpContext = ClickHouseHttpClientBuilder.createClientContext(properties); this.connection = connection; this.properties = properties == null ? new ClickHouseProperties() : properties; this.initialDatabase = this.properties.getDatabase(); @@ -108,22 +174,35 @@ public ResultSet executeQuery(String sql, // forcibly disable extremes for ResultSet queries if (additionalDBParams == null || additionalDBParams.isEmpty()) { - additionalDBParams = new EnumMap(ClickHouseQueryParam.class); + additionalDBParams = new EnumMap<>(ClickHouseQueryParam.class); } else { - additionalDBParams = new EnumMap(additionalDBParams); + additionalDBParams = new EnumMap<>(additionalDBParams); } 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); + } + InputStream is = getInputStream(sql, additionalDBParams, externalData, additionalRequestParams); + try { - if (isSelect(sql)) { + if (parsedStmt.isQuery()) { 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 @@ -155,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, ClickHouseFormat.JSONCompact), + sql, additionalDBParams, null, additionalRequestParams @@ -185,14 +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, ClickHouseFormat.RowBinary), + sql, additionalDBParams, null, additionalRequestParams ); try { - if (isSelect(sql)) { + if (parsedStmt.isQuery()) { currentUpdateCount = -1; currentRowBinaryResult = new ClickHouseRowBinaryInputStream(properties.isCompress() ? new ClickHouseLZ4Stream(is) : is, getConnection().getTimeZone(), properties); @@ -210,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); @@ -217,14 +318,14 @@ public int executeUpdate(String sql) throws SQLException { } finally { StreamUtils.close(is); } - return 1; + + return currentSummary != null ? (int) currentSummary.getWrittenRows() : 1; } @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 @@ -444,6 +545,12 @@ public boolean isWrapperFor(Class iface) throws SQLException { return iface.isAssignableFrom(getClass()); } + @Override + public ClickHouseResponseSummary getResponseSummary() { + return currentSummary; + } + + @Deprecated static String clickhousifySql(String sql) { return addFormatIfAbsent(sql, ClickHouseFormat.TabSeparatedWithNamesAndTypes); } @@ -452,6 +559,7 @@ static String clickhousifySql(String sql) { * Adding FORMAT TabSeparatedWithNamesAndTypes if not added * adds format only to select queries */ + @Deprecated private static String addFormatIfAbsent(final String sql, ClickHouseFormat format) { String cleanSQL = sql.trim(); if (!isSelect(cleanSQL)) { @@ -471,6 +579,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())); @@ -491,6 +600,7 @@ static boolean isSelect(String sql) { return false; } + @Deprecated private String extractTableName(String sql) { String s = extractDBAndTableName(sql); if (s.contains(".")) { @@ -500,6 +610,7 @@ private String extractTableName(String sql) { } } + @Deprecated private String extractDBName(String sql) { String s = extractDBAndTableName(sql); if (s.contains(".")) { @@ -509,6 +620,7 @@ private String extractDBName(String sql) { } } + @Deprecated private String extractDBAndTableName(String sql) { if (Utils.startsWithIgnoreCase(sql, "select")) { String withoutStrings = Utils.retainUnquoted(sql, '\''); @@ -531,10 +643,11 @@ private String extractDBAndTableName(String sql) { return "system.unknown"; } + @Deprecated 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; } @@ -545,7 +658,16 @@ private InputStream getInputStream( List externalData, Map additionalRequestParams ) throws ClickHouseException { - sql = clickhousifySql(sql); + 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( @@ -553,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( @@ -611,7 +732,7 @@ private InputStream getInputStream( HttpPost post = new HttpPost(uri); post.setEntity(requestEntity); - HttpResponse response = client.execute(post); + HttpResponse response = client.execute(post, httpContext); entity = response.getEntity(); checkForErrorAndThrow(entity, response); @@ -623,6 +744,13 @@ private InputStream getInputStream( entity.writeTo(baos); is = baos.convertToInputStream(); } + + // retrieve response summary + if (isQueryParamSet(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, additionalClickHouseDBParams, additionalRequestParams)) { + Header summaryHeader = response.getFirstHeader("X-ClickHouse-Summary"); + currentSummary = summaryHeader != null ? Jackson.getObjectMapper().readValue(summaryHeader.getValue(), ClickHouseResponseSummary.class) : null; + } + return is; } catch (ClickHouseException e) { throw e; @@ -670,7 +798,7 @@ private List getUrlQueryParams( Map additionalRequestParams, boolean ignoreDatabase ) { - List result = new ArrayList(); + List result = new ArrayList<>(); if (sql != null) { result.add(new BasicNameValuePair("query", sql)); @@ -700,6 +828,8 @@ private List getUrlQueryParams( params.put(ClickHouseQueryParam.DATABASE, initialDatabase); } + params.putAll(getAdditionalDBParams()); + if (additionalClickHouseDBParams != null && !additionalClickHouseDBParams.isEmpty()) { params.putAll(additionalClickHouseDBParams); } @@ -712,6 +842,12 @@ private List getUrlQueryParams( } } + for (Map.Entry entry : getRequestParams().entrySet()) { + if (!Strings.isNullOrEmpty(entry.getValue())) { + result.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + } + if (additionalRequestParams != null) { for (Map.Entry entry : additionalRequestParams.entrySet()) { if (!Strings.isNullOrEmpty(entry.getValue())) { @@ -720,16 +856,41 @@ private List getUrlQueryParams( } } - return result; } + private boolean isQueryParamSet(ClickHouseQueryParam param, Map additionalClickHouseDBParams, Map additionalRequestParams) { + String value = getQueryParamValue(param, additionalClickHouseDBParams, additionalRequestParams); + + return "true".equals(value) || "1".equals(value); + } + + private String getQueryParamValue(ClickHouseQueryParam param, Map additionalClickHouseDBParams, Map additionalRequestParams) { + if (additionalRequestParams != null && additionalRequestParams.containsKey(param.getKey()) && !Strings.isNullOrEmpty(additionalRequestParams.get(param.getKey()))) { + return additionalRequestParams.get(param.getKey()); + } + + if (getRequestParams().containsKey(param.getKey()) && !Strings.isNullOrEmpty(getRequestParams().get(param.getKey()))) { + return getRequestParams().get(param.getKey()); + } + + if (additionalClickHouseDBParams != null && additionalClickHouseDBParams.containsKey(param) && !Strings.isNullOrEmpty(additionalClickHouseDBParams.get(param))) { + return additionalClickHouseDBParams.get(param); + } + + if (getAdditionalDBParams().containsKey(param) && !Strings.isNullOrEmpty(getAdditionalDBParams().get(param))) { + return getAdditionalDBParams().get(param); + } + + return properties.asProperties().getProperty(param.getKey()); + } + private URI followRedirects(URI uri) throws IOException, URISyntaxException { if (properties.isCheckForRedirects()) { int redirects = 0; while (redirects < properties.getMaxRedirects()) { HttpGet httpGet = new HttpGet(uri); - HttpResponse response = client.execute(httpGet); + HttpResponse response = client.execute(httpGet, httpContext); if (response.getStatusLine().getStatusCode() == 307) { uri = new URI(response.getHeaders("Location")[0].getValue()); redirects++; @@ -846,10 +1007,19 @@ void sendStream(Writer writer, HttpEntity content) throws ClickHouseException { HttpPost httpPost = new HttpPost(uri); + if (writer.getCompression() != null) { + httpPost.addHeader("Content-Encoding", writer.getCompression().name()); + } httpPost.setEntity(content); - HttpResponse response = client.execute(httpPost); + HttpResponse response = client.execute(httpPost, httpContext); entity = response.getEntity(); checkForErrorAndThrow(entity, response); + + // retrieve response summary + if (isQueryParamSet(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, writer.getAdditionalDBParams(), writer.getRequestParams())) { + Header summaryHeader = response.getFirstHeader("X-ClickHouse-Summary"); + currentSummary = summaryHeader != null ? Jackson.getObjectMapper().readValue(summaryHeader.getValue(), ClickHouseResponseSummary.class) : null; + } } catch (ClickHouseException e) { throw e; } catch (Exception e) { @@ -877,10 +1047,12 @@ private void checkForErrorAndThrow(HttpEntity entity, HttpResponse response) thr } } + @Override public void closeOnCompletion() throws SQLException { closeOnCompletion = true; } + @Override public boolean isCloseOnCompletion() throws SQLException { return closeOnCompletion; } @@ -919,6 +1091,6 @@ private Map addQueryIdTo(Map dbParams) { this.additionalDBParams = new HashMap(); if (null != dbParams) { @@ -49,4 +54,9 @@ public T option(String key, String value) { return (T) this; } + public T removeOption(String key) { + additionalRequestParams.remove(key); + return (T) this; + } + } diff --git a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java index 8225b5456..f3c17ff5c 100644 --- a/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java +++ b/src/main/java/ru/yandex/clickhouse/PreparedStatementParser.java @@ -30,12 +30,18 @@ private PreparedStatementParser() { valuesMode = false; } + @Deprecated static PreparedStatementParser parse(String sql) { + return parse(sql, -1); + } + + @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); + parser.parseSQL(sql, valuesEndPosition); return parser; } @@ -57,7 +63,7 @@ private void reset() { valuesMode = false; } - private void parseSQL(String sql) { + private void parseSQL(String sql, int valuesEndPosition) { reset(); List currentParamList = new ArrayList(); boolean afterBackSlash = false; @@ -66,21 +72,30 @@ private void parseSQL(String sql) { boolean inSingleLineComment = false; boolean inMultiLineComment = false; boolean whiteSpace = false; - Matcher matcher = VALUES.matcher(sql); - if (matcher.find()) { + int endPosition = 0; + if (valuesEndPosition > 0) { valuesMode = true; + endPosition = valuesEndPosition; + } 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 +124,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 +173,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/Writer.java b/src/main/java/ru/yandex/clickhouse/Writer.java index f61b643b9..a93ea8fa6 100644 --- a/src/main/java/ru/yandex/clickhouse/Writer.java +++ b/src/main/java/ru/yandex/clickhouse/Writer.java @@ -2,6 +2,7 @@ import org.apache.http.HttpEntity; import org.apache.http.entity.InputStreamEntity; +import ru.yandex.clickhouse.domain.ClickHouseCompression; import ru.yandex.clickhouse.domain.ClickHouseFormat; import ru.yandex.clickhouse.util.ClickHouseStreamCallback; import ru.yandex.clickhouse.util.ClickHouseStreamHttpEntity; @@ -17,7 +18,7 @@ public class Writer extends ConfigurableApi { private ClickHouseFormat format = TabSeparated; - + private ClickHouseCompression compression = null; private String table = null; private String sql = null; private InputStreamProvider streamProvider = null; @@ -81,6 +82,22 @@ public Writer data(File input) { return this; } + public Writer data(InputStream stream, ClickHouseFormat format, ClickHouseCompression compression) { + return dataCompression(compression).format(format).data(stream); + } + + public Writer data(File input, ClickHouseFormat format, ClickHouseCompression compression) { + return dataCompression(compression).format(format).data(input); + } + + public Writer dataCompression(ClickHouseCompression compression) { + if (null == compression) { + throw new NullPointerException("Compression can not be null"); + } + this.compression = compression; + return this; + } + public Writer data(File input, ClickHouseFormat format) { return format(format).data(input); } @@ -184,4 +201,8 @@ public InputStream get() throws IOException { return stream; } } + + public ClickHouseCompression getCompression() { + return compression; + } } diff --git a/src/main/java/ru/yandex/clickhouse/domain/ClickHouseCompression.java b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseCompression.java new file mode 100644 index 000000000..d9269d631 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/domain/ClickHouseCompression.java @@ -0,0 +1,9 @@ +package ru.yandex.clickhouse.domain; + +public enum ClickHouseCompression { + none, + gzip, + brotli, + deflate, + zstd; +} 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..f21d13f69 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlStatement.java @@ -0,0 +1,286 @@ +package ru.yandex.clickhouse.jdbc.parser; + +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; + +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 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"; + + 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 List parameters; + private final Map positions; + + public ClickHouseSqlStatement(String sql) { + 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, null); + } + + public ClickHouseSqlStatement(String sql, StatementType stmtType, String cluster, String database, String table, + String format, String outfile, List parameters, 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 (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()) { + String keyword = e.getKey(); + Integer position = e.getValue(); + + if (keyword != null && position != null) { + p.put(keyword.toUpperCase(Locale.ROOT), 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() { + 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() { + 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 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(); + } + + 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 List getParameters() { + return this.parameters; + } + + public int getStartPosition(String keyword) { + int position = -1; + + if (!this.positions.isEmpty() && keyword != null) { + Integer p = this.positions.get(keyword.toUpperCase(Locale.ROOT)); + 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; + } + + 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(", parameters=").append(parameters).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 + ((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()); + 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 (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; + } 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..d9d244487 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/ParseHandler.java @@ -0,0 +1,49 @@ +package ru.yandex.clickhouse.jdbc.parser; + +import java.util.List; +import java.util.Map; + +public abstract class 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 + */ + public String handleMacro(String name, List parameters) { + return null; + } + + /** + * Handle parameter. + * + * @param cluster cluster + * @param database database + * @param table table + * @param columnIndex columnIndex(starts from 1 not 0) + * @return parameter value + */ + 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 new file mode 100644 index 000000000..3797026d9 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/jdbc/parser/StatementType.java @@ -0,0 +1,53 @@ +package ru.yandex.clickhouse.jdbc.parser; + +public enum StatementType { + 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, boolean idempotent) { + this.langType = langType; + this.opType = operationType; + this.idempotent = idempotent; + } + + LanguageType getLanguageType() { + return this.langType; + } + + OperationType getOperationType() { + return this.opType; + } + + boolean isIdempotent() { + return this.idempotent; + } +} diff --git a/src/main/java/ru/yandex/clickhouse/response/ClickHouseLZ4Stream.java b/src/main/java/ru/yandex/clickhouse/response/ClickHouseLZ4Stream.java index dda09690d..75139a5e3 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ClickHouseLZ4Stream.java +++ b/src/main/java/ru/yandex/clickhouse/response/ClickHouseLZ4Stream.java @@ -14,7 +14,7 @@ public class ClickHouseLZ4Stream extends InputStream { - private static final LZ4Factory factory = LZ4Factory.safeInstance(); + private static final LZ4Factory factory = LZ4Factory.fastestInstance(); public static final int MAGIC = 0x82; diff --git a/src/main/java/ru/yandex/clickhouse/response/ClickHouseResponseSummary.java b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResponseSummary.java new file mode 100644 index 000000000..5b0702758 --- /dev/null +++ b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResponseSummary.java @@ -0,0 +1,40 @@ +package ru.yandex.clickhouse.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ClickHouseResponseSummary { + final private long readRows; // number of read rows for selects (may be more than rows in result set) + final private long writtenRows; // number of written rows for inserts + final private long readBytes; + final private long writtenBytes; + final private long totalRowsToRead; + + public ClickHouseResponseSummary(@JsonProperty("read_rows") long readRows, @JsonProperty("written_rows") long writtenRows, @JsonProperty("read_bytes") long readBytes, + @JsonProperty("written_bytes") long writtenBytes, @JsonProperty("total_rows_to_read") long totalRowsToRead) { + this.readRows = readRows; + this.writtenRows = writtenRows; + this.readBytes = readBytes; + this.writtenBytes = writtenBytes; + this.totalRowsToRead = totalRowsToRead; + } + + public long getReadRows() { + return readRows; + } + + public long getWrittenRows() { + return writtenRows; + } + + public long getReadBytes() { + return readBytes; + } + + public long getWrittenBytes() { + return writtenBytes; + } + + public long getTotalRowsToRead() { + return totalRowsToRead; + } +} diff --git a/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java index 305f4138f..11e190797 100644 --- a/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java +++ b/src/main/java/ru/yandex/clickhouse/response/ClickHouseResultSet.java @@ -646,7 +646,7 @@ private int asColNum(String column) { } } // TODO Java8 - throw new RuntimeException("no column " + column + " in columns list " + getColumnNames()); + throw new RuntimeException("no column " + column + " in columns list " + getColumnNamesString()); } private ByteFragment getValue(int colNum) { @@ -700,6 +700,14 @@ public BigDecimal getBigDecimal(int columnIndex, int scale) { return result.setScale(scale, RoundingMode.HALF_UP); } + public String[] getColumnNames() { + String[] columnNames = new String[columns.size()]; + for (int i = 0; i < columns.size(); ++i) { + columnNames[i] = columns.get(i).getColumnName(); + } + return columnNames; + } + @Override public void setFetchDirection(int direction) throws SQLException { // ignore perfomance hint @@ -719,7 +727,7 @@ public String toString() { ", bis=" + bis + ", db='" + db + '\'' + ", table='" + table + '\'' + - ", columns=" + getColumnNames() + + ", columns=" + getColumnNamesString() + ", maxRows=" + maxRows + ", values=" + Arrays.toString(values) + ", lastReadColumn=" + lastReadColumn + @@ -729,7 +737,7 @@ public String toString() { '}'; } - private String getColumnNames() { + private String getColumnNamesString() { StringBuilder sb = new StringBuilder(); for (ClickHouseColumnInfo info : columns) { sb.append(info.getColumnName()).append(' '); diff --git a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java index 202574fed..04a194863 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, ""), /** @@ -35,6 +36,7 @@ public enum ClickHouseConnectionSettings implements DriverPropertyCreator { TIME_TO_LIVE_MILLIS("timeToLiveMillis", 60 * 1000, ""), DEFAULT_MAX_PER_ROUTE("defaultMaxPerRoute", 500, ""), MAX_TOTAL("maxTotal", 10000, ""), + MAX_RETRIES("maxRetries", 3, "Maximum retries(default to 3) for idempotent operation. Set 0 to disable retry."), /** * additional @@ -47,7 +49,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 c99014c82..31af3b25e 100644 --- a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java +++ b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java @@ -1,11 +1,11 @@ package ru.yandex.clickhouse.settings; -import ru.yandex.clickhouse.util.apache.StringUtils; - import java.util.HashMap; import java.util.Map; import java.util.Properties; +import ru.yandex.clickhouse.util.apache.StringUtils; + public class ClickHouseProperties { @@ -22,6 +22,7 @@ public class ClickHouseProperties { private int timeToLiveMillis; private int defaultMaxPerRoute; private int maxTotal; + private int maxRetries; private String host; private int port; private boolean usePathAsDb; @@ -94,7 +95,10 @@ public class ClickHouseProperties { private Boolean insertDeduplicate; private Boolean insertDistributedSync; private Boolean anyJoinDistinctRightTableKeys; - + private Boolean sendProgressInHttpHeaders; + private Boolean waitEndOfQuery; + @Deprecated + private boolean useNewParser; public ClickHouseProperties() { this(new Properties()); @@ -112,6 +116,7 @@ public ClickHouseProperties(Properties info) { this.timeToLiveMillis = (Integer)getSetting(info, ClickHouseConnectionSettings.TIME_TO_LIVE_MILLIS); this.defaultMaxPerRoute = (Integer)getSetting(info, ClickHouseConnectionSettings.DEFAULT_MAX_PER_ROUTE); this.maxTotal = (Integer)getSetting(info, ClickHouseConnectionSettings.MAX_TOTAL); + this.maxRetries = (Integer)getSetting(info, ClickHouseConnectionSettings.MAX_RETRIES); this.maxCompressBufferSize = (Integer) getSetting(info, ClickHouseConnectionSettings.MAX_COMPRESS_BUFFER_SIZE); this.ssl = (Boolean) getSetting(info, ClickHouseConnectionSettings.SSL); this.sslRootCertificate = (String) getSetting(info, ClickHouseConnectionSettings.SSL_ROOT_CERTIFICATE); @@ -124,6 +129,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); @@ -162,6 +168,8 @@ public ClickHouseProperties(Properties info) { this.insertDeduplicate = getSetting(info, ClickHouseQueryParam.INSERT_DEDUPLICATE); this.insertDistributedSync = getSetting(info, ClickHouseQueryParam.INSERT_DISTRIBUTED_SYNC); this.anyJoinDistinctRightTableKeys = getSetting(info, ClickHouseQueryParam.ANY_JOIN_DISTINCT_RIGHT_TABLE_KEYS); + this.sendProgressInHttpHeaders = (Boolean)getSetting(info, ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS); + this.waitEndOfQuery = (Boolean)getSetting(info, ClickHouseQueryParam.WAIT_END_OF_QUERY); } public Properties asProperties() { @@ -176,6 +184,7 @@ public Properties asProperties() { ret.put(ClickHouseConnectionSettings.TIME_TO_LIVE_MILLIS.getKey(), String.valueOf(timeToLiveMillis)); ret.put(ClickHouseConnectionSettings.DEFAULT_MAX_PER_ROUTE.getKey(), String.valueOf(defaultMaxPerRoute)); ret.put(ClickHouseConnectionSettings.MAX_TOTAL.getKey(), String.valueOf(maxTotal)); + ret.put(ClickHouseConnectionSettings.MAX_RETRIES.getKey(), String.valueOf(maxRetries)); ret.put(ClickHouseConnectionSettings.MAX_COMPRESS_BUFFER_SIZE.getKey(), String.valueOf(maxCompressBufferSize)); ret.put(ClickHouseConnectionSettings.SSL.getKey(), String.valueOf(ssl)); ret.put(ClickHouseConnectionSettings.SSL_ROOT_CERTIFICATE.getKey(), String.valueOf(sslRootCertificate)); @@ -188,6 +197,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); @@ -226,6 +236,8 @@ public Properties asProperties() { ret.put(ClickHouseQueryParam.INSERT_DEDUPLICATE.getKey(), insertDeduplicate); ret.put(ClickHouseQueryParam.INSERT_DISTRIBUTED_SYNC.getKey(), insertDistributedSync); ret.put(ClickHouseQueryParam.ANY_JOIN_DISTINCT_RIGHT_TABLE_KEYS.getKey(), anyJoinDistinctRightTableKeys); + ret.put(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS.getKey(), sendProgressInHttpHeaders); + ret.put(ClickHouseQueryParam.WAIT_END_OF_QUERY.getKey(), waitEndOfQuery); return ret.getProperties(); } @@ -243,6 +255,7 @@ public ClickHouseProperties(ClickHouseProperties properties) { setTimeToLiveMillis(properties.timeToLiveMillis); setDefaultMaxPerRoute(properties.defaultMaxPerRoute); setMaxTotal(properties.maxTotal); + setMaxRetries(properties.maxRetries); setMaxCompressBufferSize(properties.maxCompressBufferSize); setSsl(properties.ssl); setSslRootCertificate(properties.sslRootCertificate); @@ -255,6 +268,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); @@ -292,25 +306,47 @@ public ClickHouseProperties(ClickHouseProperties properties) { setInsertDeduplicate(properties.insertDeduplicate); setInsertDistributedSync(properties.insertDistributedSync); setAnyJoinDistinctRightTableKeys(properties.anyJoinDistinctRightTableKeys); + setSendProgressInHttpHeaders(properties.sendProgressInHttpHeaders); + setWaitEndOfQuery(properties.waitEndOfQuery); } public Map buildQueryParams(boolean ignoreDatabase){ - Map params = new HashMap(); + Map params = new HashMap<>(); - if (maxParallelReplicas != null) params.put(ClickHouseQueryParam.MAX_PARALLEL_REPLICAS, String.valueOf(maxParallelReplicas)); - if (maxPartitionsPerInsertBlock != null) params.put(ClickHouseQueryParam.MAX_PARTITIONS_PER_INSERT_BLOCK, String.valueOf(maxPartitionsPerInsertBlock)); - if (maxRowsToGroupBy != null) params.put(ClickHouseQueryParam.MAX_ROWS_TO_GROUP_BY, String.valueOf(maxRowsToGroupBy)); - if (totalsMode != null) params.put(ClickHouseQueryParam.TOTALS_MODE, totalsMode); - if (quotaKey != null) params.put(ClickHouseQueryParam.QUOTA_KEY, quotaKey); - if (priority != null) params.put(ClickHouseQueryParam.PRIORITY, String.valueOf(priority)); + if (maxParallelReplicas != null) { + params.put(ClickHouseQueryParam.MAX_PARALLEL_REPLICAS, String.valueOf(maxParallelReplicas)); + } + if (maxPartitionsPerInsertBlock != null) { + params.put(ClickHouseQueryParam.MAX_PARTITIONS_PER_INSERT_BLOCK, String.valueOf(maxPartitionsPerInsertBlock)); + } + if (maxRowsToGroupBy != null) { + params.put(ClickHouseQueryParam.MAX_ROWS_TO_GROUP_BY, String.valueOf(maxRowsToGroupBy)); + } + if (totalsMode != null) { + params.put(ClickHouseQueryParam.TOTALS_MODE, totalsMode); + } + if (quotaKey != null) { + params.put(ClickHouseQueryParam.QUOTA_KEY, quotaKey); + } + if (priority != null) { + params.put(ClickHouseQueryParam.PRIORITY, String.valueOf(priority)); + } - if (!StringUtils.isBlank(database) && !ignoreDatabase) params.put(ClickHouseQueryParam.DATABASE, getDatabase()); + if (!StringUtils.isBlank(database) && !ignoreDatabase) { + params.put(ClickHouseQueryParam.DATABASE, getDatabase()); + } - if (compress) params.put(ClickHouseQueryParam.COMPRESS, "1"); - if (decompress) params.put(ClickHouseQueryParam.DECOMPRESS, "1"); + if (compress) { + params.put(ClickHouseQueryParam.COMPRESS, "1"); + } + if (decompress) { + params.put(ClickHouseQueryParam.DECOMPRESS, "1"); + } - if (extremes) params.put(ClickHouseQueryParam.EXTREMES, "1"); + if (extremes) { + params.put(ClickHouseQueryParam.EXTREMES, "1"); + } if (StringUtils.isBlank(profile)) { if (getMaxThreads() != null) { @@ -329,13 +365,16 @@ public Map buildQueryParams(boolean ignoreDatabase params.put(ClickHouseQueryParam.PROFILE, profile); } - if (user != null) params.put(ClickHouseQueryParam.USER, user); - if (password != null) params.put(ClickHouseQueryParam.PASSWORD, password); - - if (distributedAggregationMemoryEfficient) params.put(ClickHouseQueryParam.DISTRIBUTED_AGGREGATION_MEMORY_EFFICIENT, "1"); + if (distributedAggregationMemoryEfficient) { + params.put(ClickHouseQueryParam.DISTRIBUTED_AGGREGATION_MEMORY_EFFICIENT, "1"); + } - if (maxBytesBeforeExternalGroupBy != null) params.put(ClickHouseQueryParam.MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY, String.valueOf(maxBytesBeforeExternalGroupBy)); - if (maxBytesBeforeExternalSort != null) params.put(ClickHouseQueryParam.MAX_BYTES_BEFORE_EXTERNAL_SORT, String.valueOf(maxBytesBeforeExternalSort)); + if (maxBytesBeforeExternalGroupBy != null) { + params.put(ClickHouseQueryParam.MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY, String.valueOf(maxBytesBeforeExternalGroupBy)); + } + if (maxBytesBeforeExternalSort != null) { + params.put(ClickHouseQueryParam.MAX_BYTES_BEFORE_EXTERNAL_SORT, String.valueOf(maxBytesBeforeExternalSort)); + } if (maxMemoryUsage != null) { params.put(ClickHouseQueryParam.MAX_MEMORY_USAGE, String.valueOf(maxMemoryUsage)); } @@ -379,6 +418,9 @@ public Map buildQueryParams(boolean ignoreDatabase params.put(ClickHouseQueryParam.ENABLE_OPTIMIZE_PREDICATE_EXPRESSION, enableOptimizePredicateExpression ? "1" : "0"); } + addQueryParam(sendProgressInHttpHeaders, ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, params); + addQueryParam(waitEndOfQuery, ClickHouseQueryParam.WAIT_END_OF_QUERY, params); + return params; } @@ -411,8 +453,9 @@ private T getSetting(Properties info, ClickHouseConnectionSettings settings) @SuppressWarnings("unchecked") private T getSetting(Properties info, String key, Object defaultValue, Class clazz){ String val = info.getProperty(key); - if (val == null) + if (val == null) { return (T)defaultValue; + } if (clazz == int.class || clazz == Integer.class) { return (T) clazz.cast(Integer.valueOf(val)); } @@ -520,10 +563,12 @@ public void setDataTransferTimeout(int dataTransferTimeout) { this.dataTransferTimeout = dataTransferTimeout; } + @Deprecated public int getKeepAliveTimeout() { return keepAliveTimeout; } + @Deprecated public void setKeepAliveTimeout(int keepAliveTimeout) { this.keepAliveTimeout = keepAliveTimeout; } @@ -560,6 +605,14 @@ public void setMaxTotal(int maxTotal) { this.maxTotal = maxTotal; } + public int getMaxRetries() { + return maxRetries; + } + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + public int getMaxCompressBufferSize() { return maxCompressBufferSize; } @@ -631,6 +684,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; } @@ -907,6 +970,22 @@ public Boolean getAnyJoinDistinctRightTableKeys() { return anyJoinDistinctRightTableKeys; } + public Boolean getSendProgressInHttpHeaders() { + return sendProgressInHttpHeaders; + } + + public void setSendProgressInHttpHeaders(Boolean sendProgressInHttpHeaders) { + this.sendProgressInHttpHeaders = sendProgressInHttpHeaders; + } + + public Boolean getWaitEndOfQuery() { + return waitEndOfQuery; + } + + public void setWaitEndOfQuery(Boolean waitEndOfQuery) { + this.waitEndOfQuery = waitEndOfQuery; + } + private static class PropertiesBuilder { private final Properties properties; public PropertiesBuilder() { @@ -948,16 +1027,18 @@ public Properties getProperties() { public ClickHouseProperties merge(ClickHouseProperties second){ Properties properties = this.asProperties(); - for (Map.Entry entry : second.asProperties().entrySet()) + for (Map.Entry entry : second.asProperties().entrySet()) { properties.put(entry.getKey(), entry.getValue()); + } return new ClickHouseProperties(properties); } public ClickHouseProperties merge(Properties other){ Properties properties = this.asProperties(); - for (Map.Entry entry : other.entrySet()) + for (Map.Entry entry : other.entrySet()) { properties.put(entry.getKey(), entry.getValue()); + } return new ClickHouseProperties(properties); } diff --git a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseQueryParam.java b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseQueryParam.java index c1d1a991a..4d6e57baa 100644 --- a/src/main/java/ru/yandex/clickhouse/settings/ClickHouseQueryParam.java +++ b/src/main/java/ru/yandex/clickhouse/settings/ClickHouseQueryParam.java @@ -217,6 +217,8 @@ public enum ClickHouseQueryParam implements DriverPropertyCreator { SELECT_SEQUENTIAL_CONSISTENCY("select_sequential_consistency", null, Long.class, ""), + SEND_PROGRESS_IN_HTTP_HEADERS("send_progress_in_http_headers", null, Boolean.class, "Allow to populate summary in ClickHouseStatement with read/written rows/bytes"), + SEND_TIMEOUT("send_timeout", null, Integer.class, ""), SESSION_CHECK("session_check", false, Boolean.class, ""), @@ -253,6 +255,8 @@ public enum ClickHouseQueryParam implements DriverPropertyCreator { PREFERRED_BLOCK_SIZE_BYTES("preferred_block_size_bytes", null, Long.class, "Adaptively estimates number of required rows in a block."), ENABLE_OPTIMIZE_PREDICATE_EXPRESSION("enable_optimize_predicate_expression", null, Boolean.class, "See Clickhouse server description for this parameter. Default value is null so that server setting is taken."), + + WAIT_END_OF_QUERY("wait_end_of_query", null, Boolean.class, "Buffer the response server-side before sending to client. Useful when using SEND_PROGRESS_IN_HTTP_HEADERS to get accurate stats."), ; private final String key; diff --git a/src/main/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilder.java b/src/main/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilder.java index 040cbaab6..66bc9980b 100644 --- a/src/main/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilder.java +++ b/src/main/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilder.java @@ -1,30 +1,5 @@ package ru.yandex.clickhouse.util; -import org.apache.http.*; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.ConnectionConfig; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.ConnectionKeepAliveStrategy; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.DefaultConnectionReuseStrategy; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicHeaderElementIterator; -import org.apache.http.protocol.HTTP; -import org.apache.http.protocol.HttpContext; -import ru.yandex.clickhouse.settings.ClickHouseProperties; -import ru.yandex.clickhouse.util.ssl.NonValidatingTrustManager; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -44,6 +19,45 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.http.ConnectionReuseStrategy; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NoHttpResponseException; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.ConnectionConfig; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.DefaultConnectionReuseStrategy; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; + +import ru.yandex.clickhouse.settings.ClickHouseProperties; +import ru.yandex.clickhouse.util.ssl.NonValidatingTrustManager; + public class ClickHouseHttpClientBuilder { private final ClickHouseProperties properties; @@ -55,15 +69,46 @@ public ClickHouseHttpClientBuilder(ClickHouseProperties properties) { public CloseableHttpClient buildClient() throws Exception { return HttpClientBuilder.create() .setConnectionManager(getConnectionManager()) + .setRetryHandler(getRequestRetryHandler()) .setConnectionReuseStrategy(getConnectionReuseStrategy()) .setDefaultConnectionConfig(getConnectionConfig()) .setDefaultRequestConfig(getRequestConfig()) .setDefaultHeaders(getDefaultHeaders()) - .disableContentCompression() // gzip здесь ни к чему. Используется lz4 при compress=1 + .setDefaultCredentialsProvider(getDefaultCredentialsProvider()) + .disableContentCompression() // gzip is not needed. Use lz4 when compress=1 .disableRedirectHandling() .build(); } + private HttpRequestRetryHandler getRequestRetryHandler() { + final int maxRetries = properties.getMaxRetries(); + return new DefaultHttpRequestRetryHandler(maxRetries, false) { + @Override + public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { + if (executionCount > maxRetries || context == null + || !Boolean.TRUE.equals(context.getAttribute("is_idempotent"))) { + return false; + } + + return (exception instanceof NoHttpResponseException) || super.retryRequest(exception, executionCount, context); + } + }; + } + + public static HttpClientContext createClientContext(ClickHouseProperties props) { + if (props == null + || !isConfigurationValidForAuth(props)) + { + return HttpClientContext.create(); + } + AuthCache authCache = new BasicAuthCache(); + BasicScheme basicAuth = new BasicScheme(); + authCache.put(getTargetHost(props), basicAuth); + HttpClientContext ctx = HttpClientContext.create(); + ctx.setAuthCache(authCache); + return ctx; + } + private ConnectionReuseStrategy getConnectionReuseStrategy() { return new DefaultConnectionReuseStrategy() { @Override @@ -112,41 +157,19 @@ private RequestConfig getRequestConfig() { return RequestConfig.custom() .setSocketTimeout(properties.getSocketTimeout()) .setConnectTimeout(properties.getConnectionTimeout()) + .setConnectionRequestTimeout(properties.getConnectionTimeout()) .build(); } private Collection
getDefaultHeaders() { - List
headers = new ArrayList
(); + List
headers = new ArrayList<>(); if (properties.getHttpAuthorization() != null) { headers.add(new BasicHeader(HttpHeaders.AUTHORIZATION, properties.getHttpAuthorization())); } return headers; } - private ConnectionKeepAliveStrategy createKeepAliveStrategy() { - return new ConnectionKeepAliveStrategy() { - @Override - public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) { - // in case of errors keep-alive not always works. close connection just in case - if (httpResponse.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) { - return -1; - } - HeaderElementIterator it = new BasicHeaderElementIterator( - httpResponse.headerIterator(HTTP.CONN_DIRECTIVE)); - while (it.hasNext()) { - HeaderElement he = it.nextElement(); - String param = he.getName(); - //String value = he.getValue(); - if (param != null && param.equalsIgnoreCase(HTTP.CONN_KEEP_ALIVE)) { - return properties.getKeepAliveTimeout(); - } - } - return -1; - } - }; - } - - private SSLContext getSSLContext() + private SSLContext getSSLContext() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { SSLContext ctx = SSLContext.getInstance("TLS"); TrustManager[] tms = null; @@ -209,4 +232,31 @@ private KeyStore getKeyStore() caInputStream.close(); } } + + private CredentialsProvider getDefaultCredentialsProvider() { + if (!isConfigurationValidForAuth(properties)) { + return null; + } + HttpHost targetHost = getTargetHost(properties); + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(targetHost.getHostName(), targetHost.getPort()), + new UsernamePasswordCredentials( + properties.getUser() != null ? properties.getUser() : "default", + properties.getPassword() != null ? properties.getPassword() : "")); + return credsProvider; + } + + private static HttpHost getTargetHost(ClickHouseProperties props) { + return new HttpHost( + props.getHost(), + props.getPort(), + props.getSsl() ? "https" : "http"); + } + + private static boolean isConfigurationValidForAuth(ClickHouseProperties props) { + return props.getHost() != null + && props.getHttpAuthorization() == null + && (props.getUser() != null || props.getPassword() != null); + } } diff --git a/src/main/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStream.java b/src/main/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStream.java index 5c20ec2c3..07805be88 100644 --- a/src/main/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStream.java +++ b/src/main/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStream.java @@ -9,7 +9,7 @@ import java.io.OutputStream; public class ClickHouseLZ4OutputStream extends OutputStream { - private static final LZ4Factory factory = LZ4Factory.safeInstance(); + private static final LZ4Factory factory = LZ4Factory.fastestInstance(); private final LittleEndianDataOutputStream dataWrapper; private final LZ4Compressor compressor; @@ -26,8 +26,8 @@ public ClickHouseLZ4OutputStream(OutputStream stream, int maxCompressBlockSize) } /** - * @return Location of pointer in the byte buffer (bytes not yet flushed) - */ + * @return Location of pointer in the byte buffer (bytes not yet flushed) + */ public int position() { return pointer; } @@ -42,9 +42,36 @@ public void write(int b) throws IOException { } } + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + int blockSize = currentBlock.length; + int rest = blockSize - pointer; + while (len >= rest) { + System.arraycopy(b, off, currentBlock, pointer, rest); + pointer += rest; + writeBlock(); + off += rest; + len -= rest; + rest = blockSize; + } + + if (len > 0) { + System.arraycopy(b, off, currentBlock, pointer, len); + pointer += len; + } + } + @Override public void flush() throws IOException { - if (pointer != 0){ + if (pointer != 0) { writeBlock(); } dataWrapper.flush(); @@ -52,7 +79,7 @@ public void flush() throws IOException { private void writeBlock() throws IOException { int compressed = compressor.compress(currentBlock, 0, pointer, compressedBlock, 0); - ClickHouseBlockChecksum checksum = ClickHouseBlockChecksum.calculateForBlock((byte)ClickHouseLZ4Stream.MAGIC, + ClickHouseBlockChecksum checksum = ClickHouseBlockChecksum.calculateForBlock((byte) ClickHouseLZ4Stream.MAGIC, compressed + 9, pointer, compressedBlock, compressed); dataWrapper.write(checksum.asBytes()); dataWrapper.writeByte(ClickHouseLZ4Stream.MAGIC); diff --git a/src/main/javacc/ClickHouseSqlParser.jj b/src/main/javacc/ClickHouseSqlParser.jj new file mode 100644 index 000000000..0810dfecd --- /dev/null +++ b/src/main/javacc/ClickHouseSqlParser.jj @@ -0,0 +1,1132 @@ +/** + * 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 than 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; + } + + // 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. + * + * @deprecated This method will be removed in the near future. + *

+ * 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); + } + + /** + * Parse given SQL. + * + * @deprecated This method will be removed in the near future. + *

+ * Use {@link #parse(String, ClickHouseProperties, 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); + } + + 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[] { + new ClickHouseSqlStatement(sql, StatementType.UNKNOWN) }; + + if (!properties.isUseNewParser() || sql == null || sql.isEmpty()) { + return stmts; + } + + ClickHouseSqlParser p = new ClickHouseSqlParser(sql, properties, handler); + try { + 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); + } + } + + 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()) { + 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); + } + } else { + token_source.reset(); + } + } +} + +PARSER_END(ClickHouseSqlParser) + +TOKEN_MGR_DECLS: { + // 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(); + + 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 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); + + 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() { + stack.clear(); + parentToken = -1; + + builder.setLength(validTokens = 0); + + stmtType = StatementType.UNKNOWN; + cluster = null; + database = null; + table = null; + format = null; + outfile = null; + parameters.clear(); + positions.clear(); + } + + 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(); + + 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); } + | { append(image); } + | "/*" { commentNestingDepth = 1; append(image); }: MULTI_LINE_COMMENT +} + + SKIP: { + "/*" { commentNestingDepth += 1; append(image); } + | "*/" { SwitchTo(--commentNestingDepth == 0 ? DEFAULT : MULTI_LINE_COMMENT); 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) (LOOKAHEAD(2) clusterClause())? ( + LOOKAHEAD({ !tokenIn(1, UPDATE, DELETE) }) anyIdentifier() + | { 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(): { 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/ +void checkStmt(): {} { // not interested + anyExprList() +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/create/ +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(): {} { + tableIdentifier(true) ( anyExprList())? +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/describe-table/ +void describeStmt(): {} { + ( | ) { token_source.table = "columns"; } + (LOOKAHEAD({ getToken(1).kind == TABLE })
)? (LOOKAHEAD(2) tableIdentifier(true) | anyExprList()) +} + +// https://clickhouse.tech/docs/en/sql-reference/statements/detach/ +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(): { 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/ +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; } { + try { + LOOKAHEAD(2) anyIdentifier() (LOOKAHEAD(2) anyExprList())? + | LOOKAHEAD(2) t = { token_source.setPosition(t.image); } + columnExprList() + ( + 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/ +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 * + (withClause())? +
)? 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 withExpr(): {} { + nestedExpr() + ( + ( + LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) + | + )+ + | (LOOKAHEAD(2) anyExprList() )+ + | LOOKAHEAD(2) ()? + | LOOKAHEAD(2) ()? betweenExpr() + | LOOKAHEAD(2) ()? ( | ) nestedExpr() + | LOOKAHEAD(2, { noAndWithinBetween() }) (LOOKAHEAD(2, { noAndWithinBetween() }) calcExpr())+ + | LOOKAHEAD(2) ()? nestedExpr() + | LOOKAHEAD(2) nestedExpr() nestedExpr() + | LOOKAHEAD(2) columnExpr() + )? +} + +void columnsExpr(): {} { + LOOKAHEAD(allColumnsExpr()) allColumnsExpr() + ( + LOOKAHEAD(2) ( | | ) anyExprList() + )* + | nestedExpr() + ( + ( + LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) + | + )+ + | (LOOKAHEAD(2) anyExprList() )+ + | LOOKAHEAD(2) ()? + | LOOKAHEAD(2) ()? betweenExpr() + | LOOKAHEAD(2) ()? ( | ) nestedExpr() + | LOOKAHEAD(2, { noAndWithinBetween() }) (LOOKAHEAD(2, { noAndWithinBetween() }) calcExpr())+ + | LOOKAHEAD(2) ()? nestedExpr() + | LOOKAHEAD(2) nestedExpr() nestedExpr() + | LOOKAHEAD(2) aliasExpr() + )? +} + +void allColumnsExpr(): {} { + | anyIdentifier() (LOOKAHEAD(2) anyIdentifier() )? +} + +void nestedExpr(): {} { + LOOKAHEAD(2) ( | ) nestedExpr() + | LOOKAHEAD(2) (LOOKAHEAD({ getToken(1).kind != WHEN }) nestedExpr())? + ( nestedExpr() nestedExpr())+ ( nestedExpr())? + | LOOKAHEAD(2) (LOOKAHEAD(2) | nestedExpr() interval()) + | columnExpr() + ( + ( + | + )+ + | (LOOKAHEAD(2) anyExprList() )+ + | LOOKAHEAD(2) ()? + | LOOKAHEAD(2) ()? betweenExpr() + | LOOKAHEAD(2) ()? ( | ) nestedExpr() + | LOOKAHEAD(2, { noAndWithinBetween() }) (LOOKAHEAD(2, { noAndWithinBetween() }) calcExpr())+ + | LOOKAHEAD(2) ()? nestedExpr() + | LOOKAHEAD(2) nestedExpr() nestedExpr() + )? +} + +void calcExpr(): {} { + ( | | operator()) nestedExpr() +} + +void betweenExpr(): {} { + { token_source.enterToken(BETWEEN); } + nestedExpr() + { token_source.leaveToken(BETWEEN); } + nestedExpr() +} + +void functionExpr(): {} { + anyIdentifier() (anyExprList())? + // https://clickhouse.tech/docs/en/sql-reference/aggregate-functions/parametric-functions/ + (LOOKAHEAD(2) (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() ( + LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) + | + )* +} + +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() + // | (LOOKAHEAD(2, { !(tokenIn(1, INF, NAN, NULL)) }) | 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 = + ) { sb.append(t.image); } + ) + { return Token.newToken(0, sb.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 = + | 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 = | 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: { + "> + | + | + | + | + | + | + | + | + | + | + | + | ="> + | "> + | + | + | + | + | + | "> + | + | + | + | + | + | + | + | + | + | + | <#UNDERSCORE: "_"> +} + +// string literal +TOKEN: { + ( ~[] | ~["'", "\\"] | "''")* > +} + +TOKEN: { + | | ) ( | | | )* + | ()+ ( + ( | )* + | ( + | | | | | | | | | | | | + | |

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

| ) ( | )? + | (

| ) ( | )? + | ()? ( ( | )? )? + | ( ( | )? )? + | ( | )? > +} +TOKEN: { )+> } +TOKEN: { ()+> } diff --git a/src/test/java/ru/yandex/clickhouse/BalancedClickhouseDataSourceTest.java b/src/test/java/ru/yandex/clickhouse/BalancedClickhouseDataSourceTest.java index 61f69325a..b21c4b21b 100644 --- a/src/test/java/ru/yandex/clickhouse/BalancedClickhouseDataSourceTest.java +++ b/src/test/java/ru/yandex/clickhouse/BalancedClickhouseDataSourceTest.java @@ -1,15 +1,18 @@ package ru.yandex.clickhouse; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; -import ru.yandex.clickhouse.settings.ClickHouseProperties; - import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Arrays; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import ru.yandex.clickhouse.settings.ClickHouseProperties; + import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; public class BalancedClickhouseDataSourceTest { @@ -46,7 +49,6 @@ public void testUrlSplitInvalidHostName() throws Exception { } - @BeforeTest public void setUp() throws Exception { dataSource = ClickHouseContainerForTest.newBalancedDataSource(); @@ -54,7 +56,6 @@ public void setUp() throws Exception { doubleDataSource = ClickHouseContainerForTest.newBalancedDataSource(address, address); } - @Test public void testSingleDatabaseConnection() throws Exception { Connection connection = dataSource.getConnection(); @@ -77,7 +78,6 @@ public void testSingleDatabaseConnection() throws Exception { assertEquals(42, rs.getInt("i")); } - @Test public void testDoubleDatabaseConnection() throws Exception { Connection connection = doubleDataSource.getConnection(); @@ -116,7 +116,6 @@ public void testDoubleDatabaseConnection() throws Exception { } - @Test public void testCorrectActualizationDatabaseConnection() throws Exception { dataSource.actualize(); @@ -212,4 +211,105 @@ public void testConstructWithClickHouseProperties() { assertEquals(dataSource.getAllClickhouseUrls().get(1), "jdbc:clickhouse://" + ipAddr + "/click?socket_timeout=12345&user=readonly"); } + @Test + public void testConnectionWithAuth() throws SQLException { + final ClickHouseProperties properties = new ClickHouseProperties(); + final String hostAddr = ClickHouseContainerForTest.getClickHouseHttpAddress(); + final String ipAddr = ClickHouseContainerForTest.getClickHouseHttpAddress(true); + + final BalancedClickhouseDataSource dataSource0 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=foo&password=bar", + properties, + hostAddr, + ipAddr); + assertTrue(dataSource0.getConnection().createStatement().execute("SELECT 1")); + + final BalancedClickhouseDataSource dataSource1 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=foo", + properties, + hostAddr, + ipAddr); + // assertThrows(RuntimeException.class, + // () -> dataSource1.getConnection().createStatement().execute("SELECT 1")); + + + final BalancedClickhouseDataSource dataSource2 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=oof", + properties, + hostAddr, + ipAddr); + assertTrue(dataSource2.getConnection().createStatement().execute("SELECT 1")); + + properties.setUser("foo"); + properties.setPassword("bar"); + final BalancedClickhouseDataSource dataSource3 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default", + properties, + hostAddr, + ipAddr); + assertTrue(dataSource3.getConnection().createStatement().execute("SELECT 1")); + + properties.setPassword("bar"); + final BalancedClickhouseDataSource dataSource4 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=oof", + properties, + hostAddr, + ipAddr); + // JDK 1.8 + // assertThrows(RuntimeException.class, + // () -> dataSource4.getConnection().createStatement().execute("SELECT 1")); + try { + dataSource4.getConnection().createStatement().execute("SELECT 1"); + fail(); + } catch (RuntimeException re) { + // expected + } + + // it is not allowed to have query parameters per host + try { + ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=oof", + properties, + hostAddr + "/default?user=foo&password=bar", + ipAddr); + fail(); + } catch (IllegalArgumentException iae) { + // expected + } + + // the following behavior is quite unexpected, honestly + // but query params always have precedence over properties + final BalancedClickhouseDataSource dataSource5 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=foo&password=bar", + properties, + hostAddr, + ipAddr); + assertTrue( + dataSource5.getConnection("broken", "hacker").createStatement().execute("SELECT 1")); + + // now the other way round, also strange + final BalancedClickhouseDataSource dataSource6 = ClickHouseContainerForTest + .newBalancedDataSourceWithSuffix( + "default?user=broken&password=hacker", + properties, + hostAddr, + ipAddr); + // JDK 1.8 + // assertThrows(RuntimeException.class, + // () -> dataSource6.getConnection("foo", "bar").createStatement().execute("SELECT 1")); + try { + dataSource6.getConnection("foo", "bar").createStatement().execute("SELECT 1"); + fail(); + } catch (RuntimeException re) { + // expected + } + } + } diff --git a/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java b/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java index c98b649ee..05c2c1b2a 100644 --- a/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java +++ b/src/test/java/ru/yandex/clickhouse/ClickHouseContainerForTest.java @@ -1,16 +1,16 @@ package ru.yandex.clickhouse; +import java.time.Duration; + +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; import ru.yandex.clickhouse.settings.ClickHouseProperties; import ru.yandex.clickhouse.util.ClickHouseVersionNumberUtil; -import java.time.Duration; - -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; - import static java.time.temporal.ChronoUnit.SECONDS; public class ClickHouseContainerForTest { @@ -33,11 +33,15 @@ public class ClickHouseContainerForTest { } imageTag = ":" + imageTag; } - clickhouseContainer = new GenericContainer<>("yandex/clickhouse-server" + imageTag) + .withExposedPorts(HTTP_PORT, NATIVE_PORT, MYSQL_PORT) + .withClasspathResourceMapping( + "ru/yandex/clickhouse/users.d", + "/etc/clickhouse-server/users.d", + BindMode.READ_ONLY) .waitingFor(Wait.forHttp("/ping").forPort(HTTP_PORT).forStatusCode(200) - .withStartupTimeout(Duration.of(60, SECONDS))) - .withExposedPorts(HTTP_PORT, NATIVE_PORT, MYSQL_PORT); + .withStartupTimeout(Duration.of(60, SECONDS))); + } public static String getClickHouseVersion() { @@ -110,13 +114,13 @@ public static BalancedClickhouseDataSource newBalancedDataSourceWithSuffix(Strin return new BalancedClickhouseDataSource(url.toString(), properties); } - @BeforeSuite - public void beforeSuite() { + @BeforeSuite() + public static void beforeSuite() { clickhouseContainer.start(); } - @AfterSuite - public void afterSuite() { + @AfterSuite() + public static void afterSuite() { clickhouseContainer.stop(); } } diff --git a/src/test/java/ru/yandex/clickhouse/ClickHouseStatementTest.java b/src/test/java/ru/yandex/clickhouse/ClickHouseStatementTest.java index 36762fa19..337910976 100644 --- a/src/test/java/ru/yandex/clickhouse/ClickHouseStatementTest.java +++ b/src/test/java/ru/yandex/clickhouse/ClickHouseStatementTest.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Collections; import java.util.Properties; import org.apache.http.impl.client.HttpClientBuilder; @@ -13,6 +14,7 @@ import com.google.common.collect.ImmutableMap; import ru.yandex.clickhouse.settings.ClickHouseProperties; +import ru.yandex.clickhouse.settings.ClickHouseQueryParam; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -71,37 +73,38 @@ public void testCredentials() throws SQLException, URISyntaxException { assertEquals(withCredentials.getPassword(), "test_password"); ClickHouseStatementImpl statement = new ClickHouseStatementImpl( - HttpClientBuilder.create().build(),null, withCredentials, ResultSet.TYPE_FORWARD_ONLY - ); + HttpClientBuilder.create().build(), + null, withCredentials, ResultSet.TYPE_FORWARD_ONLY); URI uri = statement.buildRequestUri(null, null, null, null, false); String query = uri.getQuery(); - assertTrue(query.contains("password=test_password")); - assertTrue(query.contains("user=test_user")); + // we use Basic AUTH nowadays + assertFalse(query.contains("password=test_password")); + assertFalse(query.contains("user=test_user")); } @Test public void testMaxExecutionTime() throws Exception { ClickHouseProperties properties = new ClickHouseProperties(); properties.setMaxExecutionTime(20); - ClickHouseStatementImpl statement = new ClickHouseStatementImpl(HttpClientBuilder.create().build(), null, - properties, ResultSet.TYPE_FORWARD_ONLY); + ClickHouseStatementImpl statement = new ClickHouseStatementImpl(HttpClientBuilder.create().build(), + null, properties, ResultSet.TYPE_FORWARD_ONLY); URI uri = statement.buildRequestUri(null, null, null, null, false); String query = uri.getQuery(); assertTrue(query.contains("max_execution_time=20"), "max_execution_time param is missing in URL"); - + statement.setQueryTimeout(10); uri = statement.buildRequestUri(null, null, null, null, false); query = uri.getQuery(); assertTrue(query.contains("max_execution_time=10"), "max_execution_time param is missing in URL"); } - + @Test public void testMaxMemoryUsage() throws Exception { ClickHouseProperties properties = new ClickHouseProperties(); properties.setMaxMemoryUsage(41L); - ClickHouseStatementImpl statement = new ClickHouseStatementImpl(HttpClientBuilder.create().build(), null, - properties, ResultSet.TYPE_FORWARD_ONLY); + ClickHouseStatementImpl statement = new ClickHouseStatementImpl(HttpClientBuilder.create().build(), + null, properties, ResultSet.TYPE_FORWARD_ONLY); URI uri = statement.buildRequestUri(null, null, null, null, false); String query = uri.getQuery(); @@ -109,7 +112,7 @@ public void testMaxMemoryUsage() throws Exception { } @Test - public void testAdditionalRequestParams() throws Exception { + public void testAdditionalRequestParams() { ClickHouseProperties properties = new ClickHouseProperties(); ClickHouseStatementImpl statement = new ClickHouseStatementImpl( HttpClientBuilder.create().build(), @@ -118,15 +121,58 @@ public void testAdditionalRequestParams() throws Exception { ResultSet.TYPE_FORWARD_ONLY ); + statement.option("cache_namespace", "aaaa"); URI uri = statement.buildRequestUri( null, null, null, - ImmutableMap.of("cache_namespace", "aaaa"), + null, false ); String query = uri.getQuery(); assertTrue(query.contains("cache_namespace=aaaa"), "cache_namespace param is missing in URL"); + + uri = statement.buildRequestUri( + null, + null, + null, + ImmutableMap.of("cache_namespace", "bbbb"), + false + ); + query = uri.getQuery(); + assertTrue(query.contains("cache_namespace=bbbb"), "cache_namespace param is missing in URL"); + + // check that statement level params are given to Writer + assertEquals(statement.write().getRequestParams().get("cache_namespace"), "aaaa"); + } + + @Test + public void testAdditionalDBParams() { + ClickHouseProperties properties = new ClickHouseProperties(); + properties.setMaxThreads(1); + + ClickHouseStatementImpl statement = new ClickHouseStatementImpl( + HttpClientBuilder.create().build(), + null, + properties, + ResultSet.TYPE_FORWARD_ONLY + ); + + URI uri = statement.buildRequestUri(null, null, null, null, false); + assertTrue(uri.getQuery().contains("max_threads=1")); + + // override on statement level + statement.addDbParam(ClickHouseQueryParam.MAX_THREADS, "2"); + + uri = statement.buildRequestUri(null, null, null, null, false); + assertTrue(uri.getQuery().contains("max_threads=2")); + + // override on method level + uri = statement.buildRequestUri(null, null, Collections.singletonMap(ClickHouseQueryParam.MAX_THREADS, "3"), null, false); + assertTrue(uri.getQuery().contains("max_threads=3")); + + // check that statement level params are given to Writer + assertEquals(statement.write().getAdditionalDBParams().get(ClickHouseQueryParam.MAX_THREADS), "2"); } @Test diff --git a/src/test/java/ru/yandex/clickhouse/integration/ClickHouseConnectionImplTest.java b/src/test/java/ru/yandex/clickhouse/integration/ClickHouseConnectionImplTest.java new file mode 100644 index 000000000..990581e5e --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/integration/ClickHouseConnectionImplTest.java @@ -0,0 +1,94 @@ +package ru.yandex.clickhouse.integration; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.testng.annotations.Test; + +import ru.yandex.clickhouse.ClickHouseContainerForTest; +import ru.yandex.clickhouse.settings.ClickHouseProperties; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class ClickHouseConnectionImplTest { + + @Test + public void testDefaultEmpty() throws Exception { + assertSuccess(createDataSource(null, null)); + } + + @Test + public void testDefaultUserOnly() throws Exception { + assertSuccess(createDataSource("default", null)); + } + + @Test + public void testDefaultUserEmptyPassword() throws Exception { + assertSuccess(createDataSource("default", "")); + } + + @Test + public void testDefaultUserPass() throws Exception { + assertFailure(createDataSource("default", "bar")); + } + + @Test + public void testDefaultPass() throws Exception { + assertFailure(createDataSource(null, "bar")); + } + + @Test + public void testFooEmpty() throws Exception { + assertFailure(createDataSource("foo", null)); + } + + @Test + public void testFooWrongPass() throws Exception { + assertFailure(createDataSource("foo", "baz")); + } + + @Test + public void testFooPass() throws Exception { + assertSuccess(createDataSource("foo", "bar")); + } + + @Test + public void testFooWrongUser() throws Exception { + assertFailure(createDataSource("baz", "bar")); + } + + @Test + public void testOofNoPassword() throws Exception { + assertSuccess(createDataSource("oof", null)); + } + + @Test + public void testOofWrongPassword() throws Exception { + assertFailure(createDataSource("oof", "baz")); + } + + private static void assertSuccess(DataSource dataSource) throws Exception { + Connection connection = dataSource.getConnection(); + assertTrue(connection.createStatement().execute("SELECT 1")); + } + + private static void assertFailure(DataSource dataSource) throws Exception { + // grrr, no JDK 1.8 + // assertThrows(SQLException.class, () -> dataSource.getConnection()); + try { + dataSource.getConnection(); + fail(); + } catch (RuntimeException e) { + // exppected + } + } + + private static DataSource createDataSource(String user, String password) { + ClickHouseProperties props = new ClickHouseProperties(); + props.setUser(user); + props.setPassword(password); + return ClickHouseContainerForTest.newDataSource(props); + } +} 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 a1c6556c4..77e84e530 100644 --- a/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java +++ b/src/test/java/ru/yandex/clickhouse/integration/ClickHouseStatementImplTest.java @@ -36,6 +36,7 @@ import static org.testng.AssertJUnit.assertTrue; public class ClickHouseStatementImplTest { + private ClickHouseDataSource dataSource; private ClickHouseConnection connection; @@ -166,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(); @@ -223,12 +234,12 @@ public void testSelectQueryStartingWithWith() throws SQLException { public void cancelTest_queryId_is_not_set() throws Exception { final ClickHouseStatement firstStatement = dataSource.getConnection().createStatement(); - final AtomicReference exceptionAtomicReference = new AtomicReference(); + final AtomicReference exceptionAtomicReference = new AtomicReference<>(); Thread thread = new Thread() { @Override public void run() { try { - Map params = new EnumMap(ClickHouseQueryParam.class); + Map params = new EnumMap<>(ClickHouseQueryParam.class); params.put(ClickHouseQueryParam.CONNECT_TIMEOUT, Long.toString(TimeUnit.MINUTES.toMillis(1))); firstStatement.executeQuery("SELECT count() FROM system.numbers", params); } catch (Exception e) { @@ -261,12 +272,12 @@ public void cancelTest_queryId_is_set() throws Exception { final ClickHouseStatement firstStatement = dataSource.getConnection().createStatement(); final CountDownLatch countDownLatch = new CountDownLatch(1); - final AtomicReference exceptionAtomicReference = new AtomicReference(); + final AtomicReference exceptionAtomicReference = new AtomicReference<>(); Thread thread = new Thread() { @Override public void run() { try { - Map params = new EnumMap(ClickHouseQueryParam.class); + Map params = new EnumMap<>(ClickHouseQueryParam.class); params.put(ClickHouseQueryParam.CONNECT_TIMEOUT, Long.toString(TimeUnit.MINUTES.toMillis(1))); params.put(ClickHouseQueryParam.QUERY_ID, queryId); countDownLatch.countDown(); diff --git a/src/test/java/ru/yandex/clickhouse/integration/ResultSummaryTest.java b/src/test/java/ru/yandex/clickhouse/integration/ResultSummaryTest.java new file mode 100644 index 000000000..79ad4661a --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/integration/ResultSummaryTest.java @@ -0,0 +1,136 @@ +package ru.yandex.clickhouse.integration; + +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; +import ru.yandex.clickhouse.*; +import ru.yandex.clickhouse.settings.ClickHouseQueryParam; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +public class ResultSummaryTest { + private ClickHouseConnection connection; + + @BeforeTest + public void setUp() throws Exception { + connection = ClickHouseContainerForTest.newDataSource().getConnection(); + connection.createStatement().execute("CREATE DATABASE IF NOT EXISTS test"); + } + + @AfterTest + public void tearDown() throws Exception { + connection.createStatement().execute("DROP DATABASE IF EXISTS test"); + } + + @Test + public void select() throws Exception { + ClickHouseStatement st = connection.createStatement(); + st.executeQuery("SELECT * FROM numbers(10)", Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertTrue(st.getResponseSummary().getReadRows() >= 10); + assertTrue(st.getResponseSummary().getReadBytes() > 0); + } + + @Test + public void largeSelect() throws Exception { + ClickHouseStatement st = connection.createStatement(); + st.executeQuery("SELECT * FROM numbers(10000000)", Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertTrue(st.getResponseSummary().getReadRows() < 10000000); + assertTrue(st.getResponseSummary().getReadBytes() > 0); + } + + @Test + public void largeSelectWaitEndOfQuery() throws Exception { + ClickHouseStatement st = connection.createStatement(); + st.executeQuery("SELECT * FROM numbers(10000000)", largeSelectWaitEndOfQueryParams()); + + assertTrue(st.getResponseSummary().getReadRows() >= 10000000); + assertTrue(st.getResponseSummary().getReadBytes() > 0); + } + + private Map largeSelectWaitEndOfQueryParams() { + Map res = new HashMap<>(); + res.put(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true"); + res.put(ClickHouseQueryParam.WAIT_END_OF_QUERY, "true"); + return res; + } + + @Test + public void selectWithoutParam() throws Exception { + ClickHouseStatement st = connection.createStatement(); + st.executeQuery("SELECT * FROM numbers(10)", Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertTrue(st.getResponseSummary().getReadRows() >= 10); + assertTrue(st.getResponseSummary().getReadBytes() > 0); + } + + @Test + public void insertSingle() throws Exception { + createInsertTestTable(); + + ClickHousePreparedStatement ps = (ClickHousePreparedStatement) connection.prepareStatement("INSERT INTO test.insert_test VALUES(?)"); + ps.setLong(1, 1); + ps.executeQuery(Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertEquals(ps.getResponseSummary().getWrittenRows(), 1); + assertTrue(ps.getResponseSummary().getWrittenBytes() > 0); + } + + @Test + public void insertBatch() throws Exception { + createInsertTestTable(); + + ClickHousePreparedStatement ps = (ClickHousePreparedStatement) connection.prepareStatement("INSERT INTO test.insert_test VALUES(?)"); + for (long i = 0; i < 10; i++) { + ps.setLong(1, i); + ps.addBatch(); + } + ps.executeBatch(Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertEquals(ps.getResponseSummary().getWrittenRows(), 10); + assertTrue(ps.getResponseSummary().getWrittenBytes() > 0); + } + + @Test + public void insertSelect() throws Exception { + createInsertTestTable(); + + ClickHousePreparedStatement ps = (ClickHousePreparedStatement) connection.prepareStatement("INSERT INTO test.insert_test SELECT number FROM numbers(10)"); + ps.executeQuery(Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertEquals(ps.getResponseSummary().getWrittenRows(), 10); + assertTrue(ps.getResponseSummary().getWrittenBytes() > 0); + } + + @Test + public void insertLargeSelect() throws Exception { + createInsertTestTable(); + + ClickHousePreparedStatement ps = (ClickHousePreparedStatement) connection.prepareStatement("INSERT INTO test.insert_test SELECT number FROM numbers(10000000)"); + ps.executeQuery(Collections.singletonMap(ClickHouseQueryParam.SEND_PROGRESS_IN_HTTP_HEADERS, "true")); + + assertEquals(ps.getResponseSummary().getWrittenRows(), 10000000); + assertTrue(ps.getResponseSummary().getWrittenBytes() > 0); + } + + @Test + public void noSummary() throws Exception { + ClickHouseStatement st = connection.createStatement(); + st.executeQuery("SELECT * FROM numbers(10)"); + + assertNull(st.getResponseSummary()); + } + + private void createInsertTestTable() throws SQLException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.insert_test"); + connection.createStatement().execute("CREATE TABLE IF NOT EXISTS test.insert_test (value UInt32) ENGINE = TinyLog"); + } +} diff --git a/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java b/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java index 6975c9e12..210e75f4e 100644 --- a/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java +++ b/src/test/java/ru/yandex/clickhouse/integration/StreamSQLTest.java @@ -6,11 +6,14 @@ import ru.yandex.clickhouse.ClickHouseConnection; import ru.yandex.clickhouse.ClickHouseContainerForTest; import ru.yandex.clickhouse.ClickHouseDataSource; - +import ru.yandex.clickhouse.domain.ClickHouseCompression; +import ru.yandex.clickhouse.domain.ClickHouseFormat; +import ru.yandex.clickhouse.settings.ClickHouseProperties; import java.io.*; import java.nio.charset.Charset; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.zip.GZIPOutputStream; public class StreamSQLTest { private ClickHouseDataSource dataSource; @@ -33,7 +36,11 @@ public void simpleCSVInsert() throws SQLException { String string = "5,6\n1,6"; InputStream inputStream = new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); - connection.createStatement().sendStreamSQL(inputStream, "insert into test.csv_stream_sql format CSV"); + connection.createStatement(). + write() + .sql("insert into test.csv_stream_sql format CSV") + .data(inputStream) + .send(); ResultSet rs = connection.createStatement().executeQuery( "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.csv_stream_sql"); @@ -43,31 +50,21 @@ public void simpleCSVInsert() throws SQLException { Assert.assertEquals(rs.getLong("uniq"), 1); } - @Test - public void multiRowTSVInsert() throws SQLException { - connection.createStatement().execute("DROP TABLE IF EXISTS test.tsv_stream_sql"); - connection.createStatement().execute( - "CREATE TABLE test.tsv_stream_sql (value Int32, string_value String) ENGINE = Log()" - ); - - - final int rowsCount = 100000; - - InputStream in = new InputStream() { + private InputStream getTSVStream(final int rowsCount) { + return new InputStream() { private int si = 0; private String s = ""; private int i = 0; - private final int count = rowsCount; private boolean genNextString() { - if (i >= count) return false; + if (i >= rowsCount) return false; si = 0; s = String.format("%d\txxxx%d\n", 1, i); i++; return true; } - public int read() throws IOException { + public int read() { if (si >= s.length()) { if ( ! genNextString() ) { return -1; @@ -76,8 +73,22 @@ public int read() throws IOException { return s.charAt( si++ ); } }; + } + + @Test + public void multiRowTSVInsert() throws SQLException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.tsv_stream_sql"); + connection.createStatement().execute( + "CREATE TABLE test.tsv_stream_sql (value Int32, string_value String) ENGINE = Log()" + ); + + final int rowsCount = 100000; - connection.createStatement().sendStreamSQL(in, "insert into test.tsv_stream_sql format TSV"); + connection.createStatement(). + write() + .sql("insert into test.tsv_stream_sql format TSV") + .data(getTSVStream(rowsCount), ClickHouseFormat.TSV) + .send(); ResultSet rs = connection.createStatement().executeQuery( "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.tsv_stream_sql"); @@ -87,4 +98,122 @@ public int read() throws IOException { Assert.assertEquals(rs.getInt("uniq"), rowsCount); } + private InputStream gzStream( InputStream is ) throws IOException + { + final int bufferSize = 16384; + byte data[] = new byte[bufferSize]; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(os); + BufferedInputStream es = new BufferedInputStream(is, bufferSize); + int count; + while ( ( count = es.read( data, 0, bufferSize) ) != -1 ) + gzipOutputStream.write( data, 0, count ); + es.close(); + gzipOutputStream.close(); + + return new ByteArrayInputStream( os.toByteArray() ); + } + + @Test + public void multiRowTSVInsertCompressed() throws SQLException, IOException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.tsv_compressed_stream_sql"); + connection.createStatement().execute( + "CREATE TABLE test.tsv_compressed_stream_sql (value Int32, string_value String) ENGINE = Log()" + ); + + final int rowsCount = 100000; + + InputStream gz = gzStream(getTSVStream(rowsCount)); + connection.createStatement(). + write() + .sql("insert into test.tsv_compressed_stream_sql format TSV") + .data(gz, ClickHouseFormat.TSV, ClickHouseCompression.gzip) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.tsv_compressed_stream_sql"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), rowsCount); + Assert.assertEquals(rs.getInt("sum"), rowsCount); + Assert.assertEquals(rs.getInt("uniq"), rowsCount); + } + + + @Test + public void JSONEachRowInsert() throws SQLException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.json_stream_sql"); + connection.createStatement().execute( + "CREATE TABLE test.json_stream_sql (value Int32, string_value String) ENGINE = Log()" + ); + + String string = "{\"value\":5,\"string_value\":\"6\"}\n{\"value\":1,\"string_value\":\"6\"}"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); + + connection.createStatement(). + write() + .sql("insert into test.json_stream_sql") + .data(inputStream, ClickHouseFormat.JSONEachRow) + .data(inputStream) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.json_stream_sql"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), 2); + Assert.assertEquals(rs.getLong("sum"), 6); + Assert.assertEquals(rs.getLong("uniq"), 1); + } + + @Test + public void JSONEachRowCompressedInsert() throws SQLException, IOException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.json_comressed_stream_sql"); + connection.createStatement().execute( + "CREATE TABLE test.json_comressed_stream_sql (value Int32, string_value String) ENGINE = Log()" + ); + + String string = "{\"value\":5,\"string_value\":\"6\"}\n{\"value\":1,\"string_value\":\"6\"}"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); + + connection.createStatement(). + write() + .sql("insert into test.json_comressed_stream_sql") + .data(inputStream, ClickHouseFormat.JSONEachRow) + .data(gzStream(inputStream)) + .dataCompression(ClickHouseCompression.gzip) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.json_comressed_stream_sql"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), 2); + Assert.assertEquals(rs.getLong("sum"), 6); + Assert.assertEquals(rs.getLong("uniq"), 1); + } + + @Test + public void CSVInsertCompressedIntoTable() throws SQLException, IOException { + connection.createStatement().execute("DROP TABLE IF EXISTS test.csv_stream_compressed"); + connection.createStatement().execute( + "CREATE TABLE test.csv_stream_compressed (value Int32, string_value String) ENGINE = Log()" + ); + + String string = "5,6\n1,6"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes(Charset.forName("UTF-8"))); + + connection.createStatement(). + write() + .table("test.csv_stream_compressed") + .format(ClickHouseFormat.CSV) + .dataCompression(ClickHouseCompression.gzip) + .data(gzStream(inputStream)) + .send(); + + ResultSet rs = connection.createStatement().executeQuery( + "SELECT count() AS cnt, sum(value) AS sum, uniqExact(string_value) uniq FROM test.csv_stream_compressed"); + Assert.assertTrue(rs.next()); + Assert.assertEquals(rs.getInt("cnt"), 2); + Assert.assertEquals(rs.getLong("sum"), 6); + Assert.assertEquals(rs.getLong("uniq"), 1); + } + } 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..9e2768413 --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/jdbc/parser/ClickHouseSqlParserTest.java @@ -0,0 +1,624 @@ +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.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; +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); + + 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(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); + checkSingleStatement(parse(sql = "/*********\r\n\r\t some ***** comments*/"), 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); + } + + @Test + 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"); + checkSingleStatement(parse(sql = "ALTER TABLE tTt on cluster 'cc' delete WHERE d=123 and e=456"), sql, + StatementType.ALTER_DELETE, "system", "tTt"); + checkSingleStatement(parse(sql = "ALTER USER user DEFAULT ROLE role1, role2"), sql, StatementType.ALTER); + } + + @Test + public void testAttachStatement() { + String sql; + + checkSingleStatement(parse(sql = "ATTACH TABLE IF NOT EXISTS t.t ON CLUSTER cluster"), sql, + StatementType.ATTACH); + } + + @Test + public void testCheckStatement() { + String sql; + + 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(parse(sql = "create table a(a String) engine=Memory"), sql, StatementType.CREATE); + } + + @Test + public void testDeleteStatement() { + String sql; + + 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(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(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(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(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(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(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); + } + + @Test + public void testGrantStatement() { + String 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 = 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(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(parse(sql = "insert into function null('a UInt8') select * from number(10)"), sql, + StatementType.INSERT); + 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(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"); + + } + + @Test + public void testKillStatement() { + String sql; + + checkSingleStatement(parse(sql = "KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'"), sql, + StatementType.KILL); + checkSingleStatement(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(parse(sql = "OPTIMIZE TABLE a ON CLUSTER cluster PARTITION ID 'partition_id' FINAL"), sql, + StatementType.OPTIMIZE); + } + + @Test + public void testRenameStatement() { + String sql; + + 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); + } + + @Test + public void testRevokeStatement() { + String sql; + + 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(parse(sql = "select\n1"), new ClickHouseSqlStatement[] { + 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, 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, null) }); + + 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(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( + 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(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(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(parse(sql = "SELECT SUM(x) FROM t WHERE y = ? GROUP BY ?"), sql, StatementType.SELECT, + "system", "t"); + + assertEquals(parse(sql = loadSql("issue-441_with-totals.sql")), + new ClickHouseSqlStatement[] { new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, + "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, null) }); + assertEquals(parse(sql = loadSql("with-clause.sql")), new ClickHouseSqlStatement[] { + new ClickHouseSqlStatement(sql, StatementType.SELECT, null, null, "unknown", null, null, null, null) }); + } + + @Test + public void testSetStatement() { + String sql; + + 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(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(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(parse(sql = "truncate table a.b"), sql, StatementType.TRUNCATE, "a", "b"); + } + + @Test + public void testUpdateStatement() { + String sql; + + 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(parse(sql = "use system"), sql, StatementType.USE); + } + + @Test + public void testWatchStatement() throws ParseException { + String sql; + 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"), 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, null), + new ClickHouseSqlStatement("select 1 as `a ; a`", StatementType.SELECT) }); + } + + @Test + public void testAlias() throws ParseException { + String sql; + 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(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, + StatementType.SELECT, "a", "b"); + checkSingleStatement( + 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(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(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(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 = 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 = 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 = 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 = 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 = 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 = parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + assertEquals(stmts[0].hasWithTotals(), false); + + sql = "select 1 with totals"; + stmts = 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 = parse(sql); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), sql); + + stmts = ClickHouseSqlParser.parse(sql, new ClickHouseProperties(), new ParseHandler() { + @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 = 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; + } + } + }); + assertEquals(stmts.length, 1); + assertEquals(stmts[0].getSQL(), "select a, b from (select 1+2+3)"); + } + + @Test + public void testExtractDBAndTableName() { + String sql; + + 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(parse(sql = "/*qq*/ SELECT fromUnixTimestamp64Milli(time)from table"), sql, + StatementType.SELECT, "system", "table"); + 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"); + } + + 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/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/java/ru/yandex/clickhouse/response/ClickHouseResultSetTest.java b/src/test/java/ru/yandex/clickhouse/response/ClickHouseResultSetTest.java index 9bf131487..394acddbc 100644 --- a/src/test/java/ru/yandex/clickhouse/response/ClickHouseResultSetTest.java +++ b/src/test/java/ru/yandex/clickhouse/response/ClickHouseResultSetTest.java @@ -448,6 +448,25 @@ public void testClassNamesObjects() throws Exception { } } + @Test + public void testGetColumnNames() throws Exception { + String response = "SiteName\tCountry\n" + + "String\tString\n" + + "hello.com\tPoland\n" + + "there.com\tUSA\n" + + "\t\n" + + "other.com\t\n" + + "\n" + + "\t\n"; + + ByteArrayInputStream is = new ByteArrayInputStream(response.getBytes("UTF-8")); + + ClickHouseResultSet rs = buildResultSet(is, 1024, "db", "table", false, null, null, props); + String[] columnNames = rs.getColumnNames(); + assertEquals(2, columnNames.length); + assertEquals("SiteName", columnNames[0]); + assertEquals("Country", columnNames[1]); + } /** * By jdbc specification diff --git a/src/test/java/ru/yandex/clickhouse/settings/ClickHousePropertiesTest.java b/src/test/java/ru/yandex/clickhouse/settings/ClickHousePropertiesTest.java index 094aa5dfe..7818fa969 100644 --- a/src/test/java/ru/yandex/clickhouse/settings/ClickHousePropertiesTest.java +++ b/src/test/java/ru/yandex/clickhouse/settings/ClickHousePropertiesTest.java @@ -1,14 +1,16 @@ package ru.yandex.clickhouse.settings; +import java.net.URI; +import java.util.Map; +import java.util.Properties; + import org.testng.Assert; import org.testng.annotations.Test; + import ru.yandex.clickhouse.BalancedClickhouseDataSource; import ru.yandex.clickhouse.ClickHouseDataSource; -import java.net.URI; -import java.util.Map; -import java.util.Properties; - +import static org.testng.Assert.assertFalse; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; @@ -143,6 +145,8 @@ public void buildQueryParamsTest() { clickHouseProperties.setMaxInsertBlockSize(42L); clickHouseProperties.setInsertDeduplicate(true); clickHouseProperties.setInsertDistributedSync(true); + clickHouseProperties.setUser("myUser"); + clickHouseProperties.setPassword("myPassword"); Map clickHouseQueryParams = clickHouseProperties.buildQueryParams(true); Assert.assertEquals(clickHouseQueryParams.get(ClickHouseQueryParam.INSERT_QUORUM), "3"); @@ -151,6 +155,8 @@ public void buildQueryParamsTest() { Assert.assertEquals(clickHouseQueryParams.get(ClickHouseQueryParam.MAX_INSERT_BLOCK_SIZE), "42"); Assert.assertEquals(clickHouseQueryParams.get(ClickHouseQueryParam.INSERT_DEDUPLICATE), "1"); Assert.assertEquals(clickHouseQueryParams.get(ClickHouseQueryParam.INSERT_DISTRIBUTED_SYNC), "1"); + assertFalse(clickHouseQueryParams.containsKey(ClickHouseQueryParam.USER)); + assertFalse(clickHouseQueryParams.containsKey(ClickHouseQueryParam.PASSWORD)); } @Test diff --git a/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java b/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java new file mode 100644 index 000000000..41502c0ee --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java @@ -0,0 +1,216 @@ +package ru.yandex.clickhouse.util; + +import org.apache.http.HttpHost; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.HttpHostConnectException; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +import ru.yandex.clickhouse.settings.ClickHouseProperties; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; + + +public class ClickHouseHttpClientBuilderTest { + + private static WireMockServer mockServer; + + @BeforeClass + public static void beforeAll() { + mockServer = new WireMockServer( + WireMockConfiguration.wireMockConfig().dynamicPort()); + mockServer.start(); + } + + @AfterMethod + public void afterTest() { + mockServer.resetAll(); + } + + @AfterClass + public static void afterAll() { + mockServer.stop(); + mockServer = null; + } + + @Test + public void testCreateClientContextNull() { + assertNull(ClickHouseHttpClientBuilder.createClientContext(null).getAuthCache()); + } + + @Test + public void testCreateClientContextNoUserNoPass() { + assertNull(ClickHouseHttpClientBuilder.createClientContext(new ClickHouseProperties()) + .getAuthCache()); + } + + @Test + public void testCreateClientContextNoHost() { + ClickHouseProperties props = new ClickHouseProperties(); + props.setUser("myUser"); + props.setPassword("mySecret"); + assertNull(ClickHouseHttpClientBuilder.createClientContext(props).getAuthCache()); + } + + @Test + public void testCreateClientContextUserPass() { + ClickHouseProperties props = new ClickHouseProperties(); + props.setUser("myUser"); + props.setPassword("mySecret"); + props.setHost("127.0.0.1"); + assertEquals( + ClickHouseHttpClientBuilder.createClientContext(props).getAuthCache() + .get(HttpHost.create("http://127.0.0.1:80")).getSchemeName(), + "basic"); + } + + @Test + public void testCreateClientContextOnlyUser() { + ClickHouseProperties props = new ClickHouseProperties(); + props.setUser("myUser"); + props.setHost("127.0.0.1"); + assertEquals( + ClickHouseHttpClientBuilder.createClientContext(props).getAuthCache() + .get(HttpHost.create("http://127.0.0.1:80")).getSchemeName(), + "basic"); + } + + @Test + public void testCreateClientContextOnlyPass() { + ClickHouseProperties props = new ClickHouseProperties(); + props.setPassword("myPass"); + props.setHost("127.0.0.1"); + assertEquals( + ClickHouseHttpClientBuilder.createClientContext(props).getAuthCache() + .get(HttpHost.create("http://127.0.0.1:80")).getSchemeName(), + "basic"); + } + + + @Test(dataProvider = "authUserPassword") + public void testHttpAuthParametersCombination(String authorization, String user, + String password, String expectedAuthHeader) throws Exception + { + ClickHouseProperties props = new ClickHouseProperties(); + props.setHost("localhost"); + props.setPort(mockServer.port()); + props.setUser(user); + props.setPassword(password); + props.setHttpAuthorization(authorization); + CloseableHttpClient client = new ClickHouseHttpClientBuilder(props).buildClient(); + HttpPost post = new HttpPost(mockServer.baseUrl()); + client.execute(post, ClickHouseHttpClientBuilder.createClientContext(props)); + mockServer.verify( + postRequestedFor(WireMock.anyUrl()) + .withHeader("Authorization", equalTo(expectedAuthHeader))); + } + + @DataProvider(name = "authUserPassword") + private static Object[][] provideAuthUserPasswordTestData() { + return new Object[][] { + { + "Digest username=\"foo\"", null, null, "Digest username=\"foo\"" + }, + { + "Digest username=\"foo\"", "bar", null, "Digest username=\"foo\"" + }, + { + "Digest username=\"foo\"", null, "baz", "Digest username=\"foo\"" + }, + { + "Digest username=\"foo\"", "bar", "baz", "Digest username=\"foo\"" + }, + { + null, "bar", "baz", "Basic YmFyOmJheg==" // bar:baz + }, + { + null, "bar", null, "Basic YmFyOg==" // bar: + }, + { + null, null, "baz", "Basic ZGVmYXVsdDpiYXo=" // default:baz + }, + }; + } + + private static WireMockServer newServer() { + WireMockServer server = new WireMockServer( + WireMockConfiguration.wireMockConfig().dynamicPort()); + server.start(); + server.stubFor(WireMock.post(WireMock.urlPathMatching("/*")) + .willReturn(WireMock.aResponse().withStatus(200).withHeader("Connection", "Keep-Alive") + .withHeader("Content-Type", "text/plain; charset=UTF-8") + .withHeader("Transfer-Encoding", "chunked").withHeader("Keep-Alive", "timeout=3") + .withBody("OK.........................").withFixedDelay(2))); + return server; + } + + private static void shutDownServerWithDelay(final WireMockServer server, final long delayMs) { + new Thread() { + public void run() { + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + server.shutdownServer(); + server.stop(); + } + }.start(); + } + + // @Test(dependsOnMethods = { "testWithRetry" }, expectedExceptions = { NoHttpResponseException.class }) + public void testWithoutRetry() throws Exception { + final WireMockServer server = newServer(); + + ClickHouseProperties props = new ClickHouseProperties(); + props.setMaxRetries(0); + ClickHouseHttpClientBuilder builder = new ClickHouseHttpClientBuilder(props); + CloseableHttpClient client = builder.buildClient(); + HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%201"); + + shutDownServerWithDelay(server, 500); + + try { + client.execute(post); + } finally { + client.close(); + } + } + + // @Test(expectedExceptions = { HttpHostConnectException.class }) + public void testWithRetry() throws Exception { + final WireMockServer server = newServer(); + + ClickHouseProperties props = new ClickHouseProperties(); + // props.setMaxRetries(3); + ClickHouseHttpClientBuilder builder = new ClickHouseHttpClientBuilder(props); + CloseableHttpClient client = builder.buildClient(); + HttpContext context = new BasicHttpContext(); + context.setAttribute("is_idempotent", Boolean.TRUE); + HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%202"); + + shutDownServerWithDelay(server, 500); + + try { + client.execute(post, context); + } finally { + client.close(); + } + } +} diff --git a/src/test/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStreamTest.java b/src/test/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStreamTest.java new file mode 100644 index 000000000..8746941da --- /dev/null +++ b/src/test/java/ru/yandex/clickhouse/util/ClickHouseLZ4OutputStreamTest.java @@ -0,0 +1,209 @@ +package ru.yandex.clickhouse.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ClickHouseLZ4OutputStreamTest { + private byte[] genCompressedByts(int b, int length, int blockSize) throws IOException { + ByteArrayOutputStream bas = new ByteArrayOutputStream(blockSize * 512); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, blockSize)) { + for (int i = 0; i < length; i++) { + out.write(b); + } + out.flush(); + } + + byte[] bytes = bas.toByteArray(); + bas.close(); + return bytes; + } + + @Test + public void testWrite() throws IOException { + ByteArrayOutputStream bas = new ByteArrayOutputStream(64); + + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 2)) { + byte[] bytes = new byte[] { (byte) -36, (byte) -86, (byte) 31, (byte) 113, (byte) -106, (byte) 44, + (byte) 99, (byte) 96, (byte) 112, (byte) -7, (byte) 47, (byte) 15, (byte) -63, (byte) 39, + (byte) -73, (byte) -104, (byte) -126, (byte) 12, (byte) 0, (byte) 0, (byte) 0, (byte) 2, (byte) 0, + (byte) 0, (byte) 0, (byte) 32, (byte) 1, (byte) 2 }; + out.write(1); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.write(2); + Assert.assertEquals(bas.toByteArray(), bytes); + out.write(3); + Assert.assertEquals(bas.toByteArray(), bytes); + out.flush(); + Assert.assertEquals(bas.toByteArray(), + new byte[] { (byte) -36, (byte) -86, (byte) 31, (byte) 113, (byte) -106, (byte) 44, (byte) 99, + (byte) 96, (byte) 112, (byte) -7, (byte) 47, (byte) 15, (byte) -63, (byte) 39, (byte) -73, + (byte) -104, (byte) -126, (byte) 12, (byte) 0, (byte) 0, (byte) 0, (byte) 2, (byte) 0, + (byte) 0, (byte) 0, (byte) 32, (byte) 1, (byte) 2, (byte) 64, (byte) -39, (byte) 21, + (byte) 50, (byte) -77, (byte) -124, (byte) 25, (byte) 73, (byte) -59, (byte) 9, (byte) 112, + (byte) -38, (byte) 12, (byte) 99, (byte) 71, (byte) 74, (byte) -126, (byte) 11, (byte) 0, + (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 16, (byte) 3 }); + bas.close(); + } + } + + @Test + public void testWriteBytes() throws IOException { + Assert.assertThrows(NullPointerException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(new ByteArrayOutputStream(), 3)) { + out.write(null); + } + } + }); + + ByteArrayOutputStream bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(new byte[0]); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.flush(); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + + byte[] bytes = new byte[] { (byte) 13, (byte) 13 }; + out.write(bytes); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 2, 3)); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + byte[] bytes = new byte[] { (byte) 13, (byte) 13, (byte) 13 }; + out.write(bytes); + byte[] expected = genCompressedByts(13, 3, 3); + Assert.assertEquals(bas.toByteArray(), expected); + out.flush(); + Assert.assertEquals(bas.toByteArray(), expected); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + byte[] bytes = new byte[] { (byte) 13, (byte) 13, (byte) 13, (byte) 13 }; + out.write(bytes); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 3, 3)); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 4, 3)); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + byte[] bytes = new byte[] { (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13 }; + out.write(bytes); + byte[] expected = genCompressedByts(13, 6, 3); + Assert.assertEquals(bas.toByteArray(), expected); + out.flush(); + Assert.assertEquals(bas.toByteArray(), expected); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + byte[] bytes = new byte[] { (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13 }; + out.write(bytes); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 6, 3)); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 7, 3)); + bas.close(); + } + } + + @Test + public void testWriteBytesWithOffset() throws IOException { + Assert.assertThrows(NullPointerException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(new ByteArrayOutputStream(), 3)) { + out.write(null, 0, 1); + } + } + }); + Assert.assertThrows(IndexOutOfBoundsException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(new ByteArrayOutputStream(), 3)) { + out.write(new byte[0], 0, 1); + } + } + }); + Assert.assertThrows(IndexOutOfBoundsException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(new ByteArrayOutputStream(), 3)) { + out.write(new byte[0], -1, 0); + } + } + }); + Assert.assertThrows(IndexOutOfBoundsException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(new ByteArrayOutputStream(), 3)) { + out.write(new byte[1], 1, 1); + } + } + }); + + final byte[] bytes = new byte[] { (byte) 0, (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13, (byte) 13, + (byte) 13, (byte) 0 }; + ByteArrayOutputStream bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(bytes, 1, 0); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.flush(); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.write(bytes, 1, 2); + Assert.assertEquals(bas.toByteArray(), new byte[0]); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 2, 3)); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(bytes, 1, 3); + byte[] expected = genCompressedByts(13, 3, 3); + Assert.assertEquals(bas.toByteArray(), expected); + out.flush(); + Assert.assertEquals(bas.toByteArray(), expected); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(bytes, 1, 4); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 3, 3)); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 4, 3)); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(bytes, 1, 6); + byte[] expected = genCompressedByts(13, 6, 3); + Assert.assertEquals(bas.toByteArray(), expected); + out.flush(); + Assert.assertEquals(bas.toByteArray(), expected); + bas.close(); + } + + bas = new ByteArrayOutputStream(64); + try (ClickHouseLZ4OutputStream out = new ClickHouseLZ4OutputStream(bas, 3)) { + out.write(bytes, 1, 7); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 6, 3)); + out.flush(); + Assert.assertEquals(bas.toByteArray(), genCompressedByts(13, 7, 3)); + bas.close(); + } + } +} diff --git a/src/test/resources/ru/yandex/clickhouse/users.d/foo.xml b/src/test/resources/ru/yandex/clickhouse/users.d/foo.xml new file mode 100644 index 000000000..5083e230b --- /dev/null +++ b/src/test/resources/ru/yandex/clickhouse/users.d/foo.xml @@ -0,0 +1,12 @@ + + + + default + + ::/0 + + bar + default + + + diff --git a/src/test/resources/ru/yandex/clickhouse/users.d/oof.xml b/src/test/resources/ru/yandex/clickhouse/users.d/oof.xml new file mode 100644 index 000000000..24c239f08 --- /dev/null +++ b/src/test/resources/ru/yandex/clickhouse/users.d/oof.xml @@ -0,0 +1,12 @@ + + + + default + + ::/0 + + default + + + + 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/issue-555_custom-format.sql b/src/test/resources/sqls/issue-555_custom-format.sql new file mode 100644 index 000000000..79b7097db --- /dev/null +++ b/src/test/resources/sqls/issue-555_custom-format.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 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