Skip to content

Commit ee477c2

Browse files
feat: support Directed Read in Connection API (#2855)
* feat: support Directed Read in Connection API Adds support for setting Directed Read options in the Connection API. The value must be a valid JSON representation of a DirectedReadOptions protobuf instance. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: address review comments * fix: verify that DirectedRead is not used for DML --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 5ee5540 commit ee477c2

22 files changed

+2288
-252
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

+13
Original file line numberDiff line numberDiff line change
@@ -540,4 +540,17 @@
540540
<className>com/google/cloud/spanner/Dialect</className>
541541
<method>java.lang.String getDefaultSchema()</method>
542542
</difference>
543+
544+
<!-- Added DirectedReadOptions -->
545+
<difference>
546+
<differenceType>7012</differenceType>
547+
<className>com/google/cloud/spanner/connection/Connection</className>
548+
<method>com.google.spanner.v1.DirectedReadOptions getDirectedRead()</method>
549+
</difference>
550+
<difference>
551+
<differenceType>7012</differenceType>
552+
<className>com/google/cloud/spanner/connection/Connection</className>
553+
<method>void setDirectedRead(com.google.spanner.v1.DirectedReadOptions)</method>
554+
</difference>
555+
543556
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java

+44
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.spanner.ErrorCode;
2020
import com.google.cloud.spanner.Options.RpcPriority;
21+
import com.google.cloud.spanner.SpannerException;
2122
import com.google.cloud.spanner.SpannerExceptionFactory;
2223
import com.google.cloud.spanner.TimestampBound;
2324
import com.google.cloud.spanner.TimestampBound.Mode;
@@ -27,6 +28,7 @@
2728
import com.google.common.base.Preconditions;
2829
import com.google.protobuf.Duration;
2930
import com.google.protobuf.util.Durations;
31+
import com.google.spanner.v1.DirectedReadOptions;
3032
import com.google.spanner.v1.RequestOptions.Priority;
3133
import java.util.EnumSet;
3234
import java.util.HashMap;
@@ -306,6 +308,48 @@ public TimestampBound convert(String value) {
306308
return null;
307309
}
308310
}
311+
/**
312+
* Converter from string to possible values for {@link com.google.spanner.v1.DirectedReadOptions}.
313+
*/
314+
static class DirectedReadOptionsConverter
315+
implements ClientSideStatementValueConverter<DirectedReadOptions> {
316+
private final Pattern allowedValues;
317+
318+
public DirectedReadOptionsConverter(String allowedValues) {
319+
// Remove the single quotes at the beginning and end.
320+
this.allowedValues =
321+
Pattern.compile(
322+
"(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
323+
}
324+
325+
@Override
326+
public Class<DirectedReadOptions> getParameterClass() {
327+
return DirectedReadOptions.class;
328+
}
329+
330+
@Override
331+
public DirectedReadOptions convert(String value) {
332+
Matcher matcher = allowedValues.matcher(value);
333+
if (matcher.find()) {
334+
try {
335+
return DirectedReadOptionsUtil.parse(value);
336+
} catch (SpannerException spannerException) {
337+
throw SpannerExceptionFactory.newSpannerException(
338+
ErrorCode.INVALID_ARGUMENT,
339+
String.format(
340+
"Failed to parse '%s' as a valid value for DIRECTED_READ.\n"
341+
+ "The value should be a JSON string like this: '%s'.\n"
342+
+ "You can generate a valid JSON string from a DirectedReadOptions instance by calling %s.%s",
343+
value,
344+
"{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}",
345+
DirectedReadOptionsUtil.class.getName(),
346+
"toString(DirectedReadOptions directedReadOptions)"),
347+
spannerException);
348+
}
349+
}
350+
return null;
351+
}
352+
}
309353

310354
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
311355
static class AutocommitDmlModeConverter

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java

+17
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.cloud.spanner.Statement;
3939
import com.google.cloud.spanner.TimestampBound;
4040
import com.google.cloud.spanner.connection.StatementResult.ResultType;
41+
import com.google.spanner.v1.DirectedReadOptions;
4142
import com.google.spanner.v1.ExecuteBatchDmlRequest;
4243
import com.google.spanner.v1.ResultSetStats;
4344
import java.util.Iterator;
@@ -489,6 +490,22 @@ default String getStatementTag() {
489490
*/
490491
TimestampBound getReadOnlyStaleness();
491492

493+
/**
494+
* Sets the {@link DirectedReadOptions} to use for both single-use and multi-use read-only
495+
* transactions on this connection.
496+
*/
497+
default void setDirectedRead(DirectedReadOptions directedReadOptions) {
498+
throw new UnsupportedOperationException("Unimplemented");
499+
}
500+
501+
/**
502+
* Returns the {@link DirectedReadOptions} that are used for both single-use and multi-use
503+
* read-only transactions on this connection.
504+
*/
505+
default DirectedReadOptions getDirectedRead() {
506+
throw new UnsupportedOperationException("Unimplemented");
507+
}
508+
492509
/**
493510
* Sets the query optimizer version to use for this connection.
494511
*

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

+41-26
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.google.common.annotations.VisibleForTesting;
5454
import com.google.common.base.Preconditions;
5555
import com.google.common.util.concurrent.MoreExecutors;
56+
import com.google.spanner.v1.DirectedReadOptions;
5657
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
5758
import com.google.spanner.v1.ResultSetStats;
5859
import java.util.ArrayList;
@@ -236,6 +237,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
236237
*/
237238
private int maxPartitionedParallelism;
238239

240+
private DirectedReadOptions directedReadOptions = null;
239241
private QueryOptions queryOptions = QueryOptions.getDefaultInstance();
240242
private RpcPriority rpcPriority = null;
241243
private SavepointSupport savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK;
@@ -510,6 +512,21 @@ public TimestampBound getReadOnlyStaleness() {
510512
return this.readOnlyStaleness;
511513
}
512514

515+
@Override
516+
public void setDirectedRead(DirectedReadOptions directedReadOptions) {
517+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
518+
ConnectionPreconditions.checkState(
519+
!isTransactionStarted(),
520+
"Cannot set directed read options when a transaction has been started");
521+
this.directedReadOptions = directedReadOptions;
522+
}
523+
524+
@Override
525+
public DirectedReadOptions getDirectedRead() {
526+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
527+
return this.directedReadOptions;
528+
}
529+
513530
@Override
514531
public void setOptimizerVersion(String optimizerVersion) {
515532
Preconditions.checkNotNull(optimizerVersion);
@@ -1131,7 +1148,8 @@ public ResultSet partitionQuery(
11311148
CallType.SYNC,
11321149
parsedStatement,
11331150
getEffectivePartitionOptions(partitionOptions),
1134-
mergeDataBoost(mergeQueryRequestOptions(mergeQueryStatementTag(options)))));
1151+
mergeDataBoost(
1152+
mergeQueryRequestOptions(parsedStatement, mergeQueryStatementTag(options)))));
11351153
}
11361154

11371155
private PartitionOptions getEffectivePartitionOptions(
@@ -1427,41 +1445,38 @@ private List<ParsedStatement> parseUpdateStatements(Iterable<Statement> updates)
14271445

14281446
private QueryOption[] mergeDataBoost(QueryOption... options) {
14291447
if (this.dataBoostEnabled) {
1430-
1431-
// Shortcut for the most common scenario.
1432-
if (options == null || options.length == 0) {
1433-
options = new QueryOption[] {Options.dataBoostEnabled(true)};
1434-
} else {
1435-
options = Arrays.copyOf(options, options.length + 1);
1436-
options[options.length - 1] = Options.dataBoostEnabled(true);
1437-
}
1448+
options = appendQueryOption(options, Options.dataBoostEnabled(true));
14381449
}
14391450
return options;
14401451
}
14411452

14421453
private QueryOption[] mergeQueryStatementTag(QueryOption... options) {
14431454
if (this.statementTag != null) {
1444-
// Shortcut for the most common scenario.
1445-
if (options == null || options.length == 0) {
1446-
options = new QueryOption[] {Options.tag(statementTag)};
1447-
} else {
1448-
options = Arrays.copyOf(options, options.length + 1);
1449-
options[options.length - 1] = Options.tag(statementTag);
1450-
}
1455+
options = appendQueryOption(options, Options.tag(statementTag));
14511456
this.statementTag = null;
14521457
}
14531458
return options;
14541459
}
14551460

1456-
private QueryOption[] mergeQueryRequestOptions(QueryOption... options) {
1461+
private QueryOption[] mergeQueryRequestOptions(
1462+
ParsedStatement parsedStatement, QueryOption... options) {
14571463
if (this.rpcPriority != null) {
1458-
// Shortcut for the most common scenario.
1459-
if (options == null || options.length == 0) {
1460-
options = new QueryOption[] {Options.priority(this.rpcPriority)};
1461-
} else {
1462-
options = Arrays.copyOf(options, options.length + 1);
1463-
options[options.length - 1] = Options.priority(this.rpcPriority);
1464-
}
1464+
options = appendQueryOption(options, Options.priority(this.rpcPriority));
1465+
}
1466+
if (this.directedReadOptions != null
1467+
&& currentUnitOfWork != null
1468+
&& currentUnitOfWork.supportsDirectedReads(parsedStatement)) {
1469+
options = appendQueryOption(options, Options.directedRead(this.directedReadOptions));
1470+
}
1471+
return options;
1472+
}
1473+
1474+
private QueryOption[] appendQueryOption(QueryOption[] options, QueryOption append) {
1475+
if (options == null || options.length == 0) {
1476+
options = new QueryOption[] {append};
1477+
} else {
1478+
options = Arrays.copyOf(options, options.length + 1);
1479+
options[options.length - 1] = append;
14651480
}
14661481
return options;
14671482
}
@@ -1516,7 +1531,7 @@ private ResultSet internalExecuteQuery(
15161531
callType,
15171532
statement,
15181533
analyzeMode,
1519-
mergeQueryRequestOptions(mergeQueryStatementTag(options))));
1534+
mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))));
15201535
}
15211536

15221537
private AsyncResultSet internalExecuteQueryAsync(
@@ -1538,7 +1553,7 @@ private AsyncResultSet internalExecuteQueryAsync(
15381553
callType,
15391554
statement,
15401555
analyzeMode,
1541-
mergeQueryRequestOptions(mergeQueryStatementTag(options))),
1556+
mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))),
15421557
spanner.getAsyncExecutorProvider(),
15431558
options);
15441559
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.cloud.spanner.TimestampBound;
2121
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
2222
import com.google.protobuf.Duration;
23+
import com.google.spanner.v1.DirectedReadOptions;
2324
import com.google.spanner.v1.RequestOptions.Priority;
2425

2526
/**
@@ -65,6 +66,10 @@ interface ConnectionStatementExecutor {
6566

6667
StatementResult statementShowReadOnlyStaleness();
6768

69+
StatementResult statementSetDirectedRead(DirectedReadOptions directedReadOptions);
70+
71+
StatementResult statementShowDirectedRead();
72+
6873
StatementResult statementSetOptimizerVersion(String optimizerVersion);
6974

7075
StatementResult statementShowOptimizerVersion();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DATA_BOOST_ENABLED;
2929
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DEFAULT_TRANSACTION_ISOLATION;
3030
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
31+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DIRECTED_READ;
3132
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_MAX_PARTITIONED_PARALLELISM;
3233
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_MAX_PARTITIONS;
3334
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_STATISTICS_PACKAGE;
@@ -49,6 +50,7 @@
4950
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
5051
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DATA_BOOST_ENABLED;
5152
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
53+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DIRECTED_READ;
5254
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_MAX_PARTITIONED_PARALLELISM;
5355
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_MAX_PARTITIONS;
5456
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_STATISTICS_PACKAGE;
@@ -91,6 +93,7 @@
9193
import com.google.common.base.Preconditions;
9294
import com.google.common.collect.ImmutableMap;
9395
import com.google.protobuf.Duration;
96+
import com.google.spanner.v1.DirectedReadOptions;
9497
import com.google.spanner.v1.PlanNode;
9598
import com.google.spanner.v1.QueryPlan;
9699
import com.google.spanner.v1.RequestOptions;
@@ -283,6 +286,21 @@ public StatementResult statementShowReadOnlyStaleness() {
283286
SHOW_READ_ONLY_STALENESS);
284287
}
285288

289+
@Override
290+
public StatementResult statementSetDirectedRead(DirectedReadOptions directedReadOptions) {
291+
getConnection().setDirectedRead(directedReadOptions);
292+
return noResult(SET_DIRECTED_READ);
293+
}
294+
295+
@Override
296+
public StatementResult statementShowDirectedRead() {
297+
DirectedReadOptions directedReadOptions = getConnection().getDirectedRead();
298+
return resultSet(
299+
String.format("%sDIRECTED_READ", getNamespace(connection.getDialect())),
300+
DirectedReadOptionsUtil.toString(directedReadOptions),
301+
SHOW_DIRECTED_READ);
302+
}
303+
286304
@Override
287305
public StatementResult statementSetOptimizerVersion(String optimizerVersion) {
288306
getConnection().setOptimizerVersion(optimizerVersion);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.connection;
18+
19+
import com.google.cloud.spanner.SpannerExceptionFactory;
20+
import com.google.common.base.Strings;
21+
import com.google.protobuf.InvalidProtocolBufferException;
22+
import com.google.protobuf.util.JsonFormat;
23+
import com.google.spanner.v1.DirectedReadOptions;
24+
25+
public class DirectedReadOptionsUtil {
26+
27+
/**
28+
* Generates a valid JSON string for the given {@link DirectedReadOptions} that can be used with
29+
* the JDBC driver.
30+
*/
31+
public static String toString(DirectedReadOptions directedReadOptions) {
32+
if (directedReadOptions == null
33+
|| DirectedReadOptions.getDefaultInstance().equals(directedReadOptions)) {
34+
return "";
35+
}
36+
try {
37+
return JsonFormat.printer().omittingInsignificantWhitespace().print(directedReadOptions);
38+
} catch (InvalidProtocolBufferException invalidProtocolBufferException) {
39+
throw SpannerExceptionFactory.asSpannerException(invalidProtocolBufferException);
40+
}
41+
}
42+
43+
static DirectedReadOptions parse(String json) {
44+
if (Strings.isNullOrEmpty(json)) {
45+
return DirectedReadOptions.getDefaultInstance();
46+
}
47+
DirectedReadOptions.Builder builder = DirectedReadOptions.newBuilder();
48+
try {
49+
JsonFormat.parser().merge(json, builder);
50+
return builder.build();
51+
} catch (InvalidProtocolBufferException invalidProtocolBufferException) {
52+
throw SpannerExceptionFactory.asSpannerException(invalidProtocolBufferException);
53+
}
54+
}
55+
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java

+5
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ public boolean isReadOnly() {
107107
return true;
108108
}
109109

110+
@Override
111+
public boolean supportsDirectedReads(ParsedStatement ignore) {
112+
return true;
113+
}
114+
110115
@Override
111116
void checkAborted() {
112117
// No-op for read-only transactions as they cannot abort.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java

+5
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ public boolean isReadOnly() {
188188
return readOnly;
189189
}
190190

191+
@Override
192+
public boolean supportsDirectedReads(ParsedStatement parsedStatement) {
193+
return parsedStatement.isQuery();
194+
}
195+
191196
private void checkAndMarkUsed() {
192197
Preconditions.checkState(!used, "This single-use transaction has already been used");
193198
used = true;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ enum ClientSideStatementType {
6363
SHOW_COMMIT_RESPONSE,
6464
SHOW_READ_ONLY_STALENESS,
6565
SET_READ_ONLY_STALENESS,
66+
SHOW_DIRECTED_READ,
67+
SET_DIRECTED_READ,
6668
SHOW_OPTIMIZER_VERSION,
6769
SET_OPTIMIZER_VERSION,
6870
SHOW_OPTIMIZER_STATISTICS_PACKAGE,

0 commit comments

Comments
 (0)