Skip to content

Commit 4d243d4

Browse files
authored
impl(spanner): add uuid type (#15023)
1 parent b63f69f commit 4d243d4

File tree

6 files changed

+470
-0
lines changed

6 files changed

+470
-0
lines changed

google/cloud/spanner/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ add_library(
211211
transaction.cc
212212
transaction.h
213213
update_instance_request_builder.h
214+
uuid.cc
215+
uuid.h
214216
value.cc
215217
value.h
216218
version.cc
@@ -502,6 +504,7 @@ function (spanner_client_define_tests)
502504
timestamp_test.cc
503505
transaction_test.cc
504506
update_instance_request_builder_test.cc
507+
uuid_test.cc
505508
value_test.cc)
506509

507510
# Export the list of unit tests to a .bzl file so we do not need to maintain

google/cloud/spanner/google_cloud_cpp_spanner.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ google_cloud_cpp_spanner_hdrs = [
119119
"tracing_options.h",
120120
"transaction.h",
121121
"update_instance_request_builder.h",
122+
"uuid.h",
122123
"value.h",
123124
"version.h",
124125
"version_info.h",
@@ -198,6 +199,7 @@ google_cloud_cpp_spanner_srcs = [
198199
"sql_statement.cc",
199200
"timestamp.cc",
200201
"transaction.cc",
202+
"uuid.cc",
201203
"value.cc",
202204
"version.cc",
203205
]

google/cloud/spanner/spanner_client_unit_tests.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@ spanner_client_unit_tests = [
6969
"timestamp_test.cc",
7070
"transaction_test.cc",
7171
"update_instance_request_builder_test.cc",
72+
"uuid_test.cc",
7273
"value_test.cc",
7374
]

google/cloud/spanner/uuid.cc

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
#include "google/cloud/spanner/uuid.h"
16+
#include "google/cloud/internal/make_status.h"
17+
#include "absl/strings/str_format.h"
18+
#include "absl/strings/strip.h"
19+
#include <unordered_map>
20+
21+
namespace google {
22+
namespace cloud {
23+
namespace spanner {
24+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
25+
namespace {
26+
27+
// Helper function to parse a single hexadecimal block of a UUID.
28+
// A hexadecimal block is a 16-digit hexadecimal number, which is represented
29+
// as 8 bytes.
30+
StatusOr<std::uint64_t> ParseHexBlock(absl::string_view& str,
31+
absl::string_view original_str) {
32+
constexpr int kUuidNumberOfHexDigits = 32;
33+
constexpr int kMaxUuidBlockLength = 16;
34+
static auto const* char_to_hex = new std::unordered_map<char, std::uint8_t>(
35+
{{'0', 0x00}, {'1', 0x01}, {'2', 0x02}, {'3', 0x03}, {'4', 0x04},
36+
{'5', 0x05}, {'6', 0x06}, {'7', 0x07}, {'8', 0x08}, {'9', 0x09},
37+
{'a', 0x0a}, {'b', 0x0b}, {'c', 0x0c}, {'d', 0x0d}, {'e', 0x0e},
38+
{'f', 0x0f}, {'A', 0x0a}, {'B', 0x0b}, {'C', 0x0c}, {'D', 0x0d},
39+
{'E', 0x0e}, {'F', 0x0f}});
40+
std::uint64_t block = 0;
41+
for (int j = 0; j < kMaxUuidBlockLength; ++j) {
42+
absl::ConsumePrefix(&str, "-");
43+
if (str.empty()) {
44+
return internal::InvalidArgumentError(
45+
absl::StrFormat("UUID must contain %d hexadecimal digits: %s",
46+
kUuidNumberOfHexDigits, original_str),
47+
GCP_ERROR_INFO());
48+
}
49+
auto it = char_to_hex->find(str[0]);
50+
if (it == char_to_hex->end()) {
51+
if (str[0] == '-') {
52+
return internal::InvalidArgumentError(
53+
absl::StrFormat("UUID cannot contain consecutive hyphens: %s",
54+
original_str),
55+
GCP_ERROR_INFO());
56+
}
57+
58+
return internal::InvalidArgumentError(
59+
absl::StrFormat("UUID contains invalid character (%c): %s", str[0],
60+
original_str),
61+
GCP_ERROR_INFO());
62+
}
63+
block = (block << 4) + it->second;
64+
str.remove_prefix(1);
65+
}
66+
return block;
67+
}
68+
} // namespace
69+
70+
Uuid::Uuid(absl::uint128 value) : uuid_(value) {}
71+
72+
Uuid::Uuid(std::uint64_t high_bits, std::uint64_t low_bits)
73+
: Uuid(absl::MakeUint128(high_bits, low_bits)) {}
74+
75+
std::pair<std::uint64_t, std::uint64_t> Uuid::As64BitPair() const {
76+
return std::make_pair(Uint128High64(uuid_), Uint128Low64(uuid_));
77+
}
78+
79+
// TODO(#15043): Refactor to handle all 128 bits at once instead of splitting
80+
// into a pair of unsigned 64-bit integers.
81+
Uuid::operator std::string() const {
82+
constexpr int kUuidStringLen = 36;
83+
constexpr int kChunkLength[] = {8, 4, 4, 4, 12};
84+
auto to_hex = [](std::uint64_t v, int start_index, int end_index, char* out) {
85+
static constexpr char kHexChar[] = {'0', '1', '2', '3', '4', '5', '6', '7',
86+
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
87+
for (int i = start_index; i >= end_index; --i) {
88+
*out++ = kHexChar[(v >> (i * 4)) & 0xf];
89+
}
90+
return start_index - end_index + 1;
91+
};
92+
93+
std::string output;
94+
output.resize(kUuidStringLen);
95+
char* target = const_cast<char*>(output.data());
96+
char* const last = &((output)[output.size()]);
97+
auto bits = Uint128High64(uuid_);
98+
int start = 16;
99+
for (auto length : kChunkLength) {
100+
int end = start - length;
101+
target += to_hex(bits, start - 1, end, target);
102+
// Only hyphens write to valid addresses.
103+
if (target < last) *(target++) = '-';
104+
if (end == 0) {
105+
start = 16;
106+
bits = Uint128Low64(uuid_);
107+
} else {
108+
start = end;
109+
}
110+
}
111+
return output;
112+
}
113+
114+
StatusOr<Uuid> MakeUuid(absl::string_view str) {
115+
absl::string_view original_str = str;
116+
// Check and remove optional braces
117+
if (absl::ConsumePrefix(&str, "{")) {
118+
if (!absl::ConsumeSuffix(&str, "}")) {
119+
return internal::InvalidArgumentError(
120+
absl::StrFormat("UUID missing closing '}': %s", original_str),
121+
GCP_ERROR_INFO());
122+
}
123+
}
124+
125+
// Check for leading hyphen after stripping any surrounding braces.
126+
if (absl::StartsWith(str, "-")) {
127+
return internal::InvalidArgumentError(
128+
absl::StrFormat("UUID cannot begin with '-': %s", original_str),
129+
GCP_ERROR_INFO());
130+
}
131+
132+
// TODO(#15043): Refactor to parse all the bits at once.
133+
auto high_bits = ParseHexBlock(str, original_str);
134+
if (!high_bits.ok()) return std::move(high_bits).status();
135+
auto low_bits = ParseHexBlock(str, original_str);
136+
if (!low_bits.ok()) return std::move(low_bits).status();
137+
138+
if (!str.empty()) {
139+
return internal::InvalidArgumentError(
140+
absl::StrFormat("Extra characters found after parsing UUID: %s", str),
141+
GCP_ERROR_INFO());
142+
}
143+
144+
return Uuid(absl::MakeUint128(*high_bits, *low_bits));
145+
}
146+
147+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
148+
} // namespace spanner
149+
} // namespace cloud
150+
} // namespace google

google/cloud/spanner/uuid.h

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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_UUID_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_UUID_H
17+
18+
#include "google/cloud/spanner/version.h"
19+
#include "google/cloud/status_or.h"
20+
#include "absl/numeric/int128.h"
21+
#include "absl/strings/string_view.h"
22+
#include <cstdint>
23+
#include <iosfwd>
24+
#include <string>
25+
#include <utility>
26+
27+
namespace google {
28+
namespace cloud {
29+
namespace spanner {
30+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
31+
32+
/**
33+
* A representation of the Spanner UUID type: A fixed size 16 byte value
34+
* that can be represented as a 32-digit hexadecimal string.
35+
*
36+
* @see https://cloud.google.com/spanner/docs/data-types#uuid_type
37+
*/
38+
class Uuid {
39+
public:
40+
/// Default construction yields a zero value UUID.
41+
Uuid() = default;
42+
43+
/// Construct a UUID from one unsigned 128-bit integer.
44+
explicit Uuid(absl::uint128 value);
45+
46+
/// Construct a UUID from two unsigned 64-bit pieces.
47+
Uuid(std::uint64_t high_bits, std::uint64_t low_bits);
48+
49+
/// @name Regular value type, supporting copy, assign, move.
50+
///@{
51+
Uuid(Uuid&&) = default;
52+
Uuid& operator=(Uuid&&) = default;
53+
Uuid(Uuid const&) = default;
54+
Uuid& operator=(Uuid const&) = default;
55+
///@}
56+
57+
/// @name Relational operators
58+
///
59+
///@{
60+
friend bool operator==(Uuid const& lhs, Uuid const& rhs) {
61+
return lhs.uuid_ == rhs.uuid_;
62+
}
63+
friend bool operator!=(Uuid const& lhs, Uuid const& rhs) {
64+
return !(lhs == rhs);
65+
}
66+
friend bool operator<(Uuid const& lhs, Uuid const& rhs) {
67+
return lhs.uuid_ < rhs.uuid_;
68+
}
69+
friend bool operator<=(Uuid const& lhs, Uuid const& rhs) {
70+
return !(rhs < lhs);
71+
}
72+
friend bool operator>=(Uuid const& lhs, Uuid const& rhs) {
73+
return !(lhs < rhs);
74+
}
75+
friend bool operator>(Uuid const& lhs, Uuid const& rhs) { return rhs < lhs; }
76+
///@}
77+
78+
/// @name Returns a pair of unsigned 64-bit integers representing the UUID.
79+
std::pair<std::uint64_t, std::uint64_t> As64BitPair() const;
80+
81+
/// @name Conversion to unsigned 128-bit integer representation.
82+
explicit operator absl::uint128() const { return uuid_; }
83+
84+
/// @name Conversion to a lower case string formatted as:
85+
/// [8 hex-digits]-[4 hex-digits]-[4 hex-digits]-[4 hex-digits]-[12
86+
/// hex-digits]
87+
/// Example: 0b6ed04c-a16d-fc46-5281-7f9978c13738
88+
explicit operator std::string() const;
89+
90+
/// @name Output streaming
91+
friend std::ostream& operator<<(std::ostream& os, Uuid uuid) {
92+
return os << std::string(uuid);
93+
}
94+
95+
private:
96+
absl::uint128 uuid_ = 0;
97+
};
98+
99+
/**
100+
* Parses a textual representation a `Uuid` from a string of hexadecimal digits.
101+
* Returns an error if unable to parse the given input.
102+
*
103+
* Acceptable input strings must consist of 32 hexadecimal digits: [0-9a-fA-F].
104+
* Optional curly braces are allowed around the entire sequence of digits as are
105+
* hyphens between any pair of hexadecimal digits.
106+
*
107+
* Example acceptable inputs:
108+
* - {0b6ed04c-a16d-fc46-5281-7f9978c13738}
109+
* - 0b6ed04ca16dfc4652817f9978c13738
110+
* - 7Bf8-A7b8-1917-1919-2625-F208-c582-4254
111+
* - {DECAFBAD-DEAD-FADE-CAFE-FEEDFACEBEEF}
112+
*/
113+
StatusOr<Uuid> MakeUuid(absl::string_view s);
114+
115+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
116+
} // namespace spanner
117+
} // namespace cloud
118+
} // namespace google
119+
120+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_UUID_H

0 commit comments

Comments
 (0)