userver: userver/ydb/io/structs.hpp Source File
Loading...
Searching...
No Matches
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 }
111 return names;
112}
113
114template <typename T, std::size_t... Indices>
115constexpr auto MakeTupleOfOptionals(std::index_sequence<Indices...>) {
116 return std::tuple<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, std::index_sequence<Indices...>) {
121 return T{*std::get<Indices>(std::forward<Tuple>(tuple))...};
122}
123
124} // namespace impl
125
126template <typename T>
127struct ValueTraits<T, std::enable_if_t<!std::is_same_v<decltype(kStructMemberNames<T>), const impl::NotStruct>>> {
128 static_assert(std::is_aggregate_v<T>);
129 static constexpr auto kFieldNames = impl::WithDeducedNames<T>(kStructMemberNames<T>);
130 static constexpr auto kFieldNamesSet = utils::MakeTrivialSet<kFieldNames>();
131 static constexpr auto kFieldsCount = kFieldNames.size();
132
133 static T Parse(NYdb::TValueParser& parser, const ParseContext& context) {
134 auto parsed_fields = impl::MakeTupleOfOptionals<T>(std::make_index_sequence<kFieldsCount>{});
135 parser.OpenStruct();
136
137 while (parser.TryNextMember()) {
138 const auto& field_name = parser.GetMemberName();
139 const auto index = kFieldNamesSet.GetIndex(field_name);
140 if (!index.has_value()) {
141 throw ColumnParseError(
142 context.column_name,
143 fmt::format(
144 "Unexpected field name '{}' for '{}' struct type, "
145 "expected one of: {}",
146 field_name,
147 compiler::GetTypeName<T>(),
148 fmt::join(kFieldNames, ", ")
149 )
150 );
151 }
152 utils::WithConstexprIndex<kFieldsCount>(*index, [&](auto index_c) {
153 auto& field = std::get<decltype(index_c)::value>(parsed_fields);
154 using FieldType = typename std::decay_t<decltype(field)>::value_type;
155 field.emplace(ydb::Parse<FieldType>(parser, context));
156 });
157 }
158
159 parser.CloseStruct();
160
161 std::string_view missing_field;
162 utils::ForEachIndex<kFieldsCount>([&](auto index_c) {
163 if (!std::get<decltype(index_c)::value>(parsed_fields).has_value()) {
164 missing_field = kFieldNames[index_c];
165 }
166 });
167 if (!missing_field.empty()) {
168 throw ColumnParseError(
169 context.column_name,
170 fmt::format("Missing field '{}' for '{}' struct type", missing_field, compiler::GetTypeName<T>())
171 );
172 }
173
174 return impl::MakeFromTupleOfOptionals<T>(std::move(parsed_fields), std::make_index_sequence<kFieldsCount>{});
175 }
176
177 template <typename Builder>
178 static void Write(NYdb::TValueBuilderBase<Builder>& builder, const T& value) {
179 builder.BeginStruct();
180 boost::pfr::for_each_field(value, [&](const auto& field, std::size_t i) {
181 builder.AddMember(impl::ToString(kFieldNames[i]));
182 ydb::Write(builder, field);
183 });
184 builder.EndStruct();
185 }
186
187 static NYdb::TType MakeType() {
188 NYdb::TTypeBuilder builder;
189 builder.BeginStruct();
190 utils::ForEachIndex<kFieldsCount>([&](auto index_c) {
191 builder.AddMember(
192 impl::ToString(kFieldNames[index_c]),
193 ValueTraits<boost::pfr::tuple_element_t<decltype(index_c)::value, T>>::MakeType()
194 );
195 });
196 builder.EndStruct();
197 return builder.Build();
198 }
199};
200
201namespace impl {
202
203template <typename T>
204struct StructRowParser final {
205 static_assert(std::is_aggregate_v<T>);
206 static constexpr auto kFieldNames = impl::WithDeducedNames<T>(kStructMemberNames<T>);
207 static constexpr auto kFieldsCount = kFieldNames.size();
208
209 static std::unique_ptr<std::size_t[]> MakeCppToYdbFieldMapping(NYdb::TResultSetParser& parser) {
210 auto result = std::make_unique<std::size_t[]>(kFieldsCount);
211 for (const auto [pos, field_name] : utils::enumerate(kFieldNames)) {
212 const auto column_index = parser.ColumnIndex(impl::ToString(field_name));
213 if (column_index == -1) {
214 throw ParseError(
215 fmt::format("Missing column '{}' for '{}' struct type", field_name, compiler::GetTypeName<T>())
216 );
217 }
218 result[pos] = static_cast<std::size_t>(column_index);
219 }
220
221 const auto columns_count = parser.ColumnsCount();
222 UASSERT(columns_count >= kFieldsCount);
223 if (columns_count != kFieldsCount) {
224 throw ParseError(fmt::format(
225 "Unexpected extra columns while parsing row to '{}' struct type", compiler::GetTypeName<T>()
226 ));
227 }
228
229 return result;
230 }
231
232 static T ParseRow(NYdb::TResultSetParser& parser, const std::unique_ptr<std::size_t[]>& cpp_to_ydb_mapping) {
233 return ParseRowImpl(parser, cpp_to_ydb_mapping, std::make_index_sequence<kFieldsCount>{});
234 }
235
236 template <std::size_t... Indices>
237 static T
238 ParseRowImpl(NYdb::TResultSetParser& parser, const std::unique_ptr<std::size_t[]>& cpp_to_ydb_mapping, std::index_sequence<Indices...>) {
239 return T{
240 Parse<boost::pfr::tuple_element_t<Indices, T>>(
241 parser.ColumnParser(cpp_to_ydb_mapping[Indices]), ParseContext{/*column_name=*/kFieldNames[Indices]}
242 )...,
243 };
244 }
245};
246
247} // namespace impl
248
249} // namespace ydb
250
251USERVER_NAMESPACE_END