Date: Tue, 19 Dec 2023 07:30:49 +0100
Subject: [PATCH 03/12] perf: optimize isValid implementation (#1444)
Cloud Spanner JDBC connections do not maintain a physical connection to
Cloud Spanner, but are merely a wrapper around the underlying Java
client library. This again uses a pool of gRPC channels to communicate
with Cloud Spanner. This means that a single JDBC connection will never
lose its network connection with Cloud Spanner, and checking whether it
is valid or not by executing a query every time is not useful. Instead,
the check should:
1. Verify that a connection has successfully been established with Cloud
Spanner. The result should be cached for all JDBC connections using
the same Cloud Spanner client library instance.
2. Verify that the connection has not been closed.
The above can be achieved by checking that the dialect of the database
that the connection is connected to has been successfully fetched. This
result is cached in the client library, and being able to get that means
that there has been a valid connection.
This means that the isValid method will still return true if the network
connectivity has been lost completely between the client and Cloud
Spanner. However, this check is mostly used by connection pools to
determine whether a connection is safe to be handed out to an
application, and when all network connectivity has been lost, this will
apply to all JDBC connections and not just one, meaning that the check
is void.
The original isValid check 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.
Fixes #1443
---
.../cloud/spanner/jdbc/JdbcConnection.java | 64 ++++++++++++++++---
.../spanner/jdbc/JdbcConnectionTest.java | 4 +-
2 files changed, 56 insertions(+), 12 deletions(-)
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 9248dace4..4cef6b3f1 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.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.
+ *
+ * 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 NO_GENERATED_KEY_COLUMNS = ImmutableList.of();
private Map> 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 @@ public void setTypeMap(Map> map) throws SQLException {
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;
}
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 ff811f370..8d26bb317 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 void testIsValid() throws SQLException {
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 void testIsValid() throws SQLException {
}
// 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"));
From 774c8d31e86e3d1209678bdfbf6b1390ad6d58cd Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 19 Dec 2023 07:31:15 +0100
Subject: [PATCH 04/12] build(deps): update dependency
com.google.cloud:google-cloud-shared-config to v1.7.1 (#1442)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 90ac8a53d..1f01bee35 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
com.google.cloud
google-cloud-shared-config
- 1.6.1
+ 1.7.1
From 59975553826360b86492e50b9d49c29aecc28bab Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 19 Dec 2023 07:33:01 +0100
Subject: [PATCH 05/12] deps: update dependency org.postgresql:postgresql to
v42.7.1 (#1441)
---
samples/spring-data-jdbc/pom.xml | 2 +-
samples/spring-data-mybatis/pom.xml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml
index d1dd5748b..1a1130735 100644
--- a/samples/spring-data-jdbc/pom.xml
+++ b/samples/spring-data-jdbc/pom.xml
@@ -52,7 +52,7 @@
org.postgresql
postgresql
- 42.7.0
+ 42.7.1
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 4dbd0925b..03b3918c4 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -62,7 +62,7 @@
org.postgresql
postgresql
- 42.7.0
+ 42.7.1
From b84a1a40f45e4779113429549b501ac18c36ec7a Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Tue, 19 Dec 2023 07:40:42 +0100
Subject: [PATCH 06/12] chore(deps): update dependency
com.google.cloud:libraries-bom to v26.29.0 (#1440)
---
samples/snippets/pom.xml | 2 +-
samples/spring-data-jdbc/pom.xml | 2 +-
samples/spring-data-mybatis/pom.xml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 8a87cff39..df372f26d 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -30,7 +30,7 @@
com.google.cloud
libraries-bom
- 26.27.0
+ 26.29.0
pom
import
diff --git a/samples/spring-data-jdbc/pom.xml b/samples/spring-data-jdbc/pom.xml
index 1a1130735..cf500a9ef 100644
--- a/samples/spring-data-jdbc/pom.xml
+++ b/samples/spring-data-jdbc/pom.xml
@@ -30,7 +30,7 @@
com.google.cloud
libraries-bom
- 26.27.0
+ 26.29.0
import
pom
diff --git a/samples/spring-data-mybatis/pom.xml b/samples/spring-data-mybatis/pom.xml
index 03b3918c4..28342f9ae 100644
--- a/samples/spring-data-mybatis/pom.xml
+++ b/samples/spring-data-mybatis/pom.xml
@@ -35,7 +35,7 @@
com.google.cloud
libraries-bom
- 26.27.0
+ 26.29.0
import
pom
From 90ded1c7affede6dd3f2e3b7c31772d0c3ac4e28 Mon Sep 17 00:00:00 2001
From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com>
Date: Tue, 19 Dec 2023 07:41:10 +0100
Subject: [PATCH 07/12] build(deps): bump cryptography from 41.0.3 to 41.0.6 in
/synthtool/gcp/templates/java_library/.kokoro (#1908) (#1439)
build(deps): bump cryptography
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.6)
---
updated-dependencies:
- dependency-name: cryptography
dependency-type: indirect
...
Source-Link: https://github.com/googleapis/synthtool/commit/ea6f80056a7d22f4d3a3e8fee2d59cdc746470bd
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-java:latest@sha256:81f75d962cd28b7ad10740a643b8069b8fa0357cb495b782eef8560bb7a8fd65
Signed-off-by: dependabot[bot]
Co-authored-by: Owl Bot
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/.OwlBot.lock.yaml | 4 ++--
.kokoro/requirements.txt | 48 +++++++++++++++++++--------------------
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index d304652e1..f56920557 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -13,5 +13,5 @@
# limitations under the License.
docker:
image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest
- digest: sha256:6f431774e11cc46619cf093fd1481193c4024031073697fa18f0099b943aab88
-# created: 2023-12-01T19:50:20.444857406Z
+ digest: sha256:81f75d962cd28b7ad10740a643b8069b8fa0357cb495b782eef8560bb7a8fd65
+# created: 2023-12-05T19:16:19.735195992Z
diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt
index c5c11bbe7..445c5c1f0 100644
--- a/.kokoro/requirements.txt
+++ b/.kokoro/requirements.txt
@@ -170,30 +170,30 @@ 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
+cryptography==41.0.6 \
+ --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \
+ --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \
+ --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \
+ --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \
+ --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \
+ --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \
+ --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \
+ --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \
+ --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \
+ --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \
+ --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \
+ --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \
+ --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \
+ --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \
+ --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \
+ --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \
+ --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \
+ --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \
+ --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \
+ --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \
+ --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \
+ --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \
+ --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae
# via
# gcp-releasetool
# secretstorage
From 8a523f70cf438c9df92f26599be2205c7ccdf47f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 19 Dec 2023 07:41:44 +0100
Subject: [PATCH 08/12] build(deps): bump cryptography from 41.0.2 to 41.0.6 in
/.kokoro (#1429)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* build(deps): bump cryptography from 41.0.2 to 41.0.6 in /.kokoro
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.6)
---
updated-dependencies:
- dependency-name: cryptography
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
* 🦉 Updates from OwlBot post-processor
See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Owl Bot
From eba46bd0a703824df77ad164703d339d10a01467 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Thu, 21 Dec 2023 15:15:29 +0100
Subject: [PATCH 09/12] test(deps): update dependency com.google.truth:truth to
v1.2.0 (#1447)
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 1f01bee35..5a343b399 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,7 @@
google-cloud-spanner-jdbc
4.13.2
3.0.2
- 1.1.5
+ 1.2.0
4.11.0
2.2
0.31.1
From 0e15ba1ee070cd680a7d1ab624ed71e43c4d82f1 Mon Sep 17 00:00:00 2001
From: Mend Renovate
Date: Thu, 21 Dec 2023 15:15:48 +0100
Subject: [PATCH 10/12] test(deps): update dependency com.google.truth:truth to
v1.2.0 (#1448)
---
samples/install-without-bom/pom.xml | 2 +-
samples/snapshot/pom.xml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index d71f8cf03..86197f23c 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -42,7 +42,7 @@
com.google.truth
truth
- 1.1.5
+ 1.2.0
test
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index ce3483e6f..56200a16d 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -41,7 +41,7 @@
com.google.truth
truth
- 1.1.5
+ 1.2.0
test
From 721ff4552104efba47c19ef511282071c3b334c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?=
Date: Fri, 22 Dec 2023 11:34:06 +0100
Subject: [PATCH 11/12] feat: support PreparedStatement#getParameterMetaData()
(#1218)
* feat: support PreparedStatement#getParameterMetaData()
Add actual support for `PreparedStatement#getParameterMetaData()`. The first time
this method is called for a PreparedStatement, the connection will now send the
query to Cloud Spanner in analyze mode and without any parameter values. This
will instruct Cloud Spanner to return the names and types of any query parameters
in the statement.
Fixes #35
* fix: restore previous behavior
* fix: PostgreSQL string type name should be 'character varying'
* fix: update type name to 'character varying' in integration test
---
.../cloud/spanner/JdbcDataTypeConverter.java | 29 ++
.../spanner/jdbc/AbstractJdbcWrapper.java | 77 +++-
.../cloud/spanner/jdbc/JdbcDataType.java | 8 +-
.../spanner/jdbc/JdbcParameterMetaData.java | 139 +++++--
.../spanner/jdbc/JdbcPreparedStatement.java | 30 +-
.../spanner/jdbc/AbstractJdbcWrapperTest.java | 67 ++++
.../jdbc/JdbcPreparedStatementTest.java | 56 ++-
...PreparedStatementWithMockedServerTest.java | 182 ++++++++-
...reparedStatementParameterMetadataTest.java | 361 ++++++++++++++++++
.../jdbc/it/ITJdbcPreparedStatementTest.java | 254 +++++++++++-
10 files changed, 1140 insertions(+), 63 deletions(-)
create mode 100644 src/main/java/com/google/cloud/spanner/JdbcDataTypeConverter.java
create mode 100644 src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java
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 000000000..62d52c6c3
--- /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 f577ac3c6..b56aed037 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 @@ static int extractColumnType(Type type) {
}
}
- /** 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";
+ case BYTES:
+ return dialect == Dialect.POSTGRESQL ? "bytea[]" : "ARRAY";
+ case DATE:
+ return dialect == Dialect.POSTGRESQL ? "date[]" : "ARRAY";
+ case FLOAT64:
+ return dialect == Dialect.POSTGRESQL ? "double precision[]" : "ARRAY";
+ case INT64:
+ return dialect == Dialect.POSTGRESQL ? "bigint[]" : "ARRAY";
+ case NUMERIC:
+ return "ARRAY";
+ case PG_NUMERIC:
+ return "numeric[]";
+ case STRING:
+ return dialect == Dialect.POSTGRESQL ? "character varying[]" : "ARRAY";
+ case JSON:
+ return "ARRAY";
+ case PG_JSONB:
+ return "jsonb[]";
+ case TIMESTAMP:
+ return dialect == Dialect.POSTGRESQL
+ ? "timestamp with time zone[]"
+ : "ARRAY";
+ case STRUCT:
+ return "ARRAY";
+ }
+ 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 @@ static String getSpannerTypeName(int sqlType) {
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/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index c495bbe16..5dd082ef8 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 @@ public Set extends Class>> getSupportedJavaClasses() {
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/JdbcParameterMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
index a520e221e..82a4b9133 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 @@
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 @@ public boolean isClosed() {
@Override
public int getParameterCount() {
- ParametersInfo info = statement.getParametersInfo();
- return info.numberOfParameters;
+ return parameters.getFieldsCount();
}
@Override
@@ -53,7 +72,7 @@ public int isNullable(int param) {
}
@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 @@ public int getScale(int param) {
}
@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 @@ public int getParameterType(int param) {
}
@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 @@ public int getParameterMode(int param) {
@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 518807dd1..9ebbc98f5 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 generatedKeysColumns;
JdbcPreparedStatement(
@@ -118,7 +119,34 @@ public void addBatch() throws SQLException {
@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/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
index 372bbb090..f8473f638 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.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 void testParseTimestampWithCalendar() throws SQLException {
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", getSpannerTypeName(Type.array(Type.int64()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY", getSpannerTypeName(Type.array(Type.bool()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY",
+ getSpannerTypeName(Type.array(Type.float64()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY",
+ getSpannerTypeName(Type.array(Type.string()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY", getSpannerTypeName(Type.array(Type.bytes()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY", getSpannerTypeName(Type.array(Type.date()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY",
+ getSpannerTypeName(Type.array(Type.timestamp()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY", getSpannerTypeName(Type.array(Type.json()), Dialect.GOOGLE_STANDARD_SQL));
+ assertEquals(
+ "ARRAY",
+ 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/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
index 310d1546e..c5748d1c7 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 @@
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.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.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 void testParameters() throws SQLException, MalformedURLException {
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 void testParameters() throws SQLException, MalformedURLException {
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 void testParameters() throws SQLException, MalformedURLException {
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 void testInvalidSql() {
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 d3607d842..a3072e310 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.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 void testExecuteBatch_withException() throws SQLException {
@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/PreparedStatementParameterMetadataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java
new file mode 100644
index 000000000..8b7130ed6
--- /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", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", metadata.getParameterTypeName(index));
+ assertEquals(Types.ARRAY, metadata.getParameterType(++index));
+ assertEquals("ARRAY", 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/it/ITJdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
index 2864559c8..8f014937a 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.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.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 void test01_InsertTestData() throws SQLException {
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 void test01_InsertTestData() throws SQLException {
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 void test01_InsertTestData() throws SQLException {
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 void test01_InsertTestData() throws SQLException {
}
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"),
+ 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 void test03_Dates() throws SQLException {
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"),
+ 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 void test04_Timestamps() throws SQLException {
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"),
+ 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 void test08_InsertAllColumnTypes() throws SQLException {
+ ") 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 void test11_InsertDataUsingSpannerValue() throws SQLException {
}
}
- private void assertDefaultParameterMetaData(ParameterMetaData pmd, int expectedParamCount)
+ private void assertParameterMetaData(
+ ParameterMetaData pmd,
+ ImmutableList sqlTypes,
+ ImmutableList typeNames,
+ ImmutableList> 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 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 void test12_InsertReturningTestData() throws SQLException {
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 void test12_InsertReturningTestData() throws SQLException {
}
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 void test12_InsertReturningTestData() throws SQLException {
}
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 void test12_InsertReturningTestData() throws SQLException {
}
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"),
+ 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());
From 2be2b1c09facd91087a027d1c1f8a0f21e4a07e5 Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Fri, 22 Dec 2023 13:30:16 +0000
Subject: [PATCH 12/12] chore(main): release 2.15.0 (#1446)
:robot: I have created a release *beep* *boop*
---
## [2.15.0](https://togithub.com/googleapis/java-spanner-jdbc/compare/v2.14.6...v2.15.0) (2023-12-22)
### Features
* Support PreparedStatement#getParameterMetaData() ([#1218](https://togithub.com/googleapis/java-spanner-jdbc/issues/1218)) ([721ff45](https://togithub.com/googleapis/java-spanner-jdbc/commit/721ff4552104efba47c19ef511282071c3b334c3))
### Performance Improvements
* Optimize isValid implementation ([#1444](https://togithub.com/googleapis/java-spanner-jdbc/issues/1444)) ([914e973](https://togithub.com/googleapis/java-spanner-jdbc/commit/914e973ad7fd638fabc3ec130b7618c51f01f401)), closes [#1443](https://togithub.com/googleapis/java-spanner-jdbc/issues/1443)
### Dependencies
* Update dependency org.postgresql:postgresql to v42.7.1 ([#1441](https://togithub.com/googleapis/java-spanner-jdbc/issues/1441)) ([5997555](https://togithub.com/googleapis/java-spanner-jdbc/commit/59975553826360b86492e50b9d49c29aecc28bab))
---
This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please).
---
CHANGELOG.md | 17 +++++++++++++++++
pom.xml | 2 +-
samples/snapshot/pom.xml | 2 +-
versions.txt | 2 +-
4 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fdf14867..1f9540e33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Changelog
+## [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/pom.xml b/pom.xml
index 5a343b399..4240553d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
google-cloud-spanner-jdbc
- 2.14.7-SNAPSHOT
+ 2.15.0
jar
Google Cloud Spanner JDBC
https://github.com/googleapis/java-spanner-jdbc
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 56200a16d..9ba5de8be 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
com.google.cloud
google-cloud-spanner-jdbc
- 2.14.7-SNAPSHOT
+ 2.15.0
diff --git a/versions.txt b/versions.txt
index 6dc9f85ed..b20313946 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.0:2.15.0