Lightweight 0.20260303.0
Loading...
Searching...
No Matches
DataMapper.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include "../SqlConnection.hpp"
5#include "../SqlDataBinder.hpp"
6#include "../SqlLogger.hpp"
7#include "../SqlRealName.hpp"
8#include "../SqlStatement.hpp"
9#include "../Utils.hpp"
10#include "BelongsTo.hpp"
11#include "CollectDifferences.hpp"
12#include "Field.hpp"
13#include "HasMany.hpp"
14#include "HasManyThrough.hpp"
15#include "HasOneThrough.hpp"
16#include "QueryBuilders.hpp"
17#include "Record.hpp"
18
19#include <reflection-cpp/reflection.hpp>
20
21#include <cassert>
22#include <concepts>
23#include <memory>
24#include <tuple>
25#include <type_traits>
26#include <utility>
27
28namespace Lightweight
29{
30
31/// @defgroup DataMapper Data Mapper
32///
33/// @brief The data mapper is a high level API for mapping records to and from the database using high level C++ syntax.
34
35namespace detail
36{
37 // Converts a container of T to a container of std::shared_ptr<T>.
38 template <template <typename> class Allocator, template <typename, typename> class Container, typename Object>
39 auto ToSharedPtrList(Container<Object, Allocator<Object>> container)
40 {
41 using SharedPtrRecord = std::shared_ptr<Object>;
42 auto sharedPtrContainer = Container<SharedPtrRecord, Allocator<SharedPtrRecord>> {};
43 for (auto& object: container)
44 sharedPtrContainer.emplace_back(std::make_shared<Object>(std::move(object)));
45 return sharedPtrContainer;
46 }
47} // namespace detail
48
49/// @brief Main API for mapping records to and from the database using high level C++ syntax.
50///
51/// A DataMapper instances operates on a single SQL connection and provides methods to
52/// create, read, update and delete records in the database.
53///
54/// @see Field, BelongsTo, HasMany, HasManyThrough, HasOneThrough
55/// @ingroup DataMapper
56///
57/// @code
58/// struct Person
59/// {
60/// Field<SqlGuid, PrimaryKey::AutoAssign> id;
61/// Field<SqlAnsiString<30>> name;
62/// Field<SqlAnsiString<40>> email;
63/// };
64///
65/// auto dm = DataMapper {};
66///
67/// // Create a new person record
68/// auto person = Person { .id = SqlGuid::Create(), .name = "John Doe", .email = "johnt@doe.com" };
69///
70/// // Create the record in the database and set the primary key on the record
71/// auto const personId = dm.Create(person);
72///
73/// // Query the person record from the database
74/// auto const queriedPerson = dm.Query<Person>(personId)
75/// .Where(FieldNameOf<&Person::id>, "=", personId)
76/// .First();
77///
78/// if (queriedPerson.has_value())
79/// std::println("Queried Person: {}", DataMapper::Inspect(queriedPerson.value()));
80///
81/// // Update the person record in the database
82/// person.email = "alt@doe.com";
83/// dm.Update(person);
84///
85/// // Delete the person record from the database
86/// dm.Delete(person);
87/// @endcode
89{
90 public:
91 /// Acquires a thread-local DataMapper instance that is safe for reuse within that thread.
92 LIGHTWEIGHT_API static DataMapper& AcquireThreadLocal();
93
94 /// Constructs a new data mapper, using the default connection.
96 _connection {},
97 _stmt { _connection }
98 {
99 }
100
101 /// Constructs a new data mapper, using the given connection.
102 explicit DataMapper(SqlConnection&& connection):
103 _connection { std::move(connection) },
104 _stmt { _connection }
105 {
106 }
107
108 /// Constructs a new data mapper, using the given connection string.
109 explicit DataMapper(std::optional<SqlConnectionString> connectionString):
110 _connection { std::move(connectionString) },
111 _stmt { _connection }
112 {
113 }
114
115 DataMapper(DataMapper const&) = delete;
116 DataMapper& operator=(DataMapper const&) = delete;
117
118 /// Move constructor.
119 DataMapper(DataMapper&& other) noexcept:
120 _connection(std::move(other._connection)),
121 _stmt(_connection)
122 {
123 other._stmt = SqlStatement(std::nullopt);
124 }
125
126 /// Move assignment operator.
127 DataMapper& operator=(DataMapper&& other) noexcept
128 {
129 if (this == &other)
130 return *this;
131
132 _connection = std::move(other._connection);
133 _stmt = SqlStatement(_connection);
134 other._stmt = SqlStatement(std::nullopt);
135
136 return *this;
138
139 ~DataMapper() = default;
141 /// Returns the connection reference used by this data mapper.
142 [[nodiscard]] SqlConnection const& Connection() const noexcept
143 {
144 return _connection;
145 }
146
147 /// Returns the mutable connection reference used by this data mapper.
148 [[nodiscard]] SqlConnection& Connection() noexcept
149 {
150 return _connection;
151 }
152
153#if defined(BUILD_TESTS)
154
155 [[nodiscard]] SqlStatement& Statement(this auto&& self) noexcept
156 {
157 return self._stmt;
158 }
159
160#endif
161
162 /// Constructs a human readable string representation of the given record.
163 template <typename Record>
164 static std::string Inspect(Record const& record);
165
166 /// Constructs a string list of SQL queries to create the table for the given record type.
167 template <typename Record>
168 std::vector<std::string> CreateTableString(SqlServerType serverType);
169
170 /// Constructs a string list of SQL queries to create the tables for the given record types.
171 template <typename FirstRecord, typename... MoreRecords>
172 std::vector<std::string> CreateTablesString(SqlServerType serverType);
173
174 /// Creates the table for the given record type.
175 template <typename Record>
176 void CreateTable();
177
178 /// Creates the tables for the given record types.
179 template <typename FirstRecord, typename... MoreRecords>
180 void CreateTables();
181
182 /// @brief Creates a new record in the database.
183 ///
184 /// The record is inserted into the database and the primary key is set on this record.
185 ///
186 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
187 /// @tparam Record The record type to insert.
188 /// @param record The record to insert. The primary key field is updated in-place after the insert.
189 /// @return The primary key of the newly created record.
190 template <DataMapperOptions QueryOptions = {}, typename Record>
191 RecordPrimaryKeyType<Record> Create(Record& record);
192
193 /// @brief Creates a new record in the database.
194 ///
195 /// @note This is a variation of the Create() method and does not update the record's primary key.
196 ///
197 /// @tparam Record The record type to insert.
198 /// @param record The record to insert. Unlike Create(), the primary key field is NOT updated in-place.
199 /// @return The primary key of the newly created record.
200 template <typename Record>
201 RecordPrimaryKeyType<Record> CreateExplicit(Record const& record);
202
203 /// @brief Creates a copy of an existing record in the database.
204 ///
205 /// This method is useful for duplicating a database record while assigning a new primary key.
206 /// All fields except primary key(s) are copied from the original record.
207 /// The primary key is automatically generated (auto-incremented or auto-assigned).
208 ///
209 /// @param originalRecord The record to copy.
210 /// @return The primary key of the newly created record.
211 template <DataMapperOptions QueryOptions = {}, typename Record>
212 [[nodiscard]] RecordPrimaryKeyType<Record> CreateCopyOf(Record const& originalRecord);
213
214 /// @brief Queries a single record (based on primary key) from the database.
215 ///
216 /// The primary key(s) are used to identify the record to load.
217 /// If the record is not found, std::nullopt is returned.
218 ///
219 /// @tparam Record The record type to query and materialize.
220 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior,
221 /// such as whether related records should be auto-loaded. For example,
222 /// set the relation loading option to false to disable auto-loading of
223 /// relations when reading a single record.
224 /// @tparam PrimaryKeyTypes The type(s) of the primary key value(s) used to look up the record.
225 /// @param primaryKeys The primary key value(s) identifying the record to load.
226 /// @return An initialized Record if found; otherwise std::nullopt.
227 ///
228 /// @code
229 /// // Example: disable auto-loading of relations when querying a single record
230 /// auto result = dataMapper
231 /// .QuerySingle<MyRecord, DataMapperOptions{ .loadRelations = false }>(primaryKeyValue);
232 /// if (result)
233 /// {
234 /// // use *result; relations have not been auto-loaded
235 /// }
236 /// @endcode
237 template <typename Record, DataMapperOptions QueryOptions = {}, typename... PrimaryKeyTypes>
238 std::optional<Record> QuerySingle(PrimaryKeyTypes&&... primaryKeys);
239
240 /// Queries multiple records from the database, based on the given query.
241 ///
242 /// @tparam Record The record type to query and materialize.
243 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
244 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
245 /// @param selectQuery The composed SQL select query to execute.
246 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
247 /// @return A vector of records populated from the query results.
248 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
249 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
250
251 /// Queries multiple records from the database, based on the given query.
252 ///
253 /// @param sqlQueryString The SQL query string to execute.
254 /// @param inputParameters The input parameters for the query to be bound before executing.
255 /// @return A vector of records of the given type that were found via the query.
256 ///
257 /// example:
258 /// @code
259 /// struct Person
260 /// {
261 /// int id;
262 /// std::string name;
263 /// std::string email;
264 /// std::string phone;
265 /// std::string address;
266 /// std::string city;
267 /// std::string country;
268 /// };
269 ///
270 /// void example(DataMapper& dm)
271 /// {
272 /// auto const sqlQueryString = R"(SELECT * FROM "Person" WHERE "city" = ? AND "country" = ?)";
273 /// auto const records = dm.Query<Person>(sqlQueryString, "Berlin", "Germany");
274 /// for (auto const& record: records)
275 /// {
276 /// std::println("Person: {}", DataMapper::Inspect(record));
277 /// }
278 /// }
279 /// @endcode
280 template <typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
281 std::vector<Record> Query(std::string_view sqlQueryString, InputParameters&&... inputParameters);
282
283 /// Queries records from the database, based on the given query and can be used to retrieve only part of the record
284 /// by specifying the ElementMask.
285 ///
286 /// @tparam ElementMask A SqlElements<Idx...> specialization specifying the zero-based field indices to populate.
287 /// @tparam Record The record type to query and materialize.
288 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
289 /// @tparam InputParameters The types of the input parameters to bind before executing the query.
290 /// @param selectQuery The composed SQL select query to execute. Only the columns listed in the SELECT clause
291 /// are bound; the remaining fields of Record are left at their default values.
292 /// @param inputParameters Zero or more values to bind as positional parameters in the query.
293 /// @return A vector of partially populated records; only fields at the specified indices are filled in.
294 ///
295 /// @code
296 ///
297 /// struct Person
298 /// {
299 /// Field<int> id;
300 /// Field<std::string> name; // index 1
301 /// Field<std::string> email;
302 /// Field<std::string> phone;
303 /// Field<std::string> address;
304 /// Field<std::string> city; // index 5
305 /// Field<std::string> country;
306 /// };
307 ///
308 /// void example(DataMapper& dm)
309 /// {
310 /// auto const query = dm.FromTable(RecordTableName<Person>)
311 /// .Select()
312 /// .Fields({ "name"sv, "city"sv })
313 /// .All();
314 /// auto const infos = dm.Query<SqlElements<1, 5>, Person>(query);
315 /// for (auto const& info : infos)
316 /// {
317 /// // only info.name and info.city are populated
318 /// }
319 /// }
320 /// @endcode
321 template <typename ElementMask, typename Record, DataMapperOptions QueryOptions = {}, typename... InputParameters>
322 std::vector<Record> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters);
323
324 /// Queries records of different types from the database, based on the given query.
325 /// User can constructed query that selects columns from the multiple tables
326 /// this function is used to get result of the query
327 ///
328 /// @tparam First The first record type to materialize from each result row.
329 /// @tparam Second The second record type to materialize from each result row.
330 /// @tparam Rest Zero or more additional record types to materialize from each result row.
331 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
332 /// @param selectQuery The composed SQL select query whose column list covers all fields of First, Second, and Rest.
333 /// @return A vector of tuples, each containing one instance of every requested record type per result row.
334 ///
335 /// @code
336 ///
337 /// struct JointA{};
338 /// struct JointB{};
339 /// struct JointC{};
340 ///
341 /// // the following query will construct statement to fetch all elements of JointA and JointC types
342 /// auto dm = DataMapper {};
343 /// auto const query = dm.FromTable(RecordTableName<JoinTestA>)
344 /// .Select()
345 /// .Fields<JointA, JointC>()
346 /// .InnerJoin<&JointB::a_id, &JointA::id>()
347 /// .InnerJoin<&JointC::id, &JointB::c_id>()
348 /// .All();
349 /// auto const records = dm.Query<JointA, JointC>(query);
350 /// for(const auto [elementA, elementC] : records)
351 /// {
352 /// // do something with elementA and elementC
353 /// }
354 /// @endcode
355 template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions = {}>
356 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
357 std::vector<std::tuple<First, Second, Rest...>> Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery);
358
359 /// Queries records of given Record type.
360 ///
361 /// The query builder can be used to further refine the query.
362 /// The query builder will execute the query when a method like All(), First(n), etc. is called.
363 ///
364 /// @tparam Record The record type to query and materialize.
365 /// @tparam QueryOptions A specialization of DataMapperOptions that controls query behavior.
366 /// @return A query builder for the given Record type.
367 ///
368 /// @code
369 /// auto const records = dm.Query<Person>()
370 /// .Where(FieldNameOf<&Person::is_active>, "=", true)
371 /// .All();
372 /// @endcode
373 template <typename Record, DataMapperOptions QueryOptions = {}>
375 {
376 std::string fields;
377 Reflection::EnumerateMembers<Record>([&fields]<size_t I, typename Field>() {
378 if (!fields.empty())
379 fields += ", ";
380 fields += '"';
381 fields += RecordTableName<Record>;
382 fields += "\".\"";
383 fields += FieldNameAt<I, Record>;
384 fields += '"';
385 });
386 return SqlAllFieldsQueryBuilder<Record, QueryOptions>(*this, std::move(fields));
387 }
388
389 /// Returns a SqlQueryBuilder using the default query formatter.
390 ///
391 /// This can be used to build custom queries separately from the DataMapper
392 /// and execute them via the DataMapper's typed Query() overloads that accept a SqlSelectQueryBuilder.
393 ///
394 /// @return A SqlQueryBuilder bound to the connection's query formatter.
396 {
397 return SqlQueryBuilder(_connection.QueryFormatter());
398 }
399
400 /// Updates the record in the database.
401 ///
402 /// Only fields that have been modified since the record was last loaded or saved are written.
403 /// Fields that were not changed are excluded from the UPDATE statement.
404 ///
405 /// @tparam Record The record type to update.
406 /// @param record The record to update. Only its modified fields are written to the database.
407 template <typename Record>
408 void Update(Record& record);
409
410 /// Deletes the record from the database.
411 ///
412 /// The record is identified by its primary key(s). The row is removed from the backing table.
413 ///
414 /// @tparam Record The record type to delete.
415 /// @param record The record to delete. Its primary key field(s) identify the row to remove.
416 /// @return The number of rows deleted (typically 1 if the record was found, 0 otherwise).
417 template <typename Record>
418 std::size_t Delete(Record const& record);
419
420 /// Constructs an SQL query builder for the given table name.
421 SqlQueryBuilder FromTable(std::string_view tableName)
422 {
423 return _connection.Query(tableName);
424 }
425
426 /// Checks if the record has any modified fields.
427 ///
428 /// @tparam Record The record type to inspect.
429 /// @param record The record to check.
430 /// @return True if at least one field has been modified since the record was last loaded or saved.
431 template <typename Record>
432 bool IsModified(Record const& record) const noexcept;
433
434 /// Enum to set the modified state of a record.
435 enum class ModifiedState : uint8_t
436 {
437 Modified,
438 NotModified
439 };
440
441 /// Sets the modified state of the record after receiving from the database.
442 /// This marks all fields as not modified.
443 ///
444 /// @tparam state The target modified state for all fields (Modified or NotModified).
445 /// @tparam Record The record type whose fields are to be updated.
446 /// @param record The record whose field modification flags are set to @p state.
447 template <ModifiedState state, typename Record>
448 void SetModifiedState(Record& record) noexcept;
449
450 /// Loads all direct relations to this record.
451 ///
452 /// @tparam Record The record type whose relation fields are to be populated.
453 /// @param record The record whose BelongsTo, HasMany, HasOneThrough, and HasManyThrough fields are loaded.
454 template <typename Record>
455 void LoadRelations(Record& record);
456
457 /// Configures the auto loading of relations for the given record.
458 ///
459 /// This means, that no explicit loading of relations is required.
460 /// The relations are automatically loaded when accessed.
461 ///
462 /// @tparam Record The record type to configure auto-loading for.
463 /// @param record The record whose relation fields are set up to load lazily on first access.
464 template <typename Record>
465 void ConfigureRelationAutoLoading(Record& record);
466
467 /// Helper function that allow to execute query directly via data mapper
468 /// and get scalar result without need to create SqlStatement manually
469 ///
470 /// @tparam T The scalar type of the expected result value.
471 /// @param sqlQueryString The SQL query string to execute.
472 /// @return The first column of the first result row cast to T, or std::nullopt if the query returns no rows.
473 template <typename T>
474 [[nodiscard]] std::optional<T> Execute(std::string_view sqlQueryString);
475
476 private:
477 /// @brief Queries a single record from the database based on the given query.
478 ///
479 /// @param selectQuery The SQL select query to execute.
480 /// @param args The input parameters for the query.
481 ///
482 /// @return The record if found, otherwise std::nullopt.
483 template <typename Record, typename... Args>
484 std::optional<Record> QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args);
485
486 template <typename Record, typename ValueType>
487 void SetId(Record& record, ValueType&& id);
488
489 template <typename Record, size_t InitialOffset = 1>
490 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
491
492 template <typename ElementMask, typename Record, size_t InitialOffset = 1>
493 Record& BindOutputColumns(Record& record, SqlResultCursor& cursor);
494
495 template <typename FieldType>
496 std::optional<typename FieldType::ReferencedRecord> LoadBelongsTo(FieldType::ValueType value);
497
498 template <size_t FieldIndex, typename Record, typename OtherRecord>
499 void LoadHasMany(Record& record, HasMany<OtherRecord>& field);
500
501 template <typename ReferencedRecord, typename ThroughRecord, typename Record>
502 void LoadHasOneThrough(Record& record, HasOneThrough<ReferencedRecord, ThroughRecord>& field);
503
504 template <typename ReferencedRecord, typename ThroughRecord, typename Record>
505 void LoadHasManyThrough(Record& record, HasManyThrough<ReferencedRecord, ThroughRecord>& field);
506
507 template <size_t FieldIndex, typename Record, typename OtherRecord, typename Callable>
508 void CallOnHasMany(Record& record, Callable const& callback);
509
510 template <size_t FieldIndex, typename OtherRecord>
511 SqlSelectQueryBuilder BuildHasManySelectQuery();
512
513 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename Callable>
514 void CallOnHasManyThrough(Record& record, Callable const& callback);
515
516 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue, typename Callable>
517 void CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback);
518
519 template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue>
520 std::shared_ptr<ReferencedRecord> LoadHasOneThroughByPK(PKValue const& pkValue);
521
522 enum class PrimaryKeySource : std::uint8_t
523 {
524 Record,
525 Override,
526 };
527
528 template <typename Record>
529 std::optional<RecordPrimaryKeyType<Record>> GenerateAutoAssignPrimaryKey(Record const& record);
530
531 template <PrimaryKeySource UsePkOverride, typename Record>
532 RecordPrimaryKeyType<Record> CreateInternal(
533 Record const& record,
534 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
535 pkOverride = std::nullopt);
536
537 SqlConnection _connection;
538 SqlStatement _stmt;
539};
540
541// ------------------------------------------------------------------------------------------------
542
543namespace detail
544{
545 template <typename FieldType>
546 constexpr bool CanSafelyBindOutputColumn(SqlServerType sqlServerType) noexcept
547 {
548 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
549 return true;
550
551 // Test if we have some columns that might not be sufficient to store the result (e.g. string truncation),
552 // then don't call BindOutputColumn but SQLFetch to get the result, because
553 // regrowing previously bound columns is not supported in MS-SQL's ODBC driver, so it seems.
554 bool result = true;
555 if constexpr (IsField<FieldType>)
556 {
557 if constexpr (detail::OneOf<typename FieldType::ValueType,
558 std::string,
559 std::wstring,
560 std::u16string,
561 std::u32string,
562 SqlBinary>
563 || IsSqlDynamicString<typename FieldType::ValueType>
564 || IsSqlDynamicBinary<typename FieldType::ValueType>)
565 {
566 // Known types that MAY require growing due to truncation.
567 result = false;
568 }
569 }
570 return result;
571 }
572
573 template <DataMapperRecord Record>
574 constexpr bool CanSafelyBindOutputColumns(SqlServerType sqlServerType) noexcept
575 {
576 if (sqlServerType != SqlServerType::MICROSOFT_SQL)
577 return true;
578
579 bool result = true;
580 Reflection::EnumerateMembers<Record>([&result]<size_t I, typename Field>() {
581 if constexpr (IsField<Field>)
582 {
583 if constexpr (detail::OneOf<typename Field::ValueType,
584 std::string,
585 std::wstring,
586 std::u16string,
587 std::u32string,
588 SqlBinary>
589 || IsSqlDynamicString<typename Field::ValueType>
590 || IsSqlDynamicBinary<typename Field::ValueType>)
591 {
592 // Known types that MAY require growing due to truncation.
593 result = false;
594 }
595 }
596 });
597 return result;
598 }
599
600 template <typename Record>
601 void BindAllOutputColumnsWithOffset(SqlResultCursor& reader, Record& record, SQLUSMALLINT startOffset)
602 {
603 Reflection::EnumerateMembers(record,
604 [reader = &reader, i = startOffset]<size_t I, typename Field>(Field& field) mutable {
605 if constexpr (IsField<Field>)
606 {
607 reader->BindOutputColumn(i++, &field.MutableValue());
608 }
609 else if constexpr (IsBelongsTo<Field>)
610 {
611 reader->BindOutputColumn(i++, &field.MutableValue());
612 }
613 else if constexpr (SqlOutputColumnBinder<Field>)
614 {
615 reader->BindOutputColumn(i++, &field);
616 }
617 });
618 }
619
620 template <typename Record>
621 void BindAllOutputColumns(SqlResultCursor& reader, Record& record)
622 {
623 BindAllOutputColumnsWithOffset(reader, record, 1);
624 }
625
626 // when we iterate over all columns using element mask
627 // indexes of the mask corresponds to the indexe of the field
628 // inside the structure, not inside the SQL result set
629 template <typename ElementMask, typename Record>
630 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
631 {
632 Reflection::EnumerateMembers<ElementMask>(
633 record, [reader = &reader, &indexFromQuery]<size_t I, typename Field>(Field& field) mutable {
634 ++indexFromQuery;
635 if constexpr (IsField<Field>)
636 {
637 if constexpr (Field::IsOptional)
638 field.MutableValue() =
639 reader->GetNullableColumn<typename Field::ValueType::value_type>(indexFromQuery);
640 else
641 field.MutableValue() = reader->GetColumn<typename Field::ValueType>(indexFromQuery);
642 }
643 else if constexpr (SqlGetColumnNativeType<Field>)
644 {
645 if constexpr (IsOptionalBelongsTo<Field>)
646 field = reader->GetNullableColumn<typename Field::BaseType>(indexFromQuery);
647 else
648 field = reader->GetColumn<Field>(indexFromQuery);
649 }
650 });
651 }
652
653 template <typename Record>
654 void GetAllColumns(SqlResultCursor& reader, Record& record, SQLUSMALLINT indexFromQuery = 0)
655 {
656 return GetAllColumns<std::make_integer_sequence<size_t, Reflection::CountMembers<Record>>, Record>(
657 reader, record, indexFromQuery);
658 }
659
660 template <typename FirstRecord, typename SecondRecord>
661 // TODO we need to remove this at some points and provide generic bindings for tuples
662 void GetAllColumns(SqlResultCursor& reader, std::tuple<FirstRecord, SecondRecord>& record)
663 {
664 auto& [firstRecord, secondRecord] = record;
665
666 Reflection::EnumerateMembers(firstRecord, [reader = &reader]<size_t I, typename Field>(Field& field) mutable {
667 if constexpr (IsField<Field>)
668 {
669 if constexpr (Field::IsOptional)
670 field.MutableValue() = reader->GetNullableColumn<typename Field::ValueType::value_type>(I + 1);
671 else
672 field.MutableValue() = reader->GetColumn<typename Field::ValueType>(I + 1);
673 }
674 else if constexpr (SqlGetColumnNativeType<Field>)
675 {
676 if constexpr (Field::IsOptional)
677 field = reader->GetNullableColumn<typename Field::BaseType>(I + 1);
678 else
679 field = reader->GetColumn<Field>(I + 1);
680 }
681 });
682
683 Reflection::EnumerateMembers(secondRecord, [reader = &reader]<size_t I, typename Field>(Field& field) mutable {
684 if constexpr (IsField<Field>)
685 {
686 if constexpr (Field::IsOptional)
687 field.MutableValue() = reader->GetNullableColumn<typename Field::ValueType::value_type>(
688 Reflection::CountMembers<FirstRecord> + I + 1);
689 else
690 field.MutableValue() =
691 reader->GetColumn<typename Field::ValueType>(Reflection::CountMembers<FirstRecord> + I + 1);
692 }
693 else if constexpr (SqlGetColumnNativeType<Field>)
694 {
695 if constexpr (Field::IsOptional)
696 field =
697 reader->GetNullableColumn<typename Field::BaseType>(Reflection::CountMembers<FirstRecord> + I + 1);
698 else
699 field = reader->GetColumn<Field>(Reflection::CountMembers<FirstRecord> + I + 1);
700 }
701 });
702 }
703
704 template <typename Record>
705 bool ReadSingleResult(SqlServerType sqlServerType, SqlResultCursor& reader, Record& record)
706 {
707 auto const outputColumnsBound = CanSafelyBindOutputColumns<Record>(sqlServerType);
708
709 if (outputColumnsBound)
710 BindAllOutputColumns(reader, record);
711
712 if (!reader.FetchRow())
713 return false;
714
715 if (!outputColumnsBound)
716 GetAllColumns(reader, record);
717
718 return true;
719 }
720} // namespace detail
721
722template <typename Record, typename Derived, DataMapperOptions QueryOptions>
724 DataMapper& dm, std::string fields) noexcept:
725 _dm { dm },
726 _formatter { dm.Connection().QueryFormatter() },
727 _fields { std::move(fields) }
728{
729 this->_query.searchCondition.inputBindings = &_boundInputs;
730}
731
732template <typename Record, typename Derived, DataMapperOptions QueryOptions>
734{
735 auto stmt = SqlStatement { _dm.Connection() };
736 stmt.Prepare(_formatter.SelectCount(this->_query.distinct,
737 RecordTableName<Record>,
738 this->_query.searchCondition.tableAlias,
739 this->_query.searchCondition.tableJoins,
740 this->_query.searchCondition.condition));
741 auto reader = stmt.ExecuteWithVariants(_boundInputs);
742 if (reader.FetchRow())
743 return reader.GetColumn<size_t>(1);
744 return 0;
745}
746
747template <typename Record, typename Derived, DataMapperOptions QueryOptions>
749{
750 auto stmt = SqlStatement { _dm.Connection() };
751
752 auto const query = _formatter.SelectFirst(this->_query.distinct,
753 _fields,
754 RecordTableName<Record>,
755 this->_query.searchCondition.tableAlias,
756 this->_query.searchCondition.tableJoins,
757 this->_query.searchCondition.condition,
758 this->_query.orderBy,
759 1);
760
761 stmt.Prepare(query);
762 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
763 return true;
764 return false;
765}
766
767template <typename Record, typename Derived, DataMapperOptions QueryOptions>
769{
770 auto stmt = SqlStatement { _dm.Connection() };
771
772 auto const query = _formatter.Delete(RecordTableName<Record>,
773 this->_query.searchCondition.tableAlias,
774 this->_query.searchCondition.tableJoins,
775 this->_query.searchCondition.condition);
776
777 stmt.Prepare(query);
778 stmt.Prepare(query);
779 [[maybe_unused]] auto cursor = stmt.ExecuteWithVariants(_boundInputs);
780}
781
782template <typename Record, typename Derived, DataMapperOptions QueryOptions>
784{
785
786 auto records = std::vector<Record> {};
787 auto stmt = SqlStatement { _dm.Connection() };
788 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
789 _fields,
790 RecordTableName<Record>,
791 this->_query.searchCondition.tableAlias,
792 this->_query.searchCondition.tableJoins,
793 this->_query.searchCondition.condition,
794 this->_query.orderBy,
795 this->_query.groupBy));
796 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
797 if constexpr (DataMapperRecord<Record>)
798 {
799 // This can be called when record type is not plain aggregate type
800 // but more complex tuple, like std::tuple<A, B>
801 // for now we do not unwrap this type and just skip auto-loading configuration
802 if constexpr (QueryOptions.loadRelations)
803 {
804 for (auto& record: records)
805 {
806 _dm.ConfigureRelationAutoLoading(record);
807 }
808 }
809 }
810 return records;
811}
812
813template <typename Record, typename Derived, DataMapperOptions QueryOptions>
814template <auto Field>
815#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
816 requires(is_aggregate_type(parent_of(Field)))
817#else
818 requires std::is_member_object_pointer_v<decltype(Field)>
819#endif
820auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::All() -> std::vector<ReferencedFieldTypeOf<Field>>
821{
822 using value_type = ReferencedFieldTypeOf<Field>;
823 auto result = std::vector<value_type> {};
824
825 auto stmt = SqlStatement { _dm.Connection() };
826 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
827 FullyQualifiedNamesOf<Field>.string_view(),
828 RecordTableName<Record>,
829 this->_query.searchCondition.tableAlias,
830 this->_query.searchCondition.tableJoins,
831 this->_query.searchCondition.condition,
832 this->_query.orderBy,
833 this->_query.groupBy));
834 auto reader = stmt.ExecuteWithVariants(_boundInputs);
835 auto const outputColumnsBound = detail::CanSafelyBindOutputColumn<value_type>(stmt.Connection().ServerType());
836 while (true)
837 {
838 auto& value = result.emplace_back();
839 if (outputColumnsBound)
840 reader.BindOutputColumn(1, &value);
841
842 if (!reader.FetchRow())
843 {
844 result.pop_back();
845 break;
846 }
847
848 if (!outputColumnsBound)
849 value = reader.GetColumn<value_type>(1);
850 }
851
852 return result;
853}
854
855template <typename Record, typename Derived, DataMapperOptions QueryOptions>
856template <auto... ReferencedFields>
857 requires(sizeof...(ReferencedFields) >= 2)
858auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::All() -> std::vector<Record>
859{
860 auto records = std::vector<Record> {};
861 auto stmt = SqlStatement { _dm.Connection() };
862
863 stmt.Prepare(_formatter.SelectAll(this->_query.distinct,
864 FullyQualifiedNamesOf<ReferencedFields...>.string_view(),
865 RecordTableName<Record>,
866 this->_query.searchCondition.tableAlias,
867 this->_query.searchCondition.tableJoins,
868 this->_query.searchCondition.condition,
869 this->_query.orderBy,
870 this->_query.groupBy));
871
872 auto reader = stmt.ExecuteWithVariants(_boundInputs);
873 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
874 while (true)
875 {
876 auto& record = records.emplace_back();
877 if (outputColumnsBound)
878#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
879 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
880#else
881 reader.BindOutputColumns(&(record.*ReferencedFields)...);
882#endif
883 if (!reader.FetchRow())
884 {
885 records.pop_back();
886 break;
887 }
888 if (!outputColumnsBound)
889 {
890 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
891 detail::GetAllColumns<ElementMask>(reader, record);
892 }
893 }
894
895 return records;
896}
897
898template <typename Record, typename Derived, DataMapperOptions QueryOptions>
900{
901 std::optional<Record> record {};
902 auto stmt = SqlStatement { _dm.Connection() };
903 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
904 _fields,
905 RecordTableName<Record>,
906 this->_query.searchCondition.tableAlias,
907 this->_query.searchCondition.tableJoins,
908 this->_query.searchCondition.condition,
909 this->_query.orderBy,
910 1));
911 Derived::ReadResult(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &record);
912 if constexpr (QueryOptions.loadRelations)
913 {
914 if (record)
915 _dm.ConfigureRelationAutoLoading(record.value());
916 }
917 return record;
918}
919
920template <typename Record, typename Derived, DataMapperOptions QueryOptions>
921template <auto Field>
922#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
923 requires(is_aggregate_type(parent_of(Field)))
924#else
925 requires std::is_member_object_pointer_v<decltype(Field)>
926#endif
927auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::First() -> std::optional<ReferencedFieldTypeOf<Field>>
928{
929 auto constexpr count = 1;
930 auto stmt = SqlStatement { _dm.Connection() };
931 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
932 FullyQualifiedNamesOf<Field>.string_view(),
933 RecordTableName<Record>,
934 this->_query.searchCondition.tableAlias,
935 this->_query.searchCondition.tableJoins,
936 this->_query.searchCondition.condition,
937 this->_query.orderBy,
938 count));
939 if (auto reader = stmt.ExecuteWithVariants(_boundInputs); reader.FetchRow())
940 return reader.template GetColumn<ReferencedFieldTypeOf<Field>>(1);
941 return std::nullopt;
942}
943
944template <typename Record, typename Derived, DataMapperOptions QueryOptions>
945template <auto... ReferencedFields>
946 requires(sizeof...(ReferencedFields) >= 2)
947auto SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::First() -> std::optional<Record>
948{
949 auto optionalRecord = std::optional<Record> {};
950
951 auto stmt = SqlStatement { _dm.Connection() };
952 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
953 FullyQualifiedNamesOf<ReferencedFields...>.string_view(),
954 RecordTableName<Record>,
955 this->_query.searchCondition.tableAlias,
956 this->_query.searchCondition.tableJoins,
957 this->_query.searchCondition.condition,
958 this->_query.orderBy,
959 1));
960
961 auto& record = optionalRecord.emplace();
962 auto reader = stmt.ExecuteWithVariants(_boundInputs);
963 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
964 if (outputColumnsBound)
965#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
966 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
967#else
968 reader.BindOutputColumns(&(record.*ReferencedFields)...);
969#endif
970 if (!reader.FetchRow())
971 return std::nullopt;
972 if (!outputColumnsBound)
973 {
974 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
975 detail::GetAllColumns<ElementMask>(reader, record);
976 }
977
978 if constexpr (QueryOptions.loadRelations)
979 _dm.ConfigureRelationAutoLoading(record);
980
981 return optionalRecord;
982}
983
984template <typename Record, typename Derived, DataMapperOptions QueryOptions>
986{
987 auto records = std::vector<Record> {};
988 auto stmt = SqlStatement { _dm.Connection() };
989 records.reserve(n);
990 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
991 _fields,
992 RecordTableName<Record>,
993 this->_query.searchCondition.tableAlias,
994 this->_query.searchCondition.tableJoins,
995 this->_query.searchCondition.condition,
996 this->_query.orderBy,
997 n));
998 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
999
1000 if constexpr (QueryOptions.loadRelations)
1001 {
1002 for (auto& record: records)
1003 _dm.ConfigureRelationAutoLoading(record);
1004 }
1005 return records;
1006}
1007
1008template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1009std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::Range(size_t offset, size_t limit)
1010{
1011 auto records = std::vector<Record> {};
1012 auto stmt = SqlStatement { _dm.Connection() };
1013 records.reserve(limit);
1014 stmt.Prepare(
1015 _formatter.SelectRange(this->_query.distinct,
1016 _fields,
1017 RecordTableName<Record>,
1018 this->_query.searchCondition.tableAlias,
1019 this->_query.searchCondition.tableJoins,
1020 this->_query.searchCondition.condition,
1021 !this->_query.orderBy.empty()
1022 ? this->_query.orderBy
1023 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1024 this->_query.groupBy,
1025 offset,
1026 limit));
1027 Derived::ReadResults(stmt.Connection().ServerType(), stmt.ExecuteWithVariants(_boundInputs), &records);
1028 if constexpr (QueryOptions.loadRelations)
1029 {
1030 for (auto& record: records)
1031 _dm.ConfigureRelationAutoLoading(record);
1032 }
1033 return records;
1034}
1035
1036template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1037template <auto... ReferencedFields>
1038std::vector<Record> SqlCoreDataMapperQueryBuilder<Record, Derived, QueryOptions>::Range(size_t offset, size_t limit)
1039{
1040 auto records = std::vector<Record> {};
1041 auto stmt = SqlStatement { _dm.Connection() };
1042 records.reserve(limit);
1043 stmt.Prepare(
1044 _formatter.SelectRange(this->_query.distinct,
1045 FullyQualifiedNamesOf<ReferencedFields...>.string_view(),
1046 RecordTableName<Record>,
1047 this->_query.searchCondition.tableAlias,
1048 this->_query.searchCondition.tableJoins,
1049 this->_query.searchCondition.condition,
1050 !this->_query.orderBy.empty()
1051 ? this->_query.orderBy
1052 : std::format(" ORDER BY \"{}\" ASC", FieldNameAt<RecordPrimaryKeyIndex<Record>, Record>),
1053 this->_query.groupBy,
1054 offset,
1055 limit));
1056
1057 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1058 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1059 while (true)
1060 {
1061 auto& record = records.emplace_back();
1062 if (outputColumnsBound)
1063#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1064 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1065#else
1066 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1067#endif
1068 if (!reader.FetchRow())
1069 {
1070 records.pop_back();
1071 break;
1072 }
1073 if (!outputColumnsBound)
1074 {
1075 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1076 detail::GetAllColumns<ElementMask>(reader, record);
1077 }
1078 }
1079
1080 if constexpr (QueryOptions.loadRelations)
1081 {
1082 for (auto& record: records)
1083 _dm.ConfigureRelationAutoLoading(record);
1084 }
1085
1086 return records;
1087}
1088
1089template <typename Record, typename Derived, DataMapperOptions QueryOptions>
1090template <auto... ReferencedFields>
1092{
1093 auto records = std::vector<Record> {};
1094 auto stmt = SqlStatement { _dm.Connection() };
1095 records.reserve(n);
1096 stmt.Prepare(_formatter.SelectFirst(this->_query.distinct,
1097 FullyQualifiedNamesOf<ReferencedFields...>.string_view(),
1098 RecordTableName<Record>,
1099 this->_query.searchCondition.tableAlias,
1100 this->_query.searchCondition.tableJoins,
1101 this->_query.searchCondition.condition,
1102 this->_query.orderBy,
1103 n));
1104
1105 auto reader = stmt.ExecuteWithVariants(_boundInputs);
1106 auto const outputColumnsBound = detail::CanSafelyBindOutputColumns<Record>(stmt.Connection().ServerType());
1107 while (true)
1108 {
1109 auto& record = records.emplace_back();
1110 if (outputColumnsBound)
1111#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1112 reader.BindOutputColumns(&(record.[:ReferencedFields:])...);
1113#else
1114 reader.BindOutputColumns(&(record.*ReferencedFields)...);
1115#endif
1116 if (!reader.FetchRow())
1117 {
1118 records.pop_back();
1119 break;
1120 }
1121 if (!outputColumnsBound)
1122 {
1123 using ElementMask = std::integer_sequence<size_t, MemberIndexOf<ReferencedFields>...>;
1124 detail::GetAllColumns<ElementMask>(reader, record);
1125 }
1126 }
1127
1128 if constexpr (QueryOptions.loadRelations)
1129 {
1130 for (auto& record: records)
1131 _dm.ConfigureRelationAutoLoading(record);
1132 }
1133
1134 return records;
1135}
1136
1137template <typename Record, DataMapperOptions QueryOptions>
1139 SqlResultCursor reader,
1140 std::vector<Record>* records)
1141{
1142 while (true)
1143 {
1144 Record& record = records->emplace_back();
1145 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1146 {
1147 records->pop_back();
1148 break;
1149 }
1150 }
1151}
1152
1153template <typename Record, DataMapperOptions QueryOptions>
1154void SqlAllFieldsQueryBuilder<Record, QueryOptions>::ReadResult(SqlServerType sqlServerType,
1155 SqlResultCursor reader,
1156 std::optional<Record>* optionalRecord)
1157{
1158 Record& record = optionalRecord->emplace();
1159 if (!detail::ReadSingleResult(sqlServerType, reader, record))
1160 optionalRecord->reset();
1161}
1162
1163template <typename FirstRecord, typename SecondRecord, DataMapperOptions QueryOptions>
1164void SqlAllFieldsQueryBuilder<std::tuple<FirstRecord, SecondRecord>, QueryOptions>::ReadResults(
1165 SqlServerType sqlServerType, SqlResultCursor reader, std::vector<RecordType>* records)
1166{
1167 while (true)
1168 {
1169 auto& record = records->emplace_back();
1170 auto& [firstRecord, secondRecord] = record;
1171
1172 using FirstRecordType = std::remove_cvref_t<decltype(firstRecord)>;
1173 using SecondRecordType = std::remove_cvref_t<decltype(secondRecord)>;
1174
1175 auto const outputColumnsBoundFirst = detail::CanSafelyBindOutputColumns<FirstRecordType>(sqlServerType);
1176 auto const outputColumnsBoundSecond = detail::CanSafelyBindOutputColumns<SecondRecordType>(sqlServerType);
1177 auto const canSafelyBindAll = outputColumnsBoundFirst && outputColumnsBoundSecond;
1178
1179 if (canSafelyBindAll)
1180 {
1181 detail::BindAllOutputColumnsWithOffset(reader, firstRecord, 1);
1182 detail::BindAllOutputColumnsWithOffset(reader, secondRecord, 1 + Reflection::CountMembers<FirstRecord>);
1183 }
1184
1185 if (!reader.FetchRow())
1186 {
1187 records->pop_back();
1188 break;
1189 }
1190
1191 if (!canSafelyBindAll)
1192 detail::GetAllColumns(reader, record);
1193 }
1194}
1195
1196template <typename Record>
1197std::string DataMapper::Inspect(Record const& record)
1198{
1199 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1200
1201 std::string str;
1202 Reflection::CallOnMembers(record, [&str]<typename Name, typename Value>(Name const& name, Value const& value) {
1203 if (!str.empty())
1204 str += '\n';
1205
1206 if constexpr (FieldWithStorage<Value>)
1207 {
1208 if constexpr (Value::IsOptional)
1209 {
1210 if (!value.Value().has_value())
1211 {
1212 str += std::format("{} {} := <nullopt>", Reflection::TypeNameOf<Value>, name);
1213 }
1214 else
1215 {
1216 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value().value());
1217 }
1218 }
1219 else if constexpr (IsBelongsTo<Value>)
1220 {
1221 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.Value());
1222 }
1223 else if constexpr (std::same_as<typename Value::ValueType, char>)
1224 {
1225 }
1226 else
1227 {
1228 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value.InspectValue());
1229 }
1230 }
1231 else if constexpr (!IsHasMany<Value> && !IsHasManyThrough<Value> && !IsHasOneThrough<Value> && !IsBelongsTo<Value>)
1232 str += std::format("{} {} := {}", Reflection::TypeNameOf<Value>, name, value);
1233 });
1234 return "{\n" + std::move(str) + "\n}";
1235}
1236
1237template <typename Record>
1238std::vector<std::string> DataMapper::CreateTableString(SqlServerType serverType)
1239{
1240 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1241
1242 auto migration = SqlQueryBuilder(*SqlQueryFormatter::Get(serverType)).Migration();
1243 auto createTable = migration.CreateTable(RecordTableName<Record>);
1244 detail::PopulateCreateTableBuilder<Record>(createTable);
1245 return migration.GetPlan().ToSql();
1246}
1247
1248template <typename FirstRecord, typename... MoreRecords>
1249std::vector<std::string> DataMapper::CreateTablesString(SqlServerType serverType)
1250{
1251 std::vector<std::string> output;
1252 auto const append = [&output](auto const& sql) {
1253 output.insert(output.end(), sql.begin(), sql.end());
1254 };
1255 append(CreateTableString<FirstRecord>(serverType));
1256 (append(CreateTableString<MoreRecords>(serverType)), ...);
1257 return output;
1258}
1259
1260template <typename Record>
1262{
1263 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1264
1265 ZoneScopedN("DataMapper::CreateTable");
1266 ZoneTextObject(RecordTableName<Record>);
1267
1268 auto const sqlQueryStrings = CreateTableString<Record>(_connection.ServerType());
1269 for (auto const& sqlQueryString: sqlQueryStrings) [[maybe_unused]]
1270 auto cursor = _stmt.ExecuteDirect(sqlQueryString);
1271}
1272
1273template <typename FirstRecord, typename... MoreRecords>
1275{
1276 CreateTable<FirstRecord>();
1277 (CreateTable<MoreRecords>(), ...);
1278}
1279
1280template <typename Record>
1281std::optional<RecordPrimaryKeyType<Record>> DataMapper::GenerateAutoAssignPrimaryKey(Record const& record)
1282{
1283 std::optional<RecordPrimaryKeyType<Record>> result;
1284 Reflection::EnumerateMembers(
1285 record, [this, &result]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
1286 if constexpr (IsField<PrimaryKeyType> && IsPrimaryKey<PrimaryKeyType>
1287 && detail::IsAutoAssignPrimaryKeyField<PrimaryKeyType>::value)
1288 {
1289 using ValueType = PrimaryKeyType::ValueType;
1290 if constexpr (std::same_as<ValueType, SqlGuid>)
1291 {
1292 if (!primaryKeyField.Value())
1293 [&](auto& res) {
1294 res.emplace(SqlGuid::Create());
1295 }(result);
1296 }
1297 else if constexpr (requires { ValueType {} + 1; })
1298 {
1299 if (primaryKeyField.Value() == ValueType {})
1300 {
1301 auto maxId = SqlStatement { _connection }.ExecuteDirectScalar<ValueType>(
1302 std::format(R"sql(SELECT MAX("{}") FROM "{}")sql",
1303 FieldNameAt<PrimaryKeyIndex, Record>,
1304 RecordTableName<Record>));
1305 result = maxId.value_or(ValueType {}) + 1;
1306 }
1307 }
1308 }
1309 });
1310 return result;
1311}
1312
1313template <DataMapper::PrimaryKeySource UsePkOverride, typename Record>
1314RecordPrimaryKeyType<Record> DataMapper::CreateInternal(
1315 Record const& record,
1316 std::optional<std::conditional_t<std::is_void_v<RecordPrimaryKeyType<Record>>, int, RecordPrimaryKeyType<Record>>>
1317 pkOverride)
1318{
1319 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1320
1321 auto query = _connection.Query(RecordTableName<Record>).Insert(nullptr);
1322
1323#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1324 constexpr auto ctx = std::meta::access_context::current();
1325 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1326 {
1327 using FieldType = typename[:std::meta::type_of(el):];
1328 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1329 query.Set(FieldNameOf<el>, SqlWildcard);
1330 }
1331#else
1332 Reflection::EnumerateMembers(record, [&query]<auto I, typename FieldType>(FieldType const& /*field*/) {
1333 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1334 query.Set(FieldNameAt<I, Record>, SqlWildcard);
1335 });
1336#endif
1337
1338 _stmt.Prepare(query);
1339
1340#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1341 int i = 1;
1342 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1343 {
1344 using FieldType = typename[:std::meta::type_of(el):];
1345 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1346 {
1347 if constexpr (IsPrimaryKey<FieldType> && UsePkOverride == PrimaryKeySource::Override)
1348 _stmt.BindInputParameter(i++, *pkOverride, std::meta::identifier_of(el));
1349 else
1350 _stmt.BindInputParameter(i++, record.[:el:], std::meta::identifier_of(el));
1351 }
1352 }
1353#else
1354 Reflection::CallOnMembers(record,
1355 [this, &pkOverride, i = SQLSMALLINT { 1 }]<typename Name, typename FieldType>(
1356 Name const& name, FieldType const& field) mutable {
1357 if constexpr (SqlInputParameterBinder<FieldType> && !IsAutoIncrementPrimaryKey<FieldType>)
1358 {
1359 if constexpr (IsPrimaryKey<FieldType> && UsePkOverride == PrimaryKeySource::Override)
1360 _stmt.BindInputParameter(i++, *pkOverride, name);
1361 else
1362 _stmt.BindInputParameter(i++, field, name);
1363 }
1364 });
1365#endif
1366 [[maybe_unused]] auto cursor = _stmt.Execute();
1367
1368 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1369 return { _stmt.LastInsertId(RecordTableName<Record>) };
1370 else if constexpr (HasPrimaryKey<Record>)
1371 {
1372 if constexpr (UsePkOverride == PrimaryKeySource::Override)
1373 return *pkOverride; // NOLINT(bugprone-unchecked-optional-access)
1374 else
1375 return RecordPrimaryKeyOf(record).Value();
1376 }
1377
1378 return {};
1379}
1380
1381template <typename Record>
1382RecordPrimaryKeyType<Record> DataMapper::CreateExplicit(Record const& record)
1383{
1384 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1385 return CreateInternal<PrimaryKeySource::Record>(record);
1386}
1387
1388template <DataMapperOptions QueryOptions, typename Record>
1389RecordPrimaryKeyType<Record> DataMapper::CreateCopyOf(Record const& originalRecord)
1390{
1391 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1392 static_assert(HasPrimaryKey<Record>, "CreateCopyOf requires a record type with a primary key");
1393
1394 auto generatedKey = GenerateAutoAssignPrimaryKey(originalRecord);
1395 if (generatedKey)
1396 return CreateInternal<PrimaryKeySource::Override>(originalRecord, generatedKey);
1397
1398 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1399 return CreateInternal<PrimaryKeySource::Record>(originalRecord);
1400
1401 return CreateInternal<PrimaryKeySource::Override>(originalRecord, RecordPrimaryKeyType<Record> {});
1402}
1403
1404template <DataMapperOptions QueryOptions, typename Record>
1405RecordPrimaryKeyType<Record> DataMapper::Create(Record& record)
1406{
1407 static_assert(!std::is_const_v<Record>);
1408 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1409
1410 ZoneScopedN("DataMapper::Create");
1411 ZoneTextObject(RecordTableName<Record>);
1412
1413 auto generatedKey = GenerateAutoAssignPrimaryKey(record);
1414 if (generatedKey)
1415 SetId(record, *generatedKey);
1416
1417 auto pk = CreateInternal<PrimaryKeySource::Record>(record);
1418
1419 if constexpr (HasAutoIncrementPrimaryKey<Record>)
1420 SetId(record, pk);
1421
1422 SetModifiedState<ModifiedState::NotModified>(record);
1423
1424 if constexpr (QueryOptions.loadRelations)
1426
1427 if constexpr (HasPrimaryKey<Record>)
1428 return GetPrimaryKeyField(record);
1429}
1430
1431template <typename Record>
1432bool DataMapper::IsModified(Record const& record) const noexcept
1433{
1434 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1435
1436 bool modified = false;
1437
1438#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1439 auto constexpr ctx = std::meta::access_context::current();
1440 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1441 {
1442 if constexpr (requires { record.[:el:].IsModified(); })
1443 {
1444 modified = modified || record.[:el:].IsModified();
1445 }
1446 }
1447#else
1448 Reflection::CallOnMembers(record, [&modified](auto const& /*name*/, auto const& field) {
1449 if constexpr (requires { field.IsModified(); })
1450 {
1451 modified = modified || field.IsModified();
1452 }
1453 });
1454#endif
1455
1456 return modified;
1457}
1458
1459template <typename Record>
1460void DataMapper::Update(Record& record)
1461{
1462 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1463
1464 ZoneScopedN("DataMapper::Update");
1465 ZoneTextObject(RecordTableName<Record>);
1466
1467 auto query = _connection.Query(RecordTableName<Record>).Update();
1468
1469#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1470 auto constexpr ctx = std::meta::access_context::current();
1471 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1472 {
1473 using FieldType = typename[:std::meta::type_of(el):];
1474 if constexpr (FieldWithStorage<FieldType>)
1475 {
1476 if (record.[:el:].IsModified())
1477 query.Set(FieldNameOf<el>, SqlWildcard);
1478 if constexpr (IsPrimaryKey<FieldType>)
1479 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
1480 }
1481 }
1482#else
1483 Reflection::CallOnMembersWithoutName(record, [&query]<size_t I, typename FieldType>(FieldType const& field) {
1484 if (field.IsModified())
1485 query.Set(FieldNameAt<I, Record>, SqlWildcard);
1486 // for some reason compiler do not want to properly deduce FieldType, so here we
1487 // directly infer the type from the Record type and index
1488 if constexpr (IsPrimaryKey<Reflection::MemberTypeOf<I, Record>>)
1489 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
1490 });
1491#endif
1492 _stmt.Prepare(query);
1493
1494 SQLSMALLINT i = 1;
1495
1496#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1497 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1498 {
1499 if (record.[:el:].IsModified())
1500 {
1501 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
1502 }
1503 }
1504
1505 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1506 {
1507 using FieldType = typename[:std::meta::type_of(el):];
1508 if constexpr (FieldType::IsPrimaryKey)
1509 {
1510 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
1511 }
1512 }
1513#else
1514 // Bind the SET clause
1515 Reflection::CallOnMembersWithoutName(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
1516 if (field.IsModified())
1517 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
1518 });
1519
1520 // Bind the WHERE clause
1521 Reflection::CallOnMembersWithoutName(record, [this, &i]<size_t I, typename FieldType>(FieldType const& field) {
1522 if constexpr (IsPrimaryKey<Reflection::MemberTypeOf<I, Record>>)
1523 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
1524 });
1525#endif
1526
1527 [[maybe_unused]] auto cursor = _stmt.Execute();
1528
1529 SetModifiedState<ModifiedState::NotModified>(record);
1530}
1531
1532template <typename Record>
1533std::size_t DataMapper::Delete(Record const& record)
1534{
1535 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1536
1537 ZoneScopedN("DataMapper::Delete");
1538 ZoneTextObject(RecordTableName<Record>);
1539
1540 auto query = _connection.Query(RecordTableName<Record>).Delete();
1541
1542#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1543 auto constexpr ctx = std::meta::access_context::current();
1544 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1545 {
1546 using FieldType = typename[:std::meta::type_of(el):];
1547 if constexpr (FieldType::IsPrimaryKey)
1548 std::ignore = query.Where(FieldNameOf<el>, SqlWildcard);
1549 }
1550#else
1551 Reflection::CallOnMembersWithoutName(record, [&query]<size_t I, typename FieldType>(FieldType const& /*field*/) {
1552 if constexpr (IsPrimaryKey<Reflection::MemberTypeOf<I, Record>>)
1553 std::ignore = query.Where(FieldNameAt<I, Record>, SqlWildcard);
1554 });
1555#endif
1556
1557 _stmt.Prepare(query);
1558
1559#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1560 SQLSMALLINT i = 1;
1561 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
1562 {
1563 using FieldType = typename[:std::meta::type_of(el):];
1564 if constexpr (FieldType::IsPrimaryKey)
1565 {
1566 _stmt.BindInputParameter(i++, record.[:el:].Value(), FieldNameOf<el>);
1567 }
1568 }
1569#else
1570 // Bind the WHERE clause
1571 Reflection::CallOnMembersWithoutName(
1572 record, [this, i = SQLSMALLINT { 1 }]<size_t I, typename FieldType>(FieldType const& field) mutable {
1573 if constexpr (IsPrimaryKey<Reflection::MemberTypeOf<I, Record>>)
1574 _stmt.BindInputParameter(i++, field.Value(), FieldNameAt<I, Record>);
1575 });
1576#endif
1577
1578 auto cursor = _stmt.Execute();
1579
1580 return cursor.NumRowsAffected();
1581}
1582
1583template <typename Record, DataMapperOptions QueryOptions, typename... PrimaryKeyTypes>
1584std::optional<Record> DataMapper::QuerySingle(PrimaryKeyTypes&&... primaryKeys)
1585{
1586 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1587
1588 ZoneScopedN("DataMapper::QuerySingle(PK)");
1589 ZoneTextObject(RecordTableName<Record>);
1590
1591 auto queryBuilder = _connection.Query(RecordTableName<Record>).Select();
1592
1593 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1594 if constexpr (FieldWithStorage<FieldType>)
1595 {
1596 queryBuilder.Field(FieldNameAt<I, Record>);
1597
1598 if constexpr (FieldType::IsPrimaryKey)
1599 std::ignore = queryBuilder.Where(FieldNameAt<I, Record>, SqlWildcard);
1600 }
1601 });
1602
1603 _stmt.Prepare(queryBuilder.First());
1604 auto reader = _stmt.Execute(std::forward<PrimaryKeyTypes>(primaryKeys)...);
1605
1606 auto resultRecord = std::optional<Record> { Record {} };
1607 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
1608 return std::nullopt;
1609
1610 if (resultRecord)
1611 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
1612
1613 if constexpr (QueryOptions.loadRelations)
1614 {
1615 if (resultRecord)
1616 ConfigureRelationAutoLoading(*resultRecord);
1617 }
1618
1619 return resultRecord;
1620}
1621
1622template <typename Record, typename... Args>
1623std::optional<Record> DataMapper::QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args)
1624{
1625 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1626
1627 ZoneScopedN("DataMapper::QuerySingle(Builder)");
1628 ZoneTextObject(RecordTableName<Record>);
1629
1630 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1631 if constexpr (FieldWithStorage<FieldType>)
1632 selectQuery.Field(SqlQualifiedTableColumnName { RecordTableName<Record>, FieldNameAt<I, Record> });
1633 });
1634 auto const composedSql = selectQuery.First().ToSql();
1635 ZoneTextObject(composedSql);
1636 _stmt.Prepare(composedSql);
1637 auto reader = _stmt.Execute(std::forward<Args>(args)...);
1638
1639 auto resultRecord = std::optional<Record> { Record {} };
1640 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
1641 return std::nullopt;
1642
1643 if (resultRecord)
1644 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
1645
1646 return resultRecord;
1647}
1648
1649// TODO: Provide Query(QueryBuilder, ...) method variant
1650
1651/// Queries multiple records from the database using a composed query and optional input parameters.
1652template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1653inline LIGHTWEIGHT_FORCE_INLINE std::vector<Record> DataMapper::Query(
1654 SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters)
1655{
1656 static_assert(DataMapperRecord<Record> || std::same_as<Record, SqlVariantRow>, "Record must satisfy DataMapperRecord");
1657
1658 ZoneScopedN("DataMapper::Query(ComposedQuery)");
1659 return Query<Record, QueryOptions>(selectQuery.ToSql(), std::forward<InputParameters>(inputParameters)...);
1660}
1661
1662template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1663std::vector<Record> DataMapper::Query(std::string_view sqlQueryString, InputParameters&&... inputParameters)
1664{
1665 ZoneScopedN("DataMapper::Query(string)");
1666 ZoneTextObject(sqlQueryString);
1667
1668 auto result = std::vector<Record> {};
1669 if constexpr (std::same_as<Record, SqlVariantRow>)
1670 {
1671 _stmt.Prepare(sqlQueryString);
1672 SqlResultCursor cursor = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1673 size_t const numResultColumns = cursor.NumColumnsAffected();
1674 while (cursor.FetchRow())
1675 {
1676 auto& record = result.emplace_back();
1677 record.reserve(numResultColumns);
1678 for (auto const i: std::views::iota(1U, numResultColumns + 1))
1679 record.emplace_back(cursor.GetColumn<SqlVariant>(static_cast<SQLUSMALLINT>(i)));
1680 }
1681 }
1682 else
1683 {
1684 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1685
1686 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
1687
1688 _stmt.Prepare(sqlQueryString);
1689 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1690
1691 for (;;)
1692 {
1693 auto& record = result.emplace_back();
1694
1695 if (canSafelyBindOutputColumns)
1696 BindOutputColumns(record, reader);
1697
1698 if (!reader.FetchRow())
1699 break;
1700
1701 if (!canSafelyBindOutputColumns)
1702 detail::GetAllColumns(reader, record);
1703 }
1704
1705 // Drop the last record, which we failed to fetch (End of result set).
1706 result.pop_back();
1707
1708 for (auto& record: result)
1709 {
1710 SetModifiedState<ModifiedState::NotModified>(record);
1711 if constexpr (QueryOptions.loadRelations)
1713 }
1714 }
1715
1716 return result;
1717}
1718
1719template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions>
1720 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
1721std::vector<std::tuple<First, Second, Rest...>> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery)
1722{
1723 using value_type = std::tuple<First, Second, Rest...>;
1724 auto result = std::vector<value_type> {};
1725
1726 ZoneScopedN("DataMapper::Query(ComposedQuery -> tuple)");
1727 auto const tupleSql = selectQuery.ToSql();
1728 ZoneTextObject(tupleSql);
1729 _stmt.Prepare(tupleSql);
1730 auto reader = _stmt.Execute();
1731
1732 constexpr auto calculateOffset = []<size_t I, typename Tuple>() {
1733 size_t offset = 1;
1734
1735 if constexpr (I > 0)
1736 {
1737 [&]<size_t... Indices>(std::index_sequence<Indices...>) {
1738 ((Indices < I ? (offset += Reflection::CountMembers<std::tuple_element_t<Indices, Tuple>>) : 0), ...);
1739 }(std::make_index_sequence<I> {});
1740 }
1741 return offset;
1742 };
1743
1744 auto const BindElements = [&](auto& record) {
1745 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1746 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
1747 auto& element = std::get<I>(record);
1748 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
1749 this->BindOutputColumns<TupleElement, offset>(element, reader);
1750 });
1751 };
1752
1753 auto const GetElements = [&](auto& record) {
1754 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1755 auto& element = std::get<I>(record);
1756 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
1757 detail::GetAllColumns(reader, element, offset - 1);
1758 });
1759 };
1760
1761 bool const canSafelyBindOutputColumns = [&]() {
1762 bool result = true;
1763 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1764 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
1765 result &= detail::CanSafelyBindOutputColumns<TupleElement>(_stmt.Connection().ServerType());
1766 });
1767 return result;
1768 }();
1769
1770 for (;;)
1771 {
1772 auto& record = result.emplace_back();
1773
1774 if (canSafelyBindOutputColumns)
1775 BindElements(record);
1776
1777 if (!reader.FetchRow())
1778 break;
1779
1780 if (!canSafelyBindOutputColumns)
1781 GetElements(record);
1782 }
1783
1784 // Drop the last record, which we failed to fetch (End of result set).
1785 result.pop_back();
1786
1787 for (auto& record: result)
1788 {
1789 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1790 auto& element = std::get<I>(record);
1791 SetModifiedState<ModifiedState::NotModified>(element);
1792 if constexpr (QueryOptions.loadRelations)
1793 {
1795 }
1796 });
1797 }
1798
1799 return result;
1800}
1801
1802template <typename ElementMask, typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1803std::vector<Record> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery,
1804 InputParameters&&... inputParameters)
1805{
1806 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1807
1808 ZoneScopedN("DataMapper::Query(ComposedQuery, ElementMask)");
1809 auto const maskedSql = selectQuery.ToSql();
1810 ZoneTextObject(maskedSql);
1811 _stmt.Prepare(maskedSql);
1812
1813 auto records = std::vector<Record> {};
1814
1815 // TODO: We could optimize this further by only considering ElementMask fields in Record.
1816 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
1817
1818 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1819
1820 for (;;)
1821 {
1822 auto& record = records.emplace_back();
1823
1824 if (canSafelyBindOutputColumns)
1825 BindOutputColumns<ElementMask>(record, reader);
1826
1827 if (!reader.FetchRow())
1828 break;
1829
1830 if (!canSafelyBindOutputColumns)
1831 detail::GetAllColumns<ElementMask>(reader, record);
1832 }
1833
1834 // Drop the last record, which we failed to fetch (End of result set).
1835 records.pop_back();
1836
1837 for (auto& record: records)
1838 {
1839 SetModifiedState<ModifiedState::NotModified>(record);
1840 if constexpr (QueryOptions.loadRelations)
1842 }
1843
1844 return records;
1845}
1846
1847template <DataMapper::ModifiedState state, typename Record>
1848void DataMapper::SetModifiedState(Record& record) noexcept
1849{
1850 static_assert(!std::is_const_v<Record>);
1851 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1852
1853 Reflection::EnumerateMembers(record, []<size_t I, typename FieldType>(FieldType& field) {
1854 if constexpr (requires { field.SetModified(false); })
1855 {
1856 if constexpr (state == ModifiedState::Modified)
1857 field.SetModified(true);
1858 else
1859 field.SetModified(false);
1860 }
1861 });
1862}
1863
1864template <typename Record, typename Callable>
1865inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Record& record, Callable const& callable)
1866{
1867 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1868
1869 Reflection::EnumerateMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
1870 if constexpr (IsField<FieldType>)
1871 {
1872 if constexpr (FieldType::IsPrimaryKey)
1873 {
1874 return callable.template operator()<I, FieldType>(field);
1875 }
1876 }
1877 });
1878}
1879
1880template <typename Record, typename Callable>
1881inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Callable const& callable)
1882{
1883 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1884
1885 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1886 if constexpr (IsField<FieldType>)
1887 {
1888 if constexpr (FieldType::IsPrimaryKey)
1889 {
1890 return callable.template operator()<I, FieldType>();
1891 }
1892 }
1893 });
1894}
1895
1896template <typename Record, typename Callable>
1897inline LIGHTWEIGHT_FORCE_INLINE void CallOnBelongsTo(Callable const& callable)
1898{
1899 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1900
1901 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1902 if constexpr (IsBelongsTo<FieldType>)
1903 {
1904 return callable.template operator()<I, FieldType>();
1905 }
1906 });
1907}
1908
1909template <typename FieldType>
1910std::optional<typename FieldType::ReferencedRecord> DataMapper::LoadBelongsTo(FieldType::ValueType value)
1911{
1912 using ReferencedRecord = FieldType::ReferencedRecord;
1913
1914 ZoneScopedN("DataMapper::LoadBelongsTo");
1915 ZoneTextObject(RecordTableName<ReferencedRecord>);
1916
1917 std::optional<ReferencedRecord> record { std::nullopt };
1918
1919#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1920 auto constexpr ctx = std::meta::access_context::current();
1921 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^ReferencedRecord, ctx)))
1922 {
1923 using BelongsToFieldType = typename[:std::meta::type_of(el):];
1924 if constexpr (IsField<BelongsToFieldType>)
1925 if constexpr (BelongsToFieldType::IsPrimaryKey)
1926 {
1927 if (auto result = QuerySingle<ReferencedRecord>(value); result)
1928 record = std::move(result);
1929 else
1931 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
1932 }
1933 }
1934#else
1935 CallOnPrimaryKey<ReferencedRecord>([&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>() {
1936 if (auto result = QuerySingle<ReferencedRecord>(value); result)
1937 record = std::move(result);
1938 else
1940 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
1941 });
1942#endif
1943 return record;
1944}
1945
1946template <size_t FieldIndex, typename Record, typename OtherRecord, typename Callable>
1947void DataMapper::CallOnHasMany(Record& record, Callable const& callback)
1948{
1949 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1950 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
1951
1952 using FieldType = HasMany<OtherRecord>;
1953 using ReferencedRecord = FieldType::ReferencedRecord;
1954
1955 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
1956 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
1957 .Select()
1958 .Build([&](auto& query) {
1959 Reflection::EnumerateMembers<ReferencedRecord>(
1960 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
1961 if constexpr (FieldWithStorage<ReferencedFieldType>)
1962 {
1963 query.Field(FieldNameAt<ReferencedFieldIndex, ReferencedRecord>);
1964 }
1965 });
1966 })
1967 .Where(FieldNameAt<FieldIndex, ReferencedRecord>, SqlWildcard)
1968 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord>);
1969 callback(query, primaryKeyField);
1970 });
1971}
1972
1973template <size_t FieldIndex, typename OtherRecord>
1974SqlSelectQueryBuilder DataMapper::BuildHasManySelectQuery()
1975{
1976 return _connection.Query(RecordTableName<OtherRecord>)
1977 .Select()
1978 .Build([](auto& q) {
1979 Reflection::EnumerateMembers<OtherRecord>([&]<size_t I, typename F>() {
1980 if constexpr (FieldWithStorage<F>)
1981 q.Field(FieldNameAt<I, OtherRecord>);
1982 });
1983 })
1984 .Where(FieldNameAt<FieldIndex, OtherRecord>, SqlWildcard)
1985 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<OtherRecord>, OtherRecord>);
1986}
1987
1988template <size_t FieldIndex, typename Record, typename OtherRecord>
1989void DataMapper::LoadHasMany(Record& record, HasMany<OtherRecord>& field)
1990{
1991 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1992 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
1993
1994 ZoneScopedN("DataMapper::LoadHasMany");
1995 ZoneTextObject(RecordTableName<OtherRecord>);
1996
1997 CallOnHasMany<FieldIndex, Record, OtherRecord>(record, [&](SqlSelectQueryBuilder selectQuery, auto& primaryKeyField) {
1998 field.Emplace(detail::ToSharedPtrList(Query<OtherRecord>(selectQuery.All(), primaryKeyField.Value())));
1999 });
2000}
2001
2002template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2003void DataMapper::LoadHasOneThrough(Record& record, HasOneThrough<ReferencedRecord, ThroughRecord>& field)
2004{
2005 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2006 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2007
2008 ZoneScopedN("DataMapper::LoadHasOneThrough");
2009 ZoneTextObject(RecordTableName<ReferencedRecord>);
2010
2011 // Find the PK of Record
2012 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2013 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2014 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2015 // Find the PK of ThroughRecord
2016 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2017 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2018 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2019 // Query the ReferencedRecord where:
2020 // - the BelongsTo of ReferencedRecord points to the PK of ThroughRecord,
2021 // - and the BelongsTo of ThroughRecord points to the PK of Record
2022 auto query =
2023 _connection.Query(RecordTableName<ReferencedRecord>)
2024 .Select()
2025 .Build([&](auto& query) {
2026 Reflection::EnumerateMembers<ReferencedRecord>(
2027 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2028 if constexpr (FieldWithStorage<ReferencedFieldType>)
2029 {
2030 query.Field(SqlQualifiedTableColumnName {
2031 RecordTableName<ReferencedRecord>,
2032 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2033 }
2034 });
2035 })
2036 .InnerJoin(RecordTableName<ThroughRecord>,
2037 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2038 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2039 .InnerJoin(RecordTableName<Record>,
2040 FieldNameAt<PrimaryKeyIndex, Record>,
2041 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2042 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2043 .Where(
2044 SqlQualifiedTableColumnName {
2045 RecordTableName<Record>,
2046 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2047 },
2048 SqlWildcard);
2049 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), primaryKeyField.Value()); link)
2050 {
2051 field.EmplaceRecord(std::make_shared<ReferencedRecord>(std::move(*link)));
2052 }
2053 });
2054 });
2055 });
2056 });
2057}
2058
2059template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue>
2060std::shared_ptr<ReferencedRecord> DataMapper::LoadHasOneThroughByPK(PKValue const& pkValue)
2061{
2062 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2063
2064 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2065 std::shared_ptr<ReferencedRecord> result;
2066
2067 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2068 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2069 // Find the PK of ThroughRecord
2070 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2071 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2072 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2073 auto query =
2074 _connection.Query(RecordTableName<ReferencedRecord>)
2075 .Select()
2076 .Build([&](auto& query) {
2077 Reflection::EnumerateMembers<ReferencedRecord>(
2078 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2079 if constexpr (FieldWithStorage<ReferencedFieldType>)
2080 {
2081 query.Field(SqlQualifiedTableColumnName {
2082 RecordTableName<ReferencedRecord>,
2083 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2084 }
2085 });
2086 })
2087 .InnerJoin(RecordTableName<ThroughRecord>,
2088 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2089 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2090 .InnerJoin(RecordTableName<Record>,
2091 FieldNameAt<PrimaryKeyIndex, Record>,
2092 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2093 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2094 .Where(
2095 SqlQualifiedTableColumnName {
2096 RecordTableName<Record>,
2097 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2098 },
2099 SqlWildcard);
2100 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), pkValue); link)
2101 result = std::make_shared<ReferencedRecord>(std::move(*link));
2102 });
2103 });
2104 });
2105
2106 return result;
2107}
2108
2109template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename Callable>
2110void DataMapper::CallOnHasManyThrough(Record& record, Callable const& callback)
2111{
2112 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2113
2114 // Find the PK of Record
2115 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2116 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2117 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2118 using ThroughBelongsToRecordFieldType = Reflection::MemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2119 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2120 {
2121 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2122 CallOnBelongsTo<ThroughRecord>(
2123 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2124 using ThroughBelongsToReferenceRecordFieldType =
2125 Reflection::MemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2126 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2127 ReferencedRecord>)
2128 {
2129 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2130 .Select()
2131 .Build([&](auto& query) {
2132 Reflection::EnumerateMembers<ReferencedRecord>(
2133 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2134 if constexpr (FieldWithStorage<ReferencedFieldType>)
2135 {
2136 query.Field(SqlQualifiedTableColumnName {
2137 RecordTableName<ReferencedRecord>,
2138 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2139 }
2140 });
2141 })
2142 .InnerJoin(RecordTableName<ThroughRecord>,
2143 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2144 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2145 FieldNameAt<PrimaryKeyIndex, Record> })
2146 .Where(
2147 SqlQualifiedTableColumnName {
2148 RecordTableName<ThroughRecord>,
2149 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2150 },
2151 SqlWildcard);
2152 callback(query, primaryKeyField);
2153 }
2154 });
2155 }
2156 });
2157 });
2158}
2159
2160template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue, typename Callable>
2161void DataMapper::CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback)
2162{
2163 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2164
2165 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2166
2167 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2168 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2169 using ThroughBelongsToRecordFieldType = Reflection::MemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2170 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2171 {
2172 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2173 CallOnBelongsTo<ThroughRecord>(
2174 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2175 using ThroughBelongsToReferenceRecordFieldType =
2176 Reflection::MemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2177 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2178 ReferencedRecord>)
2179 {
2180 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2181 .Select()
2182 .Build([&](auto& query) {
2183 Reflection::EnumerateMembers<ReferencedRecord>(
2184 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2185 if constexpr (FieldWithStorage<ReferencedFieldType>)
2186 {
2187 query.Field(SqlQualifiedTableColumnName {
2188 RecordTableName<ReferencedRecord>,
2189 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2190 }
2191 });
2192 })
2193 .InnerJoin(RecordTableName<ThroughRecord>,
2194 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2195 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2196 FieldNameAt<PrimaryKeyIndex, Record> })
2197 .Where(
2198 SqlQualifiedTableColumnName {
2199 RecordTableName<ThroughRecord>,
2200 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2201 },
2202 SqlWildcard);
2203 callback(query, pkValue);
2204 }
2205 });
2206 }
2207 });
2208}
2209
2210template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2211void DataMapper::LoadHasManyThrough(Record& record, HasManyThrough<ReferencedRecord, ThroughRecord>& field)
2212{
2213 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2214
2215 ZoneScopedN("DataMapper::LoadHasManyThrough");
2216 ZoneTextObject(RecordTableName<ReferencedRecord>);
2217
2218 CallOnHasManyThrough<ReferencedRecord, ThroughRecord>(
2219 record, [&](SqlSelectQueryBuilder& selectQuery, auto& primaryKeyField) {
2220 field.Emplace(detail::ToSharedPtrList(Query<ReferencedRecord>(selectQuery.All(), primaryKeyField.Value())));
2221 });
2222}
2223
2224template <typename Record>
2225void DataMapper::LoadRelations(Record& record)
2226{
2227 static_assert(!std::is_const_v<Record>);
2228 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2229
2230 ZoneScopedN("DataMapper::LoadRelations");
2231 ZoneTextObject(RecordTableName<Record>);
2232
2233#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2234 constexpr auto ctx = std::meta::access_context::current();
2235 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2236 {
2237 using FieldType = typename[:std::meta::type_of(el):];
2238 if constexpr (IsBelongsTo<FieldType>)
2239 {
2240 auto& field = record.[:el:];
2241 field = LoadBelongsTo<FieldType>(field.Value());
2242 }
2243 else if constexpr (IsHasMany<FieldType>)
2244 {
2245 LoadHasMany<el>(record, record.[:el:]);
2246 }
2247 else if constexpr (IsHasOneThrough<FieldType>)
2248 {
2249 LoadHasOneThrough(record, record.[:el:]);
2250 }
2251 else if constexpr (IsHasManyThrough<FieldType>)
2252 {
2253 LoadHasManyThrough(record, record.[:el:]);
2254 }
2255 }
2256#else
2257 Reflection::EnumerateMembers(record, [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2258 if constexpr (IsBelongsTo<FieldType>)
2259 {
2260 field = LoadBelongsTo<FieldType>(field.Value());
2261 }
2262 else if constexpr (IsHasMany<FieldType>)
2263 {
2264 LoadHasMany<FieldIndex>(record, field);
2265 }
2266 else if constexpr (IsHasOneThrough<FieldType>)
2267 {
2268 LoadHasOneThrough(record, field);
2269 }
2270 else if constexpr (IsHasManyThrough<FieldType>)
2271 {
2272 LoadHasManyThrough(record, field);
2273 }
2274 });
2275#endif
2276}
2277
2278/// Sets the primary key field(s) of the given record to the specified id value.
2279template <typename Record, typename ValueType>
2280inline LIGHTWEIGHT_FORCE_INLINE void DataMapper::SetId(Record& record, ValueType&& id)
2281{
2282 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2283 // static_assert(HasPrimaryKey<Record>);
2284
2285#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2286
2287 auto constexpr ctx = std::meta::access_context::current();
2288 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2289 {
2290 using FieldType = typename[:std::meta::type_of(el):];
2291 if constexpr (IsField<FieldType>)
2292 {
2293 if constexpr (FieldType::IsPrimaryKey)
2294 {
2295 record.[:el:] = std::forward<ValueType>(id);
2296 }
2297 }
2298 }
2299#else
2300 Reflection::EnumerateMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
2301 if constexpr (IsField<FieldType>)
2302 {
2303 if constexpr (FieldType::IsPrimaryKey)
2304 {
2305 field = std::forward<FieldType>(id);
2306 }
2307 }
2308 });
2309#endif
2310}
2311
2312/// Binds all output columns of the record via the given cursor.
2313template <typename Record, size_t InitialOffset>
2314inline LIGHTWEIGHT_FORCE_INLINE Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2315{
2316 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2317 return BindOutputColumns<std::make_integer_sequence<size_t, Reflection::CountMembers<Record>>, Record, InitialOffset>(
2318 record, cursor);
2319}
2320
2321template <typename ElementMask, typename Record, size_t InitialOffset>
2322Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2323{
2324 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2325 static_assert(!std::is_const_v<Record>);
2326
2327#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2328 auto constexpr ctx = std::meta::access_context::current();
2329 SQLSMALLINT i = SQLSMALLINT { InitialOffset };
2330 template for (constexpr auto index: define_static_array(template_arguments_of(^^ElementMask)) | std::views::drop(1))
2331 {
2332 constexpr auto el = nonstatic_data_members_of(^^Record, ctx)[[:index:]];
2333 using FieldType = typename[:std::meta::type_of(el):];
2334 if constexpr (IsField<FieldType>)
2335 {
2336 cursor.BindOutputColumn(i++, &record.[:el:].MutableValue());
2337 }
2338 else if constexpr (SqlOutputColumnBinder<FieldType>)
2339 {
2340 cursor.BindOutputColumn(i++, &record.[:el:]);
2341 }
2342 }
2343#else
2344 Reflection::EnumerateMembers<ElementMask>(
2345 record, [&cursor, i = SQLUSMALLINT { InitialOffset }]<size_t I, typename Field>(Field& field) mutable {
2346 if constexpr (IsField<Field>)
2347 {
2348 cursor.BindOutputColumn(i++, &field.MutableValue());
2349 }
2350 else if constexpr (SqlOutputColumnBinder<Field>)
2351 {
2352 cursor.BindOutputColumn(i++, &field);
2353 }
2354 });
2355#endif
2356
2357 return record;
2358}
2359template <typename Record>
2360// NOLINTNEXTLINE(readability-function-cognitive-complexity)
2362{
2363 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2364
2365 auto const callback = [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2366 if constexpr (IsBelongsTo<FieldType>)
2367 {
2368 field.SetAutoLoader(typename FieldType::Loader {
2369 .loadReference = [value = field.Value()]() -> std::optional<typename FieldType::ReferencedRecord> {
2371 return dm.LoadBelongsTo<FieldType>(value);
2372 },
2373 });
2374 }
2375 if constexpr (IsHasMany<FieldType>)
2376 {
2377 if constexpr (HasPrimaryKey<Record>)
2378 {
2379 using ReferencedRecord = FieldType::ReferencedRecord;
2380 HasMany<ReferencedRecord>& hasMany = field;
2381 // Capture the PK value by value to avoid dangling references if the record is moved.
2382 auto pkValue = GetPrimaryKeyField(record);
2383 hasMany.SetAutoLoader(typename FieldType::Loader {
2384 .count = [pkValue]() -> size_t {
2386 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2387 dm._stmt.Prepare(selectQuery.Count());
2388 SqlResultCursor cursor = dm._stmt.Execute(pkValue);
2389 size_t count = 0;
2390 if (cursor.FetchRow())
2391 count = cursor.GetColumn<size_t>(1);
2392 return count;
2393 },
2394 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2396 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2397 return detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pkValue));
2398 },
2399 .each =
2400 [pkValue](auto const& each) {
2402 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2403 auto stmt = SqlStatement { dm._connection };
2404 stmt.Prepare(selectQuery.All());
2405 auto cursor = stmt.Execute(pkValue);
2406
2407 auto referencedRecord = ReferencedRecord {};
2408 dm.BindOutputColumns(referencedRecord, cursor);
2409 dm.ConfigureRelationAutoLoading(referencedRecord);
2410
2411 while (cursor.FetchRow())
2412 {
2413 each(referencedRecord);
2414 dm.BindOutputColumns(referencedRecord, cursor);
2415 }
2416 },
2417 });
2418 }
2419 }
2420 if constexpr (IsHasOneThrough<FieldType> && HasPrimaryKey<Record>)
2421 {
2422 using ReferencedRecord = FieldType::ReferencedRecord;
2423 using ThroughRecord = FieldType::ThroughRecord;
2425 // Capture the PK value by value to avoid dangling references if the record is moved.
2426 auto pkValue = GetPrimaryKeyField(record);
2427 hasOneThrough.SetAutoLoader(typename FieldType::Loader {
2428 .loadReference = [pkValue]() -> std::shared_ptr<ReferencedRecord> {
2430 return dm.LoadHasOneThroughByPK<ReferencedRecord, ThroughRecord, Record>(pkValue);
2431 },
2432 });
2433 }
2434 if constexpr (IsHasManyThrough<FieldType> && HasPrimaryKey<Record>)
2435 {
2436 using ReferencedRecord = FieldType::ReferencedRecord;
2437 using ThroughRecord = FieldType::ThroughRecord;
2439 // Capture the PK value by value to avoid dangling references if the record is moved.
2440 auto pkValue = GetPrimaryKeyField(record);
2441 hasManyThrough.SetAutoLoader(typename FieldType::Loader {
2442 .count = [pkValue]() -> size_t {
2443 // Load result for Count()
2444 size_t count = 0;
2446 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2447 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2448 dm._stmt.Prepare(selectQuery.Count());
2449 SqlResultCursor cursor = dm._stmt.Execute(pk);
2450 if (cursor.FetchRow())
2451 count = cursor.GetColumn<size_t>(1);
2452 });
2453 return count;
2454 },
2455 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2456 // Load result for All()
2458 typename FieldType::ReferencedRecordList result;
2459 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2460 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2461 result = detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pk));
2462 });
2463 return result;
2464 },
2465 .each =
2466 [pkValue](auto const& each) {
2467 // Load result for Each()
2469 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2470 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2471 auto stmt = SqlStatement { dm._connection };
2472 stmt.Prepare(selectQuery.All());
2473 auto cursor = stmt.Execute(pk);
2474 auto referencedRecord = ReferencedRecord {};
2475 dm.BindOutputColumns(referencedRecord, cursor);
2476 dm.ConfigureRelationAutoLoading(referencedRecord);
2477
2478 while (cursor.FetchRow())
2479 {
2480 each(referencedRecord);
2481 dm.BindOutputColumns(referencedRecord, cursor);
2482 }
2483 });
2484 },
2485 });
2486 }
2487 };
2488
2489#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2490 constexpr auto ctx = std::meta::access_context::current();
2491
2492 Reflection::template_for<0, nonstatic_data_members_of(^^Record, ctx).size()>([&callback, &record]<auto I>() {
2493 constexpr auto localctx = std::meta::access_context::current();
2494 constexpr auto members = define_static_array(nonstatic_data_members_of(^^Record, localctx));
2495 using FieldType = typename[:std::meta::type_of(members[I]):];
2496 callback.template operator()<I, FieldType>(record.[:members[I]:]);
2497 });
2498#else
2499 Reflection::EnumerateMembers(record, callback);
2500#endif
2501}
2502
2503template <typename T>
2504std::optional<T> DataMapper::Execute(std::string_view sqlQueryString)
2505{
2506 ZoneScopedN("DataMapper::Execute(string)");
2507 ZoneTextObject(sqlQueryString);
2508 return _stmt.ExecuteDirectScalar<T>(sqlQueryString);
2509}
2510
2511} // namespace Lightweight
Main API for mapping records to and from the database using high level C++ syntax.
DataMapper(DataMapper &&other) noexcept
Move constructor.
void Update(Record &record)
SqlConnection const & Connection() const noexcept
Returns the connection reference used by this data mapper.
bool IsModified(Record const &record) const noexcept
DataMapper()
Constructs a new data mapper, using the default connection.
std::vector< std::string > CreateTableString(SqlServerType serverType)
Constructs a string list of SQL queries to create the table for the given record type.
SqlQueryBuilder Query()
void LoadRelations(Record &record)
DataMapper & operator=(DataMapper &&other) noexcept
Move assignment operator.
static LIGHTWEIGHT_API DataMapper & AcquireThreadLocal()
Acquires a thread-local DataMapper instance that is safe for reuse within that thread.
void SetModifiedState(Record &record) noexcept
std::optional< T > Execute(std::string_view sqlQueryString)
DataMapper(std::optional< SqlConnectionString > connectionString)
Constructs a new data mapper, using the given connection string.
std::size_t Delete(Record const &record)
RecordPrimaryKeyType< Record > CreateCopyOf(Record const &originalRecord)
Creates a copy of an existing record in the database.
DataMapper(SqlConnection &&connection)
Constructs a new data mapper, using the given connection.
void CreateTable()
Creates the table for the given record type.
SqlAllFieldsQueryBuilder< Record, QueryOptions > Query()
std::optional< Record > QuerySingle(PrimaryKeyTypes &&... primaryKeys)
Queries a single record (based on primary key) from the database.
SqlConnection & Connection() noexcept
Returns the mutable connection reference used by this data mapper.
void CreateTables()
Creates the tables for the given record types.
static std::string Inspect(Record const &record)
Constructs a human readable string representation of the given record.
RecordPrimaryKeyType< Record > CreateExplicit(Record const &record)
Creates a new record in the database.
std::vector< std::string > CreateTablesString(SqlServerType serverType)
Constructs a string list of SQL queries to create the tables for the given record types.
void ConfigureRelationAutoLoading(Record &record)
SqlQueryBuilder FromTable(std::string_view tableName)
Constructs an SQL query builder for the given table name.
ModifiedState
Enum to set the modified state of a record.
RecordPrimaryKeyType< Record > Create(Record &record)
Creates a new record in the database.
std::vector< Record > Query(SqlSelectQueryBuilder::ComposedQuery const &selectQuery, InputParameters &&... inputParameters)
This API represents a many-to-many relationship between two records through a third record.
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
This HasMany<OtherRecord> represents a simple one-to-many relationship between two records.
Definition HasMany.hpp:34
void SetAutoLoader(Loader loader) noexcept
Used internally to configure on-demand loading of the records.
Definition HasMany.hpp:144
Represents a one-to-one relationship through a join table.
void SetAutoLoader(Loader loader)
Used internally to configure on-demand loading of the record.
Represents a query builder that retrieves all fields of a record.
Represents a connection to a SQL database.
SqlServerType ServerType() const noexcept
Retrieves the type of the server.
LIGHTWEIGHT_API SqlQueryBuilder Query(std::string_view const &table={}) const
SqlQueryFormatter const & QueryFormatter() const noexcept
Retrieves a query formatter suitable for the SQL server being connected.
std::optional< Record > First()
Executes a SELECT query for the first record found and returns it.
std::vector< Record > All()
Executes a SELECT query and returns all records found.
LIGHTWEIGHT_FORCE_INLINE SqlCoreDataMapperQueryBuilder(DataMapper &dm, std::string fields) noexcept
Constructs a query builder with the given data mapper and field list.
std::vector< Record > Range(size_t offset, size_t limit)
Executes a SELECT query for a range of records and returns them.
void Delete()
Executes a DELETE query.
size_t Count()
Executes a SELECT COUNT query and returns the number of records found.
static LIGHTWEIGHT_API SqlLogger & GetLogger()
Retrieves the currently configured logger.
virtual void OnWarning(std::string_view const &message)=0
Invoked on a warning.
LIGHTWEIGHT_API SqlCreateTableQueryBuilder CreateTable(std::string_view tableName)
Creates a new table.
API Entry point for building SQL queries.
Definition SqlQuery.hpp:32
LIGHTWEIGHT_API SqlInsertQueryBuilder Insert(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
LIGHTWEIGHT_API SqlSelectQueryBuilder Select() noexcept
Initiates SELECT query building.
LIGHTWEIGHT_API SqlDeleteQueryBuilder Delete() noexcept
Initiates DELETE query building.
LIGHTWEIGHT_API SqlMigrationQueryBuilder Migration()
Initiates query for building database migrations.
LIGHTWEIGHT_API SqlUpdateQueryBuilder Update(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
static SqlQueryFormatter const * Get(SqlServerType serverType) noexcept
Retrieves the SQL query formatter for the given SqlServerType.
API for reading an SQL query result set.
LIGHTWEIGHT_FORCE_INLINE bool GetColumn(SQLUSMALLINT column, T *result) const
LIGHTWEIGHT_FORCE_INLINE size_t NumColumnsAffected() const
Retrieves the number of columns affected by the last query.
LIGHTWEIGHT_FORCE_INLINE size_t NumRowsAffected() const
Retrieves the number of rows affected by the last query.
LIGHTWEIGHT_FORCE_INLINE void BindOutputColumn(SQLUSMALLINT columnIndex, T *arg)
Binds a single output column at the given index to store fetched data.
LIGHTWEIGHT_FORCE_INLINE bool FetchRow()
Fetches the next row of the result set.
Query builder for building SELECT ... queries.
Definition Select.hpp:86
SqlSelectQueryBuilder & Build(Callable const &callable)
Builds the query using a callable.
LIGHTWEIGHT_API ComposedQuery First(size_t count=1)
Finalizes building the query as SELECT TOP n field names FROM ... query.
LIGHTWEIGHT_API SqlSelectQueryBuilder & Field(std::string_view const &fieldName)
Adds a single column to the SELECT clause.
High level API for (prepared) raw SQL statements.
LIGHTWEIGHT_API void Prepare(std::string_view query) &
LIGHTWEIGHT_API size_t LastInsertId(std::string_view tableName)
Retrieves the last insert ID of the given table.
LIGHTWEIGHT_API SqlConnection & Connection() noexcept
Retrieves the connection associated with this statement.
SqlResultCursor Execute(Args const &... args)
Binds the given arguments to the prepared statement and executes it.
void BindInputParameter(SQLSMALLINT columnIndex, Arg const &arg)
Binds an input parameter to the prepared statement at the given column index.
LIGHTWEIGHT_API SqlResultCursor ExecuteDirect(std::string_view const &query, std::source_location location=std::source_location::current())
Executes the given query directly.
std::optional< T > ExecuteDirectScalar(std::string_view const &query, std::source_location location=std::source_location::current())
Represents a record type that can be used with the DataMapper.
Definition Record.hpp:47
LIGHTWEIGHT_FORCE_INLINE RecordPrimaryKeyType< Record > GetPrimaryKeyField(Record const &record) noexcept
Definition Record.hpp:178
constexpr std::string_view FieldNameAt
Returns the SQL field name of the given field index in the record.
Definition Utils.hpp:170
std::remove_cvref_t< decltype(std::declval< MemberClassType< decltype(Field)> >().*Field)>::ValueType ReferencedFieldTypeOf
Retrieves the type of a member field in a record.
Definition Field.hpp:392
Represents a single column in a table.
Definition Field.hpp:84
T ValueType
The underlying value type of this field.
Definition Field.hpp:86
static constexpr auto IsOptional
Indicates if the field is optional, i.e., it can be NULL.
Definition Field.hpp:118
static SqlGuid Create() noexcept
Creates a new non-empty GUID.
SqlQualifiedTableColumnName represents a column name qualified with a table name.
Definition Core.hpp:60
Represents a value that can be any of the supported SQL data types.