Skip to content

Commit

Permalink
perf: use PLAN mode to get result metadata (#388)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
olavloite committed Mar 18, 2021
1 parent ca852a7 commit 8c7b665
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 15 deletions.
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -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 {
Expand Down
Expand Up @@ -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. */
Expand Down Expand Up @@ -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.<Struct>of());
resultSet.next();
return new JdbcResultSetMetaData(JdbcResultSet.of(resultSet), this);
}
try (ResultSet rs = analyzeQuery(createStatement(), QueryAnalyzeMode.PLAN)) {
return rs.getMetaData();
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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());
}
}
}
Expand Up @@ -16,18 +16,20 @@

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;
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;

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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Singer> createSingers() {
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 8c7b665

Please sign in to comment.