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