Repository: googleapis/java-spanner-jdbc
Pull request #1445 (2023-12-15 16:26:53)

test: add tag tests
Adds a couple of tests for statement and transaction tags.


Patch:
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index d304652..dc05a72 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -1,4 +1,4 @@
-# Copyright 2023 Google LLC
+# 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.
@@ -13,5 +13,5 @@
 # limitations under the License.
 docker:
   image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest
-  digest: sha256:6f431774e11cc46619cf093fd1481193c4024031073697fa18f0099b943aab88
-# created: 2023-12-01T19:50:20.444857406Z
+  digest: sha256:a6aa751984f1e905c3ae5a3aac78fc7b68210626ce91487dc7ff4f0a06f010cc
+# created: 2024-01-22T14:14:20.913785597Z
diff --git a/.github/workflows/renovate_config_check.yaml b/.github/workflows/renovate_config_check.yaml
new file mode 100644
index 0000000..87d8eb2
--- /dev/null
+++ b/.github/workflows/renovate_config_check.yaml
@@ -0,0 +1,25 @@
+name: Renovate Bot Config Validation
+
+on:
+  pull_request:
+    paths:
+      - 'renovate.json'
+
+jobs:
+  renovate_bot_config_validation:
+    runs-on: ubuntu-22.04
+
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Set up Node.js
+      uses: actions/setup-node@v3
+      with:
+        node-version: '20'
+
+    - name: Install Renovate and Config Validator
+      run: |
+        npm install -g npm@latest
+        npm install --global renovate
+        renovate-config-validator
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 c5c11bb..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.2 \
-    --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
-    --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
-    --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
-    --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
-    --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
-    --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
-    --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
-    --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
-    --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
-    --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
-    --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
-    --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
-    --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
-    --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
-    --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
-    --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
-    --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
-    --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
-    --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
-    --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
-    --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
-    --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
-    --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
-    # 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 8fdf148..a0b5356 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,44 @@
 # Changelog
 
+## [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)
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.21.0 ([#1454](https://github.com/googleapis/java-spanner-jdbc/issues/1454)) ([d1c1d2c](https://github.com/googleapis/java-spanner-jdbc/commit/d1c1d2cf1ad86d987fc285eb97d9b9ca3c53f753))
+* Update dependency com.google.cloud:google-cloud-spanner-bom to v6.56.0 ([#1455](https://github.com/googleapis/java-spanner-jdbc/issues/1455)) ([90fad38](https://github.com/googleapis/java-spanner-jdbc/commit/90fad3870fb3b704d041d1a79f2e8fc6f32631d1))
+
+## [2.15.0](https://github.com/googleapis/java-spanner-jdbc/compare/v2.14.6...v2.15.0) (2023-12-22)
+
+
+### Features
+
+* Support PreparedStatement#getParameterMetaData() ([#1218](https://github.com/googleapis/java-spanner-jdbc/issues/1218)) ([721ff45](https://github.com/googleapis/java-spanner-jdbc/commit/721ff4552104efba47c19ef511282071c3b334c3))
+
+
+### Performance Improvements
+
+* Optimize isValid implementation ([#1444](https://github.com/googleapis/java-spanner-jdbc/issues/1444)) ([914e973](https://github.com/googleapis/java-spanner-jdbc/commit/914e973ad7fd638fabc3ec130b7618c51f01f401)), closes [#1443](https://github.com/googleapis/java-spanner-jdbc/issues/1443)
+
+
+### Dependencies
+
+* Update dependency org.postgresql:postgresql to v42.7.1 ([#1441](https://github.com/googleapis/java-spanner-jdbc/issues/1441)) ([5997555](https://github.com/googleapis/java-spanner-jdbc/commit/59975553826360b86492e50b9d49c29aecc28bab))
+
 ## [2.14.6](https://github.com/googleapis/java-spanner-jdbc/compare/v2.14.5...v2.14.6) (2023-12-04)
 
 
diff --git a/owlbot.py b/owlbot.py
index 05f32bb..0403a65 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -22,5 +22,7 @@ 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'
 ])
diff --git a/pom.xml b/pom.xml
index 90ac8a5..8fd009c 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.14.7-SNAPSHOT</version><!-- {x-version-update:google-cloud-spanner-jdbc:current} -->
+  <version>2.15.3-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>
@@ -14,7 +14,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.6.1</version>
+    <version>1.7.1</version>
   </parent>
   <developers>
     <developer>
@@ -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.1.5</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.55.0</version>
+        <version>6.57.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-shared-dependencies</artifactId>
-        <version>3.20.0</version>
+        <version>3.24.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index d71f8cf..ae742ca 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.14.6</version>
+      <version>2.15.2</version>
     </dependency>
     <!-- [END spanner-jdbc_install_without_bom] -->
 
@@ -42,7 +42,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.1.5</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 ce3483e..054e7d9 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.14.7-SNAPSHOT</version>
+      <version>2.15.3-SNAPSHOT</version>
     </dependency>
     <!-- {x-version-update-end} -->
 
@@ -41,7 +41,7 @@
     <dependency>
       <groupId>com.google.truth</groupId>
       <artifactId>truth</artifactId>
-      <version>1.1.5</version>
+      <version>1.4.0</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 8a87cff..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.27.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 d1dd574..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.27.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. -->
@@ -52,7 +52,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.0</version>
+      <version>42.7.1</version>
     </dependency>
 
     <dependency>
@@ -87,7 +87,7 @@
       <plugin>
         <groupId>com.spotify.fmt</groupId>
         <artifactId>fmt-maven-plugin</artifactId>
-        <version>2.21.1</version>
+        <version>2.22.1</version>
         <executions>
           <execution>
             <goals>
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 4c6d3f2..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
@@ -17,16 +17,22 @@
 package com.google.cloud.spanner.sample.entities;
 
 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 {
 
   /** This ID is generated using a (bit-reversed) sequence. */
   @Id private Long id;
 
-  private OffsetDateTime createdAt;
+  @CreatedDate private OffsetDateTime createdAt;
 
-  private OffsetDateTime updatedAt;
+  @LastModifiedDate private OffsetDateTime updatedAt;
+
+  @PersistenceCreator
+  public AbstractEntity() {}
 
   @Override
   public boolean equals(Object o) {
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 4dbd092..a86e50d 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -13,7 +13,7 @@
   <parent>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-parent</artifactId>
-    <version>3.1.5</version>
+    <version>3.2.2</version>
     <relativePath/>
   </parent>
   <properties>
@@ -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.27.0</version>
+        <version>26.31.0</version>
         <scope>import</scope>
         <type>pom</type>
       </dependency>
@@ -62,7 +62,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <version>42.7.0</version>
+      <version>42.7.1</version>
     </dependency>
 
     <dependency>
@@ -97,7 +97,7 @@
       <plugin>
         <groupId>com.spotify.fmt</groupId>
         <artifactId>fmt-maven-plugin</artifactId>
-        <version>2.21.1</version>
+        <version>2.22.1</version>
         <executions>
           <execution>
             <goals>
diff --git a/src/main/java/com/google/cloud/spanner/JdbcDataTypeConverter.java b/src/main/java/com/google/cloud/spanner/JdbcDataTypeConverter.java
new file mode 100644
index 0000000..62d52c6
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/JdbcDataTypeConverter.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import com.google.api.core.InternalApi;
+
+@InternalApi
+public class JdbcDataTypeConverter {
+
+  /** Converts a protobuf type to a Spanner type. */
+  @InternalApi
+  public static Type toSpannerType(com.google.spanner.v1.Type proto) {
+    return Type.fromProto(proto);
+  }
+}
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
index f577ac3..b56aed0 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.spanner.jdbc;
 
+import com.google.cloud.spanner.Dialect;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.Code;
 import com.google.common.base.Preconditions;
@@ -69,7 +70,74 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     }
   }
 
-  /** Extract Spanner type name from {@link java.sql.Types} code. */
+  static String getSpannerTypeName(Type type, Dialect dialect) {
+    // TODO: Use com.google.cloud.spanner.Type#getSpannerTypeName() when available.
+    Preconditions.checkNotNull(type);
+    switch (type.getCode()) {
+      case BOOL:
+        return dialect == Dialect.POSTGRESQL ? "boolean" : "BOOL";
+      case BYTES:
+        return dialect == Dialect.POSTGRESQL ? "bytea" : "BYTES";
+      case DATE:
+        return dialect == Dialect.POSTGRESQL ? "date" : "DATE";
+      case FLOAT64:
+        return dialect == Dialect.POSTGRESQL ? "double precision" : "FLOAT64";
+      case INT64:
+        return dialect == Dialect.POSTGRESQL ? "bigint" : "INT64";
+      case NUMERIC:
+        return "NUMERIC";
+      case PG_NUMERIC:
+        return "numeric";
+      case STRING:
+        return dialect == Dialect.POSTGRESQL ? "character varying" : "STRING";
+      case JSON:
+        return "JSON";
+      case PG_JSONB:
+        return "jsonb";
+      case TIMESTAMP:
+        return dialect == Dialect.POSTGRESQL ? "timestamp with time zone" : "TIMESTAMP";
+      case STRUCT:
+        return "STRUCT";
+      case ARRAY:
+        switch (type.getArrayElementType().getCode()) {
+          case BOOL:
+            return dialect == Dialect.POSTGRESQL ? "boolean[]" : "ARRAY<BOOL>";
+          case BYTES:
+            return dialect == Dialect.POSTGRESQL ? "bytea[]" : "ARRAY<BYTES>";
+          case DATE:
+            return dialect == Dialect.POSTGRESQL ? "date[]" : "ARRAY<DATE>";
+          case FLOAT64:
+            return dialect == Dialect.POSTGRESQL ? "double precision[]" : "ARRAY<FLOAT64>";
+          case INT64:
+            return dialect == Dialect.POSTGRESQL ? "bigint[]" : "ARRAY<INT64>";
+          case NUMERIC:
+            return "ARRAY<NUMERIC>";
+          case PG_NUMERIC:
+            return "numeric[]";
+          case STRING:
+            return dialect == Dialect.POSTGRESQL ? "character varying[]" : "ARRAY<STRING>";
+          case JSON:
+            return "ARRAY<JSON>";
+          case PG_JSONB:
+            return "jsonb[]";
+          case TIMESTAMP:
+            return dialect == Dialect.POSTGRESQL
+                ? "timestamp with time zone[]"
+                : "ARRAY<TIMESTAMP>";
+          case STRUCT:
+            return "ARRAY<STRUCT>";
+        }
+      default:
+        return null;
+    }
+  }
+
+  /**
+   * Extract Spanner type name from {@link java.sql.Types} code.
+   *
+   * @deprecated Use {@link #getSpannerTypeName(Type, Dialect)} instead.
+   */
+  @Deprecated
   static String getSpannerTypeName(int sqlType) {
     if (sqlType == Types.BOOLEAN) return Type.bool().getCode().name();
     if (sqlType == Types.BINARY) return Type.bytes().getCode().name();
@@ -89,7 +157,12 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     return OTHER_NAME;
   }
 
-  /** Get corresponding Java class name from {@link java.sql.Types} code. */
+  /**
+   * Get corresponding Java class name from {@link java.sql.Types} code.
+   *
+   * @deprecated Use {@link #getClassName(Type)} instead.
+   */
+  @Deprecated
   static String getClassName(int sqlType) {
     if (sqlType == Types.BOOLEAN) return Boolean.class.getName();
     if (sqlType == Types.BINARY) return Byte[].class.getName();
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
index 9248dac..2c7d6cc 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
@@ -29,6 +29,7 @@ import com.google.cloud.spanner.connection.Connection;
 import com.google.cloud.spanner.connection.ConnectionOptions;
 import com.google.cloud.spanner.connection.SavepointSupport;
 import com.google.cloud.spanner.connection.TransactionMode;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterators;
 import java.sql.Array;
@@ -57,14 +58,42 @@ class JdbcConnection extends AbstractJdbcConnection {
       "Only result sets with concurrency CONCUR_READ_ONLY are supported";
   private static final String ONLY_CLOSE_CURSORS_AT_COMMIT =
       "Only result sets with holdability CLOSE_CURSORS_AT_COMMIT are supported";
-  static final String IS_VALID_QUERY = "SELECT 1";
+
+  /**
+   * This query is used to check the aliveness of the connection if legacy alive check has been
+   * enabled. As Cloud Spanner JDBC connections do not maintain a physical or logical connection to
+   * Cloud Spanner, there is also no point in repeatedly executing a simple query to check whether a
+   * connection is alive. Instead, we rely on the result from the initial query to Spanner that
+   * determines the dialect to determine whether the connection is alive or not. This result is
+   * cached for all JDBC connections using the same {@link com.google.cloud.spanner.Spanner}
+   * instance.
+   *
+   * <p>The legacy {@link #isValid(int)} check using a SELECT 1 statement can be enabled by setting
+   * the System property spanner.jdbc.use_legacy_is_valid_check to true or setting the environment
+   * variable SPANNER_JDBC_USE_LEGACY_IS_VALID_CHECK to true.
+   */
+  static final String LEGACY_IS_VALID_QUERY = "SELECT 1";
 
   static final ImmutableList<String> NO_GENERATED_KEY_COLUMNS = ImmutableList.of();
 
   private Map<String, Class<?>> typeMap = new HashMap<>();
 
+  private final boolean useLegacyIsValidCheck;
+
   JdbcConnection(String connectionUrl, ConnectionOptions options) throws SQLException {
     super(connectionUrl, options);
+    this.useLegacyIsValidCheck = useLegacyValidCheck();
+  }
+
+  static boolean useLegacyValidCheck() {
+    String value = System.getProperty("spanner.jdbc.use_legacy_is_valid_check");
+    if (Strings.isNullOrEmpty(value)) {
+      value = System.getenv("SPANNER_JDBC_USE_LEGACY_IS_VALID_CHECK");
+    }
+    if (!Strings.isNullOrEmpty(value)) {
+      return Boolean.parseBoolean(value);
+    }
+    return false;
   }
 
   @Override
@@ -347,23 +376,38 @@ class JdbcConnection extends AbstractJdbcConnection {
     this.typeMap = new HashMap<>(map);
   }
 
+  boolean isUseLegacyIsValidCheck() {
+    return useLegacyIsValidCheck;
+  }
+
   @Override
   public boolean isValid(int timeout) throws SQLException {
     JdbcPreconditions.checkArgument(timeout >= 0, "timeout must be >= 0");
     if (!isClosed()) {
+      if (isUseLegacyIsValidCheck()) {
+        return legacyIsValid(timeout);
+      }
       try {
-        Statement statement = createStatement();
-        statement.setQueryTimeout(timeout);
-        try (ResultSet rs = statement.executeQuery(IS_VALID_QUERY)) {
-          if (rs.next()) {
-            if (rs.getLong(1) == 1L) {
-              return true;
-            }
+        return getDialect() != null;
+      } catch (Exception ignore) {
+        // ignore and fall through.
+      }
+    }
+    return false;
+  }
+
+  private boolean legacyIsValid(int timeout) throws SQLException {
+    try (Statement statement = createStatement()) {
+      statement.setQueryTimeout(timeout);
+      try (ResultSet rs = statement.executeQuery(LEGACY_IS_VALID_QUERY)) {
+        if (rs.next()) {
+          if (rs.getLong(1) == 1L) {
+            return true;
           }
         }
-      } catch (SQLException e) {
-        // ignore
       }
+    } catch (SQLException e) {
+      // ignore and fall through.
     }
     return false;
   }
@@ -448,6 +492,7 @@ class JdbcConnection extends AbstractJdbcConnection {
 
   @Nonnull
   String getDefaultSchema() {
+    // TODO: Update to use getDialect()#getDefaultSchema() when available.
     switch (getDialect()) {
       case POSTGRESQL:
         return "public";
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index c495bbe..5dd082e 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
@@ -390,14 +390,18 @@ enum JdbcDataType {
 
   public static JdbcDataType getType(Class<?> clazz) {
     for (JdbcDataType type : JdbcDataType.values()) {
-      if (type.getSupportedJavaClasses().contains(clazz)) return type;
+      if (type.getSupportedJavaClasses().contains(clazz)) {
+        return type;
+      }
     }
     return null;
   }
 
   public static JdbcDataType getType(Code code) {
     for (JdbcDataType type : JdbcDataType.values()) {
-      if (type.getCode() == code) return type;
+      if (type.getCode() == code) {
+        return type;
+      }
     }
     return null;
   }
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/JdbcParameterMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
index a520e22..82a4b91 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
@@ -16,7 +16,13 @@
 
 package com.google.cloud.spanner.jdbc;
 
-import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
+import com.google.cloud.spanner.JdbcDataTypeConverter;
+import com.google.cloud.spanner.ResultSet;
+import com.google.rpc.Code;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeCode;
 import java.math.BigDecimal;
 import java.sql.Date;
 import java.sql.ParameterMetaData;
@@ -29,9 +35,23 @@ import java.sql.Types;
 class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMetaData {
   private final JdbcPreparedStatement statement;
 
-  JdbcParameterMetaData(JdbcPreparedStatement statement) throws SQLException {
+  private final StructType parameters;
+
+  JdbcParameterMetaData(JdbcPreparedStatement statement, ResultSet resultSet) {
     this.statement = statement;
-    statement.getParameters().fetchMetaData(statement.getConnection());
+    this.parameters = resultSet.getMetadata().getUndeclaredParameters();
+  }
+
+  private Field getField(int param) throws SQLException {
+    JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
+    String paramName = "p" + param;
+    return parameters.getFieldsList().stream()
+        .filter(field -> field.getName().equals(paramName))
+        .findAny()
+        .orElseThrow(
+            () ->
+                JdbcSqlExceptionFactory.of(
+                    "Unknown parameter: " + paramName, Code.INVALID_ARGUMENT));
   }
 
   @Override
@@ -41,8 +61,7 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
 
   @Override
   public int getParameterCount() {
-    ParametersInfo info = statement.getParametersInfo();
-    return info.numberOfParameters;
+    return parameters.getFieldsCount();
   }
 
   @Override
@@ -53,7 +72,7 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
   }
 
   @Override
-  public boolean isSigned(int param) {
+  public boolean isSigned(int param) throws SQLException {
     int type = getParameterType(param);
     return type == Types.DOUBLE
         || type == Types.FLOAT
@@ -77,9 +96,34 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
   }
 
   @Override
-  public int getParameterType(int param) {
+  public int getParameterType(int param) throws SQLException {
+    JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
+    int typeFromValue = getParameterTypeFromValue(param);
+    if (typeFromValue != Types.OTHER) {
+      return typeFromValue;
+    }
+
+    Type type = getField(param).getType();
+    // JDBC only has a generic ARRAY type.
+    if (type.getCode() == TypeCode.ARRAY) {
+      return Types.ARRAY;
+    }
+    JdbcDataType jdbcDataType =
+        JdbcDataType.getType(JdbcDataTypeConverter.toSpannerType(type).getCode());
+    return jdbcDataType == null ? Types.OTHER : jdbcDataType.getSqlType();
+  }
+
+  /**
+   * This method returns the parameter type based on the parameter value that has been set. This was
+   * previously the only way to get the parameter types of a statement. Cloud Spanner can now return
+   * the types and names of parameters in a SQL string, which is what this method should return.
+   */
+  // TODO: Remove this method for the next major version bump.
+  private int getParameterTypeFromValue(int param) {
     Integer type = statement.getParameters().getType(param);
-    if (type != null) return type;
+    if (type != null) {
+      return type;
+    }
 
     Object value = statement.getParameters().getParameter(param);
     if (value == null) {
@@ -116,16 +160,49 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
   }
 
   @Override
-  public String getParameterTypeName(int param) {
-    return getSpannerTypeName(getParameterType(param));
+  public String getParameterTypeName(int param) throws SQLException {
+    JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
+    String typeNameFromValue = getParameterTypeNameFromValue(param);
+    if (typeNameFromValue != null) {
+      return typeNameFromValue;
+    }
+
+    com.google.cloud.spanner.Type type =
+        JdbcDataTypeConverter.toSpannerType(getField(param).getType());
+    return getSpannerTypeName(type, statement.getConnection().getDialect());
+  }
+
+  private String getParameterTypeNameFromValue(int param) {
+    int type = getParameterTypeFromValue(param);
+    if (type != Types.OTHER) {
+      return getSpannerTypeName(type);
+    }
+    return null;
   }
 
   @Override
-  public String getParameterClassName(int param) {
+  public String getParameterClassName(int param) throws SQLException {
+    JdbcPreconditions.checkArgument(param > 0 && param <= parameters.getFieldsCount(), param);
+    String classNameFromValue = getParameterClassNameFromValue(param);
+    if (classNameFromValue != null) {
+      return classNameFromValue;
+    }
+
+    com.google.cloud.spanner.Type type =
+        JdbcDataTypeConverter.toSpannerType(getField(param).getType());
+    return getClassName(type);
+  }
+
+  // TODO: Remove this method for the next major version bump.
+  private String getParameterClassNameFromValue(int param) {
     Object value = statement.getParameters().getParameter(param);
-    if (value != null) return value.getClass().getName();
+    if (value != null) {
+      return value.getClass().getName();
+    }
     Integer type = statement.getParameters().getType(param);
-    if (type != null) return getClassName(type);
+    if (type != null) {
+      return getClassName(type);
+    }
     return null;
   }
 
@@ -136,22 +213,26 @@ class JdbcParameterMetaData extends AbstractJdbcWrapper implements ParameterMeta
 
   @Override
   public String toString() {
-    StringBuilder res = new StringBuilder();
-    res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ")
-        .append(getParameterCount());
-    for (int param = 1; param <= getParameterCount(); param++) {
-      res.append("\nParameter ")
-          .append(param)
-          .append(":\n\t Class name: ")
-          .append(getParameterClassName(param));
-      res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param));
-      res.append(",\n\t Parameter type: ").append(getParameterType(param));
-      res.append(",\n\t Parameter precision: ").append(getPrecision(param));
-      res.append(",\n\t Parameter scale: ").append(getScale(param));
-      res.append(",\n\t Parameter signed: ").append(isSigned(param));
-      res.append(",\n\t Parameter nullable: ").append(isNullable(param));
-      res.append(",\n\t Parameter mode: ").append(getParameterMode(param));
+    try {
+      StringBuilder res = new StringBuilder();
+      res.append("CloudSpannerPreparedStatementParameterMetaData, parameter count: ")
+          .append(getParameterCount());
+      for (int param = 1; param <= getParameterCount(); param++) {
+        res.append("\nParameter ")
+            .append(param)
+            .append(":\n\t Class name: ")
+            .append(getParameterClassName(param));
+        res.append(",\n\t Parameter type name: ").append(getParameterTypeName(param));
+        res.append(",\n\t Parameter type: ").append(getParameterType(param));
+        res.append(",\n\t Parameter precision: ").append(getPrecision(param));
+        res.append(",\n\t Parameter scale: ").append(getScale(param));
+        res.append(",\n\t Parameter signed: ").append(isSigned(param));
+        res.append(",\n\t Parameter nullable: ").append(isNullable(param));
+        res.append(",\n\t Parameter mode: ").append(getParameterMode(param));
+      }
+      return res.toString();
+    } catch (SQLException exception) {
+      return "Failed to get parameter metadata: " + exception;
     }
-    return res.toString();
   }
 }
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
index 518807d..9ebbc98 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
@@ -40,6 +40,7 @@ class JdbcPreparedStatement extends AbstractJdbcPreparedStatement
   private static final char POS_PARAM_CHAR = '?';
   private final String sql;
   private final ParametersInfo parameters;
+  private JdbcParameterMetaData cachedParameterMetadata;
   private final ImmutableList<String> generatedKeysColumns;
 
   JdbcPreparedStatement(
@@ -118,7 +119,34 @@ class JdbcPreparedStatement extends AbstractJdbcPreparedStatement
   @Override
   public JdbcParameterMetaData getParameterMetaData() throws SQLException {
     checkClosed();
-    return new JdbcParameterMetaData(this);
+    if (cachedParameterMetadata == null) {
+      if (getConnection().getParser().isUpdateStatement(sql)
+          && !getConnection().getParser().checkReturningClause(sql)) {
+        cachedParameterMetadata = getParameterMetadataForUpdate();
+      } else {
+        cachedParameterMetadata = getParameterMetadataForQuery();
+      }
+    }
+    return cachedParameterMetadata;
+  }
+
+  private JdbcParameterMetaData getParameterMetadataForUpdate() {
+    try (com.google.cloud.spanner.ResultSet resultSet =
+        getConnection()
+            .getSpannerConnection()
+            .analyzeUpdateStatement(
+                Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
+      return new JdbcParameterMetaData(this, resultSet);
+    }
+  }
+
+  private JdbcParameterMetaData getParameterMetadataForQuery() {
+    try (com.google.cloud.spanner.ResultSet resultSet =
+        getConnection()
+            .getSpannerConnection()
+            .analyzeQuery(Statement.of(parameters.sqlWithNamedParameters), QueryAnalyzeMode.PLAN)) {
+      return new JdbcParameterMetaData(this, resultSet);
+    }
   }
 
   @Override
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
index 7d362ca..622c0ee 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
@@ -16,17 +16,28 @@
 
 SELECT *
 FROM (
-	SELECT	PARENT.TABLE_CATALOG AS PKTABLE_CAT, PARENT.TABLE_SCHEMA AS PKTABLE_SCHEM, PARENT.TABLE_NAME AS PKTABLE_NAME,
-			PARENT.COLUMN_NAME AS PKCOLUMN_NAME, CHILD.TABLE_CATALOG AS FKTABLE_CAT, CHILD.TABLE_SCHEMA AS FKTABLE_SCHEM,
-			CHILD.TABLE_NAME AS FKTABLE_NAME, CHILD.COLUMN_NAME AS FKCOLUMN_NAME, CHILD.ORDINAL_POSITION AS KEY_SEQ, 3 AS UPDATE_RULE,
-	        CASE WHEN TABLES.ON_DELETE_ACTION='CASCADE' THEN 0 ELSE 3 END AS DELETE_RULE, NULL AS FK_NAME, 'PRIMARY_KEY' AS PK_NAME,
+	SELECT	PARENT_TABLE.TABLE_CATALOG AS PKTABLE_CAT, PARENT_TABLE.TABLE_SCHEMA AS PKTABLE_SCHEM, PARENT_TABLE.TABLE_NAME AS PKTABLE_NAME,
+			PARENT_INDEX_COLUMNS.COLUMN_NAME AS PKCOLUMN_NAME, CHILD_TABLE.TABLE_CATALOG AS FKTABLE_CAT, CHILD_TABLE.TABLE_SCHEMA AS FKTABLE_SCHEM,
+			CHILD_TABLE.TABLE_NAME AS FKTABLE_NAME, PARENT_INDEX_COLUMNS.COLUMN_NAME AS FKCOLUMN_NAME, PARENT_INDEX_COLUMNS.ORDINAL_POSITION AS KEY_SEQ,
+			3 AS UPDATE_RULE,
+	        CASE WHEN CHILD_TABLE.ON_DELETE_ACTION='CASCADE' THEN 0 ELSE 3 END AS DELETE_RULE,
+	        NULL AS FK_NAME, 'PRIMARY_KEY' AS PK_NAME,
 	        7 AS DEFERRABILITY
-	FROM INFORMATION_SCHEMA.INDEX_COLUMNS PARENT
-	INNER JOIN INFORMATION_SCHEMA.INDEXES PARENT_INDEX ON PARENT.INDEX_NAME=PARENT_INDEX.INDEX_NAME AND PARENT.TABLE_NAME=PARENT_INDEX.TABLE_NAME AND PARENT.TABLE_SCHEMA=PARENT_INDEX.TABLE_SCHEMA AND PARENT.TABLE_CATALOG=PARENT_INDEX.TABLE_CATALOG AND PARENT_INDEX.INDEX_TYPE='PRIMARY_KEY'
-	INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS CHILD ON NOT PARENT.TABLE_NAME=CHILD.TABLE_NAME AND PARENT.COLUMN_NAME=CHILD.COLUMN_NAME
-	INNER JOIN INFORMATION_SCHEMA.INDEXES CHILD_INDEX ON CHILD.INDEX_NAME=CHILD_INDEX.INDEX_NAME AND CHILD.TABLE_NAME=CHILD_INDEX.TABLE_NAME AND CHILD.TABLE_SCHEMA=CHILD_INDEX.TABLE_SCHEMA AND CHILD.TABLE_CATALOG=CHILD_INDEX.TABLE_CATALOG AND CHILD_INDEX.INDEX_TYPE='PRIMARY_KEY'
-	INNER JOIN INFORMATION_SCHEMA.TABLES ON CHILD.TABLE_CATALOG=TABLES.TABLE_CATALOG AND CHILD.TABLE_SCHEMA=TABLES.TABLE_SCHEMA AND CHILD.TABLE_NAME=TABLES.TABLE_NAME AND PARENT.TABLE_NAME=TABLES.PARENT_TABLE_NAME
-	WHERE PARENT_INDEX.INDEX_TYPE='PRIMARY_KEY' AND CHILD.ORDINAL_POSITION IS NOT NULL
+	FROM INFORMATION_SCHEMA.TABLES PARENT_TABLE
+	INNER JOIN INFORMATION_SCHEMA.INDEXES PARENT_INDEX ON
+                PARENT_INDEX.TABLE_NAME=PARENT_TABLE.TABLE_NAME
+            AND PARENT_INDEX.TABLE_SCHEMA=PARENT_TABLE.TABLE_SCHEMA
+            AND PARENT_INDEX.TABLE_CATALOG=PARENT_TABLE.TABLE_CATALOG
+            AND PARENT_INDEX.INDEX_TYPE='PRIMARY_KEY'
+    INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS PARENT_INDEX_COLUMNS ON
+                PARENT_INDEX_COLUMNS.TABLE_NAME=PARENT_INDEX.TABLE_NAME
+            AND PARENT_INDEX_COLUMNS.TABLE_SCHEMA=PARENT_INDEX.TABLE_SCHEMA
+            AND PARENT_INDEX_COLUMNS.TABLE_CATALOG=PARENT_INDEX.TABLE_CATALOG
+            AND PARENT_INDEX_COLUMNS.INDEX_NAME=PARENT_INDEX.INDEX_NAME
+    INNER JOIN INFORMATION_SCHEMA.TABLES CHILD_TABLE ON
+                CHILD_TABLE.PARENT_TABLE_NAME=PARENT_TABLE.TABLE_NAME
+            AND CHILD_TABLE.TABLE_SCHEMA=PARENT_TABLE.TABLE_SCHEMA
+            AND CHILD_TABLE.TABLE_CATALOG=PARENT_TABLE.TABLE_CATALOG
 	
 	UNION ALL
 	
@@ -36,8 +47,15 @@ FROM (
 	        3 AS DELETE_RULE, CONSTRAINTS.CONSTRAINT_NAME AS FK_NAME, CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS PK_NAME,
 	        7 AS DEFERRABILITY
 	FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS
-	INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON CONSTRAINTS.CONSTRAINT_CATALOG=CHILD.CONSTRAINT_CATALOG AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
-	INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG=PARENT.CONSTRAINT_CATALOG AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
+	INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD ON
+	            CONSTRAINTS.CONSTRAINT_CATALOG=CHILD.CONSTRAINT_CATALOG
+	        AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA
+	        AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
+	INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON
+	            CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG=PARENT.CONSTRAINT_CATALOG
+	        AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA
+	        AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME
+	        AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
 ) AS CROSS_REF
 WHERE UPPER(PKTABLE_CAT) LIKE ?
 AND UPPER(PKTABLE_SCHEM) LIKE ?
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
index 631886d..2eb4e60 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
@@ -26,10 +26,15 @@ FROM (
 	       NULL AS FK_NAME, 'PRIMARY_KEY' AS PK_NAME,
 	       7 AS DEFERRABILITY -- 7 = importedKeyNotDeferrable
 	FROM INFORMATION_SCHEMA.TABLES PARENT
-	INNER JOIN INFORMATION_SCHEMA.TABLES CHILD ON CHILD.PARENT_TABLE_NAME=PARENT.TABLE_NAME
+	INNER JOIN INFORMATION_SCHEMA.TABLES CHILD ON
+                CHILD.TABLE_CATALOG=PARENT.TABLE_CATALOG
+            AND CHILD.TABLE_SCHEMA=PARENT.TABLE_SCHEMA
+            AND CHILD.PARENT_TABLE_NAME=PARENT.TABLE_NAME
 	INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS PARENT_INDEX_COLUMNS ON
-	           PARENT_INDEX_COLUMNS.TABLE_NAME=PARENT.TABLE_NAME
-	       AND PARENT_INDEX_COLUMNS.INDEX_NAME='PRIMARY_KEY'
+                PARENT_INDEX_COLUMNS.TABLE_CATALOG=PARENT.TABLE_CATALOG
+            AND PARENT_INDEX_COLUMNS.TABLE_SCHEMA=PARENT.TABLE_SCHEMA
+            AND PARENT_INDEX_COLUMNS.TABLE_NAME=PARENT.TABLE_NAME
+            AND PARENT_INDEX_COLUMNS.INDEX_NAME='PRIMARY_KEY'
 	
 	UNION ALL
 	
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
index 50994a5..3daa672 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
@@ -26,9 +26,14 @@ FROM (
 	       NULL AS FK_NAME, 'PRIMARY_KEY' AS PK_NAME,
 	       7 AS DEFERRABILITY -- 7 = importedKeyNotDeferrable
 	FROM INFORMATION_SCHEMA.TABLES PARENT
-	INNER JOIN INFORMATION_SCHEMA.TABLES CHILD ON CHILD.PARENT_TABLE_NAME=PARENT.TABLE_NAME
+	INNER JOIN INFORMATION_SCHEMA.TABLES CHILD ON
+	           CHILD.TABLE_CATALOG=PARENT.TABLE_CATALOG
+	       AND CHILD.TABLE_SCHEMA=PARENT.TABLE_SCHEMA
+	       AND CHILD.PARENT_TABLE_NAME=PARENT.TABLE_NAME
 	INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS PARENT_INDEX_COLUMNS ON
-	           PARENT_INDEX_COLUMNS.TABLE_NAME=PARENT.TABLE_NAME
+	           PARENT_INDEX_COLUMNS.TABLE_CATALOG=PARENT.TABLE_CATALOG
+	       AND PARENT_INDEX_COLUMNS.TABLE_SCHEMA=PARENT.TABLE_SCHEMA
+	       AND PARENT_INDEX_COLUMNS.TABLE_NAME=PARENT.TABLE_NAME
 	       AND PARENT_INDEX_COLUMNS.INDEX_NAME='PRIMARY_KEY'
 	
 	UNION ALL
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql
index 33b398d..d444404 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql
@@ -20,8 +20,15 @@ SELECT	PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SC
           3 AS "DELETE_RULE", CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME",
           7 AS "DEFERRABILITY"
 FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON
+                 COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON
+                 COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME
+             AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
 WHERE UPPER(COALESCE(PARENT.TABLE_CATALOG, '')) LIKE ?
   AND UPPER(PARENT.TABLE_SCHEMA) LIKE ?
   AND UPPER(PARENT.TABLE_NAME) LIKE ?
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql
index f2a9fb2..e59a8b8 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql
@@ -23,8 +23,15 @@ SELECT	PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SC
           CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME",
           7 AS "DEFERRABILITY" -- 7 = importedKeyNotDeferrable
 FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON
+                 COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON
+                 COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME
+             AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
 WHERE UPPER(COALESCE(PARENT.TABLE_CATALOG, '')) LIKE ?
   AND UPPER(PARENT.TABLE_SCHEMA) LIKE ?
   AND UPPER(PARENT.TABLE_NAME) LIKE ?
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql
index c921b1c..fb510a1 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql
@@ -23,8 +23,15 @@ SELECT	PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SC
           CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME",
           7 AS "DEFERRABILITY" -- 7 = importedKeyNotDeferrable
 FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
-         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '') AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD  ON
+                 COALESCE(CONSTRAINTS.CONSTRAINT_CATALOG, '')=COALESCE(CHILD.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME
+         INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON
+                 COALESCE(CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG, '')=COALESCE(PARENT.CONSTRAINT_CATALOG, '')
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA
+             AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME
+             AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT
 WHERE UPPER(COALESCE(CHILD.TABLE_CATALOG, '')) LIKE ?
   AND UPPER(CHILD.TABLE_SCHEMA) LIKE ?
   AND UPPER(CHILD.TABLE_NAME) LIKE ?
diff --git a/versions.txt b/versions.txt
index 6dc9f85..8974286 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-spanner-jdbc:2.14.6:2.14.7-SNAPSHOT
+google-cloud-spanner-jdbc:2.15.2:2.15.3-SNAPSHOT


Test patch:
diff --git a/samples/spring-data-jdbc/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java b/samples/spring-data-jdbc/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
index 726b32c..81b0d4c 100644
--- a/samples/spring-data-jdbc/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
+++ b/samples/spring-data-jdbc/src/test/java/com/google/cloud/spanner/sample/ApplicationTest.java
@@ -138,6 +138,11 @@ public class ApplicationTest extends AbstractMockServerTest {
                                     .build())
                         .collect(Collectors.toList()))
                 .build()));
+    mockSpanner.putPartialStatementResult(
+        StatementResult.update(
+            Statement.of(
+                "INSERT INTO \"albums\" (\"cover_picture\", \"created_at\", \"marketing_budget\", \"release_date\", \"singer_id\", \"title\", \"updated_at\") VALUES ($1, $2, $3, $4, $5, $6, $7)"),
+            1L));
     mockSpanner.putPartialStatementResult(
         StatementResult.update(
             Statement.of(
@@ -747,7 +752,7 @@ public class ApplicationTest extends AbstractMockServerTest {
     SpringApplication.run(Application.class).close();
 
     assertEquals(
-        34,
+        37,
         mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
             .filter(request -> !request.getSql().equals("SELECT 1"))
             .count());
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
index 372bbb0..f8473f6 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.spanner.jdbc;
 
+import static com.google.cloud.spanner.jdbc.AbstractJdbcWrapper.getSpannerTypeName;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -23,6 +24,8 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.Type;
 import com.google.rpc.Code;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -426,4 +429,68 @@ public class AbstractJdbcWrapperTest {
       assertThat(((JdbcSqlException) e).getCode()).isEqualTo(Code.INVALID_ARGUMENT);
     }
   }
+
+  @Test
+  public void testGoogleSQLTypeNames() {
+    assertEquals("INT64", getSpannerTypeName(Type.int64(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("BOOL", getSpannerTypeName(Type.bool(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("FLOAT64", getSpannerTypeName(Type.float64(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("STRING", getSpannerTypeName(Type.string(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("BYTES", getSpannerTypeName(Type.bytes(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("DATE", getSpannerTypeName(Type.date(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("TIMESTAMP", getSpannerTypeName(Type.timestamp(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("JSON", getSpannerTypeName(Type.json(), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals("NUMERIC", getSpannerTypeName(Type.numeric(), Dialect.GOOGLE_STANDARD_SQL));
+
+    assertEquals(
+        "ARRAY<INT64>", getSpannerTypeName(Type.array(Type.int64()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<BOOL>", getSpannerTypeName(Type.array(Type.bool()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<FLOAT64>",
+        getSpannerTypeName(Type.array(Type.float64()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<STRING>",
+        getSpannerTypeName(Type.array(Type.string()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<BYTES>", getSpannerTypeName(Type.array(Type.bytes()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<DATE>", getSpannerTypeName(Type.array(Type.date()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<TIMESTAMP>",
+        getSpannerTypeName(Type.array(Type.timestamp()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<JSON>", getSpannerTypeName(Type.array(Type.json()), Dialect.GOOGLE_STANDARD_SQL));
+    assertEquals(
+        "ARRAY<NUMERIC>",
+        getSpannerTypeName(Type.array(Type.numeric()), Dialect.GOOGLE_STANDARD_SQL));
+  }
+
+  @Test
+  public void testPostgreSQLTypeNames() {
+    assertEquals("bigint", getSpannerTypeName(Type.int64(), Dialect.POSTGRESQL));
+    assertEquals("boolean", getSpannerTypeName(Type.bool(), Dialect.POSTGRESQL));
+    assertEquals("double precision", getSpannerTypeName(Type.float64(), Dialect.POSTGRESQL));
+    assertEquals("character varying", getSpannerTypeName(Type.string(), Dialect.POSTGRESQL));
+    assertEquals("bytea", getSpannerTypeName(Type.bytes(), Dialect.POSTGRESQL));
+    assertEquals("date", getSpannerTypeName(Type.date(), Dialect.POSTGRESQL));
+    assertEquals(
+        "timestamp with time zone", getSpannerTypeName(Type.timestamp(), Dialect.POSTGRESQL));
+    assertEquals("jsonb", getSpannerTypeName(Type.pgJsonb(), Dialect.POSTGRESQL));
+    assertEquals("numeric", getSpannerTypeName(Type.pgNumeric(), Dialect.POSTGRESQL));
+
+    assertEquals("bigint[]", getSpannerTypeName(Type.array(Type.int64()), Dialect.POSTGRESQL));
+    assertEquals("boolean[]", getSpannerTypeName(Type.array(Type.bool()), Dialect.POSTGRESQL));
+    assertEquals(
+        "double precision[]", getSpannerTypeName(Type.array(Type.float64()), Dialect.POSTGRESQL));
+    assertEquals(
+        "character varying[]", getSpannerTypeName(Type.array(Type.string()), Dialect.POSTGRESQL));
+    assertEquals("bytea[]", getSpannerTypeName(Type.array(Type.bytes()), Dialect.POSTGRESQL));
+    assertEquals("date[]", getSpannerTypeName(Type.array(Type.date()), Dialect.POSTGRESQL));
+    assertEquals(
+        "timestamp with time zone[]",
+        getSpannerTypeName(Type.array(Type.timestamp()), Dialect.POSTGRESQL));
+    assertEquals("jsonb[]", getSpannerTypeName(Type.array(Type.pgJsonb()), Dialect.POSTGRESQL));
+    assertEquals("numeric[]", getSpannerTypeName(Type.array(Type.pgNumeric()), Dialect.POSTGRESQL));
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
index ff811f3..8d26bb3 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
@@ -502,7 +502,7 @@ public class JdbcConnectionTest {
         mock(com.google.cloud.spanner.connection.Connection.class);
     when(spannerConnection.getDialect()).thenReturn(dialect);
     when(options.getConnection()).thenReturn(spannerConnection);
-    Statement statement = Statement.of(JdbcConnection.IS_VALID_QUERY);
+    Statement statement = Statement.of(JdbcConnection.LEGACY_IS_VALID_QUERY);
 
     // Verify that an opened connection that returns a result set is valid.
     try (JdbcConnection connection = new JdbcConnection("url", options)) {
@@ -517,7 +517,7 @@ public class JdbcConnectionTest {
       }
 
       // Now let the query return an error. isValid should now return false.
-      when(spannerConnection.executeQuery(statement))
+      when(spannerConnection.getDialect())
           .thenThrow(
               SpannerExceptionFactory.newSpannerException(
                   ErrorCode.ABORTED, "the current transaction has been aborted"));
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
index 310d154..c5748d1 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
@@ -18,9 +18,9 @@ package com.google.cloud.spanner.jdbc;
 
 import static com.google.cloud.spanner.jdbc.JdbcConnection.NO_GENERATED_KEY_COLUMNS;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.mock;
@@ -39,6 +39,10 @@ import com.google.cloud.spanner.Type.StructField;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.connection.AbstractStatementParser;
 import com.google.cloud.spanner.connection.Connection;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.TypeCode;
 import java.io.ByteArrayInputStream;
 import java.io.StringReader;
 import java.math.BigDecimal;
@@ -55,6 +59,8 @@ import java.util.Calendar;
 import java.util.Collections;
 import java.util.TimeZone;
 import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -158,7 +164,8 @@ public class JdbcPreparedStatementTest {
     final int numberOfParams = 53;
     String sql = generateSqlWithParameters(numberOfParams);
 
-    JdbcConnection connection = createMockConnection();
+    Connection spannerConnection = createMockConnectionWithAnalyzeResults(numberOfParams);
+    JdbcConnection connection = createMockConnection(spannerConnection);
     try (JdbcPreparedStatement ps =
         new JdbcPreparedStatement(connection, sql, NO_GENERATED_KEY_COLUMNS)) {
       ps.setArray(1, connection.createArrayOf("INT64", new Long[] {1L, 2L, 3L}));
@@ -252,10 +259,14 @@ public class JdbcPreparedStatementTest {
       assertEquals(String.class.getName(), pmd.getParameterClassName(35));
       assertEquals(String.class.getName(), pmd.getParameterClassName(36));
       assertEquals(String.class.getName(), pmd.getParameterClassName(37));
-      assertNull(pmd.getParameterClassName(38));
-      assertNull(pmd.getParameterClassName(39));
+
+      // These parameter values are not set, so the driver returns the type that was returned by
+      // Cloud Spanner.
+      assertEquals(String.class.getName(), pmd.getParameterClassName(38));
+      assertEquals(String.class.getName(), pmd.getParameterClassName(39));
+
       assertEquals(Short.class.getName(), pmd.getParameterClassName(40));
-      assertNull(pmd.getParameterClassName(41));
+      assertEquals(String.class.getName(), pmd.getParameterClassName(41));
       assertEquals(String.class.getName(), pmd.getParameterClassName(42));
       assertEquals(Time.class.getName(), pmd.getParameterClassName(43));
       assertEquals(Time.class.getName(), pmd.getParameterClassName(44));
@@ -279,8 +290,11 @@ public class JdbcPreparedStatementTest {
   public void testSetNullValues() throws SQLException {
     final int numberOfParameters = 31;
     String sql = generateSqlWithParameters(numberOfParameters);
+
+    JdbcConnection connection =
+        createMockConnection(createMockConnectionWithAnalyzeResults(numberOfParameters));
     try (JdbcPreparedStatement ps =
-        new JdbcPreparedStatement(createMockConnection(), sql, NO_GENERATED_KEY_COLUMNS)) {
+        new JdbcPreparedStatement(connection, sql, NO_GENERATED_KEY_COLUMNS)) {
       int index = 0;
       ps.setNull(++index, Types.BLOB);
       ps.setNull(++index, Types.NVARCHAR);
@@ -396,4 +410,34 @@ public class JdbcPreparedStatementTest {
     assertEquals(
         ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value(), jdbcSqlException.getErrorCode());
   }
+
+  private Connection createMockConnectionWithAnalyzeResults(int numParams) {
+    Connection spannerConnection = mock(Connection.class);
+    ResultSet resultSet = mock(ResultSet.class);
+    when(spannerConnection.analyzeUpdateStatement(any(Statement.class), eq(QueryAnalyzeMode.PLAN)))
+        .thenReturn(resultSet);
+    when(spannerConnection.analyzeQuery(any(Statement.class), eq(QueryAnalyzeMode.PLAN)))
+        .thenReturn(resultSet);
+    ResultSetMetadata metadata =
+        ResultSetMetadata.newBuilder()
+            .setUndeclaredParameters(
+                StructType.newBuilder()
+                    .addAllFields(
+                        IntStream.range(0, numParams)
+                            .mapToObj(
+                                i ->
+                                    Field.newBuilder()
+                                        .setName("p" + (i + 1))
+                                        .setType(
+                                            com.google.spanner.v1.Type.newBuilder()
+                                                .setCode(TypeCode.STRING)
+                                                .build())
+                                        .build())
+                            .collect(Collectors.toList()))
+                    .build())
+            .build();
+    when(resultSet.getMetadata()).thenReturn(metadata);
+
+    return spannerConnection;
+  }
 }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementWithMockedServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementWithMockedServerTest.java
index d3607d8..a3072e3 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementWithMockedServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementWithMockedServerTest.java
@@ -28,6 +28,13 @@ import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.connection.SpannerPool;
 import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlBatchUpdateException;
+import com.google.spanner.v1.ResultSet;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.ResultSetStats;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeCode;
 import io.grpc.Server;
 import io.grpc.Status;
 import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
@@ -198,11 +205,180 @@ public class JdbcPreparedStatementWithMockedServerTest {
 
   @Test
   public void testInsertUntypedNullValues() throws SQLException {
+    String sql =
+        "insert into all_nullable_types (ColInt64, ColFloat64, ColBool, ColString, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson, ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColBytesArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray) "
+            + "values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18)";
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(sql),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ResultSetMetadata.newBuilder()
+                        .setUndeclaredParameters(
+                            StructType.newBuilder()
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p1")
+                                        .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p2")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.FLOAT64).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p3")
+                                        .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p4")
+                                        .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p5")
+                                        .setType(Type.newBuilder().setCode(TypeCode.BYTES).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p6")
+                                        .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p7")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p8")
+                                        .setType(
+                                            Type.newBuilder().setCode(TypeCode.NUMERIC).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p9")
+                                        .setType(Type.newBuilder().setCode(TypeCode.JSON).build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p10")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.INT64)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p11")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.FLOAT64)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p12")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.BOOL)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p13")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.STRING)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p14")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.BYTES)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p15")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.DATE)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p16")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.TIMESTAMP)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p17")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.NUMERIC)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .addFields(
+                                    Field.newBuilder()
+                                        .setName("p18")
+                                        .setType(
+                                            Type.newBuilder()
+                                                .setCode(TypeCode.ARRAY)
+                                                .setArrayElementType(
+                                                    Type.newBuilder()
+                                                        .setCode(TypeCode.JSON)
+                                                        .build())
+                                                .build())
+                                        .build())
+                                .build())
+                        .build())
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
     mockSpanner.putStatementResult(
         StatementResult.update(
-            Statement.newBuilder(
-                    "insert into all_nullable_types (ColInt64, ColFloat64, ColBool, ColString, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson, ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColBytesArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray) "
-                        + "values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18)")
+            Statement.newBuilder(sql)
                 .bind("p1")
                 .to((Value) null)
                 .bind("p2")
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/PreparedStatementParameterMetadataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java
new file mode 100644
index 0000000..8b7130e
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.jdbc;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.MockSpannerServiceImpl;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractMockServerTest;
+import com.google.cloud.spanner.connection.SpannerPool;
+import com.google.spanner.v1.ResultSet;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.ResultSetStats;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeAnnotationCode;
+import com.google.spanner.v1.TypeCode;
+import java.sql.Connection;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PreparedStatementParameterMetadataTest extends AbstractMockServerTest {
+
+  @After
+  public void reset() {
+    // This ensures that each test gets a fresh Spanner instance. This is necessary to get a new
+    // dialect result for each connection.
+    SpannerPool.closeSpannerPool();
+  }
+
+  @Test
+  public void testAllTypesParameterMetadata_GoogleSql() throws SQLException {
+    mockSpanner.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL));
+
+    String baseSql =
+        "insert into all_types (col_bool, col_bytes, col_date, col_float64, col_int64, "
+            + "col_json, col_numeric, col_string, col_timestamp, col_bool_array, col_bytes_array, "
+            + "col_date_array, col_float64_array, col_int64_array, col_json_array, col_numeric_array, "
+            + "col_string_array, col_timestamp_array) values (%s)";
+    String jdbcSql =
+        String.format(
+            baseSql,
+            IntStream.range(0, 18).mapToObj(ignored -> "?").collect(Collectors.joining(", ")));
+    String googleSql =
+        String.format(
+            baseSql,
+            IntStream.range(1, 19)
+                .mapToObj(index -> "@p" + index)
+                .collect(Collectors.joining(", ")));
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(googleSql),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ResultSetMetadata.newBuilder()
+                        .setUndeclaredParameters(
+                            createAllTypesParameters(Dialect.GOOGLE_STANDARD_SQL))
+                        .build())
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
+
+    try (Connection connection = createJdbcConnection()) {
+      try (PreparedStatement statement = connection.prepareStatement(jdbcSql)) {
+        ParameterMetaData metadata = statement.getParameterMetaData();
+        assertEquals(18, metadata.getParameterCount());
+        int index = 0;
+        assertEquals(Types.BOOLEAN, metadata.getParameterType(++index));
+        assertEquals("BOOL", metadata.getParameterTypeName(index));
+        assertEquals(Types.BINARY, metadata.getParameterType(++index));
+        assertEquals("BYTES", metadata.getParameterTypeName(index));
+        assertEquals(Types.DATE, metadata.getParameterType(++index));
+        assertEquals("DATE", metadata.getParameterTypeName(index));
+        assertEquals(Types.DOUBLE, metadata.getParameterType(++index));
+        assertEquals("FLOAT64", metadata.getParameterTypeName(index));
+        assertEquals(Types.BIGINT, metadata.getParameterType(++index));
+        assertEquals("INT64", metadata.getParameterTypeName(index));
+        assertEquals(JsonType.VENDOR_TYPE_NUMBER, metadata.getParameterType(++index));
+        assertEquals("JSON", metadata.getParameterTypeName(index));
+        assertEquals(Types.NUMERIC, metadata.getParameterType(++index));
+        assertEquals("NUMERIC", metadata.getParameterTypeName(index));
+        assertEquals(Types.NVARCHAR, metadata.getParameterType(++index));
+        assertEquals("STRING", metadata.getParameterTypeName(index));
+        assertEquals(Types.TIMESTAMP, metadata.getParameterType(++index));
+        assertEquals("TIMESTAMP", metadata.getParameterTypeName(index));
+
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<BOOL>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<BYTES>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<DATE>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<FLOAT64>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<INT64>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<JSON>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<NUMERIC>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<STRING>", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("ARRAY<TIMESTAMP>", metadata.getParameterTypeName(index));
+      }
+    }
+  }
+
+  @Test
+  public void testAllTypesParameterMetadata_PostgreSQL() throws SQLException {
+    mockSpanner.putStatementResult(
+        MockSpannerServiceImpl.StatementResult.detectDialectResult(Dialect.POSTGRESQL));
+
+    String baseSql =
+        "insert into all_types (col_bool, col_bytes, col_date, col_float64, col_int64, "
+            + "col_json, col_numeric, col_string, col_timestamp, col_bool_array, col_bytes_array, "
+            + "col_date_array, col_float64_array, col_int64_array, col_json_array, col_numeric_array, "
+            + "col_string_array, col_timestamp_array) values (%s)";
+    String jdbcSql =
+        String.format(
+            baseSql,
+            IntStream.range(0, 18).mapToObj(ignored -> "?").collect(Collectors.joining(", ")));
+    String googleSql =
+        String.format(
+            baseSql,
+            IntStream.range(1, 19)
+                .mapToObj(index -> "$" + index)
+                .collect(Collectors.joining(", ")));
+    mockSpanner.putStatementResult(
+        StatementResult.query(
+            Statement.of(googleSql),
+            ResultSet.newBuilder()
+                .setMetadata(
+                    ResultSetMetadata.newBuilder()
+                        .setUndeclaredParameters(createAllTypesParameters(Dialect.POSTGRESQL))
+                        .build())
+                .setStats(ResultSetStats.newBuilder().build())
+                .build()));
+
+    try (Connection connection = createJdbcConnection()) {
+      try (PreparedStatement statement = connection.prepareStatement(jdbcSql)) {
+        ParameterMetaData metadata = statement.getParameterMetaData();
+        assertEquals(18, metadata.getParameterCount());
+        int index = 0;
+        assertEquals(Types.BOOLEAN, metadata.getParameterType(++index));
+        assertEquals("boolean", metadata.getParameterTypeName(index));
+        assertEquals(Types.BINARY, metadata.getParameterType(++index));
+        assertEquals("bytea", metadata.getParameterTypeName(index));
+        assertEquals(Types.DATE, metadata.getParameterType(++index));
+        assertEquals("date", metadata.getParameterTypeName(index));
+        assertEquals(Types.DOUBLE, metadata.getParameterType(++index));
+        assertEquals("double precision", metadata.getParameterTypeName(index));
+        assertEquals(Types.BIGINT, metadata.getParameterType(++index));
+        assertEquals("bigint", metadata.getParameterTypeName(index));
+        assertEquals(PgJsonbType.VENDOR_TYPE_NUMBER, metadata.getParameterType(++index));
+        assertEquals("jsonb", metadata.getParameterTypeName(index));
+        assertEquals(Types.NUMERIC, metadata.getParameterType(++index));
+        assertEquals("numeric", metadata.getParameterTypeName(index));
+        assertEquals(Types.NVARCHAR, metadata.getParameterType(++index));
+        assertEquals("character varying", metadata.getParameterTypeName(index));
+        assertEquals(Types.TIMESTAMP, metadata.getParameterType(++index));
+        assertEquals("timestamp with time zone", metadata.getParameterTypeName(index));
+
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("boolean[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("bytea[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("date[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("double precision[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("bigint[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("jsonb[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("numeric[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("character varying[]", metadata.getParameterTypeName(index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals("timestamp with time zone[]", metadata.getParameterTypeName(index));
+      }
+    }
+  }
+
+  static StructType createAllTypesParameters(Dialect dialect) {
+    return StructType.newBuilder()
+        .addFields(
+            Field.newBuilder()
+                .setName("p1")
+                .setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p2")
+                .setType(Type.newBuilder().setCode(TypeCode.BYTES).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p3")
+                .setType(Type.newBuilder().setCode(TypeCode.DATE).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p4")
+                .setType(Type.newBuilder().setCode(TypeCode.FLOAT64).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p5")
+                .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p6")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.JSON)
+                        .setTypeAnnotation(
+                            dialect == Dialect.POSTGRESQL
+                                ? TypeAnnotationCode.PG_JSONB
+                                : TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED)
+                        .build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p7")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.NUMERIC)
+                        .setTypeAnnotation(
+                            dialect == Dialect.POSTGRESQL
+                                ? TypeAnnotationCode.PG_NUMERIC
+                                : TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED)
+                        .build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p8")
+                .setType(Type.newBuilder().setCode(TypeCode.STRING).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p9")
+                .setType(Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p10")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.BOOL).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p11")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p12")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p13")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p14")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.INT64).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p15")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(
+                            Type.newBuilder()
+                                .setCode(TypeCode.JSON)
+                                .setTypeAnnotation(
+                                    dialect == Dialect.POSTGRESQL
+                                        ? TypeAnnotationCode.PG_JSONB
+                                        : TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED)
+                                .build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p16")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(
+                            Type.newBuilder()
+                                .setCode(TypeCode.NUMERIC)
+                                .setTypeAnnotation(
+                                    dialect == Dialect.POSTGRESQL
+                                        ? TypeAnnotationCode.PG_NUMERIC
+                                        : TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED)
+                                .build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p17")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING).build()))
+                .build())
+        .addFields(
+            Field.newBuilder()
+                .setName("p18")
+                .setType(
+                    Type.newBuilder()
+                        .setCode(TypeCode.ARRAY)
+                        .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP).build()))
+                .build())
+        .build();
+  }
+}
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));
+  }
+}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
index 2864559..8f01493 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
@@ -21,7 +21,6 @@ import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -35,6 +34,7 @@ import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.jdbc.JsonType;
 import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.CharStreams;
 import java.io.IOException;
@@ -394,7 +394,26 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       try (PreparedStatement ps =
           connection.prepareStatement(
               "INSERT INTO Singers (SingerId, FirstName, LastName, SingerInfo, BirthDate) values (?,?,?,?,?)")) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 5);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.BINARY, Types.NVARCHAR)
+                : ImmutableList.of(
+                    Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.BINARY, Types.DATE),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "character varying",
+                    "character varying",
+                    "bytea",
+                    "character varying")
+                : ImmutableList.of("INT64", "STRING", "STRING", "BYTES", "DATE"),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Long.class, String.class, String.class, byte[].class, String.class)
+                : ImmutableList.of(
+                    Long.class, String.class, String.class, byte[].class, Date.class));
         for (Singer singer : createSingers()) {
           singer.setPreparedStatement(ps, getDialect());
           assertInsertSingerParameterMetadata(ps.getParameterMetaData());
@@ -410,7 +429,13 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       try (PreparedStatement ps =
           connection.prepareStatement(
               "INSERT INTO Albums (SingerId, AlbumId, AlbumTitle, MarketingBudget) VALUES (?,?,?,?)")) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 4);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            ImmutableList.of(Types.BIGINT, Types.BIGINT, Types.NVARCHAR, Types.BIGINT),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of("bigint", "bigint", "character varying", "bigint")
+                : ImmutableList.of("INT64", "INT64", "STRING", "INT64"),
+            ImmutableList.of(Long.class, Long.class, String.class, Long.class));
         for (Album album : createAlbums()) {
           ps.setLong(1, album.singerId);
           ps.setLong(2, album.albumId);
@@ -425,7 +450,26 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       try (PreparedStatement ps =
           connection.prepareStatement(
               "INSERT INTO Songs (SingerId, AlbumId, TrackId, SongName, Duration, SongGenre) VALUES (?,?,?,?,?,?);")) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 6);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            ImmutableList.of(
+                Types.BIGINT,
+                Types.BIGINT,
+                Types.BIGINT,
+                Types.NVARCHAR,
+                Types.BIGINT,
+                Types.NVARCHAR),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "bigint",
+                    "bigint",
+                    "character varying",
+                    "bigint",
+                    "character varying")
+                : ImmutableList.of("INT64", "INT64", "INT64", "STRING", "INT64", "STRING"),
+            ImmutableList.of(
+                Long.class, Long.class, Long.class, String.class, Long.class, String.class));
         for (Song song : createSongs()) {
           ps.setByte(1, (byte) song.singerId);
           ps.setInt(2, (int) song.albumId);
@@ -441,8 +485,36 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       }
       try (PreparedStatement ps =
           connection.prepareStatement(getConcertsInsertQuery(dialect.dialect))) {
-        assertDefaultParameterMetaData(
-            ps.getParameterMetaData(), getConcertExpectedParamCount(dialect.dialect));
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Types.BIGINT, Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.NVARCHAR)
+                : ImmutableList.of(
+                    Types.BIGINT,
+                    Types.BIGINT,
+                    Types.DATE,
+                    Types.TIMESTAMP,
+                    Types.TIMESTAMP,
+                    Types.ARRAY),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "bigint",
+                    "character varying",
+                    "character varying",
+                    "character varying")
+                : ImmutableList.of(
+                    "INT64", "INT64", "DATE", "TIMESTAMP", "TIMESTAMP", "ARRAY<INT64>"),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(Long.class, Long.class, String.class, String.class, String.class)
+                : ImmutableList.of(
+                    Long.class,
+                    Long.class,
+                    Date.class,
+                    Timestamp.class,
+                    Timestamp.class,
+                    Long[].class));
         for (Concert concert : createConcerts()) {
           concert.setPreparedStatement(connection, ps, getDialect());
           assertInsertConcertParameterMetadata(ps.getParameterMetaData());
@@ -564,7 +636,24 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
           try (PreparedStatement ps =
               connection.prepareStatement(
                   "INSERT INTO Concerts (VenueId, SingerId, ConcertDate, BeginTime, EndTime, TicketPrices) VALUES (?,?,?,?,?,?);")) {
-            assertDefaultParameterMetaData(ps.getParameterMetaData(), 6);
+            assertParameterMetaData(
+                ps.getParameterMetaData(),
+                ImmutableList.of(
+                    Types.BIGINT,
+                    Types.BIGINT,
+                    Types.DATE,
+                    Types.TIMESTAMP,
+                    Types.TIMESTAMP,
+                    Types.ARRAY),
+                ImmutableList.of(
+                    "INT64", "INT64", "DATE", "TIMESTAMP", "TIMESTAMP", "ARRAY<INT64>"),
+                ImmutableList.of(
+                    Long.class,
+                    Long.class,
+                    Date.class,
+                    Timestamp.class,
+                    Timestamp.class,
+                    Long[].class));
             ps.setLong(1, 100);
             ps.setLong(2, 19);
             ps.setDate(3, testDate);
@@ -660,7 +749,24 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
           try (PreparedStatement ps =
               connection.prepareStatement(
                   "INSERT INTO Concerts (VenueId, SingerId, ConcertDate, BeginTime, EndTime, TicketPrices) VALUES (?,?,?,?,?,?);")) {
-            assertDefaultParameterMetaData(ps.getParameterMetaData(), 6);
+            assertParameterMetaData(
+                ps.getParameterMetaData(),
+                ImmutableList.of(
+                    Types.BIGINT,
+                    Types.BIGINT,
+                    Types.DATE,
+                    Types.TIMESTAMP,
+                    Types.TIMESTAMP,
+                    Types.ARRAY),
+                ImmutableList.of(
+                    "INT64", "INT64", "DATE", "TIMESTAMP", "TIMESTAMP", "ARRAY<INT64>"),
+                ImmutableList.of(
+                    Long.class,
+                    Long.class,
+                    Date.class,
+                    Timestamp.class,
+                    Timestamp.class,
+                    Long[].class));
             ps.setLong(1, 100);
             ps.setLong(2, 19);
             ps.setDate(3, new Date(System.currentTimeMillis()));
@@ -868,7 +974,33 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
             + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
     try (Connection con = createConnection(env, database)) {
       try (PreparedStatement ps = con.prepareStatement(sql)) {
+        ParameterMetaData metadata = ps.getParameterMetaData();
+        assertEquals(22, metadata.getParameterCount());
         int index = 0;
+        assertEquals(Types.BIGINT, metadata.getParameterType(++index));
+        assertEquals(Types.DOUBLE, metadata.getParameterType(++index));
+        assertEquals(Types.BOOLEAN, metadata.getParameterType(++index));
+        assertEquals(Types.NVARCHAR, metadata.getParameterType(++index));
+        assertEquals(Types.NVARCHAR, metadata.getParameterType(++index));
+        assertEquals(Types.BINARY, metadata.getParameterType(++index));
+        assertEquals(Types.BINARY, metadata.getParameterType(++index));
+        assertEquals(Types.DATE, metadata.getParameterType(++index));
+        assertEquals(Types.TIMESTAMP, metadata.getParameterType(++index));
+        assertEquals(Types.NUMERIC, metadata.getParameterType(++index));
+        assertEquals(JsonType.VENDOR_TYPE_NUMBER, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+        assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+
+        index = 0;
         ps.setLong(++index, 1L);
         ps.setDouble(++index, 2D);
         ps.setBoolean(++index, true);
@@ -1182,18 +1314,28 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
     }
   }
 
-  private void assertDefaultParameterMetaData(ParameterMetaData pmd, int expectedParamCount)
+  private void assertParameterMetaData(
+      ParameterMetaData pmd,
+      ImmutableList<Integer> sqlTypes,
+      ImmutableList<String> typeNames,
+      ImmutableList<Class<?>> classNames)
       throws SQLException {
-    assertEquals(expectedParamCount, pmd.getParameterCount());
-    for (int param = 1; param <= expectedParamCount; param++) {
-      assertEquals(Types.OTHER, pmd.getParameterType(param));
-      assertEquals("OTHER", pmd.getParameterTypeName(param));
+    assertEquals(sqlTypes.size(), typeNames.size());
+    assertEquals(sqlTypes.size(), classNames.size());
+
+    ImmutableList<Integer> signedTypes =
+        ImmutableList.of(Types.BIGINT, Types.NUMERIC, Types.DOUBLE);
+    assertEquals(sqlTypes.size(), pmd.getParameterCount());
+    for (int param = 1; param <= sqlTypes.size(); param++) {
+      String msg = "Param " + param;
+      assertEquals(msg, sqlTypes.get(param - 1).intValue(), pmd.getParameterType(param));
+      assertEquals(msg, typeNames.get(param - 1), pmd.getParameterTypeName(param));
       assertEquals(0, pmd.getPrecision(param));
       assertEquals(0, pmd.getScale(param));
-      assertNull(pmd.getParameterClassName(param));
+      assertEquals(msg, classNames.get(param - 1).getName(), pmd.getParameterClassName(param));
       assertEquals(ParameterMetaData.parameterModeIn, pmd.getParameterMode(param));
       assertEquals(ParameterMetaData.parameterNullableUnknown, pmd.isNullable(param));
-      assertFalse(pmd.isSigned(param));
+      assertEquals(msg, signedTypes.contains(sqlTypes.get(param - 1)), pmd.isSigned(param));
     }
   }
 
@@ -1214,7 +1356,26 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       deleteStatements.executeBatch();
       try (PreparedStatement ps =
           connection.prepareStatement(getSingersInsertReturningQuery(dialect.dialect))) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 5);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.BINARY, Types.NVARCHAR)
+                : ImmutableList.of(
+                    Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.BINARY, Types.DATE),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "character varying",
+                    "character varying",
+                    "bytea",
+                    "character varying")
+                : ImmutableList.of("INT64", "STRING", "STRING", "BYTES", "DATE"),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Long.class, String.class, String.class, byte[].class, String.class)
+                : ImmutableList.of(
+                    Long.class, String.class, String.class, byte[].class, Date.class));
         for (Singer singer : createSingers()) {
           singer.setPreparedStatement(ps, getDialect());
           assertInsertSingerParameterMetadata(ps.getParameterMetaData());
@@ -1229,7 +1390,13 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       }
       try (PreparedStatement ps =
           connection.prepareStatement(getAlbumsInsertReturningQuery(dialect.dialect))) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 4);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            ImmutableList.of(Types.BIGINT, Types.BIGINT, Types.NVARCHAR, Types.BIGINT),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of("bigint", "bigint", "character varying", "bigint")
+                : ImmutableList.of("INT64", "INT64", "STRING", "INT64"),
+            ImmutableList.of(Long.class, Long.class, String.class, Long.class));
         for (Album album : createAlbums()) {
           ps.setLong(1, album.singerId);
           ps.setLong(2, album.albumId);
@@ -1249,7 +1416,26 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       }
       try (PreparedStatement ps =
           connection.prepareStatement(getSongsInsertReturningQuery(dialect.dialect))) {
-        assertDefaultParameterMetaData(ps.getParameterMetaData(), 6);
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            ImmutableList.of(
+                Types.BIGINT,
+                Types.BIGINT,
+                Types.BIGINT,
+                Types.NVARCHAR,
+                Types.BIGINT,
+                Types.NVARCHAR),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "bigint",
+                    "bigint",
+                    "character varying",
+                    "bigint",
+                    "character varying")
+                : ImmutableList.of("INT64", "INT64", "INT64", "STRING", "INT64", "STRING"),
+            ImmutableList.of(
+                Long.class, Long.class, Long.class, String.class, Long.class, String.class));
         for (Song song : createSongs()) {
           ps.setByte(1, (byte) song.singerId);
           ps.setInt(2, (int) song.albumId);
@@ -1277,8 +1463,36 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       }
       try (PreparedStatement ps =
           connection.prepareStatement(getConcertsInsertReturningQuery(dialect.dialect))) {
-        assertDefaultParameterMetaData(
-            ps.getParameterMetaData(), getConcertExpectedParamCount(dialect.dialect));
+        assertParameterMetaData(
+            ps.getParameterMetaData(),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    Types.BIGINT, Types.BIGINT, Types.NVARCHAR, Types.NVARCHAR, Types.NVARCHAR)
+                : ImmutableList.of(
+                    Types.BIGINT,
+                    Types.BIGINT,
+                    Types.DATE,
+                    Types.TIMESTAMP,
+                    Types.TIMESTAMP,
+                    Types.ARRAY),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(
+                    "bigint",
+                    "bigint",
+                    "character varying",
+                    "character varying",
+                    "character varying")
+                : ImmutableList.of(
+                    "INT64", "INT64", "DATE", "TIMESTAMP", "TIMESTAMP", "ARRAY<INT64>"),
+            dialect.dialect == Dialect.POSTGRESQL
+                ? ImmutableList.of(Long.class, Long.class, String.class, String.class, String.class)
+                : ImmutableList.of(
+                    Long.class,
+                    Long.class,
+                    Date.class,
+                    Timestamp.class,
+                    Timestamp.class,
+                    Long[].class));
         for (Concert concert : createConcerts()) {
           concert.setPreparedStatement(connection, ps, getDialect());
           assertInsertConcertParameterMetadata(ps.getParameterMetaData());