Skip to content

Commit f74959a

Browse files
authored
feat(spanner): add support for lock_hint enum (#15249)
* feat(spanner): add support for lock_hint enum
1 parent 65a6980 commit f74959a

File tree

11 files changed

+233
-13
lines changed

11 files changed

+233
-13
lines changed

google/cloud/spanner/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ add_library(
176176
json.h
177177
keys.cc
178178
keys.h
179+
lock_hint.h
179180
mutations.cc
180181
mutations.h
181182
numeric.cc

google/cloud/spanner/client.cc

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,44 +53,51 @@ RowStream Client::Read(std::string table, KeySet keys,
5353
std::vector<std::string> columns, Options opts) {
5454
opts = internal::MergeOptions(std::move(opts), opts_);
5555
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
56+
auto lock_hint = ExtractOpt<LockHintOption>(opts);
5657
internal::OptionsSpan span(std::move(opts));
5758
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
5859
Transaction::ReadOnlyOptions()),
5960
std::move(table), std::move(keys), std::move(columns),
6061
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
61-
false, std::move(directed_read_option)});
62+
false, std::move(directed_read_option),
63+
std::move(lock_hint)});
6264
}
6365

6466
RowStream Client::Read(Transaction::SingleUseOptions transaction_options,
6567
std::string table, KeySet keys,
6668
std::vector<std::string> columns, Options opts) {
6769
opts = internal::MergeOptions(std::move(opts), opts_);
6870
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
71+
auto lock_hint = ExtractOpt<LockHintOption>(opts);
6972
internal::OptionsSpan span(std::move(opts));
7073
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
7174
std::move(transaction_options)),
7275
std::move(table), std::move(keys), std::move(columns),
7376
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
74-
false, std::move(directed_read_option)});
77+
false, std::move(directed_read_option),
78+
std::move(lock_hint)});
7579
}
7680

7781
RowStream Client::Read(Transaction transaction, std::string table, KeySet keys,
7882
std::vector<std::string> columns, Options opts) {
7983
opts = internal::MergeOptions(std::move(opts), opts_);
8084
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
85+
auto lock_hint = ExtractOpt<LockHintOption>(opts);
8186
internal::OptionsSpan span(std::move(opts));
8287
return conn_->Read({std::move(transaction), std::move(table), std::move(keys),
8388
std::move(columns),
8489
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
85-
false, std::move(directed_read_option)});
90+
false, std::move(directed_read_option),
91+
std::move(lock_hint)});
8692
}
8793

8894
RowStream Client::Read(ReadPartition const& read_partition, Options opts) {
8995
opts = internal::MergeOptions(std::move(opts), opts_);
9096
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
97+
auto lock_hint = ExtractOpt<LockHintOption>(opts);
9198
internal::OptionsSpan span(std::move(opts));
9299
return conn_->Read(spanner_internal::MakeReadParams(
93-
read_partition, std::move(directed_read_option)));
100+
read_partition, std::move(directed_read_option), std::move(lock_hint)));
94101
}
95102

96103
StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
@@ -100,7 +107,8 @@ StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
100107
return conn_->PartitionRead(
101108
{{std::move(transaction), std::move(table), std::move(keys),
102109
std::move(columns), ToReadOptions(internal::CurrentOptions()),
103-
absl::nullopt, false, DirectedReadOption::Type{}},
110+
absl::nullopt, false, DirectedReadOption::Type{},
111+
LockHint::kLockHintUnspecified},
104112
ToPartitionOptions(internal::CurrentOptions())});
105113
}
106114

google/cloud/spanner/client_test.cc

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,58 @@ TEST(ClientTest, ReadSuccess) {
138138
IsOkAndHolds(RowType("Ann", 42))));
139139
}
140140

141+
TEST(ClientTest, ReadWithLockHint) {
142+
auto conn = std::make_shared<MockConnection>();
143+
Client client(conn);
144+
145+
auto constexpr kText = R"pb(
146+
row_type: {
147+
fields: {
148+
name: "Name",
149+
type: { code: INT64 }
150+
}
151+
fields: {
152+
name: "Id",
153+
type: { code: INT64 }
154+
}
155+
}
156+
)pb";
157+
google::spanner::v1::ResultSetMetadata metadata;
158+
ASSERT_TRUE(TextFormat::ParseFromString(kText, &metadata));
159+
160+
EXPECT_CALL(*conn, Read)
161+
.WillOnce([&metadata](Connection::ReadParams const& params) {
162+
EXPECT_THAT(
163+
params.directed_read_option,
164+
VariantWith<IncludeReplicas>(AllOf(
165+
Property(&IncludeReplicas::replica_selections,
166+
ElementsAre(ReplicaSelection(ReplicaType::kReadOnly))),
167+
Property(&IncludeReplicas::auto_failover_disabled, true))));
168+
EXPECT_THAT(params.lock_hint, Eq(LockHint::kLockHintShared));
169+
auto source = std::make_unique<MockResultSetSource>();
170+
EXPECT_CALL(*source, Metadata()).WillRepeatedly(Return(metadata));
171+
EXPECT_CALL(*source, NextRow())
172+
.WillOnce(Return(spanner_mocks::MakeRow("Steve", 12)))
173+
.WillOnce(Return(spanner_mocks::MakeRow("Ann", 42)))
174+
.WillOnce(Return(Row()));
175+
return RowStream(std::move(source));
176+
});
177+
178+
KeySet keys = KeySet::All();
179+
auto rows = client.Read("table", std::move(keys), {"column1", "column2"},
180+
Options{}
181+
.set<DirectedReadOption>(IncludeReplicas(
182+
{ReplicaSelection(ReplicaType::kReadOnly)},
183+
/*auto_failover_disabled=*/true))
184+
.set<LockHintOption>(LockHint::kLockHintShared));
185+
186+
using RowType = std::tuple<std::string, std::int64_t>;
187+
auto stream = StreamOf<RowType>(rows);
188+
auto actual = std::vector<StatusOr<RowType>>{stream.begin(), stream.end()};
189+
EXPECT_THAT(actual, ElementsAre(IsOkAndHolds(RowType("Steve", 12)),
190+
IsOkAndHolds(RowType("Ann", 42))));
191+
}
192+
141193
TEST(ClientTest, ReadFailure) {
142194
auto conn = std::make_shared<MockConnection>();
143195
Client client(conn);
@@ -404,7 +456,8 @@ TEST(ClientTest, CommitMutatorSuccess) {
404456

405457
auto conn = std::make_shared<MockConnection>();
406458
Transaction txn = MakeReadWriteTransaction(); // placeholder
407-
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
459+
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
460+
{}, {}, {}, {}};
408461
Connection::CommitParams actual_commit_params{txn, {}, {}};
409462

410463
auto source = std::make_unique<MockResultSetSource>();
@@ -453,7 +506,8 @@ TEST(ClientTest, CommitMutatorSuccess) {
453506
TEST(ClientTest, CommitMutatorRollback) {
454507
auto conn = std::make_shared<MockConnection>();
455508
Transaction txn = MakeReadWriteTransaction(); // placeholder
456-
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
509+
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
510+
{}, {}, {}, {}};
457511

458512
auto source = std::make_unique<MockResultSetSource>();
459513
auto constexpr kText = R"pb(
@@ -495,7 +549,8 @@ TEST(ClientTest, CommitMutatorRollback) {
495549
TEST(ClientTest, CommitMutatorRollbackError) {
496550
auto conn = std::make_shared<MockConnection>();
497551
Transaction txn = MakeReadWriteTransaction(); // placeholder
498-
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
552+
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
553+
{}, {}, {}, {}};
499554

500555
auto source = std::make_unique<MockResultSetSource>();
501556
auto constexpr kText = R"pb(

google/cloud/spanner/connection.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "google/cloud/spanner/commit_options.h"
2020
#include "google/cloud/spanner/commit_result.h"
2121
#include "google/cloud/spanner/keys.h"
22+
#include "google/cloud/spanner/lock_hint.h"
2223
#include "google/cloud/spanner/mutations.h"
2324
#include "google/cloud/spanner/options.h"
2425
#include "google/cloud/spanner/partition_options.h"
@@ -81,6 +82,7 @@ class Connection {
8182
absl::optional<std::string> partition_token;
8283
bool partition_data_boost = false; // when partition_token
8384
DirectedReadOption::Type directed_read_option;
85+
LockHint lock_hint;
8486
};
8587

8688
/// Wrap the arguments to `PartitionRead()`.

google/cloud/spanner/google_cloud_cpp_spanner.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ google_cloud_cpp_spanner_hdrs = [
9696
"interval.h",
9797
"json.h",
9898
"keys.h",
99+
"lock_hint.h",
99100
"mutations.h",
100101
"numeric.h",
101102
"oid.h",

google/cloud/spanner/internal/connection_impl.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,21 @@ google::spanner::v1::RequestOptions_Priority ProtoRequestPriority(
190190
return google::spanner::v1::RequestOptions::PRIORITY_UNSPECIFIED;
191191
}
192192

193+
google::spanner::v1::ReadRequest_LockHint ProtoLockHint(
194+
absl::optional<spanner::LockHint> const& order_by) {
195+
if (order_by) {
196+
switch (*order_by) {
197+
case spanner::LockHint::kLockHintUnspecified:
198+
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_UNSPECIFIED;
199+
case spanner::LockHint::kLockHintShared:
200+
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_SHARED;
201+
case spanner::LockHint::kLockHintExclusive:
202+
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_EXCLUSIVE;
203+
}
204+
}
205+
return google::spanner::v1::ReadRequest_LockHint_LOCK_HINT_UNSPECIFIED;
206+
}
207+
193208
// Converts a `google::protobuf::Timestamp` to a `spanner::Timestamp`, but
194209
// substitutes the maximal value for any conversion error. This is needed
195210
// when, for example, a response commit_timestamp is out of range but the
@@ -560,6 +575,7 @@ spanner::RowStream ConnectionImpl::ReadImpl(
560575
*request->mutable_transaction() = *s;
561576
request->set_table(std::move(params.table));
562577
request->set_index(std::move(params.read_options.index_name));
578+
request->set_lock_hint(ProtoLockHint(params.lock_hint));
563579
for (auto&& column : params.columns) {
564580
request->add_columns(std::move(column));
565581
}

google/cloud/spanner/internal/connection_impl_test.cc

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ MATCHER_P(HasReplicaType, type, "has replica type") {
211211
return arg.type() == type;
212212
}
213213

214+
MATCHER_P(HasLockHint, lock_hint, "has lock_hint") {
215+
return arg.lock_hint() == lock_hint;
216+
}
217+
214218
// Ideally this would be a matcher, but matcher args are `const` and `RowStream`
215219
// only has non-const methods.
216220
bool ContainsNoRows(spanner::RowStream& rows) {
@@ -4091,6 +4095,85 @@ TEST(ConnectionImplTest, RollbackSessionNotFound) {
40914095
EXPECT_THAT(txn, HasBadSession());
40924096
}
40934097

4098+
TEST(ConnectionImplTest, ReadRequestLockHintParameterUnspecified) {
4099+
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
4100+
auto db = spanner::Database("project", "instance", "database");
4101+
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
4102+
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
4103+
EXPECT_CALL(*mock,
4104+
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
4105+
.WillOnce(Return(make_ready_future(Status{})));
4106+
Sequence s;
4107+
EXPECT_CALL(
4108+
*mock,
4109+
StreamingRead(
4110+
_, _,
4111+
AllOf(HasSession("test-session-name"),
4112+
HasLockHint(
4113+
google::spanner::v1::ReadRequest::LOCK_HINT_UNSPECIFIED))))
4114+
.InSequence(s)
4115+
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
4116+
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));
4117+
4118+
auto conn = MakeConnectionImpl(db, mock);
4119+
internal::OptionsSpan span(MakeLimitedTimeOptions());
4120+
4121+
// Scenario 1: No explicit OrderBy (should map to UNSPECIFIED)
4122+
spanner::ReadOptions read_options;
4123+
spanner::Transaction txn1 =
4124+
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
4125+
auto rows1 = conn->Read(
4126+
{txn1, "table", spanner::KeySet::All(), {"col"}, read_options});
4127+
for (auto const& row : rows1) {
4128+
(void)row;
4129+
}
4130+
EXPECT_THAT(txn1,
4131+
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
4132+
}
4133+
4134+
TEST(ConnectionImplTest, ReadRequestLockHintShared) {
4135+
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
4136+
auto db = spanner::Database("project", "instance", "database");
4137+
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
4138+
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
4139+
EXPECT_CALL(*mock,
4140+
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
4141+
.WillOnce(Return(make_ready_future(Status{})));
4142+
Sequence s;
4143+
EXPECT_CALL(
4144+
*mock,
4145+
StreamingRead(
4146+
_, _,
4147+
AllOf(
4148+
HasSession("test-session-name"),
4149+
HasLockHint(google::spanner::v1::ReadRequest::LOCK_HINT_SHARED))))
4150+
.InSequence(s)
4151+
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
4152+
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));
4153+
4154+
auto conn = MakeConnectionImpl(db, mock);
4155+
internal::OptionsSpan span(MakeLimitedTimeOptions());
4156+
spanner::ReadOptions read_options;
4157+
spanner::Transaction txn1 =
4158+
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
4159+
auto read_params =
4160+
spanner::Connection::ReadParams{txn1,
4161+
"table",
4162+
spanner::KeySet::All(),
4163+
{"col"},
4164+
read_options,
4165+
absl::nullopt,
4166+
false,
4167+
spanner::DirectedReadOption::Type{},
4168+
spanner::LockHint::kLockHintShared};
4169+
auto rows1 = conn->Read(read_params);
4170+
for (auto const& row : rows1) {
4171+
(void)row;
4172+
}
4173+
EXPECT_THAT(txn1,
4174+
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
4175+
}
4176+
40944177
TEST(ConnectionImplTest, OperationsFailOnInvalidatedTransaction) {
40954178
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
40964179
auto db = spanner::Database("placeholder_project", "placeholder_instance",

google/cloud/spanner/lock_hint.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H
17+
18+
#include "google/cloud/spanner/version.h"
19+
20+
namespace google {
21+
namespace cloud {
22+
namespace spanner {
23+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
24+
25+
// A lock hint mechanism for reads done within a transaction.
26+
enum class LockHint {
27+
kLockHintUnspecified,
28+
kLockHintShared,
29+
kLockHintExclusive,
30+
};
31+
32+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
33+
} // namespace spanner
34+
} // namespace cloud
35+
} // namespace google
36+
37+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_LOCK_HINT_H

google/cloud/spanner/options.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include "google/cloud/spanner/backoff_policy.h"
4242
#include "google/cloud/spanner/directed_read_replicas.h"
4343
#include "google/cloud/spanner/internal/session.h"
44+
#include "google/cloud/spanner/lock_hint.h"
4445
#include "google/cloud/spanner/polling_policy.h"
4546
#include "google/cloud/spanner/request_priority.h"
4647
#include "google/cloud/spanner/retry_policy.h"
@@ -195,6 +196,16 @@ struct SessionPoolActionOnExhaustionOption {
195196
using Type = spanner::ActionOnExhaustion;
196197
};
197198

199+
/**
200+
* Option for `google::cloud::Options` to set the lock hint mechanism for reads
201+
* done within a transaction.
202+
*
203+
* @ingroup google-cloud-spanner-options
204+
*/
205+
struct LockHintOption {
206+
using Type = spanner::LockHint;
207+
};
208+
198209
/**
199210
* Option for `google::cloud::Options` to set the interval at which we refresh
200211
* sessions so they don't get collected by the backend GC.

0 commit comments

Comments
 (0)