Skip to content

Commit

Permalink
feat: add support for CommitStats (#261)
Browse files Browse the repository at this point in the history
* feat: add support for CommitStats

* fix: remove overload delay

* fix: gracefully close pool after each test

* chore: update copyright year, comments and assertions

* deps: use Spanner client lib 5.0.0

* fix: add new methods to clirr

* test: add tests for SQLException
  • Loading branch information
olavloite committed Mar 10, 2021
1 parent f0cdf11 commit b32e7ae
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 0 deletions.
17 changes: 17 additions & 0 deletions clirr-ignored-differences.xml
Expand Up @@ -94,4 +94,21 @@
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
<method>void setTransactionMode(com.google.cloud.spanner.connection.TransactionMode)</method>
</difference>

<!-- Commit stats -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
<method>com.google.cloud.spanner.CommitResponse getCommitResponse()</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
<method>boolean isReturnCommitStats()</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection</className>
<method>void setReturnCommitStats(boolean)</method>
</difference>
</differences>
Expand Up @@ -17,6 +17,8 @@
package com.google.cloud.spanner.jdbc;

import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.CommitStats;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.TimestampBound;
Expand Down Expand Up @@ -152,6 +154,24 @@ public interface CloudSpannerJdbcConnection extends Connection {
*/
Timestamp getCommitTimestamp() throws SQLException;

/**
* @return the {@link CommitResponse} of the last read/write transaction. If the last transaction
* was not a read/write transaction, or a read/write transaction that did not return a {@link
* CommitResponse} because the transaction was not committed, the method will throw a {@link
* SQLException}. The {@link CommitResponse} will include {@link CommitStats} if {@link
* #isReturnCommitStats()} returns true.
*/
CommitResponse getCommitResponse() throws SQLException;

/**
* Sets whether this connection should request commit statistics from Cloud Spanner for read/write
* transactions and for DML statements in autocommit mode.
*/
void setReturnCommitStats(boolean returnCommitStats) throws SQLException;

/** @return true if this connection requests commit statistics from Cloud Spanner. */
boolean isReturnCommitStats() throws SQLException;

/**
* @return the read {@link Timestamp} of the last read-only transaction. If the last transaction
* was not a read-only transaction, or a read-only transaction that did not return a read
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
Expand Up @@ -17,6 +17,7 @@
package com.google.cloud.spanner.jdbc;

import com.google.api.client.util.Preconditions;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.TimestampBound;
Expand Down Expand Up @@ -381,6 +382,36 @@ public Timestamp getCommitTimestamp() throws SQLException {
}
}

@Override
public CommitResponse getCommitResponse() throws SQLException {
checkClosed();
try {
return getSpannerConnection().getCommitResponse();
} catch (SpannerException e) {
throw JdbcSqlExceptionFactory.of(e);
}
}

@Override
public void setReturnCommitStats(boolean returnCommitStats) throws SQLException {
checkClosed();
try {
getSpannerConnection().setReturnCommitStats(returnCommitStats);
} catch (SpannerException e) {
throw JdbcSqlExceptionFactory.of(e);
}
}

@Override
public boolean isReturnCommitStats() throws SQLException {
checkClosed();
try {
return getSpannerConnection().isReturnCommitStats();
} catch (SpannerException e) {
throw JdbcSqlExceptionFactory.of(e);
}
}

@Override
public Timestamp getReadTimestamp() throws SQLException {
checkClosed();
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/com/google/cloud/spanner/jdbc/JdbcCommitStatsTest.java
@@ -0,0 +1,123 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.jdbc;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.connection.AbstractMockServerTest;
import com.google.cloud.spanner.connection.SpannerPool;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class JdbcCommitStatsTest extends AbstractMockServerTest {

@After
public void closeSpannerPool() {
SpannerPool.closeSpannerPool();
}

@Test
public void testDefaultReturnCommitStats() throws SQLException {
try (java.sql.Connection connection = createJdbcConnection()) {
try (java.sql.ResultSet rs =
connection.createStatement().executeQuery("SHOW VARIABLE RETURN_COMMIT_STATS")) {
assertTrue(rs.next());
assertFalse(rs.getBoolean("RETURN_COMMIT_STATS"));
assertFalse(rs.next());
}
}
}

@Test
public void testReturnCommitStatsInConnectionUrl() throws SQLException {
try (java.sql.Connection connection =
DriverManager.getConnection(
String.format("jdbc:%s;returnCommitStats=true", getBaseUrl()))) {
try (java.sql.ResultSet rs =
connection.createStatement().executeQuery("SHOW VARIABLE RETURN_COMMIT_STATS")) {
assertTrue(rs.next());
assertTrue(rs.getBoolean("RETURN_COMMIT_STATS"));
assertFalse(rs.next());
}
}
}

@Test
public void testSetReturnCommitStats() throws SQLException {
try (java.sql.Connection connection = createJdbcConnection()) {
connection.createStatement().execute("SET RETURN_COMMIT_STATS=true");
try (java.sql.ResultSet rs =
connection.createStatement().executeQuery("SHOW VARIABLE RETURN_COMMIT_STATS")) {
assertTrue(rs.next());
assertTrue(rs.getBoolean("RETURN_COMMIT_STATS"));
assertFalse(rs.next());
}
connection.createStatement().execute("SET RETURN_COMMIT_STATS=false");
try (java.sql.ResultSet rs =
connection.createStatement().executeQuery("SHOW VARIABLE RETURN_COMMIT_STATS")) {
assertTrue(rs.next());
assertFalse(rs.getBoolean("RETURN_COMMIT_STATS"));
assertFalse(rs.next());
}
}
}

@Test
public void testSetAndUseReturnCommitStats() throws SQLException {
try (CloudSpannerJdbcConnection connection =
createJdbcConnection().unwrap(CloudSpannerJdbcConnection.class)) {
connection.setReturnCommitStats(true);
connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build());
connection.commit();
CommitResponse response = connection.getCommitResponse();
assertNotNull(response);
assertNotNull(response.getCommitStats());
assertThat(response.getCommitStats().getMutationCount()).isAtLeast(1);
}
}

@Test
public void testSetAndUseReturnCommitStatsUsingSql() throws SQLException {
try (java.sql.Connection connection = createJdbcConnection()) {
connection.createStatement().execute("SET RETURN_COMMIT_STATS=true");
// Use a Mutation as the mock server only returns a non-zero mutation count for mutations, and
// not for DML statements.
connection
.unwrap(CloudSpannerJdbcConnection.class)
.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build());
connection.commit();
try (ResultSet rs =
connection.createStatement().executeQuery("SHOW VARIABLE COMMIT_RESPONSE")) {
assertTrue(rs.next());
assertNotNull(rs.getTimestamp("COMMIT_TIMESTAMP"));
assertThat(rs.getLong("MUTATION_COUNT")).isAtLeast(1L);
assertFalse(rs.next());
}
}
}
}
Expand Up @@ -18,7 +18,11 @@

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -51,6 +55,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

@RunWith(JUnit4.class)
public class JdbcConnectionTest {
Expand Down Expand Up @@ -277,6 +282,14 @@ public void testClosedJdbcConnection()
"releaseSavepoint",
new Class<?>[] {Savepoint.class},
new Object[] {null});

testClosed(CloudSpannerJdbcConnection.class, "isReturnCommitStats");
testClosed(
CloudSpannerJdbcConnection.class,
"setReturnCommitStats",
new Class<?>[] {boolean.class},
new Object[] {true});
testClosed(CloudSpannerJdbcConnection.class, "getCommitResponse");
}

private void testClosed(Class<? extends Connection> clazz, String name)
Expand Down Expand Up @@ -705,4 +718,81 @@ public void testSchema() throws SQLException {
}
}
}

@Test
public void testIsReturnCommitStats() throws SQLException {
try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
assertFalse(connection.isReturnCommitStats());
connection.setReturnCommitStats(true);
assertTrue(connection.isReturnCommitStats());
}
}

@Test
public void testIsReturnCommitStats_throwsSqlException() throws SQLException {
ConnectionOptions options = mock(ConnectionOptions.class);
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
when(spannerConnection.isReturnCommitStats())
.thenThrow(
SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION, "test exception"));
try (JdbcConnection connection =
new JdbcConnection(
"jdbc:cloudspanner://localhost/projects/project/instances/instance/databases/database;credentialsUrl=url",
options)) {
connection.isReturnCommitStats();
fail("missing expected exception");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.FAILED_PRECONDITION, ((JdbcSqlException) e).getCode());
}
}

@Test
public void testSetReturnCommitStats_throwsSqlException() throws SQLException {
ConnectionOptions options = mock(ConnectionOptions.class);
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
Mockito.doThrow(
SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION, "test exception"))
.when(spannerConnection)
.setReturnCommitStats(any(boolean.class));
try (JdbcConnection connection =
new JdbcConnection(
"jdbc:cloudspanner://localhost/projects/project/instances/instance/databases/database;credentialsUrl=url",
options)) {
connection.setReturnCommitStats(true);
fail("missing expected exception");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.FAILED_PRECONDITION, ((JdbcSqlException) e).getCode());
}
}

@Test
public void testGetCommitResponse_throwsSqlException() throws SQLException {
ConnectionOptions options = mock(ConnectionOptions.class);
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
Mockito.doThrow(
SpannerExceptionFactory.newSpannerException(
ErrorCode.FAILED_PRECONDITION, "test exception"))
.when(spannerConnection)
.setReturnCommitStats(any(boolean.class));
try (JdbcConnection connection =
new JdbcConnection(
"jdbc:cloudspanner://localhost/projects/project/instances/instance/databases/database;credentialsUrl=url",
options)) {
connection.setReturnCommitStats(true);
fail("missing expected exception");
} catch (SQLException e) {
assertTrue(e instanceof JdbcSqlException);
assertEquals(Code.FAILED_PRECONDITION, ((JdbcSqlException) e).getCode());
}
}
}

0 comments on commit b32e7ae

Please sign in to comment.