diff --git a/CHANGELOG.md b/CHANGELOG.md
index af3d292a3..a1f93aa82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [2.11.8](https://github.com/googleapis/java-spanner-jdbc/compare/v2.11.7...v2.11.8) (2023-08-15)
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.45.2 ([#1318](https://github.com/googleapis/java-spanner-jdbc/issues/1318)) ([e924178](https://github.com/googleapis/java-spanner-jdbc/commit/e9241787b94cb614f658f5e6c977ffc008fd3397))
+
## [2.11.7](https://github.com/googleapis/java-spanner-jdbc/compare/v2.11.6...v2.11.7) (2023-08-13)
diff --git a/pom.xml b/pom.xml
index 4847ce9d5..7e85f5a45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
google-cloud-spanner-jdbc
- 2.11.7
+ 2.11.8
jar
Google Cloud Spanner JDBC
https://github.com/googleapis/java-spanner-jdbc
@@ -62,7 +62,7 @@
com.google.cloud
google-cloud-spanner-bom
- 6.45.1
+ 6.45.2
pom
import
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index e5c2fd93b..192263619 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.11.6
+ 2.11.7
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 74c89c6c2..73e594f27 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.11.7
+ 2.11.8
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
index 051ed9d6d..3e7c86f9f 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
@@ -23,9 +23,11 @@
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
import com.google.cloud.spanner.connection.StatementResult;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.rpc.Code;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -33,9 +35,13 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
/** Implementation of {@link java.sql.Statement} for Google Cloud Spanner. */
class JdbcStatement extends AbstractJdbcStatement {
+ static final ImmutableList ALL_COLUMNS = ImmutableList.of("*");
+
enum BatchType {
NONE,
DML,
@@ -98,6 +104,81 @@ public long executeLargeUpdate(String sql) throws SQLException {
}
}
+ /**
+ * Adds a THEN RETURN/RETURNING clause to the given statement if the following conditions are all
+ * met:
+ *
+ *
+ * - The generatedKeysColumns is not null or empty
+ *
- The statement is a DML statement
+ *
- The DML statement does not already contain a THEN RETURN/RETURNING clause
+ *
+ */
+ Statement addReturningToStatement(
+ Statement statement, @Nullable ImmutableList generatedKeysColumns)
+ throws SQLException {
+ if (generatedKeysColumns == null || generatedKeysColumns.isEmpty()) {
+ return statement;
+ }
+ // Check if the statement is a DML statement or not.
+ ParsedStatement parsedStatement = getConnection().getParser().parse(statement);
+ if (parsedStatement.isUpdate() && !parsedStatement.hasReturningClause()) {
+ if (generatedKeysColumns.size() == 1
+ && ALL_COLUMNS.get(0).equals(generatedKeysColumns.get(0))) {
+ // Add a 'THEN RETURN/RETURNING *' clause to the statement.
+ return statement
+ .toBuilder()
+ .replace(statement.getSql() + getReturningAllColumnsClause())
+ .build();
+ }
+ // Add a 'THEN RETURN/RETURNING col1, col2, ...' to the statement.
+ // The column names will be quoted using the dialect-specific identifier quoting character.
+ return statement
+ .toBuilder()
+ .replace(
+ generatedKeysColumns.stream()
+ .map(this::quoteColumn)
+ .collect(
+ Collectors.joining(
+ ", ", statement.getSql() + getReturningClause() + " ", "")))
+ .build();
+ }
+ return statement;
+ }
+
+ /** Returns the dialect-specific clause for returning values from a DML statement. */
+ String getReturningAllColumnsClause() {
+ switch (getConnection().getDialect()) {
+ case POSTGRESQL:
+ return "\nRETURNING *";
+ case GOOGLE_STANDARD_SQL:
+ default:
+ return "\nTHEN RETURN *";
+ }
+ }
+
+ /** Returns the dialect-specific clause for returning values from a DML statement. */
+ String getReturningClause() {
+ switch (getConnection().getDialect()) {
+ case POSTGRESQL:
+ return "\nRETURNING";
+ case GOOGLE_STANDARD_SQL:
+ default:
+ return "\nTHEN RETURN";
+ }
+ }
+
+ /** Adds dialect-specific quotes to the given column name. */
+ String quoteColumn(String column) {
+ switch (getConnection().getDialect()) {
+ case POSTGRESQL:
+ return "\"" + column + "\"";
+ case GOOGLE_STANDARD_SQL:
+ default:
+ return "`" + column + "`";
+ }
+ }
+
@Override
public boolean execute(String sql) throws SQLException {
checkClosed();
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
index c7b6a6cbd..d44583587 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -39,6 +40,7 @@
import com.google.cloud.spanner.connection.StatementResult;
import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
+import com.google.common.collect.ImmutableList;
import com.google.rpc.Code;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -47,6 +49,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -599,4 +602,157 @@ public void testConvertUpdateCountsToSuccessNoInfo() throws SQLException {
(long) Statement.SUCCESS_NO_INFO);
}
}
+
+ @Test
+ public void testAddReturningToStatement() throws SQLException {
+ JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
+ when(connection.getParser()).thenReturn(AbstractStatementParser.getInstance(dialect));
+ try (JdbcStatement statement = new JdbcStatement(connection)) {
+ assertAddReturningSame(statement, "insert into test (id, value) values (1, 'One')", null);
+ assertAddReturningSame(
+ statement, "insert into test (id, value) values (1, 'One')", ImmutableList.of());
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "insert into test (id, value) values (1, 'One')\nRETURNING \"id\""
+ : "insert into test (id, value) values (1, 'One')\nTHEN RETURN `id`",
+ "insert into test (id, value) values (1, 'One')",
+ ImmutableList.of("id"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "insert into test (id, value) values (1, 'One')\nRETURNING \"id\", \"value\""
+ : "insert into test (id, value) values (1, 'One')\nTHEN RETURN `id`, `value`",
+ "insert into test (id, value) values (1, 'One')",
+ ImmutableList.of("id", "value"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "insert into test (id, value) values (1, 'One')\nRETURNING *"
+ : "insert into test (id, value) values (1, 'One')\nTHEN RETURN *",
+ "insert into test (id, value) values (1, 'One')",
+ ImmutableList.of("*"));
+ // Requesting generated keys for a DML statement that already contains a returning clause is a
+ // no-op.
+ assertAddReturningSame(
+ statement,
+ "insert into test (id, value) values (1, 'One') "
+ + statement.getReturningClause()
+ + " value",
+ ImmutableList.of("id"));
+ // Requesting generated keys for a query is a no-op.
+ for (ImmutableList keys :
+ ImmutableList.of(
+ ImmutableList.of("id"), ImmutableList.of("id", "value"), ImmutableList.of("*"))) {
+ assertAddReturningSame(statement, "select id, value from test", keys);
+ }
+
+ // Update statements may also request generated keys.
+ assertAddReturningSame(statement, "update test set value='Two' where id=1", null);
+ assertAddReturningSame(
+ statement, "update test set value='Two' where id=1", ImmutableList.of());
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "update test set value='Two' where id=1\nRETURNING \"value\""
+ : "update test set value='Two' where id=1\nTHEN RETURN `value`",
+ "update test set value='Two' where id=1",
+ ImmutableList.of("value"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "update test set value='Two' where id=1\nRETURNING \"value\", \"id\""
+ : "update test set value='Two' where id=1\nTHEN RETURN `value`, `id`",
+ "update test set value='Two' where id=1",
+ ImmutableList.of("value", "id"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "update test set value='Two' where id=1\nRETURNING *"
+ : "update test set value='Two' where id=1\nTHEN RETURN *",
+ "update test set value='Two' where id=1",
+ ImmutableList.of("*"));
+ // Requesting generated keys for a DML statement that already contains a returning clause is a
+ // no-op.
+ assertAddReturningSame(
+ statement,
+ "update test set value='Two' where id=1 " + statement.getReturningClause() + " value",
+ ImmutableList.of("value"));
+
+ // Delete statements may also request generated keys.
+ assertAddReturningSame(statement, "delete test where id=1", null);
+ assertAddReturningSame(statement, "delete test where id=1", ImmutableList.of());
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "delete test where id=1\nRETURNING \"value\""
+ : "delete test where id=1\nTHEN RETURN `value`",
+ "delete test where id=1",
+ ImmutableList.of("value"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "delete test where id=1\nRETURNING \"id\", \"value\""
+ : "delete test where id=1\nTHEN RETURN `id`, `value`",
+ "delete test where id=1",
+ ImmutableList.of("id", "value"));
+ assertAddReturningEquals(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "delete test where id=1\nRETURNING *"
+ : "delete test where id=1\nTHEN RETURN *",
+ "delete test where id=1",
+ ImmutableList.of("*"));
+ // Requesting generated keys for a DML statement that already contains a returning clause is a
+ // no-op.
+ for (ImmutableList keys :
+ ImmutableList.of(
+ ImmutableList.of("id"), ImmutableList.of("id", "value"), ImmutableList.of("*"))) {
+ assertAddReturningSame(
+ statement,
+ "delete test where id=1 "
+ + (dialect == Dialect.POSTGRESQL
+ ? "delete test where id=1\nRETURNING"
+ : "delete test where id=1\nTHEN RETURN")
+ + " value",
+ keys);
+ }
+
+ // Requesting generated keys for DDL is a no-op.
+ for (ImmutableList keys :
+ ImmutableList.of(
+ ImmutableList.of("id"), ImmutableList.of("id", "value"), ImmutableList.of("*"))) {
+ assertAddReturningSame(
+ statement,
+ dialect == Dialect.POSTGRESQL
+ ? "create table test (id bigint primary key, value varchar)"
+ : "create table test (id int64, value string(max)) primary key (id)",
+ keys);
+ }
+ }
+ }
+
+ private void assertAddReturningSame(
+ JdbcStatement statement, String sql, @Nullable ImmutableList generatedKeysColumns)
+ throws SQLException {
+ com.google.cloud.spanner.Statement spannerStatement =
+ com.google.cloud.spanner.Statement.of(sql);
+ assertSame(
+ spannerStatement,
+ statement.addReturningToStatement(spannerStatement, generatedKeysColumns));
+ }
+
+ private void assertAddReturningEquals(
+ JdbcStatement statement,
+ String expectedSql,
+ String sql,
+ @Nullable ImmutableList generatedKeysColumns)
+ throws SQLException {
+ com.google.cloud.spanner.Statement spannerStatement =
+ com.google.cloud.spanner.Statement.of(sql);
+ assertEquals(
+ com.google.cloud.spanner.Statement.of(expectedSql),
+ statement.addReturningToStatement(spannerStatement, generatedKeysColumns));
+ }
}
diff --git a/versions.txt b/versions.txt
index 874269315..e9d28b678 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
# Format:
# module:released-version:current-version
-google-cloud-spanner-jdbc:2.11.7:2.11.7
+google-cloud-spanner-jdbc:2.11.8:2.11.8