From 1d2c9043a8814d2f54c2f0c6ab38cbf57b32564e Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Fri, 5 Jul 2019 21:26:49 +0700 Subject: [PATCH] JDBC batch updates Add support for JDBC batch updates. It includes an implementation of Statement.*Batch(...) as well as PreparedStatement.*Batch() methods. Under the hood SQLConnection uses the pipelining sending requests one by one asynchronously and awaiting all of them. There are some issues regarding vinyl storage engine where execution order are not specified and DDL statements which are not transactional. Closes: #62 --- README.md | 62 +++++++ .../tarantool/jdbc/SQLBatchResultHolder.java | 26 +++ .../org/tarantool/jdbc/SQLConnection.java | 117 ++++++++++--- .../tarantool/jdbc/SQLDatabaseMetadata.java | 2 +- .../tarantool/jdbc/SQLPreparedStatement.java | 79 +++++---- .../org/tarantool/jdbc/SQLQueryHolder.java | 28 ++++ .../org/tarantool/jdbc/SQLResultHolder.java | 10 +- .../java/org/tarantool/jdbc/SQLStatement.java | 49 +++++- .../jdbc/JdbcExceptionHandlingTest.java | 12 +- .../jdbc/JdbcPreparedStatementIT.java | 155 ++++++++++++++++++ .../org/tarantool/jdbc/JdbcStatementIT.java | 106 ++++++++++++ 11 files changed, 573 insertions(+), 73 deletions(-) create mode 100644 src/main/java/org/tarantool/jdbc/SQLBatchResultHolder.java create mode 100644 src/main/java/org/tarantool/jdbc/SQLQueryHolder.java 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);