diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/ServerSettings.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/ServerSettings.java index 83b558c35..c3164484e 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/ServerSettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/ServerSettings.java @@ -33,6 +33,10 @@ public final class ServerSettings { */ public static final String RESULT_OVERFLOW_MODE = "result_overflow_mode"; + public static final String RESULT_OVERFLOW_MODE_THROW = "throw"; + + public static final String RESULT_OVERFLOW_MODE_BREAK = "break"; + public static final String ASYNC_INSERT = "async_insert"; public static final String WAIT_ASYNC_INSERT = "wait_for_async_insert"; diff --git a/client-v2/src/main/java/com/clickhouse/client/api/sql/SQLUtils.java b/client-v2/src/main/java/com/clickhouse/client/api/sql/SQLUtils.java new file mode 100644 index 000000000..03d3715c4 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/sql/SQLUtils.java @@ -0,0 +1,135 @@ +package com.clickhouse.client.api.sql; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SQLUtils { + /** + * Escapes and quotes a string literal for use in SQL queries. + * + * @param str the string to be quoted, cannot be null + * @return the quoted and escaped string + * @throws IllegalArgumentException if the input string is null + */ + public static String enquoteLiteral(String str) { + if (str == null) { + throw new IllegalArgumentException("Input string cannot be null"); + } + return "'" + str.replace("'", "''") + "'"; + } + + /** + * Escapes and quotes an SQL identifier (e.g., table or column name) by enclosing it in double quotes. + * Any existing double quotes in the identifier are escaped by doubling them. + * + * @param identifier the identifier to be quoted, cannot be null + * @param quotesRequired if false, the identifier will only be quoted if it contains special characters + * @return the quoted and escaped identifier, or the original identifier if quoting is not required + * @throws IllegalArgumentException if the input identifier is null + */ + public static String enquoteIdentifier(String identifier, boolean quotesRequired) { + if (identifier == null) { + throw new IllegalArgumentException("Identifier cannot be null"); + } + + if (!quotesRequired && !needsQuoting(identifier)) { + return identifier; + } + return "\"" + identifier.replace("\"", "\"\"") + "\""; + } + + /** + * Escapes and quotes an SQL identifier, always adding quotes. + * + * @param identifier the identifier to be quoted, cannot be null + * @return the quoted and escaped identifier + * @throws IllegalArgumentException if the input identifier is null + * @see #enquoteIdentifier(String, boolean) + */ + public static String enquoteIdentifier(String identifier) { + return enquoteIdentifier(identifier, true); + } + + /** + * Checks if an identifier needs to be quoted. + * An identifier needs quoting if it: + * - Is empty + * - Contains any non-alphanumeric characters except underscore + * - Starts with a digit + * - Is a reserved keyword (not implemented in this basic version) + * + * @param identifier the identifier to check + * @return true if the identifier needs to be quoted, false otherwise + */ + private static boolean needsQuoting(String identifier) { + if (identifier == null) { + throw new IllegalArgumentException("identifier cannot be null"); + } + + if (identifier.isEmpty()) { + return true; + } + + // Check if first character is a digit + if (Character.isDigit(identifier.charAt(0))) { + return true; + } + + // Check all characters are alphanumeric or underscore + for (int i = 0; i < identifier.length(); i++) { + char c = identifier.charAt(i); + if (!(Character.isLetterOrDigit(c) || c == '_')) { + return true; + } + } + + return false; + } + + /** + * Checks if the given string is a valid simple SQL identifier that doesn't require quoting. + * A simple identifier must: + * + * + * @param identifier the identifier to check + * @return true if the identifier is a valid simple SQL identifier, false otherwise + * @throws IllegalArgumentException if the input identifier is null + */ + // Compiled pattern for simple SQL identifiers + private static final java.util.regex.Pattern SIMPLE_IDENTIFIER_PATTERN = + java.util.regex.Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{0,127}$"); + + /** + * Checks if the given string is a valid simple SQL identifier using a compiled regex pattern. + * A simple identifier must match the pattern: ^[a-zA-Z][a-zA-Z0-9_]{0,127}$ + * + * @param identifier the identifier to check + * @return true if the identifier is a valid simple SQL identifier, false otherwise + * @throws IllegalArgumentException if the input identifier is null + */ + public static boolean isSimpleIdentifier(String identifier) { + if (identifier == null) { + throw new IllegalArgumentException("Identifier cannot be null"); + } + return SIMPLE_IDENTIFIER_PATTERN.matcher(identifier).matches(); + } + + private final static Pattern UNQUOTE_INDENTIFIER = Pattern.compile( + "^[\\\"`]?(.+?)[\\\"`]?$" + ); + + public static String unquoteIdentifier(String str) { + Matcher matcher = UNQUOTE_INDENTIFIER.matcher(str.trim()); + if (matcher.find()) { + return matcher.group(1); + } else { + return str; + } + } +} diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTests.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTests.java deleted file mode 100644 index 578b5f172..000000000 --- a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTests.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.clickhouse.client.api.data_formats.internal; - -public class SerializerUtilsTests { - -} diff --git a/client-v2/src/test/java/com/clickhouse/client/api/sql/SQLUtilsTest.java b/client-v2/src/test/java/com/clickhouse/client/api/sql/SQLUtilsTest.java new file mode 100644 index 000000000..5b0a51e4f --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/sql/SQLUtilsTest.java @@ -0,0 +1,143 @@ +package com.clickhouse.client.api.sql; + +import org.apache.commons.lang3.StringUtils; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +@Test(groups = {"unit"}) +public class SQLUtilsTest { + // Test data for enquoteLiteral + @DataProvider(name = "enquoteLiteralTestData") + public Object[][] enquoteLiteralTestData() { + return new Object[][] { + // input, expected output + {"test 123", "'test 123'"}, + {"こんにちは世界", "'こんにちは世界'"}, + {"O'Reilly", "'O''Reilly'"}, + {"😊👍", "'😊👍'"}, + {"", "''"}, + {"single'quote'double''quote\"", "'single''quote''double''''quote\"'"} + }; + } + + // Test data for enquoteIdentifier + @DataProvider(name = "enquoteIdentifierTestData") + public Object[][] enquoteIdentifierTestData() { + return new Object[][] { + // input, expected output + {"column1", "\"column1\""}, + {"table.name", "\"table.name\""}, + {"column with spaces", "\"column with spaces\""}, + {"column\"with\"quotes", "\"column\"\"with\"\"quotes\""}, + {"UPPERCASE", "\"UPPERCASE\""}, + {"1column", "\"1column\""}, + {"column-with-hyphen", "\"column-with-hyphen\""}, + {"😊👍", "\"😊👍\""}, + {"", "\"\""} + }; + } + + @Test(dataProvider = "enquoteLiteralTestData") + public void testEnquoteLiteral(String input, String expected) { + assertEquals(SQLUtils.enquoteLiteral(input), expected); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEnquoteLiteral_NullInput() { + SQLUtils.enquoteLiteral(null); + } + + @Test(dataProvider = "enquoteIdentifierTestData") + public void testEnquoteIdentifier(String input, String expected) { + // Test with quotesRequired = true (always quote) + assertEquals(SQLUtils.enquoteIdentifier(input), expected); + assertEquals(SQLUtils.enquoteIdentifier(input, true), expected); + + // Test with quotesRequired = false (quote only if needed) + boolean needsQuoting = !input.matches("[a-zA-Z_][a-zA-Z0-9_]*"); + String expectedUnquoted = needsQuoting ? expected : input; + assertEquals(SQLUtils.enquoteIdentifier(input, false), expectedUnquoted); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEnquoteIdentifier_NullInput() { + SQLUtils.enquoteIdentifier(null); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEnquoteIdentifier_NullInput_WithQuotesRequired() { + SQLUtils.enquoteIdentifier(null, true); + } + + @Test + public void testEnquoteIdentifier_NoQuotesWhenNotNeeded() { + // These identifiers don't need quoting + String[] simpleIdentifiers = { + "column1", "table_name", "_id", "a1b2c3", "ColumnName" + }; + + for (String id : simpleIdentifiers) { + // With quotesRequired=false, should return as-is + assertEquals(SQLUtils.enquoteIdentifier(id, false), id); + // With quotesRequired=true, should be quoted + assertEquals(SQLUtils.enquoteIdentifier(id, true), "\"" + id + "\""); + } + } + + @DataProvider(name = "simpleIdentifierTestData") + public Object[][] simpleIdentifierTestData() { + return new Object[][] { + // identifier, expected result + {"Hello", true}, + {"hello_world", true}, + {"Hello123", true}, + {"H", true}, // minimum length + {StringUtils.repeat("a", 128), true}, // maximum length + + // Test cases from requirements + {"G'Day", false}, + {"\"\"Bruce Wayne\"\"", false}, + {"GoodDay$", false}, + {"Hello\"\"World", false}, + {"\"\"Hello\"\"World\"\"", false}, + + // Additional test cases + {"", false}, // empty string + {"123test", false}, // starts with number + {"_test", false}, // starts with underscore + {"test-name", false}, // contains hyphen + {"test name", false}, // contains space + {"test\"name", false}, // contains quote + {"test.name", false}, // contains dot + {StringUtils.repeat("a", 129), false}, // exceeds max length + {"testName", true}, + {"TEST_NAME", true}, + {"test123", true}, + {"t123", true}, + {"t", true} + }; + } + + @Test(dataProvider = "simpleIdentifierTestData") + public void testIsSimpleIdentifier(String identifier, boolean expected) { + assertEquals(SQLUtils.isSimpleIdentifier(identifier), expected, + String.format("Failed for identifier: %s", identifier)); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testIsSimpleIdentifier_NullInput() { + SQLUtils.isSimpleIdentifier(null); + } + + @Test + public void testUnquoteIdentifier() { + String[] names = new String[]{"test", "`test name1`", "\"test name 2\""}; + String[] expected = new String[]{"test", "test name1", "test name 2"}; + + for (int i = 0; i < names.length; i++) { + assertEquals(SQLUtils.unquoteIdentifier(names[i]), expected[i]); + } + } +} \ No newline at end of file diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index dc7007b3d..84f408253 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -127,79 +127,79 @@ private String buildSQL() { @Override public ResultSet executeQuery() throws SQLException { - checkClosed(); + ensureOpen(); return super.executeQueryImpl(buildSQL(), localSettings); } @Override public int executeUpdate() throws SQLException { - checkClosed(); - return super.executeUpdateImpl(buildSQL(), localSettings); + ensureOpen(); + return (int) super.executeUpdateImpl(buildSQL(), localSettings); } @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { - checkClosed(); + ensureOpen(); setNull(parameterIndex, sqlType, null); } @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setLong(int parameterIndex, long x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setString(int parameterIndex, String x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBytes(int parameterIndex, byte[] x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @@ -220,25 +220,25 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { @Override public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void clearParameters() throws SQLException { - checkClosed(); + ensureOpen(); Arrays.fill(this.values, null); } @@ -248,31 +248,31 @@ int getParametersCount() { @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { - checkClosed(); + ensureOpen(); setObject(parameterIndex, x, targetSqlType, 0); } @Override public void setObject(int parameterIndex, Object x) throws SQLException { - checkClosed(); + ensureOpen(); setObject(parameterIndex, x, Types.OTHER); } @Override public boolean execute() throws SQLException { - checkClosed(); + ensureOpen(); if (parsedPreparedStatement.isHasResultSet()) { - super.executeQueryImpl(buildSQL(), localSettings); + currentResultSet = super.executeQueryImpl(buildSQL(), localSettings); return true; } else { - super.executeUpdateImpl(buildSQL(), localSettings); + currentUpdateCount = super.executeUpdateImpl(buildSQL(), localSettings); return false; } } @Override public void addBatch() throws SQLException { - checkClosed(); + ensureOpen(); if (insertStmtWithValues) { StringBuilder valuesClause = new StringBuilder(valueListTmpl); @@ -290,7 +290,7 @@ public void addBatch() throws SQLException { @Override public int[] executeBatch() throws SQLException { - checkClosed(); + ensureOpen(); if (insertStmtWithValues) { // run executeBatch @@ -298,7 +298,7 @@ public int[] executeBatch() throws SQLException { } else { List results = new ArrayList<>(); for (String sql : batch) { - results.add(executeUpdateImpl(sql, localSettings)); + results.add((int) executeUpdateImpl(sql, localSettings)); } return results.stream().mapToInt(Integer::intValue).toArray(); } @@ -306,14 +306,14 @@ public int[] executeBatch() throws SQLException { @Override public long[] executeLargeBatch() throws SQLException { - checkClosed(); + ensureOpen(); if (insertStmtWithValues) { return executeInsertBatch().stream().mapToLong(Integer::longValue).toArray(); } else { List results = new ArrayList<>(); for (String sql : batch) { - results.add(executeUpdateImpl(sql, localSettings)); + results.add((int) executeUpdateImpl(sql, localSettings)); } return results.stream().mapToLong(Integer::longValue).toArray(); } @@ -328,7 +328,7 @@ private List executeInsertBatch() throws SQLException { } insertSql.setLength(insertSql.length() - 1); - int updateCount = super.executeUpdateImpl(insertSql.toString(), localSettings); + int updateCount = (int) super.executeUpdateImpl(insertSql.toString(), localSettings); if (updateCount == batchValues.size()) { return Collections.nCopies(batchValues.size(), 1); } else { @@ -338,13 +338,13 @@ private List executeInsertBatch() throws SQLException { @Override public void setCharacterStream(int parameterIndex, Reader x, int length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setRef(int parameterIndex, Ref x) throws SQLException { - checkClosed(); + ensureOpen(); if (!connection.config.isIgnoreUnsupportedRequests()) { throw new SQLFeatureNotSupportedException("Ref is not supported.", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); } @@ -352,25 +352,25 @@ public void setRef(int parameterIndex, Ref x) throws SQLException { @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Clob x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setArray(int parameterIndex, Array x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public ResultSetMetaData getMetaData() throws SQLException { - checkClosed(); + ensureOpen(); if (resultSetMetaData == null && currentResultSet == null) { // before execution @@ -431,7 +431,7 @@ public static String replaceQuestionMarks(String sql, final String replacement) @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(sqlDateToInstant(x, cal)); } @@ -445,7 +445,7 @@ protected Instant sqlDateToInstant(Date x, Calendar cal) { @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(sqlTimeToInstant(x, cal)); } @@ -459,7 +459,7 @@ protected Instant sqlTimeToInstant(Time x, Calendar cal) { @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(sqlTimestampToZDT(x, cal)); } @@ -473,13 +473,13 @@ protected ZonedDateTime sqlTimestampToZDT(Timestamp x, Calendar cal) { @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(null); } @Override public void setURL(int parameterIndex, URL x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @@ -493,134 +493,134 @@ public void setURL(int parameterIndex, URL x) throws SQLException { */ @Override public ParameterMetaData getParameterMetaData() throws SQLException { - checkClosed(); + ensureOpen(); return parameterMetaData; } @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException("ROWID type is not supported by ClickHouse.", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); } @Override public void setNString(int parameterIndex, String x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setNCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, NClob x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBlob(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setSQLXML(int parameterIndex, SQLXML x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { - checkClosed(); + ensureOpen(); setObject(parameterIndex, x, JDBCType.valueOf(targetSqlType), scaleOrLength); } @Override public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setCharacterStream(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setNCharacterStream(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setBlob(int parameterIndex, InputStream x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { - checkClosed(); + ensureOpen(); values[parameterIndex - 1] = encodeObject(x); } @Override public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException { - checkClosed(); + ensureOpen(); setObject(parameterIndex, x, targetSqlType, 0); } @@ -631,7 +631,7 @@ public long executeLargeUpdate() throws SQLException { @Override public final void addBatch(String sql) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "addBatch(String) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -639,7 +639,7 @@ public final void addBatch(String sql) throws SQLException { @Override public final boolean execute(String sql) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "execute(String) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -647,7 +647,7 @@ public final boolean execute(String sql) throws SQLException { @Override public final boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "execute(String, int) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -655,7 +655,7 @@ public final boolean execute(String sql, int autoGeneratedKeys) throws SQLExcept @Override public final boolean execute(String sql, int[] columnIndexes) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "execute(String, int[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -663,7 +663,7 @@ public final boolean execute(String sql, int[] columnIndexes) throws SQLExceptio @Override public final boolean execute(String sql, String[] columnNames) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "execute(String, String[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -671,7 +671,7 @@ public final boolean execute(String sql, String[] columnNames) throws SQLExcepti @Override public final long executeLargeUpdate(String sql) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeLargeUpdate(String) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -679,7 +679,7 @@ public final long executeLargeUpdate(String sql) throws SQLException { @Override public final long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeLargeUpdate(String, int) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -687,7 +687,7 @@ public final long executeLargeUpdate(String sql, int autoGeneratedKeys) throws S @Override public final long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeLargeUpdate(String, int[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -695,7 +695,7 @@ public final long executeLargeUpdate(String sql, int[] columnIndexes) throws SQL @Override public final long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeLargeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -703,7 +703,7 @@ public final long executeLargeUpdate(String sql, String[] columnNames) throws SQ @Override public final ResultSet executeQuery(String sql) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeQuery(String) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -711,7 +711,7 @@ public final ResultSet executeQuery(String sql) throws SQLException { @Override public final int executeUpdate(String sql) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeUpdate(String) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -719,7 +719,7 @@ public final int executeUpdate(String sql) throws SQLException { @Override public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeUpdate(String, int) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -727,7 +727,7 @@ public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLExce @Override public final int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeUpdate(String, int[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); @@ -735,7 +735,7 @@ public final int executeUpdate(String sql, int[] columnIndexes) throws SQLExcept @Override public final int executeUpdate(String sql, String[] columnNames) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException( "executeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!", ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index 5463beb7e..fa6cd26a0 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -130,6 +130,7 @@ public void close() throws SQLException { response = null; } } + parentStatement.onResultSetClosed(this); } if (e != null) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index f79a7da07..03edf4568 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -3,10 +3,10 @@ import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.internal.ServerSettings; -import com.clickhouse.client.api.metrics.OperationMetrics; -import com.clickhouse.client.api.metrics.ServerMetrics; import com.clickhouse.client.api.query.QueryResponse; import com.clickhouse.client.api.query.QuerySettings; +import com.clickhouse.client.api.sql.SQLUtils; +import com.clickhouse.jdbc.internal.DriverProperties; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.ParsedStatement; import org.slf4j.Logger; @@ -14,12 +14,12 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; public class StatementImpl implements Statement, JdbcV2Wrapper { @@ -31,52 +31,62 @@ public class StatementImpl implements Statement, JdbcV2Wrapper { protected boolean isPoolable = false; // Statement is not poolable by default // State - protected boolean closed; + private volatile boolean closed; + private final ConcurrentLinkedQueue resultSets; // all result sets linked to this statement protected ResultSetImpl currentResultSet; - protected OperationMetrics metrics; + protected long currentUpdateCount = -1; protected List batch; private String lastStatementSql; private ParsedStatement parsedStatement; protected volatile String lastQueryId; - private int maxRows; + private long maxRows; + private boolean closeOnCompletion; + private boolean resultSetAutoClose; + private int maxFieldSize; + private boolean escapeProcessingEnabled; + + // settings local to a statement protected QuerySettings localSettings; public StatementImpl(ConnectionImpl connection) throws SQLException { this.connection = connection; this.queryTimeout = 0; this.closed = false; - this.currentResultSet = null; - this.metrics = null; this.batch = new ArrayList<>(); this.maxRows = 0; this.localSettings = QuerySettings.merge(connection.getDefaultQuerySettings(), new QuerySettings()); + this.resultSets= new ConcurrentLinkedQueue<>(); + this.resultSetAutoClose = connection.getJdbcConfig().isSet(DriverProperties.RESULTSET_AUTO_CLOSE); + this.escapeProcessingEnabled = true; } - protected void checkClosed() throws SQLException { + protected void ensureOpen() throws SQLException { if (closed) { throw new SQLException("Statement is closed", ExceptionUtils.SQL_STATE_CONNECTION_EXCEPTION); } } - protected static String parseJdbcEscapeSyntax(String sql) { + private String parseJdbcEscapeSyntax(String sql) { LOG.trace("Original SQL: {}", sql); - // Replace {d 'YYYY-MM-DD'} with corresponding SQL date format - sql = sql.replaceAll("\\{d '([^']*)'\\}", "toDate('$1')"); + if (escapeProcessingEnabled) { + // Replace {d 'YYYY-MM-DD'} with corresponding SQL date format + sql = sql.replaceAll("\\{d '([^']*)'\\}", "toDate('$1')"); - // Replace {ts 'YYYY-MM-DD HH:mm:ss'} with corresponding SQL timestamp format - sql = sql.replaceAll("\\{ts '([^']*)'\\}", "timestamp('$1')"); + // Replace {ts 'YYYY-MM-DD HH:mm:ss'} with corresponding SQL timestamp format + sql = sql.replaceAll("\\{ts '([^']*)'\\}", "timestamp('$1')"); - // Replace function escape syntax {fn } (e.g., {fn UCASE(name)}) - sql = sql.replaceAll("\\{fn ([^\\}]*)\\}", "$1"); + // Replace function escape syntax {fn } (e.g., {fn UCASE(name)}) + sql = sql.replaceAll("\\{fn ([^\\}]*)\\}", "$1"); - // Handle outer escape syntax - //sql = sql.replaceAll("\\{escape '([^']*)'\\}", "'$1'"); + // Handle outer escape syntax + //sql = sql.replaceAll("\\{escape '([^']*)'\\}", "'$1'"); - // Clean new empty lines in sql - sql = sql.replaceAll("(?m)^\\s*$\\n?", ""); - // Add more replacements as needed for other JDBC escape sequences - LOG.trace("Parsed SQL: {}", sql); + // Clean new empty lines in sql + sql = sql.replaceAll("(?m)^\\s*$\\n?", ""); + // Add more replacements as needed for other JDBC escape sequences + } + LOG.trace("Escaped SQL: {}", sql); return sql; } @@ -86,11 +96,13 @@ protected String getLastStatementSql() { @Override public ResultSet executeQuery(String sql) throws SQLException { - checkClosed(); - return executeQueryImpl(sql, localSettings); + ensureOpen(); + currentUpdateCount = -1; + currentResultSet = executeQueryImpl(sql, localSettings); + return currentResultSet; } - private void closePreviousResultSet() { + private void closeCurrentResultSet() { if (currentResultSet != null) { LOG.debug("Previous result set is open [resultSet = " + currentResultSet + "]"); // Closing request blindly assuming that user do not care about it anymore (DDL request for example) @@ -99,38 +111,32 @@ private void closePreviousResultSet() { } catch (Exception e) { LOG.error("Failed to close previous result set", e); } finally { - currentResultSet = null; + currentResultSet = null; // no need to remember we have closed it already } } } protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) throws SQLException { - checkClosed(); - - // TODO: method should throw exception if no result set returned + ensureOpen(); // Closing before trying to do next request. Otherwise, deadlock because previous connection will not be // release before this one completes. - closePreviousResultSet(); - - QuerySettings mergedSettings = QuerySettings.merge(connection.getDefaultQuerySettings(), settings); - if (maxRows > 0) { - mergedSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), maxRows); - mergedSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), "break"); + if (resultSetAutoClose) { + closeCurrentResultSet(); } - if (mergedSettings.getQueryId() != null) { - lastQueryId = mergedSettings.getQueryId(); - } else { - lastQueryId = UUID.randomUUID().toString(); - mergedSettings.setQueryId(lastQueryId); + QuerySettings mergedSettings = QuerySettings.merge(settings, new QuerySettings()); + if (mergedSettings.getQueryId() == null) { + final String queryId = UUID.randomUUID().toString(); + mergedSettings.setQueryId(queryId); } + lastQueryId = mergedSettings.getQueryId(); LOG.debug("Query ID: {}", lastQueryId); + QueryResponse response = null; try { lastStatementSql = parseJdbcEscapeSyntax(sql); LOG.trace("SQL Query: {}", lastStatementSql); // this is not secure for create statements because of passwords - QueryResponse response; if (queryTimeout == 0) { response = connection.client.query(lastStatementSql, mergedSettings).get(); } else { @@ -142,50 +148,52 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr ExceptionUtils.SQL_STATE_CLIENT_ERROR); } ClickHouseBinaryFormatReader reader = connection.client.newBinaryFormatReader(response); - - currentResultSet = new ResultSetImpl(this, response, reader); - metrics = response.getMetrics(); + if (reader.getSchema() == null) { + throw new SQLException("Called method expects empty or filled result set but query has returned none. Consider using `java.sql.Statement.execute(java.lang.String)`", ExceptionUtils.SQL_STATE_CLIENT_ERROR); + } + return new ResultSetImpl(this, response, reader); } catch (Exception e) { + if (response != null) { + try { + response.close(); + } catch (Exception ex) { + LOG.warn("Failed to close response after exception", e); + } + } + onResultSetClosed(null); throw ExceptionUtils.toSqlState(e); } - - return currentResultSet; } @Override public int executeUpdate(String sql) throws SQLException { - checkClosed(); - parsedStatement = connection.getSqlParser().parsedStatement(sql); - int updateCount = executeUpdateImpl(sql, localSettings); - postUpdateActions(); - return updateCount; + ensureOpen(); + return (int)executeLargeUpdate(sql); } - protected int executeUpdateImpl(String sql, QuerySettings settings) throws SQLException { - checkClosed(); + protected long executeUpdateImpl(String sql, QuerySettings settings) throws SQLException { + ensureOpen(); - // TODO: method should throw exception if result set returned // Closing before trying to do next request. Otherwise, deadlock because previous connection will not be // release before this one completes. - closePreviousResultSet(); + if (resultSetAutoClose) { + closeCurrentResultSet(); + } QuerySettings mergedSettings = QuerySettings.merge(connection.getDefaultQuerySettings(), settings); - if (mergedSettings.getQueryId() != null) { - lastQueryId = mergedSettings.getQueryId(); - } else { - lastQueryId = UUID.randomUUID().toString(); - mergedSettings.setQueryId(lastQueryId); + if (mergedSettings.getQueryId() == null) { + final String queryId = UUID.randomUUID().toString(); + mergedSettings.setQueryId(queryId); } + lastQueryId = mergedSettings.getQueryId(); lastStatementSql = parseJdbcEscapeSyntax(sql); LOG.trace("SQL Query: {}", lastStatementSql); int updateCount = 0; try (QueryResponse response = queryTimeout == 0 ? connection.client.query(lastStatementSql, mergedSettings).get() : connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) { - currentResultSet = null; updateCount = Math.max(0, (int) response.getWrittenRows()); // when statement alters schema no result rows returned. - metrics = response.getMetrics(); lastQueryId = response.getQueryId(); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); @@ -208,65 +216,59 @@ protected void postUpdateActions() { @Override public void close() throws SQLException { closed = true; - if (currentResultSet != null) { - try { - currentResultSet.close(); - } catch (Exception e) { - LOG.debug("Failed to close current result set", e); - } finally { - currentResultSet = null; + closeCurrentResultSet(); + for (ResultSetImpl resultSet : resultSets) { + if (resultSet != null && !resultSet.isClosed()) { + try { + resultSet.close(); + } catch (Exception e) { + LOG.error("Failed to close result set", e); + } } } } @Override public int getMaxFieldSize() throws SQLException { - checkClosed(); - return 0; + ensureOpen(); + return this.maxFieldSize; } @Override public void setMaxFieldSize(int max) throws SQLException { - checkClosed(); - if (!connection.config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Set max field size is not supported.", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + ensureOpen(); + if (max < 0) { + throw new SQLException("max should be a positive integer."); } + this.maxFieldSize = max; } @Override public int getMaxRows() throws SQLException { - checkClosed(); - return maxRows; + ensureOpen(); + return (int) getLargeMaxRows(); // skip overflow check. } @Override public void setMaxRows(int max) throws SQLException { - checkClosed(); - maxRows = max; - if (max > 0) { - localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), maxRows); - localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), "break"); - } else { - localSettings.resetOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS)); - localSettings.resetOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE)); - } + setLargeMaxRows(max); } @Override public void setEscapeProcessing(boolean enable) throws SQLException { - checkClosed(); - //TODO: Should we support this? + ensureOpen(); + this.escapeProcessingEnabled = enable; } @Override public int getQueryTimeout() throws SQLException { - checkClosed(); + ensureOpen(); return queryTimeout; } @Override public void setQueryTimeout(int seconds) throws SQLException { - checkClosed(); + ensureOpen(); queryTimeout = seconds; } @@ -287,29 +289,31 @@ public void cancel() throws SQLException { @Override public SQLWarning getWarnings() throws SQLException { - checkClosed(); + ensureOpen(); return null; } @Override public void clearWarnings() throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setCursorName(String name) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public boolean execute(String sql) throws SQLException { - checkClosed(); + ensureOpen(); parsedStatement = connection.getSqlParser().parsedStatement(sql); + currentUpdateCount = -1; + currentResultSet = null; if (parsedStatement.isHasResultSet()) { - currentResultSet = executeQueryImpl(sql, localSettings); // keep open to allow getResultSet() + currentResultSet = executeQueryImpl(sql, localSettings); return true; } else { - executeUpdateImpl(sql, localSettings); + currentUpdateCount = executeUpdateImpl(sql, localSettings); postUpdateActions(); return false; } @@ -317,77 +321,69 @@ public boolean execute(String sql) throws SQLException { @Override public ResultSet getResultSet() throws SQLException { - checkClosed(); + ensureOpen(); - ResultSet resultSet = currentResultSet; - currentResultSet = null; - return resultSet; + return currentResultSet; } @Override public int getUpdateCount() throws SQLException { - checkClosed(); - if (currentResultSet == null && metrics != null) { - int updateCount = (int) metrics.getMetric(ServerMetrics.NUM_ROWS_WRITTEN).getLong(); - metrics = null;// clear metrics - return updateCount; - } - - return -1; + ensureOpen(); + return (int) getLargeUpdateCount(); } @Override public boolean getMoreResults() throws SQLException { - checkClosed(); - return false; + ensureOpen(); + return getMoreResults(Statement.CLOSE_CURRENT_RESULT); } @Override public void setFetchDirection(int direction) throws SQLException { - checkClosed(); - if (!connection.config.isIgnoreUnsupportedRequests()) { - throw new SQLFeatureNotSupportedException("Set fetch direction is not supported.", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); + ensureOpen(); + if (direction != ResultSet.FETCH_FORWARD && direction != ResultSet.FETCH_REVERSE && direction != ResultSet.FETCH_UNKNOWN) { + throw new SQLException("Invalid fetch direction: " + direction + ". Should be one of ResultSet.FETCH_FORWARD, ResultSet.FETCH_REVERSE, or ResultSet.FETCH_UNKNOWN"); } } @Override public int getFetchDirection() throws SQLException { - checkClosed(); + ensureOpen(); return ResultSet.FETCH_FORWARD; } @Override public void setFetchSize(int rows) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public int getFetchSize() throws SQLException { - checkClosed(); + ensureOpen(); return 0; } @Override public int getResultSetConcurrency() throws SQLException { - checkClosed(); + ensureOpen(); return ResultSet.CONCUR_READ_ONLY; } @Override public int getResultSetType() throws SQLException { - checkClosed(); + ensureOpen(); return ResultSet.TYPE_FORWARD_ONLY; } @Override public void addBatch(String sql) throws SQLException { - checkClosed(); + ensureOpen(); batch.add(sql); } @Override public void clearBatch() throws SQLException { - checkClosed(); + ensureOpen(); batch.clear(); } @@ -397,7 +393,7 @@ public int[] executeBatch() throws SQLException { } private List executeBatchImpl() throws SQLException { - checkClosed(); + ensureOpen(); List results = new ArrayList<>(); for (String sql : batch) { results.add(executeUpdate(sql)); @@ -410,10 +406,51 @@ public ConnectionImpl getConnection() throws SQLException { return connection; } + /** + * Returns instance of local settings. Can be used to override settings. + * + * @return QuerySettings that is used as base for each request. + */ + public QuerySettings getLocalSettings() { + return localSettings; + } + @Override public boolean getMoreResults(int current) throws SQLException { - // TODO: implement query batches. When multiple selects in the batch. - return false; + // This method designed to iterate over multiple resultsets after "execute(sql)" method is called + // But we have at most only one always + // Then we should close any existing and return false to indicate that no more result are present + + if (currentResultSet != null && current != Statement.KEEP_CURRENT_RESULT) { + currentResultSet.close(); + } + + currentResultSet = null; + currentUpdateCount = -1; + return false; // false indicates that no more results (or it is an update count) + } + + @Override + public String enquoteLiteral(String val) throws SQLException { + return SQLUtils.enquoteLiteral(val); + } + + @Override + public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException { + return SQLUtils.enquoteIdentifier(identifier, alwaysQuote); + } + + @Override + public boolean isSimpleIdentifier(String identifier) throws SQLException { + return SQLUtils.isSimpleIdentifier(identifier); + } + + @Override + public String enquoteNCharLiteral(String val) throws SQLException { + if (val == null) { + throw new NullPointerException(); + } + return "N" + SQLUtils.enquoteLiteral(val); } @Override @@ -464,7 +501,7 @@ public boolean isClosed() throws SQLException { @Override public void setPoolable(boolean poolable) throws SQLException { - checkClosed(); + ensureOpen(); this.isPoolable = poolable; } @@ -475,30 +512,60 @@ public boolean isPoolable() throws SQLException { @Override public void closeOnCompletion() throws SQLException { - checkClosed(); + ensureOpen(); + this.closeOnCompletion = true; + } + + // called each time query is complete or result set is closed + public void onResultSetClosed(ResultSetImpl resultSet) throws SQLException { + if (resultSet != null) { + this.resultSets.remove(resultSet); + } + + if (this.closeOnCompletion) { + if ((resultSets.isEmpty()) && (currentResultSet == null || currentResultSet.isClosed())) { + // last result set is closed. + this.closed = true; + } + } } @Override public boolean isCloseOnCompletion() throws SQLException { - return false; + return this.closeOnCompletion; } @Override public long getLargeUpdateCount() throws SQLException { - checkClosed(); - return getUpdateCount(); + ensureOpen(); + return currentUpdateCount; } @Override public void setLargeMaxRows(long max) throws SQLException { - checkClosed(); - Statement.super.setLargeMaxRows(max); + ensureOpen(); + maxRows = max; + // This method override user set overflow mode on purpose: + // 1. Spec clearly states that after calling this method with a limit > 0 all rows over limit are dropped. + // 2. Calling this method should not cause throwing exception for future queries what only `break` can guarantee + // 3. If user wants different behavior then they are can use connection properties. + if (max > 0) { + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), maxRows); + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), + ServerSettings.RESULT_OVERFLOW_MODE_BREAK); + } else { + // overriding potential client settings (set thru connection setup) + // there is no no limit value so we use very large limit. + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), Long.MAX_VALUE); + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), + ServerSettings.RESULT_OVERFLOW_MODE_BREAK); + } } @Override public long getLargeMaxRows() throws SQLException { - checkClosed(); - return getMaxRows(); + ensureOpen(); + return this.maxRows; } @Override @@ -508,22 +575,25 @@ public long[] executeLargeBatch() throws SQLException { @Override public long executeLargeUpdate(String sql) throws SQLException { - return executeUpdate(sql); + parsedStatement = connection.getSqlParser().parsedStatement(sql); + long updateCount = executeUpdateImpl(sql, localSettings); + postUpdateActions(); + return updateCount; } @Override public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - return executeUpdate(sql, autoGeneratedKeys); + return executeLargeUpdate(sql); } @Override public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { - return executeUpdate(sql, columnIndexes); + return executeLargeUpdate(sql); } @Override public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { - return executeUpdate(sql, columnNames); + return executeLargeUpdate(sql); } /** diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java index c910575cf..e8a69aa8e 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java @@ -82,7 +82,7 @@ private void resetWriter() throws IOException { @Override public ResultSet executeQuery() throws SQLException { - checkClosed(); + ensureOpen(); throw new UnsupportedOperationException("bug. This PreparedStatement implementation should not be used with queries"); } @@ -103,7 +103,7 @@ public ResultSet getResultSet() throws SQLException { @Override public long executeLargeUpdate() throws SQLException { - checkClosed(); + ensureOpen(); // commit whatever changes try { @@ -118,9 +118,7 @@ public long executeLargeUpdate() throws SQLException { try (InsertResponse response = queryTimeout == 0 ? connection.client.insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get() : connection.client.insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get(queryTimeout, TimeUnit.SECONDS)) { - currentResultSet = null; updateCount = Math.max(0, (int) response.getWrittenRows()); // when statement alters schema no result rows returned. - metrics = response.getMetrics(); lastQueryId = response.getQueryId(); } catch (Exception e) { throw ExceptionUtils.toSqlState(e); @@ -136,67 +134,67 @@ public long executeLargeUpdate() throws SQLException { @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, null); } @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, x); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setByte(parameterIndex, x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setShort(parameterIndex, x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setInteger(parameterIndex, x); } @Override public void setLong(int parameterIndex, long x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setLong(parameterIndex, x); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setFloat(parameterIndex, x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setDouble(parameterIndex, x); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setBigDecimal(parameterIndex, x); } @Override public void setString(int parameterIndex, String x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setString(parameterIndex, x); } @Override public void setBytes(int parameterIndex, byte[] x) throws SQLException { - checkClosed(); + ensureOpen(); } @@ -217,74 +215,74 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { @Override public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setCharacterStream(int parameterIndex, Reader x, int length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setCharacterStream(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setNCharacterStream(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setNCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void clearParameters() throws SQLException { - checkClosed(); + ensureOpen(); writer.clearRow(); } @@ -296,7 +294,7 @@ public boolean execute() throws SQLException { @Override public void addBatch() throws SQLException { - checkClosed(); + ensureOpen(); try { writer.commitRow(); } catch (Exception e) { @@ -306,128 +304,128 @@ public void addBatch() throws SQLException { @Override public void setClob(int parameterIndex, Clob x) throws SQLException { - checkClosed(); + ensureOpen(); setClob(parameterIndex, x.getCharacterStream()); } @Override public void setClob(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); setClob(parameterIndex, x, -1); } @Override public void setClob(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); writer.setReader(parameterIndex, x, length); } @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { - checkClosed(); + ensureOpen(); setBlob(parameterIndex, x.getBinaryStream(), x.length()); } @Override public void setBlob(int parameterIndex, InputStream x, long length) throws SQLException { - checkClosed(); + ensureOpen(); writer.setInputStream(parameterIndex, x, length); } @Override public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { - checkClosed(); + ensureOpen(); writer.setInputStream(parameterIndex, inputStream, -1); } @Override public void setNClob(int parameterIndex, Reader x, long length) throws SQLException { - checkClosed(); + ensureOpen(); writer.setReader(parameterIndex, x, length); } @Override public void setNClob(int parameterIndex, NClob x) throws SQLException { - checkClosed(); + ensureOpen(); setNClob(parameterIndex, x.getCharacterStream(), x.length()); } @Override public void setNClob(int parameterIndex, Reader x) throws SQLException { - checkClosed(); + ensureOpen(); setNClob(parameterIndex, x, -1); } @Override public void setSQLXML(int parameterIndex, SQLXML x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setReader(parameterIndex, x.getCharacterStream(), -1); } @Override public void setArray(int parameterIndex, Array x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, x.getArray()); } @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, sqlDateToInstant(x, cal)); } @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, sqlTimeToInstant(x, cal)); } @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { - checkClosed(); + ensureOpen(); writer.setDateTime(parameterIndex, sqlTimestampToZDT(x, cal)); } @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, null); } @Override public void setURL(int parameterIndex, URL x) throws SQLException { - checkClosed(); + ensureOpen(); } @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { - checkClosed(); + ensureOpen(); throw new SQLException("ROWID is not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); } @Override public void setNString(int parameterIndex, String value) throws SQLException { - checkClosed(); + ensureOpen(); writer.setString(parameterIndex, value); } @Override public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { - checkClosed(); + ensureOpen(); // TODO: make proper data conversion in setObject methods writer.setValue(parameterIndex, x); } @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, x); } @Override public void setObject(int parameterIndex, Object x) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, x); } @@ -438,7 +436,7 @@ public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throw @Override public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { - checkClosed(); + ensureOpen(); writer.setValue(parameterIndex, x); } @@ -467,7 +465,7 @@ public void cancel() throws SQLException { @Override public int[] executeBatch() throws SQLException { - checkClosed(); + ensureOpen(); int batchSize = writer.getRowCount(); long rowsInserted = executeLargeUpdate(); int[] results = new int[batchSize]; @@ -477,7 +475,7 @@ public int[] executeBatch() throws SQLException { @Override public long[] executeLargeBatch() throws SQLException { - checkClosed(); + ensureOpen(); int batchSize = writer.getRowCount(); long rowsInserted = executeLargeUpdate(); long[] results = new long[batchSize]; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DriverProperties.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DriverProperties.java index 2a257c4b5..6d57f6bfd 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DriverProperties.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/DriverProperties.java @@ -19,7 +19,7 @@ public enum DriverProperties { SECURE_CONNECTION("ssl", "false"), /** - * query settings to be passed along with query operation. + * Query settings to be passed along with query operation. * {@see com.clickhouse.client.api.query.QuerySettings} */ DEFAULT_QUERY_SETTINGS("default_query_settings", null), @@ -31,7 +31,13 @@ public enum DriverProperties { */ BETA_ROW_BINARY_WRITER("beta.row_binary_for_simple_insert", "false"), + /** + * Enables closing result set before + */ + RESULTSET_AUTO_CLOSE("jdbc_resultset_auto_close", "true"), ; + + private final String key; private final String defaultValue; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java index bfedbec55..168905336 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java @@ -277,6 +277,11 @@ public String getDriverProperty(String key, String defaultValue) { return driverProperties.getOrDefault(key, defaultValue); } + public Boolean isSet(DriverProperties driverProp) { + String v = driverProperties.getOrDefault(driverProp.getKey(), driverProp.getDefaultValue()); + return Boolean.parseBoolean(v); + } + public Client.Builder applyClientProperties(Client.Builder builder) { builder.addEndpoint(connectionUrl) .setOptions(clientProperties) diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java index 62e0493ec..794967eea 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc.internal; +import com.clickhouse.client.api.sql.SQLUtils; import org.antlr.v4.runtime.tree.ErrorNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -144,7 +145,7 @@ public void enterQueryStmt(ClickHouseParser.QueryStmtContext ctx) { @Override public void enterUseStmt(ClickHouseParser.UseStmtContext ctx) { if (ctx.databaseIdentifier() != null) { - setUseDatabase(SqlParser.unquoteIdentifier(ctx.databaseIdentifier().getText())); + setUseDatabase(SQLUtils.unquoteIdentifier(ctx.databaseIdentifier().getText())); } } @@ -155,7 +156,7 @@ public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { } else { List roles = new ArrayList<>(); for (ClickHouseParser.IdentifierContext id : ctx.setRolesList().identifier()) { - roles.add(SqlParser.unquoteIdentifier(id.getText())); + roles.add(SQLUtils.unquoteIdentifier(id.getText())); } setRoles(roles); } @@ -213,7 +214,7 @@ private void appendParameter(int startIndex) { @Override public void enterTableExprIdentifier(ClickHouseParser.TableExprIdentifierContext ctx) { if (ctx.tableIdentifier() != null) { - this.table = SqlParser.unquoteIdentifier(ctx.tableIdentifier().getText()); + this.table = SQLUtils.unquoteIdentifier(ctx.tableIdentifier().getText()); } } @@ -221,7 +222,7 @@ public void enterTableExprIdentifier(ClickHouseParser.TableExprIdentifierContext public void enterInsertStmt(ClickHouseParser.InsertStmtContext ctx) { ClickHouseParser.TableIdentifierContext tableId = ctx.tableIdentifier(); if (tableId != null) { - this.table = SqlParser.unquoteIdentifier(tableId.getText()); + this.table = SQLUtils.unquoteIdentifier(tableId.getText()); } ClickHouseParser.ColumnsClauseContext columns = ctx.columnsClause(); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java index f12fb8ccb..ee94eaf67 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc.internal; +import com.clickhouse.client.api.sql.SQLUtils; import org.antlr.v4.runtime.tree.ErrorNode; import java.util.ArrayList; @@ -87,7 +88,7 @@ public void enterQueryStmt(ClickHouseParser.QueryStmtContext ctx) { @Override public void enterUseStmt(ClickHouseParser.UseStmtContext ctx) { if (ctx.databaseIdentifier() != null) { - setUseDatabase(SqlParser.unquoteIdentifier(ctx.databaseIdentifier().getText())); + setUseDatabase(SQLUtils.unquoteIdentifier(ctx.databaseIdentifier().getText())); } } @@ -98,7 +99,7 @@ public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { } else { List roles = new ArrayList<>(); for (ClickHouseParser.IdentifierContext id : ctx.setRolesList().identifier()) { - roles.add(SqlParser.unquoteIdentifier(id.getText())); + roles.add(SQLUtils.unquoteIdentifier(id.getText())); } setRoles(roles); } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java index 111ae77f3..8a57a550d 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java @@ -42,28 +42,6 @@ private ClickHouseParser walkSql(String sql, ClickHouseParserBaseListener listen return parser; } - private final static Pattern UNQUOTE_INDENTIFIER = Pattern.compile( - "^[\\\"`]?(.+?)[\\\"`]?$" - ); - - public static String unquoteIdentifier(String str) { - Matcher matcher = UNQUOTE_INDENTIFIER.matcher(str.trim()); - if (matcher.find()) { - return matcher.group(1); - } else { - return str; - } - } - - public static String escapeQuotes(String str) { - if (str == null || str.isEmpty()) { - return str; - } - return str - .replace("'", "\\'") - .replace("\"", "\\\""); - } - private static class ParserErrorListener extends BaseErrorListener { @Override public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index f433f749a..44b7817e5 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -1,11 +1,13 @@ package com.clickhouse.jdbc.metadata; +import com.clickhouse.client.api.sql.SQLUtils; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.ConnectionImpl; import com.clickhouse.jdbc.Driver; import com.clickhouse.jdbc.JdbcV2Wrapper; import com.clickhouse.jdbc.ResultSetImpl; +import com.clickhouse.jdbc.StatementImpl; import com.clickhouse.jdbc.internal.ClientInfoProperties; import com.clickhouse.jdbc.internal.DriverProperties; import com.clickhouse.jdbc.internal.ExceptionUtils; @@ -862,9 +864,9 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa "'NO' as IS_AUTOINCREMENT, " + "'NO' as IS_GENERATEDCOLUMN " + " FROM system.columns" + - " WHERE database LIKE '" + (schemaPattern == null ? "%" : SqlParser.escapeQuotes(schemaPattern)) + "'" + - " AND table LIKE '" + (tableNamePattern == null ? "%" : SqlParser.escapeQuotes(tableNamePattern)) + "'" + - " AND name LIKE '" + (columnNamePattern == null ? "%" : SqlParser.escapeQuotes(columnNamePattern)) + "'" + + " WHERE database LIKE " + SQLUtils.enquoteLiteral(schemaPattern == null ? "%" : schemaPattern) + + " AND table LIKE " + SQLUtils.enquoteLiteral(tableNamePattern == null ? "%" : tableNamePattern) + + " AND name LIKE " + SQLUtils.enquoteLiteral(columnNamePattern == null ? "%" : columnNamePattern) + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; try { return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(sql)) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 2d4a31fd5..3abf10f45 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -1,13 +1,15 @@ package com.clickhouse.jdbc; import com.clickhouse.client.api.ClientConfigProperties; +import com.clickhouse.client.api.internal.ServerSettings; import com.clickhouse.client.api.query.GenericRecord; -import com.clickhouse.jdbc.internal.ClickHouseParser; +import com.clickhouse.jdbc.internal.DriverProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import com.clickhouse.data.ClickHouseVersion; import org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.net.Inet4Address; @@ -28,19 +30,23 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; -@Test(groups = { "integration" }) +@Test(groups = {"integration"}) public class StatementTest extends JdbcIntegrationTest { private static final Logger log = LoggerFactory.getLogger(StatementTest.class); - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQuerySimpleNumbers() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { + Assert.assertThrows(SQLException.class, () -> stmt.setFetchDirection(100)); + stmt.setFetchDirection(ResultSet.FETCH_REVERSE); + assertEquals(stmt.getFetchDirection(), ResultSet.FETCH_FORWARD); // we support only this direction try (ResultSet rs = stmt.executeQuery("SELECT 1 AS num")) { assertTrue(rs.next()); assertEquals(rs.getByte(1), 1); @@ -53,12 +59,12 @@ public void testExecuteQuerySimpleNumbers() throws Exception { assertEquals(rs.getLong("num"), 1); assertFalse(rs.next()); } - Assert.assertFalse(((StatementImpl)stmt).getLastQueryId().isEmpty()); + Assert.assertFalse(((StatementImpl) stmt).getLastQueryId().isEmpty()); } } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQuerySimpleFloats() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -74,7 +80,7 @@ public void testExecuteQuerySimpleFloats() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQueryBooleans() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -88,7 +94,7 @@ public void testExecuteQueryBooleans() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQueryStrings() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -102,7 +108,7 @@ public void testExecuteQueryStrings() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQueryNulls() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -118,7 +124,7 @@ public void testExecuteQueryNulls() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQueryDates() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -138,7 +144,7 @@ public void testExecuteQueryDates() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateSimpleNumbers() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -157,7 +163,7 @@ public void testExecuteUpdateSimpleNumbers() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateSimpleFloats() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -172,11 +178,12 @@ public void testExecuteUpdateSimpleFloats() throws Exception { assertEquals(rs.getFloat(1), 3.3f); assertFalse(rs.next()); } + assertEquals(stmt.getUpdateCount(), -1); } } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateBooleans() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -195,7 +202,7 @@ public void testExecuteUpdateBooleans() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateStrings() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -214,7 +221,7 @@ public void testExecuteUpdateStrings() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateNulls() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -233,7 +240,7 @@ public void testExecuteUpdateNulls() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateDates() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -256,7 +263,7 @@ public void testExecuteUpdateDates() throws Exception { } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteUpdateBatch() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -284,7 +291,7 @@ public void testExecuteUpdateBatch() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testJdbcEscapeSyntax() throws Exception { if (ClickHouseVersion.of(getServerVersion()).check("(,23.8]")) { return; // there is no `timestamp` function TODO: fix in JDBC @@ -325,10 +332,19 @@ public void testJdbcEscapeSyntax() throws Exception { assertFalse(rs.next()); } } + + try (Statement stmt = conn.createStatement()) { + stmt.setEscapeProcessing(false); + try (ResultSet rs = stmt.executeQuery("SELECT {d '2021-11-01'} AS D")) { + fail("Expected to fail"); + } catch (SQLException e) { + // ignore + } + } } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteQueryTimeout() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -343,7 +359,7 @@ public void testExecuteQueryTimeout() throws Exception { } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testSettingRole() throws SQLException { if (earlierThan(24, 4)) {//Min version is 24.4 return; @@ -429,7 +445,7 @@ public void testGettingArrays() throws Exception { Array numberArray = rs.getArray("number_array"); assertEquals(((Object[]) numberArray.getArray()).length, 3); System.out.println(((Object[]) numberArray.getArray())[0].getClass().getName()); - assertEquals(numberArray.getArray(), new short[] {1, 2, 3} ); + assertEquals(numberArray.getArray(), new short[]{1, 2, 3}); Array stringArray = rs.getArray("str_array"); assertEquals(((Object[]) stringArray.getArray()).length, 3); assertEquals(Arrays.stream(((Object[]) stringArray.getArray())).toList(), Arrays.asList("val1", "val2", "val3")); @@ -437,7 +453,7 @@ public void testGettingArrays() throws Exception { } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testWithIPs() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { @@ -465,14 +481,14 @@ public void testConnectionExhaustion() throws Exception { try (Connection conn = getJdbcConnection(properties)) { try (Statement stmt = conn.createStatement()) { - for (int i = 0; i< maxNumConnections * 2; i++) { + for (int i = 0; i < maxNumConnections * 2; i++) { stmt.executeQuery("SELECT number FROM system.numbers LIMIT 100"); } } } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testConcurrentCancel() throws Exception { int maxNumConnections = 3; Properties p = new Properties(); @@ -508,7 +524,7 @@ public void testConcurrentCancel() throws Exception { public void testTextFormatInResponse() throws Exception { try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { - Assert.expectThrows(SQLException.class, () ->stmt.executeQuery("SELECT 1 FORMAT JSON")); + Assert.expectThrows(SQLException.class, () -> stmt.executeQuery("SELECT 1 FORMAT JSON")); } } @@ -527,7 +543,7 @@ void testWithClause() throws Exception { assertEquals(count, 100); } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testSwitchDatabase() throws Exception { String databaseName = getDatabase() + "_test_switch"; String createSql = "CREATE TABLE switchDatabaseWithUse (id UInt8, words String) ENGINE = MergeTree ORDER BY ()"; @@ -555,7 +571,7 @@ public void testSwitchDatabase() throws Exception { } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testNewLineSQLParsing() throws Exception { try (Connection conn = getJdbcConnection()) { String sqlCreate = "CREATE TABLE balance ( `id` UUID, `currency` String, `amount` Decimal(64, 18), `create_time` DateTime64(6), `_version` UInt64, `_sign` UInt8 ) ENGINE = ReplacingMergeTree PRIMARY KEY id ORDER BY id;"; @@ -620,7 +636,7 @@ public void testNewLineSQLParsing() throws Exception { } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testNullableFixedStringType() throws Exception { try (Connection conn = getJdbcConnection()) { String sqlCreate = "CREATE TABLE `data_types` (`f1` FixedString(4),`f2` LowCardinality(FixedString(4)), `f3` Nullable(FixedString(4)), `f4` LowCardinality(Nullable(FixedString(4))) ) ENGINE Memory;"; @@ -628,12 +644,12 @@ public void testNullableFixedStringType() throws Exception { int r = stmt.executeUpdate(sqlCreate); assertEquals(r, 0); } - try(Statement stmt = conn.createStatement()) { + try (Statement stmt = conn.createStatement()) { String sqlInsert = "INSERT INTO `data_types` VALUES ('val1', 'val2', 'val3', 'val4')"; int r = stmt.executeUpdate(sqlInsert); assertEquals(r, 1); } - try(Statement stmt = conn.createStatement()) { + try (Statement stmt = conn.createStatement()) { String sqlSelect = "SELECT * FROM `data_types`"; ResultSet rs = stmt.executeQuery(sqlSelect); assertTrue(rs.next()); @@ -643,7 +659,7 @@ public void testNullableFixedStringType() throws Exception { assertEquals(rs.getString(4), "val4"); assertFalse(rs.next()); } - try(Statement stmt = conn.createStatement()) { + try (Statement stmt = conn.createStatement()) { String sqlSelect = "SELECT f4 FROM `data_types`"; ResultSet rs = stmt.executeQuery(sqlSelect); assertTrue(rs.next()); @@ -652,7 +668,7 @@ public void testNullableFixedStringType() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testWasNullFlagArray() throws Exception { try (Connection conn = getJdbcConnection()) { String sql = "SELECT NULL, ['value1', 'value2']"; @@ -691,10 +707,14 @@ public void testWasNullFlagArray() throws Exception { } } - @Test(groups = { "integration" }) + @Test(groups = {"integration"}) public void testExecuteWithMaxRows() throws Exception { try (Connection conn = getJdbcConnection()) { try (Statement stmt = conn.createStatement()) { + stmt.setMaxRows(10); + assertEquals(stmt.getMaxRows(), 10); + assertEquals(stmt.getLargeMaxRows(), 10); + stmt.setMaxRows(1); int count = 0; try (ResultSet rs = stmt.executeQuery("SELECT * FROM generate_series(0, 100000)")) { @@ -708,6 +728,40 @@ public void testExecuteWithMaxRows() throws Exception { assertTrue(count > 0 && count < 100000); } } + + Properties props = new Properties(); + props.setProperty(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), + ServerSettings.RESULT_OVERFLOW_MODE_THROW); + props.setProperty(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), "100"); + try (Connection conn = getJdbcConnection(props); + Statement stmt = conn.createStatement()) { + + Assert.assertThrows(SQLException.class, () -> stmt.execute("SELECT * FROM generate_series(0, 100000)")); + + { + stmt.setMaxRows(10); + + int count = 0; + try (ResultSet rs = stmt.executeQuery("SELECT * FROM generate_series(0, 100000)")) { + while (rs.next()) { + count++; + } + } + assertTrue(count > 0 && count < 100000); + } + + { + stmt.setMaxRows(0); + + int count = 0; + try (ResultSet rs = stmt.executeQuery("SELECT * FROM generate_series(0, 99999)")) { + while (rs.next()) { + count++; + } + } + assertEquals(count, 100000); + } + } } @Test(groups = {"integration"}) @@ -716,7 +770,7 @@ public void testDDLStatements() throws Exception { return; // skip because we do not want to create extra on cloud instance } try (Connection conn = getJdbcConnection()) { - try (Statement stmt = conn.createStatement()){ + try (Statement stmt = conn.createStatement()) { Assert.assertFalse(stmt.execute("CREATE USER IF NOT EXISTS 'user011' IDENTIFIED BY 'password'")); try (ResultSet rs = stmt.executeQuery("SHOW USERS")) { @@ -731,4 +785,238 @@ public void testDDLStatements() throws Exception { } } } + + @Test(groups = {"integration"}) + public void testEnquoteLiteral() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + String[] literals = {"test literal", "with single '", "with double ''", "with triple '''"}; + for (String literal : literals) { + try (ResultSet rs = stmt.executeQuery("SELECT " + stmt.enquoteLiteral(literal))) { + Assert.assertTrue(rs.next()); + assertEquals(rs.getString(1), literal); + } + } + } + } + + @Test(groups = {"integration"}) + public void testEnquoteIdentifier() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + Object[][] identifiers = {{"simple_identifier", false}, {"complex identified", true}}; + for (Object[] aCase : identifiers) { + stmt.enquoteIdentifier((String) aCase[0], (boolean) aCase[1]); + } + } + } + + @DataProvider(name = "ncharLiteralTestData") + public Object[][] ncharLiteralTestData() { + return new Object[][]{ + // input, expected output + {"test", "N'test'"}, + {"O'Reilly", "N'O''Reilly'"}, + {"", "N''"}, + {"test\nnew line", "N'test\nnew line'"}, + {"unicode: こんにちは", "N'unicode: こんにちは'"}, + {"emoji: 😊", "N'emoji: 😊'"}, + {"quote: \"", "N'quote: \"'"} + }; + } + + @Test(dataProvider = "ncharLiteralTestData") + public void testEnquoteNCharLiteral(String input, String expected) throws SQLException { + try (Statement stmt = getJdbcConnection().createStatement()) { + assertEquals(stmt.enquoteNCharLiteral(input), expected); + } + } + + @Test + public void testEnquoteNCharLiteral_NullInput() throws SQLException { + try (Statement stmt = getJdbcConnection().createStatement()) { + Assert.assertThrows(NullPointerException.class, () -> stmt.enquoteNCharLiteral(null)); + } + } + + @Test(groups = {"integration"}) + public void testIsSimpleIdentifier() throws Exception { + Object[][] identifiers = new Object[][]{ + // identifier, expected result + {"Hello", true}, + {"hello_world", true}, + {"Hello123", true}, + {"H", true}, // minimum length + {"a".repeat(128), true}, // maximum length + + // Test cases from requirements + {"G'Day", false}, + {"\"\"Bruce Wayne\"\"", false}, + {"GoodDay$", false}, + {"Hello\"\"World", false}, + {"\"\"Hello\"\"World\"\"", false}, + + // Additional test cases + {"", false}, // empty string + {"123test", false}, // starts with number + {"_test", false}, // starts with underscore + {"test-name", false}, // contains hyphen + {"test name", false}, // contains space + {"test\"name", false}, // contains quote + {"test.name", false}, // contains dot + {"a".repeat(129), false}, // exceeds max length + {"testName", true}, + {"TEST_NAME", true}, + {"test123", true}, + {"t123", true}, + {"t", true} + }; + try (Statement stmt = getJdbcConnection().createStatement()) { + for (int i = 0; i < identifiers.length; i++) { + assertEquals(stmt.isSimpleIdentifier((String) identifiers[i][0]), identifiers[i][1]); + } + } + } + + @Test(groups = {"integration"}) + public void testExecuteQueryWithNoResultSetWhenExpected() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + Assert.expectThrows(SQLException.class, () -> + stmt.executeQuery("CREATE TABLE test_empty_table (id String) Engine Memory")); + } + } + + @Test(groups = {"integration"}) + public void testUpdateQueryWithResultSet() throws Exception { + Properties props = new Properties(); + props.setProperty(DriverProperties.RESULTSET_AUTO_CLOSE.getKey(), "false"); + props.setProperty(ClientConfigProperties.HTTP_MAX_OPEN_CONNECTIONS.getKey(), "1"); + props.setProperty(ClientConfigProperties.CONNECTION_REQUEST_TIMEOUT.getKey(), "500"); + try (Connection conn = getJdbcConnection(props); Statement stmt = conn.createStatement()) { + stmt.setQueryTimeout(1); + ResultSet rs = stmt.executeQuery("SELECT 1"); + boolean failedOnTimeout = false; + try { + stmt.executeQuery("SELECT 1"); + } catch (SQLException ignore) { + failedOnTimeout = true; + } + assertTrue(failedOnTimeout, "Connection seems closed when should not"); + // no exception expected. Response should be closed automatically + rs.close(); + stmt.executeUpdate("SELECT 1"); + stmt.executeUpdate("SELECT 1"); + } + } + + @Test(groups = {"integration"}) + public void testCloseOnCompletion() throws Exception { + try (Connection conn = getJdbcConnection();) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT 1")) { + rs.next(); + } + Assert.assertFalse(stmt.isClosed()); + Assert.assertFalse(stmt.isCloseOnCompletion()); + stmt.closeOnCompletion(); + Assert.assertTrue(stmt.isCloseOnCompletion()); + + try (ResultSet rs = stmt.executeQuery("SELECT 1")) { + rs.next(); + } + Assert.assertTrue(stmt.isClosed()); + } + + try (Statement stmt = conn.createStatement()) { + stmt.closeOnCompletion(); + try (ResultSet rs = stmt.executeQuery("CREATE TABLE test_empty_table (id String) Engine Memory")) { + } catch (Exception ex) { + ex.printStackTrace(); + } + + Assert.assertTrue(stmt.isClosed()); + } + } + } + + @Test(groups = {"integration"}) + public void testMaxFieldSize() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + Assert.assertThrows(SQLException.class, () -> stmt.setMaxFieldSize(-1)); + stmt.setMaxFieldSize(300); + Assert.assertEquals(stmt.getMaxFieldSize(), 300); + stmt.setMaxFieldSize(4); + ResultSet rs = stmt.executeQuery("SELECT 'long_string'"); + rs.next(); +// Assert.assertEquals(rs.getString(1).length(), 4); +// Assert.assertEquals(rs.getString(1), "long"); + } + } + + + @Test(groups = {"integration"}) + public void testVariousSimpleMethods() throws Exception { + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + Assert.assertEquals(stmt.getQueryTimeout(), 0); + stmt.setQueryTimeout(100); + Assert.assertEquals(stmt.getQueryTimeout(), 100); + stmt.setFetchSize(100); + Assert.assertEquals(stmt.getFetchSize(), 0); // we ignore this hint + Assert.assertEquals(stmt.getResultSetConcurrency(), ResultSet.CONCUR_READ_ONLY); + Assert.assertEquals(stmt.getResultSetType(), ResultSet.TYPE_FORWARD_ONLY); + Assert.assertNotNull(stmt.getConnection()); + Assert.assertEquals(stmt.getResultSetHoldability(), ResultSet.HOLD_CURSORS_OVER_COMMIT); + assertFalse(stmt.isPoolable()); + stmt.setPoolable(true); + assertTrue(stmt.isPoolable()); + } + } + + @Test(groups = {"integration"}) + public void testExecute() throws Exception { + // This test verifies multi-resultset scenario (we may have only one resultset at a time) + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + // has result set and no update count + Assert.assertTrue(stmt.execute("SELECT 1")); + ResultSet rs = stmt.getResultSet(); + Assert.assertTrue(rs.next()); + assertEquals(rs.getInt(1), 1); + ResultSet rs2 = stmt.getResultSet(); + assertSame(rs, rs2); + Assert.assertFalse(rs.next()); + Assert.assertFalse(rs2.next()); + Assert.assertEquals(stmt.getUpdateCount(), -1); + assertFalse(rs.isClosed()); + Assert.assertFalse(stmt.getMoreResults()); + assertTrue(rs.isClosed()); + Assert.assertNull(stmt.getResultSet()); + } + + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + // no result set and update count + Assert.assertFalse(stmt.execute("CREATE TABLE test_multi_result (id Int32) Engine MergeTree ORDER BY ()")); + Assert.assertNull(stmt.getResultSet()); + Assert.assertEquals(stmt.getUpdateCount(), 0); + Assert.assertFalse(stmt.getMoreResults()); + + // no result set and has update count + Assert.assertFalse(stmt.execute("INSERT INTO test_multi_result VALUES (1), (2), (3)")); + Assert.assertNull(stmt.getResultSet()); + Assert.assertEquals(stmt.getUpdateCount(), 3); + Assert.assertFalse(stmt.getMoreResults()); + Assert.assertEquals(stmt.getUpdateCount(), -1); + } + + // keep current resultset + try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) { + // has result set and no update count + Assert.assertTrue(stmt.execute("SELECT 1")); + ResultSet rs = stmt.getResultSet(); + Assert.assertEquals(stmt.getUpdateCount(), -1); + assertFalse(rs.isClosed()); + Assert.assertFalse(stmt.getMoreResults(Statement.KEEP_CURRENT_RESULT)); + Assert.assertNull(stmt.getResultSet()); + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 1); + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java index fb7d5c1f5..6c13ad365 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java @@ -176,26 +176,6 @@ public void testPreparedStatementInsertSQL() { assertEquals(parsed.getAssignValuesGroups(), 1); } - @Test - public void testUnquoteIdentifier() { - String[] names = new String[]{"test", "`test name1`", "\"test name 2\""}; - String[] expected = new String[]{"test", "test name1", "test name 2"}; - - for (int i = 0; i < names.length; i++) { - assertEquals(SqlParser.unquoteIdentifier(names[i]), expected[i]); - } - } - - @Test - public void testEscapeQuotes() { - String[] inStr = new String[]{"%valid_name%", "' OR 1=1 --", "\" OR 1=1 --"}; - String[] outStr = new String[]{"%valid_name%", "\\' OR 1=1 --", "\\\" OR 1=1 --"}; - - for (int i = 0; i < inStr.length; i++) { - assertEquals(SqlParser.escapeQuotes(inStr[i]), outStr[i]); - } - } - @Test public void testStmtWithCasts() { String sql = "SELECT ?::integer, ?, '?:: integer' FROM table WHERE v = ?::integer"; // CAST(?, INTEGER) diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java index 029202ff2..3c79cbabd 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataTest.java @@ -372,7 +372,12 @@ public void testGetTypeInfo() throws Exception { while (rs.next()) { count++; - ClickHouseDataType dataType = ClickHouseDataType.of( rs.getString("TYPE_NAME")); + ClickHouseDataType dataType; + try { + dataType = ClickHouseDataType.of( rs.getString("TYPE_NAME")); + } catch (Exception e) { + continue; // skip. we have another test and will catch it anyway. + } assertEquals(ClickHouseDataType.of(rs.getString(1)), dataType); assertEquals(rs.getInt("DATA_TYPE"), (int) JdbcUtils.convertToSqlType(dataType).getVendorTypeNumber(), diff --git a/pom.xml b/pom.xml index c2036f1b3..1f8459814 100644 --- a/pom.xml +++ b/pom.xml @@ -713,6 +713,7 @@ jacoco-prepare-it + pre-integration-test prepare-agent @@ -720,30 +721,30 @@ ${project.build.directory}/coverage-reports/jacoco-it.exec - - jacoco-ut - - prepare-agent - - - ${skipUTs} - ${project.build.directory}/coverage-reports/jacoco-ut.exec - surefireArgLine - true - - - - jacoco-it - - prepare-agent-integration - - - ${skipITs} - ${project.build.directory}/coverage-reports/jacoco-it.exec - failsafeArgLine - true - - + + + + + + + + + + + + + + + + + + + + + + + + jacoco-it-report post-integration-test @@ -757,7 +758,7 @@ jacoco-ut-report - prepare-package + test report @@ -1025,23 +1026,6 @@ central - - - - - - - - - - - - - - - - -