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 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/README.md b/README.md index 0b4e29261f8..5f5972cf193 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.45.2' +implementation 'com.google.cloud:google-cloud-spanner:6.45.3' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.45.2" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.45.3" ``` @@ -320,6 +320,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/ | Statement Timeout Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java) | | Tag Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/TagSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/TagSample.java) | | Tracing Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/TracingSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/TracingSample.java) | +| Transaction Timeout Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/TransactionTimeoutExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/TransactionTimeoutExample.java) | | Update Database Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateDatabaseSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateDatabaseSample.java) | | Update Database With Default Leader Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateDatabaseWithDefaultLeaderSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateDatabaseWithDefaultLeaderSample.java) | | Update Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateInstanceConfigSample.java) | @@ -430,7 +431,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.45.2 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.45.3 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index a15b6b248eb..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.3 + 6.46.0 pom com.google.cloud @@ -53,48 +53,48 @@ com.google.cloud google-cloud-spanner - 6.45.3 + 6.46.0 com.google.cloud google-cloud-spanner-executor - 6.45.3 + 6.46.0 com.google.cloud google-cloud-spanner test-jar - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.45.3 + 6.46.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.45.3 + 6.46.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.45.3 + 6.46.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.45.3 + 6.46.0 diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index d38f27154e4..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.3 + 6.46.0 jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.45.3 + 6.46.0 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/pom.xml b/google-cloud-spanner/pom.xml index 464d087ef7a..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.3 + 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.3 + 6.46.0 google-cloud-spanner diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 0d804bfd933..2bd35ec7853 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -16,7 +16,6 @@ package com.google.cloud.spanner; -import com.google.api.core.BetaApi; import com.google.common.base.Preconditions; import com.google.spanner.v1.RequestOptions.Priority; import java.io.Serializable; @@ -156,11 +155,9 @@ public static ListOption pageSize(int pageSize) { } /** - * If this is for a partitioned read and query and this field is set to `true`, the request will - * be executed via Spanner independent compute resources. The method is available in Beta mode - * (and is not generally available now). + * If this is for PartitionedRead or PartitionedQuery and this field is set to `true`, the request + * will be executed via Spanner independent compute resources. */ - @BetaApi public static DataBoostQueryOption dataBoostEnabled(Boolean dataBoostEnabled) { return new DataBoostQueryOption(dataBoostEnabled); } 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/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 b5d4cdf8a2a..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). */ @@ -249,7 +253,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()); @@ -939,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 @@ -968,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); @@ -1323,32 +1386,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. @@ -1370,7 +1416,7 @@ public ApiFuture executeBatchUpdateAsync(Iterable updates) { + parsedStatement.getSqlWithoutComments()); } } - return internalExecuteBatchUpdateAsync(CallType.ASYNC, parsedStatements); + return parsedStatements; } private QueryOption[] mergeDataBoost(QueryOption... options) { 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/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/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() { 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/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()); } } 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))); + } + } } 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); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java index 11be7df0430..6598a3dca76 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java @@ -258,8 +258,6 @@ public void readUsingIndex() { @Test public void dataBoostRead() { - // TODO: Remove the following check during GA - assumeFalse("DataBoost feature is not yet generally available", true); assumeFalse("Emulator does not support data boost read", isUsingEmulator()); BitSet seenRows = new BitSet(numRows); @@ -314,8 +312,6 @@ private PartitionOptions getRandomPartitionOptions() { @Test public void dataBoostQuery() { - // TODO: Remove the following check during GA - assumeFalse("DataBoost feature is not yet generally available", true); assumeFalse("Emulator does not support data boost query", isUsingEmulator()); BitSet seenRows = new BitSet(numRows); TimestampBound bound = getRandomBound(); diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index d9c30274fb1..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.3 + 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.3 + 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 6742ac954c4..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.3 + 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.3 + 6.46.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 0dec3e97221..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.3 + 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.3 + 6.46.0 diff --git a/pom.xml b/pom.xml index a7f2c7f2c72..0274cdc4543 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.45.3 + 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.3 + 6.46.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.45.3 + 6.46.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.45.3 + 6.46.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.45.3 + 6.46.0 com.google.cloud google-cloud-spanner - 6.45.3 + 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 14d72213756..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.3 + 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.3 + 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 6061e2162c2..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.3 + 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.3 + 6.46.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 3e3c0f2a759..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.3 + 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.3 + 6.46.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 7df52fb3301..b5e84778899 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 6.45.3 + 6.46.0 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 8f312776692..4731895c8e2 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -17,6 +17,7 @@ com.google.cloud.samples shared-configuration 1.2.0 + diff --git a/samples/snippets/src/main/java/com/example/spanner/TransactionTimeoutExample.java b/samples/snippets/src/main/java/com/example/spanner/TransactionTimeoutExample.java new file mode 100644 index 00000000000..c9b92c74fbb --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/TransactionTimeoutExample.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner; + +// [START spanner_transaction_timeout] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import io.grpc.Context; +import io.grpc.Context.CancellableContext; +import io.grpc.Deadline; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Sample showing how to set a timeout for an entire transaction for the Cloud Spanner Java client. + */ +class TransactionTimeoutExample { + + static void executeTransactionWithTimeout() { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + executeTransactionWithTimeout(projectId, instanceId, databaseId, 60L, TimeUnit.SECONDS); + } + + // Execute a read/write transaction with a timeout for the entire transaction. + static void executeTransactionWithTimeout( + String projectId, + String instanceId, + String databaseId, + long timeoutValue, + TimeUnit timeoutUnit) { + try (Spanner spanner = SpannerOptions.newBuilder().setProjectId(projectId).build() + .getService()) { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + // Create a gRPC context with a deadline and with cancellation. + // gRPC context deadlines require the use of a scheduled executor. + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + try (CancellableContext context = + Context.current() + .withDeadline(Deadline.after(timeoutValue, timeoutUnit), executor) + .withCancellation()) { + context.run( + () -> { + client + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet resultSet = + transaction.executeQuery( + Statement.of( + "SELECT SingerId, FirstName, LastName\n" + + "FROM Singers\n" + + "ORDER BY LastName, FirstName"))) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + String sql = + "INSERT INTO Singers (SingerId, FirstName, LastName)\n" + + "VALUES (20, 'George', 'Washington')"; + long rowCount = transaction.executeUpdate(Statement.of(sql)); + System.out.printf("%d record inserted.%n", rowCount); + return null; + }); + }); + } + } + } +} +// [END spanner_transaction_timeout] diff --git a/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java b/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java index 03291a91f61..7c059bec1f1 100644 --- a/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/SpannerStandaloneExamplesIT.java @@ -17,15 +17,20 @@ package com.example.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Instance; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Value; import com.google.common.collect.ImmutableList; @@ -36,6 +41,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -133,6 +139,33 @@ public void executeSqlWithTimeout_shouldWriteData() { assertThat(out).contains("1 record inserted."); } + @Test + public void testTransactionWithTimeout_shouldWriteData() { + String projectId = spanner.getOptions().getProjectId(); + String out = + runExample( + () -> + TransactionTimeoutExample.executeTransactionWithTimeout( + projectId, instanceId, databaseId, 60L, TimeUnit.SECONDS)); + assertTrue(out, out.contains("1 record inserted")); + } + + @Test + public void testTransactionWithTimeout_shouldFailWithDeadlineExceeded() { + String projectId = spanner.getOptions().getProjectId(); + // Execute a transaction with a 5 millisecond timeout. The transaction executes both a read, a + // write, and a commit operation. Each of these would normally take at least 5 milliseconds. + SpannerException exception = + assertThrows( + SpannerException.class, + () -> + runExample( + () -> + TransactionTimeoutExample.executeTransactionWithTimeout( + projectId, instanceId, databaseId, 5L, TimeUnit.MILLISECONDS))); + assertEquals(ErrorCode.DEADLINE_EXCEEDED, exception.getErrorCode()); + } + @Test public void addNumericColumn_shouldSuccessfullyAddColumn() throws InterruptedException, ExecutionException { diff --git a/versions.txt b/versions.txt index 4f05391afa5..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.3 -proto-google-cloud-spanner-v1:6.45.3:6.45.3 -proto-google-cloud-spanner-admin-database-v1:6.45.3:6.45.3 -grpc-google-cloud-spanner-v1:6.45.3:6.45.3 -grpc-google-cloud-spanner-admin-instance-v1:6.45.3:6.45.3 -grpc-google-cloud-spanner-admin-database-v1:6.45.3:6.45.3 -google-cloud-spanner:6.45.3:6.45.3 -google-cloud-spanner-executor:6.45.3:6.45.3 +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