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