Skip to content

Commit b3f4cf7

Browse files
olavloiteskuruppu
andauthored
feat: add support for QueryOptions (#76)
* feat: add support for QueryOptions * fix: review comments * deps: temporarily update because of build and test errors * deps: update to released versions * fix: use grpc 1.27.2 * fix: fix invalid query hint in IT Co-authored-by: skuruppu <[email protected]>
1 parent 19eade2 commit b3f4cf7

26 files changed

+3463
-234
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
</licenses>
5959
<properties>
6060
<site.installationModule>google-cloud-spanner-jdbc</site.installationModule>
61-
<grpc.version>1.28.0</grpc.version>
61+
<grpc.version>1.27.2</grpc.version>
6262
<api-client.version>1.30.9</api-client.version>
6363
<google.core.version>1.93.3</google.core.version>
6464
<gax.version>1.54.0</gax.version>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,20 @@ public AutocommitDmlMode convert(String value) {
213213
}
214214
}
215215

216+
static class StringValueConverter implements ClientSideStatementValueConverter<String> {
217+
public StringValueConverter(String allowedValues) {}
218+
219+
@Override
220+
public Class<String> getParameterClass() {
221+
return String.class;
222+
}
223+
224+
@Override
225+
public String convert(String value) {
226+
return value;
227+
}
228+
}
229+
216230
/** Converter for converting string values to {@link TransactionMode} values. */
217231
static class TransactionModeConverter
218232
implements ClientSideStatementValueConverter<TransactionMode> {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
* <li><code>
7979
* SET READ_ONLY_STALENESS='STRONG' | 'MIN_READ_TIMESTAMP &lt;timestamp&gt;' | 'READ_TIMESTAMP &lt;timestamp&gt;' | 'MAX_STALENESS &lt;int64&gt;s|ms|mus|ns' | 'EXACT_STALENESS (&lt;int64&gt;s|ms|mus|ns)'
8080
* </code>: Sets the value of <code>READ_ONLY_STALENESS</code> for this connection.
81+
* <li><code>SHOW OPTIMIZER_VERSION</code>: Returns the current value of <code>
82+
* OPTIMIZER_VERSION</code> of this connection as a {@link ResultSet}
83+
* <li><code>
84+
* SET OPTIMIZER_VERSION='&lt;version&gt;' | 'LATEST'
85+
* </code>: Sets the value of <code>OPTIMIZER_VERSION</code> for this connection.
8186
* <li><code>BEGIN [TRANSACTION]</code>: Begins a new transaction. This statement is optional when
8287
* the connection is not in autocommit mode, as a new transaction will automatically be
8388
* started when a query or update statement is issued. In autocommit mode, this statement will
@@ -384,6 +389,24 @@ interface Connection extends AutoCloseable {
384389
*/
385390
TimestampBound getReadOnlyStaleness();
386391

392+
/**
393+
* Sets the query optimizer version to use for this connection.
394+
*
395+
* @param optimizerVersion The query optimizer version to use. Must be a valid optimizer version
396+
* number, the string <code>LATEST</code> or an empty string. The empty string will instruct
397+
* the connection to use the optimizer version that is defined in the environment variable
398+
* <code>SPANNER_OPTIMIZER_VERSION</code>. If no value is specified in the environment
399+
* variable, the default query optimizer of Cloud Spanner is used.
400+
*/
401+
void setOptimizerVersion(String optimizerVersion);
402+
403+
/**
404+
* Gets the current query optimizer version of this connection.
405+
*
406+
* @return The query optimizer version that is currently used by this connection.
407+
*/
408+
String getOptimizerVersion();
409+
387410
/**
388411
* Commits the current transaction of this connection. All mutations that have been buffered
389412
* during the current transaction will be written to the database.

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.cloud.spanner.jdbc.UnitOfWork.UnitOfWorkState;
3636
import com.google.common.annotations.VisibleForTesting;
3737
import com.google.common.base.Preconditions;
38+
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
3839
import java.util.ArrayList;
3940
import java.util.Collections;
4041
import java.util.Iterator;
@@ -192,6 +193,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
192193
private final List<TransactionRetryListener> transactionRetryListeners = new ArrayList<>();
193194
private AutocommitDmlMode autocommitDmlMode = AutocommitDmlMode.TRANSACTIONAL;
194195
private TimestampBound readOnlyStaleness = TimestampBound.strong();
196+
private QueryOptions queryOptions = QueryOptions.getDefaultInstance();
195197

196198
/** Create a connection and register it in the SpannerPool. */
197199
ConnectionImpl(ConnectionOptions options) {
@@ -204,6 +206,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
204206
this.retryAbortsInternally = options.isRetryAbortsInternally();
205207
this.readOnly = options.isReadOnly();
206208
this.autocommit = options.isAutocommit();
209+
this.queryOptions = this.queryOptions.toBuilder().mergeFrom(options.getQueryOptions()).build();
207210
this.ddlClient = createDdlClient();
208211
setDefaultTransactionOptions();
209212
}
@@ -389,6 +392,19 @@ public TimestampBound getReadOnlyStaleness() {
389392
return this.readOnlyStaleness;
390393
}
391394

395+
@Override
396+
public void setOptimizerVersion(String optimizerVersion) {
397+
Preconditions.checkNotNull(optimizerVersion);
398+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
399+
this.queryOptions = queryOptions.toBuilder().setOptimizerVersion(optimizerVersion).build();
400+
}
401+
402+
@Override
403+
public String getOptimizerVersion() {
404+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
405+
return this.queryOptions.getOptimizerVersion();
406+
}
407+
392408
@Override
393409
public void setStatementTimeout(long timeout, TimeUnit unit) {
394410
Preconditions.checkArgument(timeout > 0L, "Zero or negative timeout values are not allowed");
@@ -639,7 +655,7 @@ private void endCurrentTransaction(EndTransactionMethod endTransactionMethod) {
639655
public StatementResult execute(Statement statement) {
640656
Preconditions.checkNotNull(statement);
641657
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
642-
ParsedStatement parsedStatement = parser.parse(statement);
658+
ParsedStatement parsedStatement = parser.parse(statement, this.queryOptions);
643659
switch (parsedStatement.getType()) {
644660
case CLIENT_SIDE:
645661
return parsedStatement
@@ -680,7 +696,7 @@ private ResultSet parseAndExecuteQuery(
680696
Preconditions.checkNotNull(query);
681697
Preconditions.checkNotNull(analyzeMode);
682698
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
683-
ParsedStatement parsedStatement = parser.parse(query);
699+
ParsedStatement parsedStatement = parser.parse(query, this.queryOptions);
684700
if (parsedStatement.isQuery()) {
685701
switch (parsedStatement.getType()) {
686702
case CLIENT_SIDE:
@@ -809,7 +825,8 @@ private long[] internalExecuteBatchUpdate(final List<ParsedStatement> updates) {
809825
* Returns the current {@link UnitOfWork} of this connection, or creates a new one based on the
810826
* current transaction settings of the connection and returns that.
811827
*/
812-
private UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
828+
@VisibleForTesting
829+
UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
813830
if (this.currentUnitOfWork == null || !this.currentUnitOfWork.isActive()) {
814831
this.currentUnitOfWork = createNewUnitOfWork();
815832
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.common.annotations.VisibleForTesting;
3232
import com.google.common.base.Preconditions;
3333
import com.google.common.collect.Sets;
34+
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
3435
import java.util.ArrayList;
3536
import java.util.Arrays;
3637
import java.util.Collections;
@@ -144,6 +145,7 @@ public String[] getValidValues() {
144145
private static final String DEFAULT_OAUTH_TOKEN = null;
145146
private static final String DEFAULT_NUM_CHANNELS = null;
146147
private static final String DEFAULT_USER_AGENT = null;
148+
private static final String DEFAULT_OPTIMIZER_VERSION = "";
147149

148150
private static final String PLAIN_TEXT_PROTOCOL = "http:";
149151
private static final String HOST_PROTOCOL = "https:";
@@ -166,6 +168,8 @@ public String[] getValidValues() {
166168
public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels";
167169
/** Custom user agent string is only for other Google libraries. */
168170
private static final String USER_AGENT_PROPERTY_NAME = "userAgent";
171+
/** Query optimizer version to use for a connection. */
172+
private static final String OPTIMIZER_VERSION_PROPERTY_NAME = "optimizerVersion";
169173

170174
/** All valid connection properties. */
171175
public static final Set<ConnectionProperty> VALID_PROPERTIES =
@@ -183,7 +187,8 @@ public String[] getValidValues() {
183187
ConnectionProperty.createStringProperty(NUM_CHANNELS_PROPERTY_NAME, ""),
184188
ConnectionProperty.createBooleanProperty(
185189
USE_PLAIN_TEXT_PROPERTY_NAME, "", DEFAULT_USE_PLAIN_TEXT),
186-
ConnectionProperty.createStringProperty(USER_AGENT_PROPERTY_NAME, ""))));
190+
ConnectionProperty.createStringProperty(USER_AGENT_PROPERTY_NAME, ""),
191+
ConnectionProperty.createStringProperty(OPTIMIZER_VERSION_PROPERTY_NAME, ""))));
187192

188193
private static final Set<ConnectionProperty> INTERNAL_PROPERTIES =
189194
Collections.unmodifiableSet(
@@ -281,6 +286,7 @@ private boolean isValidUri(String uri) {
281286
* false.
282287
* <li>retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the
283288
* connection. Default is true.
289+
* <li>optimizerVersion (string): Sets the query optimizer version to use for the connection.
284290
* </ul>
285291
*
286292
* @param uri The URI of the Spanner database to connect to.
@@ -373,6 +379,7 @@ public static Builder newBuilder() {
373379
private final Credentials credentials;
374380
private final Integer numChannels;
375381
private final String userAgent;
382+
private final QueryOptions queryOptions;
376383

377384
private final boolean autocommit;
378385
private final boolean readOnly;
@@ -397,6 +404,9 @@ private ConnectionOptions(Builder builder) {
397404

398405
this.usePlainText = parseUsePlainText(this.uri);
399406
this.userAgent = parseUserAgent(this.uri);
407+
QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder();
408+
queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri));
409+
this.queryOptions = queryOptionsBuilder.build();
400410

401411
this.host =
402412
matcher.group(Builder.HOST_GROUP) == null
@@ -501,6 +511,12 @@ static String parseUserAgent(String uri) {
501511
return value != null ? value : DEFAULT_USER_AGENT;
502512
}
503513

514+
@VisibleForTesting
515+
static String parseOptimizerVersion(String uri) {
516+
String value = parseUriProperty(uri, OPTIMIZER_VERSION_PROPERTY_NAME);
517+
return value != null ? value : DEFAULT_OPTIMIZER_VERSION;
518+
}
519+
504520
@VisibleForTesting
505521
static String parseUriProperty(String uri, String property) {
506522
Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property));
@@ -633,6 +649,11 @@ String getUserAgent() {
633649
return userAgent;
634650
}
635651

652+
/** The {@link QueryOptions} to use for the connection. */
653+
QueryOptions getQueryOptions() {
654+
return queryOptions;
655+
}
656+
636657
/** Interceptors that should be executed after each statement */
637658
List<StatementExecutionInterceptor> getStatementExecutionInterceptors() {
638659
return statementExecutionInterceptors;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ interface ConnectionStatementExecutor {
6060

6161
StatementResult statementShowReadOnlyStaleness();
6262

63+
StatementResult statementSetOptimizerVersion(String optimizerVersion);
64+
65+
StatementResult statementShowOptimizerVersion();
66+
6367
StatementResult statementBeginTransaction();
6468

6569
StatementResult statementCommit();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.RUN_BATCH;
2424
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT;
2525
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE;
26+
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_OPTIMIZER_VERSION;
2627
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_READONLY;
2728
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_READ_ONLY_STALENESS;
2829
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SET_RETRY_ABORTS_INTERNALLY;
@@ -31,6 +32,7 @@
3132
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT;
3233
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE;
3334
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
35+
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_VERSION;
3436
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READONLY;
3537
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS;
3638
import static com.google.cloud.spanner.jdbc.StatementResult.ClientSideStatementType.SHOW_READ_TIMESTAMP;
@@ -183,6 +185,18 @@ public StatementResult statementShowReadOnlyStaleness() {
183185
SHOW_READ_ONLY_STALENESS);
184186
}
185187

188+
@Override
189+
public StatementResult statementSetOptimizerVersion(String optimizerVersion) {
190+
getConnection().setOptimizerVersion(optimizerVersion);
191+
return noResult(SET_OPTIMIZER_VERSION);
192+
}
193+
194+
@Override
195+
public StatementResult statementShowOptimizerVersion() {
196+
return resultSet(
197+
"OPTIMIZER_VERSION", getConnection().getOptimizerVersion(), SHOW_OPTIMIZER_VERSION);
198+
}
199+
186200
@Override
187201
public StatementResult statementBeginTransaction() {
188202
getConnection().beginTransaction();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
* connection. Default is true. @see {@link
8787
* com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection#setRetryAbortsInternally(boolean)}
8888
* for more information.
89+
* <li>optimizerVersion (string): The query optimizer version to use for the connection. The value must be either a valid version number or <code>LATEST</code>. If no value is specified, the query optimizer version specified in the environment variable <code>SPANNER_OPTIMIZER_VERSION<code> will be used. If no query optimizer version is specified in the connection URL or in the environment variable, the default query optimizer version of Cloud Spanner will be used.
8990
* </ul>
9091
*/
9192
public class JdbcDriver implements Driver {

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import com.google.cloud.spanner.SpannerExceptionFactory;
2121
import com.google.cloud.spanner.Statement;
2222
import com.google.cloud.spanner.jdbc.ClientSideStatementImpl.CompileException;
23+
import com.google.cloud.spanner.jdbc.StatementParser.ParsedStatement;
2324
import com.google.common.annotations.VisibleForTesting;
2425
import com.google.common.base.Preconditions;
2526
import com.google.common.collect.ImmutableSet;
27+
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
2628
import java.util.Collections;
29+
import java.util.Objects;
2730
import java.util.Set;
2831

2932
/**
@@ -66,8 +69,10 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
6669
return new ParsedStatement(StatementType.DDL, statement, sqlWithoutComments);
6770
}
6871

69-
private static ParsedStatement query(Statement statement, String sqlWithoutComments) {
70-
return new ParsedStatement(StatementType.QUERY, statement, sqlWithoutComments);
72+
private static ParsedStatement query(
73+
Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) {
74+
return new ParsedStatement(
75+
StatementType.QUERY, statement, sqlWithoutComments, defaultQueryOptions);
7176
}
7277

7378
private static ParsedStatement update(Statement statement, String sqlWithoutComments) {
@@ -91,14 +96,40 @@ private ParsedStatement(
9196
}
9297

9398
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
99+
this(type, statement, sqlWithoutComments, null);
100+
}
101+
102+
private ParsedStatement(
103+
StatementType type,
104+
Statement statement,
105+
String sqlWithoutComments,
106+
QueryOptions defaultQueryOptions) {
94107
Preconditions.checkNotNull(type);
95108
Preconditions.checkNotNull(statement);
96109
this.type = type;
97110
this.clientSideStatement = null;
98-
this.statement = statement;
111+
this.statement = mergeQueryOptions(statement, defaultQueryOptions);
99112
this.sqlWithoutComments = sqlWithoutComments;
100113
}
101114

115+
@Override
116+
public int hashCode() {
117+
return Objects.hash(
118+
this.type, this.clientSideStatement, this.statement, this.sqlWithoutComments);
119+
}
120+
121+
@Override
122+
public boolean equals(Object other) {
123+
if (!(other instanceof ParsedStatement)) {
124+
return false;
125+
}
126+
ParsedStatement o = (ParsedStatement) other;
127+
return Objects.equals(this.type, o.type)
128+
&& Objects.equals(this.clientSideStatement, o.clientSideStatement)
129+
&& Objects.equals(this.statement, o.statement)
130+
&& Objects.equals(this.sqlWithoutComments, o.sqlWithoutComments);
131+
}
132+
102133
StatementType getType() {
103134
return type;
104135
}
@@ -148,6 +179,26 @@ Statement getStatement() {
148179
return statement;
149180
}
150181

182+
/**
183+
* Merges the {@link QueryOptions} of the {@link Statement} with the current {@link
184+
* QueryOptions} of this connection. The {@link QueryOptions} that are already present on the
185+
* statement take precedence above the connection {@link QueryOptions}.
186+
*/
187+
Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOptions) {
188+
if (defaultQueryOptions == null
189+
|| defaultQueryOptions.equals(QueryOptions.getDefaultInstance())) {
190+
return statement;
191+
}
192+
if (statement.getQueryOptions() == null) {
193+
return statement.toBuilder().withQueryOptions(defaultQueryOptions).build();
194+
}
195+
return statement
196+
.toBuilder()
197+
.withQueryOptions(
198+
defaultQueryOptions.toBuilder().mergeFrom(statement.getQueryOptions()).build())
199+
.build();
200+
}
201+
151202
String getSqlWithoutComments() {
152203
return sqlWithoutComments;
153204
}
@@ -183,12 +234,16 @@ private StatementParser() {
183234
* @return the parsed and categorized statement.
184235
*/
185236
ParsedStatement parse(Statement statement) {
237+
return parse(statement, null);
238+
}
239+
240+
ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
186241
String sql = removeCommentsAndTrim(statement.getSql());
187242
ClientSideStatementImpl client = parseClientSideStatement(sql);
188243
if (client != null) {
189244
return ParsedStatement.clientSideStatement(client, statement, sql);
190245
} else if (isQuery(sql)) {
191-
return ParsedStatement.query(statement, sql);
246+
return ParsedStatement.query(statement, sql, defaultQueryOptions);
192247
} else if (isUpdateStatement(sql)) {
193248
return ParsedStatement.update(statement, sql);
194249
} else if (isDdlStatement(sql)) {

0 commit comments

Comments
 (0)