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