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: + * + *
    + *
  1. The generatedKeysColumns is not null or empty + *
  2. The statement is a DML statement + *
  3. 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