diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index 12657dd15b1..10017fc8284 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -1033,4 +1033,14 @@
com/google/cloud/spanner/SpannerOptions$SpannerEnvironment
boolean isEnableDirectAccess()
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ void setReadLockMode(com.google.spanner.v1.TransactionOptions$ReadWrite$ReadLockMode)
+
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ com.google.spanner.v1.TransactionOptions$ReadWrite$ReadLockMode getReadLockMode()
+
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 7418c88f68d..57916aae580 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
@@ -34,6 +34,7 @@
import com.google.common.base.Strings;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.TransactionOptions;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
@@ -425,6 +426,36 @@ public TransactionOptions.IsolationLevel convert(String value) {
}
}
+ /**
+ * Converter for converting strings to {@link
+ * com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode} values.
+ */
+ static class ReadLockModeConverter implements ClientSideStatementValueConverter {
+ static final ReadLockModeConverter INSTANCE = new ReadLockModeConverter();
+
+ private final CaseInsensitiveEnumMap values =
+ new CaseInsensitiveEnumMap<>(ReadLockMode.class);
+
+ ReadLockModeConverter() {}
+
+ /** Constructor needed for reflection. */
+ public ReadLockModeConverter(String allowedValues) {}
+
+ @Override
+ public Class getParameterClass() {
+ return ReadLockMode.class;
+ }
+
+ @Override
+ public ReadLockMode convert(String value) {
+ if (value != null && value.equalsIgnoreCase("unspecified")) {
+ // Allow 'unspecified' to be used in addition to 'read_lock_mode_unspecified'.
+ value = ReadLockMode.READ_LOCK_MODE_UNSPECIFIED.name();
+ }
+ return values.get(value);
+ }
+ }
+
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
static class AutocommitDmlModeConverter
implements ClientSideStatementValueConverter {
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 42720e00bb3..31c9834bd8f 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
@@ -43,6 +43,7 @@
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ResultSetStats;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.time.Duration;
import java.util.Iterator;
import java.util.Set;
@@ -232,6 +233,12 @@ public interface Connection extends AutoCloseable {
/** Returns the default isolation level for read/write transactions for this connection. */
IsolationLevel getDefaultIsolationLevel();
+ /** Sets the read lock mode for read/write transactions for this connection. */
+ void setReadLockMode(ReadLockMode readLockMode);
+
+ /** Returns the read lock mode for read/write transactions for this connection. */
+ ReadLockMode getReadLockMode();
+
/**
* Sets the duration the connection should wait before automatically aborting the execution of a
* statement. The default is no timeout. Statement timeouts are applied all types of statements,
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 d610719caba..8ea1054173a 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
@@ -38,6 +38,7 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_STATISTICS_PACKAGE;
import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_VERSION;
import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY;
+import static com.google.cloud.spanner.connection.ConnectionProperties.READ_LOCK_MODE;
import static com.google.cloud.spanner.connection.ConnectionProperties.READ_ONLY_STALENESS;
import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY;
import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS;
@@ -92,6 +93,7 @@
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ResultSetStats;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
@@ -486,6 +488,7 @@ private void reset(Context context, boolean inTransaction) {
this.connectionState.resetValue(AUTOCOMMIT, context, inTransaction);
this.connectionState.resetValue(READONLY, context, inTransaction);
this.connectionState.resetValue(DEFAULT_ISOLATION_LEVEL, context, inTransaction);
+ this.connectionState.resetValue(READ_LOCK_MODE, context, inTransaction);
this.connectionState.resetValue(READ_ONLY_STALENESS, context, inTransaction);
this.connectionState.resetValue(OPTIMIZER_VERSION, context, inTransaction);
this.connectionState.resetValue(OPTIMIZER_STATISTICS_PACKAGE, context, inTransaction);
@@ -668,6 +671,18 @@ private void clearLastTransactionAndSetDefaultTransactionOptions(IsolationLevel
this.currentUnitOfWork = null;
}
+ @Override
+ public void setReadLockMode(ReadLockMode readLockMode) {
+ ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
+ setConnectionPropertyValue(READ_LOCK_MODE, readLockMode);
+ }
+
+ @Override
+ public ReadLockMode getReadLockMode() {
+ ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
+ return getConnectionPropertyValue(READ_LOCK_MODE);
+ }
+
@Override
public void setAutocommitDmlMode(AutocommitDmlMode mode) {
Preconditions.checkNotNull(mode);
@@ -2255,6 +2270,7 @@ UnitOfWork createNewUnitOfWork(
.setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator())
.setDatabaseClient(dbClient)
.setIsolationLevel(transactionIsolationLevel)
+ .setReadLockMode(getConnectionPropertyValue(READ_LOCK_MODE))
.setDelayTransactionStartUntilFirstWrite(
getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE))
.setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE))
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
index ac53b800bb5..febccc3a15c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
@@ -115,6 +115,7 @@
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.IsolationLevelConverter;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.LongConverter;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter;
+import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadLockModeConverter;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadOnlyStalenessConverter;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.RpcPriorityConverter;
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.SavepointSupportConverter;
@@ -125,8 +126,10 @@
import com.google.common.collect.ImmutableMap;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.time.Duration;
import java.util.Arrays;
+import java.util.stream.Collectors;
/** Utility class that defines all known connection properties. */
public class ConnectionProperties {
@@ -451,6 +454,38 @@ public class ConnectionProperties {
},
IsolationLevelConverter.INSTANCE,
Context.USER);
+ static final ConnectionProperty READ_LOCK_MODE =
+ create(
+ "read_lock_mode",
+ "This option controls the locking behavior for read operations and queries within a"
+ + " read/write transaction. It works in conjunction with the transaction's isolation"
+ + " level.\n\n"
+ + "PESSIMISTIC: Read locks are acquired immediately on read. This mode only applies"
+ + " to SERIALIZABLE isolation. This mode prevents concurrent modifications by locking"
+ + " data throughout the transaction. This reduces commit-time aborts due to"
+ + " conflicts, but can increase how long transactions wait for locks and the overall"
+ + " contention.\n\n"
+ + "OPTIMISTIC: Locks for reads within the transaction are not acquired on read."
+ + " Instead, the locks are acquired on commit to validate that read/queried data has"
+ + " not changed since the transaction started. If a conflict is detected, the"
+ + " transaction will fail. This mode only applies to SERIALIZABLE isolation. This"
+ + " mode defers locking until commit, which can reduce contention and improve"
+ + " throughput. However, be aware that this increases the risk of transaction aborts"
+ + " if there's significant write competition on the same data.\n\n"
+ + "READ_LOCK_MODE_UNSPECIFIED: This is the default if no mode is set. The locking"
+ + " behavior depends on the isolation level:\n\n"
+ + "REPEATABLE_READ: Locking semantics default to OPTIMISTIC. However, validation"
+ + " checks at commit are only performed for queries using SELECT FOR UPDATE,"
+ + " statements with {@code LOCK_SCANNED_RANGES} hints, and DML statements.\n\n"
+ + "For all other isolation levels: If the read lock mode is not set, it defaults to"
+ + " PESSIMISTIC locking.",
+ ReadLockMode.READ_LOCK_MODE_UNSPECIFIED,
+ Arrays.stream(ReadLockMode.values())
+ .filter(mode -> !mode.equals(ReadLockMode.UNRECOGNIZED))
+ .collect(Collectors.toList())
+ .toArray(new ReadLockMode[0]),
+ ReadLockModeConverter.INSTANCE,
+ Context.USER);
static final ConnectionProperty AUTOCOMMIT_DML_MODE =
create(
"autocommit_dml_mode",
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 b8f4676fa76..e8872a48f26 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
@@ -23,6 +23,7 @@
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.TransactionOptions;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.time.Duration;
/**
@@ -190,4 +191,8 @@ StatementResult statementSetPgSessionCharacteristicsTransactionMode(
StatementResult statementSetAutoBatchDmlUpdateCountVerification(Boolean verification);
StatementResult statementShowAutoBatchDmlUpdateCountVerification();
+
+ StatementResult statementSetReadLockMode(ReadLockMode readLockMode);
+
+ StatementResult statementShowReadLockMode();
}
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 ae88bcc6ffe..0cce7490776 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
@@ -43,6 +43,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_PROTO_DESCRIPTORS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_PROTO_DESCRIPTORS_FILE_PATH;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
+import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READ_LOCK_MODE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READ_ONLY_STALENESS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETRY_ABORTS_INTERNALLY;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETURN_COMMIT_STATS;
@@ -73,6 +74,7 @@
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_PROTO_DESCRIPTORS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_PROTO_DESCRIPTORS_FILE_PATH;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READONLY;
+import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_LOCK_MODE;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_TIMESTAMP;
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY;
@@ -113,6 +115,7 @@
import com.google.spanner.v1.QueryPlan;
import com.google.spanner.v1.RequestOptions;
import com.google.spanner.v1.TransactionOptions;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
@@ -607,6 +610,20 @@ public StatementResult statementShowSavepointSupport() {
SHOW_SAVEPOINT_SUPPORT);
}
+ @Override
+ public StatementResult statementSetReadLockMode(ReadLockMode readLockMode) {
+ getConnection().setReadLockMode(readLockMode);
+ return noResult(SET_READ_LOCK_MODE);
+ }
+
+ @Override
+ public StatementResult statementShowReadLockMode() {
+ return resultSet(
+ String.format("%sREAD_LOCK_MODE", getNamespace(connection.getDialect())),
+ getConnection().getReadLockMode(),
+ SHOW_READ_LOCK_MODE);
+ }
+
@Override
public StatementResult statementShowTransactionIsolationLevel() {
return resultSet("transaction_isolation", "serializable", SHOW_TRANSACTION_ISOLATION_LEVEL);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
index 19e9d8e61c9..741e8e463b3 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
@@ -61,6 +61,7 @@
import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.v1.SpannerGrpc;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
+import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.context.Scope;
import java.time.Duration;
@@ -155,6 +156,7 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
private final ReentrantLock keepAliveLock;
private final SavepointSupport savepointSupport;
@Nonnull private final IsolationLevel isolationLevel;
+ private final ReadLockMode readLockMode;
private int transactionRetryAttempts;
private int successfulRetries;
private volatile ApiFuture txContextFuture;
@@ -207,6 +209,7 @@ static class Builder extends AbstractMultiUseTransaction.Builder data() {
+ List