From 03166fa185731e842c0ee45dabf295ed0051748d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 27 May 2020 16:36:51 +0200 Subject: [PATCH] refactor: use Connection API in client lib (#133) * refactor: use Connection API in client lib The generic Connection API has been moved to the client library. The JDBC driver still contained its own copy of this API. This change removes this own API and instead uses the one in the client library. * deps: remove unused dependencies * deps: revert to libraries-bom 5.2.0 * deps: exclude grpc-api to prevent version conflicts * deps: more exclusions * fix: exclude dependency to prevent conflicts --- clirr-ignored-differences.xml | 45 + pom.xml | 179 +- .../spanner/jdbc/AbstractBaseUnitOfWork.java | 162 -- .../spanner/jdbc/AbstractJdbcConnection.java | 7 +- .../spanner/jdbc/AbstractJdbcStatement.java | 3 +- .../jdbc/AbstractMultiUseTransaction.java | 96 - .../cloud/spanner/jdbc/AnalyzeMode.java | 52 - .../cloud/spanner/jdbc/AutocommitDmlMode.java | 42 - .../cloud/spanner/jdbc/ChecksumResultSet.java | 355 --- .../spanner/jdbc/ClientSideStatement.java | 63 - .../jdbc/ClientSideStatementExecutor.java | 54 - .../spanner/jdbc/ClientSideStatementImpl.java | 217 -- .../ClientSideStatementNoParamExecutor.java | 45 - .../jdbc/ClientSideStatementSetExecutor.java | 101 - .../ClientSideStatementValueConverter.java | 35 - .../ClientSideStatementValueConverters.java | 257 --- .../spanner/jdbc/ClientSideStatements.java | 51 - .../jdbc/CloudSpannerJdbcConnection.java | 36 +- .../google/cloud/spanner/jdbc/Connection.java | 727 ------ .../cloud/spanner/jdbc/ConnectionImpl.java | 1018 --------- .../cloud/spanner/jdbc/ConnectionOptions.java | 666 ------ .../spanner/jdbc/ConnectionPreconditions.java | 45 - .../jdbc/ConnectionStatementExecutor.java | 82 - .../jdbc/ConnectionStatementExecutorImpl.java | 247 --- .../spanner/jdbc/CredentialsService.java | 89 - .../google/cloud/spanner/jdbc/DdlBatch.java | 303 --- .../google/cloud/spanner/jdbc/DdlClient.java | 91 - .../spanner/jdbc/DirectExecuteResultSet.java | 365 ---- .../google/cloud/spanner/jdbc/DmlBatch.java | 193 -- .../cloud/spanner/jdbc/FailedBatchUpdate.java | 83 - .../cloud/spanner/jdbc/FailedQuery.java | 82 - .../cloud/spanner/jdbc/FailedUpdate.java | 67 - .../cloud/spanner/jdbc/JdbcConnection.java | 87 + .../cloud/spanner/jdbc/JdbcConstants.java | 1 - .../cloud/spanner/jdbc/JdbcDataSource.java | 1 + .../spanner/jdbc/JdbcDatabaseMetaData.java | 2 +- .../google/cloud/spanner/jdbc/JdbcDriver.java | 3 +- .../spanner/jdbc/JdbcPreparedStatement.java | 1 + .../cloud/spanner/jdbc/JdbcStatement.java | 2 + .../spanner/jdbc/ReadOnlyStalenessUtil.java | 261 --- .../spanner/jdbc/ReadOnlyTransaction.java | 178 -- .../spanner/jdbc/ReadWriteTransaction.java | 762 ------- .../jdbc/ReplaceableForwardingResultSet.java | 352 --- .../spanner/jdbc/RetriableBatchUpdate.java | 69 - .../cloud/spanner/jdbc/RetriableUpdate.java | 64 - .../spanner/jdbc/SingleUseTransaction.java | 505 ----- .../cloud/spanner/jdbc/SpannerPool.java | 413 +--- .../jdbc/StatementExecutionInterceptor.java | 40 - .../cloud/spanner/jdbc/StatementExecutor.java | 188 -- .../cloud/spanner/jdbc/StatementParser.java | 458 ---- .../cloud/spanner/jdbc/StatementResult.java | 103 - .../spanner/jdbc/StatementResultImpl.java | 187 -- .../cloud/spanner/jdbc/TransactionMode.java | 46 - .../jdbc/TransactionRetryListener.java | 22 +- .../google/cloud/spanner/jdbc/UnitOfWork.java | 182 -- .../cloud/spanner/jdbc/AbortedTest.java | 74 - .../jdbc/AbstractConnectionImplTest.java | 963 -------- .../spanner/jdbc/AbstractMockServerTest.java | 186 -- .../jdbc/AbstractSqlScriptVerifier.java | 453 ---- .../jdbc/AutocommitDmlModeConverterTest.java | 58 - .../spanner/jdbc/AutocommitDmlModeTest.java | 115 - .../spanner/jdbc/BooleanConverterTest.java | 51 - .../jdbc/ClientSideStatementsTest.java | 241 -- .../ConnectionImplAutocommitReadOnlyTest.java | 914 -------- ...ConnectionImplAutocommitReadWriteTest.java | 1325 ----------- .../ConnectionImplGeneratedSqlScriptTest.java | 124 -- .../spanner/jdbc/ConnectionImplTest.java | 1261 ----------- ...nnectionImplTransactionalReadOnlyTest.java | 1204 ---------- ...nectionImplTransactionalReadWriteTest.java | 1945 ----------------- .../spanner/jdbc/ConnectionOptionsTest.java | 389 ---- .../jdbc/ConnectionStatementExecutorTest.java | 199 -- ...nnectionStatementWithNoParametersTest.java | 167 -- ...nnectionStatementWithOneParameterTest.java | 180 -- .../cloud/spanner/jdbc/ConnectionTest.java | 98 - .../spanner/jdbc/CredentialsServiceTest.java | 85 - .../cloud/spanner/jdbc/DdlBatchTest.java | 544 ----- .../cloud/spanner/jdbc/DdlClientTest.java | 69 - .../jdbc/DirectExecuteResultSetTest.java | 255 --- .../cloud/spanner/jdbc/DmlBatchTest.java | 163 -- .../spanner/jdbc/DurationConverterTest.java | 86 - .../spanner/jdbc/ITAbstractJdbcTest.java | 7 +- .../spanner/jdbc/ITAbstractSpannerTest.java | 320 --- .../cloud/spanner/jdbc/ITConnectionImpl.java | 25 - .../jdbc/JdbcAbortedTransactionTest.java | 5 +- .../JdbcConnectionGeneratedSqlScriptTest.java | 9 +- .../spanner/jdbc/JdbcConnectionTest.java | 9 +- .../jdbc/JdbcDatabaseMetaDataTest.java | 2 + ...cDatabaseMetaDataWithMockedServerTest.java | 2 + .../cloud/spanner/jdbc/JdbcDriverTest.java | 62 +- .../cloud/spanner/jdbc/JdbcGrpcErrorTest.java | 1 + .../spanner/jdbc/JdbcParameterStoreTest.java | 17 +- .../jdbc/JdbcPreparedStatementTest.java | 1 + .../spanner/jdbc/JdbcQueryOptionsTest.java | 1 + .../spanner/jdbc/JdbcSqlScriptVerifier.java | 21 +- .../cloud/spanner/jdbc/JdbcStatementTest.java | 5 +- .../spanner/jdbc/JdbcTypeConverterTest.java | 1 + .../jdbc/RandomResultSetGenerator.java | 166 -- .../jdbc/ReadOnlyStalenessConverterTest.java | 166 -- .../spanner/jdbc/ReadOnlyStalenessTest.java | 199 -- .../jdbc/ReadOnlyStalenessUtilTest.java | 170 -- .../spanner/jdbc/ReadOnlyTransactionTest.java | 403 ---- .../jdbc/ReadWriteTransactionTest.java | 581 ----- .../ReplaceableForwardingResultSetTest.java | 310 --- .../SetReadOnlyStalenessSqlScriptTest.java | 47 - .../SetStatementTimeoutSqlScriptTest.java | 47 - .../jdbc/SingleUseTransactionTest.java | 740 ------- .../spanner/jdbc/SpannerExceptionMatcher.java | 65 - .../cloud/spanner/jdbc/SpannerPoolTest.java | 400 ---- .../cloud/spanner/jdbc/SqlScriptVerifier.java | 185 -- .../spanner/jdbc/SqlTestScriptsGenerator.java | 27 - .../spanner/jdbc/StatementParserTest.java | 723 ------ .../spanner/jdbc/StatementResultImplTest.java | 177 -- .../spanner/jdbc/StatementTimeoutTest.java | 1192 ---------- .../jdbc/TransactionModeConverterTest.java | 71 - .../spanner/jdbc/it/ITBulkConnectionTest.java | 88 - .../cloud/spanner/jdbc/it/ITDdlTest.java | 37 - .../cloud/spanner/jdbc/it/ITJdbcDdlTest.java | 2 +- .../jdbc/it/ITJdbcQueryOptionsTest.java | 2 +- .../spanner/jdbc/it/ITJdbcReadOnlyTest.java | 2 +- .../it/ITJdbcReadWriteAutocommitTest.java | 2 +- .../jdbc/it/ITJdbcSqlMusicScriptTest.java | 2 +- .../spanner/jdbc/it/ITJdbcSqlScriptTest.java | 2 +- .../jdbc/it/ITReadOnlySpannerTest.java | 227 -- .../it/ITReadWriteAutocommitSpannerTest.java | 179 -- .../spanner/jdbc/it/ITSqlMusicScriptTest.java | 205 -- .../spanner/jdbc/it/ITSqlScriptTest.java | 182 -- .../jdbc/it/ITTransactionModeTest.java | 187 -- .../jdbc/it/ITTransactionRetryTest.java | 1567 ------------- 128 files changed, 353 insertions(+), 29422 deletions(-) create mode 100644 clirr-ignored-differences.xml delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/AbstractBaseUnitOfWork.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/AbstractMultiUseTransaction.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/AnalyzeMode.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/AutocommitDmlMode.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ChecksumResultSet.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatement.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementExecutor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementImpl.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementNoParamExecutor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementSetExecutor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverter.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverters.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatements.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/Connection.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ConnectionImpl.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ConnectionOptions.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ConnectionPreconditions.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorImpl.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/CredentialsService.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/DdlBatch.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/DdlClient.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSet.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/DmlBatch.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/FailedBatchUpdate.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/FailedQuery.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/FailedUpdate.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtil.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyTransaction.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ReadWriteTransaction.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSet.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/RetriableBatchUpdate.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/RetriableUpdate.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/SingleUseTransaction.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/StatementExecutionInterceptor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/StatementExecutor.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/StatementParser.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/StatementResult.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/StatementResultImpl.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/TransactionMode.java delete mode 100644 src/main/java/com/google/cloud/spanner/jdbc/UnitOfWork.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AbortedTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AbstractConnectionImplTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AbstractMockServerTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AbstractSqlScriptVerifier.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeConverterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/BooleanConverterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ClientSideStatementsTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadOnlyTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadWriteTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplGeneratedSqlScriptTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadOnlyTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadWriteTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionOptionsTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithNoParametersTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithOneParameterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ConnectionTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/CredentialsServiceTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/DdlBatchTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/DdlClientTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSetTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/DmlBatchTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/DurationConverterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ITAbstractSpannerTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ITConnectionImpl.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetGenerator.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessConverterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtilTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyTransactionTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReadWriteTransactionTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSetTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SetReadOnlyStalenessSqlScriptTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SetStatementTimeoutSqlScriptTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SingleUseTransactionTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SpannerExceptionMatcher.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SpannerPoolTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SqlScriptVerifier.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/SqlTestScriptsGenerator.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/StatementParserTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/StatementResultImplTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/StatementTimeoutTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/TransactionModeConverterTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITBulkConnectionTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITDdlTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITReadOnlySpannerTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITReadWriteAutocommitSpannerTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlMusicScriptTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlScriptTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionModeTest.java delete mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionRetryTest.java diff --git a/clirr-ignored-differences.xml b/clirr-ignored-differences.xml new file mode 100644 index 00000000..cfa11365 --- /dev/null +++ b/clirr-ignored-differences.xml @@ -0,0 +1,45 @@ + + + + + + + 7012 + com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection + void addTransactionRetryListener(com.google.cloud.spanner.connection.TransactionRetryListener) + + + 7012 + com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection + boolean removeTransactionRetryListener(com.google.cloud.spanner.connection.TransactionRetryListener) + + + 7012 + com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection + java.util.Iterator getTransactionRetryListenersFromConnection() + + + 8001 + com/google/cloud/spanner/jdbc/ConnectionOptions$Builder + + + 8001 + com/google/cloud/spanner/jdbc/ConnectionOptions$ConnectionProperty + + + 8001 + com/google/cloud/spanner/jdbc/StatementResult$ClientSideStatementType + + + 8001 + com/google/cloud/spanner/jdbc/StatementResult$ResultType + + + 8001 + com/google/cloud/spanner/jdbc/UnitOfWork$Type + + + 8001 + com/google/cloud/spanner/jdbc/UnitOfWork$UnitOfWorkState + + diff --git a/pom.xml b/pom.xml index 0f6bf425..262795ea 100644 --- a/pom.xml +++ b/pom.xml @@ -58,225 +58,128 @@ google-cloud-spanner-jdbc - 1.29.0 - 1.30.9 - 1.93.4 - 1.56.0 - 1.34.2 - 29.0-android 4.13 - 1.18.0 - 3.11.4 + 1.53.0 + 3.0.2 1.4.4 - 1.9.0 - 1.52.0 + 1.0.1 + 1.10.19 2.2 - 2.3.4 - 0.20.0 - - io.grpc - grpc-bom - ${grpc.version} - pom - import - - - com.google.api-client - google-api-client-bom - ${api-client.version} - pom - import - com.google.cloud - google-cloud-core-bom - ${google.core.version} + libraries-bom + 5.3.0 pom import - - com.google.api - gax-bom - ${gax.version} - pom - import - - - com.google.http-client - google-http-client-bom - ${http-client-bom.version} - pom - import - - - com.google.guava - guava-bom - ${guava.version} - pom - import - - - com.google.auth - google-auth-library-bom - ${google.auth.version} - pom - import - - - com.google.errorprone - error_prone_annotations - ${error-prone.version} - - - com.google.protobuf - protobuf-java - ${protobuf.version} - - - org.hamcrest - hamcrest-core - ${hamcrest.version} - test - - + + io.grpc + grpc-api + + + com.google.cloud + google-cloud-core-grpc + com.google.api.grpc proto-google-common-protos - ${google.common-protos.version} com.google.cloud google-cloud-spanner - ${spanner.version} + + + + io.grpc + grpc-api + + + com.google.cloud + google-cloud-core-grpc + + + + + com.google.api.grpc + proto-google-cloud-spanner-v1 com.google.protobuf protobuf-java - ${protobuf.version} com.google.guava guava - ${guava.version} - - - com.google.api - gax - ${gax.version} org.threeten threetenbp ${threeten.version} - - io.grpc - grpc-api - ${grpc.version} - io.grpc grpc-netty-shaded - ${grpc.version} com.google.api api-common - ${google.api-common.version} com.google.code.findbugs jsr305 - 3.0.2 + ${findbugs.version} com.google.http-client google-http-client - ${http-client-bom.version} com.google.auth google-auth-library-oauth2-http - ${google.auth.version} com.google.cloud google-cloud-core - ${google.core.version} - - - com.google.code.gson - gson - 2.8.6 com.google.auth google-auth-library-credentials - ${google.auth.version} - - - org.apache.commons - commons-lang3 - 3.5 - - - com.google.api.grpc - proto-google-cloud-spanner-v1 - ${spanner.version} - - - com.google.api.grpc - proto-google-cloud-spanner-admin-database-v1 - ${spanner.version} - - - com.google.protobuf - protobuf-java-util - ${protobuf.version} + com.google.cloud google-cloud-spanner - ${spanner.version} + ${spanner.test.version} test-jar test com.google.api.grpc grpc-google-cloud-spanner-v1 - ${spanner.version} test com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - ${spanner.version} test com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - ${spanner.version} test com.google.truth truth - 1.0.1 - test - - - org.mockito - mockito-core - 1.10.19 + ${truth.version} test @@ -285,6 +188,18 @@ ${hamcrest.version} test + + org.mockito + mockito-core + ${mockito.version} + test + + + org.hamcrest + hamcrest-core + + + junit junit @@ -294,7 +209,6 @@ com.google.api gax-grpc - ${gax.version} testlib test @@ -304,7 +218,6 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M4 com.google.cloud.spanner.IntegrationTest sponge_log @@ -313,7 +226,6 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.0.0-M4 com.google.cloud.spanner.GceTestEnvConfig @@ -340,6 +252,7 @@ com.google.api:gax-grpc + com.google.cloud:google-cloud-core-grpc com.google.api.grpc:grpc-google-cloud-spanner-v1 com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1 com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1 diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractBaseUnitOfWork.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractBaseUnitOfWork.java deleted file mode 100644 index 099fd36e..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractBaseUnitOfWork.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.StatementExecutor.StatementTimeout; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.annotation.concurrent.GuardedBy; - -/** Base for all {@link Connection}-based transactions and batches. */ -abstract class AbstractBaseUnitOfWork implements UnitOfWork { - private final StatementExecutor statementExecutor; - private final StatementTimeout statementTimeout; - - /** - * The {@link Future} that monitors the result of the statement currently being executed for this - * unit of work. - */ - @GuardedBy("this") - private Future currentlyRunningStatementFuture = null; - - enum InterceptorsUsage { - INVOKE_INTERCEPTORS, - IGNORE_INTERCEPTORS; - } - - abstract static class Builder, T extends AbstractBaseUnitOfWork> { - private StatementExecutor statementExecutor; - private StatementTimeout statementTimeout = new StatementTimeout(); - - Builder() {} - - @SuppressWarnings("unchecked") - B self() { - return (B) this; - } - - B withStatementExecutor(StatementExecutor executor) { - Preconditions.checkNotNull(executor); - this.statementExecutor = executor; - return self(); - } - - B setStatementTimeout(StatementTimeout timeout) { - Preconditions.checkNotNull(timeout); - this.statementTimeout = timeout; - return self(); - } - - abstract T build(); - } - - AbstractBaseUnitOfWork(Builder builder) { - Preconditions.checkState(builder.statementExecutor != null, "No statement executor specified"); - this.statementExecutor = builder.statementExecutor; - this.statementTimeout = builder.statementTimeout; - } - - StatementExecutor getStatementExecutor() { - return statementExecutor; - } - - StatementTimeout getStatementTimeout() { - return statementTimeout; - } - - @Override - public void cancel() { - synchronized (this) { - if (currentlyRunningStatementFuture != null - && !currentlyRunningStatementFuture.isDone() - && !currentlyRunningStatementFuture.isCancelled()) { - currentlyRunningStatementFuture.cancel(true); - } - } - } - - T asyncExecuteStatement(ParsedStatement statement, Callable callable) { - return asyncExecuteStatement(statement, callable, InterceptorsUsage.INVOKE_INTERCEPTORS); - } - - T asyncExecuteStatement( - ParsedStatement statement, Callable callable, InterceptorsUsage interceptorUsage) { - Preconditions.checkNotNull(statement); - Preconditions.checkNotNull(callable); - - if (interceptorUsage == InterceptorsUsage.INVOKE_INTERCEPTORS) { - statementExecutor.invokeInterceptors( - statement, StatementExecutionStep.EXECUTE_STATEMENT, this); - } - Future future = statementExecutor.submit(callable); - synchronized (this) { - this.currentlyRunningStatementFuture = future; - } - T res; - try { - if (statementTimeout.hasTimeout()) { - TimeUnit unit = statementTimeout.getAppropriateTimeUnit(); - res = future.get(statementTimeout.getTimeoutValue(unit), unit); - } else { - res = future.get(); - } - } catch (TimeoutException e) { - // statement timed out, cancel the execution - future.cancel(true); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.DEADLINE_EXCEEDED, - "Statement execution timeout occurred for " + statement.getSqlWithoutComments(), - e); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - Set causes = new HashSet<>(); - while (cause != null && !causes.contains(cause)) { - if (cause instanceof SpannerException) { - throw (SpannerException) cause; - } - causes.add(cause); - cause = cause.getCause(); - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.UNKNOWN, - "Statement execution failed for " + statement.getSqlWithoutComments(), - e); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.CANCELLED, "Statement execution was interrupted", e); - } catch (CancellationException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.CANCELLED, "Statement execution was cancelled", e); - } finally { - synchronized (this) { - this.currentlyRunningStatementFuture = null; - } - } - return res; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java index 71a1c20a..aad5ad82 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.jdbc; +import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.common.annotations.VisibleForTesting; import com.google.rpc.Code; import java.sql.CallableStatement; @@ -48,7 +49,7 @@ abstract class AbstractJdbcConnection extends AbstractJdbcWrapper private final String connectionUrl; private final ConnectionOptions options; - private final com.google.cloud.spanner.jdbc.Connection spanner; + private final com.google.cloud.spanner.connection.Connection spanner; private SQLWarning firstWarning = null; private SQLWarning lastWarning = null; @@ -59,8 +60,8 @@ abstract class AbstractJdbcConnection extends AbstractJdbcWrapper this.spanner = options.getConnection(); } - /** Return the corresponding {@link com.google.cloud.spanner.jdbc.Connection} */ - com.google.cloud.spanner.jdbc.Connection getSpannerConnection() { + /** Return the corresponding {@link com.google.cloud.spanner.connection.Connection} */ + com.google.cloud.spanner.connection.Connection getSpannerConnection() { return spanner; } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java index 94ddaf17..48a002d5 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java @@ -19,7 +19,8 @@ import com.google.cloud.spanner.Options; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType; +import com.google.cloud.spanner.connection.StatementResult; +import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType; import com.google.rpc.Code; import java.sql.ResultSet; import java.sql.SQLException; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractMultiUseTransaction.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractMultiUseTransaction.java deleted file mode 100644 index 6468c004..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractMultiUseTransaction.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadContext; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; -import java.util.concurrent.Callable; - -/** - * Base class for {@link Connection}-based transactions that can be used for multiple read and - * read/write statements. - */ -abstract class AbstractMultiUseTransaction extends AbstractBaseUnitOfWork { - - AbstractMultiUseTransaction(Builder builder) { - super(builder); - } - - @Override - public Type getType() { - return Type.TRANSACTION; - } - - @Override - public boolean isActive() { - return getState().isActive(); - } - - /** - * Check that the current transaction actually has a valid underlying transaction. If not, the - * method will throw a {@link SpannerException}. - */ - abstract void checkValidTransaction(); - - /** Returns the {@link ReadContext} that can be used for queries on this transaction. */ - abstract ReadContext getReadContext(); - - @Override - public ResultSet executeQuery( - final ParsedStatement statement, - final AnalyzeMode analyzeMode, - final QueryOption... options) { - Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); - checkValidTransaction(); - return asyncExecuteStatement( - statement, - new Callable() { - @Override - public ResultSet call() throws Exception { - return DirectExecuteResultSet.ofResultSet( - internalExecuteQuery(statement, analyzeMode, options)); - } - }); - } - - ResultSet internalExecuteQuery( - final ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { - if (analyzeMode == AnalyzeMode.NONE) { - return getReadContext().executeQuery(statement.getStatement(), options); - } - return getReadContext() - .analyzeQuery(statement.getStatement(), analyzeMode.getQueryAnalyzeMode()); - } - - @Override - public long[] runBatch() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for transactions"); - } - - @Override - public void abortBatch() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for transactions"); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AnalyzeMode.java b/src/main/java/com/google/cloud/spanner/jdbc/AnalyzeMode.java deleted file mode 100644 index 457ceec2..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/AnalyzeMode.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; - -/** - * {@link AnalyzeMode} indicates whether a query should be executed as a normal query (NONE), - * whether only a query plan should be returned, or whether the query should be profiled while - * executed. - */ -enum AnalyzeMode { - NONE(null), - PLAN(QueryAnalyzeMode.PLAN), - PROFILE(QueryAnalyzeMode.PROFILE); - - private final QueryAnalyzeMode mode; - - private AnalyzeMode(QueryAnalyzeMode mode) { - this.mode = mode; - } - - QueryAnalyzeMode getQueryAnalyzeMode() { - return mode; - } - - /** Translates from the Spanner client library QueryAnalyzeMode to {@link AnalyzeMode}. */ - static AnalyzeMode of(QueryAnalyzeMode mode) { - switch (mode) { - case PLAN: - return AnalyzeMode.PLAN; - case PROFILE: - return AnalyzeMode.PROFILE; - default: - throw new IllegalArgumentException(mode + " is unknown"); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AutocommitDmlMode.java b/src/main/java/com/google/cloud/spanner/jdbc/AutocommitDmlMode.java deleted file mode 100644 index c390edd1..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/AutocommitDmlMode.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -/** Enum used to define the behavior of DML statements in autocommit mode */ -enum AutocommitDmlMode { - TRANSACTIONAL, - PARTITIONED_NON_ATOMIC; - - private final String statementString; - - private AutocommitDmlMode() { - this.statementString = name(); - } - - /** - * Use this method to get the correct format for use in a SQL statement. Autocommit dml mode must - * be wrapped between single quotes in SQL statements: - * SET AUTOCOMMIT_DML_MODE='TRANSACTIONAL' This method returns the value - * without the single quotes. - * - * @return a string representation of this {@link AutocommitDmlMode} that can be used in a SQL - * statement. - */ - public String getStatementString() { - return statementString; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ChecksumResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/ChecksumResultSet.java deleted file mode 100644 index 0e0d7b43..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ChecksumResultSet.java +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.ByteArray; -import com.google.cloud.Date; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type.Code; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.hash.Funnel; -import com.google.common.hash.HashCode; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hasher; -import com.google.common.hash.Hashing; -import com.google.common.hash.PrimitiveSink; -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -/** - * {@link ResultSet} implementation that keeps a running checksum that can be used to determine - * whether a transaction retry is possible or not. The checksum is based on all the rows that have - * actually been consumed by the user. If the user has not yet consumed any part of the result set - * (i.e. never called next()), the checksum will be null and retry will always be - * allowed. - * - *

If all the rows in the result set have been consumed, the checksum will be based on the values - * of all those rows, and a retry will only be possible if the query returns the exact same results - * during the retry as during the original transaction. - * - *

If some of the rows in the result set have been consumed, the checksum will be based on the - * values of the rows that have been consumed. A retry will succeed if the query returns the same - * results for the already consumed rows. - * - *

The checksum of a {@link ResultSet} is the SHA256 checksum of the current row together with - * the previous checksum value of the result set. The calculation of the checksum is executed in a - * separate {@link Thread} to allow the checksum calculation to lag behind the actual consumption of - * rows, and catch up again if the client slows down the consumption of rows, for example while - * waiting for more data from Cloud Spanner. If the checksum calculation queue contains more than - * {@link ChecksumExecutor#MAX_IN_CHECKSUM_QUEUE} items that have not yet been calculated, calls to - * {@link ResultSet#next()} will slow down in order to allow the calculation to catch up. - */ -@VisibleForTesting -class ChecksumResultSet extends ReplaceableForwardingResultSet implements RetriableStatement { - private final ReadWriteTransaction transaction; - private long numberOfNextCalls; - private final ParsedStatement statement; - private final AnalyzeMode analyzeMode; - private final QueryOption[] options; - private final ChecksumResultSet.ChecksumCalculator checksumCalculator = new ChecksumCalculator(); - - ChecksumResultSet( - ReadWriteTransaction transaction, - ResultSet delegate, - ParsedStatement statement, - AnalyzeMode analyzeMode, - QueryOption... options) { - super(delegate); - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(delegate); - Preconditions.checkNotNull(statement); - Preconditions.checkNotNull(statement.getStatement()); - Preconditions.checkNotNull(statement.getStatement().getSql()); - this.transaction = transaction; - this.statement = statement; - this.analyzeMode = analyzeMode; - this.options = options; - } - - /** Simple {@link Callable} for calling {@link ResultSet#next()} */ - private final class NextCallable implements Callable { - @Override - public Boolean call() throws Exception { - transaction - .getStatementExecutor() - .invokeInterceptors( - statement, StatementExecutionStep.CALL_NEXT_ON_RESULT_SET, transaction); - return ChecksumResultSet.super.next(); - } - } - - private final NextCallable nextCallable = new NextCallable(); - - @Override - public boolean next() { - // Call next() with retry. - boolean res = transaction.runWithRetry(nextCallable); - // Only update the checksum if there was another row to be consumed. - if (res) { - checksumCalculator.calculateNextChecksum(getCurrentRowAsStruct()); - } - numberOfNextCalls++; - return res; - } - - @VisibleForTesting - HashCode getChecksum() throws InterruptedException, ExecutionException { - // HashCode is immutable and can be safely returned. - return checksumCalculator.getChecksum(); - } - - /** - * Execute the same query as in the original transaction and consume the {@link ResultSet} to the - * same point as the original {@link ResultSet}. The {@link HashCode} of the new {@link ResultSet} - * is compared with the {@link HashCode} of the original {@link ResultSet} at the point where the - * consumption of the {@link ResultSet} stopped. - */ - @Override - public void retry(AbortedException aborted) throws AbortedException { - // Execute the same query and consume the result set to the same point as the original. - ChecksumResultSet.ChecksumCalculator newChecksumCalculator = new ChecksumCalculator(); - ResultSet resultSet = null; - long counter = 0L; - try { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - resultSet = - DirectExecuteResultSet.ofResultSet( - transaction.internalExecuteQuery(statement, analyzeMode, options)); - boolean next = true; - while (counter < numberOfNextCalls && next) { - transaction - .getStatementExecutor() - .invokeInterceptors( - statement, StatementExecutionStep.RETRY_NEXT_ON_RESULT_SET, transaction); - next = resultSet.next(); - if (next) { - newChecksumCalculator.calculateNextChecksum(resultSet.getCurrentRowAsStruct()); - } - counter++; - } - } catch (Throwable e) { - if (resultSet != null) { - resultSet.close(); - } - // If it was a SpannerException other than an AbortedException, the retry should fail - // because of different results from the database. - if (e instanceof SpannerException && !(e instanceof AbortedException)) { - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException( - aborted, (SpannerException) e); - } - // For other types of exceptions we should just re-throw the exception. - throw e; - } - // Check that we have the same number of rows and the same checksum. - HashCode newChecksum = newChecksumCalculator.getChecksum(); - HashCode currentChecksum = checksumCalculator.getChecksum(); - if (counter == numberOfNextCalls && Objects.equals(newChecksum, currentChecksum)) { - // Checksum is ok, we only need to replace the delegate result set if it's still open. - if (isClosed()) { - resultSet.close(); - } else { - replaceDelegate(resultSet); - } - } else { - // The results are not equal, there is an actual concurrent modification, so we cannot - // continue the transaction. - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } - } - - /** Calculates and keeps the current checksum of a {@link ChecksumResultSet} */ - private static final class ChecksumCalculator { - private static final HashFunction SHA256_FUNCTION = Hashing.sha256(); - private HashCode currentChecksum; - - private void calculateNextChecksum(Struct row) { - Hasher hasher = SHA256_FUNCTION.newHasher(); - if (currentChecksum != null) { - hasher.putBytes(currentChecksum.asBytes()); - } - hasher.putObject(row, StructFunnel.INSTANCE); - currentChecksum = hasher.hash(); - } - - private HashCode getChecksum() { - return currentChecksum; - } - } - - /** - * A {@link Funnel} implementation for calculating a {@link HashCode} for each row in a {@link - * ResultSet}. - */ - private enum StructFunnel implements Funnel { - INSTANCE; - private static final String NULL = "null"; - - @Override - public void funnel(Struct row, PrimitiveSink into) { - for (int i = 0; i < row.getColumnCount(); i++) { - if (row.isNull(i)) { - funnelValue(Code.STRING, null, into); - } else { - Code type = row.getColumnType(i).getCode(); - switch (type) { - case ARRAY: - funnelArray(row.getColumnType(i).getArrayElementType().getCode(), row, i, into); - break; - case BOOL: - funnelValue(type, row.getBoolean(i), into); - break; - case BYTES: - funnelValue(type, row.getBytes(i), into); - break; - case DATE: - funnelValue(type, row.getDate(i), into); - break; - case FLOAT64: - funnelValue(type, row.getDouble(i), into); - break; - case INT64: - funnelValue(type, row.getLong(i), into); - break; - case STRING: - funnelValue(type, row.getString(i), into); - break; - case TIMESTAMP: - funnelValue(type, row.getTimestamp(i), into); - break; - - case STRUCT: - default: - throw new IllegalArgumentException("unsupported row type"); - } - } - } - } - - private void funnelArray( - Code arrayElementType, Struct row, int columnIndex, PrimitiveSink into) { - funnelValue(Code.STRING, "BeginArray", into); - switch (arrayElementType) { - case BOOL: - into.putInt(row.getBooleanList(columnIndex).size()); - for (Boolean value : row.getBooleanList(columnIndex)) { - funnelValue(Code.BOOL, value, into); - } - break; - case BYTES: - into.putInt(row.getBytesList(columnIndex).size()); - for (ByteArray value : row.getBytesList(columnIndex)) { - funnelValue(Code.BYTES, value, into); - } - break; - case DATE: - into.putInt(row.getDateList(columnIndex).size()); - for (Date value : row.getDateList(columnIndex)) { - funnelValue(Code.DATE, value, into); - } - break; - case FLOAT64: - into.putInt(row.getDoubleList(columnIndex).size()); - for (Double value : row.getDoubleList(columnIndex)) { - funnelValue(Code.FLOAT64, value, into); - } - break; - case INT64: - into.putInt(row.getLongList(columnIndex).size()); - for (Long value : row.getLongList(columnIndex)) { - funnelValue(Code.INT64, value, into); - } - break; - case STRING: - into.putInt(row.getStringList(columnIndex).size()); - for (String value : row.getStringList(columnIndex)) { - funnelValue(Code.STRING, value, into); - } - break; - case TIMESTAMP: - into.putInt(row.getTimestampList(columnIndex).size()); - for (Timestamp value : row.getTimestampList(columnIndex)) { - funnelValue(Code.TIMESTAMP, value, into); - } - break; - - case ARRAY: - case STRUCT: - default: - throw new IllegalArgumentException("unsupported array element type"); - } - funnelValue(Code.STRING, "EndArray", into); - } - - private void funnelValue(Code type, T value, PrimitiveSink into) { - // Include the type name in case the type of a column has changed. - into.putUnencodedChars(type.name()); - if (value == null) { - if (type == Code.BYTES || type == Code.STRING) { - // Put length -1 to distinguish from the string value 'null'. - into.putInt(-1); - } - into.putUnencodedChars(NULL); - } else { - switch (type) { - case BOOL: - into.putBoolean((Boolean) value); - break; - case BYTES: - ByteArray byteArray = (ByteArray) value; - into.putInt(byteArray.length()); - into.putBytes(byteArray.toByteArray()); - break; - case DATE: - Date date = (Date) value; - into.putInt(date.getYear()).putInt(date.getMonth()).putInt(date.getDayOfMonth()); - break; - case FLOAT64: - into.putDouble((Double) value); - break; - case INT64: - into.putLong((Long) value); - break; - case STRING: - String stringValue = (String) value; - into.putInt(stringValue.length()); - into.putUnencodedChars(stringValue); - break; - case TIMESTAMP: - Timestamp timestamp = (Timestamp) value; - into.putLong(timestamp.getSeconds()).putInt(timestamp.getNanos()); - break; - case ARRAY: - case STRUCT: - default: - throw new IllegalArgumentException("invalid type for single value"); - } - } - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatement.java deleted file mode 100644 index 7bd31407..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatement.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ResultSet; -import java.util.List; - -/** - * A {@link ClientSideStatement} is a statement that is not sent to Google Cloud Spanner, but that - * is executed locally to for example set a certain state of a {@link Connection} or get a property - * of a {@link Connection}. - */ -interface ClientSideStatement { - - /** - * @return a list of example statements for this {@link ClientSideStatement}. If these statements - * are parsed, they will all result this in this {@link ClientSideStatement}. - */ - List getExampleStatements(); - - /** - * @return a list of statements that need to be executed on a new connection before the example - * statements may be executed on a connection. For GET READ_TIMESTAMP this would for example - * be a couple of statements that generate a read-only transaction. - */ - List getExamplePrerequisiteStatements(); - - /** - * @return true if this {@link ClientSideStatement} will return a {@link ResultSet}. - */ - boolean isQuery(); - - /** @return true if this {@link ClientSideStatement} will return an update count. */ - boolean isUpdate(); - - /** - * Execute this {@link ClientSideStatement} on the given {@link ConnectionStatementExecutor}. The - * executor calls the appropriate method(s) on the {@link Connection}. The statement argument is - * used to parse any additional properties that might be needed for the execution. - * - * @param executor The {@link ConnectionStatementExecutor} that will be used to call a method on - * the {@link Connection}. - * @param statement The original sql statement that has been parsed to this {@link - * ClientSideStatement}. This statement is used to get any additional arguments that are - * needed for the execution of the {@link ClientSideStatement}. - * @return the result of the execution of the statement. - */ - StatementResult execute(ConnectionStatementExecutor executor, String statement); -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementExecutor.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementExecutor.java deleted file mode 100644 index d758286d..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementExecutor.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; - -/** - * A {@link ClientSideStatementExecutor} is used to compile {@link ClientSideStatement}s from the - * json source file, and to execute these against a {@link Connection} (through a {@link - * ConnectionStatementExecutor}). - */ -interface ClientSideStatementExecutor { - - /** - * Compiles the given {@link ClientSideStatementImpl} and registers this statement with this - * executor. A statement must be compiled before it can be executed. The parser automatically - * compiles all available statements during initialization. - * - * @param statement The statement to compile. - * @throws CompileException If the statement could not be compiled. This should never happen, as - * it would indicate that an invalid statement has been defined in the source file. - */ - void compile(ClientSideStatementImpl statement) throws CompileException; - - /** - * Executes the {@link ClientSideStatementImpl} that has been compiled and registered with this - * executor on the specified connection. - * - * @param connectionExecutor The {@link ConnectionStatementExecutor} to use to execute the - * statement on a {@link Connection}. - * @param sql The sql statement that is executed. This can be used to parse any additional - * arguments that might be needed for the execution of the {@link ClientSideStatementImpl}. - * @return the result of the execution. - * @throws Exception If an error occurs while executing the statement, for example if an invalid - * argument has been specified in the sql statement, or if the statement is invalid for the - * current state of the {@link Connection}. - */ - StatementResult execute(ConnectionStatementExecutor connectionExecutor, String sql) - throws Exception; -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementImpl.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementImpl.java deleted file mode 100644 index 551ab5aa..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementImpl.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; -import com.google.common.base.Preconditions; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Implementation of the {@link ClientSideStatement} interface. The instances of this class are - * imported from the file 'ClientSideStatements.json' in the resources folder. - */ -class ClientSideStatementImpl implements ClientSideStatement { - - /** - * Statements that set a value, such as SET AUTOCOMMIT ON|OFF, must specify a {@link - * ClientSideSetStatementImpl} that defines how the value is set. - */ - static class ClientSideSetStatementImpl { - /** The property name that is to be set, e.g. AUTOCOMMIT. */ - private String propertyName; - /** The separator between the property and the value (i.e. '=' or '\s+'). */ - private String separator; - /** Regex specifying the range of allowed values for the property. */ - private String allowedValues; - /** The class name of the {@link ClientSideStatementValueConverter} to use. */ - private String converterName; - - String getPropertyName() { - return propertyName; - } - - String getSeparator() { - return separator; - } - - String getAllowedValues() { - return allowedValues; - } - - String getConverterName() { - return converterName; - } - } - - static class CompileException extends Exception { - private static final long serialVersionUID = 1L; - private final ClientSideStatementImpl statement; - - CompileException(Throwable cause, ClientSideStatementImpl statement) { - super(cause); - this.statement = statement; - } - - @Override - public String getMessage() { - return "Could not compile statement " + this.statement.name; - } - } - - static class ExecuteException extends RuntimeException { - private static final long serialVersionUID = 1L; - private final ClientSideStatementImpl statement; - private final String sql; - - private ExecuteException(Throwable cause, ClientSideStatementImpl statement, String sql) { - super(cause); - this.statement = statement; - this.sql = sql; - } - - @Override - public String getMessage() { - return "Could not execute statement " + this.statement.name + " (" + sql + ")"; - } - } - - /** The name of this statement. Used in error and info messages. */ - private String name; - - /** - * The class name of the {@link ClientSideStatementExecutor} that should be used for this - * statement. - */ - private String executorName; - - /** The result type of this statement. */ - private ResultType resultType; - - /** The regular expression that should be used to recognize this class of statements. */ - private String regex; - - /** - * The method name of the {@link ConnectionStatementExecutor} that should be called when this - * statement is executed, for example 'statementSetAutocommit'. - */ - private String method; - - /** A list of example statements that is used for testing. */ - private List exampleStatements; - - /** - * A list of statements that need to be executed before the example statements may be executed. - */ - private List examplePrerequisiteStatements; - - /** - * If this statement sets a value, the statement definition should also contain a {@link - * ClientSideSetStatementImpl} definition that defines how the value that is to be set should be - * parsed. - */ - private ClientSideSetStatementImpl setStatement; - - /** The compiled regex pattern for recognizing this statement. */ - private Pattern pattern; - - /** A reference to the executor that should be used. */ - private ClientSideStatementExecutor executor; - - /** - * Compiles this {@link ClientSideStatementImpl}. Throws a {@link CompileException} if the - * compilation fails. This should never happen, and if it does, it is a sign of a invalid - * statement definition in the ClientSideStatements.json file. - */ - ClientSideStatementImpl compile() throws CompileException { - try { - this.pattern = Pattern.compile(regex); - this.executor = - (ClientSideStatementExecutor) - Class.forName(getClass().getPackage().getName() + "." + executorName).newInstance(); - this.executor.compile(this); - return this; - } catch (Exception e) { - throw new CompileException(e, this); - } - } - - @Override - public StatementResult execute(ConnectionStatementExecutor connection, String statement) { - Preconditions.checkState(executor != null, "This statement has not been compiled"); - try { - return executor.execute(connection, statement); - } catch (SpannerException e) { - throw e; - } catch (InvocationTargetException e) { - if (e.getCause() instanceof SpannerException) { - throw (SpannerException) e.getCause(); - } - throw new ExecuteException(e.getCause(), this, statement); - } catch (Exception e) { - throw new ExecuteException(e, this, statement); - } - } - - @Override - public boolean isQuery() { - return resultType == ResultType.RESULT_SET; - } - - @Override - public boolean isUpdate() { - return resultType == ResultType.UPDATE_COUNT; - } - - boolean matches(String statement) { - Preconditions.checkState(pattern != null, "This statement has not been compiled"); - return pattern.matcher(statement).matches(); - } - - @Override - public String toString() { - return name; - } - - Pattern getPattern() { - return pattern; - } - - String getMethodName() { - return method; - } - - @Override - public List getExampleStatements() { - return Collections.unmodifiableList(exampleStatements); - } - - @Override - public List getExamplePrerequisiteStatements() { - if (examplePrerequisiteStatements == null) { - return Collections.emptyList(); - } - return Collections.unmodifiableList(examplePrerequisiteStatements); - } - - ClientSideSetStatementImpl getSetStatement() { - return setStatement; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementNoParamExecutor.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementNoParamExecutor.java deleted file mode 100644 index d2ca7fde..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementNoParamExecutor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import java.lang.reflect.Method; - -/** - * Executor to use for statements that do not set a value and do not have any parameters, such as - * SHOW AUTOCOMMIT. The executor just calls a method with no parameters. - */ -class ClientSideStatementNoParamExecutor implements ClientSideStatementExecutor { - private Method method; - - ClientSideStatementNoParamExecutor() {} - - @Override - public void compile(ClientSideStatementImpl statement) throws CompileException { - try { - this.method = ConnectionStatementExecutor.class.getDeclaredMethod(statement.getMethodName()); - } catch (NoSuchMethodException | SecurityException e) { - throw new CompileException(e, statement); - } - } - - @Override - public StatementResult execute(ConnectionStatementExecutor connection, String statement) - throws Exception { - return (StatementResult) method.invoke(connection); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementSetExecutor.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementSetExecutor.java deleted file mode 100644 index aaf01eaa..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementSetExecutor.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.common.base.Preconditions; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Executor for {@link ClientSideStatement}s that sets a value for a property, such as SET - * AUTOCOMMIT=TRUE. - */ -class ClientSideStatementSetExecutor implements ClientSideStatementExecutor { - private ClientSideStatementImpl statement; - private Method method; - private ClientSideStatementValueConverter converter; - private Pattern allowedValuesPattern; - - @SuppressWarnings("unchecked") - @Override - public void compile(ClientSideStatementImpl statement) throws CompileException { - Preconditions.checkNotNull(statement.getSetStatement()); - try { - this.statement = statement; - this.allowedValuesPattern = - Pattern.compile( - String.format( - "(?is)\\A\\s*set\\s+%s\\s*%s\\s*%s\\s*\\z", - statement.getSetStatement().getPropertyName(), - statement.getSetStatement().getSeparator(), - statement.getSetStatement().getAllowedValues())); - Class> converterClass = - (Class>) - Class.forName( - getClass().getPackage().getName() - + "." - + statement.getSetStatement().getConverterName()); - Constructor> constructor = - converterClass.getConstructor(String.class); - this.converter = constructor.newInstance(statement.getSetStatement().getAllowedValues()); - this.method = - ConnectionStatementExecutor.class.getDeclaredMethod( - statement.getMethodName(), converter.getParameterClass()); - } catch (Exception e) { - throw new CompileException(e, statement); - } - } - - @Override - public StatementResult execute(ConnectionStatementExecutor connection, String sql) - throws Exception { - return (StatementResult) method.invoke(connection, getParameterValue(sql)); - } - - T getParameterValue(String sql) { - Matcher matcher = allowedValuesPattern.matcher(sql); - if (matcher.find() && matcher.groupCount() >= 1) { - String value = matcher.group(1); - T res = converter.convert(value); - if (res != null) { - return res; - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - String.format( - "Unknown value for %s: %s", - this.statement.getSetStatement().getPropertyName(), value)); - } else { - Matcher invalidMatcher = this.statement.getPattern().matcher(sql); - if (invalidMatcher.find() && invalidMatcher.groupCount() == 1) { - String invalidValue = invalidMatcher.group(1); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - String.format( - "Unknown value for %s: %s", - this.statement.getSetStatement().getPropertyName(), invalidValue)); - } - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, String.format("Unknown statement: %s", sql)); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverter.java deleted file mode 100644 index 13b404cf..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverter.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -/** - * Interface for converters that are used by {@link ClientSideStatement} that sets a value that need - * to be converted from a string to a specific type. Implementing classes must have a public - * constructor that takes a String parameter. The String parameter will contain a regular expression - * for the allowed values for the property. - */ -interface ClientSideStatementValueConverter { - - /** The type to convert to. */ - Class getParameterClass(); - - /** - * The actual convert method. Should return null for values that could not be - * converted. - */ - T convert(String value); -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverters.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverters.java deleted file mode 100644 index 92368fec..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatementValueConverters.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.common.base.Function; -import com.google.common.base.Preconditions; -import com.google.protobuf.Duration; -import com.google.protobuf.util.Durations; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** Contains all {@link ClientSideStatementValueConverter} implementations. */ -class ClientSideStatementValueConverters { - /** Map for mapping case-insensitive strings to enums. */ - private static final class CaseInsensitiveEnumMap> { - private final Map map = new HashMap<>(); - - /** Create an map using the name of the enum elements as keys. */ - private CaseInsensitiveEnumMap(Class elementType) { - this( - elementType, - new Function() { - @Override - public String apply(E input) { - return input.name(); - } - }); - } - - /** Create a map using the specific function to get the key per enum value. */ - private CaseInsensitiveEnumMap(Class elementType, Function keyFunction) { - Preconditions.checkNotNull(elementType); - Preconditions.checkNotNull(keyFunction); - EnumSet set = EnumSet.allOf(elementType); - for (E e : set) { - if (map.put(keyFunction.apply(e).toUpperCase(), e) != null) { - throw new IllegalArgumentException( - "Enum contains multiple elements with the same case-insensitive key"); - } - } - } - - private E get(String value) { - Preconditions.checkNotNull(value); - return map.get(value.toUpperCase()); - } - } - - /** Converter from string to {@link Boolean} */ - static class BooleanConverter implements ClientSideStatementValueConverter { - - public BooleanConverter(String allowedValues) {} - - @Override - public Class getParameterClass() { - return Boolean.class; - } - - @Override - public Boolean convert(String value) { - if ("true".equalsIgnoreCase(value)) { - return Boolean.TRUE; - } - if ("false".equalsIgnoreCase(value)) { - return Boolean.FALSE; - } - return null; - } - } - - /** Converter from string to {@link Duration}. */ - static class DurationConverter implements ClientSideStatementValueConverter { - private final Pattern allowedValues; - - public DurationConverter(String allowedValues) { - // Remove the parentheses from the beginning and end. - this.allowedValues = - Pattern.compile( - "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z"); - } - - @Override - public Class getParameterClass() { - return Duration.class; - } - - @Override - public Duration convert(String value) { - Matcher matcher = allowedValues.matcher(value); - if (matcher.find()) { - if (matcher.group(0).equalsIgnoreCase("null")) { - return Durations.fromNanos(0L); - } else { - Duration duration = - ReadOnlyStalenessUtil.createDuration( - Long.valueOf(matcher.group(1)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(2))); - if (duration.getSeconds() == 0L && duration.getNanos() == 0) { - return null; - } - return duration; - } - } - return null; - } - } - - /** Converter from string to possible values for read only staleness ({@link TimestampBound}). */ - static class ReadOnlyStalenessConverter - implements ClientSideStatementValueConverter { - private final Pattern allowedValues; - private final CaseInsensitiveEnumMap values = new CaseInsensitiveEnumMap<>(Mode.class); - - public ReadOnlyStalenessConverter(String allowedValues) { - // Remove the single quotes at the beginning and end. - this.allowedValues = - Pattern.compile( - "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z"); - } - - @Override - public Class getParameterClass() { - return TimestampBound.class; - } - - @Override - public TimestampBound convert(String value) { - Matcher matcher = allowedValues.matcher(value); - if (matcher.find() && matcher.groupCount() >= 1) { - Mode mode = null; - int groupIndex = 0; - for (int group = 1; group <= matcher.groupCount(); group++) { - if (matcher.group(group) != null) { - mode = values.get(matcher.group(group)); - if (mode != null) { - groupIndex = group; - break; - } - } - } - switch (mode) { - case STRONG: - return TimestampBound.strong(); - case READ_TIMESTAMP: - return TimestampBound.ofReadTimestamp( - ReadOnlyStalenessUtil.parseRfc3339(matcher.group(groupIndex + 1))); - case MIN_READ_TIMESTAMP: - return TimestampBound.ofMinReadTimestamp( - ReadOnlyStalenessUtil.parseRfc3339(matcher.group(groupIndex + 1))); - case EXACT_STALENESS: - try { - return TimestampBound.ofExactStaleness( - Long.valueOf(matcher.group(groupIndex + 2)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(groupIndex + 3))); - } catch (IllegalArgumentException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, e.getMessage()); - } - case MAX_STALENESS: - try { - return TimestampBound.ofMaxStaleness( - Long.valueOf(matcher.group(groupIndex + 2)), - ReadOnlyStalenessUtil.parseTimeUnit(matcher.group(groupIndex + 3))); - } catch (IllegalArgumentException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, e.getMessage()); - } - default: - // fall through to allow the calling method to handle this - } - } - return null; - } - } - - /** Converter for converting strings to {@link AutocommitDmlMode} values. */ - static class AutocommitDmlModeConverter - implements ClientSideStatementValueConverter { - private final CaseInsensitiveEnumMap values = - new CaseInsensitiveEnumMap<>(AutocommitDmlMode.class); - - public AutocommitDmlModeConverter(String allowedValues) {} - - @Override - public Class getParameterClass() { - return AutocommitDmlMode.class; - } - - @Override - public AutocommitDmlMode convert(String value) { - return values.get(value); - } - } - - static class StringValueConverter implements ClientSideStatementValueConverter { - public StringValueConverter(String allowedValues) {} - - @Override - public Class getParameterClass() { - return String.class; - } - - @Override - public String convert(String value) { - return value; - } - } - - /** Converter for converting string values to {@link TransactionMode} values. */ - static class TransactionModeConverter - implements ClientSideStatementValueConverter { - private final CaseInsensitiveEnumMap values = - new CaseInsensitiveEnumMap<>( - TransactionMode.class, - new Function() { - @Override - public String apply(TransactionMode input) { - return input.getStatementString(); - } - }); - - public TransactionModeConverter(String allowedValues) {} - - @Override - public Class getParameterClass() { - return TransactionMode.class; - } - - @Override - public TransactionMode convert(String value) { - // Transaction mode may contain multiple spaces. - String valueWithSingleSpaces = value.replaceAll("\\s+", " "); - return values.get(valueWithSingleSpaces); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatements.java b/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatements.java deleted file mode 100644 index 3f3d872f..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ClientSideStatements.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.gson.Gson; -import java.io.InputStreamReader; -import java.util.Set; - -/** This class reads and parses the {@link ClientSideStatement}s from the json file. */ -class ClientSideStatements { - private static final String STATEMENTS_DEFINITION_FILE = "ClientSideStatements.json"; - static final ClientSideStatements INSTANCE = importStatements(); - - /** - * Reads statement definitions from ClientSideStatements.json and parses these as Java objects. - */ - private static ClientSideStatements importStatements() { - Gson gson = new Gson(); - return gson.fromJson( - new InputStreamReader( - ClientSideStatements.class.getResourceAsStream(STATEMENTS_DEFINITION_FILE)), - ClientSideStatements.class); - } - - private Set statements; - - private ClientSideStatements() {} - - /** Compiles and returns all statements from the resource file. */ - Set getCompiledStatements() throws CompileException { - for (ClientSideStatementImpl statement : statements) { - statement.compile(); - } - return statements; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java index d4c6629b..dc52ca35 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java @@ -152,21 +152,45 @@ public interface CloudSpannerJdbcConnection extends Connection { /** * @see - * com.google.cloud.spanner.jdbc.Connection#addTransactionRetryListener(TransactionRetryListener) + * com.google.cloud.spanner.connection.Connection#addTransactionRetryListener(TransactionRetryListener) * @throws SQLException if the {@link Connection} is closed. */ - void addTransactionRetryListener(TransactionRetryListener listener) throws SQLException; + void addTransactionRetryListener( + com.google.cloud.spanner.connection.TransactionRetryListener listener) throws SQLException; + + /** + * Use {@link + * #addTransactionRetryListener(com.google.cloud.spanner.jdbc.TransactionRetryListener)} + */ + @Deprecated + void addTransactionRetryListener(com.google.cloud.spanner.jdbc.TransactionRetryListener listener) + throws SQLException; /** * @see - * com.google.cloud.spanner.jdbc.Connection#removeTransactionRetryListener(TransactionRetryListener) + * com.google.cloud.spanner.connection.Connection#removeTransactionRetryListener(TransactionRetryListener) * @throws SQLException if the {@link Connection} is closed. */ - boolean removeTransactionRetryListener(TransactionRetryListener listener) throws SQLException; + boolean removeTransactionRetryListener( + com.google.cloud.spanner.connection.TransactionRetryListener listener) throws SQLException; + + /** + * Use {@link + * #removeTransactionRetryListener(com.google.cloud.spanner.jdbc.TransactionRetryListener)} + */ + @Deprecated + boolean removeTransactionRetryListener( + com.google.cloud.spanner.jdbc.TransactionRetryListener listener) throws SQLException; + + /** Use {@link #getTransactionRetryListenersFromConnection()} */ + @Deprecated + Iterator getTransactionRetryListeners() + throws SQLException; /** - * @see com.google.cloud.spanner.jdbc.Connection#getTransactionRetryListeners() + * @see com.google.cloud.spanner.connection.Connection#getTransactionRetryListeners() * @throws SQLException if the {@link Connection} is closed. */ - Iterator getTransactionRetryListeners() throws SQLException; + Iterator + getTransactionRetryListenersFromConnection() throws SQLException; } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/Connection.java b/src/main/java/com/google/cloud/spanner/jdbc/Connection.java deleted file mode 100644 index c3b32f8e..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/Connection.java +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerBatchUpdateException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; -import java.util.Iterator; -import java.util.concurrent.TimeUnit; - -/** - * A connection to a Cloud Spanner database. Connections are not designed to be thread-safe. The - * only exception is the {@link Connection#cancel()} method that may be called by any other thread - * to stop the execution of the current statement on the connection. - * - *

Connections accept a number of additional SQL statements for setting or changing the state of - * a {@link Connection}. These statements can only be executed using the {@link - * Connection#execute(Statement)} method: - * - *

    - *
  • SHOW AUTOCOMMIT: Returns the current value of AUTOCOMMIT of this - * connection as a {@link ResultSet} - *
  • SET AUTOCOMMIT=TRUE|FALSE: Sets the value of AUTOCOMMIT for this - * connection - *
  • SHOW READONLY: Returns the current value of READONLY of this - * connection as a {@link ResultSet} - *
  • SET READONLY=TRUE|FALSE: Sets the value of READONLY for this - * connection - *
  • SHOW RETRY_ABORTS_INTERNALLY: Returns the current value of - * RETRY_ABORTS_INTERNALLY of this connection as a {@link ResultSet} - *
  • SET RETRY_ABORTS_INTERNALLY=TRUE|FALSE: Sets the value of - * RETRY_ABORTS_INTERNALLY for this connection - *
  • SHOW AUTOCOMMIT_DML_MODE: Returns the current value of - * AUTOCOMMIT_DML_MODE of this connection as a {@link ResultSet} - *
  • SET AUTOCOMMIT_DML_MODE='TRANSACTIONAL' | 'PARTITIONED_NON_ATOMIC': Sets the - * value of AUTOCOMMIT_DML_MODE for this connection - *
  • SHOW STATEMENT_TIMEOUT: Returns the current value of STATEMENT_TIMEOUT - * of this connection as a {@link ResultSet} - *
  • SET STATEMENT_TIMEOUT='<int64>s|ms|us|ns' | NULL: Sets the value of - * STATEMENT_TIMEOUT for this connection. The supported {@link TimeUnit}s are: - *
      - *
    • s - Seconds - *
    • ms - Milliseconds - *
    • us - Microseconds - *
    • ns - Nanoseconds - *
    - * Setting the STATEMENT_TIMEOUT to NULL will clear the value for the STATEMENT_TIMEOUT on the - * connection. - *
  • SHOW READ_TIMESTAMP: Returns the last READ_TIMESTAMP of this - * connection as a {@link ResultSet} - *
  • SHOW COMMIT_TIMESTAMP: Returns the last COMMIT_TIMESTAMP of this - * connection as a {@link ResultSet} - *
  • SHOW READ_ONLY_STALENESS: Returns the current value of - * READ_ONLY_STALENESS of this connection as a {@link ResultSet} - *
  • - * SET READ_ONLY_STALENESS='STRONG' | 'MIN_READ_TIMESTAMP <timestamp>' | 'READ_TIMESTAMP <timestamp>' | 'MAX_STALENESS <int64>s|ms|mus|ns' | 'EXACT_STALENESS (<int64>s|ms|mus|ns)' - * : Sets the value of READ_ONLY_STALENESS for this connection. - *
  • SHOW OPTIMIZER_VERSION: Returns the current value of - * OPTIMIZER_VERSION of this connection as a {@link ResultSet} - *
  • - * SET OPTIMIZER_VERSION='<version>' | 'LATEST' - * : Sets the value of OPTIMIZER_VERSION for this connection. - *
  • BEGIN [TRANSACTION]: Begins a new transaction. This statement is optional when - * the connection is not in autocommit mode, as a new transaction will automatically be - * started when a query or update statement is issued. In autocommit mode, this statement will - * temporarily put the connection in transactional mode, and return the connection to - * autocommit mode when COMMIT [TRANSACTION] or ROLLBACK [TRANSACTION] - * is executed - *
  • COMMIT [TRANSACTION]: Commits the current transaction - *
  • ROLLBACK [TRANSACTION]: Rollbacks the current transaction - *
  • SET TRANSACTION READ ONLY|READ WRITE: Sets the type for the current - * transaction. May only be executed before a transaction is actually running (i.e. before any - * statements have been executed in the transaction) - *
  • START BATCH DDL: Starts a batch of DDL statements. May only be executed when - * no transaction has been started and the connection is in read/write mode. The connection - * will only accept DDL statements while a DDL batch is active. - *
  • START BATCH DML: Starts a batch of DML statements. May only be executed when - * the connection is in read/write mode. The connection will only accept DML statements while - * a DML batch is active. - *
  • RUN BATCH: Ends the current batch, sends the batched DML or DDL statements to - * Spanner and blocks until all statements have been executed or an error occurs. May only be - * executed when a (possibly empty) batch is active. The statement will return the update - * counts of the batched statements as {@link ResultSet} with an ARRAY<INT64> column. In - * case of a DDL batch, this array will always be empty. - *
  • ABORT BATCH: Ends the current batch and removes any DML or DDL statements from - * the buffer without sending any statements to Spanner. May only be executed when a (possibly - * empty) batch is active. - *
- * - * Note that Cloud Spanner could abort read/write transactions in the background, and that - * any database call during a read/write transaction could fail with an {@link - * AbortedException}. This also includes calls to {@link ResultSet#next()}. - * - *

If {@link Connection#isRetryAbortsInternally()} is true, then the connection will - * silently handle any {@link AbortedException}s by internally re-acquiring all transactional locks - * and verifying (via the use of cryptographic checksums) that no underlying data has changed. If a - * change to the underlying data is detected, then an {@link - * AbortedDueToConcurrentModificationException} error will be thrown. If your application already - * uses retry loops to handle these Aborted errors, then it will be most efficient to set {@link - * Connection#isRetryAbortsInternally()} to false. - * - *

Use {@link ConnectionOptions} to create a {@link Connection}. - */ -interface Connection extends AutoCloseable { - - /** Closes this connection. This is a no-op if the {@link Connection} has alread been closed. */ - @Override - void close(); - - /** @return true if this connection has been closed. */ - boolean isClosed(); - - /** - * Sets autocommit on/off for this {@link Connection}. Connections in autocommit mode will apply - * any changes to the database directly without waiting for an explicit commit. DDL- and DML - * statements as well as {@link Mutation}s are sent directly to Spanner, and committed - * automatically unless the statement caused an error. The statement is retried in case of an - * {@link AbortedException}. All other errors will cause the underlying transaction to be rolled - * back. - * - *

A {@link Connection} that is in autocommit and read/write mode will allow all types of - * statements: Queries, DML, DDL, and Mutations (writes). If the connection is in read-only mode, - * only queries will be allowed. - * - *

{@link Connection}s in autocommit mode may also accept partitioned DML statements. See - * {@link Connection#setAutocommitDmlMode(AutocommitDmlMode)} for more information. - * - * @param autocommit true/false to turn autocommit on/off - */ - void setAutocommit(boolean autocommit); - - /** @return true if this connection is in autocommit mode */ - boolean isAutocommit(); - - /** - * Sets this connection to read-only or read-write. This method may only be called when no - * transaction is active. A connection that is in read-only mode, will never allow any kind of - * changes to the database to be submitted. - * - * @param readOnly true/false to turn read-only mode on/off - */ - void setReadOnly(boolean readOnly); - - /** @return true if this connection is in read-only mode */ - boolean isReadOnly(); - - /** - * Sets the duration the connection should wait before automatically aborting the execution of a - * statement. The default is no timeout. Statement timeouts are applied all types of statements, - * both in autocommit and transactional mode. They also apply to {@link Connection#commit()} and - * {@link Connection#rollback()} statements. - * - *

A DML statement in autocommit mode may or may not have actually been applied to the - * database, depending on when the timeout occurred. - * - *

A DML statement in a transaction that times out may still have been applied to the - * transaction. If you still decide to commit the transaction after such a timeout, the DML - * statement may or may not have been part of the transaction, depending on whether the timeout - * occurred before or after the statement was (successfully) sent to Spanner. You should therefore - * either always rollback a transaction that had a DML statement that timed out, or you should - * accept that the timed out statement still might have been applied to the database. - * - *

DDL statements and DML statements in {@link AutocommitDmlMode#PARTITIONED_NON_ATOMIC} mode - * cannot be rolled back. If such a statement times out, it may or may not have been applied to - * the database. The same applies to commit and rollback statements. - * - *

Statements that time out will throw a {@link SpannerException} with error code {@link - * ErrorCode#DEADLINE_EXCEEDED}. - * - * @param timeout The number of {@link TimeUnit}s before a statement is automatically aborted by - * the connection. Zero or negative values are not allowed. The maximum allowed value is - * 315,576,000,000 seconds. Use {@link Connection#clearStatementTimeout()} to remove a timeout - * value that has been set. - * @param unit The {@link TimeUnit} to specify the timeout value in. Must be one of {@link - * TimeUnit#NANOSECONDS}, {@link TimeUnit#MICROSECONDS}, {@link TimeUnit#MILLISECONDS}, {@link - * TimeUnit#SECONDS}. - */ - void setStatementTimeout(long timeout, TimeUnit unit); - - /** - * Clears the statement timeout value for this connection. This is a no-op if there is currently - * no statement timeout set on this connection. - */ - void clearStatementTimeout(); - - /** - * @param unit The {@link TimeUnit} to get the timeout value in. Must be one of {@link - * TimeUnit#NANOSECONDS}, {@link TimeUnit#MICROSECONDS}, {@link TimeUnit#MILLISECONDS}, {@link - * TimeUnit#SECONDS} - * @return the current statement timeout value or 0 if no timeout value has been set. - */ - long getStatementTimeout(TimeUnit unit); - - /** @return true if this {@link Connection} has a statement timeout value. */ - boolean hasStatementTimeout(); - - /** - * Cancels the currently running statement on this {@link Connection} (if any). If canceling the - * statement execution succeeds, the statement will be terminated and a {@link SpannerException} - * with code {@link ErrorCode#CANCELLED} will be thrown. The result of the statement will be the - * same as when a statement times out (see {@link Connection#setStatementTimeout(long, TimeUnit)} - * for more information). - * - *

Canceling a DDL statement in autocommit mode or a RUN BATCH statement of a DDL batch will - * cause the connection to try to cancel the execution of the DDL statement(s). This is not - * guaranteed to cancel the execution of the statement(s) on Cloud Spanner. See - * https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operations.CancelOperation - * for more information. - * - *

Canceling a DML statement that is running in {@link - * AutocommitDmlMode#PARTITIONED_NON_ATOMIC} mode will not cancel a statement on Cloud Spanner - * that is already being executed, and its effects will still be applied to the database. - */ - void cancel(); - - /** - * Begins a new transaction for this connection. - * - *

    - *
  • Calling this method on a connection that has no transaction and that is - * not in autocommit mode, will register a new transaction that has not yet - * started on this connection - *
  • Calling this method on a connection that has no transaction and that is - * in autocommit mode, will register a new transaction that has not yet started on this - * connection, and temporarily turn off autocommit mode until the next commit/rollback - *
  • Calling this method on a connection that already has a transaction that has not yet - * started, will cause a {@link SpannerException} - *
  • Calling this method on a connection that already has a transaction that has started, will - * cause a {@link SpannerException} (no nested transactions) - *
- */ - void beginTransaction(); - - /** - * Sets the transaction mode to use for current transaction. This method may only be called when - * in a transaction, and before the transaction is actually started, i.e. before any statements - * have been executed in the transaction. - * - * @param transactionMode The transaction mode to use for the current transaction. - *
    - *
  • {@link TransactionMode#READ_ONLY_TRANSACTION} will create a read-only transaction and - * prevent any changes to written to the database through this transaction. The read - * timestamp to be used will be determined based on the current readOnlyStaleness - * setting of this connection. It is recommended to use {@link - * TransactionMode#READ_ONLY_TRANSACTION} instead of {@link - * TransactionMode#READ_WRITE_TRANSACTION} when possible, as read-only transactions do - * not acquire locks on Cloud Spanner, and read-only transactions never abort. - *
  • {@link TransactionMode#READ_WRITE_TRANSACTION} this value is only allowed when the - * connection is not in read-only mode and will create a read-write transaction. If - * {@link Connection#isRetryAbortsInternally()} is true, each read/write - * transaction will keep track of a running SHA256 checksum for each {@link ResultSet} - * that is returned in order to be able to retry the transaction in case the transaction - * is aborted by Spanner. - *
- */ - void setTransactionMode(TransactionMode transactionMode); - - /** - * @return the transaction mode of the current transaction. This method may only be called when - * the connection is in a transaction. - */ - TransactionMode getTransactionMode(); - - /** - * @return true if this connection will automatically retry read/write transactions - * that abort. This method may only be called when the connection is in read/write - * transactional mode and no transaction has been started yet. - */ - boolean isRetryAbortsInternally(); - - /** - * Sets whether this connection will internally retry read/write transactions that abort. The - * default is true. When internal retry is enabled, the {@link Connection} will keep - * track of a running SHA256 checksum of all {@link ResultSet}s that have been returned from Cloud - * Spanner. If the checksum that is calculated during an internal retry differs from the original - * checksum, the transaction will abort with an {@link - * AbortedDueToConcurrentModificationException}. - * - *

Note that retries of a read/write transaction that calls a non-deterministic function on - * Cloud Spanner, such as CURRENT_TIMESTAMP(), will never be successful, as the data returned - * during the retry will always be different from the original transaction. - * - *

It is also highly recommended that all queries in a read/write transaction have an ORDER BY - * clause that guarantees that the data is returned in the same order as in the original - * transaction if the transaction is internally retried. The most efficient way to achieve this is - * to always include the primary key columns at the end of the ORDER BY clause. - * - *

This method may only be called when the connection is in read/write transactional mode and - * no transaction has been started yet. - * - * @param retryAbortsInternally Set to true to internally retry transactions that are - * aborted by Spanner. When set to false, any database call on a transaction that - * has been aborted by Cloud Spanner will throw an {@link AbortedException} instead of being - * retried. Set this to false if your application already uses retry loops to handle {@link - * AbortedException}s. - */ - void setRetryAbortsInternally(boolean retryAbortsInternally); - - /** - * Add a {@link TransactionRetryListener} to this {@link Connection} for testing and logging - * purposes. The method {@link TransactionRetryListener#retryStarting(Timestamp, long, int)} will - * be called before an automatic retry is started for a read/write transaction on this connection. - * The method {@link TransactionRetryListener#retryFinished(Timestamp, long, int, - * TransactionRetryListener.RetryResult)} will be called after the retry has finished. - * - * @param listener The listener to add to this connection. - */ - void addTransactionRetryListener(TransactionRetryListener listener); - - /** - * Removes one existing {@link TransactionRetryListener} from this {@link Connection}, if it is - * present (optional operation). - * - * @param listener The listener to remove from the connection. - * @return true if a listener was removed from the connection. - */ - boolean removeTransactionRetryListener(TransactionRetryListener listener); - - /** - * @return an unmodifiable iterator of the {@link TransactionRetryListener}s registered for this - * connection. - */ - Iterator getTransactionRetryListeners(); - - /** - * Sets the mode for executing DML statements in autocommit mode for this connection. This setting - * is only used when the connection is in autocommit mode, and may only be set while the - * transaction is in autocommit mode and not in a temporary transaction. The autocommit - * transaction mode is reset to its default value of {@link AutocommitDmlMode#TRANSACTIONAL} when - * autocommit mode is changed on the connection. - * - * @param mode The DML autocommit mode to use - *

    - *
  • {@link AutocommitDmlMode#TRANSACTIONAL} DML statements are executed as single - * read-write transaction. After successful execution, the DML statement is guaranteed - * to have been applied exactly once to the database - *
  • {@link AutocommitDmlMode#PARTITIONED_NON_ATOMIC} DML statements are executed as - * partitioned DML transactions. If an error occurs during the execution of the DML - * statement, it is possible that the statement has been applied to some but not all of - * the rows specified in the statement. - *
- */ - void setAutocommitDmlMode(AutocommitDmlMode mode); - - /** - * @return the current {@link AutocommitDmlMode} setting for this connection. This method may only - * be called on a connection that is in autocommit mode and not while in a temporary - * transaction. - */ - AutocommitDmlMode getAutocommitDmlMode(); - - /** - * Sets the staleness to use for the current read-only transaction. This method may only be called - * when the transaction mode of the current transaction is {@link - * TransactionMode#READ_ONLY_TRANSACTION} and there is no transaction that has started, or when - * the connection is in read-only and autocommit mode. - * - * @param staleness The staleness to use for the current but not yet started read-only transaction - */ - void setReadOnlyStaleness(TimestampBound staleness); - - /** - * @return the read-only staleness setting for the current read-only transaction. This method may - * only be called when the current transaction is a read-only transaction, or when the - * connection is in read-only and autocommit mode. - */ - TimestampBound getReadOnlyStaleness(); - - /** - * Sets the query optimizer version to use for this connection. - * - * @param optimizerVersion The query optimizer version to use. Must be a valid optimizer version - * number, the string LATEST or an empty string. The empty string will instruct - * the connection to use the optimizer version that is defined in the environment variable - * SPANNER_OPTIMIZER_VERSION. If no value is specified in the environment - * variable, the default query optimizer of Cloud Spanner is used. - */ - void setOptimizerVersion(String optimizerVersion); - - /** - * Gets the current query optimizer version of this connection. - * - * @return The query optimizer version that is currently used by this connection. - */ - String getOptimizerVersion(); - - /** - * Commits the current transaction of this connection. All mutations that have been buffered - * during the current transaction will be written to the database. - * - *

If the connection is in autocommit mode, and there is a temporary transaction active on this - * connection, calling this method will cause the connection to go back to autocommit mode after - * calling this method. - * - *

This method will throw a {@link SpannerException} with code {@link - * ErrorCode#DEADLINE_EXCEEDED} if a statement timeout has been set on this connection, and the - * commit operation takes longer than this timeout. - * - *

    - *
  • Calling this method on a connection in autocommit mode and with no temporary transaction, - * will cause an exception - *
  • Calling this method while a DDL batch is active will cause an exception - *
  • Calling this method on a connection with a transaction that has not yet started, will end - * that transaction and any properties that might have been set on that transaction, and - * return the connection to its previous state. This means that if a transaction is created - * and set to read-only, and then committed before any statements have been executed, the - * read-only transaction is ended and any subsequent statements will be executed in a new - * transaction. If the connection is in read-write mode, the default for new transactions - * will be {@link TransactionMode#READ_WRITE_TRANSACTION}. Committing an empty transaction - * also does not generate a read timestamp or a commit timestamp, and calling one of the - * methods {@link Connection#getReadTimestamp()} or {@link Connection#getCommitTimestamp()} - * will cause an exception. - *
  • Calling this method on a connection with a {@link TransactionMode#READ_ONLY_TRANSACTION} - * transaction will end that transaction. If the connection is in read-write mode, any - * subsequent transaction will by default be a {@link - * TransactionMode#READ_WRITE_TRANSACTION} transaction, unless any following transaction is - * explicitly set to {@link TransactionMode#READ_ONLY_TRANSACTION} - *
  • Calling this method on a connection with a {@link TransactionMode#READ_WRITE_TRANSACTION} - * transaction will send all buffered mutations to the database, commit any DML statements - * that have been executed during this transaction and end the transaction. - *
- */ - void commit(); - - /** - * Rollbacks the current transaction of this connection. All mutations or DDL statements that have - * been buffered during the current transaction will be removed from the buffer. - * - *

If the connection is in autocommit mode, and there is a temporary transaction active on this - * connection, calling this method will cause the connection to go back to autocommit mode after - * calling this method. - * - *

    - *
  • Calling this method on a connection in autocommit mode and with no temporary transaction - * will cause an exception - *
  • Calling this method while a DDL batch is active will cause an exception - *
  • Calling this method on a connection with a transaction that has not yet started, will end - * that transaction and any properties that might have been set on that transaction, and - * return the connection to its previous state. This means that if a transaction is created - * and set to read-only, and then rolled back before any statements have been executed, the - * read-only transaction is ended and any subsequent statements will be executed in a new - * transaction. If the connection is in read-write mode, the default for new transactions - * will be {@link TransactionMode#READ_WRITE_TRANSACTION}. - *
  • Calling this method on a connection with a {@link TransactionMode#READ_ONLY_TRANSACTION} - * transaction will end that transaction. If the connection is in read-write mode, any - * subsequent transaction will by default be a {@link - * TransactionMode#READ_WRITE_TRANSACTION} transaction, unless any following transaction is - * explicitly set to {@link TransactionMode#READ_ONLY_TRANSACTION} - *
  • Calling this method on a connection with a {@link TransactionMode#READ_WRITE_TRANSACTION} - * transaction will clear all buffered mutations, rollback any DML statements that have been - * executed during this transaction and end the transaction. - *
- */ - void rollback(); - - /** - * @return true if this connection has a transaction (that has not necessarily - * started). This method will only return false when the {@link Connection} is in autocommit - * mode and no explicit transaction has been started by calling {@link - * Connection#beginTransaction()}. If the {@link Connection} is not in autocommit mode, there - * will always be a transaction. - */ - boolean isInTransaction(); - - /** - * @return true if this connection has a transaction that has started. A transaction - * is automatically started by the first statement that is executed in the transaction. - */ - boolean isTransactionStarted(); - - /** - * Returns the read timestamp of the current/last {@link TransactionMode#READ_ONLY_TRANSACTION} - * transaction, or the read timestamp of the last query in autocommit mode. - * - *
    - *
  • When in autocommit mode: The method will return the read timestamp of the last statement - * if the last statement was a query. - *
  • When in a {@link TransactionMode#READ_ONLY_TRANSACTION} transaction that has started (a - * query has been executed), or that has just committed: The read timestamp of the - * transaction. If the read-only transaction was committed without ever executing a query, - * calling this method after the commit will also throw a {@link SpannerException} - *
  • In all other cases the method will throw a {@link SpannerException}. - *
- * - * @return the read timestamp of the current/last read-only transaction. - */ - Timestamp getReadTimestamp(); - - /** - * @return the commit timestamp of the last {@link TransactionMode#READ_WRITE_TRANSACTION} - * transaction. This method will throw a {@link SpannerException} if there is no last {@link - * TransactionMode#READ_WRITE_TRANSACTION} transaction (i.e. the last transaction was a {@link - * TransactionMode#READ_ONLY_TRANSACTION}), or if the last {@link - * TransactionMode#READ_WRITE_TRANSACTION} transaction rolled back. It will also throw a - * {@link SpannerException} if the last {@link TransactionMode#READ_WRITE_TRANSACTION} - * transaction was empty when committed. - */ - Timestamp getCommitTimestamp(); - - /** - * Starts a new DDL batch on this connection. A DDL batch allows several DDL statements to be - * grouped into a batch that can be executed as a group. DDL statements that are issued during the - * batch are buffered locally and will return immediately with an OK. It is not guaranteed that a - * DDL statement that has been issued during a batch will eventually succeed when running the - * batch. Aborting a DDL batch will clear the DDL buffer and will have made no changes to the - * database. Running a DDL batch will send all buffered DDL statements to Spanner, and Spanner - * will try to execute these. The result will be OK if all the statements executed successfully. - * If a statement cannot be executed, Spanner will stop execution at that point and return an - * error message for the statement that could not be executed. Preceding statements of the batch - * may have been executed. - * - *

This method may only be called when the connection is in read/write mode, autocommit mode is - * enabled or no read/write transaction has been started, and there is not already another batch - * active. The connection will only accept DDL statements while a DDL batch is active. - */ - void startBatchDdl(); - - /** - * Starts a new DML batch on this connection. A DML batch allows several DML statements to be - * grouped into a batch that can be executed as a group. DML statements that are issued during the - * batch are buffered locally and will return immediately with an OK. It is not guaranteed that a - * DML statement that has been issued during a batch will eventually succeed when running the - * batch. Aborting a DML batch will clear the DML buffer and will have made no changes to the - * database. Running a DML batch will send all buffered DML statements to Spanner, and Spanner - * will try to execute these. The result will be OK if all the statements executed successfully. - * If a statement cannot be executed, Spanner will stop execution at that point and return {@link - * SpannerBatchUpdateException} for the statement that could not be executed. Preceding statements - * of the batch will have been executed, and the update counts of those statements can be - * retrieved through {@link SpannerBatchUpdateException#getUpdateCounts()}. - * - *

This method may only be called when the connection is in read/write mode, autocommit mode is - * enabled or no read/write transaction has been started, and there is not already another batch - * active. The connection will only accept DML statements while a DML batch is active. - */ - void startBatchDml(); - - /** - * Sends all buffered DML or DDL statements of the current batch to the database, waits for these - * to be executed and ends the current batch. The method will throw an exception for the first - * statement that cannot be executed, or return successfully if all statements could be executed. - * If an exception is thrown for a statement in the batch, the preceding statements in the same - * batch may still have been applied to the database. - * - *

This method may only be called when a (possibly empty) batch is active. - * - * @return the update counts in case of a DML batch. Returns an array containing 1 for each - * successful statement and 0 for each failed statement or statement that was not executed DDL - * in case of a DDL batch. - */ - long[] runBatch(); - - /** - * Clears all buffered statements in the current batch and ends the batch. - * - *

This method may only be called when a (possibly empty) batch is active. - */ - void abortBatch(); - - /** @return true if a DDL batch is active on this connection. */ - boolean isDdlBatchActive(); - - /** @return true if a DML batch is active on this connection. */ - boolean isDmlBatchActive(); - - /** - * Executes the given statement if allowed in the current {@link TransactionMode} and connection - * state. The returned value depends on the type of statement: - * - *

    - *
  • Queries will return a {@link ResultSet} - *
  • 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 - * @return the result of the statement - */ - StatementResult execute(Statement statement); - - /** - * Executes the given statement as a query and returns the result as a {@link ResultSet}. This - * method blocks and waits for a response from Spanner. If the statement does not contain a valid - * query, the method will throw a {@link SpannerException}. - * - * @param query The query statement to execute - * @param options the options to configure the query - * @return a {@link ResultSet} with the results of the query - */ - ResultSet executeQuery(Statement query, QueryOption... options); - - /** - * Analyzes a query and returns query plan and/or query execution statistics information. - * - *

The query plan and query statistics information is contained in {@link - * com.google.spanner.v1.ResultSetStats} that can be accessed by calling {@link - * ResultSet#getStats()} on the returned {@code ResultSet}. - * - *

-   * 
-   * {@code
-   * ResultSet resultSet =
-   *     connection.analyzeQuery(
-   *         Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"),
-   *         ReadContext.QueryAnalyzeMode.PROFILE);
-   * while (resultSet.next()) {
-   *   // Discard the results. We're only processing because getStats() below requires it.
-   * }
-   * ResultSetStats stats = resultSet.getStats();
-   * }
-   * 
-   * 
- * - * @param query the query statement to execute - * @param queryMode the mode in which to execute the query - */ - ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode); - - /** - * Executes the given statement as a DML statement. If the statement does not contain a valid DML - * statement, the method will throw a {@link SpannerException}. - * - * @param update The update statement to execute - * @return the number of records that were inserted/updated/deleted by this statement - */ - long executeUpdate(Statement update); - - /** - * Executes a list of DML statements in a single request. The statements will be executed in order - * and the semantics is the same as if each statement is executed by {@link - * Connection#executeUpdate(Statement)} in a loop. This method returns an array of long integers, - * each representing the number of rows modified by each statement. - * - *

If an individual statement fails, execution stops and a {@code SpannerBatchUpdateException} - * is returned, which includes the error and the number of rows affected by the statements that - * are run prior to the error. - * - *

For example, if statements contains 3 statements, and the 2nd one is not a valid DML. This - * method throws a {@code SpannerBatchUpdateException} that contains the error message from the - * 2nd statement, and an array of length 1 that contains the number of rows modified by the 1st - * statement. The 3rd statement will not run. Executes the given statements as DML statements in - * one batch. If one of the statements does not contain a valid DML statement, the method will - * throw a {@link SpannerException}. - * - * @param updates The update statements that will be executed as one batch. - * @return an array containing the update counts per statement. - */ - long[] executeBatchUpdate(Iterable updates); - - /** - * Writes the specified mutation directly to the database and commits the change. The value is - * readable after the successful completion of this method. Writing multiple mutations to a - * database by calling this method multiple times mode is inefficient, as each call will need a - * round trip to the database. Instead, you should consider writing the mutations together by - * calling {@link Connection#write(Iterable)}. - * - *

Calling this method is only allowed in autocommit mode. See {@link - * Connection#bufferedWrite(Iterable)} for writing mutations in transactions. - * - * @param mutation The {@link Mutation} to write to the database - * @throws SpannerException if the {@link Connection} is not in autocommit mode - */ - void write(Mutation mutation); - - /** - * Writes the specified mutations directly to the database and commits the changes. The values are - * readable after the successful completion of this method. - * - *

Calling this method is only allowed in autocommit mode. See {@link - * Connection#bufferedWrite(Iterable)} for writing mutations in transactions. - * - * @param mutations The {@link Mutation}s to write to the database - * @throws SpannerException if the {@link Connection} is not in autocommit mode - */ - void write(Iterable mutations); - - /** - * Buffers the given mutation locally on the current transaction of this {@link Connection}. The - * mutation will be written to the database at the next call to {@link Connection#commit()}. The - * value will not be readable on this {@link Connection} before the transaction is committed. - * - *

Calling this method is only allowed when not in autocommit mode. See {@link - * Connection#write(Mutation)} for writing mutations in autocommit mode. - * - * @param mutation the {@link Mutation} to buffer for writing to the database on the next commit - * @throws SpannerException if the {@link Connection} is in autocommit mode - */ - void bufferedWrite(Mutation mutation); - - /** - * Buffers the given mutations locally on the current transaction of this {@link Connection}. The - * mutations will be written to the database at the next call to {@link Connection#commit()}. The - * values will not be readable on this {@link Connection} before the transaction is committed. - * - *

Calling this method is only allowed when not in autocommit mode. See {@link - * Connection#write(Iterable)} for writing mutations in autocommit mode. - * - * @param mutations the {@link Mutation}s to buffer for writing to the database on the next commit - * @throws SpannerException if the {@link Connection} is in autocommit mode - */ - void bufferedWrite(Iterable mutations); -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionImpl.java b/src/main/java/com/google/cloud/spanner/jdbc/ConnectionImpl.java deleted file mode 100644 index 2a83fca4..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionImpl.java +++ /dev/null @@ -1,1018 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.jdbc.StatementExecutor.StatementTimeout; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Stack; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import org.threeten.bp.Instant; - -/** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */ -class ConnectionImpl implements Connection { - private static final String CLOSED_ERROR_MSG = "This connection is closed"; - private static final String ONLY_ALLOWED_IN_AUTOCOMMIT = - "This method may only be called while in autocommit mode"; - private static final String NOT_ALLOWED_IN_AUTOCOMMIT = - "This method may not be called while in autocommit mode"; - - /** - * Exception that is used to register the stacktrace of the code that opened a {@link Connection}. - * This exception is logged if the application closes without first closing the connection. - */ - static class LeakedConnectionException extends RuntimeException { - private static final long serialVersionUID = 7119433786832158700L; - - private LeakedConnectionException() { - super("Connection was opened at " + Instant.now()); - } - } - - private volatile LeakedConnectionException leakedException = new LeakedConnectionException(); - private final SpannerPool spannerPool; - private final StatementParser parser = StatementParser.INSTANCE; - /** - * The {@link ConnectionStatementExecutor} is responsible for translating parsed {@link - * ClientSideStatement}s into actual method calls on this {@link ConnectionImpl}. I.e. the {@link - * ClientSideStatement} 'SET AUTOCOMMIT ON' will be translated into the method call {@link - * ConnectionImpl#setAutocommit(boolean)} with value true. - */ - private final ConnectionStatementExecutor connectionStatementExecutor = - new ConnectionStatementExecutorImpl(this); - - /** Simple thread factory that is used for fire-and-forget rollbacks. */ - static final class DaemonThreadFactory implements ThreadFactory { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName("connection-rollback-executor"); - t.setDaemon(true); - return t; - } - } - - /** - * Statements are executed using a separate thread in order to be able to cancel these. Statements - * are automatically cancelled if the configured {@link ConnectionImpl#statementTimeout} is - * exceeded. In autocommit mode, the connection will try to rollback the effects of an update - * statement, but this is not guaranteed to actually succeed. - */ - private final StatementExecutor statementExecutor; - - /** - * The {@link ConnectionOptions} that were used to create this {@link ConnectionImpl}. This is - * retained as it is used for getting a {@link Spanner} object and removing this connection from - * the {@link SpannerPool}. - */ - private final ConnectionOptions options; - - /** The supported batch modes. */ - enum BatchMode { - NONE, - DDL, - DML; - } - - /** - * This query option is used internally to indicate that a query is executed by the library itself - * to fetch metadata. These queries are specifically allowed to be executed even when a DDL batch - * is active. - */ - static final class InternalMetadataQuery implements QueryOption { - static final InternalMetadataQuery INSTANCE = new InternalMetadataQuery(); - - private InternalMetadataQuery() {} - } - - /** The combination of all transaction modes and batch modes. */ - enum UnitOfWorkType { - READ_ONLY_TRANSACTION { - @Override - TransactionMode getTransactionMode() { - return TransactionMode.READ_ONLY_TRANSACTION; - } - }, - READ_WRITE_TRANSACTION { - @Override - TransactionMode getTransactionMode() { - return TransactionMode.READ_WRITE_TRANSACTION; - } - }, - DML_BATCH { - @Override - TransactionMode getTransactionMode() { - return TransactionMode.READ_WRITE_TRANSACTION; - } - }, - DDL_BATCH { - @Override - TransactionMode getTransactionMode() { - return null; - } - }; - - abstract TransactionMode getTransactionMode(); - - static UnitOfWorkType of(TransactionMode transactionMode) { - switch (transactionMode) { - case READ_ONLY_TRANSACTION: - return UnitOfWorkType.READ_ONLY_TRANSACTION; - case READ_WRITE_TRANSACTION: - return UnitOfWorkType.READ_WRITE_TRANSACTION; - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Unknown transaction mode: " + transactionMode); - } - } - } - - private StatementExecutor.StatementTimeout statementTimeout = - new StatementExecutor.StatementTimeout(); - private boolean closed = false; - - private final Spanner spanner; - private DdlClient ddlClient; - private DatabaseClient dbClient; - private boolean autocommit; - private boolean readOnly; - - private UnitOfWork currentUnitOfWork = null; - /** - * The {@link ConnectionImpl#inTransaction} field is only used in autocommit mode to indicate that - * the user has explicitly started a transaction. - */ - private boolean inTransaction = false; - /** - * This field is used to indicate that a transaction begin has been indicated. This is done by - * calling beginTransaction or by setting a transaction property while not in autocommit mode. - */ - private boolean transactionBeginMarked = false; - - private BatchMode batchMode; - private UnitOfWorkType unitOfWorkType; - private final Stack transactionStack = new Stack<>(); - private boolean retryAbortsInternally; - private final List transactionRetryListeners = new ArrayList<>(); - private AutocommitDmlMode autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL; - private TimestampBound readOnlyStaleness = TimestampBound.strong(); - private QueryOptions queryOptions = QueryOptions.getDefaultInstance(); - - /** Create a connection and register it in the SpannerPool. */ - ConnectionImpl(ConnectionOptions options) { - Preconditions.checkNotNull(options); - this.statementExecutor = new StatementExecutor(options.getStatementExecutionInterceptors()); - this.spannerPool = SpannerPool.INSTANCE; - this.options = options; - this.spanner = spannerPool.getSpanner(options, this); - this.dbClient = spanner.getDatabaseClient(options.getDatabaseId()); - this.retryAbortsInternally = options.isRetryAbortsInternally(); - this.readOnly = options.isReadOnly(); - this.autocommit = options.isAutocommit(); - this.queryOptions = this.queryOptions.toBuilder().mergeFrom(options.getQueryOptions()).build(); - this.ddlClient = createDdlClient(); - setDefaultTransactionOptions(); - } - - /** Constructor only for test purposes. */ - @VisibleForTesting - ConnectionImpl( - ConnectionOptions options, - SpannerPool spannerPool, - DdlClient ddlClient, - DatabaseClient dbClient) { - Preconditions.checkNotNull(options); - Preconditions.checkNotNull(spannerPool); - Preconditions.checkNotNull(ddlClient); - Preconditions.checkNotNull(dbClient); - this.statementExecutor = - new StatementExecutor(Collections.emptyList()); - this.spannerPool = spannerPool; - this.options = options; - this.spanner = spannerPool.getSpanner(options, this); - this.ddlClient = ddlClient; - this.dbClient = dbClient; - setReadOnly(options.isReadOnly()); - setAutocommit(options.isAutocommit()); - setDefaultTransactionOptions(); - } - - private DdlClient createDdlClient() { - return DdlClient.newBuilder() - .setDatabaseAdminClient(spanner.getDatabaseAdminClient()) - .setInstanceId(options.getInstanceId()) - .setDatabaseName(options.getDatabaseName()) - .build(); - } - - @Override - public void close() { - if (!isClosed()) { - try { - if (isTransactionStarted()) { - try { - rollback(); - } catch (Exception e) { - // Ignore as we are closing the connection. - } - } - statementExecutor.shutdownNow(); - spannerPool.removeConnection(options, this); - leakedException = null; - } finally { - this.closed = true; - } - } - } - - /** Get the current unit-of-work type of this connection. */ - UnitOfWorkType getUnitOfWorkType() { - return unitOfWorkType; - } - - /** Get the current batch mode of this connection. */ - BatchMode getBatchMode() { - return batchMode; - } - - /** @return true if this connection is in a batch. */ - boolean isInBatch() { - return batchMode != BatchMode.NONE; - } - - /** Get the call stack from when the {@link Connection} was opened. */ - LeakedConnectionException getLeakedException() { - return leakedException; - } - - @Override - public boolean isClosed() { - return closed; - } - - @Override - public void setAutocommit(boolean autocommit) { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isBatchActive(), "Cannot set autocommit while in a batch"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), "Cannot set autocommit while a transaction is active"); - ConnectionPreconditions.checkState( - !(isAutocommit() && isInTransaction()), - "Cannot set autocommit while in a temporary transaction"); - ConnectionPreconditions.checkState( - !transactionBeginMarked, "Cannot set autocommit when a transaction has begun"); - this.autocommit = autocommit; - clearLastTransactionAndSetDefaultTransactionOptions(); - // Reset the readOnlyStaleness value if it is no longer compatible with the new autocommit - // value. - if (!autocommit - && (readOnlyStaleness.getMode() == Mode.MAX_STALENESS - || readOnlyStaleness.getMode() == Mode.MIN_READ_TIMESTAMP)) { - readOnlyStaleness = TimestampBound.strong(); - } - } - - @Override - public boolean isAutocommit() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return internalIsAutocommit(); - } - - private boolean internalIsAutocommit() { - return this.autocommit; - } - - @Override - public void setReadOnly(boolean readOnly) { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isBatchActive(), "Cannot set read-only while in a batch"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), "Cannot set read-only while a transaction is active"); - ConnectionPreconditions.checkState( - !(isAutocommit() && isInTransaction()), - "Cannot set read-only while in a temporary transaction"); - ConnectionPreconditions.checkState( - !transactionBeginMarked, "Cannot set read-only when a transaction has begun"); - this.readOnly = readOnly; - clearLastTransactionAndSetDefaultTransactionOptions(); - } - - @Override - public boolean isReadOnly() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.readOnly; - } - - private void clearLastTransactionAndSetDefaultTransactionOptions() { - setDefaultTransactionOptions(); - this.currentUnitOfWork = null; - } - - @Override - public void setAutocommitDmlMode(AutocommitDmlMode mode) { - Preconditions.checkNotNull(mode); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "Cannot set autocommit DML mode while in a batch"); - ConnectionPreconditions.checkState( - !isInTransaction() && isAutocommit(), - "Cannot set autocommit DML mode while not in autocommit mode or while a transaction is active"); - ConnectionPreconditions.checkState( - !isReadOnly(), "Cannot set autocommit DML mode for a read-only connection"); - this.autocommitDmlMode = mode; - } - - @Override - public AutocommitDmlMode getAutocommitDmlMode() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "Cannot get autocommit DML mode while in a batch"); - return this.autocommitDmlMode; - } - - @Override - public void setReadOnlyStaleness(TimestampBound staleness) { - Preconditions.checkNotNull(staleness); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isBatchActive(), "Cannot set read-only while in a batch"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), - "Cannot set read-only staleness when a transaction has been started"); - if (staleness.getMode() == Mode.MAX_STALENESS - || staleness.getMode() == Mode.MIN_READ_TIMESTAMP) { - // These values are only allowed in autocommit mode. - ConnectionPreconditions.checkState( - isAutocommit() && !inTransaction, - "MAX_STALENESS and MIN_READ_TIMESTAMP are only allowed in autocommit mode"); - } - this.readOnlyStaleness = staleness; - } - - @Override - public TimestampBound getReadOnlyStaleness() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isBatchActive(), "Cannot get read-only while in a batch"); - return this.readOnlyStaleness; - } - - @Override - public void setOptimizerVersion(String optimizerVersion) { - Preconditions.checkNotNull(optimizerVersion); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.queryOptions = queryOptions.toBuilder().setOptimizerVersion(optimizerVersion).build(); - } - - @Override - public String getOptimizerVersion() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.queryOptions.getOptimizerVersion(); - } - - @Override - public void setStatementTimeout(long timeout, TimeUnit unit) { - Preconditions.checkArgument(timeout > 0L, "Zero or negative timeout values are not allowed"); - Preconditions.checkArgument( - StatementTimeout.isValidTimeoutUnit(unit), - "Time unit must be one of NANOSECONDS, MICROSECONDS, MILLISECONDS or SECONDS"); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.statementTimeout.setTimeoutValue(timeout, unit); - } - - @Override - public void clearStatementTimeout() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - this.statementTimeout.clearTimeoutValue(); - } - - @Override - public long getStatementTimeout(TimeUnit unit) { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - Preconditions.checkArgument( - StatementTimeout.isValidTimeoutUnit(unit), - "Time unit must be one of NANOSECONDS, MICROSECONDS, MILLISECONDS or SECONDS"); - return this.statementTimeout.getTimeoutValue(unit); - } - - @Override - public boolean hasStatementTimeout() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.statementTimeout.hasTimeout(); - } - - @Override - public void cancel() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - if (this.currentUnitOfWork != null) { - currentUnitOfWork.cancel(); - } - } - - @Override - public TransactionMode getTransactionMode() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isDdlBatchActive(), "This connection is in a DDL batch"); - ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); - return unitOfWorkType.getTransactionMode(); - } - - @Override - public void setTransactionMode(TransactionMode transactionMode) { - Preconditions.checkNotNull(transactionMode); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "Cannot set transaction mode while in a batch"); - ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), - "The transaction mode cannot be set after the transaction has started"); - ConnectionPreconditions.checkState( - !isReadOnly() || transactionMode == TransactionMode.READ_ONLY_TRANSACTION, - "The transaction mode can only be READ_ONLY when the connection is in read_only mode"); - - this.transactionBeginMarked = true; - this.unitOfWorkType = UnitOfWorkType.of(transactionMode); - } - - /** - * Throws an {@link SpannerException} with code {@link ErrorCode#FAILED_PRECONDITION} if the - * current state of this connection does not allow changing the setting for retryAbortsInternally. - */ - private void checkSetRetryAbortsInternallyAvailable() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); - ConnectionPreconditions.checkState( - getTransactionMode() == TransactionMode.READ_WRITE_TRANSACTION, - "RetryAbortsInternally is only available for read-write transactions"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), - "RetryAbortsInternally cannot be set after the transaction has started"); - } - - @Override - public boolean isRetryAbortsInternally() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return retryAbortsInternally; - } - - @Override - public void setRetryAbortsInternally(boolean retryAbortsInternally) { - checkSetRetryAbortsInternallyAvailable(); - this.retryAbortsInternally = retryAbortsInternally; - } - - @Override - public void addTransactionRetryListener(TransactionRetryListener listener) { - Preconditions.checkNotNull(listener); - transactionRetryListeners.add(listener); - } - - @Override - public boolean removeTransactionRetryListener(TransactionRetryListener listener) { - Preconditions.checkNotNull(listener); - return transactionRetryListeners.remove(listener); - } - - @Override - public Iterator getTransactionRetryListeners() { - return Collections.unmodifiableList(transactionRetryListeners).iterator(); - } - - @Override - public boolean isInTransaction() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return internalIsInTransaction(); - } - - /** Returns true if this connection currently is in a transaction (and not a batch). */ - private boolean internalIsInTransaction() { - return !isDdlBatchActive() && (!internalIsAutocommit() || inTransaction); - } - - @Override - public boolean isTransactionStarted() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return internalIsTransactionStarted(); - } - - private boolean internalIsTransactionStarted() { - if (internalIsAutocommit() && !inTransaction) { - return false; - } - return internalIsInTransaction() - && this.currentUnitOfWork != null - && this.currentUnitOfWork.getState() == UnitOfWorkState.STARTED; - } - - @Override - public Timestamp getReadTimestamp() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - this.currentUnitOfWork != null, "There is no transaction on this connection"); - return this.currentUnitOfWork.getReadTimestamp(); - } - - Timestamp getReadTimestampOrNull() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.currentUnitOfWork == null ? null : this.currentUnitOfWork.getReadTimestampOrNull(); - } - - @Override - public Timestamp getCommitTimestamp() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - this.currentUnitOfWork != null, "There is no transaction on this connection"); - return this.currentUnitOfWork.getCommitTimestamp(); - } - - Timestamp getCommitTimestampOrNull() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.currentUnitOfWork == null - ? null - : this.currentUnitOfWork.getCommitTimestampOrNull(); - } - - /** Resets this connection to its default transaction options. */ - private void setDefaultTransactionOptions() { - if (transactionStack.isEmpty()) { - unitOfWorkType = - isReadOnly() - ? UnitOfWorkType.READ_ONLY_TRANSACTION - : UnitOfWorkType.READ_WRITE_TRANSACTION; - batchMode = BatchMode.NONE; - } else { - popUnitOfWorkFromTransactionStack(); - } - } - - @Override - public void beginTransaction() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "This connection has an active batch and cannot begin a transaction"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), - "Beginning a new transaction is not allowed when a transaction is already running"); - ConnectionPreconditions.checkState(!transactionBeginMarked, "A transaction has already begun"); - - transactionBeginMarked = true; - clearLastTransactionAndSetDefaultTransactionOptions(); - if (isAutocommit()) { - inTransaction = true; - } - } - - /** Internal interface for ending a transaction (commit/rollback). */ - private static interface EndTransactionMethod { - public void end(UnitOfWork t); - } - - private static final class Commit implements EndTransactionMethod { - @Override - public void end(UnitOfWork t) { - t.commit(); - } - } - - private final Commit commit = new Commit(); - - @Override - public void commit() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - endCurrentTransaction(commit); - } - - private static final class Rollback implements EndTransactionMethod { - @Override - public void end(UnitOfWork t) { - t.rollback(); - } - } - - private final Rollback rollback = new Rollback(); - - @Override - public void rollback() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - endCurrentTransaction(rollback); - } - - private void endCurrentTransaction(EndTransactionMethod endTransactionMethod) { - ConnectionPreconditions.checkState(!isBatchActive(), "This connection has an active batch"); - ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction"); - try { - if (isTransactionStarted()) { - endTransactionMethod.end(getCurrentUnitOfWorkOrStartNewUnitOfWork()); - } else { - this.currentUnitOfWork = null; - } - } finally { - transactionBeginMarked = false; - if (isAutocommit()) { - inTransaction = false; - } - setDefaultTransactionOptions(); - } - } - - @Override - public StatementResult execute(Statement statement) { - Preconditions.checkNotNull(statement); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = parser.parse(statement, this.queryOptions); - switch (parsedStatement.getType()) { - case CLIENT_SIDE: - return parsedStatement - .getClientSideStatement() - .execute(connectionStatementExecutor, parsedStatement.getSqlWithoutComments()); - case QUERY: - return StatementResultImpl.of(internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)); - case UPDATE: - return StatementResultImpl.of(internalExecuteUpdate(parsedStatement)); - case DDL: - executeDdl(parsedStatement); - return StatementResultImpl.noResult(); - case UNKNOWN: - default: - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Unknown statement: " + parsedStatement.getSqlWithoutComments()); - } - - @Override - public ResultSet executeQuery(Statement query, QueryOption... options) { - return parseAndExecuteQuery(query, AnalyzeMode.NONE, options); - } - - @Override - public ResultSet analyzeQuery(Statement query, QueryAnalyzeMode queryMode) { - Preconditions.checkNotNull(queryMode); - return parseAndExecuteQuery(query, AnalyzeMode.of(queryMode)); - } - - /** - * Parses the given statement as a query and executes it. Throws a {@link SpannerException} if the - * statement is not a query. - */ - private ResultSet parseAndExecuteQuery( - Statement query, AnalyzeMode analyzeMode, QueryOption... options) { - Preconditions.checkNotNull(query); - Preconditions.checkNotNull(analyzeMode); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = parser.parse(query, this.queryOptions); - if (parsedStatement.isQuery()) { - switch (parsedStatement.getType()) { - case CLIENT_SIDE: - return parsedStatement - .getClientSideStatement() - .execute(connectionStatementExecutor, parsedStatement.getSqlWithoutComments()) - .getResultSet(); - case QUERY: - return internalExecuteQuery(parsedStatement, analyzeMode, options); - case UPDATE: - case DDL: - case UNKNOWN: - default: - } - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Statement is not a query: " + parsedStatement.getSqlWithoutComments()); - } - - @Override - public long executeUpdate(Statement update) { - Preconditions.checkNotNull(update); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ParsedStatement parsedStatement = parser.parse(update); - if (parsedStatement.isUpdate()) { - switch (parsedStatement.getType()) { - case UPDATE: - return internalExecuteUpdate(parsedStatement); - case CLIENT_SIDE: - case QUERY: - case DDL: - case UNKNOWN: - default: - } - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Statement is not an update statement: " + parsedStatement.getSqlWithoutComments()); - } - - @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 = parser.parse(update); - if (parsedStatement.isUpdate()) { - 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 internalExecuteBatchUpdate(parsedStatements); - } - - private ResultSet internalExecuteQuery( - final ParsedStatement statement, - final AnalyzeMode analyzeMode, - final QueryOption... options) { - Preconditions.checkArgument( - statement.getType() == StatementType.QUERY, "Statement must be a query"); - UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeQuery(statement, analyzeMode, options); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } - } - - private long internalExecuteUpdate(final ParsedStatement update) { - Preconditions.checkArgument( - update.getType() == StatementType.UPDATE, "Statement must be an update"); - UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeUpdate(update); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } - } - - private long[] internalExecuteBatchUpdate(final List updates) { - UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(); - try { - return transaction.executeBatchUpdate(updates); - } catch (SpannerException e) { - // In case of a timed out or cancelled query we need to replace the executor to ensure that we - // have an executor that is not busy executing a statement. Although we try to cancel the - // current statement, it is not guaranteed to actually stop the execution directly. - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.statementExecutor.recreate(); - } - throw e; - } - } - - /** - * Returns the current {@link UnitOfWork} of this connection, or creates a new one based on the - * current transaction settings of the connection and returns that. - */ - @VisibleForTesting - UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { - if (this.currentUnitOfWork == null || !this.currentUnitOfWork.isActive()) { - this.currentUnitOfWork = createNewUnitOfWork(); - } - return this.currentUnitOfWork; - } - - private UnitOfWork createNewUnitOfWork() { - if (isAutocommit() && !isInTransaction() && !isInBatch()) { - return SingleUseTransaction.newBuilder() - .setDdlClient(ddlClient) - .setDatabaseClient(dbClient) - .setReadOnly(isReadOnly()) - .setReadOnlyStaleness(readOnlyStaleness) - .setAutocommitDmlMode(autocommitDmlMode) - .setStatementTimeout(statementTimeout) - .withStatementExecutor(statementExecutor) - .build(); - } else { - switch (getUnitOfWorkType()) { - case READ_ONLY_TRANSACTION: - return ReadOnlyTransaction.newBuilder() - .setDatabaseClient(dbClient) - .setReadOnlyStaleness(readOnlyStaleness) - .setStatementTimeout(statementTimeout) - .withStatementExecutor(statementExecutor) - .build(); - case READ_WRITE_TRANSACTION: - return ReadWriteTransaction.newBuilder() - .setDatabaseClient(dbClient) - .setRetryAbortsInternally(retryAbortsInternally) - .setTransactionRetryListeners(transactionRetryListeners) - .setStatementTimeout(statementTimeout) - .withStatementExecutor(statementExecutor) - .build(); - case DML_BATCH: - // A DML batch can run inside the current transaction. It should therefore only - // temporarily replace the current transaction. - pushCurrentUnitOfWorkToTransactionStack(); - return DmlBatch.newBuilder() - .setTransaction(currentUnitOfWork) - .setStatementTimeout(statementTimeout) - .withStatementExecutor(statementExecutor) - .build(); - case DDL_BATCH: - return DdlBatch.newBuilder() - .setDdlClient(ddlClient) - .setDatabaseClient(dbClient) - .setStatementTimeout(statementTimeout) - .withStatementExecutor(statementExecutor) - .build(); - default: - } - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "This connection does not have an active transaction and the state of this connection does not allow any new transactions to be started"); - } - - /** Pushes the current unit of work to the stack of nested transactions. */ - private void pushCurrentUnitOfWorkToTransactionStack() { - Preconditions.checkState(currentUnitOfWork != null, "There is no current transaction"); - transactionStack.push(currentUnitOfWork); - } - - /** Set the {@link UnitOfWork} of this connection back to the previous {@link UnitOfWork}. */ - private void popUnitOfWorkFromTransactionStack() { - Preconditions.checkState( - !transactionStack.isEmpty(), "There is no unit of work in the transaction stack"); - this.currentUnitOfWork = transactionStack.pop(); - } - - private void executeDdl(ParsedStatement ddl) { - getCurrentUnitOfWorkOrStartNewUnitOfWork().executeDdl(ddl); - } - - @Override - public void write(Mutation mutation) { - Preconditions.checkNotNull(mutation); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isAutocommit(), ONLY_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutation); - } - - @Override - public void write(Iterable mutations) { - Preconditions.checkNotNull(mutations); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isAutocommit(), ONLY_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutations); - } - - @Override - public void bufferedWrite(Mutation mutation) { - Preconditions.checkNotNull(mutation); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isAutocommit(), NOT_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutation); - } - - @Override - public void bufferedWrite(Iterable mutations) { - Preconditions.checkNotNull(mutations); - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(!isAutocommit(), NOT_ALLOWED_IN_AUTOCOMMIT); - getCurrentUnitOfWorkOrStartNewUnitOfWork().write(mutations); - } - - @Override - public void startBatchDdl() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "Cannot start a DDL batch when a batch is already active"); - ConnectionPreconditions.checkState( - !isReadOnly(), "Cannot start a DDL batch when the connection is in read-only mode"); - ConnectionPreconditions.checkState( - !isTransactionStarted(), "Cannot start a DDL batch while a transaction is active"); - ConnectionPreconditions.checkState( - !(isAutocommit() && isInTransaction()), - "Cannot start a DDL batch while in a temporary transaction"); - ConnectionPreconditions.checkState( - !transactionBeginMarked, "Cannot start a DDL batch when a transaction has begun"); - this.batchMode = BatchMode.DDL; - this.unitOfWorkType = UnitOfWorkType.DDL_BATCH; - this.currentUnitOfWork = createNewUnitOfWork(); - } - - @Override - public void startBatchDml() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState( - !isBatchActive(), "Cannot start a DML batch when a batch is already active"); - ConnectionPreconditions.checkState( - !isReadOnly(), "Cannot start a DML batch when the connection is in read-only mode"); - ConnectionPreconditions.checkState( - !(isInTransaction() && getTransactionMode() == TransactionMode.READ_ONLY_TRANSACTION), - "Cannot start a DML batch when a read-only transaction is in progress"); - // Make sure that there is a current unit of work that the batch can use. - getCurrentUnitOfWorkOrStartNewUnitOfWork(); - // Then create the DML batch. - this.batchMode = BatchMode.DML; - this.unitOfWorkType = UnitOfWorkType.DML_BATCH; - this.currentUnitOfWork = createNewUnitOfWork(); - } - - @Override - public long[] runBatch() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isBatchActive(), "This connection has no active batch"); - try { - if (this.currentUnitOfWork != null) { - return this.currentUnitOfWork.runBatch(); - } - return new long[0]; - } finally { - this.batchMode = BatchMode.NONE; - setDefaultTransactionOptions(); - } - } - - @Override - public void abortBatch() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - ConnectionPreconditions.checkState(isBatchActive(), "This connection has no active batch"); - try { - if (this.currentUnitOfWork != null) { - this.currentUnitOfWork.abortBatch(); - } - } finally { - this.batchMode = BatchMode.NONE; - setDefaultTransactionOptions(); - } - } - - private boolean isBatchActive() { - return isDdlBatchActive() || isDmlBatchActive(); - } - - @Override - public boolean isDdlBatchActive() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.batchMode == BatchMode.DDL; - } - - @Override - public boolean isDmlBatchActive() { - ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG); - return this.batchMode == BatchMode.DML; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionOptions.java b/src/main/java/com/google/cloud/spanner/jdbc/ConnectionOptions.java deleted file mode 100644 index 4ec727cd..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionOptions.java +++ /dev/null @@ -1,666 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.auth.Credentials; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.NoCredentials; -import com.google.cloud.ServiceOptions; -import com.google.cloud.spanner.DatabaseId; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.SpannerOptions; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.Sets; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Options for creating a {@link Connection} to a Google Cloud Spanner database. - * - *

Usage: - * - *

- * 
- * {@code
- * ConnectionOptions options = ConnectionOptions.newBuilder()
- *       .setUri("cloudspanner:/projects/my_project_id/instances/my_instance_id/databases/my_database_name?autocommit=false")
- *       .setCredentialsUrl("/home/cloudspanner-keys/my-key.json")
- *       .build();
- * try(Connection connection = options.getConnection()) {
- *   try(ResultSet rs = connection.executeQuery(Statement.of("SELECT SingerId, AlbumId, MarketingBudget FROM Albums"))) {
- *     while(rs.next()) {
- *       // do something
- *     }
- *   }
- * }
- * }
- * 
- * 
- */ -class ConnectionOptions { - /** Supported connection properties that can be included in the connection URI. */ - public static class ConnectionProperty { - private static final String[] BOOLEAN_VALUES = new String[] {"true", "false"}; - private final String name; - private final String description; - private final String defaultValue; - private final String[] validValues; - private final int hashCode; - - private static ConnectionProperty createStringProperty(String name, String description) { - return new ConnectionProperty(name, description, "", null); - } - - private static ConnectionProperty createBooleanProperty( - String name, String description, boolean defaultValue) { - return new ConnectionProperty( - name, description, String.valueOf(defaultValue), BOOLEAN_VALUES); - } - - private static ConnectionProperty createEmptyProperty(String name) { - return new ConnectionProperty(name, "", "", null); - } - - private ConnectionProperty( - String name, String description, String defaultValue, String[] validValues) { - Preconditions.checkNotNull(name); - Preconditions.checkNotNull(description); - Preconditions.checkNotNull(defaultValue); - this.name = name; - this.description = description; - this.defaultValue = defaultValue; - this.validValues = validValues; - this.hashCode = name.toLowerCase().hashCode(); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ConnectionProperty)) { - return false; - } - return ((ConnectionProperty) o).name.equalsIgnoreCase(this.name); - } - - /** @return the name of this connection property. */ - public String getName() { - return name; - } - - /** @return the description of this connection property. */ - public String getDescription() { - return description; - } - - /** @return the default value of this connection property. */ - public String getDefaultValue() { - return defaultValue; - } - - /** - * @return the valid values for this connection property. null indicates no - * restriction. - */ - public String[] getValidValues() { - return validValues; - } - } - - private static final boolean DEFAULT_USE_PLAIN_TEXT = false; - static final boolean DEFAULT_AUTOCOMMIT = true; - static final boolean DEFAULT_READONLY = false; - static final boolean DEFAULT_RETRY_ABORTS_INTERNALLY = true; - private static final String DEFAULT_CREDENTIALS = null; - private static final String DEFAULT_OAUTH_TOKEN = null; - private static final String DEFAULT_NUM_CHANNELS = null; - private static final String DEFAULT_USER_AGENT = null; - private static final String DEFAULT_OPTIMIZER_VERSION = ""; - - private static final String PLAIN_TEXT_PROTOCOL = "http:"; - private static final String HOST_PROTOCOL = "https:"; - private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; - /** Use plain text is only for local testing purposes. */ - private static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; - /** Name of the 'autocommit' connection property. */ - public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit"; - /** Name of the 'readonly' connection property. */ - public static final String READONLY_PROPERTY_NAME = "readonly"; - /** Name of the 'retry aborts internally' connection property. */ - public static final String RETRY_ABORTS_INTERNALLY_PROPERTY_NAME = "retryAbortsInternally"; - /** Name of the 'credentials' connection property. */ - public static final String CREDENTIALS_PROPERTY_NAME = "credentials"; - /** - * OAuth token to use for authentication. Cannot be used in combination with a credentials file. - */ - public static final String OAUTH_TOKEN_PROPERTY_NAME = "oauthToken"; - /** Name of the 'numChannels' connection property. */ - public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels"; - /** Custom user agent string is only for other Google libraries. */ - private static final String USER_AGENT_PROPERTY_NAME = "userAgent"; - /** Query optimizer version to use for a connection. */ - private static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion"; - - /** All valid connection properties. */ - public static final Set VALID_PROPERTIES = - Collections.unmodifiableSet( - new HashSet<>( - Arrays.asList( - ConnectionProperty.createBooleanProperty( - AUTOCOMMIT_PROPERTY_NAME, "", DEFAULT_AUTOCOMMIT), - ConnectionProperty.createBooleanProperty( - READONLY_PROPERTY_NAME, "", DEFAULT_READONLY), - ConnectionProperty.createBooleanProperty( - RETRY_ABORTS_INTERNALLY_PROPERTY_NAME, "", DEFAULT_RETRY_ABORTS_INTERNALLY), - ConnectionProperty.createStringProperty(CREDENTIALS_PROPERTY_NAME, ""), - ConnectionProperty.createStringProperty(OAUTH_TOKEN_PROPERTY_NAME, ""), - ConnectionProperty.createStringProperty(NUM_CHANNELS_PROPERTY_NAME, ""), - ConnectionProperty.createBooleanProperty( - USE_PLAIN_TEXT_PROPERTY_NAME, "", DEFAULT_USE_PLAIN_TEXT), - ConnectionProperty.createStringProperty(USER_AGENT_PROPERTY_NAME, ""), - ConnectionProperty.createStringProperty(OPTIMIZER_VERSION_PROPERTY_NAME, "")))); - - private static final Set INTERNAL_PROPERTIES = - Collections.unmodifiableSet( - new HashSet<>( - Arrays.asList( - ConnectionProperty.createStringProperty(USER_AGENT_PROPERTY_NAME, "")))); - private static final Set INTERNAL_VALID_PROPERTIES = - Sets.union(VALID_PROPERTIES, INTERNAL_PROPERTIES); - - /** - * Gets the default project-id for the current environment as defined by {@link - * ServiceOptions#getDefaultProjectId()}, and if none could be found, the project-id of the given - * credentials if it contains any. - * - * @param credentials The credentials to use to get the default project-id if none could be found - * in the environment. - * @return the default project-id. - */ - public static String getDefaultProjectId(Credentials credentials) { - String projectId = SpannerOptions.getDefaultProjectId(); - if (projectId == null - && credentials != null - && credentials instanceof ServiceAccountCredentials) { - projectId = ((ServiceAccountCredentials) credentials).getProjectId(); - } - return projectId; - } - - /** - * Closes all {@link Spanner} instances that have been opened by connections - * during the lifetime of this JVM. Call this method at the end of your application to free up - * resources. You must close all {@link Connection}s that have been opened by your application - * before calling this method. Failing to do so, will cause this method to throw a {@link - * SpannerException}. - * - *

This method is also automatically called by a shutdown hook (see {@link - * Runtime#addShutdownHook(Thread)}) when the JVM is shutdown gracefully. - */ - public static void closeSpanner() { - SpannerPool.INSTANCE.checkAndCloseSpanners(); - } - - /** Builder for {@link ConnectionOptions} instances. */ - public static class Builder { - private String uri; - private String credentialsUrl; - private String oauthToken; - private Credentials credentials; - private List statementExecutionInterceptors = - Collections.emptyList(); - - private Builder() {} - - /** Spanner {@link ConnectionOptions} URI format. */ - public static final String SPANNER_URI_FORMAT = - "(?:cloudspanner:)(?//[\\w.-]+(?:\\.[\\w\\.-]+)*[\\w\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=.]+)?/projects/(?(([a-z]|[-.:]|[0-9])+|(DEFAULT_PROJECT_ID)))(/instances/(?([a-z]|[-]|[0-9])+)(/databases/(?([a-z]|[-]|[_]|[0-9])+))?)?(?:[?|;].*)?"; - - private static final String SPANNER_URI_REGEX = "(?is)^" + SPANNER_URI_FORMAT + "$"; - private static final Pattern SPANNER_URI_PATTERN = Pattern.compile(SPANNER_URI_REGEX); - private static final String HOST_GROUP = "HOSTGROUP"; - private static final String PROJECT_GROUP = "PROJECTGROUP"; - private static final String INSTANCE_GROUP = "INSTANCEGROUP"; - private static final String DATABASE_GROUP = "DATABASEGROUP"; - private static final String DEFAULT_PROJECT_ID_PLACEHOLDER = "DEFAULT_PROJECT_ID"; - - private boolean isValidUri(String uri) { - return SPANNER_URI_PATTERN.matcher(uri).matches(); - } - - /** - * Sets the URI of the Cloud Spanner database to connect to. A connection URI must be specified - * in this format: - * - *

-     * cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\?property-name=property-value[;property-name=property-value]*]?
-     * 
- * - * The property-value strings should be url-encoded. - * - *

The project-id part of the URI may be filled with the placeholder DEFAULT_PROJECT_ID. This - * placeholder will be replaced by the default project id of the environment that is requesting - * a connection. - * - *

The supported properties are: - * - *

    - *
  • credentials (String): URL for the credentials file to use for the connection. This - * property is only used if no credentials have been specified using the {@link - * ConnectionOptions.Builder#setCredentialsUrl(String)} method. If you do not specify any - * credentials at all, the default credentials of the environment as returned by {@link - * GoogleCredentials#getApplicationDefault()} will be used. - *
  • autocommit (boolean): Sets the initial autocommit mode for the connection. Default is - * true. - *
  • readonly (boolean): Sets the initial readonly mode for the connection. Default is - * false. - *
  • retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the - * connection. Default is true. - *
  • optimizerVersion (string): Sets the query optimizer version to use for the connection. - *
- * - * @param uri The URI of the Spanner database to connect to. - * @return this builder - */ - public Builder setUri(String uri) { - Preconditions.checkArgument( - isValidUri(uri), - "The specified URI is not a valid Cloud Spanner connection URI. Please specify a URI in the format \"cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\\?property-name=property-value[;property-name=property-value]*]?\""); - checkValidProperties(uri); - this.uri = uri; - return this; - } - - /** - * Sets the URL of the credentials file to use for this connection. The URL may be a reference - * to a file on the local file system, or to a file on Google Cloud Storage. References to - * Google Cloud Storage files are only allowed when the application is running on Google Cloud - * and the environment has access to the specified storage location. It also requires that the - * Google Cloud Storage client library is present on the class path. The Google Cloud Storage - * library is not automatically added as a dependency by the JDBC driver. - * - *

If you do not specify a credentialsUrl (either by using this setter, or by specifying on - * the connection URI), the credentials returned by {@link - * GoogleCredentials#getApplicationDefault()} will be used for the connection. - * - * @param credentialsUrl A valid file or Google Cloud Storage URL for the credentials file to be - * used. - * @return this builder - */ - public Builder setCredentialsUrl(String credentialsUrl) { - this.credentialsUrl = credentialsUrl; - return this; - } - - /** - * Sets the OAuth token to use with this connection. The token must be a valid token with access - * to the resources (project/instance/database) that the connection will be accessing. This - * authentication method cannot be used in combination with a credentials file. If both an OAuth - * token and a credentials file is specified, the {@link #build()} method will throw an - * exception. - * - * @param oauthToken A valid OAuth token for the Google Cloud project that is used by this - * connection. - * @return this builder - */ - public Builder setOAuthToken(String oauthToken) { - this.oauthToken = oauthToken; - return this; - } - - @VisibleForTesting - Builder setStatementExecutionInterceptors(List interceptors) { - this.statementExecutionInterceptors = interceptors; - return this; - } - - @VisibleForTesting - Builder setCredentials(Credentials credentials) { - this.credentials = credentials; - return this; - } - - /** @return the {@link ConnectionOptions} */ - public ConnectionOptions build() { - Preconditions.checkState(this.uri != null, "Connection URI is required"); - return new ConnectionOptions(this); - } - } - - /** - * Create a {@link Builder} for {@link ConnectionOptions}. Use this method to create {@link - * ConnectionOptions} that can be used to obtain a {@link Connection}. - * - * @return a new {@link Builder} - */ - public static Builder newBuilder() { - return new Builder(); - } - - private final String uri; - private final String credentialsUrl; - private final String oauthToken; - - private final boolean usePlainText; - private final String host; - private final String projectId; - private final String instanceId; - private final String databaseName; - private final Credentials credentials; - private final Integer numChannels; - private final String userAgent; - private final QueryOptions queryOptions; - - private final boolean autocommit; - private final boolean readOnly; - private final boolean retryAbortsInternally; - private final List statementExecutionInterceptors; - - private ConnectionOptions(Builder builder) { - Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); - Preconditions.checkArgument( - matcher.find(), String.format("Invalid connection URI specified: %s", builder.uri)); - checkValidProperties(builder.uri); - - this.uri = builder.uri; - this.credentialsUrl = - builder.credentialsUrl != null ? builder.credentialsUrl : parseCredentials(builder.uri); - this.oauthToken = - builder.oauthToken != null ? builder.oauthToken : parseOAuthToken(builder.uri); - // Check that not both credentials and an OAuth token have been specified. - Preconditions.checkArgument( - (builder.credentials == null && this.credentialsUrl == null) || this.oauthToken == null, - "Cannot specify both credentials and an OAuth token."); - - this.usePlainText = parseUsePlainText(this.uri); - this.userAgent = parseUserAgent(this.uri); - QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder(); - queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri)); - this.queryOptions = queryOptionsBuilder.build(); - - this.host = - matcher.group(Builder.HOST_GROUP) == null - ? DEFAULT_HOST - : (usePlainText ? PLAIN_TEXT_PROTOCOL : HOST_PROTOCOL) - + matcher.group(Builder.HOST_GROUP); - this.instanceId = matcher.group(Builder.INSTANCE_GROUP); - this.databaseName = matcher.group(Builder.DATABASE_GROUP); - // Using credentials on a plain text connection is not allowed, so if the user has not specified - // any credentials and is using a plain text connection, we should not try to get the - // credentials from the environment, but default to NoCredentials. - if (builder.credentials == null - && this.credentialsUrl == null - && this.oauthToken == null - && this.usePlainText) { - this.credentials = NoCredentials.getInstance(); - } else if (this.oauthToken != null) { - this.credentials = new GoogleCredentials(new AccessToken(oauthToken, null)); - } else { - this.credentials = - builder.credentials == null - ? getCredentialsService().createCredentials(this.credentialsUrl) - : builder.credentials; - } - String numChannelsValue = parseNumChannels(builder.uri); - if (numChannelsValue != null) { - try { - this.numChannels = Integer.valueOf(numChannelsValue); - } catch (NumberFormatException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, - "Invalid numChannels value specified: " + numChannelsValue, - e); - } - } else { - this.numChannels = null; - } - - String projectId = matcher.group(Builder.PROJECT_GROUP); - if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { - projectId = getDefaultProjectId(this.credentials); - } - this.projectId = projectId; - - this.autocommit = parseAutocommit(this.uri); - this.readOnly = parseReadOnly(this.uri); - this.retryAbortsInternally = parseRetryAbortsInternally(this.uri); - this.statementExecutionInterceptors = - Collections.unmodifiableList(builder.statementExecutionInterceptors); - } - - @VisibleForTesting - CredentialsService getCredentialsService() { - return CredentialsService.INSTANCE; - } - - @VisibleForTesting - static boolean parseUsePlainText(String uri) { - String value = parseUriProperty(uri, USE_PLAIN_TEXT_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_USE_PLAIN_TEXT; - } - - @VisibleForTesting - static boolean parseAutocommit(String uri) { - String value = parseUriProperty(uri, AUTOCOMMIT_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_AUTOCOMMIT; - } - - @VisibleForTesting - static boolean parseReadOnly(String uri) { - String value = parseUriProperty(uri, READONLY_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_READONLY; - } - - @VisibleForTesting - static boolean parseRetryAbortsInternally(String uri) { - String value = parseUriProperty(uri, RETRY_ABORTS_INTERNALLY_PROPERTY_NAME); - return value != null ? Boolean.valueOf(value) : DEFAULT_RETRY_ABORTS_INTERNALLY; - } - - @VisibleForTesting - static String parseCredentials(String uri) { - String value = parseUriProperty(uri, CREDENTIALS_PROPERTY_NAME); - return value != null ? value : DEFAULT_CREDENTIALS; - } - - @VisibleForTesting - static String parseOAuthToken(String uri) { - String value = parseUriProperty(uri, OAUTH_TOKEN_PROPERTY_NAME); - return value != null ? value : DEFAULT_OAUTH_TOKEN; - } - - @VisibleForTesting - static String parseNumChannels(String uri) { - String value = parseUriProperty(uri, NUM_CHANNELS_PROPERTY_NAME); - return value != null ? value : DEFAULT_NUM_CHANNELS; - } - - @VisibleForTesting - static String parseUserAgent(String uri) { - String value = parseUriProperty(uri, USER_AGENT_PROPERTY_NAME); - return value != null ? value : DEFAULT_USER_AGENT; - } - - @VisibleForTesting - static String parseOptimizerVersion(String uri) { - String value = parseUriProperty(uri, OPTIMIZER_VERSION_PROPERTY_NAME); - return value != null ? value : DEFAULT_OPTIMIZER_VERSION; - } - - @VisibleForTesting - static String parseUriProperty(String uri, String property) { - Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property)); - Matcher matcher = pattern.matcher(uri); - if (matcher.find() && matcher.groupCount() == 1) { - return matcher.group(1); - } - return null; - } - - /** Check that only valid properties have been specified. */ - @VisibleForTesting - static void checkValidProperties(String uri) { - String invalidProperties = ""; - List properties = parseProperties(uri); - for (String property : properties) { - if (!INTERNAL_VALID_PROPERTIES.contains(ConnectionProperty.createEmptyProperty(property))) { - if (invalidProperties.length() > 0) { - invalidProperties = invalidProperties + ", "; - } - invalidProperties = invalidProperties + property; - } - } - Preconditions.checkArgument( - invalidProperties.isEmpty(), - "Invalid properties found in connection URI: " + invalidProperties.toString()); - } - - @VisibleForTesting - static List parseProperties(String uri) { - Pattern pattern = Pattern.compile("(?is)(?:\\?|;)(?.*?)=(?:.*?)"); - Matcher matcher = pattern.matcher(uri); - List res = new ArrayList<>(); - while (matcher.find() && matcher.group("PROPERTY") != null) { - res.add(matcher.group("PROPERTY")); - } - return res; - } - - /** - * Create a new {@link Connection} from this {@link ConnectionOptions}. Calling this method - * multiple times for the same {@link ConnectionOptions} will return multiple instances of {@link - * Connection}s to the same database. - * - * @return a new {@link Connection} to the database referenced by this {@link ConnectionOptions} - */ - public Connection getConnection() { - return new ConnectionImpl(this); - } - - /** The URI of this {@link ConnectionOptions} */ - public String getUri() { - return uri; - } - - /** The credentials URL of this {@link ConnectionOptions} */ - public String getCredentialsUrl() { - return credentialsUrl; - } - - /** The number of channels to use for the connection. */ - public Integer getNumChannels() { - return numChannels; - } - - /** The host and port number that this {@link ConnectionOptions} will connect to */ - public String getHost() { - return host; - } - - /** The Google Project ID that this {@link ConnectionOptions} will connect to */ - public String getProjectId() { - return projectId; - } - - /** The Spanner Instance ID that this {@link ConnectionOptions} will connect to */ - public String getInstanceId() { - return instanceId; - } - - /** The Spanner database name that this {@link ConnectionOptions} will connect to */ - public String getDatabaseName() { - return databaseName; - } - - /** The Spanner {@link DatabaseId} that this {@link ConnectionOptions} will connect to */ - public DatabaseId getDatabaseId() { - Preconditions.checkState(projectId != null, "Project ID is not specified"); - Preconditions.checkState(instanceId != null, "Instance ID is not specified"); - Preconditions.checkState(databaseName != null, "Database name is not specified"); - return DatabaseId.of(projectId, instanceId, databaseName); - } - - /** - * The {@link Credentials} of this {@link ConnectionOptions}. This is either the credentials - * specified in the credentialsUrl or the default Google application credentials - */ - public Credentials getCredentials() { - return credentials; - } - - /** The initial autocommit value for connections created by this {@link ConnectionOptions} */ - public boolean isAutocommit() { - return autocommit; - } - - /** The initial readonly value for connections created by this {@link ConnectionOptions} */ - public boolean isReadOnly() { - return readOnly; - } - - /** - * The initial retryAbortsInternally value for connections created by this {@link - * ConnectionOptions} - */ - public boolean isRetryAbortsInternally() { - return retryAbortsInternally; - } - - /** Use http instead of https. Only valid for (local) test servers. */ - boolean isUsePlainText() { - return usePlainText; - } - - /** - * The (custom) user agent string to use for this connection. If null, then the - * default JDBC user agent string will be used. - */ - String getUserAgent() { - return userAgent; - } - - /** The {@link QueryOptions} to use for the connection. */ - QueryOptions getQueryOptions() { - return queryOptions; - } - - /** Interceptors that should be executed after each statement */ - List getStatementExecutionInterceptors() { - return statementExecutionInterceptors; - } - - @Override - public String toString() { - return getUri(); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionPreconditions.java b/src/main/java/com/google/cloud/spanner/jdbc/ConnectionPreconditions.java deleted file mode 100644 index 02f39533..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionPreconditions.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import javax.annotation.Nullable; - -/** - * Static convenience methods that help a method or constructor in the Connection API to check - * whether it was invoked correctly. - */ -class ConnectionPreconditions { - /** - * Ensures the truth of an expression involving the state of the calling instance, but not - * involving any parameters to the calling method. - * - * @param expression a boolean expression - * @param errorMessage the exception message to use if the check fails; will be converted to a - * string using {@link String#valueOf(Object)}. - * @throws SpannerException with {@link ErrorCode#FAILED_PRECONDITION} if {@code expression} is - * false. - */ - static void checkState(boolean expression, @Nullable Object errorMessage) { - if (!expression) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, String.valueOf(errorMessage)); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutor.java b/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutor.java deleted file mode 100644 index 4de1f3f0..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.TimestampBound; -import com.google.protobuf.Duration; - -/** - * The Cloud Spanner JDBC driver supports a number of client side statements that are interpreted by - * the driver and that can modify the current state of a connection, or report the current state of - * a connection. Each of the methods in this interface correspond with one such client side - * statement. - * - *

The methods in this interface are called by the different {@link ClientSideStatement}s. These - * method calls are then forwarded into the appropriate method of a {@link Connection} instance. - * - *

The client side statements are defined in the ClientSideStatements.json file. - */ -interface ConnectionStatementExecutor { - - StatementResult statementSetAutocommit(Boolean autocommit); - - StatementResult statementShowAutocommit(); - - StatementResult statementSetReadOnly(Boolean readOnly); - - StatementResult statementShowReadOnly(); - - StatementResult statementSetRetryAbortsInternally(Boolean retryAbortsInternally); - - StatementResult statementShowRetryAbortsInternally(); - - StatementResult statementSetAutocommitDmlMode(AutocommitDmlMode mode); - - StatementResult statementShowAutocommitDmlMode(); - - StatementResult statementSetStatementTimeout(Duration duration); - - StatementResult statementShowStatementTimeout(); - - StatementResult statementShowReadTimestamp(); - - StatementResult statementShowCommitTimestamp(); - - StatementResult statementSetReadOnlyStaleness(TimestampBound staleness); - - StatementResult statementShowReadOnlyStaleness(); - - StatementResult statementSetOptimizerVersion(String optimizerVersion); - - StatementResult statementShowOptimizerVersion(); - - StatementResult statementBeginTransaction(); - - StatementResult statementCommit(); - - StatementResult statementRollback(); - - StatementResult statementSetTransactionMode(TransactionMode mode); - - StatementResult statementStartBatchDdl(); - - StatementResult statementStartBatchDml(); - - StatementResult statementRunBatch(); - - StatementResult statementAbortBatch(); -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorImpl.java b/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorImpl.java deleted file mode 100644 index c8aa40d7..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorImpl.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.ABORT_BATCH; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.BEGIN; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.COMMIT; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.ROLLBACK; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.RUN_BATCH; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_OPTIMIZER_VERSION; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_READONLY; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_READ_ONLY_STALENESS; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_RETRY_ABORTS_INTERNALLY; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_STATEMENT_TIMEOUT; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_TRANSACTION_MODE; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_VERSION; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READONLY; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READ_TIMESTAMP; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_STATEMENT_TIMEOUT; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.START_BATCH_DDL; -import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.START_BATCH_DML; -import static com.google.cloud.spanner.jdbc.StatementResultImpl.noResult; -import static com.google.cloud.spanner.jdbc.StatementResultImpl.resultSet; - -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.DurationValueGetter; -import com.google.common.base.Preconditions; -import com.google.protobuf.Duration; -import java.util.concurrent.TimeUnit; - -/** - * The methods in this class are called by the different {@link ClientSideStatement}s. These method - * calls are then forwarded into a {@link Connection}. - */ -class ConnectionStatementExecutorImpl implements ConnectionStatementExecutor { - static final class StatementTimeoutGetter implements DurationValueGetter { - private final Connection connection; - - public StatementTimeoutGetter(Connection connection) { - this.connection = connection; - } - - @Override - public long getDuration(TimeUnit unit) { - return connection.getStatementTimeout(unit); - } - - @Override - public boolean hasDuration() { - return connection.hasStatementTimeout(); - } - } - - /** The connection to execute the statements on. */ - private final ConnectionImpl connection; - - ConnectionStatementExecutorImpl(ConnectionImpl connection) { - this.connection = connection; - } - - ConnectionImpl getConnection() { - return connection; - } - - @Override - public StatementResult statementSetAutocommit(Boolean autocommit) { - Preconditions.checkNotNull(autocommit); - getConnection().setAutocommit(autocommit); - return noResult(SET_AUTOCOMMIT); - } - - @Override - public StatementResult statementShowAutocommit() { - return resultSet("AUTOCOMMIT", getConnection().isAutocommit(), SHOW_AUTOCOMMIT); - } - - @Override - public StatementResult statementSetReadOnly(Boolean readOnly) { - Preconditions.checkNotNull(readOnly); - getConnection().setReadOnly(readOnly); - return noResult(SET_READONLY); - } - - @Override - public StatementResult statementShowReadOnly() { - return StatementResultImpl.resultSet("READONLY", getConnection().isReadOnly(), SHOW_READONLY); - } - - @Override - public StatementResult statementSetRetryAbortsInternally(Boolean retryAbortsInternally) { - Preconditions.checkNotNull(retryAbortsInternally); - getConnection().setRetryAbortsInternally(retryAbortsInternally); - return noResult(SET_RETRY_ABORTS_INTERNALLY); - } - - @Override - public StatementResult statementShowRetryAbortsInternally() { - return StatementResultImpl.resultSet( - "RETRY_ABORTS_INTERNALLY", - getConnection().isRetryAbortsInternally(), - SHOW_RETRY_ABORTS_INTERNALLY); - } - - @Override - public StatementResult statementSetAutocommitDmlMode(AutocommitDmlMode mode) { - getConnection().setAutocommitDmlMode(mode); - return noResult(SET_AUTOCOMMIT_DML_MODE); - } - - @Override - public StatementResult statementShowAutocommitDmlMode() { - return resultSet( - "AUTOCOMMIT_DML_MODE", getConnection().getAutocommitDmlMode(), SHOW_AUTOCOMMIT_DML_MODE); - } - - @Override - public StatementResult statementSetStatementTimeout(Duration duration) { - if (duration.getSeconds() == 0L && duration.getNanos() == 0) { - getConnection().clearStatementTimeout(); - } else { - TimeUnit unit = - ReadOnlyStalenessUtil.getAppropriateTimeUnit( - new ReadOnlyStalenessUtil.DurationGetter(duration)); - getConnection() - .setStatementTimeout(ReadOnlyStalenessUtil.durationToUnits(duration, unit), unit); - } - return noResult(SET_STATEMENT_TIMEOUT); - } - - @Override - public StatementResult statementShowStatementTimeout() { - return resultSet( - "STATEMENT_TIMEOUT", - getConnection().hasStatementTimeout() - ? ReadOnlyStalenessUtil.durationToString(new StatementTimeoutGetter(getConnection())) - : null, - SHOW_STATEMENT_TIMEOUT); - } - - @Override - public StatementResult statementShowReadTimestamp() { - return resultSet( - "READ_TIMESTAMP", getConnection().getReadTimestampOrNull(), SHOW_READ_TIMESTAMP); - } - - @Override - public StatementResult statementShowCommitTimestamp() { - return resultSet( - "COMMIT_TIMESTAMP", getConnection().getCommitTimestampOrNull(), SHOW_COMMIT_TIMESTAMP); - } - - @Override - public StatementResult statementSetReadOnlyStaleness(TimestampBound staleness) { - getConnection().setReadOnlyStaleness(staleness); - return noResult(SET_READ_ONLY_STALENESS); - } - - @Override - public StatementResult statementShowReadOnlyStaleness() { - TimestampBound staleness = getConnection().getReadOnlyStaleness(); - return resultSet( - "READ_ONLY_STALENESS", - ReadOnlyStalenessUtil.timestampBoundToString(staleness), - SHOW_READ_ONLY_STALENESS); - } - - @Override - public StatementResult statementSetOptimizerVersion(String optimizerVersion) { - getConnection().setOptimizerVersion(optimizerVersion); - return noResult(SET_OPTIMIZER_VERSION); - } - - @Override - public StatementResult statementShowOptimizerVersion() { - return resultSet( - "OPTIMIZER_VERSION", getConnection().getOptimizerVersion(), SHOW_OPTIMIZER_VERSION); - } - - @Override - public StatementResult statementBeginTransaction() { - getConnection().beginTransaction(); - return noResult(BEGIN); - } - - @Override - public StatementResult statementCommit() { - getConnection().commit(); - return noResult(COMMIT); - } - - @Override - public StatementResult statementRollback() { - getConnection().rollback(); - return noResult(ROLLBACK); - } - - @Override - public StatementResult statementSetTransactionMode(TransactionMode mode) { - getConnection().setTransactionMode(mode); - return noResult(SET_TRANSACTION_MODE); - } - - @Override - public StatementResult statementStartBatchDdl() { - getConnection().startBatchDdl(); - return noResult(START_BATCH_DDL); - } - - @Override - public StatementResult statementStartBatchDml() { - getConnection().startBatchDml(); - return noResult(START_BATCH_DML); - } - - @Override - public StatementResult statementRunBatch() { - long[] updateCounts = getConnection().runBatch(); - return resultSet("UPDATE_COUNTS", updateCounts, RUN_BATCH); - } - - @Override - public StatementResult statementAbortBatch() { - getConnection().abortBatch(); - return noResult(ABORT_BATCH); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/CredentialsService.java b/src/main/java/com/google/cloud/spanner/jdbc/CredentialsService.java deleted file mode 100644 index 1bb314e7..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/CredentialsService.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** Service class for getting credentials from key files. */ -class CredentialsService { - static final String GCS_NOT_SUPPORTED_MSG = - "Credentials that is stored on Google Cloud Storage is no longer supported. Download the credentials to a local file and reference the local file in the connection URL."; - static final CredentialsService INSTANCE = new CredentialsService(); - - CredentialsService() {} - - /** - * Create credentials from the given URL pointing to a credentials json file. This may be a local - * file or a file on Google Cloud Storage. Credentials on Google Cloud Storage can only be used if - * the application is running in an environment where application default credentials have been - * set. - * - * @param credentialsUrl The URL of the credentials file to read. If null, then this - * method will return the application default credentials of the environment. - * @return the {@link GoogleCredentials} object pointed to by the URL. - * @throws SpannerException If the URL does not point to a valid credentials file, or if the file - * cannot be accessed. - */ - GoogleCredentials createCredentials(String credentialsUrl) { - try { - if (credentialsUrl == null) { - return internalGetApplicationDefault(); - } else { - return getCredentialsFromUrl(credentialsUrl); - } - } catch (IOException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Invalid credentials path specified", e); - } - } - - @VisibleForTesting - GoogleCredentials internalGetApplicationDefault() throws IOException { - return GoogleCredentials.getApplicationDefault(); - } - - private GoogleCredentials getCredentialsFromUrl(String credentialsUrl) throws IOException { - Preconditions.checkNotNull(credentialsUrl); - Preconditions.checkArgument( - credentialsUrl.length() > 0, "credentialsUrl may not be an empty string"); - if (credentialsUrl.startsWith("gs://")) { - throw new IOException(GCS_NOT_SUPPORTED_MSG); - } else { - return getCredentialsFromLocalFile(credentialsUrl); - } - } - - private GoogleCredentials getCredentialsFromLocalFile(String filePath) throws IOException { - File credentialsFile = new File(filePath); - if (!credentialsFile.isFile()) { - throw new IOException( - String.format("Error reading credential file %s: File does not exist", filePath)); - } - try (InputStream credentialsStream = new FileInputStream(credentialsFile)) { - return GoogleCredentials.fromStream(credentialsStream); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/DdlBatch.java b/src/main/java/com/google/cloud/spanner/jdbc/DdlBatch.java deleted file mode 100644 index 3431b296..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/DdlBatch.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ConnectionImpl.InternalMetadataQuery; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import org.apache.commons.lang3.ArrayUtils; - -/** - * {@link UnitOfWork} that is used when a DDL batch is started. These batches only accept DDL - * statements. All DDL statements are buffered locally and sent to Spanner when runBatch() is - * called. Running a {@link DdlBatch} is not an atomic operation. If the execution fails, then some - * (possibly empty) prefix of the statements in the batch have been successfully applied to the - * database, and the others have not. Note that the statements that succeed may not all happen at - * the same time, but they will always happen in order. - */ -class DdlBatch extends AbstractBaseUnitOfWork { - private final DdlClient ddlClient; - private final DatabaseClient dbClient; - private final List statements = new ArrayList<>(); - private UnitOfWorkState state = UnitOfWorkState.STARTED; - - static class Builder extends AbstractBaseUnitOfWork.Builder { - private DdlClient ddlClient; - private DatabaseClient dbClient; - - private Builder() {} - - Builder setDdlClient(DdlClient client) { - Preconditions.checkNotNull(client); - this.ddlClient = client; - return this; - } - - Builder setDatabaseClient(DatabaseClient client) { - Preconditions.checkNotNull(client); - this.dbClient = client; - return this; - } - - @Override - DdlBatch build() { - Preconditions.checkState(ddlClient != null, "No DdlClient specified"); - Preconditions.checkState(dbClient != null, "No DbClient specified"); - return new DdlBatch(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private DdlBatch(Builder builder) { - super(builder); - this.ddlClient = builder.ddlClient; - this.dbClient = builder.dbClient; - } - - @Override - public Type getType() { - return Type.BATCH; - } - - @Override - public UnitOfWorkState getState() { - return this.state; - } - - @Override - public boolean isActive() { - return getState().isActive(); - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - public ResultSet executeQuery( - final ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { - if (options != null) { - for (int i = 0; i < options.length; i++) { - if (options[i] instanceof InternalMetadataQuery) { - Preconditions.checkNotNull(statement); - Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); - Preconditions.checkArgument( - analyzeMode == AnalyzeMode.NONE, "Analyze is not allowed for DDL batch"); - // Queries marked with internal metadata queries are allowed during a DDL batch. - // These can only be generated by library internal methods and may be used to check - // whether a database object such as table or an index exists. - final QueryOption[] internalOptions = ArrayUtils.remove(options, i); - Callable callable = - new Callable() { - @Override - public ResultSet call() throws Exception { - return DirectExecuteResultSet.ofResultSet( - dbClient.singleUse().executeQuery(statement.getStatement(), internalOptions)); - } - }; - return asyncExecuteStatement(statement, callable); - } - } - } - // Queries are by default not allowed on DDL batches. - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing queries is not allowed for DDL batches."); - } - - @Override - public Timestamp getReadTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "There is no read timestamp available for DDL batches."); - } - - @Override - public Timestamp getReadTimestampOrNull() { - return null; - } - - @Override - public Timestamp getCommitTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "There is no commit timestamp available for DDL batches."); - } - - @Override - public Timestamp getCommitTimestampOrNull() { - return null; - } - - @Override - public void executeDdl(ParsedStatement ddl) { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, - "The batch is no longer active and cannot be used for further statements"); - Preconditions.checkArgument( - ddl.getType() == StatementType.DDL, - "Only DDL statements are allowed. \"" - + ddl.getSqlWithoutComments() - + "\" is not a DDL-statement."); - statements.add(ddl.getSqlWithoutComments()); - } - - @Override - public long executeUpdate(ParsedStatement update) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing updates is not allowed for DDL batches."); - } - - @Override - public long[] executeBatchUpdate(Iterable updates) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing batch updates is not allowed for DDL batches."); - } - - @Override - public void write(Mutation mutation) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DDL batches."); - } - - @Override - public void write(Iterable mutations) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DDL batches."); - } - - /** - * Create a {@link ParsedStatement} that we can use as input for the generic execute method when - * the {@link #runBatch()} method is executed. This method uses the generic execute method that - * allows statements to be cancelled and to timeout, which requires the input to be a {@link - * ParsedStatement}. - */ - private static final ParsedStatement RUN_BATCH = - StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); - - @Override - public long[] runBatch() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran"); - try { - if (!statements.isEmpty()) { - // create a statement that can be passed in to the execute method - Callable callable = - new Callable() { - @Override - public UpdateDatabaseDdlMetadata call() throws Exception { - OperationFuture operation = - ddlClient.executeDdl(statements); - try { - // Wait until the operation has finished. - operation.get(); - // Return metadata. - return operation.getMetadata().get(); - } catch (ExecutionException e) { - SpannerException spannerException = extractSpannerCause(e); - long[] updateCounts = extractUpdateCounts(operation.getMetadata().get()); - throw SpannerExceptionFactory.newSpannerBatchUpdateException( - spannerException == null - ? ErrorCode.UNKNOWN - : spannerException.getErrorCode(), - e.getMessage(), - updateCounts); - } catch (InterruptedException e) { - long[] updateCounts = extractUpdateCounts(operation.getMetadata().get()); - throw SpannerExceptionFactory.newSpannerBatchUpdateException( - ErrorCode.CANCELLED, e.getMessage(), updateCounts); - } - } - }; - asyncExecuteStatement(RUN_BATCH, callable); - } - this.state = UnitOfWorkState.RAN; - long[] updateCounts = new long[statements.size()]; - Arrays.fill(updateCounts, 1L); - return updateCounts; - } catch (SpannerException e) { - this.state = UnitOfWorkState.RUN_FAILED; - throw e; - } - } - - private SpannerException extractSpannerCause(ExecutionException e) { - Throwable cause = e.getCause(); - Set causes = new HashSet<>(); - while (cause != null && !causes.contains(cause)) { - if (cause instanceof SpannerException) { - return (SpannerException) cause; - } - causes.add(cause); - cause = cause.getCause(); - } - return null; - } - - @VisibleForTesting - long[] extractUpdateCounts(UpdateDatabaseDdlMetadata metadata) { - long[] updateCounts = new long[metadata.getStatementsCount()]; - for (int i = 0; i < updateCounts.length; i++) { - if (metadata.getCommitTimestampsCount() > i && metadata.getCommitTimestamps(i) != null) { - updateCounts[i] = 1L; - } else { - updateCounts[i] = 0L; - } - } - return updateCounts; - } - - @Override - public void abortBatch() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be aborted."); - this.state = UnitOfWorkState.ABORTED; - } - - @Override - public void commit() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DDL batches."); - } - - @Override - public void rollback() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DDL batches."); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/DdlClient.java b/src/main/java/com/google/cloud/spanner/jdbc/DdlClient.java deleted file mode 100644 index 5f921d7b..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/DdlClient.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.spanner.DatabaseAdminClient; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; -import java.util.List; - -/** - * Convenience class for executing Data Definition Language statements on transactions that support - * DDL statements, i.e. DdlBatchTransaction and SingleUseTransaction. - */ -class DdlClient { - private final DatabaseAdminClient dbAdminClient; - private final String instanceId; - private final String databaseName; - - static class Builder { - private DatabaseAdminClient dbAdminClient; - private String instanceId; - private String databaseName; - - private Builder() {} - - Builder setDatabaseAdminClient(DatabaseAdminClient client) { - Preconditions.checkNotNull(client); - this.dbAdminClient = client; - return this; - } - - Builder setInstanceId(String instanceId) { - Preconditions.checkArgument( - !Strings.isNullOrEmpty(instanceId), "Empty instanceId is not allowed"); - this.instanceId = instanceId; - return this; - } - - Builder setDatabaseName(String name) { - Preconditions.checkArgument( - !Strings.isNullOrEmpty(name), "Empty database name is not allowed"); - this.databaseName = name; - return this; - } - - DdlClient build() { - Preconditions.checkState(dbAdminClient != null, "No DatabaseAdminClient specified"); - Preconditions.checkState(!Strings.isNullOrEmpty(instanceId), "No InstanceId specified"); - Preconditions.checkArgument( - !Strings.isNullOrEmpty(databaseName), "No database name specified"); - return new DdlClient(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private DdlClient(Builder builder) { - this.dbAdminClient = builder.dbAdminClient; - this.instanceId = builder.instanceId; - this.databaseName = builder.databaseName; - } - - /** Execute a single DDL statement. */ - OperationFuture executeDdl(String ddl) { - return executeDdl(Arrays.asList(ddl)); - } - - /** Execute a list of DDL statements as one operation. */ - OperationFuture executeDdl(List statements) { - return dbAdminClient.updateDatabaseDdl(instanceId, databaseName, statements, null); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSet.java deleted file mode 100644 index 90ca0441..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSet.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.ByteArray; -import com.google.cloud.Date; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.common.base.Preconditions; -import com.google.spanner.v1.ResultSetStats; -import java.util.List; - -/** - * {@link ResultSet} implementation used by the Spanner connection API to ensure that the query for - * a {@link ResultSet} is executed directly when it is created. This is done by calling {@link - * ResultSet#next()} directly after creation. This ensures that a statement timeout can be applied - * to the actual query execution. It also ensures that any invalid query will throw an exception at - * execution instead of the first next() call by a client. - */ -class DirectExecuteResultSet implements ResultSet { - private static final String MISSING_NEXT_CALL = "Must be preceded by a next() call"; - private final ResultSet delegate; - private boolean nextCalledByClient = false; - private final boolean initialNextResult; - private boolean nextHasReturnedFalse = false; - - /** - * Creates a new {@link DirectExecuteResultSet} from the given delegate {@link ResultSet}. This - * automatically executes the query of the given delegate {@link ResultSet} by calling next() on - * the delegate. The delegate must not have been used (i.e. next() must not have been called on - * it). - * - * @param delegate The underlying {@link ResultSet} for this {@link DirectExecuteResultSet}. - * @return a {@link DirectExecuteResultSet} that has already executed the query associated with - * the delegate {@link ResultSet}. - */ - static DirectExecuteResultSet ofResultSet(ResultSet delegate) { - return new DirectExecuteResultSet(delegate); - } - - DirectExecuteResultSet(ResultSet delegate) { - Preconditions.checkNotNull(delegate); - this.delegate = delegate; - initialNextResult = delegate.next(); - } - - @Override - public boolean next() throws SpannerException { - if (nextCalledByClient) { - boolean res = delegate.next(); - nextHasReturnedFalse = !res; - return res; - } - nextCalledByClient = true; - nextHasReturnedFalse = !initialNextResult; - return initialNextResult; - } - - @Override - public Struct getCurrentRowAsStruct() { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getCurrentRowAsStruct(); - } - - @Override - public void close() { - delegate.close(); - } - - @Override - public ResultSetStats getStats() { - if (nextHasReturnedFalse) { - return delegate.getStats(); - } - return null; - } - - @Override - public Type getType() { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getType(); - } - - @Override - public int getColumnCount() { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getColumnCount(); - } - - @Override - public int getColumnIndex(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getColumnIndex(columnName); - } - - @Override - public Type getColumnType(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getColumnType(columnIndex); - } - - @Override - public Type getColumnType(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getColumnType(columnName); - } - - @Override - public boolean isNull(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.isNull(columnIndex); - } - - @Override - public boolean isNull(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.isNull(columnName); - } - - @Override - public boolean getBoolean(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBoolean(columnIndex); - } - - @Override - public boolean getBoolean(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBoolean(columnName); - } - - @Override - public long getLong(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLong(columnIndex); - } - - @Override - public long getLong(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLong(columnName); - } - - @Override - public double getDouble(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDouble(columnIndex); - } - - @Override - public double getDouble(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDouble(columnName); - } - - @Override - public String getString(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getString(columnIndex); - } - - @Override - public String getString(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getString(columnName); - } - - @Override - public ByteArray getBytes(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBytes(columnIndex); - } - - @Override - public ByteArray getBytes(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBytes(columnName); - } - - @Override - public Timestamp getTimestamp(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getTimestamp(columnIndex); - } - - @Override - public Timestamp getTimestamp(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getTimestamp(columnName); - } - - @Override - public Date getDate(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDate(columnIndex); - } - - @Override - public Date getDate(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDate(columnName); - } - - @Override - public boolean[] getBooleanArray(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBooleanArray(columnIndex); - } - - @Override - public boolean[] getBooleanArray(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBooleanArray(columnName); - } - - @Override - public List getBooleanList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBooleanList(columnIndex); - } - - @Override - public List getBooleanList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBooleanList(columnName); - } - - @Override - public long[] getLongArray(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLongArray(columnIndex); - } - - @Override - public long[] getLongArray(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLongArray(columnName); - } - - @Override - public List getLongList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLongList(columnIndex); - } - - @Override - public List getLongList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getLongList(columnName); - } - - @Override - public double[] getDoubleArray(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDoubleArray(columnIndex); - } - - @Override - public double[] getDoubleArray(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDoubleArray(columnName); - } - - @Override - public List getDoubleList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDoubleList(columnIndex); - } - - @Override - public List getDoubleList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDoubleList(columnName); - } - - @Override - public List getStringList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getStringList(columnIndex); - } - - @Override - public List getStringList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getStringList(columnName); - } - - @Override - public List getBytesList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBytesList(columnIndex); - } - - @Override - public List getBytesList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getBytesList(columnName); - } - - @Override - public List getTimestampList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getTimestampList(columnIndex); - } - - @Override - public List getTimestampList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getTimestampList(columnName); - } - - @Override - public List getDateList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDateList(columnIndex); - } - - @Override - public List getDateList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getDateList(columnName); - } - - @Override - public List getStructList(int columnIndex) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getStructList(columnIndex); - } - - @Override - public List getStructList(String columnName) { - Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); - return delegate.getStructList(columnName); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof DirectExecuteResultSet)) { - return false; - } - return ((DirectExecuteResultSet) o).delegate.equals(delegate); - } - - @Override - public int hashCode() { - return delegate.hashCode(); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/DmlBatch.java b/src/main/java/com/google/cloud/spanner/jdbc/DmlBatch.java deleted file mode 100644 index 55da9fb1..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/DmlBatch.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.common.base.Preconditions; -import java.util.ArrayList; -import java.util.List; - -/** - * {@link UnitOfWork} that is used when a DML batch is started. These batches only accept DML - * statements. All DML statements are buffered locally and sent to Spanner when runBatch() is - * called. - */ -class DmlBatch extends AbstractBaseUnitOfWork { - private final UnitOfWork transaction; - private final List statements = new ArrayList<>(); - private UnitOfWorkState state = UnitOfWorkState.STARTED; - - static class Builder extends AbstractBaseUnitOfWork.Builder { - private UnitOfWork transaction; - - private Builder() {} - - Builder setTransaction(UnitOfWork transaction) { - Preconditions.checkNotNull(transaction); - this.transaction = transaction; - return this; - } - - @Override - DmlBatch build() { - Preconditions.checkState(transaction != null, "No transaction specified"); - return new DmlBatch(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private DmlBatch(Builder builder) { - super(builder); - this.transaction = builder.transaction; - } - - @Override - public Type getType() { - return Type.BATCH; - } - - @Override - public UnitOfWorkState getState() { - return this.state; - } - - @Override - public boolean isActive() { - return getState().isActive(); - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - public ResultSet executeQuery( - ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing queries is not allowed for DML batches."); - } - - @Override - public Timestamp getReadTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "There is no read timestamp available for DML batches."); - } - - @Override - public Timestamp getReadTimestampOrNull() { - return null; - } - - @Override - public Timestamp getCommitTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "There is no commit timestamp available for DML batches."); - } - - @Override - public Timestamp getCommitTimestampOrNull() { - return null; - } - - @Override - public void executeDdl(ParsedStatement ddl) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing DDL statements is not allowed for DML batches."); - } - - @Override - public long executeUpdate(ParsedStatement update) { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, - "The batch is no longer active and cannot be used for further statements"); - Preconditions.checkArgument( - update.getType() == StatementType.UPDATE, - "Only DML statements are allowed. \"" - + update.getSqlWithoutComments() - + "\" is not a DML-statement."); - statements.add(update); - return -1L; - } - - @Override - public long[] executeBatchUpdate(Iterable updates) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Executing batch updates is not allowed for DML batches."); - } - - @Override - public void write(Mutation mutation) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DML batches."); - } - - @Override - public void write(Iterable mutations) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Writing mutations is not allowed for DML batches."); - } - - @Override - public long[] runBatch() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran"); - try { - long[] res; - if (statements.isEmpty()) { - res = new long[0]; - } else { - res = transaction.executeBatchUpdate(statements); - } - this.state = UnitOfWorkState.RAN; - return res; - } catch (SpannerException e) { - this.state = UnitOfWorkState.RUN_FAILED; - throw e; - } - } - - @Override - public void abortBatch() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be aborted."); - this.state = UnitOfWorkState.ABORTED; - } - - @Override - public void commit() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Commit is not allowed for DML batches."); - } - - @Override - public void rollback() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Rollback is not allowed for DML batches."); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/FailedBatchUpdate.java b/src/main/java/com/google/cloud/spanner/jdbc/FailedBatchUpdate.java deleted file mode 100644 index d96d98a8..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/FailedBatchUpdate.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.SpannerBatchUpdateException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.common.base.Preconditions; -import java.util.Arrays; -import java.util.Objects; - -/** - * A batch update that failed with a {@link SpannerException} on a {@link ReadWriteTransaction}. The - * batch update can be retried if the transaction is aborted, and should throw the same exception - * during retry as during the original transaction. - */ -final class FailedBatchUpdate implements RetriableStatement { - private final ReadWriteTransaction transaction; - private final SpannerException exception; - private final Iterable statements; - - FailedBatchUpdate( - ReadWriteTransaction transaction, - SpannerException exception, - Iterable statements) { - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(exception); - Preconditions.checkNotNull(statements); - this.transaction = transaction; - this.exception = exception; - this.statements = statements; - } - - @Override - public void retry(AbortedException aborted) throws AbortedException { - transaction - .getStatementExecutor() - .invokeInterceptors( - ReadWriteTransaction.EXECUTE_BATCH_UPDATE_STATEMENT, - StatementExecutionStep.RETRY_STATEMENT, - transaction); - try { - transaction.getReadContext().batchUpdate(statements); - } catch (SpannerBatchUpdateException e) { - // Check that we got the same exception as in the original transaction. - if (exception instanceof SpannerBatchUpdateException - && e.getErrorCode() == exception.getErrorCode() - && Objects.equals(e.getMessage(), exception.getMessage())) { - // Check that the returned update counts are equal. - if (Arrays.equals( - e.getUpdateCounts(), ((SpannerBatchUpdateException) exception).getUpdateCounts())) { - return; - } - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } catch (SpannerException e) { - // Check that we got the same exception as in the original transaction. - if (e.getErrorCode() == exception.getErrorCode() - && Objects.equals(e.getMessage(), exception.getMessage())) { - return; - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/FailedQuery.java b/src/main/java/com/google/cloud/spanner/jdbc/FailedQuery.java deleted file mode 100644 index 4e2a3ecd..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/FailedQuery.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; -import java.util.Objects; - -/** - * A query that failed with a {@link SpannerException} on a {@link ReadWriteTransaction}. The query - * can be retried if the transaction is aborted, and should throw the same exception during retry as - * during the original transaction. - */ -final class FailedQuery implements RetriableStatement { - private final ReadWriteTransaction transaction; - private final SpannerException exception; - private final ParsedStatement statement; - private final AnalyzeMode analyzeMode; - private final QueryOption[] options; - - FailedQuery( - ReadWriteTransaction transaction, - SpannerException exception, - ParsedStatement statement, - AnalyzeMode analyzeMode, - QueryOption... options) { - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(exception); - Preconditions.checkNotNull(statement); - this.transaction = transaction; - this.exception = exception; - this.statement = statement; - this.analyzeMode = analyzeMode; - this.options = options; - } - - @Override - public void retry(AbortedException aborted) throws AbortedException { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - try { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - try (ResultSet rs = - DirectExecuteResultSet.ofResultSet( - transaction.internalExecuteQuery(statement, analyzeMode, options))) { - // Do nothing with the results, we are only interested in whether the statement throws the - // same exception as in the original transaction. - } - } catch (SpannerException e) { - // Check that we got the same exception as in the original transaction - if (e.getErrorCode() == exception.getErrorCode() - && Objects.equals(e.getMessage(), exception.getMessage())) { - return; - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/FailedUpdate.java b/src/main/java/com/google/cloud/spanner/jdbc/FailedUpdate.java deleted file mode 100644 index 1974f0cc..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/FailedUpdate.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; -import java.util.Objects; - -/** - * An update that failed with a {@link SpannerException} on a {@link ReadWriteTransaction}. The - * update can be retried if the transaction is aborted, and should throw the same exception during - * retry as during the original transaction. - */ -final class FailedUpdate implements RetriableStatement { - private final ReadWriteTransaction transaction; - private final SpannerException exception; - private final ParsedStatement statement; - - FailedUpdate( - ReadWriteTransaction transaction, SpannerException exception, ParsedStatement statement) { - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(exception); - Preconditions.checkNotNull(statement); - this.transaction = transaction; - this.exception = exception; - this.statement = statement; - } - - @Override - public void retry(AbortedException aborted) throws AbortedException { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - try { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - transaction.getReadContext().executeUpdate(statement.getStatement()); - } catch (SpannerException e) { - // Check that we got the same exception as in the original transaction. - if (e.getErrorCode() == exception.getErrorCode() - && Objects.equals(e.getMessage(), exception.getMessage())) { - return; - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java index 08b2a823..5aefd35b 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java @@ -16,8 +16,13 @@ package com.google.cloud.spanner.jdbc; +import com.google.api.client.util.Preconditions; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.connection.StatementParser; +import com.google.common.base.Function; +import com.google.common.collect.Iterators; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; @@ -383,22 +388,104 @@ public void bufferedWrite(Iterable mutations) throws SQLException { } } + @SuppressWarnings("deprecation") + private static final class JdbcToSpannerTransactionRetryListener + implements com.google.cloud.spanner.connection.TransactionRetryListener { + private final TransactionRetryListener delegate; + + JdbcToSpannerTransactionRetryListener(TransactionRetryListener delegate) { + this.delegate = Preconditions.checkNotNull(delegate); + } + + @Override + public void retryStarting( + com.google.cloud.Timestamp transactionStarted, long transactionId, int retryAttempt) { + delegate.retryStarting(transactionStarted, transactionId, retryAttempt); + } + + @Override + public void retryFinished( + com.google.cloud.Timestamp transactionStarted, + long transactionId, + int retryAttempt, + RetryResult result) { + delegate.retryFinished( + transactionStarted, + transactionId, + retryAttempt, + TransactionRetryListener.RetryResult.valueOf(result.name())); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof JdbcToSpannerTransactionRetryListener)) { + return false; + } + JdbcToSpannerTransactionRetryListener other = (JdbcToSpannerTransactionRetryListener) o; + return this.delegate.equals(other.delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } + + @SuppressWarnings("deprecation") @Override public void addTransactionRetryListener(TransactionRetryListener listener) throws SQLException { checkClosed(); + getSpannerConnection() + .addTransactionRetryListener(new JdbcToSpannerTransactionRetryListener(listener)); + } + + @Override + public void addTransactionRetryListener( + com.google.cloud.spanner.connection.TransactionRetryListener listener) throws SQLException { + checkClosed(); getSpannerConnection().addTransactionRetryListener(listener); } + @SuppressWarnings("deprecation") @Override public boolean removeTransactionRetryListener(TransactionRetryListener listener) throws SQLException { checkClosed(); + return getSpannerConnection() + .removeTransactionRetryListener(new JdbcToSpannerTransactionRetryListener(listener)); + } + + @Override + public boolean removeTransactionRetryListener( + com.google.cloud.spanner.connection.TransactionRetryListener listener) throws SQLException { + checkClosed(); return getSpannerConnection().removeTransactionRetryListener(listener); } + @SuppressWarnings("deprecation") @Override public Iterator getTransactionRetryListeners() throws SQLException { checkClosed(); + return Iterators.transform( + getSpannerConnection().getTransactionRetryListeners(), + new Function< + com.google.cloud.spanner.connection.TransactionRetryListener, + TransactionRetryListener>() { + @Override + public TransactionRetryListener apply( + com.google.cloud.spanner.connection.TransactionRetryListener input) { + if (input instanceof JdbcToSpannerTransactionRetryListener) { + return ((JdbcToSpannerTransactionRetryListener) input).delegate; + } + return null; + } + }); + } + + @Override + public Iterator + getTransactionRetryListenersFromConnection() throws SQLException { + checkClosed(); return getSpannerConnection().getTransactionRetryListeners(); } } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConstants.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConstants.java index a8cca8ab..58c9e5bb 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConstants.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConstants.java @@ -16,7 +16,6 @@ package com.google.cloud.spanner.jdbc; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; import java.sql.ResultSet; import java.sql.Statement; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataSource.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataSource.java index 31a4ad44..72a641cc 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataSource.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataSource.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.jdbc; +import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.rpc.Code; import java.io.PrintWriter; import java.sql.Connection; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java index a15ad721..22176595 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java @@ -23,7 +23,7 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; -import com.google.cloud.spanner.jdbc.ConnectionImpl.InternalMetadataQuery; +import com.google.cloud.spanner.connection.Connection.InternalMetadataQuery; import com.google.common.annotations.VisibleForTesting; import java.io.BufferedReader; import java.io.InputStream; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java index 229c0a6f..2d83a34d 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java @@ -19,7 +19,8 @@ import com.google.api.core.InternalApi; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.jdbc.ConnectionOptions.ConnectionProperty; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.connection.ConnectionOptions.ConnectionProperty; import com.google.rpc.Code; import java.sql.Connection; import java.sql.Driver; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java index 4d5cfe88..7a0e8423 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java @@ -18,6 +18,7 @@ import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.StatementParser; import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo; import java.sql.PreparedStatement; import java.sql.ResultSet; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java index e87558e6..b4eaa0d1 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java @@ -24,6 +24,8 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.connection.StatementParser; +import com.google.cloud.spanner.connection.StatementResult; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.rpc.Code; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtil.java b/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtil.java deleted file mode 100644 index 29166586..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtil.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.client.util.DateTime; -import com.google.api.client.util.DateTime.SecondsAndNanos; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.protobuf.Duration; -import com.google.protobuf.util.Durations; -import java.util.concurrent.TimeUnit; - -/** - * Util class for parsing and converting ReadOnlyStaleness values to/from strings. This util is used - * to parse client side statements and values for read only staleness for read-only transactions on - * Cloud Spanner. - */ -class ReadOnlyStalenessUtil { - /** - * Parses an RFC3339 date/time value with nanosecond precision and returns this as a {@link - * Timestamp}. - */ - static Timestamp parseRfc3339(String str) throws SpannerException { - try { - SecondsAndNanos secondsAndNanos = DateTime.parseRfc3339ToSecondsAndNanos(str); - return Timestamp.ofTimeSecondsAndNanos( - secondsAndNanos.getSeconds(), secondsAndNanos.getNanos()); - } catch (NumberFormatException e) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, String.format("Invalid timestamp: %s", str), e); - } - } - - /** The abbreviations for time units that may be used for client side statements. */ - enum TimeUnitAbbreviation { - NANOSECONDS("ns", TimeUnit.NANOSECONDS), - MICROSECONDS("us", TimeUnit.MICROSECONDS), - MILLISECONDS("ms", TimeUnit.MILLISECONDS), - SECONDS("s", TimeUnit.SECONDS); - - private final String abbreviation; - private final TimeUnit unit; - - private TimeUnitAbbreviation(String abbreviation, TimeUnit unit) { - this.abbreviation = abbreviation; - this.unit = unit; - } - - String getAbbreviation() { - return abbreviation; - } - - TimeUnit getUnit() { - return unit; - } - } - - /** Get the abbreviation for the given {@link TimeUnit}. */ - static String getTimeUnitAbbreviation(TimeUnit unit) { - for (TimeUnitAbbreviation abb : TimeUnitAbbreviation.values()) { - if (abb.unit == unit) return abb.abbreviation; - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Invalid option for time unit: " + unit); - } - - /** Get the {@link TimeUnit} corresponding with the given abbreviation. */ - static TimeUnit parseTimeUnit(String unit) { - for (TimeUnitAbbreviation abb : TimeUnitAbbreviation.values()) { - if (abb.abbreviation.equalsIgnoreCase(unit)) return abb.unit; - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "Invalid option for time unit: " + unit); - } - - /** - * Internal interface that is used to generalize getting a time duration from Cloud Spanner - * read-only staleness settings. - */ - static interface DurationValueGetter { - long getDuration(TimeUnit unit); - - boolean hasDuration(); - } - - static final class GetExactStaleness implements DurationValueGetter { - private final TimestampBound staleness; - - public GetExactStaleness(TimestampBound staleness) { - this.staleness = staleness; - } - - @Override - public long getDuration(TimeUnit unit) { - return staleness.getExactStaleness(unit); - } - - @Override - public boolean hasDuration() { - return staleness.getMode() == Mode.EXACT_STALENESS; - } - } - - static final class MaxStalenessGetter implements DurationValueGetter { - private final TimestampBound staleness; - - public MaxStalenessGetter(TimestampBound staleness) { - this.staleness = staleness; - } - - @Override - public long getDuration(TimeUnit unit) { - return staleness.getMaxStaleness(unit); - } - - @Override - public boolean hasDuration() { - return staleness.getMode() == Mode.MAX_STALENESS; - } - } - - static final class DurationGetter implements DurationValueGetter { - private final Duration duration; - - public DurationGetter(Duration duration) { - this.duration = duration; - } - - @Override - public long getDuration(TimeUnit unit) { - return durationToUnits(duration, unit); - } - - @Override - public boolean hasDuration() { - return duration.getNanos() > 0 || duration.getSeconds() > 0L; - } - } - - /** - * Converts a {@link TimestampBound} to a human readable string representation. - * - * @param staleness The staleness to convert - * @return a human readable representation of the staleness. - */ - static String timestampBoundToString(TimestampBound staleness) { - switch (staleness.getMode()) { - case STRONG: - return "STRONG"; - case READ_TIMESTAMP: - return "READ_TIMESTAMP " + staleness.getReadTimestamp().toString(); - case MIN_READ_TIMESTAMP: - return "MIN_READ_TIMESTAMP " + staleness.getMinReadTimestamp().toString(); - case EXACT_STALENESS: - return "EXACT_STALENESS " + durationToString(new GetExactStaleness(staleness)); - case MAX_STALENESS: - return "MAX_STALENESS " + durationToString(new MaxStalenessGetter(staleness)); - default: - throw new IllegalStateException("Unknown mode: " + staleness.getMode()); - } - } - - /** The {@link TimeUnit}s that are supported for timeout and staleness durations. */ - static final TimeUnit[] SUPPORTED_UNITS = - new TimeUnit[] { - TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS - }; - - /** - * Converts a duration value to a human readable string. The method will search for the most - * appropriate {@link TimeUnit} to use to represent the value. - * - * @param function The function that should be called to get the duration in a specific {@link - * TimeUnit}. - * @return a human readable value of the duration. - */ - static String durationToString(DurationValueGetter function) { - TimeUnit unit = getAppropriateTimeUnit(function); - return String.valueOf(function.getDuration(unit)) + getTimeUnitAbbreviation(unit); - } - - /** - * Calculates the most appropriate {@link TimeUnit} to use to represent the duration that is - * returned by the given function. The most appropriate {@link TimeUnit} is the unit with the - * least precision that still retains all information of the given input. - * - * @param durationGetter The function that will return the duration in different {@link - * TimeUnit}s. - * @return the most appropriate {@link TimeUnit} to represent the duration. - */ - static TimeUnit getAppropriateTimeUnit(DurationValueGetter durationGetter) { - int index = 0; - if (durationGetter.hasDuration()) { - for (TimeUnit unit : SUPPORTED_UNITS) { - long duration = durationGetter.getDuration(unit); - if (index + 1 < SUPPORTED_UNITS.length) { - if (duration > 0L - && duration * 1000 == durationGetter.getDuration(SUPPORTED_UNITS[index + 1])) { - return unit; - } - } else { - // last unit, we have to use this one - return unit; - } - index++; - } - throw new IllegalStateException("Unsupported duration"); - } - return TimeUnit.NANOSECONDS; - } - - /** Converts a value into a duration using the specified {@link TimeUnit}. */ - static Duration createDuration(long num, TimeUnit units) { - switch (units) { - case NANOSECONDS: - return Durations.fromNanos(num); - case MICROSECONDS: - return Durations.fromMicros(num); - case MILLISECONDS: - return Durations.fromMillis(num); - case SECONDS: - return Durations.fromSeconds(num); - default: - return Durations.fromMillis(units.toMillis(num)); - } - } - - /** Converts a duration to a number using the specified {@link TimeUnit}. */ - static long durationToUnits(Duration duration, TimeUnit units) { - switch (units) { - case NANOSECONDS: - return Durations.toNanos(duration); - case MICROSECONDS: - return Durations.toMicros(duration); - case MILLISECONDS: - return Durations.toMillis(duration); - case SECONDS: - return Durations.toSeconds(duration); - default: - throw new IllegalArgumentException(); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyTransaction.java b/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyTransaction.java deleted file mode 100644 index e0c40897..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ReadOnlyTransaction.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ReadContext; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; - -/** - * Transaction that is used when a {@link Connection} is in read-only mode or when the transaction - * mode is set to read-only. This transaction can only be used to execute queries. - */ -class ReadOnlyTransaction extends AbstractMultiUseTransaction { - private final DatabaseClient dbClient; - private final TimestampBound readOnlyStaleness; - private com.google.cloud.spanner.ReadOnlyTransaction transaction; - private UnitOfWorkState state = UnitOfWorkState.STARTED; - - static class Builder extends AbstractBaseUnitOfWork.Builder { - private DatabaseClient dbClient; - private TimestampBound readOnlyStaleness; - - private Builder() {} - - Builder setDatabaseClient(DatabaseClient client) { - Preconditions.checkNotNull(client); - this.dbClient = client; - return this; - } - - Builder setReadOnlyStaleness(TimestampBound staleness) { - Preconditions.checkNotNull(staleness); - this.readOnlyStaleness = staleness; - return this; - } - - @Override - ReadOnlyTransaction build() { - Preconditions.checkState(dbClient != null, "No DatabaseClient client specified"); - Preconditions.checkState(readOnlyStaleness != null, "No ReadOnlyStaleness specified"); - return new ReadOnlyTransaction(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private ReadOnlyTransaction(Builder builder) { - super(builder); - this.dbClient = builder.dbClient; - this.readOnlyStaleness = builder.readOnlyStaleness; - } - - @Override - public UnitOfWorkState getState() { - return this.state; - } - - @Override - public boolean isReadOnly() { - return true; - } - - @Override - void checkValidTransaction() { - if (transaction == null) { - transaction = dbClient.readOnlyTransaction(readOnlyStaleness); - } - } - - @Override - ReadContext getReadContext() { - ConnectionPreconditions.checkState(transaction != null, "Missing read-only transaction"); - return transaction; - } - - @Override - public Timestamp getReadTimestamp() { - ConnectionPreconditions.checkState( - transaction != null, "There is no read timestamp available for this transaction."); - ConnectionPreconditions.checkState( - state != UnitOfWorkState.ROLLED_BACK, "This transaction was rolled back"); - return transaction.getReadTimestamp(); - } - - @Override - public Timestamp getReadTimestampOrNull() { - if (transaction != null && state != UnitOfWorkState.ROLLED_BACK) { - try { - return transaction.getReadTimestamp(); - } catch (SpannerException e) { - // ignore - } - } - return null; - } - - @Override - public Timestamp getCommitTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "There is no commit timestamp available for this transaction."); - } - - @Override - public Timestamp getCommitTimestampOrNull() { - return null; - } - - @Override - public void executeDdl(ParsedStatement ddl) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "DDL statements are not allowed for read-only transactions"); - } - - @Override - public long executeUpdate(ParsedStatement update) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "Update statements are not allowed for read-only transactions"); - } - - @Override - public long[] executeBatchUpdate(Iterable updates) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Batch updates are not allowed for read-only transactions."); - } - - @Override - public void write(Mutation mutation) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Mutations are not allowed for read-only transactions"); - } - - @Override - public void write(Iterable mutations) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Mutations are not allowed for read-only transactions"); - } - - @Override - public void commit() { - if (this.transaction != null) { - this.transaction.close(); - } - this.state = UnitOfWorkState.COMMITTED; - } - - @Override - public void rollback() { - if (this.transaction != null) { - this.transaction.close(); - } - this.state = UnitOfWorkState.ROLLED_BACK; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ReadWriteTransaction.java b/src/main/java/com/google/cloud/spanner/jdbc/ReadWriteTransaction.java deleted file mode 100644 index a9fcbc53..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ReadWriteTransaction.java +++ /dev/null @@ -1,762 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.TransactionRetryListener.RetryResult; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Transaction that is used when a {@link Connection} is normal read/write mode (i.e. not autocommit - * and not read-only). These transactions can be automatically retried if an {@link - * AbortedException} is thrown. The transaction will keep track of a running checksum of all {@link - * ResultSet}s that have been returned, and the update counts returned by any DML statement executed - * during the transaction. As long as these checksums and update counts are equal for both the - * original transaction and the retried transaction, the retry can safely be assumed to have the - * exact same results as the original transaction. - */ -class ReadWriteTransaction extends AbstractMultiUseTransaction { - private static final Logger logger = Logger.getLogger(ReadWriteTransaction.class.getName()); - private static final AtomicLong ID_GENERATOR = new AtomicLong(); - private static final String MAX_INTERNAL_RETRIES_EXCEEDED = - "Internal transaction retry maximum exceeded"; - private static final int MAX_INTERNAL_RETRIES = 50; - private final long transactionId; - private final DatabaseClient dbClient; - private TransactionManager txManager; - private final boolean retryAbortsInternally; - private int transactionRetryAttempts; - private int successfulRetries; - private final List transactionRetryListeners; - private volatile TransactionContext txContext; - private volatile UnitOfWorkState state = UnitOfWorkState.STARTED; - private boolean timedOutOrCancelled = false; - private final List statements = new ArrayList<>(); - private final List mutations = new ArrayList<>(); - private Timestamp transactionStarted; - - static class Builder extends AbstractMultiUseTransaction.Builder { - private DatabaseClient dbClient; - private Boolean retryAbortsInternally; - private List transactionRetryListeners; - - private Builder() {} - - Builder setDatabaseClient(DatabaseClient client) { - Preconditions.checkNotNull(client); - this.dbClient = client; - return this; - } - - Builder setRetryAbortsInternally(boolean retryAbortsInternally) { - this.retryAbortsInternally = retryAbortsInternally; - return this; - } - - Builder setTransactionRetryListeners(List listeners) { - Preconditions.checkNotNull(listeners); - this.transactionRetryListeners = listeners; - return this; - } - - @Override - ReadWriteTransaction build() { - Preconditions.checkState(dbClient != null, "No DatabaseClient client specified"); - Preconditions.checkState( - retryAbortsInternally != null, "RetryAbortsInternally is not specified"); - Preconditions.checkState( - transactionRetryListeners != null, "TransactionRetryListeners are not specified"); - return new ReadWriteTransaction(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private ReadWriteTransaction(Builder builder) { - super(builder); - this.transactionId = ID_GENERATOR.incrementAndGet(); - this.dbClient = builder.dbClient; - this.retryAbortsInternally = builder.retryAbortsInternally; - this.transactionRetryListeners = builder.transactionRetryListeners; - this.txManager = dbClient.transactionManager(); - } - - @Override - public String toString() { - return new StringBuilder() - .append("ReadWriteTransaction - ID: ") - .append(transactionId) - .append("; Status: ") - .append(internalGetStateName()) - .append("; Started: ") - .append(internalGetTimeStarted()) - .append("; Retry attempts: ") - .append(transactionRetryAttempts) - .append("; Successful retries: ") - .append(successfulRetries) - .toString(); - } - - private String internalGetStateName() { - return transactionStarted == null ? "Not yet started" : getState().toString(); - } - - private String internalGetTimeStarted() { - return transactionStarted == null ? "Not yet started" : transactionStarted.toString(); - } - - @Override - public UnitOfWorkState getState() { - return this.state; - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - void checkValidTransaction() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, - "This transaction has status " - + state.name() - + ", only " - + UnitOfWorkState.STARTED - + " is allowed."); - ConnectionPreconditions.checkState( - !timedOutOrCancelled, - "The last statement of this transaction timed out or was cancelled. " - + "The transaction is no longer usable. " - + "Rollback the transaction and start a new one."); - if (txManager.getState() == null) { - transactionStarted = Timestamp.now(); - txContext = txManager.begin(); - } - if (txManager.getState() - != com.google.cloud.spanner.TransactionManager.TransactionState.STARTED) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - String.format("Invalid transaction state: %s", txManager.getState())); - } - } - - @Override - TransactionContext getReadContext() { - ConnectionPreconditions.checkState(txContext != null, "Missing transaction context"); - return txContext; - } - - @Override - public Timestamp getReadTimestamp() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "There is no read timestamp available for read/write transactions."); - } - - @Override - public Timestamp getReadTimestampOrNull() { - return null; - } - - private boolean hasCommitTimestamp() { - return txManager.getState() - == com.google.cloud.spanner.TransactionManager.TransactionState.COMMITTED; - } - - @Override - public Timestamp getCommitTimestamp() { - ConnectionPreconditions.checkState(hasCommitTimestamp(), "This transaction has not committed."); - return txManager.getCommitTimestamp(); - } - - @Override - public Timestamp getCommitTimestampOrNull() { - return hasCommitTimestamp() ? txManager.getCommitTimestamp() : null; - } - - @Override - public void executeDdl(ParsedStatement ddl) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "DDL-statements are not allowed inside a read/write transaction."); - } - - private void handlePossibleInvalidatingException(SpannerException e) { - if (e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED - || e.getErrorCode() == ErrorCode.CANCELLED) { - this.timedOutOrCancelled = true; - } - } - - @Override - public ResultSet executeQuery( - final ParsedStatement statement, - final AnalyzeMode analyzeMode, - final QueryOption... options) { - Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); - checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - statement, - new Callable() { - @Override - public ResultSet call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public ResultSet call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - statement, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - ResultSet delegate = - DirectExecuteResultSet.ofResultSet( - internalExecuteQuery(statement, analyzeMode, options)); - return createAndAddRetryResultSet( - delegate, statement, analyzeMode, options); - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedQuery(e, statement, analyzeMode, options); - throw e; - } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return super.executeQuery(statement, analyzeMode, options); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; - } - } - - @Override - public long executeUpdate(final ParsedStatement update) { - Preconditions.checkNotNull(update); - Preconditions.checkArgument(update.isUpdate(), "The statement is not an update statement"); - checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - update, - new Callable() { - @Override - public Long call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public Long call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - update, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - long updateCount = txContext.executeUpdate(update.getStatement()); - createAndAddRetriableUpdate(update, updateCount); - return updateCount; - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedUpdate(e, update); - throw e; - } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return asyncExecuteStatement( - update, - new Callable() { - @Override - public Long call() throws Exception { - return txContext.executeUpdate(update.getStatement()); - } - }); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; - } - } - - /** - * Create a RUN BATCH statement to use with the {@link #executeBatchUpdate(Iterable)} method to - * allow it to be cancelled, time out or retried. - * - *

{@link ReadWriteTransaction} uses the generic methods {@link #executeAsync(ParsedStatement, - * Callable)} and {@link #runWithRetry(Callable)} to allow statements to be cancelled, to timeout - * and to be retried. These methods require a {@link ParsedStatement} as input. When the {@link - * #executeBatchUpdate(Iterable)} method is called, we do not have one {@link ParsedStatement}, - * and the method uses this statement instead in order to use the same logic as the other - * statements. - */ - static final ParsedStatement EXECUTE_BATCH_UPDATE_STATEMENT = - StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); - - @Override - public long[] executeBatchUpdate(final Iterable updates) { - Preconditions.checkNotNull(updates); - final List updateStatements = new LinkedList<>(); - for (ParsedStatement update : updates) { - Preconditions.checkArgument( - update.isUpdate(), - "Statement is not an update statement: " + update.getSqlWithoutComments()); - updateStatements.add(update.getStatement()); - } - checkValidTransaction(); - try { - if (retryAbortsInternally) { - return asyncExecuteStatement( - EXECUTE_BATCH_UPDATE_STATEMENT, - new Callable() { - @Override - public long[] call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public long[] call() throws Exception { - try { - getStatementExecutor() - .invokeInterceptors( - EXECUTE_BATCH_UPDATE_STATEMENT, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - long[] updateCounts = txContext.batchUpdate(updateStatements); - createAndAddRetriableBatchUpdate(updateStatements, updateCounts); - return updateCounts; - } catch (AbortedException e) { - throw e; - } catch (SpannerException e) { - createAndAddFailedBatchUpdate(e, updateStatements); - throw e; - } - } - }); - } - }, - InterceptorsUsage - .IGNORE_INTERCEPTORS); // ignore interceptors here as they are invoked in the - // Callable. - } else { - return asyncExecuteStatement( - EXECUTE_BATCH_UPDATE_STATEMENT, - new Callable() { - @Override - public long[] call() throws Exception { - return txContext.batchUpdate(updateStatements); - } - }); - } - } catch (SpannerException e) { - handlePossibleInvalidatingException(e); - throw e; - } - } - - @Override - public void write(Mutation mutation) { - Preconditions.checkNotNull(mutation); - checkValidTransaction(); - mutations.add(mutation); - } - - @Override - public void write(Iterable mutations) { - Preconditions.checkNotNull(mutations); - checkValidTransaction(); - for (Mutation mutation : mutations) { - this.mutations.add(checkNotNull(mutation)); - } - } - - /** - * Create a COMMIT statement to use with the {@link #commit()} method to allow it to be cancelled, - * time out or retried. - * - *

{@link ReadWriteTransaction} uses the generic methods {@link #executeAsync(ParsedStatement, - * Callable)} and {@link #runWithRetry(Callable)} to allow statements to be cancelled, to timeout - * and to be retried. These methods require a {@link ParsedStatement} as input. When the {@link - * #commit()} method is called directly, we do not have a {@link ParsedStatement}, and the method - * uses this statement instead in order to use the same logic as the other statements. - */ - private static final ParsedStatement COMMIT_STATEMENT = - StatementParser.INSTANCE.parse(Statement.of("COMMIT")); - - private final Callable commitCallable = - new Callable() { - @Override - public Void call() throws Exception { - txContext.buffer(mutations); - txManager.commit(); - return null; - } - }; - - @Override - public void commit() { - checkValidTransaction(); - try { - if (retryAbortsInternally) { - asyncExecuteStatement( - COMMIT_STATEMENT, - new Callable() { - @Override - public Void call() throws Exception { - return runWithRetry( - new Callable() { - @Override - public Void call() throws Exception { - getStatementExecutor() - .invokeInterceptors( - COMMIT_STATEMENT, - StatementExecutionStep.EXECUTE_STATEMENT, - ReadWriteTransaction.this); - commitCallable.call(); - return null; - } - }); - } - }, - InterceptorsUsage.IGNORE_INTERCEPTORS); - } else { - asyncExecuteStatement(COMMIT_STATEMENT, commitCallable); - } - ReadWriteTransaction.this.state = UnitOfWorkState.COMMITTED; - } catch (SpannerException e) { - try { - txManager.close(); - } catch (Throwable t) { - // ignore - } - this.state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - } - - /** - * Executes a database call that could throw an {@link AbortedException}. If an {@link - * AbortedException} is thrown, the transaction will automatically be retried and the checksums of - * all {@link ResultSet}s and update counts of DML statements will be checked against the original - * values of the original transaction. If the checksums and/or update counts do not match, the - * method will throw an {@link AbortedException} that cannot be retried, as the underlying data - * have actually changed. - * - *

If {@link ReadWriteTransaction#retryAbortsInternally} has been set to false, - * this method will throw an exception instead of retrying the transaction if the transaction was - * aborted. - * - * @param callable The actual database calls. - * @return the results of the database calls. - * @throws SpannerException if the database calls threw an exception, an {@link - * AbortedDueToConcurrentModificationException} if a retry of the transaction yielded - * different results than the original transaction, or an {@link AbortedException} if the - * maximum number of retries has been exceeded. - */ - T runWithRetry(Callable callable) throws SpannerException { - while (true) { - try { - return callable.call(); - } catch (final AbortedException aborted) { - if (retryAbortsInternally) { - handleAborted(aborted); - } else { - throw aborted; - } - } catch (SpannerException e) { - throw e; - } catch (Exception e) { - throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, e.getMessage(), e); - } - } - } - - /** - * Registers a {@link ResultSet} on this transaction that must be checked during a retry, and - * returns a retryable {@link ResultSet}. - */ - private ResultSet createAndAddRetryResultSet( - ResultSet resultSet, - ParsedStatement statement, - AnalyzeMode analyzeMode, - QueryOption... options) { - if (retryAbortsInternally) { - ChecksumResultSet checksumResultSet = - createChecksumResultSet(resultSet, statement, analyzeMode, options); - addRetryStatement(checksumResultSet); - return checksumResultSet; - } - return resultSet; - } - - /** Registers the statement as a query that should return an error during a retry. */ - private void createAndAddFailedQuery( - SpannerException e, - ParsedStatement statement, - AnalyzeMode analyzeMode, - QueryOption... options) { - if (retryAbortsInternally) { - addRetryStatement(new FailedQuery(this, e, statement, analyzeMode, options)); - } - } - - private void createAndAddRetriableUpdate(ParsedStatement update, long updateCount) { - if (retryAbortsInternally) { - addRetryStatement(new RetriableUpdate(this, update, updateCount)); - } - } - - private void createAndAddRetriableBatchUpdate(Iterable updates, long[] updateCounts) { - if (retryAbortsInternally) { - addRetryStatement(new RetriableBatchUpdate(this, updates, updateCounts)); - } - } - - /** Registers the statement as an update that should return an error during a retry. */ - private void createAndAddFailedUpdate(SpannerException e, ParsedStatement update) { - if (retryAbortsInternally) { - addRetryStatement(new FailedUpdate(this, e, update)); - } - } - - /** Registers the statements as a batch of updates that should return an error during a retry. */ - private void createAndAddFailedBatchUpdate(SpannerException e, Iterable updates) { - if (retryAbortsInternally) { - addRetryStatement(new FailedBatchUpdate(this, e, updates)); - } - } - - /** - * Adds a statement to the list of statements that should be retried if this transaction aborts. - */ - private void addRetryStatement(RetriableStatement statement) { - Preconditions.checkState( - retryAbortsInternally, "retryAbortsInternally is not enabled for this transaction"); - statements.add(statement); - } - - /** - * Handles an aborted exception by checking whether the transaction may be retried internally, and - * if so, does the retry. If retry is not allowed, or if the retry fails, the method will throw an - * {@link AbortedException}. - */ - private void handleAborted(AbortedException aborted) { - if (transactionRetryAttempts >= MAX_INTERNAL_RETRIES) { - // If the same statement in transaction keeps aborting, then we need to abort here. - throwAbortWithRetryAttemptsExceeded(); - } else if (retryAbortsInternally) { - logger.fine(toString() + ": Starting internal transaction retry"); - while (true) { - // First back off and then restart the transaction. - try { - Thread.sleep(aborted.getRetryDelayInMillis() / 1000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.CANCELLED, "The statement was cancelled"); - } - try { - txContext = txManager.resetForRetry(); - // Inform listeners about the transaction retry that is about to start. - invokeTransactionRetryListenersOnStart(); - // Then retry all transaction statements. - transactionRetryAttempts++; - for (RetriableStatement statement : statements) { - statement.retry(aborted); - } - successfulRetries++; - invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_SUCCESSFUL); - logger.fine( - toString() - + ": Internal transaction retry succeeded. Starting retry of original statement."); - // Retry succeeded, return and continue the original transaction. - break; - } catch (AbortedDueToConcurrentModificationException e) { - // Retry failed because of a concurrent modification, we have to abort. - invokeTransactionRetryListenersOnFinish( - RetryResult.RETRY_ABORTED_DUE_TO_CONCURRENT_MODIFICATION); - logger.fine( - toString() + ": Internal transaction retry aborted due to a concurrent modification"); - // Try to rollback the new transaction and ignore any exceptions. - try { - txManager.rollback(); - } catch (Throwable t) { - // ignore - } - this.state = UnitOfWorkState.ABORTED; - throw e; - } catch (AbortedException e) { - // Retry aborted, do another retry of the transaction. - if (transactionRetryAttempts >= MAX_INTERNAL_RETRIES) { - throwAbortWithRetryAttemptsExceeded(); - } - invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_ABORTED_AND_RESTARTING); - logger.fine(toString() + ": Internal transaction retry aborted, trying again"); - } catch (SpannerException e) { - // unexpected exception - logger.log( - Level.FINE, - toString() + ": Internal transaction retry failed due to an unexpected exception", - e); - // Try to rollback the new transaction and ignore any exceptions. - try { - txManager.rollback(); - } catch (Throwable t) { - // ignore - } - // Set transaction state to aborted as the retry failed. - this.state = UnitOfWorkState.ABORTED; - // Re-throw underlying exception. - throw e; - } - } - } else { - try { - txManager.close(); - } catch (Throwable t) { - // ignore - } - // Internal retry is not enabled. - this.state = UnitOfWorkState.ABORTED; - throw aborted; - } - } - - private void throwAbortWithRetryAttemptsExceeded() throws SpannerException { - invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_ABORTED_AND_MAX_ATTEMPTS_EXCEEDED); - logger.fine( - toString() - + ": Internal transaction retry aborted and max number of retry attempts has been exceeded"); - // Try to rollback the transaction and ignore any exceptions. - // Normally it should not be necessary to do this, but in order to be sure we never leak - // any sessions it is better to do so. - try { - txManager.rollback(); - } catch (Throwable t) { - // ignore - } - this.state = UnitOfWorkState.ABORTED; - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.ABORTED, MAX_INTERNAL_RETRIES_EXCEEDED); - } - - private void invokeTransactionRetryListenersOnStart() { - for (TransactionRetryListener listener : transactionRetryListeners) { - listener.retryStarting(transactionStarted, transactionId, transactionRetryAttempts); - } - } - - private void invokeTransactionRetryListenersOnFinish(RetryResult result) { - for (TransactionRetryListener listener : transactionRetryListeners) { - listener.retryFinished(transactionStarted, transactionId, transactionRetryAttempts, result); - } - } - - /** The {@link Statement} and {@link Callable} for rollbacks */ - private final ParsedStatement rollbackStatement = - StatementParser.INSTANCE.parse(Statement.of("ROLLBACK")); - - private final Callable rollbackCallable = - new Callable() { - @Override - public Void call() throws Exception { - txManager.rollback(); - return null; - } - }; - - @Override - public void rollback() { - ConnectionPreconditions.checkState( - state == UnitOfWorkState.STARTED, "This transaction has status " + state.name()); - try { - asyncExecuteStatement(rollbackStatement, rollbackCallable); - } finally { - // Whatever happens, we should always call close in order to return the underlying session to - // the session pool to avoid any session leaks. - try { - txManager.close(); - } catch (Throwable e) { - // ignore - } - this.state = UnitOfWorkState.ROLLED_BACK; - } - } - - /** - * A retriable statement is a query or DML statement during a read/write transaction that can be - * retried if the original transaction aborted. - */ - interface RetriableStatement { - /** - * Retry this statement in a new transaction. Throws an {@link - * AbortedDueToConcurrentModificationException} if the retry could not successfully be executed - * because of an actual concurrent modification of the underlying data. This {@link - * AbortedDueToConcurrentModificationException} cannot be retried. - */ - void retry(AbortedException aborted) throws AbortedException; - } - - /** Creates a {@link ChecksumResultSet} for this {@link ReadWriteTransaction}. */ - @VisibleForTesting - ChecksumResultSet createChecksumResultSet( - ResultSet delegate, - ParsedStatement statement, - AnalyzeMode analyzeMode, - QueryOption... options) { - return new ChecksumResultSet(this, delegate, statement, analyzeMode, options); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSet.java deleted file mode 100644 index 0a72b369..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSet.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.ByteArray; -import com.google.cloud.Date; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.common.base.Preconditions; -import com.google.spanner.v1.ResultSetStats; -import java.util.List; - -/** - * Forwarding implementation of {@link ResultSet} that forwards all calls to a delegate that can be - * replaced. This is used by the JDBC Driver when a read/write transaction is successfully retried. - * Any {@link ResultSet} that is open during a transaction retry, must be replaced by a result set - * that is fetched using the new transaction. This is achieved by wrapping the returned result sets - * in a {@link ReplaceableForwardingResultSet} that replaces its delegate after a transaction retry. - */ -class ReplaceableForwardingResultSet implements ResultSet { - private ResultSet delegate; - private boolean closed; - - ReplaceableForwardingResultSet(ResultSet delegate) { - this.delegate = Preconditions.checkNotNull(delegate); - } - - /** Replace the underlying delegate {@link ResultSet} with a new one. */ - void replaceDelegate(ResultSet delegate) { - Preconditions.checkNotNull(delegate); - checkClosed(); - if (this.delegate != null) { - this.delegate.close(); - } - this.delegate = delegate; - } - - private void checkClosed() { - if (closed) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "This ResultSet is closed"); - } - } - - boolean isClosed() { - return closed; - } - - @Override - public boolean next() throws SpannerException { - checkClosed(); - return delegate.next(); - } - - @Override - public Struct getCurrentRowAsStruct() { - checkClosed(); - return delegate.getCurrentRowAsStruct(); - } - - @Override - public void close() { - if (delegate != null) { - delegate.close(); - delegate = null; - } - closed = true; - } - - @Override - public ResultSetStats getStats() { - checkClosed(); - return delegate.getStats(); - } - - @Override - public Type getType() { - checkClosed(); - return delegate.getType(); - } - - @Override - public int getColumnCount() { - checkClosed(); - return delegate.getColumnCount(); - } - - @Override - public int getColumnIndex(String columnName) { - checkClosed(); - return delegate.getColumnIndex(columnName); - } - - @Override - public Type getColumnType(int columnIndex) { - checkClosed(); - return delegate.getColumnType(columnIndex); - } - - @Override - public Type getColumnType(String columnName) { - checkClosed(); - return delegate.getColumnType(columnName); - } - - @Override - public boolean isNull(int columnIndex) { - checkClosed(); - return delegate.isNull(columnIndex); - } - - @Override - public boolean isNull(String columnName) { - checkClosed(); - return delegate.isNull(columnName); - } - - @Override - public boolean getBoolean(int columnIndex) { - checkClosed(); - return delegate.getBoolean(columnIndex); - } - - @Override - public boolean getBoolean(String columnName) { - checkClosed(); - return delegate.getBoolean(columnName); - } - - @Override - public long getLong(int columnIndex) { - checkClosed(); - return delegate.getLong(columnIndex); - } - - @Override - public long getLong(String columnName) { - checkClosed(); - return delegate.getLong(columnName); - } - - @Override - public double getDouble(int columnIndex) { - checkClosed(); - return delegate.getDouble(columnIndex); - } - - @Override - public double getDouble(String columnName) { - checkClosed(); - return delegate.getDouble(columnName); - } - - @Override - public String getString(int columnIndex) { - checkClosed(); - return delegate.getString(columnIndex); - } - - @Override - public String getString(String columnName) { - checkClosed(); - return delegate.getString(columnName); - } - - @Override - public ByteArray getBytes(int columnIndex) { - checkClosed(); - return delegate.getBytes(columnIndex); - } - - @Override - public ByteArray getBytes(String columnName) { - checkClosed(); - return delegate.getBytes(columnName); - } - - @Override - public Timestamp getTimestamp(int columnIndex) { - checkClosed(); - return delegate.getTimestamp(columnIndex); - } - - @Override - public Timestamp getTimestamp(String columnName) { - checkClosed(); - return delegate.getTimestamp(columnName); - } - - @Override - public Date getDate(int columnIndex) { - checkClosed(); - return delegate.getDate(columnIndex); - } - - @Override - public Date getDate(String columnName) { - checkClosed(); - return delegate.getDate(columnName); - } - - @Override - public boolean[] getBooleanArray(int columnIndex) { - checkClosed(); - return delegate.getBooleanArray(columnIndex); - } - - @Override - public boolean[] getBooleanArray(String columnName) { - checkClosed(); - return delegate.getBooleanArray(columnName); - } - - @Override - public List getBooleanList(int columnIndex) { - checkClosed(); - return delegate.getBooleanList(columnIndex); - } - - @Override - public List getBooleanList(String columnName) { - checkClosed(); - return delegate.getBooleanList(columnName); - } - - @Override - public long[] getLongArray(int columnIndex) { - checkClosed(); - return delegate.getLongArray(columnIndex); - } - - @Override - public long[] getLongArray(String columnName) { - checkClosed(); - return delegate.getLongArray(columnName); - } - - @Override - public List getLongList(int columnIndex) { - checkClosed(); - return delegate.getLongList(columnIndex); - } - - @Override - public List getLongList(String columnName) { - checkClosed(); - return delegate.getLongList(columnName); - } - - @Override - public double[] getDoubleArray(int columnIndex) { - checkClosed(); - return delegate.getDoubleArray(columnIndex); - } - - @Override - public double[] getDoubleArray(String columnName) { - checkClosed(); - return delegate.getDoubleArray(columnName); - } - - @Override - public List getDoubleList(int columnIndex) { - checkClosed(); - return delegate.getDoubleList(columnIndex); - } - - @Override - public List getDoubleList(String columnName) { - checkClosed(); - return delegate.getDoubleList(columnName); - } - - @Override - public List getStringList(int columnIndex) { - checkClosed(); - return delegate.getStringList(columnIndex); - } - - @Override - public List getStringList(String columnName) { - checkClosed(); - return delegate.getStringList(columnName); - } - - @Override - public List getBytesList(int columnIndex) { - checkClosed(); - return delegate.getBytesList(columnIndex); - } - - @Override - public List getBytesList(String columnName) { - checkClosed(); - return delegate.getBytesList(columnName); - } - - @Override - public List getTimestampList(int columnIndex) { - checkClosed(); - return delegate.getTimestampList(columnIndex); - } - - @Override - public List getTimestampList(String columnName) { - checkClosed(); - return delegate.getTimestampList(columnName); - } - - @Override - public List getDateList(int columnIndex) { - checkClosed(); - return delegate.getDateList(columnIndex); - } - - @Override - public List getDateList(String columnName) { - checkClosed(); - return delegate.getDateList(columnName); - } - - @Override - public List getStructList(int columnIndex) { - checkClosed(); - return delegate.getStructList(columnIndex); - } - - @Override - public List getStructList(String columnName) { - checkClosed(); - return delegate.getStructList(columnName); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/RetriableBatchUpdate.java b/src/main/java/com/google/cloud/spanner/jdbc/RetriableBatchUpdate.java deleted file mode 100644 index 73609e8b..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/RetriableBatchUpdate.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.common.base.Preconditions; -import java.util.Arrays; - -/** - * Retriable batch of DML statements. The check whether the statements had the same effect during - * retry is done by comparing the number of records affected. - */ -final class RetriableBatchUpdate implements RetriableStatement { - private final ReadWriteTransaction transaction; - private final Iterable statements; - private final long[] updateCounts; - - RetriableBatchUpdate( - ReadWriteTransaction transaction, Iterable statements, long[] updateCounts) { - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(statements); - this.transaction = transaction; - this.statements = statements; - this.updateCounts = updateCounts; - } - - @Override - public void retry(AbortedException aborted) throws AbortedException { - long[] newCount = null; - try { - transaction - .getStatementExecutor() - .invokeInterceptors( - ReadWriteTransaction.EXECUTE_BATCH_UPDATE_STATEMENT, - StatementExecutionStep.RETRY_STATEMENT, - transaction); - newCount = transaction.getReadContext().batchUpdate(statements); - } catch (AbortedException e) { - // Just re-throw the AbortedException and let the retry logic determine whether another try - // should be executed or not. - throw e; - } catch (SpannerException e) { - // Unexpected database error that is different from the original transaction. - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } - if (newCount == null || !Arrays.equals(updateCounts, newCount)) { - // The update counts do not match, we cannot retry the transaction. - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/RetriableUpdate.java b/src/main/java/com/google/cloud/spanner/jdbc/RetriableUpdate.java deleted file mode 100644 index ac2b8242..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/RetriableUpdate.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.jdbc.ReadWriteTransaction.RetriableStatement; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; - -/** - * Retriable DML statement. The check whether the statement had the same effect during retry is done - * by comparing the number of records affected. - */ -final class RetriableUpdate implements RetriableStatement { - private final ReadWriteTransaction transaction; - private final ParsedStatement statement; - private final long updateCount; - - RetriableUpdate(ReadWriteTransaction transaction, ParsedStatement statement, long updateCount) { - Preconditions.checkNotNull(transaction); - Preconditions.checkNotNull(statement); - this.transaction = transaction; - this.statement = statement; - this.updateCount = updateCount; - } - - @Override - public void retry(AbortedException aborted) throws AbortedException { - long newCount = -1; - try { - transaction - .getStatementExecutor() - .invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction); - newCount = transaction.getReadContext().executeUpdate(statement.getStatement()); - } catch (AbortedException e) { - // Just re-throw the AbortedException and let the retry logic determine whether another try - // should be executed or not. - throw e; - } catch (SpannerException e) { - // Unexpected database error that is different from the original transaction. - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted, e); - } - if (newCount != updateCount) { - // The update counts do not match, we cannot retry the transaction. - throw SpannerExceptionFactory.newAbortedDueToConcurrentModificationException(aborted); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/SingleUseTransaction.java b/src/main/java/com/google/cloud/spanner/jdbc/SingleUseTransaction.java deleted file mode 100644 index a5d61d5b..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/SingleUseTransaction.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadOnlyTransaction; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerBatchUpdateException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionRunner; -import com.google.cloud.spanner.TransactionRunner.TransactionCallable; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.common.base.Preconditions; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - -/** - * Transaction that is used when a {@link Connection} is in autocommit mode. Each method on this - * transaction actually starts a new transaction on Spanner. The type of transaction that is started - * depends on the type of statement that is being executed. A {@link SingleUseTransaction} will - * always try to choose the most efficient type of one-time transaction that is available for the - * statement. - * - *

A {@link SingleUseTransaction} can be used to execute any type of statement on Cloud Spanner: - * - *

    - *
  • Client side statements, e.g. SHOW VARIABLE AUTOCOMMIT - *
  • Queries, e.g. SELECT * FROM FOO - *
  • DML statements, e.g. UPDATE FOO SET BAR=1 - *
  • DDL statements, e.g. CREATE TABLE FOO (...) - *
- */ -class SingleUseTransaction extends AbstractBaseUnitOfWork { - private final boolean readOnly; - private final DdlClient ddlClient; - private final DatabaseClient dbClient; - private final TimestampBound readOnlyStaleness; - private final AutocommitDmlMode autocommitDmlMode; - private Timestamp readTimestamp = null; - private volatile TransactionManager txManager; - private TransactionRunner writeTransaction; - private boolean used = false; - private UnitOfWorkState state = UnitOfWorkState.STARTED; - - static class Builder extends AbstractBaseUnitOfWork.Builder { - private DdlClient ddlClient; - private DatabaseClient dbClient; - private boolean readOnly; - private TimestampBound readOnlyStaleness; - private AutocommitDmlMode autocommitDmlMode; - - private Builder() {} - - Builder setDdlClient(DdlClient ddlClient) { - Preconditions.checkNotNull(ddlClient); - this.ddlClient = ddlClient; - return this; - } - - Builder setDatabaseClient(DatabaseClient client) { - Preconditions.checkNotNull(client); - this.dbClient = client; - return this; - } - - Builder setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - return this; - } - - Builder setReadOnlyStaleness(TimestampBound staleness) { - Preconditions.checkNotNull(staleness); - this.readOnlyStaleness = staleness; - return this; - } - - Builder setAutocommitDmlMode(AutocommitDmlMode dmlMode) { - Preconditions.checkNotNull(dmlMode); - this.autocommitDmlMode = dmlMode; - return this; - } - - @Override - SingleUseTransaction build() { - Preconditions.checkState(ddlClient != null, "No DDL client specified"); - Preconditions.checkState(dbClient != null, "No DatabaseClient client specified"); - Preconditions.checkState(readOnlyStaleness != null, "No read-only staleness specified"); - Preconditions.checkState(autocommitDmlMode != null, "No autocommit dml mode specified"); - return new SingleUseTransaction(this); - } - } - - static Builder newBuilder() { - return new Builder(); - } - - private SingleUseTransaction(Builder builder) { - super(builder); - this.ddlClient = builder.ddlClient; - this.dbClient = builder.dbClient; - this.readOnly = builder.readOnly; - this.readOnlyStaleness = builder.readOnlyStaleness; - this.autocommitDmlMode = builder.autocommitDmlMode; - } - - @Override - public Type getType() { - return Type.TRANSACTION; - } - - @Override - public UnitOfWorkState getState() { - return state; - } - - @Override - public boolean isActive() { - // Single-use transactions are never active as they can be used only once. - return false; - } - - @Override - public boolean isReadOnly() { - return readOnly; - } - - private void checkAndMarkUsed() { - Preconditions.checkState(!used, "This single-use transaction has already been used"); - used = true; - } - - @Override - public ResultSet executeQuery( - final ParsedStatement statement, - final AnalyzeMode analyzeMode, - final QueryOption... options) { - Preconditions.checkNotNull(statement); - Preconditions.checkArgument(statement.isQuery(), "Statement is not a query"); - checkAndMarkUsed(); - - final ReadOnlyTransaction currentTransaction = - dbClient.singleUseReadOnlyTransaction(readOnlyStaleness); - Callable callable = - new Callable() { - @Override - public ResultSet call() throws Exception { - try { - ResultSet rs; - if (analyzeMode == AnalyzeMode.NONE) { - rs = currentTransaction.executeQuery(statement.getStatement(), options); - } else { - rs = - currentTransaction.analyzeQuery( - statement.getStatement(), analyzeMode.getQueryAnalyzeMode()); - } - // Return a DirectExecuteResultSet, which will directly do a next() call in order to - // ensure that the query is actually sent to Spanner. - return DirectExecuteResultSet.ofResultSet(rs); - } finally { - currentTransaction.close(); - } - } - }; - try { - ResultSet res = asyncExecuteStatement(statement, callable); - readTimestamp = currentTransaction.getReadTimestamp(); - state = UnitOfWorkState.COMMITTED; - return res; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } finally { - currentTransaction.close(); - } - } - - @Override - public Timestamp getReadTimestamp() { - ConnectionPreconditions.checkState( - readTimestamp != null, "There is no read timestamp available for this transaction."); - return readTimestamp; - } - - @Override - public Timestamp getReadTimestampOrNull() { - return readTimestamp; - } - - private boolean hasCommitTimestamp() { - return writeTransaction != null - || (txManager != null - && txManager.getState() - == com.google.cloud.spanner.TransactionManager.TransactionState.COMMITTED); - } - - @Override - public Timestamp getCommitTimestamp() { - ConnectionPreconditions.checkState( - hasCommitTimestamp(), "There is no commit timestamp available for this transaction."); - return writeTransaction != null - ? writeTransaction.getCommitTimestamp() - : txManager.getCommitTimestamp(); - } - - @Override - public Timestamp getCommitTimestampOrNull() { - if (hasCommitTimestamp()) { - try { - return writeTransaction != null - ? writeTransaction.getCommitTimestamp() - : txManager.getCommitTimestamp(); - } catch (SpannerException e) { - // ignore - } - } - return null; - } - - @Override - public void executeDdl(final ParsedStatement ddl) { - Preconditions.checkNotNull(ddl); - Preconditions.checkArgument( - ddl.getType() == StatementType.DDL, "Statement is not a ddl statement"); - ConnectionPreconditions.checkState( - !isReadOnly(), "DDL statements are not allowed in read-only mode"); - checkAndMarkUsed(); - - try { - Callable callable = - new Callable() { - @Override - public Void call() throws Exception { - OperationFuture operation = - ddlClient.executeDdl(ddl.getSqlWithoutComments()); - return operation.get(); - } - }; - asyncExecuteStatement(ddl, callable); - state = UnitOfWorkState.COMMITTED; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - } - - @Override - public long executeUpdate(final ParsedStatement update) { - Preconditions.checkNotNull(update); - Preconditions.checkArgument(update.isUpdate(), "Statement is not an update statement"); - ConnectionPreconditions.checkState( - !isReadOnly(), "Update statements are not allowed in read-only mode"); - checkAndMarkUsed(); - - long res; - try { - switch (autocommitDmlMode) { - case TRANSACTIONAL: - res = executeAsyncTransactionalUpdate(update, new TransactionalUpdateCallable(update)); - break; - case PARTITIONED_NON_ATOMIC: - res = executeAsyncPartitionedUpdate(update); - break; - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); - } - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - state = UnitOfWorkState.COMMITTED; - return res; - } - - /** Execute an update statement as a partitioned DML statement. */ - private long executeAsyncPartitionedUpdate(final ParsedStatement update) { - Callable callable = - new Callable() { - @Override - public Long call() throws Exception { - return dbClient.executePartitionedUpdate(update.getStatement()); - } - }; - return asyncExecuteStatement(update, callable); - } - - private final ParsedStatement executeBatchUpdateStatement = - StatementParser.INSTANCE.parse(Statement.of("RUN BATCH")); - - @Override - public long[] executeBatchUpdate(Iterable updates) { - Preconditions.checkNotNull(updates); - for (ParsedStatement update : updates) { - Preconditions.checkArgument( - update.isUpdate(), - "Statement is not an update statement: " + update.getSqlWithoutComments()); - } - ConnectionPreconditions.checkState( - !isReadOnly(), "Batch update statements are not allowed in read-only mode"); - checkAndMarkUsed(); - - long[] res; - try { - switch (autocommitDmlMode) { - case TRANSACTIONAL: - res = - executeAsyncTransactionalUpdate( - executeBatchUpdateStatement, new TransactionalBatchUpdateCallable(updates)); - break; - case PARTITIONED_NON_ATOMIC: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "Batch updates are not allowed in " + autocommitDmlMode); - default: - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode); - } - } catch (SpannerBatchUpdateException e) { - // Batch update exceptions does not cause a rollback. - state = UnitOfWorkState.COMMITTED; - throw e; - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - state = UnitOfWorkState.COMMITTED; - return res; - } - - /** Base class for executing DML updates (both single statements and batches). */ - private abstract class AbstractUpdateCallable implements Callable { - abstract T executeUpdate(TransactionContext txContext); - - @Override - public T call() throws Exception { - try { - txManager = dbClient.transactionManager(); - // Check the interrupted state after each (possible) round-trip to the db to allow the - // statement to be cancelled. - checkInterrupted(); - try (TransactionContext txContext = txManager.begin()) { - checkInterrupted(); - T res = executeUpdate(txContext); - checkInterrupted(); - txManager.commit(); - checkInterrupted(); - return res; - } - } finally { - if (txManager != null) { - // Calling txManager.close() will rollback the transaction if it is still active, i.e. if - // an error occurred before the commit() call returned successfully. - txManager.close(); - } - } - } - } - - /** {@link Callable} for a single update statement. */ - private final class TransactionalUpdateCallable extends AbstractUpdateCallable { - private final ParsedStatement update; - - private TransactionalUpdateCallable(ParsedStatement update) { - this.update = update; - } - - @Override - Long executeUpdate(TransactionContext txContext) { - return txContext.executeUpdate(update.getStatement()); - } - } - - /** {@link Callable} for a batch update. */ - private final class TransactionalBatchUpdateCallable extends AbstractUpdateCallable { - private final List updates; - - private TransactionalBatchUpdateCallable(Iterable updates) { - this.updates = new LinkedList<>(); - for (ParsedStatement update : updates) { - this.updates.add(update.getStatement()); - } - } - - @Override - long[] executeUpdate(TransactionContext txContext) { - return txContext.batchUpdate(updates); - } - } - - private T executeAsyncTransactionalUpdate( - final ParsedStatement update, final AbstractUpdateCallable callable) { - long startedTime = System.currentTimeMillis(); - // This method uses a TransactionManager instead of the TransactionRunner in order to be able to - // handle timeouts and canceling of a statement. - while (true) { - try { - return asyncExecuteStatement(update, callable); - } catch (AbortedException e) { - try { - Thread.sleep(e.getRetryDelayInMillis() / 1000); - } catch (InterruptedException e1) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.CANCELLED, "Statement execution was interrupted", e1); - } - // Check whether the timeout time has been exceeded. - long executionTime = System.currentTimeMillis() - startedTime; - if (getStatementTimeout().hasTimeout() - && executionTime > getStatementTimeout().getTimeoutValue(TimeUnit.MILLISECONDS)) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.DEADLINE_EXCEEDED, - "Statement execution timeout occurred for " + update.getSqlWithoutComments()); - } - } - } - } - - private void checkInterrupted() throws InterruptedException { - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - } - - @Override - public void write(final Mutation mutation) { - write(Arrays.asList(mutation)); - } - - @Override - public void write(final Iterable mutations) { - Preconditions.checkNotNull(mutations); - ConnectionPreconditions.checkState( - !isReadOnly(), "Update statements are not allowed in read-only mode"); - checkAndMarkUsed(); - - writeTransaction = dbClient.readWriteTransaction(); - try { - writeTransaction.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) throws Exception { - transaction.buffer(mutations); - return null; - } - }); - } catch (Throwable e) { - state = UnitOfWorkState.COMMIT_FAILED; - throw e; - } - state = UnitOfWorkState.COMMITTED; - } - - @Override - public void commit() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Commit is not supported for single-use transactions"); - } - - @Override - public void rollback() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Rollback is not supported for single-use transactions"); - } - - @Override - public long[] runBatch() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for single-use transactions"); - } - - @Override - public void abortBatch() { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for single-use transactions"); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/SpannerPool.java b/src/main/java/com/google/cloud/spanner/jdbc/SpannerPool.java index 771ad505..8bcbcc19 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/SpannerPool.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/SpannerPool.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,413 +16,14 @@ package com.google.cloud.spanner.jdbc; -import com.google.api.core.ApiFunction; -import com.google.auth.Credentials; -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.SpannerOptions; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import io.grpc.ManagedChannelBuilder; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.concurrent.GuardedBy; - -/** - * Pool for keeping track of {@link Spanner} instances needed by JDBC connections. - * - *

When a JDBC connection is opened for a Google Cloud Spanner database, a {@link Spanner} object - * can be opened in the background. The {@link SpannerPool} keeps track of which {@link Spanner} - * objects have been opened by connections during the lifetime of the JVM, which connections are - * still opened and closed, and which {@link Spanner} objects could be closed. - * - *

Call the method {@link SpannerPool#closeSpannerPool()} at the end of your application to - * gracefully shutdown all instances in the pool. - */ +/** @see com.google.cloud.spanner.connection.SpannerPool */ +@Deprecated public class SpannerPool { - private static final Logger logger = Logger.getLogger(SpannerPool.class.getName()); + private SpannerPool() {} - /** - * Closes the default {@link SpannerPool} and all {@link Spanner} instances that have been opened - * by connections and that are still open. Call this method at the end of your application to - * gracefully close all {@link Spanner} instances in the pool. Failing to call this method will - * keep your application running for 60 seconds after you close the last {@link - * java.sql.Connection} to Cloud Spanner, as this is the default timeout before the {@link - * SpannerPool} closes the unused {@link Spanner} instances. - */ + /** @see com.google.cloud.spanner.connection.SpannerPool#closeSpannerPool() */ + @Deprecated public static void closeSpannerPool() { - INSTANCE.checkAndCloseSpanners(); - } - - /** - * The minimum number of milliseconds a {@link Spanner} should not have been used for a connection - * before it is closed. - */ - private static final long DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED = 60000L; - - static final SpannerPool INSTANCE = - new SpannerPool(DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED); - - @VisibleForTesting - enum CheckAndCloseSpannersMode { - WARN, - ERROR; - } - - private final class CloseSpannerRunnable implements Runnable { - @Override - public void run() { - try { - checkAndCloseSpanners(CheckAndCloseSpannersMode.WARN); - } catch (Exception e) { - // ignore - } - } - } - - private final class CloseUnusedSpannersRunnable implements Runnable { - @Override - public void run() { - try { - closeUnusedSpanners(SpannerPool.this.closeSpannerAfterMillisecondsUnused); - } catch (Throwable e) { - logger.log(Level.FINE, "Scheduled call to closeUnusedSpanners failed", e); - } - } - } - - static class SpannerPoolKey { - private final String host; - private final String projectId; - private final Credentials credentials; - private final Integer numChannels; - private final boolean usePlainText; - private final String userAgent; - - private static SpannerPoolKey of(ConnectionOptions options) { - return new SpannerPoolKey(options); - } - - private SpannerPoolKey(ConnectionOptions options) { - this.host = options.getHost(); - this.projectId = options.getProjectId(); - this.credentials = options.getCredentials(); - this.numChannels = options.getNumChannels(); - this.usePlainText = options.isUsePlainText(); - this.userAgent = options.getUserAgent(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof SpannerPoolKey)) { - return false; - } - SpannerPoolKey other = (SpannerPoolKey) o; - return Objects.equals(this.host, other.host) - && Objects.equals(this.projectId, other.projectId) - && Objects.equals(this.credentials, other.credentials) - && Objects.equals(this.numChannels, other.numChannels) - && Objects.equals(this.usePlainText, other.usePlainText) - && Objects.equals(this.userAgent, other.userAgent); - } - - @Override - public int hashCode() { - return Objects.hash( - this.host, - this.projectId, - this.credentials, - this.numChannels, - this.usePlainText, - this.userAgent); - } - } - - /** - * The management threads of a {@link SpannerPool} are lazily initialized to prevent unnecessary - * threads to be created when the connection API is not used. - */ - private boolean initialized = false; - /** - * Thread that will be run as a shutdown hook on closing the application. This thread will close - * any Spanner instances opened by the Connection API that are still open. - */ - private Thread shutdownThread = null; - - /** - * Keep unused {@link Spanner} instances open and in the pool for this duration after all its - * {@link Connection}s have been closed. This prevents unnecessary opening and closing of {@link - * Spanner} instances. - */ - private final long closeSpannerAfterMillisecondsUnused; - - /** - * This scheduled task will close all {@link Spanner} objects that have not been used for an open - * connection for at least {@link SpannerPool#DEFAULT_CLOSE_SPANNER_AFTER_MILLISECONDS_UNUSED} - * milliseconds. - */ - private ScheduledExecutorService closerService; - - @GuardedBy("this") - private final Map spanners = new HashMap<>(); - - @GuardedBy("this") - private final Map> connections = new HashMap<>(); - - /** - * Keep track of the moment that the last connection for a specific {@link SpannerPoolKey} was - * closed, so that we can use this to determine whether a {@link Spanner} instance should be - * closed and removed from the pool. As {@link Spanner} instances are expensive to create and - * close, we do not want to do that unnecessarily. By adding a delay between the moment the last - * {@link Connection} for a {@link Spanner} was closed and the moment we close the {@link Spanner} - * instance, we prevent applications that open one or more connections for a process and close all - * these connections at the end of the process from getting a severe performance penalty from - * opening and closing {@link Spanner} instances all the time. - * - *

{@link Spanner} instances are closed and removed from the pool when the last connection was - * closed more than {@link #closeSpannerAfterMillisecondsUnused} milliseconds ago. - */ - @GuardedBy("this") - private final Map lastConnectionClosedAt = new HashMap<>(); - - @VisibleForTesting - SpannerPool() { - this(0L); - } - - @VisibleForTesting - SpannerPool(long closeSpannerAfterMillisecondsUnused) { - this.closeSpannerAfterMillisecondsUnused = closeSpannerAfterMillisecondsUnused; - } - - /** - * Gets a Spanner object for a connection with the properties specified in the {@link - * ConnectionOptions} object. The {@link SpannerPool} will manage a pool of opened Spanner objects - * for the different connections, and reuse Spanner objects whenever possible. Spanner objects - * will also be closed down when the application is closing. - * - * @param options The specification of the Spanner database to connect to. - * @param connection The {@link ConnectionImpl} that will be created. This {@link ConnectionImpl} - * will be tracked by the pool to know when a {@link Spanner} object can be closed. - * @return an opened {@link Spanner} object that can be used by a connection to communicate with - * the Spanner database. - */ - Spanner getSpanner(ConnectionOptions options, ConnectionImpl connection) { - Preconditions.checkNotNull(options); - Preconditions.checkNotNull(connection); - SpannerPoolKey key = SpannerPoolKey.of(options); - Spanner spanner; - synchronized (this) { - if (!initialized) { - initialize(); - } - if (spanners.get(key) != null) { - spanner = spanners.get(key); - } else { - spanner = createSpanner(key); - spanners.put(key, spanner); - } - List registeredConnectionsForSpanner = connections.get(key); - if (registeredConnectionsForSpanner == null) { - registeredConnectionsForSpanner = new ArrayList<>(); - connections.put(key, registeredConnectionsForSpanner); - } - registeredConnectionsForSpanner.add(connection); - lastConnectionClosedAt.remove(key); - return spanner; - } - } - - private void initialize() { - shutdownThread = new Thread(new CloseSpannerRunnable(), "SpannerPool shutdown hook"); - Runtime.getRuntime().addShutdownHook(shutdownThread); - if (this.closeSpannerAfterMillisecondsUnused > 0) { - this.closerService = - Executors.newSingleThreadScheduledExecutor( - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, "close-unused-spanners-worker"); - thread.setDaemon(true); - return thread; - } - }); - this.closerService.scheduleAtFixedRate( - new CloseUnusedSpannersRunnable(), - this.closeSpannerAfterMillisecondsUnused, - this.closeSpannerAfterMillisecondsUnused, - TimeUnit.MILLISECONDS); - } - initialized = true; - } - - @SuppressWarnings("rawtypes") - @VisibleForTesting - Spanner createSpanner(SpannerPoolKey key) { - SpannerOptions.Builder builder = SpannerOptions.newBuilder(); - builder - .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, JdbcDriver.getClientLibToken())) - .setHost(key.host) - .setProjectId(key.projectId) - .setCredentials(key.credentials); - if (key.numChannels != null) { - builder.setNumChannels(key.numChannels); - } - if (key.usePlainText) { - // Credentials may not be sent over a plain text channel. - builder.setCredentials(NoCredentials.getInstance()); - // Set a custom channel configurator to allow http instead of https. - builder.setChannelConfigurator( - new ApiFunction() { - @Override - public ManagedChannelBuilder apply(ManagedChannelBuilder input) { - input.usePlaintext(); - return input; - } - }); - } - return builder.build().getService(); - } - - /** - * Remove the given {@link ConnectionImpl} from the list of connections that should be monitored - * by this pool. - * - * @param options The {@link ConnectionOptions} that were used to create the connection. - * @param connection The {@link ConnectionImpl} to remove from this pool.. - */ - void removeConnection(ConnectionOptions options, ConnectionImpl connection) { - Preconditions.checkNotNull(options); - Preconditions.checkNotNull(connection); - SpannerPoolKey key = SpannerPoolKey.of(options); - synchronized (this) { - if (spanners.containsKey(key) && connections.containsKey(key)) { - List registeredConnections = connections.get(key); - // Remove the connection from the pool. - if (registeredConnections == null || !registeredConnections.remove(connection)) { - logger.log( - Level.WARNING, - "There are no connections registered for ConnectionOptions " + options.toString()); - } else { - // Check if this was the last connection for this spanner key. - if (registeredConnections.isEmpty()) { - // Register the moment the last connection for this Spanner key was removed, so we know - // which Spanner objects we could close. - lastConnectionClosedAt.put(key, System.currentTimeMillis()); - } - } - } else { - logger.log( - Level.WARNING, - "There is no Spanner registered for ConnectionOptions " + options.toString()); - } - } - } - - /** - * Checks that there are no {@link Connection}s that have been created by this {@link SpannerPool} - * that are still open, and then closes all {@link Spanner} instances in the pool. If there is at - * least one unclosed {@link Connection} left in the pool, the method will throw a {@link - * SpannerException} and no {@link Spanner} instances will be closed. - */ - void checkAndCloseSpanners() { - checkAndCloseSpanners(CheckAndCloseSpannersMode.ERROR); - } - - @VisibleForTesting - void checkAndCloseSpanners(CheckAndCloseSpannersMode mode) { - List keysStillInUse = new ArrayList<>(); - synchronized (this) { - for (Entry entry : spanners.entrySet()) { - if (!lastConnectionClosedAt.containsKey(entry.getKey())) { - keysStillInUse.add(entry.getKey()); - } - } - if (keysStillInUse.isEmpty() || mode == CheckAndCloseSpannersMode.WARN) { - if (!keysStillInUse.isEmpty()) { - logLeakedConnections(keysStillInUse); - logger.log( - Level.WARNING, - "There is/are " - + keysStillInUse.size() - + " connection(s) still open." - + " Close all connections before stopping the application"); - } - // Force close all Spanner instances by passing in a value that will always be less than the - // difference between the current time and the close time of a connection. - closeUnusedSpanners(Long.MIN_VALUE); - } else { - logLeakedConnections(keysStillInUse); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "There is/are " - + keysStillInUse.size() - + " connection(s) still open. Close all connections before calling closeSpanner()"); - } - } - } - - private void logLeakedConnections(List keysStillInUse) { - synchronized (this) { - for (SpannerPoolKey key : keysStillInUse) { - for (ConnectionImpl con : connections.get(key)) { - if (!con.isClosed() && con.getLeakedException() != null) { - logger.log(Level.WARNING, "Leaked connection", con.getLeakedException()); - } - } - } - } - } - - /** - * Closes Spanner objects that are no longer in use by connections, and where the last connection - * that used it was closed more than closeSpannerAfterMillisecondsUnused seconds ago. - * The delay ensures that Spanner objects are not closed unless there's a good reason for it. - * - * @param closeSpannerAfterMillisecondsUnused The number of milliseconds a {@link Spanner} object - * should not have been used for a {@link Connection} before it is closed by this method. - */ - @VisibleForTesting - void closeUnusedSpanners(long closeSpannerAfterMillisecondsUnused) { - List keysToBeRemoved = new ArrayList<>(); - synchronized (this) { - for (Entry entry : lastConnectionClosedAt.entrySet()) { - Long closedAt = entry.getValue(); - // Check whether the last connection was closed more than - // closeSpannerAfterMillisecondsUnused milliseconds ago. - if (closedAt != null - && ((System.currentTimeMillis() - closedAt.longValue())) - > closeSpannerAfterMillisecondsUnused) { - Spanner spanner = spanners.get(entry.getKey()); - if (spanner != null) { - try { - spanner.close(); - } finally { - // Even if the close operation failed, we should remove the spanner object as it is no - // longer valid. - spanners.remove(entry.getKey()); - keysToBeRemoved.add(entry.getKey()); - } - } - } - } - for (SpannerPoolKey key : keysToBeRemoved) { - lastConnectionClosedAt.remove(key); - } - } + com.google.cloud.spanner.connection.SpannerPool.closeSpannerPool(); } } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutionInterceptor.java b/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutionInterceptor.java deleted file mode 100644 index 227bc629..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutionInterceptor.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; - -/** Interface for interceptors that are invoked before a statement is executed. */ -interface StatementExecutionInterceptor { - void intercept(ParsedStatement statement, StatementExecutionStep step, UnitOfWork transaction); -} - -/** - * Enum passed in to a {@link StatementExecutionInterceptor} to determine what/why a statement is - * being executed. - */ -enum StatementExecutionStep { - /** The initial execution of a statement (DML/Query). */ - EXECUTE_STATEMENT, - /** A call to {@link ResultSet#next()}. */ - CALL_NEXT_ON_RESULT_SET, - /** Execution of the statement during an internal transaction retry. */ - RETRY_STATEMENT, - /** A call to {@link ResultSet#next()} during internal transaction retry. */ - RETRY_NEXT_ON_RESULT_SET; -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutor.java b/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutor.java deleted file mode 100644 index 994c4c44..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/StatementExecutor.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.DurationValueGetter; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.google.protobuf.Duration; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * {@link StatementExecutor} is responsible for executing statements on a {@link Connection}. - * Statements are executed using a separate executor to allow timeouts and cancellation of - * statements. - */ -class StatementExecutor { - - /** Simple holder class for statement timeout that allows us to pass the value by reference. */ - static class StatementTimeout { - /** - * Only {@link TimeUnit#NANOSECONDS}, {@link TimeUnit#MICROSECONDS}, {@link - * TimeUnit#MILLISECONDS} and {@link TimeUnit#SECONDS} may be used to specify a statement - * timeout. - */ - static boolean isValidTimeoutUnit(TimeUnit unit) { - return unit == TimeUnit.NANOSECONDS - || unit == TimeUnit.MICROSECONDS - || unit == TimeUnit.MILLISECONDS - || unit == TimeUnit.SECONDS; - } - - /** The statement timeout. */ - private Duration duration = null; - - /** Creates a {@link StatementTimeout} that will never timeout. */ - @VisibleForTesting - static StatementTimeout nullTimeout() { - return new StatementTimeout(); - } - - /** Creates a {@link StatementTimeout} with the given duration. */ - @VisibleForTesting - static StatementTimeout of(long timeout, TimeUnit unit) { - Preconditions.checkArgument(timeout > 0L); - Preconditions.checkArgument(isValidTimeoutUnit(unit)); - StatementTimeout res = new StatementTimeout(); - res.duration = ReadOnlyStalenessUtil.createDuration(timeout, unit); - return res; - } - - /** - * Does this {@link StatementTimeout} have an actual timeout (i.e. it will eventually timeout). - */ - boolean hasTimeout() { - return duration != null; - } - - void clearTimeoutValue() { - this.duration = null; - } - - void setTimeoutValue(long timeout, TimeUnit unit) { - Preconditions.checkArgument(timeout > 0L); - Preconditions.checkArgument(isValidTimeoutUnit(unit)); - this.duration = ReadOnlyStalenessUtil.createDuration(timeout, unit); - } - - long getTimeoutValue(TimeUnit unit) { - Preconditions.checkArgument(isValidTimeoutUnit(unit)); - return duration == null ? 0L : ReadOnlyStalenessUtil.durationToUnits(duration, unit); - } - - /** - * Returns the {@link TimeUnit} with the least precision that could be used to represent this - * {@link StatementTimeout} without loss of precision. - */ - TimeUnit getAppropriateTimeUnit() { - ConnectionPreconditions.checkState( - duration != null, "This StatementTimeout has no timeout value"); - return ReadOnlyStalenessUtil.getAppropriateTimeUnit( - new DurationValueGetter() { - @Override - public long getDuration(TimeUnit unit) { - return StatementTimeout.this.getTimeoutValue(unit); - } - - @Override - public boolean hasDuration() { - return StatementTimeout.this.hasTimeout(); - } - }); - } - } - - /** - * Use a {@link ThreadFactory} that produces daemon threads and sets recognizable name on the - * threads. - */ - private static final ThreadFactory THREAD_FACTORY = - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("connection-executor-%d") - .setThreadFactory(MoreExecutors.platformThreadFactory()) - .build(); - - /** Creates an {@link ExecutorService} for a {@link StatementExecutor}. */ - private static ExecutorService createExecutorService() { - return new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), THREAD_FACTORY); - } - - private ExecutorService executor = createExecutorService(); - - /** - * Interceptors that should be invoked before or after a statement is executed can be registered - * for a connection. This are added to this list. The interceptors are intended for test usage. - */ - private final List interceptors; - - @VisibleForTesting - StatementExecutor() { - this.interceptors = Collections.emptyList(); - } - - StatementExecutor(List interceptors) { - this.interceptors = Collections.unmodifiableList(interceptors); - } - - /** - * Recreates this {@link StatementExecutor} and its {@link ExecutorService}. This can be necessary - * if a statement times out or is cancelled, and it cannot be guaranteed that the statement - * execution can be terminated. In order to prevent the single threaded {@link ExecutorService} to - * continue to block on the timed out/cancelled statement, a new {@link ExecutorService} is - * created. - */ - void recreate() { - executor.shutdown(); - executor = createExecutorService(); - } - - /** - * Shutdown this executor now and do not wait for any statement that is being executed to finish. - */ - List shutdownNow() { - return executor.shutdownNow(); - } - - /** Execute a statement on this {@link StatementExecutor}. */ - Future submit(Callable callable) { - return executor.submit(callable); - } - - /** - * Invoke the interceptors that have been registered for this {@link StatementExecutor} for the - * given step. - */ - void invokeInterceptors( - ParsedStatement statement, StatementExecutionStep step, UnitOfWork transaction) { - for (StatementExecutionInterceptor interceptor : interceptors) { - interceptor.intercept(statement, step, transaction); - } - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/StatementParser.java b/src/main/java/com/google/cloud/spanner/jdbc/StatementParser.java deleted file mode 100644 index e3e998a2..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/StatementParser.java +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import java.util.Collections; -import java.util.Objects; -import java.util.Set; - -/** - * Internal class for the Spanner Connection API. - * - *

Parses {@link ClientSideStatement}s and normal SQL statements. The parser is able to recognize - * the type of statement, allowing the connection API to know which method on Spanner should be - * called. The parser does not validate the validity of statements, except for {@link - * ClientSideStatement}s. This means that an invalid DML statement could be accepted by the {@link - * StatementParser} and sent to Spanner, and Spanner will then reject it with some error message. - */ -class StatementParser { - /** Singleton instance of {@link StatementParser}. */ - public static final StatementParser INSTANCE = new StatementParser(); - - /** The type of statement that has been recognized by the parser. */ - enum StatementType { - CLIENT_SIDE, - DDL, - QUERY, - UPDATE, - UNKNOWN; - } - - /** A statement that has been parsed */ - static class ParsedStatement { - private final StatementType type; - private final ClientSideStatementImpl clientSideStatement; - private final Statement statement; - private final String sqlWithoutComments; - - private static ParsedStatement clientSideStatement( - ClientSideStatementImpl clientSideStatement, - Statement statement, - String sqlWithoutComments) { - return new ParsedStatement(clientSideStatement, statement, sqlWithoutComments); - } - - private static ParsedStatement ddl(Statement statement, String sqlWithoutComments) { - return new ParsedStatement(StatementType.DDL, statement, sqlWithoutComments); - } - - private static ParsedStatement query( - Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) { - return new ParsedStatement( - StatementType.QUERY, statement, sqlWithoutComments, defaultQueryOptions); - } - - private static ParsedStatement update(Statement statement, String sqlWithoutComments) { - return new ParsedStatement(StatementType.UPDATE, statement, sqlWithoutComments); - } - - private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) { - return new ParsedStatement(StatementType.UNKNOWN, statement, sqlWithoutComments); - } - - private ParsedStatement( - ClientSideStatementImpl clientSideStatement, - Statement statement, - String sqlWithoutComments) { - Preconditions.checkNotNull(clientSideStatement); - Preconditions.checkNotNull(statement); - this.type = StatementType.CLIENT_SIDE; - this.clientSideStatement = clientSideStatement; - this.statement = statement; - this.sqlWithoutComments = sqlWithoutComments; - } - - private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) { - this(type, statement, sqlWithoutComments, null); - } - - private ParsedStatement( - StatementType type, - Statement statement, - String sqlWithoutComments, - QueryOptions defaultQueryOptions) { - Preconditions.checkNotNull(type); - Preconditions.checkNotNull(statement); - this.type = type; - this.clientSideStatement = null; - this.statement = mergeQueryOptions(statement, defaultQueryOptions); - this.sqlWithoutComments = sqlWithoutComments; - } - - @Override - public int hashCode() { - return Objects.hash( - this.type, this.clientSideStatement, this.statement, this.sqlWithoutComments); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof ParsedStatement)) { - return false; - } - ParsedStatement o = (ParsedStatement) other; - return Objects.equals(this.type, o.type) - && Objects.equals(this.clientSideStatement, o.clientSideStatement) - && Objects.equals(this.statement, o.statement) - && Objects.equals(this.sqlWithoutComments, o.sqlWithoutComments); - } - - StatementType getType() { - return type; - } - - boolean isQuery() { - switch (type) { - case CLIENT_SIDE: - return getClientSideStatement().isQuery(); - case QUERY: - return true; - case UPDATE: - case DDL: - case UNKNOWN: - default: - } - return false; - } - - boolean isUpdate() { - switch (type) { - case CLIENT_SIDE: - return getClientSideStatement().isUpdate(); - case UPDATE: - return true; - case QUERY: - case DDL: - case UNKNOWN: - default: - } - return false; - } - - boolean isDdl() { - switch (type) { - case DDL: - return true; - case CLIENT_SIDE: - case UPDATE: - case QUERY: - case UNKNOWN: - default: - } - return false; - } - - Statement getStatement() { - return statement; - } - - /** - * Merges the {@link QueryOptions} of the {@link Statement} with the current {@link - * QueryOptions} of this connection. The {@link QueryOptions} that are already present on the - * statement take precedence above the connection {@link QueryOptions}. - */ - Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOptions) { - if (defaultQueryOptions == null - || defaultQueryOptions.equals(QueryOptions.getDefaultInstance())) { - return statement; - } - if (statement.getQueryOptions() == null) { - return statement.toBuilder().withQueryOptions(defaultQueryOptions).build(); - } - return statement - .toBuilder() - .withQueryOptions( - defaultQueryOptions.toBuilder().mergeFrom(statement.getQueryOptions()).build()) - .build(); - } - - String getSqlWithoutComments() { - return sqlWithoutComments; - } - - ClientSideStatement getClientSideStatement() { - Preconditions.checkState( - clientSideStatement != null, - "This ParsedStatement does not contain a ClientSideStatement"); - return clientSideStatement; - } - } - - private final Set ddlStatements = ImmutableSet.of("CREATE", "DROP", "ALTER"); - private final Set selectStatements = ImmutableSet.of("SELECT", "WITH"); - private final Set dmlStatements = ImmutableSet.of("INSERT", "UPDATE", "DELETE"); - private final Set statements; - - /** Private constructor for singleton instance. */ - private StatementParser() { - try { - statements = - Collections.unmodifiableSet(ClientSideStatements.INSTANCE.getCompiledStatements()); - } catch (CompileException e) { - throw new RuntimeException(e); - } - } - - /** - * Parses the given statement and categorizes it as one of the possible {@link StatementType}s. - * The validity of the statement is not checked, unless it is a client-side statement. - * - * @param statement The statement to parse. - * @return the parsed and categorized statement. - */ - ParsedStatement parse(Statement statement) { - return parse(statement, null); - } - - ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) { - String sql = removeCommentsAndTrim(statement.getSql()); - ClientSideStatementImpl client = parseClientSideStatement(sql); - if (client != null) { - return ParsedStatement.clientSideStatement(client, statement, sql); - } else if (isQuery(sql)) { - return ParsedStatement.query(statement, sql, defaultQueryOptions); - } else if (isUpdateStatement(sql)) { - return ParsedStatement.update(statement, sql); - } else if (isDdlStatement(sql)) { - return ParsedStatement.ddl(statement, sql); - } - return ParsedStatement.unknown(statement, sql); - } - - /** - * Parses the given statement as a client-side statement. Client-side statements are statements - * that are never sent to Cloud Spanner, but that are interpreted by the Connection API and then - * translated into some action, such as for example starting a transaction or getting the last - * commit timestamp. - * - * @param sql The statement to try to parse as a client-side statement (without any comments). - * @return a valid {@link ClientSideStatement} or null if the statement is not a client-side - * statement. - */ - @VisibleForTesting - ClientSideStatementImpl parseClientSideStatement(String sql) { - for (ClientSideStatementImpl css : statements) { - if (css.matches(sql)) { - return css; - } - } - return null; - } - - /** - * Checks whether the given statement is (probably) a DDL statement. The method does not check the - * validity of the statement, only if it is a DDL statement based on the first word in the - * statement. - * - * @param sql The statement to check (without any comments). - * @return true if the statement is a DDL statement (i.e. starts with 'CREATE', - * 'ALTER' or 'DROP'). - */ - boolean isDdlStatement(String sql) { - return statementStartsWith(sql, ddlStatements); - } - - /** - * Checks whether the given statement is (probably) a SELECT query. The method does not check the - * validity of the statement, only if it is a SELECT statement based on the first word in the - * statement. - * - * @param sql The statement to check (without any comments). - * @return true if the statement is a SELECT statement (i.e. starts with 'SELECT'). - */ - boolean isQuery(String sql) { - // Skip any query hints at the beginning of the query. - if (sql.startsWith("@")) { - sql = removeStatementHint(sql); - } - return statementStartsWith(sql, selectStatements); - } - - /** - * Checks whether the given statement is (probably) an update statement. The method does not check - * the validity of the statement, only if it is an update statement based on the first word in the - * statement. - * - * @param sql The statement to check (without any comments). - * @return true if the statement is a DML update statement (i.e. starts with - * 'INSERT', 'UPDATE' or 'DELETE'). - */ - boolean isUpdateStatement(String sql) { - return statementStartsWith(sql, dmlStatements); - } - - private boolean statementStartsWith(String sql, Iterable checkStatements) { - Preconditions.checkNotNull(sql); - String[] tokens = sql.split("\\s+", 2); - if (tokens.length > 0) { - for (String check : checkStatements) { - if (tokens[0].equalsIgnoreCase(check)) { - return true; - } - } - } - return false; - } - - /** - * Removes comments from and trims the given sql statement. Spanner supports three types of - * comments: - * - *

    - *
  • Single line comments starting with '--' - *
  • Single line comments starting with '#' - *
  • Multi line comments between '/*' and '*/' - *
- * - * Reference: https://cloud.google.com/spanner/docs/lexical#comments - * - * @param sql The sql statement to remove comments from and to trim. - * @return the sql statement without the comments and leading and trailing spaces. - */ - static String removeCommentsAndTrim(String sql) { - Preconditions.checkNotNull(sql); - final char SINGLE_QUOTE = '\''; - final char DOUBLE_QUOTE = '"'; - final char BACKTICK_QUOTE = '`'; - final char HYPHEN = '-'; - final char DASH = '#'; - final char SLASH = '/'; - final char ASTERIKS = '*'; - boolean isInQuoted = false; - boolean isInSingleLineComment = false; - boolean isInMultiLineComment = false; - char startQuote = 0; - boolean lastCharWasEscapeChar = false; - boolean isTripleQuoted = false; - StringBuilder res = new StringBuilder(sql.length()); - int index = 0; - while (index < sql.length()) { - char c = sql.charAt(index); - if (isInQuoted) { - if ((c == '\n' || c == '\r') && !isTripleQuoted) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql); - } else if (c == startQuote) { - if (lastCharWasEscapeChar) { - lastCharWasEscapeChar = false; - } else if (isTripleQuoted) { - if (sql.length() > index + 2 - && sql.charAt(index + 1) == startQuote - && sql.charAt(index + 2) == startQuote) { - isInQuoted = false; - startQuote = 0; - isTripleQuoted = false; - res.append(c).append(c); - index += 2; - } - } else { - isInQuoted = false; - startQuote = 0; - } - } else if (c == '\\') { - lastCharWasEscapeChar = true; - } else { - lastCharWasEscapeChar = false; - } - res.append(c); - } else { - // We are not in a quoted string. - if (isInSingleLineComment) { - if (c == '\n') { - isInSingleLineComment = false; - // Include the line feed in the result. - res.append(c); - } - } else if (isInMultiLineComment) { - if (sql.length() > index + 1 && c == ASTERIKS && sql.charAt(index + 1) == SLASH) { - isInMultiLineComment = false; - index++; - } - } else { - if (c == DASH - || (sql.length() > index + 1 && c == HYPHEN && sql.charAt(index + 1) == HYPHEN)) { - // This is a single line comment. - isInSingleLineComment = true; - } else if (sql.length() > index + 1 && c == SLASH && sql.charAt(index + 1) == ASTERIKS) { - isInMultiLineComment = true; - index++; - } else { - if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE || c == BACKTICK_QUOTE) { - isInQuoted = true; - startQuote = c; - // Check whether it is a triple-quote. - if (sql.length() > index + 2 - && sql.charAt(index + 1) == startQuote - && sql.charAt(index + 2) == startQuote) { - isTripleQuoted = true; - res.append(c).append(c); - index += 2; - } - } - res.append(c); - } - } - } - index++; - } - if (isInQuoted) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "SQL statement contains an unclosed literal: " + sql); - } - if (res.length() > 0 && res.charAt(res.length() - 1) == ';') { - res.deleteCharAt(res.length() - 1); - } - return res.toString().trim(); - } - - /** Removes any statement hints at the beginning of the statement. */ - static String removeStatementHint(String sql) { - // Valid statement hints at the beginning of a SQL statement can only contain a fixed set of - // possible values. Although it is possible to add a @{FORCE_INDEX=...} as a statement hint, the - // only allowed value is _BASE_TABLE. This means that we can safely assume that the statement - // hint will not contain any special characters, for example a closing curly brace, and - // that we can keep the check simple by just searching for the first occurrence of a closing - // curly brace at the end of the statement hint. - int startStatementHintIndex = sql.indexOf('{'); - int endStatementHintIndex = sql.indexOf('}'); - if (startStatementHintIndex == -1 || startStatementHintIndex > endStatementHintIndex) { - // Looks like an invalid statement hint. Just ignore at this point and let the caller handle - // the invalid query. - return sql; - } - return removeCommentsAndTrim(sql.substring(endStatementHintIndex + 1)); - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/StatementResult.java b/src/main/java/com/google/cloud/spanner/jdbc/StatementResult.java deleted file mode 100644 index 9820716a..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/StatementResult.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ResultSet; - -/** - * A result of the execution of a statement. Statements that are executed by the {@link - * Connection#execute(com.google.cloud.spanner.Statement)} method could have different types of - * return values. These are wrapped in a {@link StatementResult}. - */ -interface StatementResult { - - /** - * Enum indicating the type of result that was returned by {@link - * Connection#execute(com.google.cloud.spanner.Statement)} - */ - enum ResultType { - /** - * A result set either returned by a query on Cloud Spanner or a local result set generated by a - * client side statement. - */ - RESULT_SET, - /** An update count returned by Cloud Spanner. */ - UPDATE_COUNT, - /** - * DDL statements and client side statements that set the state of a connection return no - * result. - */ - NO_RESULT; - } - - /** The type of client side statement that was executed. */ - enum ClientSideStatementType { - SHOW_AUTOCOMMIT, - SET_AUTOCOMMIT, - SHOW_READONLY, - SET_READONLY, - SHOW_RETRY_ABORTS_INTERNALLY, - SET_RETRY_ABORTS_INTERNALLY, - SHOW_AUTOCOMMIT_DML_MODE, - SET_AUTOCOMMIT_DML_MODE, - SHOW_STATEMENT_TIMEOUT, - SET_STATEMENT_TIMEOUT, - SHOW_READ_TIMESTAMP, - SHOW_COMMIT_TIMESTAMP, - SHOW_READ_ONLY_STALENESS, - SET_READ_ONLY_STALENESS, - SHOW_OPTIMIZER_VERSION, - SET_OPTIMIZER_VERSION, - BEGIN, - COMMIT, - ROLLBACK, - SET_TRANSACTION_MODE, - START_BATCH_DDL, - START_BATCH_DML, - RUN_BATCH, - ABORT_BATCH; - } - - /** - * Returns the {@link ResultType} of this result. - * - * @return the result type. - */ - ResultType getResultType(); - - /** - * @return the {@link ClientSideStatementType} that was executed, or null if no such statement was - * executed. - */ - ClientSideStatementType getClientSideStatementType(); - - /** - * Returns the {@link ResultSet} held by this result. May only be called if the type of this - * result is {@link ResultType#RESULT_SET}. - * - * @return the {@link ResultSet} held by this result. - */ - ResultSet getResultSet(); - - /** - * Returns the update count held by this result. May only be called if the type of this result is - * {@link ResultType#UPDATE_COUNT}. - * - * @return the update count held by this result. - */ - Long getUpdateCount(); -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/StatementResultImpl.java b/src/main/java/com/google/cloud/spanner/jdbc/StatementResultImpl.java deleted file mode 100644 index 6748311e..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/StatementResultImpl.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.ResultSets; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Type.StructField; -import java.util.Arrays; - -/** Implementation of {@link StatementResult} */ -class StatementResultImpl implements StatementResult { - - /** {@link StatementResult} containing a {@link ResultSet} returned by Cloud Spanner. */ - static StatementResult of(ResultSet resultSet) { - return new StatementResultImpl(resultSet, null); - } - - /** - * {@link StatementResult} containing a {@link ResultSet} created by a {@link - * ClientSideStatement}. - */ - static StatementResult of(ResultSet resultSet, ClientSideStatementType clientSideStatementType) { - return new StatementResultImpl(resultSet, clientSideStatementType); - } - - /** {@link StatementResult} containing an update count returned by Cloud Spanner. */ - static StatementResult of(Long updateCount) { - return new StatementResultImpl(updateCount); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one BOOL column and one row that is created by a {@link ClientSideStatement}. - */ - static StatementResult resultSet( - String name, Boolean value, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.bool())), - Arrays.asList(Struct.newBuilder().set(name).to(value).build())), - clientSideStatementType); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one INT64 column and one row that is created by a {@link ClientSideStatement}. - */ - static StatementResult resultSet( - String name, Long value, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.int64())), - Arrays.asList(Struct.newBuilder().set(name).to(value).build())), - clientSideStatementType); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one ARRAY column and one row that is created by a {@link ClientSideStatement}. - */ - static StatementResult resultSet( - String name, long[] values, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.array(Type.int64()))), - Arrays.asList(Struct.newBuilder().set(name).toInt64Array(values).build())), - clientSideStatementType); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one STRING column and one row that is created by a {@link ClientSideStatement}. - */ - static StatementResult resultSet( - String name, String value, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.string())), - Arrays.asList(Struct.newBuilder().set(name).to(value).build())), - clientSideStatementType); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one STRING column containing an {@link Enum} value and one row that is created by a {@link - * ClientSideStatement}. - */ - static StatementResult resultSet( - String name, Enum value, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.string())), - Arrays.asList(Struct.newBuilder().set(name).to(value.toString()).build())), - clientSideStatementType); - } - - /** - * Convenience method for creating a {@link StatementResult} containing a {@link ResultSet} with - * one TIMESTAMP column and one row that is created by a {@link ClientSideStatement}. - */ - static StatementResult resultSet( - String name, Timestamp value, ClientSideStatementType clientSideStatementType) { - return of( - ResultSets.forRows( - Type.struct(StructField.of(name, Type.timestamp())), - Arrays.asList(Struct.newBuilder().set(name).to(value).build())), - clientSideStatementType); - } - - /** {@link StatementResult} containing no results. */ - static StatementResult noResult() { - return new StatementResultImpl((ClientSideStatementType) null); - } - - /** {@link StatementResult} containing no results created by a {@link ClientSideStatement}. */ - static StatementResult noResult(ClientSideStatementType clientSideStatementType) { - return new StatementResultImpl(clientSideStatementType); - } - - private final ResultType type; - private final ClientSideStatementType clientSideStatementType; - private final ResultSet resultSet; - private final Long updateCount; - - private StatementResultImpl( - ResultSet resultSet, ClientSideStatementType clientSideStatementType) { - this.type = ResultType.RESULT_SET; - this.clientSideStatementType = clientSideStatementType; - this.resultSet = resultSet; - this.updateCount = null; - } - - private StatementResultImpl(Long updateCount) { - this.type = ResultType.UPDATE_COUNT; - this.clientSideStatementType = null; - this.resultSet = null; - this.updateCount = updateCount; - } - - private StatementResultImpl(ClientSideStatementType clientSideStatementType) { - this.type = ResultType.NO_RESULT; - this.clientSideStatementType = clientSideStatementType; - this.resultSet = null; - this.updateCount = null; - } - - @Override - public ResultType getResultType() { - return type; - } - - @Override - public ClientSideStatementType getClientSideStatementType() { - return clientSideStatementType; - } - - @Override - public ResultSet getResultSet() { - ConnectionPreconditions.checkState( - resultSet != null, "This result does not contain a ResultSet"); - return resultSet; - } - - @Override - public Long getUpdateCount() { - ConnectionPreconditions.checkState( - updateCount != null, "This result does not contain an update count"); - return updateCount; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/TransactionMode.java b/src/main/java/com/google/cloud/spanner/jdbc/TransactionMode.java deleted file mode 100644 index ae98118c..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/TransactionMode.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -/** Enum used to define the transaction type of a {@link Connection} */ -enum TransactionMode { - READ_ONLY_TRANSACTION("READ ONLY"), - READ_WRITE_TRANSACTION("READ WRITE"); - - private final String statementString; - - private TransactionMode(String statement) { - this.statementString = statement; - } - - /** - * Use this method to get the correct format for use in a SQL statement. The SQL statement for - * setting the mode to read-only should for example be without the underscore: - * SET TRANSACTION READ ONLY - * - * @return a string representation of this {@link TransactionMode} that can be used in a SQL - * statement to set the transaction mode. - */ - public String getStatementString() { - return statementString; - } - - @Override - public String toString() { - return statementString; - } -} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/TransactionRetryListener.java b/src/main/java/com/google/cloud/spanner/jdbc/TransactionRetryListener.java index 546cd4f2..6d85dc7c 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/TransactionRetryListener.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/TransactionRetryListener.java @@ -16,24 +16,18 @@ package com.google.cloud.spanner.jdbc; +import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; import com.google.cloud.spanner.AbortedException; -/** - * Cloud Spanner can abort any read/write transaction because of potential deadlocks or other - * internal reasons. When a transaction is aborted, the entire transaction should be retried. A - * {@link Connection} can automatically retry a transaction internally and check whether the results - * that are returned during a retry attempt are equal to the results during the original - * transaction. This is done by keeping track of a SHA-256 checksum of all the results that are - * returned by Spanner during both transactions. - * - *

This listener class for internal transaction retries allow client applications to do - * additional testing or logging of transaction retries. Transaction retry listeners of a {@link - * Connection} can be added using {@link - * Connection#addTransactionRetryListener(TransactionRetryListener)}. - */ +/** Use {@link com.google.cloud.spanner.connection.TransactionRetryListener} */ +@InternalApi +@Deprecated public interface TransactionRetryListener { - /** The result of a retry. */ + /** Use {@link com.google.cloud.spanner.connection.TransactionRetryListener.RetryResult} */ + @InternalApi + @Deprecated public enum RetryResult { /** The retry executed successfully and the transaction will continue. */ RETRY_SUCCESSFUL, diff --git a/src/main/java/com/google/cloud/spanner/jdbc/UnitOfWork.java b/src/main/java/com/google/cloud/spanner/jdbc/UnitOfWork.java deleted file mode 100644 index 4287981d..00000000 --- a/src/main/java/com/google/cloud/spanner/jdbc/UnitOfWork.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.core.InternalApi; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadContext; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.spanner.v1.ResultSetStats; - -/** Internal interface for transactions and batches on {@link Connection}s. */ -@InternalApi -interface UnitOfWork { - - /** A unit of work can be either a transaction or a DDL/DML batch. */ - enum Type { - TRANSACTION, - BATCH; - } - - enum UnitOfWorkState { - STARTED, - COMMITTED, - COMMIT_FAILED, - ROLLED_BACK, - RAN, - RUN_FAILED, - ABORTED; - - public boolean isActive() { - return this == STARTED; - } - } - - /** Cancel the currently running statement (if any and the statement may be cancelled). */ - void cancel(); - - /** @return the type of unit of work. */ - Type getType(); - - /** @return the current state of this unit of work. */ - UnitOfWorkState getState(); - - /** @return true if this unit of work is still active. */ - boolean isActive(); - - /** - * Commits the changes in this unit of work to the database. For read-only transactions, this only - * closes the {@link ReadContext}. This method will throw a {@link SpannerException} if called for - * a {@link Type#BATCH}. - */ - void commit(); - - /** - * Rollbacks any changes in this unit of work. For read-only transactions, this only closes the - * {@link ReadContext}. This method will throw a {@link SpannerException} if called for a {@link - * Type#BATCH}. - */ - void rollback(); - - /** - * Sends the currently buffered statements in this unit of work to the database and ends the - * batch. This method will throw a {@link SpannerException} if called for a {@link - * Type#TRANSACTION}. - * - * @return the update counts in case of a DML batch. Returns an array containing 1 for each - * successful statement and 0 for each failed statement or statement that was not executed DDL - * in case of a DDL batch. - */ - long[] runBatch(); - - /** - * Clears the currently buffered statements in this unit of work and ends the batch. This method - * will throw a {@link SpannerException} if called for a {@link Type#TRANSACTION}. - */ - void abortBatch(); - - /** @return true if this unit of work is read-only. */ - boolean isReadOnly(); - - /** - * Executes a query with the given options. If {@link AnalyzeMode} is set to {@link - * AnalyzeMode#PLAN} or {@link AnalyzeMode#PROFILE}, the returned {@link ResultSet} will include - * {@link ResultSetStats}. - * - * @param statement The statement to execute. - * @param analyzeMode Indicates whether to include {@link ResultSetStats} in the returned {@link - * ResultSet} or not. Cannot be used in combination with {@link QueryOption}s. - * @param options the options to configure the query. May only be set if analyzeMode is set to - * {@link AnalyzeMode#NONE}. - * @return a {@link ResultSet} with the results of the query. - * @throws SpannerException if the query is not allowed on this {@link UnitOfWork}, or if a - * database error occurs. - */ - ResultSet executeQuery( - ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options); - - /** - * @return the read timestamp of this transaction. Will throw a {@link SpannerException} if there - * is no read timestamp. - */ - Timestamp getReadTimestamp(); - - /** @return the read timestamp of this transaction or null if there is no read timestamp. */ - Timestamp getReadTimestampOrNull(); - - /** - * @return the commit timestamp of this transaction. Will throw a {@link SpannerException} if - * there is no commit timestamp. - */ - Timestamp getCommitTimestamp(); - - /** @return the commit timestamp of this transaction or null if there is no commit timestamp. */ - Timestamp getCommitTimestampOrNull(); - - /** - * Executes the specified DDL statements in this unit of work. For DDL batches, this will mean - * that the statements are buffered locally and will be sent to Spanner when {@link - * UnitOfWork#commit()} is called. For {@link SingleUseTransaction}s, this will execute the DDL - * statement directly on Spanner. - * - * @param ddl The DDL statement to execute. - */ - void executeDdl(ParsedStatement ddl); - - /** - * Execute a DML statement on Spanner. - * - * @param update The DML statement to execute. - * @return the number of records that were inserted/updated/deleted by this statement. - */ - long executeUpdate(ParsedStatement update); - - /** - * Execute a batch of DML statements on Spanner. - * - * @param updates The DML statements to execute. - * @return an array containing the number of records that were inserted/updated/deleted per - * statement. - * @see TransactionContext#batchUpdate(Iterable) - */ - long[] executeBatchUpdate(Iterable updates); - - /** - * Writes a {@link Mutation} to Spanner. For {@link ReadWriteTransaction}s, this means buffering - * the {@link Mutation} locally and writing the {@link Mutation} to Spanner upon {@link - * UnitOfWork#commit()}. For {@link SingleUseTransaction}s, the {@link Mutation} will be sent - * directly to Spanner. - * - * @param mutation The mutation to write. - */ - void write(Mutation mutation); - - /** - * Writes a batch of {@link Mutation}s to Spanner. For {@link ReadWriteTransaction}s, this means - * buffering the {@link Mutation}s locally and writing the {@link Mutation}s to Spanner upon - * {@link UnitOfWork#commit()}. For {@link SingleUseTransaction}s, the {@link Mutation}s will be - * sent directly to Spanner. - * - * @param mutations The mutations to write. - */ - void write(Iterable mutations); -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbortedTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbortedTest.java deleted file mode 100644 index d10508bb..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AbortedTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest.AbortInterceptor; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest.ITConnection; -import com.google.cloud.spanner.jdbc.it.ITTransactionRetryTest.CountTransactionRetryListener; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class AbortedTest extends AbstractMockServerTest { - - @Test - public void testCommitAborted() { - // Do two iterations to ensure that each iteration gets its own transaction, and that each - // transaction is the most recent transaction of that session. - for (int i = 0; i < 2; i++) { - mockSpanner.putStatementResult( - StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT)); - mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, UPDATE_COUNT)); - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // do an insert - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // do a commit that will first abort, and then on retry will succeed - connection.commit(); - mockSpanner.putStatementResult( - StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_AFTER_INSERT)); - // verify that the insert succeeded - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbstractConnectionImplTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractConnectionImplTest.java deleted file mode 100644 index f12f5e88..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AbstractConnectionImplTest.java +++ /dev/null @@ -1,963 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.getTimeUnitAbbreviation; -import static com.google.cloud.spanner.jdbc.SpannerExceptionMatcher.matchCode; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * This test class and all its subclasses are used to generate the file - * ConnectionImplGeneratedSqlScriptTest.sql. - */ -@RunWith(JUnit4.class) -public abstract class AbstractConnectionImplTest { - public static final String UPDATE = "UPDATE foo SET bar=1"; - public static final String SELECT = "SELECT 1 AS TEST"; - public static final String DDL = - "CREATE TABLE foo (id INT64 NOT NULL, name STRING(100)) PRIMARY KEY (id)"; - - static interface ConnectionConsumer { - void accept(Connection connection); - } - - @Rule public ExpectedException exception = ExpectedException.none(); - - /** - * This test class can generate a large sql file that represents all the statements and - * verifications that are executed by this test class. This file can be fed into other test cases - * (in other programming languages) to execute the same tests as the tests covered by all the - * subclasses of {@link AbstractConnectionImplTest}. - */ - private static final String LOG_FILE = - "src/test/resources/com/google/cloud/spanner/jdbc/ConnectionImplGeneratedSqlScriptTest.sql"; - - private static final String DO_LOG_PROPERTY = "do_log_statements"; - private static boolean doLog; - private static PrintWriter writer; - - abstract Connection getConnection(); - - static void expectSpannerException( - String reason, ConnectionConsumer consumer, Connection connection) { - expectSpannerException(reason, consumer, connection, ErrorCode.FAILED_PRECONDITION); - } - - static void expectSpannerException( - String reason, ConnectionConsumer consumer, Connection connection, ErrorCode errorCode) { - SpannerException exception = null; - try { - consumer.accept(connection); - } catch (SpannerException e) { - exception = e; - } - assertThat(reason, exception, is(notNullValue())); - assertThat(reason, exception.getErrorCode(), is(equalTo(errorCode))); - } - - AbstractConnectionImplTest() {} - - /** Makes an empty test script. Can be called before a new script is to be generated. */ - static void emptyScript() { - openLog(false); - closeLog(); - } - - void log(String statement) { - if (doLog) { - writer.println(statement); - } - } - - @BeforeClass - public static void openLog() { - doLog = Boolean.valueOf(System.getProperty(DO_LOG_PROPERTY, "false")); - if (doLog) { - openLog(true); - } else { - writer = null; - } - } - - private static void openLog(boolean append) { - try { - writer = - new PrintWriter( - new OutputStreamWriter(new FileOutputStream(LOG_FILE, append), "UTF8"), true); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @AfterClass - public static void closeLog() { - if (writer != null) { - writer.close(); - } - } - - @Test - public void testClose() { - getConnection().close(); - } - - @Test - public void testIsClosed() { - Connection connection = getConnection(); - assertThat(connection.isClosed(), is(false)); - connection.close(); - assertThat(connection.isClosed(), is(true)); - } - - abstract boolean isSetAutocommitAllowed(); - - @Test - public void testSetAutocommit() { - try (Connection connection = getConnection()) { - if (isSetAutocommitAllowed()) { - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - - log("@EXPECT RESULT_SET 'AUTOCOMMIT',FALSE"); - log("SHOW VARIABLE AUTOCOMMIT;"); - assertThat(connection.isAutocommit(), is(false)); - - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - - log("@EXPECT RESULT_SET 'AUTOCOMMIT',TRUE"); - log("SHOW VARIABLE AUTOCOMMIT;"); - assertThat(connection.isAutocommit(), is(true)); - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SET AUTOCOMMIT=" + (connection.isAutocommit() ? "FALSE;" : "TRUE;")); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.setAutocommit(!connection.isAutocommit()); - } - } - } - - abstract boolean isSetReadOnlyAllowed(); - - @Test - public void testSetReadOnly() { - try (Connection connection = getConnection()) { - if (isSetReadOnlyAllowed()) { - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - - log("@EXPECT RESULT_SET 'READONLY',FALSE"); - log("SHOW VARIABLE READONLY;"); - assertThat(connection.isReadOnly(), is(false)); - - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - - log("@EXPECT RESULT_SET 'READONLY',TRUE"); - log("SHOW VARIABLE READONLY;"); - assertThat(connection.isReadOnly(), is(true)); - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SET READONLY=" + (connection.isAutocommit() ? "FALSE;" : "TRUE;")); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.setReadOnly(!connection.isReadOnly()); - } - } - } - - @Test - public void testSetStatementTimeout() { - try (Connection connection = getConnection()) { - for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { - log(String.format("SET STATEMENT_TIMEOUT='1%s';", getTimeUnitAbbreviation(unit))); - connection.setStatementTimeout(1L, unit); - - log( - String.format( - "@EXPECT RESULT_SET 'STATEMENT_TIMEOUT','1%s'", getTimeUnitAbbreviation(unit))); - log("SHOW VARIABLE STATEMENT_TIMEOUT;"); - assertThat(connection.getStatementTimeout(unit), is(equalTo(1L))); - - log("SET STATEMENT_TIMEOUT=null;"); - connection.clearStatementTimeout(); - - log("@EXPECT RESULT_SET 'STATEMENT_TIMEOUT',null"); - log("SHOW VARIABLE STATEMENT_TIMEOUT;"); - assertThat(connection.getStatementTimeout(unit), is(equalTo(0L))); - assertThat(connection.hasStatementTimeout(), is(false)); - boolean gotException = false; - try { - log("@EXPECT EXCEPTION INVALID_ARGUMENT"); - log(String.format("SET STATEMENT_TIMEOUT='0%s';", getTimeUnitAbbreviation(unit))); - connection.setStatementTimeout(0L, unit); - } catch (IllegalArgumentException e) { - gotException = true; - } - assertThat(gotException, is(true)); - log("@EXPECT RESULT_SET 'STATEMENT_TIMEOUT',null"); - log("SHOW VARIABLE STATEMENT_TIMEOUT;"); - assertThat(connection.getStatementTimeout(unit), is(equalTo(0L))); - assertThat(connection.hasStatementTimeout(), is(false)); - } - } - } - - abstract boolean isStartBatchDmlAllowed(); - - @Test - public void testStartBatchDml() { - try (Connection connection = getConnection()) { - if (isStartBatchDmlAllowed()) { - assertThat(connection.isReadOnly(), is(false)); - assertThat(connection.isDdlBatchActive() || connection.isDmlBatchActive(), is(false)); - - log("START BATCH DML;"); - connection.startBatchDml(); - assertThat(connection.isDmlBatchActive(), is(true)); - - expectSpannerException( - "Select should not be allowed after startBatchDml()", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(SELECT + ";"); - t.execute(Statement.of(SELECT)); - } - }, - connection); - expectSpannerException( - "DDL should not be allowed after startBatchDml()", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(DDL + ";"); - t.execute(Statement.of(DDL)); - } - }, - connection); - log(UPDATE + ";"); - connection.execute(Statement.of(UPDATE)); - assertThat(connection.isDmlBatchActive(), is(true)); - } - // startBatchDml is not allowed as a batch has already been started. - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("START BATCH DML;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.startBatchDml(); - } - } - - abstract boolean isStartBatchDdlAllowed(); - - @Test - public void testStartBatchDdl() { - try (Connection connection = getConnection()) { - if (isStartBatchDdlAllowed()) { - assertThat(connection.isTransactionStarted(), is(false)); - assertThat(connection.isInTransaction(), is(equalTo(!connection.isAutocommit()))); - assertThat(connection.isDdlBatchActive() || connection.isDmlBatchActive(), is(false)); - - log("START BATCH DDL;"); - connection.startBatchDdl(); - assertThat(connection.isTransactionStarted(), is(false)); - assertThat(connection.isInTransaction(), is(false)); - assertThat(connection.isDdlBatchActive(), is(true)); - - expectSpannerException( - "Select should not be allowed after startBatchDdl()", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(SELECT + ";"); - t.execute(Statement.of(SELECT)); - } - }, - connection); - expectSpannerException( - "Update should not be allowed after startBatchDdl()", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(UPDATE + ";"); - t.execute(Statement.of(UPDATE)); - } - }, - connection); - log(DDL + ";"); - connection.execute(Statement.of(DDL)); - assertThat(connection.isTransactionStarted(), is(false)); - assertThat(connection.isDdlBatchActive(), is(true)); - } - // startBatchDdl is no longer allowed as a batch has already been started - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("START BATCH DDL;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.startBatchDdl(); - } - } - - abstract boolean isRunBatchAllowed(); - - @Test - public void testRunBatch() { - try (Connection connection = getConnection()) { - if (!isRunBatchAllowed()) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - log("RUN BATCH;"); - connection.runBatch(); - } - } - - abstract boolean isAbortBatchAllowed(); - - @Test - public void testAbortBatch() { - try (Connection connection = getConnection()) { - if (!isAbortBatchAllowed()) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - log("ABORT BATCH;"); - connection.abortBatch(); - } - } - - abstract boolean isBeginTransactionAllowed(); - - abstract boolean isSelectAllowedAfterBeginTransaction(); - - abstract boolean isDmlAllowedAfterBeginTransaction(); - - abstract boolean isDdlAllowedAfterBeginTransaction(); - - @Test - public void testBeginTransaction() { - try (Connection connection = getConnection()) { - if (isBeginTransactionAllowed()) { - assertThat(connection.isTransactionStarted(), is(false)); - assertThat(connection.isInTransaction(), is(equalTo(!connection.isAutocommit()))); - - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - assertThat(connection.isTransactionStarted(), is(false)); - assertThat(connection.isInTransaction(), is(true)); - - if (isSelectAllowedAfterBeginTransaction()) { - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)); - } else { - expectSpannerException( - "Select should not be allowed after beginTransaction", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(SELECT + ";"); - t.execute(Statement.of(SELECT)); - } - }, - connection); - } - if (isDmlAllowedAfterBeginTransaction()) { - log(UPDATE + ";"); - connection.execute(Statement.of(UPDATE)); - } else { - expectSpannerException( - "Update should not be allowed after beginTransaction", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(UPDATE + ";"); - t.execute(Statement.of(UPDATE)); - } - }, - connection); - } - if (isDdlAllowedAfterBeginTransaction()) { - log(DDL + ";"); - connection.execute(Statement.of(DDL)); - } else { - expectSpannerException( - "DDL should not be allowed after beginTransaction", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(DDL + ";"); - t.execute(Statement.of(DDL)); - } - }, - connection); - } - assertThat(connection.isTransactionStarted(), is(true)); - } - // beginTransaction is no longer allowed as the transaction has already started - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("BEGIN TRANSACTION;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.beginTransaction(); - } - } - - abstract boolean isSetTransactionModeAllowed(TransactionMode mode); - - @Test - public void testSetTransactionMode() { - for (TransactionMode mode : TransactionMode.values()) { - testSetTransactionMode(mode); - } - } - - private void testSetTransactionMode(final TransactionMode mode) { - try (Connection connection = getConnection()) { - if (isSetTransactionModeAllowed(mode)) { - log("SET TRANSACTION " + mode.toString() + ";"); - connection.setTransactionMode(mode); - assertThat(connection.getTransactionMode(), is(equalTo(mode))); - } else { - expectSpannerException( - mode + " should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SET TRANSACTION " + mode.getStatementString() + ";"); - t.setTransactionMode(mode); - } - }, - connection); - } - } - } - - abstract boolean isGetTransactionModeAllowed(); - - @Test - public void testGetTransactionMode() { - try (Connection connection = getConnection()) { - if (isGetTransactionModeAllowed()) { - assertThat(connection.getTransactionMode(), is(notNullValue())); - } else { - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getTransactionMode(); - } - } - } - - abstract boolean isSetAutocommitDmlModeAllowed(); - - @Test - public void testSetAutocommitDmlMode() { - try (Connection connection = getConnection()) { - if (isSetAutocommitDmlModeAllowed()) { - for (AutocommitDmlMode mode : AutocommitDmlMode.values()) { - log("SET AUTOCOMMIT_DML_MODE='" + mode.toString() + "';"); - connection.setAutocommitDmlMode(mode); - - log("@EXPECT RESULT_SET 'AUTOCOMMIT_DML_MODE','" + mode.toString() + "'"); - log("SHOW VARIABLE AUTOCOMMIT_DML_MODE;"); - assertThat(connection.getAutocommitDmlMode(), is(equalTo(mode))); - } - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log( - "SET AUTOCOMMIT_DML_MODE='" - + AutocommitDmlMode.PARTITIONED_NON_ATOMIC.toString() - + "';"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - } - } - } - - abstract boolean isGetAutocommitDmlModeAllowed(); - - @Test - public void testGetAutocommitDmlMode() { - try (Connection connection = getConnection()) { - if (isGetAutocommitDmlModeAllowed()) { - log("@EXPECT RESULT_SET 'AUTOCOMMIT_DML_MODE'"); - log("SHOW VARIABLE AUTOCOMMIT_DML_MODE;"); - assertThat(connection.getAutocommitDmlMode(), is(notNullValue())); - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SHOW VARIABLE AUTOCOMMIT_DML_MODE;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getAutocommitDmlMode(); - } - } - } - - abstract boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode); - - @Test - public void testSetReadOnlyStaleness() { - for (TimestampBound staleness : getTestTimestampBounds()) { - testSetReadOnlyStaleness(staleness); - } - } - - private List getTestTimestampBounds() { - return Arrays.asList( - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(Timestamp.now()), - TimestampBound.ofMinReadTimestamp(Timestamp.now()), - TimestampBound.ofExactStaleness(1L, TimeUnit.SECONDS), - TimestampBound.ofMaxStaleness(100L, TimeUnit.MILLISECONDS), - TimestampBound.ofExactStaleness(100L, TimeUnit.MICROSECONDS)); - } - - private void testSetReadOnlyStaleness(final TimestampBound staleness) { - try (Connection connection = getConnection()) { - if (isSetReadOnlyStalenessAllowed(staleness.getMode())) { - log( - "SET READ_ONLY_STALENESS='" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "';"); - connection.setReadOnlyStaleness(staleness); - - log( - "@EXPECT RESULT_SET 'READ_ONLY_STALENESS','" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "'"); - log("SHOW VARIABLE READ_ONLY_STALENESS;"); - assertThat(connection.getReadOnlyStaleness(), is(equalTo(staleness))); - } else { - expectSpannerException( - staleness.getMode() + " should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log( - "SET READ_ONLY_STALENESS='" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "';"); - t.setReadOnlyStaleness(staleness); - } - }, - connection); - } - } - } - - abstract boolean isGetReadOnlyStalenessAllowed(); - - @Test - public void testGetReadOnlyStaleness() { - try (Connection connection = getConnection()) { - if (isGetReadOnlyStalenessAllowed()) { - log("@EXPECT RESULT_SET 'READ_ONLY_STALENESS'"); - log("SHOW VARIABLE READ_ONLY_STALENESS;"); - assertThat(connection.getReadOnlyStaleness(), is(notNullValue())); - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SHOW VARIABLE READ_ONLY_STALENESS;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getReadOnlyStaleness(); - } - } - } - - boolean isSetOptimizerVersionAllowed() { - return !getConnection().isClosed(); - } - - @Test - public void testSetOptimizerVersion() { - try (Connection connection = getConnection()) { - if (isSetOptimizerVersionAllowed()) { - for (String version : new String[] {"1", "2", "latest", ""}) { - log("SET OPTIMIZER_VERSION='" + version + "';"); - connection.setOptimizerVersion(version); - - log("@EXPECT RESULT_SET 'OPTIMIZER_VERSION','" + version + "'"); - log("SHOW VARIABLE OPTIMIZER_VERSION;"); - assertThat(connection.getOptimizerVersion(), is(equalTo(version))); - } - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SET OPTIMIZER_VERSION='1';"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.setOptimizerVersion("1"); - } - } - } - - boolean isGetOptimizerVersionAllowed() { - return !getConnection().isClosed(); - } - - @Test - public void testGetOptimizerVersion() { - try (Connection connection = getConnection()) { - if (isGetOptimizerVersionAllowed()) { - log("@EXPECT RESULT_SET 'OPTIMIZER_VERSION'"); - log("SHOW VARIABLE OPTIMIZER_VERSION;"); - assertThat(connection.getOptimizerVersion(), is(notNullValue())); - } else { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log("SHOW VARIABLE OPTIMIZER_VERSION;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getOptimizerVersion(); - } - } - } - - abstract boolean isCommitAllowed(); - - @Test - public void testCommit() { - try (Connection connection = getConnection()) { - if (!isCommitAllowed()) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - log("COMMIT;"); - connection.commit(); - } - } - - abstract boolean isRollbackAllowed(); - - @Test - public void testRollback() { - try (Connection connection = getConnection()) { - if (!isRollbackAllowed()) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - log("ROLLBACK;"); - connection.rollback(); - } - } - - abstract boolean expectedIsInTransaction(); - - @Test - public void testIsInTransaction() { - try (Connection connection = getConnection()) { - assertThat(connection.isInTransaction(), is(expectedIsInTransaction())); - } - } - - abstract boolean expectedIsTransactionStarted(); - - @Test - public void testIsTransactionStarted() { - try (Connection connection = getConnection()) { - assertThat(connection.isTransactionStarted(), is(expectedIsTransactionStarted())); - } - } - - abstract boolean isGetReadTimestampAllowed(); - - @Test - public void testGetReadTimestamp() { - try (Connection connection = getConnection()) { - if (isGetReadTimestampAllowed()) { - log("@EXPECT RESULT_SET 'READ_TIMESTAMP'"); - log("SHOW VARIABLE READ_TIMESTAMP;"); - assertThat(connection.getReadTimestamp(), is(notNullValue())); - } else { - log("@EXPECT RESULT_SET 'READ_TIMESTAMP',null"); - log("SHOW VARIABLE READ_TIMESTAMP;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getReadTimestamp(); - } - } - } - - abstract boolean isGetCommitTimestampAllowed(); - - @Test - public void testGetCommitTimestamp() { - try (Connection connection = getConnection()) { - if (isGetCommitTimestampAllowed()) { - log("@EXPECT RESULT_SET 'COMMIT_TIMESTAMP'"); - log("SHOW VARIABLE COMMIT_TIMESTAMP;"); - assertThat(connection.getCommitTimestamp(), is(notNullValue())); - } else { - log("@EXPECT RESULT_SET 'COMMIT_TIMESTAMP',null"); - log("SHOW VARIABLE COMMIT_TIMESTAMP;"); - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.getCommitTimestamp(); - } - } - } - - abstract boolean isExecuteAllowed(StatementType type); - - @Test - public void testExecute() { - for (StatementType type : - new StatementType[] {StatementType.QUERY, StatementType.UPDATE, StatementType.DDL}) { - testExecute(type); - } - } - - private void testExecute(final StatementType type) { - try (Connection connection = getConnection()) { - if (isExecuteAllowed(type)) { - log(getTestStatement(type).getSql() + ";"); - assertThat(connection.execute(getTestStatement(type)), is(notNullValue())); - } else { - expectSpannerException( - type + " should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(getTestStatement(type).getSql() + ";"); - t.execute(getTestStatement(type)); - } - }, - connection); - } - } - } - - private Statement getTestStatement(StatementType type) { - switch (type) { - case QUERY: - return Statement.of(SELECT); - case UPDATE: - return Statement.of(UPDATE); - case DDL: - return Statement.of(DDL); - case CLIENT_SIDE: - case UNKNOWN: - default: - throw new IllegalArgumentException("Unsupported type: " + type); - } - } - - @Test - public void testExecuteQuery() { - for (StatementType type : - new StatementType[] {StatementType.QUERY, StatementType.UPDATE, StatementType.DDL}) { - testExecuteQuery(type); - } - } - - private void testExecuteQuery(final StatementType type) { - try (Connection connection = getConnection()) { - if (type == StatementType.QUERY && isExecuteAllowed(StatementType.QUERY)) { - log("@EXPECT RESULT_SET 'TEST',1"); - log(getTestStatement(type).getSql() + ";"); - ResultSet rs = connection.executeQuery(getTestStatement(type)); - assertThat(rs, is(notNullValue())); - assertThat(rs.getStats(), is(nullValue())); - } else if (type == StatementType.QUERY) { - // it is a query, but queries are not allowed for this connection state - expectSpannerException( - type + " should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(getTestStatement(type).getSql() + ";"); - t.executeQuery(getTestStatement(type)); - } - }, - connection, - ErrorCode.FAILED_PRECONDITION); - } else { - expectSpannerException( - type + " should be an invalid argument", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.executeQuery(getTestStatement(type)); - } - }, - connection, - ErrorCode.INVALID_ARGUMENT); - } - } - } - - @Test - public void testAnalyzeQuery() { - for (StatementType type : - new StatementType[] {StatementType.QUERY, StatementType.UPDATE, StatementType.DDL}) { - testAnalyzeQuery(type); - } - } - - private void testAnalyzeQuery(final StatementType type) { - // TODO: add log statements when ANALYZE ... sql statements are supported - try (Connection connection = getConnection()) { - for (QueryAnalyzeMode mode : QueryAnalyzeMode.values()) { - final QueryAnalyzeMode currentMode = mode; - if (type == StatementType.QUERY && isExecuteAllowed(StatementType.QUERY)) { - ResultSet rs = connection.analyzeQuery(getTestStatement(type), currentMode); - assertThat(rs, is(notNullValue())); - while (rs.next()) {} - assertThat(rs.getStats(), is(notNullValue())); - } else if (type == StatementType.QUERY) { - // it is a query, but queries are not allowed for this connection state - expectSpannerException( - type + " should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.analyzeQuery(getTestStatement(type), currentMode); - } - }, - connection, - ErrorCode.FAILED_PRECONDITION); - } else { - expectSpannerException( - type + " should be an invalid argument", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.analyzeQuery(getTestStatement(type), currentMode); - } - }, - connection, - ErrorCode.INVALID_ARGUMENT); - } - } - } - } - - @Test - public void testExecuteUpdate() { - for (StatementType type : - new StatementType[] {StatementType.QUERY, StatementType.UPDATE, StatementType.DDL}) { - testExecuteUpdate(type); - } - } - - private void testExecuteUpdate(final StatementType type) { - try (Connection connection = getConnection()) { - if (type == StatementType.UPDATE && isExecuteAllowed(StatementType.UPDATE)) { - log("@EXPECT UPDATE_COUNT 1"); - log(getTestStatement(type).getSql() + ";"); - assertThat(connection.executeUpdate(getTestStatement(type)), is(notNullValue())); - } else if (type == StatementType.UPDATE) { - // it is an update statement, but updates are not allowed for this connection state - expectSpannerException( - type + "should not be allowed", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - log("@EXPECT EXCEPTION FAILED_PRECONDITION"); - log(getTestStatement(type).getSql() + ";"); - t.executeUpdate(getTestStatement(type)); - } - }, - connection, - ErrorCode.FAILED_PRECONDITION); - } else { - expectSpannerException( - type + " should be an invalid argument", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.executeUpdate(getTestStatement(type)); - } - }, - connection, - ErrorCode.INVALID_ARGUMENT); - } - } - } - - abstract boolean isWriteAllowed(); - - @Test - public void testWrite() { - try (Connection connection = getConnection()) { - if (!isWriteAllowed() || !connection.isAutocommit()) { - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - connection.write(createTestMutation()); - } - } - - @Test - public void testWriteIterable() { - try (Connection connection = getConnection()) { - if (!isWriteAllowed() || !connection.isAutocommit()) { - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - connection.write(Arrays.asList(createTestMutation())); - } - } - - @Test - public void testBufferedWrite() { - try (Connection connection = getConnection()) { - if (!isWriteAllowed() || connection.isAutocommit()) { - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - connection.bufferedWrite(createTestMutation()); - } - } - - @Test - public void testBufferedWriteIterable() { - try (Connection connection = getConnection()) { - if (!isWriteAllowed() || connection.isAutocommit()) { - exception.expect(matchCode(ErrorCode.FAILED_PRECONDITION)); - } - connection.bufferedWrite(Arrays.asList(createTestMutation())); - } - } - - private Mutation createTestMutation() { - return Mutation.newInsertBuilder("foo").set("id").to(1L).set("name").to("bar").build(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbstractMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractMockServerTest.java deleted file mode 100644 index 4117eed2..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AbstractMockServerTest.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.MockSpannerServiceImpl; -import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.admin.database.v1.MockDatabaseAdminImpl; -import com.google.cloud.spanner.admin.instance.v1.MockInstanceAdminImpl; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest.AbortInterceptor; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest.ITConnection; -import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ListValue; -import com.google.protobuf.Value; -import com.google.spanner.v1.ExecuteSqlRequest; -import com.google.spanner.v1.ResultSetMetadata; -import com.google.spanner.v1.StructType; -import com.google.spanner.v1.StructType.Field; -import com.google.spanner.v1.Type; -import com.google.spanner.v1.TypeCode; -import io.grpc.Server; -import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public abstract class AbstractMockServerTest { - static final long COUNT_BEFORE_INSERT = 0L; - static final long COUNT_AFTER_INSERT = 1L; - static final Statement SELECT_COUNT_STATEMENT = - Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"); - private static final ResultSetMetadata SELECT_COUNT_METADATA = - ResultSetMetadata.newBuilder() - .setRowType( - StructType.newBuilder() - .addFields( - Field.newBuilder() - .setName("C") - .setType(Type.newBuilder().setCode(TypeCode.INT64).build()) - .build()) - .build()) - .build(); - static final com.google.spanner.v1.ResultSet SELECT_COUNT_RESULTSET_BEFORE_INSERT = - com.google.spanner.v1.ResultSet.newBuilder() - .addRows( - ListValue.newBuilder() - .addValues( - Value.newBuilder() - .setStringValue(String.valueOf(COUNT_BEFORE_INSERT)) - .build()) - .build()) - .setMetadata(SELECT_COUNT_METADATA) - .build(); - static final com.google.spanner.v1.ResultSet SELECT_COUNT_RESULTSET_AFTER_INSERT = - com.google.spanner.v1.ResultSet.newBuilder() - .addRows( - ListValue.newBuilder() - .addValues( - Value.newBuilder().setStringValue(String.valueOf(COUNT_AFTER_INSERT)).build()) - .build()) - .setMetadata(SELECT_COUNT_METADATA) - .build(); - static final Statement INSERT_STATEMENT = - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')"); - static final int UPDATE_COUNT = 1; - - static MockSpannerServiceImpl mockSpanner; - static MockInstanceAdminImpl mockInstanceAdmin; - static MockDatabaseAdminImpl mockDatabaseAdmin; - private static Server server; - private static InetSocketAddress address; - - @BeforeClass - public static void startStaticServer() throws IOException { - mockSpanner = new MockSpannerServiceImpl(); - mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. - mockInstanceAdmin = new MockInstanceAdminImpl(); - mockDatabaseAdmin = new MockDatabaseAdminImpl(); - address = new InetSocketAddress("localhost", 0); - server = - NettyServerBuilder.forAddress(address) - .addService(mockSpanner) - .addService(mockInstanceAdmin) - .addService(mockDatabaseAdmin) - .build() - .start(); - mockSpanner.putStatementResult( - StatementResult.query(SELECT_COUNT_STATEMENT, SELECT_COUNT_RESULTSET_BEFORE_INSERT)); - mockSpanner.putStatementResult(StatementResult.update(INSERT_STATEMENT, UPDATE_COUNT)); - } - - @AfterClass - public static void stopServer() throws Exception { - SpannerPool.closeSpannerPool(); - server.shutdown(); - server.awaitTermination(); - } - - @Before - public void setupResults() { - mockSpanner.reset(); - } - - @After - public void closeSpannerPool() { - SpannerPool.closeSpannerPool(); - } - - java.sql.Connection createJdbcConnection() throws SQLException { - return DriverManager.getConnection("jdbc:" + getBaseUrl()); - } - - ITConnection createConnection() { - return createConnection( - Collections.emptyList(), - Collections.emptyList()); - } - - ITConnection createConnection( - AbortInterceptor interceptor, TransactionRetryListener transactionRetryListener) { - return createConnection( - Arrays.asList(interceptor), - Arrays.asList(transactionRetryListener)); - } - - ITConnection createConnection( - List interceptors, - List transactionRetryListeners) { - StringBuilder url = new StringBuilder(getBaseUrl()); - ConnectionOptions.Builder builder = - ConnectionOptions.newBuilder() - .setUri(url.toString()) - .setStatementExecutionInterceptors(interceptors); - ConnectionOptions options = builder.build(); - ITConnection connection = createITConnection(options); - for (TransactionRetryListener listener : transactionRetryListeners) { - connection.addTransactionRetryListener(listener); - } - return connection; - } - - String getBaseUrl() { - return String.format( - "cloudspanner://localhost:%d/projects/proj/instances/inst/databases/db?usePlainText=true;autocommit=false;retryAbortsInternally=true", - server.getPort()); - } - - ExecuteSqlRequest getLastExecuteSqlRequest() { - List requests = mockSpanner.getRequests(); - for (int i = requests.size() - 1; i >= 0; i--) { - if (requests.get(i) instanceof ExecuteSqlRequest) { - return (ExecuteSqlRequest) requests.get(i); - } - } - throw new IllegalStateException("No ExecuteSqlRequest found in requests"); - } - - private ITConnection createITConnection(ConnectionOptions options) { - return new ITConnectionImpl(options); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbstractSqlScriptVerifier.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractSqlScriptVerifier.java deleted file mode 100644 index 3e854d3e..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AbstractSqlScriptVerifier.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Scanner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Base class for SQL Script verifiers for both the generic Connection API and JDBC connections - * - *

Simple parser/verifier for sql statements. This verifier is able to parse additional @EXPECT - * statements that defines the expected behavior of a sql statement. Possible uses are: - * - *

    - *
  • @EXPECT NO_RESULT: The following statement should not return a result (no {@link ResultSet} - * and no update count) - *
  • @EXPECT UPDATE_COUNT count: The following statement should return the specified - * update count - *
  • @EXPECT RESULT_SET: The following statement should return a {@link ResultSet} with two - * columns with the names ACTUAL and EXPECTED and containing at least one row. For each row, - * the values of ACTUAL and EXPECTED must be equal - *
  • @EXPECT RESULT_SET 'columnName': The following statement should return a {@link ResultSet} - * with a column with the specified name and containing at least one row (additional columns - * in the {@link ResultSet} are allowed). For each row, the value of the column must be not - * null - *
  • @EXPECT RESULT_SET 'columnName',value: The following statement should return a {@link - * ResultSet} with a column with the specified name and containing at least one row - * (additional columns in the {@link ResultSet} are allowed). For each row, the value of the - * column must be equal to the specified value - *
  • @EXPECT EXCEPTION code ['messagePrefix']: The following statement should throw a {@link - * SpannerException} with the specified code and starting with the (optional) message prefix - *
  • @EXPECT EQUAL 'variable1','variable2': The values of the two given variables should be - * equal. The value of a variable can be set using a @PUT statement. - *
- * - * The parser can set a temporary variable value using a @PUT statement: - * @PUT 'variable_name'\nSQL statement The SQL statement must be a statement that returns a - * {@link ResultSet} containing exactly one row and one column. - * - *

In addition the verifier can create new connections if the script contains NEW_CONNECTION; - * statements and the verifier has been created with a {@link GenericConnectionProvider}. See {@link - * ConnectionImplGeneratedSqlScriptTest} for an example for this. - */ -public abstract class AbstractSqlScriptVerifier { - private static final Pattern VERIFY_PATTERN = - Pattern.compile( - "(?is)\\s*(?:@EXPECT)\\s+" - + "(?NO_RESULT" - + "|RESULT_SET\\s*(?'.*?'(?,.*?)?)?" - + "|UPDATE_COUNT\\s*(?-?\\d{1,19})" - + "|EXCEPTION\\s*(?(?CANCELLED|UNKNOWN|INVALID_ARGUMENT|DEADLINE_EXCEEDED|NOT_FOUND|ALREADY_EXISTS|PERMISSION_DENIED|UNAUTHENTICATED|RESOURCE_EXHAUSTED|FAILED_PRECONDITION|ABORTED|OUT_OF_RANGE|UNIMPLEMENTED|INTERNAL|UNAVAILABLE|DATA_LOSS)(?:\\s*)(?'.*?')?)" - + "|EQUAL\\s+(?'.+?')\\s*,\\s*(?'.+?')" - + ")" - + "(\\n(?.*))?"); - - private static final String PUT_CONDITION = - "@PUT can only be used in combination with a statement that returns a" - + " result set containing exactly one row and one column"; - private static final Pattern PUT_PATTERN = - Pattern.compile("(?is)\\s*(?:@PUT)\\s+(?'.*?')" + "\\n(?.*)"); - - protected enum ExpectedResultType { - RESULT_SET, - UPDATE_COUNT, - NO_RESULT, - EXCEPTION, - EQUAL; - - StatementResult.ResultType getStatementResultType() { - switch (this) { - case NO_RESULT: - return StatementResult.ResultType.NO_RESULT; - case RESULT_SET: - return StatementResult.ResultType.RESULT_SET; - case UPDATE_COUNT: - return StatementResult.ResultType.UPDATE_COUNT; - case EXCEPTION: - case EQUAL: - default: - throw new IllegalArgumentException("not supported"); - } - } - } - - /** Result of an executed statement */ - protected abstract static class GenericStatementResult { - protected abstract StatementResult.ResultType getResultType(); - - protected abstract GenericResultSet getResultSet(); - - protected abstract long getUpdateCount(); - } - - /** - * Generic wrapper around a connection to a database. The underlying connection could be a Spanner - * {@link com.google.cloud.spanner.jdbc.Connection} or a JDBC {@link java.sql.Connection} - */ - public abstract static class GenericConnection implements AutoCloseable { - protected abstract GenericStatementResult execute(String sql) throws Exception; - - @Override - public abstract void close() throws Exception; - } - - /** - * Generic wrapper around a result set. The underlying result set could be a Spanner {@link - * ResultSet} or a JDBC {@link java.sql.ResultSet} - */ - protected abstract static class GenericResultSet { - protected abstract boolean next() throws Exception; - - protected abstract Object getValue(String col) throws Exception; - - protected abstract int getColumnCount() throws Exception; - - protected abstract Object getFirstValue() throws Exception; - } - - public static interface GenericConnectionProvider { - public GenericConnection getConnection(); - } - - /** Reads SQL statements from a file. Any copyright header in the file will be stripped away. */ - public static List readStatementsFromFile(String filename, Class resourceClass) { - File file = new File(resourceClass.getResource(filename).getFile()); - StringBuilder builder = new StringBuilder(); - try (Scanner scanner = new Scanner(file)) { - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - builder.append(line).append("\n"); - } - scanner.close(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - String script = builder.toString().replaceAll(StatementParserTest.COPYRIGHT_PATTERN, ""); - String[] array = script.split(";"); - List res = new ArrayList<>(array.length); - for (String statement : array) { - if (statement != null && statement.trim().length() > 0) { - res.add(statement); - } - } - return res; - } - - private final GenericConnectionProvider connectionProvider; - - private final Map variables = new HashMap<>(); - - private final boolean logStatements; - - /** - * Constructor for a verifier that will take a {@link GenericConnection} as a parameter to the - * {@link AbstractSqlScriptVerifier#verifyStatementsInFile(GenericConnection, String, Class, - * boolean)} - */ - public AbstractSqlScriptVerifier() { - this(null); - } - - /** Constructor for a verifier that will use a connection provider for connections */ - public AbstractSqlScriptVerifier(GenericConnectionProvider provider) { - this.connectionProvider = provider; - this.logStatements = Boolean.parseBoolean(System.getProperty("log_sql_statements", "false")); - } - - /** - * Reads sql statements from the specified file name and executes and verifies these. Statements - * that are preceeded by an @EXPECT statement are verified against the @EXPECT specification. - * Statements without an @EXPECT statement will be executed and its result will be ignored, unless - * the statement throws an exception, which will fail the test case. - * - *

The {@link com.google.cloud.spanner.jdbc.Connection}s that the statements are executed on - * must be created by a {@link GenericConnectionProvider} - * - * @param filename The file name containing the statements. Statements must be separated by a - * semicolon (;) - * @param resourceClass The class that should be used to locate the resource specified by the file - * name - * @throws Exception - */ - public void verifyStatementsInFile(String filename, Class resourceClass) throws Exception { - verifyStatementsInFile(connectionProvider.getConnection(), filename, resourceClass); - } - - /** - * Reads sql statements from the specified file name and executes and verifies these. Statements - * that are preceeded by an @EXPECT statement are verified against the @EXPECT specification. - * Statements without an @EXPECT statement will be executed and its result will be ignored, unless - * the statement throws an exception, which will fail the test case. - * - * @param connection The {@link com.google.cloud.spanner.jdbc.Connection} to execute the - * statements against - * @param filename The file name containing the statements. Statements must be separated by a - * semicolon (;) - * @param resourceClass The class that defines the package where to find the input file - */ - public void verifyStatementsInFile( - GenericConnection connection, String filename, Class resourceClass) throws Exception { - try { - List statements = readStatementsFromFile(filename, resourceClass); - for (String statement : statements) { - String sql = statement.trim(); - if (logStatements) { - System.out.println( - "\n------------------------------------------------------\n" - + new Date() - + " ---- verifying statement:"); - System.out.println(sql); - } - if (sql.equalsIgnoreCase("NEW_CONNECTION")) { - connection.close(); - connection = connectionProvider.getConnection(); - variables.clear(); - } else { - verifyStatement(connection, sql); - } - } - } finally { - if (connection != null) { - connection.close(); - } - } - } - - private void verifyStatement(GenericConnection connection, String statement) throws Exception { - statement = replaceVariables(statement); - String statementWithoutComments = StatementParser.removeCommentsAndTrim(statement); - Matcher verifyMatcher = VERIFY_PATTERN.matcher(statementWithoutComments); - Matcher putMatcher = PUT_PATTERN.matcher(statementWithoutComments); - if (verifyMatcher.matches()) { - String sql = verifyMatcher.group("statement"); - String typeName = verifyMatcher.group("type"); - int endIndex = getFirstSpaceChar(typeName); - ExpectedResultType type = ExpectedResultType.valueOf(typeName.substring(0, endIndex)); - if (type == ExpectedResultType.EXCEPTION) { - String code = verifyMatcher.group("code"); - String messagePrefix = verifyMatcher.group("messagePrefix"); - try { - connection.execute(sql); - fail("expected exception: " + sql); - } catch (Exception e) { - verifyExpectedException(statementWithoutComments, e, code, messagePrefix); - } - } else if (type == ExpectedResultType.EQUAL) { - String variable1 = verifyMatcher.group("variable1"); - String variable2 = verifyMatcher.group("variable2"); - // get rid of the single quotes - variable1 = variable1.substring(1, variable1.length() - 1); - variable2 = variable2.substring(1, variable2.length() - 1); - assertThat( - "No variable with name " + variable1, variables.containsKey(variable1), is(true)); - assertThat( - "No variable with name " + variable2, variables.containsKey(variable2), is(true)); - Object value1 = variables.get(variable1); - Object value2 = variables.get(variable2); - if ((value1 instanceof Timestamp) && (value2 instanceof Timestamp)) { - // read timestamps are rounded - Timestamp ts1 = (Timestamp) value1; - Timestamp ts2 = (Timestamp) value2; - value1 = - Timestamp.ofTimeSecondsAndNanos(ts1.getSeconds(), (ts1.getNanos() / 1000) * 1000); - value2 = - Timestamp.ofTimeSecondsAndNanos(ts2.getSeconds(), (ts2.getNanos() / 1000) * 1000); - } - assertThat(value1, is(equalTo(value2))); - } else { - GenericStatementResult result = connection.execute(sql); - assertThat(statement, result.getResultType(), is(equalTo(type.getStatementResultType()))); - switch (type.getStatementResultType()) { - case NO_RESULT: - break; - case RESULT_SET: - String column = verifyMatcher.group("column"); - if (column == null) { - verifyActualVsExpectedResultSet(statement, result.getResultSet()); - } else { - String value = verifyMatcher.group("value"); - if (value != null) { - String parts[] = column.split(",", 2); - column = parts[0].trim(); - value = parts[1].trim(); - column = column.substring(1, column.length() - 1); - verifyResultSetValue(statement, result.getResultSet(), column, parseValue(value)); - } else { - // get rid of the quotation marks - column = column.substring(1, column.length() - 1); - verifyResultSetColumnNotNull(statement, result.getResultSet(), column); - } - } - break; - case UPDATE_COUNT: - long expectedUpdateCount = Long.valueOf(verifyMatcher.group("count").trim()); - assertThat(statement, result.getUpdateCount(), is(equalTo(expectedUpdateCount))); - break; - } - } - } else if (putMatcher.matches()) { - String sql = putMatcher.group("statement"); - String variable = putMatcher.group("variable"); - // get rid of the single quotes - variable = variable.substring(1, variable.length() - 1); - GenericStatementResult result = connection.execute(sql); - assertThat( - PUT_CONDITION, - result.getResultType(), - is(equalTo(com.google.cloud.spanner.jdbc.StatementResult.ResultType.RESULT_SET))); - GenericResultSet rs = result.getResultSet(); - assertThat(PUT_CONDITION, rs.next(), is(true)); - assertThat(PUT_CONDITION, rs.getColumnCount(), is(equalTo(1))); - variables.put(variable, rs.getFirstValue()); - assertThat(PUT_CONDITION, rs.next(), is(false)); - } else { - // just execute the statement - connection.execute(statement); - } - } - - private String replaceVariables(String sql) { - for (String key : variables.keySet()) { - sql = sql.replaceAll("%%" + key + "%%", variables.get(key).toString()); - } - return sql; - } - - protected abstract void verifyExpectedException( - String statement, Exception e, String code, String messagePrefix); - - private static final Pattern INT64_PATTERN = Pattern.compile("\\d{1,19}"); - private static final Pattern ARRAY_INT64_PATTERN = - Pattern.compile("\\[\\s*\\d{1,19}(\\s*,\\s*\\d{1,19})*\\s*\\]"); - private static final Pattern FLOAT64_PATTERN = Pattern.compile("\\d{1,19}.\\d{1,19}"); - private static final String TS_PREFIX = "ts'"; - private static final String TS_SUFFIX = "'"; - private static final Pattern BOOLEAN_PATTERN = Pattern.compile("(?is)true|false"); - - private Object parseValue(String valueString) { - if (valueString == null || "".equals(valueString) || "null".equalsIgnoreCase(valueString)) { - return null; - } - if (valueString.startsWith("'") && valueString.endsWith("'")) { - return valueString.substring(1, valueString.length() - 1); - } - if (INT64_PATTERN.matcher(valueString).matches()) { - return Long.valueOf(valueString); - } - if (ARRAY_INT64_PATTERN.matcher(valueString).matches()) { - String[] stringArray = valueString.substring(1, valueString.length() - 1).split(","); - List res = new ArrayList<>(); - for (int i = 0; i < stringArray.length; i++) { - res.add(Long.valueOf(stringArray[i])); - } - return res; - } - if (FLOAT64_PATTERN.matcher(valueString).matches()) { - return Double.valueOf(valueString); - } - if (valueString.startsWith(TS_PREFIX) && valueString.endsWith(TS_SUFFIX)) { - try { - return ReadOnlyStalenessUtil.parseRfc3339( - valueString.substring(TS_PREFIX.length(), valueString.length() - TS_SUFFIX.length())); - } catch (IllegalArgumentException e) { - // ignore, apparently not a valid a timestamp after all. - } - } - if (BOOLEAN_PATTERN.matcher(valueString).matches()) { - return Boolean.valueOf(valueString); - } - return valueString; - } - - private int getFirstSpaceChar(String input) { - for (int index = 0; index < input.length(); index++) { - if (Character.isWhitespace(input.charAt(index))) { - return index; - } - } - return input.length(); - } - - private void verifyResultSetColumnNotNull(String statement, GenericResultSet rs, String column) - throws Exception { - int count = 0; - while (rs.next()) { - assertThat(statement, getValue(rs, column), is(notNullValue())); - count++; - } - assertThat(count, is(not(equalTo(0)))); - } - - private void verifyResultSetValue( - String statement, GenericResultSet rs, String column, Object value) throws Exception { - int count = 0; - while (rs.next()) { - if (value == null) { - assertThat(statement, getValue(rs, column), is(nullValue())); - } else { - assertEquals(statement, getValue(rs, column), value); - } - count++; - } - assertThat(count, is(not(equalTo(0)))); - } - - private void verifyActualVsExpectedResultSet(String statement, GenericResultSet rs) - throws Exception { - int count = 0; - while (rs.next()) { - assertThat(statement, getValue(rs, "ACTUAL"), is(equalTo(getValue(rs, "EXPECTED")))); - count++; - } - assertThat(count, is(not(equalTo(0)))); - } - - private Object getValue(GenericResultSet rs, String col) throws Exception { - return rs.getValue(col); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeConverterTest.java deleted file mode 100644 index 4dcbd410..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeConverterTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.ClientSideStatementValueConverters.AutocommitDmlModeConverter; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class AutocommitDmlModeConverterTest { - - @Test - public void testConvert() throws CompileException { - String allowedValues = - ReadOnlyStalenessConverterTest.getAllowedValues(AutocommitDmlModeConverter.class); - assertThat(allowedValues, is(notNullValue())); - AutocommitDmlModeConverter converter = new AutocommitDmlModeConverter(allowedValues); - assertThat(converter.convert("transactional"), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - assertThat(converter.convert("TRANSACTIONAL"), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - assertThat(converter.convert("Transactional"), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - - assertThat( - converter.convert("partitioned_non_atomic"), - is(equalTo(AutocommitDmlMode.PARTITIONED_NON_ATOMIC))); - assertThat( - converter.convert("Partitioned_Non_Atomic"), - is(equalTo(AutocommitDmlMode.PARTITIONED_NON_ATOMIC))); - assertThat( - converter.convert("PARTITIONED_NON_ATOMIC"), - is(equalTo(AutocommitDmlMode.PARTITIONED_NON_ATOMIC))); - - assertThat(converter.convert(""), is(nullValue())); - assertThat(converter.convert(" "), is(nullValue())); - assertThat(converter.convert("random string"), is(nullValue())); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeTest.java deleted file mode 100644 index bf350419..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/AutocommitDmlModeTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionRunner; -import com.google.cloud.spanner.TransactionRunner.TransactionCallable; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class AutocommitDmlModeTest { - private static final String UPDATE = "UPDATE foo SET bar=1"; - private static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - - private DatabaseClient dbClient; - private TransactionContext txContext; - - @SuppressWarnings("unchecked") - private ConnectionImpl createConnection(ConnectionOptions options) { - dbClient = mock(DatabaseClient.class); - txContext = mock(TransactionContext.class); - Spanner spanner = mock(Spanner.class); - SpannerPool spannerPool = mock(SpannerPool.class); - when(spannerPool.getSpanner(any(ConnectionOptions.class), any(ConnectionImpl.class))) - .thenReturn(spanner); - DdlClient ddlClient = mock(DdlClient.class); - TransactionRunner txRunner = mock(TransactionRunner.class); - when(dbClient.readWriteTransaction()).thenReturn(txRunner); - when(txRunner.run(any(TransactionCallable.class))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) throws Throwable { - TransactionCallable callable = - (TransactionCallable) invocation.getArguments()[0]; - return callable.run(txContext); - } - }); - - TransactionManager txManager = mock(TransactionManager.class); - when(txManager.begin()).thenReturn(txContext); - when(dbClient.transactionManager()).thenReturn(txManager); - - return new ConnectionImpl(options, spannerPool, ddlClient, dbClient); - } - - @Test - public void testAutocommitDmlModeTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(connection.isAutocommit(), is(true)); - assertThat(connection.isReadOnly(), is(false)); - assertThat(connection.getAutocommitDmlMode(), is(AutocommitDmlMode.TRANSACTIONAL)); - - connection.execute(Statement.of(UPDATE)); - verify(txContext).executeUpdate(Statement.of(UPDATE)); - verify(dbClient, never()).executePartitionedUpdate(Statement.of(UPDATE)); - } - } - - @Test - public void testAutocommitDmlModePartitioned() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(connection.isAutocommit(), is(true)); - assertThat(connection.isReadOnly(), is(false)); - connection.setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - assertThat(connection.getAutocommitDmlMode(), is(AutocommitDmlMode.PARTITIONED_NON_ATOMIC)); - - connection.execute(Statement.of(UPDATE)); - verify(txContext, never()).executeUpdate(Statement.of(UPDATE)); - verify(dbClient).executePartitionedUpdate(Statement.of(UPDATE)); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/BooleanConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/BooleanConverterTest.java deleted file mode 100644 index f86295be..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/BooleanConverterTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.ClientSideStatementValueConverters.BooleanConverter; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class BooleanConverterTest { - - @Test - public void testConvert() throws CompileException { - String allowedValues = ReadOnlyStalenessConverterTest.getAllowedValues(BooleanConverter.class); - assertThat(allowedValues, is(notNullValue())); - BooleanConverter converter = new BooleanConverter(allowedValues); - assertThat(converter.convert("true"), is(equalTo(Boolean.TRUE))); - assertThat(converter.convert("TRUE"), is(equalTo(Boolean.TRUE))); - assertThat(converter.convert("True"), is(equalTo(Boolean.TRUE))); - - assertThat(converter.convert("false"), is(equalTo(Boolean.FALSE))); - assertThat(converter.convert("FALSE"), is(equalTo(Boolean.FALSE))); - assertThat(converter.convert("False"), is(equalTo(Boolean.FALSE))); - - assertThat(converter.convert(""), is(nullValue())); - assertThat(converter.convert(" "), is(nullValue())); - assertThat(converter.convert("random string"), is(nullValue())); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ClientSideStatementsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ClientSideStatementsTest.java deleted file mode 100644 index 4ba2cb7a..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ClientSideStatementsTest.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * Test that runs a pre-generated sql script for {@link ClientSideStatement}s. The sql script can be - * generated by running mvn -P generate-test-sql-scripts compile It is only necessary - * to generate a new test script if a new {@link ClientSideStatement} has been added, or the - * behavior of an existing {@link ClientSideStatement} has changed. - * - *

This class does not need to be implemented for the client libraries of other programming - * languages. All test cases are covered by the sql file ClientSideStatementsTest.sql. - */ -@RunWith(JUnit4.class) -public class ClientSideStatementsTest { - - @Test - public void testExecuteClientSideStatementsScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new TestConnectionProvider()); - verifier.verifyStatementsInFile("ClientSideStatementsTest.sql", getClass()); - } - - private static final String SCRIPT_FILE = - "src/test/resources/com/google/cloud/spanner/jdbc/ClientSideStatementsTest.sql"; - private static PrintWriter writer; - - /** Generates the test script file */ - static void generateTestScript() throws Exception { - try { - openLog(); - ClientSideStatements statements = ClientSideStatements.INSTANCE; - for (ClientSideStatementImpl statement : statements.getCompiledStatements()) { - generateTestStatements(statement); - } - } finally { - closeLog(); - } - } - - /** Writes the prerequisite statements + the given sql statement to a script file */ - private static void log(List pre, String sql) { - writeLog("NEW_CONNECTION"); - for (String prerequisite : pre) { - writeLog(prerequisite); - } - writeLog(sql); - } - - /** - * Writes the prerequisite statements + the given sql statement to a script file preceded by - * an @EXPECT EXCEPTION error statement - */ - private static void log(List pre, String statement, ErrorCode error) { - log(pre, "@EXPECT EXCEPTION " + error.name() + "\n" + statement); - } - - /** Writes the actual statement to the script file */ - private static void writeLog(String statement) { - writer.println(statement + ";"); - } - - private static void openLog() { - try { - writer = - new PrintWriter( - new OutputStreamWriter(new FileOutputStream(SCRIPT_FILE, false), "UTF8"), true); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @AfterClass - public static void closeLog() { - if (writer != null) { - writer.close(); - } - } - - static class TestConnectionProvider implements GenericConnectionProvider { - @Override - public GenericConnection getConnection() { - return SpannerGenericConnection.of( - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build())); - } - } - - /** Generates test statements for all {@link ClientSideStatement}s */ - private static void generateTestStatements(ClientSideStatementImpl statement) { - for (String sql : statement.getExampleStatements()) { - log(statement.getExamplePrerequisiteStatements(), sql); - log(statement.getExamplePrerequisiteStatements(), upper(sql)); - log(statement.getExamplePrerequisiteStatements(), lower(sql)); - log(statement.getExamplePrerequisiteStatements(), withLeadingSpaces(sql)); - log(statement.getExamplePrerequisiteStatements(), withLeadingTabs(sql)); - log(statement.getExamplePrerequisiteStatements(), withLeadingLinefeeds(sql)); - log(statement.getExamplePrerequisiteStatements(), withTrailingSpaces(sql)); - log(statement.getExamplePrerequisiteStatements(), withTrailingTabs(sql)); - log(statement.getExamplePrerequisiteStatements(), withTrailingLinefeeds(sql)); - log(statement.getExamplePrerequisiteStatements(), withSpaces(sql)); - log(statement.getExamplePrerequisiteStatements(), withTabs(sql)); - log(statement.getExamplePrerequisiteStatements(), withLinefeeds(sql)); - - log( - statement.getExamplePrerequisiteStatements(), - withInvalidPrefix(sql), - ErrorCode.INVALID_ARGUMENT); - log( - statement.getExamplePrerequisiteStatements(), - withInvalidSuffix(sql), - ErrorCode.INVALID_ARGUMENT); - - final String[] replacements = { - "%", "_", "&", "$", "@", "!", "*", "(", ")", "-", "+", "-#", "/", "\\", "?", "-/", "/#", - "/-" - }; - for (String replacement : replacements) { - log( - statement.getExamplePrerequisiteStatements(), - withPrefix(replacement, sql), - ErrorCode.INVALID_ARGUMENT); - log( - statement.getExamplePrerequisiteStatements(), - withSuffix(replacement, sql), - ErrorCode.INVALID_ARGUMENT); - log( - statement.getExamplePrerequisiteStatements(), - replaceLastSpaceWith(replacement, sql), - ErrorCode.INVALID_ARGUMENT); - } - } - } - - private static String upper(String statement) { - return statement.toUpperCase(); - } - - private static String lower(String statement) { - return statement.toLowerCase(); - } - - private static String withLeadingSpaces(String statement) { - return " " + statement; - } - - private static String withLeadingTabs(String statement) { - return "\t\t\t" + statement; - } - - private static String withLeadingLinefeeds(String statement) { - return "\n\n\n" + statement; - } - - private static String withTrailingSpaces(String statement) { - return statement + " "; - } - - private static String withTrailingTabs(String statement) { - return statement + "\t\t"; - } - - private static String withTrailingLinefeeds(String statement) { - return statement + "\n\n"; - } - - private static String withSpaces(String statement) { - return statement.replaceAll(" ", " "); - } - - private static String withTabs(String statement) { - return statement.replaceAll(" ", "\t"); - } - - private static String withLinefeeds(String statement) { - // Do not replace spaces inside quotes - Matcher matcher = Pattern.compile("(.*)('.*')").matcher(statement); - if (matcher.matches()) { - return matcher.group(1).replaceAll(" ", "\n") + matcher.group(2); - } - return statement.replaceAll(" ", "\n"); - } - - private static String withInvalidPrefix(String statement) { - return "foo " + statement; - } - - private static String withInvalidSuffix(String statement) { - return statement + " bar"; - } - - private static String withPrefix(String prefix, String statement) { - return prefix + statement; - } - - private static String withSuffix(String suffix, String statement) { - return statement + suffix; - } - - private static String replaceLastSpaceWith(String replacement, String statement) { - if (statement.lastIndexOf(' ') > -1) { - return statement.substring(0, statement.lastIndexOf(' ')) - + replacement - + statement.substring(statement.lastIndexOf(' ') + 1); - } - return statement + replacement; - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadOnlyTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadOnlyTest.java deleted file mode 100644 index 3cfd42d7..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadOnlyTest.java +++ /dev/null @@ -1,914 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import java.util.concurrent.TimeUnit; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; - -/** - * The tests in this class do not need to be implemented for client libraries in other programming - * languages, as all test cases are covered by the file ConnectionImplGeneratedSqlScriptTest.sql - */ -@RunWith(Enclosed.class) -public class ConnectionImplAutocommitReadOnlyTest { - - public static class ConnectionImplAutocommitReadOnlyNoActionsTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query has been executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadOnlyAfterSelectTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - // no call to next() on ResultSet - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last statement was a query, next() has not yet been called, but as the connection api - // returns a directly executed resultset, the read timestamp is already available - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadOnlyAfterSelectAndResultSetNextTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)).next(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last statement was a query - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadOnlyAfterBeginTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // connection is in read-only mode - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // in a transaction, only exact allowed - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadOnlyAfterTemporaryTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - // readonly - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last action was a transaction that ended with a select query - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadOnlyAfterSetReadOnlyMaxStalenessTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - TimestampBound staleness = TimestampBound.ofMaxStaleness(10L, TimeUnit.SECONDS); - log( - "SET READ_ONLY_STALENESS='" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "';"); - connection.setReadOnlyStaleness(staleness); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - // readonly - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadWriteTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadWriteTest.java deleted file mode 100644 index 304bf8f5..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplAutocommitReadWriteTest.java +++ /dev/null @@ -1,1325 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; - -/** - * The tests in this class do not need to be implemented for client libraries in other programming - * languages, as all test cases are covered by the file ConnectionImplGeneratedSqlScriptTest.sql - */ -@RunWith(Enclosed.class) -public class ConnectionImplAutocommitReadWriteTest { - - public static class ConnectionImplAutocommitReadWriteNoActionsTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query has been executed - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterSelectTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - // no next() called - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last statement was a query, next() has not yet been called, but as the connection api - // returns a directly executed resultset, the read timestamp is already available - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterSelectAndResultSetNextTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - // the @expect ensures next() is called - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // the last action was a query that has retrieved data - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterUpdateTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log(UPDATE + ";"); - connection.execute(Statement.of(UPDATE)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return true; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterDdlTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log(DDL + ";"); - connection.execute(Statement.of(DDL)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // there is no transaction - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterBeginTransactionTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // in temporary transaction - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - // default is a read-write transaction - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterTemporaryTransactionTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log(UPDATE + ";"); - connection.execute(Statement.of(UPDATE)); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return true; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return true; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return true; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterBeginReadOnlyTransactionTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log("SET TRANSACTION READ ONLY;"); - connection.setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // in temporary transaction - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - // it's a read-only transaction - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplAutocommitReadWriteAfterStartDdlBatchTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=TRUE;"); - connection.setAutocommit(true); - log("START BATCH DDL;"); - connection.startBatchDdl(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return false; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - // it's a DDL batch - return type == StatementType.CLIENT_SIDE || type == StatementType.DDL; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return true; - } - - @Override - boolean isAbortBatchAllowed() { - return true; - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplGeneratedSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplGeneratedSqlScriptTest.java deleted file mode 100644 index fff50fa8..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplGeneratedSqlScriptTest.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import com.google.common.collect.ImmutableSet; -import com.google.common.reflect.ClassPath; -import com.google.common.reflect.ClassPath.ClassInfo; -import java.io.IOException; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * This test executes a SQL script that has been generated from the log of all the subclasses of - * {@link AbstractConnectionImplTest} and covers the same test cases. Its aim is to verify that the - * connection reacts correctly in all possible states (i.e. DML statements should not be allowed - * when the connection is in read-only mode, or when a read-only transaction has started etc.) - * - *

A new test script can be generated by running: mvn -P generate-test-sql-scripts compile - * It is only necessary to generate a new test script if the behavior of {@link - * com.google.cloud.spanner.jdbc.Connection} has changed (for example calling COMMIT is currently - * not allowed in AUTOCOMMIT mode, but this has changed to be a no-op). A new test script must also - * be generated if additional test cases have been added to {@link AbstractConnectionImplTest}. - */ -@RunWith(JUnit4.class) -public class ConnectionImplGeneratedSqlScriptTest { - - static class TestConnectionProvider implements GenericConnectionProvider { - @Override - public GenericConnection getConnection() { - return SpannerGenericConnection.of( - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build())); - } - } - - @Test - public void testGeneratedScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new TestConnectionProvider()); - verifier.verifyStatementsInFile("ConnectionImplGeneratedSqlScriptTest.sql", getClass()); - } - - /** - * Generates the test SQL script. It should be noted that running this method multiple times - * without having changed anything in the underlying code, could still yield different script - * files, as the script is generated by running a number of JUnit test cases. The order in which - * these test cases are run is non-deterministic. That means that the generated sql script will - * still contain exactly the same test cases after each generation, but the order of the test - * cases in the script file is equal to the order in which the test cases were run the last time - * the script was generated. It is therefore also not recommended to include this generation in an - * automatic build, but to generate the script only when there has been some fundamental change in - * the code. - * - *

The sql test scripts can be generated by running - * mvn -P generate-test-sql-scripts compile - */ - static void generateTestScript() throws ClassNotFoundException, IOException { - // first make the current script file empty - AbstractConnectionImplTest.emptyScript(); - JUnitCore junit = new JUnitCore(); - Class[] testClasses = getAbstractConnectionImplTestSubclasses(); - Result result = junit.run(testClasses); - if (!result.wasSuccessful()) { - throw new RuntimeException("Generating test script failed!"); - } - } - - private static Class[] getAbstractConnectionImplTestSubclasses() - throws IOException, ClassNotFoundException { - List> list = new ArrayList<>(); - ClassPath cp = ClassPath.from(ConnectionImplGeneratedSqlScriptTest.class.getClassLoader()); - ImmutableSet classes = - cp.getTopLevelClassesRecursive( - ConnectionImplGeneratedSqlScriptTest.class.getPackage().getName()); - for (ClassInfo c : classes) { - Class clazz = - ConnectionImplGeneratedSqlScriptTest.class.getClassLoader().loadClass(c.getName()); - addAbstractConnectionImplTestSubclassesToList(list, clazz); - } - Class[] res = new Class[list.size()]; - for (int i = 0; i < list.size(); i++) { - res[i] = list.get(i); - } - return res; - } - - private static void addAbstractConnectionImplTestSubclassesToList( - List> list, Class clazz) { - for (Class innerClass : clazz.getDeclaredClasses()) { - addAbstractConnectionImplTestSubclassesToList(list, innerClass); - } - if (!clazz.isInterface() - && !Modifier.isAbstract(clazz.getModifiers()) - && AbstractConnectionImplTest.class.isAssignableFrom(clazz)) { - list.add(clazz); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTest.java deleted file mode 100644 index 2c2cb964..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTest.java +++ /dev/null @@ -1,1261 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.DDL; -import static com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.SELECT; -import static com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.UPDATE; -import static com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.expectSpannerException; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.NoCredentials; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ForwardingResultSet; -import com.google.cloud.spanner.Options; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ReadOnlyTransaction; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionRunner; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.ConnectionConsumer; -import com.google.cloud.spanner.jdbc.ConnectionImpl.UnitOfWorkType; -import com.google.cloud.spanner.jdbc.ConnectionStatementExecutorImpl.StatementTimeoutGetter; -import com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.GetExactStaleness; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import com.google.spanner.v1.ResultSetStats; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Matchers; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class ConnectionImplTest { - public static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - - static class SimpleTransactionManager implements TransactionManager { - private TransactionState state; - private Timestamp commitTimestamp; - private TransactionContext txContext; - - private SimpleTransactionManager(TransactionContext txContext) { - this.txContext = txContext; - } - - @Override - public TransactionContext begin() { - state = TransactionState.STARTED; - return txContext; - } - - @Override - public void commit() { - commitTimestamp = Timestamp.now(); - state = TransactionState.COMMITTED; - } - - @Override - public void rollback() { - state = TransactionState.ROLLED_BACK; - } - - @Override - public TransactionContext resetForRetry() { - return txContext; - } - - @Override - public Timestamp getCommitTimestamp() { - return commitTimestamp; - } - - @Override - public TransactionState getState() { - return state; - } - - @Override - public void close() { - if (state != TransactionState.COMMITTED) { - state = TransactionState.ROLLED_BACK; - } - } - } - - private static class SimpleResultSet extends ForwardingResultSet { - private boolean nextCalled = false; - private boolean onValidRow = false; - private boolean hasNextReturnedFalse = false; - - SimpleResultSet(ResultSet delegate) { - super(delegate); - } - - @Override - public boolean next() { - nextCalled = true; - onValidRow = super.next(); - hasNextReturnedFalse = !onValidRow; - return onValidRow; - } - - boolean isNextCalled() { - return nextCalled; - } - - @Override - public ResultSetStats getStats() { - if (hasNextReturnedFalse) { - return super.getStats(); - } - return null; - } - - @Override - public long getLong(int columnIndex) { - if (onValidRow) { - return super.getLong(columnIndex); - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "ResultSet is not positioned on a valid row"); - } - } - - private static ResultSet createSelect1MockResultSet() { - ResultSet mockResultSet = mock(ResultSet.class); - when(mockResultSet.next()).thenReturn(true, false); - when(mockResultSet.getLong(0)).thenReturn(1L); - when(mockResultSet.getLong("TEST")).thenReturn(1L); - when(mockResultSet.getColumnType(0)).thenReturn(Type.int64()); - when(mockResultSet.getColumnType("TEST")).thenReturn(Type.int64()); - return mockResultSet; - } - - private static DdlClient createDefaultMockDdlClient() { - try { - DdlClient ddlClient = mock(DdlClient.class); - @SuppressWarnings("unchecked") - final OperationFuture operation = - mock(OperationFuture.class); - when(operation.get()).thenReturn(null); - UpdateDatabaseDdlMetadata metadata = UpdateDatabaseDdlMetadata.getDefaultInstance(); - ApiFuture futureMetadata = ApiFutures.immediateFuture(metadata); - when(operation.getMetadata()).thenReturn(futureMetadata); - when(ddlClient.executeDdl(anyString())).thenCallRealMethod(); - when(ddlClient.executeDdl(anyListOf(String.class))).thenReturn(operation); - return ddlClient; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static ConnectionImpl createConnection(ConnectionOptions options) { - Spanner spanner = mock(Spanner.class); - SpannerPool spannerPool = mock(SpannerPool.class); - when(spannerPool.getSpanner(any(ConnectionOptions.class), any(ConnectionImpl.class))) - .thenReturn(spanner); - DdlClient ddlClient = createDefaultMockDdlClient(); - DatabaseClient dbClient = mock(DatabaseClient.class); - ReadOnlyTransaction singleUseReadOnlyTx = mock(ReadOnlyTransaction.class); - - ResultSet mockResultSetWithStats = createSelect1MockResultSet(); - when(mockResultSetWithStats.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - - final SimpleResultSet select1ResultSet = new SimpleResultSet(createSelect1MockResultSet()); - final SimpleResultSet select1ResultSetWithStats = new SimpleResultSet(mockResultSetWithStats); - when(singleUseReadOnlyTx.executeQuery(Statement.of(SELECT))) - .thenAnswer( - new Answer() { - @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { - if (select1ResultSet.nextCalled) { - // create a new mock - return new SimpleResultSet(createSelect1MockResultSet()); - } - return select1ResultSet; - } - }); - when(singleUseReadOnlyTx.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PLAN)) - .thenReturn(select1ResultSetWithStats); - when(singleUseReadOnlyTx.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PROFILE)) - .thenReturn(select1ResultSetWithStats); - when(singleUseReadOnlyTx.getReadTimestamp()) - .then( - new Answer() { - @Override - public Timestamp answer(InvocationOnMock invocation) throws Throwable { - if (select1ResultSet.isNextCalled() || select1ResultSetWithStats.isNextCalled()) { - return Timestamp.now(); - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, "No query has returned with any data yet"); - } - }); - when(dbClient.singleUseReadOnlyTransaction(Matchers.any(TimestampBound.class))) - .thenReturn(singleUseReadOnlyTx); - - when(dbClient.transactionManager()) - .thenAnswer( - new Answer() { - @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { - TransactionContext txContext = mock(TransactionContext.class); - when(txContext.executeQuery(Statement.of(SELECT))) - .thenAnswer( - new Answer() { - @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { - if (select1ResultSet.nextCalled) { - // create a new mock - return new SimpleResultSet(createSelect1MockResultSet()); - } - return select1ResultSet; - } - }); - when(txContext.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PLAN)) - .thenReturn(select1ResultSetWithStats); - when(txContext.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PROFILE)) - .thenReturn(select1ResultSetWithStats); - when(txContext.executeUpdate(Statement.of(UPDATE))).thenReturn(1L); - return new SimpleTransactionManager(txContext); - } - }); - - when(dbClient.readOnlyTransaction(Matchers.any(TimestampBound.class))) - .thenAnswer( - new Answer() { - @Override - public ReadOnlyTransaction answer(InvocationOnMock invocation) throws Throwable { - ReadOnlyTransaction tx = mock(ReadOnlyTransaction.class); - when(tx.executeQuery(Statement.of(SELECT))) - .thenAnswer( - new Answer() { - @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { - if (select1ResultSet.nextCalled) { - // create a new mock - return new SimpleResultSet(createSelect1MockResultSet()); - } - return select1ResultSet; - } - }); - when(tx.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PLAN)) - .thenReturn(select1ResultSetWithStats); - when(tx.analyzeQuery(Statement.of(SELECT), QueryAnalyzeMode.PROFILE)) - .thenReturn(select1ResultSetWithStats); - when(tx.getReadTimestamp()) - .then( - new Answer() { - @Override - public Timestamp answer(InvocationOnMock invocation) throws Throwable { - if (select1ResultSet.isNextCalled() - || select1ResultSetWithStats.isNextCalled()) { - return Timestamp.now(); - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, - "No query has returned with any data yet"); - } - }); - return tx; - } - }); - - when(dbClient.readWriteTransaction()) - .thenAnswer( - new Answer() { - @Override - public TransactionRunner answer(InvocationOnMock invocation) throws Throwable { - TransactionRunner runner = - new TransactionRunner() { - private Timestamp commitTimestamp; - - @SuppressWarnings("unchecked") - @Override - public T run(TransactionCallable callable) { - this.commitTimestamp = Timestamp.now(); - return (T) Long.valueOf(1L); - } - - @Override - public Timestamp getCommitTimestamp() { - return commitTimestamp; - } - - @Override - public TransactionRunner allowNestedTransaction() { - return this; - } - }; - return runner; - } - }); - return new ConnectionImpl(options, spannerPool, ddlClient, dbClient); - } - - @Test - public void testExecuteSetAutocommitOn() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI + ";autocommit=false") - .build())) { - assertThat(subject.isAutocommit(), is(false)); - - StatementResult res = subject.execute(Statement.of("set autocommit = true")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isAutocommit(), is(true)); - } - } - - @Test - public void testExecuteSetAutocommitOff() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - - StatementResult res = subject.execute(Statement.of("set autocommit = false")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isAutocommit(), is(false)); - } - } - - @Test - public void testExecuteGetAutocommit() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - - // assert that autocommit is true (default) - assertThat(subject.isAutocommit(), is(true)); - StatementResult res = subject.execute(Statement.of("show variable autocommit")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getBoolean("AUTOCOMMIT"), is(true)); - - // set autocommit to false and assert that autocommit is false - res = subject.execute(Statement.of("set autocommit = false")); - assertThat(subject.isAutocommit(), is(false)); - res = subject.execute(Statement.of("show variable autocommit")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getBoolean("AUTOCOMMIT"), is(false)); - } - } - - @Test - public void testExecuteSetReadOnlyOn() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isReadOnly(), is(false)); - - StatementResult res = subject.execute(Statement.of("set readonly = true")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isReadOnly(), is(true)); - } - } - - @Test - public void testExecuteSetReadOnlyOff() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI + ";readonly=true") - .build())) { - assertThat(subject.isReadOnly(), is(true)); - - StatementResult res = subject.execute(Statement.of("set readonly = false")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isReadOnly(), is(false)); - } - } - - @Test - public void testExecuteGetReadOnly() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - - // assert that read only is false (default) - assertThat(subject.isReadOnly(), is(false)); - StatementResult res = subject.execute(Statement.of("show variable readonly")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getBoolean("READONLY"), is(false)); - - // set read only to true and assert that read only is true - res = subject.execute(Statement.of("set readonly = true")); - assertThat(subject.isReadOnly(), is(true)); - res = subject.execute(Statement.of("show variable readonly")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getBoolean("READONLY"), is(true)); - } - } - - @Test - public void testExecuteSetAutocommitDmlMode() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - assertThat(subject.getAutocommitDmlMode(), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - - StatementResult res = - subject.execute(Statement.of("set autocommit_dml_mode='PARTITIONED_NON_ATOMIC'")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat( - subject.getAutocommitDmlMode(), is(equalTo(AutocommitDmlMode.PARTITIONED_NON_ATOMIC))); - - res = subject.execute(Statement.of("set autocommit_dml_mode='TRANSACTIONAL'")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getAutocommitDmlMode(), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - } - } - - @Test - public void testExecuteSetAutocommitDmlModeInvalidValue() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - assertThat(subject.getAutocommitDmlMode(), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - - ErrorCode expected = null; - try { - subject.execute(Statement.of("set autocommit_dml_mode='NON_EXISTENT_VALUE'")); - } catch (SpannerException e) { - expected = e.getErrorCode(); - } - assertThat(expected, is(equalTo(ErrorCode.INVALID_ARGUMENT))); - } - } - - @Test - public void testExecuteGetAutocommitDmlMode() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - assertThat(subject.getAutocommitDmlMode(), is(equalTo(AutocommitDmlMode.TRANSACTIONAL))); - - StatementResult res = subject.execute(Statement.of("show variable autocommit_dml_mode")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat( - res.getResultSet().getString("AUTOCOMMIT_DML_MODE"), - is(equalTo(AutocommitDmlMode.TRANSACTIONAL.toString()))); - - subject.execute(Statement.of("set autocommit_dml_mode='PARTITIONED_NON_ATOMIC'")); - res = subject.execute(Statement.of("show variable autocommit_dml_mode")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat( - res.getResultSet().getString("AUTOCOMMIT_DML_MODE"), - is(equalTo(AutocommitDmlMode.PARTITIONED_NON_ATOMIC.toString()))); - } - } - - @Test - public void testExecuteSetOptimizerVersion() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getOptimizerVersion(), is(equalTo(""))); - - StatementResult res = subject.execute(Statement.of("set optimizer_version='1'")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getOptimizerVersion(), is(equalTo("1"))); - - res = subject.execute(Statement.of("set optimizer_version='1000'")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getOptimizerVersion(), is(equalTo("1000"))); - - res = subject.execute(Statement.of("set optimizer_version='latest'")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getOptimizerVersion(), is(equalTo("latest"))); - - res = subject.execute(Statement.of("set optimizer_version=''")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getOptimizerVersion(), is(equalTo(""))); - } - } - - @Test - public void testExecuteSetOptimizerVersionInvalidValue() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getOptimizerVersion(), is(equalTo(""))); - - try { - subject.execute(Statement.of("set optimizer_version='NOT_A_VERSION'")); - fail("Missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.INVALID_ARGUMENT))); - } - } - } - - @Test - public void testExecuteGetOptimizerVersion() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getOptimizerVersion(), is(equalTo(""))); - - StatementResult res = subject.execute(Statement.of("show variable optimizer_version")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getString("OPTIMIZER_VERSION"), is(equalTo(""))); - - subject.execute(Statement.of("set optimizer_version='1'")); - res = subject.execute(Statement.of("show variable optimizer_version")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getString("OPTIMIZER_VERSION"), is(equalTo("1"))); - } - } - - @Test - public void testExecuteSetStatementTimeout() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getStatementTimeout(TimeUnit.MILLISECONDS), is(equalTo(0L))); - - for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { - for (Long timeout : new Long[] {1L, 100L, 10000L, 315576000000L}) { - StatementResult res = - subject.execute( - Statement.of( - String.format( - "set statement_timeout='%d%s'", - timeout, ReadOnlyStalenessUtil.getTimeUnitAbbreviation(unit)))); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getStatementTimeout(unit), is(equalTo(timeout))); - assertThat(subject.hasStatementTimeout(), is(true)); - - StatementResult resNoTimeout = - subject.execute(Statement.of("set statement_timeout=null")); - assertThat(resNoTimeout.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getStatementTimeout(unit), is(equalTo(0L))); - assertThat(subject.hasStatementTimeout(), is(false)); - } - } - } - } - - @Test - public void testExecuteSetStatementTimeoutInvalidValue() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getStatementTimeout(TimeUnit.MILLISECONDS), is(equalTo(0L))); - - ErrorCode expected = null; - try { - subject.execute(Statement.of("set statement_timeout=-1")); - } catch (SpannerException e) { - expected = e.getErrorCode(); - } - assertThat(expected, is(equalTo(ErrorCode.INVALID_ARGUMENT))); - } - } - - @Test - public void testExecuteGetStatementTimeout() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getStatementTimeout(TimeUnit.MILLISECONDS), is(equalTo(0L))); - - for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { - for (Long timeout : new Long[] {1L, 100L, 10000L, 315576000000L}) { - subject.execute( - Statement.of( - String.format( - "set statement_timeout='%d%s'", - timeout, ReadOnlyStalenessUtil.getTimeUnitAbbreviation(unit)))); - StatementResult res = subject.execute(Statement.of("show variable statement_timeout")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - TimeUnit appropriateUnit = - ReadOnlyStalenessUtil.getAppropriateTimeUnit(new StatementTimeoutGetter(subject)); - assertThat( - res.getResultSet().getString("STATEMENT_TIMEOUT"), - is( - equalTo( - subject.getStatementTimeout(appropriateUnit) - + ReadOnlyStalenessUtil.getTimeUnitAbbreviation(appropriateUnit)))); - - subject.execute(Statement.of("set statement_timeout=null")); - StatementResult resNoTimeout = - subject.execute(Statement.of("show variable statement_timeout")); - assertThat(resNoTimeout.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(resNoTimeout.getResultSet().next(), is(true)); - assertThat(resNoTimeout.getResultSet().isNull("STATEMENT_TIMEOUT"), is(true)); - } - } - } - } - - @Test - public void testExecuteGetReadTimestamp() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.beginTransaction(); - subject.setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - subject.executeQuery(Statement.of(AbstractConnectionImplTest.SELECT)); - StatementResult res = subject.execute(Statement.of("show variable read_timestamp")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getTimestamp("READ_TIMESTAMP"), is(notNullValue())); - subject.commit(); - } - } - - @Test - public void testExecuteGetCommitTimestamp() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.beginTransaction(); - subject.executeQuery(Statement.of(AbstractConnectionImplTest.SELECT)).next(); - subject.commit(); - StatementResult res = subject.execute(Statement.of("show variable commit_timestamp")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat(res.getResultSet().getTimestamp("COMMIT_TIMESTAMP"), is(notNullValue())); - } - } - - private static final class StalenessDuration { - private final long duration; - private final TimeUnit unit; - - private StalenessDuration(long duration, TimeUnit unit) { - this.duration = duration; - this.unit = unit; - } - - @Override - public String toString() { - GetExactStaleness getExactStalenessFunction = - new GetExactStaleness(TimestampBound.ofExactStaleness(duration, unit)); - return ReadOnlyStalenessUtil.durationToString(getExactStalenessFunction); - } - } - - @Test - public void testExecuteGetReadOnlyStaleness() { - Map timestamps = new HashMap<>(); - timestamps.put(Mode.READ_TIMESTAMP, ReadOnlyStalenessUtil.parseRfc3339("2018-10-08T14:05:10Z")); - timestamps.put( - Mode.MIN_READ_TIMESTAMP, ReadOnlyStalenessUtil.parseRfc3339("2018-10-08T14:05:10.12345Z")); - Map durations = new HashMap<>(); - durations.put(Mode.EXACT_STALENESS, new StalenessDuration(1000L, TimeUnit.MILLISECONDS)); - durations.put(Mode.MAX_STALENESS, new StalenessDuration(1234567L, TimeUnit.MICROSECONDS)); - List stalenesses = - Arrays.asList( - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(timestamps.get(Mode.READ_TIMESTAMP)), - TimestampBound.ofMinReadTimestamp(timestamps.get(Mode.MIN_READ_TIMESTAMP)), - TimestampBound.ofExactStaleness( - durations.get(Mode.EXACT_STALENESS).duration, - durations.get(Mode.EXACT_STALENESS).unit), - TimestampBound.ofMaxStaleness( - durations.get(Mode.MAX_STALENESS).duration, - durations.get(Mode.MAX_STALENESS).unit)); - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - for (TimestampBound staleness : stalenesses) { - subject.setReadOnlyStaleness(staleness); - StatementResult res = subject.execute(Statement.of("show variable read_only_staleness")); - assertThat(res.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(res.getResultSet().next(), is(true)); - assertThat( - res.getResultSet().getString("READ_ONLY_STALENESS"), - is(equalTo(ReadOnlyStalenessUtil.timestampBoundToString(staleness)))); - } - } - } - - @Test - public void testExecuteSetReadOnlyStaleness() { - Map timestamps = new HashMap<>(); - timestamps.put(Mode.READ_TIMESTAMP, ReadOnlyStalenessUtil.parseRfc3339("2018-10-08T12:13:14Z")); - timestamps.put( - Mode.MIN_READ_TIMESTAMP, - ReadOnlyStalenessUtil.parseRfc3339("2018-10-08T14:13:14.1234+02:00")); - Map durations = new HashMap<>(); - durations.put(Mode.EXACT_STALENESS, new StalenessDuration(1000L, TimeUnit.MILLISECONDS)); - durations.put(Mode.MAX_STALENESS, new StalenessDuration(1234567L, TimeUnit.MICROSECONDS)); - List stalenesses = - Arrays.asList( - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(timestamps.get(Mode.READ_TIMESTAMP)), - TimestampBound.ofMinReadTimestamp(timestamps.get(Mode.MIN_READ_TIMESTAMP)), - TimestampBound.ofExactStaleness( - durations.get(Mode.EXACT_STALENESS).duration, - durations.get(Mode.EXACT_STALENESS).unit), - TimestampBound.ofMaxStaleness( - durations.get(Mode.MAX_STALENESS).duration, - durations.get(Mode.MAX_STALENESS).unit)); - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - for (TimestampBound staleness : stalenesses) { - StatementResult res = - subject.execute( - Statement.of( - String.format( - "set read_only_staleness='%s'", - ReadOnlyStalenessUtil.timestampBoundToString(staleness)))); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getReadOnlyStaleness(), is(equalTo(staleness))); - } - } - } - - @Test - public void testExecuteBeginTransaction() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isInTransaction(), is(false)); - - StatementResult res = subject.execute(Statement.of("begin transaction")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isInTransaction(), is(true)); - } - } - - @Test - public void testExecuteCommitTransaction() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.execute(Statement.of("begin transaction")); - assertThat(subject.isInTransaction(), is(true)); - - StatementResult res = subject.execute(Statement.of("commit")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isInTransaction(), is(false)); - } - } - - @Test - public void testExecuteRollbackTransaction() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.execute(Statement.of("begin")); - assertThat(subject.isInTransaction(), is(true)); - - StatementResult res = subject.execute(Statement.of("rollback")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.isInTransaction(), is(false)); - } - } - - @Test - public void testExecuteSetTransactionReadOnly() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.execute(Statement.of("begin")); - assertThat(subject.getTransactionMode(), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat(subject.isInTransaction(), is(true)); - - StatementResult res = subject.execute(Statement.of("set transaction read only")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getTransactionMode(), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - } - } - - @Test - public void testExecuteSetTransactionReadWrite() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI + ";readonly=true") - .build())) { - subject.execute(Statement.of("begin")); - assertThat(subject.getTransactionMode(), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat(subject.isInTransaction(), is(true)); - - // end the current temporary transaction and turn off read-only mode - subject.execute(Statement.of("commit")); - subject.execute(Statement.of("set readonly = false")); - - subject.execute(Statement.of("begin")); - StatementResult res = subject.execute(Statement.of("set transaction read only")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getTransactionMode(), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - res = subject.execute(Statement.of("set transaction read write")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getTransactionMode(), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - } - } - - @Test - public void testExecuteStartDdlBatch() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - StatementResult res = subject.execute(Statement.of("start batch ddl")); - assertThat(res.getResultType(), is(equalTo(ResultType.NO_RESULT))); - assertThat(subject.getUnitOfWorkType(), is(equalTo(UnitOfWorkType.DDL_BATCH))); - assertThat(subject.isInTransaction(), is(false)); - } - } - - @Test - public void testDefaultIsAutocommit() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - assertThat(subject.isInTransaction(), is(false)); - } - } - - @Test - public void testDefaultIsReadWrite() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isReadOnly(), is(false)); - } - } - - @Test - public void testDefaultTransactionIsReadWrite() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - for (boolean autocommit : new Boolean[] {true, false}) { - subject.setAutocommit(autocommit); - subject.execute(Statement.of("begin")); - assertThat( - subject.getTransactionMode(), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - subject.commit(); - - subject.execute(Statement.of("begin")); - subject.execute(Statement.of("set transaction read only")); - assertThat( - subject.getTransactionMode(), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - subject.commit(); - - subject.execute(Statement.of("begin")); - assertThat( - subject.getTransactionMode(), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - subject.commit(); - - subject.execute(Statement.of("start batch ddl")); - assertThat(subject.getUnitOfWorkType(), is(equalTo(UnitOfWorkType.DDL_BATCH))); - subject.runBatch(); - - subject.execute(Statement.of("begin")); - assertThat( - subject.getTransactionMode(), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - subject.commit(); - } - } - } - - @Test - public void testDefaultTransactionIsReadOnly() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI + ";readOnly=true") - .build())) { - for (boolean autocommit : new Boolean[] {true, false}) { - subject.setAutocommit(autocommit); - subject.execute(Statement.of("begin")); - assertThat( - subject.getTransactionMode(), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - subject.commit(); - } - } - } - - /** - * ReadOnlyStaleness is a session setting for a connection. However, certain settings are only - * allowed when the connection is in autocommit mode. The setting therefore must be reset to its - * default {@link TimestampBound#strong()} when the current setting is not compatible with - * transactional mode. - */ - @Test - public void testResetReadOnlyStaleness() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.isAutocommit(), is(true)); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - - // the following values are always allowed - subject.setReadOnlyStaleness(TimestampBound.strong()); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - subject.setAutocommit(false); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - subject.setAutocommit(true); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - - subject.setReadOnlyStaleness(TimestampBound.ofReadTimestamp(Timestamp.MAX_VALUE)); - subject.setAutocommit(false); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofReadTimestamp(Timestamp.MAX_VALUE)))); - subject.setAutocommit(true); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofReadTimestamp(Timestamp.MAX_VALUE)))); - - subject.setReadOnlyStaleness(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - subject.setAutocommit(false); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)))); - subject.setAutocommit(true); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)))); - - // the following values are only allowed in autocommit mode. Turning off autocommit will - // return the setting to its default - subject.setReadOnlyStaleness(TimestampBound.ofMinReadTimestamp(Timestamp.MAX_VALUE)); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofMinReadTimestamp(Timestamp.MAX_VALUE)))); - subject.setAutocommit(false); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - subject.setAutocommit(true); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - - subject.setReadOnlyStaleness(TimestampBound.ofMaxStaleness(10L, TimeUnit.SECONDS)); - assertThat( - subject.getReadOnlyStaleness(), - is(equalTo(TimestampBound.ofMaxStaleness(10L, TimeUnit.SECONDS)))); - subject.setAutocommit(false); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - subject.setAutocommit(true); - assertThat(subject.getReadOnlyStaleness().getMode(), is(equalTo(TimestampBound.Mode.STRONG))); - } - } - - @Test - public void testChangeReadOnlyModeInAutocommit() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.execute(Statement.of(UPDATE)); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - - // change to read-only - subject.setReadOnly(true); - expectSpannerException( - "Updates should not be allowed in read-only mode", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.execute(Statement.of(UPDATE)); - } - }, - subject); - assertThat(subject.executeQuery(Statement.of(SELECT)), is(notNullValue())); - - // change back to read-write - subject.setReadOnly(false); - subject.execute(Statement.of(UPDATE)); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - - // and back to read-only - subject.setReadOnly(true); - expectSpannerException( - "DDL should not be allowed in read-only mode", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.execute(Statement.of(DDL)); - } - }, - subject); - assertThat(subject.executeQuery(Statement.of(SELECT)), is(notNullValue())); - } - } - - @Test - public void testChangeReadOnlyModeInTransactionalMode() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - subject.setAutocommit(false); - - subject.execute(Statement.of(UPDATE)); - subject.commit(); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - - // change to read-only - subject.setReadOnly(true); - expectSpannerException( - "Updates should not be allowed in read-only mode", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.execute(Statement.of(UPDATE)); - } - }, - subject); - assertThat(subject.executeQuery(Statement.of(SELECT)), is(notNullValue())); - subject.commit(); - - // change back to read-write - subject.setReadOnly(false); - subject.execute(Statement.of(UPDATE)); - subject.commit(); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - - // and back to read-only - subject.setReadOnly(true); - expectSpannerException( - "DDL should not be allowed in read-only mode", - new ConnectionConsumer() { - @Override - public void accept(Connection t) { - t.execute(Statement.of(DDL)); - } - }, - subject); - assertThat(subject.executeQuery(Statement.of(SELECT)), is(notNullValue())); - } - } - - @Test - public void testAddRemoveTransactionRetryListener() { - try (ConnectionImpl subject = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(subject.getTransactionRetryListeners().hasNext(), is(false)); - TransactionRetryListener listener = mock(TransactionRetryListener.class); - subject.addTransactionRetryListener(listener); - assertThat(subject.getTransactionRetryListeners().hasNext(), is(true)); - assertThat(subject.removeTransactionRetryListener(listener), is(true)); - assertThat(subject.getTransactionRetryListeners().hasNext(), is(false)); - assertThat(subject.removeTransactionRetryListener(listener), is(false)); - } - } - - @Test - public void testMergeQueryOptions() { - ConnectionOptions connectionOptions = mock(ConnectionOptions.class); - SpannerPool spannerPool = mock(SpannerPool.class); - DdlClient ddlClient = mock(DdlClient.class); - DatabaseClient dbClient = mock(DatabaseClient.class); - final UnitOfWork unitOfWork = mock(UnitOfWork.class); - try (ConnectionImpl impl = - new ConnectionImpl(connectionOptions, spannerPool, ddlClient, dbClient) { - @Override - UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() { - return unitOfWork; - } - }) { - // Execute query with an optimizer version set on the connection. - impl.setOptimizerVersion("1"); - impl.executeQuery(Statement.of("SELECT FOO FROM BAR")); - verify(unitOfWork) - .executeQuery( - StatementParser.INSTANCE.parse( - Statement.newBuilder("SELECT FOO FROM BAR") - .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("1").build()) - .build()), - AnalyzeMode.NONE); - - // Execute query with an optimizer version set on the connection. - impl.setOptimizerVersion("2"); - impl.executeQuery(Statement.of("SELECT FOO FROM BAR")); - verify(unitOfWork) - .executeQuery( - StatementParser.INSTANCE.parse( - Statement.newBuilder("SELECT FOO FROM BAR") - .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("2").build()) - .build()), - AnalyzeMode.NONE); - - // Execute query with an optimizer version set on the connection and PrefetchChunks query - // option specified for the query. - QueryOption prefetchOption = Options.prefetchChunks(100); - impl.setOptimizerVersion("3"); - impl.executeQuery(Statement.of("SELECT FOO FROM BAR"), prefetchOption); - verify(unitOfWork) - .executeQuery( - StatementParser.INSTANCE.parse( - Statement.newBuilder("SELECT FOO FROM BAR") - .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("3").build()) - .build()), - AnalyzeMode.NONE, - prefetchOption); - - // Execute query with an optimizer version set on the connection, and the same options also - // passed in to the query. The specific options passed in to the query should take precedence. - impl.setOptimizerVersion("4"); - impl.executeQuery( - Statement.newBuilder("SELECT FOO FROM BAR") - .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("5").build()) - .build(), - prefetchOption); - verify(unitOfWork) - .executeQuery( - StatementParser.INSTANCE.parse( - Statement.newBuilder("SELECT FOO FROM BAR") - .withQueryOptions(QueryOptions.newBuilder().setOptimizerVersion("5").build()) - .build()), - AnalyzeMode.NONE, - prefetchOption); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadOnlyTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadOnlyTest.java deleted file mode 100644 index 99c95908..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadOnlyTest.java +++ /dev/null @@ -1,1204 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import java.util.concurrent.TimeUnit; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; - -/** - * The tests in this class do not need to be implemented for client libraries in other programming - * languages, as all test cases are covered by the file ConnectionImplGeneratedSqlScriptTest.sql - */ -@RunWith(Enclosed.class) -public class ConnectionImplTransactionalReadOnlyTest { - - public static class ConnectionImplTransactionalReadOnlyNoActionsTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query has been executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterSelectTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - // no call to next() on ResultSet - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // transaction has started - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return true; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last statement was a query, next() has not yet been called, but as the connection api - // returns a directly executed resultset, the read timestamp is already available - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterSelectAndResultSetNextTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)).next(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // transaction is running - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // transaction has started - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return true; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last statement was a query - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterBeginTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // connection is in read-only mode - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // in a transaction, only exact allowed - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last action was a transaction that ended with a select query - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterRollbackTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("ROLLBACK;"); - connection.rollback(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // transaction was rolled back - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterSetReadOnlyMaxStalenessTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - TimestampBound staleness = TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS); - log( - "SET READ_ONLY_STALENESS='" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "';"); - connection.setReadOnlyStaleness(staleness); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadOnlyAfterEmptyCommitTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=TRUE;"); - connection.setReadOnly(true); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return mode == TransactionMode.READ_ONLY_TRANSACTION; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last commit was empty - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // read-only - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadWriteTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadWriteTest.java deleted file mode 100644 index ff58828e..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionImplTransactionalReadWriteTest.java +++ /dev/null @@ -1,1945 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TimestampBound.Mode; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import java.util.concurrent.TimeUnit; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; - -/** - * The tests in this class do not need to be implemented for client libraries in other programming - * languages, as all test cases are covered by the file ConnectionImplGeneratedSqlScriptTest.sql - */ -@RunWith(Enclosed.class) -public class ConnectionImplTransactionalReadWriteTest { - - public static class ConnectionImplTransactionalReadWriteNoActionsTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query has been executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterSelectTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - // no call to next() on ResultSet - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // transaction has started - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return true; - } - - @Override - boolean isGetReadTimestampAllowed() { - // read-write transactions never have a read-timestamp - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterSelectAndResultSetNextTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.executeQuery(Statement.of(SELECT)).next(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - // transaction is running - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // transaction has started - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return true; - } - - @Override - boolean isGetReadTimestampAllowed() { - // read-write transactions never have a read-timestamp - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterBeginTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalArgumentException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - // in a transaction, only exact allowed - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // read-write transaction never have a read-timestamp - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // last action was a read-write transaction - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return true; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterRollbackTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("BEGIN TRANSACTION;"); - connection.beginTransaction(); - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("ROLLBACK;"); - connection.rollback(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // transaction was rolled back - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterSetReadOnlyMaxStalenessTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - TimestampBound staleness = TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS); - log( - "SET READ_ONLY_STALENESS='" - + ReadOnlyStalenessUtil.timestampBoundToString(staleness) - + "';"); - connection.setReadOnlyStaleness(staleness); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterSetTransactionReadOnlyTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("SET TRANSACTION READ ONLY;"); - connection.setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.QUERY; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterCommittedReadOnlyTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("SET TRANSACTION READ ONLY;"); - connection.setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - // ensure there will be a read-timestamp available by calling next() - log("@EXPECT RESULT_SET 'TEST',1"); - log(SELECT + ";"); - connection.execute(Statement.of(SELECT)).getResultSet().next(); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return true; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // last transaction was a read-only transaction - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterStartDdlBatchTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("START BATCH DDL;"); - connection.startBatchDdl(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return false; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // no commit yet - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.DDL; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return true; - } - - @Override - boolean isAbortBatchAllowed() { - return true; - } - } - - public static class ConnectionImplTransactionalReadWriteInDdlBatchTransactionTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("START BATCH DDL;"); - connection.startBatchDdl(); - log(DDL + ";"); - connection.execute(Statement.of(DDL)); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - throw new IllegalStateException(); - } - - @Override - boolean isSetAutocommitAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyAllowed() { - return false; - } - - @Override - boolean isBeginTransactionAllowed() { - return false; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return false; - } - - @Override - boolean isGetTransactionModeAllowed() { - return false; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return false; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return false; - } - - @Override - boolean isCommitAllowed() { - return false; - } - - @Override - boolean isRollbackAllowed() { - return false; - } - - @Override - boolean expectedIsInTransaction() { - return false; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE || type == StatementType.DDL; - } - - @Override - boolean isWriteAllowed() { - return false; - } - - @Override - boolean isStartBatchDmlAllowed() { - return false; - } - - @Override - boolean isStartBatchDdlAllowed() { - return false; - } - - @Override - boolean isRunBatchAllowed() { - return true; - } - - @Override - boolean isAbortBatchAllowed() { - return true; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterRanDdlBatchTest - extends AbstractConnectionImplTest { - - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("START BATCH DDL;"); - connection.startBatchDdl(); - log(DDL + ";"); - connection.execute(Statement.of(DDL)); - log("RUN BATCH;"); - connection.runBatch(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // ddl-batch has no commit timestamp - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } - - public static class ConnectionImplTransactionalReadWriteAfterEmptyCommitTest - extends AbstractConnectionImplTest { - @Override - Connection getConnection() { - log("NEW_CONNECTION;"); - Connection connection = - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build()); - log("SET READONLY=FALSE;"); - connection.setReadOnly(false); - log("SET AUTOCOMMIT=FALSE;"); - connection.setAutocommit(false); - log("COMMIT;"); - connection.commit(); - return connection; - } - - @Override - boolean isSelectAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDmlAllowedAfterBeginTransaction() { - return true; - } - - @Override - boolean isDdlAllowedAfterBeginTransaction() { - return false; - } - - @Override - boolean isSetAutocommitAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyAllowed() { - return true; - } - - @Override - boolean isBeginTransactionAllowed() { - return true; - } - - @Override - boolean isSetTransactionModeAllowed(TransactionMode mode) { - return true; - } - - @Override - boolean isGetTransactionModeAllowed() { - return true; - } - - @Override - boolean isSetAutocommitDmlModeAllowed() { - return false; - } - - @Override - boolean isGetAutocommitDmlModeAllowed() { - return true; - } - - @Override - boolean isSetReadOnlyStalenessAllowed(TimestampBound.Mode mode) { - return mode == Mode.STRONG || mode == Mode.EXACT_STALENESS || mode == Mode.READ_TIMESTAMP; - } - - @Override - boolean isGetReadOnlyStalenessAllowed() { - return true; - } - - @Override - boolean isCommitAllowed() { - return true; - } - - @Override - boolean isRollbackAllowed() { - return true; - } - - @Override - boolean expectedIsInTransaction() { - return true; - } - - @Override - boolean expectedIsTransactionStarted() { - return false; - } - - @Override - boolean isGetReadTimestampAllowed() { - // no query has been executed yet - return false; - } - - @Override - boolean isGetCommitTimestampAllowed() { - // empty commit - return false; - } - - @Override - boolean isExecuteAllowed(StatementType type) { - return type == StatementType.CLIENT_SIDE - || type == StatementType.QUERY - || type == StatementType.UPDATE; - } - - @Override - boolean isWriteAllowed() { - return true; - } - - @Override - boolean isStartBatchDmlAllowed() { - return true; - } - - @Override - boolean isStartBatchDdlAllowed() { - return true; - } - - @Override - boolean isRunBatchAllowed() { - return false; - } - - @Override - boolean isAbortBatchAllowed() { - return false; - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionOptionsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionOptionsTest.java deleted file mode 100644 index 8eacbbf3..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionOptionsTest.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.spanner.SpannerOptions; -import java.util.Arrays; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ConnectionOptionsTest { - private static final String FILE_TEST_PATH = - ConnectionOptionsTest.class.getResource("test-key.json").getFile(); - private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; - - @Test - public void testBuildWithURIWithDots() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner:/projects/some-company.com:test-project-123/instances/test-instance-123/databases/test-database-123"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo(DEFAULT_HOST); - assertThat(options.getProjectId()).isEqualTo("some-company.com:test-project-123"); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(ConnectionOptions.DEFAULT_AUTOCOMMIT); - assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); - } - - @Test - public void testBuildWithValidURIAndCredentialsFileURL() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo(DEFAULT_HOST); - assertThat(options.getProjectId()).isEqualTo("test-project-123"); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(ConnectionOptions.DEFAULT_AUTOCOMMIT); - assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); - } - - @Test - public void testBuildWithValidURIAndProperties() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autocommit=false;readonly=true"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo(DEFAULT_HOST); - assertThat(options.getProjectId()).isEqualTo("test-project-123"); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(false); - assertThat(options.isReadOnly()).isEqualTo(true); - } - - @Test - public void testBuildWithHostAndValidURI() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner://test-spanner.googleapis.com/projects/test-project-123/instances/test-instance-123/databases/test-database-123"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo("https://test-spanner.googleapis.com"); - assertThat(options.getProjectId()).isEqualTo("test-project-123"); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(ConnectionOptions.DEFAULT_AUTOCOMMIT); - assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); - } - - @Test - public void testBuildWithLocalhostPortAndValidURI() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner://localhost:8443/projects/test-project-123/instances/test-instance-123/databases/test-database-123"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo("https://localhost:8443"); - assertThat(options.getProjectId()).isEqualTo("test-project-123"); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(ConnectionOptions.DEFAULT_AUTOCOMMIT); - assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); - } - - @Test - public void testBuildWithDefaultProjectPlaceholder() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - builder.setUri( - "cloudspanner:/projects/default_project_id/instances/test-instance-123/databases/test-database-123"); - builder.setCredentialsUrl(FILE_TEST_PATH); - ConnectionOptions options = builder.build(); - assertThat(options.getHost()).isEqualTo(DEFAULT_HOST); - String projectId = SpannerOptions.getDefaultProjectId(); - if (projectId == null) { - projectId = - ((ServiceAccountCredentials) new CredentialsService().createCredentials(FILE_TEST_PATH)) - .getProjectId(); - } - assertThat(options.getProjectId()).isEqualTo(projectId); - assertThat(options.getInstanceId()).isEqualTo("test-instance-123"); - assertThat(options.getDatabaseName()).isEqualTo("test-database-123"); - assertThat(options.getCredentials()) - .isEqualTo(new CredentialsService().createCredentials(FILE_TEST_PATH)); - assertThat(options.isAutocommit()).isEqualTo(ConnectionOptions.DEFAULT_AUTOCOMMIT); - assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); - } - - @Test - public void testBuilderSetUri() { - ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); - - // set valid uri's - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"); - builder.setUri("cloudspanner:/projects/test-project-123/instances/test-instance"); - builder.setUri("cloudspanner:/projects/test-project-123"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance/databases/test-database"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance"); - builder.setUri("cloudspanner://spanner.googleapis.com/projects/test-project-123"); - - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?autocommit=true"); - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance?autocommit=true"); - builder.setUri("cloudspanner:/projects/test-project-123?autocommit=true"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance/databases/test-database?autocommit=true"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance?autocommit=true"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123?autocommit=true"); - - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?autocommit=true;readonly=false"); - builder.setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance?autocommit=true;readonly=false"); - builder.setUri("cloudspanner:/projects/test-project-123?autocommit=true;readonly=false"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance/databases/test-database?autocommit=true;readonly=false"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123/instances/test-instance?autocommit=true;readonly=false"); - builder.setUri( - "cloudspanner://spanner.googleapis.com/projects/test-project-123?autocommit=true;readonly=false"); - - // set invalid uri's - setInvalidUri( - builder, "/projects/test-project-123/instances/test-instance/databases/test-database"); - setInvalidUri(builder, "cloudspanner:/test-project-123/test-instance/test-database"); - setInvalidUri( - builder, - "cloudspanner:spanner.googleapis.com/projects/test-project-123/instances/test-instance/databases/test-database"); - setInvalidUri( - builder, - "cloudspanner://spanner.googleapis.com/projects/test-project-$$$/instances/test-instance/databases/test-database"); - setInvalidUri( - builder, - "cloudspanner://spanner.googleapis.com/projects/test-project-123/databases/test-database"); - setInvalidUri( - builder, - "cloudspanner:/projects/test_project_123/instances/test-instance/databases/test-database"); - - // Set URI's that are valid, but that contain unknown properties. - setInvalidProperty( - builder, - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?read=false", - "read"); - setInvalidProperty( - builder, - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?read=false;autocommit=true", - "read"); - setInvalidProperty( - builder, - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?read=false;auto=true", - "read, auto"); - } - - private void setInvalidUri(ConnectionOptions.Builder builder, String uri) { - try { - builder.setUri(uri); - fail(uri + " should be considered an invalid uri"); - } catch (IllegalArgumentException e) { - } - } - - private void setInvalidProperty( - ConnectionOptions.Builder builder, String uri, String expectedInvalidProperties) { - try { - builder.setUri(uri); - fail("missing expected exception"); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains(expectedInvalidProperties); - } - } - - @Test - public void testParseUriProperty() { - final String baseUri = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - - assertThat(ConnectionOptions.parseUriProperty(baseUri, "autocommit")).isNull(); - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?autocommit=true", "autocommit")) - .isEqualTo("true"); - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?autocommit=false", "autocommit")) - .isEqualTo("false"); - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?autocommit=true;", "autocommit")) - .isEqualTo("true"); - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?autocommit=false;", "autocommit")) - .isEqualTo("false"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?autocommit=true;readOnly=false", "autocommit")) - .isEqualTo("true"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?autocommit=false;readOnly=false", "autocommit")) - .isEqualTo("false"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?readOnly=false;autocommit=true", "autocommit")) - .isEqualTo("true"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?readOnly=false;autocommit=false", "autocommit")) - .isEqualTo("false"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?readOnly=false;autocommit=true;foo=bar", "autocommit")) - .isEqualTo("true"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?readOnly=false;autocommit=false;foo=bar", "autocommit")) - .isEqualTo("false"); - - // case insensitive - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?AutoCommit=true", "autocommit")) - .isEqualTo("true"); - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?AutoCommit=false", "autocommit")) - .isEqualTo("false"); - - // ; instead of ? before the properties is ok - assertThat(ConnectionOptions.parseUriProperty(baseUri + ";autocommit=true", "autocommit")) - .isEqualTo("true"); - - // forgot the ? or ; before the properties - assertThat(ConnectionOptions.parseUriProperty(baseUri + "autocommit=true", "autocommit")) - .isNull(); - // substring is not ok - assertThat(ConnectionOptions.parseUriProperty(baseUri + "?isautocommit=true", "autocommit")) - .isNull(); - } - - @Test - public void testParseProperties() { - final String baseUri = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - assertThat(ConnectionOptions.parseProperties(baseUri + "?autocommit=true")) - .isEqualTo(Arrays.asList("autocommit")); - assertThat(ConnectionOptions.parseProperties(baseUri + "?autocommit=true;readonly=false")) - .isEqualTo(Arrays.asList("autocommit", "readonly")); - assertThat(ConnectionOptions.parseProperties(baseUri + "?autocommit=true;READONLY=false")) - .isEqualTo(Arrays.asList("autocommit", "READONLY")); - assertThat(ConnectionOptions.parseProperties(baseUri + ";autocommit=true;readonly=false")) - .isEqualTo(Arrays.asList("autocommit", "readonly")); - assertThat(ConnectionOptions.parseProperties(baseUri + ";autocommit=true;readonly=false;")) - .isEqualTo(Arrays.asList("autocommit", "readonly")); - } - - @Test - public void testParsePropertiesSpecifiedMultipleTimes() { - final String baseUri = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?autocommit=true;autocommit=false", "autocommit")) - .isEqualTo("true"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + "?autocommit=false;autocommit=true", "autocommit")) - .isEqualTo("false"); - assertThat( - ConnectionOptions.parseUriProperty( - baseUri + ";autocommit=false;readonly=false;autocommit=true", "autocommit")) - .isEqualTo("false"); - ConnectionOptions.newBuilder() - .setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database" - + ";autocommit=false;readonly=false;autocommit=true"); - } - - @Test - public void testParseOAuthToken() { - assertThat( - ConnectionOptions.parseUriProperty( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database" - + "?oauthtoken=RsT5OjbzRn430zqMLgV3Ia", - "OAuthToken")) - .isEqualTo("RsT5OjbzRn430zqMLgV3Ia"); - // Try to use both credentials and an OAuth token. That should fail. - ConnectionOptions.Builder builder = - ConnectionOptions.newBuilder() - .setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database" - + "?OAuthToken=RsT5OjbzRn430zqMLgV3Ia;credentials=/path/to/credentials.json"); - try { - builder.build(); - fail("missing expected exception"); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains("Cannot specify both credentials and an OAuth token"); - } - - // Now try to use only an OAuth token. - builder = - ConnectionOptions.newBuilder() - .setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database" - + "?OAuthToken=RsT5OjbzRn430zqMLgV3Ia"); - ConnectionOptions options = builder.build(); - assertThat(options.getCredentials()).isInstanceOf(GoogleCredentials.class); - GoogleCredentials credentials = (GoogleCredentials) options.getCredentials(); - assertThat(credentials.getAccessToken().getTokenValue()).isEqualTo("RsT5OjbzRn430zqMLgV3Ia"); - } - - @Test - public void testSetOAuthToken() { - ConnectionOptions options = - ConnectionOptions.newBuilder() - .setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database") - .setOAuthToken("RsT5OjbzRn430zqMLgV3Ia") - .build(); - assertThat(options.getCredentials()).isInstanceOf(GoogleCredentials.class); - GoogleCredentials credentials = (GoogleCredentials) options.getCredentials(); - assertThat(credentials.getAccessToken()).isNotNull(); - assertThat(credentials.getAccessToken().getTokenValue()).isEqualTo("RsT5OjbzRn430zqMLgV3Ia"); - } - - @Test - public void testSetOAuthTokenAndCredentials() { - try { - ConnectionOptions.newBuilder() - .setUri( - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database") - .setOAuthToken("RsT5OjbzRn430zqMLgV3Ia") - .setCredentialsUrl(FILE_TEST_PATH) - .build(); - fail("missing expected exception"); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains("Cannot specify both credentials and an OAuth token"); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorTest.java deleted file mode 100644 index 6e524c49..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementExecutorTest.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.TimestampBound; -import com.google.protobuf.Duration; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ConnectionStatementExecutorTest { - - private ConnectionImpl connection; - private ConnectionStatementExecutorImpl subject; - - @Before - public void createSubject() { - connection = mock(ConnectionImpl.class); - when(connection.getAutocommitDmlMode()).thenReturn(AutocommitDmlMode.TRANSACTIONAL); - when(connection.getReadOnlyStaleness()).thenReturn(TimestampBound.strong()); - subject = new ConnectionStatementExecutorImpl(connection); - } - - @Test - public void testGetConnection() { - assertThat(subject.getConnection(), is(equalTo(connection))); - } - - @Test - public void testStatementBeginTransaction() { - subject.statementBeginTransaction(); - verify(connection).beginTransaction(); - } - - @Test - public void testStatementCommit() { - subject.statementCommit(); - verify(connection).commit(); - } - - @Test - public void testStatementGetAutocommit() { - subject.statementShowAutocommit(); - verify(connection).isAutocommit(); - } - - @Test - public void testStatementGetAutocommitDmlMode() { - subject.statementShowAutocommitDmlMode(); - verify(connection).getAutocommitDmlMode(); - } - - @Test - public void testStatementGetCommitTimestamp() { - subject.statementShowCommitTimestamp(); - verify(connection).getCommitTimestampOrNull(); - } - - @Test - public void testStatementGetReadOnly() { - subject.statementShowReadOnly(); - verify(connection).isReadOnly(); - } - - @Test - public void testStatementGetReadOnlyStaleness() { - subject.statementShowReadOnlyStaleness(); - verify(connection).getReadOnlyStaleness(); - } - - @Test - public void testStatementGetOptimizerVersion() { - subject.statementShowOptimizerVersion(); - verify(connection).getOptimizerVersion(); - } - - @Test - public void testStatementGetReadTimestamp() { - subject.statementShowReadTimestamp(); - verify(connection).getReadTimestampOrNull(); - } - - @Test - public void testStatementGetStatementTimeout() { - subject.statementSetStatementTimeout(Duration.newBuilder().setSeconds(1L).build()); - when(connection.hasStatementTimeout()).thenReturn(true); - subject.statementShowStatementTimeout(); - verify(connection, atLeastOnce()).getStatementTimeout(any(TimeUnit.class)); - subject.statementSetStatementTimeout(Duration.getDefaultInstance()); - when(connection.hasStatementTimeout()).thenReturn(false); - } - - @Test - public void testStatementRollback() { - subject.statementRollback(); - verify(connection).rollback(); - } - - @Test - public void testStatementSetAutocommit() { - subject.statementSetAutocommit(Boolean.TRUE); - verify(connection).setAutocommit(true); - subject.statementSetAutocommit(Boolean.FALSE); - verify(connection).setAutocommit(false); - } - - @Test - public void testStatementSetAutocommitDmlMode() { - subject.statementSetAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - verify(connection).setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - subject.statementSetAutocommitDmlMode(AutocommitDmlMode.TRANSACTIONAL); - verify(connection).setAutocommitDmlMode(AutocommitDmlMode.TRANSACTIONAL); - } - - @Test - public void testStatementSetReadOnly() { - subject.statementSetReadOnly(Boolean.TRUE); - verify(connection).setReadOnly(true); - subject.statementSetReadOnly(Boolean.FALSE); - verify(connection).setReadOnly(false); - } - - @Test - public void testStatementSetReadOnlyStaleness() { - subject.statementSetReadOnlyStaleness(TimestampBound.strong()); - verify(connection).setReadOnlyStaleness(TimestampBound.strong()); - - subject.statementSetReadOnlyStaleness( - TimestampBound.ofReadTimestamp(Timestamp.parseTimestamp("2018-10-31T10:11:12.123Z"))); - verify(connection) - .setReadOnlyStaleness( - TimestampBound.ofReadTimestamp(Timestamp.parseTimestamp("2018-10-31T10:11:12.123Z"))); - - subject.statementSetReadOnlyStaleness( - TimestampBound.ofMinReadTimestamp(Timestamp.parseTimestamp("2018-10-31T10:11:12.123Z"))); - verify(connection) - .setReadOnlyStaleness( - TimestampBound.ofReadTimestamp(Timestamp.parseTimestamp("2018-10-31T10:11:12.123Z"))); - - subject.statementSetReadOnlyStaleness(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - verify(connection).setReadOnlyStaleness(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - - subject.statementSetReadOnlyStaleness( - TimestampBound.ofMaxStaleness(20L, TimeUnit.MILLISECONDS)); - verify(connection) - .setReadOnlyStaleness(TimestampBound.ofMaxStaleness(20L, TimeUnit.MILLISECONDS)); - } - - @Test - public void testStatementSetOptimizerVersion() { - subject.statementSetOptimizerVersion("1"); - verify(connection).setOptimizerVersion("1"); - subject.statementSetOptimizerVersion(""); - verify(connection).setOptimizerVersion(""); - subject.statementSetOptimizerVersion("LATEST"); - verify(connection).setOptimizerVersion("LATEST"); - } - - @Test - public void testStatementSetStatementTimeout() { - subject.statementSetStatementTimeout(Duration.newBuilder().setNanos(100).build()); - verify(connection).setStatementTimeout(100L, TimeUnit.NANOSECONDS); - } - - @Test - public void testStatementSetTransactionMode() { - subject.statementSetTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - verify(connection).setTransactionMode(TransactionMode.READ_ONLY_TRANSACTION); - subject.statementSetTransactionMode(TransactionMode.READ_WRITE_TRANSACTION); - verify(connection).setTransactionMode(TransactionMode.READ_WRITE_TRANSACTION); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithNoParametersTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithNoParametersTest.java deleted file mode 100644 index 51aa8abe..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithNoParametersTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import java.util.concurrent.TimeUnit; -import org.junit.Test; - -public class ConnectionStatementWithNoParametersTest { - private final StatementParser parser = StatementParser.INSTANCE; - - @Test - public void testExecuteGetAutocommit() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable autocommit")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowAutocommit()).thenCallRealMethod(); - statement.getClientSideStatement().execute(executor, "show variable autocommit"); - verify(connection, times(1)).isAutocommit(); - } - - @Test - public void testExecuteGetReadOnly() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable readonly")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowReadOnly()).thenCallRealMethod(); - statement.getClientSideStatement().execute(executor, "show variable readonly"); - verify(connection, times(1)).isReadOnly(); - } - - @Test - public void testExecuteGetAutocommitDmlMode() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable autocommit_dml_mode")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowAutocommitDmlMode()).thenCallRealMethod(); - when(connection.getAutocommitDmlMode()).thenReturn(AutocommitDmlMode.TRANSACTIONAL); - statement.getClientSideStatement().execute(executor, "show variable autocommit_dml_mode"); - verify(connection, times(1)).getAutocommitDmlMode(); - } - - @Test - public void testExecuteGetStatementTimeout() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable statement_timeout")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowStatementTimeout()).thenCallRealMethod(); - when(connection.hasStatementTimeout()).thenReturn(true); - when(connection.getStatementTimeout(TimeUnit.NANOSECONDS)).thenReturn(1L); - statement.getClientSideStatement().execute(executor, "show variable statement_timeout"); - verify(connection, times(2)).getStatementTimeout(TimeUnit.NANOSECONDS); - } - - @Test - public void testExecuteGetReadTimestamp() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable read_timestamp")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowReadTimestamp()).thenCallRealMethod(); - when(connection.getReadTimestampOrNull()).thenReturn(Timestamp.now()); - statement.getClientSideStatement().execute(executor, "show variable read_timestamp"); - verify(connection, times(1)).getReadTimestampOrNull(); - } - - @Test - public void testExecuteGetCommitTimestamp() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable commit_timestamp")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowCommitTimestamp()).thenCallRealMethod(); - when(connection.getCommitTimestampOrNull()).thenReturn(Timestamp.now()); - statement.getClientSideStatement().execute(executor, "show variable commit_timestamp"); - verify(connection, times(1)).getCommitTimestampOrNull(); - } - - @Test - public void testExecuteGetReadOnlyStaleness() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable read_only_staleness")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowReadOnlyStaleness()).thenCallRealMethod(); - when(connection.getReadOnlyStaleness()).thenReturn(TimestampBound.strong()); - statement.getClientSideStatement().execute(executor, "show variable read_only_staleness"); - verify(connection, times(1)).getReadOnlyStaleness(); - } - - @Test - public void testExecuteGetOptimizerVersion() throws Exception { - ParsedStatement statement = parser.parse(Statement.of("show variable optimizer_version")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementShowOptimizerVersion()).thenCallRealMethod(); - when(connection.getOptimizerVersion()).thenReturn("1"); - statement.getClientSideStatement().execute(executor, "show variable optimizer_version"); - verify(connection, times(1)).getOptimizerVersion(); - } - - @Test - public void testExecuteBegin() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("begin")); - for (String statement : subject.getClientSideStatement().getExampleStatements()) { - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementBeginTransaction()).thenCallRealMethod(); - subject.getClientSideStatement().execute(executor, statement); - verify(connection, times(1)).beginTransaction(); - } - } - - @Test - public void testExecuteCommit() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("commit")); - for (String statement : subject.getClientSideStatement().getExampleStatements()) { - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementCommit()).thenCallRealMethod(); - subject.getClientSideStatement().execute(executor, statement); - verify(connection, times(1)).commit(); - } - } - - @Test - public void testExecuteRollback() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("rollback")); - for (String statement : subject.getClientSideStatement().getExampleStatements()) { - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementRollback()).thenCallRealMethod(); - subject.getClientSideStatement().execute(executor, statement); - verify(connection, times(1)).rollback(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithOneParameterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithOneParameterTest.java deleted file mode 100644 index b9ebf1ed..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionStatementWithOneParameterTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.protobuf.Duration; -import java.util.concurrent.TimeUnit; -import org.junit.Test; - -public class ConnectionStatementWithOneParameterTest { - private final StatementParser parser = StatementParser.INSTANCE; - - @Test - public void testExecuteSetAutcommit() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set autocommit = true")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetAutocommit(any(Boolean.class))).thenCallRealMethod(); - for (Boolean mode : new Boolean[] {Boolean.FALSE, Boolean.TRUE}) { - subject - .getClientSideStatement() - .execute(executor, String.format("set autocommit = %s", mode)); - verify(connection, times(1)).setAutocommit(mode); - } - } - - @Test - public void testExecuteSetReadOnly() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set readonly = true")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetReadOnly(any(Boolean.class))).thenCallRealMethod(); - for (Boolean mode : new Boolean[] {Boolean.FALSE, Boolean.TRUE}) { - subject - .getClientSideStatement() - .execute(executor, String.format("set readonly = %s", Boolean.toString(mode))); - verify(connection, times(1)).setReadOnly(mode); - } - } - - @Test - public void testExecuteSetAutcommitDmlMode() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set autocommit_dml_mode='foo'")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetAutocommitDmlMode(any(AutocommitDmlMode.class))).thenCallRealMethod(); - for (AutocommitDmlMode mode : AutocommitDmlMode.values()) { - subject - .getClientSideStatement() - .execute(executor, String.format("set autocommit_dml_mode='%s'", mode.name())); - verify(connection, times(1)).setAutocommitDmlMode(mode); - } - } - - @Test - public void testExecuteSetStatementTimeout() throws Exception { - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.statementSetStatementTimeout(any(Duration.class))).thenCallRealMethod(); - ConnectionImpl connection = mock(ConnectionImpl.class); - when(executor.getConnection()).thenReturn(connection); - for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { - for (Long val : new Long[] {1L, 100L, 999L}) { - ParsedStatement subject = - parser.parse( - Statement.of( - String.format( - "set statement_timeout='%d%s'", - val, ReadOnlyStalenessUtil.getTimeUnitAbbreviation(unit)))); - subject - .getClientSideStatement() - .execute( - executor, - String.format( - "set statement_timeout='%d%s'", - val, ReadOnlyStalenessUtil.getTimeUnitAbbreviation(unit))); - verify(connection, times(1)).setStatementTimeout(val, unit); - } - } - ParsedStatement subject = - parser.parse(Statement.of(String.format("set statement_timeout=null"))); - subject.getClientSideStatement().execute(executor, String.format("set statement_timeout=null")); - verify(connection, times(1)).clearStatementTimeout(); - } - - @Test - public void testExecuteSetReadOnlyStaleness() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set read_only_staleness='foo'")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetReadOnlyStaleness(any(TimestampBound.class))).thenCallRealMethod(); - for (TimestampBound val : - new TimestampBound[] { - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(Timestamp.now()), - TimestampBound.ofMinReadTimestamp(Timestamp.now()), - TimestampBound.ofExactStaleness(1000L, TimeUnit.SECONDS), - TimestampBound.ofMaxStaleness(2000L, TimeUnit.MICROSECONDS) - }) { - subject - .getClientSideStatement() - .execute( - executor, String.format("set read_only_staleness='%s'", timestampBoundToString(val))); - verify(connection, times(1)).setReadOnlyStaleness(val); - } - } - - private String timestampBoundToString(TimestampBound staleness) { - switch (staleness.getMode()) { - case STRONG: - return "strong"; - case READ_TIMESTAMP: - return "read_timestamp " + staleness.getReadTimestamp().toString(); - case MIN_READ_TIMESTAMP: - return "min_read_timestamp " + staleness.getMinReadTimestamp().toString(); - case EXACT_STALENESS: - return "exact_staleness " + staleness.getExactStaleness(TimeUnit.SECONDS) + "s"; - case MAX_STALENESS: - return "max_staleness " + staleness.getMaxStaleness(TimeUnit.MICROSECONDS) + "us"; - default: - throw new IllegalStateException("Unknown mode: " + staleness.getMode()); - } - } - - @Test - public void testExecuteSetOptimizerVersion() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set optimizer_version='foo'")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetOptimizerVersion(any(String.class))).thenCallRealMethod(); - for (String version : new String[] {"1", "200", "", "LATEST"}) { - subject - .getClientSideStatement() - .execute(executor, String.format("set optimizer_version='%s'", version)); - verify(connection, times(1)).setOptimizerVersion(version); - } - } - - @Test - public void testExecuteSetTransaction() throws Exception { - ParsedStatement subject = parser.parse(Statement.of("set transaction read_only")); - ConnectionImpl connection = mock(ConnectionImpl.class); - ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); - when(executor.getConnection()).thenReturn(connection); - when(executor.statementSetTransactionMode(any(TransactionMode.class))).thenCallRealMethod(); - for (TransactionMode mode : TransactionMode.values()) { - subject - .getClientSideStatement() - .execute(executor, String.format("set transaction %s", mode.getStatementString())); - verify(connection, times(1)).setTransactionMode(mode); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ConnectionTest.java deleted file mode 100644 index d99b9a73..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ConnectionTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerOptions; -import com.google.cloud.spanner.Statement; -import com.google.spanner.v1.ExecuteSqlRequest; -import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import java.sql.SQLException; -import org.junit.Test; - -public class ConnectionTest extends AbstractMockServerTest { - @Test - public void testDefaultOptimizerVersion() throws SQLException { - try (Connection connection = createConnection()) { - try (ResultSet rs = - connection.executeQuery(Statement.of("SHOW VARIABLE OPTIMIZER_VERSION"))) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getString("OPTIMIZER_VERSION")).isEqualTo(""); - assertThat(rs.next()).isFalse(); - } - } - } - - @Test - public void testUseOptimizerVersionFromEnvironment() throws SQLException { - try { - SpannerOptions.useEnvironment( - new SpannerOptions.SpannerEnvironment() { - @Override - public String getOptimizerVersion() { - return "20"; - } - }); - try (Connection connection = createConnection()) { - // Do a query and verify that the version from the environment is used. - try (ResultSet rs = connection.executeQuery(SELECT_COUNT_STATEMENT)) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getLong(0)).isEqualTo(COUNT_BEFORE_INSERT); - assertThat(rs.next()).isFalse(); - // Verify query options from the environment. - ExecuteSqlRequest request = getLastExecuteSqlRequest(); - assertThat(request.getQueryOptions().getOptimizerVersion()).isEqualTo("20"); - } - // Now set one of the query options on the connection. That option should be used in - // combination with the other option from the environment. - connection.execute(Statement.of("SET OPTIMIZER_VERSION='30'")); - try (ResultSet rs = connection.executeQuery(SELECT_COUNT_STATEMENT)) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getLong(0)).isEqualTo(COUNT_BEFORE_INSERT); - assertThat(rs.next()).isFalse(); - - ExecuteSqlRequest request = getLastExecuteSqlRequest(); - // Optimizer version should come from the connection. - assertThat(request.getQueryOptions().getOptimizerVersion()).isEqualTo("30"); - } - // Now specify options directly for the query. These should override both the environment - // and what is set on the connection. - try (ResultSet rs = - connection.executeQuery( - Statement.newBuilder(SELECT_COUNT_STATEMENT.getSql()) - .withQueryOptions( - QueryOptions.newBuilder() - .setOptimizerVersion("user-defined-version") - .build()) - .build())) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getLong(0)).isEqualTo(COUNT_BEFORE_INSERT); - assertThat(rs.next()).isFalse(); - - ExecuteSqlRequest request = getLastExecuteSqlRequest(); - // Optimizer version should come from the query. - assertThat(request.getQueryOptions().getOptimizerVersion()) - .isEqualTo("user-defined-version"); - } - } - } finally { - SpannerOptions.useDefaultEnvironment(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/CredentialsServiceTest.java b/src/test/java/com/google/cloud/spanner/jdbc/CredentialsServiceTest.java deleted file mode 100644 index 268d4a1a..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/CredentialsServiceTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import java.io.FileInputStream; -import java.io.IOException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for reading and parsing test key files and getting service accounts. */ -@RunWith(JUnit4.class) -public class CredentialsServiceTest { - private static final String FILE_TEST_PATH = - CredentialsServiceTest.class.getResource("test-key.json").getFile(); - private static final String APP_DEFAULT_FILE_TEST_PATH = - CredentialsServiceTest.class.getResource("test-key-app-default.json").getFile(); - - private static final String TEST_PROJECT_ID = "test-project"; - private static final String APP_DEFAULT_PROJECT_ID = "app-default-test-project"; - - private final CredentialsService service = - new CredentialsService() { - - @Override - GoogleCredentials internalGetApplicationDefault() throws IOException { - // Read application default credentials directly from a specific file instead of actually - // fetching the default from the environment. - return GoogleCredentials.fromStream(new FileInputStream(APP_DEFAULT_FILE_TEST_PATH)); - } - }; - - @Test - public void testCreateCredentialsDefault() throws Exception { - ServiceAccountCredentials credentials = - (ServiceAccountCredentials) service.createCredentials(null); - assertThat(credentials.getProjectId(), is(equalTo(APP_DEFAULT_PROJECT_ID))); - } - - @Test - public void testCreateCredentialsFile() throws IOException { - ServiceAccountCredentials credentials = - (ServiceAccountCredentials) service.createCredentials(FILE_TEST_PATH); - assertThat(credentials.getProjectId(), is(equalTo(TEST_PROJECT_ID))); - } - - @Test(expected = SpannerException.class) - public void testCreateCredentialsInvalidFile() { - service.createCredentials("invalid_file_path.json"); - } - - @Test - public void testCreateCredentialsInvalidCloudStorage() { - try { - service.createCredentials("gs://test-bucket/test-blob"); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.INVALID_ARGUMENT))); - assertThat(e.getCause().getMessage(), is(equalTo(CredentialsService.GCS_NOT_SUPPORTED_MSG))); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/DdlBatchTest.java b/src/test/java/com/google/cloud/spanner/jdbc/DdlBatchTest.java deleted file mode 100644 index 76a5cc5b..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/DdlBatchTest.java +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ReadContext; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerBatchUpdateException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ConnectionImpl.InternalMetadataQuery; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState; -import com.google.protobuf.Timestamp; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import io.grpc.Status; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentMatcher; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class DdlBatchTest { - - @Rule public ExpectedException exception = ExpectedException.none(); - - private DdlClient createDefaultMockDdlClient() { - return createDefaultMockDdlClient(false, 0L); - } - - private DdlClient createDefaultMockDdlClient(boolean exceptionOnGetResult) { - return createDefaultMockDdlClient(exceptionOnGetResult, 0L); - } - - private DdlClient createDefaultMockDdlClient(long waitForMillis) { - return createDefaultMockDdlClient(false, waitForMillis); - } - - private DdlClient createDefaultMockDdlClient( - boolean exceptionOnGetResult, final long waitForMillis) { - try { - DdlClient ddlClient = mock(DdlClient.class); - @SuppressWarnings("unchecked") - final OperationFuture operation = - mock(OperationFuture.class); - if (waitForMillis > 0L) { - when(operation.get()) - .thenAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(waitForMillis); - return null; - } - }); - } else if (exceptionOnGetResult) { - when(operation.get()) - .thenThrow( - SpannerExceptionFactory.newSpannerException( - ErrorCode.UNKNOWN, "ddl statement failed")); - } else { - when(operation.get()).thenReturn(null); - } - UpdateDatabaseDdlMetadata.Builder metadataBuilder = UpdateDatabaseDdlMetadata.newBuilder(); - if (!exceptionOnGetResult) { - metadataBuilder.addCommitTimestamps( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000L)); - } - ApiFuture metadataFuture = - ApiFutures.immediateFuture(metadataBuilder.build()); - when(operation.getMetadata()).thenReturn(metadataFuture); - when(ddlClient.executeDdl(anyString())).thenReturn(operation); - when(ddlClient.executeDdl(anyListOf(String.class))).thenReturn(operation); - return ddlClient; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private DdlBatch createSubject() { - return createSubject(createDefaultMockDdlClient()); - } - - private DdlBatch createSubject(DdlClient ddlClient) { - return createSubject(ddlClient, mock(DatabaseClient.class)); - } - - private DdlBatch createSubject(DdlClient ddlClient, DatabaseClient dbClient) { - return DdlBatch.newBuilder() - .setDdlClient(ddlClient) - .setDatabaseClient(dbClient) - .withStatementExecutor(new StatementExecutor()) - .build(); - } - - @Test - public void testExecuteQuery() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); - } - - @Test - public void testExecuteMetadataQuery() { - Statement statement = Statement.of("SELECT * FROM INFORMATION_SCHEMA.TABLES"); - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.isQuery()).thenReturn(true); - when(parsedStatement.getStatement()).thenReturn(statement); - DatabaseClient dbClient = mock(DatabaseClient.class); - ReadContext singleUse = mock(ReadContext.class); - ResultSet resultSet = mock(ResultSet.class); - when(singleUse.executeQuery(statement)).thenReturn(resultSet); - when(dbClient.singleUse()).thenReturn(singleUse); - DdlBatch batch = createSubject(createDefaultMockDdlClient(), dbClient); - assertThat( - batch - .executeQuery(parsedStatement, AnalyzeMode.NONE, InternalMetadataQuery.INSTANCE) - .hashCode(), - is(equalTo(resultSet.hashCode()))); - } - - @Test - public void testExecuteUpdate() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeUpdate(mock(ParsedStatement.class)); - } - - @Test - public void testGetCommitTimestamp() { - DdlBatch batch = createSubject(); - batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getCommitTimestamp(); - } - - @Test - public void testGetReadTimestamp() { - DdlBatch batch = createSubject(); - batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getReadTimestamp(); - } - - @Test - public void testWrite() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Mutation.newInsertBuilder("foo").build()); - } - - @Test - public void testWriteIterable() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); - } - - @Test - public void testIsReadOnly() { - DdlBatch batch = createSubject(); - assertThat(batch.isReadOnly(), is(false)); - } - - @Test - public void testGetStateAndIsActive() { - DdlBatch batch = createSubject(); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - batch.runBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.RAN)); - assertThat(batch.isActive(), is(false)); - - batch = createSubject(); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - batch.abortBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.ABORTED)); - assertThat(batch.isActive(), is(false)); - - DdlClient client = mock(DdlClient.class); - doThrow(SpannerException.class).when(client).executeDdl(anyListOf(String.class)); - batch = createSubject(client); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getStatement()).thenReturn(Statement.of("CREATE TABLE FOO")); - when(statement.getSqlWithoutComments()).thenReturn("CREATE TABLE FOO"); - when(statement.getType()).thenReturn(StatementType.DDL); - batch.executeDdl(statement); - boolean exception = false; - try { - batch.runBatch(); - } catch (SpannerException e) { - exception = true; - } - assertThat(exception, is(true)); - assertThat(batch.getState(), is(UnitOfWorkState.RUN_FAILED)); - assertThat(batch.isActive(), is(false)); - } - - private static IsListOfStringsWithSize isEmptyListOfStrings() { - return new IsListOfStringsWithSize(0); - } - - private static IsListOfStringsWithSize isListOfStringsWithSize(int size) { - return new IsListOfStringsWithSize(size); - } - - private static class IsListOfStringsWithSize extends ArgumentMatcher> { - private final int size; - - private IsListOfStringsWithSize(int size) { - this.size = size; - } - - @SuppressWarnings("unchecked") - @Override - public boolean matches(Object list) { - return ((List) list).size() == size; - } - } - - @Test - public void testRunBatch() { - DdlClient client = createDefaultMockDdlClient(); - DdlBatch batch = createSubject(client); - batch.runBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.RAN)); - verify(client, never()).executeDdl(anyString()); - verify(client, never()).executeDdl(argThat(isEmptyListOfStrings())); - - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.DDL); - when(statement.getStatement()).thenReturn(Statement.of("CREATE TABLE FOO")); - when(statement.getSqlWithoutComments()).thenReturn("CREATE TABLE FOO"); - - client = createDefaultMockDdlClient(); - batch = createSubject(client); - batch.executeDdl(statement); - batch.runBatch(); - verify(client).executeDdl(argThat(isListOfStringsWithSize(1))); - - client = createDefaultMockDdlClient(); - batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); - batch.runBatch(); - verify(client).executeDdl(argThat(isListOfStringsWithSize(2))); - assertThat(batch.getState(), is(UnitOfWorkState.RAN)); - boolean exception = false; - try { - batch.runBatch(); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { - throw e; - } - exception = true; - } - assertThat(exception, is(true)); - assertThat(batch.getState(), is(UnitOfWorkState.RAN)); - exception = false; - try { - batch.executeDdl(statement); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { - throw e; - } - exception = true; - } - assertThat(exception, is(true)); - exception = false; - try { - batch.executeDdl(statement); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { - throw e; - } - exception = true; - } - assertThat(exception, is(true)); - - client = createDefaultMockDdlClient(true); - batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); - exception = false; - try { - batch.runBatch(); - } catch (SpannerException e) { - exception = true; - } - assertThat(exception, is(true)); - assertThat(batch.getState(), is(UnitOfWorkState.RUN_FAILED)); - verify(client).executeDdl(argThat(isListOfStringsWithSize(2))); - } - - @Test - public void testUpdateCount() throws InterruptedException, ExecutionException { - DdlClient client = mock(DdlClient.class); - UpdateDatabaseDdlMetadata metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000L - 1L)) - .addCommitTimestamps( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000L)) - .addAllStatements(Arrays.asList("CREATE TABLE FOO", "CREATE TABLE BAR")) - .build(); - ApiFuture metadataFuture = ApiFutures.immediateFuture(metadata); - @SuppressWarnings("unchecked") - OperationFuture operationFuture = mock(OperationFuture.class); - when(operationFuture.get()).thenReturn(null); - when(operationFuture.getMetadata()).thenReturn(metadataFuture); - when(client.executeDdl(argThat(isListOfStringsWithSize(2)))).thenReturn(operationFuture); - DdlBatch batch = - DdlBatch.newBuilder() - .withStatementExecutor(new StatementExecutor()) - .setDdlClient(client) - .setDatabaseClient(mock(DatabaseClient.class)) - .build(); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE BAR"))); - long[] updateCounts = batch.runBatch(); - assertThat(updateCounts.length, is(equalTo(2))); - assertThat(updateCounts[0], is(equalTo(1L))); - assertThat(updateCounts[1], is(equalTo(1L))); - } - - @Test - public void testFailedUpdateCount() throws InterruptedException, ExecutionException { - DdlClient client = mock(DdlClient.class); - UpdateDatabaseDdlMetadata metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000L - 1L)) - .addAllStatements(Arrays.asList("CREATE TABLE FOO", "CREATE TABLE INVALID_TABLE")) - .build(); - ApiFuture metadataFuture = ApiFutures.immediateFuture(metadata); - @SuppressWarnings("unchecked") - OperationFuture operationFuture = mock(OperationFuture.class); - when(operationFuture.get()) - .thenThrow( - new ExecutionException( - "ddl statement failed", Status.INVALID_ARGUMENT.asRuntimeException())); - when(operationFuture.getMetadata()).thenReturn(metadataFuture); - when(client.executeDdl(argThat(isListOfStringsWithSize(2)))).thenReturn(operationFuture); - DdlBatch batch = - DdlBatch.newBuilder() - .withStatementExecutor(new StatementExecutor()) - .setDdlClient(client) - .setDatabaseClient(mock(DatabaseClient.class)) - .build(); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE FOO"))); - batch.executeDdl(StatementParser.INSTANCE.parse(Statement.of("CREATE TABLE INVALID_TABLE"))); - try { - batch.runBatch(); - fail("missing expected exception"); - } catch (SpannerBatchUpdateException e) { - assertThat(e.getUpdateCounts().length, is(equalTo(2))); - assertThat(e.getUpdateCounts()[0], is(equalTo(1L))); - assertThat(e.getUpdateCounts()[1], is(equalTo(0L))); - } - } - - @Test - public void testAbort() { - DdlClient client = createDefaultMockDdlClient(); - DdlBatch batch = createSubject(client); - batch.abortBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.ABORTED)); - verify(client, never()).executeDdl(anyString()); - verify(client, never()).executeDdl(anyListOf(String.class)); - - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.DDL); - when(statement.getStatement()).thenReturn(Statement.of("CREATE TABLE FOO")); - when(statement.getSqlWithoutComments()).thenReturn("CREATE TABLE FOO"); - - client = createDefaultMockDdlClient(); - batch = createSubject(client); - batch.executeDdl(statement); - batch.abortBatch(); - verify(client, never()).executeDdl(anyListOf(String.class)); - - client = createDefaultMockDdlClient(); - batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); - batch.abortBatch(); - verify(client, never()).executeDdl(anyListOf(String.class)); - - client = createDefaultMockDdlClient(); - batch = createSubject(client); - batch.executeDdl(statement); - batch.executeDdl(statement); - batch.abortBatch(); - verify(client, never()).executeDdl(anyListOf(String.class)); - boolean exception = false; - try { - batch.runBatch(); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.FAILED_PRECONDITION) { - throw e; - } - exception = true; - } - assertThat(exception, is(true)); - verify(client, never()).executeDdl(anyListOf(String.class)); - } - - @Test - public void testCancel() { - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.DDL); - when(statement.getStatement()).thenReturn(Statement.of("CREATE TABLE FOO")); - when(statement.getSqlWithoutComments()).thenReturn("CREATE TABLE FOO"); - - DdlClient client = createDefaultMockDdlClient(10000L); - final DdlBatch batch = createSubject(client); - batch.executeDdl(statement); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - batch.cancel(); - } - }, - 100, - TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - batch.runBatch(); - } - - @Test - public void testCommit() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.commit(); - } - - @Test - public void testRollback() { - DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.rollback(); - } - - @Test - public void testExtractUpdateCounts() { - DdlBatch batch = createSubject(); - UpdateDatabaseDdlMetadata metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(1000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(2000L).build()) - .addStatements("CREATE TABLE FOO") - .addStatements("CREATE TABLE BAR") - .addStatements("CREATE TABLE BAZ") - .build(); - long[] updateCounts = batch.extractUpdateCounts(metadata); - assertThat(updateCounts, is(equalTo(new long[] {1L, 1L, 0L}))); - - metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(1000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(2000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(3000L).build()) - .addStatements("CREATE TABLE FOO") - .addStatements("CREATE TABLE BAR") - .addStatements("CREATE TABLE BAZ") - .build(); - updateCounts = batch.extractUpdateCounts(metadata); - assertThat(updateCounts, is(equalTo(new long[] {1L, 1L, 1L}))); - - metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(1000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(2000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(3000L).build()) - .addStatements("CREATE TABLE FOO") - .addStatements("CREATE TABLE BAR") - .addStatements("CREATE TABLE BAZ") - .build(); - updateCounts = batch.extractUpdateCounts(metadata); - assertThat(updateCounts, is(equalTo(new long[] {1L, 1L, 1L}))); - - // This is not something Cloud Spanner should return, but the method can handle it. - metadata = - UpdateDatabaseDdlMetadata.newBuilder() - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(1000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(2000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(3000L).build()) - .addCommitTimestamps(Timestamp.newBuilder().setSeconds(4000L).build()) - .addStatements("CREATE TABLE FOO") - .addStatements("CREATE TABLE BAR") - .addStatements("CREATE TABLE BAZ") - .build(); - updateCounts = batch.extractUpdateCounts(metadata); - assertThat(updateCounts, is(equalTo(new long[] {1L, 1L, 1L}))); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/DdlClientTest.java b/src/test/java/com/google/cloud/spanner/jdbc/DdlClientTest.java deleted file mode 100644 index 8ec9f600..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/DdlClientTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.spanner.DatabaseAdminClient; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutionException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DdlClientTest { - - private final String instanceId = "test-instance"; - private final String databaseId = "test-database"; - - private DdlClient createSubject(DatabaseAdminClient client) { - return DdlClient.newBuilder() - .setInstanceId(instanceId) - .setDatabaseName(databaseId) - .setDatabaseAdminClient(client) - .build(); - } - - @Test - public void testExecuteDdl() throws InterruptedException, ExecutionException { - DatabaseAdminClient client = mock(DatabaseAdminClient.class); - @SuppressWarnings("unchecked") - OperationFuture operation = mock(OperationFuture.class); - when(operation.get()).thenReturn(null); - when(client.updateDatabaseDdl( - eq(instanceId), eq(databaseId), anyListOf(String.class), isNull(String.class))) - .thenReturn(operation); - DdlClient subject = createSubject(client); - String ddl = "CREATE TABLE FOO"; - subject.executeDdl(ddl); - verify(client).updateDatabaseDdl(instanceId, databaseId, Arrays.asList(ddl), null); - - subject = createSubject(client); - List ddlList = Arrays.asList("CREATE TABLE FOO", "DROP TABLE FOO"); - subject.executeDdl(ddlList); - verify(client).updateDatabaseDdl(instanceId, databaseId, ddlList, null); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSetTest.java deleted file mode 100644 index 533c417b..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/DirectExecuteResultSetTest.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.ResultSets; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Type.StructField; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DirectExecuteResultSetTest { - - private DirectExecuteResultSet createSubject() { - ResultSet delegate = - ResultSets.forRows( - Type.struct(StructField.of("test", Type.int64())), - Arrays.asList(Struct.newBuilder().set("test").to(1L).build())); - return DirectExecuteResultSet.ofResultSet(delegate); - } - - @Test - public void testMethodCallBeforeNext() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = - Arrays.asList("getStats", "next", "close", "ofResultSet", "equals", "hashCode"); - DirectExecuteResultSet subject = createSubject(); - callMethods(subject, excludedMethods, IllegalStateException.class); - } - - @Test - public void testMethodCallAfterClose() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = - Arrays.asList( - "getStats", - "next", - "close", - "getType", - "getColumnCount", - "getColumnIndex", - "getColumnType", - "ofResultSet", - "equals", - "hashCode"); - DirectExecuteResultSet subject = createSubject(); - subject.next(); - subject.close(); - callMethods(subject, excludedMethods, IllegalStateException.class); - } - - @Test - public void testMethodCallAfterNextHasReturnedFalse() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = - Arrays.asList( - "getStats", - "next", - "close", - "getType", - "getColumnCount", - "getColumnIndex", - "getColumnType", - "ofResultSet", - "equals", - "hashCode"); - DirectExecuteResultSet subject = createSubject(); - subject.next(); - subject.next(); - callMethods(subject, excludedMethods, IndexOutOfBoundsException.class); - } - - private void callMethods( - DirectExecuteResultSet subject, - List excludedMethods, - Class expectedException) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - for (Method method : DirectExecuteResultSet.class.getDeclaredMethods()) { - if (Modifier.isPublic(method.getModifiers()) && !excludedMethods.contains(method.getName())) { - boolean exception = false; - int numberOfParameters = method.getParameterTypes().length; - Class firstParameterType = null; - if (numberOfParameters == 1) { - firstParameterType = method.getParameterTypes()[0]; - } - try { - switch (numberOfParameters) { - case 0: - method.invoke(subject); - break; - case 1: - if (firstParameterType == String.class) { - method.invoke(subject, "test"); - } else if (firstParameterType == int.class) { - method.invoke(subject, 0); - } else { - fail("unknown parameter type"); - } - break; - default: - fail("method with more than 1 parameter is unknown"); - } - } catch (InvocationTargetException e) { - if (e.getCause().getClass().equals(expectedException)) { - // expected - exception = true; - } else { - throw e; - } - } - assertThat( - method.getName() + " did not throw an IllegalStateException", exception, is(true)); - } - } - } - - @Test - public void testValidMethodCall() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - ResultSet delegate = mock(ResultSet.class); - when(delegate.next()).thenReturn(true, true, false); - DirectExecuteResultSet subject = DirectExecuteResultSet.ofResultSet(delegate); - subject.next(); - - subject.getBoolean(0); - verify(delegate).getBoolean(0); - subject.getBoolean("test0"); - verify(delegate).getBoolean("test0"); - subject.getBooleanArray(1); - verify(delegate).getBooleanArray(1); - subject.getBooleanArray("test1"); - verify(delegate).getBooleanArray("test1"); - subject.getBooleanList(2); - verify(delegate).getBooleanList(2); - subject.getBooleanList("test2"); - verify(delegate).getBooleanList("test2"); - - subject.getBytes(0); - verify(delegate).getBytes(0); - subject.getBytes("test0"); - verify(delegate).getBytes("test0"); - subject.getBytesList(2); - verify(delegate).getBytesList(2); - subject.getBytesList("test2"); - verify(delegate).getBytesList("test2"); - - subject.getDate(0); - verify(delegate).getDate(0); - subject.getDate("test0"); - verify(delegate).getDate("test0"); - subject.getDateList(2); - verify(delegate).getDateList(2); - subject.getDateList("test2"); - verify(delegate).getDateList("test2"); - - subject.getDouble(0); - verify(delegate).getDouble(0); - subject.getDouble("test0"); - verify(delegate).getDouble("test0"); - subject.getDoubleArray(1); - verify(delegate).getDoubleArray(1); - subject.getDoubleArray("test1"); - verify(delegate).getDoubleArray("test1"); - subject.getDoubleList(2); - verify(delegate).getDoubleList(2); - subject.getDoubleList("test2"); - verify(delegate).getDoubleList("test2"); - - subject.getLong(0); - verify(delegate).getLong(0); - subject.getLong("test0"); - verify(delegate).getLong("test0"); - subject.getLongArray(1); - verify(delegate).getLongArray(1); - subject.getLongArray("test1"); - verify(delegate).getLongArray("test1"); - subject.getLongList(2); - verify(delegate).getLongList(2); - subject.getLongList("test2"); - verify(delegate).getLongList("test2"); - - subject.getString(0); - verify(delegate).getString(0); - subject.getString("test0"); - verify(delegate).getString("test0"); - subject.getStringList(2); - verify(delegate).getStringList(2); - subject.getStringList("test2"); - verify(delegate).getStringList("test2"); - - subject.getStructList(0); - subject.getStructList("test0"); - - subject.getTimestamp(0); - verify(delegate).getTimestamp(0); - subject.getTimestamp("test0"); - verify(delegate).getTimestamp("test0"); - subject.getTimestampList(2); - verify(delegate).getTimestampList(2); - subject.getTimestampList("test2"); - verify(delegate).getTimestampList("test2"); - - subject.getColumnCount(); - verify(delegate).getColumnCount(); - subject.getColumnIndex("test"); - verify(delegate).getColumnIndex("test"); - subject.getColumnType(100); - verify(delegate).getColumnType(100); - subject.getColumnType("test"); - verify(delegate).getColumnType("test"); - subject.getCurrentRowAsStruct(); - verify(delegate).getCurrentRowAsStruct(); - subject.getType(); - verify(delegate).getType(); - subject.isNull(50); - verify(delegate).isNull(50); - subject.isNull("test"); - verify(delegate).isNull("test"); - - while (subject.next()) { - // ignore - } - subject.getStats(); - verify(delegate).getStats(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/DmlBatchTest.java b/src/test/java/com/google/cloud/spanner/jdbc/DmlBatchTest.java deleted file mode 100644 index b8a96cda..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/DmlBatchTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState; -import java.util.Arrays; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DmlBatchTest { - private final ParsedStatement statement1 = - StatementParser.INSTANCE.parse(Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2")); - private final ParsedStatement statement2 = - StatementParser.INSTANCE.parse(Statement.of("UPDATE FOO SET BAR=2 WHERE BAZ=3")); - - @Rule public ExpectedException exception = ExpectedException.none(); - - private DmlBatch createSubject() { - UnitOfWork transaction = mock(UnitOfWork.class); - when(transaction.executeBatchUpdate(Arrays.asList(statement1, statement2))) - .thenReturn(new long[] {3L, 5L}); - return createSubject(transaction); - } - - private DmlBatch createSubject(UnitOfWork transaction) { - return DmlBatch.newBuilder() - .setTransaction(transaction) - .withStatementExecutor(new StatementExecutor()) - .build(); - } - - @Test - public void testExecuteQuery() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); - } - - @Test - public void testExecuteDdl() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeDdl(mock(ParsedStatement.class)); - } - - @Test - public void testGetReadTimestamp() { - DmlBatch batch = createSubject(); - batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getReadTimestamp(); - } - - @Test - public void testIsReadOnly() { - DmlBatch batch = createSubject(); - assertThat(batch.isReadOnly(), is(false)); - } - - @Test - public void testGetCommitTimestamp() { - DmlBatch batch = createSubject(); - batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getCommitTimestamp(); - } - - @Test - public void testWrite() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Mutation.newInsertBuilder("foo").build()); - } - - @Test - public void testWriteIterable() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); - } - - @Test - public void testGetStateAndIsActive() { - DmlBatch batch = createSubject(); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - batch.runBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.RAN)); - assertThat(batch.isActive(), is(false)); - - batch = createSubject(); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - batch.abortBatch(); - assertThat(batch.getState(), is(UnitOfWorkState.ABORTED)); - assertThat(batch.isActive(), is(false)); - - UnitOfWork tx = mock(UnitOfWork.class); - doThrow(SpannerException.class).when(tx).executeBatchUpdate(anyListOf(ParsedStatement.class)); - batch = createSubject(tx); - assertThat(batch.getState(), is(UnitOfWorkState.STARTED)); - assertThat(batch.isActive(), is(true)); - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getStatement()).thenReturn(Statement.of("UPDATE TEST SET COL1=2")); - when(statement.getSqlWithoutComments()).thenReturn("UPDATE TEST SET COL1=2"); - when(statement.getType()).thenReturn(StatementType.UPDATE); - batch.executeUpdate(statement); - boolean exception = false; - try { - batch.runBatch(); - } catch (SpannerException e) { - exception = true; - } - assertThat(exception, is(true)); - assertThat(batch.getState(), is(UnitOfWorkState.RUN_FAILED)); - assertThat(batch.isActive(), is(false)); - } - - @Test - public void testCommit() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.commit(); - } - - @Test - public void testRollback() { - DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.rollback(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/DurationConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/DurationConverterTest.java deleted file mode 100644 index 77b41e9d..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/DurationConverterTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.ClientSideStatementValueConverters.DurationConverter; -import com.google.protobuf.Duration; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DurationConverterTest { - - @Test - public void testConvert() throws CompileException { - String allowedValues = ReadOnlyStalenessConverterTest.getAllowedValues(DurationConverter.class); - assertThat(allowedValues, is(notNullValue())); - DurationConverter converter = new DurationConverter(allowedValues); - assertThat( - converter.convert("'100ms'"), - is( - equalTo( - Duration.newBuilder() - .setNanos((int) TimeUnit.MILLISECONDS.toNanos(100L)) - .build()))); - assertThat(converter.convert("'0ms'"), is(nullValue())); - assertThat(converter.convert("'-100ms'"), is(nullValue())); - assertThat( - converter.convert("'315576000000000ms'"), - is(equalTo(Duration.newBuilder().setSeconds(315576000000L).build()))); - assertThat( - converter.convert("'1000ms'"), is(equalTo(Duration.newBuilder().setSeconds(1L).build()))); - assertThat( - converter.convert("'1001ms'"), - is( - equalTo( - Duration.newBuilder() - .setSeconds(1L) - .setNanos((int) TimeUnit.MILLISECONDS.toNanos(1L)) - .build()))); - - assertThat(converter.convert("'1ns'"), is(equalTo(Duration.newBuilder().setNanos(1).build()))); - assertThat( - converter.convert("'1us'"), is(equalTo(Duration.newBuilder().setNanos(1000).build()))); - assertThat( - converter.convert("'1ms'"), is(equalTo(Duration.newBuilder().setNanos(1000000).build()))); - assertThat( - converter.convert("'999999999ns'"), - is(equalTo(Duration.newBuilder().setNanos(999999999).build()))); - assertThat( - converter.convert("'1s'"), is(equalTo(Duration.newBuilder().setSeconds(1L).build()))); - - assertThat(converter.convert("''"), is(nullValue())); - assertThat(converter.convert("' '"), is(nullValue())); - assertThat(converter.convert("'random string'"), is(nullValue())); - - assertThat(converter.convert("null"), is(equalTo(Duration.getDefaultInstance()))); - assertThat(converter.convert("NULL"), is(equalTo(Duration.getDefaultInstance()))); - assertThat(converter.convert("Null"), is(equalTo(Duration.getDefaultInstance()))); - assertThat(converter.convert("'null'"), is(nullValue())); - assertThat(converter.convert("'NULL'"), is(nullValue())); - assertThat(converter.convert("'Null'"), is(nullValue())); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java index eab1fcc5..2d173977 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java @@ -20,7 +20,8 @@ import com.google.cloud.spanner.GceTestEnvConfig; import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.IntegrationTestEnv; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; +import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier; +import com.google.cloud.spanner.connection.ITAbstractSpannerTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier.JdbcGenericConnection; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -41,7 +42,9 @@ /** Base class for all JDBC integration tests. */ @Category(IntegrationTest.class) public class ITAbstractJdbcTest { - protected class ITJdbcConnectionProvider implements GenericConnectionProvider { + protected class ITJdbcConnectionProvider + implements com.google.cloud.spanner.connection.AbstractSqlScriptVerifier + .GenericConnectionProvider { public ITJdbcConnectionProvider() {} @Override diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractSpannerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractSpannerTest.java deleted file mode 100644 index 7cd39c7a..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractSpannerTest.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.Database; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.GceTestEnvConfig; -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.IntegrationTestEnv; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.SpannerOptions; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionManager.TransactionState; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import java.io.IOException; -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Random; -import java.util.concurrent.ExecutionException; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.experimental.categories.Category; - -/** - * Base class for integration tests. This class is located in this package to be able to access - * package-private methods of the Connection API - */ -@Category(IntegrationTest.class) -public abstract class ITAbstractSpannerTest { - protected class ITConnectionProvider implements GenericConnectionProvider { - public ITConnectionProvider() {} - - @Override - public GenericConnection getConnection() { - return SpannerGenericConnection.of(createConnection()); - } - } - - protected interface ITConnection extends Connection {} - - private ITConnection createITConnection(ConnectionOptions options) { - return new ITConnectionImpl(options); - } - - protected void closeSpanner() { - ConnectionOptions.closeSpanner(); - } - - public static class AbortInterceptor implements StatementExecutionInterceptor { - /** We need to replicate the enum here as it is not visibible outside the connection package */ - public enum ExecutionStep { - /** The initial execution of a statement (DML/Query) */ - EXECUTE_STATEMENT, - /** A call to {@link ResultSet#next()} */ - CALL_NEXT_ON_RESULT_SET, - /** Execution of the statement during a transaction retry */ - RETRY_STATEMENT, - /** A call to {@link ResultSet#next()} during transaction retry */ - RETRY_NEXT_ON_RESULT_SET; - - static ExecutionStep of(StatementExecutionStep step) { - return ExecutionStep.valueOf(step.name()); - } - } - - private double probability; - private boolean onlyInjectOnce = false; - private final Random random = new Random(); - - public AbortInterceptor(double probability) { - Preconditions.checkArgument(probability >= 0.0D && probability <= 1.0D); - this.probability = probability; - } - - public void setProbability(double probability) { - Preconditions.checkArgument(probability >= 0.0D && probability <= 1.0D); - this.probability = probability; - } - - /** Set this value to true to automatically set the probability to zero after an abort */ - public void setOnlyInjectOnce(boolean value) { - this.onlyInjectOnce = value; - } - - protected boolean shouldAbort(String statement, ExecutionStep step) { - return probability > random.nextDouble(); - } - - @Override - public void intercept( - ParsedStatement statement, StatementExecutionStep step, UnitOfWork transaction) { - if (shouldAbort(statement.getSqlWithoutComments(), ExecutionStep.of(step))) { - // ugly hack warning: inject the aborted state into the transaction manager to simulate an - // abort - if (transaction instanceof ReadWriteTransaction) { - try { - Field field = ReadWriteTransaction.class.getDeclaredField("txManager"); - field.setAccessible(true); - TransactionManager tx = (TransactionManager) field.get(transaction); - Class cls = Class.forName("com.google.cloud.spanner.TransactionManagerImpl"); - Class cls2 = - Class.forName("com.google.cloud.spanner.SessionPool$AutoClosingTransactionManager"); - Field delegateField = cls2.getDeclaredField("delegate"); - delegateField.setAccessible(true); - TransactionManager delegate = (TransactionManager) delegateField.get(tx); - Field stateField = cls.getDeclaredField("txnState"); - stateField.setAccessible(true); - - // First rollback the delegate, and then pretend it aborted. - // We should call rollback on the delegate and not the wrapping - // AutoClosingTransactionManager, as the latter would cause the session to be returned - // to the session pool. - delegate.rollback(); - stateField.set(delegate, TransactionState.ABORTED); - } catch (Exception e) { - throw new RuntimeException(e); - } - if (onlyInjectOnce) { - probability = 0; - } - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.ABORTED, "Transaction was aborted by interceptor"); - } - } - } - } - - @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - private static final String DEFAULT_KEY_FILE = null; - private static Database database; - - protected static String getKeyFile() { - return System.getProperty(GceTestEnvConfig.GCE_CREDENTIALS_FILE, DEFAULT_KEY_FILE); - } - - protected static boolean hasValidKeyFile() { - return getKeyFile() != null && Files.exists(Paths.get(getKeyFile())); - } - - protected static IntegrationTestEnv getTestEnv() { - return env; - } - - protected static Database getDatabase() { - return database; - } - - /** - * Returns a connection URL that is extracted from the given {@link SpannerOptions} and database - * in the form - * cloudspanner:[//host]/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID - */ - static StringBuilder extractConnectionUrl(SpannerOptions options, Database database) { - StringBuilder url = new StringBuilder("cloudspanner:"); - if (options.getHost() != null) { - url.append(options.getHost().substring(options.getHost().indexOf(':') + 1)); - } - url.append("/").append(database.getId().getName()); - return url; - } - - @BeforeClass - public static void setup() throws IOException, InterruptedException, ExecutionException { - database = env.getTestHelper().createTestDatabase(); - } - - /** - * Creates a new default connection to a test database. Use the method {@link - * ITAbstractSpannerTest#appendConnectionUri(StringBuilder)} to append additional connection - * options to the connection URI. - * - * @return the newly opened connection. - */ - public ITConnection createConnection() { - return createConnection( - Collections.emptyList(), - Collections.emptyList()); - } - - public ITConnection createConnection(AbortInterceptor interceptor) { - return createConnection( - Arrays.asList(interceptor), - Collections.emptyList()); - } - - public ITConnection createConnection( - AbortInterceptor interceptor, TransactionRetryListener transactionRetryListener) { - return createConnection( - Arrays.asList(interceptor), - Arrays.asList(transactionRetryListener)); - } - - /** - * Creates a new default connection to a test database. Use the method {@link - * ITAbstractSpannerTest#appendConnectionUri(StringBuilder)} to append additional connection - * options to the connection URI. - * - * @param interceptors Interceptors that should be executed after each statement - * @param transactionRetryListeners Transaction retry listeners that should be added to the {@link - * Connection} - * @return the newly opened connection. - */ - public ITConnection createConnection( - List interceptors, - List transactionRetryListeners) { - StringBuilder url = - extractConnectionUrl(getTestEnv().getTestHelper().getOptions(), getDatabase()); - appendConnectionUri(url); - ConnectionOptions.Builder builder = - ConnectionOptions.newBuilder() - .setUri(url.toString()) - .setStatementExecutionInterceptors(interceptors); - if (hasValidKeyFile()) { - builder.setCredentialsUrl(getKeyFile()); - } - ConnectionOptions options = builder.build(); - ITConnection connection = createITConnection(options); - for (TransactionRetryListener listener : transactionRetryListeners) { - connection.addTransactionRetryListener(listener); - } - return connection; - } - - protected void appendConnectionUri(StringBuilder uri) {} - - /** - * Override this method to instruct the test to create a default test table in the form: - * - *

-   * CREATE TABLE TEST (ID INT64 NOT NULL, NAME STRING(100) NOT NULL) PRIMARY KEY (ID)
-   * 
- * - * Note that the table is not re-created for each test case, but is preserved between test cases. - * It is the responsibility of the test class to either empty the table at the end of each test - * case, or keep track of the state of the test table and execute the test cases in a specific - * order. - * - * @return true if the default test table should be created. - */ - protected boolean doCreateDefaultTestTable() { - return false; - } - - @Before - public void createTestTable() throws Exception { - if (doCreateDefaultTestTable()) { - try (Connection connection = createConnection()) { - connection.setAutocommit(true); - if (!tableExists(connection, "TEST")) { - connection.setAutocommit(false); - connection.startBatchDdl(); - connection.execute( - Statement.of( - "CREATE TABLE TEST (ID INT64 NOT NULL, NAME STRING(100) NOT NULL) PRIMARY KEY (ID)")); - connection.runBatch(); - } - } - } - } - - protected boolean tableExists(Connection connection, String table) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(table)); - try (ResultSet rs = - connection.executeQuery( - Statement.newBuilder( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_NAME)=@table_name") - .bind("table_name") - .to(table.toUpperCase()) - .build())) { - while (rs.next()) { - return true; - } - } - return false; - } - - protected boolean indexExists(Connection connection, String table, String index) { - Preconditions.checkArgument(!Strings.isNullOrEmpty(index)); - try (ResultSet rs = - connection.executeQuery( - Statement.newBuilder( - "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE UPPER(TABLE_NAME)=@table_name AND UPPER(INDEX_NAME)=@index_name") - .bind("table_name") - .to(table) - .bind("index_name") - .to(index.toUpperCase()) - .build())) { - while (rs.next()) { - return true; - } - } - return false; - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITConnectionImpl.java b/src/test/java/com/google/cloud/spanner/jdbc/ITConnectionImpl.java deleted file mode 100644 index c6361d0f..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ITConnectionImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest.ITConnection; - -/** Implementation of {@link ITConnection} for Spanner generic (not JDBC) connections. */ -class ITConnectionImpl extends ConnectionImpl implements ITConnection { - ITConnectionImpl(ConnectionOptions options) { - super(options); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcAbortedTransactionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcAbortedTransactionTest.java index 681f6052..9c562839 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcAbortedTransactionTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcAbortedTransactionTest.java @@ -26,6 +26,9 @@ import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.admin.database.v1.MockDatabaseAdminImpl; import com.google.cloud.spanner.admin.instance.v1.MockInstanceAdminImpl; +import com.google.cloud.spanner.connection.RandomResultSetGenerator; +import com.google.cloud.spanner.connection.SpannerPool; +import com.google.cloud.spanner.connection.TransactionRetryListener; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcAbortedDueToConcurrentModificationException; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcAbortedException; import com.google.protobuf.ListValue; @@ -154,7 +157,7 @@ private int getRetryCount(Connection connection) throws SQLException { return ((TransactionRetryCounter) connection .unwrap(CloudSpannerJdbcConnection.class) - .getTransactionRetryListeners() + .getTransactionRetryListenersFromConnection() .next()) .retriesFinished; } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java index fb4fccbc..b835668b 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java @@ -19,8 +19,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; +import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier.GenericConnection; +import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier.GenericConnectionProvider; +import com.google.cloud.spanner.connection.ConnectionImplTest; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier.JdbcGenericConnection; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +43,7 @@ static class TestConnectionProvider implements GenericConnectionProvider { public GenericConnection getConnection() { ConnectionOptions options = mock(ConnectionOptions.class); when(options.getUri()).thenReturn(ConnectionImplTest.URI); - com.google.cloud.spanner.jdbc.Connection spannerConnection = + com.google.cloud.spanner.connection.Connection spannerConnection = ConnectionImplTest.createConnection(options); when(options.getConnection()).thenReturn(spannerConnection); JdbcConnection connection = diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java index 8d49440a..a80e8009 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java @@ -29,6 +29,9 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.connection.AbstractConnectionImplTest; +import com.google.cloud.spanner.connection.ConnectionImplTest; +import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; import com.google.rpc.Code; import java.lang.reflect.InvocationTargetException; @@ -57,7 +60,7 @@ public class JdbcConnectionTest { Arrays.asList(Struct.newBuilder().set("").to(1L).build())); private JdbcConnection createConnection(ConnectionOptions options) { - com.google.cloud.spanner.jdbc.Connection spannerConnection = + com.google.cloud.spanner.connection.Connection spannerConnection = ConnectionImplTest.createConnection(options); when(options.getConnection()).thenReturn(spannerConnection); return new JdbcConnection( @@ -427,8 +430,8 @@ public void testSetClientInfo() throws SQLException { public void testIsValid() throws SQLException { // Setup. ConnectionOptions options = mock(ConnectionOptions.class); - com.google.cloud.spanner.jdbc.Connection spannerConnection = - mock(com.google.cloud.spanner.jdbc.Connection.class); + com.google.cloud.spanner.connection.Connection spannerConnection = + mock(com.google.cloud.spanner.connection.Connection.class); when(options.getConnection()).thenReturn(spannerConnection); Statement statement = Statement.of(JdbcConnection.IS_VALID_QUERY); diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java index 4b7fb713..48d55214 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java @@ -24,6 +24,8 @@ import static org.mockito.Mockito.when; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.connection.ConnectionOptionsTest; import java.io.IOException; import java.sql.Connection; import java.sql.DatabaseMetaData; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java index 95e0acae..0b8cd64c 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java @@ -19,6 +19,8 @@ import com.google.cloud.spanner.MockSpannerServiceImpl; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.SpannerPool; +import com.google.cloud.spanner.connection.StatementParser; import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo; import com.google.protobuf.ListValue; import com.google.protobuf.Value; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDriverTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDriverTest.java index 1215434c..9eb10474 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDriverTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDriverTest.java @@ -20,13 +20,23 @@ import static org.junit.Assert.fail; import com.google.cloud.spanner.MockSpannerServiceImpl; +import com.google.cloud.spanner.connection.ConnectionOptions; +import com.google.cloud.spanner.connection.ConnectionOptions.ConnectionProperty; +import com.google.cloud.spanner.connection.SpannerPool; +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.rpc.Code; import io.grpc.Server; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import java.io.IOException; import java.net.InetSocketAddress; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; import java.sql.SQLException; +import java.util.Collection; +import java.util.Properties; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -51,7 +61,7 @@ public class JdbcDriverTest { private static Server server; private static InetSocketAddress address; private static final String TEST_KEY_PATH = - ConnectionOptionsTest.class.getResource("test-key.json").getFile(); + JdbcDriverTest.class.getResource("test-key.json").getFile(); @BeforeClass public static void startStaticServer() throws IOException { @@ -67,6 +77,27 @@ public static void stopServer() throws Exception { server.awaitTermination(); } + @Test + public void testClientLibToken() { + assertThat(JdbcDriver.getClientLibToken()).isEqualTo("sp-jdbc"); + } + + @Test + public void testRegister() throws SQLException { + assertThat(JdbcDriver.isRegistered()).isTrue(); + JdbcDriver.deregister(); + assertThat(JdbcDriver.isRegistered()).isFalse(); + try { + JdbcDriver.getRegisteredDriver(); + fail("missing expected exception"); + } catch (SQLException e) { + assertThat(e.getErrorCode()).isEqualTo(Code.FAILED_PRECONDITION_VALUE); + } + JdbcDriver.register(); + assertThat(JdbcDriver.isRegistered()).isTrue(); + assertThat(JdbcDriver.getRegisteredDriver()).isNotNull(); + } + @Test public void testConnect() throws SQLException { try (Connection connection = @@ -101,4 +132,33 @@ public void testConnectWithCredentialsAndOAuthToken() throws SQLException { assertThat(e.getMessage()).contains("Cannot specify both credentials and an OAuth token"); } } + + @Test + public void testGetPropertyInfo() throws SQLException { + DriverPropertyInfo[] props = + JdbcDriver.getRegisteredDriver() + .getPropertyInfo( + "jdbc:cloudspanner:/projects/p/instances/i/databases/d", new Properties()); + assertThat(props).hasLength(ConnectionOptions.VALID_PROPERTIES.size()); + + Collection validConnectionPropertyNames = + Collections2.transform( + ConnectionOptions.VALID_PROPERTIES, + new Function() { + @Override + public String apply(ConnectionProperty input) { + return input.getName(); + } + }); + Collection driverPropertyNames = + Collections2.transform( + ImmutableList.copyOf(props), + new Function() { + @Override + public String apply(DriverPropertyInfo input) { + return input.name; + } + }); + assertThat(driverPropertyNames).containsExactlyElementsIn(validConnectionPropertyNames); + } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java index a902207b..41672132 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.admin.database.v1.MockDatabaseAdminImpl; import com.google.cloud.spanner.admin.instance.v1.MockInstanceAdminImpl; +import com.google.cloud.spanner.connection.SpannerPool; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; import com.google.protobuf.ListValue; import com.google.protobuf.Value; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java index cf93d69d..1b201f9a 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java @@ -19,7 +19,6 @@ import static com.google.cloud.spanner.jdbc.JdbcParameterStore.convertPositionalParametersToNamedParameters; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; @@ -28,6 +27,7 @@ import com.google.cloud.spanner.Value; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; import com.google.common.io.CharStreams; +import com.google.common.truth.Truth; import com.google.rpc.Code; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -740,21 +740,18 @@ public void testConvertPositionalParametersToNamedParameters() throws SQLExcepti } private void assertUnclosedLiteral(String sql) { - boolean exception = false; try { convertPositionalParametersToNamedParameters(sql); + fail("missing expected exception"); } catch (SQLException t) { - assertThat(t instanceof JdbcSqlException, is(true)); + Truth.assertThat((Throwable) t).isInstanceOf(JdbcSqlException.class); JdbcSqlException e = (JdbcSqlException) t; - assertThat(e.getCode(), is(Code.INVALID_ARGUMENT)); - assertThat( - e.getMessage(), - startsWith( + Truth.assertThat(e.getCode()).isSameInstanceAs(Code.INVALID_ARGUMENT); + Truth.assertThat(e.getMessage()) + .startsWith( Code.INVALID_ARGUMENT.name() + ": SQL statement contains an unclosed literal: " - + sql)); - exception = true; + + sql); } - assertThat(exception, is(true)); } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java index 9b829e54..bcf0454f 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java @@ -30,6 +30,7 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.connection.Connection; import com.google.rpc.Code; import java.io.ByteArrayInputStream; import java.io.StringReader; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcQueryOptionsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcQueryOptionsTest.java index 460e07f6..f628e17e 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcQueryOptionsTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcQueryOptionsTest.java @@ -21,6 +21,7 @@ import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.connection.AbstractMockServerTest; import com.google.common.base.MoreObjects; import com.google.spanner.v1.ExecuteSqlRequest; import java.sql.DriverManager; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java index 7a454308..4f41771e 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java @@ -16,12 +16,12 @@ package com.google.cloud.spanner.jdbc; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; +import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier; +import com.google.cloud.spanner.connection.StatementParser; +import com.google.cloud.spanner.connection.StatementResult.ResultType; import com.google.rpc.Code; import java.sql.Array; import java.sql.Connection; @@ -172,14 +172,13 @@ public JdbcSqlScriptVerifier(GenericConnectionProvider connectionProvider) { @Override protected void verifyExpectedException( String statement, Exception e, String code, String messagePrefix) { - assertThat(e instanceof JdbcSqlException, is(true)); + assertThat(e).isInstanceOf(JdbcSqlException.class); JdbcSqlException jdbcException = (JdbcSqlException) e; - assertThat(statement, jdbcException.getCode(), is(equalTo(Code.valueOf(code)))); + assertWithMessage(statement).that(jdbcException.getCode()).isEqualTo(Code.valueOf(code)); if (messagePrefix != null) { - assertThat( - statement, - e.getMessage(), - startsWith(messagePrefix.substring(1, messagePrefix.length() - 1))); + assertWithMessage(statement) + .that(e.getMessage()) + .startsWith(messagePrefix.substring(1, messagePrefix.length() - 1)); } } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java index 12e0921e..c1b1e992 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java @@ -25,8 +25,11 @@ import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.connection.Connection; +import com.google.cloud.spanner.connection.StatementParser; +import com.google.cloud.spanner.connection.StatementResult; +import com.google.cloud.spanner.connection.StatementResult.ResultType; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; import com.google.rpc.Code; import java.sql.ResultSet; import java.sql.SQLException; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java index 328c01f1..870711d0 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java @@ -34,6 +34,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; import com.google.rpc.Code; import java.math.BigDecimal; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetGenerator.java b/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetGenerator.java deleted file mode 100644 index 44e326cf..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetGenerator.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.api.client.util.Base64; -import com.google.cloud.Date; -import com.google.cloud.Timestamp; -import com.google.protobuf.ListValue; -import com.google.protobuf.NullValue; -import com.google.protobuf.Value; -import com.google.protobuf.util.Timestamps; -import com.google.spanner.v1.ResultSet; -import com.google.spanner.v1.ResultSetMetadata; -import com.google.spanner.v1.StructType; -import com.google.spanner.v1.StructType.Field; -import com.google.spanner.v1.Type; -import com.google.spanner.v1.TypeCode; -import java.util.Random; - -public class RandomResultSetGenerator { - private static final Type TYPES[] = - new Type[] { - Type.newBuilder().setCode(TypeCode.BOOL).build(), - Type.newBuilder().setCode(TypeCode.INT64).build(), - Type.newBuilder().setCode(TypeCode.FLOAT64).build(), - Type.newBuilder().setCode(TypeCode.STRING).build(), - Type.newBuilder().setCode(TypeCode.BYTES).build(), - Type.newBuilder().setCode(TypeCode.DATE).build(), - Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.BOOL)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.INT64)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) - .build(), - }; - - private static final ResultSetMetadata generateMetadata() { - StructType.Builder rowTypeBuilder = StructType.newBuilder(); - for (int col = 0; col < TYPES.length; col++) { - rowTypeBuilder.addFields(Field.newBuilder().setName("COL" + col).setType(TYPES[col])).build(); - } - ResultSetMetadata.Builder builder = ResultSetMetadata.newBuilder(); - builder.setRowType(rowTypeBuilder.build()); - return builder.build(); - } - - private static final ResultSetMetadata METADATA = generateMetadata(); - - private final int rowCount; - private final Random random = new Random(); - - public RandomResultSetGenerator(int rowCount) { - this.rowCount = rowCount; - } - - public ResultSet generate() { - ResultSet.Builder builder = ResultSet.newBuilder(); - for (int row = 0; row < rowCount; row++) { - ListValue.Builder rowBuilder = ListValue.newBuilder(); - for (int col = 0; col < TYPES.length; col++) { - Value.Builder valueBuilder = Value.newBuilder(); - setRandomValue(valueBuilder, TYPES[col]); - rowBuilder.addValues(valueBuilder.build()); - } - builder.addRows(rowBuilder.build()); - } - builder.setMetadata(METADATA); - return builder.build(); - } - - private void setRandomValue(Value.Builder builder, Type type) { - if (randomNull()) { - builder.setNullValue(NullValue.NULL_VALUE); - } else { - switch (type.getCode()) { - case ARRAY: - int length = random.nextInt(20) + 1; - ListValue.Builder arrayBuilder = ListValue.newBuilder(); - for (int i = 0; i < length; i++) { - Value.Builder valueBuilder = Value.newBuilder(); - setRandomValue(valueBuilder, type.getArrayElementType()); - arrayBuilder.addValues(valueBuilder.build()); - } - builder.setListValue(arrayBuilder.build()); - break; - case BOOL: - builder.setBoolValue(random.nextBoolean()); - break; - case STRING: - case BYTES: - byte[] bytes = new byte[random.nextInt(200)]; - random.nextBytes(bytes); - builder.setStringValue(Base64.encodeBase64String(bytes)); - break; - case DATE: - Date date = - Date.fromYearMonthDay( - random.nextInt(2019) + 1, random.nextInt(11) + 1, random.nextInt(28) + 1); - builder.setStringValue(date.toString()); - break; - case FLOAT64: - builder.setNumberValue(random.nextDouble()); - break; - case INT64: - builder.setStringValue(String.valueOf(random.nextLong())); - break; - case TIMESTAMP: - com.google.protobuf.Timestamp ts = - Timestamps.add( - Timestamps.EPOCH, - com.google.protobuf.Duration.newBuilder() - .setSeconds(random.nextInt(100_000_000)) - .setNanos(random.nextInt(1000_000_000)) - .build()); - builder.setStringValue(Timestamp.fromProto(ts).toString()); - break; - case STRUCT: - case TYPE_CODE_UNSPECIFIED: - case UNRECOGNIZED: - default: - throw new IllegalArgumentException("Unknown or unsupported type: " + type.getCode()); - } - } - } - - private boolean randomNull() { - return random.nextInt(10) == 0; - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessConverterTest.java deleted file mode 100644 index 12f1094f..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessConverterTest.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.ClientSideStatementValueConverters.ReadOnlyStalenessConverter; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ReadOnlyStalenessConverterTest { - - static String getAllowedValues( - Class> converterClass) - throws CompileException { - Set statements = ClientSideStatements.INSTANCE.getCompiledStatements(); - for (ClientSideStatementImpl statement : statements) { - if (statement.getSetStatement() != null - && converterClass.getName().endsWith(statement.getSetStatement().getConverterName())) { - return statement.getSetStatement().getAllowedValues(); - } - } - return null; - } - - @Test - public void testConvert() throws CompileException { - String allowedValues = getAllowedValues(ReadOnlyStalenessConverter.class); - assertThat(allowedValues, is(notNullValue())); - ReadOnlyStalenessConverter converter = new ReadOnlyStalenessConverter(allowedValues); - - assertThat(converter.convert("strong"), is(equalTo(TimestampBound.strong()))); - assertThat(converter.convert("Strong"), is(equalTo(TimestampBound.strong()))); - assertThat(converter.convert("STRONG"), is(equalTo(TimestampBound.strong()))); - - assertThat( - converter.convert("read_timestamp 2018-10-01T23:11:15.10001Z"), - is( - equalTo( - TimestampBound.ofReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.10001Z"))))); - assertThat( - converter.convert("Read_Timestamp 2018-10-01T23:11:15.999Z"), - is( - equalTo( - TimestampBound.ofReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.999Z"))))); - assertThat( - converter.convert("READ_TIMESTAMP 2018-10-01T23:11:15.1000Z"), - is( - equalTo( - TimestampBound.ofReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.1000Z"))))); - assertThat( - converter.convert("read_timestamp 2018-10-01T23:11:15.999999999Z"), - is( - equalTo( - TimestampBound.ofReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.999999999Z"))))); - assertThat( - converter.convert("read_timestamp\t2018-10-01T23:11:15.10001Z"), - is( - equalTo( - TimestampBound.ofReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.10001Z"))))); - assertThat(converter.convert("read_timestamp\n2018-10-01T23:11:15.10001Z"), is(nullValue())); - - assertThat( - converter.convert("min_read_timestamp 2018-10-01T23:11:15.10001Z"), - is( - equalTo( - TimestampBound.ofMinReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.10001Z"))))); - assertThat( - converter.convert("Min_Read_Timestamp 2018-10-01T23:11:15.999Z"), - is( - equalTo( - TimestampBound.ofMinReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.999Z"))))); - assertThat( - converter.convert("MIN_READ_TIMESTAMP 2018-10-01T23:11:15.1000Z"), - is( - equalTo( - TimestampBound.ofMinReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.1000Z"))))); - assertThat( - converter.convert("min_read_timestamp 2018-10-01T23:11:15.999999999Z"), - is( - equalTo( - TimestampBound.ofMinReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.999999999Z"))))); - assertThat( - converter.convert("min_read_timestamp\t2018-10-01T23:11:15.10001Z"), - is( - equalTo( - TimestampBound.ofMinReadTimestamp( - Timestamp.parseTimestamp("2018-10-01T23:11:15.10001Z"))))); - assertThat( - converter.convert("min_read_timestamp\n2018-10-01T23:11:15.10001Z"), is(nullValue())); - - assertThat( - converter.convert("exact_staleness 10s"), - is(equalTo(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)))); - assertThat( - converter.convert("Exact_Staleness 100ms"), - is(equalTo(TimestampBound.ofExactStaleness(100L, TimeUnit.MILLISECONDS)))); - assertThat( - converter.convert("EXACT_STALENESS 99999us"), - is(equalTo(TimestampBound.ofExactStaleness(99999L, TimeUnit.MICROSECONDS)))); - assertThat( - converter.convert("exact_staleness 999999999ns"), - is(equalTo(TimestampBound.ofExactStaleness(999999999L, TimeUnit.NANOSECONDS)))); - assertThat( - converter.convert("exact_staleness " + Long.MAX_VALUE + "ns"), - is(equalTo(TimestampBound.ofExactStaleness(Long.MAX_VALUE, TimeUnit.NANOSECONDS)))); - - assertThat( - converter.convert("max_staleness 10s"), - is(equalTo(TimestampBound.ofMaxStaleness(10L, TimeUnit.SECONDS)))); - assertThat( - converter.convert("Max_Staleness 100ms"), - is(equalTo(TimestampBound.ofMaxStaleness(100L, TimeUnit.MILLISECONDS)))); - assertThat( - converter.convert("MAX_STALENESS 99999us"), - is(equalTo(TimestampBound.ofMaxStaleness(99999L, TimeUnit.MICROSECONDS)))); - assertThat( - converter.convert("max_staleness 999999999ns"), - is(equalTo(TimestampBound.ofMaxStaleness(999999999L, TimeUnit.NANOSECONDS)))); - assertThat( - converter.convert("max_staleness " + Long.MAX_VALUE + "ns"), - is(equalTo(TimestampBound.ofMaxStaleness(Long.MAX_VALUE, TimeUnit.NANOSECONDS)))); - - assertThat(converter.convert(""), is(nullValue())); - assertThat(converter.convert(" "), is(nullValue())); - assertThat(converter.convert("random string"), is(nullValue())); - assertThat(converter.convert("read_timestamp"), is(nullValue())); - assertThat(converter.convert("min_read_timestamp"), is(nullValue())); - assertThat(converter.convert("exact_staleness"), is(nullValue())); - assertThat(converter.convert("max_staleness"), is(nullValue())); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessTest.java deleted file mode 100644 index bc43e2aa..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessTest.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.NoCredentials; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ReadOnlyTransaction; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Matchers; - -@RunWith(JUnit4.class) -public class ReadOnlyStalenessTest { - private static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database?readOnly=true"; - private static final String SELECT = "select foo from bar"; - - private final DatabaseClient dbClient = mock(DatabaseClient.class); - - private ConnectionImpl createConnection(ConnectionOptions options) { - Spanner spanner = mock(Spanner.class); - SpannerPool spannerPool = mock(SpannerPool.class); - when(spannerPool.getSpanner(any(ConnectionOptions.class), any(ConnectionImpl.class))) - .thenReturn(spanner); - DdlClient ddlClient = mock(DdlClient.class); - ReadOnlyTransaction singleUseReadOnlyTx = mock(ReadOnlyTransaction.class); - when(singleUseReadOnlyTx.executeQuery(Statement.of(SELECT))).thenReturn(mock(ResultSet.class)); - when(dbClient.singleUseReadOnlyTransaction(Matchers.any(TimestampBound.class))) - .thenReturn(singleUseReadOnlyTx); - ReadOnlyTransaction readOnlyTx = mock(ReadOnlyTransaction.class); - when(readOnlyTx.executeQuery(Statement.of(SELECT))).thenReturn(mock(ResultSet.class)); - when(dbClient.readOnlyTransaction(Matchers.any(TimestampBound.class))).thenReturn(readOnlyTx); - - return new ConnectionImpl(options, spannerPool, ddlClient, dbClient); - } - - @Test - public void testDefaultReadOnlyStalenessAutocommitOnce() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(connection.isAutocommit(), is(true)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).singleUseReadOnlyTransaction(TimestampBound.strong()); - } - } - - @Test - public void testDefaultReadOnlyStalenessAutocommitTwice() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(connection.isAutocommit(), is(true)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - connection.execute(Statement.of(SELECT)); - verify(dbClient, times(2)).singleUseReadOnlyTransaction(TimestampBound.strong()); - } - } - - @Test - public void testDefaultReadOnlyStalenessAutocommitChanging() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - assertThat(connection.isAutocommit(), is(true)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).singleUseReadOnlyTransaction(TimestampBound.strong()); - - connection.setReadOnlyStaleness(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - connection.execute(Statement.of(SELECT)); - verify(dbClient) - .singleUseReadOnlyTransaction(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - - connection.setReadOnlyStaleness(TimestampBound.ofMaxStaleness(5L, TimeUnit.SECONDS)); - connection.execute(Statement.of(SELECT)); - verify(dbClient) - .singleUseReadOnlyTransaction(TimestampBound.ofMaxStaleness(5L, TimeUnit.SECONDS)); - - connection.setReadOnlyStaleness(TimestampBound.ofReadTimestamp(Timestamp.MIN_VALUE)); - connection.execute(Statement.of(SELECT)); - verify(dbClient) - .singleUseReadOnlyTransaction(TimestampBound.ofReadTimestamp(Timestamp.MIN_VALUE)); - - connection.setReadOnlyStaleness(TimestampBound.ofMinReadTimestamp(Timestamp.MAX_VALUE)); - connection.execute(Statement.of(SELECT)); - verify(dbClient) - .singleUseReadOnlyTransaction(TimestampBound.ofMinReadTimestamp(Timestamp.MAX_VALUE)); - } - } - - @Test - public void testDefaultReadOnlyStalenessTransactionalOnce() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - assertThat(connection.isAutocommit(), is(false)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).readOnlyTransaction(TimestampBound.strong()); - } - } - - @Test - public void testDefaultReadOnlyStalenessTransactionalTwice() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - assertThat(connection.isAutocommit(), is(false)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - connection.execute(Statement.of(SELECT)); - connection.commit(); - // one transaction - verify(dbClient, times(1)).readOnlyTransaction(TimestampBound.strong()); - - connection.execute(Statement.of(SELECT)); - connection.commit(); - connection.execute(Statement.of(SELECT)); - // two transactions (plus one above) - verify(dbClient, times(3)).readOnlyTransaction(TimestampBound.strong()); - } - } - - @Test - public void testDefaultReadOnlyStalenessTransactionalChanging() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - assertThat(connection.isAutocommit(), is(false)); - assertThat(connection.isReadOnly(), is(true)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).readOnlyTransaction(TimestampBound.strong()); - connection.commit(); - - connection.setReadOnlyStaleness(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).readOnlyTransaction(TimestampBound.ofExactStaleness(10L, TimeUnit.SECONDS)); - connection.commit(); - - connection.setReadOnlyStaleness(TimestampBound.ofReadTimestamp(Timestamp.MIN_VALUE)); - connection.execute(Statement.of(SELECT)); - verify(dbClient).readOnlyTransaction(TimestampBound.ofReadTimestamp(Timestamp.MIN_VALUE)); - connection.commit(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtilTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtilTest.java deleted file mode 100644 index 3af641f1..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyStalenessUtilTest.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.durationToString; -import static com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.getTimeUnitAbbreviation; -import static com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.parseRfc3339; -import static com.google.cloud.spanner.jdbc.ReadOnlyStalenessUtil.parseTimeUnit; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.TimestampBound; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ReadOnlyStalenessUtilTest { - - @Test - public void testParseRfc3339() { - Map timestamps = new HashMap<>(); - timestamps.put( - "2018-03-01T10:11:12.999Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 999000000)); - timestamps.put("2018-10-28T02:00:00+02:00", Timestamp.ofTimeSecondsAndNanos(1540684800L, 0)); - timestamps.put("2018-10-28T03:00:00+01:00", Timestamp.ofTimeSecondsAndNanos(1540692000L, 0)); - timestamps.put( - "2018-01-01T00:00:00.000000001Z", Timestamp.ofTimeSecondsAndNanos(1514764800L, 1)); - timestamps.put("2018-10-28T02:00:00Z", Timestamp.ofTimeSecondsAndNanos(1540692000L, 0)); - timestamps.put( - "2018-12-31T23:59:59.999999999Z", Timestamp.ofTimeSecondsAndNanos(1546300799L, 999999999)); - timestamps.put( - "2018-03-01T10:11:12.9999Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 999900000)); - timestamps.put( - "2018-03-01T10:11:12.000000001Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 1)); - timestamps.put( - "2018-03-01T10:11:12.100000000Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 100000000)); - timestamps.put( - "2018-03-01T10:11:12.100000001Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 100000001)); - timestamps.put("2018-03-01T10:11:12-10:00", Timestamp.ofTimeSecondsAndNanos(1519935072L, 0)); - timestamps.put( - "2018-03-01T10:11:12.999999999Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 999999999)); - timestamps.put("2018-03-01T10:11:12-12:00", Timestamp.ofTimeSecondsAndNanos(1519942272L, 0)); - timestamps.put("2018-10-28T03:00:00Z", Timestamp.ofTimeSecondsAndNanos(1540695600L, 0)); - timestamps.put("2018-10-28T02:30:00Z", Timestamp.ofTimeSecondsAndNanos(1540693800L, 0)); - timestamps.put( - "2018-03-01T10:11:12.123Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 123000000)); - timestamps.put("2018-10-28T02:30:00+02:00", Timestamp.ofTimeSecondsAndNanos(1540686600L, 0)); - timestamps.put( - "2018-03-01T10:11:12.123456789Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 123456789)); - timestamps.put( - "2018-03-01T10:11:12.1000Z", Timestamp.ofTimeSecondsAndNanos(1519899072L, 100000000)); - - for (Entry ts : timestamps.entrySet()) { - Timestamp gTimestamp = parseRfc3339(ts.getKey()); - assertThat( - "Seconds for timestamp " + ts + " do not match", - gTimestamp.getSeconds(), - is(equalTo(ts.getValue().getSeconds()))); - assertThat( - "Nanos for timestamp " + ts + " do not match", - gTimestamp.getNanos(), - is(equalTo(ts.getValue().getNanos()))); - } - } - - @Test - public void testParseTimeUnit() { - assertThat(parseTimeUnit("s"), is(equalTo(TimeUnit.SECONDS))); - assertThat(parseTimeUnit("ms"), is(equalTo(TimeUnit.MILLISECONDS))); - assertThat(parseTimeUnit("us"), is(equalTo(TimeUnit.MICROSECONDS))); - assertThat(parseTimeUnit("ns"), is(equalTo(TimeUnit.NANOSECONDS))); - } - - @Test - public void testGetTimeUnitAbbreviation() { - assertThat(getTimeUnitAbbreviation(TimeUnit.SECONDS), is(equalTo("s"))); - assertThat(getTimeUnitAbbreviation(TimeUnit.MILLISECONDS), is(equalTo("ms"))); - assertThat(getTimeUnitAbbreviation(TimeUnit.MICROSECONDS), is(equalTo("us"))); - assertThat(getTimeUnitAbbreviation(TimeUnit.NANOSECONDS), is(equalTo("ns"))); - - List supportedTimeUnits = - Arrays.asList( - TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS); - for (TimeUnit unit : TimeUnit.values()) { - if (supportedTimeUnits.contains(unit)) { - assertThat(getTimeUnitAbbreviation(unit), is(notNullValue())); - } else { - String value = null; - try { - value = getTimeUnitAbbreviation(unit); - } catch (SpannerException e) { - if (e.getErrorCode() == ErrorCode.INVALID_ARGUMENT) { - value = "unsupported"; - } - } - assertThat(value, is(equalTo("unsupported"))); - } - } - } - - @Test - public void testStalenessToString() { - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(10L, TimeUnit.NANOSECONDS))), - is(equalTo("10ns"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(1000L, TimeUnit.NANOSECONDS))), - is(equalTo("1us"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(100000L, TimeUnit.NANOSECONDS))), - is(equalTo("100us"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(999999L, TimeUnit.NANOSECONDS))), - is(equalTo("999999ns"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(1L, TimeUnit.SECONDS))), - is(equalTo("1s"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(1000L, TimeUnit.MILLISECONDS))), - is(equalTo("1s"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(1001L, TimeUnit.MILLISECONDS))), - is(equalTo("1001ms"))); - assertThat( - durationToString( - new ReadOnlyStalenessUtil.MaxStalenessGetter( - TimestampBound.ofMaxStaleness(1000000000L, TimeUnit.NANOSECONDS))), - is(equalTo("1s"))); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyTransactionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyTransactionTest.java deleted file mode 100644 index 0642b75a..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReadOnlyTransactionTest.java +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Key; -import com.google.cloud.spanner.KeySet; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.Options.ReadOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState; -import com.google.spanner.v1.ResultSetStats; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ReadOnlyTransactionTest { - @Rule public ExpectedException exception = ExpectedException.none(); - - private static final class SimpleReadOnlyTransaction - implements com.google.cloud.spanner.ReadOnlyTransaction { - private Timestamp readTimestamp = null; - private final TimestampBound staleness; - - private SimpleReadOnlyTransaction(TimestampBound staleness) { - this.staleness = staleness; - } - - @Override - public ResultSet read( - String table, KeySet keys, Iterable columns, ReadOption... options) { - return null; - } - - @Override - public ResultSet readUsingIndex( - String table, String index, KeySet keys, Iterable columns, ReadOption... options) { - return null; - } - - @Override - public Struct readRow(String table, Key key, Iterable columns) { - return null; - } - - @Override - public Struct readRowUsingIndex(String table, String index, Key key, Iterable columns) { - return null; - } - - @Override - public ResultSet executeQuery(Statement statement, QueryOption... options) { - if (readTimestamp == null) { - switch (staleness.getMode()) { - case STRONG: - readTimestamp = Timestamp.now(); - break; - case READ_TIMESTAMP: - readTimestamp = staleness.getReadTimestamp(); - break; - case MIN_READ_TIMESTAMP: - readTimestamp = staleness.getMinReadTimestamp(); - break; - case EXACT_STALENESS: - Calendar cal = Calendar.getInstance(); - cal.add( - Calendar.MILLISECOND, (int) -staleness.getExactStaleness(TimeUnit.MILLISECONDS)); - readTimestamp = Timestamp.of(cal.getTime()); - break; - case MAX_STALENESS: - cal = Calendar.getInstance(); - cal.add(Calendar.MILLISECOND, (int) -staleness.getMaxStaleness(TimeUnit.MILLISECONDS)); - readTimestamp = Timestamp.of(cal.getTime()); - break; - default: - throw new IllegalStateException(); - } - } - return mock(ResultSet.class); - } - - @Override - public ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode queryMode) { - ResultSet res = executeQuery(statement); - when(res.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - return res; - } - - @Override - public void close() {} - - @Override - public Timestamp getReadTimestamp() { - return readTimestamp; - } - } - - private ReadOnlyTransaction createSubject() { - return createSubject(TimestampBound.strong()); - } - - private ReadOnlyTransaction createSubject(TimestampBound staleness) { - DatabaseClient client = mock(DatabaseClient.class); - when(client.readOnlyTransaction(staleness)) - .thenReturn(new SimpleReadOnlyTransaction(staleness)); - return ReadOnlyTransaction.newBuilder() - .setDatabaseClient(client) - .setReadOnlyStaleness(staleness) - .withStatementExecutor(new StatementExecutor()) - .build(); - } - - @Test - public void testExecuteDdl() { - ParsedStatement ddl = mock(ParsedStatement.class); - when(ddl.getType()).thenReturn(StatementType.DDL); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().executeDdl(ddl); - } - - @Test - public void testExecuteUpdate() { - ParsedStatement update = mock(ParsedStatement.class); - when(update.getType()).thenReturn(StatementType.UPDATE); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().executeUpdate(update); - } - - @Test - public void testWrite() { - Mutation mutation = Mutation.newInsertBuilder("foo").build(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().write(mutation); - } - - @Test - public void testWriteIterable() { - Mutation mutation = Mutation.newInsertBuilder("foo").build(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().write(Arrays.asList(mutation, mutation)); - } - - @Test - public void testRunBatch() { - ReadOnlyTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.runBatch(); - } - - @Test - public void testAbortBatch() { - ReadOnlyTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.abortBatch(); - } - - @Test - public void testGetCommitTimestamp() { - ReadOnlyTransaction transaction = createSubject(); - transaction.commit(); - assertThat(transaction.getState(), is(UnitOfWorkState.COMMITTED)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getCommitTimestamp(); - } - - @Test - public void testIsReadOnly() { - assertThat(createSubject().isReadOnly(), is(true)); - } - - @Test - public void testExecuteQuery() { - for (TimestampBound staleness : getTestTimestampBounds()) { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - - ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.NONE); - assertThat(rs, is(notNullValue())); - assertThat(rs.getStats(), is(nullValue())); - } - } - - @Test - public void testExecuteQueryWithOptionsTest() { - String sql = "SELECT * FROM FOO"; - QueryOption option = Options.prefetchChunks(10000); - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of(sql); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - DatabaseClient client = mock(DatabaseClient.class); - com.google.cloud.spanner.ReadOnlyTransaction tx = - mock(com.google.cloud.spanner.ReadOnlyTransaction.class); - ResultSet resWithOptions = mock(ResultSet.class); - ResultSet resWithoutOptions = mock(ResultSet.class); - when(tx.executeQuery(Statement.of(sql), option)).thenReturn(resWithOptions); - when(tx.executeQuery(Statement.of(sql))).thenReturn(resWithoutOptions); - when(client.readOnlyTransaction(TimestampBound.strong())).thenReturn(tx); - - ReadOnlyTransaction transaction = - ReadOnlyTransaction.newBuilder() - .setDatabaseClient(client) - .setReadOnlyStaleness(TimestampBound.strong()) - .withStatementExecutor(new StatementExecutor()) - .build(); - ResultSet expectedWithOptions = DirectExecuteResultSet.ofResultSet(resWithOptions); - assertThat( - transaction.executeQuery(parsedStatement, AnalyzeMode.NONE, option), - is(equalTo(expectedWithOptions))); - ResultSet expectedWithoutOptions = DirectExecuteResultSet.ofResultSet(resWithoutOptions); - assertThat( - transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), - is(equalTo(expectedWithoutOptions))); - } - - @Test - public void testPlanQuery() { - for (TimestampBound staleness : getTestTimestampBounds()) { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - - ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PLAN); - assertThat(rs, is(notNullValue())); - // get all results and then get the stats - while (rs.next()) { - // do nothing - } - assertThat(rs.getStats(), is(notNullValue())); - } - } - - @Test - public void testProfileQuery() { - for (TimestampBound staleness : getTestTimestampBounds()) { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - - ReadOnlyTransaction transaction = createSubject(staleness); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PROFILE); - assertThat(rs, is(notNullValue())); - // get all results and then get the stats - while (rs.next()) { - // do nothing - } - assertThat(rs.getStats(), is(notNullValue())); - } - } - - @Test - public void testGetReadTimestamp() { - for (TimestampBound staleness : getTestTimestampBounds()) { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - - ReadOnlyTransaction transaction = createSubject(staleness); - boolean expectedException = false; - try { - transaction.getReadTimestamp(); - } catch (SpannerException e) { - if (e.getErrorCode() == ErrorCode.FAILED_PRECONDITION) { - expectedException = true; - } - } - assertThat(expectedException, is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - assertThat(transaction.getReadTimestamp(), is(notNullValue())); - } - } - - private List getTestTimestampBounds() { - return Arrays.asList( - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(Timestamp.now()), - TimestampBound.ofMinReadTimestamp(Timestamp.now()), - TimestampBound.ofExactStaleness(1L, TimeUnit.SECONDS), - TimestampBound.ofMaxStaleness(100L, TimeUnit.MILLISECONDS)); - } - - @Test - public void testState() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - when(parsedStatement.getSqlWithoutComments()).thenReturn(statement.getSql()); - - ReadOnlyTransaction transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - transaction.commit(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED))); - assertThat(transaction.isActive(), is(false)); - - transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - - transaction.commit(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED))); - assertThat(transaction.isActive(), is(false)); - - // start a new transaction - transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - transaction.rollback(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); - assertThat(transaction.isActive(), is(false)); - - transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - transaction.rollback(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); - assertThat(transaction.isActive(), is(false)); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReadWriteTransactionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReadWriteTransactionTest.java deleted file mode 100644 index 54a83c62..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReadWriteTransactionTest.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.ResultSets; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionManager.TransactionState; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Type.StructField; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.spanner.v1.ResultSetStats; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.ExecutionException; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class ReadWriteTransactionTest { - - @Rule public final ExpectedException exception = ExpectedException.none(); - - private enum CommitBehavior { - SUCCEED, - FAIL, - ABORT; - } - - private static class SimpleTransactionManager implements TransactionManager { - private TransactionState state; - private Timestamp commitTimestamp; - private TransactionContext txContext; - private CommitBehavior commitBehavior; - - private SimpleTransactionManager(TransactionContext txContext, CommitBehavior commitBehavior) { - this.txContext = txContext; - this.commitBehavior = commitBehavior; - } - - @Override - public TransactionContext begin() { - state = TransactionState.STARTED; - return txContext; - } - - @Override - public void commit() { - switch (commitBehavior) { - case SUCCEED: - commitTimestamp = Timestamp.now(); - state = TransactionState.COMMITTED; - break; - case FAIL: - state = TransactionState.COMMIT_FAILED; - throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "commit failed"); - case ABORT: - state = TransactionState.COMMIT_FAILED; - commitBehavior = CommitBehavior.SUCCEED; - throw SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "commit aborted"); - default: - throw new IllegalStateException(); - } - } - - @Override - public void rollback() { - state = TransactionState.ROLLED_BACK; - } - - @Override - public TransactionContext resetForRetry() { - return txContext; - } - - @Override - public Timestamp getCommitTimestamp() { - return commitTimestamp; - } - - @Override - public TransactionState getState() { - return state; - } - - @Override - public void close() { - if (state != TransactionState.COMMITTED) { - state = TransactionState.ROLLED_BACK; - } - } - } - - private ReadWriteTransaction createSubject() { - return createSubject(CommitBehavior.SUCCEED, false); - } - - private ReadWriteTransaction createSubject(CommitBehavior commitBehavior) { - return createSubject(commitBehavior, false); - } - - private ReadWriteTransaction createSubject( - final CommitBehavior commitBehavior, boolean withRetry) { - DatabaseClient client = mock(DatabaseClient.class); - when(client.transactionManager()) - .thenAnswer( - new Answer() { - @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { - TransactionContext txContext = mock(TransactionContext.class); - when(txContext.executeQuery(any(Statement.class))) - .thenReturn(mock(ResultSet.class)); - ResultSet rsWithStats = mock(ResultSet.class); - when(rsWithStats.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - when(txContext.analyzeQuery(any(Statement.class), any(QueryAnalyzeMode.class))) - .thenReturn(rsWithStats); - when(txContext.executeUpdate(any(Statement.class))).thenReturn(1L); - return new SimpleTransactionManager(txContext, commitBehavior); - } - }); - return ReadWriteTransaction.newBuilder() - .setDatabaseClient(client) - .setRetryAbortsInternally(withRetry) - .setTransactionRetryListeners(Collections.emptyList()) - .withStatementExecutor(new StatementExecutor()) - .build(); - } - - @Test - public void testExecuteDdl() { - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.DDL); - - ReadWriteTransaction transaction = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.executeDdl(statement); - } - - @Test - public void testRunBatch() { - ReadWriteTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.runBatch(); - } - - @Test - public void testAbortBatch() { - ReadWriteTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.abortBatch(); - } - - @Test - public void testExecuteQuery() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.NONE); - assertThat(rs, is(notNullValue())); - assertThat(rs.getStats(), is(nullValue())); - } - - @Test - public void testPlanQuery() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PLAN); - assertThat(rs, is(notNullValue())); - while (rs.next()) { - // do nothing - } - assertThat(rs.getStats(), is(notNullValue())); - } - - @Test - public void testProfileQuery() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - ResultSet rs = transaction.executeQuery(parsedStatement, AnalyzeMode.PROFILE); - assertThat(rs, is(notNullValue())); - while (rs.next()) { - // do nothing - } - assertThat(rs.getStats(), is(notNullValue())); - } - - @Test - public void testExecuteUpdate() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.UPDATE); - when(parsedStatement.isUpdate()).thenReturn(true); - Statement statement = Statement.of("UPDATE FOO SET BAR=1 WHERE ID=2"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); - } - - @Test - public void testGetCommitTimestampBeforeCommit() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.UPDATE); - when(parsedStatement.isUpdate()).thenReturn(true); - Statement statement = Statement.of("UPDATE FOO SET BAR=1 WHERE ID=2"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); - - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getCommitTimestamp(); - } - - @Test - public void testGetCommitTimestampAfterCommit() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.UPDATE); - when(parsedStatement.isUpdate()).thenReturn(true); - Statement statement = Statement.of("UPDATE FOO SET BAR=1 WHERE ID=2"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeUpdate(parsedStatement), is(1L)); - transaction.commit(); - - assertThat(transaction.getCommitTimestamp(), is(notNullValue())); - } - - @Test - public void testGetReadTimestamp() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getReadTimestamp(); - } - - @Test - public void testState() { - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - - ReadWriteTransaction transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - - transaction.commit(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED))); - assertThat(transaction.isActive(), is(false)); - - // start a new transaction - transaction = createSubject(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - transaction.rollback(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.ROLLED_BACK))); - assertThat(transaction.isActive(), is(false)); - - // start a new transaction that will fail on commit - transaction = createSubject(CommitBehavior.FAIL); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - try { - transaction.commit(); - } catch (SpannerException e) { - // ignore - } - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMIT_FAILED))); - assertThat(transaction.isActive(), is(false)); - - // start a new transaction that will abort on commit - transaction = createSubject(CommitBehavior.ABORT); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - try { - transaction.commit(); - } catch (AbortedException e) { - // ignore - } - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMIT_FAILED))); - assertThat(transaction.isActive(), is(false)); - - // Start a new transaction that will abort on commit, but with internal retry enabled, so it - // will in the end succeed. - transaction = createSubject(CommitBehavior.ABORT, true); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.STARTED))); - assertThat(transaction.isActive(), is(true)); - transaction.commit(); - assertThat( - transaction.getState(), - is(equalTo(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED))); - assertThat(transaction.isActive(), is(false)); - } - - @Test - public void testIsReadOnly() { - assertThat(createSubject().isReadOnly(), is(false)); - } - - private enum RetryResults { - SAME, - DIFFERENT; - } - - @Test - public void testRetry() { - for (RetryResults results : RetryResults.values()) { - String sql1 = "UPDATE FOO SET BAR=1 WHERE BAZ>=100 AND BAZ<200"; - String sql2 = "UPDATE FOO SET BAR=2 WHERE BAZ>=200 AND BAZ<300"; - DatabaseClient client = mock(DatabaseClient.class); - ParsedStatement update1 = mock(ParsedStatement.class); - when(update1.getType()).thenReturn(StatementType.UPDATE); - when(update1.isUpdate()).thenReturn(true); - when(update1.getStatement()).thenReturn(Statement.of(sql1)); - ParsedStatement update2 = mock(ParsedStatement.class); - when(update2.getType()).thenReturn(StatementType.UPDATE); - when(update2.isUpdate()).thenReturn(true); - when(update2.getStatement()).thenReturn(Statement.of(sql2)); - - TransactionManager txManager = mock(TransactionManager.class); - TransactionContext txContext1 = mock(TransactionContext.class); - when(txManager.begin()).thenReturn(txContext1); - when(txManager.getState()).thenReturn(null, TransactionState.STARTED); - when(client.transactionManager()).thenReturn(txManager); - when(txContext1.executeUpdate(Statement.of(sql1))).thenReturn(90L); - when(txContext1.executeUpdate(Statement.of(sql2))).thenReturn(80L); - - TransactionContext txContext2 = mock(TransactionContext.class); - when(txManager.resetForRetry()).thenReturn(txContext2); - when(client.transactionManager()).thenReturn(txManager); - if (results == RetryResults.SAME) { - when(txContext2.executeUpdate(Statement.of(sql1))).thenReturn(90L); - when(txContext2.executeUpdate(Statement.of(sql2))).thenReturn(80L); - } else if (results == RetryResults.DIFFERENT) { - when(txContext2.executeUpdate(Statement.of(sql1))).thenReturn(90L); - when(txContext2.executeUpdate(Statement.of(sql2))).thenReturn(90L); - } - - // first abort, then do nothing - doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "commit aborted")) - .doNothing() - .when(txManager) - .commit(); - - ReadWriteTransaction subject = - ReadWriteTransaction.newBuilder() - .setRetryAbortsInternally(true) - .setTransactionRetryListeners(Collections.emptyList()) - .setDatabaseClient(client) - .withStatementExecutor(new StatementExecutor()) - .build(); - subject.executeUpdate(update1); - subject.executeUpdate(update2); - boolean expectedException = false; - try { - subject.commit(); - } catch (SpannerException e) { - if (results == RetryResults.DIFFERENT && e.getErrorCode() == ErrorCode.ABORTED) { - // expected - expectedException = true; - } else { - throw e; - } - } - assertThat(expectedException, is(results == RetryResults.DIFFERENT)); - } - } - - @Test - public void testChecksumResultSet() throws InterruptedException, ExecutionException { - DatabaseClient client = mock(DatabaseClient.class); - ReadWriteTransaction transaction = - ReadWriteTransaction.newBuilder() - .setRetryAbortsInternally(true) - .setTransactionRetryListeners(Collections.emptyList()) - .setDatabaseClient(client) - .withStatementExecutor(new StatementExecutor()) - .build(); - ParsedStatement parsedStatement = mock(ParsedStatement.class); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - ResultSet delegate1 = - ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), - Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build())); - ChecksumResultSet rs1 = - transaction.createChecksumResultSet(delegate1, parsedStatement, AnalyzeMode.NONE); - ResultSet delegate2 = - ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), - Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build())); - ChecksumResultSet rs2 = - transaction.createChecksumResultSet(delegate2, parsedStatement, AnalyzeMode.NONE); - // rs1 and rs2 are equal, rs3 contains the same rows, but in a different order - ResultSet delegate3 = - ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), - Arrays.asList( - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build(), - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build())); - ChecksumResultSet rs3 = - transaction.createChecksumResultSet(delegate3, parsedStatement, AnalyzeMode.NONE); - - // rs4 contains the same rows as rs1 and rs2, but also an additional row - ResultSet delegate4 = - ResultSets.forRows( - Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), - Arrays.asList( - Struct.newBuilder().set("ID").to(1l).set("NAME").to("TEST 1").build(), - Struct.newBuilder().set("ID").to(2l).set("NAME").to("TEST 2").build(), - Struct.newBuilder().set("ID").to(3l).set("NAME").to("TEST 3").build())); - ChecksumResultSet rs4 = - transaction.createChecksumResultSet(delegate4, parsedStatement, AnalyzeMode.NONE); - - assertThat(rs1.getChecksum(), is(equalTo(rs2.getChecksum()))); - while (rs1.next() && rs2.next() && rs3.next() && rs4.next()) { - assertThat(rs1.getChecksum(), is(equalTo(rs2.getChecksum()))); - assertThat(rs1.getChecksum(), is(not(equalTo(rs3.getChecksum())))); - assertThat(rs1.getChecksum(), is(equalTo(rs4.getChecksum()))); - } - assertThat(rs1.getChecksum(), is(equalTo(rs2.getChecksum()))); - assertThat(rs1.getChecksum(), is(not(equalTo(rs3.getChecksum())))); - // rs4 contains one more row than rs1, but the last row of rs4 hasn't been consumed yet - assertThat(rs1.getChecksum(), is(equalTo(rs4.getChecksum()))); - assertThat(rs4.next(), is(true)); - assertThat(rs1.getChecksum(), is(not(equalTo(rs4.getChecksum())))); - } - - @Test - public void testChecksumResultSetWithArray() throws InterruptedException, ExecutionException { - DatabaseClient client = mock(DatabaseClient.class); - ReadWriteTransaction transaction = - ReadWriteTransaction.newBuilder() - .setRetryAbortsInternally(true) - .setTransactionRetryListeners(Collections.emptyList()) - .setDatabaseClient(client) - .withStatementExecutor(new StatementExecutor()) - .build(); - ParsedStatement parsedStatement = mock(ParsedStatement.class); - Statement statement = Statement.of("SELECT * FROM FOO"); - when(parsedStatement.getStatement()).thenReturn(statement); - ResultSet delegate1 = - ResultSets.forRows( - Type.struct( - StructField.of("ID", Type.int64()), - StructField.of("PRICES", Type.array(Type.int64()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(1l) - .set("PRICES") - .toInt64Array(new long[] {1L, 2L}) - .build(), - Struct.newBuilder() - .set("ID") - .to(2l) - .set("PRICES") - .toInt64Array(new long[] {3L, 4L}) - .build())); - ChecksumResultSet rs1 = - transaction.createChecksumResultSet(delegate1, parsedStatement, AnalyzeMode.NONE); - ResultSet delegate2 = - ResultSets.forRows( - Type.struct( - StructField.of("ID", Type.int64()), - StructField.of("PRICES", Type.array(Type.int64()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(1l) - .set("PRICES") - .toInt64Array(new long[] {1L, 2L}) - .build(), - Struct.newBuilder() - .set("ID") - .to(2l) - .set("PRICES") - .toInt64Array(new long[] {3L, 5L}) - .build())); - ChecksumResultSet rs2 = - transaction.createChecksumResultSet(delegate2, parsedStatement, AnalyzeMode.NONE); - - rs1.next(); - rs2.next(); - assertThat(rs1.getChecksum(), is(equalTo(rs2.getChecksum()))); - rs1.next(); - rs2.next(); - assertThat(rs1.getChecksum(), is(not(equalTo(rs2.getChecksum())))); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSetTest.java deleted file mode 100644 index 4f90773c..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/ReplaceableForwardingResultSetTest.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.ResultSets; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Type.StructField; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ReplaceableForwardingResultSetTest { - - private ReplaceableForwardingResultSet createSubject() { - ResultSet delegate = - ResultSets.forRows( - Type.struct(StructField.of("test", Type.int64())), - Arrays.asList(Struct.newBuilder().set("test").to(1L).build())); - return new ReplaceableForwardingResultSet(delegate); - } - - @Test - public void testReplace() { - ResultSet delegate1 = - ResultSets.forRows( - Type.struct(StructField.of("test", Type.int64())), - Arrays.asList( - Struct.newBuilder().set("test").to(1L).build(), - Struct.newBuilder().set("test").to(2L).build())); - // First verify the behavior without replacing. - try (ResultSet rs = new ReplaceableForwardingResultSet(delegate1)) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("test"), is(equalTo(1L))); - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("test"), is(equalTo(2L))); - assertThat(rs.next(), is(false)); - } - - delegate1 = - ResultSets.forRows( - Type.struct(StructField.of("test", Type.int64())), - Arrays.asList( - Struct.newBuilder().set("test").to(1L).build(), - Struct.newBuilder().set("test").to(2L).build())); - ResultSet delegate2 = - ResultSets.forRows( - Type.struct(StructField.of("test", Type.int64())), - Arrays.asList( - Struct.newBuilder().set("test").to(1L).build(), - Struct.newBuilder().set("test").to(3L).build())); - // Then verify the behavior with replacing. - try (ReplaceableForwardingResultSet rs = new ReplaceableForwardingResultSet(delegate1)) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("test"), is(equalTo(1L))); - // Advance the delegate result set that will be used as replacement. - delegate2.next(); - // Replace the result set. - rs.replaceDelegate(delegate2); - // Verify that the replacement is being used. - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("test"), is(equalTo(3L))); - assertThat(rs.next(), is(false)); - } - } - - @Test - public void testMethodCallBeforeNext() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = Arrays.asList("getStats", "next", "close", "equals", "hashCode"); - ReplaceableForwardingResultSet subject = createSubject(); - // Test that all methods throw an IllegalStateException except the excluded methods when called - // before a call to ResultSet#next(). - callMethods(subject, excludedMethods, IllegalStateException.class); - } - - @Test - public void testMethodCallAfterClose() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = - Arrays.asList( - "getStats", - "next", - "close", - "getType", - "getColumnCount", - "getColumnIndex", - "getColumnType", - "ofResultSet", - "equals", - "hashCode"); - ReplaceableForwardingResultSet subject = createSubject(); - subject.next(); - subject.close(); - // Test that all methods throw an SpannerException except the excluded methods when called on a - // closed ResultSet. - callMethods(subject, excludedMethods, SpannerException.class); - } - - @Test - public void testMethodCallAfterNextHasReturnedFalse() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - List excludedMethods = - Arrays.asList( - "getStats", - "next", - "close", - "getType", - "getColumnCount", - "getColumnIndex", - "getColumnType", - "ofResultSet", - "equals", - "hashCode"); - ReplaceableForwardingResultSet subject = createSubject(); - subject.next(); - subject.next(); - // Test that all methods throw an IndexOutOfBoundsException except the excluded methods when - // called after a call to ResultSet#next() has returned false. - callMethods(subject, excludedMethods, IndexOutOfBoundsException.class); - } - - private void callMethods( - ReplaceableForwardingResultSet subject, - List excludedMethods, - Class expectedException) - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - for (Method method : ReplaceableForwardingResultSet.class.getDeclaredMethods()) { - if (Modifier.isPublic(method.getModifiers()) && !excludedMethods.contains(method.getName())) { - boolean exception = false; - int numberOfParameters = method.getParameterTypes().length; - Class firstParameterType = null; - if (numberOfParameters == 1) { - firstParameterType = method.getParameterTypes()[0]; - } - try { - switch (numberOfParameters) { - case 0: - method.invoke(subject); - break; - case 1: - if (firstParameterType == String.class) { - method.invoke(subject, "test"); - } else if (firstParameterType == int.class) { - method.invoke(subject, 0); - } else { - fail("unknown parameter type"); - } - break; - default: - fail("method with more than 1 parameter is unknown"); - } - } catch (InvocationTargetException e) { - if (e.getCause().getClass().equals(expectedException)) { - // expected - exception = true; - } else { - throw e; - } - } - assertThat( - method.getName() + " did not throw an IllegalStateException", exception, is(true)); - } - } - } - - @Test - public void testValidMethodCall() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - ResultSet delegate = mock(ResultSet.class); - when(delegate.next()).thenReturn(true, true, false); - try (ReplaceableForwardingResultSet subject = new ReplaceableForwardingResultSet(delegate)) { - subject.next(); - - // Cloud Spanner result sets use zero-based column indices, as opposed to the one-based column - // indices used by JDBC. The subject.getBoolean(0) and further zero-based calls below should - // therefore not cause any exceptions. - subject.getBoolean(0); - verify(delegate).getBoolean(0); - subject.getBoolean("test0"); - verify(delegate).getBoolean("test0"); - subject.getBooleanArray(1); - verify(delegate).getBooleanArray(1); - subject.getBooleanArray("test1"); - verify(delegate).getBooleanArray("test1"); - subject.getBooleanList(2); - verify(delegate).getBooleanList(2); - subject.getBooleanList("test2"); - verify(delegate).getBooleanList("test2"); - - subject.getBytes(0); - verify(delegate).getBytes(0); - subject.getBytes("test0"); - verify(delegate).getBytes("test0"); - subject.getBytesList(2); - verify(delegate).getBytesList(2); - subject.getBytesList("test2"); - verify(delegate).getBytesList("test2"); - - subject.getDate(0); - verify(delegate).getDate(0); - subject.getDate("test0"); - verify(delegate).getDate("test0"); - subject.getDateList(2); - verify(delegate).getDateList(2); - subject.getDateList("test2"); - verify(delegate).getDateList("test2"); - - subject.getDouble(0); - verify(delegate).getDouble(0); - subject.getDouble("test0"); - verify(delegate).getDouble("test0"); - subject.getDoubleArray(1); - verify(delegate).getDoubleArray(1); - subject.getDoubleArray("test1"); - verify(delegate).getDoubleArray("test1"); - subject.getDoubleList(2); - verify(delegate).getDoubleList(2); - subject.getDoubleList("test2"); - verify(delegate).getDoubleList("test2"); - - subject.getLong(0); - verify(delegate).getLong(0); - subject.getLong("test0"); - verify(delegate).getLong("test0"); - subject.getLongArray(1); - verify(delegate).getLongArray(1); - subject.getLongArray("test1"); - verify(delegate).getLongArray("test1"); - subject.getLongList(2); - verify(delegate).getLongList(2); - subject.getLongList("test2"); - verify(delegate).getLongList("test2"); - - subject.getString(0); - verify(delegate).getString(0); - subject.getString("test0"); - verify(delegate).getString("test0"); - subject.getStringList(2); - verify(delegate).getStringList(2); - subject.getStringList("test2"); - verify(delegate).getStringList("test2"); - - subject.getStructList(0); - subject.getStructList("test0"); - - subject.getTimestamp(0); - verify(delegate).getTimestamp(0); - subject.getTimestamp("test0"); - verify(delegate).getTimestamp("test0"); - subject.getTimestampList(2); - verify(delegate).getTimestampList(2); - subject.getTimestampList("test2"); - verify(delegate).getTimestampList("test2"); - - subject.getColumnCount(); - verify(delegate).getColumnCount(); - subject.getColumnIndex("test"); - verify(delegate).getColumnIndex("test"); - subject.getColumnType(100); - verify(delegate).getColumnType(100); - subject.getColumnType("test"); - verify(delegate).getColumnType("test"); - subject.getCurrentRowAsStruct(); - verify(delegate).getCurrentRowAsStruct(); - subject.getType(); - verify(delegate).getType(); - subject.isNull(50); - verify(delegate).isNull(50); - subject.isNull("test"); - verify(delegate).isNull("test"); - - while (subject.next()) { - // ignore - } - subject.getStats(); - verify(delegate).getStats(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SetReadOnlyStalenessSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/SetReadOnlyStalenessSqlScriptTest.java deleted file mode 100644 index de32e0f5..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SetReadOnlyStalenessSqlScriptTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class SetReadOnlyStalenessSqlScriptTest { - - static class TestConnectionProvider implements GenericConnectionProvider { - @Override - public GenericConnection getConnection() { - return SpannerGenericConnection.of( - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(ConnectionImplTest.URI) - .build())); - } - } - - @Test - public void testSetReadOnlyStalenessScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new TestConnectionProvider()); - verifier.verifyStatementsInFile("SetReadOnlyStalenessTest.sql", getClass()); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SetStatementTimeoutSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/SetStatementTimeoutSqlScriptTest.java deleted file mode 100644 index 90afdf05..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SetStatementTimeoutSqlScriptTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnectionProvider; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class SetStatementTimeoutSqlScriptTest { - - static class TestConnectionProvider implements GenericConnectionProvider { - @Override - public GenericConnection getConnection() { - return SpannerGenericConnection.of( - ConnectionImplTest.createConnection( - ConnectionOptions.newBuilder() - .setUri(ConnectionImplTest.URI) - .setCredentials(NoCredentials.getInstance()) - .build())); - } - } - - @Test - public void testSetStatementTimeoutScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new TestConnectionProvider()); - verifier.verifyStatementsInFile("SetStatementTimeoutTest.sql", getClass()); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SingleUseTransactionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/SingleUseTransactionTest.java deleted file mode 100644 index 720567c2..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SingleUseTransactionTest.java +++ /dev/null @@ -1,740 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Key; -import com.google.cloud.spanner.KeySet; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options; -import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.Options.ReadOption; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Struct; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionRunner; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import com.google.cloud.spanner.jdbc.StatementParser.StatementType; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import com.google.spanner.v1.ResultSetStats; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class SingleUseTransactionTest { - private static final String VALID_QUERY = "SELECT * FROM FOO"; - private static final String INVALID_QUERY = "SELECT * FROM BAR"; - private static final String SLOW_QUERY = "SELECT * FROM SLOW_TABLE"; - private static final String VALID_UPDATE = "UPDATE FOO SET BAR=1"; - private static final String INVALID_UPDATE = "UPDATE BAR SET FOO=1"; - private static final String SLOW_UPDATE = "UPDATE SLOW_TABLE SET FOO=1"; - private static final long VALID_UPDATE_COUNT = 99L; - - @Rule public ExpectedException exception = ExpectedException.none(); - - private final StatementExecutor executor = new StatementExecutor(); - - private enum CommitBehavior { - SUCCEED, - FAIL, - ABORT; - } - - private static class SimpleTransactionManager implements TransactionManager { - private TransactionState state; - private Timestamp commitTimestamp; - private TransactionContext txContext; - private CommitBehavior commitBehavior; - - private SimpleTransactionManager(TransactionContext txContext, CommitBehavior commitBehavior) { - this.txContext = txContext; - this.commitBehavior = commitBehavior; - } - - @Override - public TransactionContext begin() { - state = TransactionState.STARTED; - return txContext; - } - - @Override - public void commit() { - switch (commitBehavior) { - case SUCCEED: - commitTimestamp = Timestamp.now(); - state = TransactionState.COMMITTED; - break; - case FAIL: - state = TransactionState.COMMIT_FAILED; - throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "commit failed"); - case ABORT: - state = TransactionState.COMMIT_FAILED; - commitBehavior = CommitBehavior.SUCCEED; - throw SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "commit aborted"); - default: - throw new IllegalStateException(); - } - } - - @Override - public void rollback() { - state = TransactionState.ROLLED_BACK; - } - - @Override - public TransactionContext resetForRetry() { - return txContext; - } - - @Override - public Timestamp getCommitTimestamp() { - return commitTimestamp; - } - - @Override - public TransactionState getState() { - return state; - } - - @Override - public void close() { - if (state != TransactionState.COMMITTED) { - state = TransactionState.ROLLED_BACK; - } - } - } - - private static final class SimpleReadOnlyTransaction - implements com.google.cloud.spanner.ReadOnlyTransaction { - private Timestamp readTimestamp = null; - private final TimestampBound staleness; - - private SimpleReadOnlyTransaction(TimestampBound staleness) { - this.staleness = staleness; - } - - @Override - public ResultSet read( - String table, KeySet keys, Iterable columns, ReadOption... options) { - return null; - } - - @Override - public ResultSet readUsingIndex( - String table, String index, KeySet keys, Iterable columns, ReadOption... options) { - return null; - } - - @Override - public Struct readRow(String table, Key key, Iterable columns) { - return null; - } - - @Override - public Struct readRowUsingIndex(String table, String index, Key key, Iterable columns) { - return null; - } - - @Override - public ResultSet executeQuery(Statement statement, QueryOption... options) { - if (statement.equals(Statement.of(VALID_QUERY))) { - if (readTimestamp == null) { - switch (staleness.getMode()) { - case STRONG: - readTimestamp = Timestamp.now(); - break; - case READ_TIMESTAMP: - readTimestamp = staleness.getReadTimestamp(); - break; - case MIN_READ_TIMESTAMP: - readTimestamp = staleness.getMinReadTimestamp(); - break; - case EXACT_STALENESS: - Calendar cal = Calendar.getInstance(); - cal.add( - Calendar.MILLISECOND, (int) -staleness.getExactStaleness(TimeUnit.MILLISECONDS)); - readTimestamp = Timestamp.of(cal.getTime()); - break; - case MAX_STALENESS: - cal = Calendar.getInstance(); - cal.add( - Calendar.MILLISECOND, (int) -staleness.getMaxStaleness(TimeUnit.MILLISECONDS)); - readTimestamp = Timestamp.of(cal.getTime()); - break; - default: - throw new IllegalStateException(); - } - } - return mock(ResultSet.class); - } else if (statement.equals(Statement.of(SLOW_QUERY))) { - try { - Thread.sleep(10L); - } catch (InterruptedException e) { - // ignore - } - readTimestamp = Timestamp.now(); - return mock(ResultSet.class); - } else if (statement.equals(Statement.of(INVALID_QUERY))) { - throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "invalid query"); - } else { - throw new IllegalArgumentException(); - } - } - - @Override - public ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode queryMode) { - ResultSet rs = executeQuery(statement); - when(rs.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - return rs; - } - - @Override - public void close() {} - - @Override - public Timestamp getReadTimestamp() { - return readTimestamp; - } - } - - private DdlClient createDefaultMockDdlClient() { - try { - DdlClient ddlClient = mock(DdlClient.class); - @SuppressWarnings("unchecked") - final OperationFuture operation = - mock(OperationFuture.class); - when(operation.get()).thenReturn(null); - when(ddlClient.executeDdl(anyString())).thenCallRealMethod(); - when(ddlClient.executeDdl(anyListOf(String.class))).thenReturn(operation); - return ddlClient; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private SingleUseTransaction createSubject() { - return createSubject( - createDefaultMockDdlClient(), - false, - TimestampBound.strong(), - AutocommitDmlMode.TRANSACTIONAL, - CommitBehavior.SUCCEED, - 0L); - } - - private SingleUseTransaction createSubjectWithTimeout(long timeout) { - return createSubject( - createDefaultMockDdlClient(), - false, - TimestampBound.strong(), - AutocommitDmlMode.TRANSACTIONAL, - CommitBehavior.SUCCEED, - timeout); - } - - private SingleUseTransaction createSubject(AutocommitDmlMode dmlMode) { - return createSubject( - createDefaultMockDdlClient(), - false, - TimestampBound.strong(), - dmlMode, - CommitBehavior.SUCCEED, - 0L); - } - - private SingleUseTransaction createSubject(CommitBehavior commitBehavior) { - return createSubject( - createDefaultMockDdlClient(), - false, - TimestampBound.strong(), - AutocommitDmlMode.TRANSACTIONAL, - commitBehavior, - 0L); - } - - private SingleUseTransaction createDdlSubject(DdlClient ddlClient) { - return createSubject( - ddlClient, - false, - TimestampBound.strong(), - AutocommitDmlMode.TRANSACTIONAL, - CommitBehavior.SUCCEED, - 0L); - } - - private SingleUseTransaction createReadOnlySubject(TimestampBound staleness) { - return createSubject( - createDefaultMockDdlClient(), - true, - staleness, - AutocommitDmlMode.TRANSACTIONAL, - CommitBehavior.SUCCEED, - 0L); - } - - private SingleUseTransaction createSubject( - DdlClient ddlClient, - boolean readOnly, - TimestampBound staleness, - AutocommitDmlMode dmlMode, - final CommitBehavior commitBehavior, - long timeout) { - DatabaseClient dbClient = mock(DatabaseClient.class); - com.google.cloud.spanner.ReadOnlyTransaction singleUse = - new SimpleReadOnlyTransaction(staleness); - when(dbClient.singleUseReadOnlyTransaction(staleness)).thenReturn(singleUse); - - TransactionContext txContext = mock(TransactionContext.class); - when(txContext.executeUpdate(Statement.of(VALID_UPDATE))).thenReturn(VALID_UPDATE_COUNT); - when(txContext.executeUpdate(Statement.of(SLOW_UPDATE))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(10L); - return VALID_UPDATE_COUNT; - } - }); - when(txContext.executeUpdate(Statement.of(INVALID_UPDATE))) - .thenThrow( - SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "invalid update")); - SimpleTransactionManager txManager = new SimpleTransactionManager(txContext, commitBehavior); - when(dbClient.transactionManager()).thenReturn(txManager); - - when(dbClient.executePartitionedUpdate(Statement.of(VALID_UPDATE))) - .thenReturn(VALID_UPDATE_COUNT); - when(dbClient.executePartitionedUpdate(Statement.of(INVALID_UPDATE))) - .thenThrow( - SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "invalid update")); - - when(dbClient.readWriteTransaction()) - .thenAnswer( - new Answer() { - @Override - public TransactionRunner answer(InvocationOnMock invocation) throws Throwable { - TransactionRunner runner = - new TransactionRunner() { - private Timestamp commitTimestamp; - - @SuppressWarnings("unchecked") - @Override - public T run(TransactionCallable callable) { - if (commitBehavior == CommitBehavior.SUCCEED) { - this.commitTimestamp = Timestamp.now(); - return (T) Long.valueOf(1L); - } else if (commitBehavior == CommitBehavior.FAIL) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.UNKNOWN, "commit failed"); - } else { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.ABORTED, "commit aborted"); - } - } - - @Override - public Timestamp getCommitTimestamp() { - if (commitTimestamp == null) { - throw new IllegalStateException("no commit timestamp"); - } - return commitTimestamp; - } - - @Override - public TransactionRunner allowNestedTransaction() { - return this; - } - }; - return runner; - } - }); - - return SingleUseTransaction.newBuilder() - .setDatabaseClient(dbClient) - .setDdlClient(ddlClient) - .setAutocommitDmlMode(dmlMode) - .setReadOnly(readOnly) - .setReadOnlyStaleness(staleness) - .setStatementTimeout( - timeout == 0L - ? StatementExecutor.StatementTimeout.nullTimeout() - : StatementExecutor.StatementTimeout.of(timeout, TimeUnit.MILLISECONDS)) - .withStatementExecutor(executor) - .build(); - } - - private ParsedStatement createParsedDdl(String sql) { - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.DDL); - when(statement.getStatement()).thenReturn(Statement.of(sql)); - when(statement.getSqlWithoutComments()).thenReturn(sql); - return statement; - } - - private ParsedStatement createParsedQuery(String sql) { - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.QUERY); - when(statement.isQuery()).thenReturn(true); - when(statement.getStatement()).thenReturn(Statement.of(sql)); - return statement; - } - - private ParsedStatement createParsedUpdate(String sql) { - ParsedStatement statement = mock(ParsedStatement.class); - when(statement.getType()).thenReturn(StatementType.UPDATE); - when(statement.isUpdate()).thenReturn(true); - when(statement.getStatement()).thenReturn(Statement.of(sql)); - return statement; - } - - private List getTestTimestampBounds() { - return Arrays.asList( - TimestampBound.strong(), - TimestampBound.ofReadTimestamp(Timestamp.now()), - TimestampBound.ofMinReadTimestamp(Timestamp.now()), - TimestampBound.ofExactStaleness(1L, TimeUnit.SECONDS), - TimestampBound.ofMaxStaleness(100L, TimeUnit.MILLISECONDS)); - } - - @Test - public void testCommit() { - SingleUseTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.commit(); - } - - @Test - public void testRollback() { - SingleUseTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.rollback(); - } - - @Test - public void testRunBatch() { - SingleUseTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.runBatch(); - } - - @Test - public void testAbortBatch() { - SingleUseTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.abortBatch(); - } - - @Test - public void testExecuteDdl() { - String sql = "CREATE TABLE FOO"; - ParsedStatement ddl = createParsedDdl(sql); - DdlClient ddlClient = createDefaultMockDdlClient(); - SingleUseTransaction subject = createDdlSubject(ddlClient); - subject.executeDdl(ddl); - verify(ddlClient).executeDdl(sql); - } - - @Test - public void testExecuteQuery() { - for (TimestampBound staleness : getTestTimestampBounds()) { - for (AnalyzeMode analyzeMode : AnalyzeMode.values()) { - SingleUseTransaction subject = createReadOnlySubject(staleness); - ResultSet rs = subject.executeQuery(createParsedQuery(VALID_QUERY), analyzeMode); - assertThat(rs, is(notNullValue())); - assertThat(subject.getReadTimestamp(), is(notNullValue())); - assertThat( - subject.getState(), - is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - while (rs.next()) { - // just loop to the end to get stats - } - if (analyzeMode == AnalyzeMode.NONE) { - assertThat(rs.getStats(), is(nullValue())); - } else { - assertThat(rs.getStats(), is(notNullValue())); - } - } - } - for (TimestampBound staleness : getTestTimestampBounds()) { - SingleUseTransaction subject = createReadOnlySubject(staleness); - boolean expectedException = false; - try { - subject.executeQuery(createParsedQuery(INVALID_QUERY), AnalyzeMode.NONE); - } catch (SpannerException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertThat( - subject.getState(), - is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMIT_FAILED)); - } - } - - @Test - public void testExecuteQueryWithOptionsTest() { - String sql = "SELECT * FROM FOO"; - QueryOption option = Options.prefetchChunks(10000); - ParsedStatement parsedStatement = mock(ParsedStatement.class); - when(parsedStatement.getType()).thenReturn(StatementType.QUERY); - when(parsedStatement.isQuery()).thenReturn(true); - Statement statement = Statement.of(sql); - when(parsedStatement.getStatement()).thenReturn(statement); - DatabaseClient client = mock(DatabaseClient.class); - com.google.cloud.spanner.ReadOnlyTransaction tx = - mock(com.google.cloud.spanner.ReadOnlyTransaction.class); - when(tx.executeQuery(Statement.of(sql), option)).thenReturn(mock(ResultSet.class)); - when(client.singleUseReadOnlyTransaction(TimestampBound.strong())).thenReturn(tx); - - SingleUseTransaction transaction = - SingleUseTransaction.newBuilder() - .setDatabaseClient(client) - .setDdlClient(mock(DdlClient.class)) - .setAutocommitDmlMode(AutocommitDmlMode.TRANSACTIONAL) - .withStatementExecutor(executor) - .setReadOnlyStaleness(TimestampBound.strong()) - .build(); - assertThat( - transaction.executeQuery(parsedStatement, AnalyzeMode.NONE, option), is(notNullValue())); - } - - @Test - public void testExecuteUpdate_Transactional_Valid() { - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - SingleUseTransaction subject = createSubject(); - long updateCount = subject.executeUpdate(update); - assertThat(updateCount, is(equalTo(VALID_UPDATE_COUNT))); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - assertThat( - subject.getState(), is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - } - - @Test - public void testExecuteUpdate_Transactional_Invalid() { - ParsedStatement update = createParsedUpdate(INVALID_UPDATE); - SingleUseTransaction subject = createSubject(); - exception.expect( - SpannerExceptionMatcher.matchCodeAndMessage(ErrorCode.UNKNOWN, "invalid update")); - subject.executeUpdate(update); - } - - @Test - public void testExecuteUpdate_Transactional_Valid_FailedCommit() { - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); - exception.expect( - SpannerExceptionMatcher.matchCodeAndMessage(ErrorCode.UNKNOWN, "commit failed")); - subject.executeUpdate(update); - } - - @Test - public void testExecuteUpdate_Transactional_Valid_AbortedCommit() { - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - SingleUseTransaction subject = createSubject(CommitBehavior.ABORT); - // even though the transaction aborts at first, it will be retried and eventually succeed - long updateCount = subject.executeUpdate(update); - assertThat(updateCount, is(equalTo(VALID_UPDATE_COUNT))); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - assertThat( - subject.getState(), is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - } - - @Test - public void testExecuteUpdate_Partitioned_Valid() { - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - SingleUseTransaction subject = createSubject(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - long updateCount = subject.executeUpdate(update); - assertThat(updateCount, is(equalTo(VALID_UPDATE_COUNT))); - assertThat( - subject.getState(), is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - } - - @Test - public void testExecuteUpdate_Partitioned_Invalid() { - ParsedStatement update = createParsedUpdate(INVALID_UPDATE); - SingleUseTransaction subject = createSubject(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - exception.expect( - SpannerExceptionMatcher.matchCodeAndMessage(ErrorCode.UNKNOWN, "invalid update")); - subject.executeUpdate(update); - } - - @Test - public void testWrite() { - SingleUseTransaction subject = createSubject(); - subject.write(Mutation.newInsertBuilder("FOO").build()); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - assertThat( - subject.getState(), is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - } - - @Test - public void testWriteFail() { - SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); - exception.expect( - SpannerExceptionMatcher.matchCodeAndMessage(ErrorCode.UNKNOWN, "commit failed")); - subject.write(Mutation.newInsertBuilder("FOO").build()); - } - - @Test - public void testWriteIterable() { - SingleUseTransaction subject = createSubject(); - Mutation mutation = Mutation.newInsertBuilder("FOO").build(); - subject.write(Arrays.asList(mutation, mutation)); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - assertThat( - subject.getState(), is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMITTED)); - } - - @Test - public void testWriteIterableFail() { - SingleUseTransaction subject = createSubject(CommitBehavior.FAIL); - Mutation mutation = Mutation.newInsertBuilder("FOO").build(); - exception.expect( - SpannerExceptionMatcher.matchCodeAndMessage(ErrorCode.UNKNOWN, "commit failed")); - subject.write(Arrays.asList(mutation, mutation)); - } - - @Test - public void testMultiUse() { - for (TimestampBound staleness : getTestTimestampBounds()) { - SingleUseTransaction subject = createReadOnlySubject(staleness); - ResultSet rs = subject.executeQuery(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE); - assertThat(rs, is(notNullValue())); - assertThat(subject.getReadTimestamp(), is(notNullValue())); - boolean expectedException = false; - try { - subject.executeQuery(createParsedQuery(VALID_QUERY), AnalyzeMode.NONE); - } catch (IllegalStateException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - } - - String sql = "CREATE TABLE FOO"; - ParsedStatement ddl = createParsedDdl(sql); - DdlClient ddlClient = createDefaultMockDdlClient(); - SingleUseTransaction subject = createDdlSubject(ddlClient); - subject.executeDdl(ddl); - verify(ddlClient).executeDdl(sql); - boolean expectedException = false; - try { - subject.executeDdl(ddl); - } catch (IllegalStateException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - - ParsedStatement update = createParsedUpdate(VALID_UPDATE); - subject = createSubject(); - long updateCount = subject.executeUpdate(update); - assertThat(updateCount, is(equalTo(VALID_UPDATE_COUNT))); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - expectedException = false; - try { - subject.executeUpdate(update); - } catch (IllegalStateException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - - subject = createSubject(); - subject.write(Mutation.newInsertBuilder("FOO").build()); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - expectedException = false; - try { - subject.write(Mutation.newInsertBuilder("FOO").build()); - } catch (IllegalStateException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - - subject = createSubject(); - Mutation mutation = Mutation.newInsertBuilder("FOO").build(); - subject.write(Arrays.asList(mutation, mutation)); - assertThat(subject.getCommitTimestamp(), is(notNullValue())); - expectedException = false; - try { - subject.write(Arrays.asList(mutation, mutation)); - } catch (IllegalStateException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - } - - @Test - public void testExecuteQueryWithTimeout() { - SingleUseTransaction subject = createSubjectWithTimeout(1L); - try { - subject.executeQuery(createParsedQuery(SLOW_QUERY), AnalyzeMode.NONE); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.DEADLINE_EXCEEDED) { - throw e; - } - } - assertThat( - subject.getState(), - is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMIT_FAILED)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getReadTimestamp(); - } - - @Test - public void testExecuteUpdateWithTimeout() { - SingleUseTransaction subject = createSubjectWithTimeout(1L); - boolean timeoutException = false; - try { - subject.executeUpdate(createParsedUpdate(SLOW_UPDATE)); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.DEADLINE_EXCEEDED) { - throw e; - } - timeoutException = true; - } - assertThat(timeoutException, is(true)); - assertThat( - subject.getState(), - is(com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState.COMMIT_FAILED)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getCommitTimestamp(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SpannerExceptionMatcher.java b/src/test/java/com/google/cloud/spanner/jdbc/SpannerExceptionMatcher.java deleted file mode 100644 index 81cb8832..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SpannerExceptionMatcher.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.SpannerException; -import com.google.common.base.Preconditions; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; - -public final class SpannerExceptionMatcher extends BaseMatcher { - private final ErrorCode errorCode; - private final String message; - - public static SpannerExceptionMatcher matchCode(ErrorCode errorCode) { - Preconditions.checkNotNull(errorCode); - return new SpannerExceptionMatcher(errorCode, null); - } - - public static SpannerExceptionMatcher matchCodeAndMessage(ErrorCode errorCode, String message) { - Preconditions.checkNotNull(errorCode); - Preconditions.checkNotNull(message); - return new SpannerExceptionMatcher(errorCode, message); - } - - private SpannerExceptionMatcher(ErrorCode errorCode, String message) { - this.errorCode = errorCode; - this.message = message; - } - - @Override - public boolean matches(Object item) { - if (item instanceof SpannerException) { - SpannerException exception = (SpannerException) item; - if (message == null) { - return exception.getErrorCode().equals(errorCode); - } - return exception.getErrorCode().equals(errorCode) - && exception.getMessage().equals(errorCode.name() + ": " + message); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText(SpannerException.class.getName() + " with code " + errorCode.name()); - if (message != null) { - description.appendText(" - " + SpannerException.class.getName() + " with message " + message); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SpannerPoolTest.java b/src/test/java/com/google/cloud/spanner/jdbc/SpannerPoolTest.java deleted file mode 100644 index fd6e901e..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SpannerPoolTest.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.jdbc.ConnectionImpl.LeakedConnectionException; -import com.google.cloud.spanner.jdbc.SpannerPool.CheckAndCloseSpannersMode; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.logging.Handler; -import java.util.logging.Logger; -import java.util.logging.StreamHandler; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class SpannerPoolTest { - private static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - private ConnectionImpl connection1 = mock(ConnectionImpl.class); - private ConnectionImpl connection2 = mock(ConnectionImpl.class); - private ConnectionImpl connection3 = mock(ConnectionImpl.class); - private GoogleCredentials credentials1 = mock(GoogleCredentials.class); - private GoogleCredentials credentials2 = mock(GoogleCredentials.class); - private ConnectionOptions options1 = mock(ConnectionOptions.class); - private ConnectionOptions options2 = mock(ConnectionOptions.class); - private ConnectionOptions options3 = mock(ConnectionOptions.class); - private ConnectionOptions options4 = mock(ConnectionOptions.class); - - private SpannerPool createSubjectAndMocks() { - return createSubjectAndMocks(0L); - } - - private SpannerPool createSubjectAndMocks(long closeSpannerAfterMillisecondsUnused) { - SpannerPool pool = - new SpannerPool(closeSpannerAfterMillisecondsUnused) { - @Override - Spanner createSpanner(SpannerPoolKey key) { - return mock(Spanner.class); - } - }; - - when(options1.getCredentials()).thenReturn(credentials1); - when(options1.getProjectId()).thenReturn("test-project-1"); - when(options2.getCredentials()).thenReturn(credentials2); - when(options2.getProjectId()).thenReturn("test-project-1"); - - when(options3.getCredentials()).thenReturn(credentials1); - when(options3.getProjectId()).thenReturn("test-project-2"); - when(options4.getCredentials()).thenReturn(credentials2); - when(options4.getProjectId()).thenReturn("test-project-2"); - - return pool; - } - - @AfterClass - public static void closeSpannerPool() { - SpannerPool.closeSpannerPool(); - } - - @Test - public void testGetSpanner() { - SpannerPool pool = createSubjectAndMocks(); - Spanner spanner1; - Spanner spanner2; - - // assert equal - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options1, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - spanner1 = pool.getSpanner(options2, connection1); - spanner2 = pool.getSpanner(options2, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - spanner1 = pool.getSpanner(options3, connection1); - spanner2 = pool.getSpanner(options3, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - spanner1 = pool.getSpanner(options4, connection1); - spanner2 = pool.getSpanner(options4, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - - // assert not equal - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options2, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options3, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options4, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - spanner1 = pool.getSpanner(options2, connection1); - spanner2 = pool.getSpanner(options3, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - spanner1 = pool.getSpanner(options2, connection1); - spanner2 = pool.getSpanner(options4, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - spanner1 = pool.getSpanner(options3, connection1); - spanner2 = pool.getSpanner(options4, connection2); - assertThat(spanner1, not(equalTo(spanner2))); - } - - @Test - public void testRemoveConnection() { - SpannerPool pool = createSubjectAndMocks(); - Spanner spanner1; - Spanner spanner2; - - // assert equal - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options1, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - // one connection removed, assert that we would still get the same Spanner - pool.removeConnection(options1, connection1); - spanner1 = pool.getSpanner(options1, connection1); - assertThat(spanner1, is(equalTo(spanner2))); - // remove two connections, assert that we would still get the same Spanner, as Spanners are not - // directly closed and removed. - pool.removeConnection(options1, connection1); - pool.removeConnection(options1, connection2); - spanner1 = pool.getSpanner(options1, connection1); - assertThat(spanner1, is(equalTo(spanner2))); - // remove the last connection again - pool.removeConnection(options1, connection1); - } - - private static Logger log = Logger.getLogger(SpannerPool.class.getName()); - private static OutputStream logCapturingStream; - private static StreamHandler customLogHandler; - - private void attachLogCapturer() { - logCapturingStream = new ByteArrayOutputStream(); - Logger currentLogger = log; - Handler[] handlers = new Handler[0]; - while (handlers.length == 0 && currentLogger != null) { - handlers = currentLogger.getHandlers(); - currentLogger = currentLogger.getParent(); - } - if (handlers.length == 0) { - throw new IllegalStateException("no handlers found for logger"); - } - customLogHandler = new StreamHandler(logCapturingStream, handlers[0].getFormatter()); - log.addHandler(customLogHandler); - } - - public String getTestCapturedLog() throws IOException { - customLogHandler.flush(); - return logCapturingStream.toString(); - } - - @Test - public void testRemoveConnectionOptionsNotRegistered() throws IOException { - attachLogCapturer(); - final String expectedLogPart = "There is no Spanner registered for ConnectionOptions"; - SpannerPool pool = createSubjectAndMocks(); - pool.getSpanner(options1, connection1); - pool.removeConnection(options2, connection1); - String capturedLog = getTestCapturedLog(); - assertThat(capturedLog.contains(expectedLogPart), is(true)); - } - - @Test - public void testRemoveConnectionConnectionNotRegistered() throws IOException { - attachLogCapturer(); - final String expectedLogPart = "There are no connections registered for ConnectionOptions"; - SpannerPool pool = createSubjectAndMocks(); - pool.getSpanner(options1, connection1); - pool.removeConnection(options1, connection2); - String capturedLog = getTestCapturedLog(); - assertThat(capturedLog.contains(expectedLogPart), is(true)); - } - - @Test - public void testRemoveConnectionConnectionAlreadyRemoved() throws IOException { - attachLogCapturer(); - final String expectedLogPart = "There are no connections registered for ConnectionOptions"; - SpannerPool pool = createSubjectAndMocks(); - pool.getSpanner(options1, connection1); - pool.removeConnection(options1, connection1); - pool.removeConnection(options1, connection1); - String capturedLog = getTestCapturedLog(); - assertThat(capturedLog.contains(expectedLogPart), is(true)); - } - - @Test - public void testCloseSpanner() throws IOException { - SpannerPool pool = createSubjectAndMocks(); - Spanner spanner = pool.getSpanner(options1, connection1); - // verify that closing is not possible until all connections have been removed - boolean exception = false; - try { - pool.checkAndCloseSpanners(); - } catch (SpannerException e) { - exception = e.getErrorCode() == ErrorCode.FAILED_PRECONDITION; - } - assertThat(exception, is(true)); - - // remove the connection and verify that it is possible to close - pool.removeConnection(options1, connection1); - pool.checkAndCloseSpanners(); - verify(spanner).close(); - - final String expectedLogPart = - "WARNING: There is/are 1 connection(s) still open. Close all connections before stopping the application"; - Spanner spanner2 = pool.getSpanner(options1, connection1); - pool.checkAndCloseSpanners(CheckAndCloseSpannersMode.WARN); - String capturedLog = getTestCapturedLog(); - assertThat(capturedLog.contains(expectedLogPart), is(true)); - verify(spanner2, never()).close(); - - // remove the connection and verify that it is possible to close - pool.removeConnection(options1, connection1); - pool.checkAndCloseSpanners(CheckAndCloseSpannersMode.WARN); - verify(spanner2).close(); - } - - @Test - public void testLeakedConnection() throws IOException { - ConnectionOptions options = - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(); - // create an actual connection object but not in a try-with-resources block - Connection connection = options.getConnection(); - // try to close the application which should fail - try { - ConnectionOptions.closeSpanner(); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.FAILED_PRECONDITION))); - } - String capturedLog = getTestCapturedLog(); - assertThat(capturedLog.contains(LeakedConnectionException.class.getName()), is(true)); - assertThat(capturedLog.contains("testLeakedConnection"), is(true)); - // Now close the connection to avoid trouble with other test cases. - connection.close(); - } - - @Test - public void testCloseUnusedSpanners() { - SpannerPool pool = createSubjectAndMocks(); - Spanner spanner1; - Spanner spanner2; - Spanner spanner3; - - // create two connections that use the same Spanner - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options1, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - - // all spanners are in use, this should have no effect - pool.closeUnusedSpanners(-1L); - verify(spanner1, never()).close(); - - // close one connection. This should also have no effect. - pool.removeConnection(options1, connection1); - pool.closeUnusedSpanners(-1L); - verify(spanner1, never()).close(); - - // close the other connection as well, the Spanner object should now be closed. - pool.removeConnection(options1, connection2); - pool.closeUnusedSpanners(-1L); - verify(spanner1).close(); - - // create three connections that use two different Spanners - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options2, connection2); - spanner3 = pool.getSpanner(options2, connection3); - assertThat(spanner1, not(equalTo(spanner2))); - assertThat(spanner2, is(equalTo(spanner3))); - - // all spanners are in use, this should have no effect - pool.closeUnusedSpanners(-1L); - verify(spanner1, never()).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection1. That should also mark spanner1 as no longer in use - pool.removeConnection(options1, connection1); - pool.closeUnusedSpanners(-1L); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection2. That should have no effect, as connection3 is still using spanner2 - pool.removeConnection(options2, connection2); - pool.closeUnusedSpanners(-1L); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection3. Now all should be closed. - pool.removeConnection(options2, connection3); - pool.closeUnusedSpanners(-1L); - verify(spanner1).close(); - verify(spanner2).close(); - verify(spanner3).close(); - } - - /** Allow the automatic close test to be run multiple times to ensure it is stable */ - private static final int NUMBER_OF_AUTOMATIC_CLOSE_TEST_RUNS = 1; - - private static final long TEST_AUTOMATIC_CLOSE_TIMEOUT = 2L; - private static final long SLEEP_BEFORE_VERIFICATION = 100L; - - @Test - public void testAutomaticCloser() throws InterruptedException { - for (int testRun = 0; testRun < NUMBER_OF_AUTOMATIC_CLOSE_TEST_RUNS; testRun++) { - // create a pool that will close unused spanners after 5 milliseconds - SpannerPool pool = createSubjectAndMocks(TEST_AUTOMATIC_CLOSE_TIMEOUT); - Spanner spanner1; - Spanner spanner2; - Spanner spanner3; - - // create two connections that use the same Spanner - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options1, connection2); - assertThat(spanner1, is(equalTo(spanner2))); - - // all spanners are in use, this should have no effect - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - - // close one connection. This should also have no effect. - pool.removeConnection(options1, connection1); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - - // close the other connection as well, the Spanner object should now be closed. - pool.removeConnection(options1, connection2); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - - // create three connections that use two different Spanners - spanner1 = pool.getSpanner(options1, connection1); - spanner2 = pool.getSpanner(options2, connection2); - spanner3 = pool.getSpanner(options2, connection3); - assertThat(spanner1, not(equalTo(spanner2))); - assertThat(spanner2, is(equalTo(spanner3))); - - // all spanners are in use, this should have no effect - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1, never()).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection1. That should also mark spanner1 as no longer in use - pool.removeConnection(options1, connection1); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection2. That should have no effect, as connection3 is still using spanner2 - pool.removeConnection(options2, connection2); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2, never()).close(); - verify(spanner3, never()).close(); - - // close connection3. Now all should be closed. - pool.removeConnection(options2, connection3); - Thread.sleep(SLEEP_BEFORE_VERIFICATION); - verify(spanner1).close(); - verify(spanner2).close(); - verify(spanner3).close(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SqlScriptVerifier.java b/src/test/java/com/google/cloud/spanner/jdbc/SqlScriptVerifier.java deleted file mode 100644 index df683b5f..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SqlScriptVerifier.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; - -/** - * SQL script verifier implementation for Spanner {@link com.google.cloud.spanner.jdbc.Connection} - * - * @see AbstractSqlScriptVerifier for more information - */ -public class SqlScriptVerifier extends AbstractSqlScriptVerifier { - - static class ConnectionGenericStatementResult extends GenericStatementResult { - private final StatementResult result; - - private ConnectionGenericStatementResult(StatementResult result) { - this.result = result; - } - - @Override - protected ResultType getResultType() { - return result.getResultType(); - } - - @Override - protected GenericResultSet getResultSet() { - return new ConnectionGenericResultSet(result.getResultSet()); - } - - @Override - protected long getUpdateCount() { - return result.getUpdateCount(); - } - } - - static class ConnectionGenericResultSet extends GenericResultSet { - private final ResultSet resultSet; - - private ConnectionGenericResultSet(ResultSet resultSet) { - this.resultSet = resultSet; - } - - @Override - protected boolean next() { - return resultSet.next(); - } - - @Override - protected Object getValue(String col) { - if (resultSet.isNull(col)) { - return null; - } - Type type = resultSet.getColumnType(col); - switch (type.getCode()) { - case ARRAY: - return getArrayValue(resultSet, col, type.getArrayElementType()); - case BOOL: - return resultSet.getBoolean(col); - case BYTES: - return resultSet.getBytes(col); - case DATE: - return resultSet.getDate(col); - case FLOAT64: - return resultSet.getDouble(col); - case INT64: - return resultSet.getLong(col); - case STRING: - return resultSet.getString(col); - case TIMESTAMP: - return resultSet.getTimestamp(col); - case STRUCT: - throw new IllegalArgumentException("type struct not supported"); - } - throw new IllegalArgumentException("unknown type: " + type); - } - - private Object getArrayValue(ResultSet rs, String col, Type type) { - switch (type.getCode()) { - case BOOL: - return rs.getBooleanList(col); - case BYTES: - return rs.getBytesList(col); - case DATE: - return rs.getDateList(col); - case FLOAT64: - return rs.getDoubleList(col); - case INT64: - return rs.getLongList(col); - case STRING: - return rs.getStringList(col); - case STRUCT: - return rs.getStructList(col); - case TIMESTAMP: - return rs.getTimestampList(col); - case ARRAY: - throw new IllegalArgumentException("array of array not supported"); - } - throw new IllegalArgumentException("unknown type: " + type); - } - - @Override - protected int getColumnCount() throws Exception { - return resultSet.getColumnCount(); - } - - @Override - protected Object getFirstValue() throws Exception { - return getValue(resultSet.getType().getStructFields().get(0).getName()); - } - } - - public static class SpannerGenericConnection extends GenericConnection { - private final Connection connection; - - public static SpannerGenericConnection of(Connection connection) { - return new SpannerGenericConnection(connection); - } - - private SpannerGenericConnection(Connection connection) { - this.connection = connection; - } - - @Override - protected GenericStatementResult execute(String sql) { - return new ConnectionGenericStatementResult(connection.execute(Statement.of(sql))); - } - - @Override - public void close() throws Exception { - if (this.connection != null) { - this.connection.close(); - } - } - } - - public SqlScriptVerifier() { - this(null); - } - - public SqlScriptVerifier(GenericConnectionProvider provider) { - super(provider); - } - - @Override - protected void verifyExpectedException( - String statement, Exception e, String code, String messagePrefix) { - assertThat(e instanceof SpannerException, is(true)); - SpannerException spannerException = (SpannerException) e; - assertThat( - statement + " resulted in " + spannerException.toString(), - spannerException.getErrorCode(), - is(equalTo(ErrorCode.valueOf(code)))); - if (messagePrefix != null) { - assertThat( - statement, - e.getMessage(), - startsWith(messagePrefix.substring(1, messagePrefix.length() - 1))); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SqlTestScriptsGenerator.java b/src/test/java/com/google/cloud/spanner/jdbc/SqlTestScriptsGenerator.java deleted file mode 100644 index 875ca0a0..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/SqlTestScriptsGenerator.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -/** Class that runs all generators of SQL test scripts for the Connection API */ -public class SqlTestScriptsGenerator { - - /** Main method for generating the test script */ - public static void main(String[] args) throws Exception { - ClientSideStatementsTest.generateTestScript(); - ConnectionImplGeneratedSqlScriptTest.generateTestScript(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/StatementParserTest.java b/src/test/java/com/google/cloud/spanner/jdbc/StatementParserTest.java deleted file mode 100644 index 1344a0f3..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/StatementParserTest.java +++ /dev/null @@ -1,723 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement; -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.junit.Test; - -public class StatementParserTest { - public static final String COPYRIGHT_PATTERN = - "\\/\\*\n" - + " \\* Copyright \\d{4} Google LLC\n" - + " \\*\n" - + " \\* Licensed under the Apache License, Version 2.0 \\(the \"License\"\\);\n" - + " \\* you may not use this file except in compliance with the License.\n" - + " \\* You may obtain a copy of the License at\n" - + " \\*\n" - + " \\* http://www.apache.org/licenses/LICENSE-2.0\n" - + " \\*\n" - + " \\* Unless required by applicable law or agreed to in writing, software\n" - + " \\* distributed under the License is distributed on an \"AS IS\" BASIS,\n" - + " \\* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" - + " \\* See the License for the specific language governing permissions and\n" - + " \\* limitations under the License.\n" - + " \\*\\/\n"; - private final StatementParser parser = StatementParser.INSTANCE; - private static final Pattern EXPECT_PATTERN = Pattern.compile("(?is)\\s*(?:@EXPECT)\\s+'(.*)'"); - - @Test - public void testRemoveComments() { - List statements = readStatementsFromFile("CommentsTest.sql"); - String currentlyExpected = ""; - for (String statement : statements) { - String sql = statement.trim(); - if (sql.startsWith("@EXPECT")) { - Matcher matcher = EXPECT_PATTERN.matcher(sql); - if (matcher.matches()) { - currentlyExpected = matcher.group(1); - } else { - throw new IllegalArgumentException("Unknown @EXPECT statement: " + sql); - } - } else { - assertThat( - StatementParser.removeCommentsAndTrim(statement), is(equalTo(currentlyExpected))); - } - } - - assertThat(StatementParser.removeCommentsAndTrim(""), is(equalTo(""))); - assertThat( - StatementParser.removeCommentsAndTrim("SELECT * FROM FOO"), - is(equalTo("SELECT * FROM FOO"))); - assertThat( - StatementParser.removeCommentsAndTrim("-- This is a one line comment\nSELECT * FROM FOO"), - is(equalTo("SELECT * FROM FOO"))); - assertThat( - StatementParser.removeCommentsAndTrim( - "/* This is a simple multi line comment */\nSELECT * FROM FOO"), - is(equalTo("SELECT * FROM FOO"))); - assertThat( - StatementParser.removeCommentsAndTrim( - "/* This is a \nmulti line comment */\nSELECT * FROM FOO"), - is(equalTo("SELECT * FROM FOO"))); - assertThat( - StatementParser.removeCommentsAndTrim( - "/* This\nis\na\nmulti\nline\ncomment */\nSELECT * FROM FOO"), - is(equalTo("SELECT * FROM FOO"))); - } - - @Test - public void testStatementWithCommentContainingSlash() { - String sql = - "/*\n" - + " * Script for testing invalid/unrecognized statements\n" - + " */\n" - + "\n" - + "-- MERGE into test comment MERGE -- \n" - + "@EXPECT EXCEPTION INVALID_ARGUMENT 'INVALID_ARGUMENT: Unknown statement'\n" - + "MERGE INTO Singers s\n" - + "/*** test ****/" - + "USING (VALUES (1, 'John', 'Doe')) v\n" - + "ON v.column1 = s.SingerId\n" - + "WHEN NOT MATCHED \n" - + " INSERT VALUES (v.column1, v.column2, v.column3)\n" - + "WHEN MATCHED\n" - + " UPDATE SET FirstName = v.column2,\n" - + " LastName = v.column3;"; - String sqlWithoutComments = - "@EXPECT EXCEPTION INVALID_ARGUMENT 'INVALID_ARGUMENT: Unknown statement'\n" - + "MERGE INTO Singers s\n" - + "USING (VALUES (1, 'John', 'Doe')) v\n" - + "ON v.column1 = s.SingerId\n" - + "WHEN NOT MATCHED \n" - + " INSERT VALUES (v.column1, v.column2, v.column3)\n" - + "WHEN MATCHED\n" - + " UPDATE SET FirstName = v.column2,\n" - + " LastName = v.column3"; - ParsedStatement statement = parser.parse(Statement.of(sql)); - assertThat(statement.getSqlWithoutComments(), is(equalTo(sqlWithoutComments))); - } - - @Test - public void testStatementWithCommentContainingSlashAndNoAsteriskOnNewLine() { - String sql = - "/*\n" - + " * Script for testing invalid/unrecognized statements\n" - + " foo bar baz" - + " */\n" - + "\n" - + "-- MERGE INTO test comment MERGE\n" - + "@EXPECT EXCEPTION INVALID_ARGUMENT 'INVALID_ARGUMENT: Unknown statement'\n" - + "MERGE INTO Singers s\n" - + "USING (VALUES (1, 'John', 'Doe')) v\n" - + "ON v.column1 = s.SingerId\n" - + "-- test again --\n" - + "WHEN NOT MATCHED \n" - + " INSERT VALUES (v.column1, v.column2, v.column3)\n" - + "WHEN MATCHED\n" - + " UPDATE SET FirstName = v.column2,\n" - + " LastName = v.column3;"; - String sqlWithoutComments = - "@EXPECT EXCEPTION INVALID_ARGUMENT 'INVALID_ARGUMENT: Unknown statement'\n" - + "MERGE INTO Singers s\n" - + "USING (VALUES (1, 'John', 'Doe')) v\n" - + "ON v.column1 = s.SingerId\n" - + "\nWHEN NOT MATCHED \n" - + " INSERT VALUES (v.column1, v.column2, v.column3)\n" - + "WHEN MATCHED\n" - + " UPDATE SET FirstName = v.column2,\n" - + " LastName = v.column3"; - ParsedStatement statement = parser.parse(Statement.of(sql)); - assertThat(statement.getSqlWithoutComments(), is(equalTo(sqlWithoutComments))); - } - - @Test - public void testStatementWithHashTagSingleLineComment() { - assertThat( - parser - .parse(Statement.of("# this is a comment\nselect * from foo")) - .getSqlWithoutComments(), - is(equalTo("select * from foo"))); - assertThat( - parser.parse(Statement.of("select * from foo\n#this is a comment")).getSqlWithoutComments(), - is(equalTo("select * from foo"))); - assertThat( - parser - .parse(Statement.of("select *\nfrom foo # this is a comment\nwhere bar=1")) - .getSqlWithoutComments(), - is(equalTo("select *\nfrom foo \nwhere bar=1"))); - } - - @Test - public void testIsDdlStatement() { - assertFalse(parser.isDdlStatement("")); - assertFalse(parser.isDdlStatement("random text")); - assertFalse(parser.isDdlStatement("CREATETABLE")); - assertFalse(parser.isDdlStatement("CCREATE TABLE")); - assertFalse(parser.isDdlStatement("SELECT 1")); - assertFalse(parser.isDdlStatement("SELECT FOO FROM BAR")); - assertFalse(parser.isDdlStatement("INSERT INTO FOO (ID, NAME) VALUES (1, 'NAME')")); - assertFalse(parser.isDdlStatement("UPDATE FOO SET NAME='NAME' WHERE ID=1")); - assertFalse(parser.isDdlStatement("DELETE FROM FOO")); - - assertTrue( - parser.isDdlStatement("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - assertTrue(parser.isDdlStatement("alter table foo add Description string(100)")); - assertTrue(parser.isDdlStatement("drop table foo")); - assertTrue(parser.isDdlStatement("Create index BAR on foo (name)")); - - assertTrue( - parser - .parse( - Statement.of( - "\t\tCREATE\n\t TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "\n\n\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "-- this is a comment\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "/* multi line comment\n* with more information on the next line\n*/\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "/** java doc comment\n* with more information on the next line\n*/\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "-- SELECT in a single line comment \nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "/* SELECT in a multi line comment\n* with more information on the next line\n*/\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - assertTrue( - parser - .parse( - Statement.of( - "/** SELECT in a java doc comment\n* with more information on the next line\n*/\nCREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")) - .isDdl()); - } - - @Test - public void testIsQuery() { - assertFalse(parser.isQuery("")); - assertFalse(parser.isQuery("random text")); - assertFalse(parser.isQuery("SELECT1")); - assertFalse(parser.isQuery("SSELECT 1")); - assertTrue(parser.isQuery("SELECT 1")); - assertTrue(parser.isQuery("select 1")); - assertTrue(parser.isQuery("SELECT foo FROM bar WHERE id=@id")); - assertFalse(parser.isQuery("INSERT INTO FOO (ID, NAME) VALUES (1, 'NAME')")); - assertFalse(parser.isQuery("UPDATE FOO SET NAME='NAME' WHERE ID=1")); - assertFalse(parser.isQuery("DELETE FROM FOO")); - assertFalse(parser.isQuery("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - assertFalse(parser.isQuery("alter table foo add Description string(100)")); - assertFalse(parser.isQuery("drop table foo")); - assertFalse(parser.isQuery("Create index BAR on foo (name)")); - assertTrue(parser.isQuery("select * from foo")); - assertFalse(parser.isQuery("INSERT INTO FOO (ID, NAME) SELECT ID+1, NAME FROM FOO")); - - assertTrue( - parser.isQuery( - "WITH subQ1 AS (SELECT SchoolID FROM Roster),\n" - + " subQ2 AS (SELECT OpponentID FROM PlayerStats)\n" - + "SELECT * FROM subQ1\n" - + "UNION ALL\n" - + "SELECT * FROM subQ2")); - assertTrue( - parser.isQuery( - "with subQ1 AS (SELECT SchoolID FROM Roster),\n" - + " subQ2 AS (SELECT OpponentID FROM PlayerStats)\n" - + "select * FROM subQ1\n" - + "UNION ALL\n" - + "SELECT * FROM subQ2")); - assertTrue( - parser - .parse( - Statement.of( - "-- this is a comment\nwith foo as (select * from bar)\nselect * from foo")) - .isQuery()); - - assertTrue(parser.parse(Statement.of("-- this is a comment\nselect * from foo")).isQuery()); - assertTrue( - parser - .parse( - Statement.of( - "/* multi line comment\n* with more information on the next line\n*/\nSELECT ID, NAME\nFROM\tTEST\n\tWHERE ID=1")) - .isQuery()); - assertTrue( - parser - .parse( - Statement.of( - "/** java doc comment\n* with more information on the next line\n*/\nselect max(id) from test")) - .isQuery()); - assertTrue( - parser.parse(Statement.of("-- INSERT in a single line comment \n select 1")).isQuery()); - assertTrue( - parser - .parse( - Statement.of( - "/* UPDATE in a multi line comment\n* with more information on the next line\n*/\nSELECT 1")) - .isQuery()); - assertTrue( - parser - .parse( - Statement.of( - "/** DELETE in a java doc comment\n* with more information on the next line\n*/\n\n\n\n -- UPDATE test\nSELECT 1")) - .isQuery()); - } - - @Test - public void testQueryHints() { - // Valid query hints. - assertTrue(parser.isQuery("@{JOIN_METHOD=HASH_JOIN} SELECT * FROM PersonsTable")); - assertTrue(parser.isQuery("@ {JOIN_METHOD=HASH_JOIN} SELECT * FROM PersonsTable")); - assertTrue(parser.isQuery("@{ JOIN_METHOD=HASH_JOIN} SELECT * FROM PersonsTable")); - assertTrue(parser.isQuery("@{JOIN_METHOD=HASH_JOIN } SELECT * FROM PersonsTable")); - assertTrue(parser.isQuery("@{JOIN_METHOD=HASH_JOIN}\nSELECT * FROM PersonsTable")); - assertTrue(parser.isQuery("@{\nJOIN_METHOD = HASH_JOIN \t}\n\t SELECT * FROM PersonsTable")); - assertTrue( - parser.isQuery( - "@{JOIN_METHOD=HASH_JOIN}\n -- Single line comment\nSELECT * FROM PersonsTable")); - assertTrue( - parser.isQuery( - "@{JOIN_METHOD=HASH_JOIN}\n /* Multi line comment\n with more comments\n */SELECT * FROM PersonsTable")); - assertTrue( - parser.isQuery( - "@{JOIN_METHOD=HASH_JOIN} WITH subQ1 AS (SELECT SchoolID FROM Roster),\n" - + " subQ2 AS (SELECT OpponentID FROM PlayerStats)\n" - + "SELECT * FROM subQ1\n" - + "UNION ALL\n" - + "SELECT * FROM subQ2")); - - // Invalid query hints. - assertFalse(parser.isQuery("@{JOIN_METHOD=HASH_JOIN SELECT * FROM PersonsTable")); - assertFalse(parser.isQuery("@JOIN_METHOD=HASH_JOIN} SELECT * FROM PersonsTable")); - assertFalse(parser.isQuery("@JOIN_METHOD=HASH_JOIN SELECT * FROM PersonsTable")); - } - - @Test - public void testIsUpdate_InsertStatements() { - assertFalse(parser.isUpdateStatement("")); - assertFalse(parser.isUpdateStatement("random text")); - assertFalse(parser.isUpdateStatement("INSERTINTO FOO (ID) VALUES (1)")); - assertFalse(parser.isUpdateStatement("IINSERT INTO FOO (ID) VALUES (1)")); - assertTrue(parser.isUpdateStatement("INSERT INTO FOO (ID) VALUES (1)")); - assertTrue(parser.isUpdateStatement("insert into foo (id) values (1)")); - assertTrue(parser.isUpdateStatement("INSERT into Foo (id)\nSELECT id FROM bar WHERE id=@id")); - assertFalse(parser.isUpdateStatement("SELECT 1")); - assertFalse(parser.isUpdateStatement("SELECT NAME FROM FOO WHERE ID=1")); - assertFalse( - parser.isUpdateStatement("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - assertFalse(parser.isUpdateStatement("alter table foo add Description string(100)")); - assertFalse(parser.isUpdateStatement("drop table foo")); - assertFalse(parser.isUpdateStatement("Create index BAR on foo (name)")); - assertFalse(parser.isUpdateStatement("select * from foo")); - assertTrue(parser.isUpdateStatement("INSERT INTO FOO (ID, NAME) SELECT ID+1, NAME FROM FOO")); - assertTrue( - parser - .parse(Statement.of("-- this is a comment\ninsert into foo (id) values (1)")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* multi line comment\n* with more information on the next line\n*/\nINSERT INTO FOO\n(ID)\tVALUES\n\t(1)")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** java doc comment\n* with more information on the next line\n*/\nInsert intO foo (id) select 1")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "-- SELECT in a single line comment \n insert into foo (id) values (1)")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* CREATE in a multi line comment\n* with more information on the next line\n*/\nINSERT INTO FOO (ID) VALUES (1)")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** DROP in a java doc comment\n* with more information on the next line\n*/\n\n\n\n -- SELECT test\ninsert into foo (id) values (1)")) - .isUpdate()); - } - - @Test - public void testIsUpdate_UpdateStatements() { - assertFalse(parser.isUpdateStatement("")); - assertFalse(parser.isUpdateStatement("random text")); - assertFalse(parser.isUpdateStatement("UPDATEFOO SET NAME='foo' WHERE ID=1")); - assertFalse(parser.isUpdateStatement("UUPDATE FOO SET NAME='foo' WHERE ID=1")); - assertTrue(parser.isUpdateStatement("UPDATE FOO SET NAME='foo' WHERE ID=1")); - assertTrue(parser.isUpdateStatement("update foo set name='foo' where id=1")); - assertTrue( - parser.isUpdateStatement("update foo set name=\n(SELECT name FROM bar WHERE id=@id)")); - assertFalse(parser.isUpdateStatement("SELECT 1")); - assertFalse(parser.isUpdateStatement("SELECT NAME FROM FOO WHERE ID=1")); - assertFalse( - parser.isUpdateStatement("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - assertFalse(parser.isUpdateStatement("alter table foo add Description string(100)")); - assertFalse(parser.isUpdateStatement("drop table foo")); - assertFalse(parser.isUpdateStatement("Create index BAR on foo (name)")); - assertFalse(parser.isUpdateStatement("select * from foo")); - assertTrue( - parser.isUpdateStatement( - "UPDATE FOO SET NAME=(SELECT NAME FROM FOO) WHERE ID=(SELECT ID+1 FROM FOO)")); - - assertTrue( - parser - .parse(Statement.of("-- this is a comment\nupdate foo set name='foo' where id=@id")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* multi line comment\n* with more information on the next line\n*/\nUPDATE FOO\nSET NAME=\t'foo'\n\tWHERE ID=1")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** java doc comment\n* with more information on the next line\n*/\nUPDATE FOO SET NAME=(select 'bar')")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of("-- SELECT in a single line comment \n update foo set name='bar'")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* CREATE in a multi line comment\n* with more information on the next line\n*/\nUPDATE FOO SET NAME='BAR'")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** DROP in a java doc comment\n* with more information on the next line\n*/\n\n\n\n -- SELECT test\nupdate foo set bar='foo'")) - .isUpdate()); - } - - @Test - public void testIsUpdate_DeleteStatements() { - assertFalse(parser.isUpdateStatement("")); - assertFalse(parser.isUpdateStatement("random text")); - assertFalse(parser.isUpdateStatement("DELETEFROM FOO WHERE ID=1")); - assertFalse(parser.isUpdateStatement("DDELETE FROM FOO WHERE ID=1")); - assertTrue(parser.isUpdateStatement("DELETE FROM FOO WHERE ID=1")); - assertTrue(parser.isUpdateStatement("delete from foo where id=1")); - assertTrue( - parser.isUpdateStatement( - "delete from foo where name=\n(SELECT name FROM bar WHERE id=@id)")); - assertFalse(parser.isUpdateStatement("SELECT 1")); - assertFalse(parser.isUpdateStatement("SELECT NAME FROM FOO WHERE ID=1")); - assertFalse( - parser.isUpdateStatement("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - assertFalse(parser.isUpdateStatement("alter table foo add Description string(100)")); - assertFalse(parser.isUpdateStatement("drop table foo")); - assertFalse(parser.isUpdateStatement("Create index BAR on foo (name)")); - assertFalse(parser.isUpdateStatement("select * from foo")); - assertTrue( - parser.isUpdateStatement( - "UPDATE FOO SET NAME=(SELECT NAME FROM FOO) WHERE ID=(SELECT ID+1 FROM FOO)")); - - assertTrue( - parser - .parse(Statement.of("-- this is a comment\ndelete from foo where id=@id")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* multi line comment\n* with more information on the next line\n*/\nDELETE FROM FOO\n\n\tWHERE ID=1")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** java doc comment\n* with more information on the next line\n*/\nDELETE FROM FOO WHERE NAME=(select 'bar')")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "-- SELECT in a single line comment \n delete from foo where name='bar'")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/* CREATE in a multi line comment\n* with more information on the next line\n*/\nDELETE FROM FOO WHERE NAME='BAR'")) - .isUpdate()); - assertTrue( - parser - .parse( - Statement.of( - "/** DROP in a java doc comment\n* with more information on the next line\n*/\n\n\n\n -- SELECT test\ndelete from foo where bar='foo'")) - .isUpdate()); - } - - @Test - public void testParseStatementsWithNoParameters() throws CompileException { - for (ClientSideStatementImpl statement : getAllStatements()) { - if (statement.getSetStatement() == null) { - for (String testStatement : statement.getExampleStatements()) { - testParseStatement(testStatement, statement.getClass()); - } - } - } - } - - @Test - public void testParseStatementsWithOneParameterAtTheEnd() throws CompileException { - for (ClientSideStatementImpl statement : getAllStatements()) { - if (statement.getSetStatement() != null) { - for (String testStatement : statement.getExampleStatements()) { - testParseStatementWithOneParameterAtTheEnd(testStatement, statement.getClass()); - } - } - } - } - - private Set getAllStatements() throws CompileException { - return ClientSideStatements.INSTANCE.getCompiledStatements(); - } - - private void assertParsing( - String value, Class statementClass) { - assertThat(this.parse(value), is(equalTo(statementClass))); - } - - private void testParseStatement( - String statement, Class statementClass) { - assertThat( - "\"" + statement + "\" should be " + statementClass.getName(), - this.parse(statement), - is(equalTo(statementClass))); - assertParsing(upper(statement), statementClass); - assertParsing(lower(statement), statementClass); - assertParsing(withSpaces(statement), statementClass); - assertParsing(withTabs(statement), statementClass); - assertParsing(withLinefeeds(statement), statementClass); - assertParsing(withLeadingSpaces(statement), statementClass); - assertParsing(withLeadingTabs(statement), statementClass); - assertParsing(withLeadingLinefeeds(statement), statementClass); - assertParsing(withTrailingSpaces(statement), statementClass); - assertParsing(withTrailingTabs(statement), statementClass); - assertParsing(withTrailingLinefeeds(statement), statementClass); - - assertThat(parse(withInvalidPrefix(statement)), is(nullValue())); - assertThat(parse(withInvalidSuffix(statement)), is(nullValue())); - - assertNull(parse(withPrefix("%", statement))); - assertNull(parse(withPrefix("_", statement))); - assertNull(parse(withPrefix("&", statement))); - assertNull(parse(withPrefix("$", statement))); - assertNull(parse(withPrefix("@", statement))); - assertNull(parse(withPrefix("!", statement))); - assertNull(parse(withPrefix("*", statement))); - assertNull(parse(withPrefix("(", statement))); - assertNull(parse(withPrefix(")", statement))); - - assertThat( - withSuffix("%", statement) + " is not a valid statement", - parse(withSuffix("%", statement)), - is(nullValue())); - assertNull(parse(withSuffix("_", statement))); - assertNull(parse(withSuffix("&", statement))); - assertNull(parse(withSuffix("$", statement))); - assertNull(parse(withSuffix("@", statement))); - assertNull(parse(withSuffix("!", statement))); - assertNull(parse(withSuffix("*", statement))); - assertNull(parse(withSuffix("(", statement))); - assertNull(parse(withSuffix(")", statement))); - } - - private void testParseStatementWithOneParameterAtTheEnd( - String statement, Class statementClass) { - assertThat( - "\"" + statement + "\" should be " + statementClass.getName(), - this.parse(statement), - is(equalTo(statementClass))); - assertParsing(upper(statement), statementClass); - assertParsing(lower(statement), statementClass); - assertParsing(withSpaces(statement), statementClass); - assertParsing(withTabs(statement), statementClass); - assertParsing(withLinefeeds(statement), statementClass); - assertParsing(withLeadingSpaces(statement), statementClass); - assertParsing(withLeadingTabs(statement), statementClass); - assertParsing(withLeadingLinefeeds(statement), statementClass); - assertParsing(withTrailingSpaces(statement), statementClass); - assertParsing(withTrailingTabs(statement), statementClass); - assertParsing(withTrailingLinefeeds(statement), statementClass); - - assertNull(parse(withInvalidPrefix(statement))); - assertParsing(withInvalidSuffix(statement), statementClass); - - assertNull(parse(withPrefix("%", statement))); - assertNull(parse(withPrefix("_", statement))); - assertNull(parse(withPrefix("&", statement))); - assertNull(parse(withPrefix("$", statement))); - assertNull(parse(withPrefix("@", statement))); - assertNull(parse(withPrefix("!", statement))); - assertNull(parse(withPrefix("*", statement))); - assertNull(parse(withPrefix("(", statement))); - assertNull(parse(withPrefix(")", statement))); - - assertParsing(withSuffix("%", statement), statementClass); - assertParsing(withSuffix("_", statement), statementClass); - assertParsing(withSuffix("&", statement), statementClass); - assertParsing(withSuffix("$", statement), statementClass); - assertParsing(withSuffix("@", statement), statementClass); - assertParsing(withSuffix("!", statement), statementClass); - assertParsing(withSuffix("*", statement), statementClass); - assertParsing(withSuffix("(", statement), statementClass); - assertParsing(withSuffix(")", statement), statementClass); - } - - @SuppressWarnings("unchecked") - private Class parse(String statement) { - ClientSideStatementImpl optional = parser.parseClientSideStatement(statement); - return optional != null ? (Class) optional.getClass() : null; - } - - private String upper(String statement) { - return statement.toUpperCase(); - } - - private String lower(String statement) { - return statement.toLowerCase(); - } - - private String withLeadingSpaces(String statement) { - return " " + statement; - } - - private String withLeadingTabs(String statement) { - return "\t\t\t" + statement; - } - - private String withLeadingLinefeeds(String statement) { - return "\n\n\n" + statement; - } - - private String withTrailingSpaces(String statement) { - return statement + " "; - } - - private String withTrailingTabs(String statement) { - return statement + "\t\t"; - } - - private String withTrailingLinefeeds(String statement) { - return statement + "\n\n"; - } - - private String withSpaces(String statement) { - return statement.replaceAll(" ", " "); - } - - private String withTabs(String statement) { - return statement.replaceAll(" ", "\t"); - } - - private String withLinefeeds(String statement) { - return statement.replaceAll(" ", "\n"); - } - - private String withInvalidPrefix(String statement) { - return "foo " + statement; - } - - private String withInvalidSuffix(String statement) { - return statement + " bar"; - } - - private String withPrefix(String prefix, String statement) { - return prefix + statement; - } - - private String withSuffix(String suffix, String statement) { - return statement + suffix; - } - - private List readStatementsFromFile(String filename) { - File file = new File(getClass().getResource(filename).getFile()); - StringBuilder builder = new StringBuilder(); - try (Scanner scanner = new Scanner(file)) { - while (scanner.hasNextLine()) { - String line = scanner.nextLine(); - builder.append(line).append("\n"); - } - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - String script = builder.toString().replaceAll(COPYRIGHT_PATTERN, ""); - String[] array = script.split(";"); - List res = new ArrayList<>(array.length); - for (String statement : array) { - if (statement != null && statement.trim().length() > 0) { - res.add(statement); - } - } - return res; - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/StatementResultImplTest.java b/src/test/java/com/google/cloud/spanner/jdbc/StatementResultImplTest.java deleted file mode 100644 index ea0b751a..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/StatementResultImplTest.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.mock; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType; -import com.google.cloud.spanner.jdbc.StatementResult.ResultType; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class StatementResultImplTest { - @Rule public ExpectedException exception = ExpectedException.none(); - - @Test - public void testNoResultGetResultSet() { - StatementResult subject = StatementResultImpl.noResult(); - assertThat(subject.getResultType(), is(equalTo(ResultType.NO_RESULT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getResultSet(); - } - - @Test - public void testNoResultGetUpdateCount() { - StatementResult subject = StatementResultImpl.noResult(); - assertThat(subject.getResultType(), is(equalTo(ResultType.NO_RESULT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getUpdateCount(); - } - - @Test - public void testResultSetGetResultSet() { - StatementResult subject = StatementResultImpl.of(mock(ResultSet.class)); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat(subject.getResultSet(), is(notNullValue())); - } - - @Test - public void testResultSetGetUpdateCount() { - StatementResult subject = StatementResultImpl.of(mock(ResultSet.class)); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getUpdateCount(); - } - - @Test - public void testUpdateCountGetResultSet() { - StatementResult subject = StatementResultImpl.of(1L); - assertThat(subject.getResultType(), is(equalTo(ResultType.UPDATE_COUNT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getResultSet(); - } - - @Test - public void testUpdateCountGetUpdateCount() { - StatementResult subject = StatementResultImpl.of(1L); - assertThat(subject.getResultType(), is(equalTo(ResultType.UPDATE_COUNT))); - assertThat(subject.getUpdateCount(), is(notNullValue())); - } - - @Test - public void testBooleanResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet("foo", Boolean.TRUE, ClientSideStatementType.SHOW_AUTOCOMMIT); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), is(equalTo(ClientSideStatementType.SHOW_AUTOCOMMIT))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat(subject.getResultSet().getBoolean("foo"), is(true)); - assertThat(subject.getResultSet().next(), is(false)); - } - - @Test - public void testLongResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet("foo", 10L, ClientSideStatementType.SHOW_READ_ONLY_STALENESS); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), - is(equalTo(ClientSideStatementType.SHOW_READ_ONLY_STALENESS))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat(subject.getResultSet().getLong("foo"), is(equalTo(10L))); - assertThat(subject.getResultSet().next(), is(false)); - } - - @Test - public void testLongArrayResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet( - "foo", new long[] {1L, 2L, 3L}, ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), - is(equalTo(ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat(subject.getResultSet().getLongArray("foo"), is(equalTo(new long[] {1L, 2L, 3L}))); - assertThat(subject.getResultSet().next(), is(false)); - } - - @Test - public void testStringResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet( - "foo", "bar", ClientSideStatementType.SHOW_READ_ONLY_STALENESS); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), - is(equalTo(ClientSideStatementType.SHOW_READ_ONLY_STALENESS))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat(subject.getResultSet().getString("foo"), is(equalTo("bar"))); - assertThat(subject.getResultSet().next(), is(false)); - } - - @Test - public void testEnumResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet( - "foo", TransactionMode.READ_ONLY_TRANSACTION, ClientSideStatementType.SHOW_READONLY); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), is(equalTo(ClientSideStatementType.SHOW_READONLY))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat( - subject.getResultSet().getString("foo"), - is(equalTo(TransactionMode.READ_ONLY_TRANSACTION.toString()))); - assertThat(subject.getResultSet().next(), is(false)); - } - - @Test - public void testTimestampResultSetGetResultSet() { - StatementResult subject = - StatementResultImpl.resultSet( - "foo", - Timestamp.ofTimeSecondsAndNanos(10L, 10), - ClientSideStatementType.SHOW_READ_TIMESTAMP); - assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - assertThat( - subject.getClientSideStatementType(), - is(equalTo(ClientSideStatementType.SHOW_READ_TIMESTAMP))); - assertThat(subject.getResultSet(), is(notNullValue())); - assertThat(subject.getResultSet().next(), is(true)); - assertThat( - subject.getResultSet().getTimestamp("foo"), - is(equalTo(Timestamp.ofTimeSecondsAndNanos(10L, 10)))); - assertThat(subject.getResultSet().next(), is(false)); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/StatementTimeoutTest.java b/src/test/java/com/google/cloud/spanner/jdbc/StatementTimeoutTest.java deleted file mode 100644 index 5a6fb740..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/StatementTimeoutTest.java +++ /dev/null @@ -1,1192 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.longrunning.OperationFuture; -import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.ReadOnlyTransaction; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.TimestampBound; -import com.google.cloud.spanner.TransactionContext; -import com.google.cloud.spanner.TransactionManager; -import com.google.cloud.spanner.TransactionManager.TransactionState; -import com.google.cloud.spanner.jdbc.AbstractConnectionImplTest.ConnectionConsumer; -import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; -import java.util.Arrays; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Matchers; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -@RunWith(JUnit4.class) -public class StatementTimeoutTest { - private static final String URI = - "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; - private static final String SLOW_SELECT = "SELECT foo FROM bar"; - private static final String INVALID_SELECT = "SELECT FROM bar"; // missing columns / * - private static final String FAST_SELECT = "SELECT fast_column FROM fast_table"; - private static final String SLOW_DDL = "CREATE TABLE foo"; - private static final String FAST_DDL = "CREATE TABLE fast_table"; - private static final String SLOW_UPDATE = "UPDATE foo SET col1=1 WHERE id=2"; - private static final String FAST_UPDATE = "UPDATE fast_table SET foo=1 WHERE bar=2"; - - /** Execution time for statements that have been defined as slow. */ - private static final long EXECUTION_TIME_SLOW_STATEMENT = 10_000L; - /** - * This timeout should be high enough that it will never be exceeded, even on a slow build - * environment, but still significantly lower than the expected execution time of the slow - * statements. - */ - private static final long TIMEOUT_FOR_FAST_STATEMENTS = 1000L; - - /** - * This timeout should be low enough that it will not make the test case unnecessarily slow, but - * still high enough that it would normally not be exceeded for a statement that is executed - * directly. - */ - private static final long TIMEOUT_FOR_SLOW_STATEMENTS = 20L; - /** - * The number of milliseconds to wait before cancelling a query should be high enough to not cause - * flakiness on a slow environment, but at the same time low enough that it does not slow down the - * test case unnecessarily. - */ - private static final int WAIT_BEFORE_CANCEL = 100; - - private enum CommitRollbackBehavior { - FAST, - SLOW_COMMIT, - SLOW_ROLLBACK; - } - - @Rule public ExpectedException expected = ExpectedException.none(); - - private static final class DelayedQueryExecution implements Answer { - @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return mock(ResultSet.class); - } - } - - private DdlClient createDefaultMockDdlClient(final long waitForMillis) { - try { - DdlClient ddlClient = mock(DdlClient.class); - UpdateDatabaseDdlMetadata metadata = UpdateDatabaseDdlMetadata.getDefaultInstance(); - ApiFuture futureMetadata = ApiFutures.immediateFuture(metadata); - @SuppressWarnings("unchecked") - final OperationFuture operation = - mock(OperationFuture.class); - if (waitForMillis > 0L) { - when(operation.get()) - .thenAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(waitForMillis); - return null; - } - }); - } else { - when(operation.get()).thenReturn(null); - } - when(operation.getMetadata()).thenReturn(futureMetadata); - when(ddlClient.executeDdl(SLOW_DDL)).thenCallRealMethod(); - when(ddlClient.executeDdl(anyListOf(String.class))).thenReturn(operation); - - @SuppressWarnings("unchecked") - final OperationFuture fastOperation = - mock(OperationFuture.class); - when(fastOperation.isDone()).thenReturn(true); - when(fastOperation.get()).thenReturn(null); - when(fastOperation.getMetadata()).thenReturn(futureMetadata); - when(ddlClient.executeDdl(FAST_DDL)).thenReturn(fastOperation); - when(ddlClient.executeDdl(Arrays.asList(FAST_DDL))).thenReturn(fastOperation); - return ddlClient; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private ConnectionImpl createConnection(ConnectionOptions options) { - return createConnection(options, CommitRollbackBehavior.FAST); - } - - /** - * Creates a connection on which the statements {@link StatementTimeoutTest#SLOW_SELECT} and - * {@link StatementTimeoutTest#SLOW_DDL} will take at least 10,000 milliseconds - */ - private ConnectionImpl createConnection( - ConnectionOptions options, final CommitRollbackBehavior commitRollbackBehavior) { - DatabaseClient dbClient = mock(DatabaseClient.class); - Spanner spanner = mock(Spanner.class); - SpannerPool spannerPool = mock(SpannerPool.class); - when(spannerPool.getSpanner(any(ConnectionOptions.class), any(ConnectionImpl.class))) - .thenReturn(spanner); - DdlClient ddlClient = createDefaultMockDdlClient(EXECUTION_TIME_SLOW_STATEMENT); - final ResultSet invalidResultSet = mock(ResultSet.class); - when(invalidResultSet.next()) - .thenThrow( - SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, "invalid query")); - - ReadOnlyTransaction singleUseReadOnlyTx = mock(ReadOnlyTransaction.class); - when(singleUseReadOnlyTx.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(singleUseReadOnlyTx.executeQuery(Statement.of(FAST_SELECT))) - .thenReturn(mock(ResultSet.class)); - when(singleUseReadOnlyTx.executeQuery(Statement.of(INVALID_SELECT))) - .thenReturn(invalidResultSet); - when(dbClient.singleUseReadOnlyTransaction(Matchers.any(TimestampBound.class))) - .thenReturn(singleUseReadOnlyTx); - - ReadOnlyTransaction readOnlyTx = mock(ReadOnlyTransaction.class); - when(readOnlyTx.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(readOnlyTx.executeQuery(Statement.of(FAST_SELECT))).thenReturn(mock(ResultSet.class)); - when(readOnlyTx.executeQuery(Statement.of(INVALID_SELECT))).thenReturn(invalidResultSet); - when(dbClient.readOnlyTransaction(Matchers.any(TimestampBound.class))).thenReturn(readOnlyTx); - - when(dbClient.transactionManager()) - .thenAnswer( - new Answer() { - @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { - TransactionManager txManager = mock(TransactionManager.class); - when(txManager.getState()).thenReturn(null, TransactionState.STARTED); - when(txManager.begin()) - .thenAnswer( - new Answer() { - @Override - public TransactionContext answer(InvocationOnMock invocation) - throws Throwable { - TransactionContext txContext = mock(TransactionContext.class); - when(txContext.executeQuery(Statement.of(SLOW_SELECT))) - .thenAnswer(new DelayedQueryExecution()); - when(txContext.executeQuery(Statement.of(FAST_SELECT))) - .thenReturn(mock(ResultSet.class)); - when(txContext.executeQuery(Statement.of(INVALID_SELECT))) - .thenReturn(invalidResultSet); - when(txContext.executeUpdate(Statement.of(SLOW_UPDATE))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) - throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return 1L; - } - }); - when(txContext.executeUpdate(Statement.of(FAST_UPDATE))).thenReturn(1L); - return txContext; - } - }); - if (commitRollbackBehavior == CommitRollbackBehavior.SLOW_COMMIT) { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return null; - } - }) - .when(txManager) - .commit(); - } - if (commitRollbackBehavior == CommitRollbackBehavior.SLOW_ROLLBACK) { - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return null; - } - }) - .when(txManager) - .rollback(); - } - - return txManager; - } - }); - when(dbClient.executePartitionedUpdate(Statement.of(FAST_UPDATE))).thenReturn(1L); - when(dbClient.executePartitionedUpdate(Statement.of(SLOW_UPDATE))) - .thenAnswer( - new Answer() { - @Override - public Long answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(EXECUTION_TIME_SLOW_STATEMENT); - return 1L; - } - }); - return new ConnectionImpl(options, spannerPool, ddlClient, dbClient); - } - - @Test - public void testTimeoutExceptionReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testTimeoutExceptionReadOnlyAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testTimeoutExceptionReadOnlyTransactionMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // do a rollback without any chance of a timeout - connection.clearStatementTimeout(); - connection.rollback(); - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitSlowUpdate() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(SLOW_UPDATE)); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitSlowUpdateMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.execute(Statement.of(SLOW_UPDATE)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new update that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.execute(Statement.of(FAST_UPDATE)).getUpdateCount(), is(equalTo(1L))); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - // First verify that the fast update does not timeout when in transactional mode (as it is the - // commit that is slow). - connection.setAutocommit(false); - connection.execute(Statement.of(FAST_UPDATE)); - connection.rollback(); - - // Then verify that the update does timeout when executed in autocommit mode, as the commit - // gRPC call will be slow. - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - connection.setAutocommit(true); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(FAST_UPDATE)); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitSlowCommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.execute(Statement.of(FAST_UPDATE)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionReadWriteAutocommitPartitioned() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommitDmlMode(AutocommitDmlMode.PARTITIONED_NON_ATOMIC); - // first verify that the fast update does not timeout - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.execute(Statement.of(FAST_UPDATE)); - - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(SLOW_UPDATE)); - } - } - - @Test - public void testTimeoutExceptionReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testTimeoutExceptionReadWriteTransactionMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - // Assert that multiple statements after each other will timeout the first time, and then - // throw a SpannerException with code FAILED_PRECONDITION. - boolean timedOut = false; - for (int i = 0; i < 2; i++) { - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - if (i == 0) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.DEADLINE_EXCEEDED))); - timedOut = true; - } else { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.FAILED_PRECONDITION))); - } - } - } - assertThat(timedOut, is(true)); - // do a rollback without any chance of a timeout - connection.clearStatementTimeout(); - connection.rollback(); - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionReadWriteTransactionalSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { - connection.setAutocommit(false); - - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.executeQuery(Statement.of(FAST_SELECT)); - - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.commit(); - } - } - - @Test - public void testTimeoutExceptionReadWriteTransactionalSlowRollback() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_ROLLBACK)) { - connection.setAutocommit(false); - - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.executeQuery(Statement.of(FAST_SELECT)); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.rollback(); - } - } - - private static final class ConnectionReadOnlyAutocommit implements ConnectionConsumer { - @Override - public void accept(Connection t) { - t.setReadOnly(true); - } - } - - @Test - public void testInterruptedExceptionReadOnlyAutocommit() - throws InterruptedException, ExecutionException { - testInterruptedException(new ConnectionReadOnlyAutocommit()); - } - - private static final class ConnectionReadOnlyTransactional implements ConnectionConsumer { - @Override - public void accept(Connection t) { - t.setReadOnly(true); - t.setAutocommit(false); - } - } - - @Test - public void testInterruptedExceptionReadOnlyTransactional() - throws InterruptedException, ExecutionException { - testInterruptedException(new ConnectionReadOnlyTransactional()); - } - - private static final class ConnectionReadWriteAutocommit implements ConnectionConsumer { - @Override - public void accept(Connection t) {} - } - - @Test - public void testInterruptedExceptionReadWriteAutocommit() - throws InterruptedException, ExecutionException { - testInterruptedException(new ConnectionReadWriteAutocommit()); - } - - private static final class ConnectionReadWriteTransactional implements ConnectionConsumer { - @Override - public void accept(Connection t) { - t.setAutocommit(false); - } - } - - @Test - public void testInterruptedExceptionReadWriteTransactional() - throws InterruptedException, ExecutionException { - testInterruptedException(new ConnectionReadWriteTransactional()); - } - - private void testInterruptedException(final ConnectionConsumer consumer) - throws InterruptedException, ExecutionException { - ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = - executor.submit( - new Callable() { - @Override - public Boolean call() throws Exception { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - consumer.accept(connection); - connection.setStatementTimeout(10000L, TimeUnit.MILLISECONDS); - - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - if (e.getErrorCode() == ErrorCode.CANCELLED) { - return Boolean.TRUE; - } else { - return Boolean.FALSE; - } - } - return Boolean.FALSE; - } - }); - // wait a little bit to ensure that the task has started - Thread.sleep(10L); - executor.shutdownNow(); - assertThat(future.get(), is(true)); - } - - @Test - public void testInvalidQueryReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setUri(URI) - .setCredentials(NoCredentials.getInstance()) - .build())) { - connection.setReadOnly(true); - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); - } - } - - @Test - public void testInvalidQueryReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); - } - } - - @Test - public void testInvalidQueryReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); - } - } - - @Test - public void testInvalidQueryReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); - } - } - - @Test - public void testCancelReadOnlyAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testCancelReadOnlyAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - boolean cancelled = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; - } - assertThat(cancelled, is(true)); - - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testCancelReadOnlyTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testCancelReadOnlyTransactionalMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setReadOnly(true); - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - boolean cancelled = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; - } - assertThat(cancelled, is(true)); - - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - // rollback and do another fast query - connection.rollback(); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testCancelReadWriteAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testCancelReadWriteAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - boolean cancelled = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; - } - assertThat(cancelled, is(true)); - - // try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testCancelReadWriteAutocommitSlowUpdate() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.execute(Statement.of(SLOW_UPDATE)); - } - } - - @Test - public void testCancelReadWriteAutocommitSlowCommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build(), - CommitRollbackBehavior.SLOW_COMMIT)) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.execute(Statement.of(FAST_UPDATE)); - } - } - - @Test - public void testCancelReadWriteTransactional() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); - } - } - - @Test - public void testCancelReadWriteTransactionalMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - boolean cancelled = false; - try { - connection.executeQuery(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - cancelled = e.getErrorCode() == ErrorCode.CANCELLED; - } - assertThat(cancelled, is(true)); - // Rollback the transaction as it is no longer usable. - connection.rollback(); - - // Try to do a new query that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.executeQuery(Statement.of(FAST_SELECT)), is(notNullValue())); - } - } - - @Test - public void testCancelDdlBatch() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.startBatchDdl(); - connection.execute(Statement.of(SLOW_DDL)); - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.runBatch(); - } - } - - @Test - public void testCancelDdlAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - Executors.newSingleThreadScheduledExecutor() - .schedule( - new Runnable() { - @Override - public void run() { - connection.cancel(); - } - }, - WAIT_BEFORE_CANCEL, - TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.execute(Statement.of(SLOW_DDL)); - } - } - - @Test - public void testTimeoutExceptionDdlAutocommit() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(SLOW_DDL)); - } - } - - @Test - public void testTimeoutExceptionDdlAutocommitMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - try { - connection.execute(Statement.of(SLOW_DDL)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new DDL statement that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - assertThat(connection.execute(Statement.of(FAST_DDL)), is(notNullValue())); - } - } - - @Test - public void testTimeoutExceptionDdlBatch() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.startBatchDdl(); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - - // the following statement will NOT timeout as the statement is only buffered locally - connection.execute(Statement.of(SLOW_DDL)); - // the commit sends the statement to the server and should timeout - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.runBatch(); - } - } - - @Test - public void testTimeoutExceptionDdlBatchMultipleStatements() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - connection.setAutocommit(false); - connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - - // assert that multiple statements after each other also time out - for (int i = 0; i < 2; i++) { - boolean timedOut = false; - connection.startBatchDdl(); - connection.execute(Statement.of(SLOW_DDL)); - try { - connection.runBatch(); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - // try to do a new DDL statement that is fast. - connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - connection.startBatchDdl(); - assertThat(connection.execute(Statement.of(FAST_DDL)), is(notNullValue())); - connection.runBatch(); - } - } - - @Test - public void testTimeoutDifferentTimeUnits() { - try (Connection connection = - createConnection( - ConnectionOptions.newBuilder() - .setCredentials(NoCredentials.getInstance()) - .setUri(URI) - .build())) { - for (TimeUnit unit : ReadOnlyStalenessUtil.SUPPORTED_UNITS) { - connection.setStatementTimeout(1L, unit); - boolean timedOut = false; - try { - connection.execute(Statement.of(SLOW_SELECT)); - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/TransactionModeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/TransactionModeConverterTest.java deleted file mode 100644 index 60df4fdd..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/TransactionModeConverterTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException; -import com.google.cloud.spanner.jdbc.ClientSideStatementValueConverters.TransactionModeConverter; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class TransactionModeConverterTest { - - @Test - public void testConvert() throws CompileException { - String allowedValues = - ReadOnlyStalenessConverterTest.getAllowedValues(TransactionModeConverter.class); - assertThat(allowedValues, is(notNullValue())); - TransactionModeConverter converter = new TransactionModeConverter(allowedValues); - assertThat( - converter.convert("read write"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat( - converter.convert("READ WRITE"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat( - converter.convert("Read Write"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat( - converter.convert("read write"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat( - converter.convert("READ\nWRITE"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - assertThat( - converter.convert("Read\tWrite"), is(equalTo(TransactionMode.READ_WRITE_TRANSACTION))); - - assertThat(converter.convert("read only"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat(converter.convert("READ ONLY"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat(converter.convert("Read Only"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat( - converter.convert("read only"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat(converter.convert("READ\nONLY"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - assertThat(converter.convert("Read\tOnly"), is(equalTo(TransactionMode.READ_ONLY_TRANSACTION))); - - assertThat(converter.convert(""), is(nullValue())); - assertThat(converter.convert(" "), is(nullValue())); - assertThat(converter.convert("random string"), is(nullValue())); - assertThat(converter.convert("read_write"), is(nullValue())); - assertThat(converter.convert("Read_Write"), is(nullValue())); - assertThat(converter.convert("READ_WRITE"), is(nullValue())); - assertThat(converter.convert("read_only"), is(nullValue())); - assertThat(converter.convert("Read_Only"), is(nullValue())); - assertThat(converter.convert("READ_ONLY"), is(nullValue())); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITBulkConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITBulkConnectionTest.java deleted file mode 100644 index 13496366..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITBulkConnectionTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Test opening multiple generic (not JDBC) Spanner connections. */ -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -public class ITBulkConnectionTest extends ITAbstractSpannerTest { - private static final int NUMBER_OF_TEST_CONNECTIONS = 250; - - @Test - public void testBulkCreateConnectionsSingleThreaded() { - List connections = new ArrayList<>(); - for (int i = 0; i < NUMBER_OF_TEST_CONNECTIONS; i++) { - connections.add(createConnection()); - } - for (ITConnection connection : connections) { - try (ResultSet rs = connection.executeQuery(Statement.of("select 1"))) { - assertThat(rs.next(), is(true)); - assertThat(connection.getReadTimestamp(), is(notNullValue())); - } - } - for (ITConnection connection : connections) { - connection.close(); - } - // close Spanner instances explicitly. This method will throw an exception if there are any - // connections still open in the pool - closeSpanner(); - } - - @Test - public void testBulkCreateConnectionsMultiThreaded() throws InterruptedException { - ExecutorService executor = Executors.newFixedThreadPool(50); - for (int i = 0; i < NUMBER_OF_TEST_CONNECTIONS; i++) { - executor.submit( - new Callable() { - @Override - public Void call() throws Exception { - try (ITConnection connection = createConnection()) { - try (ResultSet rs = connection.executeQuery(Statement.of("select 1"))) { - assertThat(rs.next(), is(true)); - assertThat(connection.getReadTimestamp(), is(notNullValue())); - } - } - return null; - } - }); - } - executor.shutdown(); - executor.awaitTermination(10L, TimeUnit.SECONDS); - // close Spanner instances explicitly. This method will throw an exception if there are any - // connections still open in the pool - closeSpanner(); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITDdlTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITDdlTest.java deleted file mode 100644 index 1642c26b..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITDdlTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Execute DDL statements using the generic connection API. */ -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -public class ITDdlTest extends ITAbstractSpannerTest { - - @Test - public void testSqlScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider()); - verifier.verifyStatementsInFile("ITDdlTest.sql", SqlScriptVerifier.class); - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDdlTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDdlTest.java index 4fe79a3f..0b192127 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDdlTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDdlTest.java @@ -17,9 +17,9 @@ package com.google.cloud.spanner.jdbc.it; import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcQueryOptionsTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcQueryOptionsTest.java index 8e72aa24..0516a144 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcQueryOptionsTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcQueryOptionsTest.java @@ -21,9 +21,9 @@ import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.connection.SpannerPool; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlException; -import com.google.cloud.spanner.jdbc.SpannerPool; import com.google.rpc.Code; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import java.sql.Connection; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadOnlyTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadOnlyTest.java index 163f555e..db5c8424 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadOnlyTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadOnlyTest.java @@ -18,10 +18,10 @@ import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; import java.math.BigInteger; import java.sql.Connection; import java.sql.SQLException; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadWriteAutocommitTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadWriteAutocommitTest.java index fdc8bb89..e6a29628 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadWriteAutocommitTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcReadWriteAutocommitTest.java @@ -22,10 +22,10 @@ import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.experimental.categories.Category; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlMusicScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlMusicScriptTest.java index 29b0b054..244e9f1d 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlMusicScriptTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlMusicScriptTest.java @@ -17,10 +17,10 @@ package com.google.cloud.spanner.jdbc.it; import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier.JdbcGenericConnection; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; import java.sql.Connection; import org.junit.Test; import org.junit.experimental.categories.Category; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlScriptTest.java index 87f5abec..07a1ab8f 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlScriptTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcSqlScriptTest.java @@ -22,10 +22,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier; import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier.JdbcGenericConnection; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadOnlySpannerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadOnlySpannerTest.java deleted file mode 100644 index 599a9cf6..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadOnlySpannerTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.Options; -import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SpannerExceptionMatcher; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import java.math.BigInteger; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * This test class runs a SQL script for testing a connection in read-only mode, but also contains a - * number of separate test methods that cannot be expressed in a pure SQL test. - */ -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -public class ITReadOnlySpannerTest extends ITAbstractSpannerTest { - private static final Logger logger = Logger.getLogger(ITReadOnlySpannerTest.class.getName()); - private static final long TEST_ROWS_COUNT = 1000L; - - @Rule public ExpectedException exception = ExpectedException.none(); - - @Override - protected void appendConnectionUri(StringBuilder url) { - url.append(";readOnly=true"); - } - - @Before - public void createTestTables() throws Exception { - try (ITConnection connection = createConnection()) { - if (!(tableExists(connection, "NUMBERS") && tableExists(connection, "PRIME_NUMBERS"))) { - // create tables - SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider()); - verifier.verifyStatementsInFile( - "ITReadOnlySpannerTest_CreateTables.sql", SqlScriptVerifier.class); - - // fill tables with data - connection.setAutocommit(false); - connection.setReadOnly(false); - for (long number = 1L; number <= TEST_ROWS_COUNT; number++) { - connection.bufferedWrite( - Mutation.newInsertBuilder("NUMBERS") - .set("number") - .to(number) - .set("name") - .to(Long.toBinaryString(number)) - .build()); - } - for (long number = 1L; number <= TEST_ROWS_COUNT; number++) { - if (BigInteger.valueOf(number).isProbablePrime(Integer.MAX_VALUE)) { - connection.bufferedWrite( - Mutation.newInsertBuilder("PRIME_NUMBERS") - .set("prime_number") - .to(number) - .set("binary_representation") - .to(Long.toBinaryString(number)) - .build()); - } - } - connection.commit(); - } - } - } - - @Test - public void testSqlScript() throws Exception { - // Wait 100ms to ensure that staleness tests in the script succeed. - Thread.sleep(100L); - SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider()); - verifier.verifyStatementsInFile("ITReadOnlySpannerTest.sql", SqlScriptVerifier.class); - } - - @Test - public void testStatementTimeoutTransactional() throws Exception { - try (ITConnection connection = createConnection()) { - connection.beginTransaction(); - connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - try (ResultSet rs = - connection.executeQuery( - Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) {} - // should never be reached - connection.commit(); - } - } - - @Test - public void testStatementTimeoutTransactionalMultipleStatements() throws Exception { - long startTime = System.currentTimeMillis(); - try (ITConnection connection = createConnection()) { - connection.beginTransaction(); - for (int i = 0; i < 3; i++) { - boolean timedOut = false; - connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - try (ResultSet rs = - connection.executeQuery( - Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) { - } catch (SpannerException e) { - timedOut = e.getErrorCode() == ErrorCode.DEADLINE_EXCEEDED; - } - assertThat(timedOut, is(true)); - } - connection.commit(); - } - long endTime = System.currentTimeMillis(); - long executionTime = endTime - startTime; - if (executionTime > 25L) { - logger.warning("Total test execution time exceeded 25 milliseconds: " + executionTime); - } else { - logger.info("Total test execution time: " + executionTime); - } - } - - @Test - public void testStatementTimeoutAutocommit() throws Exception { - try (ITConnection connection = createConnection()) { - assertThat(connection.isAutocommit(), is(true)); - connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - try (ResultSet rs = - connection.executeQuery( - Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) {} - } - } - - @Test - public void testAnalyzeQuery() { - try (ITConnection connection = createConnection()) { - for (QueryAnalyzeMode mode : QueryAnalyzeMode.values()) { - try (ResultSet rs = - connection.analyzeQuery( - Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"), - mode)) { - // next has not yet returned false - assertThat(rs.getStats(), is(nullValue())); - while (rs.next()) { - // ignore - } - assertThat(rs.getStats(), is(notNullValue())); - } - } - } - } - - @Test - public void testQueryWithOptions() { - try (ITConnection connection = createConnection()) { - try (ResultSet rs = - connection.executeQuery( - Statement.of( - "SELECT (SELECT CAST(COUNT(*) AS FLOAT64) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"), - Options.prefetchChunks(100000))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getDouble(0), is(notNullValue())); - assertThat(rs.next(), is(false)); - } - } - } - - @Test - public void testMultipleOpenResultSets() throws InterruptedException { - try (ITConnection connection = createConnection()) { - final ResultSet rs1 = connection.executeQuery(Statement.of("SELECT * FROM PRIME_NUMBERS")); - final ResultSet rs2 = connection.executeQuery(Statement.of("SELECT * FROM NUMBERS")); - ExecutorService exec = Executors.newFixedThreadPool(2); - exec.submit( - new Runnable() { - @Override - public void run() { - while (rs1.next()) {} - } - }); - exec.submit( - new Runnable() { - @Override - public void run() { - while (rs2.next()) {} - } - }); - exec.shutdown(); - exec.awaitTermination(1000L, TimeUnit.SECONDS); - rs1.close(); - rs2.close(); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadWriteAutocommitSpannerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadWriteAutocommitSpannerTest.java deleted file mode 100644 index 14834cc6..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITReadWriteAutocommitSpannerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerBatchUpdateException; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import org.junit.FixMethodOrder; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.junit.runners.MethodSorters; - -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ITReadWriteAutocommitSpannerTest extends ITAbstractSpannerTest { - - @Rule public ExpectedException exception = ExpectedException.none(); - - @Override - protected void appendConnectionUri(StringBuilder uri) { - uri.append(";autocommit=true"); - } - - @Override - public boolean doCreateDefaultTestTable() { - return true; - } - - @Test - public void test01_SqlScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider()); - verifier.verifyStatementsInFile( - "ITReadWriteAutocommitSpannerTest.sql", SqlScriptVerifier.class); - } - - @Test - public void test02_WriteMutation() throws Exception { - try (ITConnection connection = createConnection()) { - connection.write( - Mutation.newInsertBuilder("TEST").set("ID").to(9999L).set("NAME").to("FOO").build()); - assertThat(connection.getCommitTimestamp(), is(notNullValue())); - } - } - - @Test - public void test03_MultipleStatements_WithTimeouts() throws InterruptedException { - try (ITConnection connection = createConnection()) { - // do an insert that should succeed - assertThat( - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1000, 'test')")), - is(equalTo(1L))); - // check that the insert succeeded - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM TEST WHERE ID=1000"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("test"))); - assertThat(rs.next(), is(false)); - } - - // do an update that should time out - connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - try { - connection.executeUpdate(Statement.of("UPDATE TEST SET NAME='test18' WHERE ID=1000")); - } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.DEADLINE_EXCEEDED) { - throw e; - } - } - // remove the timeout setting - connection.clearStatementTimeout(); - - // do a delete that should succeed - connection.executeUpdate(Statement.of("DELETE FROM TEST WHERE ID=1000")); - // verify that the delete did succeed - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM TEST WHERE ID=1000"))) { - assertThat(rs.next(), is(false)); - } - } - } - - @Test - public void test04_BatchUpdate() { - try (ITConnection connection = createConnection()) { - long[] updateCounts = - connection.executeBatchUpdate( - Arrays.asList( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (10, 'Batch value 1')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (11, 'Batch value 2')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (12, 'Batch value 3')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (13, 'Batch value 4')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (14, 'Batch value 5')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (15, 'Batch value 6')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (16, 'Batch value 7')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (17, 'Batch value 8')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (18, 'Batch value 9')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (19, 'Batch value 10')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (20, 'Batch value 11')"))); - assertThat( - updateCounts, is(equalTo(new long[] {1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L}))); - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT COUNT(*) FROM TEST WHERE ID>=10 AND ID<=20"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong(0), is(equalTo(11L))); - } - } - } - - @Test - public void test05_BatchUpdateWithException() { - try (ITConnection con1 = createConnection(); - ITConnection con2 = createConnection()) { - try { - con1.executeBatchUpdate( - Arrays.asList( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (21, 'Batch value 1')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (22, 'Batch value 2')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (23, 'Batch value 3')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (24, 'Batch value 4')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (25, 'Batch value 5')"), - Statement.of("INSERT INTO TEST_NOT_FOUND (ID, NAME) VALUES (26, 'Batch value 6')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (27, 'Batch value 7')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (28, 'Batch value 8')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (29, 'Batch value 9')"), - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (30, 'Batch value 10')"))); - fail("Missing batch update exception"); - } catch (SpannerBatchUpdateException e) { - assertThat(e.getUpdateCounts(), is(equalTo(new long[] {1L, 1L, 1L, 1L, 1L}))); - } - // Verify that the values cannot be read on the connection that did the insert. - try (ResultSet rs = - con1.executeQuery(Statement.of("SELECT COUNT(*) FROM TEST WHERE ID>=21 AND ID<=30"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong(0), is(equalTo(0L))); - } - // Verify that the values can also not be read on another connection. - try (ResultSet rs = - con2.executeQuery(Statement.of("SELECT COUNT(*) FROM TEST WHERE ID>=21 AND ID<=30"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong(0), is(equalTo(0L))); - } - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlMusicScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlMusicScriptTest.java deleted file mode 100644 index d981a368..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlMusicScriptTest.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.AbstractSqlScriptVerifier.GenericConnection; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import java.util.ArrayList; -import java.util.List; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.junit.runners.MethodSorters; - -/** - * Integration test that runs one long sql script using the default Singers/Albums/Songs/Concerts - * data model - */ -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ITSqlMusicScriptTest extends ITAbstractSpannerTest { - private static final String SCRIPT_FILE = "ITSqlMusicScriptTest.sql"; - - @Test - public void test01_RunScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(); - try (GenericConnection connection = SpannerGenericConnection.of(createConnection())) { - verifier.verifyStatementsInFile(connection, SCRIPT_FILE, SqlScriptVerifier.class); - } - } - - @Test - public void test02_RunAbortedTest() { - final long SINGER_ID = 2L; - final long VENUE_ID = 68L; - final long NUMBER_OF_SINGERS = 30L; - final long NUMBER_OF_ALBUMS = 60L; - final long NUMBER_OF_SONGS = 149L; - final long NUMBER_OF_CONCERTS = 100L; - long numberOfSongs = 0L; - AbortInterceptor interceptor = new AbortInterceptor(0.0D); - try (ITConnection connection = createConnection(interceptor)) { - connection.setAutocommit(false); - connection.setRetryAbortsInternally(true); - // Read all data from the different music tables in the transaction - // The previous test deleted the first two Singers records. - long expectedId = 3L; - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM Singers ORDER BY SingerId"))) { - while (rs.next()) { - assertThat(rs.getLong("SingerId"), is(equalTo(expectedId))); - expectedId++; - } - } - assertThat(expectedId, is(equalTo(NUMBER_OF_SINGERS + 1L))); - expectedId = 3L; - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM Albums ORDER BY AlbumId"))) { - while (rs.next()) { - assertThat(rs.getLong("AlbumId"), is(equalTo(expectedId))); - expectedId++; - // 31 and 32 were deleted by the first test script. - if (expectedId == 31L || expectedId == 32L) { - expectedId = 33L; - } - } - } - assertThat(expectedId, is(equalTo(NUMBER_OF_ALBUMS + 1L))); - expectedId = 1L; - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM Songs ORDER BY TrackId"))) { - while (rs.next()) { - assertThat(rs.getLong("TrackId"), is(equalTo(expectedId))); - expectedId++; - numberOfSongs++; - // 40, 64, 76, 86 and 96 were deleted by the first test script. - if (expectedId == 40L - || expectedId == 64L - || expectedId == 76L - || expectedId == 86L - || expectedId == 96L) { - expectedId++; - } - } - } - assertThat(expectedId, is(equalTo(NUMBER_OF_SONGS + 1L))); - // Concerts are not in the table hierarchy, so no records have been deleted. - expectedId = 1L; - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT * FROM Concerts ORDER BY VenueId"))) { - while (rs.next()) { - assertThat(rs.getLong("VenueId"), is(equalTo(expectedId))); - expectedId++; - } - } - assertThat(expectedId, is(equalTo(NUMBER_OF_CONCERTS + 1L))); - - // make one small concurrent change in a different transaction - List originalPrices; - List newPrices; - try (ITConnection connection2 = createConnection()) { - assertThat(connection2.isAutocommit(), is(true)); - try (ResultSet rs = - connection2.executeQuery( - Statement.newBuilder( - "SELECT TicketPrices FROM Concerts WHERE SingerId=@singer AND VenueId=@venue") - .bind("singer") - .to(SINGER_ID) - .bind("venue") - .to(VENUE_ID) - .build())) { - assertThat(rs.next(), is(true)); - originalPrices = rs.getLongList(0); - // increase one of the prices by 1 - newPrices = new ArrayList<>(originalPrices); - newPrices.set(1, originalPrices.get(1) + 1); - connection2.executeUpdate( - Statement.newBuilder( - "UPDATE Concerts SET TicketPrices=@prices WHERE SingerId=@singer AND VenueId=@venue") - .bind("prices") - .toInt64Array(newPrices) - .bind("singer") - .to(SINGER_ID) - .bind("venue") - .to(VENUE_ID) - .build()); - } - } - - // try to add a new song and then try to commit, but trigger an abort on commit - connection.bufferedWrite( - Mutation.newInsertBuilder("Songs") - .set("SingerId") - .to(3L) - .set("AlbumId") - .to(3L) - .set("TrackId") - .to(1L) - .set("SongName") - .to("Aborted") - .set("Duration") - .to(1L) - .set("SongGenre") - .to("Unknown") - .build()); - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // the transaction retry should fail because of the concurrent modification - boolean expectedException = false; - try { - connection.commit(); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - // verify that the commit aborted, an internal retry was started and then aborted because of - // the concurrent modification - assertThat(expectedException, is(true)); - // verify that the prices were changed - try (ResultSet rs = - connection.executeQuery( - Statement.newBuilder( - "SELECT TicketPrices FROM Concerts WHERE SingerId=@singer AND VenueId=@venue") - .bind("singer") - .to(SINGER_ID) - .bind("venue") - .to(VENUE_ID) - .build())) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLongList(0), is(equalTo(newPrices))); - } - // verify that the new song was not written to the database - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) FROM Songs"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong(0), is(equalTo(numberOfSongs))); - } - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlScriptTest.java deleted file mode 100644 index 9f60ae6c..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITSqlScriptTest.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier.SpannerGenericConnection; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.junit.runners.MethodSorters; - -/** - * Integration test that creates and fills a test database entirely using only sql scripts, and then - * performs all possible operations on this test database using only sql scripts. This test uses the - * generic connection API. - */ -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ITSqlScriptTest extends ITAbstractSpannerTest { - private static final String CREATE_TABLES_FILE = "ITSqlScriptTest_CreateTables.sql"; - private static final String INSERT_AND_VERIFY_TEST_DATA = "ITSqlScriptTest_InsertTestData.sql"; - private static final String TEST_GET_READ_TIMESTAMP = "ITSqlScriptTest_TestGetReadTimestamp.sql"; - private static final String TEST_GET_COMMIT_TIMESTAMP = - "ITSqlScriptTest_TestGetCommitTimestamp.sql"; - private static final String TEST_TEMPORARY_TRANSACTIONS = - "ITSqlScriptTest_TestTemporaryTransactions.sql"; - private static final String TEST_TRANSACTION_MODE = "ITSqlScriptTest_TestTransactionMode.sql"; - private static final String TEST_TRANSACTION_MODE_READ_ONLY = - "ITSqlScriptTest_TestTransactionMode_ReadOnly.sql"; - private static final String TEST_READ_ONLY_STALENESS = - "ITSqlScriptTest_TestReadOnlyStaleness.sql"; - private static final String TEST_AUTOCOMMIT_DML_MODE = - "ITSqlScriptTest_TestAutocommitDmlMode.sql"; - private static final String TEST_AUTOCOMMIT_READ_ONLY = - "ITSqlScriptTest_TestAutocommitReadOnly.sql"; - private static final String TEST_STATEMENT_TIMEOUT = "ITSqlScriptTest_TestStatementTimeout.sql"; - private static final String TEST_SET_STATEMENTS = "ITSqlScriptTest_TestSetStatements.sql"; - private static final String TEST_INVALID_STATEMENTS = "ITSqlScriptTest_TestInvalidStatements.sql"; - - private final SqlScriptVerifier verifier = new SqlScriptVerifier(); - - @Test - public void test01_CreateTables() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), CREATE_TABLES_FILE, SqlScriptVerifier.class); - } - } - - @Test - public void test02_InsertTestData() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - INSERT_AND_VERIFY_TEST_DATA, - SqlScriptVerifier.class); - } - } - - @Test - public void test03_TestGetReadTimestamp() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_GET_READ_TIMESTAMP, - SqlScriptVerifier.class); - } - } - - @Test - public void test04_TestGetCommitTimestamp() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_GET_COMMIT_TIMESTAMP, - SqlScriptVerifier.class); - } - } - - @Test - public void test05_TestTemporaryTransactions() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_TEMPORARY_TRANSACTIONS, - SqlScriptVerifier.class); - } - } - - @Test - public void test06_TestTransactionMode() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), TEST_TRANSACTION_MODE, SqlScriptVerifier.class); - } - } - - @Test - public void test07_TestTransactionModeReadOnly() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_TRANSACTION_MODE_READ_ONLY, - SqlScriptVerifier.class); - } - } - - @Test - public void test08_TestReadOnlyStaleness() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_READ_ONLY_STALENESS, - SqlScriptVerifier.class); - } - } - - @Test - public void test09_TestAutocommitDmlMode() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_AUTOCOMMIT_DML_MODE, - SqlScriptVerifier.class); - } - } - - @Test - public void test10_TestAutocommitReadOnly() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_AUTOCOMMIT_READ_ONLY, - SqlScriptVerifier.class); - } - } - - @Test - public void test11_TestStatementTimeout() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), TEST_STATEMENT_TIMEOUT, SqlScriptVerifier.class); - } - } - - @Test - public void test12_TestSetStatements() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), TEST_SET_STATEMENTS, SqlScriptVerifier.class); - } - } - - @Test - public void test13_TestInvalidStatements() throws Exception { - try (ITConnection connection = createConnection()) { - verifier.verifyStatementsInFile( - SpannerGenericConnection.of(connection), - TEST_INVALID_STATEMENTS, - SqlScriptVerifier.class); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionModeTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionModeTest.java deleted file mode 100644 index 219d2153..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionModeTest.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.spanner.ErrorCode; -import com.google.cloud.spanner.IntegrationTest; -import com.google.cloud.spanner.Key; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.SpannerExceptionMatcher; -import com.google.cloud.spanner.jdbc.SqlScriptVerifier; -import java.util.Arrays; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@Category(IntegrationTest.class) -@RunWith(JUnit4.class) -public class ITTransactionModeTest extends ITAbstractSpannerTest { - @Rule public ExpectedException exception = ExpectedException.none(); - - @Override - public void appendConnectionUri(StringBuilder uri) { - uri.append("?autocommit=false"); - } - - @Override - public boolean doCreateDefaultTestTable() { - return true; - } - - @Test - public void testSqlScript() throws Exception { - SqlScriptVerifier verifier = new SqlScriptVerifier(new ITConnectionProvider()); - verifier.verifyStatementsInFile("ITTransactionModeTest.sql", SqlScriptVerifier.class); - } - - @Test - public void testDoAllowBufferedWriteInReadWriteTransaction() { - try (ITConnection connection = createConnection()) { - assertThat(connection.isAutocommit(), is(false)); - connection.bufferedWrite( - Mutation.newInsertBuilder("TEST").set("ID").to(1L).set("NAME").to("TEST").build()); - connection.commit(); - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT NAME FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST"))); - assertThat(rs.next(), is(false)); - } - connection.bufferedWrite( - Mutation.newUpdateBuilder("TEST").set("ID").to(1L).set("NAME").to("TEST2").build()); - connection.commit(); - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT NAME FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST2"))); - assertThat(rs.next(), is(false)); - } - connection.bufferedWrite(Mutation.delete("TEST", Key.of(1L))); - connection.commit(); - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT NAME FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(false)); - } - } - } - - @Test - public void testDoAllowBufferedWriteIterableInReadWriteTransaction() { - try (ITConnection connection = createConnection()) { - assertThat(connection.isAutocommit(), is(false)); - connection.bufferedWrite( - Arrays.asList( - Mutation.newInsertBuilder("TEST").set("ID").to(1L).set("NAME").to("TEST-1").build(), - Mutation.newInsertBuilder("TEST").set("ID").to(2L).set("NAME").to("TEST-2").build())); - connection.commit(); - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT NAME FROM TEST WHERE ID IN (1,2) ORDER BY ID"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST-1"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST-2"))); - assertThat(rs.next(), is(false)); - } - connection.bufferedWrite( - Arrays.asList( - Mutation.newUpdateBuilder("TEST").set("ID").to(1L).set("NAME").to("TEST-1-2").build(), - Mutation.newUpdateBuilder("TEST") - .set("ID") - .to(2L) - .set("NAME") - .to("TEST-2-2") - .build())); - connection.commit(); - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT NAME FROM TEST WHERE ID IN (1,2) ORDER BY ID"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST-1-2"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("NAME"), is(equalTo("TEST-2-2"))); - assertThat(rs.next(), is(false)); - } - connection.bufferedWrite( - Arrays.asList(Mutation.delete("TEST", Key.of(1L)), Mutation.delete("TEST", Key.of(2L)))); - connection.commit(); - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT NAME FROM TEST WHERE ID IN (1,2) ORDER BY ID"))) { - assertThat(rs.next(), is(false)); - } - } - } - - @Test - public void testDoNotAllowBufferedWriteInReadOnlyTransaction() { - try (ITConnection connection = createConnection()) { - connection.execute(Statement.of("SET TRANSACTION READ ONLY")); - assertThat(connection.isAutocommit(), is(false)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); - } - } - - @Test - public void testDoNotAllowBufferedWriteIterableInReadOnlyTransaction() { - try (ITConnection connection = createConnection()) { - connection.execute(Statement.of("SET TRANSACTION READ ONLY")); - assertThat(connection.isAutocommit(), is(false)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite( - Arrays.asList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), - Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); - } - } - - @Test - public void testDoNotAllowBufferedWriteInDdlBatch() { - try (ITConnection connection = createConnection()) { - connection.startBatchDdl(); - assertThat(connection.isAutocommit(), is(false)); - assertThat(connection.isDdlBatchActive(), is(true)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); - } - } - - @Test - public void testDoNotAllowBufferedWriteIterableInDdlBatch() { - try (ITConnection connection = createConnection()) { - connection.startBatchDdl(); - assertThat(connection.isAutocommit(), is(false)); - assertThat(connection.isDdlBatchActive(), is(true)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite( - Arrays.asList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), - Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); - } - } -} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionRetryTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionRetryTest.java deleted file mode 100644 index e3fc90c4..00000000 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITTransactionRetryTest.java +++ /dev/null @@ -1,1567 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.spanner.jdbc.it; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; -import com.google.cloud.spanner.AbortedException; -import com.google.cloud.spanner.KeySet; -import com.google.cloud.spanner.Mutation; -import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.Statement; -import com.google.cloud.spanner.jdbc.ITAbstractSpannerTest; -import com.google.cloud.spanner.jdbc.TransactionRetryListener; -import java.sql.Connection; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * This integration test tests the different scenarios for automatically retrying read/write - * transactions, both when possible and when the transaction must abort because of a concurrent - * update. - */ -@RunWith(JUnit4.class) -public class ITTransactionRetryTest extends ITAbstractSpannerTest { - private static final Logger logger = Logger.getLogger(ITTransactionRetryTest.class.getName()); - - @Rule public TestName testName = new TestName(); - - @Override - protected void appendConnectionUri(StringBuilder uri) { - uri.append(";autocommit=false;retryAbortsInternally=true"); - } - - @Override - public boolean doCreateDefaultTestTable() { - return true; - } - - /** Clear the test table before each test run */ - @Before - public void clearTable() { - try (ITConnection connection = createConnection()) { - connection.bufferedWrite(Mutation.delete("TEST", KeySet.all())); - connection.commit(); - } - } - - @Before - public void clearStatistics() { - RETRY_STATISTICS.clear(); - } - - @Before - public void logStart() { - logger.fine( - "--------------------------------------------------------------\n" - + testName.getMethodName() - + " started"); - } - - @After - public void logFinished() { - logger.fine( - "--------------------------------------------------------------\n" - + testName.getMethodName() - + " finished"); - } - - /** Simple data structure to keep track of retry statistics */ - private static class RetryStatistics { - private int totalRetryAttemptsStarted; - private int totalRetryAttemptsFinished; - private int totalSuccessfulRetries; - private int totalErroredRetries; - private int totalNestedAborts; - private int totalMaxAttemptsExceeded; - private int totalConcurrentModifications; - - private void clear() { - totalRetryAttemptsStarted = 0; - totalRetryAttemptsFinished = 0; - totalSuccessfulRetries = 0; - totalErroredRetries = 0; - totalNestedAborts = 0; - totalMaxAttemptsExceeded = 0; - totalConcurrentModifications = 0; - } - } - - /** - * Static to allow access from the {@link CountTransactionRetryListener}. Statistics are - * automatically cleared before each test case. - */ - public static final RetryStatistics RETRY_STATISTICS = new RetryStatistics(); - - /** - * Simple {@link TransactionRetryListener} that keeps track of the total count of the different - * transaction retry events of a {@link Connection}. Note that as {@link - * TransactionRetryListener}s are instantiated once per connection, the listener keeps track of - * the total statistics of a connection and not only of the last transaction. - */ - public static class CountTransactionRetryListener implements TransactionRetryListener { - - @Override - public void retryStarting(Timestamp transactionStarted, long transactionId, int retryAttempt) { - RETRY_STATISTICS.totalRetryAttemptsStarted++; - } - - @Override - public void retryFinished( - Timestamp transactionStarted, long transactionId, int retryAttempt, RetryResult result) { - RETRY_STATISTICS.totalRetryAttemptsFinished++; - switch (result) { - case RETRY_ABORTED_AND_MAX_ATTEMPTS_EXCEEDED: - RETRY_STATISTICS.totalMaxAttemptsExceeded++; - break; - case RETRY_ABORTED_AND_RESTARTING: - RETRY_STATISTICS.totalNestedAborts++; - break; - case RETRY_ABORTED_DUE_TO_CONCURRENT_MODIFICATION: - RETRY_STATISTICS.totalConcurrentModifications++; - break; - case RETRY_ERROR: - RETRY_STATISTICS.totalErroredRetries++; - break; - case RETRY_SUCCESSFUL: - RETRY_STATISTICS.totalSuccessfulRetries++; - break; - default: - break; - } - } - } - - /** Test successful retry when the commit aborts */ - @Test - public void testCommitAborted() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // do an insert - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // do a commit that will first abort, and then on retry will succeed - connection.commit(); - assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted >= 1, is(true)); - assertThat(RETRY_STATISTICS.totalRetryAttemptsFinished >= 1, is(true)); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - assertThat(RETRY_STATISTICS.totalErroredRetries, is(equalTo(0))); - assertThat(RETRY_STATISTICS.totalConcurrentModifications, is(equalTo(0))); - assertThat(RETRY_STATISTICS.totalMaxAttemptsExceeded, is(equalTo(0))); - // verify that the insert succeeded - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry when an insert statement aborts */ - @Test - public void testInsertAborted() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // do an insert that will abort - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - // do a commit - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that the insert succeeded - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry when an update statement aborts */ - @Test - public void testUpdateAborted() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // insert a test record - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // do an update that will abort - connection.executeUpdate(Statement.of("UPDATE TEST SET NAME='update aborted' WHERE ID=1")); - // do a commit - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that the update succeeded - try (ResultSet rs = - connection.executeQuery( - Statement.of( - "SELECT COUNT(*) AS C FROM TEST WHERE ID=1 AND NAME='update aborted'"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry when a query aborts */ - @Test - public void testQueryAborted() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // insert a test record - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - // do a query that will abort - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - // do a commit - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that the update succeeded - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry when a call to {@link ResultSet#next()} aborts */ - @Test - public void testNextCallAborted() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // do a query - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - // the first record should be accessible without any problems - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(1L))); - - // indicate that the next statement should abort - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(2L))); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // there should be only two records - assertThat(rs.next(), is(false)); - } - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that the transaction succeeded - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(2L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry after multiple aborts */ - @Test - public void testMultipleAborts() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // do three inserts which all will abort and retry - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 3, is(true)); - assertThat( - RETRY_STATISTICS.totalNestedAborts, - is(equalTo(RETRY_STATISTICS.totalSuccessfulRetries - 3))); - // verify that the insert succeeded - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(3L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** - * Tests that a transaction retry can be successful after a select, as long as the select returns - * the same results during the retry - */ - @Test - public void testAbortAfterSelect() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // verify that the there is no test record - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(0L))); - assertThat(rs.next(), is(false)); - } - // insert a test record - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - // select the test record - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(1L))); - assertThat(rs.getString("NAME"), is(equalTo("test 1"))); - assertThat(rs.next(), is(false)); - } - // do another insert that will abort and retry - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the first test record again - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(1L))); - assertThat(rs.getString("NAME"), is(equalTo("test 1"))); - assertThat(rs.next(), is(false)); - } - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - } - } - - /** - * Test a successful retry when a {@link ResultSet} has been consumed half way. The {@link - * ResultSet} should still be at the same position and still behave as if the original transaction - * did not abort. - */ - @Test - public void testAbortWithResultSetHalfway() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - // iterate one step - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(1L))); - // do another insert that will abort and retry - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - // iterate another step - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(2L))); - // ensure we are at the end of the result set - assertThat(rs.next(), is(false)); - } - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that all the inserts succeeded - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(3L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** Test successful retry after a {@link ResultSet} has been fully consumed. */ - @Test - public void testAbortWithResultSetFullyConsumed() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records and iterate over them - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing, just consume the result set - } - } - // do another insert that will abort and retry - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - // verify that all the inserts succeeded - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(3L))); - assertThat(rs.next(), is(false)); - } - } - } - - @Test - public void testAbortWithConcurrentInsert() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records and consume the entire result set - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing - } - } - // open a new connection and transaction and do an additional insert - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertRetryStatistics(1, 1, 0); - } - } - - @Test - public void testAbortWithConcurrentDelete() { - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // open a new connection and select the two test records - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // select the test records and consume the entire result set - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing - } - } - // open a new connection and transaction and remove one of the test records - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("DELETE FROM TEST WHERE ID=1")); - connection2.commit(); - } - // now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertRetryStatistics(1, 1, 0); - } - } - - @Test - public void testAbortWithConcurrentUpdate() { - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // open a new connection and select the two test records - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // select the test records and consume the entire result set - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing - } - } - // open a new connection and transaction and update one of the test records - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("UPDATE TEST SET NAME='test updated' WHERE ID=2")); - connection2.commit(); - } - // now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertRetryStatistics(1, 1, 0); - } - } - - /** - * Test that shows that a transaction retry is possible even when there is a concurrent insert - * that has an impact on a query that has been executed, as long as the user hasn't actually seen - * the relevant part of the result of the query - */ - @Test - public void testAbortWithUnseenConcurrentInsert() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records and consume part of the result set - ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID")); - assertThat(rs.next(), is(true)); - assertThat(rs.next(), is(true)); - // Open a new connection and transaction and do an additional insert. This insert will be - // included in a retry of the above query, but this has not yet been 'seen' by the user, - // hence is not a problem for retrying the transaction. - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // now try to do an insert that will abort. The retry should still succeed. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - int currentRetryCount = RETRY_STATISTICS.totalRetryAttemptsStarted; - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); - assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted >= currentRetryCount + 1, is(true)); - // Consume the rest of the result set. The insert by the other transaction should now be - // included in the result set as the transaction retried. Although this means that the result - // is different after a retry, it is not different as seen by the user, as the user didn't - // know that the result set did not have any more results before the transaction retry. - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(3L))); - // record with id 4 should not be visible, as it was added to the transaction after the query - // was executed - assertThat(rs.next(), is(false)); - rs.close(); - connection.commit(); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 1, is(true)); - } - } - - /** - * This test shows what happens when an abort occurs on a call to {@link ResultSet#next()} on a - * {@link ResultSet} that has an concurrent insert. As long as the user hasn't consumed the {@link - * ResultSet} so far that the concurrent insert has been seen, the retry will succeed. When the - * user has consumed the {@link ResultSet} to the point where the concurrent insert is visible, - * the retry will fail. - */ - @Test - public void testAbortWithUnseenConcurrentInsertAbortOnNext() { - // no calls to next(), this should succeed - assertThat(testAbortWithUnseenConcurrentInsertAbortOnNext(0) >= 1, is(true)); - // 1 call to next() should also succeed, as there were 2 records in the original result set - assertThat(testAbortWithUnseenConcurrentInsertAbortOnNext(1) >= 1, is(true)); - // 2 calls to next() should also succeed, as there were 2 records in the original result set and - // the user doesn't know yet that the next call to next() will return true instead of false - // after the concurrent insert - assertThat(testAbortWithUnseenConcurrentInsertAbortOnNext(2) >= 1, is(true)); - - boolean expectedException = false; - try { - // 3 calls to next() should fail, as the user would now see the inserted record - testAbortWithUnseenConcurrentInsertAbortOnNext(3); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - } - - private int testAbortWithUnseenConcurrentInsertAbortOnNext(int callsToNext) - throws AbortedDueToConcurrentModificationException { - int retries = 0; - clearTable(); - clearStatistics(); - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - int totalRecordsSeen = 0; - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records and consume part or all of the result set - ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID")); - for (int counter = 0; counter < callsToNext; counter++) { - if (rs.next()) { - totalRecordsSeen++; - } - } - // Open a new connection and transaction and do an additional insert. This insert will be - // included in a retry of the above query. Any transaction retry will fail/succeed depending - // on whether the user has consumed enough of the result set to potentially have seen this - // insert. - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // Now consume the rest of the result set, but trigger a transaction retry by aborting the - // first next() call. Without a retry, the result set should only contain 2 records. With a - // successful retry, the result set contains 3 results. The retry will only succeed as long - // as the user has not consumed enough of the result set to know whether there should have - // been a record with ID 3 or not. - - // First verify that the transaction has not yet retried. - int currentRetryCount = RETRY_STATISTICS.totalRetryAttemptsStarted; - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - - // Try to consume the rest of the result set. - // This will fail with an AbortedDueToConcurrentModificationException if the retry fails. - while (rs.next()) { - totalRecordsSeen++; - if (totalRecordsSeen == 3) { - assertThat(rs.getLong("ID"), is(equalTo(3L))); - } - } - // Verify that the transaction retried. - assertThat(RETRY_STATISTICS.totalSuccessfulRetries > currentRetryCount, is(true)); - rs.close(); - connection.commit(); - retries = RETRY_STATISTICS.totalSuccessfulRetries; - } - return retries; - } - - /** - * Test that shows that a transaction that has aborted is considered to be rolled back, and new - * statements will be executed in a new transaction - */ - @Test - public void testAbortWithConcurrentInsertAndContinue() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // Select the test records and consume the entire result set. - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing - } - } - // Open a new connection and transaction and do an additional insert - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // Now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertRetryStatistics(1, 1, 0); - // the next statement should be in a new transaction as the previous transaction rolled back - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - // there should be one record from the transaction on connection2 - assertThat(rs.next(), is(true)); - assertThat(rs.next(), is(false)); - } - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. The transaction aborts at commit - *
  2. A retry starts and succeeds - *
  3. The commit is applied again and aborts again - *
  4. The retry is started again and then succeeds - *
- */ - @Test - public void testAbortTwiceOnCommit() { - AbortInterceptor interceptor = - new AbortInterceptor(0) { - private int commitCount = 0; - - @Override - protected boolean shouldAbort(String statement, ExecutionStep step) { - if ("COMMIT".equalsIgnoreCase(statement)) { - commitCount++; - return commitCount <= 2; - } - return false; - } - }; - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - connection.commit(); - // Assert that the transaction was retried twice. - assertRetryStatistics(2, 0, 2); - // Verify that the insert succeeded. - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. The transaction aborts at commit - *
  2. A retry starts and then aborts at the insert statement - *
  3. The retry is restarted and then succeeds - *
- */ - @Test - public void testNestedAbortOnInsert() { - AbortInterceptor interceptor = - new AbortInterceptor(0) { - private int commitCount = 0; - private int insertCount = 0; - - @Override - protected boolean shouldAbort(String statement, ExecutionStep step) { - if ("COMMIT".equalsIgnoreCase(statement)) { - commitCount++; - return commitCount == 1; - } else if (statement.startsWith("INSERT INTO TEST")) { - insertCount++; - return insertCount == 2; - } - return false; - } - }; - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - connection.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test aborted')")); - connection.commit(); - // Assert that the transaction was retried (a restarted retry is counted as one successful - // retry). - assertRetryStatistics(2, 0, 1); - assertThat(RETRY_STATISTICS.totalNestedAborts > 0, is(true)); - // Verify that the insert succeeded. - try (ResultSet rs = - connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE ID=1"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(1L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. The transaction aborts at commit - *
  2. A retry starts and then aborts at a next call in a result set - *
  3. The retry is restarted and then succeeds - *
- */ - @Test - public void testNestedAbortOnNextCall() { - AbortInterceptor interceptor = - new AbortInterceptor(0) { - private int nextCallsDuringRetry = 0; - private int commitCount = 0; - - @Override - protected boolean shouldAbort(String statement, ExecutionStep step) { - if ("COMMIT".equalsIgnoreCase(statement)) { - // Note that commit always has ExecutionStep == EXECUTE_STATEMENT, as a commit can - // never - // really be retried (it is always the last statement in a transaction, and if it - // fails - // because of an aborted exception, the entire transaction is retried, and the commit - // statement is then applied again). - commitCount++; - return commitCount == 1; - } else if (statement.equals("SELECT * FROM TEST ORDER BY ID") - && step == ExecutionStep.RETRY_NEXT_ON_RESULT_SET) { - nextCallsDuringRetry++; - return nextCallsDuringRetry == 1; - } - return false; - } - }; - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // Insert two test records. - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // Select the test records. - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - // Iterate one step. This step should abort during the retry the first time. - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(1L))); - // Do another insert that will not be visible to the result set. - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - // iterate another step - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("ID"), is(equalTo(2L))); - // Ensure we are at the end of the result set. - assertThat(rs.next(), is(false)); - } - connection.commit(); - // Verify that the transaction retried. - assertRetryStatistics(2, 0, 1); - assertThat(RETRY_STATISTICS.totalNestedAborts > 0, is(true)); - // Verify that all the inserts succeeded. - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(3L))); - assertThat(rs.next(), is(false)); - } - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. Transaction 1 does two inserts in table TEST - *
  2. Transaction 1 selects all records from table TEST - *
  3. Transaction 2 inserts a record into TEST - *
  4. Transaction 1 does another insert into TEST that aborts - *
  5. Transaction 1 starts a retry that aborts at the SELECT statement (i.e. before the - * concurrent modification has been seen) - *
  6. Transaction 1 restarts the retry that now aborts due to a concurrent modification - * exception - *
- */ - @Test - public void testNestedAbortWithConcurrentInsert() { - AbortInterceptor interceptor = - new AbortInterceptor(0) { - private boolean alreadyAborted = false; - - @Override - protected boolean shouldAbort(String statement, ExecutionStep step) { - // Abort during retry on the select statement. - if (!alreadyAborted - && statement.equals("SELECT * FROM TEST ORDER BY ID") - && step == ExecutionStep.RETRY_STATEMENT) { - alreadyAborted = true; - return true; - } - return super.shouldAbort(statement, step); - } - }; - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert two test records - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - // select the test records and consume the entire result set - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing - } - } - // open a new connection and transaction and do an additional insert - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // Now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertThat(expectedException, is(true)); - assertRetryStatistics(2, 1, 0); - assertThat(RETRY_STATISTICS.totalNestedAborts > 0, is(true)); - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit - *
  2. Transaction 1 updates the names of all records in the TEST table - *
  3. Transaction 2 inserts a record in the TEST table and commits - *
  4. Transaction 1 does another insert into TEST that aborts - *
  5. Transaction 1 starts a retry that aborts due to a concurrent modification exception as - * the number of updated records will be different - *
- */ - @Test - public void testAbortWithDifferentUpdateCount() { - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // open a new connection and update one of the records - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - connection.executeUpdate( - Statement.of("UPDATE TEST SET NAME='test update that will fail' WHERE TRUE")); - // open a new connection and transaction and update the same test record - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - connection2.commit(); - } - // Now try to do an insert that will abort. The retry should now fail as there has been a - // concurrent modification. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (4, 'test 4')")); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - assertRetryStatistics(1, 1, 0); - assertThat(expectedException, is(true)); - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit - *
  2. Try to query a non-existing table. This will lead to an exception. - *
  3. Query all the records from the TEST table and consume the result set - *
  4. Insert another record into TEST that aborts - *
  5. The transaction successfully retries - *
- */ - @Test - public void testAbortWithExceptionOnSelect() { - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // do a select that will fail - boolean expectedException = false; - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM FOO"))) { - while (rs.next()) { - // do nothing - } - } catch (SpannerException e) { - // expected - expectedException = true; - } - assertThat(expectedException, is(true)); - // do a select that will succeed - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // now try to do an insert that will abort. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - assertRetryStatistics(1, 0, 1); - } - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit. - *
  2. Try to query the non-existing table FOO. This will lead to an exception. - *
  3. Query all the records from the TEST table and consume the result set. - *
  4. Open another connection and create the table FOO. - *
  5. Insert another record into TEST that aborts. - *
  6. The transaction is internally retried. The retry fails as the SELECT statement on FOO - * will now succeed. - *
- */ - @Test - public void testAbortWithExceptionOnSelectAndConcurrentModification() { - boolean abortedDueToConcurrentModification = false; - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // do a select that will fail - boolean expectedException = false; - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM FOO"))) { - while (rs.next()) { - // do nothing - } - } catch (SpannerException e) { - // expected - expectedException = true; - } - assertThat(expectedException, is(true)); - // do a select that will succeed - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // CREATE FOO - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute( - Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - } - // Now try to do an insert that will abort. The subsequent retry will fail as the SELECT * - // FROM FOO now returns a result. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - abortedDueToConcurrentModification = true; - } - } - // DROP FOO regardless of the result to avoid any interference with other test cases - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute(Statement.of("DROP TABLE FOO")); - } - assertThat(abortedDueToConcurrentModification, is(true)); - assertRetryStatistics(1, 1, 0); - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit. - *
  2. Try to insert a record in the non-existing table FOO. This will lead to an exception. - *
  3. Query all the records from the TEST table and consume the result set. - *
  4. Open another connection and create the table FOO. - *
  5. Insert another record into TEST that aborts. - *
  6. The transaction is internally retried. The retry fails as the insert statement on FOO - * will now succeed. - *
- */ - @Test - public void testAbortWithExceptionOnInsertAndConcurrentModification() { - boolean abortedDueToConcurrentModification = false; - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // do an insert that will fail - boolean expectedException = false; - try { - connection.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (1, 'test 1')")); - } catch (SpannerException e) { - // expected - expectedException = true; - } - assertThat(expectedException, is(true)); - // do a select that will succeed - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // CREATE FOO - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute( - Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - } - // Now try to do an insert that will abort. The subsequent retry will fail as the INSERT INTO - // FOO now succeeds. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - abortedDueToConcurrentModification = true; - } - } - // DROP FOO regardless of the result to avoid any interference with other test cases - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute(Statement.of("DROP TABLE FOO")); - } - assertThat(abortedDueToConcurrentModification, is(true)); - assertRetryStatistics(1, 1, 0); - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit. - *
  2. Create the table FOO and insert a test record. - *
  3. Query the table FOO. - *
  4. Query all the records from the TEST table and consume the result set. - *
  5. Open another connection and drop the table FOO. - *
  6. Insert another record into TEST that aborts. - *
  7. The transaction is internally retried. The retry fails as the SELECT statement on FOO - * will now fail. - *
- */ - @Test - public void testAbortWithDroppedTableConcurrentModification() { - boolean abortedDueToConcurrentModification = false; - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // CREATE FOO - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute( - Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - connection2.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (1, 'test 1')")); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM FOO"))) { - while (rs.next()) { - // do nothing - } - } - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // DROP FOO using a different connection - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute(Statement.of("DROP TABLE FOO")); - } - // Now try to do an insert that will abort. The subsequent retry will fail as the SELECT * - // FROM FOO now fails. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - abortedDueToConcurrentModification = true; - } - } - assertThat(abortedDueToConcurrentModification, is(true)); - assertRetryStatistics(1, 1, 0); - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit. - *
  2. Create the table FOO and insert a test record and commit. - *
  3. Insert another record into the table FOO. - *
  4. Query all the records from the TEST table and consume the result set. - *
  5. Open another connection and drop the table FOO. - *
  6. Insert another record into TEST that aborts. - *
  7. The transaction is internally retried. The retry fails as the INSERT statement on FOO - * will now fail. - *
- */ - @Test - public void testAbortWithInsertOnDroppedTableConcurrentModification() { - boolean abortedDueToConcurrentModification = false; - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // CREATE FOO - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute( - Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - connection2.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (1, 'test 1')")); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert a record into FOO - connection.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (2, 'test 2')")); - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // DROP FOO using a different connection - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute(Statement.of("DROP TABLE FOO")); - } - // Now try to do an insert that will abort. The subsequent retry will fail as the INSERT INTO - // FOO now fails. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - try { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (3, 'test 3')")); - } catch (AbortedDueToConcurrentModificationException e) { - abortedDueToConcurrentModification = true; - } - } - assertThat(abortedDueToConcurrentModification, is(true)); - assertRetryStatistics(1, 1, 0); - } - - /** - * Test that shows the following: - * - *
    - *
  1. Insert two records into table TEST and commit. - *
  2. Create the table FOO and insert two test records and commit. - *
  3. Query all the records from the TEST table and consume the result set. - *
  4. Query all the records from the FOO table and consume only part of the result set. - *
  5. Open another connection and drop the table FOO. - *
  6. Try to consume the rest of the FOO result set. This aborts. - *
  7. The transaction is internally retried. The retry fails as the SELECT statement on FOO - * will now fail. - *
- */ - @Test - public void testAbortWithCursorHalfwayDroppedTableConcurrentModification() { - boolean abortedDueToConcurrentModification = false; - AbortInterceptor interceptor = new AbortInterceptor(0); - // first insert two test records - try (ITConnection connection = createConnection()) { - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); - connection.commit(); - } - // CREATE FOO - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute( - Statement.of("CREATE TABLE FOO (ID INT64, NAME STRING(100)) PRIMARY KEY (ID)")); - connection2.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (1, 'test 1')")); - connection2.executeUpdate(Statement.of("INSERT INTO FOO (ID, NAME) VALUES (2, 'test 2')")); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { - while (rs.next()) { - // do nothing - } - } - // SELECT FROM FOO and consume part of the result set - ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM FOO")); - assertThat(rs.next(), is(true)); - // DROP FOO using a different connection - try (ITConnection connection2 = createConnection()) { - connection2.setAutocommit(true); - connection2.execute(Statement.of("DROP TABLE FOO")); - } - // try to continue to consume the result set, but this will now abort. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - try { - // This will fail as the retry will not succeed. - rs.next(); - } catch (AbortedDueToConcurrentModificationException e) { - abortedDueToConcurrentModification = true; - } finally { - rs.close(); - } - } - assertThat(abortedDueToConcurrentModification, is(true)); - assertRetryStatistics(1, 1, 0); - } - - /** Test the successful retry of a transaction with a large {@link ResultSet} */ - @Test - public void testRetryLargeResultSet() { - final int NUMBER_OF_TEST_RECORDS = 100000; - final long UPDATED_RECORDS = 1000L; - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = createConnection()) { - // insert test records - for (int i = 0; i < NUMBER_OF_TEST_RECORDS; i++) { - connection.bufferedWrite( - Mutation.newInsertBuilder("TEST").set("ID").to(i).set("NAME").to("test " + i).build()); - if (i % 1000 == 0) { - connection.commit(); - } - } - connection.commit(); - } - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // select the test records and iterate over them - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing, just consume the result set - } - } - // Do an update that will abort and retry. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - connection.executeUpdate( - Statement.newBuilder("UPDATE TEST SET NAME='updated' WHERE ID<@max_id") - .bind("max_id") - .to(UPDATED_RECORDS) - .build()); - connection.commit(); - // verify that the update succeeded - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE NAME='updated'"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(UPDATED_RECORDS))); - assertThat(rs.next(), is(false)); - } - // Verify that the transaction retried. - assertRetryStatistics(1, 0, 1); - } - } - - /** Test the successful retry of a transaction with a high chance of multiple aborts */ - @Test - public void testRetryHighAbortRate() { - final int NUMBER_OF_TEST_RECORDS = 10000; - final long UPDATED_RECORDS = 1000L; - // abort on 25% of all statements - AbortInterceptor interceptor = new AbortInterceptor(0.25D); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // insert test records - for (int i = 0; i < NUMBER_OF_TEST_RECORDS; i++) { - connection.bufferedWrite( - Mutation.newInsertBuilder("TEST").set("ID").to(i).set("NAME").to("test " + i).build()); - if (i % 1000 == 0) { - connection.commit(); - } - } - connection.commit(); - // select the test records and iterate over them - // reduce the abort rate to 0.01% as each next() call could abort - interceptor.setProbability(0.0001D); - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - while (rs.next()) { - // do nothing, just consume the result set - } - } - // increase the abort rate to 50% - interceptor.setProbability(0.50D); - connection.executeUpdate( - Statement.newBuilder("UPDATE TEST SET NAME='updated' WHERE ID<@max_id") - .bind("max_id") - .to(UPDATED_RECORDS) - .build()); - connection.commit(); - // verify that the update succeeded - try (ResultSet rs = - connection.executeQuery( - Statement.of("SELECT COUNT(*) AS C FROM TEST WHERE NAME='updated'"))) { - assertThat(rs.next(), is(true)); - assertThat(rs.getLong("C"), is(equalTo(UPDATED_RECORDS))); - assertThat(rs.next(), is(false)); - } - connection.commit(); - } catch (AbortedException e) { - // This could happen if the number of aborts exceeds the max number of retries. - logger.log(Level.FINE, "testRetryHighAbortRate aborted because of too many retries", e); - } - logger.fine("Total number of retries started: " + RETRY_STATISTICS.totalRetryAttemptsStarted); - logger.fine("Total number of retries finished: " + RETRY_STATISTICS.totalRetryAttemptsFinished); - logger.fine("Total number of retries successful: " + RETRY_STATISTICS.totalSuccessfulRetries); - logger.fine("Total number of retries aborted: " + RETRY_STATISTICS.totalNestedAborts); - logger.fine( - "Total number of times the max retry count was exceeded: " - + RETRY_STATISTICS.totalMaxAttemptsExceeded); - } - - @Test - public void testAbortWithConcurrentInsertOnEmptyTable() { - AbortInterceptor interceptor = new AbortInterceptor(0); - try (ITConnection connection = - createConnection(interceptor, new CountTransactionRetryListener())) { - // select the test records but do not consume the result set - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - // Open a new connection and transaction and do an insert. This insert will be - // included in a retry of the above query, but this has not yet been 'seen' by the user, - // hence is not a problem for retrying the transaction. - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection2.commit(); - } - // Now try to consume the result set, but the call to next() will throw an AbortedException. - // The retry should still succeed. - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - int currentSuccessfulRetryCount = RETRY_STATISTICS.totalSuccessfulRetries; - assertThat(rs.next(), is(true)); - assertThat( - RETRY_STATISTICS.totalSuccessfulRetries, is(equalTo(currentSuccessfulRetryCount + 1))); - assertThat(rs.next(), is(false)); - } - connection.commit(); - - // Now do the same, but this time we will consume the empty result set. The retry should now - // fail. - clearTable(); - clearStatistics(); - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { - assertThat(rs.next(), is(false)); - // Open a new connection and transaction and do an insert. This insert will be - // included in a retry of the above query, and this time it will cause the retry to fail. - try (ITConnection connection2 = createConnection()) { - connection2.executeUpdate( - Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); - connection2.commit(); - } - // this time the abort will occur on the call to commit() - interceptor.setProbability(1.0); - interceptor.setOnlyInjectOnce(true); - boolean expectedException = false; - try { - connection.commit(); - } catch (AbortedDueToConcurrentModificationException e) { - expectedException = true; - } - // No successful retries. - assertRetryStatistics(1, 1, 0); - assertThat(expectedException, is(true)); - } - } - } - - private void assertRetryStatistics( - int minAttemptsStartedExpected, - int concurrentModificationsExpected, - int successfulRetriesExpected) { - assertThat(RETRY_STATISTICS.totalRetryAttemptsStarted >= minAttemptsStartedExpected, is(true)); - assertThat( - RETRY_STATISTICS.totalConcurrentModifications, - is(equalTo(concurrentModificationsExpected))); - assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= successfulRetriesExpected, is(true)); - } -}