From 762ce2fff44bd08b95358bfed850b3fc15423fa3 Mon Sep 17 00:00:00 2001 From: nicktorwald Date: Mon, 13 May 2019 01:37:10 +0700 Subject: [PATCH] Support Statement.closeOnCompletion. Check a statement after all its dependent result set are closed. Extract TarantoolStatement as tarantool specific extension interface to be used for internal purposes (incompatible vendor API). Closes: #180 --- .../tarantool/jdbc/SQLDatabaseMetadata.java | 6 +- .../java/org/tarantool/jdbc/SQLResultSet.java | 20 ++-- .../java/org/tarantool/jdbc/SQLStatement.java | 51 ++++++--- .../tarantool/jdbc/TarantoolStatement.java | 30 ++++++ .../TarantoolConnectionSQLOpsIT.java | 4 +- .../jdbc/JdbcConnectionTimeoutIT.java | 9 +- .../org/tarantool/jdbc/JdbcResultSetIT.java | 6 +- .../org/tarantool/jdbc/JdbcStatementIT.java | 101 ++++++++++++++++++ .../tarantool/jdbc/ds/JdbcDataSourceIT.java | 19 +++- 9 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/tarantool/jdbc/TarantoolStatement.java diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index ee7e8710..b3b0d024 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -1126,8 +1126,8 @@ private SQLNullResultSet sqlNullResultSet(List columnNames, List T ensureType(Class cls, Object v) throws Exception { @@ -1152,7 +1152,7 @@ private SQLNullResultSet emptyResultSet(List colNames) throws SQLExcepti protected class SQLNullResultSet extends SQLResultSet { - public SQLNullResultSet(SQLResultHolder holder, SQLStatement ownerStatement) throws SQLException { + public SQLNullResultSet(SQLResultHolder holder, TarantoolStatement ownerStatement) throws SQLException { super(holder, ownerStatement); } diff --git a/src/main/java/org/tarantool/jdbc/SQLResultSet.java b/src/main/java/org/tarantool/jdbc/SQLResultSet.java index f707da7b..2771965f 100644 --- a/src/main/java/org/tarantool/jdbc/SQLResultSet.java +++ b/src/main/java/org/tarantool/jdbc/SQLResultSet.java @@ -38,13 +38,13 @@ public class SQLResultSet implements ResultSet { - private final CursorIterator> iterator; - private final SQLResultSetMetaData metaData; + private CursorIterator> iterator; + private SQLResultSetMetaData metaData; private Map columnByNameLookups; private boolean lastColumnWasNull; - private final Statement statement; + private final TarantoolStatement statement; private final int maxRows; private AtomicBoolean isClosed = new AtomicBoolean(false); @@ -53,7 +53,7 @@ public class SQLResultSet implements ResultSet { private final int concurrencyLevel; private final int holdability; - public SQLResultSet(SQLResultHolder holder, SQLStatement ownerStatement) throws SQLException { + public SQLResultSet(SQLResultHolder holder, TarantoolStatement ownerStatement) throws SQLException { metaData = new SQLResultSetMetaData(holder.getSqlMetadata()); statement = ownerStatement; scrollType = statement.getResultSetType(); @@ -108,7 +108,13 @@ protected Number getNullableNumber(int columnIndex) throws SQLException { @Override public void close() throws SQLException { if (isClosed.compareAndSet(false, true)) { - iterator.close(); + try { + iterator.close(); + iterator = null; + metaData = null; + } finally { + statement.checkCompletion(); + } } } @@ -393,11 +399,13 @@ public String getCursorName() throws SQLException { @Override public ResultSetMetaData getMetaData() throws SQLException { + checkNotClosed(); return metaData; } @Override public int findColumn(String columnLabel) throws SQLException { + checkNotClosed(); return findColumnIndex(columnLabel); } @@ -1122,7 +1130,7 @@ public T unwrap(Class type) throws SQLException { if (isWrapperFor(type)) { return type.cast(this); } - throw new SQLNonTransientException("ResultSet does not wrap " + type.getName()); + throw new SQLNonTransientException("SQLResultSet does not wrap " + type.getName()); } @Override diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index f5ae17d3..142c3f60 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -21,7 +21,7 @@ * types of cursors. * Supports only {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} holdability type. */ -public class SQLStatement implements Statement { +public class SQLStatement implements TarantoolStatement { protected final SQLConnection connection; @@ -31,6 +31,8 @@ public class SQLStatement implements Statement { protected SQLResultSet resultSet; protected int updateCount; + private boolean isCloseOnCompletion; + private final int resultSetType; private final int resultSetConcurrency; private final int resultSetHoldability; @@ -310,15 +312,36 @@ public boolean isPoolable() throws SQLException { throw new SQLFeatureNotSupportedException(); } + /** + * {@inheritDoc} + *

+ * Impl Note: this method doesn't affect + * execution methods which close the last result set implicitly. + * It is applied only when {@link ResultSet#close()} is invoked + * explicitly by the app. + * + * @throws SQLException if this method is called on a closed + * {@code Statement} + */ @Override public void closeOnCompletion() throws SQLException { - + checkNotClosed(); + isCloseOnCompletion = true; } @Override public boolean isCloseOnCompletion() throws SQLException { checkNotClosed(); - return false; + return isCloseOnCompletion; + } + + @Override + public void checkCompletion() throws SQLException { + if (isCloseOnCompletion && + resultSet != null && + resultSet.isClosed()) { + close(); + } } @Override @@ -326,7 +349,7 @@ public T unwrap(Class type) throws SQLException { if (isWrapperFor(type)) { return type.cast(this); } - throw new SQLNonTransientException("Statement does not wrap " + type.getName()); + throw new SQLNonTransientException("SQLStatement does not wrap " + type.getName()); } @Override @@ -338,15 +361,18 @@ public boolean isWrapperFor(Class type) throws SQLException { * Clears the results of the most recent execution. */ protected void discardLastResults() throws SQLException { + final SQLResultSet lastResultSet = resultSet; + clearWarnings(); updateCount = -1; - if (resultSet != null) { + resultSet = null; + + if (lastResultSet != null) { try { - resultSet.close(); + lastResultSet.close(); } catch (Exception ignored) { // No-op. } - resultSet = null; } } @@ -375,16 +401,7 @@ protected boolean executeInternal(String sql, Object... params) throws SQLExcept return holder.isQueryResult(); } - /** - * Returns {@link ResultSet} which will be initialized by data. - * - * @param data predefined result to be wrapped by {@link ResultSet} - * - * @return wrapped result - * - * @throws SQLException if a database access error occurs or - * this method is called on a closed Statement - */ + @Override public ResultSet executeMetadata(SQLResultHolder data) throws SQLException { checkNotClosed(); return createResultSet(data); diff --git a/src/main/java/org/tarantool/jdbc/TarantoolStatement.java b/src/main/java/org/tarantool/jdbc/TarantoolStatement.java new file mode 100644 index 00000000..879a03d6 --- /dev/null +++ b/src/main/java/org/tarantool/jdbc/TarantoolStatement.java @@ -0,0 +1,30 @@ +package org.tarantool.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Tarantool specific statement extensions. + */ +public interface TarantoolStatement extends Statement { + + /** + * Checks for statement completion and closes itself, + * according to {@link Statement#closeOnCompletion()}. + */ + void checkCompletion() throws SQLException; + + /** + * Returns {@link ResultSet} which will be initialized by data. + * + * @param data predefined result to be wrapped by {@link ResultSet} + * + * @return wrapped result + * + * @throws SQLException if a database access error occurs or + * this method is called on a closed Statement + */ + ResultSet executeMetadata(SQLResultHolder data) throws SQLException; + +} diff --git a/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java b/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java index 102959cc..dcc6b388 100644 --- a/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java +++ b/src/test/java/org/tarantool/TarantoolConnectionSQLOpsIT.java @@ -22,7 +22,9 @@ public void setup() { @AfterEach public void tearDown() { - connection.close(); + if (connection != null) { + connection.close(); + } } @Override diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java index 6c031abe..a554b95f 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionTimeoutIT.java @@ -3,9 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; import static org.tarantool.TestUtils.makeInstanceEnv; +import org.tarantool.ServerVersion; import org.tarantool.TarantoolClientConfig; +import org.tarantool.TarantoolConsole; import org.tarantool.TarantoolControl; import org.tarantool.protocol.TarantoolPacket; @@ -25,6 +28,7 @@ public class JdbcConnectionTimeoutIT { protected static final String LUA_FILE = "jdk-testing.lua"; + private static final String HOST = "localhost"; protected static final int LISTEN = 3301; protected static final int ADMIN = 3313; private static final String INSTANCE_NAME = "jdk-testing"; @@ -47,6 +51,7 @@ public static void tearDownEnv() { @BeforeEach void setUp() throws SQLException { + assumeMinimalServerVersion(TarantoolConsole.open(HOST, ADMIN), ServerVersion.V_2_1); connection = new SQLConnection("", new Properties()) { @Override protected SQLTarantoolClientImpl makeSqlClient(String address, TarantoolClientConfig config) { @@ -66,7 +71,9 @@ protected void completeSql(TarantoolOp operation, TarantoolPacket pack) { @AfterEach void tearDown() throws SQLException { - connection.close(); + if (connection != null) { + connection.close(); + } } @Test diff --git a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java index 2f355aae..b4cf15c9 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcResultSetIT.java @@ -167,8 +167,12 @@ public void testResultSetMetadataAfterClose() throws SQLException { assertNotNull(resultSet); ResultSetMetaData metaData = resultSet.getMetaData(); assertNotNull(metaData); + + int expectedColumnSize = 2; + assertEquals(expectedColumnSize, metaData.getColumnCount()); + resultSet.close(); - assertEquals(metaData, resultSet.getMetaData()); + assertEquals(expectedColumnSize, metaData.getColumnCount()); } @Test diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 3292cebb..955d15fa 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -157,12 +157,14 @@ public void execute() throws Throwable { @Test public void testUnwrap() throws SQLException { + assertEquals(stmt, stmt.unwrap(TarantoolStatement.class)); assertEquals(stmt, stmt.unwrap(SQLStatement.class)); assertThrows(SQLException.class, () -> stmt.unwrap(Integer.class)); } @Test public void testIsWrapperFor() throws SQLException { + assertTrue(stmt.isWrapperFor(TarantoolStatement.class)); assertTrue(stmt.isWrapperFor(SQLStatement.class)); assertFalse(stmt.isWrapperFor(Integer.class)); } @@ -247,4 +249,103 @@ void testStatementConnection() throws SQLException { Statement statement = conn.createStatement(); assertEquals(conn, statement.getConnection()); } + + @Test + void testCloseOnCompletion() throws SQLException { + assertFalse(stmt.isCloseOnCompletion()); + stmt.closeOnCompletion(); + assertTrue(stmt.isCloseOnCompletion()); + } + + @Test + void testCloseOnCompletionDisabled() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=1"); + assertFalse(stmt.isClosed()); + assertFalse(resultSet.isClosed()); + + resultSet.close(); + assertTrue(resultSet.isClosed()); + assertFalse(stmt.isClosed()); + } + + @Test + void testCloseOnCompletionEnabled() throws SQLException { + stmt.closeOnCompletion(); + ResultSet resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=1"); + + assertFalse(stmt.isClosed()); + assertFalse(resultSet.isClosed()); + + resultSet.close(); + assertTrue(resultSet.isClosed()); + assertTrue(stmt.isClosed()); + } + + @Test + void testCloseOnCompletionAfterResultSet() throws SQLException { + ResultSet resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=1"); + stmt.closeOnCompletion(); + + assertFalse(stmt.isClosed()); + assertFalse(resultSet.isClosed()); + + resultSet.close(); + assertTrue(resultSet.isClosed()); + assertTrue(stmt.isClosed()); + } + + @Test + void testCloseOnCompletionMultipleResultSets() throws SQLException { + stmt.closeOnCompletion(); + ResultSet resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=1"); + ResultSet anotherResultSet = stmt.executeQuery("SELECT val FROM test WHERE id=2"); + + assertTrue(resultSet.isClosed()); + assertFalse(anotherResultSet.isClosed()); + assertFalse(stmt.isClosed()); + + anotherResultSet.close(); + assertTrue(anotherResultSet.isClosed()); + assertTrue(stmt.isClosed()); + } + + @Test + void testCloseOnCompletionUpdateQueries() throws SQLException { + stmt.closeOnCompletion(); + + int updateCount = stmt.executeUpdate("INSERT INTO test(id, val) VALUES (5, 'five')"); + assertEquals(1, updateCount); + assertFalse(stmt.isClosed()); + + updateCount = stmt.executeUpdate("INSERT INTO test(id, val) VALUES (6, 'six')"); + assertEquals(1, updateCount); + assertFalse(stmt.isClosed()); + } + + @Test + void testCloseOnCompletionMixedQueries() throws SQLException { + stmt.closeOnCompletion(); + + int updateCount = stmt.executeUpdate("INSERT INTO test(id, val) VALUES (7, 'seven')"); + assertEquals(1, updateCount); + assertFalse(stmt.isClosed()); + + ResultSet resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=7"); + assertFalse(resultSet.isClosed()); + assertFalse(stmt.isClosed()); + + updateCount = stmt.executeUpdate("INSERT INTO test(id, val) VALUES (8, 'eight')"); + assertEquals(1, updateCount); + assertTrue(resultSet.isClosed()); + assertFalse(stmt.isClosed()); + + resultSet = stmt.executeQuery("SELECT val FROM test WHERE id=8"); + assertFalse(resultSet.isClosed()); + assertFalse(stmt.isClosed()); + + resultSet.close(); + assertTrue(resultSet.isClosed()); + assertTrue(stmt.isClosed()); + } + } diff --git a/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java b/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java index d635a778..caba3618 100644 --- a/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java +++ b/src/test/java/org/tarantool/jdbc/ds/JdbcDataSourceIT.java @@ -7,12 +7,16 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.tarantool.TestAssumptions.assumeMinimalServerVersion; import static org.tarantool.TestUtils.makeInstanceEnv; +import org.tarantool.ServerVersion; +import org.tarantool.TarantoolConsole; import org.tarantool.TarantoolControl; import org.tarantool.jdbc.SQLProperty; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.ThrowingSupplier; @@ -27,23 +31,28 @@ class JdbcDataSourceIT { private static final String LUA_FILE = "jdk-testing.lua"; + private static final String HOST = "localhost"; private static final int LISTEN = 3301; private static final int ADMIN = 3313; private static final String INSTANCE_NAME = "data-source-testing"; private SQLDataSource dataSource; - @BeforeEach - void setUp() { + @BeforeAll + static void setUpEnv() { TarantoolControl control = new TarantoolControl(); control.createInstance(INSTANCE_NAME, LUA_FILE, makeInstanceEnv(LISTEN, ADMIN)); control.start(INSTANCE_NAME); + } + @BeforeEach + void setUp() { + assumeMinimalServerVersion(TarantoolConsole.open(HOST, ADMIN), ServerVersion.V_2_1); dataSource = new SQLDataSource(); } - @AfterEach - void tearDown() { + @AfterAll + static void tearDownEnv() { TarantoolControl control = new TarantoolControl(); control.stop(INSTANCE_NAME); }