From 55bdeff72feb2d76046bca68ca6b5a7161b8185a Mon Sep 17 00:00:00 2001 From: Mridula <66699525+mpeddada1@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:39:51 +0000 Subject: [PATCH 01/10] chore: use sdk-platform-java-config to consolidate build configs (#2865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: use sdk-platform-java-config to consolidate build configs * update trampoline images * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix property name * exclude graalvm cfg from owlbot --------- Co-authored-by: Owl Bot --- .kokoro/presubmit/graalvm-native-17.cfg | 2 +- .kokoro/presubmit/graalvm-native.cfg | 2 +- google-cloud-spanner-bom/pom.xml | 4 ++-- owlbot.py | 2 ++ pom.xml | 7 +++---- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg index fb5bb678ffc..90d9a20a085 100644 --- a/.kokoro/presubmit/graalvm-native-17.cfg +++ b/.kokoro/presubmit/graalvm-native-17.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/graalvm17:22.3.3" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.24.0" } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg index 59efee340c5..948177be87f 100644 --- a/.kokoro/presubmit/graalvm-native.cfg +++ b/.kokoro/presubmit/graalvm-native.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/graalvm:22.3.3" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.24.0" } env_vars: { diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 2166bc5c6a0..fc5249803fc 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -7,8 +7,8 @@ pom com.google.cloud - google-cloud-shared-config - 1.6.1 + sdk-platform-java-config + 3.24.0 Google Cloud Spanner BOM diff --git a/owlbot.py b/owlbot.py index 4b41b1cf272..d5b6eb69579 100644 --- a/owlbot.py +++ b/owlbot.py @@ -33,6 +33,8 @@ ".kokoro/presubmit/java8-samples.cfg", ".kokoro/presubmit/java11-samples.cfg", ".kokoro/presubmit/samples.cfg", + ".kokoro/presubmit/graalvm-native.cfg", + ".kokoro/presubmit/graalvm-native-17.cfg", ".kokoro/release/common.cfg", "samples/install-without-bom/pom.xml", "samples/snapshot/pom.xml", diff --git a/pom.xml b/pom.xml index 9eaf48ff710..2329040546d 100644 --- a/pom.xml +++ b/pom.xml @@ -13,8 +13,8 @@ com.google.cloud - google-cloud-shared-config - 1.6.1 + sdk-platform-java-config + 3.24.0 @@ -54,7 +54,6 @@ UTF-8 github google-cloud-spanner-parent - 3.24.0 @@ -116,7 +115,7 @@ com.google.cloud google-cloud-shared-dependencies - ${google.cloud.shared-dependencies.version} + ${google-cloud-shared-dependencies.version} pom import From 3ba604dff20ed54c4f18bfff99815f696dcab138 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:10:30 +0530 Subject: [PATCH 02/10] chore(java): make org.graalvm.buildtools:junit-platform-native a test dependency upgrade in renovate (#1922) (#2858) Source-Link: https://github.com/googleapis/synthtool/commit/ee0dedaa6aa1276d9876dddd06655c988f8bd6a2 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-java:latest@sha256:1fb09a3eb66af09221da69087fd1b4d075bc7c79e508d0708f5dc0f842069da2 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- renovate.json | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index dc05a72762f..b9c5b09c7f5 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest - digest: sha256:a6aa751984f1e905c3ae5a3aac78fc7b68210626ce91487dc7ff4f0a06f010cc -# created: 2024-01-22T14:14:20.913785597Z + digest: sha256:1fb09a3eb66af09221da69087fd1b4d075bc7c79e508d0708f5dc0f842069da2 +# created: 2024-02-05T19:43:08.106031548Z diff --git a/renovate.json b/renovate.json index 86460a69342..b5cf3239c07 100644 --- a/renovate.json +++ b/renovate.json @@ -14,6 +14,17 @@ ".kokoro/requirements.txt", ".github/workflows/**" ], + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "^.kokoro/presubmit/graalvm-native.*.cfg$" + ], + "matchStrings": ["value: \"gcr.io/cloud-devrel-public-resources/graalvm.*:(?.*?)\""], + "depNameTemplate": "com.google.cloud:sdk-platform-java-config", + "datasourceTemplate": "maven" + } + ], "packageRules": [ { "packagePatterns": [ @@ -55,7 +66,8 @@ "^com.google.truth:truth", "^org.mockito:mockito-core", "^org.objenesis:objenesis", - "^com.google.cloud:google-cloud-conformance-tests" + "^com.google.cloud:google-cloud-conformance-tests", + "^org.graalvm.buildtools:junit-platform-native" ], "semanticCommitType": "test", "semanticCommitScope": "deps" From 0e96d1ff3b6c5802ee25f4a64b49dc5cad99c594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Fri, 9 Feb 2024 16:52:49 +0100 Subject: [PATCH 03/10] refactor: move inner classes to top level (#2846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: move inner classes to top level Move the gRPC-related inner classes from AbstractResultSet to top-level classes, so they are easier to modify and maintain. This change only contains modifications that are needed to move these inner classes. There are no functional changes. * feat: support lazy decoding of query results (#2847) * feat: support lazy decoding of query results Adds an option for lazy decoding of query results. Currently, all values in a query result row are decoded from protobuf values to plain Java objects at the moment that the result set is advanced to the next row. This means that all values are decoded, regardless whether the application actually fetches these or not. Lazy decoding also enables the possibility for (internal) consumers of a result set to access the protobuf value before it is converted to a plain Java object. This for example allows ChecksumResultSet to calculate the checksum based on the protobuf value, instead of a Java object, which can be more efficient. * fix: add null check * perf: calculate checksum using protobuf values (#2848) * perf: calculate checksum using protobuf values * chore: cleanup * test: remove unrelated test * fix: undo change to public API * chore: cleanup| * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- README.md | 6 +- .../cloud/spanner/AbstractReadContext.java | 18 +- .../cloud/spanner/AbstractResultSet.java | 1276 +---------------- .../google/cloud/spanner/BatchClientImpl.java | 2 + .../com/google/cloud/spanner/DecodeMode.java | 35 + .../cloud/spanner/ForwardingResultSet.java | 18 +- .../google/cloud/spanner/GrpcResultSet.java | 132 ++ .../cloud/spanner/GrpcStreamIterator.java | 172 +++ .../com/google/cloud/spanner/GrpcStruct.java | 766 ++++++++++ .../cloud/spanner/GrpcValueIterator.java | 212 +++ .../com/google/cloud/spanner/Options.java | 32 + .../cloud/spanner/ProtobufResultSet.java | 42 + .../com/google/cloud/spanner/ResultSets.java | 15 +- .../spanner/ResumableStreamIterator.java | 277 ++++ .../com/google/cloud/spanner/SessionImpl.java | 4 + .../com/google/cloud/spanner/SpannerImpl.java | 4 + .../google/cloud/spanner/SpannerOptions.java | 20 + .../java/com/google/cloud/spanner/Value.java | 4 + .../spanner/connection/ChecksumResultSet.java | 375 +++-- .../connection/DirectExecuteResultSet.java | 18 +- .../connection/ReadWriteTransaction.java | 7 +- .../ReplaceableForwardingResultSet.java | 22 +- .../cloud/spanner/connection/SpannerPool.java | 4 + .../cloud/spanner/DatabaseClientImplTest.java | 1 - .../cloud/spanner/GrpcResultSetTest.java | 16 +- .../cloud/spanner/MockSpannerServiceImpl.java | 1 - .../cloud/spanner/ReadFormatTestRunner.java | 11 +- .../cloud/spanner/ResultSetsHelper.java | 1 - .../spanner/ResumableStreamIteratorTest.java | 7 +- .../spanner/connection/DecodeModeTest.java | 128 ++ .../DirectExecuteResultSetTest.java | 3 + .../connection/RandomResultSetGenerator.java | 6 +- .../connection/ReadWriteTransactionTest.java | 435 +++--- .../ReplaceableForwardingResultSetTest.java | 11 +- 34 files changed, 2341 insertions(+), 1740 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/DecodeMode.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcValueIterator.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/ProtobufResultSet.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DecodeModeTest.java diff --git a/README.md b/README.md index 288afeef263..56d668b0a65 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.57.0' +implementation 'com.google.cloud:google-cloud-spanner:6.58.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.57.0" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.58.0" ``` @@ -444,7 +444,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.57.0 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.58.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java index 0714b7651cc..ae18a1e4f5f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java @@ -28,9 +28,6 @@ import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; -import com.google.cloud.spanner.AbstractResultSet.GrpcResultSet; -import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator; -import com.google.cloud.spanner.AbstractResultSet.ResumableStreamIterator; import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; import com.google.cloud.spanner.Options.QueryOption; @@ -73,6 +70,7 @@ abstract static class Builder, T extends AbstractReadCon private TraceWrapper tracer; private int defaultPrefetchChunks = SpannerOptions.Builder.DEFAULT_PREFETCH_CHUNKS; private QueryOptions defaultQueryOptions = SpannerOptions.Builder.DEFAULT_QUERY_OPTIONS; + private DecodeMode defaultDecodeMode = SpannerOptions.Builder.DEFAULT_DECODE_MODE; private DirectedReadOptions defaultDirectedReadOption; private ExecutorProvider executorProvider; private Clock clock = new Clock(); @@ -114,6 +112,11 @@ B setDefaultQueryOptions(QueryOptions defaultQueryOptions) { return self(); } + B setDefaultDecodeMode(DecodeMode defaultDecodeMode) { + this.defaultDecodeMode = defaultDecodeMode; + return self(); + } + B setExecutorProvider(ExecutorProvider executorProvider) { this.executorProvider = executorProvider; return self(); @@ -414,8 +417,8 @@ void initTransaction() { TraceWrapper tracer; private final int defaultPrefetchChunks; private final QueryOptions defaultQueryOptions; - private final DirectedReadOptions defaultDirectedReadOptions; + private final DecodeMode defaultDecodeMode; private final Clock clock; @GuardedBy("lock") @@ -441,6 +444,7 @@ void initTransaction() { this.defaultPrefetchChunks = builder.defaultPrefetchChunks; this.defaultQueryOptions = builder.defaultQueryOptions; this.defaultDirectedReadOptions = builder.defaultDirectedReadOption; + this.defaultDecodeMode = builder.defaultDecodeMode; this.span = builder.span; this.executorProvider = builder.executorProvider; this.clock = builder.clock; @@ -730,7 +734,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - return new GrpcResultSet(stream, this); + return new GrpcResultSet( + stream, this, options.hasDecodeMode() ? options.decodeMode() : defaultDecodeMode); } /** @@ -874,7 +879,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - return new GrpcResultSet(stream, this); + return new GrpcResultSet( + stream, this, readOptions.hasDecodeMode() ? readOptions.decodeMode() : defaultDecodeMode); } private Struct consumeSingleRow(ResultSet resultSet) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 4904a382f92..6cce03e72cb 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -17,72 +17,32 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; -import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerExceptionForCancellation; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.api.client.util.BackOff; -import com.google.api.client.util.ExponentialBackOff; -import com.google.api.gax.grpc.GrpcStatusCode; -import com.google.api.gax.retrying.RetrySettings; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.StatusCode.Code; + import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Type.StructField; -import com.google.cloud.spanner.spi.v1.SpannerRpc; -import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.Lists; -import com.google.common.io.CharSource; -import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ByteString; import com.google.protobuf.ListValue; -import com.google.protobuf.NullValue; import com.google.protobuf.ProtocolMessageEnum; import com.google.protobuf.Value.KindCase; -import com.google.spanner.v1.PartialResultSet; -import com.google.spanner.v1.ResultSetMetadata; -import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; -import com.google.spanner.v1.TypeCode; -import io.grpc.Context; import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.util.AbstractList; -import java.util.ArrayList; import java.util.Base64; import java.util.BitSet; -import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Objects; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.threeten.bp.Duration; /** Implementation of {@link ResultSet}. */ abstract class AbstractResultSet extends AbstractStructReader implements ResultSet { - private static final com.google.protobuf.Value NULL_VALUE = - com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); interface Listener { /** @@ -99,271 +59,6 @@ void onTransactionMetadata(Transaction transaction, boolean shouldIncludeId) void onDone(boolean withBeginTransaction); } - @VisibleForTesting - static class GrpcResultSet extends AbstractResultSet> { - private final GrpcValueIterator iterator; - private final Listener listener; - private ResultSetMetadata metadata; - private GrpcStruct currRow; - private SpannerException error; - private ResultSetStats statistics; - private boolean closed; - - GrpcResultSet(CloseableIterator iterator, Listener listener) { - this.iterator = new GrpcValueIterator(iterator); - this.listener = listener; - } - - @Override - protected GrpcStruct currRow() { - checkState(!closed, "ResultSet is closed"); - checkState(currRow != null, "next() call required"); - return currRow; - } - - @Override - public boolean next() throws SpannerException { - if (error != null) { - throw newSpannerException(error); - } - try { - if (currRow == null) { - metadata = iterator.getMetadata(); - if (metadata.hasTransaction()) { - listener.onTransactionMetadata( - metadata.getTransaction(), iterator.isWithBeginTransaction()); - } else if (iterator.isWithBeginTransaction()) { - // The query should have returned a transaction. - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.FAILED_PRECONDITION, AbstractReadContext.NO_TRANSACTION_RETURNED_MSG); - } - currRow = new GrpcStruct(iterator.type(), new ArrayList<>()); - } - boolean hasNext = currRow.consumeRow(iterator); - if (!hasNext) { - statistics = iterator.getStats(); - } - return hasNext; - } catch (Throwable t) { - throw yieldError( - SpannerExceptionFactory.asSpannerException(t), - iterator.isWithBeginTransaction() && currRow == null); - } - } - - @Override - @Nullable - public ResultSetStats getStats() { - return statistics; - } - - @Override - public ResultSetMetadata getMetadata() { - checkState(metadata != null, "next() call required"); - return metadata; - } - - @Override - public void close() { - listener.onDone(iterator.isWithBeginTransaction()); - iterator.close("ResultSet closed"); - closed = true; - } - - @Override - public Type getType() { - checkState(currRow != null, "next() call required"); - return currRow.getType(); - } - - private SpannerException yieldError(SpannerException e, boolean beginTransaction) { - SpannerException toThrow = listener.onError(e, beginTransaction); - close(); - throw toThrow; - } - } - /** - * Adapts a stream of {@code PartialResultSet} messages into a stream of {@code Value} messages. - */ - private static class GrpcValueIterator extends AbstractIterator { - private enum StreamValue { - METADATA, - RESULT, - } - - private final CloseableIterator stream; - private ResultSetMetadata metadata; - private Type type; - private PartialResultSet current; - private int pos; - private ResultSetStats statistics; - - GrpcValueIterator(CloseableIterator stream) { - this.stream = stream; - } - - @SuppressWarnings("unchecked") - @Override - protected com.google.protobuf.Value computeNext() { - if (!ensureReady(StreamValue.RESULT)) { - endOfData(); - return null; - } - com.google.protobuf.Value value = current.getValues(pos++); - KindCase kind = value.getKindCase(); - - if (!isMergeable(kind)) { - if (pos == current.getValuesCount() && current.getChunkedValue()) { - throw newSpannerException(ErrorCode.INTERNAL, "Unexpected chunked PartialResultSet."); - } else { - return value; - } - } - if (!current.getChunkedValue() || pos != current.getValuesCount()) { - return value; - } - - Object merged = - kind == KindCase.STRING_VALUE - ? value.getStringValue() - : new ArrayList<>(value.getListValue().getValuesList()); - while (current.getChunkedValue() && pos == current.getValuesCount()) { - if (!ensureReady(StreamValue.RESULT)) { - throw newSpannerException( - ErrorCode.INTERNAL, "Stream closed in the middle of chunked value"); - } - com.google.protobuf.Value newValue = current.getValues(pos++); - if (newValue.getKindCase() != kind) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Unexpected type in middle of chunked value. Expected: " - + kind - + " but got: " - + newValue.getKindCase()); - } - if (kind == KindCase.STRING_VALUE) { - merged = merged + newValue.getStringValue(); - } else { - concatLists( - (List) merged, newValue.getListValue().getValuesList()); - } - } - if (kind == KindCase.STRING_VALUE) { - return com.google.protobuf.Value.newBuilder().setStringValue((String) merged).build(); - } else { - return com.google.protobuf.Value.newBuilder() - .setListValue( - ListValue.newBuilder().addAllValues((List) merged)) - .build(); - } - } - - ResultSetMetadata getMetadata() throws SpannerException { - if (metadata == null) { - if (!ensureReady(StreamValue.METADATA)) { - throw newSpannerException(ErrorCode.INTERNAL, "Stream closed without sending metadata"); - } - } - return metadata; - } - - /** - * Get the query statistics. Query statistics are delivered with the last PartialResultSet in - * the stream. Any attempt to call this method before the caller has finished consuming the - * results will return null. - */ - @Nullable - ResultSetStats getStats() { - return statistics; - } - - Type type() { - checkState(type != null, "metadata has not been received"); - return type; - } - - private boolean ensureReady(StreamValue requiredValue) throws SpannerException { - while (current == null || pos >= current.getValuesCount()) { - if (!stream.hasNext()) { - return false; - } - current = stream.next(); - pos = 0; - if (type == null) { - // This is the first message on the stream. - if (!current.hasMetadata() || !current.getMetadata().hasRowType()) { - throw newSpannerException(ErrorCode.INTERNAL, "Missing type metadata in first message"); - } - metadata = current.getMetadata(); - com.google.spanner.v1.Type typeProto = - com.google.spanner.v1.Type.newBuilder() - .setCode(TypeCode.STRUCT) - .setStructType(metadata.getRowType()) - .build(); - try { - type = Type.fromProto(typeProto); - } catch (IllegalArgumentException e) { - throw newSpannerException( - ErrorCode.INTERNAL, "Invalid type metadata: " + e.getMessage(), e); - } - } - if (current.hasStats()) { - statistics = current.getStats(); - } - if (requiredValue == StreamValue.METADATA) { - return true; - } - } - return true; - } - - void close(@Nullable String message) { - stream.close(message); - } - - boolean isWithBeginTransaction() { - return stream.isWithBeginTransaction(); - } - - /** @param a is a mutable list and b will be concatenated into a. */ - private void concatLists(List a, List b) { - if (a.size() == 0 || b.size() == 0) { - a.addAll(b); - return; - } else { - com.google.protobuf.Value last = a.get(a.size() - 1); - com.google.protobuf.Value first = b.get(0); - KindCase lastKind = last.getKindCase(); - KindCase firstKind = first.getKindCase(); - if (isMergeable(lastKind) && lastKind == firstKind) { - com.google.protobuf.Value merged; - if (lastKind == KindCase.STRING_VALUE) { - String lastStr = last.getStringValue(); - String firstStr = first.getStringValue(); - merged = - com.google.protobuf.Value.newBuilder().setStringValue(lastStr + firstStr).build(); - } else { // List - List mergedList = new ArrayList<>(); - mergedList.addAll(last.getListValue().getValuesList()); - concatLists(mergedList, first.getListValue().getValuesList()); - merged = - com.google.protobuf.Value.newBuilder() - .setListValue(ListValue.newBuilder().addAllValues(mergedList)) - .build(); - } - a.set(a.size() - 1, merged); - a.addAll(b.subList(1, b.size())); - } else { - a.addAll(b); - } - } - } - - private boolean isMergeable(KindCase kind) { - return kind == KindCase.STRING_VALUE || kind == KindCase.LIST_VALUE; - } - } - static final class LazyByteArray implements Serializable { private static final Base64.Encoder ENCODER = Base64.getEncoder(); private static final Base64.Decoder DECODER = Base64.getDecoder(); @@ -437,603 +132,6 @@ private boolean lazyByteArraysEqual(LazyByteArray other) { } } - static class GrpcStruct extends Struct implements Serializable { - private final Type type; - private final List rowData; - - /** - * Builds an immutable version of this struct using {@link Struct#newBuilder()} which is used as - * a serialization proxy. - */ - private Object writeReplace() { - Builder builder = Struct.newBuilder(); - List structFields = getType().getStructFields(); - for (int i = 0; i < structFields.size(); i++) { - Type.StructField field = structFields.get(i); - String fieldName = field.getName(); - Object value = rowData.get(i); - Type fieldType = field.getType(); - switch (fieldType.getCode()) { - case BOOL: - builder.set(fieldName).to((Boolean) value); - break; - case INT64: - builder.set(fieldName).to((Long) value); - break; - case FLOAT64: - builder.set(fieldName).to((Double) value); - break; - case NUMERIC: - builder.set(fieldName).to((BigDecimal) value); - break; - case PG_NUMERIC: - builder.set(fieldName).to((String) value); - break; - case STRING: - builder.set(fieldName).to((String) value); - break; - case JSON: - builder.set(fieldName).to(Value.json((String) value)); - break; - case PROTO: - builder - .set(fieldName) - .to( - Value.protoMessage( - value == null ? null : ((LazyByteArray) value).getByteArray(), - fieldType.getProtoTypeFqn())); - break; - case ENUM: - builder.set(fieldName).to(Value.protoEnum((Long) value, fieldType.getProtoTypeFqn())); - break; - case PG_JSONB: - builder.set(fieldName).to(Value.pgJsonb((String) value)); - break; - case BYTES: - builder - .set(fieldName) - .to( - Value.bytesFromBase64( - value == null ? null : ((LazyByteArray) value).getBase64String())); - break; - case TIMESTAMP: - builder.set(fieldName).to((Timestamp) value); - break; - case DATE: - builder.set(fieldName).to((Date) value); - break; - case ARRAY: - final Type elementType = fieldType.getArrayElementType(); - switch (elementType.getCode()) { - case BOOL: - builder.set(fieldName).toBoolArray((Iterable) value); - break; - case INT64: - case ENUM: - builder.set(fieldName).toInt64Array((Iterable) value); - break; - case FLOAT64: - builder.set(fieldName).toFloat64Array((Iterable) value); - break; - case NUMERIC: - builder.set(fieldName).toNumericArray((Iterable) value); - break; - case PG_NUMERIC: - builder.set(fieldName).toPgNumericArray((Iterable) value); - break; - case STRING: - builder.set(fieldName).toStringArray((Iterable) value); - break; - case JSON: - builder.set(fieldName).toJsonArray((Iterable) value); - break; - case PG_JSONB: - builder.set(fieldName).toPgJsonbArray((Iterable) value); - break; - case BYTES: - case PROTO: - builder - .set(fieldName) - .toBytesArrayFromBase64( - value == null - ? null - : ((List) value) - .stream() - .map( - element -> - element == null ? null : element.getBase64String()) - .collect(Collectors.toList())); - break; - case TIMESTAMP: - builder.set(fieldName).toTimestampArray((Iterable) value); - break; - case DATE: - builder.set(fieldName).toDateArray((Iterable) value); - break; - case STRUCT: - builder.set(fieldName).toStructArray(elementType, (Iterable) value); - break; - default: - throw new AssertionError("Unhandled array type code: " + elementType); - } - break; - case STRUCT: - if (value == null) { - builder.set(fieldName).to(fieldType, null); - } else { - builder.set(fieldName).to((Struct) value); - } - break; - default: - throw new AssertionError("Unhandled type code: " + fieldType.getCode()); - } - } - return builder.build(); - } - - GrpcStruct(Type type, List rowData) { - this.type = type; - this.rowData = rowData; - } - - @Override - public String toString() { - return this.rowData.toString(); - } - - boolean consumeRow(Iterator iterator) { - rowData.clear(); - if (!iterator.hasNext()) { - return false; - } - for (Type.StructField fieldType : getType().getStructFields()) { - if (!iterator.hasNext()) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Invalid value stream: end of stream reached before row is complete"); - } - com.google.protobuf.Value value = iterator.next(); - rowData.add(decodeValue(fieldType.getType(), value)); - } - return true; - } - - private static Object decodeValue(Type fieldType, com.google.protobuf.Value proto) { - if (proto.getKindCase() == KindCase.NULL_VALUE) { - return null; - } - switch (fieldType.getCode()) { - case BOOL: - checkType(fieldType, proto, KindCase.BOOL_VALUE); - return proto.getBoolValue(); - case INT64: - case ENUM: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Long.parseLong(proto.getStringValue()); - case FLOAT64: - return valueProtoToFloat64(proto); - case NUMERIC: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return new BigDecimal(proto.getStringValue()); - case PG_NUMERIC: - case STRING: - case JSON: - case PG_JSONB: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return proto.getStringValue(); - case BYTES: - case PROTO: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return new LazyByteArray(proto.getStringValue()); - case TIMESTAMP: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Timestamp.parseTimestamp(proto.getStringValue()); - case DATE: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return Date.parseDate(proto.getStringValue()); - case ARRAY: - checkType(fieldType, proto, KindCase.LIST_VALUE); - ListValue listValue = proto.getListValue(); - return decodeArrayValue(fieldType.getArrayElementType(), listValue); - case STRUCT: - checkType(fieldType, proto, KindCase.LIST_VALUE); - ListValue structValue = proto.getListValue(); - return decodeStructValue(fieldType, structValue); - case UNRECOGNIZED: - return proto; - default: - throw new AssertionError("Unhandled type code: " + fieldType.getCode()); - } - } - - private static Struct decodeStructValue(Type structType, ListValue structValue) { - List fieldTypes = structType.getStructFields(); - checkArgument( - structValue.getValuesCount() == fieldTypes.size(), - "Size mismatch between type descriptor and actual values."); - List fields = new ArrayList<>(fieldTypes.size()); - List fieldValues = structValue.getValuesList(); - for (int i = 0; i < fieldTypes.size(); ++i) { - fields.add(decodeValue(fieldTypes.get(i).getType(), fieldValues.get(i))); - } - return new GrpcStruct(structType, fields); - } - - static Object decodeArrayValue(Type elementType, ListValue listValue) { - switch (elementType.getCode()) { - case INT64: - case ENUM: - // For int64/float64/enum types, use custom containers. These avoid wrapper object - // creation for non-null arrays. - return new Int64Array(listValue); - case FLOAT64: - return new Float64Array(listValue); - case BOOL: - case NUMERIC: - case PG_NUMERIC: - case STRING: - case JSON: - case PG_JSONB: - case BYTES: - case TIMESTAMP: - case DATE: - case STRUCT: - case PROTO: - return Lists.transform( - listValue.getValuesList(), input -> decodeValue(elementType, input)); - default: - throw new AssertionError("Unhandled type code: " + elementType.getCode()); - } - } - - private static void checkType( - Type fieldType, com.google.protobuf.Value proto, KindCase expected) { - if (proto.getKindCase() != expected) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Invalid value for column type " - + fieldType - + " expected " - + expected - + " but was " - + proto.getKindCase()); - } - } - - Struct immutableCopy() { - return new GrpcStruct(type, new ArrayList<>(rowData)); - } - - @Override - public Type getType() { - return type; - } - - @Override - public boolean isNull(int columnIndex) { - return rowData.get(columnIndex) == null; - } - - @Override - protected T getProtoMessageInternal(int columnIndex, T message) { - Preconditions.checkNotNull( - message, - "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); - try { - return (T) - message - .toBuilder() - .mergeFrom( - Base64.getDecoder() - .wrap( - CharSource.wrap(((LazyByteArray) rowData.get(columnIndex)).base64String) - .asByteSource(StandardCharsets.UTF_8) - .openStream())) - .build(); - } catch (IOException ioException) { - throw SpannerExceptionFactory.asSpannerException(ioException); - } - } - - @Override - protected T getProtoEnumInternal( - int columnIndex, Function method) { - Preconditions.checkNotNull( - method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); - return (T) method.apply((int) getLongInternal(columnIndex)); - } - - @Override - protected boolean getBooleanInternal(int columnIndex) { - return (Boolean) rowData.get(columnIndex); - } - - @Override - protected long getLongInternal(int columnIndex) { - return (Long) rowData.get(columnIndex); - } - - @Override - protected double getDoubleInternal(int columnIndex) { - return (Double) rowData.get(columnIndex); - } - - @Override - protected BigDecimal getBigDecimalInternal(int columnIndex) { - return (BigDecimal) rowData.get(columnIndex); - } - - @Override - protected String getStringInternal(int columnIndex) { - return (String) rowData.get(columnIndex); - } - - @Override - protected String getJsonInternal(int columnIndex) { - return (String) rowData.get(columnIndex); - } - - @Override - protected String getPgJsonbInternal(int columnIndex) { - return (String) rowData.get(columnIndex); - } - - @Override - protected ByteArray getBytesInternal(int columnIndex) { - return getLazyBytesInternal(columnIndex).getByteArray(); - } - - LazyByteArray getLazyBytesInternal(int columnIndex) { - return (LazyByteArray) rowData.get(columnIndex); - } - - @Override - protected Timestamp getTimestampInternal(int columnIndex) { - return (Timestamp) rowData.get(columnIndex); - } - - @Override - protected Date getDateInternal(int columnIndex) { - return (Date) rowData.get(columnIndex); - } - - protected com.google.protobuf.Value getProtoValueInternal(int columnIndex) { - return (com.google.protobuf.Value) rowData.get(columnIndex); - } - - @Override - protected Value getValueInternal(int columnIndex) { - final List structFields = getType().getStructFields(); - final StructField structField = structFields.get(columnIndex); - final Type columnType = structField.getType(); - final boolean isNull = rowData.get(columnIndex) == null; - switch (columnType.getCode()) { - case BOOL: - return Value.bool(isNull ? null : getBooleanInternal(columnIndex)); - case INT64: - return Value.int64(isNull ? null : getLongInternal(columnIndex)); - case ENUM: - return Value.protoEnum( - isNull ? null : getLongInternal(columnIndex), columnType.getProtoTypeFqn()); - case NUMERIC: - return Value.numeric(isNull ? null : getBigDecimalInternal(columnIndex)); - case PG_NUMERIC: - return Value.pgNumeric(isNull ? null : getStringInternal(columnIndex)); - case FLOAT64: - return Value.float64(isNull ? null : getDoubleInternal(columnIndex)); - case STRING: - return Value.string(isNull ? null : getStringInternal(columnIndex)); - case JSON: - return Value.json(isNull ? null : getJsonInternal(columnIndex)); - case PG_JSONB: - return Value.pgJsonb(isNull ? null : getPgJsonbInternal(columnIndex)); - case BYTES: - return Value.internalBytes(isNull ? null : getLazyBytesInternal(columnIndex)); - case PROTO: - return Value.protoMessage( - isNull ? null : getBytesInternal(columnIndex), columnType.getProtoTypeFqn()); - case TIMESTAMP: - return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); - case DATE: - return Value.date(isNull ? null : getDateInternal(columnIndex)); - case STRUCT: - return Value.struct(isNull ? null : getStructInternal(columnIndex)); - case UNRECOGNIZED: - return Value.unrecognized( - isNull ? NULL_VALUE : getProtoValueInternal(columnIndex), columnType); - case ARRAY: - final Type elementType = columnType.getArrayElementType(); - switch (elementType.getCode()) { - case BOOL: - return Value.boolArray(isNull ? null : getBooleanListInternal(columnIndex)); - case INT64: - return Value.int64Array(isNull ? null : getLongListInternal(columnIndex)); - case NUMERIC: - return Value.numericArray(isNull ? null : getBigDecimalListInternal(columnIndex)); - case PG_NUMERIC: - return Value.pgNumericArray(isNull ? null : getStringListInternal(columnIndex)); - case FLOAT64: - return Value.float64Array(isNull ? null : getDoubleListInternal(columnIndex)); - case STRING: - return Value.stringArray(isNull ? null : getStringListInternal(columnIndex)); - case JSON: - return Value.jsonArray(isNull ? null : getJsonListInternal(columnIndex)); - case PG_JSONB: - return Value.pgJsonbArray(isNull ? null : getPgJsonbListInternal(columnIndex)); - case BYTES: - return Value.bytesArray(isNull ? null : getBytesListInternal(columnIndex)); - case PROTO: - return Value.protoMessageArray( - isNull ? null : getBytesListInternal(columnIndex), elementType.getProtoTypeFqn()); - case ENUM: - return Value.protoEnumArray( - isNull ? null : getLongListInternal(columnIndex), elementType.getProtoTypeFqn()); - case TIMESTAMP: - return Value.timestampArray(isNull ? null : getTimestampListInternal(columnIndex)); - case DATE: - return Value.dateArray(isNull ? null : getDateListInternal(columnIndex)); - case STRUCT: - return Value.structArray( - elementType, isNull ? null : getStructListInternal(columnIndex)); - default: - throw new IllegalArgumentException( - "Invalid array value type " + this.type.getArrayElementType()); - } - default: - throw new IllegalArgumentException("Invalid value type " + this.type); - } - } - - @Override - protected Struct getStructInternal(int columnIndex) { - return (Struct) rowData.get(columnIndex); - } - - @Override - protected boolean[] getBooleanArrayInternal(int columnIndex) { - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - List values = (List) rowData.get(columnIndex); - boolean[] r = new boolean[values.size()]; - for (int i = 0; i < values.size(); ++i) { - if (values.get(i) == null) { - throw throwNotNull(columnIndex); - } - r[i] = values.get(i); - } - return r; - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBooleanListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - protected long[] getLongArrayInternal(int columnIndex) { - return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - protected Int64Array getLongListInternal(int columnIndex) { - return (Int64Array) rowData.get(columnIndex); - } - - @Override - protected double[] getDoubleArrayInternal(int columnIndex) { - return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - protected Float64Array getDoubleListInternal(int columnIndex) { - return (Float64Array) rowData.get(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBigDecimalListInternal(int columnIndex) { - return (List) rowData.get(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getStringListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getJsonListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getProtoMessageListInternal( - int columnIndex, T message) { - Preconditions.checkNotNull( - message, - "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); - - List bytesArray = (List) rowData.get(columnIndex); - - try { - List protoMessagesList = new ArrayList<>(bytesArray.size()); - for (LazyByteArray protoMessageBytes : bytesArray) { - if (protoMessageBytes == null) { - protoMessagesList.add(null); - } else { - protoMessagesList.add( - (T) - message - .toBuilder() - .mergeFrom( - Base64.getDecoder() - .wrap( - CharSource.wrap(protoMessageBytes.base64String) - .asByteSource(StandardCharsets.UTF_8) - .openStream())) - .build()); - } - } - return protoMessagesList; - } catch (IOException ioException) { - throw SpannerExceptionFactory.asSpannerException(ioException); - } - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getProtoEnumListInternal( - int columnIndex, Function method) { - Preconditions.checkNotNull( - method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); - - List enumIntArray = (List) rowData.get(columnIndex); - List protoEnumList = new ArrayList<>(enumIntArray.size()); - for (Long enumIntValue : enumIntArray) { - if (enumIntValue == null) { - protoEnumList.add(null); - } else { - protoEnumList.add((T) method.apply(enumIntValue.intValue())); - } - } - - return protoEnumList; - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getPgJsonbListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBytesListInternal(int columnIndex) { - return Lists.transform( - (List) rowData.get(columnIndex), l -> l == null ? null : l.getByteArray()); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getTimestampListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getDateListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY> produces a List. - protected List getStructListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - } - @VisibleForTesting interface CloseableIterator extends Iterator { @@ -1047,378 +145,6 @@ interface CloseableIterator extends Iterator { boolean isWithBeginTransaction(); } - /** Adapts a streaming read/query call into an iterator over partial result sets. */ - @VisibleForTesting - static class GrpcStreamIterator extends AbstractIterator - implements CloseableIterator { - private static final Logger logger = Logger.getLogger(GrpcStreamIterator.class.getName()); - private static final PartialResultSet END_OF_STREAM = PartialResultSet.newBuilder().build(); - - private final ConsumerImpl consumer = new ConsumerImpl(); - private final BlockingQueue stream; - private final Statement statement; - - private SpannerRpc.StreamingCall call; - private volatile boolean withBeginTransaction; - private TimeUnit streamWaitTimeoutUnit; - private long streamWaitTimeoutValue; - private SpannerException error; - - @VisibleForTesting - GrpcStreamIterator(int prefetchChunks) { - this(null, prefetchChunks); - } - - @VisibleForTesting - GrpcStreamIterator(Statement statement, int prefetchChunks) { - this.statement = statement; - // One extra to allow for END_OF_STREAM message. - this.stream = new LinkedBlockingQueue<>(prefetchChunks + 1); - } - - protected final SpannerRpc.ResultStreamConsumer consumer() { - return consumer; - } - - public void setCall(SpannerRpc.StreamingCall call, boolean withBeginTransaction) { - this.call = call; - this.withBeginTransaction = withBeginTransaction; - ApiCallContext callContext = call.getCallContext(); - Duration streamWaitTimeout = callContext == null ? null : callContext.getStreamWaitTimeout(); - if (streamWaitTimeout != null) { - // Determine the timeout unit to use. This reduces the precision to seconds if the timeout - // value is more than 1 second, which is lower than the precision that would normally be - // used by the stream watchdog (which uses a precision of 10 seconds by default). - if (streamWaitTimeout.getSeconds() > 0L) { - streamWaitTimeoutValue = streamWaitTimeout.getSeconds(); - streamWaitTimeoutUnit = TimeUnit.SECONDS; - } else if (streamWaitTimeout.getNano() > 0) { - streamWaitTimeoutValue = streamWaitTimeout.getNano(); - streamWaitTimeoutUnit = TimeUnit.NANOSECONDS; - } - // Note that if the stream-wait-timeout is zero, we won't set a timeout at all. - // That is consistent with ApiCallContext#withStreamWaitTimeout(Duration.ZERO). - } - } - - @Override - public void close(@Nullable String message) { - if (call != null) { - call.cancel(message); - } - } - - @Override - public boolean isWithBeginTransaction() { - return withBeginTransaction; - } - - @Override - protected final PartialResultSet computeNext() { - PartialResultSet next; - try { - if (streamWaitTimeoutUnit != null) { - next = stream.poll(streamWaitTimeoutValue, streamWaitTimeoutUnit); - if (next == null) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.DEADLINE_EXCEEDED, "stream wait timeout"); - } - } else { - next = stream.take(); - } - } catch (InterruptedException e) { - // Treat interrupt as a request to cancel the read. - throw SpannerExceptionFactory.propagateInterrupt(e); - } - if (next != END_OF_STREAM) { - call.request(1); - return next; - } - - // All done - close() no longer needs to cancel the call. - call = null; - - if (error != null) { - throw SpannerExceptionFactory.newSpannerException(error); - } - - endOfData(); - return null; - } - - private void addToStream(PartialResultSet results) { - // We assume that nothing from the user will interrupt gRPC event threads. - Uninterruptibles.putUninterruptibly(stream, results); - } - - private class ConsumerImpl implements SpannerRpc.ResultStreamConsumer { - @Override - public void onPartialResultSet(PartialResultSet results) { - addToStream(results); - } - - @Override - public void onCompleted() { - addToStream(END_OF_STREAM); - } - - @Override - public void onError(SpannerException e) { - if (statement != null) { - if (logger.isLoggable(Level.FINEST)) { - // Include parameter values if logging level is set to FINEST or higher. - e = - SpannerExceptionFactory.newSpannerExceptionPreformatted( - e.getErrorCode(), - String.format("%s - Statement: '%s'", e.getMessage(), statement.toString()), - e); - logger.log(Level.FINEST, "Error executing statement", e); - } else { - e = - SpannerExceptionFactory.newSpannerExceptionPreformatted( - e.getErrorCode(), - String.format("%s - Statement: '%s'", e.getMessage(), statement.getSql()), - e); - } - } - error = e; - addToStream(END_OF_STREAM); - } - } - } - - /** - * Wraps an iterator over partial result sets, supporting resuming RPCs on error. This class keeps - * track of the most recent resume token seen, and will buffer partial result set chunks that do - * not have a resume token until one is seen or buffer space is exceeded, which reduces the chance - * of yielding data to the caller that cannot be resumed. - */ - @VisibleForTesting - abstract static class ResumableStreamIterator extends AbstractIterator - implements CloseableIterator { - private static final RetrySettings DEFAULT_STREAMING_RETRY_SETTINGS = - SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetrySettings(); - private final RetrySettings streamingRetrySettings; - private final Set retryableCodes; - private static final Logger logger = Logger.getLogger(ResumableStreamIterator.class.getName()); - private final BackOff backOff; - private final LinkedList buffer = new LinkedList<>(); - private final int maxBufferSize; - private final ISpan span; - private final TraceWrapper tracer; - private CloseableIterator stream; - private ByteString resumeToken; - private boolean finished; - /** - * Indicates whether it is currently safe to retry RPCs. This will be {@code false} if we have - * reached the maximum buffer size without seeing a restart token; in this case, we will drain - * the buffer and remain in this state until we see a new restart token. - */ - private boolean safeToRetry = true; - - protected ResumableStreamIterator( - int maxBufferSize, - String streamName, - ISpan parent, - TraceWrapper tracer, - RetrySettings streamingRetrySettings, - Set retryableCodes) { - checkArgument(maxBufferSize >= 0); - this.maxBufferSize = maxBufferSize; - this.tracer = tracer; - this.span = tracer.spanBuilderWithExplicitParent(streamName, parent); - this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings); - this.retryableCodes = Preconditions.checkNotNull(retryableCodes); - this.backOff = newBackOff(); - } - - private ExponentialBackOff newBackOff() { - if (Objects.equals(streamingRetrySettings, DEFAULT_STREAMING_RETRY_SETTINGS)) { - return new ExponentialBackOff.Builder() - .setMultiplier(streamingRetrySettings.getRetryDelayMultiplier()) - .setInitialIntervalMillis( - Math.max(10, (int) streamingRetrySettings.getInitialRetryDelay().toMillis())) - .setMaxIntervalMillis( - Math.max(1000, (int) streamingRetrySettings.getMaxRetryDelay().toMillis())) - .setMaxElapsedTimeMillis( - Integer.MAX_VALUE) // Prevent Backoff.STOP from getting returned. - .build(); - } - return new ExponentialBackOff.Builder() - .setMultiplier(streamingRetrySettings.getRetryDelayMultiplier()) - // All of these values must be > 0. - .setInitialIntervalMillis( - Math.max( - 1, - (int) - Math.min( - streamingRetrySettings.getInitialRetryDelay().toMillis(), - Integer.MAX_VALUE))) - .setMaxIntervalMillis( - Math.max( - 1, - (int) - Math.min( - streamingRetrySettings.getMaxRetryDelay().toMillis(), Integer.MAX_VALUE))) - .setMaxElapsedTimeMillis( - Math.max( - 1, - (int) - Math.min( - streamingRetrySettings.getTotalTimeout().toMillis(), Integer.MAX_VALUE))) - .build(); - } - - private void backoffSleep(Context context, BackOff backoff) throws SpannerException { - backoffSleep(context, nextBackOffMillis(backoff)); - } - - private static long nextBackOffMillis(BackOff backoff) throws SpannerException { - try { - return backoff.nextBackOffMillis(); - } catch (IOException e) { - throw newSpannerException(ErrorCode.INTERNAL, e.getMessage(), e); - } - } - - private void backoffSleep(Context context, long backoffMillis) throws SpannerException { - tracer.getCurrentSpan().addAnnotation("Backing off", "Delay", backoffMillis); - final CountDownLatch latch = new CountDownLatch(1); - final Context.CancellationListener listener = - ignored -> { - // Wakeup on cancellation / DEADLINE_EXCEEDED. - latch.countDown(); - }; - - context.addListener(listener, DirectExecutor.INSTANCE); - try { - if (backoffMillis == BackOff.STOP) { - // Highly unlikely but we handle it just in case. - backoffMillis = streamingRetrySettings.getMaxRetryDelay().toMillis(); - } - if (latch.await(backoffMillis, TimeUnit.MILLISECONDS)) { - // Woken by context cancellation. - throw newSpannerExceptionForCancellation(context, null); - } - } catch (InterruptedException interruptExcept) { - throw newSpannerExceptionForCancellation(context, interruptExcept); - } finally { - context.removeListener(listener); - } - } - - private enum DirectExecutor implements Executor { - INSTANCE; - - @Override - public void execute(Runnable command) { - command.run(); - } - } - - abstract CloseableIterator startStream(@Nullable ByteString resumeToken); - - @Override - public void close(@Nullable String message) { - if (stream != null) { - stream.close(message); - span.end(); - stream = null; - } - } - - @Override - public boolean isWithBeginTransaction() { - return stream != null && stream.isWithBeginTransaction(); - } - - @Override - protected PartialResultSet computeNext() { - Context context = Context.current(); - while (true) { - // Eagerly start stream before consuming any buffered items. - if (stream == null) { - span.addAnnotation( - "Starting/Resuming stream", - "ResumeToken", - resumeToken == null ? "null" : resumeToken.toStringUtf8()); - try (IScope scope = tracer.withSpan(span)) { - // When start a new stream set the Span as current to make the gRPC Span a child of - // this Span. - stream = checkNotNull(startStream(resumeToken)); - } - } - // Buffer contains items up to a resume token or has reached capacity: flush. - if (!buffer.isEmpty() - && (finished || !safeToRetry || !buffer.getLast().getResumeToken().isEmpty())) { - return buffer.pop(); - } - try { - if (stream.hasNext()) { - PartialResultSet next = stream.next(); - boolean hasResumeToken = !next.getResumeToken().isEmpty(); - if (hasResumeToken) { - resumeToken = next.getResumeToken(); - safeToRetry = true; - } - // If the buffer is empty and this chunk has a resume token or we cannot resume safely - // anyway, we can yield it immediately rather than placing it in the buffer to be - // returned on the next iteration. - if ((hasResumeToken || !safeToRetry) && buffer.isEmpty()) { - return next; - } - buffer.add(next); - if (buffer.size() > maxBufferSize && buffer.getLast().getResumeToken().isEmpty()) { - // We need to flush without a restart token. Errors encountered until we see - // such a token will fail the read. - safeToRetry = false; - } - } else { - finished = true; - if (buffer.isEmpty()) { - endOfData(); - return null; - } - } - } catch (SpannerException spannerException) { - if (safeToRetry && isRetryable(spannerException)) { - span.addAnnotation("Stream broken. Safe to retry", spannerException); - logger.log(Level.FINE, "Retryable exception, will sleep and retry", spannerException); - // Truncate any items in the buffer before the last retry token. - while (!buffer.isEmpty() && buffer.getLast().getResumeToken().isEmpty()) { - buffer.removeLast(); - } - assert buffer.isEmpty() || buffer.getLast().getResumeToken().equals(resumeToken); - stream = null; - try (IScope s = tracer.withSpan(span)) { - long delay = spannerException.getRetryDelayInMillis(); - if (delay != -1) { - backoffSleep(context, delay); - } else { - backoffSleep(context, backOff); - } - } - - continue; - } - span.addAnnotation("Stream broken. Not safe to retry", spannerException); - span.setStatus(spannerException); - throw spannerException; - } catch (RuntimeException e) { - span.addAnnotation("Stream broken. Not safe to retry", e); - span.setStatus(e); - throw e; - } - } - } - - boolean isRetryable(SpannerException spannerException) { - return spannerException.isRetryable() - || retryableCodes.contains( - GrpcStatusCode.of(spannerException.getErrorCode().getGrpcStatusCode()).getCode()); - } - } - static double valueProtoToFloat64(com.google.protobuf.Value proto) { if (proto.getKindCase() == KindCase.STRING_VALUE) { switch (proto.getStringValue()) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java index 664cde1edbb..22fb9f710c1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java @@ -60,6 +60,7 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(TimestampBound bound) { sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId())) .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider()) .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()) + .setDefaultDecodeMode(sessionClient.getSpanner().getDefaultDecodeMode()) .setDefaultDirectedReadOptions( sessionClient.getSpanner().getOptions().getDirectedReadOptions()) .setSpan(sessionClient.getSpanner().getTracer().getCurrentSpan()) @@ -81,6 +82,7 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(BatchTransactionId batc sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId())) .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider()) .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()) + .setDefaultDecodeMode(sessionClient.getSpanner().getDefaultDecodeMode()) .setDefaultDirectedReadOptions( sessionClient.getSpanner().getOptions().getDirectedReadOptions()) .setSpan(sessionClient.getSpanner().getTracer().getCurrentSpan()) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DecodeMode.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DecodeMode.java new file mode 100644 index 00000000000..c1bea9a3ce1 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DecodeMode.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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; + +/** Specifies how and when to decode a value from protobuf to a plain Java object. */ +public enum DecodeMode { + /** + * Decodes all columns of a row directly when a {@link ResultSet} is advanced to the next row with + * {@link ResultSet#next()} + */ + DIRECT, + /** + * Decodes all columns of a row the first time a {@link ResultSet} value is retrieved from the + * row. + */ + LAZY_PER_ROW, + /** + * Decodes a columns of a row the first time the value of that column is retrieved from the row. + */ + LAZY_PER_COL, +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java index c29282879ed..18ecbeceb0f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java @@ -23,7 +23,7 @@ import com.google.spanner.v1.ResultSetStats; /** Forwarding implementation of ResultSet that forwards all calls to a delegate. */ -public class ForwardingResultSet extends ForwardingStructReader implements ResultSet { +public class ForwardingResultSet extends ForwardingStructReader implements ProtobufResultSet { private Supplier delegate; @@ -55,6 +55,22 @@ public boolean next() throws SpannerException { return delegate.get().next(); } + @Override + public boolean canGetProtobufValue(int columnIndex) { + ResultSet resultSetDelegate = delegate.get(); + return (resultSetDelegate instanceof ProtobufResultSet) + && ((ProtobufResultSet) resultSetDelegate).canGetProtobufValue(columnIndex); + } + + @Override + public com.google.protobuf.Value getProtobufValue(int columnIndex) { + ResultSet resultSetDelegate = delegate.get(); + Preconditions.checkState( + resultSetDelegate instanceof ProtobufResultSet, + "The result set does not support protobuf values"); + return ((ProtobufResultSet) resultSetDelegate).getProtobufValue(columnIndex); + } + @Override public Struct getCurrentRowAsStruct() { return delegate.get().getCurrentRowAsStruct(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java new file mode 100644 index 00000000000..37a4792ad87 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java @@ -0,0 +1,132 @@ +/* + * Copyright 2024 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; + +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.Value; +import com.google.spanner.v1.PartialResultSet; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.ResultSetStats; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +@VisibleForTesting +class GrpcResultSet extends AbstractResultSet> implements ProtobufResultSet { + private final GrpcValueIterator iterator; + private final Listener listener; + private final DecodeMode decodeMode; + private ResultSetMetadata metadata; + private GrpcStruct currRow; + private SpannerException error; + private ResultSetStats statistics; + private boolean closed; + + GrpcResultSet(CloseableIterator iterator, Listener listener) { + this(iterator, listener, DecodeMode.DIRECT); + } + + GrpcResultSet( + CloseableIterator iterator, Listener listener, DecodeMode decodeMode) { + this.iterator = new GrpcValueIterator(iterator); + this.listener = listener; + this.decodeMode = decodeMode; + } + + @Override + public boolean canGetProtobufValue(int columnIndex) { + return !closed && currRow != null && currRow.canGetProtoValue(columnIndex); + } + + @Override + public Value getProtobufValue(int columnIndex) { + checkState(!closed, "ResultSet is closed"); + checkState(currRow != null, "next() call required"); + return currRow.getProtoValueInternal(columnIndex); + } + + @Override + protected GrpcStruct currRow() { + checkState(!closed, "ResultSet is closed"); + checkState(currRow != null, "next() call required"); + return currRow; + } + + @Override + public boolean next() throws SpannerException { + if (error != null) { + throw newSpannerException(error); + } + try { + if (currRow == null) { + metadata = iterator.getMetadata(); + if (metadata.hasTransaction()) { + listener.onTransactionMetadata( + metadata.getTransaction(), iterator.isWithBeginTransaction()); + } else if (iterator.isWithBeginTransaction()) { + // The query should have returned a transaction. + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, AbstractReadContext.NO_TRANSACTION_RETURNED_MSG); + } + currRow = new GrpcStruct(iterator.type(), new ArrayList<>(), decodeMode); + } + boolean hasNext = currRow.consumeRow(iterator); + if (!hasNext) { + statistics = iterator.getStats(); + } + return hasNext; + } catch (Throwable t) { + throw yieldError( + SpannerExceptionFactory.asSpannerException(t), + iterator.isWithBeginTransaction() && currRow == null); + } + } + + @Override + @Nullable + public ResultSetStats getStats() { + return statistics; + } + + @Override + public ResultSetMetadata getMetadata() { + checkState(metadata != null, "next() call required"); + return metadata; + } + + @Override + public void close() { + listener.onDone(iterator.isWithBeginTransaction()); + iterator.close("ResultSet closed"); + closed = true; + } + + @Override + public Type getType() { + checkState(currRow != null, "next() call required"); + return currRow.getType(); + } + + private SpannerException yieldError(SpannerException e, boolean beginTransaction) { + SpannerException toThrow = listener.onError(e, beginTransaction); + close(); + throw toThrow; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java new file mode 100644 index 00000000000..dde6b69c461 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024 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; + +import com.google.api.gax.rpc.ApiCallContext; +import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; +import com.google.cloud.spanner.spi.v1.SpannerRpc; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.AbstractIterator; +import com.google.common.util.concurrent.Uninterruptibles; +import com.google.spanner.v1.PartialResultSet; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import org.threeten.bp.Duration; + +/** Adapts a streaming read/query call into an iterator over partial result sets. */ +@VisibleForTesting +class GrpcStreamIterator extends AbstractIterator + implements CloseableIterator { + private static final Logger logger = Logger.getLogger(GrpcStreamIterator.class.getName()); + private static final PartialResultSet END_OF_STREAM = PartialResultSet.newBuilder().build(); + + private final ConsumerImpl consumer = new ConsumerImpl(); + private final BlockingQueue stream; + private final Statement statement; + + private SpannerRpc.StreamingCall call; + private volatile boolean withBeginTransaction; + private TimeUnit streamWaitTimeoutUnit; + private long streamWaitTimeoutValue; + private SpannerException error; + + @VisibleForTesting + GrpcStreamIterator(int prefetchChunks) { + this(null, prefetchChunks); + } + + @VisibleForTesting + GrpcStreamIterator(Statement statement, int prefetchChunks) { + this.statement = statement; + // One extra to allow for END_OF_STREAM message. + this.stream = new LinkedBlockingQueue<>(prefetchChunks + 1); + } + + protected final SpannerRpc.ResultStreamConsumer consumer() { + return consumer; + } + + public void setCall(SpannerRpc.StreamingCall call, boolean withBeginTransaction) { + this.call = call; + this.withBeginTransaction = withBeginTransaction; + ApiCallContext callContext = call.getCallContext(); + Duration streamWaitTimeout = callContext == null ? null : callContext.getStreamWaitTimeout(); + if (streamWaitTimeout != null) { + // Determine the timeout unit to use. This reduces the precision to seconds if the timeout + // value is more than 1 second, which is lower than the precision that would normally be + // used by the stream watchdog (which uses a precision of 10 seconds by default). + if (streamWaitTimeout.getSeconds() > 0L) { + streamWaitTimeoutValue = streamWaitTimeout.getSeconds(); + streamWaitTimeoutUnit = TimeUnit.SECONDS; + } else if (streamWaitTimeout.getNano() > 0) { + streamWaitTimeoutValue = streamWaitTimeout.getNano(); + streamWaitTimeoutUnit = TimeUnit.NANOSECONDS; + } + // Note that if the stream-wait-timeout is zero, we won't set a timeout at all. + // That is consistent with ApiCallContext#withStreamWaitTimeout(Duration.ZERO). + } + } + + @Override + public void close(@Nullable String message) { + if (call != null) { + call.cancel(message); + } + } + + @Override + public boolean isWithBeginTransaction() { + return withBeginTransaction; + } + + @Override + protected final PartialResultSet computeNext() { + PartialResultSet next; + try { + if (streamWaitTimeoutUnit != null) { + next = stream.poll(streamWaitTimeoutValue, streamWaitTimeoutUnit); + if (next == null) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.DEADLINE_EXCEEDED, "stream wait timeout"); + } + } else { + next = stream.take(); + } + } catch (InterruptedException e) { + // Treat interrupt as a request to cancel the read. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + if (next != END_OF_STREAM) { + call.request(1); + return next; + } + + // All done - close() no longer needs to cancel the call. + call = null; + + if (error != null) { + throw SpannerExceptionFactory.newSpannerException(error); + } + + endOfData(); + return null; + } + + private void addToStream(PartialResultSet results) { + // We assume that nothing from the user will interrupt gRPC event threads. + Uninterruptibles.putUninterruptibly(stream, results); + } + + private class ConsumerImpl implements SpannerRpc.ResultStreamConsumer { + @Override + public void onPartialResultSet(PartialResultSet results) { + addToStream(results); + } + + @Override + public void onCompleted() { + addToStream(END_OF_STREAM); + } + + @Override + public void onError(SpannerException e) { + if (statement != null) { + if (logger.isLoggable(Level.FINEST)) { + // Include parameter values if logging level is set to FINEST or higher. + e = + SpannerExceptionFactory.newSpannerExceptionPreformatted( + e.getErrorCode(), + String.format("%s - Statement: '%s'", e.getMessage(), statement.toString()), + e); + logger.log(Level.FINEST, "Error executing statement", e); + } else { + e = + SpannerExceptionFactory.newSpannerExceptionPreformatted( + e.getErrorCode(), + String.format("%s - Statement: '%s'", e.getMessage(), statement.getSql()), + e); + } + } + error = e; + addToStream(END_OF_STREAM); + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java new file mode 100644 index 00000000000..e4951d7bee4 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStruct.java @@ -0,0 +1,766 @@ +/* + * Copyright 2024 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; + +import static com.google.cloud.spanner.AbstractResultSet.throwNotNull; +import static com.google.cloud.spanner.AbstractResultSet.valueProtoToFloat64; +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbstractResultSet.Float64Array; +import com.google.cloud.spanner.AbstractResultSet.Int64Array; +import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; +import com.google.cloud.spanner.Type.Code; +import com.google.cloud.spanner.Type.StructField; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.io.CharSource; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.ProtocolMessageEnum; +import com.google.protobuf.Value.KindCase; +import java.io.IOException; +import java.io.Serializable; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.BitSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +class GrpcStruct extends Struct implements Serializable { + private static final com.google.protobuf.Value NULL_VALUE = + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + + private final Type type; + private final List rowData; + private final DecodeMode decodeMode; + private final BitSet colDecoded; + private boolean rowDecoded; + + /** + * Builds an immutable version of this struct using {@link Struct#newBuilder()} which is used as a + * serialization proxy. + */ + private Object writeReplace() { + Builder builder = Struct.newBuilder(); + List structFields = getType().getStructFields(); + for (int i = 0; i < structFields.size(); i++) { + Type.StructField field = structFields.get(i); + String fieldName = field.getName(); + Object value = rowData.get(i); + Type fieldType = field.getType(); + switch (fieldType.getCode()) { + case BOOL: + builder.set(fieldName).to((Boolean) value); + break; + case INT64: + builder.set(fieldName).to((Long) value); + break; + case FLOAT64: + builder.set(fieldName).to((Double) value); + break; + case NUMERIC: + builder.set(fieldName).to((BigDecimal) value); + break; + case PG_NUMERIC: + builder.set(fieldName).to((String) value); + break; + case STRING: + builder.set(fieldName).to((String) value); + break; + case JSON: + builder.set(fieldName).to(Value.json((String) value)); + break; + case PROTO: + builder + .set(fieldName) + .to( + Value.protoMessage( + value == null ? null : ((LazyByteArray) value).getByteArray(), + fieldType.getProtoTypeFqn())); + break; + case ENUM: + builder.set(fieldName).to(Value.protoEnum((Long) value, fieldType.getProtoTypeFqn())); + break; + case PG_JSONB: + builder.set(fieldName).to(Value.pgJsonb((String) value)); + break; + case BYTES: + builder + .set(fieldName) + .to( + Value.bytesFromBase64( + value == null ? null : ((LazyByteArray) value).getBase64String())); + break; + case TIMESTAMP: + builder.set(fieldName).to((Timestamp) value); + break; + case DATE: + builder.set(fieldName).to((Date) value); + break; + case ARRAY: + final Type elementType = fieldType.getArrayElementType(); + switch (elementType.getCode()) { + case BOOL: + builder.set(fieldName).toBoolArray((Iterable) value); + break; + case INT64: + case ENUM: + builder.set(fieldName).toInt64Array((Iterable) value); + break; + case FLOAT64: + builder.set(fieldName).toFloat64Array((Iterable) value); + break; + case NUMERIC: + builder.set(fieldName).toNumericArray((Iterable) value); + break; + case PG_NUMERIC: + builder.set(fieldName).toPgNumericArray((Iterable) value); + break; + case STRING: + builder.set(fieldName).toStringArray((Iterable) value); + break; + case JSON: + builder.set(fieldName).toJsonArray((Iterable) value); + break; + case PG_JSONB: + builder.set(fieldName).toPgJsonbArray((Iterable) value); + break; + case BYTES: + case PROTO: + builder + .set(fieldName) + .toBytesArrayFromBase64( + value == null + ? null + : ((List) value) + .stream() + .map( + element -> element == null ? null : element.getBase64String()) + .collect(Collectors.toList())); + break; + case TIMESTAMP: + builder.set(fieldName).toTimestampArray((Iterable) value); + break; + case DATE: + builder.set(fieldName).toDateArray((Iterable) value); + break; + case STRUCT: + builder.set(fieldName).toStructArray(elementType, (Iterable) value); + break; + default: + throw new AssertionError("Unhandled array type code: " + elementType); + } + break; + case STRUCT: + if (value == null) { + builder.set(fieldName).to(fieldType, null); + } else { + builder.set(fieldName).to((Struct) value); + } + break; + default: + throw new AssertionError("Unhandled type code: " + fieldType.getCode()); + } + } + return builder.build(); + } + + GrpcStruct(Type type, List rowData, DecodeMode decodeMode) { + this( + type, + rowData, + decodeMode, + /* rowDecoded = */ false, + /* colDecoded = */ decodeMode == DecodeMode.LAZY_PER_COL + ? new BitSet(type.getStructFields().size()) + : null); + } + + private GrpcStruct( + Type type, + List rowData, + DecodeMode decodeMode, + boolean rowDecoded, + BitSet colDecoded) { + this.type = type; + this.rowData = rowData; + this.decodeMode = decodeMode; + this.rowDecoded = rowDecoded; + this.colDecoded = colDecoded; + } + + @Override + public String toString() { + return this.rowData.toString(); + } + + boolean consumeRow(Iterator iterator) { + rowData.clear(); + if (decodeMode == DecodeMode.LAZY_PER_ROW) { + rowDecoded = false; + } else if (decodeMode == DecodeMode.LAZY_PER_COL) { + colDecoded.clear(); + } + if (!iterator.hasNext()) { + return false; + } + for (Type.StructField fieldType : getType().getStructFields()) { + if (!iterator.hasNext()) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Invalid value stream: end of stream reached before row is complete"); + } + com.google.protobuf.Value value = iterator.next(); + if (decodeMode == DecodeMode.DIRECT) { + rowData.add(decodeValue(fieldType.getType(), value)); + } else { + rowData.add(value); + } + } + return true; + } + + private static Object decodeValue(Type fieldType, com.google.protobuf.Value proto) { + if (proto.getKindCase() == KindCase.NULL_VALUE) { + return null; + } + switch (fieldType.getCode()) { + case BOOL: + checkType(fieldType, proto, KindCase.BOOL_VALUE); + return proto.getBoolValue(); + case INT64: + case ENUM: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return Long.parseLong(proto.getStringValue()); + case FLOAT64: + return valueProtoToFloat64(proto); + case NUMERIC: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return new BigDecimal(proto.getStringValue()); + case PG_NUMERIC: + case STRING: + case JSON: + case PG_JSONB: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return proto.getStringValue(); + case BYTES: + case PROTO: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return new LazyByteArray(proto.getStringValue()); + case TIMESTAMP: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return Timestamp.parseTimestamp(proto.getStringValue()); + case DATE: + checkType(fieldType, proto, KindCase.STRING_VALUE); + return Date.parseDate(proto.getStringValue()); + case ARRAY: + checkType(fieldType, proto, KindCase.LIST_VALUE); + ListValue listValue = proto.getListValue(); + return decodeArrayValue(fieldType.getArrayElementType(), listValue); + case STRUCT: + checkType(fieldType, proto, KindCase.LIST_VALUE); + ListValue structValue = proto.getListValue(); + return decodeStructValue(fieldType, structValue); + case UNRECOGNIZED: + return proto; + default: + throw new AssertionError("Unhandled type code: " + fieldType.getCode()); + } + } + + private static Struct decodeStructValue(Type structType, ListValue structValue) { + List fieldTypes = structType.getStructFields(); + checkArgument( + structValue.getValuesCount() == fieldTypes.size(), + "Size mismatch between type descriptor and actual values."); + List fields = new ArrayList<>(fieldTypes.size()); + List fieldValues = structValue.getValuesList(); + for (int i = 0; i < fieldTypes.size(); ++i) { + fields.add(decodeValue(fieldTypes.get(i).getType(), fieldValues.get(i))); + } + return new GrpcStruct(structType, fields, DecodeMode.DIRECT); + } + + static Object decodeArrayValue(Type elementType, ListValue listValue) { + switch (elementType.getCode()) { + case INT64: + case ENUM: + // For int64/float64/enum types, use custom containers. These avoid wrapper object + // creation for non-null arrays. + return new Int64Array(listValue); + case FLOAT64: + return new Float64Array(listValue); + case BOOL: + case NUMERIC: + case PG_NUMERIC: + case STRING: + case JSON: + case PG_JSONB: + case BYTES: + case TIMESTAMP: + case DATE: + case STRUCT: + case PROTO: + return Lists.transform(listValue.getValuesList(), input -> decodeValue(elementType, input)); + default: + throw new AssertionError("Unhandled type code: " + elementType.getCode()); + } + } + + private static void checkType( + Type fieldType, com.google.protobuf.Value proto, KindCase expected) { + if (proto.getKindCase() != expected) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Invalid value for column type " + + fieldType + + " expected " + + expected + + " but was " + + proto.getKindCase()); + } + } + + Struct immutableCopy() { + return new GrpcStruct( + type, + new ArrayList<>(rowData), + this.decodeMode, + this.rowDecoded, + this.colDecoded == null ? null : (BitSet) this.colDecoded.clone()); + } + + @Override + public Type getType() { + return type; + } + + @Override + public boolean isNull(int columnIndex) { + if ((decodeMode == DecodeMode.LAZY_PER_ROW && !rowDecoded) + || (decodeMode == DecodeMode.LAZY_PER_COL && !colDecoded.get(columnIndex))) { + return ((com.google.protobuf.Value) rowData.get(columnIndex)).hasNullValue(); + } + return rowData.get(columnIndex) == null; + } + + @Override + protected T getProtoMessageInternal(int columnIndex, T message) { + Preconditions.checkNotNull( + message, + "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); + try { + return (T) + message + .toBuilder() + .mergeFrom( + Base64.getDecoder() + .wrap( + CharSource.wrap( + ((LazyByteArray) rowData.get(columnIndex)).getBase64String()) + .asByteSource(StandardCharsets.UTF_8) + .openStream())) + .build(); + } catch (IOException ioException) { + throw SpannerExceptionFactory.asSpannerException(ioException); + } + } + + @Override + protected T getProtoEnumInternal( + int columnIndex, Function method) { + Preconditions.checkNotNull( + method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); + return (T) method.apply((int) getLongInternal(columnIndex)); + } + + @Override + protected boolean getBooleanInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Boolean) rowData.get(columnIndex); + } + + @Override + protected long getLongInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Long) rowData.get(columnIndex); + } + + @Override + protected double getDoubleInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Double) rowData.get(columnIndex); + } + + @Override + protected BigDecimal getBigDecimalInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (BigDecimal) rowData.get(columnIndex); + } + + @Override + protected String getStringInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (String) rowData.get(columnIndex); + } + + @Override + protected String getJsonInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (String) rowData.get(columnIndex); + } + + @Override + protected String getPgJsonbInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (String) rowData.get(columnIndex); + } + + @Override + protected ByteArray getBytesInternal(int columnIndex) { + ensureDecoded(columnIndex); + return getLazyBytesInternal(columnIndex).getByteArray(); + } + + LazyByteArray getLazyBytesInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (LazyByteArray) rowData.get(columnIndex); + } + + @Override + protected Timestamp getTimestampInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Timestamp) rowData.get(columnIndex); + } + + @Override + protected Date getDateInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Date) rowData.get(columnIndex); + } + + private boolean isUnrecognizedType(int columnIndex) { + return type.getStructFields().get(columnIndex).getType().getCode() == Code.UNRECOGNIZED; + } + + boolean canGetProtoValue(int columnIndex) { + return isUnrecognizedType(columnIndex) + || (decodeMode == DecodeMode.LAZY_PER_ROW && !rowDecoded) + || (decodeMode == DecodeMode.LAZY_PER_COL && !colDecoded.get(columnIndex)); + } + + protected com.google.protobuf.Value getProtoValueInternal(int columnIndex) { + checkProtoValueSupported(columnIndex); + return (com.google.protobuf.Value) rowData.get(columnIndex); + } + + private void checkProtoValueSupported(int columnIndex) { + // Unrecognized types are returned as protobuf values. + if (isUnrecognizedType(columnIndex)) { + return; + } + Preconditions.checkState( + decodeMode != DecodeMode.DIRECT, + "Getting proto value is not supported when DecodeMode#DIRECT is used."); + Preconditions.checkState( + !(decodeMode == DecodeMode.LAZY_PER_ROW && rowDecoded), + "Getting proto value after the row has been decoded is not supported."); + Preconditions.checkState( + !(decodeMode == DecodeMode.LAZY_PER_COL && colDecoded.get(columnIndex)), + "Getting proto value after the column has been decoded is not supported."); + } + + private void ensureDecoded(int columnIndex) { + if (decodeMode == DecodeMode.LAZY_PER_ROW && !rowDecoded) { + for (int i = 0; i < rowData.size(); i++) { + rowData.set( + i, + decodeValue( + type.getStructFields().get(i).getType(), + (com.google.protobuf.Value) rowData.get(i))); + } + rowDecoded = true; + } else if (decodeMode == DecodeMode.LAZY_PER_COL && !colDecoded.get(columnIndex)) { + rowData.set( + columnIndex, + decodeValue( + type.getStructFields().get(columnIndex).getType(), + (com.google.protobuf.Value) rowData.get(columnIndex))); + colDecoded.set(columnIndex); + } + } + + @Override + protected Value getValueInternal(int columnIndex) { + ensureDecoded(columnIndex); + final List structFields = getType().getStructFields(); + final StructField structField = structFields.get(columnIndex); + final Type columnType = structField.getType(); + final boolean isNull = rowData.get(columnIndex) == null; + switch (columnType.getCode()) { + case BOOL: + return Value.bool(isNull ? null : getBooleanInternal(columnIndex)); + case INT64: + return Value.int64(isNull ? null : getLongInternal(columnIndex)); + case ENUM: + return Value.protoEnum( + isNull ? null : getLongInternal(columnIndex), columnType.getProtoTypeFqn()); + case NUMERIC: + return Value.numeric(isNull ? null : getBigDecimalInternal(columnIndex)); + case PG_NUMERIC: + return Value.pgNumeric(isNull ? null : getStringInternal(columnIndex)); + case FLOAT64: + return Value.float64(isNull ? null : getDoubleInternal(columnIndex)); + case STRING: + return Value.string(isNull ? null : getStringInternal(columnIndex)); + case JSON: + return Value.json(isNull ? null : getJsonInternal(columnIndex)); + case PG_JSONB: + return Value.pgJsonb(isNull ? null : getPgJsonbInternal(columnIndex)); + case BYTES: + return Value.internalBytes(isNull ? null : getLazyBytesInternal(columnIndex)); + case PROTO: + return Value.protoMessage( + isNull ? null : getBytesInternal(columnIndex), columnType.getProtoTypeFqn()); + case TIMESTAMP: + return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); + case DATE: + return Value.date(isNull ? null : getDateInternal(columnIndex)); + case STRUCT: + return Value.struct(isNull ? null : getStructInternal(columnIndex)); + case UNRECOGNIZED: + return Value.unrecognized( + isNull ? NULL_VALUE : getProtoValueInternal(columnIndex), columnType); + case ARRAY: + final Type elementType = columnType.getArrayElementType(); + switch (elementType.getCode()) { + case BOOL: + return Value.boolArray(isNull ? null : getBooleanListInternal(columnIndex)); + case INT64: + return Value.int64Array(isNull ? null : getLongListInternal(columnIndex)); + case NUMERIC: + return Value.numericArray(isNull ? null : getBigDecimalListInternal(columnIndex)); + case PG_NUMERIC: + return Value.pgNumericArray(isNull ? null : getStringListInternal(columnIndex)); + case FLOAT64: + return Value.float64Array(isNull ? null : getDoubleListInternal(columnIndex)); + case STRING: + return Value.stringArray(isNull ? null : getStringListInternal(columnIndex)); + case JSON: + return Value.jsonArray(isNull ? null : getJsonListInternal(columnIndex)); + case PG_JSONB: + return Value.pgJsonbArray(isNull ? null : getPgJsonbListInternal(columnIndex)); + case BYTES: + return Value.bytesArray(isNull ? null : getBytesListInternal(columnIndex)); + case PROTO: + return Value.protoMessageArray( + isNull ? null : getBytesListInternal(columnIndex), elementType.getProtoTypeFqn()); + case ENUM: + return Value.protoEnumArray( + isNull ? null : getLongListInternal(columnIndex), elementType.getProtoTypeFqn()); + case TIMESTAMP: + return Value.timestampArray(isNull ? null : getTimestampListInternal(columnIndex)); + case DATE: + return Value.dateArray(isNull ? null : getDateListInternal(columnIndex)); + case STRUCT: + return Value.structArray( + elementType, isNull ? null : getStructListInternal(columnIndex)); + default: + throw new IllegalArgumentException( + "Invalid array value type " + this.type.getArrayElementType()); + } + default: + throw new IllegalArgumentException("Invalid value type " + this.type); + } + } + + @Override + protected Struct getStructInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Struct) rowData.get(columnIndex); + } + + @Override + protected boolean[] getBooleanArrayInternal(int columnIndex) { + ensureDecoded(columnIndex); + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + List values = (List) rowData.get(columnIndex); + boolean[] r = new boolean[values.size()]; + for (int i = 0; i < values.size(); ++i) { + if (values.get(i) == null) { + throw throwNotNull(columnIndex); + } + r[i] = values.get(i); + } + return r; + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBooleanListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + protected long[] getLongArrayInternal(int columnIndex) { + ensureDecoded(columnIndex); + return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + protected Int64Array getLongListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Int64Array) rowData.get(columnIndex); + } + + @Override + protected double[] getDoubleArrayInternal(int columnIndex) { + ensureDecoded(columnIndex); + return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + protected Float64Array getDoubleListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (Float64Array) rowData.get(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBigDecimalListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return (List) rowData.get(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getStringListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getJsonListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getProtoMessageListInternal( + int columnIndex, T message) { + Preconditions.checkNotNull( + message, + "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); + ensureDecoded(columnIndex); + + List bytesArray = (List) rowData.get(columnIndex); + + try { + List protoMessagesList = new ArrayList<>(bytesArray.size()); + for (LazyByteArray protoMessageBytes : bytesArray) { + if (protoMessageBytes == null) { + protoMessagesList.add(null); + } else { + protoMessagesList.add( + (T) + message + .toBuilder() + .mergeFrom( + Base64.getDecoder() + .wrap( + CharSource.wrap(protoMessageBytes.getBase64String()) + .asByteSource(StandardCharsets.UTF_8) + .openStream())) + .build()); + } + } + return protoMessagesList; + } catch (IOException ioException) { + throw SpannerExceptionFactory.asSpannerException(ioException); + } + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getProtoEnumListInternal( + int columnIndex, Function method) { + Preconditions.checkNotNull( + method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); + ensureDecoded(columnIndex); + + List enumIntArray = (List) rowData.get(columnIndex); + List protoEnumList = new ArrayList<>(enumIntArray.size()); + for (Long enumIntValue : enumIntArray) { + if (enumIntValue == null) { + protoEnumList.add(null); + } else { + protoEnumList.add((T) method.apply(enumIntValue.intValue())); + } + } + + return protoEnumList; + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getPgJsonbListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBytesListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Lists.transform( + (List) rowData.get(columnIndex), l -> l == null ? null : l.getByteArray()); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getTimestampListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getDateListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY> produces a List. + protected List getStructListInternal(int columnIndex) { + ensureDecoded(columnIndex); + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcValueIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcValueIterator.java new file mode 100644 index 00000000000..0a2e17bd2b5 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcValueIterator.java @@ -0,0 +1,212 @@ +/* + * Copyright 2024 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; + +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.common.base.Preconditions.checkState; + +import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; +import com.google.common.collect.AbstractIterator; +import com.google.protobuf.ListValue; +import com.google.protobuf.Value.KindCase; +import com.google.spanner.v1.PartialResultSet; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.ResultSetStats; +import com.google.spanner.v1.TypeCode; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +/** Adapts a stream of {@code PartialResultSet} messages into a stream of {@code Value} messages. */ +class GrpcValueIterator extends AbstractIterator { + private enum StreamValue { + METADATA, + RESULT, + } + + private final CloseableIterator stream; + private ResultSetMetadata metadata; + private Type type; + private PartialResultSet current; + private int pos; + private ResultSetStats statistics; + + GrpcValueIterator(CloseableIterator stream) { + this.stream = stream; + } + + @SuppressWarnings("unchecked") + @Override + protected com.google.protobuf.Value computeNext() { + if (!ensureReady(StreamValue.RESULT)) { + endOfData(); + return null; + } + com.google.protobuf.Value value = current.getValues(pos++); + KindCase kind = value.getKindCase(); + + if (!isMergeable(kind)) { + if (pos == current.getValuesCount() && current.getChunkedValue()) { + throw newSpannerException(ErrorCode.INTERNAL, "Unexpected chunked PartialResultSet."); + } else { + return value; + } + } + if (!current.getChunkedValue() || pos != current.getValuesCount()) { + return value; + } + + Object merged = + kind == KindCase.STRING_VALUE + ? value.getStringValue() + : new ArrayList<>(value.getListValue().getValuesList()); + while (current.getChunkedValue() && pos == current.getValuesCount()) { + if (!ensureReady(StreamValue.RESULT)) { + throw newSpannerException( + ErrorCode.INTERNAL, "Stream closed in the middle of chunked value"); + } + com.google.protobuf.Value newValue = current.getValues(pos++); + if (newValue.getKindCase() != kind) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Unexpected type in middle of chunked value. Expected: " + + kind + + " but got: " + + newValue.getKindCase()); + } + if (kind == KindCase.STRING_VALUE) { + merged = merged + newValue.getStringValue(); + } else { + concatLists( + (List) merged, newValue.getListValue().getValuesList()); + } + } + if (kind == KindCase.STRING_VALUE) { + return com.google.protobuf.Value.newBuilder().setStringValue((String) merged).build(); + } else { + return com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder().addAllValues((List) merged)) + .build(); + } + } + + ResultSetMetadata getMetadata() throws SpannerException { + if (metadata == null) { + if (!ensureReady(StreamValue.METADATA)) { + throw newSpannerException(ErrorCode.INTERNAL, "Stream closed without sending metadata"); + } + } + return metadata; + } + + /** + * Get the query statistics. Query statistics are delivered with the last PartialResultSet in the + * stream. Any attempt to call this method before the caller has finished consuming the results + * will return null. + */ + @Nullable + ResultSetStats getStats() { + return statistics; + } + + Type type() { + checkState(type != null, "metadata has not been received"); + return type; + } + + private boolean ensureReady(StreamValue requiredValue) throws SpannerException { + while (current == null || pos >= current.getValuesCount()) { + if (!stream.hasNext()) { + return false; + } + current = stream.next(); + pos = 0; + if (type == null) { + // This is the first message on the stream. + if (!current.hasMetadata() || !current.getMetadata().hasRowType()) { + throw newSpannerException(ErrorCode.INTERNAL, "Missing type metadata in first message"); + } + metadata = current.getMetadata(); + com.google.spanner.v1.Type typeProto = + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.STRUCT) + .setStructType(metadata.getRowType()) + .build(); + try { + type = Type.fromProto(typeProto); + } catch (IllegalArgumentException e) { + throw newSpannerException( + ErrorCode.INTERNAL, "Invalid type metadata: " + e.getMessage(), e); + } + } + if (current.hasStats()) { + statistics = current.getStats(); + } + if (requiredValue == StreamValue.METADATA) { + return true; + } + } + return true; + } + + void close(@Nullable String message) { + stream.close(message); + } + + boolean isWithBeginTransaction() { + return stream.isWithBeginTransaction(); + } + + /** @param a is a mutable list and b will be concatenated into a. */ + private void concatLists(List a, List b) { + if (a.size() == 0 || b.size() == 0) { + a.addAll(b); + return; + } else { + com.google.protobuf.Value last = a.get(a.size() - 1); + com.google.protobuf.Value first = b.get(0); + KindCase lastKind = last.getKindCase(); + KindCase firstKind = first.getKindCase(); + if (isMergeable(lastKind) && lastKind == firstKind) { + com.google.protobuf.Value merged; + if (lastKind == KindCase.STRING_VALUE) { + String lastStr = last.getStringValue(); + String firstStr = first.getStringValue(); + merged = + com.google.protobuf.Value.newBuilder().setStringValue(lastStr + firstStr).build(); + } else { // List + List mergedList = new ArrayList<>(); + mergedList.addAll(last.getListValue().getValuesList()); + concatLists(mergedList, first.getListValue().getValuesList()); + merged = + com.google.protobuf.Value.newBuilder() + .setListValue(ListValue.newBuilder().addAllValues(mergedList)) + .build(); + } + a.set(a.size() - 1, merged); + a.addAll(b.subList(1, b.size())); + } else { + a.addAll(b); + } + } + } + + private boolean isMergeable(KindCase kind) { + return kind == KindCase.STRING_VALUE || kind == KindCase.LIST_VALUE; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 57feabbfcca..76d0f24225a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -243,6 +243,10 @@ public static ReadAndQueryOption directedRead(DirectedReadOptions directedReadOp return new DirectedReadOption(directedReadOptions); } + public static ReadAndQueryOption decodeMode(DecodeMode decodeMode) { + return new DecodeOption(decodeMode); + } + /** Option to request {@link CommitStats} for read/write transactions. */ static final class CommitStatsOption extends InternalOption implements TransactionOption { @Override @@ -374,6 +378,19 @@ void appendToOptions(Options options) { } } + static final class DecodeOption extends InternalOption implements ReadAndQueryOption { + private final DecodeMode decodeMode; + + DecodeOption(DecodeMode decodeMode) { + this.decodeMode = Preconditions.checkNotNull(decodeMode, "DecodeMode cannot be null"); + } + + @Override + void appendToOptions(Options options) { + options.decodeMode = decodeMode; + } + } + private boolean withCommitStats; private Duration maxCommitDelay; @@ -391,6 +408,7 @@ void appendToOptions(Options options) { private Boolean withOptimisticLock; private Boolean dataBoostEnabled; private DirectedReadOptions directedReadOptions; + private DecodeMode decodeMode; // Construction is via factory methods below. private Options() {} @@ -507,6 +525,14 @@ DirectedReadOptions directedReadOptions() { return directedReadOptions; } + boolean hasDecodeMode() { + return decodeMode != null; + } + + DecodeMode decodeMode() { + return decodeMode; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -552,6 +578,9 @@ public String toString() { if (directedReadOptions != null) { b.append("directedReadOptions: ").append(directedReadOptions).append(' '); } + if (decodeMode != null) { + b.append("decodeMode: ").append(decodeMode).append(' '); + } return b.toString(); } @@ -640,6 +669,9 @@ public int hashCode() { if (directedReadOptions != null) { result = 31 * result + directedReadOptions.hashCode(); } + if (decodeMode != null) { + result = 31 * result + decodeMode.hashCode(); + } return result; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ProtobufResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ProtobufResultSet.java new file mode 100644 index 00000000000..bbd8c41291f --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ProtobufResultSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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; + +import com.google.api.core.InternalApi; +import com.google.protobuf.Value; + +/** Interface for {@link ResultSet}s that can return a protobuf value. */ +@InternalApi +public interface ProtobufResultSet extends ResultSet { + + /** Returns true if the protobuf value for the given column is still available. */ + boolean canGetProtobufValue(int columnIndex); + + /** + * Returns the column value as a protobuf value. + * + *

This is an internal method not intended for external usage. + * + *

This method may only be called before the column value has been decoded to a plain Java + * object. This means that the {@link DecodeMode} that is used for the {@link ResultSet} must be + * one of {@link DecodeMode#LAZY_PER_ROW} and {@link DecodeMode#LAZY_PER_COL}, and that the + * corresponding {@link ResultSet#getValue(int)}, {@link ResultSet#getBoolean(int)}, ... method + * may not yet have been called for the column. + */ + @InternalApi + Value getProtobufValue(int columnIndex); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index d55d4091b9f..a6cc7c729e5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -109,7 +109,7 @@ public ResultSet get() { } } - private static class PrePopulatedResultSet implements ResultSet { + private static class PrePopulatedResultSet implements ProtobufResultSet { private final List rows; private final Type type; private int index = -1; @@ -137,6 +137,19 @@ public boolean next() throws SpannerException { return ++index < rows.size(); } + @Override + public boolean canGetProtobufValue(int columnIndex) { + return !closed && index >= 0 && index < rows.size(); + } + + @Override + public com.google.protobuf.Value getProtobufValue(int columnIndex) { + Preconditions.checkState(!closed, "ResultSet is closed"); + Preconditions.checkState(index >= 0, "Must be preceded by a next() call"); + Preconditions.checkElementIndex(index, rows.size(), "All rows have been yielded"); + return getValue(columnIndex).toProto(); + } + @Override public Struct getCurrentRowAsStruct() { Preconditions.checkState(!closed, "ResultSet is closed"); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java new file mode 100644 index 00000000000..590797c0999 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java @@ -0,0 +1,277 @@ +/* + * Copyright 2024 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; + +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerExceptionForCancellation; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.util.BackOff; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; +import com.google.cloud.spanner.v1.stub.SpannerStubSettings; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.AbstractIterator; +import com.google.protobuf.ByteString; +import com.google.spanner.v1.PartialResultSet; +import io.grpc.Context; +import java.io.IOException; +import java.util.LinkedList; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Wraps an iterator over partial result sets, supporting resuming RPCs on error. This class keeps + * track of the most recent resume token seen, and will buffer partial result set chunks that do not + * have a resume token until one is seen or buffer space is exceeded, which reduces the chance of + * yielding data to the caller that cannot be resumed. + */ +@VisibleForTesting +abstract class ResumableStreamIterator extends AbstractIterator + implements CloseableIterator { + private static final RetrySettings DEFAULT_STREAMING_RETRY_SETTINGS = + SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetrySettings(); + private final RetrySettings streamingRetrySettings; + private final Set retryableCodes; + private static final Logger logger = Logger.getLogger(ResumableStreamIterator.class.getName()); + private final BackOff backOff; + private final LinkedList buffer = new LinkedList<>(); + private final int maxBufferSize; + private final ISpan span; + private final TraceWrapper tracer; + private CloseableIterator stream; + private ByteString resumeToken; + private boolean finished; + /** + * Indicates whether it is currently safe to retry RPCs. This will be {@code false} if we have + * reached the maximum buffer size without seeing a restart token; in this case, we will drain the + * buffer and remain in this state until we see a new restart token. + */ + private boolean safeToRetry = true; + + protected ResumableStreamIterator( + int maxBufferSize, + String streamName, + ISpan parent, + TraceWrapper tracer, + RetrySettings streamingRetrySettings, + Set retryableCodes) { + checkArgument(maxBufferSize >= 0); + this.maxBufferSize = maxBufferSize; + this.tracer = tracer; + this.span = tracer.spanBuilderWithExplicitParent(streamName, parent); + this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings); + this.retryableCodes = Preconditions.checkNotNull(retryableCodes); + this.backOff = newBackOff(); + } + + private ExponentialBackOff newBackOff() { + if (Objects.equals(streamingRetrySettings, DEFAULT_STREAMING_RETRY_SETTINGS)) { + return new ExponentialBackOff.Builder() + .setMultiplier(streamingRetrySettings.getRetryDelayMultiplier()) + .setInitialIntervalMillis( + Math.max(10, (int) streamingRetrySettings.getInitialRetryDelay().toMillis())) + .setMaxIntervalMillis( + Math.max(1000, (int) streamingRetrySettings.getMaxRetryDelay().toMillis())) + .setMaxElapsedTimeMillis(Integer.MAX_VALUE) // Prevent Backoff.STOP from getting returned. + .build(); + } + return new ExponentialBackOff.Builder() + .setMultiplier(streamingRetrySettings.getRetryDelayMultiplier()) + // All of these values must be > 0. + .setInitialIntervalMillis( + Math.max( + 1, + (int) + Math.min( + streamingRetrySettings.getInitialRetryDelay().toMillis(), + Integer.MAX_VALUE))) + .setMaxIntervalMillis( + Math.max( + 1, + (int) + Math.min( + streamingRetrySettings.getMaxRetryDelay().toMillis(), Integer.MAX_VALUE))) + .setMaxElapsedTimeMillis( + Math.max( + 1, + (int) + Math.min( + streamingRetrySettings.getTotalTimeout().toMillis(), Integer.MAX_VALUE))) + .build(); + } + + private void backoffSleep(Context context, BackOff backoff) throws SpannerException { + backoffSleep(context, nextBackOffMillis(backoff)); + } + + private static long nextBackOffMillis(BackOff backoff) throws SpannerException { + try { + return backoff.nextBackOffMillis(); + } catch (IOException e) { + throw newSpannerException(ErrorCode.INTERNAL, e.getMessage(), e); + } + } + + private void backoffSleep(Context context, long backoffMillis) throws SpannerException { + tracer.getCurrentSpan().addAnnotation("Backing off", "Delay", backoffMillis); + final CountDownLatch latch = new CountDownLatch(1); + final Context.CancellationListener listener = + ignored -> { + // Wakeup on cancellation / DEADLINE_EXCEEDED. + latch.countDown(); + }; + + context.addListener(listener, DirectExecutor.INSTANCE); + try { + if (backoffMillis == BackOff.STOP) { + // Highly unlikely but we handle it just in case. + backoffMillis = streamingRetrySettings.getMaxRetryDelay().toMillis(); + } + if (latch.await(backoffMillis, TimeUnit.MILLISECONDS)) { + // Woken by context cancellation. + throw newSpannerExceptionForCancellation(context, null); + } + } catch (InterruptedException interruptExcept) { + throw newSpannerExceptionForCancellation(context, interruptExcept); + } finally { + context.removeListener(listener); + } + } + + private enum DirectExecutor implements Executor { + INSTANCE; + + @Override + public void execute(Runnable command) { + command.run(); + } + } + + abstract CloseableIterator startStream(@Nullable ByteString resumeToken); + + @Override + public void close(@Nullable String message) { + if (stream != null) { + stream.close(message); + span.end(); + stream = null; + } + } + + @Override + public boolean isWithBeginTransaction() { + return stream != null && stream.isWithBeginTransaction(); + } + + @Override + protected PartialResultSet computeNext() { + Context context = Context.current(); + while (true) { + // Eagerly start stream before consuming any buffered items. + if (stream == null) { + span.addAnnotation( + "Starting/Resuming stream", + "ResumeToken", + resumeToken == null ? "null" : resumeToken.toStringUtf8()); + try (IScope scope = tracer.withSpan(span)) { + // When start a new stream set the Span as current to make the gRPC Span a child of + // this Span. + stream = checkNotNull(startStream(resumeToken)); + } + } + // Buffer contains items up to a resume token or has reached capacity: flush. + if (!buffer.isEmpty() + && (finished || !safeToRetry || !buffer.getLast().getResumeToken().isEmpty())) { + return buffer.pop(); + } + try { + if (stream.hasNext()) { + PartialResultSet next = stream.next(); + boolean hasResumeToken = !next.getResumeToken().isEmpty(); + if (hasResumeToken) { + resumeToken = next.getResumeToken(); + safeToRetry = true; + } + // If the buffer is empty and this chunk has a resume token or we cannot resume safely + // anyway, we can yield it immediately rather than placing it in the buffer to be + // returned on the next iteration. + if ((hasResumeToken || !safeToRetry) && buffer.isEmpty()) { + return next; + } + buffer.add(next); + if (buffer.size() > maxBufferSize && buffer.getLast().getResumeToken().isEmpty()) { + // We need to flush without a restart token. Errors encountered until we see + // such a token will fail the read. + safeToRetry = false; + } + } else { + finished = true; + if (buffer.isEmpty()) { + endOfData(); + return null; + } + } + } catch (SpannerException spannerException) { + if (safeToRetry && isRetryable(spannerException)) { + span.addAnnotation("Stream broken. Safe to retry", spannerException); + logger.log(Level.FINE, "Retryable exception, will sleep and retry", spannerException); + // Truncate any items in the buffer before the last retry token. + while (!buffer.isEmpty() && buffer.getLast().getResumeToken().isEmpty()) { + buffer.removeLast(); + } + assert buffer.isEmpty() || buffer.getLast().getResumeToken().equals(resumeToken); + stream = null; + try (IScope s = tracer.withSpan(span)) { + long delay = spannerException.getRetryDelayInMillis(); + if (delay != -1) { + backoffSleep(context, delay); + } else { + backoffSleep(context, backOff); + } + } + + continue; + } + span.addAnnotation("Stream broken. Not safe to retry", spannerException); + span.setStatus(spannerException); + throw spannerException; + } catch (RuntimeException e) { + span.addAnnotation("Stream broken. Not safe to retry", e); + span.setStatus(e); + throw e; + } + } + } + + boolean isRetryable(SpannerException spannerException) { + return spannerException.isRetryable() + || retryableCodes.contains( + GrpcStatusCode.of(spannerException.getErrorCode().getGrpcStatusCode()).getCode()); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 29928f61cec..81b00001105 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -263,6 +263,7 @@ public ReadContext singleUse(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setDefaultDecodeMode(spanner.getDefaultDecodeMode()) .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions()) .setSpan(currentSpan) .setTracer(tracer) @@ -284,6 +285,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setDefaultDecodeMode(spanner.getDefaultDecodeMode()) .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions()) .setSpan(currentSpan) .setTracer(tracer) @@ -305,6 +307,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setDefaultDecodeMode(spanner.getDefaultDecodeMode()) .setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions()) .setSpan(currentSpan) .setTracer(tracer) @@ -423,6 +426,7 @@ TransactionContextImpl newTransaction(Options options) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setDefaultDecodeMode(spanner.getDefaultDecodeMode()) .setSpan(currentSpan) .setTracer(tracer) .setExecutorProvider(spanner.getAsyncExecutorProvider()) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 326a51d803e..8fe06f76cc8 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -151,6 +151,10 @@ int getDefaultPrefetchChunks() { return getOptions().getPrefetchChunks(); } + DecodeMode getDefaultDecodeMode() { + return getOptions().getDecodeMode(); + } + /** Returns the default query options that should be used for the specified database. */ QueryOptions getDefaultQueryOptions(DatabaseId databaseId) { return getOptions().getDefaultQueryOptions(databaseId); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 9c6044aa938..a16be179ce3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -113,6 +113,7 @@ public class SpannerOptions extends ServiceOptions { private final GrpcInterceptorProvider interceptorProvider; private final SessionPoolOptions sessionPoolOptions; private final int prefetchChunks; + private final DecodeMode decodeMode; private final int numChannels; private final String transportChannelExecutorThreadNameFormat; private final String databaseRole; @@ -616,6 +617,7 @@ protected SpannerOptions(Builder builder) { ? builder.sessionPoolOptions : SessionPoolOptions.newBuilder().build(); prefetchChunks = builder.prefetchChunks; + decodeMode = builder.decodeMode; databaseRole = builder.databaseRole; sessionLabels = builder.sessionLabels; try { @@ -704,6 +706,9 @@ public static class Builder extends ServiceOptions.Builder { static final int DEFAULT_PREFETCH_CHUNKS = 4; static final QueryOptions DEFAULT_QUERY_OPTIONS = QueryOptions.getDefaultInstance(); + // TODO: Set the default to DecodeMode.DIRECT before merging to keep the current default. + // It is currently set to LAZY_PER_COL so it is used in all tests. + static final DecodeMode DEFAULT_DECODE_MODE = DecodeMode.LAZY_PER_COL; static final RetrySettings DEFAULT_ADMIN_REQUESTS_LIMIT_EXCEEDED_RETRY_SETTINGS = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofSeconds(5L)) @@ -730,6 +735,7 @@ public static class Builder private String transportChannelExecutorThreadNameFormat = "Cloud-Spanner-TransportChannel-%d"; private int prefetchChunks = DEFAULT_PREFETCH_CHUNKS; + private DecodeMode decodeMode = DEFAULT_DECODE_MODE; private SessionPoolOptions sessionPoolOptions; private String databaseRole; private ImmutableMap sessionLabels; @@ -797,6 +803,7 @@ protected Builder() { options.transportChannelExecutorThreadNameFormat; this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; + this.decodeMode = options.decodeMode; this.databaseRole = options.databaseRole; this.sessionLabels = options.sessionLabels; this.spannerStubSettingsBuilder = options.spannerStubSettings.toBuilder(); @@ -1224,6 +1231,15 @@ public Builder setPrefetchChunks(int prefetchChunks) { return this; } + /** + * Specifies how values that are returned from a query should be decoded and converted from + * protobuf values into plain Java objects. + */ + public Builder setDecodeMode(DecodeMode decodeMode) { + this.decodeMode = decodeMode; + return this; + } + @Override public Builder setHost(String host) { super.setHost(host); @@ -1568,6 +1584,10 @@ public int getPrefetchChunks() { return prefetchChunks; } + public DecodeMode getDecodeMode() { + return decodeMode; + } + public static GrpcTransportOptions getDefaultGrpcTransportOptions() { return GrpcTransportOptions.newBuilder().build(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index a845eb118bf..3f0155e4a5e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -1518,6 +1518,10 @@ void valueToString(StringBuilder b) { @Override boolean valueEquals(Value v) { + // NaN == NaN always returns false, so we need a custom check. + if (Double.isNaN(this.value)) { + return Double.isNaN(((Float64Impl) v).value); + } return ((Float64Impl) v).value == value; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index dc373cf03bd..c642d7e505a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -16,28 +16,28 @@ package com.google.cloud.spanner.connection; -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.ErrorCode; import com.google.cloud.spanner.Options.QueryOption; +import com.google.cloud.spanner.ProtobufResultSet; 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.cloud.spanner.Type.Code; +import com.google.cloud.spanner.Type.StructField; import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; import com.google.cloud.spanner.connection.ReadWriteTransaction.RetriableStatement; 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.math.BigDecimal; -import java.util.Objects; +import com.google.protobuf.Value; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; @@ -71,11 +71,11 @@ class ChecksumResultSet extends ReplaceableForwardingResultSet implements Retria private final ParsedStatement statement; private final AnalyzeMode analyzeMode; private final QueryOption[] options; - private final ChecksumResultSet.ChecksumCalculator checksumCalculator = new ChecksumCalculator(); + private final ChecksumCalculator checksumCalculator = new ChecksumCalculator(); ChecksumResultSet( ReadWriteTransaction transaction, - ResultSet delegate, + ProtobufResultSet delegate, ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { @@ -91,6 +91,13 @@ class ChecksumResultSet extends ReplaceableForwardingResultSet implements Retria this.options = options; } + @Override + public Value getProtobufValue(int columnIndex) { + // We can safely cast to ProtobufResultSet here, as the constructor of this class only accepts + // instances of ProtobufResultSet. + return ((ProtobufResultSet) getDelegate()).getProtobufValue(columnIndex); + } + /** Simple {@link Callable} for calling {@link ResultSet#next()} */ private final class NextCallable implements Callable { @Override @@ -102,7 +109,7 @@ public Boolean call() { boolean res = ChecksumResultSet.super.next(); // Only update the checksum if there was another row to be consumed. if (res) { - checksumCalculator.calculateNextChecksum(getCurrentRowAsStruct()); + checksumCalculator.calculateNextChecksum(ChecksumResultSet.this); } numberOfNextCalls.incrementAndGet(); return res; @@ -118,8 +125,9 @@ public boolean next() { } @VisibleForTesting - HashCode getChecksum() { - // HashCode is immutable and can be safely returned. + byte[] getChecksum() { + // Getting the checksum from the checksumCalculator will create a clone of the current digest + // and return the checksum from the clone, so it is safe to return this value. return checksumCalculator.getChecksum(); } @@ -132,8 +140,8 @@ HashCode getChecksum() { @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; + ChecksumCalculator newChecksumCalculator = new ChecksumCalculator(); + ProtobufResultSet resultSet = null; long counter = 0L; try { transaction @@ -150,7 +158,7 @@ public void retry(AbortedException aborted) throws AbortedException { statement, StatementExecutionStep.RETRY_NEXT_ON_RESULT_SET, transaction); next = resultSet.next(); if (next) { - newChecksumCalculator.calculateNextChecksum(resultSet.getCurrentRowAsStruct()); + newChecksumCalculator.calculateNextChecksum(resultSet); } counter++; } @@ -168,9 +176,9 @@ public void retry(AbortedException aborted) throws AbortedException { 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.get() && Objects.equals(newChecksum, currentChecksum)) { + byte[] newChecksum = newChecksumCalculator.getChecksum(); + byte[] currentChecksum = checksumCalculator.getChecksum(); + if (counter == numberOfNextCalls.get() && Arrays.equals(newChecksum, currentChecksum)) { // Checksum is ok, we only need to replace the delegate result set if it's still open. if (isClosed()) { resultSet.close(); @@ -184,222 +192,165 @@ public void retry(AbortedException aborted) throws AbortedException { } } - /** Calculates and keeps the current checksum of a {@link ChecksumResultSet} */ + /** + * Calculates a running checksum for all the data that has been seen sofar in this result set. The + * calculation is performed on the protobuf values that were returned by Cloud Spanner, which + * means that no decoding of the results is needed (or allowed!) before calculating the checksum. + * This is more efficient, both in terms of CPU usage and memory consumption, especially if the + * consumer of the result set does not read all values, or is only reading the underlying protobuf + * values. + */ private static final class ChecksumCalculator { - private static final HashFunction SHA256_FUNCTION = Hashing.sha256(); - private HashCode currentChecksum; + // Use a buffer of max 1Mb to hash string data. This means that strings of up to 1Mb in size + // will be hashed in one go, while strings larger than 1Mb will be chunked into pieces of at + // most 1Mb and then fed into the digest. The digest internally creates a copy of the string + // that is being hashed, so chunking large strings prevents them from being loaded into memory + // twice. + private static final int MAX_BUFFER_SIZE = 1 << 20; - private void calculateNextChecksum(Struct row) { - Hasher hasher = SHA256_FUNCTION.newHasher(); - if (currentChecksum != null) { - hasher.putBytes(currentChecksum.asBytes()); + private boolean firstRow = true; + private final MessageDigest digest; + private ByteBuffer buffer; + private ByteBuffer float64Buffer; + + ChecksumCalculator() { + try { + // This is safe, as all Java implementations are required to have MD5 implemented. + // See https://docs.oracle.com/javase/8/docs/api/java/security/MessageDigest.html + // MD5 requires less CPU power than SHA-256, and still offers a low enough collision + // probability for the use case at hand here. + digest = MessageDigest.getInstance("MD5"); + } catch (Throwable t) { + throw SpannerExceptionFactory.asSpannerException(t); } - hasher.putObject(row, StructFunnel.INSTANCE); - currentChecksum = hasher.hash(); } - private HashCode getChecksum() { - return currentChecksum; + private byte[] getChecksum() { + try { + // This is safe, as the MD5 MessageDigest is known to be cloneable. + MessageDigest clone = (MessageDigest) digest.clone(); + return clone.digest(); + } catch (CloneNotSupportedException e) { + throw SpannerExceptionFactory.asSpannerException(e); + } } - } - /** - * 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); + private void calculateNextChecksum(ProtobufResultSet resultSet) { + if (firstRow) { + for (StructField field : resultSet.getType().getStructFields()) { + digest.update(field.getType().toString().getBytes(StandardCharsets.UTF_8)); + } + } + for (int col = 0; col < resultSet.getColumnCount(); col++) { + Type type = resultSet.getColumnType(col); + if (resultSet.canGetProtobufValue(col)) { + Value value = resultSet.getProtobufValue(col); + digest.update((byte) value.getKindCase().getNumber()); + pushValue(type, value); } 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: - case PROTO: - 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 NUMERIC: - funnelValue(type, row.getBigDecimal(i), into); - break; - case PG_NUMERIC: - funnelValue(type, row.getString(i), into); - break; - case INT64: - case ENUM: - funnelValue(type, row.getLong(i), into); - break; - case STRING: - funnelValue(type, row.getString(i), into); - break; - case JSON: - funnelValue(type, row.getJson(i), into); - break; - case PG_JSONB: - funnelValue(type, row.getPgJsonb(i), into); - break; - case TIMESTAMP: - funnelValue(type, row.getTimestamp(i), into); - break; - - case STRUCT: - default: - throw new IllegalArgumentException("unsupported row type"); - } + // This will normally not happen, unless the user explicitly sets the decoding mode to + // DIRECT for a query in a read/write transaction. The default decoding mode in the + // Connection API is set to LAZY_PER_COL. + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, + "Failed to get the underlying protobuf value for the column " + + resultSet.getMetadata().getRowType().getFields(col).getName() + + ". " + + "Executing queries with DecodeMode#DIRECT is not supported in read/write transactions."); } } + firstRow = false; } - 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); - } + private void pushValue(Type type, Value value) { + // Protobuf Value has a very limited set of possible types of values. All Cloud Spanner types + // are mapped to one of the protobuf values listed here, meaning that no changes are needed to + // this calculation when a new type is added to Cloud Spanner. + switch (value.getKindCase()) { + case NULL_VALUE: + // nothing needed, writing the KindCase is enough. break; - case BYTES: - case PROTO: - into.putInt(row.getBytesList(columnIndex).size()); - for (ByteArray value : row.getBytesList(columnIndex)) { - funnelValue(Code.BYTES, value, into); - } + case BOOL_VALUE: + digest.update(value.getBoolValue() ? (byte) 1 : 0); break; - case DATE: - into.putInt(row.getDateList(columnIndex).size()); - for (Date value : row.getDateList(columnIndex)) { - funnelValue(Code.DATE, value, into); - } + case STRING_VALUE: + putString(value.getStringValue()); break; - case FLOAT64: - into.putInt(row.getDoubleList(columnIndex).size()); - for (Double value : row.getDoubleList(columnIndex)) { - funnelValue(Code.FLOAT64, value, into); + case NUMBER_VALUE: + if (float64Buffer == null) { + // Create an 8-byte buffer that can be re-used for all float64 values in this result + // set. + float64Buffer = ByteBuffer.allocate(Double.BYTES); + } else { + float64Buffer.clear(); } + float64Buffer.putDouble(value.getNumberValue()); + float64Buffer.flip(); + digest.update(float64Buffer); break; - case NUMERIC: - into.putInt(row.getBigDecimalList(columnIndex).size()); - for (BigDecimal value : row.getBigDecimalList(columnIndex)) { - funnelValue(Code.NUMERIC, value, into); + case LIST_VALUE: + if (type.getCode() == Code.ARRAY) { + for (Value item : value.getListValue().getValuesList()) { + digest.update((byte) item.getKindCase().getNumber()); + pushValue(type.getArrayElementType(), item); + } + } else { + // This should not be possible. + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, + "List values that are not an ARRAY are not supported"); } break; - case PG_NUMERIC: - into.putInt(row.getStringList(columnIndex).size()); - for (String value : row.getStringList(columnIndex)) { - funnelValue(Code.STRING, value, into); + case STRUCT_VALUE: + if (type.getCode() == Code.STRUCT) { + for (int col = 0; col < type.getStructFields().size(); col++) { + String name = type.getStructFields().get(col).getName(); + putString(name); + Value item = value.getStructValue().getFieldsMap().get(name); + digest.update((byte) item.getKindCase().getNumber()); + pushValue(type.getStructFields().get(col).getType(), item); + } + } else { + // This should not be possible. + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.FAILED_PRECONDITION, + "Struct values without a struct type are not supported"); } break; - case INT64: - case ENUM: - 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 JSON: - into.putInt(row.getJsonList(columnIndex).size()); - for (String value : row.getJsonList(columnIndex)) { - funnelValue(Code.JSON, value, into); - } - break; - case PG_JSONB: - into.putInt(row.getPgJsonbList(columnIndex).size()); - for (String value : row.getPgJsonbList(columnIndex)) { - funnelValue(Code.PG_JSONB, 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"); + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNIMPLEMENTED, "Unsupported protobuf value: " + value.getKindCase()); } - 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); + /** Hashes a string value in blocks of max MAX_BUFFER_SIZE. */ + private void putString(String stringValue) { + int length = stringValue.length(); + if (buffer == null || (buffer.capacity() < MAX_BUFFER_SIZE && buffer.capacity() < length)) { + // Create a ByteBuffer with a maximum buffer size. + // This buffer is re-used for all string values in the result set. + buffer = ByteBuffer.allocate(Math.min(MAX_BUFFER_SIZE, length)); } else { - switch (type) { - case BOOL: - into.putBoolean((Boolean) value); - break; - case BYTES: - case PROTO: - 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 NUMERIC: - String stringRepresentation = value.toString(); - into.putInt(stringRepresentation.length()); - into.putUnencodedChars(stringRepresentation); - break; - case INT64: - case ENUM: - into.putLong((Long) value); - break; - case PG_NUMERIC: - case STRING: - case JSON: - case PG_JSONB: - 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"); - } + buffer.clear(); + } + + // Wrap the string in a CharBuffer. This allows us to read from the string in sections without + // creating a new copy of (a part of) the string. E.g. using something like substring(..) + // would create a copy of that part of the string, using CharBuffer.wrap(..) does not. + CharBuffer source = CharBuffer.wrap(stringValue); + CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder(); + // source.hasRemaining() returns false when all the characters in the string have been + // processed. + while (source.hasRemaining()) { + // Encode the string into bytes and write them into the byte buffer. + // At most MAX_BUFFER_SIZE bytes will be written. + encoder.encode(source, buffer, false); + // Flip the buffer so we can read from the start. + buffer.flip(); + // Put the bytes from the buffer into the digest. + digest.update(buffer); + // Flip the buffer again, so we can repeat and write to the start of the buffer again. + buffer.flip(); } } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java index dff915e2cce..1b15ec50822 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java @@ -19,6 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.ProtobufResultSet; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; @@ -40,7 +41,7 @@ * 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 { +class DirectExecuteResultSet implements ProtobufResultSet { private static final String MISSING_NEXT_CALL = "Must be preceded by a next() call"; private final ResultSet delegate; private boolean nextCalledByClient = false; @@ -79,6 +80,21 @@ public boolean next() throws SpannerException { return initialNextResult; } + @Override + public boolean canGetProtobufValue(int columnIndex) { + return nextCalledByClient + && delegate instanceof ProtobufResultSet + && ((ProtobufResultSet) delegate).canGetProtobufValue(columnIndex); + } + + @Override + public com.google.protobuf.Value getProtobufValue(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + Preconditions.checkState( + delegate instanceof ProtobufResultSet, "The result set does not support protobuf values"); + return ((ProtobufResultSet) delegate).getProtobufValue(columnIndex); + } + @Override public Struct getCurrentRowAsStruct() { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java index e1fb87e4ade..6c4290c3b18 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java @@ -39,6 +39,7 @@ import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; +import com.google.cloud.spanner.ProtobufResultSet; import com.google.cloud.spanner.ReadContext; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; @@ -427,7 +428,7 @@ public ApiFuture executeQueryAsync( statement, StatementExecutionStep.EXECUTE_STATEMENT, ReadWriteTransaction.this); - ResultSet delegate = + DirectExecuteResultSet delegate = DirectExecuteResultSet.ofResultSet( internalExecuteQuery(statement, analyzeMode, options)); return createAndAddRetryResultSet( @@ -797,7 +798,7 @@ T runWithRetry(Callable callable) throws SpannerException { * returns a retryable {@link ResultSet}. */ private ResultSet createAndAddRetryResultSet( - ResultSet resultSet, + ProtobufResultSet resultSet, ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { @@ -1091,7 +1092,7 @@ interface RetriableStatement { /** Creates a {@link ChecksumResultSet} for this {@link ReadWriteTransaction}. */ @VisibleForTesting ChecksumResultSet createChecksumResultSet( - ResultSet delegate, + ProtobufResultSet delegate, ParsedStatement statement, AnalyzeMode analyzeMode, QueryOption... options) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java index 7370551a46f..a8de14e5121 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java @@ -20,6 +20,7 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.ProtobufResultSet; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; @@ -42,7 +43,7 @@ * 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 { +class ReplaceableForwardingResultSet implements ProtobufResultSet { private ResultSet delegate; private boolean closed; @@ -60,6 +61,10 @@ void replaceDelegate(ResultSet delegate) { this.delegate = delegate; } + protected ResultSet getDelegate() { + return this.delegate; + } + private void checkClosed() { if (closed) { throw SpannerExceptionFactory.newSpannerException( @@ -77,6 +82,21 @@ public boolean next() throws SpannerException { return delegate.next(); } + @Override + public boolean canGetProtobufValue(int columnIndex) { + return !closed + && delegate instanceof ProtobufResultSet + && ((ProtobufResultSet) delegate).canGetProtobufValue(columnIndex); + } + + @Override + public com.google.protobuf.Value getProtobufValue(int columnIndex) { + checkClosed(); + Preconditions.checkState( + delegate instanceof ProtobufResultSet, "The result set does not support protobuf values"); + return ((ProtobufResultSet) getDelegate()).getProtobufValue(columnIndex); + } + @Override public Struct getCurrentRowAsStruct() { checkClosed(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java index 2a5a805c2c7..da8da78d92e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner.connection; import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.DecodeMode; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SessionPoolOptions; import com.google.cloud.spanner.Spanner; @@ -342,6 +343,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, CONNECTION_API_CLIENT_LIB_TOKEN)) .setHost(key.host) .setProjectId(key.projectId) + // Use lazy decoding, so we can use the protobuf values for calculating the checksum that is + // needed for read/write transactions. + .setDecodeMode(DecodeMode.LAZY_PER_COL) .setDatabaseRole(options.getDatabaseRole()) .setCredentials(options.getCredentials()); builder.setSessionPoolOption(key.sessionPoolOptions); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index ccbf3c0b2b9..8dfbb986eb8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -46,7 +46,6 @@ import com.google.cloud.ByteArray; import com.google.cloud.NoCredentials; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator; import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 914ce391f4a..cb73618d998 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -56,13 +56,13 @@ import org.junit.runners.JUnit4; import org.threeten.bp.Duration; -/** Unit tests for {@link com.google.cloud.spanner.AbstractResultSet.GrpcResultSet}. */ +/** Unit tests for {@link GrpcResultSet}. */ @RunWith(JUnit4.class) public class GrpcResultSetTest { - private AbstractResultSet.GrpcResultSet resultSet; + private GrpcResultSet resultSet; private SpannerRpc.ResultStreamConsumer consumer; - private AbstractResultSet.GrpcStreamIterator stream; + private GrpcStreamIterator stream; private final Duration streamWaitTimeout = Duration.ofNanos(1L); private static class NoOpListener implements AbstractResultSet.Listener { @@ -81,7 +81,7 @@ public void onDone(boolean withBeginTransaction) {} @Before public void setUp() { - stream = new AbstractResultSet.GrpcStreamIterator(10); + stream = new GrpcStreamIterator(10); stream.setCall( new SpannerRpc.StreamingCall() { @Override @@ -97,11 +97,11 @@ public void request(int numMessages) {} }, false); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new GrpcResultSet(stream, new NoOpListener()); } - public AbstractResultSet.GrpcResultSet resultSetWithMode(QueryMode queryMode) { - return new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + public GrpcResultSet resultSetWithMode(QueryMode queryMode) { + return new GrpcResultSet(stream, new NoOpListener()); } @Test @@ -609,7 +609,7 @@ public com.google.protobuf.Value apply(@Nullable Value input) { private void verifySerialization( Function protoFn, Value... values) { - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new GrpcResultSet(stream, new NoOpListener()); PartialResultSet.Builder builder = PartialResultSet.newBuilder(); List types = new ArrayList<>(); for (Value value : values) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 7bf9f51a4ea..f27aa405aaa 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -19,7 +19,6 @@ import com.google.api.gax.grpc.testing.MockGrpcService; import com.google.cloud.ByteArray; import com.google.cloud.Date; -import com.google.cloud.spanner.AbstractResultSet.GrpcStruct; import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; import com.google.cloud.spanner.SessionPool.SessionPoolTransactionContext; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java index af558d14dd4..a72c9872faf 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java @@ -104,9 +104,9 @@ protected List getChildren() { } private static class TestCaseRunner { - private AbstractResultSet.GrpcResultSet resultSet; + private GrpcResultSet resultSet; private SpannerRpc.ResultStreamConsumer consumer; - private AbstractResultSet.GrpcStreamIterator stream; + private GrpcStreamIterator stream; private JSONObject testCase; TestCaseRunner(JSONObject testCase) { @@ -114,7 +114,7 @@ private static class TestCaseRunner { } private void run() throws Exception { - stream = new AbstractResultSet.GrpcStreamIterator(10); + stream = new GrpcStreamIterator(10); stream.setCall( new SpannerRpc.StreamingCall() { @Override @@ -130,7 +130,7 @@ public void request(int numMessages) {} }, false); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new GrpcResultSet(stream, new NoOpListener()); JSONArray chunks = testCase.getJSONArray("chunks"); JSONObject expectedResult = testCase.getJSONObject("result"); @@ -143,8 +143,7 @@ public void request(int numMessages) {} assertResultSet(resultSet, expectedResult.getJSONArray("value")); } - private void assertResultSet(AbstractResultSet.GrpcResultSet actual, JSONArray expected) - throws Exception { + private void assertResultSet(GrpcResultSet actual, JSONArray expected) throws Exception { int i = 0; while (actual.next()) { Struct actualRow = actual.getCurrentRowAsStruct(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsHelper.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsHelper.java index 51cca1bc684..fc494c6f3ff 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsHelper.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsHelper.java @@ -17,7 +17,6 @@ package com.google.cloud.spanner; import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; -import com.google.cloud.spanner.AbstractResultSet.GrpcResultSet; import com.google.cloud.spanner.AbstractResultSet.Listener; import com.google.protobuf.ListValue; import com.google.spanner.v1.PartialResultSet; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java index 217e818d42c..d153696ab45 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.when; import com.google.api.client.util.BackOff; -import com.google.cloud.spanner.AbstractResultSet.ResumableStreamIterator; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.common.collect.AbstractIterator; import com.google.common.collect.Lists; @@ -56,7 +55,7 @@ import org.junit.runners.JUnit4; import org.mockito.Mockito; -/** Unit tests for {@link AbstractResultSet.ResumableStreamIterator}. */ +/** Unit tests for {@link ResumableStreamIterator}. */ @RunWith(JUnit4.class) public class ResumableStreamIteratorTest { interface Starter { @@ -131,7 +130,7 @@ public boolean isWithBeginTransaction() { } Starter starter = Mockito.mock(Starter.class); - AbstractResultSet.ResumableStreamIterator resumableStreamIterator; + ResumableStreamIterator resumableStreamIterator; @Before public void setUp() { @@ -143,7 +142,7 @@ public void setUp() { private void initWithLimit(int maxBufferSize) { resumableStreamIterator = - new AbstractResultSet.ResumableStreamIterator( + new ResumableStreamIterator( maxBufferSize, "", new OpenTelemetrySpan(mock(io.opentelemetry.api.trace.Span.class)), diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DecodeModeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DecodeModeTest.java new file mode 100644 index 00000000000..6a6125e1dda --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DecodeModeTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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.connection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.spanner.DecodeMode; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.MockSpannerServiceImpl; +import com.google.cloud.spanner.Options; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class DecodeModeTest extends AbstractMockServerTest { + + @After + public void clearRequests() { + mockSpanner.clearRequests(); + } + + @Test + public void testAllDecodeModes() { + int numRows = 10; + RandomResultSetGenerator generator = new RandomResultSetGenerator(numRows); + String sql = "select * from random"; + Statement statement = Statement.of(sql); + mockSpanner.putStatementResult( + MockSpannerServiceImpl.StatementResult.query(statement, generator.generate())); + + try (Connection connection = createConnection()) { + for (boolean readonly : new boolean[] {true, false}) { + for (boolean autocommit : new boolean[] {true, false}) { + connection.setReadOnly(readonly); + connection.setAutocommit(autocommit); + + int receivedRows = 0; + // DecodeMode#DIRECT is not supported in read/write transactions, as the protobuf value is + // used for checksum calculation. + try (ResultSet direct = + connection.executeQuery( + statement, + !readonly && !autocommit + ? Options.decodeMode(DecodeMode.LAZY_PER_ROW) + : Options.decodeMode(DecodeMode.DIRECT)); + ResultSet lazyPerRow = + connection.executeQuery(statement, Options.decodeMode(DecodeMode.LAZY_PER_ROW)); + ResultSet lazyPerCol = + connection.executeQuery(statement, Options.decodeMode(DecodeMode.LAZY_PER_COL))) { + while (direct.next() && lazyPerRow.next() && lazyPerCol.next()) { + assertEquals(direct.getColumnCount(), lazyPerRow.getColumnCount()); + assertEquals(direct.getColumnCount(), lazyPerCol.getColumnCount()); + for (int col = 0; col < direct.getColumnCount(); col++) { + // Test getting the entire row as a struct both as the first thing we do, and as the + // last thing we do. This ensures that the method works as expected both when a row + // is lazily decoded by this method, and when it has already been decoded by another + // method. + if (col % 2 == 0) { + assertEquals(direct.getCurrentRowAsStruct(), lazyPerRow.getCurrentRowAsStruct()); + assertEquals(direct.getCurrentRowAsStruct(), lazyPerCol.getCurrentRowAsStruct()); + } + assertEquals(direct.isNull(col), lazyPerRow.isNull(col)); + assertEquals(direct.isNull(col), lazyPerCol.isNull(col)); + assertEquals(direct.getValue(col), lazyPerRow.getValue(col)); + assertEquals(direct.getValue(col), lazyPerCol.getValue(col)); + if (col % 2 == 1) { + assertEquals(direct.getCurrentRowAsStruct(), lazyPerRow.getCurrentRowAsStruct()); + assertEquals(direct.getCurrentRowAsStruct(), lazyPerCol.getCurrentRowAsStruct()); + } + } + receivedRows++; + } + assertEquals(numRows, receivedRows); + } + if (!autocommit) { + connection.commit(); + } + } + } + } + } + + @Test + public void testDecodeModeDirect_failsInReadWriteTransaction() { + int numRows = 1; + RandomResultSetGenerator generator = new RandomResultSetGenerator(numRows); + String sql = "select * from random"; + Statement statement = Statement.of(sql); + mockSpanner.putStatementResult( + MockSpannerServiceImpl.StatementResult.query(statement, generator.generate())); + + try (Connection connection = createConnection()) { + connection.setAutocommit(false); + try (ResultSet resultSet = + connection.executeQuery(statement, Options.decodeMode(DecodeMode.DIRECT))) { + SpannerException exception = assertThrows(SpannerException.class, resultSet::next); + assertEquals(ErrorCode.FAILED_PRECONDITION, exception.getErrorCode()); + assertTrue( + exception.getMessage(), + exception + .getMessage() + .contains( + "Executing queries with DecodeMode#DIRECT is not supported in read/write transactions.")); + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java index 1e4f96d1568..b14f837ff7b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java @@ -59,6 +59,7 @@ public void testMethodCallBeforeNext() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = Arrays.asList( + "canGetProtobufValue", "getStats", "getMetadata", "next", @@ -79,6 +80,7 @@ public void testMethodCallAfterClose() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = Arrays.asList( + "canGetProtobufValue", "getStats", "getMetadata", "next", @@ -101,6 +103,7 @@ public void testMethodCallAfterNextHasReturnedFalse() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = Arrays.asList( + "canGetProtobufValue", "getStats", "getMetadata", "next", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java index 3091364e17a..2067d36b5ea 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java @@ -34,6 +34,7 @@ import com.google.spanner.v1.TypeAnnotationCode; import com.google.spanner.v1.TypeCode; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -239,7 +240,10 @@ private void setRandomValue(Value.Builder builder, Type type) { if (dialect == Dialect.POSTGRESQL && randomNaN()) { builder.setStringValue("NaN"); } else { - builder.setStringValue(BigDecimal.valueOf(random.nextDouble()).toString()); + builder.setStringValue( + BigDecimal.valueOf(random.nextDouble()) + .setScale(9, RoundingMode.HALF_UP) + .toString()); } break; case INT64: diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 0f083fd1e50..8e643cf6e24 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -37,6 +37,7 @@ import com.google.cloud.spanner.CommitResponse; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.ProtobufResultSet; import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; @@ -518,193 +519,197 @@ public void testChecksumResultSet() { .setGenre(Genre.FOLK) .build(); ProtocolMessageEnum protoEnumVal = Genre.ROCK; - ResultSet delegate1 = - ResultSets.forRows( - Type.struct( - StructField.of("ID", Type.int64()), - StructField.of("NAME", Type.string()), - StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json()), - StructField.of( - "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), - StructField.of( - "PROTOENUM", - Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(1L) - .set("NAME") - .to("TEST 1") - .set("AMOUNT") - .to(BigDecimal.valueOf(550, 2)) - .set("JSON") - .to(Value.json(simpleJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(protoEnumVal) - .build(), - Struct.newBuilder() - .set("ID") - .to(2L) - .set("NAME") - .to("TEST 2") - .set("AMOUNT") - .to(BigDecimal.valueOf(750, 2)) - .set("JSON") - .to(Value.json(arrayJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(Genre.JAZZ) - .build())); + ProtobufResultSet delegate1 = + (ProtobufResultSet) + ResultSets.forRows( + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), + Arrays.asList( + Struct.newBuilder() + .set("ID") + .to(1L) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(BigDecimal.valueOf(550, 2)) + .set("JSON") + .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) + .build(), + Struct.newBuilder() + .set("ID") + .to(2L) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(BigDecimal.valueOf(750, 2)) + .set("JSON") + .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) + .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()), - StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json()), - StructField.of( - "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), - StructField.of( - "PROTOENUM", - Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(1L) - .set("NAME") - .to("TEST 1") - .set("AMOUNT") - .to(new BigDecimal("5.50")) - .set("JSON") - .to(Value.json(simpleJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(protoEnumVal) - .build(), - Struct.newBuilder() - .set("ID") - .to(2L) - .set("NAME") - .to("TEST 2") - .set("AMOUNT") - .to(new BigDecimal("7.50")) - .set("JSON") - .to(Value.json(arrayJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(Genre.JAZZ) - .build())); + ProtobufResultSet delegate2 = + (ProtobufResultSet) + ResultSets.forRows( + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), + Arrays.asList( + Struct.newBuilder() + .set("ID") + .to(1L) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) + .build(), + Struct.newBuilder() + .set("ID") + .to(2L) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) + .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()), - StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json()), - StructField.of( - "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), - StructField.of( - "PROTOENUM", - Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(2L) - .set("NAME") - .to("TEST 2") - .set("AMOUNT") - .to(new BigDecimal("7.50")) - .set("JSON") - .to(Value.json(arrayJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(Genre.JAZZ) - .build(), - Struct.newBuilder() - .set("ID") - .to(1L) - .set("NAME") - .to("TEST 1") - .set("AMOUNT") - .to(new BigDecimal("5.50")) - .set("JSON") - .to(Value.json(simpleJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(protoEnumVal) - .build())); + ProtobufResultSet delegate3 = + (ProtobufResultSet) + ResultSets.forRows( + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), + Arrays.asList( + Struct.newBuilder() + .set("ID") + .to(2L) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) + .build(), + Struct.newBuilder() + .set("ID") + .to(1L) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) + .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()), - StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json()), - StructField.of( - "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), - StructField.of( - "PROTOENUM", - Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), - Arrays.asList( - Struct.newBuilder() - .set("ID") - .to(1L) - .set("NAME") - .to("TEST 1") - .set("AMOUNT") - .to(new BigDecimal("5.50")) - .set("JSON") - .to(Value.json(simpleJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(protoEnumVal) - .build(), - Struct.newBuilder() - .set("ID") - .to(2L) - .set("NAME") - .to("TEST 2") - .set("AMOUNT") - .to(new BigDecimal("7.50")) - .set("JSON") - .to(Value.json(arrayJson)) - .set("PROTO") - .to(protoMessageVal) - .set("PROTOENUM") - .to(Genre.JAZZ) - .build(), - Struct.newBuilder() - .set("ID") - .to(3L) - .set("NAME") - .to("TEST 3") - .set("AMOUNT") - .to(new BigDecimal("9.99")) - .set("JSON") - .to(Value.json(emptyArrayJson)) - .set("PROTO") - .to(null, SingerInfo.getDescriptor()) - .set("PROTOENUM") - .to(Genre.POP) - .build())); + ProtobufResultSet delegate4 = + (ProtobufResultSet) + ResultSets.forRows( + Type.struct( + StructField.of("ID", Type.int64()), + StructField.of("NAME", Type.string()), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), + Arrays.asList( + Struct.newBuilder() + .set("ID") + .to(1L) + .set("NAME") + .to("TEST 1") + .set("AMOUNT") + .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) + .build(), + Struct.newBuilder() + .set("ID") + .to(2L) + .set("NAME") + .to("TEST 2") + .set("AMOUNT") + .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) + .build(), + Struct.newBuilder() + .set("ID") + .to(3L) + .set("NAME") + .to("TEST 3") + .set("AMOUNT") + .to(new BigDecimal("9.99")) + .set("JSON") + .to(Value.json(emptyArrayJson)) + .set("PROTO") + .to(null, SingerInfo.getDescriptor()) + .set("PROTOENUM") + .to(Genre.POP) + .build())); ChecksumResultSet rs4 = transaction.createChecksumResultSet(delegate4, parsedStatement, AnalyzeMode.NONE); @@ -736,44 +741,46 @@ public void testChecksumResultSetWithArray() { 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())); + ProtobufResultSet delegate1 = + (ProtobufResultSet) + 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())); + ProtobufResultSet delegate2 = + (ProtobufResultSet) + 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); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java index bbb34675147..4617c47bc6b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java @@ -104,7 +104,14 @@ public void testReplace() { public void testMethodCallBeforeNext() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = - Arrays.asList("getStats", "getMetadata", "next", "close", "equals", "hashCode"); + Arrays.asList( + "canGetProtobufValue", + "getStats", + "getMetadata", + "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(). @@ -116,6 +123,7 @@ public void testMethodCallAfterClose() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = Arrays.asList( + "canGetProtobufValue", "getStats", "getMetadata", "next", @@ -140,6 +148,7 @@ public void testMethodCallAfterNextHasReturnedFalse() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List excludedMethods = Arrays.asList( + "canGetProtobufValue", "getStats", "getMetadata", "next", From 53bcb3eca2e814472c3def24e8e03d47652a8e42 Mon Sep 17 00:00:00 2001 From: Arpan Mishra Date: Tue, 13 Feb 2024 17:42:29 +0530 Subject: [PATCH 04/10] feat: support public methods to use autogenerated admin clients. (#2878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent illegal negative timeout values into thread sleep() method while retrying exceptions in unit tests. * For details on issue see - https://github.com/googleapis/java-spanner/issues/2206 * Fixing lint issues. * feat: support emulator with autogenerated admin clients. * chore: modifying public interfaces and adding an integration test. * chore: add deprecated annotations and add docs. * chore: making interface methods default. * chore: add clirr ignore checks for public methods. * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java Co-authored-by: Knut Olav Løite * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java Co-authored-by: Knut Olav Løite * chore: address PR comments. * chore: decouple client stub objects and rely on common stub settings object. * chore: remove unused map objects. * chore: fix broken unit tests. * chore: handle case when client is terminated/shutdown * chore: change to create methods and avoid stale instances. * Revert "chore: fix broken unit tests." This reverts commit 07c9cd1121f2cf1f6e51d89a8981944325b2ad7b. * test: add more unit tests. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Knut Olav Løite Co-authored-by: Owl Bot --- README.md | 2 +- .../clirr-ignored-differences.xml | 27 +- .../com/google/cloud/spanner/Spanner.java | 59 +++- .../com/google/cloud/spanner/SpannerImpl.java | 29 ++ .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 31 +- .../cloud/spanner/spi/v1/SpannerRpc.java | 22 ++ .../google/cloud/spanner/SpannerImplTest.java | 72 +++++ .../it/ITAutogeneratedAdminClientTest.java | 283 ++++++++++++++++++ .../spanner/spi/v1/GapicSpannerRpcTest.java | 31 ++ 9 files changed, 541 insertions(+), 15 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java diff --git a/README.md b/README.md index 56d668b0a65..e70ee5eb3aa 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.31.0') +implementation platform('com.google.cloud:libraries-bom:26.32.0') implementation 'com.google.cloud:google-cloud-spanner' ``` diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 54eae11d51b..fbbc0153f89 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -539,6 +539,30 @@ com/google/cloud/spanner/Dialect java.lang.String getDefaultSchema() + + + 7012 + com/google/cloud/spanner/Spanner + com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient createDatabaseAdminClient() + + + 7012 + com/google/cloud/spanner/Spanner + com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient createInstanceAdminClient() + + + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings getDatabaseAdminStubSettings() + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings getInstanceAdminStubSettings() + + 7005 com/google/cloud/spanner/PartitionedDmlTransaction @@ -556,6 +580,5 @@ 7012 com/google/cloud/spanner/connection/Connection void setDirectedRead(com.google.spanner.v1.DirectedReadOptions) - - + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java index 52c35cb7130..7ccbc88d978 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java @@ -26,7 +26,12 @@ * quota. */ public interface Spanner extends Service, AutoCloseable { - /** Returns a {@code DatabaseAdminClient} to do admin operations on Cloud Spanner databases. */ + + /** + * Returns a {@code DatabaseAdminClient} to execute admin operations on Cloud Spanner databases. + * + * @return {@code DatabaseAdminClient} + */ /* * *

{@code
@@ -38,7 +43,34 @@ public interface Spanner extends Service, AutoCloseable {
    */
   DatabaseAdminClient getDatabaseAdminClient();
 
-  /** Returns an {@code InstanceAdminClient} to do admin operations on Cloud Spanner instances. */
+  /**
+   * Returns a {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} to execute
+   * admin operations on Cloud Spanner databases. This method always creates a new instance of
+   * {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} which is an {@link
+   * AutoCloseable} resource. For optimising the number of clients, caller may choose to cache the
+   * clients instead of repeatedly invoking this method and creating new instances.
+   *
+   * @return {@link com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient}
+   */
+  /*
+   * 
+   * 
{@code
+   * SpannerOptions options = SpannerOptions.newBuilder().build();
+   * Spanner spanner = options.getService();
+   * DatabaseAdminClient dbAdminClient = spanner.createDatabaseAdminClient();
+   * }
+ * + */ + default com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient + createDatabaseAdminClient() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Returns an {@code InstanceAdminClient} to execute admin operations on Cloud Spanner instances. + * + * @return {@code InstanceAdminClient} + */ /* * *
{@code
@@ -50,6 +82,29 @@ public interface Spanner extends Service, AutoCloseable {
    */
   InstanceAdminClient getInstanceAdminClient();
 
+  /**
+   * Returns a {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} to execute
+   * admin operations on Cloud Spanner databases. This method always creates a new instance of
+   * {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} which is an {@link
+   * AutoCloseable} resource. For optimising the number of clients, caller may choose to cache the
+   * clients instead of repeatedly invoking this method and creating new instances.
+   *
+   * @return {@link com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient}
+   */
+  /*
+   * 
+   * 
{@code
+   * SpannerOptions options = SpannerOptions.newBuilder().build();
+   * Spanner spanner = options.getService();
+   * InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient();
+   * }
+ * + */ + default com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient + createInstanceAdminClient() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns a {@code DatabaseClient} for the given database. It uses a pool of sessions to talk to * the database. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 8fe06f76cc8..2ab75d74174 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -25,6 +25,8 @@ import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.SessionClient.SessionId; import com.google.cloud.spanner.SpannerOptions.CloseableExecutorProvider; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.spi.v1.GapicSpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; @@ -40,6 +42,7 @@ import io.opencensus.trace.Tracing; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -207,11 +210,37 @@ public DatabaseAdminClient getDatabaseAdminClient() { return dbAdminClient; } + @Override + public com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient + createDatabaseAdminClient() { + try { + final DatabaseAdminStubSettings settings = + Preconditions.checkNotNull(gapicRpc.getDatabaseAdminStubSettings()); + return com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.create( + settings.createStub()); + } catch (IOException ex) { + throw SpannerExceptionFactory.newSpannerException(ex); + } + } + @Override public InstanceAdminClient getInstanceAdminClient() { return instanceClient; } + @Override + public com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient + createInstanceAdminClient() { + try { + final InstanceAdminStubSettings settings = + Preconditions.checkNotNull(gapicRpc.getInstanceAdminStubSettings()); + return com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient.create( + settings.createStub()); + } catch (IOException ex) { + throw SpannerExceptionFactory.newSpannerException(ex); + } + } + @Override public DatabaseClient getDatabaseClient(DatabaseId db) { synchronized (this) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index c9aa5987663..0f4b2275717 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -243,6 +243,7 @@ public class GapicSpannerRpc implements SpannerRpc { private final Set readRetryableCodes; private final SpannerStub partitionedDmlStub; private final RetrySettings partitionedDmlRetrySettings; + private final InstanceAdminStubSettings instanceAdminStubSettings; private final InstanceAdminStub instanceAdminStub; private final DatabaseAdminStubSettings databaseAdminStubSettings; private final DatabaseAdminStub databaseAdminStub; @@ -435,16 +436,15 @@ public GapicSpannerRpc(final SpannerOptions options) { .withCheckInterval(pdmlSettings.getStreamWatchdogCheckInterval())); } this.partitionedDmlStub = GrpcSpannerStub.create(pdmlSettings.build()); - - this.instanceAdminStub = - GrpcInstanceAdminStub.create( - options - .getInstanceAdminStubSettings() - .toBuilder() - .setTransportChannelProvider(channelProvider) - .setCredentialsProvider(credentialsProvider) - .setStreamWatchdogProvider(watchdogProvider) - .build()); + this.instanceAdminStubSettings = + options + .getInstanceAdminStubSettings() + .toBuilder() + .setTransportChannelProvider(channelProvider) + .setCredentialsProvider(credentialsProvider) + .setStreamWatchdogProvider(watchdogProvider) + .build(); + this.instanceAdminStub = GrpcInstanceAdminStub.create(instanceAdminStubSettings); this.databaseAdminStubSettings = options @@ -510,6 +510,7 @@ public UnaryCallable createUnaryCalla this.executeQueryRetryableCodes = null; this.partitionedDmlStub = null; this.databaseAdminStubSettings = null; + this.instanceAdminStubSettings = null; this.spannerWatchdog = null; this.partitionedDmlRetrySettings = null; } @@ -2004,6 +2005,16 @@ public boolean isClosed() { return rpcIsClosed; } + @Override + public DatabaseAdminStubSettings getDatabaseAdminStubSettings() { + return databaseAdminStubSettings; + } + + @Override + public InstanceAdminStubSettings getInstanceAdminStubSettings() { + return instanceAdminStubSettings; + } + private static final class GrpcStreamingCall implements StreamingCall { private final ApiCallContext callContext; private final StreamController controller; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 89659e4741e..7868f3ec099 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -28,7 +28,9 @@ import com.google.cloud.spanner.Restore; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.common.collect.ImmutableList; import com.google.iam.v1.GetPolicyOptions; @@ -500,4 +502,24 @@ TestIamPermissionsResponse testInstanceAdminIAMPermissions( void shutdown(); boolean isClosed(); + + /** + * Getter method to obtain the auto-generated instance admin client stub settings. + * + * @return InstanceAdminStubSettings + */ + @InternalApi + default InstanceAdminStubSettings getInstanceAdminStubSettings() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Getter method to obtain the auto-generated database admin client stub settings. + * + * @return DatabaseAdminStubSettings + */ + @InternalApi + default DatabaseAdminStubSettings getDatabaseAdminStubSettings() { + throw new UnsupportedOperationException("Not implemented"); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java index 31a6cad4c8a..3cf13dc58d3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.when; @@ -29,9 +30,16 @@ import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly; import com.google.cloud.spanner.SpannerImpl.ClosedException; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; +import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import io.opentelemetry.api.OpenTelemetry; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; @@ -55,6 +63,10 @@ public class SpannerImplTest { @Mock private SpannerRpc rpc; @Mock private SpannerOptions spannerOptions; + @Mock private DatabaseAdminStubSettings databaseAdminStubSettings; + @Mock private DatabaseAdminStub databaseAdminStub; + @Mock private InstanceAdminStubSettings instanceAdminStubSettings; + @Mock private InstanceAdminStub instanceAdminStub; private SpannerImpl impl; @Captor ArgumentCaptor> options; @@ -286,6 +298,66 @@ public void testClosedException() { assertThat(sw.toString()).contains("closeSpannerAndIncludeStacktrace"); } + @Test + public void testCreateDatabaseAdminClient_whenNullAdminSettings_assertPreconditionFailure() { + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + assertThrows(NullPointerException.class, spanner::createDatabaseAdminClient); + } + + @Test + public void testCreateDatabaseAdminClient_whenMockAdminSettings_assertMethodInvocation() + throws IOException { + when(rpc.getDatabaseAdminStubSettings()).thenReturn(databaseAdminStubSettings); + when(databaseAdminStubSettings.createStub()).thenReturn(databaseAdminStub); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient(); + assertNotNull(databaseAdminClient); + } + + @Test(expected = SpannerException.class) + public void testCreateDatabaseAdminClient_whenMockAdminSettings_assertException() + throws IOException { + when(rpc.getDatabaseAdminStubSettings()).thenReturn(databaseAdminStubSettings); + when(databaseAdminStubSettings.createStub()).thenThrow(IOException.class); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient(); + assertNotNull(databaseAdminClient); + } + + @Test + public void testCreateInstanceAdminClient_whenNullAdminSettings_assertPreconditionFailure() { + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + assertThrows(NullPointerException.class, spanner::createInstanceAdminClient); + } + + @Test + public void testCreateInstanceAdminClient_whenMockAdminSettings_assertMethodInvocation() + throws IOException { + when(rpc.getInstanceAdminStubSettings()).thenReturn(instanceAdminStubSettings); + when(instanceAdminStubSettings.createStub()).thenReturn(instanceAdminStub); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient(); + assertNotNull(instanceAdminClient); + } + + @Test(expected = SpannerException.class) + public void testCreateInstanceAdminClient_whenMockAdminSettings_assertException() + throws IOException { + when(rpc.getInstanceAdminStubSettings()).thenReturn(instanceAdminStubSettings); + when(instanceAdminStubSettings.createStub()).thenThrow(IOException.class); + + Spanner spanner = new SpannerImpl(rpc, spannerOptions); + + InstanceAdminClient instanceAdminClient = spanner.createInstanceAdminClient(); + assertNotNull(instanceAdminClient); + } + private void closeSpannerAndIncludeStacktrace(Spanner spanner) { spanner.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java new file mode 100644 index 00000000000..a1c6a35819f --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAutogeneratedAdminClientTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2024 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.it; + +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; + +import com.google.api.gax.rpc.PermissionDeniedException; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.InstanceName; +import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.ProjectName; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Integration tests for testing the auto-generated database admin {@link + * com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient} and instance admin clients {@link + * com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient} + */ +@Category(ParallelIntegrationTest.class) +@RunWith(Parameterized.class) +public class ITAutogeneratedAdminClientTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static DatabaseAdminClient dbAdminClient; + + private static InstanceAdminClient instanceAdminClient; + private static RemoteSpannerHelper testHelper; + + private static List databasesToDrop; + + @Parameter public DatabaseDialect dialect; + + @Parameters(name = "Dialect = {0}") + public static List data() { + return ImmutableList.of(DatabaseDialect.GOOGLE_STANDARD_SQL, DatabaseDialect.POSTGRESQL); + } + + @BeforeClass + public static void setUp() { + assumeFalse("Emulator does not support database roles", isUsingEmulator()); + testHelper = env.getTestHelper(); + dbAdminClient = testHelper.getClient().createDatabaseAdminClient(); + instanceAdminClient = testHelper.getClient().createInstanceAdminClient(); + databasesToDrop = new ArrayList<>(); + } + + @AfterClass + public static void cleanup() throws Exception { + if (databasesToDrop != null) { + for (DatabaseName databaseName : databasesToDrop) { + try { + dbAdminClient.dropDatabase(databaseName); + } catch (Exception e) { + System.err.println( + "Failed to drop database " + databaseName + ", skipping...: " + e.getMessage()); + } + } + } + } + + @Test + public void grantAndRevokeDatabaseRolePermissions() throws Exception { + // Create database with table and role permission. + final String dbRoleParent = "parent"; + final String databaseId = testHelper.getUniqueDatabaseId(); + final InstanceId instanceId = testHelper.getInstanceId(); + + final String createTableT = getCreateTableStatement(); + final String createRoleParent = String.format("CREATE ROLE %s", dbRoleParent); + final String grantSelectOnTableToParent = + dialect == DatabaseDialect.POSTGRESQL + ? String.format("GRANT SELECT ON TABLE T TO %s", dbRoleParent) + : String.format("GRANT SELECT ON TABLE T TO ROLE %s", dbRoleParent); + final Database createdDatabase = + createAndUpdateDatabase( + testHelper.getOptions().getProjectId(), + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleParent, grantSelectOnTableToParent)); + + // Connect to db with dbRoleParent. + SpannerOptions options = + testHelper.getOptions().toBuilder().setDatabaseRole(dbRoleParent).build(); + + Spanner spanner = options.getService(); + DatabaseId id = DatabaseId.of(createdDatabase.getName()); + DatabaseClient dbClient = spanner.getDatabaseClient(id); + + // Test SELECT permissions to role dbRoleParent on table T. + // Query using dbRoleParent should return result. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + assertTrue(rs.next()); + assertEquals(dbClient.getDatabaseRole(), dbRoleParent); + } catch (PermissionDeniedException e) { + // This is not expected + fail("Got PermissionDeniedException when it should not have occurred."); + } + + // Revoke select Permission for dbRoleParent. + final String revokeSelectOnTableFromParent = + dialect == DatabaseDialect.POSTGRESQL + ? String.format("REVOKE SELECT ON TABLE T FROM %s", dbRoleParent) + : String.format("REVOKE SELECT ON TABLE T FROM ROLE %s", dbRoleParent); + + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(revokeSelectOnTableFromParent)) + .get(5, TimeUnit.MINUTES); + + // Test SELECT permissions to role dbRoleParent on table T. + // Query using dbRoleParent should return PermissionDeniedException. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.PERMISSION_DENIED); + assertThat(e.getMessage()).contains(dbRoleParent); + } + // Drop role and table. + final String dropTableT = "DROP TABLE T"; + final String dropRoleParent = String.format("DROP ROLE %s", dbRoleParent); + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(dropTableT, dropRoleParent)) + .get(5, TimeUnit.MINUTES); + databasesToDrop.add(DatabaseName.parse(createdDatabase.getName())); + } + + @Test + public void roleWithNoPermissions() throws Exception { + final String dbRoleOrphan = testHelper.getUniqueDatabaseRole(); + final String databaseId = testHelper.getUniqueDatabaseId(); + final InstanceId instanceId = testHelper.getInstanceId(); + + final String createTableT = getCreateTableStatement(); + final String createRoleOrphan = String.format("CREATE ROLE %s", dbRoleOrphan); + + final Database createdDatabase = + createAndUpdateDatabase( + testHelper.getOptions().getProjectId(), + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleOrphan)); + + // Connect to db with dbRoleOrphan + SpannerOptions options = + testHelper.getOptions().toBuilder().setDatabaseRole(dbRoleOrphan).build(); + + Spanner spanner = options.getService(); + DatabaseId id = DatabaseId.of(createdDatabase.getName()); + DatabaseClient dbClient = spanner.getDatabaseClient(id); + + // Test SELECT permissions to role dbRoleOrphan on table T. + // Query using dbRoleOrphan should return PermissionDeniedException. + try (ResultSet rs = + dbClient.singleUse().executeQuery(Statement.of("SELECT COUNT(*) as cnt FROM T"))) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.PERMISSION_DENIED); + assertThat(e.getMessage()).contains(dbRoleOrphan); + } + // Drop role and table. + final String dropTableT = "DROP TABLE T"; + final String dropRoleParent = String.format("DROP ROLE %s", dbRoleOrphan); + dbAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(options.getProjectId(), instanceId.getInstance(), databaseId), + ImmutableList.of(dropTableT, dropRoleParent)) + .get(5, TimeUnit.MINUTES); + + databasesToDrop.add(DatabaseName.parse(createdDatabase.getName())); + } + + @Test + public void instanceConfigOperations() { + List configs = new ArrayList<>(); + Iterators.addAll( + configs, + instanceAdminClient + .listInstanceConfigs(ProjectName.of(testHelper.getOptions().getProjectId())) + .iterateAll() + .iterator()); + assertThat(configs.isEmpty()).isFalse(); + InstanceConfig config = instanceAdminClient.getInstanceConfig(configs.get(0).getName()); + assertThat(config.getName()).isEqualTo(configs.get(0).getName()); + } + + private Database createAndUpdateDatabase( + String projectId, + final InstanceId instanceId, + final String databaseId, + final List statements) + throws Exception { + if (dialect == DatabaseDialect.POSTGRESQL) { + // DDL statements other than are not allowed in database creation request + // for PostgreSQL-enabled databases. + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setParent(InstanceName.of(projectId, instanceId.getInstance()).toString()) + .setCreateStatement(getCreateDatabaseStatement(databaseId, dialect)) + .setDatabaseDialect(dialect) + .build(); + Database database = + dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(10, TimeUnit.MINUTES); + dbAdminClient.updateDatabaseDdlAsync(database.getName(), statements).get(5, TimeUnit.MINUTES); + return database; + } else { + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setParent(InstanceName.of(projectId, instanceId.getInstance()).toString()) + .setCreateStatement(getCreateDatabaseStatement(databaseId, dialect)) + .setDatabaseDialect(dialect) + .addAllExtraStatements(statements) + .build(); + return dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(10, TimeUnit.MINUTES); + } + } + + private String getCreateTableStatement() { + if (dialect == DatabaseDialect.POSTGRESQL) { + return "CREATE TABLE T (" + " \"K\" VARCHAR PRIMARY KEY" + ")"; + } else { + return "CREATE TABLE T (" + " K STRING(MAX)" + ") PRIMARY KEY (K)"; + } + } + + static String getCreateDatabaseStatement( + final String databaseName, final DatabaseDialect dialect) { + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + return "CREATE DATABASE `" + databaseName + "`"; + } else { + return "CREATE DATABASE \"" + databaseName + "\""; + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java index bbe23aff630..fb139dc89d7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java @@ -637,6 +637,37 @@ public void testCustomClientLibToken_alsoContainsDefaultToken() { Objects.requireNonNull(lastSeenHeaders.get(key)).contains("gl-java/")); } + @Test + public void testGetDatabaseAdminStubSettings_whenStubInitialized_assertNonNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, true); + + assertNotNull(rpc.getDatabaseAdminStubSettings()); + + rpc.shutdown(); + } + + @Test + public void testGetInstanceAdminStubSettings_whenStubInitialized_assertNonNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, true); + + assertNotNull(rpc.getInstanceAdminStubSettings()); + + rpc.shutdown(); + } + + @Test + public void testAdminStubSettings_whenStubNotInitialized_assertNullClientSetting() { + SpannerOptions options = createSpannerOptions(); + GapicSpannerRpc rpc = new GapicSpannerRpc(options, false); + + assertNull(rpc.getDatabaseAdminStubSettings()); + assertNull(rpc.getInstanceAdminStubSettings()); + + rpc.shutdown(); + } + private SpannerOptions createSpannerOptions() { String endpoint = address.getHostString() + ":" + server.getPort(); return SpannerOptions.newBuilder() From 3ef6728a0d6cbb0155cf7a8f43e2ee713cf68d11 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Wed, 14 Feb 2024 14:38:22 +0530 Subject: [PATCH 05/10] chore: add a new directory for archived samples of admin APIs. (#2886) --- .../src/main/java/com/example/spanner/admin/archived/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/archived/.gitkeep diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/archived/.gitkeep b/samples/snippets/src/main/java/com/example/spanner/admin/archived/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d From 9282ed6c8a900bde85f9b2ae089fd56d01436a67 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:44:15 +0000 Subject: [PATCH 06/10] chore(main): release 6.58.1-SNAPSHOT (#2867) :robot: I have created a release *beep* *boop* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- google-cloud-spanner-bom/pom.xml | 18 ++++++++--------- google-cloud-spanner-executor/pom.xml | 4 ++-- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-executor-v1/pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 20 +++++++++---------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 20 +++++++++---------- 14 files changed, 50 insertions(+), 50 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index fc5249803fc..d5a42eca56d 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 6.58.0 + 6.58.1-SNAPSHOT pom com.google.cloud @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.58.0 + 6.58.1-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index b0dda360aac..54c87fe72a6 100644 --- a/google-cloud-spanner-executor/pom.xml +++ b/google-cloud-spanner-executor/pom.xml @@ -5,14 +5,14 @@ 4.0.0 com.google.cloud google-cloud-spanner-executor - 6.58.0 + 6.58.1-SNAPSHOT jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 5792068ef23..9288da99d4b 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 6.58.0 + 6.58.1-SNAPSHOT jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index be7ce3b4dc7..2e4d216b980 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index b3645ac4f41..9fc4769ee08 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-executor-v1/pom.xml b/grpc-google-cloud-spanner-executor-v1/pom.xml index 1f9c0bb75c8..53b9b707946 100644 --- a/grpc-google-cloud-spanner-executor-v1/pom.xml +++ b/grpc-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.58.0 + 6.58.1-SNAPSHOT grpc-google-cloud-spanner-executor-v1 GRPC library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index e41d7181438..b8cf954216d 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 2329040546d..6e4b50744b1 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.58.0 + 6.58.1-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -61,47 +61,47 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT com.google.cloud google-cloud-spanner - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index afc8a9d036c..0f5038c9a48 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.0 + 6.58.1-SNAPSHOT proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 30c4451663c..719060986b5 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.0 + 6.58.1-SNAPSHOT proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-executor-v1/pom.xml b/proto-google-cloud-spanner-executor-v1/pom.xml index 4890cc004b3..291c4db5a02 100644 --- a/proto-google-cloud-spanner-executor-v1/pom.xml +++ b/proto-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.58.0 + 6.58.1-SNAPSHOT proto-google-cloud-spanner-executor-v1 Proto library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 0691dc941f4..a2f445500e3 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.0 + 6.58.1-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index a17883b0acc..41a6a0caad3 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 6.58.0 + 6.58.1-SNAPSHOT diff --git a/versions.txt b/versions.txt index 5bfa2637503..83b483b77d7 100644 --- a/versions.txt +++ b/versions.txt @@ -1,13 +1,13 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.0 -proto-google-cloud-spanner-v1:6.58.0:6.58.0 -proto-google-cloud-spanner-admin-database-v1:6.58.0:6.58.0 -grpc-google-cloud-spanner-v1:6.58.0:6.58.0 -grpc-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.0 -grpc-google-cloud-spanner-admin-database-v1:6.58.0:6.58.0 -google-cloud-spanner:6.58.0:6.58.0 -google-cloud-spanner-executor:6.58.0:6.58.0 -proto-google-cloud-spanner-executor-v1:6.58.0:6.58.0 -grpc-google-cloud-spanner-executor-v1:6.58.0:6.58.0 +proto-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.1-SNAPSHOT +proto-google-cloud-spanner-v1:6.58.0:6.58.1-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:6.58.0:6.58.1-SNAPSHOT +grpc-google-cloud-spanner-v1:6.58.0:6.58.1-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.1-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:6.58.0:6.58.1-SNAPSHOT +google-cloud-spanner:6.58.0:6.58.1-SNAPSHOT +google-cloud-spanner-executor:6.58.0:6.58.1-SNAPSHOT +proto-google-cloud-spanner-executor-v1:6.58.0:6.58.1-SNAPSHOT +grpc-google-cloud-spanner-executor-v1:6.58.0:6.58.1-SNAPSHOT From c8632f5b2f462420a8c2a1f4308a68a18a414472 Mon Sep 17 00:00:00 2001 From: surbhigarg92 Date: Thu, 15 Feb 2024 05:46:17 +0000 Subject: [PATCH 07/10] docs: README for OpenTelemetry metrics and traces (#2880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: README for OpenTelemetry metrics and traces * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .readme-partials.yaml | 234 +++++++++++++++++++++++++++++++++++++----- README.md | 234 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 412 insertions(+), 56 deletions(-) diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 940c2c3d1f9..5c4e1db63b7 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -50,55 +50,133 @@ custom_content: | for in-depth background information about sessions and gRPC channels and how these are handled in the Cloud Spanner Java client. - ## OpenCensus Metrics + ## Metrics - Cloud Spanner client supports [Opencensus Metrics](https://opencensus.io/stats/), - which gives insight into the client internals and aids in debugging/troubleshooting - production issues. OpenCensus metrics will provide you with enough data to enable you to - spot, and investigate the cause of any unusual deviations from normal behavior. - - All Cloud Spanner Metrics are prefixed with `cloud.google.com/java/spanner/`. The - metrics will be tagged with: - * `database`: the target database name. - * `instance_id`: the instance id of the target Spanner instance. - * `client_id`: the user defined database client id. - * `library_version`: the version of the library that you're using. - - > Note: RPC level metrics can be gleaned from gRPC’s metrics, which are prefixed - with `grpc.io/client/`. ### Available client-side metrics: - * `cloud.google.com/java/spanner/max_in_use_sessions`: This returns the maximum + * `spanner/max_in_use_sessions`: This returns the maximum number of sessions that have been in use during the last maintenance window interval, so as to provide an indication of the amount of activity currently in the database. - * `cloud.google.com/java/spanner/max_allowed_sessions`: This shows the maximum + * `spanner/max_allowed_sessions`: This shows the maximum number of sessions allowed. - * `cloud.google.com/java/spanner/num_sessions_in_pool`: This metric allows users to - see instance-level and database-level data for the total number of sessions in - the pool at this very moment. + * `spanner/num_sessions_in_pool`: This metric allows users to + see instance-level and database-level data for the total number of sessions in + the pool at this very moment. - * `cloud.google.com/java/spanner/num_acquired_sessions`: This metric allows + * `spanner/num_acquired_sessions`: This metric allows users to see the total number of acquired sessions. - * `cloud.google.com/java/spanner/num_released_sessions`: This metric allows + * `spanner/num_released_sessions`: This metric allows users to see the total number of released (destroyed) sessions. - * `cloud.google.com/java/spanner/get_session_timeouts`: This gives you an + * `spanner/get_session_timeouts`: This gives you an indication of the total number of get session timed-out instead of being granted (the thread that requested the session is placed in a wait queue where it waits until a session is released into the pool by another thread) due to pool exhaustion since the server process started. - * `cloud.google.com/java/spanner/gfe_latency`: This metric shows latency between + * `spanner/gfe_latency`: This metric shows latency between Google's network receiving an RPC and reading back the first byte of the response. - * `cloud.google.com/java/spanner/gfe_header_missing_count`: This metric shows the + * `spanner/gfe_header_missing_count`: This metric shows the number of RPC responses received without the server-timing header, most likely indicating that the RPC never reached Google's network. + ### Instrument with OpenTelemetry + + Cloud Spanner client supports [OpenTelemetry Metrics](https://opentelemetry.io/), + which gives insight into the client internals and aids in debugging/troubleshooting + production issues. OpenTelemetry metrics will provide you with enough data to enable you to + spot, and investigate the cause of any unusual deviations from normal behavior. + + All Cloud Spanner Metrics are prefixed with `spanner/` and uses `cloud.google.com/java` as [Instrumentation Scope](https://opentelemetry.io/docs/concepts/instrumentation-scope/). The + metrics will be tagged with: + * `database`: the target database name. + * `instance_id`: the instance id of the target Spanner instance. + * `client_id`: the user defined database client id. + + By default, the functionality is disabled. You need to add OpenTelemetry dependencies, enable OpenTelemetry metrics and must configure the OpenTelemetry with appropriate exporters at the startup of your application: + + #### OpenTelemetry Dependencies + If you are using Maven, add this to your pom.xml file + ```xml + + io.opentelemetry + opentelemetry-sdk + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-sdk-metrics + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-otlp + {opentelemetry.version} + + ``` + If you are using Gradle, add this to your dependencies + ```Groovy + compile 'io.opentelemetry:opentelemetry-sdk:{opentelemetry.version}' + compile 'io.opentelemetry:opentelemetry-sdk-metrics:{opentelemetry.version}' + compile 'io.opentelemetry:opentelemetry-exporter-oltp:{opentelemetry.version}' + ``` + + #### OpenTelemetry Configuration + By default, all metrics are disabled. To enable metrics and configure the OpenTelemetry follow below: + + ```java + // Enable OpenTelemetry metrics before injecting OpenTelemetry object. + SpannerOptions.enableOpenTelemetryMetrics(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + // Use Otlp exporter or any other exporter of your choice. + .registerMetricReader(PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().build()) + .build()) + .build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .build() + + SpannerOptions options = SpannerOptions.newBuilder() + // Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global + .setOpenTelemetry(openTelemetry) + .build(); + + Spanner spanner = options.getService(); + ``` + + ### Instrument with OpenCensus + + > Note: OpenCensus project is deprecated. See [Sunsetting OpenCensus](https://opentelemetry.io/blog/2023/sunsetting-opencensus/). + We recommend migrating to OpenTelemetry, the successor project. + + Cloud Spanner client supports [Opencensus Metrics](https://opencensus.io/stats/), + which gives insight into the client internals and aids in debugging/troubleshooting + production issues. OpenCensus metrics will provide you with enough data to enable you to + spot, and investigate the cause of any unusual deviations from normal behavior. + + All Cloud Spanner Metrics are prefixed with `cloud.google.com/java/spanner` + + The metrics are tagged with: + * `database`: the target database name. + * `instance_id`: the instance id of the target Spanner instance. + * `client_id`: the user defined database client id. + * `library_version`: the version of the library that you're using. + + + By default, the functionality is disabled. You need to include opencensus-impl + dependency to collect the data and exporter dependency to export to backend. + + [Click here](https://medium.com/google-cloud/troubleshooting-cloud-spanner-applications-with-opencensus-2cf424c4c590) for more information. + + #### OpenCensus Dependencies + If you are using Maven, add this to your pom.xml file ```xml @@ -119,6 +197,8 @@ custom_content: | compile 'io.opencensus:opencensus-exporter-stats-stackdriver:0.30.0' ``` + #### Configure the OpenCensus Exporter + At the start of your application configure the exporter: ```java @@ -130,9 +210,107 @@ custom_content: | // The minimum reporting period for Stackdriver is 1 minute. StackdriverStatsExporter.createAndRegister(); ``` + #### Enable RPC Views - By default, the functionality is disabled. You need to include opencensus-impl - dependency to collect the data and exporter dependency to export to backend. + By default, all session metrics are enabled. To enable RPC views, use either of the following method: - [Click here](https://medium.com/google-cloud/troubleshooting-cloud-spanner-applications-with-opencensus-2cf424c4c590) for more information. + ```java + // Register views for GFE metrics, including gfe_latency and gfe_header_missing_count. + SpannerRpcViews.registerGfeLatencyAndHeaderMissingCountViews(); + + // Register GFE Latency view. + SpannerRpcViews.registerGfeLatencyView(); + + // Register GFE Header Missing Count view. + SpannerRpcViews.registerGfeHeaderMissingCountView(); + ``` + + ## Traces + Cloud Spanner client supports OpenTelemetry Traces, which gives insight into the client internals and aids in debugging/troubleshooting production issues. + + By default, the functionality is disabled. You need to add OpenTelemetry dependencies, enable OpenTelemetry traces and must configure the OpenTelemetry with appropriate exporters at the startup of your application. + + #### OpenTelemetry Dependencies + + If you are using Maven, add this to your pom.xml file + ```xml + + io.opentelemetry + opentelemetry-sdk + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-sdk-trace + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-otlp + {opentelemetry.version} + + ``` + If you are using Gradle, add this to your dependencies + ```Groovy + compile 'io.opentelemetry:opentelemetry-sdk:{opentelemetry.version}' + compile 'io.opentelemetry:opentelemetry-sdk-trace:{opentelemetry.version}' + compile 'io.opentelemetry:opentelemetry-exporter-oltp:{opentelemetry.version}' + ``` + #### OpenTelemetry Configuration + + > Note: Enabling OpenTelemetry traces will automatically disable OpenCensus traces. + + ```java + // Enable OpenTelemetry traces + SpannerOptions.enableOpenTelemetryTraces(); + + // Create a new tracer provider + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + // Use Otlp exporter or any other exporter of your choice. + .addSpanProcessor(SimpleSpanProcessor.builder(OtlpGrpcSpanExporter + .builder().build()).build()) + .build(); + + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .build() + + SpannerOptions options = SpannerOptions.newBuilder() + // Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global + .setOpenTelemetry(openTelemetry) + .build(); + + Spanner spanner = options.getService(); + ``` + + ## Migrate from OpenCensus to OpenTelemetry + + > Using the [OpenTelemetry OpenCensus Bridge](https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-opencensus-shim), you can immediately begin exporting your metrics and traces with OpenTelemetry + + #### Disable OpenCensus metrics + Disable OpenCensus metrics for Spanner by including the following code if you still possess OpenCensus dependencies and exporter. + + ```java + SpannerOptions.disableOpenCensusMetrics(); + ``` + + #### Disable OpenCensus traces + Enabling OpenTelemetry traces for Spanner will automatically disable OpenCensus traces. + + ```java + SpannerOptions.enableOpenTelemetryTraces(); + ``` + + #### Remove OpenCensus Dependencies and Code + Remove any OpenCensus-related code and dependencies from your codebase if all your dependencies are ready to move to OpenTelemetry. + + * Remove the OpenCensus Exporters which were configured [here](#configure-the-opencensus-exporter) + * Remove SpannerRPCViews reference which were configured [here](#enable-rpc-views) + * Remove the OpenCensus dependencies which were added [here](#opencensus-dependencies) + + #### Update your Dashboards and Alerts + Update your dashboards and alerts to reflect below changes + * **Metrics name** : `cloud.google.com/java` prefix has been removed from OpenTelemery metrics and instead has been added as Instrumenation Scope. + * **Metrics namespace** : OpenTelmetry exporters uses `workload.googleapis.com` namespace opposed to `custom.googleapis.com` with OpenCensus. \ No newline at end of file diff --git a/README.md b/README.md index e70ee5eb3aa..82dca0538c2 100644 --- a/README.md +++ b/README.md @@ -156,55 +156,133 @@ See [Session Pool and Channel Pool Configuration](session-and-channel-pool-confi for in-depth background information about sessions and gRPC channels and how these are handled in the Cloud Spanner Java client. -## OpenCensus Metrics +## Metrics -Cloud Spanner client supports [Opencensus Metrics](https://opencensus.io/stats/), -which gives insight into the client internals and aids in debugging/troubleshooting -production issues. OpenCensus metrics will provide you with enough data to enable you to -spot, and investigate the cause of any unusual deviations from normal behavior. - -All Cloud Spanner Metrics are prefixed with `cloud.google.com/java/spanner/`. The -metrics will be tagged with: -* `database`: the target database name. -* `instance_id`: the instance id of the target Spanner instance. -* `client_id`: the user defined database client id. -* `library_version`: the version of the library that you're using. - -> Note: RPC level metrics can be gleaned from gRPC’s metrics, which are prefixed -with `grpc.io/client/`. ### Available client-side metrics: -* `cloud.google.com/java/spanner/max_in_use_sessions`: This returns the maximum +* `spanner/max_in_use_sessions`: This returns the maximum number of sessions that have been in use during the last maintenance window interval, so as to provide an indication of the amount of activity currently in the database. -* `cloud.google.com/java/spanner/max_allowed_sessions`: This shows the maximum +* `spanner/max_allowed_sessions`: This shows the maximum number of sessions allowed. -* `cloud.google.com/java/spanner/num_sessions_in_pool`: This metric allows users to - see instance-level and database-level data for the total number of sessions in - the pool at this very moment. +* `spanner/num_sessions_in_pool`: This metric allows users to + see instance-level and database-level data for the total number of sessions in + the pool at this very moment. -* `cloud.google.com/java/spanner/num_acquired_sessions`: This metric allows +* `spanner/num_acquired_sessions`: This metric allows users to see the total number of acquired sessions. -* `cloud.google.com/java/spanner/num_released_sessions`: This metric allows +* `spanner/num_released_sessions`: This metric allows users to see the total number of released (destroyed) sessions. -* `cloud.google.com/java/spanner/get_session_timeouts`: This gives you an +* `spanner/get_session_timeouts`: This gives you an indication of the total number of get session timed-out instead of being granted (the thread that requested the session is placed in a wait queue where it waits until a session is released into the pool by another thread) due to pool exhaustion since the server process started. -* `cloud.google.com/java/spanner/gfe_latency`: This metric shows latency between +* `spanner/gfe_latency`: This metric shows latency between Google's network receiving an RPC and reading back the first byte of the response. -* `cloud.google.com/java/spanner/gfe_header_missing_count`: This metric shows the +* `spanner/gfe_header_missing_count`: This metric shows the number of RPC responses received without the server-timing header, most likely indicating that the RPC never reached Google's network. +### Instrument with OpenTelemetry + +Cloud Spanner client supports [OpenTelemetry Metrics](https://opentelemetry.io/), +which gives insight into the client internals and aids in debugging/troubleshooting +production issues. OpenTelemetry metrics will provide you with enough data to enable you to +spot, and investigate the cause of any unusual deviations from normal behavior. + +All Cloud Spanner Metrics are prefixed with `spanner/` and uses `cloud.google.com/java` as [Instrumentation Scope](https://opentelemetry.io/docs/concepts/instrumentation-scope/). The +metrics will be tagged with: +* `database`: the target database name. +* `instance_id`: the instance id of the target Spanner instance. +* `client_id`: the user defined database client id. + +By default, the functionality is disabled. You need to add OpenTelemetry dependencies, enable OpenTelemetry metrics and must configure the OpenTelemetry with appropriate exporters at the startup of your application: + +#### OpenTelemetry Dependencies +If you are using Maven, add this to your pom.xml file +```xml + + io.opentelemetry + opentelemetry-sdk + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-sdk-metrics + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-otlp + {opentelemetry.version} + +``` +If you are using Gradle, add this to your dependencies +```Groovy +compile 'io.opentelemetry:opentelemetry-sdk:{opentelemetry.version}' +compile 'io.opentelemetry:opentelemetry-sdk-metrics:{opentelemetry.version}' +compile 'io.opentelemetry:opentelemetry-exporter-oltp:{opentelemetry.version}' +``` + +#### OpenTelemetry Configuration +By default, all metrics are disabled. To enable metrics and configure the OpenTelemetry follow below: + +```java +// Enable OpenTelemetry metrics before injecting OpenTelemetry object. +SpannerOptions.enableOpenTelemetryMetrics(); + +SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() +// Use Otlp exporter or any other exporter of your choice. + .registerMetricReader(PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().build()) + .build()) + .build(); + +OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .build() + +SpannerOptions options = SpannerOptions.newBuilder() +// Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global + .setOpenTelemetry(openTelemetry) + .build(); + +Spanner spanner = options.getService(); +``` + +### Instrument with OpenCensus + +> Note: OpenCensus project is deprecated. See [Sunsetting OpenCensus](https://opentelemetry.io/blog/2023/sunsetting-opencensus/). +We recommend migrating to OpenTelemetry, the successor project. + +Cloud Spanner client supports [Opencensus Metrics](https://opencensus.io/stats/), +which gives insight into the client internals and aids in debugging/troubleshooting +production issues. OpenCensus metrics will provide you with enough data to enable you to +spot, and investigate the cause of any unusual deviations from normal behavior. + +All Cloud Spanner Metrics are prefixed with `cloud.google.com/java/spanner` + +The metrics are tagged with: +* `database`: the target database name. +* `instance_id`: the instance id of the target Spanner instance. +* `client_id`: the user defined database client id. +* `library_version`: the version of the library that you're using. + + +By default, the functionality is disabled. You need to include opencensus-impl +dependency to collect the data and exporter dependency to export to backend. + +[Click here](https://medium.com/google-cloud/troubleshooting-cloud-spanner-applications-with-opencensus-2cf424c4c590) for more information. + +#### OpenCensus Dependencies + If you are using Maven, add this to your pom.xml file ```xml @@ -225,6 +303,8 @@ compile 'io.opencensus:opencensus-impl:0.30.0' compile 'io.opencensus:opencensus-exporter-stats-stackdriver:0.30.0' ``` +#### Configure the OpenCensus Exporter + At the start of your application configure the exporter: ```java @@ -236,12 +316,110 @@ import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter; // The minimum reporting period for Stackdriver is 1 minute. StackdriverStatsExporter.createAndRegister(); ``` +#### Enable RPC Views -By default, the functionality is disabled. You need to include opencensus-impl -dependency to collect the data and exporter dependency to export to backend. +By default, all session metrics are enabled. To enable RPC views, use either of the following method: -[Click here](https://medium.com/google-cloud/troubleshooting-cloud-spanner-applications-with-opencensus-2cf424c4c590) for more information. +```java +// Register views for GFE metrics, including gfe_latency and gfe_header_missing_count. +SpannerRpcViews.registerGfeLatencyAndHeaderMissingCountViews(); + +// Register GFE Latency view. +SpannerRpcViews.registerGfeLatencyView(); + +// Register GFE Header Missing Count view. +SpannerRpcViews.registerGfeHeaderMissingCountView(); +``` + +## Traces +Cloud Spanner client supports OpenTelemetry Traces, which gives insight into the client internals and aids in debugging/troubleshooting production issues. + +By default, the functionality is disabled. You need to add OpenTelemetry dependencies, enable OpenTelemetry traces and must configure the OpenTelemetry with appropriate exporters at the startup of your application. + +#### OpenTelemetry Dependencies + +If you are using Maven, add this to your pom.xml file +```xml + + io.opentelemetry + opentelemetry-sdk + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-sdk-trace + {opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-otlp + {opentelemetry.version} + +``` +If you are using Gradle, add this to your dependencies +```Groovy +compile 'io.opentelemetry:opentelemetry-sdk:{opentelemetry.version}' +compile 'io.opentelemetry:opentelemetry-sdk-trace:{opentelemetry.version}' +compile 'io.opentelemetry:opentelemetry-exporter-oltp:{opentelemetry.version}' +``` +#### OpenTelemetry Configuration + +> Note: Enabling OpenTelemetry traces will automatically disable OpenCensus traces. + +```java +// Enable OpenTelemetry traces +SpannerOptions.enableOpenTelemetryTraces(); + +// Create a new tracer provider +SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + // Use Otlp exporter or any other exporter of your choice. + .addSpanProcessor(SimpleSpanProcessor.builder(OtlpGrpcSpanExporter + .builder().build()).build()) + .build(); + + +OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .build() + +SpannerOptions options = SpannerOptions.newBuilder() +// Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global + .setOpenTelemetry(openTelemetry) + .build(); + +Spanner spanner = options.getService(); +``` + +## Migrate from OpenCensus to OpenTelemetry + +> Using the [OpenTelemetry OpenCensus Bridge](https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-opencensus-shim), you can immediately begin exporting your metrics and traces with OpenTelemetry + +#### Disable OpenCensus metrics +Disable OpenCensus metrics for Spanner by including the following code if you still possess OpenCensus dependencies and exporter. + +```java +SpannerOptions.disableOpenCensusMetrics(); +``` + +#### Disable OpenCensus traces +Enabling OpenTelemetry traces for Spanner will automatically disable OpenCensus traces. + +```java +SpannerOptions.enableOpenTelemetryTraces(); +``` + +#### Remove OpenCensus Dependencies and Code +Remove any OpenCensus-related code and dependencies from your codebase if all your dependencies are ready to move to OpenTelemetry. + +* Remove the OpenCensus Exporters which were configured [here](#configure-the-opencensus-exporter) +* Remove SpannerRPCViews reference which were configured [here](#enable-rpc-views) +* Remove the OpenCensus dependencies which were added [here](#opencensus-dependencies) + +#### Update your Dashboards and Alerts +Update your dashboards and alerts to reflect below changes +* **Metrics name** : `cloud.google.com/java` prefix has been removed from OpenTelemery metrics and instead has been added as Instrumenation Scope. +* **Metrics namespace** : OpenTelmetry exporters uses `workload.googleapis.com` namespace opposed to `custom.googleapis.com` with OpenCensus. From 14ae01cd82e455a0dc22d7e3bb8c362e541ede12 Mon Sep 17 00:00:00 2001 From: Arpan Mishra Date: Thu, 15 Feb 2024 13:52:16 +0530 Subject: [PATCH 08/10] docs: samples and tests for database Admin APIs. (#2775) Adds samples and tests for auto-generated Database Admin APIs. --- README.md | 23 ++- samples/pom.xml | 1 + .../generated/AddAndDropDatabaseRole.java | 75 +++++++++ .../admin/generated/AddJsonColumnSample.java | 49 ++++++ .../admin/generated/AddJsonbColumnSample.java | 50 ++++++ .../generated/AddNumericColumnSample.java | 50 ++++++ .../admin/generated/AlterSequenceSample.java | 98 +++++++++++ ...ableWithForeignKeyDeleteCascadeSample.java | 56 +++++++ ...CreateDatabaseWithDefaultLeaderSample.java | 76 --------- ...abaseWithVersionRetentionPeriodSample.java | 71 ++++++++ .../generated/CreateInstanceExample.java | 56 +++---- .../admin/generated/CreateSequenceSample.java | 102 ++++++++++++ ...ableWithForeignKeyDeleteCascadeSample.java | 63 ++++++++ ...reignKeyConstraintDeleteCascadeSample.java | 53 ++++++ .../admin/generated/DropSequenceSample.java | 66 ++++++++ .../generated/EnableFineGrainedAccess.java | 106 ++++++++++++ .../admin/generated/GetDatabaseDdlSample.java | 49 ++++++ .../admin/generated/ListDatabaseRoles.java | 52 ++++++ .../admin/generated/ListDatabasesSample.java | 54 +++++++ .../generated/PgAlterSequenceSample.java | 92 +++++++++++ .../generated/PgCaseSensitivitySample.java | 153 ++++++++++++++++++ .../generated/PgCreateSequenceSample.java | 99 ++++++++++++ .../admin/generated/PgDropSequenceSample.java | 71 ++++++++ .../generated/PgInterleavedTableSample.java | 72 +++++++++ .../admin/generated/UpdateDatabaseSample.java | 76 +++++++++ ...UpdateDatabaseWithDefaultLeaderSample.java | 69 ++++++++ ...leWithForeignKeyDeleteCascadeSampleIT.java | 70 ++++++++ ...eateDatabaseWithDefaultLeaderSampleIT.java | 61 ------- ...aseWithVersionRetentionPeriodSampleIT.java | 47 ++++++ ...leWithForeignKeyDeleteCascadeSampleIT.java | 57 +++++++ .../admin/generated/DatabaseRolesIT.java | 126 +++++++++++++++ ...ignKeyConstraintDeleteCascadeSampleIT.java | 68 ++++++++ .../generated/GetDatabaseDdlSampleIT.java | 82 ++++++++++ ...anceSampleIT.java => ListDatabasesIT.java} | 24 ++- .../generated/PgCaseSensitivitySampleIT.java | 45 ++++++ .../generated/PgInterleavedTableSampleIT.java | 44 +++++ .../admin/generated/SampleTestBaseV2.java | 15 +- .../admin/generated/SequenceSampleIT.java | 142 ++++++++++++++++ .../generated/UpdateDatabaseSampleIT.java | 71 ++++++++ ...dateDatabaseWithDefaultLeaderSampleIT.java | 63 ++++++++ 40 files changed, 2515 insertions(+), 182 deletions(-) create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java delete mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSampleIT.java delete mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/DatabaseRolesIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/GetDatabaseDdlSampleIT.java rename samples/snippets/src/test/java/com/example/spanner/admin/generated/{CreateInstanceSampleIT.java => ListDatabasesIT.java} (57%) create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/PgCaseSensitivitySampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/PgInterleavedTableSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/SequenceSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseSampleIT.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSampleIT.java diff --git a/README.md b/README.md index 82dca0538c2..bced4b3972e 100644 --- a/README.md +++ b/README.md @@ -509,15 +509,36 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/ | Update Jsonb Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateJsonbDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateJsonbDataSample.java) | | Update Numeric Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateNumericDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateNumericDataSample.java) | | Update Using Dml Returning Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateUsingDmlReturningSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateUsingDmlReturningSample.java) | -| Create Database With Default Leader Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java) | +| Add And Drop Database Role | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java) | +| Add Json Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java) | +| Add Jsonb Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java) | +| Add Numeric Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java) | +| Alter Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java) | +| Alter Table With Foreign Key Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java) | +| Create Database With Version Retention Period Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java) | | Create Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceConfigSample.java) | | Create Instance Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java) | | Create Instance With Autoscaling Config Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceWithAutoscalingConfigExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceWithAutoscalingConfigExample.java) | | Create Instance With Processing Units Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceWithProcessingUnitsExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceWithProcessingUnitsExample.java) | +| Create Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java) | +| Create Table With Foreign Key Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java) | | Delete Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/DeleteInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/DeleteInstanceConfigSample.java) | +| Drop Foreign Key Constraint Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java) | +| Drop Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java) | +| Enable Fine Grained Access | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java) | +| Get Database Ddl Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java) | | Get Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/GetInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/GetInstanceConfigSample.java) | +| List Database Roles | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java) | +| List Databases Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java) | | List Instance Config Operations Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListInstanceConfigOperationsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/ListInstanceConfigOperationsSample.java) | | List Instance Configs Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListInstanceConfigsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/ListInstanceConfigsSample.java) | +| Pg Alter Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java) | +| Pg Case Sensitivity Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java) | +| Pg Create Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java) | +| Pg Drop Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java) | +| Pg Interleaved Table Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java) | +| Update Database Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java) | +| Update Database With Default Leader Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java) | | Update Instance Config Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateInstanceConfigSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateInstanceConfigSample.java) | diff --git a/samples/pom.xml b/samples/pom.xml index be994d055bd..07b17a608cd 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -19,6 +19,7 @@ com.google.cloud.samples shared-configuration 1.2.0 + diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java new file mode 100644 index 00000000000..d88fe72dc8f --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddAndDropDatabaseRole.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_add_and_drop_database_role] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class AddAndDropDatabaseRole { + + static void addAndDropDatabaseRole() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String parentRole = "parent_role"; + String childRole = "child_role"; + addAndDropDatabaseRole(projectId, instanceId, databaseId, parentRole, childRole); + } + + static void addAndDropDatabaseRole( + String projectId, String instanceId, String databaseId, String parentRole, String childRole) + throws IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + try { + System.out.println("Waiting for role create operation to complete..."); + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + String.format("CREATE ROLE %s", parentRole), + String.format("GRANT SELECT ON TABLE Albums TO ROLE %s", parentRole), + String.format("CREATE ROLE %s", childRole), + String.format("GRANT ROLE %s TO ROLE %s", parentRole, childRole))) + .get(5, TimeUnit.MINUTES); + System.out.printf( + "Created roles %s and %s and granted privileges%n", parentRole, childRole); + // Delete role and membership. + System.out.println("Waiting for role revoke & drop operation to complete..."); + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + String.format("REVOKE ROLE %s FROM ROLE %s", parentRole, childRole), + String.format("DROP ROLE %s", childRole))).get(5, TimeUnit.MINUTES); + System.out.printf("Revoked privileges and dropped role %s%n", childRole); + } catch (ExecutionException | TimeoutException e) { + System.out.printf( + "Error: AddAndDropDatabaseRole failed with error message %s\n", e.getMessage()); + e.printStackTrace(); + } catch (InterruptedException e) { + System.out.println( + "Error: Waiting for AddAndDropDatabaseRole operation to finish was interrupted"); + } + } +} +// [END spanner_add_and_drop_database_role] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java new file mode 100644 index 00000000000..32d1daea2d1 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonColumnSample.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_add_json_column] +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +class AddJsonColumnSample { + + static void addJsonColumn() throws InterruptedException, ExecutionException, IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + addJsonColumn(projectId, instanceId, databaseId); + } + + static void addJsonColumn(String projectId, String instanceId, String databaseId) + throws InterruptedException, ExecutionException, IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + // Wait for the operation to finish. + // This will throw an ExecutionException if the operation fails. + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of("ALTER TABLE Venues ADD COLUMN VenueDetails JSON")).get(); + System.out.printf("Successfully added column `VenueDetails`%n"); + } +} +// [END spanner_add_json_column] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java new file mode 100644 index 00000000000..800c2d3d655 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddJsonbColumnSample.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_postgresql_jsonb_add_column] +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +class AddJsonbColumnSample { + + static void addJsonbColumn() throws InterruptedException, ExecutionException, IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + addJsonbColumn(projectId, instanceId, databaseId); + } + + static void addJsonbColumn(String projectId, String instanceId, String databaseId) + throws InterruptedException, ExecutionException, IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + // JSONB datatype is only supported with PostgreSQL-dialect databases. + // Wait for the operation to finish. + // This will throw an ExecutionException if the operation fails. + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of("ALTER TABLE Venues ADD COLUMN VenueDetails JSONB")).get(); + System.out.printf("Successfully added column `VenueDetails`%n"); + } +} +// [END spanner_postgresql_jsonb_add_column] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java new file mode 100644 index 00000000000..191e0377b66 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AddNumericColumnSample.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_add_numeric_column] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +class AddNumericColumnSample { + + static void addNumericColumn() throws InterruptedException, ExecutionException, IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + addNumericColumn(projectId, instanceId, databaseId); + } + + static void addNumericColumn(String projectId, String instanceId, String databaseId) + throws InterruptedException, ExecutionException, IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + // Wait for the operation to finish. + // This will throw an ExecutionException if the operation fails. + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of("ALTER TABLE Venues ADD COLUMN Revenue NUMERIC")).get(); + System.out.printf("Successfully added column `Revenue`%n"); + } +} +// [END spanner_add_numeric_column] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java new file mode 100644 index 00000000000..05ae63a7a53 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterSequenceSample.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_alter_sequence] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class AlterSequenceSample { + + static void alterSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + alterSequence(projectId, instanceId, databaseId); + } + + static void alterSequence(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + + databaseAdminClient + .updateDatabaseDdlAsync(DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "ALTER SEQUENCE Seq SET OPTIONS " + + "(skip_range_min = 1000, skip_range_max = 5000000)")) + .get(5, TimeUnit.MINUTES); + + System.out.println( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000"); + + final DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + + Long insertCount = + dbClient + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = + transaction.executeQuery( + Statement.of( + "INSERT INTO Customers (CustomerName) VALUES " + + "('Lea'), ('Catalina'), ('Smith') " + + "THEN RETURN CustomerId"))) { + while (rs.next()) { + System.out.printf( + "Inserted customer record with CustomerId: %d\n", rs.getLong(0)); + } + return Objects.requireNonNull(rs.getStats()).getRowCountExact(); + } + }); + System.out.printf("Number of customer records inserted is: %d\n", insertCount); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_alter_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java new file mode 100644 index 00000000000..5784bfab0c7 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSample.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_alter_table_with_foreign_key_delete_cascade] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; + +class AlterTableWithForeignKeyDeleteCascadeSample { + + static void alterForeignKeyDeleteCascadeConstraint() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + alterForeignKeyDeleteCascadeConstraint(projectId, instanceId, databaseId); + } + + static void alterForeignKeyDeleteCascadeConstraint( + String projectId, String instanceId, String databaseId) throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + databaseAdminClient.updateDatabaseDdlAsync(DatabaseName.of(projectId, instanceId, + databaseId), + ImmutableList.of( + "ALTER TABLE ShoppingCarts\n" + + " ADD CONSTRAINT FKShoppingCartsCustomerName\n" + + " FOREIGN KEY (CustomerName)\n" + + " REFERENCES Customers(CustomerName)\n" + + " ON DELETE CASCADE\n")); + System.out.printf( + String.format( + "Altered ShoppingCarts table with FKShoppingCartsCustomerName\n" + + "foreign key constraint on database %s on instance %s", + databaseId, instanceId)); + } +} +// [END spanner_alter_table_with_foreign_key_delete_cascade] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java deleted file mode 100644 index 853ec557b94..00000000000 --- a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSample.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2023 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.example.spanner.admin.generated; - -//[START spanner_create_database_with_default_leader] - -import com.google.cloud.spanner.SpannerException; -import com.google.cloud.spanner.SpannerExceptionFactory; -import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; -import com.google.common.collect.ImmutableList; -import com.google.spanner.admin.database.v1.CreateDatabaseRequest; -import com.google.spanner.admin.database.v1.Database; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -public class CreateDatabaseWithDefaultLeaderSample { - - static void createDatabaseWithDefaultLeader() throws IOException { - // TODO(developer): Replace these variables before running the sample. - final String instanceName = "projects/my-project/instances/my-instance-id"; - final String databaseId = "my-database-name"; - final String defaultLeader = "my-default-leader"; - createDatabaseWithDefaultLeader(instanceName, databaseId, defaultLeader); - } - - static void createDatabaseWithDefaultLeader(String instanceName, String databaseId, - String defaultLeader) throws IOException { - try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) { - Database createdDatabase = - databaseAdminClient.createDatabaseAsync( - CreateDatabaseRequest.newBuilder() - .setParent(instanceName) - .setCreateStatement("CREATE DATABASE `" + databaseId + "`") - .addAllExtraStatements( - ImmutableList.of("CREATE TABLE Singers (" - + " SingerId INT64 NOT NULL," - + " FirstName STRING(1024)," - + " LastName STRING(1024)," - + " SingerInfo BYTES(MAX)" - + ") PRIMARY KEY (SingerId)", - "CREATE TABLE Albums (" - + " SingerId INT64 NOT NULL," - + " AlbumId INT64 NOT NULL," - + " AlbumTitle STRING(MAX)" - + ") PRIMARY KEY (SingerId, AlbumId)," - + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE", - "ALTER DATABASE " + "`" + databaseId + "`" - + " SET OPTIONS ( default_leader = '" + defaultLeader + "' )")) - .build()).get(); - System.out.println("Created database [" + createdDatabase.getName() + "]"); - System.out.println("\tDefault leader: " + createdDatabase.getDefaultLeader()); - } catch (ExecutionException e) { - // If the operation failed during execution, expose the cause. - throw (SpannerException) e.getCause(); - } catch (InterruptedException e) { - // Throw when a thread is waiting, sleeping, or otherwise occupied, - // and the thread is interrupted, either before or during the activity. - throw SpannerExceptionFactory.propagateInterrupt(e); - } - } -} -//[END spanner_create_database_with_default_leader] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java new file mode 100644 index 00000000000..8d26f1ced56 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSample.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +// [START spanner_create_database_with_version_retention_period] + +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.Lists; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.InstanceName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +public class CreateDatabaseWithVersionRetentionPeriodSample { + + static void createDatabaseWithVersionRetentionPeriod() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String versionRetentionPeriod = "7d"; + + createDatabaseWithVersionRetentionPeriod(projectId, instanceId, databaseId, + versionRetentionPeriod); + } + + static void createDatabaseWithVersionRetentionPeriod(String projectId, + String instanceId, String databaseId, String versionRetentionPeriod) throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try { + CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setParent(InstanceName.of(projectId, instanceId).toString()) + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .addAllExtraStatements(Lists.newArrayList("ALTER DATABASE " + "`" + databaseId + "`" + + " SET OPTIONS ( version_retention_period = '" + versionRetentionPeriod + "' )")) + .build(); + Database database = + databaseAdminClient.createDatabaseAsync(request).get(); + System.out.println("Created database [" + database.getName() + "]"); + System.out.println("\tVersion retention period: " + database.getVersionRetentionPeriod()); + System.out.println("\tEarliest version time: " + database.getEarliestVersionTime()); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} +// [END spanner_create_database_with_version_retention_period] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java index 8664c85b444..925b0984992 100644 --- a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateInstanceExample.java @@ -36,35 +36,35 @@ static void createInstance() throws IOException { } static void createInstance(String projectId, String instanceId) throws IOException { - try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) { - // Set Instance configuration. - int nodeCount = 2; - String displayName = "Descriptive name"; + InstanceAdminClient instanceAdminClient = InstanceAdminClient.create(); - // Create an Instance object that will be used to create the instance. - Instance instance = - Instance.newBuilder() - .setDisplayName(displayName) - .setNodeCount(nodeCount) - .setConfig( - InstanceConfigName.of(projectId, "regional-us-central1").toString()) - .build(); - try { - // Wait for the createInstance operation to finish. - Instance createdInstance = instanceAdminClient.createInstanceAsync( - CreateInstanceRequest.newBuilder() - .setParent(ProjectName.of(projectId).toString()) - .setInstanceId(instanceId) - .setInstance(instance) - .build()).get(); - System.out.printf("Instance %s was successfully created%n", createdInstance.getName()); - } catch (ExecutionException e) { - System.out.printf( - "Error: Creating instance %s failed with error message %s%n", - instance.getName(), e.getMessage()); - } catch (InterruptedException e) { - System.out.println("Error: Waiting for createInstance operation to finish was interrupted"); - } + // Set Instance configuration. + int nodeCount = 2; + String displayName = "Descriptive name"; + + // Create an Instance object that will be used to create the instance. + Instance instance = + Instance.newBuilder() + .setDisplayName(displayName) + .setNodeCount(nodeCount) + .setConfig( + InstanceConfigName.of(projectId, "regional-us-central1").toString()) + .build(); + try { + // Wait for the createInstance operation to finish. + Instance createdInstance = instanceAdminClient.createInstanceAsync( + CreateInstanceRequest.newBuilder() + .setParent(ProjectName.of(projectId).toString()) + .setInstanceId(instanceId) + .setInstance(instance) + .build()).get(); + System.out.printf("Instance %s was successfully created%n", createdInstance.getName()); + } catch (ExecutionException e) { + System.out.printf( + "Error: Creating instance %s failed with error message %s%n", + instance.getName(), e.getMessage()); + } catch (InterruptedException e) { + System.out.println("Error: Waiting for createInstance operation to finish was interrupted"); } } } diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java new file mode 100644 index 00000000000..6614d988b78 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateSequenceSample.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_create_sequence] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class CreateSequenceSample { + + static void createSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + createSequence(projectId, instanceId, databaseId); + } + + static void createSequence(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + + databaseAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "CREATE SEQUENCE Seq OPTIONS (sequence_kind = 'bit_reversed_positive')", + "CREATE TABLE Customers (CustomerId INT64 DEFAULT " + + "(GET_NEXT_SEQUENCE_VALUE(SEQUENCE Seq)), CustomerName STRING(1024)) " + + "PRIMARY KEY (CustomerId)")) + .get(5, TimeUnit.MINUTES); + + System.out.println( + "Created Seq sequence and Customers table, where the key column CustomerId " + + "uses the sequence as a default value"); + + final DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + + Long insertCount = + dbClient + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = + transaction.executeQuery( + Statement.of( + "INSERT INTO Customers (CustomerName) VALUES " + + "('Alice'), ('David'), ('Marc') THEN RETURN CustomerId"))) { + while (rs.next()) { + System.out.printf( + "Inserted customer record with CustomerId: %d\n", rs.getLong(0)); + } + return Objects.requireNonNull(rs.getStats()).getRowCountExact(); + } + }); + System.out.printf("Number of customer records inserted is: %d\n", insertCount); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_create_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java new file mode 100644 index 00000000000..cc46f1e214e --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSample.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_create_table_with_foreign_key_delete_cascade] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; + +class CreateTableWithForeignKeyDeleteCascadeSample { + + static void createForeignKeyDeleteCascadeConstraint() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + createForeignKeyDeleteCascadeConstraint(projectId, instanceId, databaseId); + } + + static void createForeignKeyDeleteCascadeConstraint( + String projectId, String instanceId, String databaseId) throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerId FOREIGN KEY (CustomerId)\n" + + " REFERENCES Customers (CustomerId) ON DELETE CASCADE\n" + + " ) PRIMARY KEY (CartId)\n")); + + System.out.printf( + String.format( + "Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId\n" + + "foreign key constraint on database %s on instance %s\n", + databaseId, instanceId)); + } +} +// [END spanner_create_table_with_foreign_key_delete_cascade] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java new file mode 100644 index 00000000000..7d569acea8d --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSample.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_drop_foreign_key_constraint_delete_cascade] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; + +class DropForeignKeyConstraintDeleteCascadeSample { + + static void deleteForeignKeyDeleteCascadeConstraint() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + + deleteForeignKeyDeleteCascadeConstraint(projectId, instanceId, databaseId); + } + + static void deleteForeignKeyDeleteCascadeConstraint( + String projectId, String instanceId, String databaseId) throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "ALTER TABLE ShoppingCarts\n" + + " DROP CONSTRAINT FKShoppingCartsCustomerName\n")); + + System.out.printf( + String.format( + "Altered ShoppingCarts table to drop FKShoppingCartsCustomerName\n" + + "foreign key constraint on database %s on instance %s\n", + databaseId, instanceId)); + } +} +// [END spanner_drop_foreign_key_constraint_delete_cascade] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java new file mode 100644 index 00000000000..f9917677de1 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/DropSequenceSample.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_drop_sequence] + +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class DropSequenceSample { + + static void dropSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + dropSequence(projectId, instanceId, databaseId); + } + + static void dropSequence(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + try { + databaseAdminClient + .updateDatabaseDdlAsync(DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq")) + .get(5, TimeUnit.MINUTES); + System.out.println( + "Altered Customers table to drop DEFAULT from CustomerId column " + + "and dropped the Seq sequence"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_drop_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java new file mode 100644 index 00000000000..7f66992cad1 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/EnableFineGrainedAccess.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_enable_fine_grained_access] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.iam.v1.Binding; +import com.google.iam.v1.GetIamPolicyRequest; +import com.google.iam.v1.GetPolicyOptions; +import com.google.iam.v1.Policy; +import com.google.iam.v1.SetIamPolicyRequest; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.type.Expr; +import java.io.IOException; + +public class EnableFineGrainedAccess { + + static void enableFineGrainedAccess() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + String iamMember = "user:alice@example.com"; + String role = "my-role"; + String title = "my-condition-title"; + enableFineGrainedAccess(projectId, instanceId, databaseId, iamMember, title, role); + } + + static void enableFineGrainedAccess( + String projectId, + String instanceId, + String databaseId, + String iamMember, + String title, + String role) throws IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + final GetPolicyOptions options = + GetPolicyOptions.newBuilder().setRequestedPolicyVersion(3).build(); + final GetIamPolicyRequest getRequest = + GetIamPolicyRequest.newBuilder() + .setResource(DatabaseName.of(projectId, instanceId, databaseId).toString()) + .setOptions(options).build(); + final Policy policy = databaseAdminClient.getIamPolicy(getRequest); + int policyVersion = policy.getVersion(); + // The policy in the response from getDatabaseIAMPolicy might use the policy version + // that you specified, or it might use a lower policy version. For example, if you + // specify version 3, but the policy has no conditional role bindings, the response + // uses version 1. Valid values are 0, 1, and 3. + if (policy.getVersion() < 3) { + // conditional role bindings work with policy version 3 + policyVersion = 3; + } + + Binding binding1 = + Binding.newBuilder() + .setRole("roles/spanner.fineGrainedAccessUser") + .addAllMembers(ImmutableList.of(iamMember)) + .build(); + + Binding binding2 = + Binding.newBuilder() + .setRole("roles/spanner.databaseRoleUser") + .setCondition( + Expr.newBuilder().setDescription(title).setExpression( + String.format("resource.name.endsWith(\"/databaseRoles/%s\")", role) + ).setTitle(title).build()) + .addAllMembers(ImmutableList.of(iamMember)) + .build(); + ImmutableList bindings = + ImmutableList.builder() + .addAll(policy.getBindingsList()) + .add(binding1) + .add(binding2) + .build(); + Policy policyWithConditions = + Policy.newBuilder() + .setVersion(policyVersion) + .setEtag(policy.getEtag()) + .addAllBindings(bindings) + .build(); + final SetIamPolicyRequest setRequest = + SetIamPolicyRequest.newBuilder() + .setResource(DatabaseName.of(projectId, instanceId, databaseId).toString()) + .setPolicy(policyWithConditions).build(); + final Policy response = databaseAdminClient.setIamPolicy(setRequest); + System.out.printf( + "Enabled fine-grained access in IAM with version %d%n", response.getVersion()); + } +} +// [END spanner_enable_fine_grained_access] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java new file mode 100644 index 00000000000..250136c1fb6 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/GetDatabaseDdlSample.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +//[START spanner_get_database_ddl] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse; +import java.io.IOException; + +public class GetDatabaseDdlSample { + + static void getDatabaseDdl() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + getDatabaseDdl(projectId, instanceId, databaseId); + } + + static void getDatabaseDdl( + String projectId, String instanceId, String databaseId) throws IOException { + + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + final GetDatabaseDdlResponse response = + databaseAdminClient.getDatabaseDdl(DatabaseName.of(projectId, instanceId, databaseId)); + System.out.println("Retrieved database DDL for " + databaseId); + for (String ddl : response.getStatementsList()) { + System.out.println(ddl); + } + } +} +//[END spanner_get_database_ddl] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java new file mode 100644 index 00000000000..d5d8859673d --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabaseRoles.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.admin.generated; + +// [START spanner_list_database_roles] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabaseRolesPage; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabaseRolesPagedResponse; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.DatabaseRole; +import java.io.IOException; + +public class ListDatabaseRoles { + + static void listDatabaseRoles() throws IOException { + // TODO(developer): Replace these variables before running the sample. + String projectId = "my-project"; + String instanceId = "my-instance"; + String databaseId = "my-database"; + listDatabaseRoles(projectId, instanceId, databaseId); + } + + static void listDatabaseRoles(String projectId, String instanceId, String databaseId) + throws IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + DatabaseName databaseName = DatabaseName.of(projectId, instanceId, databaseId); + ListDatabaseRolesPagedResponse response + = databaseAdminClient.listDatabaseRoles(databaseName); + System.out.println("List of Database roles"); + for (ListDatabaseRolesPage page : response.iteratePages()) { + for (DatabaseRole role : page.iterateAll()) { + System.out.printf("Obtained role %s%n", role.getName()); + } + } + } +} +// [END spanner_list_database_roles] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java new file mode 100644 index 00000000000..7518064f2cd --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/ListDatabasesSample.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +//[START spanner_list_databases] + +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabasesPage; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient.ListDatabasesPagedResponse; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.InstanceName; +import java.io.IOException; + +public class ListDatabasesSample { + + static void listDatabases() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + listDatabases(projectId, instanceId); + } + + static void listDatabases(String projectId, String instanceId) throws IOException { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + ListDatabasesPagedResponse response = + databaseAdminClient.listDatabases(InstanceName.of(projectId, instanceId)); + + System.out.println("Databases for projects/" + projectId + "/instances/" + instanceId); + + for (ListDatabasesPage page : response.iteratePages()) { + for (Database database : page.iterateAll()) { + final String defaultLeader = database.getDefaultLeader().equals("") + ? "" : "(default leader = " + database.getDefaultLeader() + ")"; + System.out.println("\t" + database.getName() + " " + defaultLeader); + } + } + } +} +//[END spanner_list_databases] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java new file mode 100644 index 00000000000..39a4cd543a6 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgAlterSequenceSample.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_postgresql_alter_sequence] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class PgAlterSequenceSample { + + static void pgAlterSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + pgAlterSequence(projectId, instanceId, databaseId); + } + + static void pgAlterSequence(String projectId, String instanceId, String databaseId) + throws IOException { + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + final DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + databaseAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of("ALTER SEQUENCE Seq SKIP RANGE 1000 5000000")) + .get(5, TimeUnit.MINUTES); + System.out.println( + "Altered Seq sequence to skip an inclusive range between 1000 and 5000000"); + final DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + Long insertCount = + dbClient + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = + transaction.executeQuery( + Statement.of( + "INSERT INTO Customers (CustomerName) VALUES " + + "('Lea'), ('Catalina'), ('Smith') RETURNING CustomerId"))) { + while (rs.next()) { + System.out.printf( + "Inserted customer record with CustomerId: %d\n", rs.getLong(0)); + } + return Objects.requireNonNull(rs.getStats()).getRowCountExact(); + } + }); + System.out.printf("Number of customer records inserted is: %d\n", insertCount); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_postgresql_alter_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java new file mode 100644 index 00000000000..951b46446f1 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCaseSensitivitySample.java @@ -0,0 +1,153 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +// [START spanner_postgresql_identifier_case_sensitivity] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.Lists; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.ExecutionException; + +public class PgCaseSensitivitySample { + + static void pgCaseSensitivity() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + pgCaseSensitivity(projectId, instanceId, databaseId); + } + + static void pgCaseSensitivity(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try (Spanner spanner = + SpannerOptions.newBuilder() + .setProjectId(projectId) + .build() + .getService()) { + + // Spanner PostgreSQL follows the case sensitivity rules of PostgreSQL. This means that: + // 1. Identifiers that are not double-quoted are folded to lower case. + // 2. Identifiers that are double-quoted retain their case and are case-sensitive. + // See https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + // for more information. + databaseAdminClient.updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + Lists.newArrayList( + "CREATE TABLE Singers (" + // SingerId will be folded to `singerid`. + + " SingerId bigint NOT NULL PRIMARY KEY," + // FirstName and LastName are double-quoted and will therefore retain their + // mixed case and are case-sensitive. This means that any statement that + // references any of these columns must use double quotes. + + " \"FirstName\" varchar(1024) NOT NULL," + + " \"LastName\" varchar(1024) NOT NULL" + + ")")).get(); + + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + + client.write( + Collections.singleton( + Mutation.newInsertBuilder("Singers") + .set("singerid") + .to(1L) + // Column names in mutations are always case-insensitive, regardless whether the + // columns were double-quoted or not during creation. + .set("firstname") + .to("Bruce") + .set("lastname") + .to("Allison") + .build())); + + try (ResultSet singers = + client + .singleUse() + .executeQuery( + Statement.of("SELECT SingerId, \"FirstName\", \"LastName\" FROM Singers"))) { + while (singers.next()) { + System.out.printf( + "SingerId: %d, FirstName: %s, LastName: %s\n", + // SingerId is automatically folded to lower case. Accessing the column by its name in + // a result set must therefore use all lower-case letters. + singers.getLong("singerid"), + // FirstName and LastName were double-quoted during creation, and retain their mixed + // case when returned in a result set. + singers.getString("FirstName"), + singers.getString("LastName")); + } + } + + // Aliases are also identifiers, and specifying an alias in double quotes will make the alias + // retain its case. + try (ResultSet singers = + client + .singleUse() + .executeQuery( + Statement.of( + "SELECT " + + "singerid AS \"SingerId\", " + + "concat(\"FirstName\", ' '::varchar, \"LastName\") AS \"FullName\" " + + "FROM Singers"))) { + while (singers.next()) { + System.out.printf( + "SingerId: %d, FullName: %s\n", + // The aliases are double-quoted and therefore retains their mixed case. + singers.getLong("SingerId"), singers.getString("FullName")); + } + } + + // DML statements must also follow the PostgreSQL case rules. + client + .readWriteTransaction() + .run( + transaction -> + transaction.executeUpdate( + Statement.newBuilder( + "INSERT INTO Singers (SingerId, \"FirstName\", \"LastName\") " + + "VALUES ($1, $2, $3)") + .bind("p1") + .to(2L) + .bind("p2") + .to("Alice") + .bind("p3") + .to("Bruxelles") + .build())); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} +// [END spanner_postgresql_identifier_case_sensitivity] \ No newline at end of file diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java new file mode 100644 index 00000000000..6e7d9c34c57 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgCreateSequenceSample.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_postgresql_create_sequence] + +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class PgCreateSequenceSample { + + static void pgCreateSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + pgCreateSequence(projectId, instanceId, databaseId); + } + + static void pgCreateSequence(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + databaseAdminClient + .updateDatabaseDdlAsync(DatabaseName.of(projectId, instanceId, databaseId).toString(), + ImmutableList.of( + "CREATE SEQUENCE Seq BIT_REVERSED_POSITIVE;", + "CREATE TABLE Customers (CustomerId BIGINT DEFAULT nextval('Seq'), " + + "CustomerName character varying(1024), PRIMARY KEY (CustomerId))")) + .get(5, TimeUnit.MINUTES); + + System.out.println( + "Created Seq sequence and Customers table, where the key column " + + "CustomerId uses the sequence as a default value"); + + final DatabaseClient dbClient = + spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + + Long insertCount = + dbClient + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = + transaction.executeQuery( + Statement.of( + "INSERT INTO Customers (CustomerName) VALUES " + + "('Alice'), ('David'), ('Marc') RETURNING CustomerId"))) { + while (rs.next()) { + System.out.printf( + "Inserted customer record with CustomerId: %d\n", rs.getLong(0)); + } + return Objects.requireNonNull(rs.getStats()).getRowCountExact(); + } + }); + System.out.printf("Number of customer records inserted is: %d\n", insertCount); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_postgresql_create_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java new file mode 100644 index 00000000000..dec927d20ac --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgDropSequenceSample.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_postgresql_drop_sequence] + +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class PgDropSequenceSample { + + static void pgDropSequence() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + pgDropSequence(projectId, instanceId, databaseId); + } + + static void pgDropSequence(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try (Spanner spanner = + SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) { + databaseAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + ImmutableList.of( + "ALTER TABLE Customers ALTER COLUMN CustomerId DROP DEFAULT", + "DROP SEQUENCE Seq")) + .get(5, TimeUnit.MINUTES); + System.out.println( + "Altered Customers table to drop DEFAULT from " + + "CustomerId column and dropped the Seq sequence"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + // If the operation timed out propagate the timeout + throw SpannerExceptionFactory.propagateTimeout(e); + } + } +} +// [END spanner_postgresql_drop_sequence] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java new file mode 100644 index 00000000000..14af53dc5a8 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/PgInterleavedTableSample.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +// [START spanner_postgresql_interleaved_table] + +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; + +public class PgInterleavedTableSample { + + static void pgInterleavedTable() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + pgInterleavedTable(projectId, instanceId, databaseId); + } + + static void pgInterleavedTable(String projectId, String instanceId, String databaseId) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + try { + // The Spanner PostgreSQL dialect extends the PostgreSQL dialect with certain Spanner + // specific features, such as interleaved tables. + // See https://cloud.google.com/spanner/docs/postgresql/data-definition-language#create_table + // for the full CREATE TABLE syntax. + databaseAdminClient.updateDatabaseDdlAsync(DatabaseName.of(projectId, + instanceId, + databaseId), + Arrays.asList( + "CREATE TABLE Singers (" + + " SingerId bigint NOT NULL PRIMARY KEY," + + " FirstName varchar(1024) NOT NULL," + + " LastName varchar(1024) NOT NULL" + + ")", + "CREATE TABLE Albums (" + + " SingerId bigint NOT NULL," + + " AlbumId bigint NOT NULL," + + " Title varchar(1024) NOT NULL," + + " PRIMARY KEY (SingerId, AlbumId)" + + ") INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).get(); + System.out.println("Created interleaved table hierarchy using PostgreSQL dialect"); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} +// [END spanner_postgresql_interleaved_table] \ No newline at end of file diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java new file mode 100644 index 00000000000..958be6e20a4 --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseSample.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +// [START spanner_update_database] + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.common.collect.Lists; +import com.google.protobuf.FieldMask; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseRequest; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class UpdateDatabaseSample { + + static void updateDatabase() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + + updateDatabase(projectId, instanceId, databaseId); + } + + static void updateDatabase( + String projectId, String instanceId, String databaseId) throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + try { + final Database database = + Database.newBuilder() + .setName(DatabaseName.of(projectId, instanceId, databaseId).toString()) + .setEnableDropProtection(true).build(); + final UpdateDatabaseRequest updateDatabaseRequest = + UpdateDatabaseRequest.newBuilder() + .setDatabase(database) + .setUpdateMask( + FieldMask.newBuilder().addAllPaths( + Lists.newArrayList("enable_drop_protection")).build()) + .build(); + OperationFuture operation = + databaseAdminClient.updateDatabaseAsync(updateDatabaseRequest); + System.out.printf("Waiting for update operation for %s to complete...\n", databaseId); + Database updatedDb = operation.get(5, TimeUnit.MINUTES); + System.out.printf("Updated database %s.\n", updatedDb.getName()); + } catch (ExecutionException | TimeoutException e) { + // If the operation failed during execution, expose the cause. + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} +// [END spanner_update_database] diff --git a/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java b/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java new file mode 100644 index 00000000000..5306358714e --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSample.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +//[START spanner_update_database_with_default_leader] + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.spanner.admin.database.v1.DatabaseName; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.ExecutionException; + +public class UpdateDatabaseWithDefaultLeaderSample { + + static void updateDatabaseWithDefaultLeader() throws IOException { + // TODO(developer): Replace these variables before running the sample. + final String projectId = "my-project"; + final String instanceId = "my-instance"; + final String databaseId = "my-database"; + final String defaultLeader = "my-default-leader"; + updateDatabaseWithDefaultLeader(projectId, instanceId, databaseId, defaultLeader); + } + + static void updateDatabaseWithDefaultLeader( + String projectId, String instanceId, String databaseId, String defaultLeader) + throws IOException { + DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create(); + + try { + databaseAdminClient + .updateDatabaseDdlAsync( + DatabaseName.of(projectId, instanceId, databaseId), + Collections.singletonList( + String.format( + "ALTER DATABASE `%s` SET OPTIONS (default_leader = '%s')", + databaseId, + defaultLeader + ) + ) + ).get(); + System.out.println("Updated default leader to " + defaultLeader); + } catch (ExecutionException e) { + // If the operation failed during execution, expose the cause. + throw (SpannerException) e.getCause(); + } catch (InterruptedException e) { + // Throw when a thread is waiting, sleeping, or otherwise occupied, + // and the thread is interrupted, either before or during the activity. + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} +//[END spanner_update_database_with_default_leader] diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..2295119b69c --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/AlterTableWithForeignKeyDeleteCascadeSampleIT.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.example.spanner.SampleTestBase; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.InstanceName; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class AlterTableWithForeignKeyDeleteCascadeSampleIT extends SampleTestBaseV2 { + + @Test + public void testAlterTableWithForeignKeyDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .setParent(InstanceName.of(projectId, instanceId).toString()) + .addAllExtraStatements(Arrays.asList( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerId" + + " FOREIGN KEY (CustomerId)\n" + + " REFERENCES Customers (CustomerId)\n" + + " ) PRIMARY KEY (CartId)\n")).build(); + databaseAdminClient.createDatabaseAsync(request).get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + AlterTableWithForeignKeyDeleteCascadeSample.alterForeignKeyDeleteCascadeConstraint( + projectId, instanceId, databaseId)); + + assertTrue( + "Expected to have created database " + + databaseId + + " with tables containing " + + "foreign key constraints.", + out.contains("Altered ShoppingCarts table " + "with FKShoppingCartsCustomerName")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSampleIT.java deleted file mode 100644 index 39d02e1ed5c..00000000000 --- a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithDefaultLeaderSampleIT.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 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.example.spanner.admin.generated; - -import static org.junit.Assert.assertTrue; - -import com.example.spanner.SampleRunner; -import com.google.spanner.admin.instance.v1.InstanceConfig; -import com.google.spanner.admin.instance.v1.InstanceName; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class CreateDatabaseWithDefaultLeaderSampleIT extends SampleTestBaseV2 { - - @Test - public void testCreateDatabaseWithDefaultLeader() throws Exception { - final String databaseId = idGenerator.generateDatabaseId(); - - // Finds possible default leader - - final String instanceConfigId = instanceAdminClient.getInstance( - InstanceName.of(projectId, multiRegionalInstanceId)).getConfig(); - final InstanceConfig config = instanceAdminClient.getInstanceConfig(instanceConfigId); - assertTrue( - "Expected instance config " + instanceConfigId + " to have at least one leader option", - config.getLeaderOptionsCount() > 0 - ); - final String defaultLeader = config.getLeaderOptions(0); - - // Runs sample - final String out = SampleRunner.runSample(() -> - CreateDatabaseWithDefaultLeaderSample.createDatabaseWithDefaultLeader( - getInstanceName(projectId, multiRegionalInstanceId), - databaseId, - defaultLeader - ) - ); - - assertTrue( - "Expected created database to have default leader " + defaultLeader + "." - + " Output received was " + out, - out.contains("Default leader: " + defaultLeader) - ); - } -} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSampleIT.java new file mode 100644 index 00000000000..64400fbf210 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateDatabaseWithVersionRetentionPeriodSampleIT.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +import static com.google.common.truth.Truth.assertThat; + +import com.example.spanner.SampleRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link com.example.spanner.CreateDatabaseWithVersionRetentionPeriodSample} + */ +@RunWith(JUnit4.class) +public class CreateDatabaseWithVersionRetentionPeriodSampleIT extends SampleTestBaseV2 { + + @Test + public void createsDatabaseWithVersionRetentionPeriod() throws Exception { + final String databaseId = idGenerator.generateDatabaseId(); + final String versionRetentionPeriod = "7d"; + + final String out = SampleRunner.runSample(() -> CreateDatabaseWithVersionRetentionPeriodSample + .createDatabaseWithVersionRetentionPeriod( + projectId, instanceId, databaseId, versionRetentionPeriod + )); + + assertThat(out).contains( + "Created database [projects/" + projectId + "/instances/" + instanceId + "/databases/" + + databaseId + "]"); + assertThat(out).contains("Version retention period: " + versionRetentionPeriod); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..6fd9aeddb16 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateTableWithForeignKeyDeleteCascadeSampleIT.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.example.spanner.SampleTestBase; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.InstanceName; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class CreateTableWithForeignKeyDeleteCascadeSampleIT extends SampleTestBaseV2 { + + @Test + public void testCreateTableWithForeignKeyDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .setParent(InstanceName.of(projectId, instanceId).toString()).build(); + databaseAdminClient.createDatabaseAsync(request).get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + CreateTableWithForeignKeyDeleteCascadeSample + .createForeignKeyDeleteCascadeConstraint(projectId, instanceId, databaseId)); + + assertTrue( + "Expected to have created database " + + databaseId + + " with tables containing " + + "foreign key constraints.", + out.contains( + "Created Customers and ShoppingCarts table " + "with FKShoppingCartsCustomerId")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/DatabaseRolesIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/DatabaseRolesIT.java new file mode 100644 index 00000000000..93b19a2b330 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/DatabaseRolesIT.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.common.collect.Lists; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for FGAC samples for GoogleStandardSql dialect. + */ +@RunWith(JUnit4.class) +public class DatabaseRolesIT extends SampleTestBaseV2 { + + private static DatabaseId databaseId; + + @BeforeClass + public static void createTestDatabase() throws Exception { + final String database = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setParent( + com.google.spanner.admin.database.v1.InstanceName.of(projectId, instanceId) + .toString()) + .setCreateStatement("CREATE DATABASE `" + database + "`") + .addAllExtraStatements(Lists.newArrayList( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)," + + " FullName STRING(2048) AS " + + " (ARRAY_TO_STRING([FirstName, LastName], \" \")) STORED" + + ") PRIMARY KEY (SingerId)", + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)," + + " MarketingBudget INT64" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).build(); + databaseAdminClient.createDatabaseAsync(request).get(5, TimeUnit.MINUTES); + databaseId = DatabaseId.of(projectId, instanceId, database); + } + + @Before + public void insertTestData() { + final DatabaseClient client = spanner.getDatabaseClient(databaseId); + client.write( + Arrays.asList( + Mutation.newInsertOrUpdateBuilder("Singers") + .set("SingerId") + .to(1L) + .set("FirstName") + .to("Melissa") + .set("LastName") + .to("Garcia") + .build(), + Mutation.newInsertOrUpdateBuilder("Albums") + .set("SingerId") + .to(1L) + .set("AlbumId") + .to(1L) + .set("AlbumTitle") + .to("title 1") + .set("MarketingBudget") + .to(20000L) + .build())); + } + + @After + public void removeTestData() { + final DatabaseClient client = spanner.getDatabaseClient(databaseId); + client.write(Collections.singletonList(Mutation.delete("Singers", KeySet.all()))); + } + + @Test + public void testAddAndDropDatabaseRole() throws Exception { + final String out = + SampleRunner.runSample( + () -> + AddAndDropDatabaseRole.addAndDropDatabaseRole( + projectId, instanceId, databaseId.getDatabase(), "new_parent", "new_child")); + assertTrue(out.contains("Created roles new_parent and new_child and granted privileges")); + assertTrue(out.contains("Revoked privileges and dropped role new_child")); + } + + @Test + public void testListDatabaseRoles() throws Exception { + final String out = + SampleRunner.runSample( + () -> + ListDatabaseRoles.listDatabaseRoles( + projectId, instanceId, databaseId.getDatabase())); + assertTrue(out.contains("Obtained role ")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSampleIT.java new file mode 100644 index 00000000000..f2dae05237a --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/DropForeignKeyConstraintDeleteCascadeSampleIT.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.example.spanner.SampleTestBase; +import com.google.common.collect.Lists; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.InstanceName; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class DropForeignKeyConstraintDeleteCascadeSampleIT extends SampleTestBaseV2 { + + @Test + public void testDropForeignKeyConstraintDeleteCascade() throws Exception { + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .setParent(InstanceName.of(projectId, instanceId).toString()) + .addAllExtraStatements(Lists.newArrayList( + "CREATE TABLE Customers (\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " ) PRIMARY KEY (CustomerId)", + "CREATE TABLE ShoppingCarts (\n" + + " CartId INT64 NOT NULL,\n" + + " CustomerId INT64 NOT NULL,\n" + + " CustomerName STRING(62) NOT NULL,\n" + + " CONSTRAINT FKShoppingCartsCustomerName" + + " FOREIGN KEY (CustomerName)\n" + + " REFERENCES Customers (CustomerName) ON DELETE CASCADE\n" + + " ) PRIMARY KEY (CartId)\n")).build(); + databaseAdminClient.createDatabaseAsync(request).get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> + DropForeignKeyConstraintDeleteCascadeSample.deleteForeignKeyDeleteCascadeConstraint( + projectId, instanceId, databaseId)); + + assertTrue( + "Expected to have dropped foreign-key constraints from tables in created database " + + databaseId, + out.contains("Altered ShoppingCarts table to drop FKShoppingCartsCustomerName")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/GetDatabaseDdlSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/GetDatabaseDdlSampleIT.java new file mode 100644 index 00000000000..8769a125983 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/GetDatabaseDdlSampleIT.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.common.collect.Lists; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.InstanceName; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class GetDatabaseDdlSampleIT extends SampleTestBaseV2 { + + @Test + public void testGetDatabaseDdl() throws Exception { + // Finds a possible new leader option + final String instanceConfigId = instanceAdminClient.getInstance( + InstanceName.of(projectId, multiRegionalInstanceId)).getConfig(); + final InstanceConfig config = instanceAdminClient.getInstanceConfig(instanceConfigId); + assertTrue( + "Expected instance config " + instanceConfigId + " to have at least one leader option", + config.getLeaderOptionsList().size() > 0 + ); + final String defaultLeader = config.getLeaderOptions(0); + + // Creates database + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setParent( + com.google.spanner.admin.database.v1.InstanceName.of(projectId, + multiRegionalInstanceId).toString()) + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .addAllExtraStatements(Lists.newArrayList( + "CREATE TABLE Singers (Id INT64 NOT NULL) PRIMARY KEY (Id)", + "ALTER DATABASE `" + + databaseId + + "` SET OPTIONS ( default_leader = '" + + defaultLeader + + "')" + )).build(); + databaseAdminClient.createDatabaseAsync(request).get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = SampleRunner.runSample(() -> GetDatabaseDdlSample + .getDatabaseDdl(projectId, multiRegionalInstanceId, databaseId) + ); + + assertTrue( + "Expected to have retrieved database DDL for " + databaseId + "." + + " Output received was " + out, + out.contains("Retrieved database DDL for " + databaseId) + ); + assertTrue( + "Expected leader to be set to " + defaultLeader + "." + + " Output received was " + out, + out.contains("default_leader = '" + defaultLeader + "'") + ); + assertTrue( + "Expected table to have been created in " + databaseId + "." + + " Output received was " + out, + out.contains("CREATE TABLE Singers") + ); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateInstanceSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/ListDatabasesIT.java similarity index 57% rename from samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateInstanceSampleIT.java rename to samples/snippets/src/test/java/com/example/spanner/admin/generated/ListDatabasesIT.java index 7743b24f132..730205b8b48 100644 --- a/samples/snippets/src/test/java/com/example/spanner/admin/generated/CreateInstanceSampleIT.java +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/ListDatabasesIT.java @@ -1,11 +1,11 @@ /* - * Copyright 2023 Google LLC + * Copyright 2022 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 + * 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, @@ -24,20 +24,14 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class CreateInstanceSampleIT extends SampleTestBaseV2 { +public class ListDatabasesIT extends SampleTestBaseV2 { @Test - public void testCreateInstance() throws Exception { - final String instanceId = idGenerator.generateInstanceId(); - - // Runs sample - final String out = SampleRunner.runSample(() -> - CreateInstanceExample.createInstance(projectId, instanceId) - ); - - assertTrue( - "Expected created instance " + instanceId + "." - + " Output received was " + out, out.contains("was successfully created") - ); + public void testListDatabaseRoles() throws Exception { + final String out = + SampleRunner.runSample( + () -> + ListDatabasesSample.listDatabases(projectId, instanceId)); + assertTrue(out.contains("Databases for projects")); } } diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgCaseSensitivitySampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgCaseSensitivitySampleIT.java new file mode 100644 index 00000000000..88337ba8c6a --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgCaseSensitivitySampleIT.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.InstanceName; +import org.junit.Test; + +public class PgCaseSensitivitySampleIT extends SampleTestBaseV2 { + + @Test + public void testPgCaseSensitivitySample() throws Exception { + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setCreateStatement(getCreateDatabaseStatement(databaseId, DatabaseDialect.POSTGRESQL)) + .setParent(InstanceName.of(projectId, instanceId).toString()) + .setDatabaseDialect(DatabaseDialect.POSTGRESQL).build(); + databaseAdminClient.createDatabaseAsync(request).get(); + + final String out = + SampleRunner.runSample( + () -> PgCaseSensitivitySample.pgCaseSensitivity(projectId, instanceId, databaseId)); + assertTrue(out, out.contains("SingerId: 1, FirstName: Bruce, LastName: Allison")); + assertTrue(out, out.contains("SingerId: 1, FullName: Bruce Allison")); + } +} \ No newline at end of file diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgInterleavedTableSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgInterleavedTableSampleIT.java new file mode 100644 index 00000000000..59a0f4a524f --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/PgInterleavedTableSampleIT.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.InstanceName; +import org.junit.Test; + +public class PgInterleavedTableSampleIT extends SampleTestBaseV2 { + + @Test + public void testPgInterleavedTableSample() throws Exception { + final String databaseId = idGenerator.generateDatabaseId(); + final CreateDatabaseRequest request = + CreateDatabaseRequest.newBuilder() + .setCreateStatement(getCreateDatabaseStatement(databaseId, DatabaseDialect.POSTGRESQL)) + .setParent(InstanceName.of(projectId, instanceId).toString()) + .setDatabaseDialect(DatabaseDialect.POSTGRESQL).build(); + databaseAdminClient.createDatabaseAsync(request).get(); + + final String out = + SampleRunner.runSample( + () -> PgInterleavedTableSample.pgInterleavedTable(projectId, instanceId, databaseId)); + assertTrue(out.contains("Created interleaved table hierarchy using PostgreSQL dialect")); + } +} \ No newline at end of file diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/SampleTestBaseV2.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/SampleTestBaseV2.java index 941f97fe859..e5ff002110c 100644 --- a/samples/snippets/src/test/java/com/example/spanner/admin/generated/SampleTestBaseV2.java +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/SampleTestBaseV2.java @@ -17,11 +17,13 @@ package com.example.spanner.admin.generated; import com.example.spanner.SampleIdGenerator; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings; +import com.google.spanner.admin.database.v1.DatabaseDialect; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.junit.AfterClass; @@ -45,7 +47,7 @@ public class SampleTestBaseV2 { protected static final String instanceId = System.getProperty("spanner.test.instance"); protected static DatabaseAdminClient databaseAdminClient; protected static InstanceAdminClient instanceAdminClient; - + protected static Spanner spanner; protected static final String multiRegionalInstanceId = System.getProperty("spanner.test.instance.mr"); protected static final String instanceConfigName = System @@ -69,6 +71,7 @@ public static void beforeClass() throws IOException { } projectId = options.getProjectId(); + spanner = options.getService(); databaseAdminClient = DatabaseAdminClient.create(databaseAdminSettingsBuilder.build()); instanceAdminClient = InstanceAdminClient.create(instanceAdminSettingBuilder.build()); idGenerator = new SampleIdGenerator( @@ -134,6 +137,7 @@ public static void afterClass() throws InterruptedException { + ", skipping..."); } } + databaseAdminClient.close(); instanceAdminClient.close(); @@ -164,4 +168,13 @@ static String getInstanceConfigName(final String projectId, final String instanc static String getProjectName(final String projectId) { return String.format("projects/%s", projectId); } + + static String getCreateDatabaseStatement( + final String databaseName, final DatabaseDialect dialect) { + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + return "CREATE DATABASE `" + databaseName + "`"; + } else { + return "CREATE DATABASE \"" + databaseName + "\""; + } + } } diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/SequenceSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/SequenceSampleIT.java new file mode 100644 index 00000000000..b3f4df004d7 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/SequenceSampleIT.java @@ -0,0 +1,142 @@ +/* + * Copyright 2022 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.example.spanner.admin.generated; + +import static com.example.spanner.SampleRunner.runSample; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Integration tests for Bit reversed sequence samples for GoogleStandardSql and PostgreSql + * dialects. + */ +@RunWith(Parameterized.class) +public class SequenceSampleIT extends SampleTestBaseV2 { + + private static String databaseId; + + /** + * Set of dialects for which database has already been created in this test suite. This helps in + * limiting the number of databases created per dialect to one. + */ + private static final HashSet dbInitializedDialects = new HashSet<>(); + + @Parameters(name = "dialect = {0}") + public static Iterable data() { + return ImmutableList.of(DatabaseDialect.GOOGLE_STANDARD_SQL, DatabaseDialect.POSTGRESQL); + } + + @Parameter(0) + public static DatabaseDialect dialect; + + @Before + public void createTestDatabase() throws Exception { + // Limits number of created databases to one per dialect. + if (dbInitializedDialects.contains(dialect)) { + return; + } + dbInitializedDialects.add(dialect); + databaseId = idGenerator.generateDatabaseId(); + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setParent(getInstanceName(projectId, instanceId)) + .setCreateStatement(getCreateDatabaseStatement(databaseId, dialect)) + .setDatabaseDialect(dialect).build(); + databaseAdminClient + .createDatabaseAsync(createDatabaseRequest) + .get(10, TimeUnit.MINUTES); + } + + @Test + public void createSequence() throws Exception { + String out; + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + out = + runSample( + () -> + CreateSequenceSample.createSequence( + projectId, instanceId, databaseId)); + } else { + out = + runSample( + () -> + PgCreateSequenceSample.pgCreateSequence( + projectId, instanceId, databaseId)); + } + assertTrue( + out.contains( + "Created Seq sequence and Customers table, where the key column " + + "CustomerId uses the sequence as a default value")); + assertEquals(out.split("Inserted customer record with CustomerId", -1).length - 1, 3); + assertTrue(out.contains("Number of customer records inserted is: 3")); + } + + @Test + public void alterSequence() throws Exception { + String out; + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + out = + runSample( + () -> + AlterSequenceSample.alterSequence( + projectId, instanceId, databaseId)); + } else { + out = + runSample( + () -> + PgAlterSequenceSample.pgAlterSequence( + projectId, instanceId, databaseId)); + } + assertTrue( + out.contains("Altered Seq sequence to skip an inclusive range between 1000 and 5000000")); + assertEquals(out.split("Inserted customer record with CustomerId", -1).length - 1, 3); + assertTrue(out.contains("Number of customer records inserted is: 3")); + } + + @Test + public void dropSequence() throws Exception { + String out; + if (dialect == DatabaseDialect.GOOGLE_STANDARD_SQL) { + out = + runSample( + () -> + DropSequenceSample.dropSequence(projectId, instanceId, databaseId)); + } else { + out = + runSample( + () -> + PgDropSequenceSample.pgDropSequence( + projectId, instanceId, databaseId)); + } + assertTrue( + out.contains( + "Altered Customers table to drop DEFAULT from " + + "CustomerId column and dropped the Seq sequence")); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseSampleIT.java new file mode 100644 index 00000000000..acac98cde42 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseSampleIT.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.api.gax.longrunning.OperationFuture; +import com.google.common.collect.Lists; +import com.google.protobuf.FieldMask; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.database.v1.DatabaseName; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseRequest; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class UpdateDatabaseSampleIT extends SampleTestBaseV2 { + + @Test + public void testUpdateDatabase() throws Exception { + // Create database + final String databaseId = idGenerator.generateDatabaseId(); + databaseAdminClient + .createDatabaseAsync(getInstanceName(projectId, instanceId), + "CREATE DATABASE `" + databaseId + "`") + .get(5, TimeUnit.MINUTES); + + // Runs sample + final String out = + SampleRunner.runSample( + () -> UpdateDatabaseSample.updateDatabase(projectId, instanceId, databaseId)); + assertTrue( + "Expected that database would have been updated. Output received was " + out, + out.contains(String.format( + "Updated database %s", DatabaseName.of(projectId, instanceId, databaseId)))); + + // Cleanup + final com.google.spanner.admin.database.v1.Database database = + com.google.spanner.admin.database.v1.Database.newBuilder() + .setName(DatabaseName.of(projectId, instanceId, databaseId).toString()) + .setEnableDropProtection(false).build(); + final UpdateDatabaseRequest updateDatabaseRequest = + UpdateDatabaseRequest.newBuilder() + .setDatabase(database) + .setUpdateMask( + FieldMask.newBuilder().addAllPaths( + Lists.newArrayList("enable_drop_protection")).build()) + .build(); + + OperationFuture operation = + databaseAdminClient.updateDatabaseAsync(updateDatabaseRequest); + Database updatedDb = operation.get(5, TimeUnit.MINUTES); + assertFalse(updatedDb.getEnableDropProtection()); + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSampleIT.java new file mode 100644 index 00000000000..3ec35872880 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/admin/generated/UpdateDatabaseWithDefaultLeaderSampleIT.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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.example.spanner.admin.generated; + +import static org.junit.Assert.assertTrue; + +import com.example.spanner.SampleRunner; +import com.google.spanner.admin.database.v1.Database; +import com.google.spanner.admin.instance.v1.InstanceConfig; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class UpdateDatabaseWithDefaultLeaderSampleIT extends SampleTestBaseV2 { + + @Test + public void testUpdateDatabaseWithDefaultLeader() throws Exception { + // Create database + final String databaseId = idGenerator.generateDatabaseId(); + final Database createdDatabase = databaseAdminClient + .createDatabaseAsync(getInstanceName(projectId, multiRegionalInstanceId), + "CREATE DATABASE `" + databaseId + "`") + .get(5, TimeUnit.MINUTES); + final String defaultLeader = createdDatabase.getDefaultLeader(); + + // Finds a possible new leader option + final String instanceConfigId = + instanceAdminClient.getInstance(getInstanceName(projectId, multiRegionalInstanceId)) + .getConfig(); + final InstanceConfig config = instanceAdminClient.getInstanceConfig(instanceConfigId); + final String newLeader = + config.getLeaderOptionsList().stream() + .filter(leader -> !leader.equals(defaultLeader)) + .findFirst().orElseThrow(() -> + new RuntimeException("Expected to find a leader option different than " + + defaultLeader) + ); + + // Runs sample + final String out = SampleRunner.runSample(() -> UpdateDatabaseWithDefaultLeaderSample + .updateDatabaseWithDefaultLeader(projectId, multiRegionalInstanceId, databaseId, newLeader) + ); + + assertTrue( + "Expected that database new leader would had been updated to " + newLeader + "." + + " Output received was " + out, + out.contains("Updated default leader to " + newLeader) + ); + } +} From 8e2da5126263c7acd134fb7fcfeb590ca190ce8e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 15 Feb 2024 16:02:17 +0100 Subject: [PATCH 09/10] deps: update dependency com.google.cloud:sdk-platform-java-config to v3.25.0 (#2888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.cloud:sdk-platform-java-config](https://togithub.com/googleapis/java-shared-config) | `3.24.0` -> `3.25.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.cloud:sdk-platform-java-config/3.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.cloud:sdk-platform-java-config/3.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.cloud:sdk-platform-java-config/3.24.0/3.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.cloud:sdk-platform-java-config/3.24.0/3.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner). --- .kokoro/presubmit/graalvm-native-17.cfg | 2 +- .kokoro/presubmit/graalvm-native.cfg | 2 +- google-cloud-spanner-bom/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg index 90d9a20a085..5e86d37f076 100644 --- a/.kokoro/presubmit/graalvm-native-17.cfg +++ b/.kokoro/presubmit/graalvm-native-17.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.24.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.25.0" } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg index 948177be87f..8e8cded782d 100644 --- a/.kokoro/presubmit/graalvm-native.cfg +++ b/.kokoro/presubmit/graalvm-native.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.24.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.25.0" } env_vars: { diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index d5a42eca56d..0c7bf9fc40f 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -8,7 +8,7 @@ com.google.cloud sdk-platform-java-config - 3.24.0 + 3.25.0 Google Cloud Spanner BOM diff --git a/pom.xml b/pom.xml index 6e4b50744b1..29af741eb08 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.24.0 + 3.25.0 From c41e04629434b1d76e46883638b1ff6c17a24f90 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:00:29 +0000 Subject: [PATCH 10/10] chore(main): release 6.59.0 (#2887) :robot: I have created a release *beep* *boop* --- ## [6.59.0](https://togithub.com/googleapis/java-spanner/compare/v6.58.0...v6.59.0) (2024-02-15) ### Features * Support public methods to use autogenerated admin clients. ([#2878](https://togithub.com/googleapis/java-spanner/issues/2878)) ([53bcb3e](https://togithub.com/googleapis/java-spanner/commit/53bcb3eca2e814472c3def24e8e03d47652a8e42)) ### Dependencies * Update dependency com.google.cloud:sdk-platform-java-config to v3.25.0 ([#2888](https://togithub.com/googleapis/java-spanner/issues/2888)) ([8e2da51](https://togithub.com/googleapis/java-spanner/commit/8e2da5126263c7acd134fb7fcfeb590ca190ce8e)) ### Documentation * README for OpenTelemetry metrics and traces ([#2880](https://togithub.com/googleapis/java-spanner/issues/2880)) ([c8632f5](https://togithub.com/googleapis/java-spanner/commit/c8632f5b2f462420a8c2a1f4308a68a18a414472)) * Samples and tests for database Admin APIs. ([#2775](https://togithub.com/googleapis/java-spanner/issues/2775)) ([14ae01c](https://togithub.com/googleapis/java-spanner/commit/14ae01cd82e455a0dc22d7e3bb8c362e541ede12)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 18 +++++++++++++++++ google-cloud-spanner-bom/pom.xml | 18 ++++++++--------- google-cloud-spanner-executor/pom.xml | 4 ++-- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-executor-v1/pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 20 +++++++++---------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 20 +++++++++---------- 15 files changed, 68 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8680d3ccfe..b7143698df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [6.59.0](https://github.com/googleapis/java-spanner/compare/v6.58.0...v6.59.0) (2024-02-15) + + +### Features + +* Support public methods to use autogenerated admin clients. ([#2878](https://github.com/googleapis/java-spanner/issues/2878)) ([53bcb3e](https://github.com/googleapis/java-spanner/commit/53bcb3eca2e814472c3def24e8e03d47652a8e42)) + + +### Dependencies + +* Update dependency com.google.cloud:sdk-platform-java-config to v3.25.0 ([#2888](https://github.com/googleapis/java-spanner/issues/2888)) ([8e2da51](https://github.com/googleapis/java-spanner/commit/8e2da5126263c7acd134fb7fcfeb590ca190ce8e)) + + +### Documentation + +* README for OpenTelemetry metrics and traces ([#2880](https://github.com/googleapis/java-spanner/issues/2880)) ([c8632f5](https://github.com/googleapis/java-spanner/commit/c8632f5b2f462420a8c2a1f4308a68a18a414472)) +* Samples and tests for database Admin APIs. ([#2775](https://github.com/googleapis/java-spanner/issues/2775)) ([14ae01c](https://github.com/googleapis/java-spanner/commit/14ae01cd82e455a0dc22d7e3bb8c362e541ede12)) + ## [6.58.0](https://github.com/googleapis/java-spanner/compare/v6.57.0...v6.58.0) (2024-02-08) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 0c7bf9fc40f..8d28d43766c 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 6.58.1-SNAPSHOT + 6.59.0 pom com.google.cloud @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.58.1-SNAPSHOT + 6.59.0 com.google.cloud google-cloud-spanner test-jar - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index 54c87fe72a6..00107cf0d4b 100644 --- a/google-cloud-spanner-executor/pom.xml +++ b/google-cloud-spanner-executor/pom.xml @@ -5,14 +5,14 @@ 4.0.0 com.google.cloud google-cloud-spanner-executor - 6.58.1-SNAPSHOT + 6.59.0 jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 9288da99d4b..23b9bb7ab06 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 6.58.1-SNAPSHOT + 6.59.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 google-cloud-spanner diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index 2e4d216b980..0d47d322132 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 9fc4769ee08..aaabadf1a86 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/grpc-google-cloud-spanner-executor-v1/pom.xml b/grpc-google-cloud-spanner-executor-v1/pom.xml index 53b9b707946..db00d978865 100644 --- a/grpc-google-cloud-spanner-executor-v1/pom.xml +++ b/grpc-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.58.1-SNAPSHOT + 6.59.0 grpc-google-cloud-spanner-executor-v1 GRPC library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index b8cf954216d..307321b99a4 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/pom.xml b/pom.xml index 29af741eb08..b9af596216f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.58.1-SNAPSHOT + 6.59.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -61,47 +61,47 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 com.google.cloud google-cloud-spanner - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 0f5038c9a48..8063b78c273 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.58.1-SNAPSHOT + 6.59.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 719060986b5..d5de1bfde47 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.58.1-SNAPSHOT + 6.59.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/proto-google-cloud-spanner-executor-v1/pom.xml b/proto-google-cloud-spanner-executor-v1/pom.xml index 291c4db5a02..658b80327ac 100644 --- a/proto-google-cloud-spanner-executor-v1/pom.xml +++ b/proto-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.58.1-SNAPSHOT + 6.59.0 proto-google-cloud-spanner-executor-v1 Proto library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index a2f445500e3..160ea8a76e4 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.58.1-SNAPSHOT + 6.59.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 41a6a0caad3..6da54d91f6c 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 6.58.1-SNAPSHOT + 6.59.0 diff --git a/versions.txt b/versions.txt index 83b483b77d7..59fc84d023c 100644 --- a/versions.txt +++ b/versions.txt @@ -1,13 +1,13 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.1-SNAPSHOT -proto-google-cloud-spanner-v1:6.58.0:6.58.1-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:6.58.0:6.58.1-SNAPSHOT -grpc-google-cloud-spanner-v1:6.58.0:6.58.1-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:6.58.0:6.58.1-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:6.58.0:6.58.1-SNAPSHOT -google-cloud-spanner:6.58.0:6.58.1-SNAPSHOT -google-cloud-spanner-executor:6.58.0:6.58.1-SNAPSHOT -proto-google-cloud-spanner-executor-v1:6.58.0:6.58.1-SNAPSHOT -grpc-google-cloud-spanner-executor-v1:6.58.0:6.58.1-SNAPSHOT +proto-google-cloud-spanner-admin-instance-v1:6.59.0:6.59.0 +proto-google-cloud-spanner-v1:6.59.0:6.59.0 +proto-google-cloud-spanner-admin-database-v1:6.59.0:6.59.0 +grpc-google-cloud-spanner-v1:6.59.0:6.59.0 +grpc-google-cloud-spanner-admin-instance-v1:6.59.0:6.59.0 +grpc-google-cloud-spanner-admin-database-v1:6.59.0:6.59.0 +google-cloud-spanner:6.59.0:6.59.0 +google-cloud-spanner-executor:6.59.0:6.59.0 +proto-google-cloud-spanner-executor-v1:6.59.0:6.59.0 +grpc-google-cloud-spanner-executor-v1:6.59.0:6.59.0