diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index 56e946eb3d..76d2aa7c79 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -540,4 +540,17 @@
com/google/cloud/spanner/Dialect
java.lang.String getDefaultSchema()
+
+
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ com.google.spanner.v1.DirectedReadOptions getDirectedRead()
+
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ void setDirectedRead(com.google.spanner.v1.DirectedReadOptions)
+
+
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java
index a477a664ec..6be277777a 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java
@@ -18,6 +18,7 @@
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Options.RpcPriority;
+import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TimestampBound.Mode;
@@ -27,6 +28,7 @@
import com.google.common.base.Preconditions;
import com.google.protobuf.Duration;
import com.google.protobuf.util.Durations;
+import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.RequestOptions.Priority;
import java.util.EnumSet;
import java.util.HashMap;
@@ -306,6 +308,48 @@ public TimestampBound convert(String value) {
return null;
}
}
+ /**
+ * Converter from string to possible values for {@link com.google.spanner.v1.DirectedReadOptions}.
+ */
+ static class DirectedReadOptionsConverter
+ implements ClientSideStatementValueConverter {
+ private final Pattern allowedValues;
+
+ public DirectedReadOptionsConverter(String allowedValues) {
+ // Remove the single quotes at the beginning and end.
+ this.allowedValues =
+ Pattern.compile(
+ "(?is)\\A" + allowedValues.substring(1, allowedValues.length() - 1) + "\\z");
+ }
+
+ @Override
+ public Class getParameterClass() {
+ return DirectedReadOptions.class;
+ }
+
+ @Override
+ public DirectedReadOptions convert(String value) {
+ Matcher matcher = allowedValues.matcher(value);
+ if (matcher.find()) {
+ try {
+ return DirectedReadOptionsUtil.parse(value);
+ } catch (SpannerException spannerException) {
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT,
+ String.format(
+ "Failed to parse '%s' as a valid value for DIRECTED_READ.\n"
+ + "The value should be a JSON string like this: '%s'.\n"
+ + "You can generate a valid JSON string from a DirectedReadOptions instance by calling %s.%s",
+ value,
+ "{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}",
+ DirectedReadOptionsUtil.class.getName(),
+ "toString(DirectedReadOptions directedReadOptions)"),
+ spannerException);
+ }
+ }
+ return null;
+ }
+ }
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
static class AutocommitDmlModeConverter
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
index e58610b7d0..ffcef91d9c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
@@ -38,6 +38,7 @@
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.connection.StatementResult.ResultType;
+import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ResultSetStats;
import java.util.Iterator;
@@ -489,6 +490,22 @@ default String getStatementTag() {
*/
TimestampBound getReadOnlyStaleness();
+ /**
+ * Sets the {@link DirectedReadOptions} to use for both single-use and multi-use read-only
+ * transactions on this connection.
+ */
+ default void setDirectedRead(DirectedReadOptions directedReadOptions) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
+ /**
+ * Returns the {@link DirectedReadOptions} that are used for both single-use and multi-use
+ * read-only transactions on this connection.
+ */
+ default DirectedReadOptions getDirectedRead() {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
/**
* Sets the query optimizer version to use for this connection.
*
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
index 3e524e4fe9..e9aed66b6b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
@@ -53,6 +53,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ResultSetStats;
import java.util.ArrayList;
@@ -236,6 +237,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
*/
private int maxPartitionedParallelism;
+ private DirectedReadOptions directedReadOptions = null;
private QueryOptions queryOptions = QueryOptions.getDefaultInstance();
private RpcPriority rpcPriority = null;
private SavepointSupport savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK;
@@ -510,6 +512,21 @@ public TimestampBound getReadOnlyStaleness() {
return this.readOnlyStaleness;
}
+ @Override
+ public void setDirectedRead(DirectedReadOptions directedReadOptions) {
+ ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
+ ConnectionPreconditions.checkState(
+ !isTransactionStarted(),
+ "Cannot set directed read options when a transaction has been started");
+ this.directedReadOptions = directedReadOptions;
+ }
+
+ @Override
+ public DirectedReadOptions getDirectedRead() {
+ ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
+ return this.directedReadOptions;
+ }
+
@Override
public void setOptimizerVersion(String optimizerVersion) {
Preconditions.checkNotNull(optimizerVersion);
@@ -1131,7 +1148,8 @@ public ResultSet partitionQuery(
CallType.SYNC,
parsedStatement,
getEffectivePartitionOptions(partitionOptions),
- mergeDataBoost(mergeQueryRequestOptions(mergeQueryStatementTag(options)))));
+ mergeDataBoost(
+ mergeQueryRequestOptions(parsedStatement, mergeQueryStatementTag(options)))));
}
private PartitionOptions getEffectivePartitionOptions(
@@ -1427,41 +1445,38 @@ private List parseUpdateStatements(Iterable updates)
private QueryOption[] mergeDataBoost(QueryOption... options) {
if (this.dataBoostEnabled) {
-
- // Shortcut for the most common scenario.
- if (options == null || options.length == 0) {
- options = new QueryOption[] {Options.dataBoostEnabled(true)};
- } else {
- options = Arrays.copyOf(options, options.length + 1);
- options[options.length - 1] = Options.dataBoostEnabled(true);
- }
+ options = appendQueryOption(options, Options.dataBoostEnabled(true));
}
return options;
}
private QueryOption[] mergeQueryStatementTag(QueryOption... options) {
if (this.statementTag != null) {
- // Shortcut for the most common scenario.
- if (options == null || options.length == 0) {
- options = new QueryOption[] {Options.tag(statementTag)};
- } else {
- options = Arrays.copyOf(options, options.length + 1);
- options[options.length - 1] = Options.tag(statementTag);
- }
+ options = appendQueryOption(options, Options.tag(statementTag));
this.statementTag = null;
}
return options;
}
- private QueryOption[] mergeQueryRequestOptions(QueryOption... options) {
+ private QueryOption[] mergeQueryRequestOptions(
+ ParsedStatement parsedStatement, QueryOption... options) {
if (this.rpcPriority != null) {
- // Shortcut for the most common scenario.
- if (options == null || options.length == 0) {
- options = new QueryOption[] {Options.priority(this.rpcPriority)};
- } else {
- options = Arrays.copyOf(options, options.length + 1);
- options[options.length - 1] = Options.priority(this.rpcPriority);
- }
+ options = appendQueryOption(options, Options.priority(this.rpcPriority));
+ }
+ if (this.directedReadOptions != null
+ && currentUnitOfWork != null
+ && currentUnitOfWork.supportsDirectedReads(parsedStatement)) {
+ options = appendQueryOption(options, Options.directedRead(this.directedReadOptions));
+ }
+ return options;
+ }
+
+ private QueryOption[] appendQueryOption(QueryOption[] options, QueryOption append) {
+ if (options == null || options.length == 0) {
+ options = new QueryOption[] {append};
+ } else {
+ options = Arrays.copyOf(options, options.length + 1);
+ options[options.length - 1] = append;
}
return options;
}
@@ -1516,7 +1531,7 @@ private ResultSet internalExecuteQuery(
callType,
statement,
analyzeMode,
- mergeQueryRequestOptions(mergeQueryStatementTag(options))));
+ mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))));
}
private AsyncResultSet internalExecuteQueryAsync(
@@ -1538,7 +1553,7 @@ private AsyncResultSet internalExecuteQueryAsync(
callType,
statement,
analyzeMode,
- mergeQueryRequestOptions(mergeQueryStatementTag(options))),
+ mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))),
spanner.getAsyncExecutorProvider(),
options);
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java
index eb31779097..3fbc3e7a8d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java
@@ -20,6 +20,7 @@
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.protobuf.Duration;
+import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.RequestOptions.Priority;
/**
@@ -65,6 +66,10 @@ interface ConnectionStatementExecutor {
StatementResult statementShowReadOnlyStaleness();
+ StatementResult statementSetDirectedRead(DirectedReadOptions directedReadOptions);
+
+ StatementResult statementShowDirectedRead();
+
StatementResult statementSetOptimizerVersion(String optimizerVersion);
StatementResult statementShowOptimizerVersion();
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java
index a3b4f92f14..0e8bdada22 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java
@@ -28,6 +28,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DATA_BOOST_ENABLED;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DEFAULT_TRANSACTION_ISOLATION;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
+import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DIRECTED_READ;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_MAX_PARTITIONED_PARALLELISM;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_MAX_PARTITIONS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_STATISTICS_PACKAGE;
@@ -49,6 +50,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DATA_BOOST_ENABLED;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
+import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DIRECTED_READ;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_MAX_PARTITIONED_PARALLELISM;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_MAX_PARTITIONS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_STATISTICS_PACKAGE;
@@ -91,6 +93,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.Duration;
+import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.PlanNode;
import com.google.spanner.v1.QueryPlan;
import com.google.spanner.v1.RequestOptions;
@@ -283,6 +286,21 @@ public StatementResult statementShowReadOnlyStaleness() {
SHOW_READ_ONLY_STALENESS);
}
+ @Override
+ public StatementResult statementSetDirectedRead(DirectedReadOptions directedReadOptions) {
+ getConnection().setDirectedRead(directedReadOptions);
+ return noResult(SET_DIRECTED_READ);
+ }
+
+ @Override
+ public StatementResult statementShowDirectedRead() {
+ DirectedReadOptions directedReadOptions = getConnection().getDirectedRead();
+ return resultSet(
+ String.format("%sDIRECTED_READ", getNamespace(connection.getDialect())),
+ DirectedReadOptionsUtil.toString(directedReadOptions),
+ SHOW_DIRECTED_READ);
+ }
+
@Override
public StatementResult statementSetOptimizerVersion(String optimizerVersion) {
getConnection().setOptimizerVersion(optimizerVersion);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java
new file mode 100644
index 0000000000..8b1f8a9019
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtil.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.connection;
+
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.common.base.Strings;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import com.google.spanner.v1.DirectedReadOptions;
+
+public class DirectedReadOptionsUtil {
+
+ /**
+ * Generates a valid JSON string for the given {@link DirectedReadOptions} that can be used with
+ * the JDBC driver.
+ */
+ public static String toString(DirectedReadOptions directedReadOptions) {
+ if (directedReadOptions == null
+ || DirectedReadOptions.getDefaultInstance().equals(directedReadOptions)) {
+ return "";
+ }
+ try {
+ return JsonFormat.printer().omittingInsignificantWhitespace().print(directedReadOptions);
+ } catch (InvalidProtocolBufferException invalidProtocolBufferException) {
+ throw SpannerExceptionFactory.asSpannerException(invalidProtocolBufferException);
+ }
+ }
+
+ static DirectedReadOptions parse(String json) {
+ if (Strings.isNullOrEmpty(json)) {
+ return DirectedReadOptions.getDefaultInstance();
+ }
+ DirectedReadOptions.Builder builder = DirectedReadOptions.newBuilder();
+ try {
+ JsonFormat.parser().merge(json, builder);
+ return builder.build();
+ } catch (InvalidProtocolBufferException invalidProtocolBufferException) {
+ throw SpannerExceptionFactory.asSpannerException(invalidProtocolBufferException);
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
index 5a005ff805..63e5221362 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
@@ -107,6 +107,11 @@ public boolean isReadOnly() {
return true;
}
+ @Override
+ public boolean supportsDirectedReads(ParsedStatement ignore) {
+ return true;
+ }
+
@Override
void checkAborted() {
// No-op for read-only transactions as they cannot abort.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
index 52486ed43e..56ddc16322 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
@@ -188,6 +188,11 @@ public boolean isReadOnly() {
return readOnly;
}
+ @Override
+ public boolean supportsDirectedReads(ParsedStatement parsedStatement) {
+ return parsedStatement.isQuery();
+ }
+
private void checkAndMarkUsed() {
Preconditions.checkState(!used, "This single-use transaction has already been used");
used = true;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java
index 1452d906a9..d180423266 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java
@@ -63,6 +63,8 @@ enum ClientSideStatementType {
SHOW_COMMIT_RESPONSE,
SHOW_READ_ONLY_STALENESS,
SET_READ_ONLY_STALENESS,
+ SHOW_DIRECTED_READ,
+ SET_DIRECTED_READ,
SHOW_OPTIMIZER_VERSION,
SET_OPTIMIZER_VERSION,
SHOW_OPTIMIZER_STATISTICS_PACKAGE,
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java
index ea315e94c0..1c8bc6a29c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/UnitOfWork.java
@@ -128,6 +128,14 @@ public boolean isActive() {
/** @return true
if this unit of work is read-only. */
boolean isReadOnly();
+ /**
+ * @return true
if this unit of work supports {@link
+ * com.google.spanner.v1.DirectedReadOptions}
+ */
+ default boolean supportsDirectedReads(ParsedStatement parsedStatement) {
+ return false;
+ }
+
/**
* Executes a query with the given options. If {@link AnalyzeMode} is set to {@link
* AnalyzeMode#PLAN} or {@link AnalyzeMode#PROFILE}, the returned {@link ResultSet} will include
diff --git a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json
index 576991b8e0..20d7522961 100644
--- a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json
+++ b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/ClientSideStatements.json
@@ -76,6 +76,15 @@
"method": "statementShowReadOnlyStaleness",
"exampleStatements": ["show variable read_only_staleness"]
},
+ {
+ "name": "SHOW VARIABLE DIRECTED_READ",
+ "executorName": "ClientSideStatementNoParamExecutor",
+ "resultType": "RESULT_SET",
+ "statementType": "SHOW_DIRECTED_READ",
+ "regex": "(?is)\\A\\s*show\\s+variable\\s+directed_read\\s*\\z",
+ "method": "statementShowDirectedRead",
+ "exampleStatements": ["show variable directed_read"]
+ },
{
"name": "SHOW VARIABLE OPTIMIZER_VERSION",
"executorName": "ClientSideStatementNoParamExecutor",
@@ -373,6 +382,21 @@
"converterName": "ClientSideStatementValueConverters$ReadOnlyStalenessConverter"
}
},
+ {
+ "name": "SET DIRECTED_READ = ''|''",
+ "executorName": "ClientSideStatementSetExecutor",
+ "resultType": "NO_RESULT",
+ "statementType": "SET_DIRECTED_READ",
+ "regex": "(?is)\\A\\s*set\\s+directed_read\\s*(?:=)\\s*(.*)\\z",
+ "method": "statementSetDirectedRead",
+ "exampleStatements": ["set directed_read='{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}'", "set directed_read=''"],
+ "setStatement": {
+ "propertyName": "DIRECTED_READ",
+ "separator": "=",
+ "allowedValues": "'((\\S+)|())'",
+ "converterName": "ClientSideStatementValueConverters$DirectedReadOptionsConverter"
+ }
+ },
{
"name": "SET OPTIMIZER_VERSION = ''|'LATEST'|''",
"executorName": "ClientSideStatementSetExecutor",
diff --git a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json
index cb601d7277..bd04b725c2 100644
--- a/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json
+++ b/google-cloud-spanner/src/main/resources/com/google/cloud/spanner/connection/PG_ClientSideStatements.json
@@ -76,6 +76,15 @@
"method": "statementShowReadOnlyStaleness",
"exampleStatements": ["show spanner.read_only_staleness","show variable spanner.read_only_staleness"]
},
+ {
+ "name": "SHOW [VARIABLE] SPANNER.DIRECTED_READ",
+ "executorName": "ClientSideStatementNoParamExecutor",
+ "resultType": "RESULT_SET",
+ "statementType": "SHOW_DIRECTED_READ",
+ "regex": "(?is)\\A\\s*show\\s+(?:variable\\s+)?spanner\\.directed_read\\s*\\z",
+ "method": "statementShowDirectedRead",
+ "exampleStatements": ["show spanner.directed_read", "show variable spanner.directed_read"]
+ },
{
"name": "SHOW [VARIABLE] SPANNER.OPTIMIZER_VERSION",
"executorName": "ClientSideStatementNoParamExecutor",
@@ -525,6 +534,21 @@
"converterName": "ClientSideStatementValueConverters$ReadOnlyStalenessConverter"
}
},
+ {
+ "name": "SET SPANNER.DIRECTED_READ =|TO ''|''",
+ "executorName": "ClientSideStatementSetExecutor",
+ "resultType": "NO_RESULT",
+ "statementType": "SET_DIRECTED_READ",
+ "regex": "(?is)\\A\\s*set\\s+spanner\\.directed_read(?:\\s*=\\s*|\\s+to\\s+)(.*)\\z",
+ "method": "statementSetDirectedRead",
+ "exampleStatements": ["set spanner.directed_read='{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}'", "set spanner.directed_read=''"],
+ "setStatement": {
+ "propertyName": "SPANNER.DIRECTED_READ",
+ "separator": "(?:=|\\s+TO\\s+)",
+ "allowedValues": "'((\\S+)|())'",
+ "converterName": "ClientSideStatementValueConverters$DirectedReadOptionsConverter"
+ }
+ },
{
"name": "SET SPANNER.OPTIMIZER_VERSION =|TO ''|'LATEST'|''",
"executorName": "ClientSideStatementSetExecutor",
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
index 2c59439ce7..453535ee09 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
@@ -210,7 +210,7 @@ static boolean isRetryableResourceExhaustedException(SpannerException exception)
}
private void cleanUpOldDatabases(InstanceId instanceId) {
- long OLD_DB_THRESHOLD_SECS = TimeUnit.SECONDS.convert(24L, TimeUnit.HOURS);
+ long OLD_DB_THRESHOLD_SECS = TimeUnit.SECONDS.convert(6L, TimeUnit.HOURS);
Timestamp currentTimestamp = Timestamp.now();
int numDropped = 0;
Page page = databaseAdminClient.listDatabases(instanceId.getInstance());
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ClientSideStatementsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ClientSideStatementsTest.java
index c0b1e3a58d..51503fd456 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ClientSideStatementsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ClientSideStatementsTest.java
@@ -167,8 +167,10 @@ private static void generateTestStatements(
AbstractStatementParser parser, ClientSideStatementImpl statement) {
for (String sql : statement.getExampleStatements()) {
log(statement.getExamplePrerequisiteStatements(), sql);
- if (statement.getStatementType() != ClientSideStatementType.RUN_PARTITION) {
+ if (statement.getStatementType() != ClientSideStatementType.RUN_PARTITION
+ && statement.getStatementType() != ClientSideStatementType.SET_DIRECTED_READ) {
// Partition ids are case-sensitive.
+ // DirectedReadOptions are case-sensitive.
log(statement.getExamplePrerequisiteStatements(), upper(sql));
log(statement.getExamplePrerequisiteStatements(), lower(sql));
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtilTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtilTest.java
new file mode 100644
index 0000000000..0669cb5aad
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadOptionsUtilTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.connection;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.google.spanner.v1.DirectedReadOptions;
+import com.google.spanner.v1.DirectedReadOptions.ExcludeReplicas;
+import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas;
+import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection;
+import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection.Type;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests converting {@link DirectedReadOptions} to/from JSON. The test cases here are not very
+ * extensive, as it a very simple wrapper around the {@link com.google.protobuf.util.JsonFormat}
+ * class, which has its own test suite. The tests in this class only serve as a simple verification
+ * that the formatter works as expected.
+ */
+@RunWith(JUnit4.class)
+public class DirectedReadOptionsUtilTest {
+
+ @Test
+ public void testToString() {
+ assertRoundTrip("", DirectedReadOptions.newBuilder().build());
+ assertRoundTrip(
+ "{\"includeReplicas\":{}}",
+ DirectedReadOptions.newBuilder()
+ .setIncludeReplicas(IncludeReplicas.newBuilder().build())
+ .build());
+ assertRoundTrip(
+ "{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}",
+ DirectedReadOptions.newBuilder()
+ .setIncludeReplicas(
+ IncludeReplicas.newBuilder()
+ .addReplicaSelections(
+ ReplicaSelection.newBuilder()
+ .setType(Type.READ_ONLY)
+ .setLocation("eu-west1")
+ .build())
+ .build())
+ .build());
+ assertRoundTrip(
+ "{\"excludeReplicas\":{\"replicaSelections\":[{\"location\":\"eu-west1\",\"type\":\"READ_ONLY\"}]}}",
+ DirectedReadOptions.newBuilder()
+ .setExcludeReplicas(
+ ExcludeReplicas.newBuilder()
+ .addReplicaSelections(
+ ReplicaSelection.newBuilder()
+ .setType(Type.READ_ONLY)
+ .setLocation("eu-west1")
+ .build())
+ .build())
+ .build());
+ }
+
+ private void assertRoundTrip(String json, DirectedReadOptions options) {
+ assertEquals(json, DirectedReadOptionsUtil.toString(options));
+ assertEquals(options, DirectedReadOptionsUtil.parse(json));
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadTest.java
new file mode 100644
index 0000000000..099c5d1047
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectedReadTest.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.connection;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.MockSpannerServiceImpl;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Value;
+import com.google.spanner.v1.*;
+import com.google.spanner.v1.DirectedReadOptions.ExcludeReplicas;
+import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas;
+import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection;
+import com.google.spanner.v1.StructType.Field;
+import java.util.Collection;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DirectedReadTest extends AbstractMockServerTest {
+ private static final Statement READ_STATEMENT = Statement.of("SELECT 1 AS C");
+
+ private static final Statement GOOGLESQL_DML_STATEMENT =
+ Statement.of("INSERT INTO T (id) VALUES (1) THEN RETURN ID");
+ private static final Statement POSTGRESQL_DML_STATEMENT =
+ Statement.of("INSERT INTO T (id) VALUES (1) RETURNING ID");
+
+ @Parameters(name = "dialect = {0}")
+ public static Collection