diff --git a/README.md b/README.md index 025726e9..d05922a2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ To get the Java connector for Tarantool 1.6.9, visit ## Table of contents * [Getting started](#getting-started) +* [JDBC](#JDBC) * [Cluster support](#cluster-support) * [Where to get help](#where-to-get-help) @@ -170,6 +171,67 @@ System.out.println(template.query("select * from hello_world where hello=:id", C For more implementation details, see [API documentation](http://tarantool.github.io/tarantool-java/apidocs/index.html). +## JDBC + +### Batch updates + +`Statement` and `PreparedStatement` objects can be used to submit batch +updates. + +For instance, using `Statement` object: + +```java +Statement statement = connection.createStatement(); +statement.addBatch("INSERT INTO student VALUES (30, 'Joe Jones')"); +statement.addBatch("INSERT INTO faculty VALUES (2, 'Faculty of Chemistry')"); +statement.addBatch("INSERT INTO student_faculty VALUES (30, 2)"); + +int[] updateCounts = stmt.executeBatch(); +``` + +or using `PreparedStatement`: + +```java +PreparedStatement stmt = con.prepareStatement("INSERT INTO student VALUES (?, ?)"); +stmt.setInt(1, 30); +stmt.setString(2, "Michael Korj"); +stmt.addBatch(); +stmt.setInt(1, 40); +stmt.setString(2, "Linda Simpson"); +stmt.addBatch(); + +int[] updateCounts = stmt.executeBatch(); +``` + +The connector uses a pipeliing when it performs a batch request. It means +each query is asynchronously sent one-by-one in order they were specified +in the batch. + +There are a couple of caveats: + +- JDBC spec recommends that *auto-commit* mode should be turned off +to prevent the driver from committing a transaction when a batch request +is called. The connector is not support transactions and *auto-commit* is +always enabled, so each statement from the batch is executed in its own +transaction. + +- DDL operations aren't transactional in Tarantool. In this way, a batch +like this can produce an undefined behaviour (i.e. second statement can fail +with an error that `student` table does not exist). + +```java +statement.addBatch("CREATE TABLE student (id INT PRIMARY KEY, name VARCHAR(100))"); +statement.addBatch("INSERT INTO student VALUES (1, 'Alex Smith')"); +``` + +- If `vinyl` storage engine is used an execution order of batch statements is +not specified. __NOTE:__ This behaviour is incompatible with JDBC spec in the +sentence "Batch commands are executed serially (at least logically) in the +order in which they were added to the batch" + +- The driver continues processing the remaining commands in a batch once execution +of a command fails. + ## Cluster support To be more fault-tolerant the connector provides cluster extensions. In diff --git a/src/main/java/org/tarantool/jdbc/SQLBatchResultHolder.java b/src/main/java/org/tarantool/jdbc/SQLBatchResultHolder.java new file mode 100644 index 00000000..d34f26f7 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLBatchResultHolder.java @@ -0,0 +1,26 @@ +package org.tarantool.jdbc; + +import java.util.List; + +/** + * Wrapper for batch SQL query results. + */ +public class SQLBatchResultHolder { + + private final List results; + private final Exception error; + + public SQLBatchResultHolder(List results, Exception error) { + this.results = results; + this.error = error; + } + + public List getResults() { + return results; + } + + public Exception getError() { + return error; + } + +} diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index 15333871..0da87a6f 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -32,7 +32,7 @@ import java.sql.Savepoint; import java.sql.Statement; import java.sql.Struct; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -40,7 +40,9 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; +import java.util.function.Function; /** * Tarantool {@link Connection} implementation. @@ -525,20 +527,34 @@ public int getNetworkTimeout() throws SQLException { return (int) client.getOperationTimeout(); } - protected SQLResultHolder execute(long timeout, String sql, Object... args) throws SQLException { + protected SQLResultHolder execute(long timeout, SQLQueryHolder query) throws SQLException { checkNotClosed(); + return (useNetworkTimeout(timeout)) + ? executeWithNetworkTimeout(query) + : executeWithQueryTimeout(timeout, query); + } + + protected SQLBatchResultHolder executeBatch(long timeout, List queries) throws SQLException { + checkNotClosed(); + SQLTarantoolClientImpl.SQLRawOps sqlOps = client.sqlRawOps(); + SQLBatchResultHolder batchResult = useNetworkTimeout(timeout) + ? sqlOps.executeBatch(queries) + : sqlOps.executeBatch(timeout, queries); + + return batchResult; + } + + private boolean useNetworkTimeout(long timeout) throws SQLException { int networkTimeout = getNetworkTimeout(); - return (timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout)) - ? executeWithNetworkTimeout(sql, args) - : executeWithStatementTimeout(timeout, sql, args); + return timeout == 0 || (networkTimeout > 0 && networkTimeout < timeout); } - private SQLResultHolder executeWithNetworkTimeout(String sql, Object... args) throws SQLException { + private SQLResultHolder executeWithNetworkTimeout(SQLQueryHolder query) throws SQLException { try { - return client.sqlRawOps().execute(sql, args); + return client.sqlRawOps().execute(query); } catch (Exception e) { handleException(e); - throw new SQLException(formatError(sql, args), e); + throw new SQLException(formatError(query), e); } } @@ -546,25 +562,24 @@ private SQLResultHolder executeWithNetworkTimeout(String sql, Object... args) th * Executes a query using a custom timeout. * * @param timeout query timeout - * @param sql query - * @param args query bindings + * @param query query * * @return SQL result holder * * @throws StatementTimeoutException if query execution took more than query timeout * @throws SQLException if any other errors occurred */ - private SQLResultHolder executeWithStatementTimeout(long timeout, String sql, Object... args) throws SQLException { + private SQLResultHolder executeWithQueryTimeout(long timeout, SQLQueryHolder query) throws SQLException { try { - return client.sqlRawOps().execute(timeout, sql, args); + return client.sqlRawOps().execute(timeout, query); } catch (Exception e) { // statement timeout should not affect the current connection // but can be handled by the caller side if (e.getCause() instanceof TimeoutException) { - throw new StatementTimeoutException(formatError(sql, args), e.getCause()); + throw new StatementTimeoutException(formatError(query), e.getCause()); } handleException(e); - throw new SQLException(formatError(sql, args), e); + throw new SQLException(formatError(query), e); } } @@ -708,28 +723,74 @@ private void checkHoldabilitySupport(int holdability) throws SQLException { /** * Provides error message that contains parameters of failed SQL statement. * - * @param sql SQL Text. - * @param params Parameters of the SQL statement. + * @param query SQL query * * @return Formatted error message. */ - private static String formatError(String sql, Object... params) { - return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params); + private static String formatError(SQLQueryHolder query) { + return "Failed to execute SQL: " + query.getQuery() + ", params: " + query.getParams(); } static class SQLTarantoolClientImpl extends TarantoolClientImpl { + private Future executeQuery(SQLQueryHolder queryHolder) { + return exec(Code.EXECUTE, Key.SQL_TEXT, queryHolder.getQuery(), Key.SQL_BIND, queryHolder.getParams()); + } + + private Future executeQuery(SQLQueryHolder queryHolder, long timeoutMillis) { + return exec( + timeoutMillis, Code.EXECUTE, Key.SQL_TEXT, queryHolder.getQuery(), Key.SQL_BIND, queryHolder.getParams() + ); + } + final SQLRawOps sqlRawOps = new SQLRawOps() { @Override - public SQLResultHolder execute(String sql, Object... binds) { - return (SQLResultHolder) syncGet(exec(Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, binds)); + public SQLResultHolder execute(SQLQueryHolder query) { + return (SQLResultHolder) syncGet(executeQuery(query)); } @Override - public SQLResultHolder execute(long timeoutMillis, String sql, Object... binds) { - return (SQLResultHolder) syncGet( - exec(timeoutMillis, Code.EXECUTE, Key.SQL_TEXT, sql, Key.SQL_BIND, binds) - ); + public SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query) { + return (SQLResultHolder) syncGet(executeQuery(query, timeoutMillis)); + } + + @Override + public SQLBatchResultHolder executeBatch(List queries) { + return executeInternal(queries, (query) -> executeQuery(query)); + } + + @Override + public SQLBatchResultHolder executeBatch(long timeoutMillis, List queries) { + return executeInternal(queries, (query) -> executeQuery(query, timeoutMillis)); + } + + private SQLBatchResultHolder executeInternal(List queries, + Function> fetcher) { + List> sqlFutures = new ArrayList<>(); + // using queries pipelining to emulate a batch request + for (SQLQueryHolder query : queries) { + sqlFutures.add(fetcher.apply(query)); + } + // wait for all the results + Exception lastError = null; + List items = new ArrayList<>(queries.size()); + for (Future future : sqlFutures) { + try { + SQLResultHolder result = (SQLResultHolder) syncGet(future); + if (result.isQueryResult()) { + lastError = new SQLException( + "Result set is not allowed in the batch response", + SQLStates.TOO_MANY_RESULTS.getSqlState() + ); + } + items.add(result); + } catch (RuntimeException e) { + // empty result set will be treated as a wrong result + items.add(SQLResultHolder.ofEmptyQuery()); + lastError = e; + } + } + return new SQLBatchResultHolder(items, lastError); } }; @@ -758,9 +819,13 @@ protected void completeSql(TarantoolOp future, TarantoolPacket pack) { interface SQLRawOps { - SQLResultHolder execute(String sql, Object... binds); + SQLResultHolder execute(SQLQueryHolder query); + + SQLResultHolder execute(long timeoutMillis, SQLQueryHolder query); + + SQLBatchResultHolder executeBatch(List queries); - SQLResultHolder execute(long timeoutMillis, String sql, Object... binds); + SQLBatchResultHolder executeBatch(long timeoutMillis, List queries); } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index 2c0de985..f59dfaef 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -948,7 +948,7 @@ public boolean insertsAreDetected(int type) throws SQLException { @Override public boolean supportsBatchUpdates() throws SQLException { - return false; + return true; } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 6c167d9c..803ad124 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -22,21 +22,25 @@ import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; +import java.util.List; import java.util.Map; public class SQLPreparedStatement extends SQLStatement implements PreparedStatement { - static final String INVALID_CALL_MSG = "The method cannot be called on a PreparedStatement."; - final String sql; - final Map params; + private static final String INVALID_CALL_MESSAGE = "The method cannot be called on a PreparedStatement."; + private final String sql; + private final Map parameters; + + private List> batchParameters = new ArrayList<>(); public SQLPreparedStatement(SQLConnection connection, String sql) throws SQLException { super(connection); this.sql = sql; - this.params = new HashMap<>(); + this.parameters = new HashMap<>(); } public SQLPreparedStatement(SQLConnection connection, @@ -46,13 +50,13 @@ public SQLPreparedStatement(SQLConnection connection, int resultSetHoldability) throws SQLException { super(connection, resultSetType, resultSetConcurrency, resultSetHoldability); this.sql = sql; - this.params = new HashMap<>(); + this.parameters = new HashMap<>(); } @Override public ResultSet executeQuery() throws SQLException { checkNotClosed(); - if (!executeInternal(sql, getParams())) { + if (!executeInternal(sql, toParametersList(parameters))) { throw new SQLException("No results were returned", SQLStates.NO_DATA.getSqlState()); } return resultSet; @@ -60,25 +64,14 @@ public ResultSet executeQuery() throws SQLException { @Override public ResultSet executeQuery(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); - } - - protected Object[] getParams() throws SQLException { - Object[] objects = new Object[params.size()]; - for (int i = 1; i <= params.size(); i++) { - if (params.containsKey(i)) { - objects[i - 1] = params.get(i); - } else { - throw new SQLException("Parameter " + i + " is missing"); - } - } - return objects; + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); } @Override public int executeUpdate() throws SQLException { checkNotClosed(); - if (executeInternal(sql, getParams())) { + if (executeInternal(sql, toParametersList(parameters))) { throw new SQLException( "Result was returned but nothing was expected", SQLStates.TOO_MANY_RESULTS.getSqlState() @@ -89,7 +82,8 @@ public int executeUpdate() throws SQLException { @Override public int executeUpdate(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); } @Override @@ -219,7 +213,7 @@ public void setBinaryStream(int parameterIndex, InputStream x) throws SQLExcepti @Override public void clearParameters() throws SQLException { - params.clear(); + parameters.clear(); } @Override @@ -242,18 +236,19 @@ public void setObject(int parameterIndex, private void setParameter(int parameterIndex, Object value) throws SQLException { checkNotClosed(); - params.put(parameterIndex, value); + parameters.put(parameterIndex, value); } @Override public boolean execute() throws SQLException { checkNotClosed(); - return executeInternal(sql, getParams()); + return executeInternal(sql, toParametersList(parameters)); } @Override public boolean execute(String sql) throws SQLException { - throw new SQLException(INVALID_CALL_MSG); + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); } @Override @@ -374,22 +369,48 @@ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException @Override public void addBatch(String sql) throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + throw new SQLException(INVALID_CALL_MESSAGE); } @Override public void addBatch() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + // shadow copy of the current parameters + batchParameters.add(new HashMap<>(parameters)); } @Override public int[] executeBatch() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + try { + List queries = new ArrayList<>(); + for (Map p : batchParameters) { + SQLQueryHolder of = SQLQueryHolder.of(sql, toParametersList(p)); + queries.add(of); + } + return executeBatchInternal(queries); + } finally { + batchParameters.clear(); + } } @Override public void clearBatch() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + batchParameters.clear(); + } + + private Object[] toParametersList(Map parameters) throws SQLException { + Object[] objects = new Object[parameters.size()]; + for (int i = 1; i <= parameters.size(); i++) { + if (parameters.containsKey(i)) { + objects[i - 1] = parameters.get(i); + } else { + throw new SQLException("Parameter " + i + " is missing"); + } + } + return objects; } } diff --git a/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java b/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java new file mode 100644 index 00000000..e02e1ea7 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/SQLQueryHolder.java @@ -0,0 +1,28 @@ +package org.tarantool.jdbc; + +import java.util.Arrays; +import java.util.List; + +public class SQLQueryHolder { + + private final String query; + private final List params; + + public static SQLQueryHolder of(String query, Object... params) { + return new SQLQueryHolder(query, Arrays.asList(params)); + } + + private SQLQueryHolder(String query, List params) { + this.query = query; + this.params = params; + } + + public String getQuery() { + return query; + } + + public List getParams() { + return params; + } + +} diff --git a/src/main/java/org/tarantool/jdbc/SQLResultHolder.java b/src/main/java/org/tarantool/jdbc/SQLResultHolder.java index 04c7eba3..9298cae1 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultHolder.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultHolder.java @@ -11,9 +11,11 @@ */ public class SQLResultHolder { - final List sqlMetadata; - final List> rows; - final int updateCount; + public static final int NO_UPDATE_COUNT = -1; + + private final List sqlMetadata; + private final List> rows; + private final int updateCount; public SQLResultHolder(List sqlMetadata, List> rows, int updateCount) { this.sqlMetadata = sqlMetadata; @@ -23,7 +25,7 @@ public SQLResultHolder(List sqlMetadata, List sqlMetadata, final List> rows) { - return new SQLResultHolder(sqlMetadata, rows, -1); + return new SQLResultHolder(sqlMetadata, rows, NO_UPDATE_COUNT); } public static SQLResultHolder ofEmptyQuery() { diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index 7aa6c0cb..16300b00 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -3,6 +3,7 @@ import org.tarantool.util.JdbcConstants; import org.tarantool.util.SQLStates; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -11,8 +12,11 @@ import java.sql.SQLTimeoutException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; /** * Tarantool {@link Statement} implementation. @@ -31,6 +35,8 @@ public class SQLStatement implements TarantoolStatement { protected SQLResultSet resultSet; protected int updateCount; + private List batchQueries = new ArrayList<>(); + private boolean isCloseOnCompletion; private final int resultSetType; @@ -275,17 +281,28 @@ public int getResultSetType() throws SQLException { @Override public void addBatch(String sql) throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + batchQueries.add(sql); } @Override public void clearBatch() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + batchQueries.clear(); } @Override public int[] executeBatch() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + discardLastResults(); + try { + List queries = batchQueries.stream() + .map(q -> SQLQueryHolder.of(q)) + .collect(Collectors.toList()); + return executeBatchInternal(queries); + } finally { + batchQueries.clear(); + } } @Override @@ -329,7 +346,7 @@ public boolean isPoolable() throws SQLException { * explicitly by the app. * * @throws SQLException if this method is called on a closed - * {@code Statement} + * {@code Statement} */ @Override public void closeOnCompletion() throws SQLException { @@ -396,7 +413,7 @@ protected boolean executeInternal(String sql, Object... params) throws SQLExcept discardLastResults(); SQLResultHolder holder; try { - holder = connection.execute(timeout, sql, params); + holder = connection.execute(timeout, SQLQueryHolder.of(sql, params)); } catch (StatementTimeoutException e) { cancel(); throw new SQLTimeoutException(); @@ -409,6 +426,28 @@ protected boolean executeInternal(String sql, Object... params) throws SQLExcept return holder.isQueryResult(); } + /** + * Performs batch query execution. + * + * @param queries batch queries + * + * @return update count result per query + */ + protected int[] executeBatchInternal(List queries) throws SQLException { + SQLBatchResultHolder batchResult = connection.executeBatch(timeout, queries); + int[] resultCounts = batchResult.getResults().stream() + .mapToInt(result -> result.isQueryResult() + ? Statement.EXECUTE_FAILED : result.getUpdateCount() == SQLResultHolder.NO_UPDATE_COUNT + ? Statement.SUCCESS_NO_INFO : result.getUpdateCount() + ).toArray(); + + if (batchResult.getError() != null) { + throw new BatchUpdateException(resultCounts, batchResult.getError()); + } + + return resultCounts; + } + @Override public ResultSet executeMetadata(SQLResultHolder data) throws SQLException { checkNotClosed(); diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index 08c02d96..acba1a57 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -151,17 +152,12 @@ private void checkStatementCommunicationException(final ThrowingConsumer consumer.accept(stmt)); assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); assertEquals(ex, e.getCause()); @@ -173,7 +169,7 @@ private void checkPreparedStatementCommunicationException(final ThrowingConsumer throws SQLException { Exception ex = new CommunicationException("TEST"); SQLTarantoolClientImpl.SQLRawOps sqlOps = mock(SQLTarantoolClientImpl.SQLRawOps.class); - doThrow(ex).when(sqlOps).execute("TEST"); + doThrow(ex).when(sqlOps).execute(anyObject()); SQLTarantoolClientImpl client = buildSQLClient(sqlOps, null); final PreparedStatement prep = new SQLPreparedStatement( diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index 148e6425..1cc268fb 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -322,6 +323,160 @@ public void testMoreResultsButKeepCurrent() throws SQLException { assertEquals(-1, prep.getUpdateCount()); } + @Test + public void testExecuteOneBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + + prep.setInt(1, 1); + prep.setString(2, "one"); + prep.addBatch(); + + int[] updateCounts = prep.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + + assertEquals("one", consoleSelect(1).get(1)); + } + + @Test + public void testExecuteZeroBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (1, 'one')"); + int[] updateCounts = prep.executeBatch(); + assertEquals(0, updateCounts.length); + } + + @Test + public void testExecuteBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + + prep.setInt(1, 1); + prep.setString(2, "one"); + prep.addBatch(); + + prep.setInt(1, 2); + prep.setString(2, "two"); + prep.addBatch(); + + prep.setInt(1, 3); + prep.setString(2, "three"); + prep.addBatch(); + + int[] updateCounts = prep.executeBatch(); + assertEquals(3, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(1, updateCounts[1]); + assertEquals(1, updateCounts[2]); + + assertEquals("one", consoleSelect(1).get(1)); + assertEquals("two", consoleSelect(2).get(1)); + assertEquals("three", consoleSelect(3).get(1)); + } + + @Test + public void testExecuteMultiBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?), (?, ?)"); + + prep.setInt(1, 1); + prep.setString(2, "one"); + prep.setInt(3, 2); + prep.setString(4, "two"); + prep.addBatch(); + + prep.setInt(1, 3); + prep.setString(2, "three"); + prep.setInt(3, 4); + prep.setString(4, "four"); + prep.addBatch(); + + int[] updateCounts = prep.executeBatch(); + assertEquals(2, updateCounts.length); + assertEquals(2, updateCounts[0]); + assertEquals(2, updateCounts[1]); + + assertEquals("one", consoleSelect(1).get(1)); + assertEquals("two", consoleSelect(2).get(1)); + assertEquals("three", consoleSelect(3).get(1)); + assertEquals("four", consoleSelect(4).get(1)); + } + + @Test + public void testClearBatch() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + + prep.setInt(1, 1); + prep.setString(2, "one"); + prep.addBatch(); + + prep.setInt(1, 2); + prep.setString(2, "two"); + prep.addBatch(); + + prep.clearBatch(); + + int[] updateCounts = prep.executeBatch(); + assertEquals(0, updateCounts.length); + } + + @Test + public void testExecuteZeroCountsBatchQuery() throws Exception { + testHelper.executeSql("INSERT INTO test(id, val) VALUES (1, 'one')"); + + prep = conn.prepareStatement("DELETE FROM test WHERE id = ?"); + + prep.setInt(1, 1); + prep.addBatch(); + + prep.setInt(1, 2); + prep.addBatch(); + + int[] updateCounts = prep.executeBatch(); + assertEquals(2, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(0, updateCounts[1]); + } + + @Test + public void testExecuteFailedBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + + prep.setInt(1, 6); + prep.setString(2, "six"); + prep.addBatch(); + + prep.setInt(1, 6); + prep.setString(2, "six"); + prep.addBatch(); + + prep.setInt(1, 9); + prep.setString(2, "nine"); + prep.addBatch(); + + BatchUpdateException exception = assertThrows(BatchUpdateException.class, () -> prep.executeBatch()); + int[] updateCounts = exception.getUpdateCounts(); + assertEquals(3, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(Statement.EXECUTE_FAILED, updateCounts[1]); + assertEquals(1, updateCounts[2]); + + assertEquals("six", consoleSelect(6).get(1)); + assertEquals("nine", consoleSelect(9).get(1)); + } + + @Test + public void testExecuteResultSetBatchQuery() throws Exception { + prep = conn.prepareStatement("SELECT * FROM test WHERE id > ?"); + prep.setInt(1, 0); + prep.addBatch(); + + assertThrows(SQLException.class, () -> prep.executeBatch()); + } + + @Test + public void testExecuteStringBatchQuery() throws Exception { + prep = conn.prepareStatement("INSERT INTO test(id, val) VALUES (?, ?)"); + assertThrows(SQLException.class, () -> prep.addBatch("INSERT INTO test(id, val) VALUES (1, 'one')")); + } + private List consoleSelect(Object key) { List list = testHelper.evaluate(TestUtils.toLuaSelect("TEST", key)); return list == null ? Collections.emptyList() : (List) list.get(0); diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 16080204..1e7f0895 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; @@ -421,6 +422,111 @@ public void testMoreResultsButKeepCurrent() throws SQLException { assertEquals(-1, stmt.getUpdateCount()); } + @Test + public void testExecuteOneBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (1, 'one')"); + int[] updateCounts = stmt.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + + assertEquals("one", consoleSelect(1).get(1)); + } + + @Test + public void testExecuteZeroBatchQuery() throws Exception { + int[] updateCounts = stmt.executeBatch(); + assertEquals(0, updateCounts.length); + } + + @Test + public void testExecuteBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (1, 'one')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (2, 'two')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (3, 'three'), (4, 'four')"); + stmt.addBatch("DELETE FROM test WHERE id > 1"); + + int[] updateCounts = stmt.executeBatch(); + assertEquals(4, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(1, updateCounts[1]); + assertEquals(2, updateCounts[2]); + assertEquals(3, updateCounts[3]); + + assertEquals("one", consoleSelect(1).get(1)); + } + + @Test + public void testClearBatch() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (1, 'one')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (2, 'two')"); + stmt.clearBatch(); + int[] updateCounts = stmt.executeBatch(); + assertEquals(0, updateCounts.length); + } + + @Test + public void testExecuteZeroCountsBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (50, 'fifty')"); + stmt.addBatch("DELETE FROM test WHERE id > 100"); + int[] updateCounts = stmt.executeBatch(); + assertEquals(2, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(0, updateCounts[1]); + + assertEquals("fifty", consoleSelect(50).get(1)); + } + + @Test + public void testExecuteMixedBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (5, 'five')"); + stmt.addBatch("DELETE FROM test WHERE id = 5"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (5, 'five')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (6, 'six')"); + + int[] updateCounts = stmt.executeBatch(); + assertEquals(4, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(1, updateCounts[1]); + assertEquals(1, updateCounts[2]); + assertEquals(1, updateCounts[3]); + } + + @Test + public void testExecuteFailedBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (5, 'five')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (5, 'five')"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (6, 'six')"); + + BatchUpdateException exception = assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + int[] updateCounts = exception.getUpdateCounts(); + assertEquals(3, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(Statement.EXECUTE_FAILED, updateCounts[1]); + assertEquals(1, updateCounts[2]); + + assertEquals("five", consoleSelect(5).get(1)); + assertEquals("six", consoleSelect(6).get(1)); + } + + @Test + public void testExecuteResultSetBatchQuery() throws Exception { + stmt.addBatch("INSERT INTO test(id, val) VALUES (5, 'five')"); + stmt.addBatch("SELECT * FROM test WHERE id = 5"); + stmt.addBatch("INSERT INTO test(id, val) VALUES (6, 'six')"); + stmt.addBatch("SELECT id FROM test WHERE id = 8"); + + BatchUpdateException exception = assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); + int[] updateCounts = exception.getUpdateCounts(); + assertEquals(4, updateCounts.length); + assertEquals(1, updateCounts[0]); + assertEquals(Statement.EXECUTE_FAILED, updateCounts[1]); + assertEquals(1, updateCounts[2]); + assertEquals(Statement.EXECUTE_FAILED, updateCounts[3]); + + assertEquals("five", consoleSelect(5).get(1)); + assertEquals("six", consoleSelect(6).get(1)); + } + private List consoleSelect(Object key) { List list = testHelper.evaluate(TestUtils.toLuaSelect("TEST", key)); return list == null ? Collections.emptyList() : (List) list.get(0);