userver: userver/storages/postgres/io/array_types.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
array_types.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/storages/postgres/io/array_types.hpp
4/// @brief I/O support for arrays (std::array, std::set, std::unordered_set,
5/// std::vector)
6/// @ingroup userver_postgres_parse_and_format
7
8#include <array>
9#include <iterator>
10#include <set>
11#include <unordered_set>
12#include <vector>
13
14#include <boost/pfr/core.hpp>
15
16#include <userver/utils/impl/projecting_view.hpp>
17
18#include <userver/storages/postgres/exceptions.hpp>
19#include <userver/storages/postgres/io/buffer_io_base.hpp>
20#include <userver/storages/postgres/io/field_buffer.hpp>
21#include <userver/storages/postgres/io/row_types.hpp>
22#include <userver/storages/postgres/io/traits.hpp>
23#include <userver/storages/postgres/io/type_mapping.hpp>
24#include <userver/storages/postgres/io/type_traits.hpp>
25#include <userver/storages/postgres/io/user_types.hpp>
26
27USERVER_NAMESPACE_BEGIN
28
29namespace storages::postgres::io {
30
31/// @page pg_arrays uPg: Arrays
32///
33/// uPg provides support for PostgreSQL arrays when the element type is
34/// supported by the driver, including user types.
35///
36/// Array parser will throw DimensionMismatch if the dimensions of C++
37/// container do not match that of the buffer received from the server.
38///
39/// Array formatter will throw InvalidDimensions if containers on same
40/// level of depth have different sizes.
41///
42/// Example of invalid dimensions from tests:
43/// @snippet storages/postgres/tests/arrays_pgtest.cpp Invalid dimensions
44///
45/// @par Currently supported containers
46/// - std::array
47/// - std::set
48/// - std::unordered_set
49/// - std::vector
50///
51/// ----------
52///
53/// @htmlonly <div class="bottom-nav"> @endhtmlonly
54/// ⇦ @ref pg_range_types | @ref pg_bytea ⇨
55/// @htmlonly </div> @endhtmlonly
56
57namespace traits {
58
59template <typename Container>
60struct HasFixedDimensions;
61
62namespace detail {
63
64template <typename Container>
65struct HasFixedDimensionsImpl {
66 using type =
67 typename HasFixedDimensions<typename Container::value_type>::type;
68};
69
70} // namespace detail
71
72template <typename T>
73struct HasFixedDimensions
74 : std::conditional_t<kIsFixedSizeContainer<T>,
75 detail::HasFixedDimensionsImpl<T>,
76 BoolConstant<!kIsCompatibleContainer<T>>>::type {};
77
78template <typename Container>
79inline constexpr bool kHasFixedDimensions =
80 HasFixedDimensions<Container>::value;
81
82template <typename T>
83struct FixedDimensions;
84
85template <typename T>
87
88template <typename T, std::size_t N>
89struct DimensionSize<std::array<T, N>>
91
92template <typename T>
93inline constexpr std::size_t kDimensionSize = DimensionSize<T>::value;
94
95namespace detail {
96
97template <typename A, typename B>
98struct JoinSequences;
99
100template <typename T, T... U, T... V>
101struct JoinSequences<std::integer_sequence<T, U...>,
102 std::integer_sequence<T, V...>> {
103 using type = std::integer_sequence<T, U..., V...>;
104};
105
106template <typename T>
107struct FixedDimensionsImpl {
108 static_assert(kIsFixedSizeContainer<T>, "Container must have fixed size");
109 using type = typename JoinSequences<
110 std::integer_sequence<std::size_t, kDimensionSize<T>>,
111 typename FixedDimensions<typename T::value_type>::type>::type;
112};
113
114template <typename T>
115struct FixedDimensionsNonContainer {
116 using type = std::integer_sequence<std::size_t>;
117};
118
119} // namespace detail
120
121template <typename T, T... Values>
122constexpr std::array<T, sizeof...(Values)> MakeArray(
123 const std::integer_sequence<T, Values...>&) {
124 return {Values...};
125}
126
127template <typename T>
128struct FixedDimensions
129 : std::conditional_t<kIsFixedSizeContainer<T>,
130 detail::FixedDimensionsImpl<T>,
131 detail::FixedDimensionsNonContainer<T>> {};
132
133} // namespace traits
134
135namespace detail {
136
137template <typename Element>
138inline bool ForceInitElementMapping() {
139 // composite types can be parsed without an explicit mapping
140 if constexpr (io::traits::kIsMappedToPg<Element> ||
141 !io::traits::kIsCompositeType<Element>) {
142 return ForceReference(CppToPg<Element>::init_);
143 } else {
144 return true;
145 }
146}
147
148template <typename Container>
149struct ArrayBinaryParser : BufferParserBase<Container> {
150 using BaseType = BufferParserBase<Container>;
151 using ValueType = typename BaseType::ValueType;
152 using ElementType = typename traits::ContainerFinalElement<Container>::type;
153 constexpr static std::size_t dimensions = traits::kDimensionCount<Container>;
154 using Dimensions = std::array<std::size_t, dimensions>;
155 using DimensionIterator = typename Dimensions::iterator;
156 using DimensionConstIterator = typename Dimensions::const_iterator;
157 using ElementMapping = CppToPg<ElementType>;
158
159 using BaseType::BaseType;
160
161 void operator()(FieldBuffer buffer, const TypeBufferCategory& categories) {
162 using std::swap;
163
164 // read dimension count
165 Integer dim_count{0};
166 buffer.Read(dim_count, BufferCategory::kPlainBuffer);
167 if (dim_count != static_cast<Integer>(dimensions) &&
168 ForceInitElementMapping<ElementType>()) {
169 if (dim_count == 0) {
170 ValueType empty{};
171 swap(this->value, empty);
172 return;
173 }
174 throw DimensionMismatch{};
175 }
176
177 // read flags
178 Integer flags{0};
179 buffer.Read(flags, BufferCategory::kPlainBuffer);
180 // TODO check flags
181
182 // read element oid
183 Integer elem_oid{0};
184 buffer.Read(elem_oid, BufferCategory::kPlainBuffer);
185 // TODO check elem_oid
186 auto elem_category = GetTypeBufferCategory(categories, elem_oid);
187
188 // read dimension data
189 Dimensions on_the_wire;
190 for (auto& dim : on_the_wire) {
191 Integer dim_val = 0;
192 buffer.Read(dim_val, BufferCategory::kPlainBuffer);
193 dim = dim_val;
194
195 Integer lbound = 0;
196 buffer.Read(lbound, BufferCategory::kPlainBuffer);
197 }
198 if (!CheckDimensions(on_the_wire)) {
199 throw DimensionMismatch{};
200 }
201 // read elements
202 ValueType tmp;
203 ReadDimension(buffer, on_the_wire.begin(), elem_category, categories, tmp);
204 swap(this->value, tmp);
205 }
206
207 private:
208 bool CheckDimensions(const Dimensions& dims) const {
209 if constexpr (traits::kHasFixedDimensions<Container>) {
210 return dims == traits::MakeArray(
211 typename traits::FixedDimensions<Container>::type{});
212 }
213 return CheckDimensions<ValueType>(dims.begin());
214 }
215 template <typename Element>
216 bool CheckDimensions([[maybe_unused]] DimensionConstIterator dim) const {
217 if constexpr (traits::kIsFixedSizeContainer<Element>) {
218 // check subdimensions
219 if (*dim != traits::kDimensionSize<Element>) {
220 return false;
221 }
222 if constexpr (traits::kDimensionCount<Element> == 1) {
223 return true;
224 } else {
225 return CheckDimensions<typename Element::value_type>(dim + 1);
226 }
227 } else if constexpr (traits::kIsCompatibleContainer<Element>) {
228 if constexpr (traits::kDimensionCount<Element> == 1) {
229 return true;
230 } else {
231 return CheckDimensions<typename Element::value_type>(dim + 1);
232 }
233 }
234 return true;
235 }
236
237 template <typename T>
238 auto GetInserter(T& value) {
239 return std::inserter(value, value.end());
240 }
241
242 template <typename T, std::size_t n>
243 auto GetInserter(std::array<T, n>& array) {
244 return array.begin();
245 }
246
247 template <typename Element>
248 void ReadDimension(FieldBuffer& buffer, DimensionConstIterator dim,
249 BufferCategory elem_category,
250 const TypeBufferCategory& categories, Element& elem) {
251 if constexpr (traits::kIsCompatibleContainer<Element>) {
252 if constexpr (traits::kCanClear<Element>) {
253 elem.clear();
254 }
255 if constexpr (traits::kCanReserve<Element>) {
256 elem.reserve(*dim);
257 }
258 auto it = GetInserter(elem);
259 for (std::size_t i = 0; i < *dim; ++i) {
260 typename Element::value_type val;
261 if constexpr (1 < traits::kDimensionCount<Element>) {
262 // read subdimensions
263 ReadDimension(buffer, dim + 1, elem_category, categories, val);
264 } else {
265 buffer.ReadRaw(val, categories, elem_category);
266 }
267 *it++ = std::move(val);
268 }
269 }
270 }
271
272 void ReadDimension(FieldBuffer& buffer, DimensionConstIterator dim,
273 BufferCategory elem_category,
274 const TypeBufferCategory& categories,
275 std::vector<bool>& elem) {
276 elem.resize(*dim);
277 // NOLINTNEXTLINE(readability-qualified-auto)
278 auto value = elem.begin();
279 for (std::size_t i = 0; i < *dim; ++i) {
280 bool val{false};
281 buffer.ReadRaw(val, categories, elem_category);
282 *value++ = val;
283 }
284 }
285};
286
287template <typename Container>
288struct ArrayBinaryFormatter : BufferFormatterBase<Container> {
289 using BaseType = BufferFormatterBase<Container>;
290 using ValueType = typename BaseType::ValueType;
291 using ArrayMapping = CppToPg<Container>;
292 using ElementType = typename traits::ContainerFinalElement<Container>::type;
293 using ElementMapping = CppToPg<ElementType>;
294 constexpr static std::size_t dimensions = traits::kDimensionCount<Container>;
295 using Dimensions = std::array<std::size_t, dimensions>;
296 using DimensionIterator = typename Dimensions::iterator;
297 using DimensionConstIterator = typename Dimensions::const_iterator;
298
299 using BaseType::BaseType;
300
301 // top level container
302 template <typename Buffer>
303 void operator()(const UserTypes& types, Buffer& buffer,
304 Oid replace_oid = kInvalidOid) const {
305 auto elem_type_oid = ElementMapping::GetOid(types);
306 if (replace_oid != kInvalidOid &&
307 replace_oid != ArrayMapping::GetOid(types)) {
308 elem_type_oid = types.FindElementOid(replace_oid);
309 }
310
311 // Fast path for default-constructed vectors
312 if (this->value.empty()) {
313 io::WriteBuffer(types, buffer, static_cast<Integer>(0)); // dims
314 io::WriteBuffer(types, buffer, static_cast<Integer>(0)); // flags
315 io::WriteBuffer(types, buffer, static_cast<Integer>(elem_type_oid));
316 return;
317 }
318
319 // Write number of dimensions
320 io::WriteBuffer(types, buffer, static_cast<Integer>(dimensions));
321 // Write flags
322 io::WriteBuffer(types, buffer, static_cast<Integer>(0));
323 // Write element type oid
324 io::WriteBuffer(types, buffer, static_cast<Integer>(elem_type_oid));
325 Dimensions dims = GetDimensions();
326 // Write data per dimension
327 WriteDimensionData(types, buffer, dims);
328 // Write flat elements
329 WriteData(types, dims.begin(), buffer, this->value);
330 }
331
332 private:
333 template <typename Element>
334 void CalculateDimensions([[maybe_unused]] DimensionIterator dim,
335 const Element& element) const {
336 if constexpr (traits::kIsCompatibleContainer<Element>) {
337 *dim = element.size();
338 if (!element.empty()) {
339 CalculateDimensions(dim + 1, *element.begin());
340 } // TODO else logic error?
341 }
342 }
343 Dimensions GetDimensions() const {
344 if constexpr (traits::kHasFixedDimensions<Container>) {
345 return traits::MakeArray(
346 typename traits::FixedDimensions<Container>::type{});
347 } else {
348 Dimensions dims{};
349 CalculateDimensions(dims.begin(), this->value);
350 return dims;
351 }
352 }
353 template <typename Buffer>
354 void WriteDimensionData(const UserTypes& types, Buffer& buffer,
355 const Dimensions& dims) const {
356 for (auto dim : dims) {
357 io::WriteBuffer(types, buffer, static_cast<Integer>(dim));
358 io::WriteBuffer(types, buffer, static_cast<Integer>(1)); // lbound
359 }
360 }
361
362 template <typename Buffer, typename Element>
363 void WriteData(const UserTypes& types, DimensionConstIterator dim,
364 Buffer& buffer, const Element& element) const {
365 if (*dim != element.size()) {
366 throw InvalidDimensions{*dim, element.size()};
367 }
368 if constexpr (1 < traits::kDimensionCount<Element>) {
369 // this is a (sub)dimension of array
370 for (const auto& sub : element) {
371 WriteData(types, dim + 1, buffer, sub);
372 }
373 } else {
374 // this is the final dimension
375 for (const auto& sub : element) {
376 io::WriteRawBinary(types, buffer, sub);
377 }
378 }
379 }
380
381 template <typename Buffer>
382 void WriteData(const UserTypes& types, DimensionConstIterator dim,
383 Buffer& buffer, const std::vector<bool>& element) const {
384 if (*dim != element.size()) {
385 throw InvalidDimensions{*dim, element.size()};
386 }
387 for (bool sub : element) {
388 io::WriteRawBinary(types, buffer, sub);
389 }
390 }
391};
392
393template <typename Container, bool System>
394struct ArrayPgOid {
395 using Type = Container;
396 using ElementType = typename traits::ContainerFinalElement<Container>::type;
397 using ElementMapping = CppToPg<ElementType>;
398 using Mapping = ArrayPgOid<Container, System>;
399
400 static Oid GetOid(const UserTypes& types) {
401 return ElementMapping::GetArrayOid(types);
402 }
403};
404
405template <typename Container>
406struct ArrayPgOid<Container, true> {
407 using Type = Container;
408 using ElementType = typename traits::ContainerFinalElement<Container>::type;
409 using ElementMapping = CppToPg<ElementType>;
410 using Mapping = ArrayPgOid<Container, true>;
411
412 static constexpr Oid GetOid(const UserTypes&) {
413 return static_cast<Oid>(ElementMapping::array_oid);
414 }
415};
416
417template <typename Container>
418constexpr bool IsElementMappedToSystem() {
419 if constexpr (!traits::kIsCompatibleContainer<Container>) {
420 return false;
421 } else {
422 return IsTypeMappedToSystem<
423 typename traits::ContainerFinalElement<Container>::type>();
424 }
425}
426
427template <typename Container>
428constexpr bool EnableArrayParser() {
429 if constexpr (!traits::kIsCompatibleContainer<Container>) {
430 return false;
431 } else {
432 using ElementType = typename traits::ContainerFinalElement<Container>::type;
433 return traits::kHasParser<ElementType>;
434 }
435}
436template <typename Container>
437inline constexpr bool kEnableArrayParser = EnableArrayParser<Container>();
438
439template <typename Container>
440constexpr bool EnableArrayFormatter() {
441 if constexpr (!traits::kIsCompatibleContainer<Container>) {
442 return false;
443 } else {
444 using ElementType = typename traits::ContainerFinalElement<Container>::type;
445 return traits::kHasFormatter<ElementType>;
446 }
447}
448template <typename Container>
449inline constexpr bool kEnableArrayFormatter = EnableArrayFormatter<Container>();
450
451} // namespace detail
452
453template <typename T>
456
457template <typename T>
458constexpr bool IsTypeMappedToSystemArray() {
459 return traits::kIsMappedToPg<T> &&
460 std::is_same<
461 typename CppToPg<T>::Mapping,
462 io::detail::ArrayPgOid<typename CppToPg<T>::Type, true>>::value;
463}
464
465namespace traits {
466
467template <typename T>
470 using type = io::detail::ArrayBinaryParser<T>;
471};
472
473template <typename T>
477};
478
479template <typename T>
480struct ParserBufferCategory<io::detail::ArrayBinaryParser<T>>
482
483// std::vector
484template <typename... T>
485struct IsCompatibleContainer<std::vector<T...>> : std::true_type {};
486
487// std::array
488template <typename T, std::size_t Size>
489struct IsCompatibleContainer<std::array<T, Size>> : std::true_type {};
490
491// std::set
492template <typename... T>
493struct IsCompatibleContainer<std::set<T...>> : std::true_type {};
494
495// std::unordered_set
496template <typename... T>
497struct IsCompatibleContainer<std::unordered_set<T...>> : std::true_type {};
498
499// TODO Add more containers
500
501} // namespace traits
502
503namespace detail {
504
505/// A helper data type to write a container chunk to postgresql array buffer
506/// Mimics container interface (type aliases + begin/end)
507template <typename Container>
508class ContainerChunk {
509 public:
510 static_assert(
511 traits::IsCompatibleContainer<Container>{},
512 "Only containers explicitly declared as compatible are supported");
513
514 using value_type = typename Container::value_type;
515 using const_iterator_type = typename Container::const_iterator;
516
517 ContainerChunk(const_iterator_type begin, std::size_t size)
518 : begin_{begin}, end_{std::next(begin, size)}, size_{size} {}
519
520 std::size_t size() const { return size_; }
521 bool empty() const { return begin_ == end_; }
522
523 const_iterator_type begin() const { return begin_; }
524 const_iterator_type cbegin() const { return begin_; }
525
526 const_iterator_type end() const { return end_; }
527 const_iterator_type cend() const { return end_; }
528
529 private:
530 const_iterator_type begin_;
531 const_iterator_type end_;
532 std::size_t size_;
533};
534
535/// Utility class to iterate chunks of input array
536template <typename Container>
537class ContainerSplitter {
538 public:
539 static_assert(
540 traits::IsCompatibleContainer<Container>{},
541 "Only containers explicitly declared as compatible are supported");
542
543 using value_type = ContainerChunk<Container>;
544
545 class ChunkIterator {
546 public:
547 using UnderlyingIterator = typename Container::const_iterator;
548 ChunkIterator(const Container& container, UnderlyingIterator current,
549 std::size_t chunk_elements)
550 : container_{container},
551 chunk_size_{chunk_elements},
552 tail_size_{
553 static_cast<size_t>(std::distance(current, container_.end()))},
554 current_{current} {}
555
556 bool operator==(const ChunkIterator& rhs) const {
557 return current_ == rhs.current_;
558 }
559
560 bool operator!=(const ChunkIterator& rhs) const { return !(*this == rhs); }
561
562 value_type operator*() const { return {current_, NextStep()}; }
563
564 ChunkIterator& operator++() {
565 auto step = NextStep();
566 std::advance(current_, step);
567 tail_size_ -= step;
568 return *this;
569 }
570
571 ChunkIterator operator++(int) {
572 ChunkIterator tmp{*this};
573 ++(*this);
574 return tmp;
575 }
576
577 std::size_t TailSize() const { return tail_size_; }
578
579 private:
580 std::size_t NextStep() const { return std::min(chunk_size_, tail_size_); }
581
582 const Container& container_;
583 const std::size_t chunk_size_;
584 std::size_t tail_size_;
585 UnderlyingIterator current_;
586 };
587
588 ContainerSplitter(const Container& container, std::size_t chunk_elements)
589 : container_{container}, chunk_size_{chunk_elements} {}
590
591 std::size_t size() const {
592 auto sz = container_.size();
593 return sz / chunk_size_ + (sz % chunk_size_ ? 1 : 0);
594 }
595 bool empty() const { return container_.empty(); }
596
597 ChunkIterator begin() const {
598 return {container_, container_.begin(), chunk_size_};
599 }
600
601 ChunkIterator end() const {
602 return {container_, container_.end(), chunk_size_};
603 }
604
605 std::size_t ChunkSize() const { return chunk_size_; }
606 const Container& GetContainer() const { return container_; }
607
608 private:
609 const Container& container_;
610 const std::size_t chunk_size_;
611};
612
613template <typename Container,
614 typename Seq = std::make_index_sequence<
615 boost::pfr::tuple_size_v<typename Container::value_type>>>
616struct ColumnsSplitterHelper;
617
618/// Utility helper to iterate chunks of input array in column-wise way
619template <typename Container, std::size_t... Indexes>
620struct ColumnsSplitterHelper<Container, std::index_sequence<Indexes...>> final {
621 static_assert(sizeof...(Indexes) > 0,
622 "The aggregate having 0 fields doesn't make sense");
623
624 template <std::size_t Index>
625 struct FieldProjection {
626 using RowType = typename Container::value_type;
627 using FieldType =
628 boost::pfr::tuple_element_t<Index, typename Container::value_type>;
629
630 const FieldType& operator()(const RowType& value) const noexcept {
631 return boost::pfr::get<Index>(value);
632 }
633 };
634
635 template <std::size_t Index>
636 using FieldView =
637 USERVER_NAMESPACE::utils::impl::ProjectingView<const Container,
638 FieldProjection<Index>>;
639
640 template <typename Fn>
641 static void Perform(const Container& container, std::size_t chunk_elements,
642 const Fn& fn) {
643 DoSplitByChunks(chunk_elements, fn, FieldView<Indexes>{container}...);
644 }
645
646 private:
647 template <typename Fn, typename... Views>
648 static void DoSplitByChunks(std::size_t chunk_elements, const Fn& fn,
649 const Views&... views) {
650 DoIterateByChunks(fn, ContainerSplitter{views, chunk_elements}.begin()...);
651 }
652
653 template <typename Fn, typename FirstChunkIterator,
654 typename... ChunkIterators>
655 static void DoIterateByChunks(const Fn& fn, FirstChunkIterator first,
656 ChunkIterators... chunks) {
657 while (first.TailSize() > 0) {
658 fn(*first, *chunks...);
659
660 ++first;
661 (++chunks, ...);
662 }
663 }
664};
665
666/// Utility class to iterate chunks of input array in column-wise way
667template <typename Container>
668class ContainerByColumnsSplitter final {
669 public:
670 ContainerByColumnsSplitter(const Container& container,
671 std::size_t chunk_elements)
672 : container_{container}, chunk_elements_{chunk_elements} {}
673
674 template <typename Fn>
675 void Perform(const Fn& fn) {
676 ColumnsSplitterHelper<Container>::Perform(container_, chunk_elements_, fn);
677 }
678
679 private:
680 const Container& container_;
681 const std::size_t chunk_elements_;
682};
683
684} // namespace detail
685
686namespace traits {
687template <typename Container>
688struct IsCompatibleContainer<io::detail::ContainerChunk<Container>>
689 : IsCompatibleContainer<Container> {};
690
691template <typename Container, typename Projection>
693 USERVER_NAMESPACE::utils::impl::ProjectingView<const Container, Projection>>
694 : IsCompatibleContainer<Container> {};
695} // namespace traits
696
697template <typename Container>
698detail::ContainerSplitter<Container> SplitContainer(
699 const Container& container, std::size_t chunk_elements) {
700 return {container, chunk_elements};
701}
702
703template <typename Container>
704detail::ContainerByColumnsSplitter<Container> SplitContainerByColumns(
705 const Container& container, std::size_t chunk_elements) {
706 return {container, chunk_elements};
707}
708
709} // namespace storages::postgres::io
710
711USERVER_NAMESPACE_END