From 8c7b665c0c16dbec65da5040da038a320efa0a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 18 Mar 2021 06:31:54 +0100 Subject: [PATCH] perf: use PLAN mode to get result metadata (#388) * perf: use PLAN mode to get result metadata Getting the result metadata of a PreparedStatement should not execute the query, but should instead just request the query plan. This will also return the metadata of the query. * fix: add support for DML * fix: call .next() before getting metadata * fix: skip test on emulator + remove deprecated code --- .../jdbc/AbstractJdbcPreparedStatement.java | 9 ---- .../spanner/jdbc/AbstractJdbcStatement.java | 34 ++++++++++-- .../spanner/jdbc/JdbcPreparedStatement.java | 22 ++++++++ .../jdbc/JdbcPreparedStatementTest.java | 14 ++++- .../jdbc/it/ITJdbcPreparedStatementTest.java | 54 ++++++++++++++++++- 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java index bbd5975b..09409979 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java @@ -29,7 +29,6 @@ import java.sql.PreparedStatement; import java.sql.Ref; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; import java.sql.RowId; import java.sql.SQLException; import java.sql.SQLXML; @@ -232,14 +231,6 @@ public void setArray(int parameterIndex, Array value) throws SQLException { parameters.setParameter(parameterIndex, value, Types.ARRAY); } - @Override - public ResultSetMetaData getMetaData() throws SQLException { - checkClosed(); - try (ResultSet rs = executeQuery()) { - return rs.getMetaData(); - } - } - @Override public void setDate(int parameterIndex, Date value, Calendar cal) throws SQLException { checkClosed(); diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java index 48a002d5..5217396f 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java @@ -18,6 +18,7 @@ import com.google.cloud.spanner.Options; import com.google.cloud.spanner.Options.QueryOption; +import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.StatementResult; import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType; @@ -147,6 +148,20 @@ private void resetStatementTimeout(StatementTimeout originalTimeout) throws SQLE } } + /** + * Executes a SQL statement on the connection of this {@link Statement} as a query using the given + * {@link QueryAnalyzeMode}. + * + * @param statement the SQL statement to executed + * @param analyzeMode the {@link QueryAnalyzeMode} to use + * @return the result of the SQL statement as a {@link ResultSet} + * @throws SQLException if a database error occurs. + */ + ResultSet analyzeQuery(com.google.cloud.spanner.Statement statement, QueryAnalyzeMode analyzeMode) + throws SQLException { + return executeQuery(statement, analyzeMode); + } + /** * Executes a SQL statement on the connection of this {@link Statement} as a query. * @@ -157,11 +172,24 @@ private void resetStatementTimeout(StatementTimeout originalTimeout) throws SQLE */ ResultSet executeQuery(com.google.cloud.spanner.Statement statement, QueryOption... options) throws SQLException { + return executeQuery(statement, null, options); + } + + private ResultSet executeQuery( + com.google.cloud.spanner.Statement statement, + QueryAnalyzeMode analyzeMode, + QueryOption... options) + throws SQLException { StatementTimeout originalTimeout = setTemporaryStatementTimeout(); try { - return JdbcResultSet.of( - this, - connection.getSpannerConnection().executeQuery(statement, getQueryOptions(options))); + com.google.cloud.spanner.ResultSet resultSet; + if (analyzeMode == null) { + resultSet = + connection.getSpannerConnection().executeQuery(statement, getQueryOptions(options)); + } else { + resultSet = connection.getSpannerConnection().analyzeQuery(statement, analyzeMode); + } + return JdbcResultSet.of(this, resultSet); } catch (SpannerException e) { throw JdbcSqlExceptionFactory.of(e); } finally { 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 7a0e8423..d8eeb623 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java @@ -17,11 +17,17 @@ package com.google.cloud.spanner.jdbc; import com.google.cloud.spanner.Options.QueryOption; +import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; +import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.Type; import com.google.cloud.spanner.connection.StatementParser; import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo; +import com.google.common.collect.ImmutableList; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; /** Implementation of {@link PreparedStatement} for Cloud Spanner. */ @@ -86,4 +92,20 @@ public JdbcParameterMetaData getParameterMetaData() throws SQLException { checkClosed(); return new JdbcParameterMetaData(this); } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + checkClosed(); + if (StatementParser.INSTANCE.isUpdateStatement(sql)) { + // Return metadata for an empty result set as DML statements do not return any results (as a + // result set). + com.google.cloud.spanner.ResultSet resultSet = + ResultSets.forRows(Type.struct(), ImmutableList.of()); + resultSet.next(); + return new JdbcResultSetMetaData(JdbcResultSet.of(resultSet), this); + } + try (ResultSet rs = analyzeQuery(createStatement(), QueryAnalyzeMode.PLAN)) { + return rs.getMetaData(); + } + } } 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 85334b2f..5a1235f5 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Statement; @@ -293,7 +294,7 @@ public void testGetResultSetMetadata() throws SQLException { .set("AMOUNT") .to(Math.PI) .build())); - when(connection.executeQuery(Statement.of(sql))).thenReturn(rs); + when(connection.analyzeQuery(Statement.of(sql), QueryAnalyzeMode.PLAN)).thenReturn(rs); try (JdbcPreparedStatement ps = new JdbcPreparedStatement(createMockConnection(connection), sql)) { ResultSetMetaData metadata = ps.getMetaData(); @@ -306,4 +307,15 @@ public void testGetResultSetMetadata() throws SQLException { assertEquals(Types.DOUBLE, metadata.getColumnType(3)); } } + + @Test + public void testGetResultSetMetaDataForDml() throws SQLException { + Connection connection = mock(Connection.class); + try (JdbcPreparedStatement ps = + new JdbcPreparedStatement( + createMockConnection(connection), "UPDATE FOO SET BAR=1 WHERE TRUE")) { + ResultSetMetaData metadata = ps.getMetaData(); + assertEquals(0, metadata.getColumnCount()); + } + } } 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 b4e02a98..2890ed9c 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 @@ -16,6 +16,7 @@ package com.google.cloud.spanner.jdbc.it; +import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -23,11 +24,12 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; -import com.google.api.client.util.Base64; import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; import java.io.File; import java.io.FileNotFoundException; import java.io.StringReader; @@ -38,6 +40,7 @@ import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; @@ -222,7 +225,7 @@ private static Long[] parseLongArray(String value) { } private static byte[] parseBytes(String value) { - return Base64.decodeBase64(value); + return BaseEncoding.base64().decode(value); } private List createSingers() { @@ -825,6 +828,53 @@ public void test08_InsertAllColumnTypes() throws SQLException { } } + @Test + public void test09_MetaData_FromQuery() throws SQLException { + assumeFalse("The emulator does not support PLAN mode", isUsingEmulator()); + try (Connection con = createConnection()) { + try (PreparedStatement ps = + con.prepareStatement("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=?")) { + ResultSetMetaData metadata = ps.getMetaData(); + assertEquals(22, metadata.getColumnCount()); + int index = 0; + assertEquals("ColInt64", metadata.getColumnLabel(++index)); + assertEquals("ColFloat64", metadata.getColumnLabel(++index)); + assertEquals("ColBool", metadata.getColumnLabel(++index)); + assertEquals("ColString", metadata.getColumnLabel(++index)); + assertEquals("ColStringMax", metadata.getColumnLabel(++index)); + assertEquals("ColBytes", metadata.getColumnLabel(++index)); + assertEquals("ColBytesMax", metadata.getColumnLabel(++index)); + assertEquals("ColDate", metadata.getColumnLabel(++index)); + assertEquals("ColTimestamp", metadata.getColumnLabel(++index)); + assertEquals("ColCommitTS", metadata.getColumnLabel(++index)); + assertEquals("ColNumeric", metadata.getColumnLabel(++index)); + assertEquals("ColInt64Array", metadata.getColumnLabel(++index)); + assertEquals("ColFloat64Array", metadata.getColumnLabel(++index)); + assertEquals("ColBoolArray", metadata.getColumnLabel(++index)); + assertEquals("ColStringArray", metadata.getColumnLabel(++index)); + assertEquals("ColStringMaxArray", metadata.getColumnLabel(++index)); + assertEquals("ColBytesArray", metadata.getColumnLabel(++index)); + assertEquals("ColBytesMaxArray", metadata.getColumnLabel(++index)); + assertEquals("ColDateArray", metadata.getColumnLabel(++index)); + assertEquals("ColTimestampArray", metadata.getColumnLabel(++index)); + assertEquals("ColNumericArray", metadata.getColumnLabel(++index)); + assertEquals("ColComputed", metadata.getColumnLabel(++index)); + } + } + } + + @Test + public void test10_MetaData_FromDML() throws SQLException { + try (Connection con = createConnection()) { + try (PreparedStatement ps = + con.prepareStatement( + "UPDATE TableWithAllColumnTypes SET ColBool=FALSE WHERE ColInt64=?")) { + ResultSetMetaData metadata = ps.getMetaData(); + assertEquals(0, metadata.getColumnCount()); + } + } + } + private void assertDefaultParameterMetaData(ParameterMetaData pmd, int expectedParamCount) throws SQLException { assertEquals(expectedParamCount, pmd.getParameterCount());