Lightweight 0.20260921.0
Loading...
Searching...
No Matches
CompositeForeignKey.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "../SqlStatement.hpp"
6#include "../Utils.hpp"
7#include "Error.hpp"
8#include "Record.hpp"
9
10#include <array>
11#include <concepts>
12#include <cstddef>
13#include <functional>
14#include <memory>
15#include <optional>
16#include <ranges>
17#include <tuple>
18#include <type_traits>
19#include <utility>
20
21namespace Lightweight
22{
23
24namespace detail
25{
26 /// Extracts the member type from a pointer-to-member, without requiring the owning class to be
27 /// complete. `decltype(std::declval<Owner const&>().*Ptr)` would require completeness, which a
28 /// relation declared *inside* its own record cannot offer.
29 template <typename T>
30 struct MemberPointeeType;
31
32 template <typename Member, typename Owner>
33 struct MemberPointeeType<Member Owner::*>
34 {
35 using type = Member;
36 };
37} // namespace detail
38
39/// @brief One column pair of a composite foreign key: "this record's column references that one".
40///
41/// Both endpoints are pointers-to-member, so the pairing is part of the type. That is the whole point:
42/// with two parallel column *lists* the pairing would be implicit in position, and transposing two
43/// same-typed columns would be silently wrong. Here a transposition is a different `Connection`, and
44/// where the paired columns differ in type it does not even compile.
45///
46/// @tparam FromPtr Pointer to the member of *this* record holding part of the foreign key.
47/// @tparam IntoPtr Pointer to the member of the referenced record it points at, which must be a
48/// primary key there.
49///
50/// @ingroup DataMapper
51///
52/// @code
53/// CompositeForeignKey<Connection<&Child::refA, &Parent::partA>,
54/// Connection<&Child::refB, &Parent::partB>> parent;
55/// @endcode
56template <auto FromPtr, auto IntoPtr>
58{
59 /// Pointer to this record's foreign key member.
60 static constexpr auto From = FromPtr;
61
62 /// Pointer to the referenced record's primary key member.
63 static constexpr auto Into = IntoPtr;
64
65#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
66 /// The record this connection starts from.
67 using FromRecord = MemberClassType<FromPtr>;
68
69 /// The record this connection points at.
70 using IntoRecord = MemberClassType<IntoPtr>;
71#else
72 /// The record this connection starts from.
73 using FromRecord = MemberClassType<decltype(FromPtr)>;
74
75 /// The record this connection points at.
76 using IntoRecord = MemberClassType<decltype(IntoPtr)>;
77#endif
78
79#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
80 /// The field type on this record's side, e.g. `Field<int32_t>`.
81 using FromField = std::remove_cvref_t<typename[:std::meta::type_of(FromPtr):]>;
82
83 /// The field type on the referenced record's side.
84 using IntoField = std::remove_cvref_t<typename[:std::meta::type_of(IntoPtr):]>;
85#else
86 /// The field type on this record's side, e.g. `Field<int32_t>`.
87 ///
88 /// Taken from the pointer-to-member's own type rather than from a `declval` of the owning record:
89 /// the declaring record is incomplete while this relation is instantiated as one of its members.
90 using FromField = std::remove_cvref_t<typename detail::MemberPointeeType<decltype(FromPtr)>::type>;
91
92 /// The field type on the referenced record's side.
93 using IntoField = std::remove_cvref_t<typename detail::MemberPointeeType<decltype(IntoPtr)>::type>;
94#endif
95
96 /// Reads this connection's field out of @p record.
97 ///
98 /// Wraps the member access so the two reflection modes differ in exactly one place: the
99 /// non-reflection branch uses a pointer-to-member, while the C++26 branch splices the reflection.
100 ///
101 /// @param record The record holding the foreign key.
102 /// @return A reference to the field.
103 template <typename RecordT = FromRecord>
104 [[nodiscard]] static auto const& FieldOf(RecordT const& record) noexcept
105 {
106#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
107 return record.[:FromPtr:];
108#else
109 return record.*FromPtr;
110#endif
111 }
112
113 /// Member index of this record's foreign key member within its own record.
114 ///
115 /// A function rather than a variable: the declaring record is still incomplete while this relation
116 /// is instantiated as one of its members, so reflecting over it has to wait until first use.
117 [[nodiscard]] static consteval std::size_t FromMemberIndex() noexcept
118 {
119 return MemberIndexOf<FromPtr>;
120 }
121
122 /// Member index of the referenced member within the referenced record.
123 ///
124 /// This is what makes the relation independent of the order its connections are written in: the
125 /// `WHERE` clause of a primary key lookup is emitted in the referenced record's *member
126 /// declaration* order, so values must be permuted into that order before being bound. See
127 /// @ref CompositeForeignKey::OrderedValuesOf.
128 static constexpr std::size_t IntoMemberIndex = MemberIndexOf<IntoPtr>;
129};
130
131namespace detail
132{
133 template <typename T>
134 struct IsConnectionType: std::false_type
135 {
136 };
137
138 template <auto FromPtr, auto IntoPtr>
139 struct IsConnectionType<Connection<FromPtr, IntoPtr>>: std::true_type
140 {
141 };
142} // namespace detail
143
144/// @brief Satisfied by `Connection` specializations.
145///
146/// @ingroup DataMapper
147template <typename T>
148concept ConnectionType = detail::IsConnectionType<std::remove_cvref_t<T>>::value;
149
150/// @brief Represents a foreign key spanning several columns.
151///
152/// Declared as a list of `Connection`, each pairing one of this record's columns with the column it
153/// references. The referenced and referencing records are *derived* from those pointers rather than
154/// named again, so they cannot disagree with the connections.
155///
156/// This member holds no column of its own. Every foreign key column is an ordinary `Field` on the
157/// record - one data member per database column - and this relation only ties them together and
158/// navigates. It therefore contributes nothing to `RecordColumnCount` and is skipped by every
159/// projection, exactly as `HasMany` and `HasOneThrough` are.
160///
161/// Connections may be listed in any order. Values are permuted into the referenced record's member
162/// order before binding, because that is the order a primary key lookup emits its predicates in - see
163/// @ref OrderedValuesOf and `src/tests/CompositeKeyOrderingTests.cpp`.
164///
165/// @tparam Connections One `Connection` per column of the foreign key.
166///
167/// @ingroup DataMapper
168///
169/// @code
170/// struct Parent
171/// {
172/// // Composite key members are PrimaryKey::Manual; see that enumerator for why.
173/// Field<int32_t, PrimaryKey::Manual, SqlRealName { "part_a" }> partA;
174/// Field<int32_t, PrimaryKey::Manual, SqlRealName { "part_b" }> partB;
175/// };
176/// struct Child
177/// {
178/// Field<int32_t, PrimaryKey::AutoAssign> id;
179/// Field<int32_t, SqlRealName { "ref_a" }> refA;
180/// Field<int32_t, SqlRealName { "ref_b" }> refB;
181/// CompositeForeignKey<Connection<&Child::refA, &Parent::partA>,
182/// Connection<&Child::refB, &Parent::partB>> parent;
183/// };
184/// @endcode
185template <ConnectionType... Connections>
187{
188 static_assert(sizeof...(Connections) > 0, "A composite foreign key must connect at least one column.");
189
190 public:
191 /// Number of columns this foreign key spans.
192 static constexpr std::size_t Count = sizeof...(Connections);
193
194 /// The record holding the foreign key, i.e. the one declaring this member.
195 using Child = std::tuple_element_t<0, std::tuple<typename Connections::FromRecord...>>;
196
197 /// The referenced record.
198 using ReferencedRecord = std::tuple_element_t<0, std::tuple<typename Connections::IntoRecord...>>;
199
200 static_assert((std::same_as<typename Connections::FromRecord, Child> && ...),
201 "Every Connection of a composite foreign key must start at the same record. "
202 "Check that each Connection's first pointer-to-member names this record.");
203
204 static_assert((std::same_as<typename Connections::IntoRecord, ReferencedRecord> && ...),
205 "Every Connection of a composite foreign key must point at the same record. "
206 "A foreign key references one table; splitting it across two is not expressible.");
207
208 // Compared by *value* type rather than by field type: `SqlRealName` is part of `Field`'s type, so
209 // two columns holding the same kind of value have different field types whenever their column
210 // names differ - which is almost always. The value type is what has to line up for the comparison
211 // the database will perform.
212 static_assert((std::same_as<typename Connections::FromField::ValueType, typename Connections::IntoField::ValueType>
213 && ...),
214 "Each connected column pair must hold the same value type. A mismatch here usually "
215 "means two connections were transposed.");
216
217 static_assert((Connections::IntoField::IsPrimaryKey && ...),
218 "A composite foreign key must reference primary key columns. "
219 "Check the PrimaryKey marker on the referenced record's members.");
220
221 // A foreign key pointing at its own record through the same member is degenerate: it would read a
222 // value out of a record and then look that same record up by it. Each endpoint on its own passes
223 // every check above, so the pairing has to be rejected explicitly.
224
225 // The permutation below ranks each connection by how many name an earlier referenced member, which
226 // is only a total ordering while those indices are distinct. Two connections naming the same
227 // referenced member would share a rank, leaving one key slot unwritten and binding a
228 // default-constructed value against a real predicate - a wrong-row lookup with no diagnostic.
229 static_assert(
230 []() consteval {
231 auto const indices = std::array { Connections::IntoMemberIndex... };
232 for (auto const outer: std::views::iota(std::size_t { 0 }, indices.size()))
233 for (auto const inner: std::views::iota(outer + 1, indices.size()))
234 if (indices[outer] == indices[inner])
235 return false;
236 return true;
237 }(),
238 "Two Connections of a composite foreign key reference the same member of the referenced "
239 "record. Each column of the key must be connected exactly once.");
240
241 // NB: the "connections cover the whole referenced key" check cannot live here. This class is
242 // instantiated while the *declaring* record is still incomplete - the relation is one of its
243 // members - and RecordPrimaryKeyCount reflects over the referenced record, which in a mutually
244 // referencing pair is equally incomplete at that point. It is therefore checked in
245 // AssertCoversReferencedKey() below, which runs from the value accessors, i.e. at first use, when
246 // both records are complete.
247
248 /// The tuple of foreign key values, in the order the connections are declared.
249 using ValueType = std::tuple<typename Connections::FromField::ValueType...>;
250
251 private:
252 /// Referenced member index of each connection, in declaration order.
253 static constexpr auto IntoIndices = std::array { Connections::IntoMemberIndex... };
254
255 /// Index of the connection whose referenced member comes @p Slot-th in the referenced record.
256 ///
257 /// Computed by counting how many connections name an earlier member, which is a total ranking
258 /// because the referenced indices are asserted pairwise distinct above.
259 template <std::size_t Slot>
260 static constexpr std::size_t ConnectionForSlot = []() consteval {
261 for (auto const candidate: std::views::iota(std::size_t { 0 }, Count))
262 {
263 auto rank = std::size_t { 0 };
264 for (auto const other: IntoIndices)
265 if (other < IntoIndices[candidate])
266 ++rank;
267 if (rank == Slot)
268 return candidate;
269 }
270 return Count; // unreachable: the ranking is a bijection onto [0, Count)
271 }();
272
273 /// The connection occupying @p Slot of the referenced record's key order.
274 template <std::size_t Slot>
275 using ConnectionAtSlot = std::tuple_element_t<ConnectionForSlot<Slot>, std::tuple<Connections...>>;
276
277 /// Reads the value belonging in @p Slot straight out of the record's own field.
278 template <std::size_t Slot>
279 [[nodiscard]] static decltype(auto) ValueAtSlot(Child const& record)
280 {
281 return ConnectionAtSlot<Slot>::FieldOf(record).Value();
282 }
283
284 public:
285 /// The tuple of foreign key values, ordered to match the referenced record's key members.
286 ///
287 /// Differs from @ref ValueType whenever the connections are not written in the referenced record's
288 /// member order, and differs in *type* too when the key columns are heterogeneous.
289 using OrderedValueType = decltype([]<std::size_t... Slot>(std::index_sequence<Slot...>) {
290 return std::tuple<typename ConnectionAtSlot<Slot>::FromField::ValueType...> {};
291 }(std::index_sequence_for<Connections...> {}));
292
293 /// Reads this record's foreign key values, in the order the connections are declared.
294 ///
295 /// The values are not stored on the relation: there is exactly one copy of each, in the `Field`
296 /// that owns the column, so nothing can fall out of sync.
297 ///
298 /// @param record The record holding the foreign key.
299 /// @return The values, in declaration order of the connections.
300 [[nodiscard]] static ValueType ValuesOf(Child const& record)
301 {
303 return ValueType { Connections::FieldOf(record).Value()... };
304 }
305
306 /// Checks that the connections cover the referenced record's whole primary key.
307 ///
308 /// Deferred to first use rather than asserted in the class body: at class-instantiation time the
309 /// referenced record can still be incomplete, so reflecting over its members is not yet possible.
310 /// A partial key would otherwise surface only as an argument-count mismatch thrown from the first
311 /// navigation, far from the declaration that caused it.
312 static constexpr void AssertCoversReferencedKey() noexcept
313 {
314 static_assert(Count == RecordPrimaryKeyCount<ReferencedRecord>,
315 "A composite foreign key must connect every primary key column of the referenced "
316 "record. Connecting only some of them cannot identify a row.");
317
318 // Also deferred, and for the same reason: recovering a member index reflects over the owning
319 // record. Only the same-record case can be degenerate - across records the two endpoints are
320 // different entities by construction.
321 static_assert(((!std::same_as<typename Connections::FromRecord, typename Connections::IntoRecord>
322 || Connections::FromMemberIndex() != Connections::IntoMemberIndex)
323 && ...),
324 "A Connection must join two different members. Pairing a member with itself reads "
325 "a value out of a record only to look the same record up by it.");
326 }
327
328 /// Reads this record's foreign key values, permuted into the referenced record's member order.
329 ///
330 /// This is the order they must be bound in. A primary key lookup emits one `WHERE` predicate per
331 /// primary key member, in that record's *member declaration* order, and binds its arguments
332 /// positionally - so passing values in connection order would bind them to the wrong predicates
333 /// whenever the two orders differ. With same-typed key columns that yields a wrong row rather than
334 /// an error, which is why the permutation is done here rather than left to the caller.
335 ///
336 /// @param record The record holding the foreign key.
337 /// @return The values, ordered to match the referenced record's primary key members.
338 [[nodiscard]] static OrderedValueType OrderedValuesOf(Child const& record)
339 {
340 // The permutation is entirely a compile-time property of the connection list, so the ordered
341 // tuple is *built* by index rather than default-constructed and then assigned into. That keeps
342 // heterogeneous keys working - assigning through a runtime-matched index would require every
343 // slot's assignment to be well-formed, which fails as soon as two key columns differ in type -
344 // and it needs no default-constructible value type.
346 return [&]<std::size_t... Slot>(std::index_sequence<Slot...>) {
347 return OrderedValueType { ValueAtSlot<Slot>(record)... };
348 }(std::index_sequence_for<Connections...> {});
349 }
350
351 /// @brief Retrieves the referenced record, loading it on first access.
352 ///
353 /// Never throws for an unavailable record: the reason comes back as the error instead.
354 ///
355 /// @return The record; or @ref RelationError::NotFound when no row matches the key,
356 /// @ref RelationError::NotConfigured when there is no loader (a hand-built record, or one read
357 /// with `loadRelations = false`), @ref RelationError::Outdated when the default connection
358 /// string changed since the record was read, @ref RelationError::QueryFailed when the load
359 /// query failed.
360 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE RelationResult<std::reference_wrapper<ReferencedRecord const>> Record() const
361 {
362 return Load().transform([this] { return std::reference_wrapper<ReferencedRecord const> { *_record }; });
363 }
364
365 /// @return `true` if the referenced record has been loaded.
366 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE constexpr bool IsLoaded() const noexcept
367 {
368 return _record.get() != nullptr;
369 }
370
371 /// Discards the loaded record, so the next access loads it again.
372 LIGHTWEIGHT_FORCE_INLINE void Unload() noexcept
373 {
374 _record.reset();
375 _loadError.reset();
376 }
377
378 /// Adopts an already-fetched referenced record, marking the relation loaded.
379 ///
380 /// @param record The fetched record.
381 LIGHTWEIGHT_FORCE_INLINE constexpr void EmplaceRecord(std::shared_ptr<ReferencedRecord> record) noexcept
382 {
383 _record = std::move(record);
384 _loadError.reset();
385 }
386
387 /// @return A pointer to the referenced record, loading it on first access.
388 /// @throws SqlRequireLoadedError The record is unavailable; @ref Record() reports why without throwing.
389 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE ReferencedRecord const* operator->() const
390 {
391 if (auto const loaded = Load(); !loaded)
392 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>, loaded.error());
393 return _record.get();
394 }
395
396 /// Carries the deferred load, installed by the DataMapper.
397 struct Loader
398 {
399 /// Loads the referenced record: @ref RelationError::NotFound when none exists.
400 std::function<RelationResult<std::shared_ptr<ReferencedRecord>>()> loadReference {};
401
402 /// Loaders carry no comparable state of their own, so any two are considered equivalent.
403 std::weak_ordering operator<=>(Loader const& /*other*/) const noexcept
404 {
405 return std::weak_ordering::equivalent; // Loader is not comparable, so we return equivalent
406 }
407
408 /// Loaders carry no comparable state of their own, so any two compare equal.
409 ///
410 /// A defaulted `==` on the enclosing class does not derive equality from a member's `<=>` - each
411 /// member needs its own viable `==`, or the default is silently deleted. See HasMany::Loader for
412 /// the same shape (there, without this operator; equal by convention since it holds none of the
413 /// relation's state).
414 bool operator==(Loader const& /*other*/) const noexcept
415 {
416 return true;
417 }
418 };
419
420 /// Used internally to configure on-demand loading of the referenced record.
421 ///
422 /// @param loader The loader to install.
424 {
425 _loader = std::move(loader);
426 }
427
428 /// Three-way comparison operator.
429 ///
430 /// Without this, the relation is neither `std::equality_comparable` nor (owing to its private
431 /// members) an aggregate, and `Reflection::CollectDifferences` - which falls back to recursing into
432 /// non-comparable members as aggregates - hard-errors on any record holding one. HasMany,
433 /// HasOneThrough and HasManyThrough all define one for the same reason.
434 std::weak_ordering operator<=>(CompositeForeignKey const& other) const noexcept = default;
435 /// Equality comparison operator.
436 bool operator==(CompositeForeignKey const& other) const noexcept = default;
437
438 private:
439 /// Loads the record unless it already is, or its unavailability is already known.
440 ///
441 /// @ref RelationError::NotFound and @ref RelationError::Outdated are remembered until the record is
442 /// emplaced or unloaded; a failed query is retried on the next access.
443 [[nodiscard]] RelationResult<void> Load() const
444 {
445 if (_record)
446 return {};
447 if (_loadError)
448 return std::unexpected { *_loadError };
449 if (!_loader.loadReference)
450 return std::unexpected { RelationError::NotConfigured };
451
452 auto loaded = _loader.loadReference();
453 if (!loaded)
454 {
455 if (loaded.error() != RelationError::QueryFailed)
456 _loadError = loaded.error();
457 return std::unexpected { loaded.error() };
458 }
459 _record = std::move(*loaded);
460 return {};
461 }
462
463 Loader _loader {};
464 mutable std::shared_ptr<ReferencedRecord> _record {};
465 mutable std::optional<RelationError> _loadError {};
466};
467
468namespace detail
469{
470 template <typename T>
471 struct IsCompositeForeignKeyType: std::false_type
472 {
473 };
474
475 template <ConnectionType... Connections>
476 struct IsCompositeForeignKeyType<CompositeForeignKey<Connections...>>: std::true_type
477 {
478 };
479} // namespace detail
480
481/// @brief Whether @p T is a @ref CompositeForeignKey.
482///
483/// @ingroup DataMapper
484template <typename T>
485constexpr bool IsCompositeForeignKey = detail::IsCompositeForeignKeyType<std::remove_cvref_t<T>>::value;
486
487} // namespace Lightweight
Represents a foreign key spanning several columns.
LIGHTWEIGHT_FORCE_INLINE RelationResult< std::reference_wrapper< ReferencedRecord const > > Record() const
Retrieves the referenced record, loading it on first access.
LIGHTWEIGHT_FORCE_INLINE void Unload() noexcept
Discards the loaded record, so the next access loads it again.
LIGHTWEIGHT_FORCE_INLINE constexpr bool IsLoaded() const noexcept
LIGHTWEIGHT_FORCE_INLINE ReferencedRecord const * operator->() const
static constexpr std::size_t Count
Number of columns this foreign key spans.
std::weak_ordering operator<=>(CompositeForeignKey const &other) const noexcept=default
static ValueType ValuesOf(Child const &record)
std::tuple_element_t< 0, std::tuple< typename Connections::FromRecord... > > Child
The record holding the foreign key, i.e. the one declaring this member.
static OrderedValueType OrderedValuesOf(Child const &record)
LIGHTWEIGHT_FORCE_INLINE constexpr void EmplaceRecord(std::shared_ptr< ReferencedRecord > record) noexcept
std::tuple< typename Connections::FromField::ValueType... > ValueType
The tuple of foreign key values, in the order the connections are declared.
decltype([]< std::size_t... Slot >(std::index_sequence< Slot... >) { return std::tuple< typename ConnectionAtSlot< Slot >::FromField::ValueType... > {} OrderedValueType
std::tuple_element_t< 0, std::tuple< typename Connections::IntoRecord... > > ReferencedRecord
The referenced record.
static constexpr void AssertCoversReferencedKey() noexcept
bool operator==(CompositeForeignKey const &other) const noexcept=default
Equality comparison operator.
Raised by the relation shortcuts that cannot return a RelationResult - operator->,...
Definition Error.hpp:71
Satisfied by Connection specializations.
std::expected< T, RelationError > RelationResult
The outcome of a relation access: the requested value, or why it is unavailable.
Definition Error.hpp:44
constexpr bool IsCompositeForeignKey
Whether T is a CompositeForeignKey.
Carries the deferred load, installed by the DataMapper.
bool operator==(Loader const &) const noexcept
std::weak_ordering operator<=>(Loader const &) const noexcept
Loaders carry no comparable state of their own, so any two are considered equivalent.
std::function< RelationResult< std::shared_ptr< ReferencedRecord > >()> loadReference
Loads the referenced record: RelationError::NotFound when none exists.
One column pair of a composite foreign key: "this record's column references that one".
static constexpr std::size_t IntoMemberIndex
static consteval std::size_t FromMemberIndex() noexcept
MemberClassType< decltype(IntoPtr)> IntoRecord
The record this connection points at.
static auto const & FieldOf(RecordT const &record) noexcept
std::remove_cvref_t< typename detail::MemberPointeeType< decltype(FromPtr)>::type > FromField
MemberClassType< decltype(FromPtr)> FromRecord
The record this connection starts from.
static constexpr auto Into
Pointer to the referenced record's primary key member.
static constexpr auto From
Pointer to this record's foreign key member.
std::remove_cvref_t< typename detail::MemberPointeeType< decltype(IntoPtr)>::type > IntoField
The field type on the referenced record's side.