Skip to content

Commit 8c7b665

Browse files
authored
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
1 parent ca852a7 commit 8c7b665

File tree

5 files changed

+118
-15
lines changed

5 files changed

+118
-15
lines changed

src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.sql.PreparedStatement;
3030
import java.sql.Ref;
3131
import java.sql.ResultSet;
32-
import java.sql.ResultSetMetaData;
3332
import java.sql.RowId;
3433
import java.sql.SQLException;
3534
import java.sql.SQLXML;
@@ -232,14 +231,6 @@ public void setArray(int parameterIndex, Array value) throws SQLException {
232231
parameters.setParameter(parameterIndex, value, Types.ARRAY);
233232
}
234233

235-
@Override
236-
public ResultSetMetaData getMetaData() throws SQLException {
237-
checkClosed();
238-
try (ResultSet rs = executeQuery()) {
239-
return rs.getMetaData();
240-
}
241-
}
242-
243234
@Override
244235
public void setDate(int parameterIndex, Date value, Calendar cal) throws SQLException {
245236
checkClosed();

src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.spanner.Options;
2020
import com.google.cloud.spanner.Options.QueryOption;
21+
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
2122
import com.google.cloud.spanner.SpannerException;
2223
import com.google.cloud.spanner.connection.StatementResult;
2324
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
@@ -147,6 +148,20 @@ private void resetStatementTimeout(StatementTimeout originalTimeout) throws SQLE
147148
}
148149
}
149150

151+
/**
152+
* Executes a SQL statement on the connection of this {@link Statement} as a query using the given
153+
* {@link QueryAnalyzeMode}.
154+
*
155+
* @param statement the SQL statement to executed
156+
* @param analyzeMode the {@link QueryAnalyzeMode} to use
157+
* @return the result of the SQL statement as a {@link ResultSet}
158+
* @throws SQLException if a database error occurs.
159+
*/
160+
ResultSet analyzeQuery(com.google.cloud.spanner.Statement statement, QueryAnalyzeMode analyzeMode)
161+
throws SQLException {
162+
return executeQuery(statement, analyzeMode);
163+
}
164+
150165
/**
151166
* Executes a SQL statement on the connection of this {@link Statement} as a query.
152167
*
@@ -157,11 +172,24 @@ private void resetStatementTimeout(StatementTimeout originalTimeout) throws SQLE
157172
*/
158173
ResultSet executeQuery(com.google.cloud.spanner.Statement statement, QueryOption... options)
159174
throws SQLException {
175+
return executeQuery(statement, null, options);
176+
}
177+
178+
private ResultSet executeQuery(
179+
com.google.cloud.spanner.Statement statement,
180+
QueryAnalyzeMode analyzeMode,
181+
QueryOption... options)
182+
throws SQLException {
160183
StatementTimeout originalTimeout = setTemporaryStatementTimeout();
161184
try {
162-
return JdbcResultSet.of(
163-
this,
164-
connection.getSpannerConnection().executeQuery(statement, getQueryOptions(options)));
185+
com.google.cloud.spanner.ResultSet resultSet;
186+
if (analyzeMode == null) {
187+
resultSet =
188+
connection.getSpannerConnection().executeQuery(statement, getQueryOptions(options));
189+
} else {
190+
resultSet = connection.getSpannerConnection().analyzeQuery(statement, analyzeMode);
191+
}
192+
return JdbcResultSet.of(this, resultSet);
165193
} catch (SpannerException e) {
166194
throw JdbcSqlExceptionFactory.of(e);
167195
} finally {

src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717
package com.google.cloud.spanner.jdbc;
1818

1919
import com.google.cloud.spanner.Options.QueryOption;
20+
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
21+
import com.google.cloud.spanner.ResultSets;
2022
import com.google.cloud.spanner.Statement;
23+
import com.google.cloud.spanner.Struct;
24+
import com.google.cloud.spanner.Type;
2125
import com.google.cloud.spanner.connection.StatementParser;
2226
import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
27+
import com.google.common.collect.ImmutableList;
2328
import java.sql.PreparedStatement;
2429
import java.sql.ResultSet;
30+
import java.sql.ResultSetMetaData;
2531
import java.sql.SQLException;
2632

2733
/** Implementation of {@link PreparedStatement} for Cloud Spanner. */
@@ -86,4 +92,20 @@ public JdbcParameterMetaData getParameterMetaData() throws SQLException {
8692
checkClosed();
8793
return new JdbcParameterMetaData(this);
8894
}
95+
96+
@Override
97+
public ResultSetMetaData getMetaData() throws SQLException {
98+
checkClosed();
99+
if (StatementParser.INSTANCE.isUpdateStatement(sql)) {
100+
// Return metadata for an empty result set as DML statements do not return any results (as a
101+
// result set).
102+
com.google.cloud.spanner.ResultSet resultSet =
103+
ResultSets.forRows(Type.struct(), ImmutableList.<Struct>of());
104+
resultSet.next();
105+
return new JdbcResultSetMetaData(JdbcResultSet.of(resultSet), this);
106+
}
107+
try (ResultSet rs = analyzeQuery(createStatement(), QueryAnalyzeMode.PLAN)) {
108+
return rs.getMetaData();
109+
}
110+
}
89111
}

src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static org.mockito.Mockito.mock;
2626
import static org.mockito.Mockito.when;
2727

28+
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
2829
import com.google.cloud.spanner.ResultSet;
2930
import com.google.cloud.spanner.ResultSets;
3031
import com.google.cloud.spanner.Statement;
@@ -293,7 +294,7 @@ public void testGetResultSetMetadata() throws SQLException {
293294
.set("AMOUNT")
294295
.to(Math.PI)
295296
.build()));
296-
when(connection.executeQuery(Statement.of(sql))).thenReturn(rs);
297+
when(connection.analyzeQuery(Statement.of(sql), QueryAnalyzeMode.PLAN)).thenReturn(rs);
297298
try (JdbcPreparedStatement ps =
298299
new JdbcPreparedStatement(createMockConnection(connection), sql)) {
299300
ResultSetMetaData metadata = ps.getMetaData();
@@ -306,4 +307,15 @@ public void testGetResultSetMetadata() throws SQLException {
306307
assertEquals(Types.DOUBLE, metadata.getColumnType(3));
307308
}
308309
}
310+
311+
@Test
312+
public void testGetResultSetMetaDataForDml() throws SQLException {
313+
Connection connection = mock(Connection.class);
314+
try (JdbcPreparedStatement ps =
315+
new JdbcPreparedStatement(
316+
createMockConnection(connection), "UPDATE FOO SET BAR=1 WHERE TRUE")) {
317+
ResultSetMetaData metadata = ps.getMetaData();
318+
assertEquals(0, metadata.getColumnCount());
319+
}
320+
}
309321
}

src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@
1616

1717
package com.google.cloud.spanner.jdbc.it;
1818

19+
import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
1920
import static org.junit.Assert.assertArrayEquals;
2021
import static org.junit.Assert.assertEquals;
2122
import static org.junit.Assert.assertFalse;
2223
import static org.junit.Assert.assertNotNull;
2324
import static org.junit.Assert.assertNull;
2425
import static org.junit.Assert.assertTrue;
2526
import static org.junit.Assert.fail;
27+
import static org.junit.Assume.assumeFalse;
2628

27-
import com.google.api.client.util.Base64;
2829
import com.google.cloud.spanner.IntegrationTest;
2930
import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest;
3031
import com.google.common.base.Strings;
32+
import com.google.common.io.BaseEncoding;
3133
import java.io.File;
3234
import java.io.FileNotFoundException;
3335
import java.io.StringReader;
@@ -38,6 +40,7 @@
3840
import java.sql.ParameterMetaData;
3941
import java.sql.PreparedStatement;
4042
import java.sql.ResultSet;
43+
import java.sql.ResultSetMetaData;
4144
import java.sql.SQLException;
4245
import java.sql.Statement;
4346
import java.sql.Timestamp;
@@ -222,7 +225,7 @@ private static Long[] parseLongArray(String value) {
222225
}
223226

224227
private static byte[] parseBytes(String value) {
225-
return Base64.decodeBase64(value);
228+
return BaseEncoding.base64().decode(value);
226229
}
227230

228231
private List<Singer> createSingers() {
@@ -825,6 +828,53 @@ public void test08_InsertAllColumnTypes() throws SQLException {
825828
}
826829
}
827830

831+
@Test
832+
public void test09_MetaData_FromQuery() throws SQLException {
833+
assumeFalse("The emulator does not support PLAN mode", isUsingEmulator());
834+
try (Connection con = createConnection()) {
835+
try (PreparedStatement ps =
836+
con.prepareStatement("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=?")) {
837+
ResultSetMetaData metadata = ps.getMetaData();
838+
assertEquals(22, metadata.getColumnCount());
839+
int index = 0;
840+
assertEquals("ColInt64", metadata.getColumnLabel(++index));
841+
assertEquals("ColFloat64", metadata.getColumnLabel(++index));
842+
assertEquals("ColBool", metadata.getColumnLabel(++index));
843+
assertEquals("ColString", metadata.getColumnLabel(++index));
844+
assertEquals("ColStringMax", metadata.getColumnLabel(++index));
845+
assertEquals("ColBytes", metadata.getColumnLabel(++index));
846+
assertEquals("ColBytesMax", metadata.getColumnLabel(++index));
847+
assertEquals("ColDate", metadata.getColumnLabel(++index));
848+
assertEquals("ColTimestamp", metadata.getColumnLabel(++index));
849+
assertEquals("ColCommitTS", metadata.getColumnLabel(++index));
850+
assertEquals("ColNumeric", metadata.getColumnLabel(++index));
851+
assertEquals("ColInt64Array", metadata.getColumnLabel(++index));
852+
assertEquals("ColFloat64Array", metadata.getColumnLabel(++index));
853+
assertEquals("ColBoolArray", metadata.getColumnLabel(++index));
854+
assertEquals("ColStringArray", metadata.getColumnLabel(++index));
855+
assertEquals("ColStringMaxArray", metadata.getColumnLabel(++index));
856+
assertEquals("ColBytesArray", metadata.getColumnLabel(++index));
857+
assertEquals("ColBytesMaxArray", metadata.getColumnLabel(++index));
858+
assertEquals("ColDateArray", metadata.getColumnLabel(++index));
859+
assertEquals("ColTimestampArray", metadata.getColumnLabel(++index));
860+
assertEquals("ColNumericArray", metadata.getColumnLabel(++index));
861+
assertEquals("ColComputed", metadata.getColumnLabel(++index));
862+
}
863+
}
864+
}
865+
866+
@Test
867+
public void test10_MetaData_FromDML() throws SQLException {
868+
try (Connection con = createConnection()) {
869+
try (PreparedStatement ps =
870+
con.prepareStatement(
871+
"UPDATE TableWithAllColumnTypes SET ColBool=FALSE WHERE ColInt64=?")) {
872+
ResultSetMetaData metadata = ps.getMetaData();
873+
assertEquals(0, metadata.getColumnCount());
874+
}
875+
}
876+
}
877+
828878
private void assertDefaultParameterMetaData(ParameterMetaData pmd, int expectedParamCount)
829879
throws SQLException {
830880
assertEquals(expectedParamCount, pmd.getParameterCount());

0 commit comments

Comments
 (0)