Date: Tue, 29 Aug 2023 15:45:00 +0200
Subject: [PATCH 04/10] chore: allow comments in PG strings (#2604)
Allow comments in PostgreSQL query strings for extracting parameter
definitions. The comments need to be preserved, as they could contain
query hints.
---
.../connection/AbstractStatementParser.java | 7 +-
.../connection/PostgreSQLStatementParser.java | 57 ++-
.../connection/StatementParserTest.java | 339 ++++++++++++------
3 files changed, 286 insertions(+), 117 deletions(-)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
index 11549fbfc59..6c35fe5c96d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
@@ -539,11 +539,10 @@ public static class ParametersInfo {
* Converts all positional parameters (?) in the given sql string into named parameters. The
* parameters are named @p1, @p2, etc. This method is used when converting a JDBC statement that
* uses positional parameters to a Cloud Spanner {@link Statement} instance that requires named
- * parameters. The input SQL string may not contain any comments. There is an exception case if
- * the statement starts with a GSQL comment which forces it to be interpreted as a GoogleSql
- * statement.
+ * parameters. The input SQL string may not contain any comments, except for PostgreSQL-dialect
+ * SQL strings.
*
- * @param sql The sql string without comments that should be converted
+ * @param sql The sql string that should be converted
* @return A {@link ParametersInfo} object containing a string with named parameters instead of
* positional parameters and the number of parameters.
* @throws SpannerException If the input sql string contains an unclosed string/byte literal.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
index 012bfbba875..8cb2b7e464a 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
@@ -184,9 +184,8 @@ ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramCh
* Note: This is an internal API and breaking changes can be made without prior notice.
*
* Returns the PostgreSQL-style query parameters ($1, $2, ...) in the given SQL string. The
- * SQL-string is assumed to not contain any comments. Use {@link #removeCommentsAndTrim(String)}
- * to remove all comments before calling this method. Occurrences of query-parameter like strings
- * inside quoted identifiers or string literals are ignored.
+ * SQL-string is allowed to contain comments. Occurrences of query-parameter like strings inside
+ * quoted identifiers or string literals are ignored.
*
*
The following example will return a set containing ("$1", "$2").
* select col1, col2, "col$4"
@@ -195,7 +194,7 @@ ParametersInfo convertPositionalParametersToNamedParametersInternal(char paramCh
* and not col3=$1 and col4='$3'
*
*
- * @param sql the SQL-string to check for parameters. Must not contain comments.
+ * @param sql the SQL-string to check for parameters.
* @return A set containing all the parameters in the SQL-string.
*/
@InternalApi
@@ -233,12 +232,56 @@ private int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
return skipQuoted(
sql, currentIndex + dollarTag.length() + 1, currentChar, dollarTag, result);
}
+ } else if (currentChar == HYPHEN
+ && sql.length() > (currentIndex + 1)
+ && sql.charAt(currentIndex + 1) == HYPHEN) {
+ return skipSingleLineComment(sql, currentIndex, result);
+ } else if (currentChar == SLASH
+ && sql.length() > (currentIndex + 1)
+ && sql.charAt(currentIndex + 1) == ASTERISK) {
+ return skipMultiLineComment(sql, currentIndex, result);
}
appendIfNotNull(result, currentChar);
return currentIndex + 1;
}
+ static int skipSingleLineComment(String sql, int currentIndex, @Nullable StringBuilder result) {
+ int endIndex = sql.indexOf('\n', currentIndex + 2);
+ if (endIndex == -1) {
+ endIndex = sql.length();
+ } else {
+ // Include the newline character.
+ endIndex++;
+ }
+ appendIfNotNull(result, sql.substring(currentIndex, endIndex));
+ return endIndex;
+ }
+
+ static int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
+ // Current position is start + '/*'.length().
+ int pos = startIndex + 2;
+ // PostgreSQL allows comments to be nested. That is, the following is allowed:
+ // '/* test /* inner comment */ still a comment */'
+ int level = 1;
+ while (pos < sql.length()) {
+ if (sql.charAt(pos) == SLASH && sql.length() > (pos + 1) && sql.charAt(pos + 1) == ASTERISK) {
+ level++;
+ }
+ if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
+ level--;
+ if (level == 0) {
+ pos += 2;
+ appendIfNotNull(result, sql.substring(startIndex, pos));
+ return pos;
+ }
+ }
+ pos++;
+ }
+ appendIfNotNull(result, sql.substring(startIndex));
+ return sql.length();
+ }
+
private int skipQuoted(
String sql, int startIndex, char startQuote, @Nullable StringBuilder result) {
return skipQuoted(sql, startIndex, startQuote, null, result);
@@ -285,6 +328,12 @@ private void appendIfNotNull(@Nullable StringBuilder result, char currentChar) {
}
}
+ private static void appendIfNotNull(@Nullable StringBuilder result, String suffix) {
+ if (result != null) {
+ result.append(suffix);
+ }
+ }
+
private void appendIfNotNull(
@Nullable StringBuilder result, char prefix, String tag, char suffix) {
if (result != null) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java
index 8246ca10760..1c20bc94ec6 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java
@@ -1143,121 +1143,198 @@ public void testGoogleStandardSQLDialectConvertPositionalParametersToNamedParame
+ "and col8 between @p12 and @p13")));
}
+ private enum CommentInjector {
+ NONE {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, "");
+ }
+ },
+ BEFORE {
+ @Override
+ String inject(String sql, String comment) {
+ return comment + String.format(sql, "");
+ }
+ },
+ IN_THE_MIDDLE {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, comment);
+ }
+ },
+ AFTER {
+ @Override
+ String inject(String sql, String comment) {
+ return String.format(sql, "") + comment;
+ }
+ };
+
+ abstract String inject(String sql, String comment);
+ }
+
@Test
public void testPostgreSQLDialectDialectConvertPositionalParametersToNamedParameters() {
assumeTrue(dialect == Dialect.POSTGRESQL);
- assertThat(
+ for (String comment :
+ new String[] {
+ "-- test comment\n",
+ "/* another test comment */",
+ "/* comment\nwith\nmultiple\nlines\n */",
+ "/* comment /* with nested */ comment */"
+ }) {
+ for (CommentInjector injector : CommentInjector.values()) {
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("select * %sfrom foo where name=?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("select * %sfrom foo where name=$1", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'?test?\"?test?\"?'?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s'?test?\"?test?\"?'$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?'?it\\''?s'%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1'?it\\''?s'%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?'?it\\\"?s'%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1'?it\\\"?s'%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?\"?it\\\"\"?s\"%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1\"?it\\\"\"?s\"%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'''?it\\''?s'''?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s'''?it\\''?s'''$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?\"\"\"?it\\\"\"?s\"\"\"%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1\"\"\"?it\\\"\"?s\"\"\"%s$2", comment));
+
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?$$?it$?s$$%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1$$?it$?s$$%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?$tag$?it$$?s$tag$%s?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1$tag$?it$$?s$tag$%s$2", comment));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s$$?it\\'?s \n ?it\\'?s$$?", comment))
+ .sqlWithNamedParameters)
+ .isEqualTo(injector.inject("$1%s$$?it\\'?s \n ?it\\'?s$$$2", comment));
+
+ // Note: PostgreSQL allows a single-quoted string literal to contain line feeds.
+ assertEquals(
+ injector.inject("$1'?it\\''?s \n ?it\\''?s'%s$2", comment),
parser.convertPositionalParametersToNamedParameters(
- '?', "select * from foo where name=?")
- .sqlWithNamedParameters)
- .isEqualTo("select * from foo where name=$1");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?test?\"?test?\"?'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?test?\"?test?\"?'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\''?s'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?it\\''?s'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\\"?s'?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'?it\\\"?s'$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?\"?it\\\"\"?s\"?")
- .sqlWithNamedParameters)
- .isEqualTo("$1\"?it\\\"\"?s\"$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\''?s'''?")
- .sqlWithNamedParameters)
- .isEqualTo("$1'''?it\\''?s'''$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?\"\"\"?it\\\"\"?s\"\"\"?")
- .sqlWithNamedParameters)
- .isEqualTo("$1\"\"\"?it\\\"\"?s\"\"\"$2");
-
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$$?it$?s$$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$$?it$?s$$$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$tag$?it$$?s$tag$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$tag$?it$$?s$tag$$2");
- assertThat(
- parser.convertPositionalParametersToNamedParameters('?', "?$$?it\\'?s \n ?it\\'?s$$?")
- .sqlWithNamedParameters)
- .isEqualTo("$1$$?it\\'?s \n ?it\\'?s$$$2");
-
- // Note: PostgreSQL allows a single-quoted string literal to contain line feeds.
- assertEquals(
- "$1'?it\\''?s \n ?it\\''?s'$2",
- parser.convertPositionalParametersToNamedParameters('?', "?'?it\\''?s \n ?it\\''?s'?")
- .sqlWithNamedParameters);
- assertUnclosedLiteral("?'?it\\''?s \n ?it\\''?s?");
- assertEquals(
- "$1'''?it\\''?s \n ?it\\''?s'$2",
- parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\''?s \n ?it\\''?s'?")
- .sqlWithNamedParameters);
-
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select 1, ?, 'test?test', \"test?test\", foo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select 1, $1, 'test?test', \"test?test\", foo.* from `foo` where col1=$2 and col2='test' and col3=$3 and col4='?' and col5=\"?\" and col6='?''?''?'")));
+ '?', injector.inject("?'?it\\''?s \n ?it\\''?s'%s?", comment))
+ .sqlWithNamedParameters);
+ assertUnclosedLiteral("?'?it\\''?s \n ?it\\''?s?");
+ assertEquals(
+ injector.inject("$1%s'''?it\\''?s \n ?it\\''?s'$2", comment),
+ parser.convertPositionalParametersToNamedParameters(
+ '?', injector.inject("?%s'''?it\\''?s \n ?it\\''?s'?", comment))
+ .sqlWithNamedParameters);
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select * " + "from foo " + "where name=? " + "and col2 like ? " + "and col3 > ?")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select * "
- + "from foo "
- + "where name=$1 "
- + "and col2 like $2 "
- + "and col3 > $3")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?', "select * " + "from foo " + "where id between ? and ?")
- .sqlWithNamedParameters,
- is(equalTo("select * " + "from foo " + "where id between $1 and $2")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?', "select * " + "from foo " + "limit ? offset ?")
- .sqlWithNamedParameters,
- is(equalTo("select * " + "from foo " + "limit $1 offset $2")));
- assertThat(
- parser.convertPositionalParametersToNamedParameters(
- '?',
- "select * "
- + "from foo "
- + "where col1=? "
- + "and col2 like ? "
- + "and col3 > ? "
- + "and col4 < ? "
- + "and col5 != ? "
- + "and col6 not in (?, ?, ?) "
- + "and col7 in (?, ?, ?) "
- + "and col8 between ? and ?")
- .sqlWithNamedParameters,
- is(
- equalTo(
- "select * "
- + "from foo "
- + "where col1=$1 "
- + "and col2 like $2 "
- + "and col3 > $3 "
- + "and col4 < $4 "
- + "and col5 != $5 "
- + "and col6 not in ($6, $7, $8) "
- + "and col7 in ($9, $10, $11) "
- + "and col8 between $12 and $13")));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select 1, ?, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select 1, $1, 'test?test', \"test?test\", %sfoo.* from `foo` where col1=$2 and col2='test' and col3=$3 and col4='?' and col5=\"?\" and col6='?''?''?'",
+ comment))));
+
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * "
+ + "%sfrom foo "
+ + "where name=? "
+ + "and col2 like ? "
+ + "and col3 > ?",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * "
+ + "%sfrom foo "
+ + "where name=$1 "
+ + "and col2 like $2 "
+ + "and col3 > $3",
+ comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * " + "from foo " + "where id between ?%s and ?", comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * " + "from foo " + "where id between $1%s and $2", comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject("select * " + "from foo " + "limit ? %s offset ?", comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * " + "from foo " + "limit $1 %s offset $2", comment))));
+ assertThat(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ injector.inject(
+ "select * "
+ + "from foo "
+ + "where col1=? "
+ + "and col2 like ? "
+ + " %s "
+ + "and col3 > ? "
+ + "and col4 < ? "
+ + "and col5 != ? "
+ + "and col6 not in (?, ?, ?) "
+ + "and col7 in (?, ?, ?) "
+ + "and col8 between ? and ?",
+ comment))
+ .sqlWithNamedParameters,
+ is(
+ equalTo(
+ injector.inject(
+ "select * "
+ + "from foo "
+ + "where col1=$1 "
+ + "and col2 like $2 "
+ + " %s "
+ + "and col3 > $3 "
+ + "and col4 < $4 "
+ + "and col5 != $5 "
+ + "and col6 not in ($6, $7, $8) "
+ + "and col7 in ($9, $10, $11) "
+ + "and col8 between $12 and $13",
+ comment))));
+ }
+ }
}
@Test
@@ -1277,6 +1354,11 @@ public void testPostgreSQLGetQueryParameters() {
assertEquals(
ImmutableSet.of("$1"),
parser.getQueryParameters("select '$2' from foo where bar=$1 and baz=$foo"));
+ assertEquals(
+ ImmutableSet.of("$1"),
+ parser.getQueryParameters(
+ "/* @lock_scanned_ranges = exclusive */ select -- random comment\n '$2' "
+ + "from foo /* comment /* with nested comment */ outside of nested comment */ where bar=$1 and baz=$foo"));
}
@Test
@@ -1488,6 +1570,45 @@ public void testPostgreSQLReturningClause() {
parser.parse(Statement.of("insert into t1 select 10.returning*")).hasReturningClause());
}
+ int skipSingleLineComment(String sql, int startIndex) {
+ return PostgreSQLStatementParser.skipSingleLineComment(sql, startIndex, null);
+ }
+
+ int skipMultiLineComment(String sql, int startIndex) {
+ return PostgreSQLStatementParser.skipMultiLineComment(sql, startIndex, null);
+ }
+
+ @Test
+ public void testSkipSingleLineComment() {
+ assumeTrue(dialect == Dialect.POSTGRESQL);
+
+ assertEquals(7, skipSingleLineComment("-- foo\n", 0));
+ assertEquals(7, skipSingleLineComment("-- foo\nbar", 0));
+ assertEquals(6, skipSingleLineComment("-- foo", 0));
+ assertEquals(11, skipSingleLineComment("bar -- foo\n", 4));
+ assertEquals(11, skipSingleLineComment("bar -- foo\nbar", 4));
+ assertEquals(10, skipSingleLineComment("bar -- foo", 4));
+ }
+
+ @Test
+ public void testSkipMultiLineComment() {
+ assumeTrue(dialect == Dialect.POSTGRESQL);
+
+ assertEquals(9, skipMultiLineComment("/* foo */", 0));
+ assertEquals(9, skipMultiLineComment("/* foo */ bar", 0));
+ assertEquals(6, skipMultiLineComment("/* foo", 0));
+ assertEquals(8, skipMultiLineComment("/* foo *", 0));
+ assertEquals(9, skipMultiLineComment("/* foo **", 0));
+ assertEquals(10, skipMultiLineComment("/* foo **/ ", 0));
+ assertEquals(13, skipMultiLineComment("bar /* foo */", 4));
+ assertEquals(13, skipMultiLineComment("bar /* foo */bar", 4));
+ assertEquals(10, skipMultiLineComment("bar /* foo", 4));
+
+ assertEquals(
+ "/* foo /* inner comment */ not in inner comment */".length(),
+ skipMultiLineComment("/* foo /* inner comment */ not in inner comment */ bar", 0));
+ }
+
private void assertUnclosedLiteral(String sql) {
try {
parser.convertPositionalParametersToNamedParameters('?', sql);
From fbf1df9f3fb12faaead8634b88fd4843cbdedf5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Wed, 30 Aug 2023 17:04:30 +0200
Subject: [PATCH 05/10] feat: support PostgreSQL for autoConfigEmulator (#2601)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: support PostgreSQL for autoConfigEmulator
The autoConfigEmulator=true flag in the Connection API can be used to
automatically connect to the emulator and automatically create the
instance and database that is being referenced. This makes running a
quick test on the emulator much easier, as all you need to do is to
configure the correct (JDBC) connection URL, and it will automatically
work. This mode would always create a GoogleSQL database. This change
adds support for creating a PostgreSQL database if the user specifically
sets the dialect in the connection string.
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---------
Co-authored-by: Owl Bot
---
.../spanner/connection/ConnectionImpl.java | 3 +-
.../spanner/connection/ConnectionOptions.java | 20 ++-
.../spanner/connection/EmulatorUtil.java | 8 +-
.../spanner/connection/EmulatorUtilTest.java | 122 ++++++++++++------
4 files changed, 107 insertions(+), 46 deletions(-)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
index b5d4cdf8a2a..7cdfb9be52d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
@@ -249,7 +249,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.options = options;
this.spanner = spannerPool.getSpanner(options, this);
if (options.isAutoConfigEmulator()) {
- EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId());
+ EmulatorUtil.maybeCreateInstanceAndDatabase(
+ spanner, options.getDatabaseId(), options.getDialect());
}
this.dbClient = spanner.getDatabaseClient(options.getDatabaseId());
this.batchClient = spanner.getBatchClient(options.getDatabaseId());
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
index e7209eeab91..a5753eca8ca 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
@@ -26,6 +26,7 @@
import com.google.cloud.NoCredentials;
import com.google.cloud.ServiceOptions;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.SessionPoolOptions;
@@ -307,7 +308,9 @@ public String[] getValidValues() {
ConnectionProperty.createBooleanProperty("returnCommitStats", "", false),
ConnectionProperty.createBooleanProperty(
"autoConfigEmulator",
- "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). The instance and database in the connection string will automatically be created if these do not yet exist on the emulator.",
+ "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). "
+ + "The instance and database in the connection string will automatically be created if these do not yet exist on the emulator. "
+ + "Add dialect=postgresql to the connection string to make sure that the database that is created uses the PostgreSQL dialect.",
false),
ConnectionProperty.createBooleanProperty(
LENIENT_PROPERTY_NAME,
@@ -317,7 +320,8 @@ public String[] getValidValues() {
RPC_PRIORITY_NAME,
"Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH."),
ConnectionProperty.createStringProperty(
- DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."),
+ DIALECT_PROPERTY_NAME,
+ "Sets the dialect to use for new databases that are created by this connection."),
ConnectionProperty.createStringProperty(
DATABASE_ROLE_PROPERTY_NAME,
"Sets the database role to use for this connection. The default is privileges assigned to IAM role"),
@@ -626,6 +630,7 @@ public static Builder newBuilder() {
private final QueryOptions queryOptions;
private final boolean returnCommitStats;
private final boolean autoConfigEmulator;
+ private final Dialect dialect;
private final RpcPriority rpcPriority;
private final boolean delayTransactionStartUntilFirstWrite;
private final boolean trackSessionLeaks;
@@ -677,6 +682,7 @@ private ConnectionOptions(Builder builder) {
this.queryOptions = queryOptionsBuilder.build();
this.returnCommitStats = parseReturnCommitStats(this.uri);
this.autoConfigEmulator = parseAutoConfigEmulator(this.uri);
+ this.dialect = parseDialect(this.uri);
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);
this.rpcPriority = parseRPCPriority(this.uri);
@@ -939,6 +945,12 @@ static boolean parseAutoConfigEmulator(String uri) {
return Boolean.parseBoolean(value);
}
+ @VisibleForTesting
+ static Dialect parseDialect(String uri) {
+ String value = parseUriProperty(uri, DIALECT_PROPERTY_NAME);
+ return value != null ? Dialect.valueOf(value.toUpperCase()) : Dialect.GOOGLE_STANDARD_SQL;
+ }
+
@VisibleForTesting
static boolean parseLenient(String uri) {
String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME);
@@ -1259,6 +1271,10 @@ public boolean isAutoConfigEmulator() {
return autoConfigEmulator;
}
+ public Dialect getDialect() {
+ return dialect;
+ }
+
/** The {@link RpcPriority} to use for the connection. */
RpcPriority getRPCPriority() {
return rpcPriority;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
index ee291642031..d850c3d3839 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java
@@ -18,6 +18,7 @@
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.InstanceConfigId;
import com.google.cloud.spanner.InstanceInfo;
@@ -41,8 +42,10 @@ class EmulatorUtil {
*
* @param spanner a {@link Spanner} instance that connects to an emulator instance
* @param databaseId the id of the instance and the database to create
+ * @param dialect the {@link Dialect} to use for the database to create
*/
- static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseId) {
+ static void maybeCreateInstanceAndDatabase(
+ Spanner spanner, DatabaseId databaseId, Dialect dialect) {
Preconditions.checkArgument(
NoCredentials.getInstance().equals(spanner.getOptions().getCredentials()));
try {
@@ -70,7 +73,8 @@ static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseI
.getDatabaseAdminClient()
.createDatabase(
databaseId.getInstanceId().getInstance(),
- databaseId.getDatabase(),
+ dialect.createDatabaseStatementFor(databaseId.getDatabase()),
+ dialect,
ImmutableList.of())
.get();
} catch (ExecutionException executionException) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
index b9c556a9605..db1b232a37f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java
@@ -16,8 +16,9 @@
package com.google.cloud.spanner.connection;
+import static com.google.cloud.spanner.connection.EmulatorUtil.maybeCreateInstanceAndDatabase;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -29,6 +30,7 @@
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseAdminClient;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Instance;
import com.google.cloud.spanner.InstanceAdminClient;
@@ -45,10 +47,18 @@
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class EmulatorUtilTest {
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
@Test
public void testCreateInstanceAndDatabase_bothSucceed()
@@ -75,12 +85,15 @@ public void testCreateInstanceAndDatabase_bothSucceed()
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get()).thenReturn(mock(Database.class));
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
+ maybeCreateInstanceAndDatabase(
+ spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
// Verify that both the instance and the database was created.
verify(instanceClient)
@@ -90,7 +103,12 @@ public void testCreateInstanceAndDatabase_bothSucceed()
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
.setNodeCount(1)
.build());
- verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
+ verify(databaseClient)
+ .createDatabase(
+ "test-instance",
+ dialect.createDatabaseStatementFor("test-database"),
+ dialect,
+ ImmutableList.of());
}
@Test
@@ -122,7 +140,10 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get())
.thenThrow(
@@ -130,8 +151,8 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
SpannerExceptionFactory.newSpannerException(
ErrorCode.ALREADY_EXISTS, "Database already exists")));
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
+ maybeCreateInstanceAndDatabase(
+ spanner, DatabaseId.of("test-project", "test-instance", "test-database"), dialect);
// Verify that both the instance and the database was created.
verify(instanceClient)
@@ -141,7 +162,12 @@ public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists()
.setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config"))
.setNodeCount(1)
.build());
- verify(databaseClient).createDatabase("test-instance", "test-database", ImmutableList.of());
+ verify(databaseClient)
+ .createDatabase(
+ "test-instance",
+ dialect.createDatabaseStatementFor("test-database"),
+ dialect,
+ ImmutableList.of());
}
@Test
@@ -166,13 +192,15 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnInstanceCreatio
SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid instance options")));
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
}
@Test
@@ -193,13 +221,15 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnInstanceCreation
.thenReturn(instanceOperationFuture);
when(instanceOperationFuture.get()).thenThrow(new InterruptedException());
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
}
@Test
@@ -227,7 +257,10 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreatio
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get())
.thenThrow(
@@ -235,13 +268,15 @@ public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreatio
SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid database options")));
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
}
@Test
@@ -269,16 +304,21 @@ public void testCreateInstanceAndDatabase_propagatesInterruptsOnDatabaseCreation
when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient);
when(databaseClient.createDatabase(
- eq("test-instance"), eq("test-database"), eq(ImmutableList.of())))
+ eq("test-instance"),
+ eq(dialect.createDatabaseStatementFor("test-database")),
+ eq(dialect),
+ eq(ImmutableList.of())))
.thenReturn(databaseOperationFuture);
when(databaseOperationFuture.get()).thenThrow(new InterruptedException());
- try {
- EmulatorUtil.maybeCreateInstanceAndDatabase(
- spanner, DatabaseId.of("test-project", "test-instance", "test-database"));
- fail("missing expected exception");
- } catch (SpannerException e) {
- assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
- }
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () ->
+ maybeCreateInstanceAndDatabase(
+ spanner,
+ DatabaseId.of("test-project", "test-instance", "test-database"),
+ dialect));
+ assertEquals(ErrorCode.CANCELLED, exception.getErrorCode());
}
}
From 465df7bad12fbea7dbcf6dbabb1b29d088c42665 Mon Sep 17 00:00:00 2001
From: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com>
Date: Thu, 31 Aug 2023 18:39:29 +0530
Subject: [PATCH 06/10] fix: fix kokoro windows java8 ci (#2573)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore: fix kokoro windows java8 ci
* chore: update version
* chore: update file
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---------
Co-authored-by: Owl Bot
---
.kokoro/build.bat | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.kokoro/build.bat b/.kokoro/build.bat
index c896610a27d..067cf4a4c48 100644
--- a/.kokoro/build.bat
+++ b/.kokoro/build.bat
@@ -15,8 +15,4 @@
:: downstream client libraries before they are released.
:: See documentation in type-shell-output.bat
-set JAVA8_HOME=%JAVA_HOME:"=%
-choco install -y openjdk11
-set JAVA11_HOME=C:\Program Files\Eclipse Adoptium\jdk-11.0.18.10-hotspot\
-
"C:\Program Files\Git\bin\bash.exe" %~dp0build.sh
From 78470cbae673d0c168759057004f100120374e41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Wed, 6 Sep 2023 09:40:31 +0200
Subject: [PATCH 07/10] test: add a test for blind writes (#2595)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* test: add a test for blind writes
Adds a test for blind writes before executing a read or SQL statement in
a read/write transaction.
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---------
Co-authored-by: Owl Bot
---
.../spanner/InlineBeginTransactionTest.java | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
index eb3f65176b7..c7f5e396069 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java
@@ -69,6 +69,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -1359,6 +1360,55 @@ public void testInlinedBeginTxWithOnlyMutations() {
assertThat(countTransactionsStarted()).isEqualTo(1);
}
+ @Test
+ public void testInlinedBeginTxWithMutationsBeforeFailedSqlStatement() {
+ Statement insert = Statement.of("insert into foo (id) values (1)");
+ Statement update = Statement.of("update foo set value='Two' where id=2");
+ mockSpanner.putStatementResult(StatementResult.update(insert, 1L));
+ mockSpanner.putStatementResult(StatementResult.update(update, 1L));
+ // This error will be returned the first time the ExecuteSql method is called. The error is
+ // cleared after the first call, meaning that the second attempt will succeed.
+ mockSpanner.setExecuteSqlExecutionTime(
+ SimulatedExecutionTime.ofException(Status.ALREADY_EXISTS.asRuntimeException()));
+
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"));
+ AtomicInteger attempts = new AtomicInteger();
+ client
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ attempts.incrementAndGet();
+ // Buffer a blind write before executing a SQL statement.
+ transaction.buffer(
+ Collections.singletonList(
+ Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()));
+ try {
+ transaction.executeUpdate(insert);
+ } catch (SpannerException exception) {
+ assertEquals(ErrorCode.ALREADY_EXISTS, exception.getErrorCode());
+ // The error should only occur during the initial attempt.
+ assertEquals(1, attempts.get());
+ }
+ // We need to execute one more statement in the transaction in order to force a
+ // retry.
+ assertEquals(1L, transaction.executeUpdate(update));
+ return null;
+ });
+ // The transaction should be retried once.
+ assertEquals(2, attempts.get());
+ assertEquals(1, mockSpanner.countRequestsOfType(BeginTransactionRequest.class));
+ // We get 3 ExecuteSql requests:
+ // 1. The initial attempt that carries a BeginTransaction option.
+ // 2. The retry attempt that does not use a BeginTransaction option.
+ // 3. The second UPDATE statement in the transaction that is only executed during the retry.
+ assertEquals(3, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+ assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
+ CommitRequest commit = mockSpanner.getRequestsOfType(CommitRequest.class).get(0);
+ // The mutations should only be applied once.
+ assertEquals(1, commit.getMutationsCount());
+ }
+
@SuppressWarnings("resource")
@Test
public void testTransactionManagerInlinedBeginTx() {
From 1f850e93cfa6f61b85f42ebb7d02f7f5cd0b440b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Wed, 6 Sep 2023 09:43:09 +0200
Subject: [PATCH 08/10] chore: deduplicate code for batch updates (#2591)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore: deduplicate code for batch updates
De-duplicate code that is equal for both the sync and async version of
the executeBatchUpdate methods.
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---------
Co-authored-by: Owl Bot
---
.../spanner/connection/ConnectionImpl.java | 29 ++++---------------
1 file changed, 6 insertions(+), 23 deletions(-)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
index 7cdfb9be52d..674086e6cd0 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
@@ -1324,32 +1324,15 @@ public ResultSet analyzeUpdateStatement(
@Override
public long[] executeBatchUpdate(Iterable updates) {
- Preconditions.checkNotNull(updates);
- ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
- // Check that there are only DML statements in the input.
- List parsedStatements = new LinkedList<>();
- for (Statement update : updates) {
- ParsedStatement parsedStatement = getStatementParser().parse(update);
- switch (parsedStatement.getType()) {
- case UPDATE:
- parsedStatements.add(parsedStatement);
- break;
- case CLIENT_SIDE:
- case QUERY:
- case DDL:
- case UNKNOWN:
- default:
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.INVALID_ARGUMENT,
- "The batch update list contains a statement that is not an update statement: "
- + parsedStatement.getSqlWithoutComments());
- }
- }
- return get(internalExecuteBatchUpdateAsync(CallType.SYNC, parsedStatements));
+ return get(internalExecuteBatchUpdateAsync(CallType.SYNC, parseUpdateStatements(updates)));
}
@Override
public ApiFuture executeBatchUpdateAsync(Iterable updates) {
+ return internalExecuteBatchUpdateAsync(CallType.ASYNC, parseUpdateStatements(updates));
+ }
+
+ private List parseUpdateStatements(Iterable updates) {
Preconditions.checkNotNull(updates);
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
// Check that there are only DML statements in the input.
@@ -1371,7 +1354,7 @@ public ApiFuture executeBatchUpdateAsync(Iterable updates) {
+ parsedStatement.getSqlWithoutComments());
}
}
- return internalExecuteBatchUpdateAsync(CallType.ASYNC, parsedStatements);
+ return parsedStatements;
}
private QueryOption[] mergeDataBoost(QueryOption... options) {
From 2851ce8880b69c1beaa5da02c30efc74bf2bdfeb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Wed, 6 Sep 2023 10:36:26 +0200
Subject: [PATCH 09/10] chore: add execute method with allowed return types
(#2592)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore: add execute method with allowed return types
Adds an execute method to the Connection API that allows the caller to
specify the allowed result types. This can be used by driver
implementations, such as JDBC, to use a single execute method in the
Connection API, while still making sure that the method only executes
statements that it should.
The current executeUpdate method in the Connection API does not overlap
completely with semantics of executeUpdate in JDBC, as JDBC allows any
statement type that does not return a ResultSet to be executed with that
method. The Connection API only allows statements that return an update
count.
Instead of modifying the executeUpdate method in the Connection API to
match the semantics of JDBC (which would be a breaking change), this
method can be used generically for all execute*** methods in the JDBC
driver, which again can be used to clean up some code there.
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
* fix: bad merge for clirr
---------
Co-authored-by: Owl Bot
---
.../clirr-ignored-differences.xml | 12 ++
.../cloud/spanner/connection/Connection.java | 30 +++++
.../spanner/connection/ConnectionImpl.java | 64 ++++++++-
.../connection/ConnectionImplTest.java | 114 ++++++++++++++++
.../connection/ExceptionMockServerTest.java | 125 ++++++++++++++++++
5 files changed, 344 insertions(+), 1 deletion(-)
diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index c0ddc050ae5..42039d2d2b9 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -416,6 +416,18 @@
com/google/cloud/spanner/connection/Connection
void setMaxPartitions(int)
+
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ com.google.cloud.spanner.connection.StatementResult execute(com.google.cloud.spanner.Statement, java.util.Set)
+
+
7012
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
index 8a8bd57f1c6..e58610b7d03 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
@@ -41,6 +41,7 @@
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ResultSetStats;
import java.util.Iterator;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -949,6 +950,35 @@ default boolean isDelayTransactionStartUntilFirstWrite() {
*/
StatementResult execute(Statement statement);
+ /**
+ * Executes the given statement if allowed in the current {@link TransactionMode} and connection
+ * state, and if the result that would be returned is in the set of allowed result types. The
+ * statement will not be sent to Cloud Spanner if the result type would not be allowed. This
+ * method can be used by drivers that must limit the type of statements that are allowed for a
+ * given method, e.g. for the {@link java.sql.Statement#executeQuery(String)} and {@link
+ * java.sql.Statement#executeUpdate(String)} methods.
+ *
+ * The returned value depends on the type of statement:
+ *
+ *
+ * - Queries and DML statements with returning clause will return a {@link ResultSet}.
+ *
- Simple DML statements will return an update count
+ *
- DDL statements will return a {@link ResultType#NO_RESULT}
+ *
- Connection and transaction statements (SET AUTOCOMMIT=TRUE|FALSE, SHOW AUTOCOMMIT, SET
+ * TRANSACTION READ ONLY, etc) will return either a {@link ResultSet} or {@link
+ * ResultType#NO_RESULT}, depending on the type of statement (SHOW or SET)
+ *
+ *
+ * @param statement The statement to execute
+ * @param allowedResultTypes The result types that this method may return. The statement will not
+ * be sent to Cloud Spanner if the statement would return a result that is not one of the
+ * types in this set.
+ * @return the result of the statement
+ */
+ default StatementResult execute(Statement statement, Set allowedResultTypes) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
/**
* Executes the given statement if allowed in the current {@link TransactionMode} and connection
* state asynchronously. The returned value depends on the type of statement:
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
index 674086e6cd0..566a93e76bc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
@@ -47,6 +47,7 @@
import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
import com.google.cloud.spanner.connection.StatementExecutor.StatementTimeout;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState;
import com.google.common.annotations.VisibleForTesting;
@@ -60,12 +61,15 @@
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
import org.threeten.bp.Instant;
/** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */
@@ -940,9 +944,20 @@ public void rollbackToSavepoint(String name) {
@Override
public StatementResult execute(Statement statement) {
- Preconditions.checkNotNull(statement);
+ return internalExecute(Preconditions.checkNotNull(statement), null);
+ }
+
+ @Override
+ public StatementResult execute(Statement statement, Set allowedResultTypes) {
+ return internalExecute(
+ Preconditions.checkNotNull(statement), Preconditions.checkNotNull(allowedResultTypes));
+ }
+
+ private StatementResult internalExecute(
+ Statement statement, @Nullable Set allowedResultTypes) {
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
ParsedStatement parsedStatement = getStatementParser().parse(statement, this.queryOptions);
+ checkResultTypeAllowed(parsedStatement, allowedResultTypes);
switch (parsedStatement.getType()) {
case CLIENT_SIDE:
return parsedStatement
@@ -969,6 +984,53 @@ public StatementResult execute(Statement statement) {
"Unknown statement: " + parsedStatement.getSqlWithoutComments());
}
+ @VisibleForTesting
+ static void checkResultTypeAllowed(
+ ParsedStatement parsedStatement, @Nullable Set allowedResultTypes) {
+ if (allowedResultTypes == null) {
+ return;
+ }
+ ResultType resultType = getResultType(parsedStatement);
+ if (!allowedResultTypes.contains(resultType)) {
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT,
+ "This statement returns a result of type "
+ + resultType
+ + ". Only statements that return a result of one of the following types are allowed: "
+ + allowedResultTypes.stream()
+ .map(ResultType::toString)
+ .collect(Collectors.joining(", ")));
+ }
+ }
+
+ private static ResultType getResultType(ParsedStatement parsedStatement) {
+ switch (parsedStatement.getType()) {
+ case CLIENT_SIDE:
+ if (parsedStatement.getClientSideStatement().isQuery()) {
+ return ResultType.RESULT_SET;
+ } else if (parsedStatement.getClientSideStatement().isUpdate()) {
+ return ResultType.UPDATE_COUNT;
+ } else {
+ return ResultType.NO_RESULT;
+ }
+ case QUERY:
+ return ResultType.RESULT_SET;
+ case UPDATE:
+ if (parsedStatement.hasReturningClause()) {
+ return ResultType.RESULT_SET;
+ } else {
+ return ResultType.UPDATE_COUNT;
+ }
+ case DDL:
+ return ResultType.NO_RESULT;
+ case UNKNOWN:
+ default:
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT,
+ "Unknown statement: " + parsedStatement.getSqlWithoutComments());
+ }
+ }
+
@Override
public AsyncStatementResult executeAsync(Statement statement) {
Preconditions.checkNotNull(statement);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
index 09d2f92a6dc..7c5ba508e8f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java
@@ -20,6 +20,7 @@
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.SELECT;
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.UPDATE;
import static com.google.cloud.spanner.connection.AbstractConnectionImplTest.expectSpannerException;
+import static com.google.cloud.spanner.connection.ConnectionImpl.checkResultTypeAllowed;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
@@ -28,6 +29,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
@@ -73,6 +75,7 @@
import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState;
+import com.google.common.collect.ImmutableSet;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ResultSetStats;
@@ -1624,4 +1627,115 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery) {
assertNull(connection.getTransactionTag());
}
}
+
+ @Test
+ public void testCheckResultTypeAllowed() {
+ AbstractStatementParser parser =
+ AbstractStatementParser.getInstance(Dialect.GOOGLE_STANDARD_SQL);
+ String query = "select * from foo";
+ String dml = "update foo set bar=1 where true";
+ String dmlReturning = "insert into foo (id, value) values (1, 'One') then return id";
+ String ddl = "create table foo";
+ String set = "set readonly=true";
+ String show = "show variable readonly";
+ String start = "start batch dml";
+
+ // null means all statements should be allowed.
+ ImmutableSet allowedResultTypes = null;
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of();
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.UPDATE_COUNT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.NO_RESULT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET, ResultType.UPDATE_COUNT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, ddl, allowedResultTypes);
+ assertThrowResultNotAllowed(parser, set, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, start, allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.RESULT_SET, ResultType.NO_RESULT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dml, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes = ImmutableSet.of(ResultType.UPDATE_COUNT, ResultType.NO_RESULT);
+ assertThrowResultNotAllowed(parser, query, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, dmlReturning, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ assertThrowResultNotAllowed(parser, show, allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+
+ allowedResultTypes =
+ ImmutableSet.of(ResultType.RESULT_SET, ResultType.UPDATE_COUNT, ResultType.NO_RESULT);
+ checkResultTypeAllowed(parser.parse(Statement.of(query)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dml)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(dmlReturning)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(ddl)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(set)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(show)), allowedResultTypes);
+ checkResultTypeAllowed(parser.parse(Statement.of(start)), allowedResultTypes);
+ }
+
+ private void assertThrowResultNotAllowed(
+ AbstractStatementParser parser, String sql, ImmutableSet allowedResultTypes) {
+ SpannerException exception =
+ assertThrows(
+ SpannerException.class,
+ () -> checkResultTypeAllowed(parser.parse(Statement.of(sql)), allowedResultTypes));
+ assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode());
+ assertTrue(
+ exception.getMessage(),
+ exception
+ .getMessage()
+ .contains(
+ "Only statements that return a result of one of the following types are allowed"));
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
index 4b239e4cf8c..fcc05405b2d 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ExceptionMockServerTest.java
@@ -22,11 +22,18 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.StatementResult.ResultType;
+import com.google.common.collect.ImmutableSet;
+import com.google.longrunning.Operation;
+import com.google.protobuf.Any;
+import com.google.protobuf.Empty;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.util.Arrays;
@@ -152,4 +159,122 @@ public void testCommitException() {
assertEquals(0, exception.getSuppressed().length);
}
}
+
+ @Test
+ public void testAllowedResultType() {
+ mockSpanner.putStatementResult(
+ StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT));
+ mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, 1L));
+ mockSpanner.putStatementResult(
+ StatementResult.updateReturning(
+ INSERT_RETURNING_STATEMENT, SELECT_COUNT_RESULTSET_AFTER_INSERT));
+ mockDatabaseAdmin.addResponse(
+ Operation.newBuilder()
+ .setDone(true)
+ .setResponse(Any.pack(Empty.getDefaultInstance()))
+ .setMetadata(Any.pack(UpdateDatabaseDdlMetadata.getDefaultInstance()))
+ .build());
+ Statement ddl = Statement.of("create table foo");
+
+ try (Connection connection = createConnection()) {
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(SELECT_COUNT_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(SELECT_COUNT_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET))
+ .getResultSet());
+
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(INSERT_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT))
+ .getUpdateCount());
+
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(INSERT_RETURNING_STATEMENT, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () ->
+ connection.execute(
+ INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.NO_RESULT)))
+ .getErrorCode());
+ assertNotNull(
+ connection
+ .execute(INSERT_RETURNING_STATEMENT, ImmutableSet.of(ResultType.RESULT_SET))
+ .getResultSet());
+
+ // Commit the current transaction and switch to autocommit to allow DDL.
+ connection.commit();
+ connection.setAutocommit(true);
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(SpannerException.class, () -> connection.execute(ddl, ImmutableSet.of()))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(ddl, ImmutableSet.of(ResultType.RESULT_SET)))
+ .getErrorCode());
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT,
+ assertThrows(
+ SpannerException.class,
+ () -> connection.execute(ddl, ImmutableSet.of(ResultType.UPDATE_COUNT)))
+ .getErrorCode());
+ assertNotNull(connection.execute(ddl, ImmutableSet.of(ResultType.NO_RESULT)));
+ }
+ }
}
From 823582c73a7185711ce5aa256e36da88afe0df03 Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Fri, 8 Sep 2023 15:17:21 +0200
Subject: [PATCH 10/10] chore(main): release 6.46.0 (#2598)
Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
---
CHANGELOG.md | 18 +++++++++++++++++
google-cloud-spanner-bom/pom.xml | 20 +++++++++----------
google-cloud-spanner-executor/pom.xml | 4 ++--
google-cloud-spanner/pom.xml | 4 ++--
.../pom.xml | 4 ++--
.../pom.xml | 4 ++--
grpc-google-cloud-spanner-v1/pom.xml | 4 ++--
pom.xml | 16 +++++++--------
.../pom.xml | 4 ++--
.../pom.xml | 4 ++--
proto-google-cloud-spanner-v1/pom.xml | 4 ++--
samples/snapshot/pom.xml | 2 +-
versions.txt | 16 +++++++--------
13 files changed, 61 insertions(+), 43 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ddfdfc4972..ac8a686719a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## [6.46.0](https://github.com/googleapis/java-spanner/compare/v6.45.3...v6.46.0) (2023-09-06)
+
+
+### Features
+
+* Adding support for databoost ([#2505](https://github.com/googleapis/java-spanner/issues/2505)) ([dd3e9a0](https://github.com/googleapis/java-spanner/commit/dd3e9a0fe4846edcab9501b71c3d9e0fa24ed75b))
+* Support PostgreSQL for autoConfigEmulator ([#2601](https://github.com/googleapis/java-spanner/issues/2601)) ([fbf1df9](https://github.com/googleapis/java-spanner/commit/fbf1df9f3fb12faaead8634b88fd4843cbdedf5b))
+
+
+### Bug Fixes
+
+* Fix kokoro windows java8 ci ([#2573](https://github.com/googleapis/java-spanner/issues/2573)) ([465df7b](https://github.com/googleapis/java-spanner/commit/465df7bad12fbea7dbcf6dbabb1b29d088c42665))
+
+
+### Documentation
+
+* Add sample for transaction timeouts ([#2599](https://github.com/googleapis/java-spanner/issues/2599)) ([59cec9b](https://github.com/googleapis/java-spanner/commit/59cec9b9cdad169bd8de8ab7b264b04150dda7fb))
+
## [6.45.3](https://github.com/googleapis/java-spanner/compare/v6.45.2...v6.45.3) (2023-08-17)
diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml
index 046ac7ce06d..f1389078ea2 100644
--- a/google-cloud-spanner-bom/pom.xml
+++ b/google-cloud-spanner-bom/pom.xml
@@ -3,7 +3,7 @@
4.0.0
com.google.cloud
google-cloud-spanner-bom
- 6.45.4-SNAPSHOT
+ 6.46.0
pom
com.google.cloud
@@ -53,48 +53,48 @@
com.google.cloud
google-cloud-spanner
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.cloud
google-cloud-spanner-executor
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.cloud
google-cloud-spanner
test-jar
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml
index 961c6aff39a..7d166579c08 100644
--- a/google-cloud-spanner-executor/pom.xml
+++ b/google-cloud-spanner-executor/pom.xml
@@ -5,14 +5,14 @@
4.0.0
com.google.cloud
google-cloud-spanner-executor
- 6.45.4-SNAPSHOT
+ 6.46.0
jar
Google Cloud Spanner Executor
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml
index 3a7b71545a4..6b80663eead 100644
--- a/google-cloud-spanner/pom.xml
+++ b/google-cloud-spanner/pom.xml
@@ -3,7 +3,7 @@
4.0.0
com.google.cloud
google-cloud-spanner
- 6.45.4-SNAPSHOT
+ 6.46.0
jar
Google Cloud Spanner
https://github.com/googleapis/java-spanner
@@ -11,7 +11,7 @@
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
google-cloud-spanner
diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml
index 7475a8c7906..efbc3323034 100644
--- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml
+++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
grpc-google-cloud-spanner-admin-database-v1
GRPC library for grpc-google-cloud-spanner-admin-database-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
index e0fd32cded1..07a676637d4 100644
--- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
+++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
grpc-google-cloud-spanner-admin-instance-v1
GRPC library for grpc-google-cloud-spanner-admin-instance-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml
index f6ac071fec9..c87f44d3c78 100644
--- a/grpc-google-cloud-spanner-v1/pom.xml
+++ b/grpc-google-cloud-spanner-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
grpc-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
grpc-google-cloud-spanner-v1
GRPC library for grpc-google-cloud-spanner-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/pom.xml b/pom.xml
index 96d018c0dec..0274cdc4543 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.google.cloud
google-cloud-spanner-parent
pom
- 6.45.4-SNAPSHOT
+ 6.46.0
Google Cloud Spanner Parent
https://github.com/googleapis/java-spanner
@@ -62,37 +62,37 @@
com.google.api.grpc
proto-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
proto-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.api.grpc
grpc-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
com.google.cloud
google-cloud-spanner
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml
index aa68fa8c1b1..cf0137536c0 100644
--- a/proto-google-cloud-spanner-admin-database-v1/pom.xml
+++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-admin-database-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
proto-google-cloud-spanner-admin-database-v1
PROTO library for proto-google-cloud-spanner-admin-database-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml
index 35db7a3b902..fb7fc00a3a3 100644
--- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml
+++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-admin-instance-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
proto-google-cloud-spanner-admin-instance-v1
PROTO library for proto-google-cloud-spanner-admin-instance-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml
index 9f473df9328..666edb481bc 100644
--- a/proto-google-cloud-spanner-v1/pom.xml
+++ b/proto-google-cloud-spanner-v1/pom.xml
@@ -4,13 +4,13 @@
4.0.0
com.google.api.grpc
proto-google-cloud-spanner-v1
- 6.45.4-SNAPSHOT
+ 6.46.0
proto-google-cloud-spanner-v1
PROTO library for proto-google-cloud-spanner-v1
com.google.cloud
google-cloud-spanner-parent
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 6d27e3bc059..b5e84778899 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -31,7 +31,7 @@
com.google.cloud
google-cloud-spanner
- 6.45.4-SNAPSHOT
+ 6.46.0
diff --git a/versions.txt b/versions.txt
index ea31ae7356c..0b08d5572ee 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,11 +1,11 @@
# Format:
# module:released-version:current-version
-proto-google-cloud-spanner-admin-instance-v1:6.45.3:6.45.4-SNAPSHOT
-proto-google-cloud-spanner-v1:6.45.3:6.45.4-SNAPSHOT
-proto-google-cloud-spanner-admin-database-v1:6.45.3:6.45.4-SNAPSHOT
-grpc-google-cloud-spanner-v1:6.45.3:6.45.4-SNAPSHOT
-grpc-google-cloud-spanner-admin-instance-v1:6.45.3:6.45.4-SNAPSHOT
-grpc-google-cloud-spanner-admin-database-v1:6.45.3:6.45.4-SNAPSHOT
-google-cloud-spanner:6.45.3:6.45.4-SNAPSHOT
-google-cloud-spanner-executor:6.45.3:6.45.4-SNAPSHOT
+proto-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.0
+proto-google-cloud-spanner-v1:6.46.0:6.46.0
+proto-google-cloud-spanner-admin-database-v1:6.46.0:6.46.0
+grpc-google-cloud-spanner-v1:6.46.0:6.46.0
+grpc-google-cloud-spanner-admin-instance-v1:6.46.0:6.46.0
+grpc-google-cloud-spanner-admin-database-v1:6.46.0:6.46.0
+google-cloud-spanner:6.46.0:6.46.0
+google-cloud-spanner-executor:6.46.0:6.46.0