From 10ce1c1391445dced28149b5bb1e60eeca4541f6 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Sun, 25 Aug 2019 16:19:38 +0200 Subject: [PATCH 1/2] isValid should return false and not SQLException when closed --- .../cloud/spanner/jdbc/JdbcConnection.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java index 8d5d55bcc25a..c71c21b74086 100644 --- a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java @@ -41,6 +41,8 @@ class JdbcConnection extends AbstractJdbcConnection { private static final String ONLY_CLOSE_CURSORS_AT_COMMIT = "Only result sets with holdability CLOSE_CURSORS_AT_COMMIT are supported"; static final String ONLY_NO_GENERATED_KEYS = "Only NO_GENERATED_KEYS are supported"; + static final String IS_VALID_QUERY = "SELECT 1"; + private Map> typeMap = new HashMap<>(); JdbcConnection(String connectionUrl, ConnectionOptions options) { @@ -228,20 +230,21 @@ public void setTypeMap(Map> map) throws SQLException { @Override public boolean isValid(int timeout) throws SQLException { - checkClosed(); JdbcPreconditions.checkArgument(timeout >= 0, "timeout must be >= 0"); - try { - Statement statement = createStatement(); - statement.setQueryTimeout(timeout); - try (ResultSet rs = statement.executeQuery("select 1")) { - if (rs.next()) { - if (rs.getLong(1) == 1L) { - return true; + if (!isClosed()) { + try { + Statement statement = createStatement(); + statement.setQueryTimeout(timeout); + try (ResultSet rs = statement.executeQuery(IS_VALID_QUERY)) { + if (rs.next()) { + if (rs.getLong(1) == 1L) { + return true; + } } } + } catch (SQLException e) { + // ignore } - } catch (SQLException e) { - // ignore } return false; } From f610444148da8475e28d8bf4e42946c53f3e2b11 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Sun, 25 Aug 2019 19:10:56 +0200 Subject: [PATCH 2/2] add tests for JdbcConnection --- .../spanner/jdbc/JdbcConnectionTest.java | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java index 68bf06058b54..6740782a4aa3 100644 --- a/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java +++ b/google-cloud-clients/google-cloud-contrib/google-cloud-spanner-jdbc/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java @@ -21,17 +21,29 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; import com.google.rpc.Code; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.Savepoint; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Properties; @@ -45,6 +57,10 @@ @RunWith(JUnit4.class) public class JdbcConnectionTest { @Rule public final ExpectedException exception = ExpectedException.none(); + private static final com.google.cloud.spanner.ResultSet SELECT1_RESULTSET = + ResultSets.forRows( + Type.struct(StructField.of("", Type.int64())), + Arrays.asList(Struct.newBuilder().set("").to(1L).build())); private JdbcConnection createConnection(ConnectionOptions options) { com.google.cloud.spanner.jdbc.Connection spannerConnection = @@ -413,4 +429,241 @@ public void testSetClientInfo() throws SQLException { is(equalTo(AbstractJdbcConnection.CLIENT_INFO_NOT_SUPPORTED))); } } + + @Test + public void testIsValid() throws SQLException { + // Setup. + ConnectionOptions options = mock(ConnectionOptions.class); + com.google.cloud.spanner.jdbc.Connection spannerConnection = + mock(com.google.cloud.spanner.jdbc.Connection.class); + when(options.getConnection()).thenReturn(spannerConnection); + Statement statement = Statement.of(JdbcConnection.IS_VALID_QUERY); + + // Verify that an opened connection that returns a result set is valid. + try (JdbcConnection connection = new JdbcConnection("url", options)) { + when(spannerConnection.executeQuery(statement)).thenReturn(SELECT1_RESULTSET); + assertThat(connection.isValid(1), is(true)); + try { + // Invalid timeout value. + connection.isValid(-1); + fail("missing expected exception"); + } catch (JdbcSqlExceptionImpl e) { + assertThat(e.getCode(), is(equalTo(Code.INVALID_ARGUMENT))); + } + + // Now let the query return an error. isValid should now return false. + when(spannerConnection.executeQuery(statement)) + .thenThrow( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, "the current transaction has been aborted")); + assertThat(connection.isValid(1), is(false)); + } + } + + @Test + public void testIsValidOnClosedConnection() throws SQLException { + Connection connection = createConnection(mock(ConnectionOptions.class)); + connection.close(); + assertThat(connection.isValid(1), is(false)); + } + + @Test + public void testCreateStatement() throws SQLException { + try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) { + for (int resultSetType : + new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE + }) { + for (int resultSetConcurrency : + new int[] {ResultSet.CONCUR_READ_ONLY, ResultSet.CONCUR_UPDATABLE}) { + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY // Only FORWARD_ONLY is supported + && resultSetConcurrency == ResultSet.CONCUR_READ_ONLY) // Only READ_ONLY is supported + { + java.sql.Statement statement = + connection.createStatement(resultSetType, resultSetConcurrency); + assertThat(statement.getResultSetType(), is(equalTo(resultSetType))); + assertThat(statement.getResultSetConcurrency(), is(equalTo(resultSetConcurrency))); + } else { + assertCreateStatementFails(connection, resultSetType, resultSetConcurrency); + } + for (int resultSetHoldability : + new int[] {ResultSet.CLOSE_CURSORS_AT_COMMIT, ResultSet.HOLD_CURSORS_OVER_COMMIT}) { + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY // Only FORWARD_ONLY is supported + && resultSetConcurrency == ResultSet.CONCUR_READ_ONLY // Only READ_ONLY is supported + && resultSetHoldability + == ResultSet + .CLOSE_CURSORS_AT_COMMIT) // Only CLOSE_CURSORS_AT_COMMIT is supported + { + java.sql.Statement statement = + connection.createStatement( + resultSetType, resultSetConcurrency, resultSetHoldability); + assertThat(statement.getResultSetType(), is(equalTo(resultSetType))); + assertThat(statement.getResultSetConcurrency(), is(equalTo(resultSetConcurrency))); + assertThat(statement.getResultSetHoldability(), is(equalTo(resultSetHoldability))); + } else { + assertCreateStatementFails( + connection, resultSetType, resultSetConcurrency, resultSetHoldability); + } + } + } + } + } + } + + private void assertCreateStatementFails( + JdbcConnection connection, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) + throws SQLException { + try { + connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + fail( + String.format( + "missing expected exception for %d %d %d", + resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (SQLFeatureNotSupportedException e) { + // ignore, this is the expected exception. + } + } + + private void assertCreateStatementFails( + JdbcConnection connection, int resultSetType, int resultSetConcurrency) throws SQLException { + try { + connection.createStatement(resultSetType, resultSetConcurrency); + fail( + String.format( + "missing expected exception for %d %d", resultSetType, resultSetConcurrency)); + } catch (SQLFeatureNotSupportedException e) { + // ignore, this is the expected exception. + } + } + + @Test + public void testPrepareStatement() throws SQLException { + try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) { + for (int resultSetType : + new int[] { + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE + }) { + for (int resultSetConcurrency : + new int[] {ResultSet.CONCUR_READ_ONLY, ResultSet.CONCUR_UPDATABLE}) { + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY // Only FORWARD_ONLY is supported + && resultSetConcurrency == ResultSet.CONCUR_READ_ONLY) // Only READ_ONLY is supported + { + PreparedStatement ps = + connection.prepareStatement("SELECT 1", resultSetType, resultSetConcurrency); + assertThat(ps.getResultSetType(), is(equalTo(resultSetType))); + assertThat(ps.getResultSetConcurrency(), is(equalTo(resultSetConcurrency))); + } else { + assertPrepareStatementFails(connection, resultSetType, resultSetConcurrency); + } + for (int resultSetHoldability : + new int[] {ResultSet.CLOSE_CURSORS_AT_COMMIT, ResultSet.HOLD_CURSORS_OVER_COMMIT}) { + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY // Only FORWARD_ONLY is supported + && resultSetConcurrency == ResultSet.CONCUR_READ_ONLY // Only READ_ONLY is supported + && resultSetHoldability + == ResultSet + .CLOSE_CURSORS_AT_COMMIT) // Only CLOSE_CURSORS_AT_COMMIT is supported + { + PreparedStatement ps = + connection.prepareStatement( + "SELECT 1", resultSetType, resultSetConcurrency, resultSetHoldability); + assertThat(ps.getResultSetType(), is(equalTo(resultSetType))); + assertThat(ps.getResultSetConcurrency(), is(equalTo(resultSetConcurrency))); + assertThat(ps.getResultSetHoldability(), is(equalTo(resultSetHoldability))); + } else { + assertPrepareStatementFails( + connection, resultSetType, resultSetConcurrency, resultSetHoldability); + } + } + } + } + } + } + + private void assertPrepareStatementFails( + JdbcConnection connection, + int resultSetType, + int resultSetConcurrency, + int resultSetHoldability) + throws SQLException { + try { + connection.prepareStatement( + "SELECT 1", resultSetType, resultSetConcurrency, resultSetHoldability); + fail( + String.format( + "missing expected exception for %d %d %d", + resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (SQLFeatureNotSupportedException e) { + // ignore, this is the expected exception. + } + } + + private void assertPrepareStatementFails( + JdbcConnection connection, int resultSetType, int resultSetConcurrency) throws SQLException { + try { + connection.prepareStatement("SELECT 1", resultSetType, resultSetConcurrency); + fail( + String.format( + "missing expected exception for %d %d", resultSetType, resultSetConcurrency)); + } catch (SQLFeatureNotSupportedException e) { + // ignore, this is the expected exception. + } + } + + @Test + public void testPrepareStatementWithAutoGeneratedKeys() throws SQLException { + String sql = "INSERT INTO FOO (COL1) VALUES (?)"; + try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) { + PreparedStatement statement = + connection.prepareStatement(sql, java.sql.Statement.NO_GENERATED_KEYS); + ResultSet rs = statement.getGeneratedKeys(); + assertThat(rs.next(), is(false)); + try { + statement = connection.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS); + fail("missing expected SQLFeatureNotSupportedException"); + } catch (SQLFeatureNotSupportedException e) { + // ignore, this is the expected exception. + } + } + } + + @Test + public void testCatalog() throws SQLException { + ConnectionOptions options = mock(ConnectionOptions.class); + when(options.getDatabaseName()).thenReturn("test"); + try (JdbcConnection connection = createConnection(options)) { + assertThat(connection.getCatalog(), is(equalTo("test"))); + // This should be allowed. + connection.setCatalog(""); + try { + // This should cause an exception. + connection.setCatalog("other"); + fail("missing expected exception"); + } catch (JdbcSqlExceptionImpl e) { + assertThat(e.getCode(), is(equalTo(Code.INVALID_ARGUMENT))); + } + } + } + + @Test + public void testSchema() throws SQLException { + try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) { + assertThat(connection.getSchema(), is(equalTo(""))); + // This should be allowed. + connection.setSchema(""); + try { + // This should cause an exception. + connection.setSchema("other"); + fail("missing expected exception"); + } catch (JdbcSqlExceptionImpl e) { + assertThat(e.getCode(), is(equalTo(Code.INVALID_ARGUMENT))); + } + } + } }