userver: userver/storages/postgres/io/array_types.hpp Source File
Loading...
Searching...
No Matches
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