Repository: googleapis/java-spanner-jdbc
Pull request #1473 (2024-01-25 16:32:05)

test: verify execution of client side statements
Verifies that client-side statements can now also be executed using the executeQuery and executeUpdate methods.

Replaces #1152



Patch:
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index dc05a72..bf48c82 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -13,5 +13,5 @@
 # limitations under the License.
 docker:
   image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest
-  digest: sha256:a6aa751984f1e905c3ae5a3aac78fc7b68210626ce91487dc7ff4f0a06f010cc
-# created: 2024-01-22T14:14:20.913785597Z
+  digest: sha256:0d1bb26a1a99ae0456176bf891b8490e9aab424a5cb4e4d301d9703c4dc43b58
+# created: 2024-01-30T19:46:55.029238294Z
diff --git a/.github/workflows/unmanaged-dependency-check.yaml b/.github/workflows/unmanaged-dependency-check.yaml
new file mode 100644
index 0000000..4ca518a
--- /dev/null
+++ b/.github/workflows/unmanaged-dependency-check.yaml
@@ -0,0 +1,19 @@
+on:
+  pull_request:
+name: Unmanaged dependency check
+jobs:
+  unmanaged_dependency_check:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-java@v3
+      with:
+        distribution: temurin
+        java-version: 11
+    - name: Install modules
+      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
+      with:
+        bom-path: pom.xml
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
index fb5bb67..90d9a20 100644
--- a/.kokoro/presubmit/graalvm-native-17.cfg
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/graalvm17:22.3.3"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.24.0"
 }
 
 env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index 59efee3..948177b 100644
--- a/.kokoro/presubmit/graalvm-native.cfg
+++ b/.kokoro/presubmit/graalvm-native.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/graalvm:22.3.3"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.24.0"
 }
 
 env_vars: {
diff --git a/.kokoro/release/bump_snapshot.cfg b/.kokoro/release/bump_snapshot.cfg
deleted file mode 100644
index 3dc06f2..0000000
--- a/.kokoro/release/bump_snapshot.cfg
+++ /dev/null
@@ -1,53 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Build logs will be here
-action {
-  define_artifacts {
-    regex: "**/*sponge_log.xml"
-  }
-}
-
-# Download trampoline resources.
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
-
-# Use the trampoline script to run in docker.
-build_file: "java-spanner-jdbc/.kokoro/trampoline.sh"
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/bump_snapshot.sh"
-}
-
-# tokens used by release-please to keep an up-to-date release PR.
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-key-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-token-release-please"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "github-magic-proxy-url-release-please"
-    }
-  }
-}
diff --git a/.kokoro/release/bump_snapshot.sh b/.kokoro/release/bump_snapshot.sh
deleted file mode 100755
index 4d76d43..0000000
--- a/.kokoro/release/bump_snapshot.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -eo pipefail
-
-export NPM_CONFIG_PREFIX=/home/node/.npm-global
-
-if [ -f ${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please ]; then
-  # Groom the snapshot release PR immediately after publishing a release
-  npx release-please release-pr --token=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-token-release-please \
-    --repo-url=googleapis/java-spanner-jdbc \
-    --package-name="spanner-jdbc" \
-    --api-url=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-url-release-please \
-    --proxy-key=${KOKORO_KEYSTORE_DIR}/73713_github-magic-proxy-key-release-please \
-    --snapshot \
-    --release-type=java-auth-yoshi
-fi
diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg
deleted file mode 100644
index 2ace079..0000000
--- a/.kokoro/release/common.cfg
+++ /dev/null
@@ -1,49 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# Download trampoline resources.
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
-
-# Use the trampoline script to run in docker.
-build_file: "java-spanner-jdbc/.kokoro/trampoline.sh"
-
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java8"
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-keyring"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-passphrase"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "maven-gpg-pubkeyring"
-    }
-  }
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 70247
-      keyname: "sonatype-credentials"
-    }
-  }
-}
diff --git a/.kokoro/release/common.sh b/.kokoro/release/common.sh
deleted file mode 100755
index 7f78ee4..0000000
--- a/.kokoro/release/common.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# Get secrets from keystore and set and environment variables
-setup_environment_secrets() {
-  export GPG_PASSPHRASE=$(cat ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-passphrase)
-  export GPG_TTY=$(tty)
-  export GPG_HOMEDIR=/gpg
-  mkdir $GPG_HOMEDIR
-  mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-pubkeyring $GPG_HOMEDIR/pubring.gpg
-  mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-keyring $GPG_HOMEDIR/secring.gpg
-  export SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f1 -d'|')
-  export SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/70247_sonatype-credentials | cut -f2 -d'|')
-}
-
-create_settings_xml_file() {
-  echo "<settings>
-  <servers>
-    <server>
-      <id>ossrh</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-    <server>
-      <id>sonatype-nexus-staging</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-    <server>
-      <id>sonatype-nexus-snapshots</id>
-      <username>${SONATYPE_USERNAME}</username>
-      <password>${SONATYPE_PASSWORD}</password>
-    </server>
-  </servers>
-</settings>" > $1
-}
\ No newline at end of file
diff --git a/.kokoro/release/drop.cfg b/.kokoro/release/drop.cfg
deleted file mode 100644
index 245e96b..0000000
--- a/.kokoro/release/drop.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/drop.sh"
-}
diff --git a/.kokoro/release/drop.sh b/.kokoro/release/drop.sh
deleted file mode 100755
index 742ec1a..0000000
--- a/.kokoro/release/drop.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# STAGING_REPOSITORY_ID must be set
-if [ -z "${STAGING_REPOSITORY_ID}" ]; then
-  echo "Missing STAGING_REPOSITORY_ID environment variable"
-  exit 1
-fi
-
-source $(dirname "$0")/common.sh
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn nexus-staging:drop -B \
-  --settings=settings.xml \
-  -DstagingRepositoryId=${STAGING_REPOSITORY_ID}
diff --git a/.kokoro/release/promote.cfg b/.kokoro/release/promote.cfg
deleted file mode 100644
index 1ee66ee..0000000
--- a/.kokoro/release/promote.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/promote.sh"
-}
diff --git a/.kokoro/release/promote.sh b/.kokoro/release/promote.sh
deleted file mode 100755
index 3cac3d8..0000000
--- a/.kokoro/release/promote.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# STAGING_REPOSITORY_ID must be set
-if [ -z "${STAGING_REPOSITORY_ID}" ]; then
-  echo "Missing STAGING_REPOSITORY_ID environment variable"
-  exit 1
-fi
-
-source $(dirname "$0")/common.sh
-
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn nexus-staging:release -B \
-  -DperformRelease=true \
-  --settings=settings.xml \
-  -DstagingRepositoryId=${STAGING_REPOSITORY_ID}
diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg
deleted file mode 100644
index 8e17cf2..0000000
--- a/.kokoro/release/publish_javadoc.cfg
+++ /dev/null
@@ -1,23 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/doc-templates/"
-
-env_vars: {
-  key: "STAGING_BUCKET"
-  value: "docs-staging"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/publish_javadoc.sh"
-}
-
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "docuploader_service_account"
-    }
-  }
-}
diff --git a/.kokoro/release/publish_javadoc.sh b/.kokoro/release/publish_javadoc.sh
deleted file mode 100755
index 54c727b..0000000
--- a/.kokoro/release/publish_javadoc.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -eo pipefail
-
-if [[ -z "${CREDENTIALS}" ]]; then
-  CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account
-fi
-
-if [[ -z "${STAGING_BUCKET}" ]]; then
-  echo "Need to set STAGING_BUCKET environment variable"
-  exit 1
-fi
-
-# work from the git root directory
-pushd $(dirname "$0")/../../
-
-# install docuploader package
-python3 -m pip install --require-hashes -r .kokoro/requirements.txt
-
-# compile all packages
-mvn clean install -B -q -DskipTests=true
-
-export NAME=google-cloud-spanner-jdbc
-export VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
-
-# build the docs
-mvn site -B -q
-
-pushd target/site/apidocs
-
-# create metadata
-python3 -m docuploader create-metadata \
-  --name ${NAME} \
-  --version ${VERSION} \
-  --language java
-
-# upload docs
-python3 -m docuploader upload . \
-  --credentials ${CREDENTIALS} \
-  --staging-bucket ${STAGING_BUCKET}
diff --git a/.kokoro/release/publish_javadoc11.cfg b/.kokoro/release/publish_javadoc11.cfg
deleted file mode 100644
index 15fdc98..0000000
--- a/.kokoro/release/publish_javadoc11.cfg
+++ /dev/null
@@ -1,30 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-# cloud-rad production
-env_vars: {
-  key: "STAGING_BUCKET_V2"
-  value: "docs-staging-v2"
-}
-
-# Configure the docker image for kokoro-trampoline
-env_vars: {
-  key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-kokoro-resources/java11"
-}
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/publish_javadoc11.sh"
-}
-
-before_action {
-  fetch_keystore {
-    keystore_resource {
-      keystore_config_id: 73713
-      keyname: "docuploader_service_account"
-    }
-  }
-}
-
-# Downloads docfx doclet resource. This will be in ${KOKORO_GFILE_DIR}/<doclet name>
-gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/docfx"
diff --git a/.kokoro/release/publish_javadoc11.sh b/.kokoro/release/publish_javadoc11.sh
deleted file mode 100755
index 03eff88..0000000
--- a/.kokoro/release/publish_javadoc11.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-# Copyright 2021 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -eo pipefail
-
-if [[ -z "${CREDENTIALS}" ]]; then
-  CREDENTIALS=${KOKORO_KEYSTORE_DIR}/73713_docuploader_service_account
-fi
-
-if [[ -z "${STAGING_BUCKET_V2}" ]]; then
-  echo "Need to set STAGING_BUCKET_V2 environment variable"
-  exit 1
-fi
-
-# work from the git root directory
-pushd $(dirname "$0")/../../
-
-# install docuploader package
-python3 -m pip install --require-hashes -r .kokoro/requirements.txt
-
-# compile all packages
-mvn clean install -B -q -DskipTests=true
-
-export NAME=google-cloud-spanner-jdbc
-export VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3)
-
-# cloud RAD generation
-mvn clean javadoc:aggregate -B -q -P docFX
-# include CHANGELOG
-cp CHANGELOG.md target/docfx-yml/history.md
-
-pushd target/docfx-yml
-
-# create metadata
-python3 -m docuploader create-metadata \
- --name ${NAME} \
- --version ${VERSION} \
- --xrefs devsite://java/gax \
- --xrefs devsite://java/google-cloud-core \
- --xrefs devsite://java/api-common \
- --xrefs devsite://java/proto-google-common-protos \
- --xrefs devsite://java/google-api-client \
- --xrefs devsite://java/google-http-client \
- --xrefs devsite://java/protobuf \
- --language java
-
-# upload yml to production bucket
-python3 -m docuploader upload . \
- --credentials ${CREDENTIALS} \
- --staging-bucket ${STAGING_BUCKET_V2} \
- --destination-prefix docfx
diff --git a/.kokoro/release/snapshot.cfg b/.kokoro/release/snapshot.cfg
deleted file mode 100644
index aa77239..0000000
--- a/.kokoro/release/snapshot.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/snapshot.sh"
-}
\ No newline at end of file
diff --git a/.kokoro/release/snapshot.sh b/.kokoro/release/snapshot.sh
deleted file mode 100755
index 1f55b77..0000000
--- a/.kokoro/release/snapshot.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/bash
-# Copyright 2019 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -eo pipefail
-
-source $(dirname "$0")/common.sh
-MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml
-pushd $(dirname "$0")/../../
-
-# ensure we're trying to push a snapshot (no-result returns non-zero exit code)
-grep SNAPSHOT versions.txt
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-mvn clean deploy -B \
-  --settings ${MAVEN_SETTINGS_FILE} \
-  -DperformRelease=true \
-  -Dgpg.executable=gpg \
-  -Dgpg.passphrase=${GPG_PASSPHRASE} \
-  -Dgpg.homedir=${GPG_HOMEDIR}
diff --git a/.kokoro/release/stage.cfg b/.kokoro/release/stage.cfg
deleted file mode 100644
index edca2be..0000000
--- a/.kokoro/release/stage.cfg
+++ /dev/null
@@ -1,19 +0,0 @@
-# Format: //devtools/kokoro/config/proto/build.proto
-
-env_vars: {
-  key: "TRAMPOLINE_BUILD_FILE"
-  value: "github/java-spanner-jdbc/.kokoro/release/stage.sh"
-}
-
-# Need to save the properties file
-action {
-  define_artifacts {
-    regex: "github/java-spanner-jdbc/target/nexus-staging/staging/*.properties"
-    strip_prefix: "github/java-spanner-jdbc"
-  }
-}
-
-env_vars: {
-  key: "SECRET_MANAGER_KEYS"
-  value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem"
-}
diff --git a/.kokoro/release/stage.sh b/.kokoro/release/stage.sh
deleted file mode 100755
index 61e714d..0000000
--- a/.kokoro/release/stage.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-# Copyright 2018 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.
-
-set -eo pipefail
-
-# Start the releasetool reporter
-requirementsFile=$(realpath $(dirname "${BASH_SOURCE[0]}")/../requirements.txt)
-python3 -m pip install --require-hashes -r $requirementsFile
-python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
-
-source $(dirname "$0")/common.sh
-source $(dirname "$0")/../common.sh
-MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml
-pushd $(dirname "$0")/../../
-
-setup_environment_secrets
-create_settings_xml_file "settings.xml"
-
-# attempt to stage 3 times with exponential backoff (starting with 10 seconds)
-retry_with_backoff 3 10 \
-  mvn clean deploy -B \
-    --settings ${MAVEN_SETTINGS_FILE} \
-    -DskipTests=true \
-    -Dclirr.skip=true \
-    -DperformRelease=true \
-    -Dgpg.executable=gpg \
-    -Dgpg.passphrase=${GPG_PASSPHRASE} \
-    -Dgpg.homedir=${GPG_HOMEDIR}
-
-if [[ -n "${AUTORELEASE_PR}" ]]
-then
-  mvn nexus-staging:release -B \
-    -DperformRelease=true \
-    --settings=settings.xml
-fi
diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in
deleted file mode 100644
index 2092cc7..0000000
--- a/.kokoro/requirements.in
+++ /dev/null
@@ -1,6 +0,0 @@
-gcp-docuploader
-gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x
-wheel
-setuptools
-typing-extensions
-click<8.1.0
\ No newline at end of file
diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt
deleted file mode 100644
index 445c5c1..0000000
--- a/.kokoro/requirements.txt
+++ /dev/null
@@ -1,491 +0,0 @@
-#
-# This file is autogenerated by pip-compile with Python 3.9
-# by the following command:
-#
-#     pip-compile requirements.in --generate-hashes --upgrade
-#
-attrs==23.1.0 \
-    --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
-    --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
-    # via gcp-releasetool
-cachetools==5.3.1 \
-    --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \
-    --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b
-    # via google-auth
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
-    # via requests
-cffi==1.15.1 \
-    --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
-    --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
-    --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
-    --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
-    --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
-    --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
-    --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
-    --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
-    --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
-    --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
-    --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
-    --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
-    --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
-    --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
-    --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
-    --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
-    --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
-    --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
-    --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
-    --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
-    --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
-    --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
-    --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
-    --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
-    --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
-    --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
-    --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
-    --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
-    --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
-    --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
-    --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
-    --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
-    --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
-    --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
-    --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
-    --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
-    --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
-    --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
-    --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
-    --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
-    --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
-    --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
-    --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
-    --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
-    --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
-    --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
-    --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
-    --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
-    --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
-    --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
-    --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
-    --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
-    --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
-    --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
-    --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
-    --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
-    --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
-    --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
-    --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
-    --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
-    --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
-    --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
-    --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
-    --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
-    # via cryptography
-charset-normalizer==3.2.0 \
-    --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
-    --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
-    --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
-    --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
-    --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
-    --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
-    --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
-    --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
-    --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
-    --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
-    --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
-    --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
-    --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
-    --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
-    --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
-    --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
-    --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
-    --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
-    --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
-    --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
-    --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
-    --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
-    --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
-    --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
-    --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
-    --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
-    --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
-    --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
-    --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
-    --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
-    --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
-    --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
-    --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
-    --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
-    --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
-    --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
-    --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
-    --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
-    --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
-    --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
-    --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
-    --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
-    --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
-    --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
-    --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
-    --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
-    --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
-    --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
-    --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
-    --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
-    --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
-    --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
-    --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
-    --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
-    --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
-    --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
-    --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
-    --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
-    --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
-    --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
-    --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
-    --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
-    --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
-    --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
-    --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
-    --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
-    --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
-    --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
-    --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
-    --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
-    --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
-    --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
-    --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
-    --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
-    --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
-    # via requests
-click==8.0.4 \
-    --hash=sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1 \
-    --hash=sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb
-    # via
-    #   -r requirements.in
-    #   gcp-docuploader
-    #   gcp-releasetool
-colorlog==6.7.0 \
-    --hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \
-    --hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5
-    # via gcp-docuploader
-cryptography==41.0.6 \
-    --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \
-    --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \
-    --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \
-    --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \
-    --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \
-    --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \
-    --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \
-    --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \
-    --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \
-    --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \
-    --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \
-    --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \
-    --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \
-    --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \
-    --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \
-    --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \
-    --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \
-    --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \
-    --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \
-    --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \
-    --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \
-    --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \
-    --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae
-    # via
-    #   gcp-releasetool
-    #   secretstorage
-gcp-docuploader==0.6.5 \
-    --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \
-    --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea
-    # via -r requirements.in
-gcp-releasetool==1.16.0 \
-    --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \
-    --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63
-    # via -r requirements.in
-google-api-core==2.11.1 \
-    --hash=sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a \
-    --hash=sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a
-    # via
-    #   google-cloud-core
-    #   google-cloud-storage
-google-auth==2.22.0 \
-    --hash=sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce \
-    --hash=sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873
-    # via
-    #   gcp-releasetool
-    #   google-api-core
-    #   google-cloud-core
-    #   google-cloud-storage
-google-cloud-core==2.3.3 \
-    --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
-    --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
-    # via google-cloud-storage
-google-cloud-storage==2.10.0 \
-    --hash=sha256:934b31ead5f3994e5360f9ff5750982c5b6b11604dc072bc452c25965e076dc7 \
-    --hash=sha256:9433cf28801671de1c80434238fb1e7e4a1ba3087470e90f70c928ea77c2b9d7
-    # via gcp-docuploader
-google-crc32c==1.5.0 \
-    --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
-    --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
-    --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
-    --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
-    --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
-    --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
-    --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
-    --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
-    --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
-    --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
-    --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
-    --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
-    --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
-    --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
-    --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
-    --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
-    --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
-    --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
-    --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
-    --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
-    --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
-    --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
-    --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
-    --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
-    --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
-    --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
-    --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
-    --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
-    --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
-    --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
-    --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
-    --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
-    --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
-    --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
-    --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
-    --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
-    --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
-    --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
-    --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
-    --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
-    --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
-    --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
-    --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
-    --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
-    --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
-    --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
-    --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
-    --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
-    --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
-    --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
-    --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
-    --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
-    --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
-    --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
-    --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
-    --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
-    --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
-    --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
-    --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
-    --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
-    --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
-    --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
-    --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
-    --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
-    --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
-    --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
-    --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
-    --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
-    # via google-resumable-media
-google-resumable-media==2.5.0 \
-    --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \
-    --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec
-    # via google-cloud-storage
-googleapis-common-protos==1.59.1 \
-    --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \
-    --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a
-    # via google-api-core
-idna==3.4 \
-    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
-    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
-    # via requests
-importlib-metadata==6.8.0 \
-    --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
-    --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
-    # via keyring
-jaraco-classes==3.3.0 \
-    --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \
-    --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621
-    # via keyring
-jeepney==0.8.0 \
-    --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \
-    --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755
-    # via
-    #   keyring
-    #   secretstorage
-jinja2==3.1.2 \
-    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
-    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
-    # via gcp-releasetool
-keyring==24.2.0 \
-    --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \
-    --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509
-    # via gcp-releasetool
-markupsafe==2.1.3 \
-    --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
-    --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
-    --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
-    --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
-    --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
-    --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
-    --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
-    --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
-    --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
-    --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
-    --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
-    --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
-    --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
-    --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
-    --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
-    --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
-    --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
-    --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
-    --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
-    --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
-    --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
-    --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
-    --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
-    --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
-    --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
-    --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
-    --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
-    --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
-    --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
-    --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
-    --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
-    --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
-    --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
-    --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
-    --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
-    --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
-    --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
-    --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
-    --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
-    --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
-    --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
-    --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
-    --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
-    --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
-    --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
-    --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
-    --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
-    --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
-    --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
-    --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2
-    # via jinja2
-more-itertools==9.1.0 \
-    --hash=sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d \
-    --hash=sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3
-    # via jaraco-classes
-packaging==23.1 \
-    --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
-    --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
-    # via gcp-releasetool
-protobuf==3.20.3 \
-    --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \
-    --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \
-    --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \
-    --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \
-    --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \
-    --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \
-    --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \
-    --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \
-    --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \
-    --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \
-    --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \
-    --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \
-    --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \
-    --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \
-    --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \
-    --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \
-    --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \
-    --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \
-    --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \
-    --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \
-    --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \
-    --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee
-    # via
-    #   gcp-docuploader
-    #   gcp-releasetool
-    #   google-api-core
-    #   googleapis-common-protos
-pyasn1==0.5.0 \
-    --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \
-    --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde
-    # via
-    #   pyasn1-modules
-    #   rsa
-pyasn1-modules==0.3.0 \
-    --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \
-    --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d
-    # via google-auth
-pycparser==2.21 \
-    --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
-    --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
-    # via cffi
-pyjwt==2.7.0 \
-    --hash=sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1 \
-    --hash=sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074
-    # via gcp-releasetool
-pyperclip==1.8.2 \
-    --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
-    # via gcp-releasetool
-python-dateutil==2.8.2 \
-    --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
-    --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
-    # via gcp-releasetool
-requests==2.31.0 \
-    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
-    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
-    # via
-    #   gcp-releasetool
-    #   google-api-core
-    #   google-cloud-storage
-rsa==4.9 \
-    --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
-    --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
-    # via google-auth
-secretstorage==3.3.3 \
-    --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
-    --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99
-    # via keyring
-six==1.16.0 \
-    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
-    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
-    # via
-    #   gcp-docuploader
-    #   google-auth
-    #   python-dateutil
-typing-extensions==4.7.1 \
-    --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \
-    --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2
-    # via -r requirements.in
-urllib3==1.26.18 \
-    --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \
-    --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0
-    # via
-    #   google-auth
-    #   requests
-wheel==0.40.0 \
-    --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
-    --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
-    # via -r requirements.in
-zipp==3.16.1 \
-    --hash=sha256:0b37c326d826d5ca35f2b9685cd750292740774ef16190008b00a0227c256fe0 \
-    --hash=sha256:857b158da2cbf427b376da1c24fd11faecbac5a4ac7523c3607f8a01f94c2ec0
-    # via importlib-metadata
-
-# WARNING: The following packages were not pinned, but pip requires them to be
-# pinned when the requirements file includes hashes and the requirement is not
-# satisfied by a package already installed. Consider using the --allow-unsafe flag.
-# setuptools
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a2b89a..1aaad16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
 # Changelog
 
+## [2.15.3](https://github.com/googleapis/java-spanner-jdbc/compare/v2.15.2...v2.15.3) (2024-02-09)
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.24.0 ([#1481](https://github.com/googleapis/java-spanner-jdbc/issues/1481)) ([e3f58d1](https://github.com/googleapis/java-spanner-jdbc/commit/e3f58d1cad8806e120dda99e16536c7bbf837fe4))
+
+## [2.15.2](https://github.com/googleapis/java-spanner-jdbc/compare/v2.15.1...v2.15.2) (2024-01-29)
+
+
+### Dependencies
+
+* Bump Spanner client to 6.57.0 ([#1474](https://github.com/googleapis/java-spanner-jdbc/issues/1474)) ([ce5180a](https://github.com/googleapis/java-spanner-jdbc/commit/ce5180a900a6b55bba2d32f401d957f6924f8ea8))
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.22.0 ([#1463](https://github.com/googleapis/java-spanner-jdbc/issues/1463)) ([d505321](https://github.com/googleapis/java-spanner-jdbc/commit/d505321ab58f3d597550d9891c11eb7a7f17c536))
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.23.0 ([#1470](https://github.com/googleapis/java-spanner-jdbc/issues/1470)) ([58cf2ba](https://github.com/googleapis/java-spanner-jdbc/commit/58cf2baa1a8cecc4f84d60001d288c535e82b66d))
+* Update dependency com.spotify.fmt:fmt-maven-plugin to v2.22 ([#1460](https://github.com/googleapis/java-spanner-jdbc/issues/1460)) ([bbbff2f](https://github.com/googleapis/java-spanner-jdbc/commit/bbbff2ff98a0bc15ea3b5640b767e872f753452c))
+* Update dependency com.spotify.fmt:fmt-maven-plugin to v2.22.1 ([#1467](https://github.com/googleapis/java-spanner-jdbc/issues/1467)) ([449bae5](https://github.com/googleapis/java-spanner-jdbc/commit/449bae5079ce122be711490f8f1009f272082d13))
+* Update dependency org.springframework.boot:spring-boot-starter-data-jdbc to v3.2.2 ([#1424](https://github.com/googleapis/java-spanner-jdbc/issues/1424)) ([e14af90](https://github.com/googleapis/java-spanner-jdbc/commit/e14af9025a64a94cf431cc2d03bcace7eb753b32))
+* Update dependency org.springframework.boot:spring-boot-starter-parent to v3.2.2 ([#1425](https://github.com/googleapis/java-spanner-jdbc/issues/1425)) ([9ccb04a](https://github.com/googleapis/java-spanner-jdbc/commit/9ccb04afaaf355734e955f82716c94d229b958d3))
+* Update dependency org.springframework.data:spring-data-bom to v2023.1.2 ([#1421](https://github.com/googleapis/java-spanner-jdbc/issues/1421)) ([dc53309](https://github.com/googleapis/java-spanner-jdbc/commit/dc53309d7846e97ffb1ee2e5cdc0e6c13b648049))
+
 ## [2.15.1](https://github.com/googleapis/java-spanner-jdbc/compare/v2.15.0...v2.15.1) (2024-01-09)
 
 
diff --git a/owlbot.py b/owlbot.py
index 05f32bb..d470606 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -22,5 +22,9 @@ java.common_templates(excludes=[
     '.github/sync-repo-settings.yaml',
     '.github/blunderbuss.yml',
     '.kokoro/nightly/integration.cfg',
-    '.kokoro/build.sh'
+    '.kokoro/build.sh',
+    '.kokoro/requirements.in',
+    '.kokoro/requirements.txt',
+    '.kokoro/presubmit/graalvm-native.cfg',
+    '.kokoro/presubmit/graalvm-native-17.cfg'
 ])
diff --git a/pom.xml b/pom.xml
index 6b3a2f3..124b6d4 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">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-spanner-jdbc</artifactId>
-  <version>2.15.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-jdbc:current} -->
+  <version>2.15.4-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-jdbc:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Spanner JDBC</name>
   <url>https://github.com/googleapis/java-spanner-jdbc</url>
@@ -51,7 +51,7 @@
     <site.installationModule>google-cloud-spanner-jdbc</site.installationModule>
     <junit.version>4.13.2</junit.version>
     <findbugs.version>3.0.2</findbugs.version>
-    <truth.version>1.3.0</truth.version>
+    <truth.version>1.4.0</truth.version>
     <mockito.version>4.11.0</mockito.version>
     <hamcrest.version>2.2</hamcrest.version>
     <opencensus.version>0.31.1</opencensus.version>
@@ -62,14 +62,14 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-spanner-bom</artifactId>
-        <version>6.56.0</version>
+        <version>6.58.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-shared-dependencies</artifactId>
-        <version>3.23.0</version>
+        <version>3.24.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/renovate.json b/renovate.json
index efe6430..fe0d0cc 100644
--- a/renovate.json
+++ b/renovate.json
@@ -14,6 +14,17 @@
     ".kokoro/requirements.txt",
     ".github/workflows/**"
   ],
+  "customManagers": [
+    {
+      "customType": "regex",
+      "fileMatch": [
+        "^.kokoro/presubmit/graalvm-native.*.cfg$"
+      ],
+      "matchStrings": ["value: \"gcr.io/cloud-devrel-public-resources/graalvm.*:(?<currentValue>.*?)\""],
+      "depNameTemplate": "com.google.cloud:sdk-platform-java-config",
+      "datasourceTemplate": "maven"
+    }
+  ],
   "packageRules": [
     {
       "packagePatterns": [
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 902f2d9..01abf80 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-jdbc</artifactId>
-      <version>2.15.1</version>
+      <version>2.15.3</version>
     </dependency>
     <!-- [END spanner-jdbc_install_without_bom] -->
 
@@ -42,7 +42,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.3.0</version>
+      <version>1.4.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/pom.xml b/samples/pom.xml
index b31e3d0..1e8df92 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -19,6 +19,7 @@
     <groupId>com.google.cloud.samples</groupId>
     <artifactId>shared-configuration</artifactId>
     <version>1.2.0</version>
+    <relativePath />
   </parent>
 
   <properties>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 0c744d9..b651b6b 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-spanner-jdbc</artifactId>
-      <version>2.15.2-SNAPSHOT</version>
+      <version>2.15.4-SNAPSHOT</version>
     </dependency>
     <!-- {x-version-update-end} -->
 
@@ -41,7 +41,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.3.0</version>
+      <version>1.4.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 95879e0..14d5329 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -30,7 +30,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.30.0</version>
+        <version>26.31.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml
index a386b74..ea05b79 100644
--- a/samples/spring-data-jdbc/pom.xml
+++ b/samples/spring-data-jdbc/pom.xml
@@ -23,14 +23,14 @@
       <dependency>
         <groupId>org.springframework.data</groupId>
         <artifactId>spring-data-bom</artifactId>
-        <version>2023.0.5</version>
+        <version>2023.1.2</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.30.0</version>
+        <version>26.31.0</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
@@ -41,7 +41,7 @@
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-jdbc</artifactId>
-      <version>3.1.5</version>
+      <version>3.2.2</version>
     </dependency>
 
     <!-- Add both the Cloud Spanner and the PostgreSQL JDBC driver. -->
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
index 890816c..5992f26 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java
@@ -20,6 +20,7 @@ import java.time.OffsetDateTime;
 import org.springframework.data.annotation.CreatedDate;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.annotation.PersistenceCreator;
 
 public abstract class AbstractEntity {
 
@@ -30,6 +31,9 @@ public abstract class AbstractEntity {
 
   @LastModifiedDate private OffsetDateTime updatedAt;
 
+  @PersistenceCreator
+  public AbstractEntity() {}
+
   @Override
   public boolean equals(Object o) {
     if (!(o instanceof AbstractEntity)) {
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Album.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
index dfde31a..b6b7ac8 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Album.java
@@ -18,6 +18,7 @@ package com.google.cloud.spanner.sample.entities;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.relational.core.mapping.Table;
 
 @Table("albums")
@@ -33,6 +34,7 @@ public class Album extends AbstractEntity {
 
   private Long singerId;
 
+  @PersistenceCreator
   public Album() {}
 
   public Album(String title) {
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
index 29f2b6d..1075eed 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Concert.java
@@ -17,6 +17,7 @@
 package com.google.cloud.spanner.sample.entities;
 
 import java.time.OffsetDateTime;
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.relational.core.mapping.Table;
 
 @Table("concerts")
@@ -32,6 +33,9 @@ public class Concert extends AbstractEntity {
 
   private OffsetDateTime endTime;
 
+  @PersistenceCreator
+  public Concert() {}
+
   public Concert(Venue venue, Singer singer, String name) {
     this.venueId = venue.getId();
     this.singerId = singer.getId();
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
index 255459a..4b68f13 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Singer.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.spanner.sample.entities;
 
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.annotation.ReadOnlyProperty;
 import org.springframework.data.relational.core.mapping.Table;
 
@@ -31,6 +32,7 @@ public class Singer extends AbstractEntity {
 
   private Boolean active;
 
+  @PersistenceCreator
   public Singer() {}
 
   public Singer(String firstName, String lastName) {
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Track.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
index 268b6e2..c255a08 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Track.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.spanner.sample.entities;
 
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.relational.core.mapping.Column;
 import org.springframework.data.relational.core.mapping.Table;
 
@@ -44,6 +45,9 @@ public class Track extends AbstractEntity {
 
   private Double sampleRate;
 
+  @PersistenceCreator
+  public Track() {}
+
   public Track(Album album, int trackNumber, String title) {
     setAlbumId(album.getId());
     this.trackNumber = trackNumber;
diff --git a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
index e7855cd..82dc597 100644
--- a/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
+++ b/samples/spring-data-jdbc/src/main/java/com/google/cloud/spanner/sample/entities/Venue.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.spanner.sample.entities;
 
+import org.springframework.data.annotation.PersistenceCreator;
 import org.springframework.data.relational.core.mapping.Table;
 
 @Table("venues")
@@ -24,6 +25,9 @@ public class Venue extends AbstractEntity {
 
   private String description;
 
+  @PersistenceCreator
+  public Venue() {}
+
   public Venue(String name) {
     this.name = name;
   }
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 2d3ee20..a86e50d 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -28,14 +28,14 @@
       <dependency>
         <groupId>org.springframework.data</groupId>
         <artifactId>spring-data-bom</artifactId>
-        <version>2023.0.5</version>
+        <version>2023.1.2</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.30.0</version>
+        <version>26.31.0</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
index 5df6cf6..2042fee 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDriver.java
@@ -191,6 +191,8 @@ public class JdbcDriver implements Driver {
         if (matcher.matches()) {
           // strip 'jdbc:' from the URL, add any extra properties and pass on to the generic
           // Connection API
+          // TODO: Remove when statement cache should be enabled by default.
+          System.setProperty("spanner.statement_cache_size_mb", "0");
           String connectionUri = appendPropertiesToUrl(url.substring(5), info);
           ConnectionOptions options = ConnectionOptions.newBuilder().setUri(connectionUri).build();
           JdbcConnection connection = new JdbcConnection(url, options);
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 60cc3e5..1db0c40 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
@@ -153,6 +153,11 @@ class JdbcStatement extends AbstractJdbcStatement implements CloudSpannerJdbcSta
             "Result does not contain an update count", Code.FAILED_PRECONDITION);
       }
       return updateCount;
+    } catch (UnsupportedOperationException unsupportedOperationException) {
+      throw JdbcSqlExceptionFactory.of(
+          unsupportedOperationException.getMessage(),
+          Code.FAILED_PRECONDITION,
+          unsupportedOperationException);
     } finally {
       resultSet.close();
     }
diff --git a/versions.txt b/versions.txt
index 1378e8c..2a90830 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-spanner-jdbc:2.15.1:2.15.2-SNAPSHOT
+google-cloud-spanner-jdbc:2.15.3:2.15.4-SNAPSHOT


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ExecuteMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ExecuteMockServerTest.java
index 65a54a8..d85c4f8 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/ExecuteMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/ExecuteMockServerTest.java
@@ -48,6 +48,7 @@ import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.Objects;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.function.ThrowingRunnable;
@@ -63,6 +64,7 @@ import org.junit.runners.Parameterized.Parameters;
  */
 @RunWith(Parameterized.class)
 public class ExecuteMockServerTest extends AbstractMockServerTest {
+  private static Dialect currentDialect;
 
   @Parameters(name = "dialect = {0}")
   public static Object[] parameters() {
@@ -78,112 +80,123 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   private String dml;
   private String largeDml;
   private String dmlReturning;
+  private String clientSideQuery;
+  private String clientSideUpdate;
 
   @Before
   public void setupResults() {
-    query =
-        dialect == Dialect.POSTGRESQL
-            ? "/*@ lock_scanned_ranges = exclusive */ select * from my_table"
-            : "select * from my_table";
-    dml =
-        dialect == Dialect.POSTGRESQL
-            ? "/*@ lock_scanned_ranges = exclusive */ insert into my_table (id, value) values (1, 'One')"
-            : "insert into my_table (id, value) values (1, 'One')";
-    String DML_THEN_RETURN_ID =
-        dml + (dialect == Dialect.POSTGRESQL ? "\nRETURNING \"id\"" : "\nTHEN RETURN `id`");
-    largeDml =
-        dialect == Dialect.POSTGRESQL
-            ? "/*@ lock_scanned_ranges = exclusive */ update my_table set value='new value' where true"
-            : "update my_table set value='new value' where true";
-    String LARGE_DML_THEN_RETURN_ID =
-        largeDml + (dialect == Dialect.POSTGRESQL ? "\nRETURNING \"id\"" : "\nTHEN RETURN `id`");
-    dmlReturning =
-        dialect == Dialect.POSTGRESQL
-            ? "/*@ lock_scanned_ranges = exclusive */ insert into my_table (id, value) values (1, 'One') RETURNING *"
-            : "insert into my_table (id, value) values (1, 'One') THEN RETURN *";
-
-    // This forces a refresh of the Spanner instance that is used for a connection, which again is
-    // needed in order to refresh the dialect of the database.
-    SpannerPool.closeSpannerPool();
-    mockSpanner.putStatementResult(StatementResult.detectDialectResult(dialect));
-    super.setupResults();
-
-    com.google.spanner.v1.ResultSet resultSet =
-        com.google.spanner.v1.ResultSet.newBuilder()
-            .setMetadata(
-                ResultSetMetadata.newBuilder()
-                    .setRowType(
-                        StructType.newBuilder()
-                            .addFields(
-                                Field.newBuilder()
-                                    .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
-                                    .setName("id")
-                                    .build())
-                            .addFields(
-                                Field.newBuilder()
-                                    .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
-                                    .setName("value")
-                                    .build())
-                            .build())
-                    .build())
-            .addRows(
-                ListValue.newBuilder()
-                    .addValues(Value.newBuilder().setStringValue("1").build())
-                    .addValues(Value.newBuilder().setStringValue("One").build())
-                    .build())
-            .build();
-    com.google.spanner.v1.ResultSet returnIdResultSet =
-        com.google.spanner.v1.ResultSet.newBuilder()
-            .setMetadata(
-                ResultSetMetadata.newBuilder()
-                    .setRowType(
-                        StructType.newBuilder()
-                            .addFields(
-                                Field.newBuilder()
-                                    .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
-                                    .setName("id")
-                                    .build())
-                            .build())
-                    .build())
-            .addRows(
-                ListValue.newBuilder()
-                    .addValues(Value.newBuilder().setStringValue("1").build())
-                    .build())
-            .build();
-    mockSpanner.putStatementResult(
-        StatementResult.query(com.google.cloud.spanner.Statement.of(query), resultSet));
-    mockSpanner.putStatementResult(
-        StatementResult.update(com.google.cloud.spanner.Statement.of(dml), 1L));
-    mockSpanner.putStatementResult(
-        StatementResult.update(
-            com.google.cloud.spanner.Statement.of(largeDml), LARGE_UPDATE_COUNT));
-    mockSpanner.putStatementResult(
-        StatementResult.query(
-            com.google.cloud.spanner.Statement.of(dmlReturning),
-            resultSet
-                .toBuilder()
-                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
-                .build()));
-    mockSpanner.putStatementResult(
-        StatementResult.query(
-            com.google.cloud.spanner.Statement.of(DML_THEN_RETURN_ID),
-            returnIdResultSet
-                .toBuilder()
-                .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
-                .build()));
-    mockSpanner.putStatementResult(
-        StatementResult.query(
-            com.google.cloud.spanner.Statement.of(LARGE_DML_THEN_RETURN_ID),
-            returnIdResultSet
-                .toBuilder()
-                .setStats(ResultSetStats.newBuilder().setRowCountExact(LARGE_UPDATE_COUNT).build())
-                .build()));
-    mockDatabaseAdmin.addResponse(
-        Operation.newBuilder()
-            .setDone(true)
-            .setResponse(Any.pack(Empty.getDefaultInstance()))
-            .setMetadata(Any.pack(UpdateDatabaseDdlMetadata.getDefaultInstance()))
-            .build());
+    // Only register new results if the dialect has changed.
+    if (!Objects.equals(currentDialect, dialect)) {
+      query =
+          dialect == Dialect.POSTGRESQL
+              ? "/*@ lock_scanned_ranges = exclusive */ select * from my_table"
+              : "select * from my_table";
+      dml =
+          dialect == Dialect.POSTGRESQL
+              ? "/*@ lock_scanned_ranges = exclusive */ insert into my_table (id, value) values (1, 'One')"
+              : "insert into my_table (id, value) values (1, 'One')";
+      String DML_THEN_RETURN_ID =
+          dml + (dialect == Dialect.POSTGRESQL ? "\nRETURNING \"id\"" : "\nTHEN RETURN `id`");
+      largeDml =
+          dialect == Dialect.POSTGRESQL
+              ? "/*@ lock_scanned_ranges = exclusive */ update my_table set value='new value' where true"
+              : "update my_table set value='new value' where true";
+      String LARGE_DML_THEN_RETURN_ID =
+          largeDml + (dialect == Dialect.POSTGRESQL ? "\nRETURNING \"id\"" : "\nTHEN RETURN `id`");
+      dmlReturning =
+          dialect == Dialect.POSTGRESQL
+              ? "/*@ lock_scanned_ranges = exclusive */ insert into my_table (id, value) values (1, 'One') RETURNING *"
+              : "insert into my_table (id, value) values (1, 'One') THEN RETURN *";
+      clientSideQuery =
+          dialect == Dialect.POSTGRESQL ? "show spanner.readonly" : "show variable readonly";
+      clientSideUpdate =
+          dialect == Dialect.POSTGRESQL ? "set spanner.readonly=false" : "set readonly=false";
+
+      // This forces a refresh of the Spanner instance that is used for a connection, which again is
+      // needed in order to refresh the dialect of the database.
+      SpannerPool.closeSpannerPool();
+      mockSpanner.putStatementResult(StatementResult.detectDialectResult(dialect));
+
+      super.setupResults();
+
+      com.google.spanner.v1.ResultSet resultSet =
+          com.google.spanner.v1.ResultSet.newBuilder()
+              .setMetadata(
+                  ResultSetMetadata.newBuilder()
+                      .setRowType(
+                          StructType.newBuilder()
+                              .addFields(
+                                  Field.newBuilder()
+                                      .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                      .setName("id")
+                                      .build())
+                              .addFields(
+                                  Field.newBuilder()
+                                      .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                                      .setName("value")
+                                      .build())
+                              .build())
+                      .build())
+              .addRows(
+                  ListValue.newBuilder()
+                      .addValues(Value.newBuilder().setStringValue("1").build())
+                      .addValues(Value.newBuilder().setStringValue("One").build())
+                      .build())
+              .build();
+      com.google.spanner.v1.ResultSet returnIdResultSet =
+          com.google.spanner.v1.ResultSet.newBuilder()
+              .setMetadata(
+                  ResultSetMetadata.newBuilder()
+                      .setRowType(
+                          StructType.newBuilder()
+                              .addFields(
+                                  Field.newBuilder()
+                                      .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                      .setName("id")
+                                      .build())
+                              .build())
+                      .build())
+              .addRows(
+                  ListValue.newBuilder()
+                      .addValues(Value.newBuilder().setStringValue("1").build())
+                      .build())
+              .build();
+      mockSpanner.putStatementResult(
+          StatementResult.query(com.google.cloud.spanner.Statement.of(query), resultSet));
+      mockSpanner.putStatementResult(
+          StatementResult.update(com.google.cloud.spanner.Statement.of(dml), 1L));
+      mockSpanner.putStatementResult(
+          StatementResult.update(
+              com.google.cloud.spanner.Statement.of(largeDml), LARGE_UPDATE_COUNT));
+      mockSpanner.putStatementResult(
+          StatementResult.query(
+              com.google.cloud.spanner.Statement.of(dmlReturning),
+              resultSet
+                  .toBuilder()
+                  .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                  .build()));
+      mockSpanner.putStatementResult(
+          StatementResult.query(
+              com.google.cloud.spanner.Statement.of(DML_THEN_RETURN_ID),
+              returnIdResultSet
+                  .toBuilder()
+                  .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
+                  .build()));
+      mockSpanner.putStatementResult(
+          StatementResult.query(
+              com.google.cloud.spanner.Statement.of(LARGE_DML_THEN_RETURN_ID),
+              returnIdResultSet
+                  .toBuilder()
+                  .setStats(
+                      ResultSetStats.newBuilder().setRowCountExact(LARGE_UPDATE_COUNT).build())
+                  .build()));
+      mockDatabaseAdmin.addResponse(
+          Operation.newBuilder()
+              .setDone(true)
+              .setResponse(Any.pack(Empty.getDefaultInstance()))
+              .setMetadata(Any.pack(UpdateDatabaseDdlMetadata.getDefaultInstance()))
+              .build());
+    }
   }
 
   private String createUrl() {
@@ -206,9 +219,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       try (ResultSet resultSet = statement.executeQuery(dmlReturning)) {
         verifyResultSet(resultSet);
       }
+      try (ResultSet resultSet = statement.executeQuery(clientSideQuery)) {
+        verifyClientSideResultSet(resultSet);
+      }
       verifyException(() -> statement.executeQuery(dml));
       verifyException(() -> statement.executeQuery(largeDml));
       verifyException(() -> statement.executeQuery(DDL));
+      verifyException(() -> statement.executeQuery(clientSideUpdate));
     }
   }
 
@@ -218,9 +235,15 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1, statement.executeUpdate(dml));
       assertEquals(0, statement.executeUpdate(DDL));
+
+      connection.setAutoCommit(false);
+      assertEquals(0, statement.executeUpdate(clientSideUpdate));
+      connection.setAutoCommit(true);
+
       verifyOverflow(() -> statement.executeUpdate(largeDml));
       verifyException(() -> statement.executeUpdate(query));
       verifyException(() -> statement.executeUpdate(dmlReturning));
+      verifyException(() -> statement.executeUpdate(clientSideQuery));
     }
   }
 
@@ -231,9 +254,12 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       // TODO: Add tests for RETURN_GENERATED_KEYS when that is supported.
       assertEquals(1, statement.executeUpdate(dml, Statement.NO_GENERATED_KEYS));
       assertEquals(0, statement.executeUpdate(DDL, Statement.NO_GENERATED_KEYS));
+      assertEquals(0, statement.executeUpdate(clientSideUpdate, Statement.NO_GENERATED_KEYS));
+
       verifyOverflow(() -> statement.executeUpdate(largeDml, Statement.NO_GENERATED_KEYS));
       verifyException(() -> statement.executeUpdate(query, Statement.NO_GENERATED_KEYS));
       verifyException(() -> statement.executeUpdate(dmlReturning, Statement.NO_GENERATED_KEYS));
+      verifyException(() -> statement.executeUpdate(clientSideQuery, Statement.NO_GENERATED_KEYS));
     }
   }
 
@@ -243,10 +269,15 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1, statement.executeUpdate(dml, new String[] {"id"}));
       assertEquals(0, statement.executeUpdate(DDL, new String[] {"id"}));
+      assertEquals(0, statement.executeUpdate(clientSideUpdate, new String[] {"id"}));
+
       verifyOverflow(() -> statement.executeUpdate(largeDml, new String[] {"id"}));
       verifyException(
           () -> statement.executeUpdate(query, new String[] {"id"}), Code.FAILED_PRECONDITION);
       assertEquals(1, statement.executeUpdate(dmlReturning, new String[] {"id"}));
+      verifyException(
+          () -> statement.executeUpdate(clientSideQuery, new String[] {"id"}),
+          Code.FAILED_PRECONDITION);
     }
   }
 
@@ -256,9 +287,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1, statement.executeUpdate(dml, new int[] {1}));
       assertEquals(0, statement.executeUpdate(DDL, new int[] {1}));
+      assertEquals(0, statement.executeUpdate(clientSideUpdate, new int[] {1}));
       verifyOverflow(() -> statement.executeUpdate(largeDml, new int[] {1}));
       verifyException(() -> statement.executeUpdate(query, new int[] {1}));
       verifyException(() -> statement.executeUpdate(dmlReturning, new int[] {1}));
+      verifyException(() -> statement.executeUpdate(clientSideQuery, new int[] {1}));
     }
   }
 
@@ -268,9 +301,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1L, statement.executeLargeUpdate(dml));
       assertEquals(0L, statement.executeLargeUpdate(DDL));
+      assertEquals(0L, statement.executeLargeUpdate(clientSideUpdate));
       assertEquals(LARGE_UPDATE_COUNT, statement.executeLargeUpdate(largeDml));
       verifyException(() -> statement.executeLargeUpdate(query));
       verifyException(() -> statement.executeLargeUpdate(dmlReturning));
+      verifyException(() -> statement.executeLargeUpdate(clientSideQuery));
     }
   }
 
@@ -281,11 +316,14 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       // TODO: Add tests for RETURN_GENERATED_KEYS when that is supported.
       assertEquals(1, statement.executeLargeUpdate(dml, Statement.NO_GENERATED_KEYS));
       assertEquals(0, statement.executeLargeUpdate(DDL, Statement.NO_GENERATED_KEYS));
+      assertEquals(0, statement.executeLargeUpdate(clientSideUpdate, Statement.NO_GENERATED_KEYS));
       assertEquals(
           LARGE_UPDATE_COUNT, statement.executeLargeUpdate(largeDml, Statement.NO_GENERATED_KEYS));
       verifyException(() -> statement.executeLargeUpdate(query, Statement.NO_GENERATED_KEYS));
       verifyException(
           () -> statement.executeLargeUpdate(dmlReturning, Statement.NO_GENERATED_KEYS));
+      verifyException(
+          () -> statement.executeLargeUpdate(clientSideQuery, Statement.NO_GENERATED_KEYS));
     }
   }
 
@@ -295,10 +333,14 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1, statement.executeLargeUpdate(dml, new String[] {"id"}));
       assertEquals(0, statement.executeLargeUpdate(DDL, new String[] {"id"}));
+      assertEquals(0, statement.executeLargeUpdate(clientSideUpdate, new String[] {"id"}));
       assertEquals(LARGE_UPDATE_COUNT, statement.executeLargeUpdate(largeDml, new String[] {"id"}));
       verifyException(
           () -> statement.executeLargeUpdate(query, new String[] {"id"}), Code.FAILED_PRECONDITION);
       assertEquals(1L, statement.executeLargeUpdate(dmlReturning, new String[] {"id"}));
+      verifyException(
+          () -> statement.executeLargeUpdate(clientSideQuery, new String[] {"id"}),
+          Code.FAILED_PRECONDITION);
     }
   }
 
@@ -308,9 +350,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
         Statement statement = connection.createStatement()) {
       assertEquals(1, statement.executeLargeUpdate(dml, new int[] {1}));
       assertEquals(0, statement.executeLargeUpdate(DDL, new int[] {1}));
+      assertEquals(0, statement.executeLargeUpdate(clientSideUpdate, new int[] {1}));
       assertEquals(LARGE_UPDATE_COUNT, statement.executeLargeUpdate(largeDml, new int[] {1}));
       verifyException(() -> statement.executeLargeUpdate(query, new int[] {1}));
       verifyException(() -> statement.executeLargeUpdate(dmlReturning, new int[] {1}));
+      verifyException(() -> statement.executeLargeUpdate(clientSideQuery, new int[] {1}));
     }
   }
 
@@ -321,8 +365,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       verifyUpdateCount(statement, () -> statement.execute(dml), 1L);
       verifyUpdateCount(statement, () -> statement.execute(largeDml), LARGE_UPDATE_COUNT);
       verifyUpdateCount(statement, () -> statement.execute(DDL), Statement.SUCCESS_NO_INFO);
+      verifyUpdateCount(
+          statement, () -> statement.execute(clientSideUpdate), Statement.SUCCESS_NO_INFO);
       verifyResultSet(statement, () -> statement.execute(query));
       verifyResultSet(statement, () -> statement.execute(dmlReturning));
+      verifyResultSet(statement, () -> statement.execute(clientSideQuery));
     }
   }
 
@@ -340,9 +387,15 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           statement,
           () -> statement.execute(DDL, Statement.NO_GENERATED_KEYS),
           Statement.SUCCESS_NO_INFO);
+      verifyUpdateCount(
+          statement,
+          () -> statement.execute(clientSideUpdate, Statement.NO_GENERATED_KEYS),
+          Statement.SUCCESS_NO_INFO);
       verifyResultSet(statement, () -> statement.execute(query, Statement.NO_GENERATED_KEYS));
       verifyResultSet(
           statement, () -> statement.execute(dmlReturning, Statement.NO_GENERATED_KEYS));
+      verifyResultSet(
+          statement, () -> statement.execute(clientSideQuery, Statement.NO_GENERATED_KEYS));
     }
   }
 
@@ -355,8 +408,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           statement, () -> statement.execute(largeDml, new String[] {"id"}), LARGE_UPDATE_COUNT);
       verifyUpdateCount(
           statement, () -> statement.execute(DDL, new String[] {"id"}), Statement.SUCCESS_NO_INFO);
+      verifyUpdateCount(
+          statement,
+          () -> statement.execute(clientSideUpdate, new String[] {"id"}),
+          Statement.SUCCESS_NO_INFO);
       verifyResultSet(statement, () -> statement.execute(query, new String[] {"id"}));
       verifyResultSet(statement, () -> statement.execute(dmlReturning, new String[] {"id"}));
+      verifyResultSet(statement, () -> statement.execute(clientSideQuery, new String[] {"id"}));
     }
   }
 
@@ -369,8 +427,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           statement, () -> statement.execute(largeDml, new int[] {1}), LARGE_UPDATE_COUNT);
       verifyUpdateCount(
           statement, () -> statement.execute(DDL, new int[] {1}), Statement.SUCCESS_NO_INFO);
+      verifyUpdateCount(
+          statement,
+          () -> statement.execute(clientSideUpdate, new int[] {1}),
+          Statement.SUCCESS_NO_INFO);
       verifyResultSet(statement, () -> statement.execute(query, new int[] {1}));
       verifyResultSet(statement, () -> statement.execute(dmlReturning, new int[] {1}));
+      verifyResultSet(statement, () -> statement.execute(clientSideQuery, new int[] {1}));
     }
   }
 
@@ -383,9 +446,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       try (ResultSet resultSet = connection.prepareStatement(dmlReturning).executeQuery()) {
         verifyResultSet(resultSet);
       }
+      try (ResultSet resultSet = connection.prepareStatement(clientSideQuery).executeQuery()) {
+        verifyClientSideResultSet(resultSet);
+      }
       verifyException(() -> connection.prepareStatement(dml).executeQuery());
       verifyException(() -> connection.prepareStatement(largeDml).executeQuery());
       verifyException(() -> connection.prepareStatement(DDL).executeQuery());
+      verifyException(() -> connection.prepareStatement(clientSideUpdate).executeQuery());
     }
   }
 
@@ -393,13 +460,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementExecuteUpdate() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1, connection.prepareStatement(dml).executeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL).executeUpdate());
+      assertEquals(0, connection.prepareStatement(DDL).executeUpdate());
+      assertEquals(0, connection.prepareStatement(clientSideUpdate).executeUpdate());
       verifyOverflow(() -> connection.prepareStatement(largeDml).executeUpdate());
       verifyException(() -> connection.prepareStatement(query).executeUpdate());
       verifyException(
           () -> connection.prepareStatement(dmlReturning).executeUpdate(), Code.INVALID_ARGUMENT);
+      verifyException(() -> connection.prepareStatement(clientSideQuery).executeUpdate());
     }
   }
 
@@ -409,10 +476,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       // TODO: Add tests for RETURN_GENERATED_KEYS when that is supported.
       assertEquals(
           1, connection.prepareStatement(dml, Statement.NO_GENERATED_KEYS).executeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL,
-      // Statement.NO_GENERATED_KEYS).executeUpdate());
+      assertEquals(
+          0, connection.prepareStatement(DDL, Statement.NO_GENERATED_KEYS).executeUpdate());
+      assertEquals(
+          0,
+          connection
+              .prepareStatement(clientSideUpdate, Statement.NO_GENERATED_KEYS)
+              .executeUpdate());
       verifyOverflow(
           () -> connection.prepareStatement(largeDml, Statement.NO_GENERATED_KEYS).executeUpdate());
       verifyException(
@@ -423,6 +493,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
                   .prepareStatement(dmlReturning, Statement.NO_GENERATED_KEYS)
                   .executeUpdate(),
           Code.INVALID_ARGUMENT);
+      verifyException(
+          () ->
+              connection
+                  .prepareStatement(clientSideQuery, Statement.NO_GENERATED_KEYS)
+                  .executeUpdate());
     }
   }
 
@@ -430,9 +505,9 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementExecuteUpdateReturnColumnNames() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1, connection.prepareStatement(dml, new String[] {"id"}).executeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL, new String[] {"id"}).executeUpdate());
+      assertEquals(0, connection.prepareStatement(DDL, new String[] {"id"}).executeUpdate());
+      assertEquals(
+          0, connection.prepareStatement(clientSideUpdate, new String[] {"id"}).executeUpdate());
       verifyOverflow(
           () -> connection.prepareStatement(largeDml, new String[] {"id"}).executeUpdate());
       verifyException(
@@ -440,6 +515,9 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           Code.FAILED_PRECONDITION);
       assertEquals(
           1, connection.prepareStatement(dmlReturning, new String[] {"id"}).executeUpdate());
+      verifyException(
+          () -> connection.prepareStatement(clientSideQuery, new String[] {"id"}).executeUpdate(),
+          Code.FAILED_PRECONDITION);
     }
   }
 
@@ -447,14 +525,15 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementExecuteUpdateReturnColumnIndexes() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1, connection.prepareStatement(dml, new int[] {1}).executeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL, new int[] {1}).executeUpdate());
+      assertEquals(0, connection.prepareStatement(DDL, new int[] {1}).executeUpdate());
+      assertEquals(0, connection.prepareStatement(clientSideUpdate, new int[] {1}).executeUpdate());
       verifyOverflow(() -> connection.prepareStatement(largeDml, new int[] {1}).executeUpdate());
       verifyException(() -> connection.prepareStatement(query, new int[] {1}).executeUpdate());
       verifyException(
           () -> connection.prepareStatement(dmlReturning, new int[] {1}).executeUpdate(),
           Code.INVALID_ARGUMENT);
+      verifyException(
+          () -> connection.prepareStatement(clientSideQuery, new int[] {1}).executeUpdate());
     }
   }
 
@@ -462,14 +541,14 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementLargeExecuteUpdate() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1L, connection.prepareStatement(dml).executeLargeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0L, connection.prepareStatement(DDL).executeLargeUpdate());
+      assertEquals(0L, connection.prepareStatement(DDL).executeLargeUpdate());
+      assertEquals(0L, connection.prepareStatement(clientSideUpdate).executeLargeUpdate());
       assertEquals(LARGE_UPDATE_COUNT, connection.prepareStatement(largeDml).executeLargeUpdate());
       verifyException(() -> connection.prepareStatement(query).executeLargeUpdate());
       verifyException(
           () -> connection.prepareStatement(dmlReturning).executeLargeUpdate(),
           Code.INVALID_ARGUMENT);
+      verifyException(() -> connection.prepareStatement(clientSideQuery).executeLargeUpdate());
     }
   }
 
@@ -479,10 +558,13 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       // TODO: Add tests for RETURN_GENERATED_KEYS when that is supported.
       assertEquals(
           1, connection.prepareStatement(dml, Statement.NO_GENERATED_KEYS).executeLargeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL,
-      // Statement.NO_GENERATED_KEYS).executeLargeUpdate());
+      assertEquals(
+          0, connection.prepareStatement(DDL, Statement.NO_GENERATED_KEYS).executeLargeUpdate());
+      assertEquals(
+          0,
+          connection
+              .prepareStatement(clientSideUpdate, Statement.NO_GENERATED_KEYS)
+              .executeLargeUpdate());
       assertEquals(
           LARGE_UPDATE_COUNT,
           connection.prepareStatement(largeDml, Statement.NO_GENERATED_KEYS).executeLargeUpdate());
@@ -495,6 +577,11 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
                   .prepareStatement(dmlReturning, Statement.NO_GENERATED_KEYS)
                   .executeLargeUpdate(),
           Code.INVALID_ARGUMENT);
+      verifyException(
+          () ->
+              connection
+                  .prepareStatement(clientSideQuery, Statement.NO_GENERATED_KEYS)
+                  .executeLargeUpdate());
     }
   }
 
@@ -502,10 +589,10 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementExecuteLargeUpdateReturnColumnNames() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1, connection.prepareStatement(dml, new String[] {"id"}).executeLargeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL, new String[]
-      // {"id"}).executeLargeUpdate());
+      assertEquals(0, connection.prepareStatement(DDL, new String[] {"id"}).executeLargeUpdate());
+      assertEquals(
+          0,
+          connection.prepareStatement(clientSideUpdate, new String[] {"id"}).executeLargeUpdate());
       assertEquals(
           LARGE_UPDATE_COUNT,
           connection.prepareStatement(largeDml, new String[] {"id"}).executeLargeUpdate());
@@ -514,6 +601,12 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           Code.FAILED_PRECONDITION);
       assertEquals(
           1L, connection.prepareStatement(dmlReturning, new String[] {"id"}).executeLargeUpdate());
+      verifyException(
+          () ->
+              connection
+                  .prepareStatement(clientSideQuery, new String[] {"id"})
+                  .executeLargeUpdate(),
+          Code.FAILED_PRECONDITION);
     }
   }
 
@@ -521,9 +614,9 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
   public void testPreparedStatementExecuteLargeUpdateReturnColumnIndexes() throws SQLException {
     try (Connection connection = createConnection()) {
       assertEquals(1, connection.prepareStatement(dml, new int[] {1}).executeLargeUpdate());
-      // TODO: Enable the next statement once PreparedStatement supports executing DDL through the
-      //       executeUpdate() method.
-      // assertEquals(0, connection.prepareStatement(DDL, new int[] {1}).executeLargeUpdate());
+      assertEquals(0, connection.prepareStatement(DDL, new int[] {1}).executeLargeUpdate());
+      assertEquals(
+          0, connection.prepareStatement(clientSideUpdate, new int[] {1}).executeLargeUpdate());
       assertEquals(
           LARGE_UPDATE_COUNT,
           connection.prepareStatement(largeDml, new int[] {1}).executeLargeUpdate());
@@ -531,6 +624,8 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
       verifyException(
           () -> connection.prepareStatement(dmlReturning, new int[] {1}).executeLargeUpdate(),
           Code.INVALID_ARGUMENT);
+      verifyException(
+          () -> connection.prepareStatement(clientSideQuery, new int[] {1}).executeLargeUpdate());
     }
   }
 
@@ -542,9 +637,15 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           connection.prepareStatement(largeDml), PreparedStatement::execute, LARGE_UPDATE_COUNT);
       verifyPreparedUpdateCount(
           connection.prepareStatement(DDL), PreparedStatement::execute, Statement.SUCCESS_NO_INFO);
+      verifyPreparedUpdateCount(
+          connection.prepareStatement(clientSideUpdate),
+          PreparedStatement::execute,
+          Statement.SUCCESS_NO_INFO);
       verifyPreparedResultSet(connection.prepareStatement(query), PreparedStatement::execute);
       verifyPreparedResultSet(
           connection.prepareStatement(dmlReturning), PreparedStatement::execute);
+      verifyPreparedResultSet(
+          connection.prepareStatement(clientSideQuery), PreparedStatement::execute);
     }
   }
 
@@ -564,12 +665,19 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           connection.prepareStatement(DDL, Statement.NO_GENERATED_KEYS),
           PreparedStatement::execute,
           Statement.SUCCESS_NO_INFO);
+      verifyPreparedUpdateCount(
+          connection.prepareStatement(clientSideUpdate, Statement.NO_GENERATED_KEYS),
+          PreparedStatement::execute,
+          Statement.SUCCESS_NO_INFO);
       verifyPreparedResultSet(
           connection.prepareStatement(query, Statement.NO_GENERATED_KEYS),
           PreparedStatement::execute);
       verifyPreparedResultSet(
           connection.prepareStatement(dmlReturning, Statement.NO_GENERATED_KEYS),
           PreparedStatement::execute);
+      verifyPreparedResultSet(
+          connection.prepareStatement(clientSideQuery, Statement.NO_GENERATED_KEYS),
+          PreparedStatement::execute);
     }
   }
 
@@ -586,11 +694,18 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           connection.prepareStatement(DDL, new String[] {"id"}),
           PreparedStatement::execute,
           Statement.SUCCESS_NO_INFO);
+      verifyPreparedUpdateCount(
+          connection.prepareStatement(clientSideUpdate, new String[] {"id"}),
+          PreparedStatement::execute,
+          Statement.SUCCESS_NO_INFO);
       verifyPreparedResultSet(
           connection.prepareStatement(query, new String[] {"id"}), PreparedStatement::execute);
       verifyPreparedResultSet(
           connection.prepareStatement(dmlReturning, new String[] {"id"}),
           PreparedStatement::execute);
+      verifyPreparedResultSet(
+          connection.prepareStatement(clientSideQuery, new String[] {"id"}),
+          PreparedStatement::execute);
     }
   }
 
@@ -607,13 +722,27 @@ public class ExecuteMockServerTest extends AbstractMockServerTest {
           connection.prepareStatement(DDL, new int[] {1}),
           PreparedStatement::execute,
           Statement.SUCCESS_NO_INFO);
+      verifyPreparedUpdateCount(
+          connection.prepareStatement(clientSideUpdate, new int[] {1}),
+          PreparedStatement::execute,
+          Statement.SUCCESS_NO_INFO);
       verifyPreparedResultSet(
           connection.prepareStatement(query, new int[] {1}), PreparedStatement::execute);
       verifyPreparedResultSet(
           connection.prepareStatement(dmlReturning, new int[] {1}), PreparedStatement::execute);
+      verifyPreparedResultSet(
+          connection.prepareStatement(clientSideQuery, new int[] {1}), PreparedStatement::execute);
     }
   }
 
+  private void verifyClientSideResultSet(ResultSet resultSet) throws SQLException {
+    assertNotNull(resultSet.getMetaData());
+    assertEquals(1, resultSet.getMetaData().getColumnCount());
+    assertTrue(resultSet.next());
+    assertFalse(resultSet.getBoolean(1));
+    assertFalse(resultSet.next());
+  }
+
   private void verifyResultSet(ResultSet resultSet) throws SQLException {
     assertNotNull(resultSet.getMetaData());
     assertEquals(2, resultSet.getMetaData().getColumnCount());
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 7e4e749..7144bfa 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/PartitionedQueryMockServerTest.java
@@ -158,7 +158,7 @@ public class PartitionedQueryMockServerTest extends AbstractMockServerTest {
         partitionStatement.setBoolean(1, true);
         try (ResultSet results = partitionStatement.executeQuery()) {
           assertNotNull(results.getMetaData());
-          assertEquals(18, results.getMetaData().getColumnCount());
+          assertEquals(22, results.getMetaData().getColumnCount());
           int rowCount = 0;
           while (results.next()) {
             rowCount++;
@@ -376,7 +376,7 @@ public class PartitionedQueryMockServerTest extends AbstractMockServerTest {
       try (ResultSet results =
           connection.createStatement().executeQuery("select * from my_table where active=true")) {
         assertNotNull(results.getMetaData());
-        assertEquals(18, results.getMetaData().getColumnCount());
+        assertEquals(22, results.getMetaData().getColumnCount());
         int rowCount = 0;
         while (results.next()) {
           rowCount++;
@@ -482,7 +482,7 @@ public class PartitionedQueryMockServerTest extends AbstractMockServerTest {
       try (ResultSet results =
           connection.createStatement().executeQuery("select * from my_table where active=true")) {
         assertNotNull(results.getMetaData());
-        assertEquals(18, results.getMetaData().getColumnCount());
+        assertEquals(22, results.getMetaData().getColumnCount());
         int rowCount = 0;
         while (results.next()) {
           rowCount++;
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java
index 119897a..db24540 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java
@@ -491,7 +491,8 @@ public class PgNumericResultSetTest {
           resultSetMatcherFrom(resultSet, ResultSet::getBytes, ResultSet::getBytes);
 
       matcher.nextAndAssertError(
-          IllegalStateException.class, "expected BYTES but was NUMERIC<PG_NUMERIC>");
+          IllegalStateException.class,
+          "expected one of [[PROTO, BYTES]] but was NUMERIC<PG_NUMERIC>");
       matcher.nextAndAssertEquals(null);
     }
   }
@@ -600,7 +601,8 @@ public class PgNumericResultSetTest {
           resultSetMatcherFrom(resultSet, ResultSet::getBlob, ResultSet::getBlob);
 
       matcher.nextAndAssertError(
-          IllegalStateException.class, "expected BYTES but was NUMERIC<PG_NUMERIC>");
+          IllegalStateException.class,
+          "expected one of [[PROTO, BYTES]] but was NUMERIC<PG_NUMERIC>");
       matcher.nextAndAssertEquals(null);
     }
   }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/SavepointMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/SavepointMockServerTest.java
index 2c5d518..d8b438a 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/SavepointMockServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/SavepointMockServerTest.java
@@ -28,7 +28,6 @@ import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.connection.AbstractMockServerTest;
 import com.google.cloud.spanner.connection.RandomResultSetGenerator;
 import com.google.cloud.spanner.connection.SavepointSupport;
-import com.google.cloud.spanner.connection.SpannerPool;
 import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcAbortedDueToConcurrentModificationException;
 import com.google.common.base.Strings;
 import com.google.protobuf.AbstractMessage;
@@ -43,6 +42,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Savepoint;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 import org.junit.After;
 import org.junit.Before;
@@ -54,6 +54,8 @@ import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class SavepointMockServerTest extends AbstractMockServerTest {
+  private static Dialect currentDialect;
+
   @Parameter public Dialect dialect;
 
   @Parameters(name = "dialect = {0}")
@@ -63,19 +65,24 @@ public class SavepointMockServerTest extends AbstractMockServerTest {
 
   @Before
   public void setupDialect() {
-    mockSpanner.putStatementResult(StatementResult.detectDialectResult(this.dialect));
+    // Only reset the dialect result when it has actually changed. This prevents the SpannerPool
+    // from being closed after each test, which again makes the test slower.
+    if (!Objects.equals(this.dialect, currentDialect)) {
+      currentDialect = this.dialect;
+      mockSpanner.putStatementResult(StatementResult.detectDialectResult(this.dialect));
+      SpannerPool.closeSpannerPool();
+    }
   }
 
   @After
   public void clearRequests() {
     mockSpanner.clearRequests();
-    SpannerPool.closeSpannerPool();
   }
 
   private String createUrl() {
     return String.format(
         "jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s?usePlainText=true;autoCommit=false",
-        getPort(), "proj", "inst", "db");
+        getPort(), "proj", "inst", "db" + (dialect == Dialect.POSTGRESQL ? "pg" : ""));
   }
 
   private Connection createConnection() throws SQLException {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/TagMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/TagMockServerTest.java
new file mode 100644
index 0000000..5c626c0
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/jdbc/TagMockServerTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.jdbc;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.connection.AbstractMockServerTest;
+import com.google.spanner.v1.ExecuteBatchDmlRequest;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TagMockServerTest extends AbstractMockServerTest {
+  @Parameter public Dialect dialect;
+
+  @Parameters(name = "dialect = {0}")
+  public static Object[] data() {
+    return Dialect.values();
+  }
+
+  @Before
+  public void setupDialect() {
+    mockSpanner.putStatementResult(StatementResult.detectDialectResult(this.dialect));
+  }
+
+  @After
+  public void clearRequests() {
+    mockSpanner.clearRequests();
+  }
+
+  private String getVariablePrefix() {
+    return dialect == Dialect.POSTGRESQL ? "spanner." : "";
+  }
+
+  private String createUrl() {
+    return String.format(
+        "jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s?usePlainText=true;autoCommit=false",
+        getPort(), "proj", "inst", "db" + (dialect == Dialect.POSTGRESQL ? "pg" : ""));
+  }
+
+  private Connection createConnection() throws SQLException {
+    return DriverManager.getConnection(createUrl());
+  }
+
+  @Test
+  public void testStatementTagForQuery() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection
+          .createStatement()
+          .execute(String.format("set %sstatement_tag='my-tag'", getVariablePrefix()));
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery(SELECT_RANDOM_STATEMENT.getSql())) {
+        assertTrue(resultSet.next());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getRequestTag());
+
+      // Verify that the tag is cleared after having been used.
+      mockSpanner.clearRequests();
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery(SELECT_RANDOM_STATEMENT.getSql())) {
+        assertTrue(resultSet.next());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+      assertEquals("", request.getRequestOptions().getRequestTag());
+    }
+  }
+
+  @Test
+  public void testTransactionTagForQuery() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection
+          .createStatement()
+          .execute(String.format("set %stransaction_tag='my-tag'", getVariablePrefix()));
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery(SELECT_RANDOM_STATEMENT.getSql())) {
+        assertTrue(resultSet.next());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getTransactionTag());
+
+      // Verify that the tag is used for the entire transaction.
+      mockSpanner.clearRequests();
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery(SELECT_RANDOM_STATEMENT.getSql())) {
+        assertTrue(resultSet.next());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getTransactionTag());
+
+      // Verify that committing the transaction will remove the transaction tag.
+      connection.commit();
+
+      mockSpanner.clearRequests();
+      try (ResultSet resultSet =
+          connection.createStatement().executeQuery(SELECT_RANDOM_STATEMENT.getSql())) {
+        assertTrue(resultSet.next());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+      assertEquals("", request.getRequestOptions().getTransactionTag());
+    }
+  }
+
+  @Test
+  public void testStatementTagForBatchDml() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection
+          .createStatement()
+          .execute(String.format("set %sstatement_tag='my-tag'", getVariablePrefix()));
+
+      try (Statement statement = connection.createStatement()) {
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        assertArrayEquals(new int[] {1, 1}, statement.executeBatch());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+      ExecuteBatchDmlRequest request =
+          mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getRequestTag());
+
+      // Verify that the tag is cleared after having been used.
+      mockSpanner.clearRequests();
+      try (Statement statement = connection.createStatement()) {
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        assertArrayEquals(new int[] {1, 1}, statement.executeBatch());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+      assertEquals("", request.getRequestOptions().getRequestTag());
+    }
+  }
+
+  @Test
+  public void testTransactionTagForBatchDml() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection
+          .createStatement()
+          .execute(String.format("set %stransaction_tag='my-tag'", getVariablePrefix()));
+
+      try (Statement statement = connection.createStatement()) {
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        assertArrayEquals(new int[] {1, 1}, statement.executeBatch());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+      ExecuteBatchDmlRequest request =
+          mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getTransactionTag());
+
+      // Verify that the tag is used for the entire transaction.
+      mockSpanner.clearRequests();
+      try (Statement statement = connection.createStatement()) {
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        assertArrayEquals(new int[] {1, 1}, statement.executeBatch());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+      assertEquals("my-tag", request.getRequestOptions().getTransactionTag());
+
+      // Verify that committing the transaction will remove the transaction tag.
+      connection.commit();
+      mockSpanner.clearRequests();
+      try (Statement statement = connection.createStatement()) {
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        statement.addBatch(INSERT_STATEMENT.getSql());
+        assertArrayEquals(new int[] {1, 1}, statement.executeBatch());
+      }
+      assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
+      request = mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
+      assertEquals("", request.getRequestOptions().getTransactionTag());
+    }
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/TransactionMockServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/TransactionMockServerTest.java
new file mode 100644
index 0000000..a514995
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/jdbc/TransactionMockServerTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import com.google.cloud.spanner.connection.AbstractMockServerTest;
+import com.google.cloud.spanner.connection.SpannerPool;
+import com.google.spanner.v1.CommitRequest;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TransactionMockServerTest extends AbstractMockServerTest {
+
+  @AfterClass
+  public static void closeSpannerPool() {
+    SpannerPool.closeSpannerPool();
+  }
+
+  @After
+  public void clearRequests() {
+    mockSpanner.clearRequests();
+  }
+
+  private String createUrl() {
+    return String.format(
+        "jdbc:cloudspanner://localhost:%d/projects/%s/instances/%s/databases/%s?usePlainText=true;autoCommit=false",
+        getPort(), "proj", "inst", "db");
+  }
+
+  private Connection createConnection() throws SQLException {
+    return DriverManager.getConnection(createUrl());
+  }
+
+  @Test
+  public void testCommittingEmptyTransactionIsNoOp() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection.commit();
+    }
+
+    assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+
+  @Test
+  public void testCommittingEmptyExplicitTransactionIsNoOp() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection.setAutoCommit(true);
+      try (Statement statement = connection.createStatement()) {
+        statement.execute("begin transaction");
+        statement.execute("commit");
+      }
+    }
+
+    assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+
+  @Test
+  public void testRollingBackEmptyTransactionIsNoOp() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection.rollback();
+    }
+
+    assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+
+  @Test
+  public void testRollingBackEmptyExplicitTransactionIsNoOp() throws SQLException {
+    try (Connection connection = createConnection()) {
+      connection.setAutoCommit(true);
+      try (Statement statement = connection.createStatement()) {
+        statement.execute("begin transaction");
+        statement.execute("rollback");
+      }
+    }
+
+    assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
+  }
+}