From b4745098f860ea0ccbba3eb0f0ebda185112c4a4 Mon Sep 17 00:00:00 2001 From: Rajat Bhatta <93644539+rajatbhatta@users.noreply.github.com> Date: Sat, 13 Aug 2022 00:17:57 +0530 Subject: [PATCH 01/16] test: increase timeout to prevent flakiness (#1972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: increase timeout to prevent flakiness * 🦉 Updates from OwlBot post-processor Co-authored-by: Owl Bot --- README.md | 4 ++-- .../java/com/google/cloud/spanner/DatabaseClientImplTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b89595fee..8f8f7cdb8a 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,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.27.0' +implementation 'com.google.cloud:google-cloud-spanner:6.28.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.27.0" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.28.0" ``` ## Authentication 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 456d919784..c11016fea2 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 @@ -1050,7 +1050,7 @@ public void testExecutePartitionedDmlWithException() { @Test public void testPartitionedDmlDoesNotTimeout() { - mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); + mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(20, 0)); final RetrySettings retrySettings = RetrySettings.newBuilder() .setInitialRpcTimeout(Duration.ofMillis(1L)) From 993536275bcd46890012e807a1b15b42abf7ff00 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Aug 2022 17:46:15 +0200 Subject: [PATCH 02/16] build(deps): update dependency org.apache.maven.plugins:maven-assembly-plugin to v3.4.2 (#1942) 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 | |---|---|---|---|---|---| | [org.apache.maven.plugins:maven-assembly-plugin](https://maven.apache.org/plugins/) ([source](https://togithub.com/apache/maven-assembly-plugin)) | `3.4.0` -> `3.4.2` | [![age](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-assembly-plugin/3.4.2/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-assembly-plugin/3.4.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-assembly-plugin/3.4.2/compatibility-slim/3.4.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-assembly-plugin/3.4.2/confidence-slim/3.4.0)](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 this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/snippets/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 5d0602d15e..a4bf3f84f6 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -119,7 +119,7 @@ maven-assembly-plugin - 3.4.0 + 3.4.2 assembly-descriptor.xml From 7497f2b670aa0482f6f171701019e254f3dfcb49 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Aug 2022 18:00:17 +0200 Subject: [PATCH 03/16] test(deps): update dependency org.mockito:mockito-core to v4.7.0 (#1974) 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 | |---|---|---|---|---|---| | [org.mockito:mockito-core](https://togithub.com/mockito/mockito) | `4.6.1` -> `4.7.0` | [![age](https://badges.renovateapi.com/packages/maven/org.mockito:mockito-core/4.7.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/org.mockito:mockito-core/4.7.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/org.mockito:mockito-core/4.7.0/compatibility-slim/4.6.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/org.mockito:mockito-core/4.7.0/confidence-slim/4.6.1)](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**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- README.md | 2 +- google-cloud-spanner/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f8f7cdb8a..cccd2e62a4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ If you are using Maven without 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.0.0') +implementation platform('com.google.cloud:libraries-bom:26.1.0') implementation 'com.google.cloud:google-cloud-spanner' ``` diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index cccf5b25db..91e0963963 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -338,7 +338,7 @@ org.mockito mockito-core - 4.6.1 + 4.7.0 test From 6479d19dcca2b3e3df43a2858f5dcaf85685c31f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Aug 2022 18:02:26 +0200 Subject: [PATCH 04/16] deps: update dependency com.google.cloud:google-cloud-trace to v2.3.1 (#1967) 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:google-cloud-trace](https://togithub.com/googleapis/java-core) | `2.3.0` -> `2.3.1` | [![age](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-trace/2.3.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-trace/2.3.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-trace/2.3.1/compatibility-slim/2.3.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-trace/2.3.1/confidence-slim/2.3.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/java-core ### [`v2.3.1`](https://togithub.com/googleapis/java-core/blob/HEAD/CHANGELOG.md#​231-httpswwwgithubcomgoogleapisjava-corecomparev230v231-2021-11-15) [Compare Source](https://togithub.com/googleapis/java-core/compare/v2.3.0...v2.3.1)
--- ### 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 this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index ef7fd2af21..a3e41ba9b4 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.31.1 - 2.3.0 + 2.3.1 3.3.6 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 9852a1aa03..889b9e9a43 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -22,7 +22,7 @@ 1.8 UTF-8 0.31.1 - 2.3.0 + 2.3.1 3.3.6 From f8bde0f4433e6163ad4e000cc4f25eddabb1e4e4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Aug 2022 18:50:15 +0200 Subject: [PATCH 05/16] chore(deps): update dependency com.google.cloud:libraries-bom to v26.1.0 (#1973) 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:libraries-bom](https://cloud.google.com/java/docs/bom) ([source](https://togithub.com/googleapis/java-cloud-bom)) | `26.0.0` -> `26.1.0` | [![age](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.1.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.1.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.1.0/compatibility-slim/26.0.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.1.0/confidence-slim/26.0.0)](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**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- README.md | 2 +- samples/native-image/pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cccd2e62a4..5bb211d6b3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file: com.google.cloud libraries-bom - 26.0.0 + 26.1.0 pom import diff --git a/samples/native-image/pom.xml b/samples/native-image/pom.xml index 79fafad474..7f816ecdc2 100644 --- a/samples/native-image/pom.xml +++ b/samples/native-image/pom.xml @@ -28,7 +28,7 @@ com.google.cloud libraries-bom - 26.0.0 + 26.1.0 pom import diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index a4bf3f84f6..3596a04072 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -33,7 +33,7 @@ com.google.cloud libraries-bom - 26.0.0 + 26.1.0 pom import From b20bf9e8a51311a602da6b7a3804dde92b04770d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Aug 2022 21:18:25 +0200 Subject: [PATCH 06/16] build(deps): update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.4.1 (#1975) 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 | |---|---|---|---|---|---| | [org.apache.maven.plugins:maven-javadoc-plugin](https://maven.apache.org/plugins/) ([source](https://togithub.com/apache/maven-javadoc-plugin)) | `3.4.0` -> `3.4.1` | [![age](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-javadoc-plugin/3.4.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-javadoc-plugin/3.4.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-javadoc-plugin/3.4.1/compatibility-slim/3.4.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-javadoc-plugin/3.4.1/confidence-slim/3.4.0)](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**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index feee9ca7f2..535dc572ad 100644 --- a/pom.xml +++ b/pom.xml @@ -186,7 +186,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.4.0 + 3.4.1 html From e6bbac0d30682911a4d8f61b1496b07d7ef336fc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 18 Aug 2022 17:50:18 +0200 Subject: [PATCH 07/16] build(deps): update dependency org.apache.maven.plugins:maven-project-info-reports-plugin to v3.4.1 (#1976) 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 | |---|---|---|---|---|---| | [org.apache.maven.plugins:maven-project-info-reports-plugin](https://maven.apache.org/plugins/) | `3.4.0` -> `3.4.1` | [![age](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-project-info-reports-plugin/3.4.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-project-info-reports-plugin/3.4.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-project-info-reports-plugin/3.4.1/compatibility-slim/3.4.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/org.apache.maven.plugins:maven-project-info-reports-plugin/3.4.1/confidence-slim/3.4.0)](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**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 535dc572ad..3e9582ad88 100644 --- a/pom.xml +++ b/pom.xml @@ -159,7 +159,7 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 3.4.0 + 3.4.1 From 7f5bac628a53efe01362844cbc999aed1a653d93 Mon Sep 17 00:00:00 2001 From: Gaurav Purohit Date: Mon, 22 Aug 2022 11:59:47 +0530 Subject: [PATCH 08/16] test: Creating dummy spanner instance for ITInstanceAdminTest (#1977) test: Updated ITInstanceAdminTest to run in a parallel environment with a new dummy instance --- .../cloud/spanner/IntegrationTestEnv.java | 11 ++++++++++- .../cloud/spanner/it/ITInstanceAdminTest.java | 17 ++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java index 9161b1da56..460b1000ce 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java @@ -58,8 +58,17 @@ public class IntegrationTestEnv extends ExternalResource { private InstanceAdminClient instanceAdminClient; private DatabaseAdminClient databaseAdminClient; private boolean isOwnedInstance; + private final boolean alwaysCreateNewInstance; private RemoteSpannerHelper testHelper; + public IntegrationTestEnv() { + this(false); + } + + public IntegrationTestEnv(final boolean alwaysCreateNewInstance) { + this.alwaysCreateNewInstance = alwaysCreateNewInstance; + } + public RemoteSpannerHelper getTestHelper() { checkInitialized(); return testHelper; @@ -84,7 +93,7 @@ protected void before() throws Throwable { SpannerOptions options = config.spannerOptions(); String instanceProperty = System.getProperty(TEST_INSTANCE_PROPERTY, ""); InstanceId instanceId; - if (!instanceProperty.isEmpty()) { + if (!instanceProperty.isEmpty() && !alwaysCreateNewInstance) { instanceId = InstanceId.of(instanceProperty); isOwnedInstance = false; logger.log(Level.INFO, "Using existing test instance: {0}", instanceId); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java index df541e84ef..e8e8ce2b4b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java @@ -27,13 +27,13 @@ import com.google.cloud.spanner.InstanceInfo; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.Options; -import com.google.cloud.spanner.SerialIntegrationTest; +import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.common.collect.Iterators; import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata; import java.util.ArrayList; import java.util.List; import java.util.Random; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; @@ -42,14 +42,15 @@ import org.junit.runners.JUnit4; /** Integration tests for {@link com.google.cloud.spanner.InstanceAdminClient}. */ -@Category(SerialIntegrationTest.class) +@Category(ParallelIntegrationTest.class) @RunWith(JUnit4.class) public class ITInstanceAdminTest { - @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - InstanceAdminClient instanceClient; - @Before - public void setUp() { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(true); + static InstanceAdminClient instanceClient; + + @BeforeClass + public static void setUp() { instanceClient = env.getTestHelper().getClient().getInstanceAdminClient(); } @@ -78,6 +79,8 @@ public void instanceConfigLeaderOptions() { @Test public void listInstances() { + assumeFalse("The emulator does not support filtering on instances", isUsingEmulator()); + Instance instance = Iterators.getOnlyElement( instanceClient From 8034c67af6cfe24e96cc26b1cea51c3405ed98d6 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Mon, 22 Aug 2022 15:22:46 +0530 Subject: [PATCH 09/16] feat: add support for db roles list (#1916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for db roles * fix sample indentations * fix tests * Fix tests * Delete samples * skip unsupported emulator tests * 🦉 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 --- .../clirr-ignored-differences.xml | 50 ++++++ .../com/google/cloud/spanner/Database.java | 20 ++- .../cloud/spanner/DatabaseAdminClient.java | 23 ++- .../spanner/DatabaseAdminClientImpl.java | 48 +++++- .../google/cloud/spanner/DatabaseRole.java | 78 +++++++++ .../google/cloud/spanner/SessionClient.java | 12 +- .../google/cloud/spanner/SpannerOptions.java | 19 +++ .../connection/AbstractStatementParser.java | 3 +- .../spanner/connection/ConnectionOptions.java | 25 ++- .../cloud/spanner/connection/SpannerPool.java | 5 + .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 56 +++++- .../cloud/spanner/spi/v1/SpannerRpc.java | 12 +- .../spanner/testing/RemoteSpannerHelper.java | 10 ++ .../cloud/spanner/BatchClientImplTest.java | 4 +- .../spanner/DatabaseAdminClientImplTest.java | 71 +++++++- .../spanner/DatabaseAdminClientTest.java | 4 +- .../google/cloud/spanner/DatabaseTest.java | 4 +- .../spanner/MockDatabaseAdminServiceImpl.java | 23 +++ .../cloud/spanner/SessionClientTests.java | 40 ++++- .../google/cloud/spanner/SessionImplTest.java | 5 +- .../spanner/SessionPoolMaintainerTest.java | 1 + .../cloud/spanner/SessionPoolStressTest.java | 1 + .../google/cloud/spanner/SessionPoolTest.java | 2 + .../google/cloud/spanner/SpannerImplTest.java | 5 +- .../spanner/TransactionManagerImplTest.java | 14 +- .../spanner/TransactionRunnerImplTest.java | 7 +- .../cloud/spanner/it/ITDatabaseAdminTest.java | 64 +++++++ .../it/ITDatabaseRolePermissionTest.java | 161 ++++++++++++++++++ synth.metadata | 4 + 29 files changed, 726 insertions(+), 45 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index b8aca39a27..fb952176bd 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -81,4 +81,54 @@ com/google/cloud/spanner/connection/Connection com.google.spanner.v1.ResultSetStats analyzeUpdate(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode) + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.paging.Page listDatabaseRoles(java.lang.String, java.lang.String, com.google.cloud.spanner.Options$ListOption[]) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.cloud.spanner.spi.v1.SpannerRpc$Paginated listDatabaseRoles(java.lang.String, int, java.lang.String) + + + 7004 + com/google/cloud/spanner/Database + com.google.cloud.Policy getIAMPolicy() + + + 7004 + com/google/cloud/spanner/DatabaseAdminClient + com.google.cloud.Policy getDatabaseIAMPolicy(java.lang.String, java.lang.String) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.spanner.v1.Session createSession(java.lang.String, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + java.util.List batchCreateSessions(java.lang.String, int, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.spanner.v1.Session createSession(java.lang.String, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + java.util.List batchCreateSessions(java.lang.String, int, java.util.Map, java.util.Map) + + + 7004 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String) + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java index eb3afdca70..94c30c9c70 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java @@ -143,9 +143,23 @@ public Page listDatabaseOperations() { instance(), Options.filter(String.format(FILTER_DB_OPERATIONS_TEMPLATE, database()))); } - /** Returns the IAM {@link Policy} for this database. */ - public Policy getIAMPolicy() { - return dbClient.getDatabaseIAMPolicy(instance(), database()); + /** + * Returns the IAM {@link Policy} for this database. + * + *

Version specifies the format used to create the policy, valid values are 0, 1, and 3. + * Requests specifying an invalid value will be rejected. Requests for policies with any + * conditional role bindings must specify version 3. Policies with no conditional role bindings + * may specify any valid value or leave the field unset. + * + *

The policy in the response 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. + * + *

To learn which resources support conditions in their IAM policies, see the [IAM + * documentation](https://cloud.google.com/iam/help/conditions/resource-policies). + */ + public Policy getIAMPolicy(int version) { + return dbClient.getDatabaseIAMPolicy(instance(), database(), version); } /** diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java index 26c48507c4..1363118e3a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java @@ -339,6 +339,9 @@ OperationFuture restoreDatabase(Restore resto /** Lists long-running database operations on the specified instance. */ Page listDatabaseOperations(String instanceId, ListOption... options); + /** Lists database roles on the specified database. */ + Page listDatabaseRoles(String instanceId, String databaseId, ListOption... options); + /** Lists long-running backup operations on the specified instance. */ Page listBackupOperations(String instanceId, ListOption... options); @@ -491,8 +494,24 @@ OperationFuture updateDatabaseDdl( /** Gets the specified long-running operation. */ Operation getOperation(String name); - /** Returns the IAM policy for the given database. */ - Policy getDatabaseIAMPolicy(String instanceId, String databaseId); + /** + * Returns the IAM policy for the given database. + * + *

Version specifies the format used to create the policy, valid values are 0, 1, and 3. + * Requests specifying an invalid value will be rejected. Requests for policies with any + * conditional role bindings must specify version 3. Policies with no conditional role bindings + * may specify any valid value or leave the field unset. + * + *

The policy in the response 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. + * + *

To learn which resources support conditions in their IAM policies, see the + * + * @see IAM + * documentation. + */ + Policy getDatabaseIAMPolicy(String instanceId, String databaseId, int version); /** * Updates the IAM policy for the given database and returns the resulting policy. It is highly diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java index 86b0bdbb8d..73ece214c3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java @@ -29,6 +29,7 @@ import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.iam.v1.GetPolicyOptions; import com.google.longrunning.Operation; import com.google.protobuf.Empty; import com.google.protobuf.FieldMask; @@ -290,6 +291,43 @@ public Operation fromProto(Operation proto) { return pageFetcher.getNextPage(); } + @Override + public final Page listDatabaseRoles( + String instanceId, String databaseId, ListOption... options) { + final String databaseName = getDatabaseName(instanceId, databaseId); + final Options listOptions = Options.fromListOptions(options); + final int pageSize = listOptions.hasPageSize() ? listOptions.pageSize() : 0; + + PageFetcher pageFetcher = + new PageFetcher() { + @Override + public Paginated getNextPage( + String nextPageToken) { + try { + return rpc.listDatabaseRoles(databaseName, pageSize, nextPageToken); + } catch (SpannerException e) { + throw SpannerExceptionFactory.newSpannerException( + e.getErrorCode(), + String.format( + "Failed to list the databases roles of %s with pageToken %s: %s", + databaseName, + MoreObjects.firstNonNull(nextPageToken, ""), + e.getMessage()), + e); + } + } + + @Override + public DatabaseRole fromProto(com.google.spanner.admin.database.v1.DatabaseRole proto) { + return DatabaseRole.fromProto(proto); + } + }; + if (listOptions.hasPageToken()) { + pageFetcher.setNextPageToken(listOptions.pageToken()); + } + return pageFetcher.getNextPage(); + } + @Override public Page listBackups(String instanceId, ListOption... options) { final String instanceName = getInstanceName(instanceId); @@ -463,9 +501,13 @@ public Operation getOperation(String name) { } @Override - public Policy getDatabaseIAMPolicy(String instanceId, String databaseId) { + public Policy getDatabaseIAMPolicy(String instanceId, String databaseId, int version) { final String databaseName = DatabaseId.of(projectId, instanceId, databaseId).getName(); - return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName)); + GetPolicyOptions options = null; + if (version > 0) { + options = GetPolicyOptions.newBuilder().setRequestedPolicyVersion(version).build(); + } + return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName, options)); } @Override @@ -487,7 +529,7 @@ public Iterable testDatabaseIAMPermissions( @Override public Policy getBackupIAMPolicy(String instanceId, String backupId) { final String databaseName = BackupId.of(projectId, instanceId, backupId).getName(); - return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName)); + return policyMarshaller.fromPb(rpc.getDatabaseAdminIAMPolicy(databaseName, null)); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java new file mode 100644 index 0000000000..37daf1cced --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseRole.java @@ -0,0 +1,78 @@ +/* + * 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.google.cloud.spanner; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.Objects; + +/** A Cloud Spanner database role. */ +public class DatabaseRole { + + public static class Builder { + + private final String name; + + public Builder(String name) { + this.name = Preconditions.checkNotNull(name); + } + + public DatabaseRole build() { + return new DatabaseRole(this.name); + } + } + + private final String name; + + @VisibleForTesting + DatabaseRole(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(o.getClass())) { + return false; + } + DatabaseRole databaseRole = (DatabaseRole) o; + return Objects.equals(name, databaseRole.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return String.format("DatabaseRole[%s]", name); + } + + static DatabaseRole fromProto(com.google.spanner.admin.database.v1.DatabaseRole proto) { + checkArgument(!proto.getName().isEmpty(), "Missing expected 'name' field"); + return new DatabaseRole.Builder(proto.getName()).build(); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java index cc9681b44d..474d99671e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java @@ -211,7 +211,11 @@ SessionImpl createSession() { com.google.spanner.v1.Session session = spanner .getRpc() - .createSession(db.getName(), spanner.getOptions().getSessionLabels(), options); + .createSession( + db.getName(), + spanner.getOptions().getDatabaseRole(), + spanner.getOptions().getSessionLabels(), + options); return new SessionImpl(spanner, session.getName(), options); } catch (RuntimeException e) { TraceUtil.setWithFailure(span, e); @@ -297,7 +301,11 @@ private List internalBatchCreateSessions( spanner .getRpc() .batchCreateSessions( - db.getName(), sessionCount, spanner.getOptions().getSessionLabels(), options); + db.getName(), + sessionCount, + spanner.getOptions().getDatabaseRole(), + spanner.getOptions().getSessionLabels(), + options); span.addAnnotation( String.format( "Request for %d sessions returned %d sessions", sessionCount, sessions.size())); 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 b62b2d4455..b117750654 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 @@ -99,6 +99,7 @@ public class SpannerOptions extends ServiceOptions { private final int prefetchChunks; private final int numChannels; private final String transportChannelExecutorThreadNameFormat; + private final String databaseRole; private final ImmutableMap sessionLabels; private final SpannerStubSettings spannerStubSettings; private final InstanceAdminStubSettings instanceAdminStubSettings; @@ -564,6 +565,7 @@ private SpannerOptions(Builder builder) { ? builder.sessionPoolOptions : SessionPoolOptions.newBuilder().build(); prefetchChunks = builder.prefetchChunks; + databaseRole = builder.databaseRole; sessionLabels = builder.sessionLabels; try { spannerStubSettings = builder.spannerStubSettingsBuilder.build(); @@ -674,6 +676,7 @@ public static class Builder private int prefetchChunks = DEFAULT_PREFETCH_CHUNKS; private SessionPoolOptions sessionPoolOptions; + private String databaseRole; private ImmutableMap sessionLabels; private SpannerStubSettings.Builder spannerStubSettingsBuilder = SpannerStubSettings.newBuilder(); @@ -730,6 +733,7 @@ private Builder() { options.transportChannelExecutorThreadNameFormat; this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; + this.databaseRole = options.databaseRole; this.sessionLabels = options.sessionLabels; this.spannerStubSettingsBuilder = options.spannerStubSettings.toBuilder(); this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); @@ -830,6 +834,17 @@ public Builder setSessionPoolOption(SessionPoolOptions sessionPoolOptions) { return this; } + /** + * Sets the database role that should be used for connections that are created by this instance. + * The database role that is used determines the access permissions that a connection has. This + * can for example be used to create connections that are only permitted to access certain + * tables. + */ + public Builder setDatabaseRole(String databaseRole) { + this.databaseRole = databaseRole; + return this; + } + /** * Sets the labels to add to all Sessions created in this client. * @@ -1215,6 +1230,10 @@ public SessionPoolOptions getSessionPoolOptions() { return sessionPoolOptions; } + public String getDatabaseRole() { + return databaseRole; + } + public Map getSessionLabels() { return sessionLabels; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java index b2b94c0213..fb272e913e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java @@ -320,7 +320,8 @@ ClientSideStatement getClientSideStatement() { } } - static final Set ddlStatements = ImmutableSet.of("CREATE", "DROP", "ALTER", "ANALYZE"); + static final Set ddlStatements = + ImmutableSet.of("CREATE", "DROP", "ALTER", "ANALYZE", "GRANT", "REVOKE"); static final Set selectStatements = ImmutableSet.of("SELECT", "WITH", "SHOW"); static final Set dmlStatements = ImmutableSet.of("INSERT", "UPDATE", "DELETE"); private final Set statements; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 7923156da1..e44a99c1ec 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -165,6 +165,7 @@ public String[] getValidValues() { private static final String DEFAULT_MAX_SESSIONS = null; private static final String DEFAULT_NUM_CHANNELS = null; private static final String DEFAULT_CHANNEL_PROVIDER = null; + private static final String DEFAULT_DATABASE_ROLE = null; private static final String DEFAULT_USER_AGENT = null; private static final String DEFAULT_OPTIMIZER_VERSION = ""; private static final String DEFAULT_OPTIMIZER_STATISTICS_PACKAGE = ""; @@ -215,7 +216,8 @@ public String[] getValidValues() { public static final String RPC_PRIORITY_NAME = "rpcPriority"; /** Dialect to use for a connection. */ private static final String DIALECT_PROPERTY_NAME = "dialect"; - + /** Name of the 'databaseRole' connection property. */ + public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole"; /** All valid connection properties. */ public static final Set VALID_PROPERTIES = Collections.unmodifiableSet( @@ -282,7 +284,10 @@ public String[] getValidValues() { RPC_PRIORITY_NAME, "Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH."), ConnectionProperty.createStringProperty( - DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection.")))); + DIALECT_PROPERTY_NAME, "Sets the dialect to use for this connection."), + ConnectionProperty.createStringProperty( + DATABASE_ROLE_PROPERTY_NAME, + "Sets the database role to use for this connection. The default is privileges assigned to IAM role")))); private static final Set INTERNAL_PROPERTIES = Collections.unmodifiableSet( @@ -533,6 +538,7 @@ public static Builder newBuilder() { private final String channelProvider; private final Integer minSessions; private final Integer maxSessions; + private final String databaseRole; private final String userAgent; private final QueryOptions queryOptions; private final boolean returnCommitStats; @@ -620,6 +626,7 @@ private ConnectionOptions(Builder builder) { this.numChannels = parseIntegerProperty(NUM_CHANNELS_PROPERTY_NAME, parseNumChannels(builder.uri)); this.channelProvider = parseChannelProvider(builder.uri); + this.databaseRole = parseDatabaseRole(this.uri); String projectId = matcher.group(Builder.PROJECT_GROUP); if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { @@ -790,6 +797,12 @@ static String parseChannelProvider(String uri) { return value != null ? value : DEFAULT_CHANNEL_PROVIDER; } + @VisibleForTesting + static String parseDatabaseRole(String uri) { + String value = parseUriProperty(uri, DATABASE_ROLE_PROPERTY_NAME); + return value != null ? value : DEFAULT_DATABASE_ROLE; + } + @VisibleForTesting static String parseUserAgent(String uri) { String value = parseUriProperty(uri, USER_AGENT_PROPERTY_NAME); @@ -959,6 +972,14 @@ public TransportChannelProvider getChannelProvider() { } } + /** + * The database role that is used for this connection. Assigning a role to a connection can be + * used to for example restrict the access of a connection to a specific set of tables. + */ + public String getDatabaseRole() { + return databaseRole; + } + /** The host and port number that this {@link ConnectionOptions} will connect to */ public String getHost() { return host; 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 f94e27d963..6a57779020 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 @@ -153,6 +153,7 @@ static class SpannerPoolKey { private final Integer numChannels; private final boolean usePlainText; private final String userAgent; + private final String databaseRole; @VisibleForTesting static SpannerPoolKey of(ConnectionOptions options) { @@ -170,6 +171,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException { this.host = options.getHost(); this.projectId = options.getProjectId(); this.credentialsKey = CredentialsKey.create(options); + this.databaseRole = options.getDatabaseRole(); this.sessionPoolOptions = options.getSessionPoolOptions() == null ? SessionPoolOptions.newBuilder().build() @@ -190,6 +192,7 @@ public boolean equals(Object o) { && Objects.equals(this.credentialsKey, other.credentialsKey) && Objects.equals(this.sessionPoolOptions, other.sessionPoolOptions) && Objects.equals(this.numChannels, other.numChannels) + && Objects.equals(this.databaseRole, other.databaseRole) && Objects.equals(this.usePlainText, other.usePlainText) && Objects.equals(this.userAgent, other.userAgent); } @@ -203,6 +206,7 @@ public int hashCode() { this.sessionPoolOptions, this.numChannels, this.usePlainText, + this.databaseRole, this.userAgent); } } @@ -329,6 +333,7 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) { .setClientLibToken(MoreObjects.firstNonNull(key.userAgent, CONNECTION_API_CLIENT_LIB_TOKEN)) .setHost(key.host) .setProjectId(key.projectId) + .setDatabaseRole(options.getDatabaseRole()) .setCredentials(options.getCredentials()); builder.setSessionPoolOption(key.sessionPoolOptions); if (key.numChannels != null) { 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 3a20c2a3c7..937da7e8d2 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 @@ -89,6 +89,7 @@ import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.ThreadFactoryBuilder; 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.iam.v1.TestIamPermissionsRequest; @@ -111,6 +112,7 @@ import com.google.spanner.admin.database.v1.CreateDatabaseRequest; import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.DatabaseAdminGrpc; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.DeleteBackupRequest; import com.google.spanner.admin.database.v1.DropDatabaseRequest; import com.google.spanner.admin.database.v1.GetBackupRequest; @@ -122,6 +124,8 @@ import com.google.spanner.admin.database.v1.ListBackupsResponse; import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest; import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse; +import com.google.spanner.admin.database.v1.ListDatabaseRolesRequest; +import com.google.spanner.admin.database.v1.ListDatabaseRolesResponse; import com.google.spanner.admin.database.v1.ListDatabasesRequest; import com.google.spanner.admin.database.v1.ListDatabasesResponse; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; @@ -1002,6 +1006,27 @@ public Paginated listDatabaseOperations( return new Paginated<>(response.getOperationsList(), response.getNextPageToken()); } + @Override + public Paginated listDatabaseRoles( + String databaseName, int pageSize, @Nullable String pageToken) { + acquireAdministrativeRequestsRateLimiter(); + ListDatabaseRolesRequest.Builder requestBuilder = + ListDatabaseRolesRequest.newBuilder().setParent(databaseName).setPageSize(pageSize); + + if (pageToken != null) { + requestBuilder.setPageToken(pageToken); + } + final ListDatabaseRolesRequest request = requestBuilder.build(); + + final GrpcCallContext context = + newCallContext(null, databaseName, request, DatabaseAdminGrpc.getListDatabaseRolesMethod()); + ListDatabaseRolesResponse response = + runWithRetryOnAdministrativeRequestsExceeded( + () -> get(databaseAdminStub.listDatabaseRolesCallable().futureCall(request, context))); + + return new Paginated<>(response.getDatabaseRolesList(), response.getNextPageToken()); + } + @Override public Paginated listBackups( String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken) @@ -1450,6 +1475,7 @@ public void cancelOperation(String name) throws SpannerException { public List batchCreateSessions( String databaseName, int sessionCount, + @Nullable String databaseRole, @Nullable Map labels, @Nullable Map options) throws SpannerException { @@ -1457,10 +1483,14 @@ public List batchCreateSessions( BatchCreateSessionsRequest.newBuilder() .setDatabase(databaseName) .setSessionCount(sessionCount); + Session.Builder sessionBuilder = Session.newBuilder(); if (labels != null && !labels.isEmpty()) { - Session.Builder session = Session.newBuilder().putAllLabels(labels); - requestBuilder.setSessionTemplate(session); + sessionBuilder.putAllLabels(labels); + } + if (databaseRole != null && !databaseRole.isEmpty()) { + sessionBuilder.setCreatorRole(databaseRole); } + requestBuilder.setSessionTemplate(sessionBuilder); BatchCreateSessionsRequest request = requestBuilder.build(); GrpcCallContext context = newCallContext(options, databaseName, request, SpannerGrpc.getBatchCreateSessionsMethod()); @@ -1470,14 +1500,21 @@ public List batchCreateSessions( @Override public Session createSession( - String databaseName, @Nullable Map labels, @Nullable Map options) + String databaseName, + @Nullable String databaseRole, + @Nullable Map labels, + @Nullable Map options) throws SpannerException { CreateSessionRequest.Builder requestBuilder = CreateSessionRequest.newBuilder().setDatabase(databaseName); + Session.Builder sessionBuilder = Session.newBuilder(); if (labels != null && !labels.isEmpty()) { - Session.Builder session = Session.newBuilder().putAllLabels(labels); - requestBuilder.setSession(session); + sessionBuilder.putAllLabels(labels); } + if (databaseRole != null && !databaseRole.isEmpty()) { + sessionBuilder.setCreatorRole(databaseRole); + } + requestBuilder.setSession(sessionBuilder); CreateSessionRequest request = requestBuilder.build(); GrpcCallContext context = newCallContext(options, databaseName, request, SpannerGrpc.getCreateSessionMethod()); @@ -1658,10 +1695,13 @@ public PartitionResponse partitionRead( } @Override - public Policy getDatabaseAdminIAMPolicy(String resource) { + public Policy getDatabaseAdminIAMPolicy(String resource, @Nullable GetPolicyOptions options) { acquireAdministrativeRequestsRateLimiter(); - final GetIamPolicyRequest request = - GetIamPolicyRequest.newBuilder().setResource(resource).build(); + GetIamPolicyRequest.Builder builder = GetIamPolicyRequest.newBuilder().setResource(resource); + if (options != null) { + builder.setOptions(options); + } + final GetIamPolicyRequest request = builder.build(); final GrpcCallContext context = newCallContext(null, resource, request, DatabaseAdminGrpc.getGetIamPolicyMethod()); return runWithRetryOnAdministrativeRequestsExceeded( 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 00382e228f..189af2fb55 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,6 +28,7 @@ import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; import com.google.common.collect.ImmutableList; +import com.google.iam.v1.GetPolicyOptions; import com.google.iam.v1.Policy; import com.google.iam.v1.TestIamPermissionsResponse; import com.google.longrunning.Operation; @@ -253,6 +254,9 @@ Paginated listBackupOperations( Paginated listDatabaseOperations( String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken); + Paginated listDatabaseRoles( + String databaseName, int pageSize, @Nullable String pageToken); + /** Retrieves a long running operation. */ Operation getOperation(String name) throws SpannerException; @@ -262,12 +266,16 @@ Paginated listDatabaseOperations( List batchCreateSessions( String databaseName, int sessionCount, + @Nullable String databaseRole, @Nullable Map labels, @Nullable Map options) throws SpannerException; Session createSession( - String databaseName, @Nullable Map labels, @Nullable Map options) + String databaseName, + @Nullable String databaseRole, + @Nullable Map labels, + @Nullable Map options) throws SpannerException; void deleteSession(String sessionName, @Nullable Map options) throws SpannerException; @@ -321,7 +329,7 @@ PartitionResponse partitionRead(PartitionReadRequest request, @Nullable Map dbs = new ArrayList<>(); @@ -107,6 +109,14 @@ public String getUniqueDatabaseId() { return String.format("testdb_%d_%04d", dbPrefix, dbSeq.incrementAndGet()); } + /** + * Returns a database role name which is guaranteed to be unique within the context of this + * environment. + */ + public String getUniqueDatabaseRole() { + return String.format("testdbrole_%d_%04d", dbRolePrefix, dbRoleSeq.incrementAndGet()); + } + /** * Returns a backup id which is guaranteed to be unique within the context of this environment. */ diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java index 91fbac83e6..211f3969cc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.anyMap; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -66,6 +67,7 @@ public void setUp() { initMocks(this); DatabaseId db = DatabaseId.of(DB_NAME); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getPrefetchChunks()).thenReturn(1); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); @@ -83,7 +85,7 @@ public void setUp() { @Test public void testBatchReadOnlyTxnWithBound() throws Exception { Session sessionProto = Session.newBuilder().setName(SESSION_NAME).build(); - when(gapicRpc.createSession(eq(DB_NAME), anyMap(), optionsCaptor.capture())) + when(gapicRpc.createSession(eq(DB_NAME), anyString(), anyMap(), optionsCaptor.capture())) .thenReturn(sessionProto); com.google.protobuf.Timestamp timestamp = Timestamps.parse(TIMESTAMP); Transaction txnMetadata = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java index 56dfd70730..0255f6c166 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java @@ -49,9 +49,11 @@ import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.EncryptionInfo; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -74,6 +76,8 @@ public class DatabaseAdminClientImplTest { "projects/my-project/instances/my-instance/databases/my-db2"; private static final String BK_ID = "my-bk"; private static final String SOURCE_BK = "my-source-bk"; + private static final String DB_ROLE = "dummy-role"; + private static final String DB_ROLE2 = "dummy-role-2"; private static final String BK_NAME = "projects/my-project/instances/my-instance/backups/my-bk"; private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2"; private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now(); @@ -102,6 +106,14 @@ private Database getDatabaseProto() { .build(); } + private DatabaseRole getDatabaseRoleProto() { + return DatabaseRole.newBuilder().setName(DB_ROLE).build(); + } + + private DatabaseRole getAnotherDatabaseRoleProto() { + return DatabaseRole.newBuilder().setName(DB_ROLE2).build(); + } + private Database getEncryptedDatabaseProto() { return getDatabaseProto() .toBuilder() @@ -293,9 +305,60 @@ public void listDatabaseErrorWithToken() { assertThat(e.getMessage()).contains(String.format("with pageToken %s", pageToken)); } + @Test + public void listDatabaseRoles() { + String pageToken = "token"; + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenReturn(new Paginated<>(ImmutableList.of(getDatabaseRoleProto()), pageToken)); + when(rpc.listDatabaseRoles(DB_NAME, 1, pageToken)) + .thenReturn(new Paginated<>(ImmutableList.of(getAnotherDatabaseRoleProto()), "")); + + ArrayList databaseRoles = + Lists.newArrayList( + client.listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1)).iterateAll()); + assertThat(databaseRoles.get(0).getName()).isEqualTo(DB_ROLE); + assertThat(databaseRoles.get(1).getName()).isEqualTo(DB_ROLE2); + assertThat(databaseRoles.size()).isEqualTo(2); + } + + @Test + public void listDatabaseRolesError() { + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenThrow( + SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error")); + SpannerException e = + assertThrows( + SpannerException.class, + () -> client.listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1))); + assertThat(e.getMessage()).contains(INSTANCE_NAME); + // Assert that the call was done without a page token. + assertThat(e.getMessage()).contains("with pageToken "); + } + + @Test + public void listDatabaseRolesErrorWithToken() { + String pageToken = "token"; + when(rpc.listDatabaseRoles(DB_NAME, 1, null)) + .thenReturn(new Paginated<>(ImmutableList.of(getDatabaseRoleProto()), pageToken)); + when(rpc.listDatabaseRoles(DB_NAME, 1, pageToken)) + .thenThrow( + SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "Test error")); + SpannerException e = + assertThrows( + SpannerException.class, + () -> + Lists.newArrayList( + client + .listDatabaseRoles(INSTANCE_ID, DB_ID, Options.pageSize(1)) + .iterateAll())); + assertThat(e.getMessage()).contains(INSTANCE_NAME); + // Assert that the call was done without a page token. + assertThat(e.getMessage()).contains(String.format("with pageToken %s", pageToken)); + } + @Test public void getDatabaseIAMPolicy() { - when(rpc.getDatabaseAdminIAMPolicy(DB_NAME)) + when(rpc.getDatabaseAdminIAMPolicy(DB_NAME, null)) .thenReturn( Policy.newBuilder() .addBindings( @@ -304,11 +367,11 @@ public void getDatabaseIAMPolicy() { .setRole("roles/viewer") .build()) .build()); - com.google.cloud.Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + com.google.cloud.Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 0); assertThat(policy.getBindings()) .containsExactly(Role.viewer(), Sets.newHashSet(Identity.user("joe@example.com"))); - when(rpc.getDatabaseAdminIAMPolicy(DB_NAME)) + when(rpc.getDatabaseAdminIAMPolicy(DB_NAME, null)) .thenReturn( Policy.newBuilder() .addBindings( @@ -317,7 +380,7 @@ public void getDatabaseIAMPolicy() { .setRole("roles/viewer") .build()) .build()); - policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 0); assertThat(policy.getBindings()) .containsExactly( Role.viewer(), diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java index 9dcadc85ad..eee9f520a7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java @@ -754,13 +754,13 @@ public void backupListBackupOperations() @Test public void getAndSetIAMPolicy() { - Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID); + Policy policy = client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 1); assertThat(policy).isEqualTo(Policy.newBuilder().build()); Policy newPolicy = Policy.newBuilder().addIdentity(Role.editor(), Identity.user("joe@example.com")).build(); Policy returnedPolicy = client.setDatabaseIAMPolicy(INSTANCE_ID, DB_ID, newPolicy); assertThat(returnedPolicy).isEqualTo(newPolicy); - assertThat(client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID)).isEqualTo(newPolicy); + assertThat(client.getDatabaseIAMPolicy(INSTANCE_ID, DB_ID, 1)).isEqualTo(newPolicy); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java index bd09d131a6..1eb2794c8e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java @@ -183,8 +183,8 @@ public void getIAMPolicy() { Database database = new Database( DatabaseId.of("test-project", "test-instance", "test-database"), State.READY, dbClient); - database.getIAMPolicy(); - verify(dbClient).getDatabaseIAMPolicy("test-instance", "test-database"); + database.getIAMPolicy(1); + verify(dbClient).getDatabaseIAMPolicy("test-instance", "test-database", 1); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java index 6b248f7902..4165f168d8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java @@ -40,6 +40,7 @@ import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.Database.State; import com.google.spanner.admin.database.v1.DatabaseAdminGrpc.DatabaseAdminImplBase; +import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.DeleteBackupRequest; import com.google.spanner.admin.database.v1.DropDatabaseRequest; import com.google.spanner.admin.database.v1.GetBackupRequest; @@ -52,6 +53,8 @@ import com.google.spanner.admin.database.v1.ListBackupsResponse; import com.google.spanner.admin.database.v1.ListDatabaseOperationsRequest; import com.google.spanner.admin.database.v1.ListDatabaseOperationsResponse; +import com.google.spanner.admin.database.v1.ListDatabaseRolesRequest; +import com.google.spanner.admin.database.v1.ListDatabaseRolesResponse; import com.google.spanner.admin.database.v1.ListDatabasesRequest; import com.google.spanner.admin.database.v1.ListDatabasesResponse; import com.google.spanner.admin.database.v1.OperationProgress; @@ -436,6 +439,7 @@ private com.google.rpc.Status fromException(Exception e) { private final ConcurrentMap databases = new ConcurrentHashMap<>(); private final ConcurrentMap backups = new ConcurrentHashMap<>(); private final ConcurrentMap> filterMatches = new ConcurrentHashMap<>(); + private final List databaseRoles = new ArrayList<>(); private final MockOperationsServiceImpl operations; private long createBackupOperationExecutionTime; @@ -581,6 +585,25 @@ public void listDatabaseOperations( } } + @Override + public void listDatabaseRoles( + ListDatabaseRolesRequest request, + StreamObserver responseObserver) { + requests.add(request); + List dbRoles = + new ArrayList<>(databaseRoles.size()); + for (DatabaseRole entry : databaseRoles) { + dbRoles.add( + com.google.spanner.admin.database.v1.DatabaseRole.newBuilder() + .setName(entry.getName()) + .build()); + } + + responseObserver.onNext( + ListDatabaseRolesResponse.newBuilder().addAllDatabaseRoles(dbRoles).build()); + responseObserver.onCompleted(); + } + private boolean matchesFilter(Object obj, String filter) throws Exception { if (!Strings.isNullOrEmpty(filter)) { Set matches = filterMatches.get(filter); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java index 6d1f88b26f..8af1323238 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java @@ -105,6 +105,7 @@ public ScheduledExecutorService get() { when(spannerOptions.getTransportOptions()).thenReturn(transportOptions); when(spannerOptions.getNumChannels()).thenReturn(numChannels); when(spannerOptions.getPrefetchChunks()).thenReturn(1); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); when(spanner.getOptions()).thenReturn(spannerOptions); @@ -117,13 +118,16 @@ public void createAndCloseSession() { String sessionName = dbName + "/sessions/s1"; Map labels = new HashMap<>(); labels.put("env", "dev"); + String databaseRole = "role"; when(spannerOptions.getSessionLabels()).thenReturn(labels); + when(spannerOptions.getDatabaseRole()).thenReturn(databaseRole); com.google.spanner.v1.Session sessionProto = com.google.spanner.v1.Session.newBuilder() .setName(sessionName) .putAllLabels(labels) .build(); - when(rpc.createSession(Mockito.eq(dbName), Mockito.eq(labels), options.capture())) + when(rpc.createSession( + Mockito.eq(dbName), Mockito.eq(databaseRole), Mockito.eq(labels), options.capture())) .thenReturn(sessionProto); try (SessionClient client = new SessionClient(spanner, db, new TestExecutorFactory())) { @@ -144,12 +148,18 @@ public void batchCreateAndCloseSessions() { final Map labels = new HashMap<>(); labels.put("env", "dev"); when(spannerOptions.getSessionLabels()).thenReturn(labels); + String databaseRole = new String("role"); + when(spannerOptions.getDatabaseRole()).thenReturn(databaseRole); final List usedChannels = Collections.synchronizedList(new ArrayList<>()); when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.eq(labels), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.eq(databaseRole), + Mockito.eq(labels), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); usedChannels.add(channelHint); int sessionCount = invocation.getArgument(1, Integer.class); @@ -204,12 +214,17 @@ public void batchCreateSessionsDistributesMultipleRequestsOverChannels() { final String sessionName = dbName + "/sessions/s%d"; final Map labels = Collections.emptyMap(); when(spannerOptions.getSessionLabels()).thenReturn(labels); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); final Set usedChannelHints = Collections.synchronizedSet(new HashSet<>()); when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.eq(labels), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.eq(labels), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); usedChannelHints.add(channelHint); int sessionCount = invocation.getArgument(1, Integer.class); @@ -219,6 +234,7 @@ public void batchCreateSessionsDistributesMultipleRequestsOverChannels() { com.google.spanner.v1.Session.newBuilder() .setName(String.format(sessionName, i)) .putAllLabels(labels) + .setCreatorRole("role") .build()); } return res; @@ -287,10 +303,14 @@ public void batchCreateSessionsWithExceptions() { DatabaseId db = DatabaseId.of(dbName); final String sessionName = dbName + "/sessions/s%d"; when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.anyMap(), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .then( invocation -> { - Map options = invocation.getArgument(3, Map.class); + Map options = invocation.getArgument(4, Map.class); Long channelHint = (Long) options.get(Option.CHANNEL_HINT); if (errorOnChannels.contains(channelHint)) { throw SpannerExceptionFactory.newSpannerException( @@ -351,7 +371,11 @@ public void batchCreateSessionsServerReturnsLessSessionsPerBatch() { DatabaseId db = DatabaseId.of(dbName); final String sessionName = dbName + "/sessions/s%d"; when(rpc.batchCreateSessions( - Mockito.eq(dbName), Mockito.anyInt(), Mockito.anyMap(), Mockito.anyMap())) + Mockito.eq(dbName), + Mockito.anyInt(), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .then( invocation -> { int sessionCount = invocation.getArgument(1, Integer.class); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java index cf4e6dda81..1174cbf4eb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java @@ -78,6 +78,7 @@ public void setUp() { MockitoAnnotations.initMocks(this); when(spannerOptions.getNumChannels()).thenReturn(4); when(spannerOptions.getPrefetchChunks()).thenReturn(1); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); when(spannerOptions.getSessionLabels()).thenReturn(Collections.emptyMap()); @@ -92,7 +93,9 @@ public void setUp() { DatabaseId db = DatabaseId.of(dbName); Session sessionProto = Session.newBuilder().setName(sessionName).build(); - Mockito.when(rpc.createSession(Mockito.eq(dbName), Mockito.anyMap(), optionsCaptor.capture())) + Mockito.when( + rpc.createSession( + Mockito.eq(dbName), Mockito.anyString(), Mockito.anyMap(), optionsCaptor.capture())) .thenReturn(sessionProto); Transaction txn = Transaction.newBuilder().setId(ByteString.copyFromUtf8("TEST")).build(); Mockito.when( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index 160e32d084..ab6a51c926 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -59,6 +59,7 @@ public void setUp() { when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); setupMockSessionCreation(); options = SessionPoolOptions.newBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index ada30d0c8e..9a1df1c964 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -89,6 +89,7 @@ private void setupSpanner(DatabaseId db) { mockSpanner = mock(SpannerImpl.class); spannerOptions = mock(SpannerOptions.class); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); SessionClient sessionClient = mock(SessionClient.class); when(mockSpanner.getSessionClient(db)).thenReturn(sessionClient); when(mockSpanner.getOptions()).thenReturn(spannerOptions); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index 4efe2cfef3..3f018e382d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -140,6 +140,7 @@ public void setUp() { when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); options = SessionPoolOptions.newBuilder() .setMinSessions(minSessions) @@ -847,6 +848,7 @@ public void testSessionNotFoundReadWriteTransaction() { SpannerOptions spannerOptions = mock(SpannerOptions.class); when(spannerOptions.getSessionPoolOptions()).thenReturn(options); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spanner.getOptions()).thenReturn(spannerOptions); SessionPool pool = SessionPool.createPool(options, new TestExecutorFactory(), spanner.getSessionClient(db)); 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 0cc0e6e9fb..2d098d9700 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 @@ -61,6 +61,7 @@ public class SpannerImplTest { public void setUp() { MockitoAnnotations.initMocks(this); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getPrefetchChunks()).thenReturn(1); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); when(spannerOptions.getClock()).thenReturn(NanoClock.getDefaultClock()); @@ -78,6 +79,7 @@ public void getDbclientAgainGivesSame() { Map labels = new HashMap<>(); labels.put("env", "dev"); Mockito.when(spannerOptions.getSessionLabels()).thenReturn(labels); + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); String dbName = "projects/p1/instances/i1/databases/d1"; DatabaseId db = DatabaseId.of(dbName); @@ -160,7 +162,7 @@ public void getDbclientAfterCloseThrows() { .thenReturn(GrpcTransportOptions.newBuilder().build()); Mockito.when(spannerOptions.getSessionPoolOptions()) .thenReturn(SessionPoolOptions.newBuilder().build()); - + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); imp.close(); IllegalStateException e = @@ -210,6 +212,7 @@ public void testClientId() { .thenReturn(GrpcTransportOptions.newBuilder().build()); Mockito.when(spannerOptions.getSessionPoolOptions()) .thenReturn(SessionPoolOptions.newBuilder().setMinSessions(0).build()); + Mockito.when(spannerOptions.getDatabaseRole()).thenReturn("role"); DatabaseClientImpl databaseClient = (DatabaseClientImpl) impl.getDatabaseClient(db); assertThat(databaseClient.clientId).isEqualTo("client-1"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 9ca2c77f62..55df44a96d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -198,11 +198,16 @@ public void usesPreparedTransaction() { SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDatabaseRole()).thenReturn("role"); SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( @@ -257,8 +262,13 @@ public void inlineBegin() { SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(options.getDatabaseRole()).thenReturn("role"); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index 95595607bc..04ac46d887 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -142,11 +142,16 @@ public void usesPreparedTransaction() { SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDatabaseRole()).thenReturn("role"); SpannerRpc rpc = mock(SpannerRpc.class); when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); when(rpc.batchCreateSessions( - Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + Mockito.anyString(), + Mockito.eq(1), + Mockito.anyString(), + Mockito.anyMap(), + Mockito.anyMap())) .thenAnswer( invocation -> Collections.singletonList( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java index bc9a075e0c..e19272a076 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java @@ -16,15 +16,18 @@ 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.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.paging.Page; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseRole; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.IntegrationTestEnv; @@ -205,4 +208,65 @@ public void listPagination() throws Exception { } assertThat(dbIdsGot).containsAtLeastElementsIn(dbIds); } + + @Test + public void createAndListDatabaseRoles() throws Exception { + assumeFalse("Emulator does not support create & list database roles", isUsingEmulator()); + List dbRoles = + ImmutableList.of( + testHelper.getUniqueDatabaseRole(), + testHelper.getUniqueDatabaseRole(), + testHelper.getUniqueDatabaseRole()); + + String instanceId = testHelper.getInstanceId().getInstance(); + Database database = + dbAdminClient + .createDatabase(instanceId, testHelper.getUniqueDatabaseId(), ImmutableList.of()) + .get(); + + // Create the roles in Db. + List dbRolesCreateStatements = new ArrayList<>(); + for (String dbRole : dbRoles) { + dbRolesCreateStatements.add(String.format("CREATE ROLE %s", dbRole)); + } + dbAdminClient + .updateDatabaseDdl( + instanceId, database.getId().getDatabase(), dbRolesCreateStatements, null) + .get(); + + // List roles from Db. + Page page = + dbAdminClient.listDatabaseRoles(instanceId, database.getId().getDatabase()); + List dbRolesGot = new ArrayList<>(); + while (page != null && page.getValues().iterator().hasNext()) { + for (DatabaseRole value : page.getValues()) { + String[] split = value.getName().split("/"); + dbRolesGot.add(split[split.length - 1]); + } + page = page.getNextPage(); + } + assertThat(dbRolesGot).containsAtLeastElementsIn(dbRoles); + + // Delete the created roles. + List dbRolesDropStatements = new ArrayList<>(); + for (String dbRole : dbRoles) { + dbRolesDropStatements.add(String.format("DROP ROLE %s", dbRole)); + } + dbAdminClient + .updateDatabaseDdl(instanceId, database.getId().getDatabase(), dbRolesDropStatements, null) + .get(); + + // List roles from Db. Deleted roles should not be present in list. + Page pageRemainingRoles = + dbAdminClient.listDatabaseRoles(instanceId, database.getId().getDatabase()); + List dbRolesRemaining = new ArrayList<>(); + while (pageRemainingRoles != null && pageRemainingRoles.getValues().iterator().hasNext()) { + for (DatabaseRole value : pageRemainingRoles.getValues()) { + String[] split = value.getName().split("/"); + dbRolesRemaining.add(split[split.length - 1]); + } + pageRemainingRoles = pageRemainingRoles.getNextPage(); + } + assertThat(dbRolesRemaining).containsNoneIn(dbRoles); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java new file mode 100644 index 0000000000..842df028dd --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java @@ -0,0 +1,161 @@ +/* + * 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.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.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.ErrorCode; +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.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for Role Permissions using {@link com.google.cloud.spanner.DatabaseRole}. */ +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITDatabaseRolePermissionTest { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private DatabaseAdminClient dbAdminClient; + private RemoteSpannerHelper testHelper; + + @Before + public void setUp() { + assumeFalse("Emulator does not support database roles", isUsingEmulator()); + testHelper = env.getTestHelper(); + dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); + } + + @Test + public void grantAndRevokeDatabaseRolePermissions() throws Exception { + // Create database with table and role permission. + final String dbRoleParent = "parent"; + final String databaseId = testHelper.getUniqueDatabaseId(); + final String instanceId = testHelper.getInstanceId().getInstance(); + + final String createTableT = "CREATE TABLE T (\n" + " K STRING(MAX),\n" + ") PRIMARY KEY(K)"; + final String createRoleParent = String.format("CREATE ROLE %s", dbRoleParent); + final String grantSelectOnTableToParent = + String.format("GRANT SELECT ON TABLE T TO ROLE %s", dbRoleParent); + + final Database createdDatabase = + dbAdminClient + .createDatabase( + instanceId, + databaseId, + ImmutableList.of(createTableT, createRoleParent, grantSelectOnTableToParent)) + .get(5, TimeUnit.MINUTES); + + // Connect to db with dbRoleParent. + SpannerOptions options = SpannerOptions.newBuilder().setDatabaseRole(dbRoleParent).build(); + + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + + // 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()); + } catch (PermissionDeniedException e) { + // This is not expected + fail("Got PermissionDeniedException when it should not have occurred."); + } + + // Revoke select Permission for dbRoleParent. + final String revokeSelectOnTableFromParent = + String.format("REVOKE SELECT ON TABLE T FROM ROLE %s", dbRoleParent); + + dbAdminClient + .updateDatabaseDdl( + instanceId, databaseId, Arrays.asList(revokeSelectOnTableFromParent), null) + .get(); + + // 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 + .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) + .get(); + } + + @Test + public void roleWithNoPermissions() throws Exception { + final String dbRoleOrphan = testHelper.getUniqueDatabaseRole(); + final String databaseId = testHelper.getUniqueDatabaseId(); + final String instanceId = testHelper.getInstanceId().getInstance(); + + final String createTableT = "CREATE TABLE T (\n" + " K STRING(MAX),\n" + ") PRIMARY KEY(K)"; + final String createRoleOrphan = String.format("CREATE ROLE %s", dbRoleOrphan); + + final Database createdDatabase = + dbAdminClient + .createDatabase( + instanceId, databaseId, ImmutableList.of(createTableT, createRoleOrphan)) + .get(5, TimeUnit.MINUTES); + + // Connect to db with dbRoleOrphan + SpannerOptions options = SpannerOptions.newBuilder().setDatabaseRole(dbRoleOrphan).build(); + + Spanner spanner = options.getService(); + DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); + DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); + + // 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 + .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) + .get(); + } +} diff --git a/synth.metadata b/synth.metadata index 6e6fa5d805..099e30c368 100644 --- a/synth.metadata +++ b/synth.metadata @@ -201,6 +201,10 @@ "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesRequestOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesResponse.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabasesResponseOrBuilder.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesRequest.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesRequestOrBuilder.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesResponse.java", + "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/ListDatabaseRolesResponseOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OperationProgress.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OperationProgressOrBuilder.java", "proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/OptimizeRestoredDatabaseMetadata.java", From 90a1e4cd9c7a44655e8dfcce3fcdd56848cc4c90 Mon Sep 17 00:00:00 2001 From: rahul2393 Date: Tue, 23 Aug 2022 20:38:21 +0530 Subject: [PATCH 10/16] test: fix flaky tests for FGAC (#1981) * test: fix flaky tests for FGAC * fix lint * incorporate requested changes * refactoring afterClass --- .../it/ITDatabaseRolePermissionTest.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java index 842df028dd..410ed0d17e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseRolePermissionTest.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.ParallelIntegrationTest; @@ -35,9 +36,12 @@ import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.testing.RemoteSpannerHelper; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; -import org.junit.Before; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -49,14 +53,30 @@ @RunWith(JUnit4.class) public class ITDatabaseRolePermissionTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - private DatabaseAdminClient dbAdminClient; - private RemoteSpannerHelper testHelper; + private static DatabaseAdminClient dbAdminClient; + private static RemoteSpannerHelper testHelper; - @Before - public void setUp() { + private static List databasesToDrop; + + @BeforeClass + public static void setUp() { assumeFalse("Emulator does not support database roles", isUsingEmulator()); testHelper = env.getTestHelper(); dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); + databasesToDrop = new ArrayList<>(); + } + + @AfterClass + public static void cleanup() throws Exception { + if (databasesToDrop != null) { + for (DatabaseId id : databasesToDrop) { + try { + dbAdminClient.dropDatabase(id.getInstanceId().getInstance(), id.getDatabase()); + } catch (Exception e) { + System.err.println("Failed to drop database " + id + ", skipping...: " + e.getMessage()); + } + } + } } @Test @@ -84,7 +104,6 @@ public void grantAndRevokeDatabaseRolePermissions() throws Exception { Spanner spanner = options.getService(); DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); - DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); // Test SELECT permissions to role dbRoleParent on table T. // Query using dbRoleParent should return result. @@ -103,7 +122,7 @@ public void grantAndRevokeDatabaseRolePermissions() throws Exception { dbAdminClient .updateDatabaseDdl( instanceId, databaseId, Arrays.asList(revokeSelectOnTableFromParent), null) - .get(); + .get(5, TimeUnit.MINUTES); // Test SELECT permissions to role dbRoleParent on table T. // Query using dbRoleParent should return PermissionDeniedException. @@ -118,7 +137,8 @@ public void grantAndRevokeDatabaseRolePermissions() throws Exception { final String dropRoleParent = String.format("DROP ROLE %s", dbRoleParent); dbAdminClient .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) - .get(); + .get(5, TimeUnit.MINUTES); + databasesToDrop.add(createdDatabase.getId()); } @Test @@ -141,7 +161,6 @@ public void roleWithNoPermissions() throws Exception { Spanner spanner = options.getService(); DatabaseClient dbClient = spanner.getDatabaseClient(createdDatabase.getId()); - DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); // Test SELECT permissions to role dbRoleOrphan on table T. // Query using dbRoleOrphan should return PermissionDeniedException. @@ -156,6 +175,7 @@ public void roleWithNoPermissions() throws Exception { final String dropRoleParent = String.format("DROP ROLE %s", dbRoleOrphan); dbAdminClient .updateDatabaseDdl(instanceId, databaseId, Arrays.asList(dropTableT, dropRoleParent), null) - .get(); + .get(5, TimeUnit.MINUTES); + databasesToDrop.add(createdDatabase.getId()); } } From 6abb01652b23112a2eafb87a3819f32c3b36452e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 25 Aug 2022 23:20:12 +0200 Subject: [PATCH 11/16] chore(deps): update dependency com.google.cloud:google-cloud-spanner to v6.28.0 (#1971) 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:google-cloud-spanner](https://togithub.com/googleapis/java-spanner) | `6.27.0` -> `6.28.0` | [![age](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-spanner/6.28.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-spanner/6.28.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-spanner/6.28.0/compatibility-slim/6.27.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.cloud:google-cloud-spanner/6.28.0/confidence-slim/6.27.0)](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**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-spanner). --- README.md | 2 +- samples/install-without-bom/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5bb211d6b3..6699eff4ac 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ If you are using Maven without BOM, add this to your dependencies: com.google.cloud google-cloud-spanner - 6.27.0 + 6.28.0 ``` diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index a3e41ba9b4..8b400fde77 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 6.27.0 + 6.28.0 From 23e57ffc627d0f688fa656887d82f8f1f99f3675 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:11:57 +0530 Subject: [PATCH 12/16] feat: Adds auto-generated CL for googleapis for jsonb (#1983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Adds auto-generated CL for googleapis for jsonb PiperOrigin-RevId: 470167051 Source-Link: https://github.com/googleapis/googleapis/commit/343f52cd370556819da24df078308f3f709ff24b Source-Link: https://github.com/googleapis/googleapis-gen/commit/a416799a37269912fa0cfde279ce50b7c3670db1 Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiYTQxNjc5OWEzNzI2OTkxMmZhMGNmZGUyNzljZTUwYjdjMzY3MGRiMSJ9 * 🦉 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 --- .../google/spanner/v1/TypeAnnotationCode.java | 32 +++++++++++++++++++ .../java/com/google/spanner/v1/TypeProto.java | 13 ++++---- .../main/proto/google/spanner/v1/type.proto | 8 +++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeAnnotationCode.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeAnnotationCode.java index 944ab742a1..3c62b7ea1b 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeAnnotationCode.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeAnnotationCode.java @@ -57,6 +57,21 @@ public enum TypeAnnotationCode implements com.google.protobuf.ProtocolMessageEnu * PG_NUMERIC = 2; */ PG_NUMERIC(2), + /** + * + * + *

+   * PostgreSQL compatible JSONB type. This annotation needs to be applied to
+   * [Type][google.spanner.v1.Type] instances having [JSON][google.spanner.v1.TypeCode.JSON]
+   * type code to specify that values of this type should be treated as
+   * PostgreSQL JSONB values. Currently this annotation is always needed for
+   * [JSON][google.spanner.v1.TypeCode.JSON] when a client interacts with PostgreSQL-enabled
+   * Spanner databases.
+   * 
+ * + * PG_JSONB = 3; + */ + PG_JSONB(3), UNRECOGNIZED(-1), ; @@ -85,6 +100,21 @@ public enum TypeAnnotationCode implements com.google.protobuf.ProtocolMessageEnu * PG_NUMERIC = 2; */ public static final int PG_NUMERIC_VALUE = 2; + /** + * + * + *
+   * PostgreSQL compatible JSONB type. This annotation needs to be applied to
+   * [Type][google.spanner.v1.Type] instances having [JSON][google.spanner.v1.TypeCode.JSON]
+   * type code to specify that values of this type should be treated as
+   * PostgreSQL JSONB values. Currently this annotation is always needed for
+   * [JSON][google.spanner.v1.TypeCode.JSON] when a client interacts with PostgreSQL-enabled
+   * Spanner databases.
+   * 
+ * + * PG_JSONB = 3; + */ + public static final int PG_JSONB_VALUE = 3; public final int getNumber() { if (this == UNRECOGNIZED) { @@ -114,6 +144,8 @@ public static TypeAnnotationCode forNumber(int value) { return TYPE_ANNOTATION_CODE_UNSPECIFIED; case 2: return PG_NUMERIC; + case 3: + return PG_JSONB; default: return null; } diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeProto.java index 29e9151b32..ebca4e75d3 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/TypeProto.java @@ -63,13 +63,14 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\004BOOL\020\001\022\t\n\005INT64\020\002\022\013\n\007FLOAT64\020\003\022\r\n\tTIMES" + "TAMP\020\004\022\010\n\004DATE\020\005\022\n\n\006STRING\020\006\022\t\n\005BYTES\020\007\022" + "\t\n\005ARRAY\020\010\022\n\n\006STRUCT\020\t\022\013\n\007NUMERIC\020\n\022\010\n\004J" - + "SON\020\013*J\n\022TypeAnnotationCode\022$\n TYPE_ANNO" + + "SON\020\013*X\n\022TypeAnnotationCode\022$\n TYPE_ANNO" + "TATION_CODE_UNSPECIFIED\020\000\022\016\n\nPG_NUMERIC\020" - + "\002B\257\001\n\025com.google.spanner.v1B\tTypeProtoP\001" - + "Z8google.golang.org/genproto/googleapis/" - + "spanner/v1;spanner\252\002\027Google.Cloud.Spanne" - + "r.V1\312\002\027Google\\Cloud\\Spanner\\V1\352\002\032Google:" - + ":Cloud::Spanner::V1b\006proto3" + + "\002\022\014\n\010PG_JSONB\020\003B\257\001\n\025com.google.spanner.v" + + "1B\tTypeProtoP\001Z8google.golang.org/genpro" + + "to/googleapis/spanner/v1;spanner\252\002\027Googl" + + "e.Cloud.Spanner.V1\312\002\027Google\\Cloud\\Spanne" + + "r\\V1\352\002\032Google::Cloud::Spanner::V1b\006proto" + + "3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-v1/src/main/proto/google/spanner/v1/type.proto b/proto-google-cloud-spanner-v1/src/main/proto/google/spanner/v1/type.proto index 126e4a0c2f..712ec187fc 100644 --- a/proto-google-cloud-spanner-v1/src/main/proto/google/spanner/v1/type.proto +++ b/proto-google-cloud-spanner-v1/src/main/proto/google/spanner/v1/type.proto @@ -163,4 +163,12 @@ enum TypeAnnotationCode { // [NUMERIC][google.spanner.v1.TypeCode.NUMERIC] when a client interacts with PostgreSQL-enabled // Spanner databases. PG_NUMERIC = 2; + + // PostgreSQL compatible JSONB type. This annotation needs to be applied to + // [Type][google.spanner.v1.Type] instances having [JSON][google.spanner.v1.TypeCode.JSON] + // type code to specify that values of this type should be treated as + // PostgreSQL JSONB values. Currently this annotation is always needed for + // [JSON][google.spanner.v1.TypeCode.JSON] when a client interacts with PostgreSQL-enabled + // Spanner databases. + PG_JSONB = 3; } From b7382e59a1b59048be62aadcd4d0cd080859e3ae Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:12:23 +0530 Subject: [PATCH 13/16] chore(main): release 6.28.1-SNAPSHOT (#1970) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- google-cloud-spanner-bom/pom.xml | 18 +++++++++--------- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++++++------- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 4c801f0c2c..ac361ce579 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.28.0 + 6.28.1-SNAPSHOT pom com.google.cloud @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.28.0 + 6.28.1-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.28.0 + 6.28.1-SNAPSHOT diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 91e0963963..3b20ecc3f3 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.28.0 + 6.28.1-SNAPSHOT jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.28.0 + 6.28.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 b7df4e06bb..37c5d8e912 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.28.0 + 6.28.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.28.0 + 6.28.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 41fb8da702..c64b18b59c 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.28.0 + 6.28.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.28.0 + 6.28.1-SNAPSHOT diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 4ea5ca664c..490637415c 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.28.0 + 6.28.1-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.28.0 + 6.28.1-SNAPSHOT diff --git a/pom.xml b/pom.xml index 3e9582ad88..b7af24900f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.28.0 + 6.28.1-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -62,37 +62,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.28.0 + 6.28.1-SNAPSHOT com.google.cloud google-cloud-spanner - 6.28.0 + 6.28.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 d7d42ff63d..c33922f869 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.28.0 + 6.28.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.28.0 + 6.28.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 707a5dd88f..275437a032 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.28.0 + 6.28.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.28.0 + 6.28.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 1de63dfb5e..92269cd769 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.28.0 + 6.28.1-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.28.0 + 6.28.1-SNAPSHOT diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 889b9e9a43..8d2869a48f 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 6.28.0 + 6.28.1-SNAPSHOT diff --git a/versions.txt b/versions.txt index df448643a2..9d0ea61456 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.0 -proto-google-cloud-spanner-v1:6.28.0:6.28.0 -proto-google-cloud-spanner-admin-database-v1:6.28.0:6.28.0 -grpc-google-cloud-spanner-v1:6.28.0:6.28.0 -grpc-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.0 -grpc-google-cloud-spanner-admin-database-v1:6.28.0:6.28.0 -google-cloud-spanner:6.28.0:6.28.0 +proto-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.1-SNAPSHOT +proto-google-cloud-spanner-v1:6.28.0:6.28.1-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:6.28.0:6.28.1-SNAPSHOT +grpc-google-cloud-spanner-v1:6.28.0:6.28.1-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.1-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:6.28.0:6.28.1-SNAPSHOT +google-cloud-spanner:6.28.0:6.28.1-SNAPSHOT From e93ab4cc4031ee2300f4e73d7d3a8e41de1bc7ae Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 29 Aug 2022 11:42:56 +0200 Subject: [PATCH 14/16] deps: update dependency com.google.cloud:google-cloud-monitoring to v3.4.1 (#1968) --- samples/install-without-bom/pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 8b400fde77..f81ac27bb7 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.31.1 2.3.1 - 3.3.6 + 3.4.1 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 8d2869a48f..324dd107ad 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -23,7 +23,7 @@ UTF-8 0.31.1 2.3.1 - 3.3.6 + 3.4.1 From d2b426fda2cd1463dfa0719dd80f8346cbef51c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 29 Aug 2022 19:38:06 +0200 Subject: [PATCH 15/16] feat: add support for PG JSONB data type (#1964) * feat: add support for PG JSONB data type * chore: address review comments * test: temporarily disable integration test for JSONB --- .../clirr-ignored-differences.xml | 21 + .../cloud/spanner/AbstractResultSet.java | 49 +- .../cloud/spanner/AbstractStructReader.java | 34 ++ .../cloud/spanner/ForwardingStructReader.java | 24 + .../com/google/cloud/spanner/ResultSets.java | 20 + .../java/com/google/cloud/spanner/Struct.java | 14 + .../google/cloud/spanner/StructReader.java | 32 +- .../java/com/google/cloud/spanner/Type.java | 12 + .../java/com/google/cloud/spanner/Value.java | 108 ++++- .../com/google/cloud/spanner/ValueBinder.java | 5 + .../spanner/connection/ChecksumResultSet.java | 10 + .../connection/DirectExecuteResultSet.java | 24 + .../ReplaceableForwardingResultSet.java | 24 + .../AbstractStructReaderTypesTest.java | 10 + .../cloud/spanner/GrpcResultSetTest.java | 38 ++ .../google/cloud/spanner/MutationTest.java | 20 + .../google/cloud/spanner/ResultSetsTest.java | 28 +- .../com/google/cloud/spanner/TypeTest.java | 20 + .../google/cloud/spanner/ValueBinderTest.java | 14 + .../com/google/cloud/spanner/ValueTest.java | 85 ++++ .../connection/ChecksumResultSetTest.java | 338 ++++++++++++++ .../DirectExecuteResultSetTest.java | 9 + .../connection/RandomResultSetGenerator.java | 16 +- .../cloud/spanner/it/ITPgJsonbTest.java | 440 ++++++++++++++++++ 24 files changed, 1371 insertions(+), 24 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index fb952176bd..8d597d29b3 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -131,4 +131,25 @@ com/google/cloud/spanner/spi/v1/GapicSpannerRpc com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.lang.String getPgJsonb(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.lang.String getPgJsonb(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getPgJsonbList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getPgJsonbList(java.lang.String) + 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 bc4f224b93..6ccb28900f 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 @@ -67,6 +67,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** Implementation of {@link ResultSet}. */ @@ -384,6 +385,9 @@ private Object writeReplace() { case JSON: builder.set(fieldName).to(Value.json((String) value)); break; + case PG_JSONB: + builder.set(fieldName).to(Value.pgJsonb((String) value)); + break; case BYTES: builder.set(fieldName).to((ByteArray) value); break; @@ -417,6 +421,9 @@ private Object writeReplace() { case JSON: builder.set(fieldName).toJsonArray((Iterable) value); break; + case PG_JSONB: + builder.set(fieldName).toPgJsonbArray((Iterable) value); + break; case BYTES: builder.set(fieldName).toBytesArray((Iterable) value); break; @@ -491,10 +498,9 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot checkType(fieldType, proto, KindCase.STRING_VALUE); return new BigDecimal(proto.getStringValue()); case PG_NUMERIC: - checkType(fieldType, proto, KindCase.STRING_VALUE); - return proto.getStringValue(); case STRING: case JSON: + case PG_JSONB: checkType(fieldType, proto, KindCase.STRING_VALUE); return proto.getStringValue(); case BYTES: @@ -558,14 +564,14 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { return list; } case PG_NUMERIC: - return Lists.transform( - listValue.getValuesList(), - input -> input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue()); case STRING: case JSON: - return Lists.transform( - listValue.getValuesList(), - input -> input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue()); + case PG_JSONB: + return listValue.getValuesList().stream() + .map( + input -> + input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue()) + .collect(Collectors.toList()); case BYTES: { // Materialize list: element conversion is expensive and should happen only once. @@ -679,6 +685,11 @@ 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 (ByteArray) rowData.get(columnIndex); @@ -715,6 +726,8 @@ protected Value getValueInternal(int columnIndex) { 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.bytes(isNull ? null : getBytesInternal(columnIndex)); case TIMESTAMP: @@ -740,6 +753,8 @@ protected Value getValueInternal(int columnIndex) { 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 TIMESTAMP: @@ -816,11 +831,17 @@ protected List getStringListInternal(int columnIndex) { } @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. + @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 getPgJsonbListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getBytesListInternal(int columnIndex) { @@ -1352,6 +1373,11 @@ protected String getJsonInternal(int columnIndex) { return currRow().getJsonInternal(columnIndex); } + @Override + protected String getPgJsonbInternal(int columnIndex) { + return currRow().getPgJsonbInternal(columnIndex); + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return currRow().getBytesInternal(columnIndex); @@ -1417,6 +1443,11 @@ protected List getJsonListInternal(int columnIndex) { return currRow().getJsonListInternal(columnIndex); } + @Override + protected List getPgJsonbListInternal(int columnIndex) { + return currRow().getJsonListInternal(columnIndex); + } + @Override protected List getBytesListInternal(int columnIndex) { return currRow().getBytesListInternal(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index d9038466a4..1e89763624 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -48,6 +48,10 @@ protected String getJsonInternal(int columnIndex) { throw new UnsupportedOperationException("Not implemented"); } + protected String getPgJsonbInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract ByteArray getBytesInternal(int columnIndex); protected abstract Timestamp getTimestampInternal(int columnIndex); @@ -78,6 +82,10 @@ protected List getJsonListInternal(int columnIndex) { throw new UnsupportedOperationException("Not implemented"); } + protected List getPgJsonbListInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract List getBytesListInternal(int columnIndex); protected abstract List getTimestampListInternal(int columnIndex); @@ -189,6 +197,19 @@ public String getJson(String columnName) { return getJsonInternal(columnIndex); } + @Override + public String getPgJsonb(int columnIndex) { + checkNonNullOfType(columnIndex, Type.pgJsonb(), columnIndex); + return getPgJsonbInternal(columnIndex); + } + + @Override + public String getPgJsonb(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.pgJsonb(), columnName); + return getPgJsonbInternal(columnIndex); + } + @Override public ByteArray getBytes(int columnIndex) { checkNonNullOfType(columnIndex, Type.bytes(), columnIndex); @@ -365,6 +386,19 @@ public List getJsonList(String columnName) { return getJsonListInternal(columnIndex); } + @Override + public List getPgJsonbList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.pgJsonb()), columnIndex); + return getPgJsonbListInternal(columnIndex); + } + + @Override + public List getPgJsonbList(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.pgJsonb()), columnName); + return getPgJsonbListInternal(columnIndex); + } + @Override public List getBytesList(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.bytes()), columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index e225bdcc1b..2a85006fa9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -168,6 +168,18 @@ public String getJson(String columnName) { return delegate.get().getJson(columnName); } + @Override + public String getPgJsonb(int columnIndex) { + checkValidState(); + return delegate.get().getPgJsonb(columnIndex); + } + + @Override + public String getPgJsonb(String columnName) { + checkValidState(); + return delegate.get().getPgJsonb(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { checkValidState(); @@ -310,6 +322,18 @@ public List getJsonList(String columnName) { return delegate.get().getJsonList(columnName); } + @Override + public List getPgJsonbList(int columnIndex) { + checkValidState(); + return delegate.get().getPgJsonbList(columnIndex); + } + + @Override + public List getPgJsonbList(String columnName) { + checkValidState(); + return delegate.get().getPgJsonbList(columnName); + } + @Override public List getBytesList(int columnIndex) { checkValidState(); 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 af57b5b848..6eacd3208e 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 @@ -253,6 +253,16 @@ public String getJson(String columnName) { return getCurrentRowAsStruct().getJson(columnName); } + @Override + public String getPgJsonb(int columnIndex) { + return getCurrentRowAsStruct().getPgJsonb(columnIndex); + } + + @Override + public String getPgJsonb(String columnName) { + return getCurrentRowAsStruct().getPgJsonb(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { return getCurrentRowAsStruct().getBytes(columnIndex); @@ -383,6 +393,16 @@ public List getJsonList(String columnName) { return getCurrentRowAsStruct().getJsonList(columnName); } + @Override + public List getPgJsonbList(int columnIndex) { + return getCurrentRowAsStruct().getPgJsonbList(columnIndex); + } + + @Override + public List getPgJsonbList(String columnName) { + return getCurrentRowAsStruct().getPgJsonbList(columnName); + } + @Override public List getBytesList(int columnIndex) { return getCurrentRowAsStruct().getBytesList(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index c986767d3a..48c989d145 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -197,6 +197,11 @@ protected String getJsonInternal(int columnIndex) { return values.get(columnIndex).getJson(); } + @Override + protected String getPgJsonbInternal(int columnIndex) { + return values.get(columnIndex).getPgJsonb(); + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return values.get(columnIndex).getBytes(); @@ -267,6 +272,11 @@ protected List getJsonListInternal(int columnIndex) { return values.get(columnIndex).getJsonArray(); } + @Override + protected List getPgJsonbListInternal(int columnIndex) { + return values.get(columnIndex).getPgJsonbArray(); + } + @Override protected List getBytesListInternal(int columnIndex) { return values.get(columnIndex).getBytesArray(); @@ -355,6 +365,8 @@ private Object getAsObject(int columnIndex) { return getStringInternal(columnIndex); case JSON: return getJsonInternal(columnIndex); + case PG_JSONB: + return getPgJsonbInternal(columnIndex); case BYTES: return getBytesInternal(columnIndex); case TIMESTAMP: @@ -379,6 +391,8 @@ private Object getAsObject(int columnIndex) { return getStringListInternal(columnIndex); case JSON: return getJsonListInternal(columnIndex); + case PG_JSONB: + return getPgJsonbListInternal(columnIndex); case BYTES: return getBytesListInternal(columnIndex); case TIMESTAMP: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java index 3779e8067d..a96c95cb95 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java @@ -114,16 +114,26 @@ public interface StructReader { /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ String getString(String columnName); - /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ + /** Returns the value of a non-{@code NULL} column with type {@link Type#json()}. */ default String getJson(int columnIndex) { throw new UnsupportedOperationException("method should be overwritten"); } - /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ + /** Returns the value of a non-{@code NULL} column with type {@link Type#json()}. */ default String getJson(String columnName) { throw new UnsupportedOperationException("method should be overwritten"); } + /** Returns the value of a non-{@code NULL} column with type {@link Type#pgJsonb()}. */ + default String getPgJsonb(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** Returns the value of a non-{@code NULL} column with type {@link Type#pgJsonb()}. */ + default String getPgJsonb(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + } + /** Returns the value of a non-{@code NULL} column with type {@link Type#bytes()}. */ ByteArray getBytes(int columnIndex); @@ -238,16 +248,30 @@ default Value getValue(String columnName) { /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ List getStringList(String columnName); - /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.json())}. */ default List getJsonList(int columnIndex) { throw new UnsupportedOperationException("method should be overwritten"); }; - /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.json())}. */ default List getJsonList(String columnName) { throw new UnsupportedOperationException("method should be overwritten"); }; + /** + * Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.pgJsonb())}. + */ + default List getPgJsonbList(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + }; + + /** + * Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.pgJsonb())}. + */ + default List getPgJsonbList(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + }; + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.bytes())}. */ List getBytesList(int columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index 15305e0cda..7ba6b9a41e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -52,6 +52,7 @@ public final class Type implements Serializable { private static final Type TYPE_PG_NUMERIC = new Type(Code.PG_NUMERIC, null, null); private static final Type TYPE_STRING = new Type(Code.STRING, null, null); private static final Type TYPE_JSON = new Type(Code.JSON, null, null); + private static final Type TYPE_PG_JSONB = new Type(Code.PG_JSONB, null, null); private static final Type TYPE_BYTES = new Type(Code.BYTES, null, null); private static final Type TYPE_TIMESTAMP = new Type(Code.TIMESTAMP, null, null); private static final Type TYPE_DATE = new Type(Code.DATE, null, null); @@ -62,6 +63,7 @@ public final class Type implements Serializable { private static final Type TYPE_ARRAY_PG_NUMERIC = new Type(Code.ARRAY, TYPE_PG_NUMERIC, null); private static final Type TYPE_ARRAY_STRING = new Type(Code.ARRAY, TYPE_STRING, null); private static final Type TYPE_ARRAY_JSON = new Type(Code.ARRAY, TYPE_JSON, null); + private static final Type TYPE_ARRAY_PG_JSONB = new Type(Code.ARRAY, TYPE_PG_JSONB, null); private static final Type TYPE_ARRAY_BYTES = new Type(Code.ARRAY, TYPE_BYTES, null); private static final Type TYPE_ARRAY_TIMESTAMP = new Type(Code.ARRAY, TYPE_TIMESTAMP, null); private static final Type TYPE_ARRAY_DATE = new Type(Code.ARRAY, TYPE_DATE, null); @@ -115,6 +117,11 @@ public static Type json() { return TYPE_JSON; } + /** Returns the descriptor for the {@code JSONB} type. */ + public static Type pgJsonb() { + return TYPE_PG_JSONB; + } + /** Returns the descriptor for the {@code BYTES} type: a variable-length byte string. */ public static Type bytes() { return TYPE_BYTES; @@ -154,6 +161,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_STRING; case JSON: return TYPE_ARRAY_JSON; + case PG_JSONB: + return TYPE_ARRAY_PG_JSONB; case BYTES: return TYPE_ARRAY_BYTES; case TIMESTAMP: @@ -209,6 +218,7 @@ public enum Code { FLOAT64(TypeCode.FLOAT64), STRING(TypeCode.STRING), JSON(TypeCode.JSON), + PG_JSONB(TypeCode.JSON, TypeAnnotationCode.PG_JSONB), BYTES(TypeCode.BYTES), TIMESTAMP(TypeCode.TIMESTAMP), DATE(TypeCode.DATE), @@ -446,6 +456,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return string(); case JSON: return json(); + case PG_JSONB: + return pgJsonb(); case BYTES: return bytes(); case TIMESTAMP: 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 e3c53de937..3ec7b67f65 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 @@ -200,7 +200,7 @@ public static Value string(@Nullable String v) { } /** - * Returns a {@code STRING} value. + * Returns a {@code JSON} value. * * @param v the value, which may be null */ @@ -208,6 +208,15 @@ public static Value json(@Nullable String v) { return new JsonImpl(v == null, v); } + /** + * Returns a {@code PG JSONB} value. + * + * @param v the value, which may be null + */ + public static Value pgJsonb(@Nullable String v) { + return new PgJsonbImpl(v == null, v); + } + /** * Returns a {@code BYTES} value. * @@ -393,7 +402,7 @@ public static Value stringArray(@Nullable Iterable v) { } /** - * Returns an {@code ARRAY} value. + * Returns an {@code ARRAY} value. * * @param v the source of element values. This may be {@code null} to produce a value for which * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. @@ -402,6 +411,16 @@ public static Value jsonArray(@Nullable Iterable v) { return new JsonArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. + */ + public static Value pgJsonbArray(@Nullable Iterable v) { + return new PgJsonbArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY} value. * @@ -513,6 +532,15 @@ public String getJson() { throw new UnsupportedOperationException("Not implemented"); } + /** + * Returns the value of a {@code JSONB}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public String getPgJsonb() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns the value of a {@code BYTES}-typed instance. * @@ -595,6 +623,16 @@ public List getJsonArray() { throw new UnsupportedOperationException("Not implemented"); } + /** + * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself + * will never be {@code null}, elements of that list may be null. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public List getPgJsonbArray() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself * will never be {@code null}, elements of that list may be null. @@ -808,6 +846,11 @@ public String getJson() { throw defaultGetter(Type.json()); } + @Override + public String getPgJsonb() { + throw defaultGetter(Type.pgJsonb()); + } + @Override public ByteArray getBytes() { throw defaultGetter(Type.bytes()); @@ -862,6 +905,11 @@ public List getJsonArray() { throw defaultGetter(Type.array(Type.json())); } + @Override + public List getPgJsonbArray() { + throw defaultGetter(Type.array(Type.pgJsonb())); + } + @Override public List getBytesArray() { throw defaultGetter(Type.array(Type.bytes())); @@ -1229,6 +1277,34 @@ void valueToString(StringBuilder b) { } } + private static class PgJsonbImpl extends AbstractObjectValue { + + private PgJsonbImpl(boolean isNull, @Nullable String value) { + super(isNull, Type.pgJsonb(), value); + } + + @Override + public String getPgJsonb() { + checkType(Type.pgJsonb()); + checkNotNull(); + return value; + } + + @Override + public String getString() { + return getPgJsonb(); + } + + @Override + void valueToString(StringBuilder b) { + if (value.length() > MAX_DEBUG_STRING_LENGTH) { + b.append(value, 0, MAX_DEBUG_STRING_LENGTH - ELLIPSIS.length()).append(ELLIPSIS); + } else { + b.append(value); + } + } + } + private static class BytesImpl extends AbstractObjectValue { private BytesImpl(boolean isNull, ByteArray value) { @@ -1666,6 +1742,30 @@ void appendElement(StringBuilder b, String element) { } } + private static class PgJsonbArrayImpl extends AbstractArrayValue { + + private PgJsonbArrayImpl(boolean isNull, @Nullable List values) { + super(isNull, Type.pgJsonb(), values); + } + + @Override + public List getPgJsonbArray() { + checkType(getType()); + checkNotNull(); + return value; + } + + @Override + public List getStringArray() { + return this.getPgJsonbArray(); + } + + @Override + void appendElement(StringBuilder b, String element) { + b.append(element); + } + } + private static class BytesArrayImpl extends AbstractArrayValue { private BytesArrayImpl(boolean isNull, @Nullable List values) { super(isNull, Type.bytes(), values); @@ -1857,6 +1957,8 @@ private Value getValue(int fieldIndex) { return Value.string(value.getString(fieldIndex)); case JSON: return Value.json(value.getJson(fieldIndex)); + case PG_JSONB: + return Value.pgJsonb(value.getPgJsonb(fieldIndex)); case BYTES: return Value.bytes(value.getBytes(fieldIndex)); case FLOAT64: @@ -1883,6 +1985,8 @@ private Value getValue(int fieldIndex) { return Value.stringArray(value.getStringList(fieldIndex)); case JSON: return Value.jsonArray(value.getJsonList(fieldIndex)); + case PG_JSONB: + return Value.pgJsonbArray(value.getPgJsonbList(fieldIndex)); case BYTES: return Value.bytesArray(value.getBytesList(fieldIndex)); case FLOAT64: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index cdca5d84a2..ec9e5a43d8 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -188,6 +188,11 @@ public R toJsonArray(@Nullable Iterable values) { return handle(Value.jsonArray(values)); } + /** Binds to {@code Value.jsonbArray(values)} */ + public R toPgJsonbArray(@Nullable Iterable values) { + return handle(Value.pgJsonbArray(values)); + } + /** Binds to {@code Value.bytesArray(values)} */ public R toBytesArray(@Nullable Iterable values) { return handle(Value.bytesArray(values)); 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 2c01396083..bb2f2fb817 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 @@ -248,6 +248,9 @@ public void funnel(Struct row, PrimitiveSink into) { 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; @@ -318,6 +321,12 @@ private void funnelArray( 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)) { @@ -370,6 +379,7 @@ private void funnelValue(Code type, T value, PrimitiveSink into) { case PG_NUMERIC: case STRING: case JSON: + case PG_JSONB: String stringValue = (String) value; into.putInt(stringValue.length()); into.putUnencodedChars(stringValue); 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 21c3dae7ec..8fb0bbe440 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 @@ -203,6 +203,18 @@ public String getJson(String columnName) { return delegate.getJson(columnName); } + @Override + public String getPgJsonb(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getPgJsonb(columnIndex); + } + + @Override + public String getPgJsonb(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getPgJsonb(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -359,6 +371,18 @@ public List getJsonList(String columnName) { return delegate.getJsonList(columnName); } + @Override + public List getPgJsonbList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getPgJsonbList(columnIndex); + } + + @Override + public List getPgJsonbList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getPgJsonbList(columnName); + } + @Override public List getBytesList(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); 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 6bf8f046c5..cc9759a487 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 @@ -208,6 +208,18 @@ public String getJson(String columnName) { return delegate.getJson(columnName); } + @Override + public String getPgJsonb(int columnIndex) { + checkClosed(); + return delegate.getPgJsonb(columnIndex); + } + + @Override + public String getPgJsonb(String columnName) { + checkClosed(); + return delegate.getPgJsonb(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { checkClosed(); @@ -364,6 +376,18 @@ public List getJsonList(String columnName) { return delegate.getJsonList(columnName); } + @Override + public List getPgJsonbList(int columnIndex) { + checkClosed(); + return delegate.getPgJsonbList(columnIndex); + } + + @Override + public List getPgJsonbList(String columnName) { + checkClosed(); + return delegate.getPgJsonbList(columnName); + } + @Override public List getBytesList(int columnIndex) { checkClosed(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index 10167ddc9d..1b6280a636 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -75,6 +75,11 @@ protected String getJsonInternal(int columnIndex) { return null; } + @Override + protected String getPgJsonbInternal(int columnIndex) { + return null; + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return null; @@ -140,6 +145,11 @@ protected List getJsonListInternal(int columnIndex) { return null; } + @Override + protected List getPgJsonbListInternal(int columnIndex) { + return null; + } + @Override protected List getBytesListInternal(int columnIndex) { return null; 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 a11ac78b54..ff4e92a521 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 @@ -716,6 +716,25 @@ public void getJson() { assertEquals("[]", resultSet.getJson(0)); } + @Test + public void getPgJsonb() { + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.pgJsonb())))) + .addValues(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#f00\"}").toProto()) + .addValues(Value.pgJsonb("{}").toProto()) + .addValues(Value.pgJsonb("[]").toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals("{\"color\":\"red\",\"value\":\"#f00\"}", resultSet.getPgJsonb(0)); + assertTrue(resultSet.next()); + assertEquals("{}", resultSet.getPgJsonb(0)); + assertTrue(resultSet.next()); + assertEquals("[]", resultSet.getPgJsonb(0)); + } + @Test public void getBooleanArray() { boolean[] boolArray = {true, true, false}; @@ -838,4 +857,23 @@ public void getJsonList() { assertTrue(resultSet.next()); assertEquals(jsonList, resultSet.getJsonList(0)); } + + @Test + public void getPgJsonbList() { + List jsonList = new ArrayList<>(); + jsonList.add("{\"color\":\"red\",\"value\":\"#f00\"}"); + jsonList.add("{\"special\":\"%😃∮πρότερονแผ่นดินฮั่นเสื่อมሰማይᚻᛖ\"}"); + jsonList.add("[]"); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.pgJsonb()))))) + .addValues(Value.pgJsonbArray(jsonList).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals(jsonList, resultSet.getPgJsonbList(0)); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java index eb96334b50..fe2b7aec94 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java @@ -541,6 +541,14 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .to(Value.numeric(BigDecimal.TEN)) .set("pgNumericValue") .to(Value.pgNumeric("4.2")) + .set("json") + .to(Value.json("{\"key\": \"value\"}}")) + .set("jsonNull") + .to(Value.json(null)) + .set("pgJsonb") + .to(Value.pgJsonb("{\"key\": \"value\"}}")) + .set("pgJsonbNull") + .to(Value.pgJsonb(null)) .set("timestamp") .to(Timestamp.MAX_VALUE) .set("timestampNull") @@ -589,6 +597,18 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .toPgNumericArray(null) .set("pgNumericArrValue") .to(Value.pgNumericArray(ImmutableList.of("10.20", "20.30"))) + .set("jsonArr") + .toJsonArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}")) + .set("jsonArrNull") + .toJsonArray(null) + .set("jsonArrValue") + .to(Value.jsonArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}"))) + .set("pgJsonbArr") + .toPgJsonbArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}")) + .set("pgJsonbArrNull") + .toPgJsonbArray(null) + .set("pgJsonbArrValue") + .to(Value.pgJsonbArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}"))) .set("timestampArr") .toTimestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE)) .set("timestampArrNull") diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 85cdc0f687..87be602808 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; @@ -89,6 +90,7 @@ public void resultSetIteration() { Type.StructField.of("bigDecimalVal", Type.numeric()), Type.StructField.of("stringVal", Type.string()), Type.StructField.of("jsonVal", Type.json()), + Type.StructField.of("pgJsonbVal", Type.pgJsonb()), Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), @@ -100,7 +102,8 @@ public void resultSetIteration() { Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), Type.StructField.of("stringArray", Type.array(Type.string())), - Type.StructField.of("jsonArray", Type.array(Type.json()))); + Type.StructField.of("jsonArray", Type.array(Type.json())), + Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb()))); Struct struct1 = Struct.newBuilder() .set("f1") @@ -117,6 +120,8 @@ public void resultSetIteration() { .to(stringVal) .set("jsonVal") .to(Value.json(jsonVal)) + .set("pgJsonbVal") + .to(Value.pgJsonb(jsonVal)) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom(byteVal))) .set("timestamp") @@ -141,6 +146,8 @@ public void resultSetIteration() { .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") .to(Value.jsonArray(Arrays.asList(jsonArray))) + .set("pgJsonbArray") + .to(Value.pgJsonbArray(Arrays.asList(jsonArray))) .build(); Struct struct2 = Struct.newBuilder() @@ -158,6 +165,8 @@ public void resultSetIteration() { .to(stringVal) .set("jsonVal") .to(Value.json(jsonVal)) + .set("pgJsonbVal") + .to(Value.pgJsonb(jsonVal)) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom(byteVal))) .set("timestamp") @@ -182,10 +191,12 @@ public void resultSetIteration() { .to(Value.stringArray(Arrays.asList(stringArray))) .set("jsonArray") .to(Value.jsonArray(Arrays.asList(jsonArray))) + .set("pgJsonbArray") + .to(Value.pgJsonbArray(Arrays.asList(jsonArray))) .build(); ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2)); - IllegalStateException e = assertThrows(IllegalStateException.class, () -> rs.getType()); + IllegalStateException e = assertThrows(IllegalStateException.class, rs::getType); assertThat(e.getMessage()).contains("Must be preceded by a next() call"); int columnIndex = 0; @@ -227,6 +238,12 @@ public void resultSetIteration() { assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.json(jsonVal)); assertThat(rs.getJson("jsonVal")).isEqualTo(jsonVal); assertThat(rs.getValue("jsonVal")).isEqualTo(Value.json(jsonVal)); + + assertEquals(jsonVal, rs.getPgJsonb(columnIndex)); + assertEquals(Value.pgJsonb(jsonVal), rs.getValue(columnIndex++)); + assertEquals(jsonVal, rs.getPgJsonb("pgJsonbVal")); + assertEquals(Value.pgJsonb(jsonVal), rs.getValue("pgJsonbVal")); + assertThat(rs.getBytes(columnIndex)).isEqualTo(ByteArray.copyFrom(byteVal)); assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.bytes(ByteArray.copyFrom(byteVal))); assertThat(rs.getBytes("byteVal")).isEqualTo(ByteArray.copyFrom(byteVal)); @@ -285,9 +302,12 @@ public void resultSetIteration() { assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); assertThat(rs.getStringList("stringArray")).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.getValue("stringArray")).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); - assertThat(rs.getJsonList(columnIndex)).isEqualTo(Arrays.asList(jsonArray)); + assertThat(rs.getJsonList(columnIndex++)).isEqualTo(Arrays.asList(jsonArray)); assertThat(rs.getJsonList("jsonArray")).isEqualTo(Arrays.asList(jsonArray)); + assertEquals(Arrays.asList(jsonArray), rs.getPgJsonbList(columnIndex)); + assertEquals(Arrays.asList(jsonArray), rs.getPgJsonbList("pgJsonbArray")); + assertThat(rs.next()).isTrue(); assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct2); assertThat(rs.getString(0)).isEqualTo("y"); @@ -296,7 +316,7 @@ public void resultSetIteration() { assertThat(rs.next()).isFalse(); UnsupportedOperationException unsupported = - assertThrows(UnsupportedOperationException.class, () -> rs.getStats()); + assertThrows(UnsupportedOperationException.class, rs::getStats); assertThat(unsupported.getMessage()) .contains("ResultSetStats are available only for results returned from analyzeQuery"); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index 7dfe9f3a98..3ed6fc6c57 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -152,6 +152,16 @@ Type newType() { }.test(); } + @Test + public void pgJsonb() { + new ScalarTypeTester(Code.PG_JSONB, TypeCode.JSON, TypeAnnotationCode.PG_JSONB) { + @Override + Type newType() { + return Type.pgJsonb(); + } + }.test(); + } + @Test public void bytes() { new ScalarTypeTester(Type.Code.BYTES, TypeCode.BYTES) { @@ -306,6 +316,16 @@ Type newElementType() { }.test(); } + @Test + public void pgJsonbArray() { + new ArrayTypeTester(Code.PG_JSONB, TypeCode.JSON, TypeAnnotationCode.PG_JSONB, true) { + @Override + Type newElementType() { + return Type.pgJsonb(); + } + }.test(); + } + @Test public void bytesArray() { new ArrayTypeTester(Type.Code.BYTES, TypeCode.BYTES, true) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index ea26f09c2e..91263457ba 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultJson; +import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultPgJsonb; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -37,6 +38,7 @@ @RunWith(JUnit4.class) public class ValueBinderTest { private static final String JSON_METHOD_NAME = "json"; + private static final String PG_JSONB_METHOD_NAME = "pgJsonb"; private static final String PG_NUMERIC_METHOD_NAME = "pgNumeric"; public static final String DEFAULT_PG_NUMERIC = "1.23"; @@ -125,6 +127,9 @@ public void reflection() // ValueBinder.to(Value) binderMethod = ValueBinder.class.getMethod("to", Value.class); assertThat(binderMethod.invoke(binder, Value.json(null))).isEqualTo(lastReturnValue); + } else if (method.getName().equalsIgnoreCase(PG_JSONB_METHOD_NAME)) { + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.pgJsonb(null))).isEqualTo(lastReturnValue); } else if (method.getName().equalsIgnoreCase(PG_NUMERIC_METHOD_NAME)) { binderMethod = ValueBinder.class.getMethod("to", Value.class); assertThat(binderMethod.invoke(binder, Value.pgNumeric(null))) @@ -145,6 +150,11 @@ public void reflection() binderMethod = ValueBinder.class.getMethod("to", Value.class); assertThat(binderMethod.invoke(binder, Value.json(defaultJson()))) .isEqualTo(lastReturnValue); + } else if (method.getName().equalsIgnoreCase(PG_JSONB_METHOD_NAME)) { + defaultObject = defaultPgJsonb(); + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.pgJsonb(defaultPgJsonb()))) + .isEqualTo(lastReturnValue); } else if (method.getName().equalsIgnoreCase(PG_NUMERIC_METHOD_NAME)) { defaultObject = DEFAULT_PG_NUMERIC; binderMethod = ValueBinder.class.getMethod("to", Value.class); @@ -232,6 +242,10 @@ public static String defaultJson() { return "{\"color\":\"red\",\"value\":\"#f00\"}"; } + public static String defaultPgJsonb() { + return "{\"color\":\"red\",\"value\":\"#f00\"}"; + } + public static ByteArray defaultByteArray() { return ByteArray.copyFrom(new byte[] {'x'}); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 54f6799e8c..a466fab1ab 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -491,6 +491,56 @@ public void jsonNested() { assertEquals(json, v.getJson()); } + @Test + public void testPgJsonb() { + String json = "{\"color\":\"red\",\"value\":\"#f00\"}"; + Value v = Value.pgJsonb(json); + assertEquals(Type.pgJsonb(), v.getType()); + assertFalse(v.isNull()); + assertEquals(json, v.getPgJsonb()); + assertEquals(json, v.getString()); + } + + @Test + public void testPgJsonbNull() { + Value v = Value.pgJsonb(null); + assertEquals(Type.pgJsonb(), v.getType()); + assertTrue(v.isNull()); + assertEquals(NULL_STRING, v.toString()); + assertThrowsWithMessage(v::getPgJsonb, "null value"); + assertThrowsWithMessage(v::getString, "null value"); + } + + @Test + public void testPgJsonbEmpty() { + String json = "{}"; + Value v = Value.pgJsonb(json); + assertEquals(json, v.getPgJsonb()); + } + + @Test + public void testPgJsonbWithEmptyArray() { + String json = "[]"; + Value v = Value.pgJsonb(json); + assertEquals(json, v.getPgJsonb()); + } + + @Test + public void testPgJsonbWithArray() { + String json = + "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; + Value v = Value.pgJsonb(json); + assertEquals(json, v.getPgJsonb()); + } + + @Test + public void testPgJsonbNested() { + String json = + "[{\"id\":\"0001\",\"type\":\"donut\",\"name\":\"Cake\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5007\",\"type\":\"Powdered Sugar\"},{\"id\":\"5006\",\"type\":\"Chocolate with Sprinkles\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0002\",\"type\":\"donut\",\"name\":\"Raised\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0003\",\"type\":\"donut\",\"name\":\"Old Fashioned\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]}]"; + Value v = Value.pgJsonb(json); + assertEquals(json, v.getPgJsonb()); + } + @Test public void bytes() { ByteArray bytes = newByteArray("abc"); @@ -894,6 +944,41 @@ public void jsonArrayTryGetFloat64Array() { assertThrowsWithMessage(value::getFloat64Array, "Expected: ARRAY actual: ARRAY"); } + @Test + public void testPgJsonbArray() { + String one = "{}"; + String two = null; + String three = "{\"color\":\"red\",\"value\":\"#f00\"}"; + Value v = Value.pgJsonbArray(Arrays.asList(one, two, three)); + assertFalse(v.isNull()); + assertArrayEquals(new String[] {one, two, three}, v.getPgJsonbArray().toArray()); + assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.toString()); + assertArrayEquals(new String[] {one, two, three}, v.getStringArray().toArray()); + } + + @Test + public void testPgJsonbArrayNull() { + Value v = Value.pgJsonbArray(null); + assertTrue(v.isNull()); + assertEquals(NULL_STRING, v.toString()); + assertThrowsWithMessage(v::getPgJsonbArray, "null value"); + assertThrowsWithMessage(v::getStringArray, "null value"); + } + + @Test + public void testPgJsonbArrayTryGetBytesArray() { + Value value = Value.pgJsonbArray(Collections.singletonList("{}")); + assertThrowsWithMessage( + value::getBytesArray, "Expected: ARRAY actual: ARRAY>"); + } + + @Test + public void testPgJsonbArrayTryGetFloat64Array() { + Value value = Value.pgJsonbArray(Collections.singletonList("{}")); + assertThrowsWithMessage( + value::getFloat64Array, "Expected: ARRAY actual: ARRAY>"); + } + @Test public void bytesArray() { ByteArray a = newByteArray("a"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java new file mode 100644 index 0000000000..1f3f59e96a --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java @@ -0,0 +1,338 @@ +/* + * 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.google.cloud.spanner.connection; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.Struct.Builder; +import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; +import com.google.common.collect.ImmutableList; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ChecksumResultSetTest { + private static final Struct DIFFERENT_NON_NULL_VALUES = + Struct.newBuilder() + .set("boolVal") + .to(false) + .set("longVal") + .to(2 * 2) + .set("doubleVal") + .to(Value.float64(3.14d * 2d)) + .set("bigDecimalVal") + .to(Value.numeric(BigDecimal.valueOf(123 * 2, 2))) + .set("pgNumericVal") + .to(Value.pgNumeric("2.46")) + .set("stringVal") + .to("testtest") + .set("jsonVal") + .to(Value.json("{\"color\":\"red\",\"value\":\"#ff0\"}")) + .set("pgJsonbVal") + .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#00f\"}")) + .set("byteVal") + .to(Value.bytes(ByteArray.copyFrom("bytes".getBytes(StandardCharsets.UTF_8)))) + .set("timestamp") + .to(Timestamp.parseTimestamp("2022-08-04T11:20:00.123456789Z")) + .set("date") + .to(Date.fromYearMonthDay(2022, 8, 3)) + .set("boolArray") + .to(Value.boolArray(Arrays.asList(Boolean.FALSE, null, Boolean.TRUE))) + .set("longArray") + .to(Value.int64Array(Arrays.asList(2L, null, 1L, 0L))) + .set("doubleArray") + .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d, 10.1d))) + .set("bigDecimalArray") + .to(Value.numericArray(Arrays.asList(BigDecimal.TEN, null, BigDecimal.ONE))) + .set("pgNumericArray") + .to(Value.pgNumericArray(Arrays.asList("10", null, "1", "NaN"))) + .set("byteArray") + .to( + Value.bytesArray( + Arrays.asList(ByteArray.copyFrom("test2"), null, ByteArray.copyFrom("test1")))) + .set("timestampArray") + .to( + Value.timestampArray( + Arrays.asList( + Timestamp.parseTimestamp("2000-01-01T00:00:00Z"), + null, + Timestamp.parseTimestamp("2022-07-04T10:24:00.123456789Z")))) + .set("dateArray") + .to( + Value.dateArray( + Arrays.asList(Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-03")))) + .set("stringArray") + .to(Value.stringArray(Arrays.asList("test2", null, "test1"))) + .set("jsonArray") + .to(Value.jsonArray(Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "[]"))) + .set("pgJsonbArray") + .to( + Value.pgJsonbArray( + Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "[]"))) + .build(); + + @Test + public void testRetry() { + Type type = + Type.struct( + Type.StructField.of("boolVal", Type.bool()), + Type.StructField.of("longVal", Type.int64()), + Type.StructField.of("doubleVal", Type.float64()), + Type.StructField.of("bigDecimalVal", Type.numeric()), + Type.StructField.of("pgNumericVal", Type.pgNumeric()), + Type.StructField.of("stringVal", Type.string()), + Type.StructField.of("jsonVal", Type.json()), + Type.StructField.of("pgJsonbVal", Type.pgJsonb()), + Type.StructField.of("byteVal", Type.bytes()), + Type.StructField.of("timestamp", Type.timestamp()), + Type.StructField.of("date", Type.date()), + Type.StructField.of("boolArray", Type.array(Type.bool())), + Type.StructField.of("longArray", Type.array(Type.int64())), + Type.StructField.of("doubleArray", Type.array(Type.float64())), + Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())), + Type.StructField.of("pgNumericArray", Type.array(Type.pgNumeric())), + Type.StructField.of("byteArray", Type.array(Type.bytes())), + Type.StructField.of("timestampArray", Type.array(Type.timestamp())), + Type.StructField.of("dateArray", Type.array(Type.date())), + Type.StructField.of("stringArray", Type.array(Type.string())), + Type.StructField.of("jsonArray", Type.array(Type.json())), + Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb()))); + Struct rowNonNullValues = + Struct.newBuilder() + .set("boolVal") + .to(true) + .set("longVal") + .to(2) + .set("doubleVal") + .to(Value.float64(3.14d)) + .set("bigDecimalVal") + .to(Value.numeric(BigDecimal.valueOf(123, 2))) + .set("pgNumericVal") + .to(Value.pgNumeric("1.23")) + .set("stringVal") + .to("test") + .set("jsonVal") + .to(Value.json("{\"color\":\"red\",\"value\":\"#f00\"}")) + .set("pgJsonbVal") + .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#f00\"}")) + .set("byteVal") + .to(Value.bytes(ByteArray.copyFrom("test".getBytes(StandardCharsets.UTF_8)))) + .set("timestamp") + .to(Timestamp.parseTimestamp("2022-08-04T10:19:00.123456789Z")) + .set("date") + .to(Date.fromYearMonthDay(2022, 8, 4)) + .set("boolArray") + .to(Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE))) + .set("longArray") + .to(Value.int64Array(Arrays.asList(1L, null, 2L))) + .set("doubleArray") + .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d))) + .set("bigDecimalArray") + .to(Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN))) + .set("pgNumericArray") + .to(Value.pgNumericArray(Arrays.asList("1", null, "10"))) + .set("byteArray") + .to( + Value.bytesArray( + Arrays.asList(ByteArray.copyFrom("test1"), null, ByteArray.copyFrom("test2")))) + .set("timestampArray") + .to( + Value.timestampArray( + Arrays.asList( + Timestamp.parseTimestamp("2000-01-01T00:00:00Z"), + null, + Timestamp.parseTimestamp("2022-08-04T10:24:00.123456789Z")))) + .set("dateArray") + .to( + Value.dateArray( + Arrays.asList( + Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-04")))) + .set("stringArray") + .to(Value.stringArray(Arrays.asList("test1", null, "test2"))) + .set("jsonArray") + .to( + Value.jsonArray( + Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "{}"))) + .set("pgJsonbArray") + .to( + Value.pgJsonbArray( + Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "{}"))) + .build(); + Struct rowNullValues = + Struct.newBuilder() + .set("boolVal") + .to((Boolean) null) + .set("longVal") + .to((Long) null) + .set("doubleVal") + .to((Double) null) + .set("bigDecimalVal") + .to((BigDecimal) null) + .set("pgNumericVal") + .to(Value.pgNumeric(null)) + .set("stringVal") + .to((String) null) + .set("jsonVal") + .to(Value.json(null)) + .set("pgJsonbVal") + .to(Value.pgJsonb(null)) + .set("byteVal") + .to((ByteArray) null) + .set("timestamp") + .to((Timestamp) null) + .set("date") + .to((Date) null) + .set("boolArray") + .toBoolArray((Iterable) null) + .set("longArray") + .toInt64Array((Iterable) null) + .set("doubleArray") + .toFloat64Array((Iterable) null) + .set("bigDecimalArray") + .toNumericArray(null) + .set("pgNumericArray") + .toPgNumericArray(null) + .set("byteArray") + .toBytesArray(null) + .set("timestampArray") + .toTimestampArray(null) + .set("dateArray") + .toDateArray(null) + .set("stringArray") + .toStringArray(null) + .set("jsonArray") + .toJsonArray(null) + .set("pgJsonbArray") + .toPgJsonbArray(null) + .build(); + + ParsedStatement parsedStatement = mock(ParsedStatement.class); + Statement statement = Statement.of("select * from foo"); + when(parsedStatement.getStatement()).thenReturn(statement); + AbortedException abortedException = mock(AbortedException.class); + ReadWriteTransaction transaction = mock(ReadWriteTransaction.class); + when(transaction.runWithRetry(any(Callable.class))) + .thenAnswer(invocationOnMock -> ((Callable) invocationOnMock.getArgument(0)).call()); + when(transaction.getStatementExecutor()).thenReturn(mock(StatementExecutor.class)); + + ResultSet queryResult = + ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, rowNullValues)); + ChecksumResultSet resultSet = + new ChecksumResultSet( + transaction, + DirectExecuteResultSet.ofResultSet(queryResult), + parsedStatement, + AnalyzeMode.NONE); + assertTrue(resultSet.next()); + assertTrue(resultSet.next()); + + // Ensure that retrying will return the same result. + ResultSet retryResult = + ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, rowNullValues)); + when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)) + .thenReturn(retryResult); + + // There have been no changes, so the retry should succeed. + resultSet.retry(abortedException); + + // Change field value from one non-null value to another non-null value. + for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) { + Builder builder = Struct.newBuilder(); + for (StructField field : rowNonNullValues.getType().getStructFields()) { + if (field.equals(fieldToChange)) { + builder.set(field.getName()).to(DIFFERENT_NON_NULL_VALUES.getValue(field.getName())); + } else { + builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName())); + } + } + ResultSet newRetryResult = + ResultSets.forRows(type, ImmutableList.of(builder.build(), rowNullValues)); + when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)) + .thenReturn(newRetryResult); + // The query result has changed, so this should now fail. + assertThrows( + "Missing exception for " + fieldToChange.getName(), + AbortedDueToConcurrentModificationException.class, + () -> resultSet.retry(abortedException)); + } + + // Change field value from non-null value to null value. + for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) { + Builder builder = Struct.newBuilder(); + for (StructField field : rowNonNullValues.getType().getStructFields()) { + if (field.equals(fieldToChange)) { + builder.set(field.getName()).to(rowNullValues.getValue(field.getName())); + } else { + builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName())); + } + } + ResultSet newRetryResult = + ResultSets.forRows(type, ImmutableList.of(builder.build(), rowNullValues)); + when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)) + .thenReturn(newRetryResult); + // The query result has changed, so this should now fail. + assertThrows( + "Missing exception for " + fieldToChange.getName(), + AbortedDueToConcurrentModificationException.class, + () -> resultSet.retry(abortedException)); + } + + // Change field value from null value to non-null value. + for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) { + Builder builder = Struct.newBuilder(); + for (StructField field : rowNullValues.getType().getStructFields()) { + if (field.equals(fieldToChange)) { + builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName())); + } else { + builder.set(field.getName()).to(rowNullValues.getValue(field.getName())); + } + } + // In this case the modified values are in the second row that first only contained null + // values. + ResultSet newRetryResult = + ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, builder.build())); + when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE)) + .thenReturn(newRetryResult); + // The query result has changed, so this should now fail. + assertThrows( + "Missing exception for " + fieldToChange.getName(), + AbortedDueToConcurrentModificationException.class, + () -> resultSet.retry(abortedException)); + } + } +} 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 896055ee9a..094503cfbc 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 @@ -249,6 +249,15 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getJsonList("test2"); verify(delegate).getJsonList("test2"); + subject.getPgJsonb(0); + verify(delegate).getPgJsonb(0); + subject.getPgJsonb("test0"); + verify(delegate).getPgJsonb("test0"); + subject.getPgJsonbList(2); + verify(delegate).getPgJsonbList(2); + subject.getPgJsonbList("test2"); + verify(delegate).getPgJsonbList("test2"); + subject.getStructList(0); subject.getStructList("test0"); 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 024032ab76..c3ac655a40 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 @@ -51,9 +51,12 @@ private static Type[] generateTypes(Dialect dialect) { .build() : Type.newBuilder().setCode(TypeCode.NUMERIC).build(), Type.newBuilder().setCode(TypeCode.STRING).build(), - Type.newBuilder() - .setCode(dialect == Dialect.POSTGRESQL ? TypeCode.STRING : TypeCode.JSON) - .build(), + dialect == Dialect.POSTGRESQL + ? Type.newBuilder() + .setCode(TypeCode.JSON) + .setTypeAnnotation(TypeAnnotationCode.PG_JSONB) + .build() + : Type.newBuilder().setCode(TypeCode.JSON).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(), @@ -85,8 +88,11 @@ private static Type[] generateTypes(Dialect dialect) { Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType( - Type.newBuilder() - .setCode(dialect == Dialect.POSTGRESQL ? TypeCode.STRING : TypeCode.JSON)) + dialect == Dialect.POSTGRESQL + ? Type.newBuilder() + .setCode(TypeCode.JSON) + .setTypeAnnotation(TypeAnnotationCode.PG_JSONB) + : Type.newBuilder().setCode(TypeCode.JSON)) .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java new file mode 100644 index 0000000000..74edbebb0b --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java @@ -0,0 +1,440 @@ +/* + * 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.google.cloud.spanner.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.NullValue; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.threeten.bp.Duration; + +// TODO: Re-enable when jsonb is GA. +@Ignore("Feature is not yet generally available") +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITPgJsonbTest { + + private static final Duration OPERATION_TIMEOUT = Duration.ofMinutes(5); + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static RemoteSpannerHelper testHelper; + private static DatabaseAdminClient databaseAdminClient; + private static List databasesToDrop; + private static String projectId; + private static String instanceId; + private static String databaseId; + private DatabaseClient databaseClient; + private String tableName; + + @BeforeClass + public static void beforeClass() throws Exception { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + testHelper = env.getTestHelper(); + databaseAdminClient = testHelper.getClient().getDatabaseAdminClient(); + databasesToDrop = new ArrayList<>(); + projectId = testHelper.getInstanceId().getProject(); + instanceId = testHelper.getInstanceId().getInstance(); + databaseId = testHelper.getUniqueDatabaseId(); + final Database database = + databaseAdminClient + .newDatabaseBuilder(DatabaseId.of(projectId, instanceId, databaseId)) + .setDialect(Dialect.POSTGRESQL) + .build(); + databaseAdminClient + .createDatabase(database, Collections.emptyList()) + .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + databasesToDrop.add(database.getId()); + } + + @AfterClass + public static void afterClass() throws Exception { + if (databasesToDrop != null) { + for (DatabaseId id : databasesToDrop) { + try { + databaseAdminClient.dropDatabase(id.getInstanceId().getInstance(), id.getDatabase()); + } catch (Exception e) { + System.err.println("Failed to drop database " + id + ", skipping...: " + e.getMessage()); + } + } + } + } + + @Before + public void setUp() throws Exception { + databaseClient = + testHelper.getClient().getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId)); + tableName = testHelper.getUniqueDatabaseId(); + databaseAdminClient + .updateDatabaseDdl( + instanceId, + databaseId, + Collections.singletonList( + "CREATE TABLE \"" + tableName + "\" (id BIGINT PRIMARY KEY, col1 JSONB)"), + null) + .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + @Test + public void testPgJsonbAsPrimaryKey() { + // JSONB is not allowed as a primary key. + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + databaseAdminClient + .updateDatabaseDdl( + instanceId, + databaseId, + Collections.singletonList( + "CREATE TABLE with_jsonb_pk (id jsonb primary key)"), + null) + .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)); + SpannerException spannerException = + SpannerExceptionFactory.asSpannerException(executionException.getCause()); + assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode()); + assertTrue( + spannerException.getMessage(), + spannerException + .getMessage() + .contains( + "Column with_jsonb_pk.id has type PG.JSONB, but is part of the primary key.")); + } + + @Test + public void testPgJsonbInSecondaryIndex() { + // JSONB is not allowed as a key in a secondary index. + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + databaseAdminClient + .updateDatabaseDdl( + instanceId, + databaseId, + Collections.singletonList( + "CREATE INDEX idx_jsonb on \"" + tableName + "\" (col1)"), + null) + .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)); + SpannerException spannerException = + SpannerExceptionFactory.asSpannerException(executionException.getCause()); + assertEquals(ErrorCode.FAILED_PRECONDITION, spannerException.getErrorCode()); + assertTrue( + spannerException.getMessage(), + spannerException + .getMessage() + .contains("Index idx_jsonb is defined on a column of unsupported type PG.JSONB.")); + } + + private static final String JSON_VALUE_1 = "{\"color\":\"red\",\"value\":\"#f00\"}"; + private static final String JSON_VALUE_2 = + "[" + + " {\"color\":\"red\",\"value\":\"#f00\"}," + + " {\"color\":\"green\",\"value\":\"#0f0\"}," + + " {\"color\":\"blue\",\"value\":\"#00f\"}" + + "]"; + + @Test + public void testLiteralPgJsonb() { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.of( + "INSERT INTO " + + tableName + + " (id, col1) VALUES" + + " (1, '" + + JSON_VALUE_1 + + "')" + + ", (2, '" + + JSON_VALUE_2 + + "')" + + ", (3, '{}')" + + ", (4, '[]')" + + ", (5, null)")); + return null; + }); + + verifyContents(); + } + + @Test + public void testPgJsonbParameter() { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder( + "INSERT INTO " + + tableName + + " (id, col1) VALUES" + + " (1, $1)" + + ", (2, $2)" + + ", (3, $3)" + + ", (4, $4)" + + ", (5, $5)") + .bind("p1") + .to(Value.pgJsonb(JSON_VALUE_1)) + .bind("p2") + .to(Value.pgJsonb(JSON_VALUE_2)) + .bind("p3") + .to(Value.pgJsonb("{}")) + .bind("p4") + .to(Value.pgJsonb("[]")) + .bind("p5") + .to(Value.pgJsonb(null)) + .build()); + return null; + }); + + verifyContents(); + } + + @Ignore("Untyped jsonb parameters are not yet supported") + @Test + public void testPgJsonbUntypedParameter() { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + + // Verify that we can use Jsonb as an untyped parameter. This is especially important for + // PGAdapter and the JDBC driver, as these will often use untyped parameters. + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.executeUpdate( + Statement.newBuilder( + "INSERT INTO " + + tableName + + " (id, col1) VALUES" + + " (1, $1)" + + ", (2, $2)" + + ", (3, $3)" + + ", (4, $4)" + + ", (5, $5)") + .bind("p1") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setStringValue(JSON_VALUE_1) + .build())) + .bind("p2") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setStringValue(JSON_VALUE_2) + .build())) + .bind("p3") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder().setStringValue("{}").build())) + .bind("p4") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder().setStringValue("[]").build())) + .bind("p5") + .to( + Value.untyped( + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build())) + .build()); + return null; + }); + + verifyContents(); + } + + @Test + public void testMutationsWithPgJsonbAsString() { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.buffer( + ImmutableList.of( + Mutation.newInsertBuilder(tableName) + .set("id") + .to(1) + .set("col1") + .to(JSON_VALUE_1) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(2) + .set("col1") + .to(JSON_VALUE_2) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(3) + .set("col1") + .to("{}") + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(4) + .set("col1") + .to("[]") + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(5) + .set("col1") + .to((String) null) + .build())); + return null; + }); + + verifyContents(); + } + + @Test + public void testMutationsWithPgJsonbAsValue() { + assumeFalse( + "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.buffer( + ImmutableList.of( + Mutation.newInsertBuilder(tableName) + .set("id") + .to(1) + .set("col1") + .to(Value.pgJsonb(JSON_VALUE_1)) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(2) + .set("col1") + .to(Value.pgJsonb(JSON_VALUE_2)) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(3) + .set("col1") + .to(Value.pgJsonb("{}")) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(4) + .set("col1") + .to(Value.pgJsonb("[]")) + .build(), + Mutation.newInsertBuilder(tableName) + .set("id") + .to(5) + .set("col1") + .to(Value.pgJsonb(null)) + .build())); + return null; + }); + + verifyContents(); + } + + private void verifyContents() { + try (ResultSet resultSet = + databaseClient + .singleUse() + .executeQuery(Statement.of("SELECT * FROM " + tableName + " ORDER BY id"))) { + + assertTrue(resultSet.next()); + // Note: We do not use the JSON_VALUE_1 constant here, because the backend prettifies the + // value a little, which means that there is a small difference between what we insert and + // what we get back. + assertEquals("{\"color\": \"red\", \"value\": \"#f00\"}", resultSet.getPgJsonb("col1")); + assertEquals( + Value.pgJsonb("{\"color\": \"red\", \"value\": \"#f00\"}"), resultSet.getValue("col1")); + + assertTrue(resultSet.next()); + assertEquals( + "[" + + "{\"color\": \"red\", \"value\": \"#f00\"}, " + + "{\"color\": \"green\", \"value\": \"#0f0\"}, " + + "{\"color\": \"blue\", \"value\": \"#00f\"}" + + "]", + resultSet.getPgJsonb("col1")); + assertEquals( + Value.pgJsonb( + "[" + + "{\"color\": \"red\", \"value\": \"#f00\"}, " + + "{\"color\": \"green\", \"value\": \"#0f0\"}, " + + "{\"color\": \"blue\", \"value\": \"#00f\"}" + + "]"), + resultSet.getValue("col1")); + + assertTrue(resultSet.next()); + assertEquals("{}", resultSet.getPgJsonb("col1")); + assertEquals(Value.pgJsonb("{}"), resultSet.getValue("col1")); + + assertTrue(resultSet.next()); + assertEquals("[]", resultSet.getPgJsonb("col1")); + assertEquals(Value.pgJsonb("[]"), resultSet.getValue("col1")); + + assertTrue(resultSet.next()); + assertTrue(resultSet.isNull("col1")); + + assertFalse(resultSet.next()); + } + } +} From 27d0d1472b3718069f6b78bc2e44e077d5ecce9f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:52:45 +0530 Subject: [PATCH 16/16] chore(main): release 6.29.0 (#1985) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ google-cloud-spanner-bom/pom.xml | 18 +++++++++--------- google-cloud-spanner/pom.xml | 4 ++-- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- grpc-google-cloud-spanner-v1/pom.xml | 4 ++-- pom.xml | 16 ++++++++-------- .../pom.xml | 4 ++-- .../pom.xml | 4 ++-- proto-google-cloud-spanner-v1/pom.xml | 4 ++-- samples/snapshot/pom.xml | 2 +- versions.txt | 14 +++++++------- 12 files changed, 54 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f37dea2a43..d9cbf72760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [6.29.0](https://github.com/googleapis/java-spanner/compare/v6.28.0...v6.29.0) (2022-08-29) + + +### Features + +* add support for db roles list ([#1916](https://github.com/googleapis/java-spanner/issues/1916)) ([8034c67](https://github.com/googleapis/java-spanner/commit/8034c67af6cfe24e96cc26b1cea51c3405ed98d6)) +* add support for PG JSONB data type ([#1964](https://github.com/googleapis/java-spanner/issues/1964)) ([d2b426f](https://github.com/googleapis/java-spanner/commit/d2b426fda2cd1463dfa0719dd80f8346cbef51c6)) +* Adds auto-generated CL for googleapis for jsonb ([#1983](https://github.com/googleapis/java-spanner/issues/1983)) ([23e57ff](https://github.com/googleapis/java-spanner/commit/23e57ffc627d0f688fa656887d82f8f1f99f3675)) + + +### Dependencies + +* update dependency com.google.cloud:google-cloud-monitoring to v3.4.1 ([#1968](https://github.com/googleapis/java-spanner/issues/1968)) ([e93ab4c](https://github.com/googleapis/java-spanner/commit/e93ab4cc4031ee2300f4e73d7d3a8e41de1bc7ae)) +* update dependency com.google.cloud:google-cloud-trace to v2.3.1 ([#1967](https://github.com/googleapis/java-spanner/issues/1967)) ([6479d19](https://github.com/googleapis/java-spanner/commit/6479d19dcca2b3e3df43a2858f5dcaf85685c31f)) + ## [6.28.0](https://github.com/googleapis/java-spanner/compare/v6.27.0...v6.28.0) (2022-08-11) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index ac361ce579..9e775f5ed8 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.28.1-SNAPSHOT + 6.29.0 pom com.google.cloud @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.28.1-SNAPSHOT + 6.29.0 com.google.cloud google-cloud-spanner test-jar - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.28.1-SNAPSHOT + 6.29.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 3b20ecc3f3..5b19731d39 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.28.1-SNAPSHOT + 6.29.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.28.1-SNAPSHOT + 6.29.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 37c5d8e912..a887432fcd 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.28.1-SNAPSHOT + 6.29.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.28.1-SNAPSHOT + 6.29.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index c64b18b59c..965cb5f8eb 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.28.1-SNAPSHOT + 6.29.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.28.1-SNAPSHOT + 6.29.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 490637415c..f580c1ab1c 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.28.1-SNAPSHOT + 6.29.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.28.1-SNAPSHOT + 6.29.0 diff --git a/pom.xml b/pom.xml index b7af24900f..f190cbacca 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.28.1-SNAPSHOT + 6.29.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -62,37 +62,37 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.28.1-SNAPSHOT + 6.29.0 com.google.cloud google-cloud-spanner - 6.28.1-SNAPSHOT + 6.29.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index c33922f869..95500262e8 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.28.1-SNAPSHOT + 6.29.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.28.1-SNAPSHOT + 6.29.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 275437a032..703219f801 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.28.1-SNAPSHOT + 6.29.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.28.1-SNAPSHOT + 6.29.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 92269cd769..ed9cdfd71e 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.28.1-SNAPSHOT + 6.29.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.28.1-SNAPSHOT + 6.29.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 324dd107ad..d00efec645 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -31,7 +31,7 @@ com.google.cloud google-cloud-spanner - 6.28.1-SNAPSHOT + 6.29.0 diff --git a/versions.txt b/versions.txt index 9d0ea61456..b90d1ec1b8 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.1-SNAPSHOT -proto-google-cloud-spanner-v1:6.28.0:6.28.1-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:6.28.0:6.28.1-SNAPSHOT -grpc-google-cloud-spanner-v1:6.28.0:6.28.1-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:6.28.0:6.28.1-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:6.28.0:6.28.1-SNAPSHOT -google-cloud-spanner:6.28.0:6.28.1-SNAPSHOT +proto-google-cloud-spanner-admin-instance-v1:6.29.0:6.29.0 +proto-google-cloud-spanner-v1:6.29.0:6.29.0 +proto-google-cloud-spanner-admin-database-v1:6.29.0:6.29.0 +grpc-google-cloud-spanner-v1:6.29.0:6.29.0 +grpc-google-cloud-spanner-admin-instance-v1:6.29.0:6.29.0 +grpc-google-cloud-spanner-admin-database-v1:6.29.0:6.29.0 +google-cloud-spanner:6.29.0:6.29.0