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)); - } -}