Lightweight 0.20260625.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 <ranges>
16#include <tuple>
17#include <type_traits>
18#include <utility>
19
20namespace Lightweight
21{
22
23namespace detail
24{
25 /// Extracts the member type from a pointer-to-member, without requiring the owning class to be
26 /// complete. `decltype(std::declval<Owner const&>().*Ptr)` would require completeness, which a
27 /// relation declared *inside* its own record cannot offer.
28 template <typename T>
29 struct MemberPointeeType;
30
31 template <typename Member, typename Owner>
32 struct MemberPointeeType<Member Owner::*>
33 {
34 using type = Member;
35 };
36} // namespace detail
37
38/// @brief One column pair of a composite foreign key: "this record's column references that one".
39///
40/// Both endpoints are pointers-to-member, so the pairing is part of the type. That is the whole point:
41/// with two parallel column *lists* the pairing would be implicit in position, and transposing two
42/// same-typed columns would be silently wrong. Here a transposition is a different `Connection`, and
43/// where the paired columns differ in type it does not even compile.
44///
45/// @tparam FromPtr Pointer to the member of *this* record holding part of the foreign key.
46/// @tparam IntoPtr Pointer to the member of the referenced record it points at, which must be a
47/// primary key there.
48///
49/// @ingroup DataMapper
50///
51/// @code
52/// CompositeForeignKey<Connection<&Child::refA, &Parent::partA>,
53/// Connection<&Child::refB, &Parent::partB>> parent;
54/// @endcode
55template <auto FromPtr, auto IntoPtr>
57{
58 /// Pointer to this record's foreign key member.
59 static constexpr auto From = FromPtr;
60
61 /// Pointer to the referenced record's primary key member.
62 static constexpr auto Into = IntoPtr;
63
64#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
65 /// The record this connection starts from.
66 using FromRecord = MemberClassType<FromPtr>;
67
68 /// The record this connection points at.
69 using IntoRecord = MemberClassType<IntoPtr>;
70#else
71 /// The record this connection starts from.
72 using FromRecord = MemberClassType<decltype(FromPtr)>;
73
74 /// The record this connection points at.
75 using IntoRecord = MemberClassType<decltype(IntoPtr)>;
76#endif
77
78#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
79 /// The field type on this record's side, e.g. `Field<int32_t>`.
80 using FromField = std::remove_cvref_t<typename[:std::meta::type_of(FromPtr):]>;
81
82 /// The field type on the referenced record's side.
83 using IntoField = std::remove_cvref_t<typename[:std::meta::type_of(IntoPtr):]>;
84#else
85 /// The field type on this record's side, e.g. `Field<int32_t>`.
86 ///
87 /// Taken from the pointer-to-member's own type rather than from a `declval` of the owning record:
88 /// the declaring record is incomplete while this relation is instantiated as one of its members.
89 using FromField = std::remove_cvref_t<typename detail::MemberPointeeType<decltype(FromPtr)>::type>;
90
91 /// The field type on the referenced record's side.
92 using IntoField = std::remove_cvref_t<typename detail::MemberPointeeType<decltype(IntoPtr)>::type>;
93#endif
94
95 /// Reads this connection's field out of @p record.
96 ///
97 /// Wraps the member access so the two reflection modes differ in exactly one place: the
98 /// non-reflection branch uses a pointer-to-member, while the C++26 branch splices the reflection.
99 ///
100 /// @param record The record holding the foreign key.
101 /// @return A reference to the field.
102 template <typename RecordT = FromRecord>
103 [[nodiscard]] static auto const& FieldOf(RecordT const& record) noexcept
104 {
105#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
106 return record.[:FromPtr:];
107#else
108 return record.*FromPtr;
109#endif
110 }
111
112 /// Member index of this record's foreign key member within its own record.
113 ///
114 /// A function rather than a variable: the declaring record is still incomplete while this relation
115 /// is instantiated as one of its members, so reflecting over it has to wait until first use.
116 [[nodiscard]] static consteval std::size_t FromMemberIndex() noexcept
117 {
118 return MemberIndexOf<FromPtr>;
119 }
120
121 /// Member index of the referenced member within the referenced record.
122 ///
123 /// This is what makes the relation independent of the order its connections are written in: the
124 /// `WHERE` clause of a primary key lookup is emitted in the referenced record's *member
125 /// declaration* order, so values must be permuted into that order before being bound. See
126 /// @ref CompositeForeignKey::OrderedValuesOf.
127 static constexpr std::size_t IntoMemberIndex = MemberIndexOf<IntoPtr>;
128};
129
130namespace detail
131{
132 template <typename T>
133 struct IsConnectionType: std::false_type
134 {
135 };
136
137 template <auto FromPtr, auto IntoPtr>
138 struct IsConnectionType<Connection<FromPtr, IntoPtr>>: std::true_type
139 {
140 };
141} // namespace detail
142
143/// @brief Satisfied by `Connection` specializations.
144///
145/// @ingroup DataMapper
146template <typename T>
147concept ConnectionType = detail::IsConnectionType<std::remove_cvref_t<T>>::value;
148
149/// @brief Represents a foreign key spanning several columns.
150///
151/// Declared as a list of `Connection`, each pairing one of this record's columns with the column it
152/// references. The referenced and referencing records are *derived* from those pointers rather than
153/// named again, so they cannot disagree with the connections.
154///
155/// This member holds no column of its own. Every foreign key column is an ordinary `Field` on the
156/// record - one data member per database column - and this relation only ties them together and
157/// navigates. It therefore contributes nothing to `RecordColumnCount` and is skipped by every
158/// projection, exactly as `HasMany` and `HasOneThrough` are.
159///
160/// Connections may be listed in any order. Values are permuted into the referenced record's member
161/// order before binding, because that is the order a primary key lookup emits its predicates in - see
162/// @ref OrderedValuesOf and `src/tests/CompositeKeyOrderingTests.cpp`.
163///
164/// @tparam Connections One `Connection` per column of the foreign key.
165///
166/// @ingroup DataMapper
167///
168/// @code
169/// struct Parent
170/// {
171/// // Composite key members must not be PrimaryKey::AutoAssign: auto-assignment yields one value
172/// // that would be written into every key member. See GenerateAutoAssignPrimaryKey.
173/// Field<int32_t, PrimaryKey::ServerSideAutoIncrement, SqlRealName { "part_a" }> partA;
174/// Field<int32_t, PrimaryKey::ServerSideAutoIncrement, 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 /// @return The referenced record, loading it on first access.
352 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE ReferencedRecord const& Record() const
353 {
354 RequireLoaded();
355 return *_record;
356 }
357
358 /// @return `true` if the referenced record has been loaded.
359 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE constexpr bool IsLoaded() const noexcept
360 {
361 return _record.get() != nullptr;
362 }
363
364 /// Discards the loaded record, so the next access loads it again.
365 LIGHTWEIGHT_FORCE_INLINE void Unload() noexcept
366 {
367 _record.reset();
368 }
369
370 /// Adopts an already-fetched referenced record, marking the relation loaded.
371 ///
372 /// @param record The fetched record.
373 LIGHTWEIGHT_FORCE_INLINE constexpr void EmplaceRecord(std::shared_ptr<ReferencedRecord> record) noexcept
374 {
375 _record = std::move(record);
376 }
377
378 /// @return A pointer to the referenced record, loading it on first access.
379 [[nodiscard]] LIGHTWEIGHT_FORCE_INLINE constexpr ReferencedRecord const* operator->() const
380 {
381 RequireLoaded();
382 return _record.get();
383 }
384
385 /// Carries the deferred load, installed by the DataMapper.
386 struct Loader
387 {
388 /// Loads and returns the referenced record, or `nullptr` if none exists.
389 std::function<std::shared_ptr<ReferencedRecord>()> loadReference {};
390
391 /// Loaders carry no comparable state of their own, so any two are considered equivalent.
392 std::weak_ordering operator<=>(Loader const& /*other*/) const noexcept
393 {
394 return std::weak_ordering::equivalent; // Loader is not comparable, so we return equivalent
395 }
396
397 /// Loaders carry no comparable state of their own, so any two compare equal.
398 ///
399 /// A defaulted `==` on the enclosing class does not derive equality from a member's `<=>` - each
400 /// member needs its own viable `==`, or the default is silently deleted. See HasMany::Loader for
401 /// the same shape (there, without this operator; equal by convention since it holds none of the
402 /// relation's state).
403 bool operator==(Loader const& /*other*/) const noexcept
404 {
405 return true;
406 }
407 };
408
409 /// Used internally to configure on-demand loading of the referenced record.
410 ///
411 /// @param loader The loader to install.
413 {
414 _loader = std::move(loader);
415 }
416
417 /// Three-way comparison operator.
418 ///
419 /// Without this, the relation is neither `std::equality_comparable` nor (owing to its private
420 /// members) an aggregate, and `Reflection::CollectDifferences` - which falls back to recursing into
421 /// non-comparable members as aggregates - hard-errors on any record holding one. HasMany,
422 /// HasOneThrough and HasManyThrough all define one for the same reason.
423 std::weak_ordering operator<=>(CompositeForeignKey const& other) const noexcept = default;
424 /// Equality comparison operator.
425 bool operator==(CompositeForeignKey const& other) const noexcept = default;
426
427 private:
428 void RequireLoaded() const
429 {
430 if (_record)
431 return;
432
433 if (_loader.loadReference)
434 _record = _loader.loadReference();
435
436 if (!_record)
437 throw SqlRequireLoadedError(Reflection::TypeNameOf<std::remove_cvref_t<decltype(*this)>>);
438 }
439
440 Loader _loader {};
441 mutable std::shared_ptr<ReferencedRecord> _record {};
442};
443
444namespace detail
445{
446 template <typename T>
447 struct IsCompositeForeignKeyType: std::false_type
448 {
449 };
450
451 template <ConnectionType... Connections>
452 struct IsCompositeForeignKeyType<CompositeForeignKey<Connections...>>: std::true_type
453 {
454 };
455} // namespace detail
456
457/// @brief Whether @p T is a @ref CompositeForeignKey.
458///
459/// @ingroup DataMapper
460template <typename T>
461constexpr bool IsCompositeForeignKey = detail::IsCompositeForeignKeyType<std::remove_cvref_t<T>>::value;
462
463} // namespace Lightweight
Represents a foreign key spanning several columns.
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 & Record() 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
LIGHTWEIGHT_FORCE_INLINE constexpr ReferencedRecord const * operator->() const
bool operator==(CompositeForeignKey const &other) const noexcept=default
Equality comparison operator.
Represents an error when a record is required to be loaded but is not.
Definition Error.hpp:16
Satisfied by Connection specializations.
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< std::shared_ptr< ReferencedRecord >()> loadReference
Loads and returns the referenced record, or nullptr if 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.