diff --git a/clirr-ignored-differences.xml b/clirr-ignored-differences.xml
index 7e243773..ac9c147d 100644
--- a/clirr-ignored-differences.xml
+++ b/clirr-ignored-differences.xml
@@ -94,4 +94,21 @@
com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection
void setTransactionMode(com.google.cloud.spanner.connection.TransactionMode)
+
+
+
+ 7012
+ com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection
+ com.google.cloud.spanner.CommitResponse getCommitResponse()
+
+
+ 7012
+ com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection
+ boolean isReturnCommitStats()
+
+
+ 7012
+ com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection
+ void setReturnCommitStats(boolean)
+
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
index f13a64f5..4a5f001a 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
@@ -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;
@@ -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
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
index 6c23db49..bf9120f5 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
@@ -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;
@@ -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();
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcCommitStatsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcCommitStatsTest.java
new file mode 100644
index 00000000..0782566d
--- /dev/null
+++ b/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());
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
index 27389b4c..cd0f9449 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
@@ -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;
@@ -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 {
@@ -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)
@@ -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());
+ }
+ }
}