Lightweight 0.20261031.0
Loading...
Searching...
No Matches
HasMany.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../DataBinder/Core.hpp"
6#include "../DataBinder/SqlNullValue.hpp"
7#include "../SqlStatement.hpp"
8#include "BelongsTo.hpp"
9#include "Error.hpp"
10#include "Field.hpp"
11#include "Record.hpp"
12
13#include <reflection-cpp/reflection.hpp>
14
15#include <compare>
16#include <functional>
17#include <memory>
18#include <optional>
19#include <type_traits>
20#include <utility>
21#include <vector>
22
23namespace Lightweight
24{
25
26/// @brief This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
27///
28/// The HasMany<OtherRecord> is a member of the "one" side of the relationship.
29///
30/// `OtherRecord` must declare a `BelongsTo` member that points back to this "one" side. That member is
31/// located by matching the relationship *type*, not by its position in either record, so the two
32/// relationship members may be declared at any index. Declaring no such `BelongsTo` is a compile-time
33/// error.
34///
35/// When `OtherRecord` holds more than one foreign key into this record's table - say a meeting that
36/// references the same person table both as its organizer and as whoever writes the minutes - the
37/// inverse is ambiguous. Name the foreign key column through @p TheInverseSelector to single one out:
38///
39/// @code
40/// struct Meeting;
41/// struct Human
42/// {
43/// Field<int, PrimaryKey::AutoAssign> id;
44/// HasMany<Meeting, SqlRealName { "organizer_id" }> organizedMeetings;
45/// HasMany<Meeting, SqlRealName { "minute_taker_id" }> minutedMeetings;
46/// };
47/// struct Meeting
48/// {
49/// Field<int, PrimaryKey::AutoAssign> id;
50/// BelongsTo<&Human::id, SqlRealName { "organizer_id" }> organizer;
51/// BelongsTo<&Human::id, SqlRealName { "minute_taker_id" }, SqlNullable::Null> minuteTaker;
52/// };
53/// @endcode
54///
55/// A meeting with *many* attendees is a many-to-many instead - see `HasManyThrough`. The worked
56/// example in `docs/sql-to-lightweight.md` combines both shapes.
57///
58/// @tparam OtherRecord The record type on the "many" side of the relationship.
59/// @tparam TheInverseSelector Singles out one of several foreign keys, see the RelationSelector concept.
60///
61/// @see InverseBelongsToIndexOf, RelationSelector
62///
63/// @see DataMapper, Field, HasManyThrough
64/// @ingroup DataMapper
65template <typename OtherRecord, auto TheInverseSelector = AutoDetectRelation>
67{
69 "The second template argument of HasMany must be a foreign key column name (a SqlRealName) "
70 "or std::nullopt to resolve the relationship automatically.");
71
72 public:
73 /// The record type of the "many" side of the relationship.
74 using ReferencedRecord = OtherRecord;
75
76 /// Singles out the foreign key of `OtherRecord` that backs this relationship.
77 static constexpr auto InverseSelector = TheInverseSelector;
78
79 /// The list of records on the "many" side of the relationship.
80 using ReferencedRecordList = std::vector<std::shared_ptr<OtherRecord>>;
81
82 /// Record type of the "many" side of the relationship.
83 using value_type = OtherRecord;
84
85 /// Iterator type for the list of records.
86 using iterator = ReferencedRecordList::iterator;
87
88 /// Const iterator type for the list of records.
89 using const_iterator = ReferencedRecordList::const_iterator;
90
91 /// @brief Retrieves the records, loading them on first access.
92 ///
93 /// Never throws for unavailable records: the reason comes back as the error instead.
94 ///
95 /// @return The records; or @ref RelationError::NotConfigured when there is no loader (a hand-built
96 /// record, or one read with `loadRelations = false`), @ref RelationError::Outdated when the
97 /// default connection string changed since the record was read, @ref RelationError::QueryFailed
98 /// when the load query failed.
100
101 /// @copydoc All() const
103
104 /// @brief Calls @p callable for each record, without holding them all in memory when not loaded yet.
105 ///
106 /// Use this to iterate over all records when the full data set would be too large to keep.
107 /// An exception thrown by @p callable propagates unchanged.
108 ///
109 /// @param callable Called once per record.
110 /// @return Nothing, or why the records are unavailable (see @ref All()).
111 template <typename Callable>
112 RelationResult<void> Each(Callable const& callable);
113
114 /// Emplaces the given list of records.
116
117 /// @brief Returns the already-loaded records, or `nullptr` when the relation is not loaded.
118 ///
119 /// Unlike `All()`, this never runs the on-demand loader: it reports what is present right now,
120 /// which is what lets the batched relation loading walk one level deeper (`With<A, B>()`).
121 ///
122 /// @return Pointer to the loaded list, or `nullptr` if the relation was never loaded.
123 [[nodiscard]] ReferencedRecordList* LoadedRecords() noexcept
124 {
125 return _records ? &*_records : nullptr;
126 }
127
128 /// @return The number of records in this 1-to-many relationship - counted by a query, without
129 /// loading them, unless they are loaded already - or why it is unavailable (see @ref All()).
130 [[nodiscard]] RelationResult<std::size_t> Count() const;
131
132 /// @return Whether this 1-to-many relationship is empty, or why that is unknown (see @ref All()).
133 [[nodiscard]] RelationResult<bool> IsEmpty() const;
134
135 /// @brief Retrieves the record at the given index, loading the records on first access.
136 ///
137 /// @param index The index of the record to retrieve.
138 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
139 /// @throws std::out_of_range @p index is out of bounds.
140 [[nodiscard]] OtherRecord const& At(std::size_t index) const;
141
142 /// @copydoc At(std::size_t) const
143 [[nodiscard]] OtherRecord& At(std::size_t index);
144
145 /// @brief Retrieves the record at the given index, loading the records on first access.
146 ///
147 /// @param index The index of the record to retrieve; out of bounds is undefined behaviour.
148 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
149 [[nodiscard]] OtherRecord const& operator[](std::size_t index) const;
150
151 /// @copydoc operator[](std::size_t) const
152 [[nodiscard]] OtherRecord& operator[](std::size_t index);
153
154 /// Returns an iterator to the beginning of the record list, loading the records on first access.
155 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
156 [[nodiscard]] iterator begin();
157 /// Returns an iterator to the end of the record list, loading the records on first access.
158 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
159 [[nodiscard]] iterator end();
160 /// Returns a const iterator to the beginning of the record list, loading the records on first access.
161 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
162 [[nodiscard]] const_iterator begin() const;
163 /// Returns a const iterator to the end of the record list, loading the records on first access.
164 /// @throws SqlRequireLoadedError The records are unavailable; @ref All() reports why without throwing.
165 [[nodiscard]] const_iterator end() const;
166
167 /// Three-way comparison operator.
168 constexpr std::weak_ordering operator<=>(HasMany const& other) const noexcept = default;
169 /// Equality comparison operator.
170 constexpr bool operator==(HasMany const& other) const noexcept = default;
171 /// Inequality comparison operator.
172 constexpr bool operator!=(HasMany const& other) const noexcept = default;
173
174 /// Carries the deferred loads, installed by the DataMapper.
175 struct Loader
176 {
177 /// Counts the records without loading them.
178 std::function<RelationResult<size_t>()> count {};
179 /// Loads all records.
180 std::function<RelationResult<ReferencedRecordList>()> all {};
181 /// Streams the records to a callback.
182 std::function<RelationResult<void>(std::function<void(ReferencedRecord const&)>)> each {};
183
184 /// Loaders carry no comparable state of their own, so any two are considered equivalent.
185 std::weak_ordering operator<=>(Loader const& /*other*/) const noexcept
186 {
187 return std::weak_ordering::equivalent; // Loader is not comparable, so we return equivalent
188 }
189 };
190
191 /// Used internally to configure on-demand loading of the records.
192 void SetAutoLoader(Loader loader) noexcept;
193
194 private:
195 /// Loads the records unless they already are, or their unavailability is already known.
196 ///
197 /// @ref RelationError::Outdated is remembered - it does not change by asking again - until records
198 /// are emplaced; a failed query is retried on the next access.
199 [[nodiscard]] RelationResult<void> Load() const;
200
201 /// @return The loaded records, or throws SqlRequireLoadedError.
202 [[nodiscard]] ReferencedRecordList& LoadOrThrow() const;
203
204 Loader _loader;
205 mutable std::optional<ReferencedRecordList> _records;
206 mutable std::optional<size_t> _count;
207 mutable std::optional<RelationError> _loadError;
208};
209
210namespace detail
211{
212 template <typename T>
213 struct IsHasManyType: std::false_type
214 {
215 };
216
217 template <typename OtherRecord, auto InverseSelector>
218 struct IsHasManyType<HasMany<OtherRecord, InverseSelector>>: std::true_type
219 {
220 };
221
222} // namespace detail
223
224template <typename T>
225constexpr bool IsHasMany = detail::IsHasManyType<std::remove_cvref_t<T>>::value;
226
227template <typename OtherRecord, auto InverseSelector>
228inline LIGHTWEIGHT_FORCE_INLINE void HasMany<OtherRecord, InverseSelector>::SetAutoLoader(Loader loader) noexcept
229{
230 _loader = std::move(loader);
231}
232
233template <typename OtherRecord, auto InverseSelector>
234inline LIGHTWEIGHT_FORCE_INLINE RelationResult<void> HasMany<OtherRecord, InverseSelector>::Load() const
235{
236 if (_records)
237 return {};
238 if (_loadError)
239 return std::unexpected { *_loadError };
240
241 // The loader is only populated by ConfigureRelationAutoLoading(). A hand-constructed record
242 // never went through it, so calling the empty std::function would be std::bad_function_call.
243 if (!_loader.all)
244 return std::unexpected { RelationError::NotConfigured };
245
246 auto loaded = _loader.all();
247 if (!loaded)
248 {
249 if (loaded.error() != RelationError::QueryFailed)
250 _loadError = loaded.error();
251 return std::unexpected { loaded.error() };
252 }
253 _records = std::move(*loaded);
254 return {};
255}
256
257template <typename OtherRecord, auto InverseSelector>
258inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::ReferencedRecordList& HasMany<
259 OtherRecord,
260 InverseSelector>::LoadOrThrow() const
261{
262 if (auto const loaded = Load(); !loaded)
263 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>, loaded.error());
264 return *_records; // NOLINT(bugprone-unchecked-optional-access)
265}
266
267template <typename OtherRecord, auto InverseSelector>
268inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::ReferencedRecordList& HasMany<
269 OtherRecord,
270 InverseSelector>::Emplace(ReferencedRecordList&& records) noexcept
271{
272 _records = { std::move(records) };
273 _loadError.reset();
274 return *_records;
275}
276
277template <typename OtherRecord, auto InverseSelector>
278inline LIGHTWEIGHT_FORCE_INLINE auto HasMany<OtherRecord, InverseSelector>::All()
280{
281 return Load().transform([this] { return std::reference_wrapper<ReferencedRecordList> { *_records }; });
282}
283
284template <typename OtherRecord, auto InverseSelector>
285inline LIGHTWEIGHT_FORCE_INLINE auto HasMany<OtherRecord, InverseSelector>::All() const
286 -> RelationResult<std::reference_wrapper<ReferencedRecordList const>>
287{
288 return Load().transform([this] { return std::reference_wrapper<ReferencedRecordList const> { *_records }; });
289}
290
291template <typename OtherRecord, auto InverseSelector>
292template <typename Callable>
294{
295 if (!_records && !_loadError && _loader.each)
296 {
297 auto streamed = _loader.each(callable);
298 if (!streamed && streamed.error() != RelationError::QueryFailed)
299 _loadError = streamed.error();
300 return streamed;
301 }
302
303 return All().transform([&callable](ReferencedRecordList const& records) {
304 for (auto const& record: records)
305 callable(*record);
306 });
307}
308
309template <typename OtherRecord, auto InverseSelector>
311{
312 if (_records)
313 return _records->size();
314 if (_count)
315 return *_count;
316 if (_loadError)
317 return std::unexpected { *_loadError };
318 if (!_loader.count)
319 return std::unexpected { RelationError::NotConfigured };
320
321 auto counted = _loader.count();
322 if (counted)
323 _count = *counted;
324 else if (counted.error() != RelationError::QueryFailed)
325 _loadError = counted.error();
326 return counted;
327}
328
329template <typename OtherRecord, auto InverseSelector>
331{
332 return Count().transform([](std::size_t count) { return count == 0; });
333}
334
335template <typename OtherRecord, auto InverseSelector>
336inline LIGHTWEIGHT_FORCE_INLINE OtherRecord const& HasMany<OtherRecord, InverseSelector>::At(std::size_t index) const
337{
338 return *LoadOrThrow().at(index);
339}
340
341template <typename OtherRecord, auto InverseSelector>
342inline LIGHTWEIGHT_FORCE_INLINE OtherRecord& HasMany<OtherRecord, InverseSelector>::At(std::size_t index)
343{
344 return *LoadOrThrow().at(index);
345}
346
347template <typename OtherRecord, auto InverseSelector>
348inline LIGHTWEIGHT_FORCE_INLINE OtherRecord const& HasMany<OtherRecord, InverseSelector>::operator[](std::size_t index) const
349{
350 return *LoadOrThrow()[index];
351}
352
353template <typename OtherRecord, auto InverseSelector>
354inline LIGHTWEIGHT_FORCE_INLINE OtherRecord& HasMany<OtherRecord, InverseSelector>::operator[](std::size_t index)
355{
356 return *LoadOrThrow()[index];
357}
358
359template <typename OtherRecord, auto InverseSelector>
360inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::iterator HasMany<OtherRecord,
361 InverseSelector>::begin()
362{
363 return LoadOrThrow().begin();
364}
365
366template <typename OtherRecord, auto InverseSelector>
368{
369 return LoadOrThrow().end();
370}
371
372template <typename OtherRecord, auto InverseSelector>
373inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::const_iterator HasMany<OtherRecord,
374 InverseSelector>::begin() const
375{
376 return std::as_const(LoadOrThrow()).begin();
377}
378
379template <typename OtherRecord, auto InverseSelector>
380inline LIGHTWEIGHT_FORCE_INLINE HasMany<OtherRecord, InverseSelector>::const_iterator HasMany<OtherRecord,
381 InverseSelector>::end() const
382{
383 return std::as_const(LoadOrThrow()).end();
384}
385
386} // namespace Lightweight
This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
Definition HasMany.hpp:67
OtherRecord const & At(std::size_t index) const
Retrieves the record at the given index, loading the records on first access.
Definition HasMany.hpp:336
OtherRecord & operator[](std::size_t index)
Retrieves the record at the given index, loading the records on first access.
Definition HasMany.hpp:354
ReferencedRecordList::iterator iterator
Iterator type for the list of records.
Definition HasMany.hpp:86
constexpr std::weak_ordering operator<=>(HasMany const &other) const noexcept=default
Three-way comparison operator.
ReferencedRecordList::const_iterator const_iterator
Const iterator type for the list of records.
Definition HasMany.hpp:89
ReferencedRecordList * LoadedRecords() noexcept
Returns the already-loaded records, or nullptr when the relation is not loaded.
Definition HasMany.hpp:123
OtherRecord & At(std::size_t index)
Retrieves the record at the given index, loading the records on first access.
Definition HasMany.hpp:342
OtherRecord ReferencedRecord
The record type of the "many" side of the relationship.
Definition HasMany.hpp:74
constexpr bool operator!=(HasMany const &other) const noexcept=default
Inequality comparison operator.
constexpr bool operator==(HasMany const &other) const noexcept=default
Equality comparison operator.
RelationResult< std::reference_wrapper< ReferencedRecordList const > > All() const
Retrieves the records, loading them on first access.
Definition HasMany.hpp:285
OtherRecord const & operator[](std::size_t index) const
Retrieves the record at the given index, loading the records on first access.
Definition HasMany.hpp:348
const_iterator begin() const
Definition HasMany.hpp:374
OtherRecord value_type
Record type of the "many" side of the relationship.
Definition HasMany.hpp:83
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
Definition HasMany.hpp:228
RelationResult< void > Each(Callable const &callable)
Calls callable for each record, without holding them all in memory when not loaded yet.
Definition HasMany.hpp:293
static constexpr auto InverseSelector
Singles out the foreign key of OtherRecord that backs this relationship.
Definition HasMany.hpp:77
RelationResult< std::reference_wrapper< ReferencedRecordList > > All()
Retrieves the records, loading them on first access.
Definition HasMany.hpp:278
std::vector< std::shared_ptr< OtherRecord > > ReferencedRecordList
The list of records on the "many" side of the relationship.
Definition HasMany.hpp:80
const_iterator end() const
Definition HasMany.hpp:381
RelationResult< bool > IsEmpty() const
Definition HasMany.hpp:330
RelationResult< std::size_t > Count() const
Definition HasMany.hpp:310
ReferencedRecordList & Emplace(ReferencedRecordList &&records) noexcept
Emplaces the given list of records.
Definition HasMany.hpp:270
Constrains what may be used to single out one of several foreign keys into the same table.
Definition Record.hpp:153
@ Count
Number of enumerators; not an operation itself.
std::expected< T, RelationError > RelationResult
The outcome of a relation access: the requested value, or why it is unavailable.
Definition Error.hpp:44
Carries the deferred loads, installed by the DataMapper.
Definition HasMany.hpp:176
std::weak_ordering operator<=>(Loader const &) const noexcept
Loaders carry no comparable state of their own, so any two are considered equivalent.
Definition HasMany.hpp:185
std::function< RelationResult< void >(std::function< void(ReferencedRecord const &)>)> each
Streams the records to a callback.
Definition HasMany.hpp:182
std::function< RelationResult< ReferencedRecordList >()> all
Loads all records.
Definition HasMany.hpp:180
std::function< RelationResult< size_t >()> count
Counts the records without loading them.
Definition HasMany.hpp:178