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 9ebbc98f5..ad7423422 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java @@ -29,6 +29,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.rpc.Code; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -117,8 +118,16 @@ public void addBatch() throws SQLException { } @Override - public JdbcParameterMetaData getParameterMetaData() throws SQLException { + public ParameterMetaData getParameterMetaData() throws SQLException { checkClosed(); + + // NOTE: JdbcSimpleParameterMetaData is an experimental feature that can be removed without + // warning in a future version. Your application should not assume that this feature will + // continue to be supported. + if (JdbcSimpleParameterMetaData.useSimpleParameterMetadata()) { + return new JdbcSimpleParameterMetaData(this.parameters); + } + if (cachedParameterMetadata == null) { if (getConnection().getParser().isUpdateStatement(sql) && !getConnection().getParser().checkReturningClause(sql)) { diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcSimpleParameterMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcSimpleParameterMetaData.java new file mode 100644 index 000000000..b458573d0 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcSimpleParameterMetaData.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.jdbc; + +import com.google.api.core.BetaApi; +import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo; +import java.sql.ParameterMetaData; +import java.sql.SQLException; +import java.sql.Types; + +/** + * {@link JdbcSimpleParameterMetaData} implements {@link ParameterMetaData} without a round-trip to + * Spanner. This is an experimental feature that can be removed in a future version without prior + * warning. + */ +@BetaApi +class JdbcSimpleParameterMetaData implements ParameterMetaData { + static final String USE_SIMPLE_PARAMETER_METADATA_KEY = + "spanner.jdbc.use_simple_parameter_metadata"; + private final ParametersInfo parametersInfo; + + /** + * This is an experimental feature that can be removed in a future version without prior warning. + */ + @BetaApi + static boolean useSimpleParameterMetadata() { + return Boolean.parseBoolean(System.getProperty(USE_SIMPLE_PARAMETER_METADATA_KEY, "false")); + } + + JdbcSimpleParameterMetaData(ParametersInfo parametersInfo) { + this.parametersInfo = parametersInfo; + } + + @Override + public int getParameterCount() throws SQLException { + return this.parametersInfo.numberOfParameters; + } + + @Override + public int isNullable(int param) throws SQLException { + return ParameterMetaData.parameterNullableUnknown; + } + + @Override + public boolean isSigned(int param) throws SQLException { + return false; + } + + @Override + public int getPrecision(int param) throws SQLException { + return 0; + } + + @Override + public int getScale(int param) throws SQLException { + return 0; + } + + @Override + public int getParameterType(int param) throws SQLException { + return Types.OTHER; + } + + @Override + public String getParameterTypeName(int param) throws SQLException { + return "unknown"; + } + + @Override + public String getParameterClassName(int param) throws SQLException { + return Object.class.getName(); + } + + @Override + public int getParameterMode(int param) throws SQLException { + return ParameterMetaData.parameterModeIn; + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLException("This is not a wrapper for " + iface.getName()); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } +} 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 c5748d1c7..c95c38b7a 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java @@ -221,7 +221,7 @@ public void testParameters() throws SQLException, MalformedURLException { ps.setObject(52, "{}", JsonType.VENDOR_TYPE_NUMBER); ps.setObject(53, "{}", PgJsonbType.VENDOR_TYPE_NUMBER); - JdbcParameterMetaData pmd = ps.getParameterMetaData(); + JdbcParameterMetaData pmd = (JdbcParameterMetaData) ps.getParameterMetaData(); assertEquals(numberOfParams, pmd.getParameterCount()); assertEquals(JdbcArray.class.getName(), pmd.getParameterClassName(1)); assertEquals(ByteArrayInputStream.class.getName(), pmd.getParameterClassName(2)); @@ -281,7 +281,7 @@ public void testParameters() throws SQLException, MalformedURLException { assertEquals(String.class.getName(), pmd.getParameterClassName(51)); ps.clearParameters(); - pmd = ps.getParameterMetaData(); + pmd = (JdbcParameterMetaData) ps.getParameterMetaData(); assertEquals(numberOfParams, pmd.getParameterCount()); } } @@ -329,12 +329,12 @@ public void testSetNullValues() throws SQLException { ps.setNull(++index, Types.NULL); assertEquals(numberOfParameters, index); - JdbcParameterMetaData pmd = ps.getParameterMetaData(); + JdbcParameterMetaData pmd = (JdbcParameterMetaData) ps.getParameterMetaData(); assertEquals(numberOfParameters, pmd.getParameterCount()); assertEquals(Timestamp.class.getName(), pmd.getParameterClassName(15)); ps.clearParameters(); - pmd = ps.getParameterMetaData(); + pmd = (JdbcParameterMetaData) ps.getParameterMetaData(); assertEquals(numberOfParameters, pmd.getParameterCount()); } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java index a6d7c3542..55c93cca1 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/PreparedStatementParameterMetadataTest.java @@ -24,6 +24,7 @@ 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.ExecuteSqlRequest; import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.ResultSetStats; @@ -133,6 +134,7 @@ public void testAllTypesParameterMetadata_GoogleSql() throws SQLException { assertEquals(Types.ARRAY, metadata.getParameterType(++index)); assertEquals("ARRAY", metadata.getParameterTypeName(index)); } + assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class)); } } @@ -214,6 +216,36 @@ public void testAllTypesParameterMetadata_PostgreSQL() throws SQLException { assertEquals(Types.ARRAY, metadata.getParameterType(++index)); assertEquals("timestamp with time zone[]", metadata.getParameterTypeName(index)); } + assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class)); + } + } + + @Test + public void testSimpleJdbcParameterMetadata() throws SQLException { + mockSpanner.putStatementResult( + MockSpannerServiceImpl.StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + String baseSql = + "insert into all_types (col_bool, col_bytes, col_date, col_float32, col_float64, col_int64, " + + "col_json, col_numeric, col_string, col_timestamp, col_bool_array, col_bytes_array, " + + "col_date_array, col_float32_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, 20).mapToObj(ignored -> "?").collect(Collectors.joining(", "))); + + System.setProperty(JdbcSimpleParameterMetaData.USE_SIMPLE_PARAMETER_METADATA_KEY, "true"); + try (Connection connection = createJdbcConnection()) { + try (PreparedStatement statement = connection.prepareStatement(jdbcSql)) { + ParameterMetaData metadata = statement.getParameterMetaData(); + assertEquals(20, metadata.getParameterCount()); + for (int i = 0; i < metadata.getParameterCount(); i++) { + assertEquals(Types.OTHER, metadata.getParameterType(i)); + } + } + assertEquals(0, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class)); + } finally { + System.clearProperty(JdbcSimpleParameterMetaData.USE_SIMPLE_PARAMETER_METADATA_KEY); } }