userver: userver/ydb/io/structs.hpp Source File
Loading...
Searching...
No Matches
structs.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/ydb/io/structs.hpp
4/// @brief YDB struct serialization traits and customization
5
6#include <ydb-cpp-sdk/client/result/result.h>
7#include <ydb-cpp-sdk/client/value/value.h>
8
9#include <cstddef>
10#include <memory>
11#include <optional>
12#include <string_view>
13#include <tuple>
14#include <type_traits>
15
16#include <fmt/ranges.h>
17#include <boost/pfr/core.hpp>
18#include <boost/pfr/core_name.hpp>
19
20#include <userver/utils/assert.hpp>
21#include <userver/utils/constexpr_indices.hpp>
22#include <userver/utils/enumerate.hpp>
23#include <userver/utils/forward_like.hpp>
24#include <userver/utils/trivial_map.hpp>
25
26#include <userver/ydb/exceptions.hpp>
27#include <userver/ydb/impl/cast.hpp>
28#include <userver/ydb/io/generic_optional.hpp>
29#include <userver/ydb/io/traits.hpp>
30
31USERVER_NAMESPACE_BEGIN
32
33namespace ydb {
34
35namespace impl {
36
37struct NotStruct final {};
38
39template <typename T>
40constexpr decltype(T::kYdbMemberNames) DetectStructMemberNames() noexcept {
41 return T::kYdbMemberNames;
42}
43
44template <typename T, typename... Args>
45constexpr NotStruct DetectStructMemberNames(Args&&...) noexcept {
46 return NotStruct{};
47}
48
49// To avoid including heavy <array> header for std::array<T, 0>.
50template <typename T>
51class EmptyRange final {
52public:
53 constexpr T* begin() const noexcept { return nullptr; }
54 constexpr T* end() const noexcept { return nullptr; }
55};
56
57} // namespace impl
58
59/// @see ydb::CustomMemberNames
60struct CustomMemberName final {
61 std::string_view cpp_name;
62 std::string_view ydb_name;
63};
64
65/// @brief Specifies C++ to YDB struct member names mapping. It's enough to only
66/// specify the names that are different between C++ and YDB.
67/// @see ydb::kStructMemberNames
68template <std::size_t N>
69struct StructMemberNames final {
70 CustomMemberName custom_names[N];
71};
72
73/// @cond
74template <>
75struct StructMemberNames<0> final {
76 impl::EmptyRange<CustomMemberName> custom_names;
77};
78/// @endcond
79
80// clang-format off
81StructMemberNames() -> StructMemberNames<0>;
82// clang-format on
83
84template <std::size_t N>
85StructMemberNames(CustomMemberName (&&)[N]) -> StructMemberNames<N>;
86
87/// @brief Customization point for YDB serialization of structs.
88///
89/// In order to get serialization for a struct, you need to define
90/// `kYdbMemberNames` inside it:
91///
92/// @snippet ydb/small_table.hpp struct sample
93///
94/// Field names can be overridden:
95///
96/// @snippet ydb/tests/struct_io_test.cpp custom names
97///
98/// To enable YDB serialization for an external struct, specialize
99/// ydb::kStructMemberNames for it:
100///
101/// @snippet ydb/tests/struct_io_test.cpp external specialization
102///
103/// @warning The struct must not reside in an anonymous namespace, otherwise
104/// struct member names detection will break.
105///
106/// For extra fields on C++ side, parsing throws ydb::ParseError.
107/// For extra fields on YDB side, parsing throws ydb::ParseError.
108template <typename T>
110
111namespace impl {
112
113template <typename T, std::size_t N>
114constexpr auto WithDeducedNames(const StructMemberNames<N>& given_names) {
115 auto names = boost::pfr::names_as_array<T>();
116 for (const CustomMemberName& entry : given_names.custom_names) {
117 std::size_t count = 0;
118 for (auto& name : names) {
119 if (name == entry.cpp_name) {
120 name = entry.ydb_name;
121 ++count;
122 break;
123 }
124 }
125 if (count != 1) {
126 throw BaseError(
127 "In a StructMemberNames, each cpp_name must match the C++ name of "
128 "exactly 1 member of struct T"
129 );
130 }
131 }
132 return names;
133}
134
135template <typename T, std::size_t... Indices>
136constexpr auto MakeTupleOfOptionals(std::index_sequence<Indices...>) {
137 return std::tuple<std::optional<boost::pfr::tuple_element_t<Indices, T>>...>();
138}
139
140template <typename T, typename Tuple, std::size_t... Indices>
141constexpr auto MakeFromTupleOfOptionals(Tuple&& tuple, std::index_sequence<Indices...>) {
142 return T{*utils::ForwardLike<Tuple>(std::get<Indices>(tuple))...};
143}
144
145template <typename T>
146concept IsStruct = !std::is_same_v<decltype(kStructMemberNames<T>), const impl::NotStruct>;
147
148} // namespace impl
149
150template <typename T>
151requires impl::IsStruct<T>
152struct ValueTraits<T> {
153 static_assert(std::is_aggregate_v<T>);
154 static constexpr auto kFieldNames = impl::WithDeducedNames<T>(kStructMemberNames<T>);
155 static constexpr auto kFieldNamesSet = utils::MakeTrivialSet<kFieldNames>();
156 static constexpr auto kFieldsCount = kFieldNames.size();
157
158 static T Parse(NYdb::TValueParser& parser, const ParseContext& context) {
159 auto parsed_fields = impl::MakeTupleOfOptionals<T>(std::make_index_sequence<kFieldsCount>{});
160 parser.OpenStruct();
161
162 while (parser.TryNextMember()) {
163 const auto& field_name = parser.GetMemberName();
164 const auto index = kFieldNamesSet.GetIndex(field_name);
165 if (!index.has_value()) {
166 throw ColumnParseError(
167 context.column_name,
168 fmt::format(
169 "Unexpected field name '{}' for '{}' struct type, "
170 "expected one of: {}",
171 field_name,
172 compiler::GetTypeName<T>(),
173 fmt::join(kFieldNames, ", ")
174 )
175 );
176 }
177 utils::WithConstexprIndex<kFieldsCount>(*index, [&](auto index_c) {
178 auto& field = std::get<decltype(index_c)::value>(parsed_fields);
179 using FieldType = typename std::decay_t<decltype(field)>::value_type;
180 field.emplace(ydb::Parse<FieldType>(parser, context));
181 });
182 }
183
184 parser.CloseStruct();
185
186 std::string_view missing_field;
187 utils::ForEachIndex<kFieldsCount>([&](auto index_c) {
188 if (!std::get<decltype(index_c)::value>(parsed_fields).has_value()) {
189 missing_field = kFieldNames[index_c];
190 }
191 });
192 if (!missing_field.empty()) {
193 throw ColumnParseError(
194 context.column_name,
195 fmt::format("Missing field '{}' for '{}' struct type", missing_field, compiler::GetTypeName<T>())
196 );
197 }
198
199 return impl::MakeFromTupleOfOptionals<T>(std::move(parsed_fields), std::make_index_sequence<kFieldsCount>{});
200 }
201
202 template <typename Builder>
203 static void Write(NYdb::TValueBuilderBase<Builder>& builder, const T& value) {
204 builder.BeginStruct();
205 boost::pfr::for_each_field(value, [&](const auto& field, std::size_t i) {
206 builder.AddMember(impl::ToString(kFieldNames[i]));
207 ydb::Write(builder, field);
208 });
209 builder.EndStruct();
210 }
211
212 static NYdb::TType MakeType() {
213 NYdb::TTypeBuilder builder;
214 builder.BeginStruct();
215 utils::ForEachIndex<kFieldsCount>([&](auto index_c) {
216 builder.AddMember(
217 impl::ToString(kFieldNames[index_c]),
218 ValueTraits<boost::pfr::tuple_element_t<decltype(index_c)::value, T>>::MakeType()
219 );
220 });
221 builder.EndStruct();
222 return builder.Build();
223 }
224};
225
226template <typename T>
227requires impl::IsStruct<T>
228struct ValueTraits<std::optional<T>> : impl::GenericOptionalValueTraits<T> {};
229
230namespace impl {
231
232template <typename T>
233struct StructRowParser final {
234 static_assert(std::is_aggregate_v<T>);
235 static constexpr auto kFieldNames = impl::WithDeducedNames<T>(kStructMemberNames<T>);
236 static constexpr auto kFieldsCount = kFieldNames.size();
237
238 static std::unique_ptr<std::size_t[]> MakeCppToYdbFieldMapping(NYdb::TResultSetParser& parser) {
239 auto result = std::make_unique<std::size_t[]>(kFieldsCount);
240 for (const auto [pos, field_name] : utils::enumerate(kFieldNames)) {
241 const auto column_index = parser.ColumnIndex(impl::ToString(field_name));
242 if (column_index == -1) {
243 throw ParseError(
244 fmt::format("Missing column '{}' for '{}' struct type", field_name, compiler::GetTypeName<T>())
245 );
246 }
247 result[pos] = static_cast<std::size_t>(column_index);
248 }
249
250 const auto columns_count = parser.ColumnsCount();
251 UASSERT(columns_count >= kFieldsCount);
252 if (columns_count != kFieldsCount) {
253 throw ParseError(fmt::format(
254 "Unexpected extra columns while parsing row to '{}' struct type",
255 compiler::GetTypeName<T>()
256 ));
257 }
258
259 return result;
260 }
261
262 static T ParseRow(NYdb::TResultSetParser& parser, const std::unique_ptr<std::size_t[]>& cpp_to_ydb_mapping) {
263 return ParseRowImpl(parser, cpp_to_ydb_mapping, std::make_index_sequence<kFieldsCount>{});
264 }
265
266 template <std::size_t... Indices>
267 static T ParseRowImpl(NYdb::TResultSetParser& parser, const std::unique_ptr<std::size_t[]>& cpp_to_ydb_mapping, std::index_sequence<Indices...>) {
268 return T{
269 Parse<boost::pfr::tuple_element_t<Indices, T>>(
270 parser.ColumnParser(cpp_to_ydb_mapping[Indices]),
271 ParseContext{.column_name = kFieldNames[Indices]}
272 )...,
273 };
274 }
275};
276
277} // namespace impl
278
279} // namespace ydb
280
281USERVER_NAMESPACE_END