Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions google/cloud/spanner/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ add_library(
numeric.h
oid.h
options.h
order_by.h
partition_options.cc
partition_options.h
partitioned_dml_result.h
Expand Down
19 changes: 14 additions & 5 deletions google/cloud/spanner/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,44 +53,52 @@ RowStream Client::Read(std::string table, KeySet keys,
std::vector<std::string> columns, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto order_by = ExtractOpt<OrderByOption>(opts);
internal::OptionsSpan span(std::move(opts));
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
Transaction::ReadOnlyOptions()),
std::move(table), std::move(keys), std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(order_by)});
}

RowStream Client::Read(Transaction::SingleUseOptions transaction_options,
std::string table, KeySet keys,
std::vector<std::string> columns, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto order_by = ExtractOpt<OrderByOption>(opts);

internal::OptionsSpan span(std::move(opts));
return conn_->Read({spanner_internal::MakeSingleUseTransaction(
std::move(transaction_options)),
std::move(table), std::move(keys), std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(order_by)});
}

RowStream Client::Read(Transaction transaction, std::string table, KeySet keys,
std::vector<std::string> columns, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto order_by = ExtractOpt<OrderByOption>(opts);
internal::OptionsSpan span(std::move(opts));
return conn_->Read({std::move(transaction), std::move(table), std::move(keys),
std::move(columns),
ToReadOptions(internal::CurrentOptions()), absl::nullopt,
false, std::move(directed_read_option)});
false, std::move(directed_read_option),
std::move(order_by)});
}

RowStream Client::Read(ReadPartition const& read_partition, Options opts) {
opts = internal::MergeOptions(std::move(opts), opts_);
auto directed_read_option = ExtractOpt<DirectedReadOption>(opts);
auto order_by = ExtractOpt<OrderByOption>(opts);
internal::OptionsSpan span(std::move(opts));
return conn_->Read(spanner_internal::MakeReadParams(
read_partition, std::move(directed_read_option)));
read_partition, std::move(directed_read_option), std::move(order_by)));
}

StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
Expand All @@ -100,7 +108,8 @@ StatusOr<std::vector<ReadPartition>> Client::PartitionRead(
return conn_->PartitionRead(
{{std::move(transaction), std::move(table), std::move(keys),
std::move(columns), ToReadOptions(internal::CurrentOptions()),
absl::nullopt, false, DirectedReadOption::Type{}},
absl::nullopt, false, DirectedReadOption::Type{},
OrderBy::kOrderByUnspecified},
ToPartitionOptions(internal::CurrentOptions())});
}

Expand Down
9 changes: 6 additions & 3 deletions google/cloud/spanner/client_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ TEST(ClientTest, CommitMutatorSuccess) {

auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};
Connection::CommitParams actual_commit_params{txn, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
Expand Down Expand Up @@ -453,7 +454,8 @@ TEST(ClientTest, CommitMutatorSuccess) {
TEST(ClientTest, CommitMutatorRollback) {
auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
auto constexpr kText = R"pb(
Expand Down Expand Up @@ -495,7 +497,8 @@ TEST(ClientTest, CommitMutatorRollback) {
TEST(ClientTest, CommitMutatorRollbackError) {
auto conn = std::make_shared<MockConnection>();
Transaction txn = MakeReadWriteTransaction(); // placeholder
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {}, {}, {}, {}};
Connection::ReadParams actual_read_params{txn, {}, {}, {}, {},
{}, {}, {}, {}};

auto source = std::make_unique<MockResultSetSource>();
auto constexpr kText = R"pb(
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/spanner/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "google/cloud/spanner/keys.h"
#include "google/cloud/spanner/mutations.h"
#include "google/cloud/spanner/options.h"
#include "google/cloud/spanner/order_by.h"
#include "google/cloud/spanner/partition_options.h"
#include "google/cloud/spanner/partitioned_dml_result.h"
#include "google/cloud/spanner/query_options.h"
Expand Down Expand Up @@ -81,6 +82,7 @@ class Connection {
absl::optional<std::string> partition_token;
bool partition_data_boost = false; // when partition_token
DirectedReadOption::Type directed_read_option;
OrderBy order_by;
};

/// Wrap the arguments to `PartitionRead()`.
Expand Down
1 change: 1 addition & 0 deletions google/cloud/spanner/google_cloud_cpp_spanner.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ google_cloud_cpp_spanner_hdrs = [
"numeric.h",
"oid.h",
"options.h",
"order_by.h",
"partition_options.h",
"partitioned_dml_result.h",
"polling_policy.h",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ class ClientIntegrationTest : public spanner_testing::DatabaseIntegrationTest {
spanner_testing::DatabaseIntegrationTest::TearDownTestSuite();
}

static void InsertUnorderedSingers() {
auto commit_result = client_->Commit(
Mutations{InsertMutationBuilder("Singers",
{"SingerId", "FirstName", "LastName"})
.EmplaceRow(3, "test-fname-3", "test-lname-3")
.EmplaceRow(1, "test-fname-1", "test-lname-1")
.EmplaceRow(2, "test-fname-2", "test-lname-2")
.Build()},
Options{}.set<GrpcCompressionAlgorithmOption>(GRPC_COMPRESS_DEFLATE));
ASSERT_STATUS_OK(commit_result);
}

static std::unique_ptr<Client> client_;
};

Expand Down Expand Up @@ -834,6 +846,23 @@ TEST_F(ClientIntegrationTest, DirectedReadWithinReadWriteTransaction) {
"in a read-only transaction")));
}

/// @test Verify that Read() returns rows ordered by primary key.
TEST_F(ClientIntegrationTest, ReadWithOrderByPrimaryKey) {
ASSERT_NO_FATAL_FAILURE(InsertUnorderedSingers());

auto rows = client_->Read(
"Singers", KeySet::All(), {"SingerId", "FirstName", "LastName"},
Options{}.set<OrderByOption>(OrderBy::kOrderByPrimaryKey));

using RowType = std::tuple<std::int64_t, std::string, std::string>;
std::vector<std::int64_t> actual_singer_ids;
for (auto& row : StreamOf<RowType>(rows)) {
if (!row) break;
actual_singer_ids.push_back(std::get<0>(*row));
}
EXPECT_THAT(actual_singer_ids, ::testing::ElementsAre(1, 2, 3));
}

StatusOr<std::vector<std::vector<Value>>> AddSingerDataToTable(Client client) {
std::vector<std::vector<Value>> expected_rows;
auto commit = client.Commit(
Expand Down
16 changes: 16 additions & 0 deletions google/cloud/spanner/internal/connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ google::spanner::v1::RequestOptions_Priority ProtoRequestPriority(
return google::spanner::v1::RequestOptions::PRIORITY_UNSPECIFIED;
}

google::spanner::v1::ReadRequest_OrderBy ProtoOrderBy(
absl::optional<spanner::OrderBy> const& order_by) {
if (order_by) {
switch (*order_by) {
case spanner::OrderBy::kOrderByUnspecified:
return google::spanner::v1::ReadRequest_OrderBy_ORDER_BY_UNSPECIFIED;
case spanner::OrderBy::kOrderByPrimaryKey:
return google::spanner::v1::ReadRequest_OrderBy_ORDER_BY_PRIMARY_KEY;
case spanner::OrderBy::kOrderByNoOrder:
return google::spanner::v1::ReadRequest_OrderBy_ORDER_BY_NO_ORDER;
}
}
return google::spanner::v1::ReadRequest_OrderBy_ORDER_BY_UNSPECIFIED;
}

// Converts a `google::protobuf::Timestamp` to a `spanner::Timestamp`, but
// substitutes the maximal value for any conversion error. This is needed
// when, for example, a response commit_timestamp is out of range but the
Expand Down Expand Up @@ -548,6 +563,7 @@ spanner::RowStream ConnectionImpl::ReadImpl(
*request->mutable_transaction() = *s;
request->set_table(std::move(params.table));
request->set_index(std::move(params.read_options.index_name));
request->set_order_by(ProtoOrderBy(params.order_by));
for (auto&& column : params.columns) {
request->add_columns(std::move(column));
}
Expand Down
83 changes: 83 additions & 0 deletions google/cloud/spanner/internal/connection_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ MATCHER_P(HasReplicaType, type, "has replica type") {
return arg.type() == type;
}

MATCHER_P(HasOrderBy, order_by, "has order_by") {
return arg.order_by() == order_by;
}

// Ideally this would be a matcher, but matcher args are `const` and `RowStream`
// only has non-const methods.
bool ContainsNoRows(spanner::RowStream& rows) {
Expand Down Expand Up @@ -4091,6 +4095,85 @@ TEST(ConnectionImplTest, RollbackSessionNotFound) {
EXPECT_THAT(txn, HasBadSession());
}

TEST(ConnectionImplTest, ReadRequestOrderByParameterUnspecified) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("project", "instance", "database");
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
EXPECT_CALL(*mock,
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
.WillOnce(Return(make_ready_future(Status{})));
Sequence s;
EXPECT_CALL(
*mock,
StreamingRead(
_, _,
AllOf(HasSession("test-session-name"),
HasOrderBy(
google::spanner::v1::ReadRequest::ORDER_BY_UNSPECIFIED))))
.InSequence(s)
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));

auto conn = MakeConnectionImpl(db, mock);
internal::OptionsSpan span(MakeLimitedTimeOptions());

// Scenario 1: No explicit OrderBy (should map to UNSPECIFIED)
spanner::ReadOptions read_options;
spanner::Transaction txn1 =
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
auto rows1 = conn->Read(
{txn1, "table", spanner::KeySet::All(), {"col"}, read_options});
for (auto const& row : rows1) {
(void)row;
}
EXPECT_THAT(txn1,
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
}

TEST(ConnectionImplTest, ReadRequestOrderByParameterNoOrder) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("project", "instance", "database");
EXPECT_CALL(*mock, BatchCreateSessions(_, _, HasDatabase(db)))
.WillOnce(Return(MakeSessionsResponse({"test-session-name"})));
EXPECT_CALL(*mock,
AsyncDeleteSession(_, _, _, HasSessionName("test-session-name")))
.WillOnce(Return(make_ready_future(Status{})));
Sequence s;
EXPECT_CALL(
*mock,
StreamingRead(
_, _,
AllOf(
HasSession("test-session-name"),
HasOrderBy(google::spanner::v1::ReadRequest::ORDER_BY_NO_ORDER))))
.InSequence(s)
.WillOnce(Return(ByMove(MakeReader<google::spanner::v1::PartialResultSet>(
{R"pb(metadata: { transaction: { id: "txn1" } })pb"}))));

auto conn = MakeConnectionImpl(db, mock);
internal::OptionsSpan span(MakeLimitedTimeOptions());
spanner::ReadOptions read_options;
spanner::Transaction txn1 =
MakeReadOnlyTransaction(spanner::Transaction::ReadOnlyOptions());
auto read_params =
spanner::Connection::ReadParams{txn1,
"table",
spanner::KeySet::All(),
{"col"},
read_options,
absl::nullopt,
false,
spanner::DirectedReadOption::Type{},
spanner::OrderBy::kOrderByNoOrder};
auto rows1 = conn->Read(read_params);
for (auto const& row : rows1) {
(void)row;
}
EXPECT_THAT(txn1,
HasSessionAndTransaction("test-session-name", "txn1", false, ""));
}

TEST(ConnectionImplTest, OperationsFailOnInvalidatedTransaction) {
auto mock = std::make_shared<spanner_testing::MockSpannerStub>();
auto db = spanner::Database("placeholder_project", "placeholder_instance",
Expand Down
11 changes: 11 additions & 0 deletions google/cloud/spanner/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "google/cloud/spanner/backoff_policy.h"
#include "google/cloud/spanner/directed_read_replicas.h"
#include "google/cloud/spanner/internal/session.h"
#include "google/cloud/spanner/order_by.h"
#include "google/cloud/spanner/polling_policy.h"
#include "google/cloud/spanner/request_priority.h"
#include "google/cloud/spanner/retry_policy.h"
Expand Down Expand Up @@ -195,6 +196,16 @@ struct SessionPoolActionOnExhaustionOption {
using Type = spanner::ActionOnExhaustion;
};

/**
* Option for `google::cloud::Options` to set the order in which the rows are
* returned from a read request.
*
* @ingroup google-cloud-spanner-options
*/
struct OrderByOption {
using Type = spanner::OrderBy;
};

/**
* Option for `google::cloud::Options` to set the interval at which we refresh
* sessions so they don't get collected by the backend GC.
Expand Down
37 changes: 37 additions & 0 deletions google/cloud/spanner/order_by.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2025 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
//
// https://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.

#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_ORDER_BY_H
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_ORDER_BY_H

#include "google/cloud/spanner/version.h"

namespace google {
namespace cloud {
namespace spanner {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

// Controls the order in which rows are returned from a read request.
enum class OrderBy {
kOrderByUnspecified,
kOrderByPrimaryKey,
kOrderByNoOrder,
};

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace spanner
} // namespace cloud
} // namespace google

#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_ORDER_BY_H
14 changes: 9 additions & 5 deletions google/cloud/spanner/read_partition.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ struct ReadPartitionInternals {

static spanner::Connection::ReadParams MakeReadParams(
spanner::ReadPartition const& read_partition,
spanner::DirectedReadOption::Type directed_read_option) {
spanner::DirectedReadOption::Type directed_read_option,
spanner::OrderByOption::Type order_by_option) {
return spanner::Connection::ReadParams{
MakeTransactionFromIds(
read_partition.SessionId(), read_partition.TransactionId(),
Expand All @@ -185,7 +186,8 @@ struct ReadPartitionInternals {
read_partition.ReadOptions(),
read_partition.PartitionToken(),
read_partition.DataBoost(),
std::move(directed_read_option)};
std::move(directed_read_option),
std::move(order_by_option)};
}
};

Expand All @@ -204,9 +206,11 @@ inline spanner::ReadPartition MakeReadPartition(

inline spanner::Connection::ReadParams MakeReadParams(
spanner::ReadPartition const& read_partition,
spanner::DirectedReadOption::Type directed_read_option) {
return ReadPartitionInternals::MakeReadParams(
read_partition, std::move(directed_read_option));
spanner::DirectedReadOption::Type directed_read_option,
spanner::OrderByOption::Type order_by_option) {
return ReadPartitionInternals::MakeReadParams(read_partition,
std::move(directed_read_option),
std::move(order_by_option));
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
Loading
Loading