Lightweight 0.20260522.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 detail::FullyQualifiedNamesOf<Field>,
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 detail::FullyQualifiedNamesOf<ReferencedFields...>,
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 detail::FullyQualifiedNamesOf<Field>,
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 detail::FullyQualifiedNamesOf<ReferencedFields...>,
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 detail::FullyQualifiedNamesOf<ReferencedFields...>,
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 detail::FullyQualifiedNamesOf<ReferencedFields...>,
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 // Starter doesn't expose finalizers / Where until at least one column is
1592 // projected. The reflection enumeration below is constexpr-conditional, so
1593 // which iteration adds the first column isn't known up front — promote on the
1594 // first storage field via the returned Builder&, then reuse that pointer.
1595 auto selectStarter = _connection.Query(RecordTableName<Record>).Select();
1596 SqlSelectQueryBuilder* queryBuilder = nullptr;
1597 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1598 if constexpr (FieldWithStorage<FieldType>)
1599 {
1600 if (queryBuilder == nullptr)
1601 queryBuilder = &selectStarter.Field(FieldNameAt<I, Record>);
1602 else
1603 queryBuilder->Field(FieldNameAt<I, Record>);
1604
1605 if constexpr (FieldType::IsPrimaryKey)
1606 std::ignore = queryBuilder->Where(FieldNameAt<I, Record>, SqlWildcard);
1607 }
1608 });
1609
1610 _stmt.Prepare(queryBuilder->First());
1611 auto reader = _stmt.Execute(std::forward<PrimaryKeyTypes>(primaryKeys)...);
1612
1613 auto resultRecord = std::optional<Record> { Record {} };
1614 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
1615 return std::nullopt;
1616
1617 if (resultRecord)
1618 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
1619
1620 if constexpr (QueryOptions.loadRelations)
1621 {
1622 if (resultRecord)
1623 ConfigureRelationAutoLoading(*resultRecord);
1624 }
1625
1626 return resultRecord;
1627}
1628
1629template <typename Record, typename... Args>
1630std::optional<Record> DataMapper::QuerySingle(SqlSelectQueryBuilder selectQuery, Args&&... args)
1631{
1632 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1633
1634 ZoneScopedN("DataMapper::QuerySingle(Builder)");
1635 ZoneTextObject(RecordTableName<Record>);
1636
1637 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1638 if constexpr (FieldWithStorage<FieldType>)
1639 selectQuery.Field(SqlQualifiedTableColumnName { RecordTableName<Record>, FieldNameAt<I, Record> });
1640 });
1641 auto const composedSql = selectQuery.First().ToSql();
1642 ZoneTextObject(composedSql);
1643 _stmt.Prepare(composedSql);
1644 auto reader = _stmt.Execute(std::forward<Args>(args)...);
1645
1646 auto resultRecord = std::optional<Record> { Record {} };
1647 if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord))
1648 return std::nullopt;
1649
1650 if (resultRecord)
1651 SetModifiedState<ModifiedState::NotModified>(resultRecord.value());
1652
1653 return resultRecord;
1654}
1655
1656// TODO: Provide Query(QueryBuilder, ...) method variant
1657
1658/// Queries multiple records from the database using a composed query and optional input parameters.
1659template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1660inline LIGHTWEIGHT_FORCE_INLINE std::vector<Record> DataMapper::Query(
1661 SqlSelectQueryBuilder::ComposedQuery const& selectQuery, InputParameters&&... inputParameters)
1662{
1663 static_assert(DataMapperRecord<Record> || std::same_as<Record, SqlVariantRow>, "Record must satisfy DataMapperRecord");
1664
1665 ZoneScopedN("DataMapper::Query(ComposedQuery)");
1666 return Query<Record, QueryOptions>(selectQuery.ToSql(), std::forward<InputParameters>(inputParameters)...);
1667}
1668
1669template <typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1670std::vector<Record> DataMapper::Query(std::string_view sqlQueryString, InputParameters&&... inputParameters)
1671{
1672 ZoneScopedN("DataMapper::Query(string)");
1673 ZoneTextObject(sqlQueryString);
1674
1675 auto result = std::vector<Record> {};
1676 if constexpr (std::same_as<Record, SqlVariantRow>)
1677 {
1678 _stmt.Prepare(sqlQueryString);
1679 SqlResultCursor cursor = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1680 size_t const numResultColumns = cursor.NumColumnsAffected();
1681 while (cursor.FetchRow())
1682 {
1683 auto& record = result.emplace_back();
1684 record.reserve(numResultColumns);
1685 for (auto const i: std::views::iota(1U, numResultColumns + 1))
1686 record.emplace_back(cursor.GetColumn<SqlVariant>(static_cast<SQLUSMALLINT>(i)));
1687 }
1688 }
1689 else
1690 {
1691 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1692
1693 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
1694
1695 _stmt.Prepare(sqlQueryString);
1696 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1697
1698 for (;;)
1699 {
1700 auto& record = result.emplace_back();
1701
1702 if (canSafelyBindOutputColumns)
1703 BindOutputColumns(record, reader);
1704
1705 if (!reader.FetchRow())
1706 break;
1707
1708 if (!canSafelyBindOutputColumns)
1709 detail::GetAllColumns(reader, record);
1710 }
1711
1712 // Drop the last record, which we failed to fetch (End of result set).
1713 result.pop_back();
1714
1715 for (auto& record: result)
1716 {
1717 SetModifiedState<ModifiedState::NotModified>(record);
1718 if constexpr (QueryOptions.loadRelations)
1720 }
1721 }
1722
1723 return result;
1724}
1725
1726template <typename First, typename Second, typename... Rest, DataMapperOptions QueryOptions>
1727 requires DataMapperRecord<First> && DataMapperRecord<Second> && DataMapperRecords<Rest...>
1728std::vector<std::tuple<First, Second, Rest...>> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery)
1729{
1730 using value_type = std::tuple<First, Second, Rest...>;
1731 auto result = std::vector<value_type> {};
1732
1733 ZoneScopedN("DataMapper::Query(ComposedQuery -> tuple)");
1734 auto const tupleSql = selectQuery.ToSql();
1735 ZoneTextObject(tupleSql);
1736 _stmt.Prepare(tupleSql);
1737 auto reader = _stmt.Execute();
1738
1739 constexpr auto calculateOffset = []<size_t I, typename Tuple>() {
1740 size_t offset = 1;
1741
1742 if constexpr (I > 0)
1743 {
1744 [&]<size_t... Indices>(std::index_sequence<Indices...>) {
1745 ((Indices < I ? (offset += Reflection::CountMembers<std::tuple_element_t<Indices, Tuple>>) : 0), ...);
1746 }(std::make_index_sequence<I> {});
1747 }
1748 return offset;
1749 };
1750
1751 auto const BindElements = [&](auto& record) {
1752 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1753 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
1754 auto& element = std::get<I>(record);
1755 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
1756 this->BindOutputColumns<TupleElement, offset>(element, reader);
1757 });
1758 };
1759
1760 auto const GetElements = [&](auto& record) {
1761 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1762 auto& element = std::get<I>(record);
1763 constexpr size_t offset = calculateOffset.template operator()<I, value_type>();
1764 detail::GetAllColumns(reader, element, offset - 1);
1765 });
1766 };
1767
1768 bool const canSafelyBindOutputColumns = [&]() {
1769 bool result = true;
1770 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1771 using TupleElement = std::decay_t<std::tuple_element_t<I, value_type>>;
1772 result &= detail::CanSafelyBindOutputColumns<TupleElement>(_stmt.Connection().ServerType());
1773 });
1774 return result;
1775 }();
1776
1777 for (;;)
1778 {
1779 auto& record = result.emplace_back();
1780
1781 if (canSafelyBindOutputColumns)
1782 BindElements(record);
1783
1784 if (!reader.FetchRow())
1785 break;
1786
1787 if (!canSafelyBindOutputColumns)
1788 GetElements(record);
1789 }
1790
1791 // Drop the last record, which we failed to fetch (End of result set).
1792 result.pop_back();
1793
1794 for (auto& record: result)
1795 {
1796 Reflection::template_for<0, std::tuple_size_v<value_type>>([&]<auto I>() {
1797 auto& element = std::get<I>(record);
1798 SetModifiedState<ModifiedState::NotModified>(element);
1799 if constexpr (QueryOptions.loadRelations)
1800 {
1802 }
1803 });
1804 }
1805
1806 return result;
1807}
1808
1809template <typename ElementMask, typename Record, DataMapperOptions QueryOptions, typename... InputParameters>
1810std::vector<Record> DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const& selectQuery,
1811 InputParameters&&... inputParameters)
1812{
1813 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1814
1815 ZoneScopedN("DataMapper::Query(ComposedQuery, ElementMask)");
1816 auto const maskedSql = selectQuery.ToSql();
1817 ZoneTextObject(maskedSql);
1818 _stmt.Prepare(maskedSql);
1819
1820 auto records = std::vector<Record> {};
1821
1822 // TODO: We could optimize this further by only considering ElementMask fields in Record.
1823 bool const canSafelyBindOutputColumns = detail::CanSafelyBindOutputColumns<Record>(_stmt.Connection().ServerType());
1824
1825 auto reader = _stmt.Execute(std::forward<InputParameters>(inputParameters)...);
1826
1827 for (;;)
1828 {
1829 auto& record = records.emplace_back();
1830
1831 if (canSafelyBindOutputColumns)
1832 BindOutputColumns<ElementMask>(record, reader);
1833
1834 if (!reader.FetchRow())
1835 break;
1836
1837 if (!canSafelyBindOutputColumns)
1838 detail::GetAllColumns<ElementMask>(reader, record);
1839 }
1840
1841 // Drop the last record, which we failed to fetch (End of result set).
1842 records.pop_back();
1843
1844 for (auto& record: records)
1845 {
1846 SetModifiedState<ModifiedState::NotModified>(record);
1847 if constexpr (QueryOptions.loadRelations)
1849 }
1850
1851 return records;
1852}
1853
1854template <DataMapper::ModifiedState state, typename Record>
1855void DataMapper::SetModifiedState(Record& record) noexcept
1856{
1857 static_assert(!std::is_const_v<Record>);
1858 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1859
1860 Reflection::EnumerateMembers(record, []<size_t I, typename FieldType>(FieldType& field) {
1861 if constexpr (requires { field.SetModified(false); })
1862 {
1863 if constexpr (state == ModifiedState::Modified)
1864 field.SetModified(true);
1865 else
1866 field.SetModified(false);
1867 }
1868 });
1869}
1870
1871template <typename Record, typename Callable>
1872inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Record& record, Callable const& callable)
1873{
1874 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1875
1876 Reflection::EnumerateMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
1877 if constexpr (IsField<FieldType>)
1878 {
1879 if constexpr (FieldType::IsPrimaryKey)
1880 {
1881 return callable.template operator()<I, FieldType>(field);
1882 }
1883 }
1884 });
1885}
1886
1887template <typename Record, typename Callable>
1888inline LIGHTWEIGHT_FORCE_INLINE void CallOnPrimaryKey(Callable const& callable)
1889{
1890 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1891
1892 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1893 if constexpr (IsField<FieldType>)
1894 {
1895 if constexpr (FieldType::IsPrimaryKey)
1896 {
1897 return callable.template operator()<I, FieldType>();
1898 }
1899 }
1900 });
1901}
1902
1903template <typename Record, typename Callable>
1904inline LIGHTWEIGHT_FORCE_INLINE void CallOnBelongsTo(Callable const& callable)
1905{
1906 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1907
1908 Reflection::EnumerateMembers<Record>([&]<size_t I, typename FieldType>() {
1909 if constexpr (IsBelongsTo<FieldType>)
1910 {
1911 return callable.template operator()<I, FieldType>();
1912 }
1913 });
1914}
1915
1916template <typename FieldType>
1917std::optional<typename FieldType::ReferencedRecord> DataMapper::LoadBelongsTo(FieldType::ValueType value)
1918{
1919 using ReferencedRecord = FieldType::ReferencedRecord;
1920
1921 ZoneScopedN("DataMapper::LoadBelongsTo");
1922 ZoneTextObject(RecordTableName<ReferencedRecord>);
1923
1924 std::optional<ReferencedRecord> record { std::nullopt };
1925
1926#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
1927 auto constexpr ctx = std::meta::access_context::current();
1928 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^ReferencedRecord, ctx)))
1929 {
1930 using BelongsToFieldType = typename[:std::meta::type_of(el):];
1931 if constexpr (IsField<BelongsToFieldType>)
1932 if constexpr (BelongsToFieldType::IsPrimaryKey)
1933 {
1934 if (auto result = QuerySingle<ReferencedRecord>(value); result)
1935 record = std::move(result);
1936 else
1938 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
1939 }
1940 }
1941#else
1942 CallOnPrimaryKey<ReferencedRecord>([&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>() {
1943 if (auto result = QuerySingle<ReferencedRecord>(value); result)
1944 record = std::move(result);
1945 else
1947 std::format("Loading BelongsTo failed for {}", RecordTableName<ReferencedRecord>));
1948 });
1949#endif
1950 return record;
1951}
1952
1953template <size_t FieldIndex, typename Record, typename OtherRecord, typename Callable>
1954void DataMapper::CallOnHasMany(Record& record, Callable const& callback)
1955{
1956 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1957 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
1958
1959 using FieldType = HasMany<OtherRecord>;
1960 using ReferencedRecord = FieldType::ReferencedRecord;
1961
1962 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
1963 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
1964 .Select()
1965 .Build([&](auto& query) {
1966 Reflection::EnumerateMembers<ReferencedRecord>(
1967 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
1968 if constexpr (FieldWithStorage<ReferencedFieldType>)
1969 {
1970 query.Field(FieldNameAt<ReferencedFieldIndex, ReferencedRecord>);
1971 }
1972 });
1973 })
1974 .Where(FieldNameAt<FieldIndex, ReferencedRecord>, SqlWildcard)
1975 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<ReferencedRecord>, ReferencedRecord>);
1976 callback(query, primaryKeyField);
1977 });
1978}
1979
1980template <size_t FieldIndex, typename OtherRecord>
1981SqlSelectQueryBuilder DataMapper::BuildHasManySelectQuery()
1982{
1983 return _connection.Query(RecordTableName<OtherRecord>)
1984 .Select()
1985 .Build([](auto& q) {
1986 Reflection::EnumerateMembers<OtherRecord>([&]<size_t I, typename F>() {
1987 if constexpr (FieldWithStorage<F>)
1988 q.Field(FieldNameAt<I, OtherRecord>);
1989 });
1990 })
1991 .Where(FieldNameAt<FieldIndex, OtherRecord>, SqlWildcard)
1992 .OrderBy(FieldNameAt<RecordPrimaryKeyIndex<OtherRecord>, OtherRecord>);
1993}
1994
1995template <size_t FieldIndex, typename Record, typename OtherRecord>
1996void DataMapper::LoadHasMany(Record& record, HasMany<OtherRecord>& field)
1997{
1998 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
1999 static_assert(DataMapperRecord<OtherRecord>, "OtherRecord must satisfy DataMapperRecord");
2000
2001 ZoneScopedN("DataMapper::LoadHasMany");
2002 ZoneTextObject(RecordTableName<OtherRecord>);
2003
2004 CallOnHasMany<FieldIndex, Record, OtherRecord>(record, [&](SqlSelectQueryBuilder selectQuery, auto& primaryKeyField) {
2005 field.Emplace(detail::ToSharedPtrList(Query<OtherRecord>(selectQuery.All(), primaryKeyField.Value())));
2006 });
2007}
2008
2009template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2010void DataMapper::LoadHasOneThrough(Record& record, HasOneThrough<ReferencedRecord, ThroughRecord>& field)
2011{
2012 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2013 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2014
2015 ZoneScopedN("DataMapper::LoadHasOneThrough");
2016 ZoneTextObject(RecordTableName<ReferencedRecord>);
2017
2018 // Find the PK of Record
2019 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2020 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2021 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2022 // Find the PK of ThroughRecord
2023 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2024 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2025 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2026 // Query the ReferencedRecord where:
2027 // - the BelongsTo of ReferencedRecord points to the PK of ThroughRecord,
2028 // - and the BelongsTo of ThroughRecord points to the PK of Record
2029 auto query =
2030 _connection.Query(RecordTableName<ReferencedRecord>)
2031 .Select()
2032 .Build([&](auto& query) {
2033 Reflection::EnumerateMembers<ReferencedRecord>(
2034 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2035 if constexpr (FieldWithStorage<ReferencedFieldType>)
2036 {
2037 query.Field(SqlQualifiedTableColumnName {
2038 RecordTableName<ReferencedRecord>,
2039 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2040 }
2041 });
2042 })
2043 .InnerJoin(RecordTableName<ThroughRecord>,
2044 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2045 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2046 .InnerJoin(RecordTableName<Record>,
2047 FieldNameAt<PrimaryKeyIndex, Record>,
2048 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2049 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2050 .Where(
2051 SqlQualifiedTableColumnName {
2052 RecordTableName<Record>,
2053 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2054 },
2055 SqlWildcard);
2056 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), primaryKeyField.Value()); link)
2057 {
2058 field.EmplaceRecord(std::make_shared<ReferencedRecord>(std::move(*link)));
2059 }
2060 });
2061 });
2062 });
2063 });
2064}
2065
2066template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue>
2067std::shared_ptr<ReferencedRecord> DataMapper::LoadHasOneThroughByPK(PKValue const& pkValue)
2068{
2069 static_assert(DataMapperRecord<ThroughRecord>, "ThroughRecord must satisfy DataMapperRecord");
2070
2071 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2072 std::shared_ptr<ReferencedRecord> result;
2073
2074 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2075 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToIndex, typename ThroughBelongsToType>() {
2076 // Find the PK of ThroughRecord
2077 CallOnPrimaryKey<ThroughRecord>([&]<size_t ThroughPrimaryKeyIndex, typename ThroughPrimaryKeyType>() {
2078 // Find the BelongsTo of ReferencedRecord pointing to the PK of ThroughRecord
2079 CallOnBelongsTo<ReferencedRecord>([&]<size_t ReferencedKeyIndex, typename ReferencedKeyType>() {
2080 auto query =
2081 _connection.Query(RecordTableName<ReferencedRecord>)
2082 .Select()
2083 .Build([&](auto& query) {
2084 Reflection::EnumerateMembers<ReferencedRecord>(
2085 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2086 if constexpr (FieldWithStorage<ReferencedFieldType>)
2087 {
2088 query.Field(SqlQualifiedTableColumnName {
2089 RecordTableName<ReferencedRecord>,
2090 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2091 }
2092 });
2093 })
2094 .InnerJoin(RecordTableName<ThroughRecord>,
2095 FieldNameAt<ThroughPrimaryKeyIndex, ThroughRecord>,
2096 FieldNameAt<ReferencedKeyIndex, ReferencedRecord>)
2097 .InnerJoin(RecordTableName<Record>,
2098 FieldNameAt<PrimaryKeyIndex, Record>,
2099 SqlQualifiedTableColumnName { RecordTableName<ThroughRecord>,
2100 FieldNameAt<ThroughBelongsToIndex, ThroughRecord> })
2101 .Where(
2102 SqlQualifiedTableColumnName {
2103 RecordTableName<Record>,
2104 FieldNameAt<PrimaryKeyIndex, ThroughRecord>,
2105 },
2106 SqlWildcard);
2107 if (auto link = QuerySingle<ReferencedRecord>(std::move(query), pkValue); link)
2108 result = std::make_shared<ReferencedRecord>(std::move(*link));
2109 });
2110 });
2111 });
2112
2113 return result;
2114}
2115
2116template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename Callable>
2117void DataMapper::CallOnHasManyThrough(Record& record, Callable const& callback)
2118{
2119 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2120
2121 // Find the PK of Record
2122 CallOnPrimaryKey(record, [&]<size_t PrimaryKeyIndex, typename PrimaryKeyType>(PrimaryKeyType const& primaryKeyField) {
2123 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2124 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2125 using ThroughBelongsToRecordFieldType = Reflection::MemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2126 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2127 {
2128 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2129 CallOnBelongsTo<ThroughRecord>(
2130 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2131 using ThroughBelongsToReferenceRecordFieldType =
2132 Reflection::MemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2133 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2134 ReferencedRecord>)
2135 {
2136 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2137 .Select()
2138 .Build([&](auto& query) {
2139 Reflection::EnumerateMembers<ReferencedRecord>(
2140 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2141 if constexpr (FieldWithStorage<ReferencedFieldType>)
2142 {
2143 query.Field(SqlQualifiedTableColumnName {
2144 RecordTableName<ReferencedRecord>,
2145 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2146 }
2147 });
2148 })
2149 .InnerJoin(RecordTableName<ThroughRecord>,
2150 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2151 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2152 FieldNameAt<PrimaryKeyIndex, Record> })
2153 .Where(
2154 SqlQualifiedTableColumnName {
2155 RecordTableName<ThroughRecord>,
2156 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2157 },
2158 SqlWildcard);
2159 callback(query, primaryKeyField);
2160 }
2161 });
2162 }
2163 });
2164 });
2165}
2166
2167template <typename ReferencedRecord, typename ThroughRecord, typename Record, typename PKValue, typename Callable>
2168void DataMapper::CallOnHasManyThroughByPK(PKValue const& pkValue, Callable const& callback)
2169{
2170 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2171
2172 constexpr size_t PrimaryKeyIndex = RecordPrimaryKeyIndex<Record>;
2173
2174 // Find the BelongsTo of ThroughRecord pointing to the PK of Record
2175 CallOnBelongsTo<ThroughRecord>([&]<size_t ThroughBelongsToRecordIndex, typename ThroughBelongsToRecordType>() {
2176 using ThroughBelongsToRecordFieldType = Reflection::MemberTypeOf<ThroughBelongsToRecordIndex, ThroughRecord>;
2177 if constexpr (std::is_same_v<typename ThroughBelongsToRecordFieldType::ReferencedRecord, Record>)
2178 {
2179 // Find the BelongsTo of ThroughRecord pointing to the PK of ReferencedRecord
2180 CallOnBelongsTo<ThroughRecord>(
2181 [&]<size_t ThroughBelongsToReferenceRecordIndex, typename ThroughBelongsToReferenceRecordType>() {
2182 using ThroughBelongsToReferenceRecordFieldType =
2183 Reflection::MemberTypeOf<ThroughBelongsToReferenceRecordIndex, ThroughRecord>;
2184 if constexpr (std::is_same_v<typename ThroughBelongsToReferenceRecordFieldType::ReferencedRecord,
2185 ReferencedRecord>)
2186 {
2187 auto query = _connection.Query(RecordTableName<ReferencedRecord>)
2188 .Select()
2189 .Build([&](auto& query) {
2190 Reflection::EnumerateMembers<ReferencedRecord>(
2191 [&]<size_t ReferencedFieldIndex, typename ReferencedFieldType>() {
2192 if constexpr (FieldWithStorage<ReferencedFieldType>)
2193 {
2194 query.Field(SqlQualifiedTableColumnName {
2195 RecordTableName<ReferencedRecord>,
2196 FieldNameAt<ReferencedFieldIndex, ReferencedRecord> });
2197 }
2198 });
2199 })
2200 .InnerJoin(RecordTableName<ThroughRecord>,
2201 FieldNameAt<ThroughBelongsToReferenceRecordIndex, ThroughRecord>,
2202 SqlQualifiedTableColumnName { RecordTableName<ReferencedRecord>,
2203 FieldNameAt<PrimaryKeyIndex, Record> })
2204 .Where(
2205 SqlQualifiedTableColumnName {
2206 RecordTableName<ThroughRecord>,
2207 FieldNameAt<ThroughBelongsToRecordIndex, ThroughRecord>,
2208 },
2209 SqlWildcard);
2210 callback(query, pkValue);
2211 }
2212 });
2213 }
2214 });
2215}
2216
2217template <typename ReferencedRecord, typename ThroughRecord, typename Record>
2218void DataMapper::LoadHasManyThrough(Record& record, HasManyThrough<ReferencedRecord, ThroughRecord>& field)
2219{
2220 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2221
2222 ZoneScopedN("DataMapper::LoadHasManyThrough");
2223 ZoneTextObject(RecordTableName<ReferencedRecord>);
2224
2225 CallOnHasManyThrough<ReferencedRecord, ThroughRecord>(
2226 record, [&](SqlSelectQueryBuilder& selectQuery, auto& primaryKeyField) {
2227 field.Emplace(detail::ToSharedPtrList(Query<ReferencedRecord>(selectQuery.All(), primaryKeyField.Value())));
2228 });
2229}
2230
2231template <typename Record>
2232void DataMapper::LoadRelations(Record& record)
2233{
2234 static_assert(!std::is_const_v<Record>);
2235 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2236
2237 ZoneScopedN("DataMapper::LoadRelations");
2238 ZoneTextObject(RecordTableName<Record>);
2239
2240#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2241 constexpr auto ctx = std::meta::access_context::current();
2242 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2243 {
2244 using FieldType = typename[:std::meta::type_of(el):];
2245 if constexpr (IsBelongsTo<FieldType>)
2246 {
2247 auto& field = record.[:el:];
2248 field = LoadBelongsTo<FieldType>(field.Value());
2249 }
2250 else if constexpr (IsHasMany<FieldType>)
2251 {
2252 LoadHasMany<el>(record, record.[:el:]);
2253 }
2254 else if constexpr (IsHasOneThrough<FieldType>)
2255 {
2256 LoadHasOneThrough(record, record.[:el:]);
2257 }
2258 else if constexpr (IsHasManyThrough<FieldType>)
2259 {
2260 LoadHasManyThrough(record, record.[:el:]);
2261 }
2262 }
2263#else
2264 Reflection::EnumerateMembers(record, [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2265 if constexpr (IsBelongsTo<FieldType>)
2266 {
2267 field = LoadBelongsTo<FieldType>(field.Value());
2268 }
2269 else if constexpr (IsHasMany<FieldType>)
2270 {
2271 LoadHasMany<FieldIndex>(record, field);
2272 }
2273 else if constexpr (IsHasOneThrough<FieldType>)
2274 {
2275 LoadHasOneThrough(record, field);
2276 }
2277 else if constexpr (IsHasManyThrough<FieldType>)
2278 {
2279 LoadHasManyThrough(record, field);
2280 }
2281 });
2282#endif
2283}
2284
2285/// Sets the primary key field(s) of the given record to the specified id value.
2286template <typename Record, typename ValueType>
2287inline LIGHTWEIGHT_FORCE_INLINE void DataMapper::SetId(Record& record, ValueType&& id)
2288{
2289 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2290 // static_assert(HasPrimaryKey<Record>);
2291
2292#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2293
2294 auto constexpr ctx = std::meta::access_context::current();
2295 template for (constexpr auto el: define_static_array(nonstatic_data_members_of(^^Record, ctx)))
2296 {
2297 using FieldType = typename[:std::meta::type_of(el):];
2298 if constexpr (IsField<FieldType>)
2299 {
2300 if constexpr (FieldType::IsPrimaryKey)
2301 {
2302 record.[:el:] = std::forward<ValueType>(id);
2303 }
2304 }
2305 }
2306#else
2307 Reflection::EnumerateMembers(record, [&]<size_t I, typename FieldType>(FieldType& field) {
2308 if constexpr (IsField<FieldType>)
2309 {
2310 if constexpr (FieldType::IsPrimaryKey)
2311 {
2312 field = std::forward<FieldType>(id);
2313 }
2314 }
2315 });
2316#endif
2317}
2318
2319/// Binds all output columns of the record via the given cursor.
2320template <typename Record, size_t InitialOffset>
2321inline LIGHTWEIGHT_FORCE_INLINE Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2322{
2323 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2324 return BindOutputColumns<std::make_integer_sequence<size_t, Reflection::CountMembers<Record>>, Record, InitialOffset>(
2325 record, cursor);
2326}
2327
2328template <typename ElementMask, typename Record, size_t InitialOffset>
2329Record& DataMapper::BindOutputColumns(Record& record, SqlResultCursor& cursor)
2330{
2331 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2332 static_assert(!std::is_const_v<Record>);
2333
2334#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2335 auto constexpr ctx = std::meta::access_context::current();
2336 SQLSMALLINT i = SQLSMALLINT { InitialOffset };
2337 template for (constexpr auto index: define_static_array(template_arguments_of(^^ElementMask)) | std::views::drop(1))
2338 {
2339 constexpr auto el = nonstatic_data_members_of(^^Record, ctx)[[:index:]];
2340 using FieldType = typename[:std::meta::type_of(el):];
2341 if constexpr (IsField<FieldType>)
2342 {
2343 cursor.BindOutputColumn(i++, &record.[:el:].MutableValue());
2344 }
2345 else if constexpr (SqlOutputColumnBinder<FieldType>)
2346 {
2347 cursor.BindOutputColumn(i++, &record.[:el:]);
2348 }
2349 }
2350#else
2351 Reflection::EnumerateMembers<ElementMask>(
2352 record, [&cursor, i = SQLUSMALLINT { InitialOffset }]<size_t I, typename Field>(Field& field) mutable {
2353 if constexpr (IsField<Field>)
2354 {
2355 cursor.BindOutputColumn(i++, &field.MutableValue());
2356 }
2357 else if constexpr (SqlOutputColumnBinder<Field>)
2358 {
2359 cursor.BindOutputColumn(i++, &field);
2360 }
2361 });
2362#endif
2363
2364 return record;
2365}
2366template <typename Record>
2367// NOLINTNEXTLINE(readability-function-cognitive-complexity)
2369{
2370 static_assert(DataMapperRecord<Record>, "Record must satisfy DataMapperRecord");
2371
2372 auto const callback = [&]<size_t FieldIndex, typename FieldType>(FieldType& field) {
2373 if constexpr (IsBelongsTo<FieldType>)
2374 {
2375 field.SetAutoLoader(typename FieldType::Loader {
2376 .loadReference = [value = field.Value()]() -> std::optional<typename FieldType::ReferencedRecord> {
2378 return dm.LoadBelongsTo<FieldType>(value);
2379 },
2380 });
2381 }
2382 if constexpr (IsHasMany<FieldType>)
2383 {
2384 if constexpr (HasPrimaryKey<Record>)
2385 {
2386 using ReferencedRecord = FieldType::ReferencedRecord;
2387 HasMany<ReferencedRecord>& hasMany = field;
2388 // Capture the PK value by value to avoid dangling references if the record is moved.
2389 auto pkValue = GetPrimaryKeyField(record);
2390 hasMany.SetAutoLoader(typename FieldType::Loader {
2391 .count = [pkValue]() -> size_t {
2393 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2394 dm._stmt.Prepare(selectQuery.Count());
2395 SqlResultCursor cursor = dm._stmt.Execute(pkValue);
2396 size_t count = 0;
2397 if (cursor.FetchRow())
2398 count = cursor.GetColumn<size_t>(1);
2399 return count;
2400 },
2401 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2403 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2404 return detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pkValue));
2405 },
2406 .each =
2407 [pkValue](auto const& each) {
2409 auto selectQuery = dm.BuildHasManySelectQuery<FieldIndex, ReferencedRecord>();
2410 auto stmt = SqlStatement { dm._connection };
2411 stmt.Prepare(selectQuery.All());
2412 auto cursor = stmt.Execute(pkValue);
2413
2414 auto referencedRecord = ReferencedRecord {};
2415 dm.BindOutputColumns(referencedRecord, cursor);
2416 dm.ConfigureRelationAutoLoading(referencedRecord);
2417
2418 while (cursor.FetchRow())
2419 {
2420 each(referencedRecord);
2421 dm.BindOutputColumns(referencedRecord, cursor);
2422 }
2423 },
2424 });
2425 }
2426 }
2427 if constexpr (IsHasOneThrough<FieldType> && HasPrimaryKey<Record>)
2428 {
2429 using ReferencedRecord = FieldType::ReferencedRecord;
2430 using ThroughRecord = FieldType::ThroughRecord;
2432 // Capture the PK value by value to avoid dangling references if the record is moved.
2433 auto pkValue = GetPrimaryKeyField(record);
2434 hasOneThrough.SetAutoLoader(typename FieldType::Loader {
2435 .loadReference = [pkValue]() -> std::shared_ptr<ReferencedRecord> {
2437 return dm.LoadHasOneThroughByPK<ReferencedRecord, ThroughRecord, Record>(pkValue);
2438 },
2439 });
2440 }
2441 if constexpr (IsHasManyThrough<FieldType> && HasPrimaryKey<Record>)
2442 {
2443 using ReferencedRecord = FieldType::ReferencedRecord;
2444 using ThroughRecord = FieldType::ThroughRecord;
2446 // Capture the PK value by value to avoid dangling references if the record is moved.
2447 auto pkValue = GetPrimaryKeyField(record);
2448 hasManyThrough.SetAutoLoader(typename FieldType::Loader {
2449 .count = [pkValue]() -> size_t {
2450 // Load result for Count()
2451 size_t count = 0;
2453 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2454 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2455 dm._stmt.Prepare(selectQuery.Count());
2456 SqlResultCursor cursor = dm._stmt.Execute(pk);
2457 if (cursor.FetchRow())
2458 count = cursor.GetColumn<size_t>(1);
2459 });
2460 return count;
2461 },
2462 .all = [pkValue]() -> FieldType::ReferencedRecordList {
2463 // Load result for All()
2465 typename FieldType::ReferencedRecordList result;
2466 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2467 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2468 result = detail::ToSharedPtrList(dm.Query<ReferencedRecord>(selectQuery.All(), pk));
2469 });
2470 return result;
2471 },
2472 .each =
2473 [pkValue](auto const& each) {
2474 // Load result for Each()
2476 dm.CallOnHasManyThroughByPK<ReferencedRecord, ThroughRecord, Record>(
2477 pkValue, [&](SqlSelectQueryBuilder& selectQuery, auto const& pk) {
2478 auto stmt = SqlStatement { dm._connection };
2479 stmt.Prepare(selectQuery.All());
2480 auto cursor = stmt.Execute(pk);
2481 auto referencedRecord = ReferencedRecord {};
2482 dm.BindOutputColumns(referencedRecord, cursor);
2483 dm.ConfigureRelationAutoLoading(referencedRecord);
2484
2485 while (cursor.FetchRow())
2486 {
2487 each(referencedRecord);
2488 dm.BindOutputColumns(referencedRecord, cursor);
2489 }
2490 });
2491 },
2492 });
2493 }
2494 };
2495
2496#if defined(LIGHTWEIGHT_CXX26_REFLECTION)
2497 constexpr auto ctx = std::meta::access_context::current();
2498
2499 Reflection::template_for<0, nonstatic_data_members_of(^^Record, ctx).size()>([&callback, &record]<auto I>() {
2500 constexpr auto localctx = std::meta::access_context::current();
2501 constexpr auto members = define_static_array(nonstatic_data_members_of(^^Record, localctx));
2502 using FieldType = typename[:std::meta::type_of(members[I]):];
2503 callback.template operator()<I, FieldType>(record.[:members[I]:]);
2504 });
2505#else
2506 Reflection::EnumerateMembers(record, callback);
2507#endif
2508}
2509
2510template <typename T>
2511std::optional<T> DataMapper::Execute(std::string_view sqlQueryString)
2512{
2513 ZoneScopedN("DataMapper::Execute(string)");
2514 ZoneTextObject(sqlQueryString);
2515 return _stmt.ExecuteDirectScalar<T>(sqlQueryString);
2516}
2517
2518} // 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 SqlSelectQueryStarter Select() noexcept
LIGHTWEIGHT_API SqlInsertQueryBuilder Insert(std::vector< SqlVariant > *boundInputs=nullptr) noexcept
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:99
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())
Derived & Where(ColumnName const &columnName, std::string_view binaryOp, T const &value)
Constructs or extends a WHERE clause to test for a binary operation.
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:394
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 Utils.hpp:227
Represents a value that can be any of the supported SQL data types.