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 26a159f42..e26856363 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -1,6 +1,7 @@ package com.clickhouse.jdbc; import com.clickhouse.client.api.metadata.TableSchema; +import com.clickhouse.client.api.query.QuerySettings; import com.clickhouse.data.Tuple; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; @@ -101,13 +102,14 @@ private String compileSql(String []segments) { @Override public ResultSet executeQuery() throws SQLException { checkClosed(); - return executeQuery(compileSql(sqlSegments)); + return super.executeQueryImpl(compileSql(sqlSegments), new QuerySettings().setDatabase(connection.getSchema())); } @Override public int executeUpdate() throws SQLException { checkClosed(); - return executeUpdate(compileSql(sqlSegments)); + return super.executeUpdateImpl(compileSql(sqlSegments), statementType, + new QuerySettings().setDatabase(connection.getSchema())); } @Override @@ -234,7 +236,8 @@ public void setObject(int parameterIndex, Object x) throws SQLException { @Override public boolean execute() throws SQLException { checkClosed(); - return execute(compileSql(sqlSegments)); + return super.executeImpl(compileSql(sqlSegments), statementType, + new QuerySettings().setDatabase(connection.getSchema())); } @Override @@ -242,9 +245,9 @@ public void addBatch() throws SQLException { checkClosed(); if (statementType == StatementType.INSERT) { // adding values to the end of big INSERT statement. - addBatch(compileSql(valueSegments)); + super.addBatch(compileSql(valueSegments)); } else { - addBatch(compileSql(sqlSegments)); + super.addBatch(compileSql(sqlSegments)); } } @@ -259,7 +262,8 @@ public int[] executeBatch() throws SQLException { sb.append(sql).append(","); } sb.setCharAt(sb.length() - 1, ';'); - int rowsInserted = executeUpdate(sb.toString()); + int rowsInserted = executeUpdateImpl(sb.toString(), statementType, + new QuerySettings().setDatabase(connection.getSchema())); // clear batch and re-add insert into int[] results = new int[batch.size()]; if (rowsInserted == batch.size()) { @@ -274,18 +278,22 @@ public int[] executeBatch() throws SQLException { return results; } else { // run executeBatch - return super.executeBatch(); + return executeBatchImpl().stream().mapToInt(Integer::intValue).toArray(); } } @Override public long[] executeLargeBatch() throws SQLException { - int[] results = executeBatch(); - long[] longResults = new long[results.length]; - for (int i = 0; i < results.length; i++) { - longResults[i] = results[i]; + return executeBatchImpl().stream().mapToLong(Integer::longValue).toArray(); + } + + private List executeBatchImpl() throws SQLException { + List results = new ArrayList<>(); + QuerySettings settings = new QuerySettings().setDatabase(connection.getSchema()); + for (String sql : batch) { + results.add(executeUpdateImpl(sql, statementType, settings)); } - return longResults; + return results; } @Override @@ -412,7 +420,8 @@ public ParameterMetaData getParameterMetaData() throws SQLException { @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + throw new SQLException("ROWID type is not supported by ClickHouse.", + ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED); } @Override @@ -540,6 +549,118 @@ public long executeLargeUpdate() throws SQLException { return executeUpdate(); } + @Override + public final void addBatch(String sql) throws SQLException { + checkClosed(); + throw new SQLException( + "addBatch(String) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final boolean execute(String sql) throws SQLException { + checkClosed(); + throw new SQLException( + "execute(String) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + checkClosed(); + throw new SQLException( + "execute(String, int) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final boolean execute(String sql, int[] columnIndexes) throws SQLException { + checkClosed(); + throw new SQLException( + "execute(String, int[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final boolean execute(String sql, String[] columnNames) throws SQLException { + checkClosed(); + throw new SQLException( + "execute(String, String[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final long executeLargeUpdate(String sql) throws SQLException { + checkClosed(); + throw new SQLException( + "executeLargeUpdate(String) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkClosed(); + throw new SQLException( + "executeLargeUpdate(String, int) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkClosed(); + throw new SQLException( + "executeLargeUpdate(String, int[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + checkClosed(); + throw new SQLException( + "executeLargeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final ResultSet executeQuery(String sql) throws SQLException { + checkClosed(); + throw new SQLException( + "executeQuery(String) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final int executeUpdate(String sql) throws SQLException { + checkClosed(); + throw new SQLException( + "executeUpdate(String) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkClosed(); + throw new SQLException( + "executeUpdate(String, int) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkClosed(); + throw new SQLException( + "executeUpdate(String, int[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + + @Override + public final int executeUpdate(String sql, String[] columnNames) throws SQLException { + checkClosed(); + throw new SQLException( + "executeUpdate(String, String[]) cannot be called in PreparedStatement or CallableStatement!", + ExceptionUtils.SQL_STATE_WRONG_OBJECT_TYPE); + } + private static String encodeObject(Object x) throws SQLException { LOG.trace("Encoding object: {}", x); 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 ce4bcf013..85c271b8d 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -56,7 +56,7 @@ protected void checkClosed() throws SQLException { } } - protected enum StatementType { + public enum StatementType { SELECT, INSERT, DELETE, UPDATE, CREATE, DROP, ALTER, TRUNCATE, USE, SHOW, DESCRIBE, EXPLAIN, SET, KILL, OTHER, INSERT_INTO_SELECT } @@ -148,7 +148,7 @@ protected String getLastSql() { @Override public ResultSet executeQuery(String sql) throws SQLException { checkClosed(); - return executeQuery(sql, new QuerySettings().setDatabase(schema)); + return executeQueryImpl(sql, new QuerySettings().setDatabase(schema)); } private void closePreviousResultSet() { @@ -165,7 +165,7 @@ private void closePreviousResultSet() { } } - public ResultSetImpl executeQuery(String sql, QuerySettings settings) throws SQLException { + public ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) throws SQLException { checkClosed(); // Closing before trying to do next request. Otherwise, deadlock because previous connection will not be // release before this one completes. @@ -213,13 +213,12 @@ public ResultSetImpl executeQuery(String sql, QuerySettings settings) throws SQL @Override public int executeUpdate(String sql) throws SQLException { checkClosed(); - return executeUpdate(sql, new QuerySettings().setDatabase(schema)); + return executeUpdateImpl(sql, parseStatementType(sql), new QuerySettings().setDatabase(schema)); } - public int executeUpdate(String sql, QuerySettings settings) throws SQLException { - // TODO: close current result set? + protected int executeUpdateImpl(String sql, StatementType type, QuerySettings settings) throws SQLException { checkClosed(); - StatementType type = parseStatementType(sql); + if (type == StatementType.SELECT || type == StatementType.SHOW || type == StatementType.DESCRIBE || type == StatementType.EXPLAIN) { throw new SQLException("executeUpdate() cannot be called with a SELECT/SHOW/DESCRIBE/EXPLAIN statement", ExceptionUtils.SQL_STATE_SQL_ERROR); } @@ -344,18 +343,16 @@ public void setCursorName(String name) throws SQLException { @Override public boolean execute(String sql) throws SQLException { checkClosed(); - return execute(sql, new QuerySettings().setDatabase(schema)); + return executeImpl(sql, parseStatementType(sql), new QuerySettings().setDatabase(schema)); } - public boolean execute(String sql, QuerySettings settings) throws SQLException { + public boolean executeImpl(String sql, StatementType type, QuerySettings settings) throws SQLException { checkClosed(); - StatementType type = parseStatementType(sql); - if (type == StatementType.SELECT || type == StatementType.SHOW || type == StatementType.DESCRIBE || type == StatementType.EXPLAIN) { - executeQuery(sql, settings); // keep open to allow getResultSet() + executeQueryImpl(sql, settings); // keep open to allow getResultSet() return true; } else if(type == StatementType.SET) { - executeUpdate(sql, settings); + executeUpdateImpl(sql, type, settings); //SET ROLE List tokens = JdbcUtils.tokenizeSQL(sql); if (JdbcUtils.containsIgnoresCase(tokens, "ROLE")) { @@ -379,14 +376,14 @@ public boolean execute(String sql, QuerySettings settings) throws SQLException { } return false; } else if (type == StatementType.USE) { - executeUpdate(sql, settings); + executeUpdateImpl(sql, type, settings); //USE Database List tokens = JdbcUtils.tokenizeSQL(sql); this.schema = tokens.get(1).replace("\"", ""); LOG.debug("Changed statement schema {}", schema); return false; } else { - executeUpdate(sql, settings); + executeUpdateImpl(sql, type, settings); return false; } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ExceptionUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ExceptionUtils.java index 97cd32564..b3a64fe4e 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ExceptionUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ExceptionUtils.java @@ -24,7 +24,10 @@ public final class ExceptionUtils { public static final String SQL_STATE_INVALID_SCHEMA = "3F000"; public static final String SQL_STATE_INVALID_TX_STATE = "25000"; public static final String SQL_STATE_DATA_EXCEPTION = "22000"; + // Used only when feature is not supported public static final String SQL_STATE_FEATURE_NOT_SUPPORTED = "0A000"; + // Used only when method is called on wrong object type (for example, PreparedStatement.addBatch(String)) + public static final String SQL_STATE_WRONG_OBJECT_TYPE = "42809"; private ExceptionUtils() {}//Private constructor diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 646efc0df..f48408ea0 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc; +import com.clickhouse.client.api.query.QuerySettings; import org.apache.commons.lang3.RandomStringUtils; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -11,6 +12,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.*; @@ -628,4 +630,39 @@ void testWriteCollection() throws Exception { } } + + @Test + void testMethodsNotAllowedToBeCalled() throws Exception { + /* Story About Broken API + * There is a Statement interface. It is designed to operate with single statements. + * So there are method like execute(String) and addBatch(String). + * Some statements may be repeated over and over again. And they should be constructed + * over and over again. PreparedStatement was created to solve the issue by accepting + * an SQL statement as constructor parameter and making its method work in context of + * one, prepared SQL statement. + * But someone missed their OOP classes and done this: + * "interface PreparedStatement extends Statement" + * and + * declared some method from Statement interface not to be called on PreparedStatement + * instances. + * That is how today we have a great confusion and have to check it in all implementations. + */ + String sql = "SELECT number FROM system.numbers WHERE number = ?"; + try (Connection conn = getJdbcConnection(); + PreparedStatementImpl ps = (PreparedStatementImpl) conn.prepareStatement(sql)) { + + Assert.assertThrows(SQLException.class, () -> ps.addBatch(sql)); + Assert.assertThrows(SQLException.class, () -> ps.executeQuery(sql)); + Assert.assertThrows(SQLException.class, () -> ps.executeQueryImpl(sql, null)); + Assert.assertThrows(SQLException.class, () -> ps.execute(sql)); + Assert.assertThrows(SQLException.class, () -> ps.execute(sql, new int[]{0})); + Assert.assertThrows(SQLException.class, () -> ps.execute(sql, new String[]{""})); + Assert.assertThrows(SQLException.class, () -> ps.executeUpdate(sql)); + Assert.assertThrows(SQLException.class, () -> ps.executeUpdate(sql, new int[]{0})); + Assert.assertThrows(SQLException.class, () -> ps.executeUpdate(sql, new String[]{""})); + Assert.assertThrows(SQLException.class, () -> ps.executeLargeUpdate(sql)); + Assert.assertThrows(SQLException.class, () -> ps.executeLargeUpdate(sql, new int[]{0})); + Assert.assertThrows(SQLException.class, () -> ps.executeLargeUpdate(sql, new String[]{""})); + } + } }