From 7adca95cf6b230b9197668c174610e207cd46715 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:24:16 +0000 Subject: [PATCH 01/29] chore(main): release 2.16.2-SNAPSHOT (#1542) :robot: I have created a release *beep* *boop* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- versions.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0d3858e85..841d2648b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 google-cloud-spanner-jdbc - 2.16.1 + 2.16.2-SNAPSHOT jar Google Cloud Spanner JDBC https://github.com/googleapis/java-spanner-jdbc diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 2cf73702e..baaf8bb89 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-spanner-jdbc - 2.16.1 + 2.16.2-SNAPSHOT diff --git a/versions.txt b/versions.txt index 5aa7bb71c..21a50e390 100644 --- a/versions.txt +++ b/versions.txt @@ -1,4 +1,4 @@ # Format: # module:released-version:current-version -google-cloud-spanner-jdbc:2.16.1:2.16.1 +google-cloud-spanner-jdbc:2.16.1:2.16.2-SNAPSHOT From 5b8fd2e2f100312a05fd8401a05385ae5306aa33 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 25 Mar 2024 16:19:10 +0100 Subject: [PATCH 02/29] chore(deps): update dependency com.google.cloud:google-cloud-spanner-jdbc to v2.16.1 (#1543) --- samples/install-without-bom/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index a52052ae6..a2537a7e6 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -29,7 +29,7 @@ com.google.cloud google-cloud-spanner-jdbc - 2.16.0 + 2.16.1 From fb8377edbb8ab115657a831c1b5c1a892e7be172 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:43:15 +0100 Subject: [PATCH 03/29] chore: update ignore paths in renovate config (#1945) (#1545) Source-Link: https://github.com/googleapis/synthtool/commit/571a0916913e0aff0f66ca513514072893d534eb Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-java:latest@sha256:81c3ec554428c8ff6c92f0d58668b7ef52265d053a82284c97a326745e786949 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- renovate.json | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index db1099bec..6167135e9 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest - digest: sha256:084ad4c60551b075846bcb2405ec1c14b0d00ec1eb5503d4dd0d2a92cdc2d3e2 -# created: 2024-03-15T14:33:32.257974519Z + digest: sha256:81c3ec554428c8ff6c92f0d58668b7ef52265d053a82284c97a326745e786949 +# created: 2024-03-27T17:59:25.436353226Z diff --git a/renovate.json b/renovate.json index 15ce4ecae..d47519b15 100644 --- a/renovate.json +++ b/renovate.json @@ -12,7 +12,10 @@ ], "ignorePaths": [ ".kokoro/requirements.txt", - ".github/workflows/**" + ".github/workflows/approve-readme.yaml", + ".github/workflows/ci.yaml", + ".github/workflows/renovate_config_check.yaml", + ".github/workflows/samples.yaml" ], "customManagers": [ { @@ -23,6 +26,15 @@ "matchStrings": ["value: \"gcr.io/cloud-devrel-public-resources/graalvm.*:(?.*?)\""], "depNameTemplate": "com.google.cloud:sdk-platform-java-config", "datasourceTemplate": "maven" + }, + { + "customType": "regex", + "fileMatch": [ + "^.github/workflows/unmanaged_dependency_check.yaml$" + ], + "matchStrings": ["uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v(?.+?)\\n"], + "depNameTemplate": "com.google.cloud:sdk-platform-java-config", + "datasourceTemplate": "maven" } ], "packageRules": [ From 36be4e113c4b46523061c89b70f3d748a814ba2a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Apr 2024 08:41:03 +0200 Subject: [PATCH 04/29] chore(deps): update dependency com.google.cloud:libraries-bom to v26.35.0 (#1551) --- samples/snippets/pom.xml | 2 +- samples/spring-data-jdbc/pom.xml | 2 +- samples/spring-data-mybatis/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 6c9638733..f36192293 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.34.0 + 26.35.0 pom import diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml index 34664f77b..4d35a2087 100644 --- a/samples/spring-data-jdbc/pom.xml +++ b/samples/spring-data-jdbc/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.34.0 + 26.35.0 import pom diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml index 4f8dccf9d..1ec0e4b92 100644 --- a/samples/spring-data-mybatis/pom.xml +++ b/samples/spring-data-mybatis/pom.xml @@ -35,7 +35,7 @@ com.google.cloud libraries-bom - 26.34.0 + 26.35.0 import pom From ac75b9faf0eaeb499428ecefda1f3285b3d28e67 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Apr 2024 08:41:30 +0200 Subject: [PATCH 05/29] deps: update dependency com.google.cloud:google-cloud-spanner-bom to v6.63.0 (#1552) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 841d2648b..38d49832b 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ com.google.cloud google-cloud-spanner-bom - 6.62.0 + 6.63.0 pom import From 18c5ad4d4124f095547d50c0d2e154bc06380642 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Apr 2024 09:01:25 +0200 Subject: [PATCH 06/29] deps: update actions/checkout digest to b4ffde6 (#1546) --- .github/workflows/integration-tests-against-emulator.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml index f299be4f9..eff700ea5 100644 --- a/.github/workflows/integration-tests-against-emulator.yaml +++ b/.github/workflows/integration-tests-against-emulator.yaml @@ -16,7 +16,7 @@ jobs: - 9020:9020 steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: stCarolas/setup-maven@v4 with: maven-version: 3.8.1 From 736e3afa54149dd11803bd715569afd9ec8e87f2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Apr 2024 09:01:48 +0200 Subject: [PATCH 07/29] deps: update actions/checkout action to v4 (#1547) --- .github/workflows/spring-data-jdbc-sample.yaml | 2 +- .github/workflows/spring-data-mybatis-sample.yaml | 2 +- .github/workflows/unmanaged-dependency-check.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/spring-data-jdbc-sample.yaml b/.github/workflows/spring-data-jdbc-sample.yaml index 028a48631..fc488ab89 100644 --- a/.github/workflows/spring-data-jdbc-sample.yaml +++ b/.github/workflows/spring-data-jdbc-sample.yaml @@ -20,7 +20,7 @@ jobs: spring-data-jdbc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: temurin diff --git a/.github/workflows/spring-data-mybatis-sample.yaml b/.github/workflows/spring-data-mybatis-sample.yaml index 39be0e6e6..636f71258 100644 --- a/.github/workflows/spring-data-mybatis-sample.yaml +++ b/.github/workflows/spring-data-mybatis-sample.yaml @@ -20,7 +20,7 @@ jobs: spring-data-jdbc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: temurin diff --git a/.github/workflows/unmanaged-dependency-check.yaml b/.github/workflows/unmanaged-dependency-check.yaml index 4ca518aea..a8a2e2ee3 100644 --- a/.github/workflows/unmanaged-dependency-check.yaml +++ b/.github/workflows/unmanaged-dependency-check.yaml @@ -5,7 +5,7 @@ jobs: unmanaged_dependency_check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: distribution: temurin From d1d422cdf0a74231c468262662fdf5ce4d27b8ef Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 1 Apr 2024 09:02:14 +0200 Subject: [PATCH 08/29] deps: update actions/github-script action to v7 (#1548) --- .github/workflows/auto-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index 7a106d007..18d92e5a2 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest if: contains(github.head_ref, 'release-please') steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: github-token: ${{secrets.YOSHI_APPROVER_TOKEN}} debug: true From 961222c15e09ddee3f3fbc2354fcaa9bba3ecfe1 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 4 Apr 2024 17:42:18 +0200 Subject: [PATCH 09/29] chore(deps): update dependency com.google.cloud:libraries-bom to v26.36.0 (#1555) 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.35.0` -> `26.36.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.cloud:libraries-bom/26.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.cloud:libraries-bom/26.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.cloud:libraries-bom/26.35.0/26.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.cloud:libraries-bom/26.35.0/26.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/java-cloud-bom (com.google.cloud:libraries-bom) ### [`v26.36.0`](https://togithub.com/googleapis/java-cloud-bom/blob/HEAD/CHANGELOG.md#26360-2024-04-03) [Compare Source](https://togithub.com/googleapis/java-cloud-bom/compare/v26.35.0...v26.36.0) ##### Dependencies - update dependency com.google.cloud:gapic-libraries-bom to v1.34.0 ([#​6519](https://togithub.com/googleapis/java-cloud-bom/issues/6519)) ([602ba0c](https://togithub.com/googleapis/java-cloud-bom/commit/602ba0c894123089b0c8eb297d32c638265ccf03))
--- ### 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, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc). --- samples/snippets/pom.xml | 2 +- samples/spring-data-jdbc/pom.xml | 2 +- samples/spring-data-mybatis/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index f36192293..dfe37f9db 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.35.0 + 26.36.0 pom import diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml index 4d35a2087..ed88ebf92 100644 --- a/samples/spring-data-jdbc/pom.xml +++ b/samples/spring-data-jdbc/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.35.0 + 26.36.0 import pom diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml index 1ec0e4b92..4aeb75380 100644 --- a/samples/spring-data-mybatis/pom.xml +++ b/samples/spring-data-mybatis/pom.xml @@ -35,7 +35,7 @@ com.google.cloud libraries-bom - 26.35.0 + 26.36.0 import pom From 25be8ea07b3f9ea0138ee3580bf65fddc166f2c7 Mon Sep 17 00:00:00 2001 From: Joe Wang <106995533+JoeWang1127@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:41:24 +0000 Subject: [PATCH 10/29] chore: update version of unmanaged dependency check (#1556) --- ...ed-dependency-check.yaml => unmanaged_dependency_check.yaml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{unmanaged-dependency-check.yaml => unmanaged_dependency_check.yaml} (87%) diff --git a/.github/workflows/unmanaged-dependency-check.yaml b/.github/workflows/unmanaged_dependency_check.yaml similarity index 87% rename from .github/workflows/unmanaged-dependency-check.yaml rename to .github/workflows/unmanaged_dependency_check.yaml index a8a2e2ee3..4406831cb 100644 --- a/.github/workflows/unmanaged-dependency-check.yaml +++ b/.github/workflows/unmanaged_dependency_check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@unmanaged-dependencies-check-latest + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.28.1 with: bom-path: pom.xml From 121d08e16db0bbb1f6041a201d620829e7121f4d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 5 Apr 2024 06:59:37 +0200 Subject: [PATCH 11/29] deps: update stcarolas/setup-maven action to v5 (#1550) --- .github/workflows/integration-tests-against-emulator.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml index eff700ea5..0a330e1a8 100644 --- a/.github/workflows/integration-tests-against-emulator.yaml +++ b/.github/workflows/integration-tests-against-emulator.yaml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: stCarolas/setup-maven@v4 + - uses: stCarolas/setup-maven@v5 with: maven-version: 3.8.1 - uses: actions/setup-java@v1 From cb2b911b0b332e97f85974ec880a5ab7a12a7578 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 5 Apr 2024 09:57:31 +0200 Subject: [PATCH 12/29] deps: update actions/setup-java action to v4 (#1549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps: update actions/setup-java action to v4 * build: add distribution + actual version number * build: remove redundant Maven setup --------- Co-authored-by: Knut Olav LΓΈite --- .github/workflows/integration-tests-against-emulator.yaml | 8 +++----- .github/workflows/spring-data-jdbc-sample.yaml | 2 +- .github/workflows/spring-data-mybatis-sample.yaml | 2 +- .github/workflows/unmanaged_dependency_check.yaml | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml index 0a330e1a8..ccf1ebd1f 100644 --- a/.github/workflows/integration-tests-against-emulator.yaml +++ b/.github/workflows/integration-tests-against-emulator.yaml @@ -16,12 +16,10 @@ jobs: - 9020:9020 steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: stCarolas/setup-maven@v5 - with: - maven-version: 3.8.1 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: + distribution: temurin java-version: 8 - run: java -version - run: .kokoro/build.sh diff --git a/.github/workflows/spring-data-jdbc-sample.yaml b/.github/workflows/spring-data-jdbc-sample.yaml index fc488ab89..0185a2905 100644 --- a/.github/workflows/spring-data-jdbc-sample.yaml +++ b/.github/workflows/spring-data-jdbc-sample.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/spring-data-mybatis-sample.yaml b/.github/workflows/spring-data-mybatis-sample.yaml index 636f71258..5267d6f59 100644 --- a/.github/workflows/spring-data-mybatis-sample.yaml +++ b/.github/workflows/spring-data-mybatis-sample.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml index 4406831cb..c724f3dab 100644 --- a/.github/workflows/unmanaged_dependency_check.yaml +++ b/.github/workflows/unmanaged_dependency_check.yaml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 11 From 32a6fa3b315911a18f38a2ecfd909263b704238d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 5 Apr 2024 19:08:00 +0200 Subject: [PATCH 13/29] chore(deps): update dependency com.google.cloud:libraries-bom to v26.37.0 (#1557) --- samples/snippets/pom.xml | 2 +- samples/spring-data-jdbc/pom.xml | 2 +- samples/spring-data-mybatis/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index dfe37f9db..29bc0371c 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.36.0 + 26.37.0 pom import diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml index ed88ebf92..c9fbf898d 100644 --- a/samples/spring-data-jdbc/pom.xml +++ b/samples/spring-data-jdbc/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 26.36.0 + 26.37.0 import pom diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml index 4aeb75380..16e8c3d37 100644 --- a/samples/spring-data-mybatis/pom.xml +++ b/samples/spring-data-mybatis/pom.xml @@ -35,7 +35,7 @@ com.google.cloud libraries-bom - 26.36.0 + 26.37.0 import pom From 6f85f5c785b5bed9983544e7baabba82a2e4e05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 9 Apr 2024 14:07:54 +0200 Subject: [PATCH 14/29] chore: add connection helper to get Spanner instance (#1558) Add a connection helper to get the underlying Spanner instance. This helper will be removed once the getSpanner() method has been made public in the Java client library. --- .../spanner/connection/ConnectionHelper.java | 31 +++++++++++++++++++ .../spanner/jdbc/AbstractJdbcConnection.java | 6 ++++ 2 files changed, 37 insertions(+) create mode 100644 src/main/java/com/google/cloud/spanner/connection/ConnectionHelper.java diff --git a/src/main/java/com/google/cloud/spanner/connection/ConnectionHelper.java b/src/main/java/com/google/cloud/spanner/connection/ConnectionHelper.java new file mode 100644 index 000000000..5f71ec605 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/connection/ConnectionHelper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import com.google.cloud.spanner.Spanner; + +/** Static helper class to get the {@link Spanner} instance from the underlying connection. */ +public class ConnectionHelper { + + /** Private constructor to prevent instantiation. */ + private ConnectionHelper() {} + + public static Spanner getSpanner(Connection connection) { + // TODO: Remove once getSpanner() has been added to the public interface. + return ((ConnectionImpl) connection).getSpanner(); + } +} diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java index 33cf3bc57..340b6d409 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java @@ -17,8 +17,10 @@ package com.google.cloud.spanner.jdbc; import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.AbstractStatementParser; +import com.google.cloud.spanner.connection.ConnectionHelper; import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.common.annotations.VisibleForTesting; import com.google.rpc.Code; @@ -78,6 +80,10 @@ ConnectionOptions getConnectionOptions() { return options; } + Spanner getSpanner() { + return ConnectionHelper.getSpanner(this.spanner); + } + @Override public Dialect getDialect() { return spanner.getDialect(); From 194c8205dee9cc4144b18e219df43027b9f15cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 10 Apr 2024 14:28:02 +0200 Subject: [PATCH 15/29] docs: create samples for quickstart guide (#1536) * docs: create samples for quickstart guide * docs: add createConnection sample * docs: add sample for DML * docs: add sample for mutations * feat: add dml batch sample * feat: add more samples * feat: add more samples * feat: add data boost sample * chore: fix style * chore: add run commands * fix: shorten method length * test: add sample test runner * fix: duplicate name * chore: fix tag prefix * chore: add more commands * feat: add sample for emulator * feat: add tagging sample * chore: use fixed port binding for now * chore: remove changes to JdbcDriver --- .github/workflows/sample-tests.yml | 30 + samples/snippets/java.header | 15 + samples/snippets/license-checks.xml | 10 + samples/snippets/pom.xml | 92 +- .../com/example/spanner/jdbc/JdbcSample.java | 1830 +++++++++++++++++ .../example/spanner/jdbc/package-info.java | 18 + .../example/spanner/jdbc/JdbcSampleTest.java | 396 ++++ 7 files changed, 2382 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/sample-tests.yml create mode 100644 samples/snippets/java.header create mode 100644 samples/snippets/license-checks.xml create mode 100644 samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java create mode 100644 samples/snippets/src/main/java/com/example/spanner/jdbc/package-info.java create mode 100644 samples/snippets/src/test/java/com/example/spanner/jdbc/JdbcSampleTest.java diff --git a/.github/workflows/sample-tests.yml b/.github/workflows/sample-tests.yml new file mode 100644 index 000000000..cec19c7a2 --- /dev/null +++ b/.github/workflows/sample-tests.yml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Github action job to test core java library features on +# downstream client libraries before they are released. +on: + pull_request: +name: samples +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + - name: Run sample tests + run: mvn --quiet --batch-mode test + working-directory: samples/snippets diff --git a/samples/snippets/java.header b/samples/snippets/java.header new file mode 100644 index 000000000..d0970ba7d --- /dev/null +++ b/samples/snippets/java.header @@ -0,0 +1,15 @@ +^/\*$ +^ \* Copyright \d\d\d\d,? Google (Inc\.|LLC)$ +^ \*$ +^ \* Licensed under the Apache License, Version 2\.0 \(the "License"\);$ +^ \* you may not use this file except in compliance with the License\.$ +^ \* You may obtain a copy of the License at$ +^ \*$ +^ \*[ ]+https?://www.apache.org/licenses/LICENSE-2\.0$ +^ \*$ +^ \* Unless required by applicable law or agreed to in writing, software$ +^ \* distributed under the License is distributed on an "AS IS" BASIS,$ +^ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.$ +^ \* See the License for the specific language governing permissions and$ +^ \* limitations under the License\.$ +^ \*/$ diff --git a/samples/snippets/license-checks.xml b/samples/snippets/license-checks.xml new file mode 100644 index 000000000..a7a611940 --- /dev/null +++ b/samples/snippets/license-checks.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 29bc0371c..047c0c761 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -1,20 +1,16 @@ 4.0.0 - com.google.cloud spanner-jdbc-snippets jar Google Google Cloud Spanner JDBC Snippets https://github.com/googleapis/java-spanner-jdbc - - com.google.cloud.samples - shared-configuration - 1.2.0 + com.google.cloud + sdk-platform-java-config + 3.27.0 + @@ -42,6 +38,84 @@ com.google.cloud google-cloud-spanner-jdbc + + + + com.google.cloud + google-cloud-spanner + + + + org.testcontainers + testcontainers + 1.19.7 + test + + + junit + junit + 4.13.2 + test + - + + + + + maven-resources-plugin + + + copy-resources + validate + + copy-resources + + + ${project.build.directory}/jdbc-snippets + + + resources + true + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/jdbc-snippets/lib + false + false + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + + jdbc-snippets/jdbc-samples + + false + + com.example.spanner.jdbc.JdbcSample + true + lib/ + + + + + + diff --git a/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java b/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java new file mode 100644 index 000000000..2e46058de --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java @@ -0,0 +1,1830 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.jdbc; + +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; +import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.CreateDatabaseRequest; +import com.google.spanner.admin.database.v1.DatabaseDialect; +import com.google.spanner.admin.instance.v1.InstanceName; +import com.google.spanner.v1.DatabaseName; +import io.grpc.ManagedChannelBuilder; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; + +public final class JdbcSample { + static class Singer { + + /** Primary key in the Singers table. */ + private final long singerId; + + /** Mapped to the FirstName column. */ + private final String firstName; + + /** Mapped to the FirstName column. */ + private final String lastName; + + Singer(final long id, final String first, final String last) { + this.singerId = id; + this.firstName = first; + this.lastName = last; + } + + public long getSingerId() { + return singerId; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + } + + static class Album { + + /** The first part of the primary key of Albums. */ + private final long singerId; + + /** The second part of the primary key of Albums. */ + private final long albumId; + + /** Mapped to the AlbumTitle column. */ + private final String albumTitle; + + Album(final long singer, final long album, final String title) { + this.singerId = singer; + this.albumId = album; + this.albumTitle = title; + } + + public long getSingerId() { + return singerId; + } + + public long getAlbumId() { + return albumId; + } + + public String getAlbumTitle() { + return albumTitle; + } + } + + // [START spanner_insert_data] + // [START spanner_postgresql_insert_data] + /** The list of Singers to insert. */ + static final List SINGERS = + Arrays.asList( + new Singer(1, "Marc", "Richards"), + new Singer(2, "Catalina", "Smith"), + new Singer(3, "Alice", "Trentor"), + new Singer(4, "Lea", "Martin"), + new Singer(5, "David", "Lomond")); + + /** The list of Albums to insert. */ + static final List ALBUMS = + Arrays.asList( + new Album(1, 1, "Total Junk"), + new Album(1, 2, "Go, Go, Go"), + new Album(2, 1, "Green"), + new Album(2, 2, "Forever Hold Your Peace"), + new Album(2, 3, "Terrified")); + + // [END spanner_insert_data] + // [END spanner_postgresql_insert_data] + + private JdbcSample() { + } + + // [START spanner_create_database] + static void createDatabase( + final DatabaseAdminClient dbAdminClient, + final InstanceName instanceName, + final String databaseId, + final Properties properties) throws SQLException { + // Use the Spanner admin client to create a database. + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + .setCreateStatement("CREATE DATABASE `" + databaseId + "`") + .setParent(instanceName.toString()) + .build(); + try { + dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + + // Connect to the database with the JDBC driver and create two test tables. + String projectId = instanceName.getProject(); + String instanceId = instanceName.getInstance(); + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + projectId, instanceId, databaseId), + properties)) { + try (Statement statement = connection.createStatement()) { + // Create the tables in one batch. + statement.addBatch( + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo BYTES(MAX)," + + " FullName STRING(2048) AS " + + " (ARRAY_TO_STRING([FirstName, LastName], \" \")) STORED" + + ") PRIMARY KEY (SingerId)"); + statement.addBatch( + "CREATE TABLE Albums (" + + " SingerId INT64 NOT NULL," + + " AlbumId INT64 NOT NULL," + + " AlbumTitle STRING(MAX)" + + ") PRIMARY KEY (SingerId, AlbumId)," + + " INTERLEAVE IN PARENT Singers ON DELETE CASCADE"); + statement.executeBatch(); + } + } + System.out.printf( + "Created database [%s]\n", + DatabaseName.of(projectId, instanceId, databaseId)); + } + // [END spanner_create_database] + + // [START spanner_postgresql_create_database] + static void createPostgreSQLDatabase( + final DatabaseAdminClient dbAdminClient, + final InstanceName instanceName, + final String databaseId, + final Properties properties) throws SQLException { + // Use the Spanner admin client to create a database. + CreateDatabaseRequest createDatabaseRequest = + CreateDatabaseRequest.newBuilder() + // PostgreSQL database names and other identifiers + // must be quoted using double quotes. + .setCreateStatement("create database \"" + databaseId + "\"") + .setParent(instanceName.toString()) + .setDatabaseDialect(DatabaseDialect.POSTGRESQL) + .build(); + try { + dbAdminClient.createDatabaseAsync(createDatabaseRequest).get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + + // Connect to the database with the JDBC driver and create two test tables. + String projectId = instanceName.getProject(); + String instanceId = instanceName.getInstance(); + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + projectId, instanceId, databaseId), + properties)) { + try (Statement statement = connection.createStatement()) { + // Create the tables in one batch. + statement.addBatch( + "create table singers (" + + " singer_id bigint primary key not null," + + " first_name varchar(1024)," + + " last_name varchar(1024)," + + " singer_info bytea," + + " full_name varchar(2048) generated always as (\n" + + " case when first_name is null then last_name\n" + + " when last_name is null then first_name\n" + + " else first_name || ' ' || last_name\n" + + " end) stored" + + ")"); + statement.addBatch( + "create table albums (" + + " singer_id bigint not null," + + " album_id bigint not null," + + " album_title varchar," + + " primary key (singer_id, album_id)" + + ") interleave in parent singers on delete cascade"); + statement.executeBatch(); + } + } + System.out.printf( + "Created database [%s]\n", + DatabaseName.of(projectId, instanceId, databaseId)); + } + // [END spanner_postgresql_create_database] + + // [START spanner_create_jdbc_connection] + static void createConnection( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + // Connection properties can be specified both with in a Properties object + // and in the connection URL. + properties.put("numChannels", "8"); + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s" + + ";minSessions=400;maxSessions=400", + project, instance, database), + properties)) { + try (ResultSet resultSet = + connection.createStatement().executeQuery("select 'Hello World!'")) { + while (resultSet.next()) { + System.out.println(resultSet.getString(1)); + } + } + } + } + // [END spanner_create_jdbc_connection] + + // [START spanner_create_jdbc_connection_with_emulator] + static void createConnectionWithEmulator( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + // Add autoConfigEmulator=true to the connection URL to instruct the JDBC + // driver to connect to the Spanner emulator on localhost:9010. + // The Spanner instance and database are automatically created if these + // don't already exist. + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s" + + ";autoConfigEmulator=true", + project, instance, database), + properties)) { + try (ResultSet resultSet = + connection.createStatement().executeQuery("select 'Hello World!'")) { + while (resultSet.next()) { + System.out.println(resultSet.getString(1)); + } + } + } + } + // [END spanner_create_jdbc_connection_with_emulator] + + // [START spanner_dml_getting_started_insert] + static void writeDataWithDml( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Add 4 rows in one statement. + // JDBC always uses '?' as a parameter placeholder. + try (PreparedStatement preparedStatement = + connection.prepareStatement( + "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?)")) { + + final ImmutableList singers = + ImmutableList.of( + new Singer(/* SingerId = */ 12L, "Melissa", "Garcia"), + new Singer(/* SingerId = */ 13L, "Russel", "Morales"), + new Singer(/* SingerId = */ 14L, "Jacqueline", "Long"), + new Singer(/* SingerId = */ 15L, "Dylan", "Shaw")); + + // Note that JDBC parameters start at index 1. + int paramIndex = 0; + for (Singer singer : singers) { + preparedStatement.setLong(++paramIndex, singer.singerId); + preparedStatement.setString(++paramIndex, singer.firstName); + preparedStatement.setString(++paramIndex, singer.lastName); + } + + int updateCount = preparedStatement.executeUpdate(); + System.out.printf("%d records inserted.\n", updateCount); + } + } + } + // [END spanner_dml_getting_started_insert] + + // [START spanner_postgresql_dml_getting_started_insert] + static void writeDataWithDmlPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Add 4 rows in one statement. + // JDBC always uses '?' as a parameter placeholder. + try (PreparedStatement preparedStatement = + connection.prepareStatement( + "INSERT INTO singers (singer_id, first_name, last_name) VALUES " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?), " + + "(?, ?, ?)")) { + + final ImmutableList singers = + ImmutableList.of( + new Singer(/* SingerId = */ 12L, "Melissa", "Garcia"), + new Singer(/* SingerId = */ 13L, "Russel", "Morales"), + new Singer(/* SingerId = */ 14L, "Jacqueline", "Long"), + new Singer(/* SingerId = */ 15L, "Dylan", "Shaw")); + + // Note that JDBC parameters start at index 1. + int paramIndex = 0; + for (Singer singer : singers) { + preparedStatement.setLong(++paramIndex, singer.singerId); + preparedStatement.setString(++paramIndex, singer.firstName); + preparedStatement.setString(++paramIndex, singer.lastName); + } + + int updateCount = preparedStatement.executeUpdate(); + System.out.printf("%d records inserted.\n", updateCount); + } + } + } + // [END spanner_postgresql_dml_getting_started_insert] + + // [START spanner_dml_batch] + static void writeDataWithDmlBatch( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Add multiple rows in one DML batch. + // JDBC always uses '?' as a parameter placeholder. + try (PreparedStatement preparedStatement = + connection.prepareStatement( + "INSERT INTO Singers (SingerId, FirstName, LastName) " + + "VALUES (?, ?, ?)")) { + final ImmutableList singers = + ImmutableList.of( + new Singer(/* SingerId = */ 16L, "Sarah", "Wilson"), + new Singer(/* SingerId = */ 17L, "Ethan", "Miller"), + new Singer(/* SingerId = */ 18L, "Maya", "Patel")); + + for (Singer singer : singers) { + // Note that JDBC parameters start at index 1. + int paramIndex = 0; + preparedStatement.setLong(++paramIndex, singer.singerId); + preparedStatement.setString(++paramIndex, singer.firstName); + preparedStatement.setString(++paramIndex, singer.lastName); + preparedStatement.addBatch(); + } + + int[] updateCounts = preparedStatement.executeBatch(); + System.out.printf( + "%d records inserted.\n", + Arrays.stream(updateCounts).sum()); + } + } + } + // [END spanner_dml_batch] + + // [START spanner_postgresql_dml_batch] + static void writeDataWithDmlBatchPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Add multiple rows in one DML batch. + // JDBC always uses '?' as a parameter placeholder. + try (PreparedStatement preparedStatement = + connection.prepareStatement( + "INSERT INTO singers (singer_id, first_name, last_name)" + + " VALUES (?, ?, ?)")) { + final ImmutableList singers = + ImmutableList.of( + new Singer(/* SingerId = */ 16L, "Sarah", "Wilson"), + new Singer(/* SingerId = */ 17L, "Ethan", "Miller"), + new Singer(/* SingerId = */ 18L, "Maya", "Patel")); + + for (Singer singer : singers) { + // Note that JDBC parameters start at index 1. + int paramIndex = 0; + preparedStatement.setLong(++paramIndex, singer.singerId); + preparedStatement.setString(++paramIndex, singer.firstName); + preparedStatement.setString(++paramIndex, singer.lastName); + preparedStatement.addBatch(); + } + + int[] updateCounts = preparedStatement.executeBatch(); + System.out.printf( + "%d records inserted.\n", + Arrays.stream(updateCounts).sum()); + } + } + } + // [END spanner_postgresql_dml_batch] + + // [START spanner_insert_data] + static void writeDataWithMutations( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Unwrap the CloudSpannerJdbcConnection interface + // from the java.sql.Connection. + CloudSpannerJdbcConnection cloudSpannerJdbcConnection = + connection.unwrap(CloudSpannerJdbcConnection.class); + + List mutations = new ArrayList<>(); + for (Singer singer : SINGERS) { + mutations.add( + Mutation.newInsertBuilder("Singers") + .set("SingerId") + .to(singer.singerId) + .set("FirstName") + .to(singer.firstName) + .set("LastName") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("Albums") + .set("SingerId") + .to(album.singerId) + .set("AlbumId") + .to(album.albumId) + .set("AlbumTitle") + .to(album.albumTitle) + .build()); + } + // Apply the mutations atomically to Spanner. + cloudSpannerJdbcConnection.write(mutations); + System.out.printf("Inserted %d rows.\n", mutations.size()); + } + } + // [END spanner_insert_data] + + // [START spanner_postgresql_insert_data] + static void writeDataWithMutationsPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Unwrap the CloudSpannerJdbcConnection interface + // from the java.sql.Connection. + CloudSpannerJdbcConnection cloudSpannerJdbcConnection = + connection.unwrap(CloudSpannerJdbcConnection.class); + + List mutations = new ArrayList<>(); + for (Singer singer : SINGERS) { + mutations.add( + Mutation.newInsertBuilder("singers") + .set("singer_id") + .to(singer.singerId) + .set("first_name") + .to(singer.firstName) + .set("last_name") + .to(singer.lastName) + .build()); + } + for (Album album : ALBUMS) { + mutations.add( + Mutation.newInsertBuilder("albums") + .set("singer_id") + .to(album.singerId) + .set("album_id") + .to(album.albumId) + .set("album_title") + .to(album.albumTitle) + .build()); + } + // Apply the mutations atomically to Spanner. + cloudSpannerJdbcConnection.write(mutations); + System.out.printf("Inserted %d rows.\n", mutations.size()); + } + } + // [END spanner_postgresql_insert_data] + + // [START spanner_query_data] + static void queryData( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + resultSet.getString("AlbumTitle")); + } + } + } + } + // [END spanner_query_data] + + // [START spanner_postgresql_query_data] + static void queryDataPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT singer_id, album_id, album_title " + + "FROM albums")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("singer_id"), + resultSet.getLong("album_id"), + resultSet.getString("album_title")); + } + } + } + } + // [END spanner_postgresql_query_data] + + // [START spanner_query_with_parameter] + static void queryWithParameter( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (PreparedStatement statement = + connection.prepareStatement( + "SELECT SingerId, FirstName, LastName " + + "FROM Singers " + + "WHERE LastName = ?")) { + statement.setString(1, "Garcia"); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + } + } + } + // [END spanner_query_with_parameter] + + // [START spanner_postgresql_query_with_parameter] + static void queryWithParameterPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (PreparedStatement statement = + connection.prepareStatement( + "SELECT singer_id, first_name, last_name " + + "FROM singers " + + "WHERE last_name = ?")) { + statement.setString(1, "Garcia"); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("singer_id"), + resultSet.getString("first_name"), + resultSet.getString("last_name")); + } + } + } + } + } + // [END spanner_postgresql_query_with_parameter] + + // [START spanner_add_column] + static void addColumn( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + connection + .createStatement() + .execute("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"); + System.out.println("Added MarketingBudget column"); + } + } + // [END spanner_add_column] + + // [START spanner_postgresql_add_column] + static void addColumnPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + connection + .createStatement() + .execute("alter table albums add column marketing_budget bigint"); + System.out.println("Added marketing_budget column"); + } + } + // [END spanner_postgresql_add_column] + + // [START spanner_ddl_batch] + static void ddlBatch( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (Statement statement = connection.createStatement()) { + // Create two new tables in one batch. + statement.addBatch( + "CREATE TABLE Venues (" + + " VenueId INT64 NOT NULL," + + " Name STRING(1024)," + + " Description JSON" + + ") PRIMARY KEY (VenueId)"); + statement.addBatch( + "CREATE TABLE Concerts (" + + " ConcertId INT64 NOT NULL," + + " VenueId INT64 NOT NULL," + + " SingerId INT64 NOT NULL," + + " StartTime TIMESTAMP," + + " EndTime TIMESTAMP," + + " CONSTRAINT Fk_Concerts_Venues FOREIGN KEY" + + " (VenueId) REFERENCES Venues (VenueId)," + + " CONSTRAINT Fk_Concerts_Singers FOREIGN KEY" + + " (SingerId) REFERENCES Singers (SingerId)," + + ") PRIMARY KEY (ConcertId)"); + statement.executeBatch(); + } + System.out.println("Added Venues and Concerts tables"); + } + } + // [END spanner_ddl_batch] + + // [START spanner_postgresql_ddl_batch] + static void ddlBatchPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + try (Statement statement = connection.createStatement()) { + // Create two new tables in one batch. + statement.addBatch( + "CREATE TABLE venues (" + + " venue_id bigint not null primary key," + + " name varchar(1024)," + + " description jsonb" + + ")"); + statement.addBatch( + "CREATE TABLE concerts (" + + " concert_id bigint not null primary key ," + + " venue_id bigint not null," + + " singer_id bigint not null," + + " start_time timestamptz," + + " end_time timestamptz," + + " constraint fk_concerts_venues foreign key" + + " (venue_id) references venues (venue_id)," + + " constraint fk_concerts_singers foreign key" + + " (singer_id) references singers (singer_id)" + + ")"); + statement.executeBatch(); + } + System.out.println("Added venues and concerts tables"); + } + } + // [END spanner_postgresql_ddl_batch] + + // [START spanner_update_data] + static void updateDataWithMutations( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Unwrap the CloudSpannerJdbcConnection interface + // from the java.sql.Connection. + CloudSpannerJdbcConnection cloudSpannerJdbcConnection = + connection.unwrap(CloudSpannerJdbcConnection.class); + + final long marketingBudgetAlbum1 = 100000L; + final long marketingBudgetAlbum2 = 500000L; + // Mutation can be used to update/insert/delete a single row in a table. + // Here we use newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(1) + .set("AlbumId") + .to(1) + .set("MarketingBudget") + .to(marketingBudgetAlbum1) + .build(), + Mutation.newUpdateBuilder("Albums") + .set("SingerId") + .to(2) + .set("AlbumId") + .to(2) + .set("MarketingBudget") + .to(marketingBudgetAlbum2) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + cloudSpannerJdbcConnection.write(mutations); + System.out.println("Updated albums"); + } + } + // [END spanner_update_data] + + // [START spanner_postgresql_update_data] + static void updateDataWithMutationsPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Unwrap the CloudSpannerJdbcConnection interface + // from the java.sql.Connection. + CloudSpannerJdbcConnection cloudSpannerJdbcConnection = + connection.unwrap(CloudSpannerJdbcConnection.class); + + final long marketingBudgetAlbum1 = 100000L; + final long marketingBudgetAlbum2 = 500000L; + // Mutation can be used to update/insert/delete a single row in a table. + // Here we use newUpdateBuilder to create update mutations. + List mutations = + Arrays.asList( + Mutation.newUpdateBuilder("albums") + .set("singer_id") + .to(1) + .set("album_id") + .to(1) + .set("marketing_budget") + .to(marketingBudgetAlbum1) + .build(), + Mutation.newUpdateBuilder("albums") + .set("singer_id") + .to(2) + .set("album_id") + .to(2) + .set("marketing_budget") + .to(marketingBudgetAlbum2) + .build()); + // This writes all the mutations to Cloud Spanner atomically. + cloudSpannerJdbcConnection.write(mutations); + System.out.println("Updated albums"); + } + } + // [END spanner_postgresql_update_data] + + // [START spanner_query_data_with_new_column] + static void queryDataWithNewColumn( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Rows without an explicit value for MarketingBudget will have a + // MarketingBudget equal to null. + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT SingerId, AlbumId, MarketingBudget " + + "FROM Albums")) { + while (resultSet.next()) { + // Use the ResultSet#getObject(String) method to get data + // of any type from the ResultSet. + System.out.printf( + "%s %s %s\n", + resultSet.getObject("SingerId"), + resultSet.getObject("AlbumId"), + resultSet.getObject("MarketingBudget")); + } + } + } + } + // [END spanner_query_data_with_new_column] + + // [START spanner_postgresql_query_data_with_new_column] + static void queryDataWithNewColumnPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Rows without an explicit value for marketing_budget will have a + // marketing_budget equal to null. + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "select singer_id, album_id, marketing_budget " + + "from albums")) { + while (resultSet.next()) { + // Use the ResultSet#getObject(String) method to get data + // of any type from the ResultSet. + System.out.printf( + "%s %s %s\n", + resultSet.getObject("singer_id"), + resultSet.getObject("album_id"), + resultSet.getObject("marketing_budget")); + } + } + } + } + // [END spanner_postgresql_query_data_with_new_column] + + // [START spanner_dml_getting_started_update] + static void writeWithTransactionUsingDml( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + + // Transfer marketing budget from one album to another. + // We do it in a transaction to ensure that the transfer is atomic. + // There is no need to explicitly start the transaction. The first + // statement on the connection will start a transaction when + // AutoCommit=false. + String selectMarketingBudgetSql = + "SELECT MarketingBudget " + + "FROM Albums " + + "WHERE SingerId = ? AND AlbumId = ?"; + long album2Budget = 0; + try (PreparedStatement selectMarketingBudgetStatement = + connection.prepareStatement(selectMarketingBudgetSql)) { + // Bind the query parameters to SingerId=2 and AlbumId=2. + selectMarketingBudgetStatement.setLong(1, 2); + selectMarketingBudgetStatement.setLong(2, 2); + try (ResultSet resultSet = + selectMarketingBudgetStatement.executeQuery()) { + while (resultSet.next()) { + album2Budget = resultSet.getLong("MarketingBudget"); + } + } + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + final long transfer = 200000; + if (album2Budget >= transfer) { + long album1Budget = 0; + // Re-use the existing PreparedStatement for selecting the + // MarketingBudget to get the budget for Album 1. + // Bind the query parameters to SingerId=1 and AlbumId=1. + selectMarketingBudgetStatement.setLong(1, 1); + selectMarketingBudgetStatement.setLong(2, 1); + try (ResultSet resultSet = + selectMarketingBudgetStatement.executeQuery()) { + while (resultSet.next()) { + album1Budget = resultSet.getLong("MarketingBudget"); + } + } + + // Transfer part of the marketing budget of Album 2 to Album 1. + album1Budget += transfer; + album2Budget -= transfer; + String updateSql = + "UPDATE Albums " + + "SET MarketingBudget = ? " + + "WHERE SingerId = ? and AlbumId = ?"; + try (PreparedStatement updateStatement = + connection.prepareStatement(updateSql)) { + // Update Album 1. + int paramIndex = 0; + updateStatement.setLong(++paramIndex, album1Budget); + updateStatement.setLong(++paramIndex, 1); + updateStatement.setLong(++paramIndex, 1); + // Create a DML batch by calling addBatch on + // the current PreparedStatement. + updateStatement.addBatch(); + + // Update Album 2 in the same DML batch. + paramIndex = 0; + updateStatement.setLong(++paramIndex, album2Budget); + updateStatement.setLong(++paramIndex, 2); + updateStatement.setLong(++paramIndex, 2); + updateStatement.addBatch(); + + // Execute both DML statements in one batch. + updateStatement.executeBatch(); + } + } + } + // Commit the current transaction. + connection.commit(); + System.out.println( + "Transferred marketing budget from Album 2 to Album 1"); + } + } + // [END spanner_dml_getting_started_update] + + // [START spanner_postgresql_dml_getting_started_update] + static void writeWithTransactionUsingDmlPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + + // Transfer marketing budget from one album to another. We do it in a + // transaction to ensure that the transfer is atomic. There is no need + // to explicitly start the transaction. The first statement on the + // connection will start a transaction when AutoCommit=false. + String selectMarketingBudgetSql = + "SELECT marketing_budget " + + "from albums " + + "WHERE singer_id = ? and album_id = ?"; + long album2Budget = 0; + try (PreparedStatement selectMarketingBudgetStatement = + connection.prepareStatement(selectMarketingBudgetSql)) { + // Bind the query parameters to SingerId=2 and AlbumId=2. + selectMarketingBudgetStatement.setLong(1, 2); + selectMarketingBudgetStatement.setLong(2, 2); + try (ResultSet resultSet = + selectMarketingBudgetStatement.executeQuery()) { + while (resultSet.next()) { + album2Budget = resultSet.getLong("marketing_budget"); + } + } + // The transaction will only be committed if this condition still holds + // at the time of commit. Otherwise, the transaction will be aborted. + final long transfer = 200000; + if (album2Budget >= transfer) { + long album1Budget = 0; + // Re-use the existing PreparedStatement for selecting the + // marketing_budget to get the budget for Album 1. + // Bind the query parameters to SingerId=1 and AlbumId=1. + selectMarketingBudgetStatement.setLong(1, 1); + selectMarketingBudgetStatement.setLong(2, 1); + try (ResultSet resultSet = + selectMarketingBudgetStatement.executeQuery()) { + while (resultSet.next()) { + album1Budget = resultSet.getLong("marketing_budget"); + } + } + + // Transfer part of the marketing budget of Album 2 to Album 1. + album1Budget += transfer; + album2Budget -= transfer; + String updateSql = + "UPDATE albums " + + "SET marketing_budget = ? " + + "WHERE singer_id = ? and album_id = ?"; + try (PreparedStatement updateStatement = + connection.prepareStatement(updateSql)) { + // Update Album 1. + int paramIndex = 0; + updateStatement.setLong(++paramIndex, album1Budget); + updateStatement.setLong(++paramIndex, 1); + updateStatement.setLong(++paramIndex, 1); + // Create a DML batch by calling addBatch + // on the current PreparedStatement. + updateStatement.addBatch(); + + // Update Album 2 in the same DML batch. + paramIndex = 0; + updateStatement.setLong(++paramIndex, album2Budget); + updateStatement.setLong(++paramIndex, 2); + updateStatement.setLong(++paramIndex, 2); + updateStatement.addBatch(); + + // Execute both DML statements in one batch. + updateStatement.executeBatch(); + } + } + } + // Commit the current transaction. + connection.commit(); + System.out.println( + "Transferred marketing budget from Album 2 to Album 1"); + } + } + // [END spanner_postgresql_dml_getting_started_update] + + // [START spanner_transaction_and_statement_tag] + static void tags( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + // Set the TRANSACTION_TAG session variable to set a transaction tag + // for the current transaction. + connection + .createStatement() + .execute("SET TRANSACTION_TAG='example-tx-tag'"); + + // Set the STATEMENT_TAG session variable to set the request tag + // that should be included with the next SQL statement. + connection + .createStatement() + .execute("SET STATEMENT_TAG='query-marketing-budget'"); + long marketingBudget = 0L; + long singerId = 1L; + long albumId = 1L; + try (PreparedStatement statement = connection.prepareStatement( + "SELECT MarketingBudget " + + "FROM Albums " + + "WHERE SingerId=? AND AlbumId=?")) { + statement.setLong(1, singerId); + statement.setLong(2, albumId); + try (ResultSet albumResultSet = statement.executeQuery()) { + while (albumResultSet.next()) { + marketingBudget = albumResultSet.getLong(1); + } + } + } + // Reduce the marketing budget by 10% if it is more than 1,000. + final long maxMarketingBudget = 1000L; + final float reduction = 0.1f; + if (marketingBudget > maxMarketingBudget) { + marketingBudget -= (long) (marketingBudget * reduction); + connection + .createStatement() + .execute("SET STATEMENT_TAG='reduce-marketing-budget'"); + try (PreparedStatement statement = connection.prepareStatement( + "UPDATE Albums SET MarketingBudget=? " + + "WHERE SingerId=? AND AlbumId=?")) { + int paramIndex = 0; + statement.setLong(++paramIndex, marketingBudget); + statement.setLong(++paramIndex, singerId); + statement.setLong(++paramIndex, albumId); + statement.executeUpdate(); + } + } + + // Commit the current transaction. + connection.commit(); + System.out.println("Reduced marketing budget"); + } + } + // [END spanner_transaction_and_statement_tag] + + // [START spanner_postgresql_transaction_and_statement_tag] + static void tagsPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + // Set the TRANSACTION_TAG session variable to set a transaction tag + // for the current transaction. + connection + .createStatement() + .execute("set spanner.transaction_TAG='example-tx-tag'"); + + // Set the STATEMENT_TAG session variable to set the request tag + // that should be included with the next SQL statement. + connection + .createStatement() + .execute("set spanner.statement_tag='query-marketing-budget'"); + long marketingBudget = 0L; + long singerId = 1L; + long albumId = 1L; + try (PreparedStatement statement = connection.prepareStatement( + "select marketing_budget " + + "from albums " + + "where singer_id=? and album_id=?")) { + statement.setLong(1, singerId); + statement.setLong(2, albumId); + try (ResultSet albumResultSet = statement.executeQuery()) { + while (albumResultSet.next()) { + marketingBudget = albumResultSet.getLong(1); + } + } + } + // Reduce the marketing budget by 10% if it is more than 1,000. + final long maxMarketingBudget = 1000L; + final float reduction = 0.1f; + if (marketingBudget > maxMarketingBudget) { + marketingBudget -= (long) (marketingBudget * reduction); + connection + .createStatement() + .execute("set spanner.statement_tag='reduce-marketing-budget'"); + try (PreparedStatement statement = connection.prepareStatement( + "update albums set marketing_budget=? " + + "where singer_id=? AND album_id=?")) { + int paramIndex = 0; + statement.setLong(++paramIndex, marketingBudget); + statement.setLong(++paramIndex, singerId); + statement.setLong(++paramIndex, albumId); + statement.executeUpdate(); + } + } + + // Commit the current transaction. + connection.commit(); + System.out.println("Reduced marketing budget"); + } + } + // [END spanner_postgresql_transaction_and_statement_tag] + + // [START spanner_read_only_transaction] + static void readOnlyTransaction( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + // This SQL statement instructs the JDBC driver to use + // a read-only transaction. + connection.createStatement().execute("SET TRANSACTION READ ONLY"); + + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums " + + "ORDER BY SingerId, AlbumId")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + resultSet.getString("AlbumTitle")); + } + } + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT SingerId, AlbumId, AlbumTitle " + + "FROM Albums " + + "ORDER BY AlbumTitle")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("SingerId"), + resultSet.getLong("AlbumId"), + resultSet.getString("AlbumTitle")); + } + } + // End the read-only transaction by calling commit(). + connection.commit(); + } + } + // [END spanner_read_only_transaction] + + // [START spanner_postgresql_read_only_transaction] + static void readOnlyTransactionPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Set AutoCommit=false to enable transactions. + connection.setAutoCommit(false); + // This SQL statement instructs the JDBC driver to use + // a read-only transaction. + connection.createStatement().execute("set transaction read only"); + + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT singer_id, album_id, album_title " + + "FROM albums " + + "ORDER BY singer_id, album_id")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("singer_id"), + resultSet.getLong("album_id"), + resultSet.getString("album_title")); + } + } + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "SELECT singer_id, album_id, album_title " + + "FROM albums " + + "ORDER BY album_title")) { + while (resultSet.next()) { + System.out.printf( + "%d %d %s\n", + resultSet.getLong("singer_id"), + resultSet.getLong("album_id"), + resultSet.getString("album_title")); + } + } + // End the read-only transaction by calling commit(). + connection.commit(); + } + } + // [END spanner_postgresql_read_only_transaction] + + // [START spanner_data_boost] + static void dataBoost( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // This enables Data Boost for all partitioned queries on this connection. + connection.createStatement().execute("SET DATA_BOOST_ENABLED=TRUE"); + + // Run a partitioned query. This query will use Data Boost. + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "RUN PARTITIONED QUERY " + + "SELECT SingerId, FirstName, LastName " + + "FROM Singers")) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("SingerId"), + resultSet.getString("FirstName"), + resultSet.getString("LastName")); + } + } + } + } + // [END spanner_data_boost] + + // [START spanner_postgresql_data_boost] + static void dataBoostPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // This enables Data Boost for all partitioned queries on this connection. + connection + .createStatement() + .execute("set spanner.data_boost_enabled=true"); + + // Run a partitioned query. This query will use Data Boost. + try (ResultSet resultSet = + connection + .createStatement() + .executeQuery( + "run partitioned query " + + "select singer_id, first_name, last_name " + + "from singers")) { + while (resultSet.next()) { + System.out.printf( + "%d %s %s\n", + resultSet.getLong("singer_id"), + resultSet.getString("first_name"), + resultSet.getString("last_name")); + } + } + } + } + // [END spanner_postgresql_data_boost] + + // [START spanner_partitioned_dml] + static void partitionedDml( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Enable Partitioned DML on this connection. + connection + .createStatement() + .execute("SET AUTOCOMMIT_DML_MODE='PARTITIONED_NON_ATOMIC'"); + // Back-fill a default value for the MarketingBudget column. + long lowerBoundUpdateCount = + connection + .createStatement() + .executeUpdate("UPDATE Albums " + + "SET MarketingBudget=0 " + + "WHERE MarketingBudget IS NULL"); + System.out.printf("Updated at least %d albums\n", lowerBoundUpdateCount); + } + } + // [END spanner_partitioned_dml] + + // [START spanner_postgresql_partitioned_dml] + static void partitionedDmlPostgreSQL( + final String project, + final String instance, + final String database, + final Properties properties) throws SQLException { + try (Connection connection = + DriverManager.getConnection( + String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database), + properties)) { + // Enable Partitioned DML on this connection. + connection + .createStatement() + .execute("set spanner.autocommit_dml_mode='partitioned_non_atomic'"); + // Back-fill a default value for the MarketingBudget column. + long lowerBoundUpdateCount = + connection + .createStatement() + .executeUpdate("update albums " + + "set marketing_budget=0 " + + "where marketing_budget is null"); + System.out.printf("Updated at least %d albums\n", lowerBoundUpdateCount); + } + } + // [END spanner_postgresql_partitioned_dml] + + /** The expected number of command line arguments. */ + private static final int NUM_EXPECTED_ARGS = 3; + + /** + * Main method for running a sample. + * + * @param args the command line arguments + */ + public static void main(final String[] args) throws Exception { + if (args.length != NUM_EXPECTED_ARGS) { + printUsageAndExit(); + } + try (DatabaseAdminClient dbAdminClient = createDatabaseAdminClient()) { + final String command = args[0]; + DatabaseId databaseId = DatabaseId.of( + SpannerOptions.getDefaultInstance().getProjectId(), + args[1], + args[2]); + + run(dbAdminClient, command, databaseId); + } + System.out.println(); + System.out.println("Finished running sample"); + } + + static DatabaseAdminClient createDatabaseAdminClient() throws Exception { + String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST"); + if (!Strings.isNullOrEmpty(emulatorHost)) { + return DatabaseAdminClient.create( + DatabaseAdminSettings.newBuilder() + .setTransportChannelProvider( + InstantiatingGrpcChannelProvider.newBuilder() + .setEndpoint(emulatorHost) + .setChannelConfigurator( + ManagedChannelBuilder::usePlaintext) + .build()) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build()); + } + return DatabaseAdminClient.create(); + } + + static Properties createProperties() { + Properties properties = new Properties(); + String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST"); + if (!Strings.isNullOrEmpty(emulatorHost)) { + properties.put("autoConfigEmulator", "true"); + properties.put("endpoint", emulatorHost); + } + return properties; + } + + static void run( + final DatabaseAdminClient dbAdminClient, + final String command, + final DatabaseId database) throws Exception { + if ( + !runGoogleSQLSample(dbAdminClient, command, database) + && !runPostgreSQLSample(dbAdminClient, command, database)) { + System.err.println(); + System.err.println("Unknown command: " + command); + System.err.println(); + printUsageAndExit(); + } + } + + static boolean runGoogleSQLSample( + final DatabaseAdminClient dbAdminClient, + final String command, + final DatabaseId database) throws Exception { + switch (command) { + case "createdatabase": + createDatabase( + dbAdminClient, + InstanceName.of( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance()), + database.getDatabase(), + createProperties()); + return true; + case "writeusingdml": + writeDataWithDml( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "writeusingdmlbatch": + writeDataWithDmlBatch( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "write": + writeDataWithMutations( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "query": + queryData( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "querywithparameter": + queryWithParameter( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "addmarketingbudget": + addColumn( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "ddlbatch": + ddlBatch( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "update": + updateDataWithMutations( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "querymarketingbudget": + queryDataWithNewColumn( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "writewithtransactionusingdml": + writeWithTransactionUsingDml( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "tags": + tags( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "readonlytransaction": + readOnlyTransaction( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "databoost": + dataBoost( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "pdml": + partitionedDml( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + default: + return false; + } + } + + static boolean runPostgreSQLSample( + final DatabaseAdminClient dbAdminClient, + final String command, + final DatabaseId database) throws Exception { + switch (command) { + case "createpgdatabase": + createPostgreSQLDatabase( + dbAdminClient, + InstanceName.of( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance()), + database.getDatabase(), + createProperties()); + return true; + case "writeusingdmlpg": + writeDataWithDmlPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "writeusingdmlbatchpg": + writeDataWithDmlBatchPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "writepg": + writeDataWithMutationsPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "querypg": + queryDataPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "querywithparameterpg": + queryWithParameterPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "addmarketingbudgetpg": + addColumnPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "ddlbatchpg": + ddlBatchPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "updatepg": + updateDataWithMutationsPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "querymarketingbudgetpg": + queryDataWithNewColumnPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "writewithtransactionusingdmlpg": + writeWithTransactionUsingDmlPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "tagspg": + tagsPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "readonlytransactionpg": + readOnlyTransactionPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "databoostpg": + dataBoostPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + case "pdmlpg": + partitionedDmlPostgreSQL( + database.getInstanceId().getProject(), + database.getInstanceId().getInstance(), + database.getDatabase(), + createProperties()); + return true; + default: + return false; + } + } + + static void printUsageAndExit() { + System.err.println("Usage:"); + System.err.println(" JdbcSample "); + System.err.println(); + System.err.println("Examples:"); + System.err.println(" JdbcSample createdatabase my-instance example-db"); + System.exit(1); + } +} diff --git a/samples/snippets/src/main/java/com/example/spanner/jdbc/package-info.java b/samples/snippets/src/main/java/com/example/spanner/jdbc/package-info.java new file mode 100644 index 000000000..bdaf4070c --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/jdbc/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Sample package for the Spanner JDBC driver. */ +package com.example.spanner.jdbc; diff --git a/samples/snippets/src/test/java/com/example/spanner/jdbc/JdbcSampleTest.java b/samples/snippets/src/test/java/com/example/spanner/jdbc/JdbcSampleTest.java new file mode 100644 index 000000000..9365ad9bf --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/jdbc/JdbcSampleTest.java @@ -0,0 +1,396 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.spanner.jdbc; + +import static com.example.spanner.jdbc.JdbcSample.addColumn; +import static com.example.spanner.jdbc.JdbcSample.addColumnPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.createConnection; +import static com.example.spanner.jdbc.JdbcSample.createConnectionWithEmulator; +import static com.example.spanner.jdbc.JdbcSample.createDatabase; +import static com.example.spanner.jdbc.JdbcSample.createPostgreSQLDatabase; +import static com.example.spanner.jdbc.JdbcSample.dataBoost; +import static com.example.spanner.jdbc.JdbcSample.dataBoostPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.ddlBatch; +import static com.example.spanner.jdbc.JdbcSample.ddlBatchPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.partitionedDml; +import static com.example.spanner.jdbc.JdbcSample.partitionedDmlPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.queryData; +import static com.example.spanner.jdbc.JdbcSample.queryDataPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.queryDataWithNewColumn; +import static com.example.spanner.jdbc.JdbcSample.queryDataWithNewColumnPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.queryWithParameter; +import static com.example.spanner.jdbc.JdbcSample.queryWithParameterPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.readOnlyTransaction; +import static com.example.spanner.jdbc.JdbcSample.readOnlyTransactionPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.tags; +import static com.example.spanner.jdbc.JdbcSample.tagsPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.updateDataWithMutations; +import static com.example.spanner.jdbc.JdbcSample.updateDataWithMutationsPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithDml; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithDmlBatch; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithDmlBatchPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithDmlPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithMutations; +import static com.example.spanner.jdbc.JdbcSample.writeDataWithMutationsPostgreSQL; +import static com.example.spanner.jdbc.JdbcSample.writeWithTransactionUsingDml; +import static com.example.spanner.jdbc.JdbcSample.writeWithTransactionUsingDmlPostgreSQL; +import static org.junit.Assert.assertEquals; + +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient; +import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminClient; +import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings; +import com.google.cloud.spanner.connection.SpannerPool; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.instance.v1.Instance; +import com.google.spanner.admin.instance.v1.InstanceConfig; +import com.google.spanner.admin.instance.v1.InstanceName; +import com.google.spanner.admin.instance.v1.ProjectName; +import com.google.spanner.v1.DatabaseName; +import io.grpc.ManagedChannelBuilder; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Properties; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +@RunWith(JUnit4.class) +public class JdbcSampleTest { + private static final String PROJECT_ID = "emulator-project"; + private static final String INSTANCE_ID = "test-instance"; + private static final String DATABASE_ID = "test-database"; + private static final String PG_DATABASE_ID = "pg-test-database"; + + private static final ProjectName PROJECT_NAME = ProjectName.of(PROJECT_ID); + + private static final InstanceName INSTANCE_NAME = InstanceName.of(PROJECT_ID, INSTANCE_ID); + + private static GenericContainer emulator; + + private static Properties properties; + + @BeforeClass + public static void setup() throws Exception { + emulator = + new GenericContainer<>( + DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:latest")) + // .withExposedPorts(9010) + .waitingFor(Wait.forListeningPort()); + // TODO: Remove and replace with dynamic port binding when Spanner client library 6.64.0 has + // been released. + emulator.setPortBindings(ImmutableList.of("9010:9010")); + emulator.start(); + try (InstanceAdminClient client = + InstanceAdminClient.create( + InstanceAdminSettings.newBuilder() + .setTransportChannelProvider( + InstantiatingGrpcChannelProvider.newBuilder() + .setEndpoint(emulator.getHost() + ":" + emulator.getMappedPort(9010)) + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build())) { + InstanceConfig config = + client.listInstanceConfigs(PROJECT_NAME).iterateAll().iterator().next(); + client + .createInstanceAsync( + PROJECT_NAME, + INSTANCE_ID, + Instance.newBuilder() + .setConfig(config.getName()) + .setDisplayName("Test Instance") + .setNodeCount(1) + .build()) + .get(); + } + // Create properties for the JDBC driver to connect to the emulator. + properties = new Properties(); + properties.put("autoConfigEmulator", "true"); + properties.put("lenient", "true"); + properties.put("endpoint", emulator.getHost() + ":" + emulator.getMappedPort(9010)); + } + + @AfterClass + public static void cleanup() { + SpannerPool.closeSpannerPool(); + emulator.stop(); + } + + DatabaseAdminClient createDatabaseAdminClient() throws Exception { + return DatabaseAdminClient.create( + DatabaseAdminSettings.newBuilder() + .setTransportChannelProvider( + InstantiatingGrpcChannelProvider.newBuilder() + .setEndpoint(emulator.getHost() + ":" + emulator.getMappedPort(9010)) + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build()); + } + + @Test + public void testGoogleSQLSamples() throws Exception { + String result; + try (DatabaseAdminClient client = createDatabaseAdminClient()) { + result = runSample(() -> createDatabase(client, INSTANCE_NAME, DATABASE_ID, properties)); + } + assertEquals( + "Created database [" + DatabaseName.of(PROJECT_ID, INSTANCE_ID, DATABASE_ID) + "]\n", + result); + + result = runSample(() -> createConnection(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Hello World!\n", result); + + result = runSample(() -> createConnectionWithEmulator(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Hello World!\n", result); + + result = runSample(() -> writeDataWithDml(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("4 records inserted.\n", result); + + result = + runSample(() -> writeDataWithDmlBatch(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("3 records inserted.\n", result); + + result = + runSample(() -> writeDataWithMutations(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Inserted 10 rows.\n", result); + + result = runSample(() -> queryData(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals( + "1 2 Go, Go, Go\n" + + "2 2 Forever Hold Your Peace\n" + + "1 1 Total Junk\n" + + "2 1 Green\n" + + "2 3 Terrified\n", + result); + + result = runSample(() -> queryWithParameter(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("12 Melissa Garcia\n", result); + + result = runSample(() -> addColumn(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Added MarketingBudget column\n", result); + + result = runSample(() -> ddlBatch(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Added Venues and Concerts tables\n", result); + + result = + runSample(() -> updateDataWithMutations(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Updated albums\n", result); + + result = + runSample(() -> queryDataWithNewColumn(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals( + "1 2 null\n" + "2 2 500000\n" + "1 1 100000\n" + "2 1 null\n" + "2 3 null\n", result); + + result = + runSample( + () -> writeWithTransactionUsingDml(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Transferred marketing budget from Album 2 to Album 1\n", result); + + result = + runSample( + () -> tags(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Reduced marketing budget\n", result); + + result = runSample(() -> readOnlyTransaction(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals( + "1 1 Total Junk\n" + + "1 2 Go, Go, Go\n" + + "2 1 Green\n" + + "2 2 Forever Hold Your Peace\n" + + "2 3 Terrified\n" + + "2 2 Forever Hold Your Peace\n" + + "1 2 Go, Go, Go\n" + + "2 1 Green\n" + + "2 3 Terrified\n" + + "1 1 Total Junk\n", + result); + + result = runSample(() -> dataBoost(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals( + "2 Catalina Smith\n" + + "4 Lea Martin\n" + + "12 Melissa Garcia\n" + + "14 Jacqueline Long\n" + + "16 Sarah Wilson\n" + + "18 Maya Patel\n" + + "1 Marc Richards\n" + + "3 Alice Trentor\n" + + "5 David Lomond\n" + + "13 Russel Morales\n" + + "15 Dylan Shaw\n" + + "17 Ethan Miller\n", + result); + + result = runSample(() -> partitionedDml(PROJECT_ID, INSTANCE_ID, DATABASE_ID, properties)); + assertEquals("Updated at least 3 albums\n", result); + } + + @Test + public void testPostgreSQLSamples() throws Exception { + String result; + try (DatabaseAdminClient client = createDatabaseAdminClient()) { + result = + runSample( + () -> createPostgreSQLDatabase(client, INSTANCE_NAME, PG_DATABASE_ID, properties)); + } + assertEquals( + "Created database [" + DatabaseName.of(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID) + "]\n", + result); + + result = runSample(() -> createConnection(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Hello World!\n", result); + + result = runSample(() -> createConnectionWithEmulator(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Hello World!\n", result); + + result = + runSample( + () -> writeDataWithDmlPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("4 records inserted.\n", result); + + result = + runSample( + () -> + writeDataWithDmlBatchPostgreSQL( + PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("3 records inserted.\n", result); + + result = + runSample( + () -> + writeDataWithMutationsPostgreSQL( + PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Inserted 10 rows.\n", result); + + result = + runSample(() -> queryDataPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals( + "1 2 Go, Go, Go\n" + + "2 2 Forever Hold Your Peace\n" + + "1 1 Total Junk\n" + + "2 1 Green\n" + + "2 3 Terrified\n", + result); + + result = + runSample( + () -> + queryWithParameterPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("12 Melissa Garcia\n", result); + + result = + runSample(() -> addColumnPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Added marketing_budget column\n", result); + + result = + runSample(() -> ddlBatchPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Added venues and concerts tables\n", result); + + result = + runSample( + () -> + updateDataWithMutationsPostgreSQL( + PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Updated albums\n", result); + + result = + runSample( + () -> + queryDataWithNewColumnPostgreSQL( + PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals( + "1 2 null\n" + "2 2 500000\n" + "1 1 100000\n" + "2 1 null\n" + "2 3 null\n", result); + + result = + runSample( + () -> + writeWithTransactionUsingDmlPostgreSQL( + PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Transferred marketing budget from Album 2 to Album 1\n", result); + + result = + runSample( + () -> + tagsPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Reduced marketing budget\n", result); + + result = + runSample( + () -> + readOnlyTransactionPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals( + "1 1 Total Junk\n" + + "1 2 Go, Go, Go\n" + + "2 1 Green\n" + + "2 2 Forever Hold Your Peace\n" + + "2 3 Terrified\n" + + "2 2 Forever Hold Your Peace\n" + + "1 2 Go, Go, Go\n" + + "2 1 Green\n" + + "2 3 Terrified\n" + + "1 1 Total Junk\n", + result); + + result = + runSample(() -> dataBoostPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals( + "2 Catalina Smith\n" + + "4 Lea Martin\n" + + "12 Melissa Garcia\n" + + "14 Jacqueline Long\n" + + "16 Sarah Wilson\n" + + "18 Maya Patel\n" + + "1 Marc Richards\n" + + "3 Alice Trentor\n" + + "5 David Lomond\n" + + "13 Russel Morales\n" + + "15 Dylan Shaw\n" + + "17 Ethan Miller\n", + result); + + result = + runSample( + () -> partitionedDmlPostgreSQL(PROJECT_ID, INSTANCE_ID, PG_DATABASE_ID, properties)); + assertEquals("Updated at least 3 albums\n", result); + } + + interface Sample { + void run() throws Exception; + } + + String runSample(Sample sample) throws Exception { + PrintStream stdOut = System.out; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bout); + System.setOut(out); + try { + sample.run(); + } finally { + System.setOut(stdOut); + } + return bout.toString(); + } +} From 2860069896e47810d815647b3a7abc19800ef4dd Mon Sep 17 00:00:00 2001 From: Hengfeng Li Date: Wed, 10 Apr 2024 23:20:08 +1000 Subject: [PATCH 16/29] chore: add OpenTelemetry metric for client lib latencies (#1559) --- pom.xml | 11 ++++ .../spanner/jdbc/AbstractJdbcStatement.java | 5 ++ .../cloud/spanner/jdbc/JdbcConnection.java | 31 +++++++++++ .../google/cloud/spanner/jdbc/Metrics.java | 53 +++++++++++++++++++ .../JdbcConnectionGeneratedSqlScriptTest.java | 2 + .../spanner/jdbc/JdbcConnectionTest.java | 7 ++- 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/google/cloud/spanner/jdbc/Metrics.java diff --git a/pom.xml b/pom.xml index 38d49832b..2e9478636 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,13 @@ pom import + + io.opentelemetry + opentelemetry-bom + 1.36.0 + pom + import + com.google.cloud google-cloud-shared-dependencies @@ -152,6 +159,10 @@ com.google.api.grpc proto-google-cloud-spanner-v1 + + io.opentelemetry + opentelemetry-api + diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java index 97131724d..61c272fb7 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java @@ -24,11 +24,13 @@ import com.google.cloud.spanner.connection.Connection; import com.google.cloud.spanner.connection.StatementResult; import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType; +import com.google.common.base.Stopwatch; import com.google.rpc.Code; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; +import java.time.Duration; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -227,7 +229,10 @@ private T doWithStatementTimeout( StatementTimeout originalTimeout = setTemporaryStatementTimeout(); T result = null; try { + Stopwatch stopwatch = Stopwatch.createStarted(); result = runnable.get(); + Duration executionDuration = stopwatch.elapsed(); + connection.recordClientLibLatencyMetric(executionDuration.toMillis()); return result; } catch (SpannerException spannerException) { throw JdbcSqlExceptionFactory.of(spannerException); diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java index 2c7d6cc00..bafdc6b09 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java @@ -21,17 +21,23 @@ import com.google.api.client.util.Preconditions; import com.google.cloud.spanner.CommitResponse; +import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.connection.AutocommitDmlMode; import com.google.cloud.spanner.connection.Connection; import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.cloud.spanner.connection.SavepointSupport; import com.google.cloud.spanner.connection.TransactionMode; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; @@ -80,9 +86,21 @@ class JdbcConnection extends AbstractJdbcConnection { private final boolean useLegacyIsValidCheck; + private final Metrics metrics; + + private final Attributes metricAttributes; + JdbcConnection(String connectionUrl, ConnectionOptions options) throws SQLException { super(connectionUrl, options); this.useLegacyIsValidCheck = useLegacyValidCheck(); + OpenTelemetry openTelemetry; + if (SpannerOptions.isEnabledOpenTelemetryMetrics()) { + openTelemetry = this.getSpanner().getOptions().getOpenTelemetry(); + } else { + openTelemetry = OpenTelemetry.noop(); + } + this.metrics = new Metrics(openTelemetry); + this.metricAttributes = createMetricAttributes(this.getConnectionOptions().getDatabaseId()); } static boolean useLegacyValidCheck() { @@ -96,6 +114,19 @@ static boolean useLegacyValidCheck() { return false; } + @VisibleForTesting + static Attributes createMetricAttributes(DatabaseId databaseId) { + AttributesBuilder attributesBuilder = Attributes.builder(); + attributesBuilder.put("database", databaseId.getDatabase()); + attributesBuilder.put("instance_id", databaseId.getInstanceId().getInstance()); + attributesBuilder.put("project_id", databaseId.getInstanceId().getProject()); + return attributesBuilder.build(); + } + + public void recordClientLibLatencyMetric(long value) { + metrics.recordClientLibLatency(value, metricAttributes); + } + @Override public Statement createStatement() throws SQLException { checkClosed(); diff --git a/src/main/java/com/google/cloud/spanner/jdbc/Metrics.java b/src/main/java/com/google/cloud/spanner/jdbc/Metrics.java new file mode 100644 index 000000000..77f238466 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/jdbc/Metrics.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.spanner.jdbc; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import java.util.Arrays; +import java.util.List; + +class Metrics { + static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java"; + static final String SPANNER_CLIENT_LIB_LATENCY = "spanner/jdbc/client_lib_latencies"; + static final String SPANNER_CLIENT_LIB_LATENCY_DESCRIPTION = + "Latency when the client library receives a call and returns a response"; + + private final LongHistogram spannerClientLibLatencies; + + Metrics(OpenTelemetry openTelemetry) { + Meter meter = openTelemetry.getMeter(INSTRUMENTATION_SCOPE); + List RPC_MILLIS_BUCKET_BOUNDARIES = + Arrays.asList( + 1L, 2L, 3L, 4L, 5L, 6L, 8L, 10L, 13L, 16L, 20L, 25L, 30L, 40L, 50L, 65L, 80L, 100L, + 130L, 160L, 200L, 250L, 300L, 400L, 500L, 650L, 800L, 1000L, 2000L, 5000L, 10000L, + 20000L, 50000L, 100000L); + spannerClientLibLatencies = + meter + .histogramBuilder(SPANNER_CLIENT_LIB_LATENCY) + .ofLongs() + .setDescription(SPANNER_CLIENT_LIB_LATENCY_DESCRIPTION) + .setUnit("ms") + .setExplicitBucketBoundariesAdvice(RPC_MILLIS_BUCKET_BOUNDARIES) + .build(); + } + + void recordClientLibLatency(long value, Attributes attributes) { + spannerClientLibLatencies.record(value, attributes); + } +} diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java index 5844feefb..a5d350501 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.connection.AbstractConnectionImplTest; @@ -64,6 +65,7 @@ public GenericConnection getConnection() { ConnectionImplTest.createConnection(options, dialect); when(spannerConnection.getDialect()).thenReturn(dialect); when(options.getConnection()).thenReturn(spannerConnection); + when(options.getDatabaseId()).thenReturn(DatabaseId.of("project", "instance", "database")); try { JdbcConnection connection = new JdbcConnection( diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java index 8d26bb317..96ea74381 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java @@ -23,10 +23,12 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.ResultSets; @@ -80,13 +82,16 @@ private JdbcConnection createConnection(ConnectionOptions options) throws SQLExc ConnectionImplTest.createConnection(options, dialect); when(spannerConnection.getDialect()).thenReturn(dialect); when(options.getConnection()).thenReturn(spannerConnection); + when(options.getDatabaseId()).thenReturn(DatabaseId.of("project", "instance", "database")); return new JdbcConnection( "jdbc:cloudspanner://localhost/projects/project/instances/instance/databases/database;credentialsUrl=url", options); } private ConnectionOptions mockOptions() { - return mock(ConnectionOptions.class); + ConnectionOptions options = mock(ConnectionOptions.class); + when(options.getDatabaseId()).thenReturn(DatabaseId.of("project", "instance", "database")); + return options; } @Test From afcbe5ea5701a799729543c9564759570f05feb8 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 10 Apr 2024 15:44:18 +0200 Subject: [PATCH 17/29] deps: update dependency com.google.cloud:sdk-platform-java-config to v3.28.1 (#1560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.cloud:sdk-platform-java-config](https://togithub.com/googleapis/java-shared-config) | `3.27.0` -> `3.28.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.cloud:sdk-platform-java-config/3.28.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.cloud:sdk-platform-java-config/3.28.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.cloud:sdk-platform-java-config/3.27.0/3.28.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.cloud:sdk-platform-java-config/3.27.0/3.28.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ”• **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc). --- 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 047c0c761..5928ec6ab 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -9,7 +9,7 @@ com.google.cloud sdk-platform-java-config - 3.27.0 + 3.28.1 From dbbcca342a83476b1f942aab23f21469cf6c8304 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 12 Apr 2024 15:26:51 +0200 Subject: [PATCH 18/29] deps: update dependency org.springframework.data:spring-data-bom to v2023.1.5 (#1564) --- samples/spring-data-jdbc/pom.xml | 2 +- samples/spring-data-mybatis/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml index c9fbf898d..c1dc7cc00 100644 --- a/samples/spring-data-jdbc/pom.xml +++ b/samples/spring-data-jdbc/pom.xml @@ -23,7 +23,7 @@ org.springframework.data spring-data-bom - 2023.1.4 + 2023.1.5 import pom diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml index 16e8c3d37..ef9d1d89e 100644 --- a/samples/spring-data-mybatis/pom.xml +++ b/samples/spring-data-mybatis/pom.xml @@ -28,7 +28,7 @@ org.springframework.data spring-data-bom - 2023.1.4 + 2023.1.5 import pom From 01d4de1df21144e9c3bcf0b4e5192b12cd19dc82 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 12 Apr 2024 15:27:09 +0200 Subject: [PATCH 19/29] deps: update actions/setup-java action to v4 (#1563) --- .github/workflows/sample-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sample-tests.yml b/.github/workflows/sample-tests.yml index cec19c7a2..7e464ef17 100644 --- a/.github/workflows/sample-tests.yml +++ b/.github/workflows/sample-tests.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 From 6053d79816546130eca7a7016dc9299c079e411f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sun, 14 Apr 2024 15:28:01 +0200 Subject: [PATCH 20/29] deps: update actions/checkout action to v4 (#1561) --- .github/workflows/sample-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sample-tests.yml b/.github/workflows/sample-tests.yml index 7e464ef17..eae9314cf 100644 --- a/.github/workflows/sample-tests.yml +++ b/.github/workflows/sample-tests.yml @@ -20,7 +20,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin From a71c4f5b3171ab7ec65c4d5fd2de4a3bc0b2bb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 16 Apr 2024 08:07:25 +0200 Subject: [PATCH 21/29] chore: make connection property all lower-case (#1566) --- .../src/main/java/com/example/spanner/jdbc/JdbcSample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java b/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java index 2e46058de..b17c6b4d8 100644 --- a/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java +++ b/samples/snippets/src/main/java/com/example/spanner/jdbc/JdbcSample.java @@ -1235,7 +1235,7 @@ static void tagsPostgreSQL( // for the current transaction. connection .createStatement() - .execute("set spanner.transaction_TAG='example-tx-tag'"); + .execute("set spanner.transaction_tag='example-tx-tag'"); // Set the STATEMENT_TAG session variable to set the request tag // that should be included with the next SQL statement. From 2258ae3331a7e89036a202f243b9284108301fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 16 Apr 2024 10:44:09 +0200 Subject: [PATCH 22/29] fix: release ResultSet on Statement#close() (#1567) * fix: release ResultSet on Statement#close() Release the underlying resources of any ResultSet when Statement#close() is called. This is necessary in case someone executes a query in auto-commit mode using Statement#execute(String), and never reads and/or closes the result that was returned. * fix: close result set when Statement is closed --- .../spanner/jdbc/AbstractJdbcStatement.java | 2 +- .../cloud/spanner/jdbc/JdbcStatement.java | 19 +++- .../spanner/jdbc/StatementResourcesTest.java | 88 +++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/google/cloud/spanner/jdbc/StatementResourcesTest.java diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java index 61c272fb7..d12678bf2 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java @@ -359,7 +359,7 @@ public void cancel() throws SQLException { } @Override - public void close() { + public void close() throws SQLException { this.closed = true; } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java index 1db0c4070..19b325654 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java @@ -62,6 +62,12 @@ enum BatchType { super(connection); } + @Override + public void close() throws SQLException { + setCurrentResultSet(null); + super.close(); + } + @Override public ResultSet executeQuery(String sql) throws SQLException { checkClosed(); @@ -267,7 +273,7 @@ boolean executeStatement(Statement statement, ImmutableList generatedKey // keys. We can safely use '==', as the addReturningToStatement(..) method returns the same // instance if no generated keys were requested. if (statementWithReturning == statement) { - currentResultSet = JdbcResultSet.of(this, result.getResultSet()); + setCurrentResultSet(JdbcResultSet.of(this, result.getResultSet())); currentUpdateCount = JdbcConstants.STATEMENT_RESULT_SET; return true; } @@ -275,11 +281,11 @@ boolean executeStatement(Statement statement, ImmutableList generatedKey this.currentUpdateCount = extractUpdateCountAndClose(result.getResultSet()); return false; case UPDATE_COUNT: - currentResultSet = null; + setCurrentResultSet(null); currentUpdateCount = result.getUpdateCount(); return false; case NO_RESULT: - currentResultSet = null; + setCurrentResultSet(null); currentUpdateCount = JdbcConstants.STATEMENT_NO_RESULT; return false; default: @@ -294,6 +300,13 @@ public ResultSet getResultSet() throws SQLException { return currentResultSet; } + void setCurrentResultSet(ResultSet resultSet) throws SQLException { + if (this.currentResultSet != null) { + this.currentResultSet.close(); + } + this.currentResultSet = resultSet; + } + /** * Returns the update count of the last update statement. Will return {@link * JdbcConstants#STATEMENT_RESULT_SET} if the last statement returned a {@link ResultSet} and will diff --git a/src/test/java/com/google/cloud/spanner/jdbc/StatementResourcesTest.java b/src/test/java/com/google/cloud/spanner/jdbc/StatementResourcesTest.java new file mode 100644 index 000000000..ea1b313ac --- /dev/null +++ b/src/test/java/com/google/cloud/spanner/jdbc/StatementResourcesTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.jdbc; + +import com.google.cloud.spanner.connection.AbstractMockServerTest; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class StatementResourcesTest extends AbstractMockServerTest { + private static final int MIN_SESSIONS = 1; + private static final int MAX_SESSIONS = 2; + + private String createUrl() { + return String.format( + "jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s" + + "?usePlainText=true;minSessions=%d;maxSessions=%d", + getPort(), "proj", "inst", "db", MIN_SESSIONS, MAX_SESSIONS); + } + + @Test + public void testMultipleQueriesOnOneStatement() throws SQLException { + try (Connection connection = DriverManager.getConnection(createUrl())) { + try (Statement statement = connection.createStatement()) { + for (int i = 0; i < MAX_SESSIONS + 1; i++) { + // Execute a query without reading or closing the result. + statement.execute("SELECT 1"); + } + } + } + } + + @Test + public void testMultipleStatementsWithOneQuery() throws SQLException { + try (Connection connection = DriverManager.getConnection(createUrl())) { + for (int i = 0; i < MAX_SESSIONS + 1; i++) { + try (Statement statement = connection.createStatement()) { + // Execute a query without reading or closing the result. + statement.execute("SELECT 1"); + } + } + } + } + + @Test + public void testMultipleQueriesOnOnePreparedStatement() throws SQLException { + try (Connection connection = DriverManager.getConnection(createUrl())) { + try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { + for (int i = 0; i < MAX_SESSIONS + 1; i++) { + // Execute a query without reading or closing the result. + statement.execute(); + } + } + } + } + + @Test + public void testMultiplePreparedStatementsWithOneQuery() throws SQLException { + try (Connection connection = DriverManager.getConnection(createUrl())) { + for (int i = 0; i < MAX_SESSIONS + 1; i++) { + try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { + // Execute a query without reading or closing the result. + statement.execute(); + } + } + } + } +} From b57662fb65b74b329103ef63265192d7026b2c2d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 16 Apr 2024 13:04:10 +0200 Subject: [PATCH 23/29] deps: update dependency com.google.cloud:google-cloud-spanner-bom to v6.64.0 (#1565) 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-bom](https://togithub.com/googleapis/java-spanner) | `6.63.0` -> `6.64.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.cloud:google-cloud-spanner-bom/6.64.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.cloud:google-cloud-spanner-bom/6.64.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.cloud:google-cloud-spanner-bom/6.63.0/6.64.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.cloud:google-cloud-spanner-bom/6.63.0/6.64.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/java-spanner (com.google.cloud:google-cloud-spanner-bom) ### [`v6.64.0`](https://togithub.com/googleapis/java-spanner/blob/HEAD/CHANGELOG.md#6640-2024-04-12) [Compare Source](https://togithub.com/googleapis/java-spanner/compare/v6.63.0...v6.64.0) ##### Features - Add endpoint connection URL property ([#​2969](https://togithub.com/googleapis/java-spanner/issues/2969)) ([c9be29c](https://togithub.com/googleapis/java-spanner/commit/c9be29c717924d7f4c5acd8fe09ee371d0101642)) - Add PG OID support ([#​2736](https://togithub.com/googleapis/java-spanner/issues/2736)) ([ba2a4af](https://togithub.com/googleapis/java-spanner/commit/ba2a4afa5c1d64c932e9687d52b15c28d9dd7d91)) - Add SessionPoolOptions, SpannerOptions protos in executor protos ([#​2932](https://togithub.com/googleapis/java-spanner/issues/2932)) ([1673fd7](https://togithub.com/googleapis/java-spanner/commit/1673fd70df4ebfaa4b5fa07112d152119427699a)) - Support max_commit_delay in Connection API ([#​2954](https://togithub.com/googleapis/java-spanner/issues/2954)) ([a8f1852](https://togithub.com/googleapis/java-spanner/commit/a8f185261c812e7d6c92cb61ecc1f9c78ba3c4d9)) ##### Bug Fixes - Executor framework changes skipped in clirr checks, and added exception for partition methods in admin class ([#​3000](https://togithub.com/googleapis/java-spanner/issues/3000)) ([c2d8e95](https://togithub.com/googleapis/java-spanner/commit/c2d8e955abddb0117f1b3b94c2d9650d2cf4fdfd)) ##### Dependencies - Update actions/checkout action to v4 ([#​3006](https://togithub.com/googleapis/java-spanner/issues/3006)) ([368a9f3](https://togithub.com/googleapis/java-spanner/commit/368a9f33758961d8e3fd387ec94d380e7c6460cc)) - Update actions/github-script action to v7 ([#​3007](https://togithub.com/googleapis/java-spanner/issues/3007)) ([b0cfea6](https://togithub.com/googleapis/java-spanner/commit/b0cfea6e73b7293f564357e8d1c8c6bb2e0cf855)) - Update actions/setup-java action to v4 ([#​3008](https://togithub.com/googleapis/java-spanner/issues/3008)) ([d337080](https://togithub.com/googleapis/java-spanner/commit/d337080089dbd58cb4bf94f2cb5925f627435d39)) - Update dependency com.google.cloud:google-cloud-monitoring to v3.42.0 ([#​2997](https://togithub.com/googleapis/java-spanner/issues/2997)) ([0615beb](https://togithub.com/googleapis/java-spanner/commit/0615beb806ef62dbbfcc6bbffd082adc9c62372c)) - Update dependency com.google.cloud:google-cloud-trace to v2.41.0 ([#​2998](https://togithub.com/googleapis/java-spanner/issues/2998)) ([f50cd04](https://togithub.com/googleapis/java-spanner/commit/f50cd04660f480c62ddbd6c8a9e892cd95ec16b0)) - Update dependency commons-io:commons-io to v2.16.1 ([#​3020](https://togithub.com/googleapis/java-spanner/issues/3020)) ([aafd5b9](https://togithub.com/googleapis/java-spanner/commit/aafd5b9514c14a0dbfd0bf2616990f3c347ac0c6)) - Update opentelemetry.version to v1.37.0 ([#​3021](https://togithub.com/googleapis/java-spanner/issues/3021)) ([8f1ed2a](https://togithub.com/googleapis/java-spanner/commit/8f1ed2ac20896fb413749bb18652764096f1fb2d)) - Update stcarolas/setup-maven action to v5 ([#​3009](https://togithub.com/googleapis/java-spanner/issues/3009)) ([541acd2](https://togithub.com/googleapis/java-spanner/commit/541acd23aaf2c9336615406e30618fb65606e6c5))
--- ### 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, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-spanner-jdbc). --- pom.xml | 2 +- .../spanner/jdbc/AbstractJdbcWrapper.java | 8 ++-- .../cloud/spanner/jdbc/JdbcResultSet.java | 43 +++++++++++-------- .../cloud/spanner/jdbc/JdbcTypeConverter.java | 16 +++++++ .../spanner/jdbc/AllTypesMockServerTest.java | 34 +++++++++++++-- .../jdbc/PartitionedQueryMockServerTest.java | 3 +- .../spanner/jdbc/RandomResultSetTest.java | 16 +++++-- 7 files changed, 93 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 2e9478636..f4a9fc855 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ com.google.cloud google-cloud-spanner-bom - 6.63.0 + 6.64.0 pom import diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java index b06e5999e..aa65ee6f9 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner.jdbc; +import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.getMainTypeCode; + import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.Code; @@ -42,7 +44,7 @@ abstract class AbstractJdbcWrapper implements Wrapper { */ static int extractColumnType(Type type) { Preconditions.checkNotNull(type); - switch (type.getCode()) { + switch (getMainTypeCode(type)) { case BOOL: return Types.BOOLEAN; case BYTES: @@ -139,7 +141,7 @@ static String getClassName(int sqlType) { */ static String getClassName(Type type) { Preconditions.checkNotNull(type); - switch (type.getCode()) { + switch (getMainTypeCode(type)) { case BOOL: return Boolean.class.getName(); case BYTES: @@ -162,7 +164,7 @@ static String getClassName(Type type) { case TIMESTAMP: return Timestamp.class.getName(); case ARRAY: - switch (type.getArrayElementType().getCode()) { + switch (getMainTypeCode(type.getArrayElementType())) { case BOOL: return Boolean[].class.getName(); case BYTES: diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java index 91ec976b8..dcb259857 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner.jdbc; +import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.getMainTypeCode; + import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; @@ -195,7 +197,7 @@ public String getString(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? null : String.valueOf(spanner.getBoolean(spannerIndex)); @@ -233,7 +235,7 @@ public boolean getBoolean(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return !isNull && spanner.getBoolean(spannerIndex); @@ -266,7 +268,7 @@ public byte getByte(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? (byte) 0 : (spanner.getBoolean(spannerIndex) ? (byte) 1 : 0); @@ -305,7 +307,7 @@ public short getShort(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (short) 1 : 0); @@ -344,7 +346,7 @@ public int getInt(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? 1 : 0); @@ -383,7 +385,7 @@ public long getLong(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? 0L : (spanner.getBoolean(spannerIndex) ? 1L : 0L); @@ -418,7 +420,7 @@ public float getFloat(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (float) 1 : 0); @@ -451,7 +453,7 @@ public double getDouble(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case BOOL: return isNull ? 0 : (spanner.getBoolean(spannerIndex) ? (double) 1 : 0); @@ -492,7 +494,7 @@ public Date getDate(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case DATE: return isNull ? null : JdbcTypeConverter.toSqlDate(spanner.getDate(spannerIndex)); @@ -523,7 +525,7 @@ public Time getTime(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case STRING: return isNull ? null : parseTime(spanner.getString(spannerIndex)); @@ -551,7 +553,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case DATE: return isNull ? null : JdbcTypeConverter.toSqlTimestamp(spanner.getDate(spannerIndex)); @@ -716,6 +718,7 @@ public Object getObject(int columnIndex) throws SQLException { } private Object getObject(Type type, int columnIndex) throws SQLException { + // TODO: Refactor to check based on type code. if (type == Type.bool()) return getBoolean(columnIndex); if (type == Type.bytes()) return getBytes(columnIndex); if (type == Type.date()) return getDate(columnIndex); @@ -723,7 +726,9 @@ private Object getObject(Type type, int columnIndex) throws SQLException { return getFloat(columnIndex); } if (type == Type.float64()) return getDouble(columnIndex); - if (type == Type.int64()) return getLong(columnIndex); + if (type == Type.int64() || type == Type.pgOid()) { + return getLong(columnIndex); + } if (type == Type.numeric()) return getBigDecimal(columnIndex); if (type == Type.pgNumeric()) { final String value = getString(columnIndex); @@ -734,7 +739,9 @@ private Object getObject(Type type, int columnIndex) throws SQLException { } } if (type == Type.string()) return getString(columnIndex); - if (type == Type.json()) return getString(columnIndex); + if (type == Type.json() || type == Type.pgJsonb()) { + return getString(columnIndex); + } if (type == Type.timestamp()) return getTimestamp(columnIndex); if (type.getCode() == Code.ARRAY) return getArray(columnIndex); throw JdbcSqlExceptionFactory.of("Unknown type: " + type, com.google.rpc.Code.INVALID_ARGUMENT); @@ -792,7 +799,7 @@ public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLExcepti private BigDecimal getBigDecimal(int columnIndex, boolean fixedScale, int scale) throws SQLException { int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); boolean isNull = isNull(columnIndex); BigDecimal res; switch (type) { @@ -888,7 +895,7 @@ public Array getArray(int columnIndex) throws SQLException { throw JdbcSqlExceptionFactory.of( "Column with index " + columnIndex + " does not contain an array", com.google.rpc.Code.INVALID_ARGUMENT); - final Code elementCode = type.getArrayElementType().getCode(); + final Code elementCode = getMainTypeCode(type.getArrayElementType()); final JdbcDataType dataType = JdbcDataType.getType(elementCode); try { List elements = dataType.getArrayElements(spanner, columnIndex - 1); @@ -907,7 +914,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { return null; } int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case DATE: return JdbcTypeConverter.toSqlDate(spanner.getDate(spannerIndex), cal); @@ -941,7 +948,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { checkClosedAndValidRow(); boolean isNull = isNull(columnIndex); int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case STRING: return isNull ? null : parseTime(spanner.getString(spannerIndex), cal); @@ -975,7 +982,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException return null; } int spannerIndex = columnIndex - 1; - Code type = spanner.getColumnType(spannerIndex).getCode(); + Code type = getMainTypeCode(spanner.getColumnType(spannerIndex)); switch (type) { case DATE: return JdbcTypeConverter.toSqlTimestamp(spanner.getDate(spannerIndex), cal); diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java index c6be0a824..1733302c5 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java @@ -22,6 +22,7 @@ import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.Code; import com.google.cloud.spanner.Value; +import com.google.common.base.Preconditions; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.Charset; @@ -46,6 +47,17 @@ class JdbcTypeConverter { private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; private static final Charset UTF8 = StandardCharsets.UTF_8; + // TODO: Remove when this is supported in the Java client library. + static Code getMainTypeCode(Type type) { + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case PG_OID: + return Code.INT64; + default: + return type.getCode(); + } + } + /** * Converts the given value from the Google {@link Type} to the Java {@link Class} type. The input * value and the {@link Type} must be consistent with each other. @@ -205,6 +217,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx Arrays.asList((Double[]) ((java.sql.Array) value).getArray())); case INT64: return Value.int64Array(Arrays.asList((Long[]) ((java.sql.Array) value).getArray())); + case PG_OID: + return Value.pgOidArray(Arrays.asList((Long[]) ((java.sql.Array) value).getArray())); case NUMERIC: return Value.numericArray( Arrays.asList((BigDecimal[]) ((java.sql.Array) value).getArray())); @@ -238,6 +252,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx return Value.float64((Double) value); case INT64: return Value.int64((Long) value); + case PG_OID: + return Value.pgOid((Long) value); case NUMERIC: return Value.numeric((BigDecimal) value); case PG_NUMERIC: diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AllTypesMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AllTypesMockServerTest.java index 4a1eb6a6f..cf4b7dfb5 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/AllTypesMockServerTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/AllTypesMockServerTest.java @@ -75,6 +75,9 @@ public void testSelectAllTypes() { DATE_VALUE.getYear() - 1900, DATE_VALUE.getMonth() - 1, DATE_VALUE.getDayOfMonth()), resultSet.getDate(++col)); assertEquals(TIMESTAMP_VALUE.toSqlTimestamp(), resultSet.getTimestamp(++col)); + if (dialect == Dialect.POSTGRESQL) { + assertEquals(PG_OID_VALUE, resultSet.getLong(++col)); + } assertEquals( BOOL_ARRAY_VALUE, Arrays.asList((Boolean[]) resultSet.getArray(++col).getArray())); @@ -122,6 +125,10 @@ public void testSelectAllTypes() { .map(timestamp -> timestamp == null ? null : timestamp.toSqlTimestamp()) .collect(Collectors.toList()), Arrays.asList((Timestamp[]) resultSet.getArray(++col).getArray())); + if (dialect == Dialect.POSTGRESQL) { + assertEquals( + PG_OID_ARRAY_VALUE, Arrays.asList((Long[]) resultSet.getArray(++col).getArray())); + } assertFalse(resultSet.next()); } @@ -140,7 +147,7 @@ public void testInsertAllTypes() { insertStatement .toBuilder() .replace(insertStatement.getSql().replaceAll("@p", "\\$")) - .bind("p15") + .bind("p16") .to( com.google.cloud.spanner.Value.pgNumericArray( NUMERIC_ARRAY_VALUE.stream() @@ -179,6 +186,9 @@ public void testInsertAllTypes() { DATE_VALUE.getMonth() - 1, DATE_VALUE.getDayOfMonth())); statement.setTimestamp(++param, TIMESTAMP_VALUE.toSqlTimestamp()); + if (dialect == Dialect.POSTGRESQL) { + statement.setLong(++param, PG_OID_VALUE); + } // TODO: Support PostgreSQL type names for creating arrays. statement.setArray( @@ -241,6 +251,10 @@ public void testInsertAllTypes() { TIMESTAMP_ARRAY_VALUE.stream() .map(timestamp -> timestamp == null ? null : timestamp.toSqlTimestamp()) .toArray(Timestamp[]::new))); + if (dialect == Dialect.POSTGRESQL) { + statement.setArray( + ++param, connection.createArrayOf("INT64", PG_OID_ARRAY_VALUE.toArray(new Long[0]))); + } assertEquals(1, statement.executeUpdate()); } @@ -249,8 +263,8 @@ public void testInsertAllTypes() { ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0); Map paramTypes = request.getParamTypesMap(); Map params = request.getParams().getFieldsMap(); - assertEquals(20, paramTypes.size()); - assertEquals(20, params.size()); + assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, paramTypes.size()); + assertEquals(dialect == Dialect.POSTGRESQL ? 22 : 20, params.size()); // Verify param types. ImmutableList expectedTypes = @@ -265,6 +279,10 @@ public void testInsertAllTypes() { TypeCode.BYTES, TypeCode.DATE, TypeCode.TIMESTAMP); + if (dialect == Dialect.POSTGRESQL) { + expectedTypes = + ImmutableList.builder().addAll(expectedTypes).add(TypeCode.INT64).build(); + } for (int col = 0; col < expectedTypes.size(); col++) { assertEquals(expectedTypes.get(col), paramTypes.get("p" + (col + 1)).getCode()); int arrayCol = col + expectedTypes.size(); @@ -290,6 +308,9 @@ public void testInsertAllTypes() { params.get("p" + ++col).getStringValue()); assertEquals(DATE_VALUE.toString(), params.get("p" + ++col).getStringValue()); assertEquals(TIMESTAMP_VALUE.toString(), params.get("p" + ++col).getStringValue()); + if (dialect == Dialect.POSTGRESQL) { + assertEquals(String.valueOf(PG_OID_VALUE), params.get("p" + ++col).getStringValue()); + } assertEquals( BOOL_ARRAY_VALUE, @@ -365,6 +386,13 @@ public void testInsertAllTypes() { ? null : com.google.cloud.Timestamp.parseTimestamp(value.getStringValue())) .collect(Collectors.toList())); + if (dialect == Dialect.POSTGRESQL) { + assertEquals( + PG_OID_ARRAY_VALUE, + params.get("p" + ++col).getListValue().getValuesList().stream() + .map(value -> value.hasNullValue() ? null : Long.valueOf(value.getStringValue())) + .collect(Collectors.toList())); + } } catch (SQLException sqlException) { throw new RuntimeException(sqlException); } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java index 31842409b..5b521b621 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java @@ -73,7 +73,8 @@ public void clearRequests() { private int getExpectedColumnCount(Dialect dialect) { // GoogleSQL also adds 4 PROTO columns. - return dialect == Dialect.GOOGLE_STANDARD_SQL ? 24 : 20; + // PostgreSQL adds 2 OID columns. + return dialect == Dialect.GOOGLE_STANDARD_SQL ? 24 : 22; } private String createUrl() { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetTest.java index 4d24068dc..e00907ee9 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/RandomResultSetTest.java @@ -78,6 +78,9 @@ public void testSelectRandomResults() throws SQLException { assertEquals(Types.BINARY, metadata.getColumnType(++col)); assertEquals(Types.DATE, metadata.getColumnType(++col)); assertEquals(Types.TIMESTAMP, metadata.getColumnType(++col)); + if (dialect == Dialect.POSTGRESQL) { + assertEquals(Types.BIGINT, metadata.getColumnType(++col)); + } assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // boolean assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // bigint @@ -89,6 +92,9 @@ public void testSelectRandomResults() throws SQLException { assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // binary assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // date assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // timestamp + if (dialect == Dialect.POSTGRESQL) { + assertEquals(Types.ARRAY, metadata.getColumnType(++col)); // oid + } // GoogleSQL also includes proto columns. if (dialect == Dialect.GOOGLE_STANDARD_SQL) { @@ -108,9 +114,7 @@ public void testSelectRandomResults() throws SQLException { for (col = 1; col <= resultSet.getMetaData().getColumnCount(); col++) { if (dialect == Dialect.GOOGLE_STANDARD_SQL && col > 20) { // Proto columns are not yet supported, so skipping. - } else if (dialect == Dialect.POSTGRESQL && col == 7) { - // PG_JSONB is not yet recognized by the JDBC driver, so skipping. - } else if (dialect == Dialect.POSTGRESQL && col == 15) { + } else if (dialect == Dialect.POSTGRESQL && col == 16) { // getObject for ARRAY tries to get the array as a List. // That fails if the array contains a NaN, so skipping. } else { @@ -130,6 +134,9 @@ public void testSelectRandomResults() throws SQLException { resultSet.getBytes(++col); resultSet.getDate(++col); resultSet.getTimestamp(++col); + if (dialect == Dialect.POSTGRESQL) { + resultSet.getLong(++col); // oid + } resultSet.getArray(++col); resultSet.getArray(++col); @@ -147,6 +154,9 @@ public void testSelectRandomResults() throws SQLException { resultSet.getArray(++col); resultSet.getArray(++col); resultSet.getArray(++col); + if (dialect == Dialect.POSTGRESQL) { + resultSet.getArray(++col); // oid[] + } // GoogleSQL also includes proto columns. if (dialect == Dialect.GOOGLE_STANDARD_SQL) { From 22f766f098944c23084776c70dbd9dba21efa59c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 18 Apr 2024 12:57:46 +0200 Subject: [PATCH 24/29] deps: update dependency io.opentelemetry:opentelemetry-bom to v1.37.0 (#1562) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f4a9fc855..4d9824792 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ io.opentelemetry opentelemetry-bom - 1.36.0 + 1.37.0 pom import From 3d437076f2c699e261daf7dcb470085765dba14f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 19 Apr 2024 08:05:28 +0200 Subject: [PATCH 25/29] deps: update dependency com.google.cloud:sdk-platform-java-config to v3.29.0 (#1572) --- .github/workflows/unmanaged_dependency_check.yaml | 2 +- .kokoro/presubmit/graalvm-native-17.cfg | 2 +- .kokoro/presubmit/graalvm-native.cfg | 2 +- pom.xml | 2 +- samples/snippets/pom.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml index c724f3dab..a17720ffb 100644 --- a/.github/workflows/unmanaged_dependency_check.yaml +++ b/.github/workflows/unmanaged_dependency_check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.28.1 + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.29.0 with: bom-path: pom.xml diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg index c2a88196e..326361c6b 100644 --- a/.kokoro/presubmit/graalvm-native-17.cfg +++ b/.kokoro/presubmit/graalvm-native-17.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.28.1" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.29.0" } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg index 94e00cbaa..1b1d4c4bf 100644 --- a/.kokoro/presubmit/graalvm-native.cfg +++ b/.kokoro/presubmit/graalvm-native.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.28.1" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.29.0" } env_vars: { diff --git a/pom.xml b/pom.xml index 4d9824792..5657f0250 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.28.1 + 3.29.0 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 5928ec6ab..d1768c622 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -9,7 +9,7 @@ com.google.cloud sdk-platform-java-config - 3.28.1 + 3.29.0 From 7c38b9d6681178a5cda42fcc6d6cd83006555058 Mon Sep 17 00:00:00 2001 From: Alice <65933803+alicejli@users.noreply.github.com> Date: Fri, 19 Apr 2024 02:06:33 -0400 Subject: [PATCH 26/29] chore: include recommended_package in repo-metadata.json (#1571) --- .repo-metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.repo-metadata.json b/.repo-metadata.json index 6a760b5e4..8887ec91f 100644 --- a/.repo-metadata.json +++ b/.repo-metadata.json @@ -10,6 +10,7 @@ "repo_short": "java-spanner-jdbc", "distribution_name": "com.google.cloud:google-cloud-spanner-jdbc", "library_type": "OTHER", - "codeowner_team": "@googleapis/api-spanner-java" + "codeowner_team": "@googleapis/api-spanner-java", + "recommended_package": "com.google.cloud.spanner.jdbc" } From f54d4dd1211508785cb899e0a3c9b585c0908421 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 19 Apr 2024 08:07:47 +0200 Subject: [PATCH 27/29] deps: update dependency org.springframework.boot:spring-boot-starter-parent to v3.2.5 (#1570) --- samples/spring-data-mybatis/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml index ef9d1d89e..92e914214 100644 --- a/samples/spring-data-mybatis/pom.xml +++ b/samples/spring-data-mybatis/pom.xml @@ -13,7 +13,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.2.5 From 784ac1e68ac29628fe55d7b9e772326f10ffeaec Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 19 Apr 2024 08:08:26 +0200 Subject: [PATCH 28/29] deps: update dependency org.springframework.boot:spring-boot-starter-data-jdbc to v3.2.5 (#1569) --- samples/spring-data-jdbc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml index c1dc7cc00..960fe8da4 100644 --- a/samples/spring-data-jdbc/pom.xml +++ b/samples/spring-data-jdbc/pom.xml @@ -41,7 +41,7 @@ org.springframework.boot spring-boot-starter-data-jdbc - 3.2.4 + 3.2.5 From d06be0e1d330385f0cb1e598f5f4a7f3cff65f88 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 19:43:30 +0200 Subject: [PATCH 29/29] chore(main): release 2.16.2 (#1554) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ pom.xml | 2 +- samples/snapshot/pom.xml | 2 +- versions.txt | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd7acea51..38941b4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## [2.16.2](https://github.com/googleapis/java-spanner-jdbc/compare/v2.16.1...v2.16.2) (2024-04-19) + + +### Bug Fixes + +* Release ResultSet on Statement#close() ([#1567](https://github.com/googleapis/java-spanner-jdbc/issues/1567)) ([2258ae3](https://github.com/googleapis/java-spanner-jdbc/commit/2258ae3331a7e89036a202f243b9284108301fc0)) + + +### Dependencies + +* Update actions/checkout action to v4 ([#1547](https://github.com/googleapis/java-spanner-jdbc/issues/1547)) ([736e3af](https://github.com/googleapis/java-spanner-jdbc/commit/736e3afa54149dd11803bd715569afd9ec8e87f2)) +* Update actions/checkout action to v4 ([#1561](https://github.com/googleapis/java-spanner-jdbc/issues/1561)) ([6053d79](https://github.com/googleapis/java-spanner-jdbc/commit/6053d79816546130eca7a7016dc9299c079e411f)) +* Update actions/checkout digest to b4ffde6 ([#1546](https://github.com/googleapis/java-spanner-jdbc/issues/1546)) ([18c5ad4](https://github.com/googleapis/java-spanner-jdbc/commit/18c5ad4d4124f095547d50c0d2e154bc06380642)) +* Update actions/github-script action to v7 ([#1548](https://github.com/googleapis/java-spanner-jdbc/issues/1548)) ([d1d422c](https://github.com/googleapis/java-spanner-jdbc/commit/d1d422cdf0a74231c468262662fdf5ce4d27b8ef)) +* Update actions/setup-java action to v4 ([#1549](https://github.com/googleapis/java-spanner-jdbc/issues/1549)) ([cb2b911](https://github.com/googleapis/java-spanner-jdbc/commit/cb2b911b0b332e97f85974ec880a5ab7a12a7578)) +* Update actions/setup-java action to v4 ([#1563](https://github.com/googleapis/java-spanner-jdbc/issues/1563)) ([01d4de1](https://github.com/googleapis/java-spanner-jdbc/commit/01d4de1df21144e9c3bcf0b4e5192b12cd19dc82)) +* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.63.0 ([#1552](https://github.com/googleapis/java-spanner-jdbc/issues/1552)) ([ac75b9f](https://github.com/googleapis/java-spanner-jdbc/commit/ac75b9faf0eaeb499428ecefda1f3285b3d28e67)) +* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.64.0 ([#1565](https://github.com/googleapis/java-spanner-jdbc/issues/1565)) ([b57662f](https://github.com/googleapis/java-spanner-jdbc/commit/b57662fb65b74b329103ef63265192d7026b2c2d)) +* Update dependency com.google.cloud:sdk-platform-java-config to v3.28.1 ([#1560](https://github.com/googleapis/java-spanner-jdbc/issues/1560)) ([afcbe5e](https://github.com/googleapis/java-spanner-jdbc/commit/afcbe5ea5701a799729543c9564759570f05feb8)) +* Update dependency com.google.cloud:sdk-platform-java-config to v3.29.0 ([#1572](https://github.com/googleapis/java-spanner-jdbc/issues/1572)) ([3d43707](https://github.com/googleapis/java-spanner-jdbc/commit/3d437076f2c699e261daf7dcb470085765dba14f)) +* Update dependency io.opentelemetry:opentelemetry-bom to v1.37.0 ([#1562](https://github.com/googleapis/java-spanner-jdbc/issues/1562)) ([22f766f](https://github.com/googleapis/java-spanner-jdbc/commit/22f766f098944c23084776c70dbd9dba21efa59c)) +* Update dependency org.springframework.boot:spring-boot-starter-data-jdbc to v3.2.5 ([#1569](https://github.com/googleapis/java-spanner-jdbc/issues/1569)) ([784ac1e](https://github.com/googleapis/java-spanner-jdbc/commit/784ac1e68ac29628fe55d7b9e772326f10ffeaec)) +* Update dependency org.springframework.boot:spring-boot-starter-parent to v3.2.5 ([#1570](https://github.com/googleapis/java-spanner-jdbc/issues/1570)) ([f54d4dd](https://github.com/googleapis/java-spanner-jdbc/commit/f54d4dd1211508785cb899e0a3c9b585c0908421)) +* Update dependency org.springframework.data:spring-data-bom to v2023.1.5 ([#1564](https://github.com/googleapis/java-spanner-jdbc/issues/1564)) ([dbbcca3](https://github.com/googleapis/java-spanner-jdbc/commit/dbbcca342a83476b1f942aab23f21469cf6c8304)) +* Update stcarolas/setup-maven action to v5 ([#1550](https://github.com/googleapis/java-spanner-jdbc/issues/1550)) ([121d08e](https://github.com/googleapis/java-spanner-jdbc/commit/121d08e16db0bbb1f6041a201d620829e7121f4d)) + + +### Documentation + +* Create samples for quickstart guide ([#1536](https://github.com/googleapis/java-spanner-jdbc/issues/1536)) ([194c820](https://github.com/googleapis/java-spanner-jdbc/commit/194c8205dee9cc4144b18e219df43027b9f15cf2)) + ## [2.16.1](https://github.com/googleapis/java-spanner-jdbc/compare/v2.16.0...v2.16.1) (2024-03-22) diff --git a/pom.xml b/pom.xml index 5657f0250..6edcf1c68 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 google-cloud-spanner-jdbc - 2.16.2-SNAPSHOT + 2.16.2 jar Google Cloud Spanner JDBC https://github.com/googleapis/java-spanner-jdbc diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index baaf8bb89..f50a9b998 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-spanner-jdbc - 2.16.2-SNAPSHOT + 2.16.2 diff --git a/versions.txt b/versions.txt index 21a50e390..d27860f6a 100644 --- a/versions.txt +++ b/versions.txt @@ -1,4 +1,4 @@ # Format: # module:released-version:current-version -google-cloud-spanner-jdbc:2.16.1:2.16.2-SNAPSHOT +google-cloud-spanner-jdbc:2.16.2:2.16.2